8000 fix(postgres): give a large statement the probes it needs by colinhacks · Pull Request #50 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(postgres): give a large statement the probes it needs - #50

Merged
colinhacks merged 1 commit into
mainfrom
probe-budget
Oct 23, 2026
Merged

colinhacks merged 1 commit into
mainfrom
probe-budget

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

A statement of about a hundred lines (three or four WITH queries, a derived table, scalar subqueries building JSON) needs about a dozen probes, and the analysis allowed eight. Past the eighth, every probe was refused, so the JSON columns read last came back with unknown fields. Small statements with the same constructs were typed precisely, which is why only large ones showed it.

Statement (oracle/cases/large_queries) main This PR
window_beside_json.sql: a lag() column beside a json_agg subquery last_order_items: [{ sku: unknown?, … }]? [{ sku: text, name: text, quantity: int4, … }]?
repeated_table_outer_join.sql: a table read again in a subquery beside a left join last_order_items: [{ sku: unknown?, … }]? [{ sku: text, … }]?
budget_spent.sql: a chain of fourteen WITH queries, past the new budget — n: text? where its probes stop, never a wrong type

The budget is 24, which covers the largest of 225 variants of a hundred-line reporting query (13 probes) with room to spare. A larger budget alone made those statements slower: each probe settled the next only once described, so a statement took a round per nested probe (15 rounds for one of them, against the host's limit of 16), and each round replayed the whole analysis. So:

  • Every probe is asked in one round. Once the statement's describe has given the types a probe pins on its placeholders, the texts of all of its probes follow from the SQL: the statements its FROM items run, each statement's leaf probe, set-operation arms, and the JSON shapes' probes. They are asked together (prewarm_tree), bounded by the budget. The casts are set before the first probe, so a probe asked early and read late is one text, one describe.
  • A settled probe is not settled again. Each probe re-settled every probe below it on every call, which doubles with each link of a WITH chain; its outcome is now kept for the rest of the analysis, unless it rested on a recursive query's assumption about itself or met itself.
  • A statement's text is read once. The parse, the parse-tree analysis, whether it needs the relations' columns, and its FROM items' statements are pure functions of its text, and are kept by text (stack::kept, 64 of each), which a host's replay, round after round and edit after edit, reads again.

Editor latency

tsc --lsp with the content mapper, the time from an edit to a .sql file until the hover shows the new column (10 edits per run, 3 runs per engine, alternating, Postgres 18 on localhost; the machine was busy with other builds, so compare within a row):

Query Rounds, main → this PR Median edit to hover, main Median edit to hover, this PR
The hundred-line demo (top_customers.sql, 7 probes) 12 → 6 197–245 ms 46–48 ms
json_over_with_param, 4 WITH queries and 2 JSON columns (13 probes) 10 → 6 196–332 ms 59–80 ms

Checked

  • The engine's native tests; every oracle case on Postgres 18 (json_item_shapes/with_probe_budget.sql gains its types; the new large_queries case runs every statement over seeded values); the whole workspace builds with no warning.
  • packages/postgres/engine.wasm rebuilt; nub run typecheck and nub run test against Postgres 18.

Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:18
@vercel
vercel Bot commented Oct 22, 2026 •
Copy link
Copy Markdown

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

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 23, 2026 3:24am 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.

< 8000 blockquote>

ℹ️ No correctness issues found. One doc comment overstates how many speculative describes get asked.

Reviewed changes

I reviewed the Postgres engine's probe budget, the one-round prewarm of pinned probes, the per-analysis cache of settled probes, the text-keyed thread-local caches and the new large_queries oracle case.

  • Budget raised to 24: MAX_PROBES goes from 8 to 24, so large statements get the probes their JSON columns need. Past the cap, columns are left nullable rather than typed wrong (budget_spent.sql).
  • Casts pinned before settle: described now sets casts before the first probe, and returns pending until every parameter's oid is in the registry. insert_unknown guarantees that happens once types resolves, so this cannot stall.
  • prewarm_tree: asks for the pinned describes of FROM-item statements, set-operation arms and leaf probes in one round, recursively. It runs from described and from run for the JSON shapes' probes.
  • Engine::settled: probe outcomes are cached by text for the rest of the analysis. I traced every way a probe's outcome can depend on context: the with_stack assumption read in item_columns and the probing self-meet. The global assumptions counter covers both, and it marks every ancestor probe too. Pending outcomes are never cached, and a cache hit only ever gives back an outcome that held in a context with no assumption, so the cache is sound.
  • Thread-local stack::kept caches: these cover parse, analyze, item_reads and levels::needed, each a pure function of its text. Natively, stack::run runs each top-level call on a scoped thread, so these caches last for one call there. In wasm they last across rounds and edits, which is where the latency win comes from.
  • Oracle: there is a new large_queries case, and with_probe_budget.sql now gets its types. tests/fingerprint.test.ts passes against the committed packages/postgres/engine.wasm.

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

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. The prewarm bound now holds per analysis, as the doc comment says.

Reviewed changes

I reviewed the one commit since the last review, which shares a single prewarm budget between the columns' probes and the JSON shapes' probes.

  • Shared Engine::prewarmed set: this one set replaces the per-call seen sets in described and in the JSON-shape prewarm in run. Both sites seed it with the analyzed statement, so across both sites at most MAX_PROBES guesses are asked in each round. described runs before the shape prewarm, so the statement tree's guesses are asked first. Any shape probes past the cap are settled in later rounds, which can cost a round but never changes a type.
  • Doc comment on prewarm_tree: it now states the shared bound, which resolves the earlier thread.

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

A hundred-line statement needs about a dozen probes, and eight left its
JSON columns unknown. The budget is 24, and the probes are cheaper: every
one of them is asked in one round, a settled probe is not settled again,
and the readings of a statement's text are kept.
@colinhacks
colinhacks merged commit 1b6c659 into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the probe-budget branch October 23, 2026 03:31
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