8000 fix(postgres): carry the with clause into a probe that reads it by colinhacks · Pull Request #58 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(postgres): carry the with clause into a probe that reads it - #58

Merged
colinhacks merged 2 commits into
mainfrom
probe-with-clause
Oct 23, 2026
Merged

colinhacks merged 2 commits into
mainfrom
probe-with-clause

Conversation

@colinhacks
Copy link
Copy Markdown
Owner
7438

A JSON probe dropped the statement's whole WITH clause whenever any query in it held a placeholder, since a probe binds nothing. A probe that read one of those queries then named a relation that did not exist (the server answered relation "monthly" does not exist), and the JSON built over the query's columns came back with unknown fields. Short statements whose WITH queries held no placeholder were typed, which made it look like a large-statement problem.

Statement main This PR
large_queries/json_over_with_param_short.sql: with recent as (… where org_id = :org_id) select u.id, (select json_agg(json_build_object('at', r.placed_at)) from recent r …) recent: [{ at: unknown? }]? recent: [{ at: timestamptz }]?
large_queries/json_over_with_param.sql: four WITH queries and two JSON columns, one over a WITH query monthly_spend: [{ month: unknown?, cents: unknown? }]? [{ month: timestamptz, cents: int4 }]?
pg18_syntax/returning_with_query_param.sql j: { now: unknown?, was: unknown? } j: { now: text?, was: text? }
inference_precision/whole_row_beside_with.sql: jsonb_agg(g) of a data-modifying WITH query's row gone: [unknown?] gone: [record?]
  • A probe that reads a WITH query carries the clause (jsonshape::select_text, which the leaf probe of an expression column goes through too). Its placeholders stay, and the describe pins each to the type the statement's own describe gave it; only the placeholders outside the clause are written NULL, as before. The probe of a WITH query read under a leaf probe is then the same text as the statement's own probe of it, settled once. With the placeholders written NULL, it was another statement to settle, spending the budget twice, and a column selecting a placeholder came out nullable there. A probe that reads none of the queries goes without the clause and binds nothing, so it is still described in the first round.
  • The pinned placeholders are numbered from 1 (Engine::pin): the server cannot type $2 in a statement that never mentions $1, which a probe reading only some of the statement's parameters would be.
  • A write whose target goes by the name of one of its WITH queries gets no shapes. with users as (select :n::int as name) update users … returning jsonb_build_object('n', name) writes the table, while a probe carrying the clause would read the query by that name and type n as int4 (json_subqueries/returning_target_named_by_with.sql, now j: json). Before this PR the same misreading happened wherever the clause held no placeholder.

Checked

  • The engine's native tests (unit tests for the probe text and the numbering); every Postgres and SQLite oracle case with --strict-coverage; the whole workspace builds with no warning.
  • packages/postgres/engine.wasm rebuilt (with the other two in one run; only this one changes); nub run typecheck and nub run test against Postgres 18.

A probe of JSON dropped the statement's whole WITH clause when any query
in it held a placeholder, so one reading a WITH query was refused and
its fields came back unknown. Such a probe now carries the clause with
its placeholders, pinned to their types, and numbers them from 1.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 03:35
@vercel
vercel Bot commented Oct 23, 2026 •
Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
reylo Ignored Ignored Preview Oct 23, 2026 3:48am UTC

Request Review

Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No blocking issues. Two small suggestions inline.

Reviewed changes

Reviewed the one commit that sends the WITH clause with Postgres probes that read it, renumbers pinned placeholders and blocks JSON shapes for writes whose target has the same name as a WITH query.

  • Conditional WITH on probes: grouped_select_text attaches the WITH clause only when reads_with finds an unqualified relation named after one of its queries. The clause keeps its $n, and only the probe body after it has without_placeholders applied. The text is split with strip_suffix, and if the split fails, every placeholder is replaced with NULL.
  • Dense renumbering in Engine::pin: the probe's placeholders are numbered from 1 in sorted order, and each cast is still looked up by its original number. pin only feeds probe and prewarm describes (postgres.rs:939, postgres.rs:1520), so settle and analyze still read the original numbering, as they did before.
  • target_named_by_with guard: if an INSERT, UPDATE, DELETE or MERGE writes a target that has the same unqualified name as a WITH query, the statement gets no JSON shapes. The probe would otherwise read the WITH query in place of the table.
  • Oracle cases: new cases in large_queries and json_subqueries, plus re-blessed lines in inference_precision and pg18_syntax. Each changed line goes from unknown to a concrete type, or to json for the case where the target has a WITH query's name.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread engine/postgres/src/jsonshape.rs Resolved
Comment thread engine/postgres/src/postgres.rs Resolved
@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. Both earlier comments are addressed.

Reviewed changes

This pass covered adec33e, the commit pushed after the previous Pullfrog review.

  • Leaf plan guard: target_named_by_with is now pub(crate), and infer::plan_here returns Plan::default() when it holds. A RETURNING leaf probe can no longer read a WITH query in place of the table it writes. Returning the empty plan for the whole statement, not just dropping the leaves, is the right call. Withs and Level::of behind Rows would also resolve the target's name to the WITH query.
  • Oracle: added returning_leaf_target_named_by_with.sql, now typed e: text?. returning_target_named_by_with.sql moves from j: json to j: json?. That is less precise because the empty plan also drops the claim that jsonb_build_object is non-null, but it is still sound.
  • Nit: removed the extra blank line in postgres.rs tests.

The PR description still lists returning_target_named_by_with.sql as "now j: json". After this commit it is j: json?.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit b480b81 into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the probe-with-clause branch October 23, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0