Repository navigation
get_pg_top_queries: make it parse, and stop rounding the PostgreSQL int8 identities (#2554, #2548) - #2553
Merged
Conversation
…#2548) `get_pg_top_queries` put `queryid` on the wire as a JSON number. PostgreSQL's `queryid` is a signed int8 derived from a hash of the post-parse-analysis tree, so its values are spread over the whole 64-bit range and most sit past 2^53 -- where every parser that decodes JSON numbers as IEEE-754 doubles (`JSON.parse`, `json.loads`, most agent tooling) silently rounds. Reproduced against real `JSON.parse`: `-4185925123159566327` comes back `-4185925123159566300`. The value was never wrong on the wire; it was unrecoverable after parsing, which for an identity is the same thing. `queryid` is the ONLY identity a PostgreSQL statement has, and every use of it -- `WHERE queryid = ...`, matching a row on our screen to one on the instance, quoting it in a ticket -- is an equality join. A rounded metric is still approximately true; a rounded key matches nothing. Breaking response-shape change, taken deliberately. `get_pg_blocking`'s `root_backend_id` had the same defect in a WORSE form, and is fixed here rather than filed for later. The collector builds it by concatenating the backend's start epoch with its zero-padded pid, so every value is a 17-digit integer around 1.79e16 -- about 2x past 2^53, where adjacent doubles are 2 apart. Half of all backend ids are odd and have no representation at all, and an unrepresentable one does not round to nothing: it rounds onto its even NEIGHBOUR, which is a different backend. Measured over 200 adjacent pids, 100 lost precision and 99 landed on an id belonging to another backend. That is the field the tool's own description tells a reader to prefer for comparing a root blocker across captures. Scoped to those two. `database_id` / `user_id` are PostgreSQL oids (unsigned 32-bit, structurally unable to reach the range), `root_pid` is an int, SQL Server's `query_hash` / `plan_hash` already reach this surface as `0x...` text, and Query Store's `query_id` / `plan_id` are sequential bigint IDENTITY columns nowhere near 2^53. None has a defect to fix; none was touched. Both response bodies are split into `BuildTopQueriesJson` / `BuildBlockingChainsJson` so the wire shape can be asserted against the SHIPPED serializer instead of a re-implementation that would keep passing as the code drifted. `PgInt64IdentityWireShapeTests` pins both fields as strings, pins `database_id` and `root_pid` as NUMBERS so "stringify everything" cannot pass, asserts its own fixture is genuinely out of double range, and checks that four distinct backends stay four distinct ids after a double-decoding parse. Proven red by reverting only the two `.ToString(...)` calls: 15 checks fail. The web needs no change -- the Query ID column already rendered the raw value, so it simply starts showing the true digits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ility gate (#2554) Two defects found by driving all nine get_pg_* reads against a live PostgreSQL 16 target. Eight answered; this one returned `42702: column reference "queryid" is ambiguous`. **Defect 1 — the SQL did not parse, on any engine.** #2219 added `LEFT JOIN collect.pg_statement_text AS t` so the read could carry statement text. That put `t.queryid` in scope beside `differenced.queryid` and made the unqualified references ambiguous. 42702 is a PARSE-time error, so the read threw on every call from #2219 onward — on Aurora, the one engine it exists for, as much as anywhere — and a store with zero rows failed exactly the way a full one did. The one-word fix in the report does NOT work, and that was measured rather than assumed: qualifying only the `GROUP BY` still fails on the select-list `queryid`, which is the reference PostgreSQL names first. Both are qualified. **Nothing caught it because nothing executed it.** About a dozen tests assert things about this query's TEXT and all of them passed throughout; a substring assertion cannot resolve a name, only a server can. So the deliverable is not the qualifier but `DarlingPgReadSqlParsesLiveTests`, which PREPAREs EVERY shipped PostgreSQL read against a real PostgreSQL. PREPARE runs the whole front end and stops before execution, so it needs no fixture and no rows — which is exactly why it catches a defect that zero rows was never a defence against. The reads are discovered by reflection, not listed, so a new one is covered the day it lands; the discovered COUNT is asserted too, so a filter that stopped matching cannot turn the guard into a test that passes by finding no work. All 12 shipped read constants across the 9 readers now parse. **Defect 2 — the capability gate was downstream of the throw.** It sat inside `if (rows.Count == 0)`, so it only spoke when the query SUCCEEDED and returned nothing. On stock PostgreSQL, where pg_statement_stats can never run at all, the parse error surfaced as a raw SQL error where the sibling get_pg_wait_stats gives "does not run on that engine ... and never will". The gate is now consulted on the throw path. Deliberately NOT moved ahead of the read. DarlingEngineCapability's contract is explicit that every call site asks AFTER its read came back empty, so a server whose registry row says one engine while its collected rows say another still gets its DATA rather than an explanation of why it cannot have any. Asking first trades this defect for that one across every read; on the throw path there is no data to prefer, so the same reasoning points the other way. Guarded without being vacuous. Once defect 1 is fixed the read no longer throws, so an assertion on the ordinary empty path would pass with or without the gate fix. The throw is therefore INDUCED — delta_calls at bigint extremes makes the read's `CAST(SUM(...) AS bigint)` overflow at runtime, deterministic and with no DDL against the shared store — and the discriminating half is the Aurora case: the same fault on an engine that CAN collect must still read as an error, or the fix would just be an exception swallower. Sibling sweep: all nine PostgreSQL tools have exactly one catch block and only this one consults the gate there, but with defect 1 fixed no other read can throw, so the other eight are recorded rather than changed. Three text pins that asserted the bare `GROUP BY queryid, database_id` are updated to the qualified form, and say which form is correct rather than merely that a GROUP BY exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReviewWent through the full diff (CHANGELOG, Correctness
Web parity claim (checked, not just taken on faith)
Lite/Darling parity
Security
Nothing to flag — the fix is scoped correctly, the qualification fix is measured rather than assumed (matches the PR's own note that the GROUP BY-only fix doesn't work), and the wire-shape guard tests exercise the shipped serializers rather than a re-implementation. |
This was referenced Aug 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2548. Fixes #2554.
Both are
queryiddefects inget_pg_top_queries, and #2554's touches the same statement, so they are taken together.#2554 — the read had never returned a row, on any engine
Defect 1: the SQL did not parse
[#2219] added
LEFT JOIN collect.pg_statement_text AS tso the read could carry statement text. That putt.queryidin scope besidedifferenced.queryidand made the unqualified references ambiguous. 42702 is a parse-time error, so the read threw on every call from #2219 onward — on Aurora, the one engine it exists for, as much as anywhere — and a store with zero rows failed exactly the way a full one did.Reproduced against a real PostgreSQL by
PREPAREing the shipped constant (dumped from the built assembly, not retyped):The one-word fix in the issue does not work, and that was measured rather than assumed. Qualifying only the
GROUP BY— the repair the report proposes — still fails, on the select-listqueryid, which is the reference PostgreSQL names first:FAILS 42702 ... LINE 27: queryid,GROUP BY differenced.queryidonlyFAILS 42702 ... LINE 27: queryid,PARSESDefect 2: the capability gate was downstream of the throw
The gate sat inside
if (rows.Count == 0), so it could only speak when the query succeeded and returned nothing. On stock PostgreSQL — wherepg_statement_statscan never run, itsAppliesTobeing Aurora-only — the parse error surfaced as a raw SQL error where the siblingget_pg_wait_statsgives the honest "does not run on that engine … and never will".Fixed by consulting the gate on the throw path, deliberately NOT by moving it ahead of the read.
DarlingEngineCapability's own contract is explicit on this:Moving it first would trade this defect for that one, across every read. On the throw path there is no data to prefer, so the same reasoning points the other way.
Nothing caught either, because nothing executed the query
About a dozen tests assert things about this query's text and every one passed throughout. A substring assertion cannot resolve a name — only a server can. So the deliverable is not the qualifier:
DarlingPgReadSqlParsesLiveTestsPREPAREs every shipped PostgreSQL read against a real PostgreSQL.PREPAREruns the whole front end — name resolution, ambiguity detection, type inference — and stops before execution, so it needs no fixture and no seeded rows, which is exactly why it catches a defect that zero rows was never a defence against. The reads are discovered by reflection, not listed, so a new one is covered the day it lands; and the discovered count is asserted, so a filter that quietly stopped matching cannot turn the guard into a test that passes by finding no work to do. All 12 shipped read constants across the 9 readers now parse.DarlingPgTopQueriesLiveTestsexecutes the tool end to end. The awkward part is guarding defect 2 without being vacuous: once defect 1 is fixed the read no longer throws, so an assertion on the ordinary empty path would pass with or without the gate fix. The throw is therefore induced —delta_callsatbigintextremes makes the read'sCAST(SUM(...) AS bigint)overflow at runtime, deterministic and needing no DDL against the shared store — and the discriminating half is the Aurora case: the same fault on an engine that can collect must still read as anerror, or the "fix" would just be an exception swallower.Sibling sweep (issue's ask #2)
catchblock, and only this one consults the gate there. But with defect 1 fixed no other read can currently throw, so the other eight are recorded here rather than changed.#2548 —
queryidwas a JSON number, and JSON numbers roundPostgreSQL's
queryidis a signedint8derived from a hash, so its values are spread over the whole 64-bit range and most sit past 2^53 — where every parser that decodes JSON numbers as IEEE-754 doubles rounds. Reproduced against realJSON.parsein node, before any fix:The issue's illustrative value was exactly right. The value was never wrong on the wire; it was unrecoverable after parsing, which for an identity is the same thing.
queryidis the ONLY identity a PostgreSQL statement has, and every use of it is an equality join. A rounded metric is still approximately true; a rounded key matches nothing. Option 1 from the issue, taken deliberately — and it matches how SQL Server'squery_hashalready reaches this surface, as0x…text.root_backend_idis the same defect in a worse form, and is fixed hereThe issue flagged it as "worth a look, though neither is used as a join key the way queryid is". That aside is wrong, in the dangerous direction.
PgBlockingCollectorbuilds the id by CONCATENATING the backend's start epoch with its zero-padded pid, giving a 17-digit integer around 1.79e16 — about 2x past 2^53, where adjacent doubles are 2 apart. Half of all backend ids are odd and have no representation at all, and an unrepresentable one rounds onto its even neighbour, which is a different backend:queryidloses the join;root_backend_idmakes the wrong one — in the field the tool's own description tells a reader to prefer overroot_pidfor exactly that cross-capture comparison.What was deliberately LEFT
Swept every
long/long?reaching JSON across both MCP surfaces. Nothing else has a defect to fix:database_id/user_idare PostgreSQLoids (unsigned 32-bit, structurally unable to reach the range);root_pidand the pid arrays areint; Query Storequery_id/plan_id(both apps, plusstructured_remediation.force_plan_targets[]) are sequentialbigintIDENTITY columns nowhere near 2^53, so changing them is a breaking change across Lite + Darling + web + the remediation contract with no defect to point at;query_hash/plan_hash/sql_handle/plan_handlealready reach this surface as0x…text; AG LSNs are already strings; PVS transaction ids, health-parser broker/node ids andview_idare small.Guard
PgInt64IdentityWireShapeTestspins both fields as strings anddatabase_id/root_pidas numbers (so "stringify everything" cannot pass), asserts its own fixture is genuinely out of double range, and asserts four distinct backends stay four distinct ids after a double-decoding parse.Writing that guard caught two of my own false claims, both of which would have made it look rigorous while proving nothing:
long.MinValueis exactly-2^63, a power of two, so a double holds it exactly and it belongs in the control set; and "everybackend_idis corrupted" is false — only the odd ones are, which is why the test states it asAssert.Equal(id % 2 == 0, SurvivesDouble(id)).Verification, and its limits
Every fix was proven red first, by reverting only that fix, then green:
.ToString(CultureInfo.InvariantCulture)callschains[1]decoding ontochains[0]'s idthe read EXECUTES instead of throwing 42702FAILS —42702: column reference "queryid" is ambiguouson stock PostgreSQL the same throw answers not_collectedFAILS — goterror: 22003 bigint out of rangeRun on macOS against a real migrated PostgreSQL store in Docker, driving the shipped
GetPgTopQueriesthrough a realNpgsqlDataSource, plus realJSON.parsein node for the wire shape. Harnesses are gitignored underDarling/tools/pg-harnesses/.What I could not verify:
Darling.Teststargetsnet10.0-windows— it compiles on macOS but cannot run there (No frameworks were found / Microsoft.WindowsDesktop.App). The two new live test classes are mirrored assertion-for-assertion by harnesses I did run against real PostgreSQL, but CI is the arbiter for the xUnit run itself. Not run against a live Aurora target; the Aurora-specific columns are exercised only as stored data.Web
No change needed, and that is checked rather than assumed. The Query ID column is
{ key: "queryid", label: "Query ID", mono: true }with noformat, sopanels.jsrenders it viaString(raw)and simply starts showing the true digits.inferFormatalready returns"text"for any string, so custom-view auto-detect picks the right formatter.root_backend_idis on no grid.