You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 6d4d5d3
Browse filesBrowse the repository at this point in the historyBrowse files
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)
* fix(driver-sql): fold an all-NULL sum to 0 in the aggregate presentation, 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
* test(conformance): keep a NULL aggregate answer as null in the SQL-family 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
* fix(driver-turso): the remote transport folds an all-NULL sum to 0 like 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
---------
Co-authored-by: Claude <noreply@anthropic.com>
`SqlDriver.aggregate` answers `0` — not `null` — for a `sum` over a group whose aggregand is NULL in every row, matching the engine's in-memory aggregate tier and the identity `emptyGroupValueFor` already declares (#15546; maintainer ruling 2026-09-07, option A: a non-empty group whose aggregand is absent and an empty group are the SAME case for `sum`, and the SQL face is the one that moves).
8
+
9
+
SQL `SUM` skips NULLs and answers NULL once it has skipped everything, so on every dialect this driver targets (measured on better-sqlite3, live PostgreSQL 16.13 and live MySQL 8.0.46) a grouped list view with a `sum` summary on a nullable number or currency column rendered a BLANK total for a group whose column was empty in every row — while the same view on a deployment whose query took the engine's in-memory path rendered `0`. Which path answered was decided by a driver capability bit the caller never sees. The fold is part of the driver's aggregate presentation (`foldEmptyAggregateAnswers`): the compiled statement is unchanged (no `COALESCE`), the answer is the JS number `0` on every dialect, and `avg`/`min`/`max` — which have no identity over nothing — still answer `null`. The identity is read from `emptyGroupValueFor` rather than restated, so the two faces cannot drift apart on it again.
10
+
11
+
`@objectstack/driver-turso`: the REMOTE transport's `aggregate` carries the same fold (`RemoteTransport.foldEmptyAggregateAnswers`). `TursoDriver` picks the remote compiler or the local `SqlDriver` one from `url`, so without it the same driver would have answered the all-NULL `sum` as `0` locally and `null` remotely — one query, two answers, decided by a connection string, the seam the shared conformance table exists to close. Measured `null` on the enrolled remote face before the fold.
12
+
13
+
`@objectstack/spec`: the aggregate-vocabulary conformance fixture gains a NULLABLE numeric column. `AggregationRow.amount` (`number | null`) is NULL in every row of the `east` group and in two of the four `west` rows, and `AGGREGATION_CASES` gains the three cases that pin the ruled answer on every enrolled face — `sum(amount)` grouped by region (`east` 0 / `west` 40), its `count(amount)` reachability control (`east` 0 / `west` 2, which is what proves the nulls were stored as nulls), and the ungrouped partial-null control (40). A harness that runs the table MUST declare `amount` as a nullable numeric column and seed its nulls AS nulls, exactly as it already must for `stage`; a `0` written in place of a null turns the cell green for the wrong reason.
0 commit comments