Skip to content

Commit 1c47678

Browse files
committed
docs: state the moved sum / avg residual in the pending driver-sql note and its comments
The rows path now adds with compensated summation, so the residual the pending driver-sql double-accumulation note, the AGGREGATE_ACCUMULATION comment and the driver-sql test header stated ("SQLite's native face alone", "every other face 0.6000000000000001") no longer held. Each now states it as PostgreSQL / MySQL native against SQLite and the rows path. Text only; no logic moves. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
1 parent 925368f commit 1c47678

4 files changed

Lines changed: 22 additions & 17 deletions

File tree

‎.changeset/20387-aggregate-one-double.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ the rows path add exact decimals and round once. It was rejected because SQLite'
3939
adds the stored doubles (`0.30000000000000004`), so the rows path would then disagree with SQLite
4040
for exactly `0.1 + 0.2`.
4141

42-
**Residual, stated.** Double sums are added in row order, one after another, as the rows path
43-
adds them. SQLite 3.43 and later adds with compensated summation. So for a group of three or
44-
more fractions, SQLite's native answer can still differ from every other face in the last
45-
place (`0.1 + 0.2 + 0.3`: SQLite `0.6`, every other face `0.6000000000000001`). This was
46-
already true of SQLite's two paths before this change. Two addends cannot differ.
42+
**Residual, stated.** On PostgreSQL and MySQL the double sums are added in row order, one after
43+
another, without compensation. SQLite 3.43 and later adds with compensated summation, and since
44+
#20489 so does the engine's rows path. So for a group of three or more fractions, the PostgreSQL
45+
and MySQL native answer can still differ from SQLite's and the rows path's in the last place
46+
(`0.1 + 0.2 + 0.3`: PostgreSQL / MySQL native `0.6000000000000001`, SQLite and the rows path
47+
`0.6`). Before #20489, SQLite's own two paths differed there too. Two addends cannot differ.
4748

4849
A consumer that compared `sum` / `avg` over a fractional column with a decimal literal on
4950
PostgreSQL or MySQL (`$eq: 0.3`) now gets the answer SQLite and the rows path already gave:

‎.changeset/20489-rows-path-compensated-sum.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ JS number.
3232
**Residual, stated.** PostgreSQL and MySQL add `sum` / `avg` natively in double without
3333
compensation, and that arithmetic is the database's own. So over three or more fractions their
3434
native path can still differ from the rows path in the last place (`0.1 + 0.2 + 0.3`: native
35-
`0.6000000000000001`, rows path `0.6`). This replaces the residual the `@objectstack/driver-sql`
36-
entry for the double accumulation states: that difference is no longer SQLite's native path
37-
against every other face; it is now PostgreSQL / MySQL native against the rows path. The
35+
`0.6000000000000001`, rows path `0.6`). The `@objectstack/driver-sql` entry for the double
36+
accumulation states the same residual: the difference is no longer SQLite's native path against
37+
every other face; it is PostgreSQL / MySQL native against SQLite and the rows path. The
3838
in-memory driver (`@objectstack/driver-memory`) still adds naively in its own `aggregate`, so on
3939
that driver the two paths can now differ in the same last place. An exact `$eq` on a fractional
4040
sum compares doubles: compare with a range.

‎packages/drivers/driver-sql/src/sql-driver-20387-aggregate-double-accumulation.test.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
* `having` is evaluated by the engine, over the value this door answers
2626
* (`having-filter.ts`: `$eq` compares the value with `==`), so the value pinned
2727
* here is what decides `having` `$eq` / `$in` on every face. The expected
28-
* values are computed from `find()`'s own rows with the rows path's arithmetic
28+
* values are computed from `find()`'s own rows, added in row order
2929
* ({@link rowsPathSum}), plus the literal the triage named, so a decimal
30-
* answer, a compensated sum or a string each fail.
30+
* answer or a string each fail. Over these fixtures — two addends, and
31+
* integer-valued columns within 2^53 — that sum is also the rows path's, which
32+
* adds with compensation since #20489: the two folds cannot differ there.
3133
*
3234
* What stays as it was, pinned too: `count` / `min` / `max`, and a `sum` over
3335
* an integer-valued column, which keeps the database's exact total (above 2^53
@@ -49,10 +51,10 @@ function toNumber(v: unknown): number {
4951
return Number.isFinite(n) ? n : 0;
5052
}
5153

52-
/** The rows path's `sum`: `values.reduce((a, b) => a + toNumber(b), 0)`, in row order. */
54+
/** `values.reduce((a, b) => a + toNumber(b), 0)`, in row order: the rows path's `sum` over this file's fixtures (see the header). */
5355
const rowsPathSum = (values: readonly unknown[]) => values.reduce<number>((a, b) => a + toNumber(b), 0);
5456

55-
/** The rows path's `avg`: the non-null values' `sum`, divided by how many there are. */
57+
/** The non-null values' {@link rowsPathSum}, divided by how many there are: the rows path's `avg` over this file's fixtures. */
5658
function rowsPathAvg(values: readonly unknown[]): number | null {
5759
const defined = values.filter((v) => v != null);
5860
return defined.length === 0 ? null : rowsPathSum(defined) / defined.length;

‎packages/drivers/driver-sql/src/sql-driver.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,11 +1594,13 @@ const AGGREGATE_ANSWER_KIND: Readonly<Record<AggregationFunction, 'number' | 'co
15941594
* would widen the binary32 value (`0.1` → `0.10000000149011612`) where the
15951595
* client reads `0.1`. MySQL's `CAST(… AS DOUBLE)` needs 8.0.17 or later.
15961596
*
1597-
* ⚠️ Residual, stated: the double sums are added in scan order, one after
1598-
* another, as the rows path adds them. SQLite (3.43+) adds with compensated
1599-
* (Kahan-Babuska-Neumaier) summation, so a group of three or more fractions can
1600-
* still differ in the last place on SQLite's native face alone (`0.1 + 0.2 +
1601-
* 0.3`: SQLite `0.6`, every other face `0.6000000000000001`). Two addends
1597+
* ⚠️ Residual, stated: on PostgreSQL and MySQL the double sums are added in
1598+
* scan order, one after another, without compensation. SQLite (3.43+) adds with
1599+
* compensated (Kahan-Babuska-Neumaier) summation, and since #20489 so does the
1600+
* engine's rows path (`in-memory-aggregation.ts`, `compensatedSum`), so a group
1601+
* of three or more fractions can still differ in the last place between the
1602+
* PostgreSQL / MySQL native faces and those two (`0.1 + 0.2 + 0.3`: PostgreSQL /
1603+
* MySQL `0.6000000000000001`, SQLite and the rows path `0.6`). Two addends
16021604
* cannot differ, which is why the pin is `0.1 + 0.2`.
16031605
*
16041606
* A `Record` over `AggregationFunction` for the same reason as

0 commit comments

Comments
 (0)