Uh oh!
There was an error while loading. Please reload this page.
fix(driver-sql): aggregate() joins the enveloped read exits — a dialect fault it cannot attribute leaves as DATABASE_ERROR/500 instead of raw - #11545
Merged
Conversation
`SqlDriver.aggregate()` executed its statement bare, so every dialect error left the driver as the backend's own object — a raw SQLSTATE in `code`, `status` undefined, and the compiled statement as the message. Measured on live PostgreSQL 16.13, `avg`/`sum`/`min`/`max` over a `boolean` column raise `42883 function avg(boolean) does not exist`, which is on no list `@objectstack/rest` reads: an ordinary analytics shape was logged as an unhandled server fault. The door now composes the same terminal `backendStatementFaultError` that `find()` and `count()` have carried since #8931 — `DATABASE_ERROR` / 500, the dialect diagnostic to the server log, the original kept as a non-enumerable `cause`. No new error code (ADR-0112 D3/D4 closed the vocabulary), no boolean-specific recognizer, and no verdict about whether a boolean aggregate should answer a number or refuse — that contract question is #11152's, and #11249's for `min`/`max`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-zhuang
marked this pull request as ready for review
August 24, 2026 03:12
os-zhuang
enabled auto-merge
August 24, 2026 03:12
os-zhuang
disabled auto-merge
August 24, 2026 03:14
os-zhuang
enabled auto-merge
August 24, 2026 03:14
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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#11455
SqlDriver.aggregate()executed its statement bare — notry, no envelope. Every dialect error the backend raised left the driver as the backend's own error object: acodefrom the backend's vocabulary, nostatusat all, and a message opening with the compiled statement.find()andcount()have carried the terminal ADR-0112 envelope since #8931; this third read door was simply never given it.The measurement
Live PostgreSQL 16.13 (
Asia/Shanghaiserver zone,TZ=America/New_Yorkprocess, CI parity),driver-sqlmaps abooleanfield to a real PGbooleancolumn andSQL_AGGREGATE_FUNCTIONSlowers the arithmetic aggregates to a bare function name with no cast:A raw
42883is on no list@objectstack/restreads, so withstatusundefined an ordinary analytics shape — a rate measure over a flag column — was logged as an unhandled server fault, and the statement's shape travelled to the caller with it.The change
One guarded execution in
aggregate()(packages/drivers/driver-sql/src/sql-driver.ts, a single hunk at@@ -7634,7 +7634,57 @@):Verified at the call sites, not at the symbol name (a good fix names the old symbol in its docblock, so a symbol-name grep proves nothing):
whole-file literal count of that call expression: 3; definition sites of
protected backendStatementFault(: 1.Why
DATABASE_ERROR/ 500 — argued from the code, and no code was mintedADR-0112's vocabulary is closed. D3/D4 closed
StandardErrorCode, and D2's 2026-08-18 amendment retired three members on the reasoning that an unreachable-but-declared code teaches a branch that can never fire. So the choice is among catalogued members, and the one chosen is the one the sibling read exits already answer with.backendStatementFaultError(#8931, maintainer ruling 2026-08-17 「同意 C」) asserts exactly one thing — the backend would not run this statement — and its docblock states why the alternatives are unavailable here, word for word applicable to this door: 400 "would assert the caller's request is at fault, and this envelope exists precisely because the driver cannot attribute the failure"; 500 "keeps the answer insidedeclaresServerFault, so both HTTP doors withhold the prose and log it". The registered near-misses were already measured and rejected on that card (UNSUPPORTEDandUNSUPPORTED_QUERY_PARAMare 400s on the client-mistake list,UNSUPPORTED_TRANSFORMbelongs to@objectstack/rest's import mapper).The three refusals this door can attribute keep their own precise codes and are unaffected — all are raised while the statement is built, upstream of the guarded execution:
INVALID_QUERY/ 400count_distinctwith nofieldINVALID_QUERY/ 400filterNOT_IMPLEMENTED/ 501The fence: this is the envelope half, and it decides no contract
Whether the platform should answer a number for an arithmetic aggregate over a boolean (by casting boolean to int in the lowering) or refuse is the open contract question on #11152, and on #11249 for
min/max. Nothing here pre-empts either card, and both remain open:SQL_AGGREGATE_FUNCTIONSis untouched.42883or the words "does not exist". That is driver-sql: MySQL's unresolvable-column wording is matched by neither arm of the recovery/refusal predicate, so MySQL gets no ADR-0112 envelope and no #3821 recovery #8926's lesson applied in advance — that card is the one where a predicate arm's wording match silently failed to fire on a dialect nobody had measured, and this fix has no wording to match and no dialect that can be forgotten. The all-dialect sweep in the new suite asserts that structurally on a route (a table that was never provisioned) that involves no boolean at all.INVALID_QUERY/ 400 is asserted absent on the boolean route. That code would be the platform saying "a mean over a flag column is your mistake" — which is one of the two answers [finding]AGGREGATION_ROWShas no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152 has yet to choose between.Cross-dialect reading (the card asked for it; MySQL was unmeasured)
Measured 2026-08-24 on the live servers in this container, raw SQL,
booleancolumn asdriver-sqlcreates it:sumavgminmaxcountboolean42883428834288342883tinyint(1)So MySQL answers arithmetically where Postgres refuses — three dialects, three behaviours, for one
QueryAST. That divergence is reported to the PM rather than filed or folded in here, since it is evidence for #11152's contract question, not a defect of this envelope.Reverse verification — the pins were seen red
The new suite was run against the pre-fix source. 23 of its cases failed, reproducing the card verbatim on all three dialects:
Test Files 1 failed | 125 passed (126)·Tests 23 failed | 2520 passed (2543)— the rest of the package was green pre-fix, so the 23 are this suite alone.Tests — run at
31edafe0, a clean tree, live PG + MySQL provisionedgit rev-parse --short HEADat the union run:31edafe0;git status --porcelainempty.pnpm --filter @objectstack/driver-sql test -- --maxWorkers=2Test Files 126 passed (126)·Tests 2543 passed (2543), 0 skippedpnpm --filter @objectstack/driver-sql typecheck(tsc --noEmit)pnpm lint(eslint . --no-inline-config, whole repo)pnpm --filter @objectstack/driver-turso testTest Files 39 passed (39)·Tests 1006 passed (1006)pnpm --filter @objectstack/driver-sqlite-wasm testTest Files 25 passed (25)·Tests 395 passed (395)Both subclass drivers extend
SqlDriverand inherit this method, which is why they are the consumer set that was run.Gate family derived from the real change set with
node scripts/pm/dispatch-gates.mjs(no hand-written path list), every one green, each quoted from the gate's own verdict line rather than from a shell exit code:check:nul-bytes(OK (scanned 6461 text file(s) … no raw ASCII control bytes)) ·check:driver-conformance(OK — 45 covered cell(s), 0 in the DEBT ledger) ·check:published-files·check:slot-lookup(107 unswept site(s) …, none new;baseline key set verified against daacc10: no files added) ·check:test-source-alias·check:type-source-resolution·check:query-options-erasure·check:type-check-coverage·check:engine-double-contract(OK — 394 pinned, 133 in the DEBT ledger) ·check:cross-package-test-inputs(14 package(s) read outside themselves, all declared) ·check:where-matcher(290 matcher(s) …, 0 silently-wrong … none new) ·check:error-code-casing·check:changeset-gate-self-tests·check:objectui-changeset·check-adr-0087-registration(adds no declared-breaking changeset (1 non-breaking changeset(s) seen)) ·check-changeset-no-major·check-empty-changeset(1 declaring changeset(s) added) ·check-ci-filter-parity·check-plugin-teardown-shape·docs-audit/check-affected-docs.One declared narrowing:
check:type-check-debt's--re-measurehalf was not run locally — it re-runstscper ledger entry, and@objectstack/driver-sqlcarries no entry inDEBT,EXEMPTorTEST_DEBT(all three checked inscripts/check-type-check-coverage.mjs). Itstsconfig.jsonincludeissrc/**/*with no test exclusion, so the new test file is inside the program thatpnpm --filter @objectstack/driver-sql typecheckjust compiled clean, and the structural half (check:type-check-coverage, plus its self-test) is green. CI runs the ratchet regardless.The new suite
packages/drivers/driver-sql/src/sql-driver-11455-aggregate-fault-envelope.test.ts, built onlive-dialect-matrix.testkit.tsso the dialect axis is a declared cell list rather than a hard-coded client — an unprovisioned cell is a named skip, never a silent pass, and the axis itself is asserted (['sqlite','pg','mysql']) before it is iterated.All three dialects: the envelope on an unclassified fault · one answer across
find/count/aggregate· no filter or query verdict claimed · the original kept as a non-enumerablecause(soisMissingTableErrorstays truthful andJSON.stringifycarries no dialect text) · the dialect diagnostic present in the server log. Postgres only: each ofsum/avg/min/maxover a boolean column carryingDATABASE_ERROR/ 500 with SQLSTATE42883on thecause, plus a positive control that the log really did containfunction avg(boolean) does not exist— so the caller-visible absence is a withholding, not an assertion about a string that never held it.Controls that would catch the two ways this could go wrong: a working aggregate untouched, and all three precise refusals still winning over the catch-all.
Region fence
sql-driver.tscarries exactly one hunk,@@ -7634,7 +7634,57 @@. The three sibling-claimed regions are untouched —toDateOnly,createColumnandintrospectForeignKeyseach appear on 0 changed lines of the diff.Out of scope, filed rather than fixed
#11541 — an unresolvable WHERE column on
aggregate()answersDATABASE_ERROR/ 500 wherefind()andcount()answerINVALID_FILTER/ 400. Measured with this branch in place. It is not fixed here because a blanketisUnresolvableColumnErrorarm on this door would be wrong: the refusal's words are "Filter on '…' names a column that object '…' has no column for", and this door names columns in three clauses — the WHERE, thegroupByfields and the aggregationfield— so an author who wroteavg('nosuchcol')would be told their filter was at fault. Answering it truthfully needs a clause classifier this card did not measure. #11541 remains open and carries the measurement.Not addressed here, out of scope: #11152 and #11249, the contract question above.
Generated by Claude Code