fix(driver-sql): an all-NULL sum answers 0 on every face — fold at the aggregate door, conformance cell on every enrolled face (#15546) - #16720
Conversation
…ion, and pin it on every face SQL SUM skips NULLs and answers NULL once it has skipped every row of a group, so driver-sql answered null where the engine's in-memory tier, driver-memory and driver-mongodb answer 0 and emptyGroupValueFor rules 0. Measured null on better-sqlite3, live PostgreSQL 16.13 and live MySQL 8.0.46. Ruled option A on #15546 (maintainer, 2026-09-07): the SQL face moves. The fold reads the identity from emptyGroupValueFor at the aggregate door; the statement is unchanged. The conformance fixture gains a nullable numeric column, amount, NULL in every east row, and three cases pin the ruled answer on every enrolled face; every harness that seeds the rows declares the column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…mily and in-memory harnesses Number(null) is 0 — the ruled answer for the all-null sum cell — so the unconditional Number(r.n) coercion made that cell green with the driver-sql fold ablated (measured 85/85 green against a driver answering null). The harnesses, not the faces, were holding the observable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…ke the local face TursoDriver picks the remote compiler or SqlDriver's from url, so without this the same driver answered the all-NULL sum 0 locally and null remotely. Measured null on the enrolled remote face with a null-preserving harness. Same fold, same policy read (#15546). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
📓 Docs Drift CheckThis PR changes 3 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 134 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 492599f4d60e44773d53e18455c4c90876a2e578 && git checkout 492599f4d60e44773d53e18455c4c90876a2e578
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6ba0db4e0b6622b6e7adfbea875f1681cd484141 8b73784c030641eb8a9db3a1f75805aca39a2bf2 && git checkout -B drift-repro 6ba0db4e0b6622b6e7adfbea875f1681cd484141 && git merge --no-ff 8b73784c030641eb8a9db3a1f75805aca39a2bf2
node scripts/docs-audit/affected-docs.mjs --json 6ba0db4e0b6622b6e7adfbea875f1681cd484141
|
Contract review (
|
| ruling item | on head | verdict |
|---|---|---|
| fold in driver-sql's aggregate presentation (read door, not the statement) | SqlDriver.aggregate registers foldedOutput per aliased aggregation and applies foldEmptyAggregateAnswers after the rows return, before presentReadColumns; the ['sum', { sql: 'sum', distinct: false }] lowering and the emitted SQL are unchanged (no COALESCE) |
implemented |
conformance cell in aggregation-conformance.ts, every face |
AggregationRow.amount: number | null, fixture rows extended, three new AGGREGATION_CASES (§3) |
implemented |
emptyGroupValueFor / the spec reducer not touched |
git diff 6ba0db4e0..head -- packages/spec = the conformance file only; aggregation-policy.ts byte-identical |
implemented |
changeset minor for @objectstack/driver-sql |
.changeset/driver-sql-all-null-sum-folds-to-zero.md: "@objectstack/driver-sql": minor |
implemented |
Clause-②: yes carried |
first line of the PR body | present |
Widenings beyond the ruling's named surfaces:
packages/drivers/driver-turso/src/remote-transport.ts+ its harness — the ruling names@objectstack/driver-sqlas "the side that moves" and does not name driver-turso. But the ruling also requires the cell to hold "on every face", and the conformance file onorigin/mainenrollsdriver-tursoTurso REMOTE compiler —RemoteTransport.aggregateon its own account (its LOCAL mode is deliberately not enrolled because it inheritsSqlDriver). With a null-preserving harness that enrolled face is red without a fold. I read this as within the ruling's "conformance cell on every enrolled face" scope, not an unruled scope expansion; the PM seat ruled the same at5577543465Q1. Recorded as F1 because it is a second published package moving under a ruling that named one.packages/spec/src/data/aggregation-conformance.ts— explicitly named by the ruling ("a conformance cell inpackages/spec/src/data/aggregation-conformance.ts"). Not a widening. The card's older "separatedomain:speccard" text is superseded by the ruling, as the PR says.@objectstack/spec: minorand@objectstack/driver-turso: minorchangeset entries — not in the ruling; graded in §4 / F2.
2. The fold (sql-driver.ts, measured on head)
Hunks, from git diff -U0: @@ -23 +23 @@ (import), @@ -8500,0 +8501,6 @@, @@ -8629,0 +8636,6 @@, @@ -8721 +8733,54 @@. All inside aggregate() (:8478) and the new method directly under it (:8778). Nothing in :9600–:11300 (the #16570 fence). The three read-presentation helpers sit at :12939 / :12966 / :16720 on head and are untouched (the #16619 fence held too).
- (a) Only
sumfolds.foldedOutput.set(agg.alias, identity)runs only whenemptyGroupValueFor(funcName) !== undefined, i.e. forcount,count_distinct,sum.avg/min/maxreturnundefinedfrom the policy, are never registered, and their NULL passes through.count/count_distinctare registered butCOUNTnever answers NULL (node-pg hands back the string"0"), so the entry is inert for them — the docblock says so, deliberately. - (b) Read from spec, not hard-coded.
import { AggregationFunction, emptyGroupValueFor } from '@objectstack/spec/data'at:23; the fold value isemptyGroupValueFor(funcName); no literal0appears in the fold path. - (c) Aliased branch only. The registration sits inside
if (agg.alias) { … }; the unaliased branch is untouched. The stated reason checks out:AggregationNodeSchema.aliasisz.string()— required — (query.zod.ts:275), so the unaliased branch is unreachable through the protocol and lands under a dialect-dependent column name; the pre-existingpresentedOutputcomment makes the same call. - (d)
foldEmptyAggregateAnswersfolds only=== null(anundefinedcolumn stays visible), mutates in place aspresentReadColumnsdoes, and isprotectedsoSqliteWasmDriverandTursoDriver(LOCAL) inherit it.
Observation: aggregate() emits no HAVING; the engine's applyHaving runs on the driver's returned rows, so a having on a folded sum now sees 0 on the SQL face exactly as on the in-memory face. Consistent, no action.
3. Conformance fixture and every enrolled harness
aggregation-conformance.ts on head: amount: number | null at :223; rows 1..6 carry amount = 10, null, 30, null, null, null (east = rows 5,6 → all null; west = 10+30 = 40, two nulls). Three new cases at :525–:565: sum(amount) grouped by region → east 0 / west 40; count(amount) grouped by region → east 0 / west 2; sum(amount) whole table → 40. score unchanged (10,20,30,40,50,60, all distinct, score: number at :201).
Harness audit — declares amount nullable numeric / keeps null as null:
| harness | declares amount |
null coercion on head |
|---|---|---|
driver-sql sql-driver-aggregation-conformance.test.ts (sqlite/pg/mysql cells) |
amount: { type: 'number' }, no NOT NULL |
r.n === null ? null : Number(r.n); read-back asserts 4 null amounts, 2 of them east |
| driver-sql #11635 suite | amount: { type: 'number' } (seeds the verbatim rows) |
does not iterate AGGREGATION_CASES; its Number(...) asserts are on boolean/score cases — not this cell |
| driver-sqlite-wasm | amount: { type: 'number' } |
r.n === null ? null : Number(r.n) |
| driver-turso REMOTE | amount: { type: 'number' } |
r.n === null ? null : Number(r.n) |
| objectql in-memory | n/a (pure function over AGGREGATION_ROWS) |
r.n === null ? null : Number(r.n) |
| driver-memory (both doors) | n/a (InMemoryDriver.create({...row})) |
never coerced (value: r.n, by design since #6814) |
| driver-mongodb lowering | n/a (evaluator over AGGREGATION_ROWS) |
raw row[MEASURE], never coerced; iterates every case |
No enrolled harness coerces Number(null) → 0 any more. The cell is not vacuous on any face.
4. Changeset level for @objectstack/spec
Batch #35 ruling text (pr-automation.yml:660–682): "A purely additive widening of a published package's public surface (a new exported symbol on an index, a new accepted key or value) takes at least minor. … During the launch window major stays refused by check-changeset-no-major and breaking-ness is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level." scripts/check-changeset-no-major.mjs header: "Until then [GA] … a required member on a published interface … grades major" only after GA; now it is minor plus a BREAKING banner if breaking.
AggregationRow (interface), AGGREGATION_ROWS, AGGREGATION_CASES are all on packages/spec/api-surface/data.json — published. The act is additive on the fixture (new key on every row, three new cases): minor is the floor and is what the PR grades. Is a required amount on AggregationRow breaking? Only for a consumer that constructs AggregationRow values; no consumer in the tree does (every harness reads AGGREGATION_ROWS), the docblock defines the interface as the shape of the shipped fixture, and the third-party consequence — a harness that does not declare amount goes red on the three new cases — is the conformance table doing its job, stated in the changeset body ("A harness that runs the table MUST declare amount as a nullable numeric column and seed its nulls AS nulls"). I grade minor correct, no **BREAKING** banner / ADR-0087 disposition owed. The flag column precedent (#12947, spec patch, 2026-08-28) is pre-rule and does not bind. CI's LEVEL AXIS did grade the spec entry (green); it is blind to packages/drivers/* (#16713), so the two driver minor entries are ungraded by CI but follow the same rule.
5. Files in the diff (9) and governed paths
.changeset/driver-sql-all-null-sum-folds-to-zero.md (A), packages/drivers/driver-sql/src/sql-driver-11635-boolean-aggregand-answers.test.ts, packages/drivers/driver-sql/src/sql-driver-aggregation-conformance.test.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-aggregation-conformance.test.ts, packages/drivers/driver-turso/src/remote-transport.ts, packages/drivers/driver-turso/src/turso-remote-aggregation-conformance.test.ts, packages/objectql/src/in-memory-aggregation-conformance.test.ts, packages/spec/src/data/aggregation-conformance.ts.
Governed paths touched: NO — none of docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/**.
6. Tests: ablation reasoning and CI measurement
Ablating the driver-sql fold makes SQL's SUM(amount) for east arrive as null; with the harness now keeping null, actualFor yields { east: null } ≠ { east: 0 } → red on exactly sum(amount) grouped by region — east is NULL in every row and sums to 0. The whole-table case (40) and count(amount) (COUNT never NULL) stay green; every score case is non-null. One case × three dialect cells = 3 failed, matching the PR's 3/82. The west: 40 expectation also refuses a fold-everything-to-0 implementation.
Is the cell measured in CI, not only locally? Yes. ci.yml Temporal Conformance (live PG + MySQL) (:894) runs pnpm --filter @objectstack/driver-sql test — the whole suite — against service containers with OS_TEST_POSTGRES_URL, OS_TEST_MYSQL_URL and OS_EXPECT_LIVE_DIALECT_MATRIX: '1'; live-dialect-matrix.testkit.ts turns a missing URL into a named failure under that flag. The job is gated on filter.outputs.core, which packages/** sets. Test Core shards carry the sqlite cell plus the turso, sqlite-wasm, objectql, driver-memory and driver-mongodb harnesses. All of those are green on head.
7. CI on 8b73784c0 and base drift
Check runs: 50 total — 36 success, 14 skipped, 0 failure, 0 in_progress. Skipped are path-filtered/opt-in only (Auto Label ×5, Check PR Size ×5, Packed-tarball smoke ×2, Console Pin Gate, Build Docs). Red or pending: none. Named greens: Temporal Conformance (live PG + MySQL), Test Core 1–6/6, Build Core, Type Check ×4, Lint & Repo Gates, Check Changeset, Governed Surface Queue Guard, single-writer-path and same-issue guards, Spec property liveness, Dogfood ×3 + Verify CLI.
Base: merge-base with origin/main is 6ba0db4e0 (the PR's base SHA); origin/main is 18 commits ahead (73053ed27). Only one of them touches a file in this diff: 45cfa1b88 (#16619, sql-driver.ts), whose hunks are at :264–:404, :4147, :5309 and :12863+ (the read-presentation helpers) — not in aggregate(). aggregation-conformance.ts, aggregation-policy.ts and remote-transport.ts are untouched on main since the base. git merge-tree origin/main head merges clean. The head does not yet contain #16619; nothing overlaps.
Findings
- F1 — second package moved under a ruling that named one.
RemoteTransport.aggregate(driver-turso) carries a twin fold. It is inside the ruling's "every enrolled face" scope (Turso REMOTE is enrolled on its own account in the conformance file) and the PM seat ruled keep at5577543465; the maintainer's text itself names only driver-sql. Expectation: keep as is; the merger should be aware they are landing a driver-turso behaviour change (null→0on the remote face) under this card. If it is ever split, the harness must keepnullasnull— the split must never be made green by restoring the coercion. - F2 —
@objectstack/spec: minoris the right level and needs no BREAKING banner (§4). Expectation: none required. Optional: one clause in the spec paragraph of the changeset body namingAggregationRow.amountas a required member of the interface (the PR body says it; the changeset says "gains a NULLABLE numeric column"). - F3 — driver-memory fixture read-back does not assert the
amountnulls (it checksregion/stage/scoreonly). Not vacuous: thecount(amount)control answerseast 0through both doors only if the nulls were stored. Expectation: none required; a one-lineamountassertion in that read-back would mirror the driver-sql harness. - F4 —
count/count_distinctare registered infoldedOutputthough they never arrive as NULL. Deliberate and documented; inert. Expectation: none. - F5 — the two driver
minorentries are not graded by CI's LEVEL AXIS (PUBLISHED_SOURCE_PATHdoes not matchpackages/drivers/*, [finding] The changeset LEVEL axis is blind to every NESTED package:packages/*/src/**matches one segment, so 51 of 74 workspace packages (all drivers/services/adapters) can pairClause-②: yeswithpatchand stay green #16713). They are correct under batch [WIP] Add query enhancements and advanced validation features #35 by the same reading. Expectation: none for this PR. - F6 — the PR body's confidence-gap table calls the mongodb reading "the LOWERING, not MongoDB" and the mongodb evaluator's
$sumreduces from0over the numeric subset, so that face's0is a model of the stages, as stated. Expectation: none; the caveat is already on the record.
Hard limits observed: no approval, no review-with-changes, no merge, no label/body edits, no file edits. One comment.
Generated by Claude Code
Fixes #15546
Clause-②: yes — a published aggregate output changes on the SQL driver (and on the turso REMOTE transport): a
sumover a group whose aggregand is NULL in every row now answers0where it answerednull.The ruling this PR executes
Recorded on the card (comment 5572006116, director seat, decision batch #1, 2026-09-07, maintainer reply 「同意」): option A — three rows whose aggregand is absent and zero rows are the SAME case for
sum; the answer is0, the valueemptyGroupValueForalready gives the in-memory tier; the driver-sql side moves; the spec reducer is not touched. Execution scope, per the ruling: one PR — the fold in driver-sql's aggregate presentation, a conformance cell asserting the same answer on every face (SQLite, live Postgres, live MySQL, the in-memory tier), changesetminorfor@objectstack/driver-sql. The older text on the card that split the conformance change into a separatedomain:speccard is superseded by the ruling and was not acted on.1. The confidence-gap measurement — taken FIRST, before any fix
A five-row probe table (
void_unit× 3 withamount[number] andprice[currency] NULL in every row;full_unit× 2 with 10/20), grouped bybusiness_unit, through each face's own aggregate door. Every row below EXECUTED — none was a skip.sum(number)sum(currency)sumnullnullnulltimezone=Asia/Shanghai, processTZ=America/New_Yorknullnullnulltime_zone=+08:00nullnullnullapplyInMemoryAggregation)0(number)0find()door ANDaggregate(AST)door0(number)0buildAggregationPipelinethrough the package's in-process pipeline evaluator — the instrument its enrolled suite uses; a model of the stages, NOT a live mongod (none is installed here)0(number)[])RemoteTransport.aggregate, libsql-is-SQLite stub)nullavg/min/maxanswernullon every face andcount_distinctanswers0on every face —sumis the only divergence, exactly as the card said. driver-sql probe run: 9 tests, 9 passed, 0 skipped (3 tests × 3 cells) underOS_EXPECT_LIVE_DIALECT_MATRIX=1, so a missing URL would have been a named red, not a skip. node-pg handscount/count_distinctback as strings ("3","0") and float sums as numbers — the pre-existing #11456 reading, untouched.driver-memoryanddriver-mongodbboth answer the ruled0today: no divergence to file, and nothing in either package is changed by this PR (scope). The mongodb reading is a reading of the LOWERING, not of MongoDB.2. Symbols re-derived on the branch point
2e6a2ea4c9— line numbers as foundpackages/spec/src/data/aggregation-policy.ts—export function emptyGroupValueFor(at:52. Unchanged by this PR, and unchanged by the fiveorigin/maincommits merged since (empty diff, with a positive control onmigrations/registry.tsthat fired).packages/drivers/driver-sql/src/sql-driver.ts—['sum', { sql: 'sum', distinct: false }],at:1324(unchanged: the fold is not in the lowering table);async aggregate(at:8478; thereadPresentationKind(table, agg.field)call at:8653; definitionsreadPresentationKind:12874,presentReadValue:12901,formatOutput:16655.packages/spec/src/data/aggregation-conformance.ts—score: number;at:200.packages/objectql/src/engine.ts— thetypeof drv.aggregate === 'function' && allStructuredSupported && !tzRequiresInMemory && !hasAggregationFilterfork at:13816. Unchanged.None of these numbers was taken from the card; every one matches the PM's own re-derivation.
3. How the all-NULL cell became reachable through the table
scorestays non-null — its all-distinct property is load-bearing for thecount_distinctcontrol. The fixture gains a second numeric column,AggregationRow.amount: number | null, NULL in everyeastrow and in two of the fourwestrows, andAGGREGATION_CASESgains three cases:sum(amount) grouped by region — east is NULL in every row and sums to 0→east0 /west40 (the ruled cell;westat 40 keeps a face that folded EVERY sum to 0 from passing);count(amount) grouped by region — the nulls are real→east0 /west2 (the reachability control:COUNT(col)is defined over non-null values on every backend, soeastat 0 proves the seed stored NULLs; a harness that wrote 0 for null answers 2);sum(amount) over the whole table skips the nulls→ 40 (the partial-null control, the case that was always green).Every harness that seeds
{...row}into a declared object now declaresamountas a nullable numeric column (driver-sql conformance, the #11635 suite, sqlite-wasm, turso remote). The driver-sql harness also reads the nulls back throughfind()and asserts four null amounts, botheastrows among them.4. Two-leg ablation — and the layer that was holding the observable
Leg 1 (fold in place): driver-sql conformance 85/85 on sqlite + live postgres + live mysql, 0 skipped.
Leg 2, first attempt (fold ablated by neutralising the registration line; on-disk proof: marker
ABLATION-155460 → 1, anchor 1 → 0, mutated blobd4f0496e…differs from HEAD'sd418bec2…): 85/85 STILL GREEN. Per the order I did not call the ablation wrong. The holder was the harness:actualForcoercedvalue: Number(r.n), andNumber(null)is0— the ruled answer. The same coercion sat in the turso remote, sqlite-wasm and objectql harnesses; the driver-memory harness alone had refused it, for exactly this reason. All four now keepnullasnull(r.n === null ? null : Number(r.n); node-pg's stringcountstill coerces).Leg 2, second attempt (same mutation, same on-disk proof): 3 failed / 82 passed — exactly
sum(amount) grouped by region, on sqlite, live postgres and live mysql; nothing else moved. Restore bygit checkout HEAD -- path, verified by blob hashd418bec214e6a86349f32aa60da61a8184567f5cequal to theHEAD:blob,git diff HEADempty,git status --porcelainempty, marker count 0. driver-sql's suite resolves./index.jsfromsrc, so nodistsits on that leg's resolution path.turso REMOTE legs (its own compiler,
remote-transport.ts): leg 1 29/29; leg 2 (same mutation shape, marker 0 → 1) 1 failed / 28 passed — the same cell; restore blob-verified44612eb6511a783fc0708dbc5e0d30b5adba31e9equal to HEAD.sqlite-wasm went red once for a different reason: its tests import
@objectstack/driver-sqlfromdist(theKNOWN_UNALIASED_TEST_IMPORTSledger) anddistpredated the fix. Afterpnpm --filter @objectstack/driver-sql build,node scripts/ablation-dist-preflight.mjs @objectstack/driver-sql foldEmptyAggregateAnswersreports the marker in 4 built files, and the suite is 25/25.5. One in-place fix outside driver-sql: the turso REMOTE transport
With a null-preserving harness the shared cell was RED on
driver-turso's enrolled REMOTE face:RemoteTransport.aggregateis a second compiler and hands SQL's NULL through. Leaving it would make the ruled cell unlandable on an enrolled face, or green only through the coercion that hid the defect. The four in-place conditions hold: same defect class; mechanical, the same fold shape readingemptyGroupValueFor; no other claim onremote-transport.ts— all five remote branches naming turso have tips from 2026-02 to 2026-05 and their diff againstorigin/mainshows the file as 3892 insertions on main's side, i.e. it did not exist at those tips (positive control: my own branch's diff on the turso test is non-empty); same gate family (the turso conformance suite, already enrolled). File surface added to the claim:packages/drivers/driver-turso/src/remote-transport.tsandpackages/drivers/driver-turso/src/turso-remote-aggregation-conformance.test.ts. The changeset carries@objectstack/driver-turso: minor. If the seat wants this split out, the turso half is separable: commit99d4549570plus the turso hunk of3611073dea.6. Hot-file fence — held
Hunks in
sql-driver.tssit at:23(import),:8501–8506,:8636–8641and:8733–8786— all insideaggregate()and the method added directly under it. The three helper definitions are untouched (now at:12939,:12966,:16720after the shift). Nothing lands in:9600–:11300(#16570's region). Nothing had to be stopped and reported on the fence.7. Changeset
minorfor@objectstack/driver-sqlbecause the dispatch order and the ruling sayminor. Two further entries are my choice under AGENTS.md's "add a changeset for anything that publishes":@objectstack/driver-turso: minor(§5) and@objectstack/spec: minor(AggregationRowgains a requiredamountkey andAGGREGATION_CASESthree cases — a published fixture third-party driver authors run). Strike either at review if unwanted.8. Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(the order's.claude/scripts/…path does not exist; the agent file's path was used and the PM confirmed). 80 commands, all exit 0 after two prerequisite refusals were resolved by building every package'sdist(72 turbo tasks) —check:dual-build-cjs-loadsthen passed at 104 entry points across 67 packages;check:type-check-debtOOM'd its whole-repotscre-measure at the 4 GB heap (Ineffective mark-compacts near heap limit) and passed at 8 GB (76/80 packages type-checked, 4 in the DEBT ledger, 1 exempt). Reconciliation, verbatim:✓ dispatch-gates --ran: 80 derived famil(ies) accounted for — 80 run, 0 NOT-MEASURED.on the merged tree8b73784c03.tsc --noEmitexit 0;pnpm --filter @objectstack/spec check:generated: all 15 generated artifacts up to date (nothing to regenerate).8b73784c03, with the fix: driver-sql conformance + driver-sql: boolean aggregands need a lowering cast on PG (+ a MySQL min/max presentation check) — the ruledfalse/true+ arithmetic answers are unproducible on the PG face #11635 113/113, 0 skipped, three cells live; turso remote 29/29; sqlite-wasm 25/25; driver-memory 54/54; driver-mongodb 86/86 (three files); objectql in-memory 28/28.验收备注
count/count_distinctas strings while float sums come back as numbers (the [finding] sql-driver-aggregation-conformance.test.ts hard-codes better-sqlite3, so the aggregate-vocabulary standard never runs on Postgres — its pagination sibling already uses the live-dialect cells #11456 reading) — unchanged here; the harnesses coerce non-null answers as before.SqlDriver.aggregate(noalias) is not folded, for the reasonpresentedOutputalready gives —aliasis required byAggregationNodeSchema, and that branch lands under a dialect-dependent column name.[]) where the SQL faces answer one row ofcount: 0, sum: null— the "database omits the row" shape theemptyGroupValueFordocblock describes, repaired today in the engine's in-memory tier, outside this card's cell..claude/scripts/dispatch-gates.mjs; the script lives atscripts/pm/dispatch-gates.mjs.🤖 Generated with Claude Code
Generated by Claude Code