Skip to content

Commit c9bd758

Browse files
committed
chore(changeset): driver-sql sum / avg one-double accumulation (patch)
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
1 parent 07f81ea commit c9bd758

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
'@objectstack/driver-sql': patch
3+
---
4+
5+
fix(driver-sql): `sum` / `avg` accumulate in double on PostgreSQL and MySQL, as they do on SQLite and on the engine's rows path
6+
7+
Clause-②: no
8+
9+
`SqlDriver.aggregate` let PostgreSQL (`numeric`) and MySQL (`DECIMAL`) add exact decimals,
10+
while SQLite and the engine's rows path add JS doubles. So a `number` column holding `0.1`
11+
and `0.2` summed to `0.3` on the PostgreSQL and MySQL native paths and to
12+
`0.30000000000000004` everywhere else, and `having { s: { $eq: 0.3 } }` kept the group on
13+
those two faces only. `avg` over an integer column diverged too: MySQL rounds a decimal
14+
average to 4 places (`avg` of 1, 2, 2 answered `1.6667`), and PostgreSQL's `numeric`
15+
average rounds to 16 places before the answer becomes a double (`11 / 9` answered
16+
`1.2222222222222222`, where every other face answers `1.2222222222222223`).
17+
18+
**The precision policy, applied to the arithmetic.** The policy already stated for the
19+
answer's type (one JS double on every dialect, the loss beyond a double's precision declared)
20+
now also decides how the answer is computed:
21+
22+
- `avg` accumulates in double on PostgreSQL and MySQL, over every declared numeric or
23+
boolean column.
24+
- `sum` accumulates in double over a column that holds fractions: `number`, `currency`,
25+
`percent`, `slider`, `progress`, `summary`, and the driver's `float` alias.
26+
- `sum` over an integer-valued column (`rating`, the `integer` / `int` aliases, a boolean)
27+
keeps the database's exact integer total, rounded once to the double.
28+
- `count`, `count_distinct`, `min` and `max` are unchanged. SQLite is unchanged.
29+
30+
Each value added is the column's text parsed as a double: the value the SQL client hands
31+
`find()`, and so the value the rows path adds. For the exact-decimal columns this equals a
32+
plain cast. For a binary `real` / `FLOAT` column, which a table created before the
33+
exact-decimal columns still has, a plain cast would add the widened binary value
34+
(`0.30000000447034836` for `0.1 + 0.2`). MySQL's `CAST(… AS DOUBLE)` needs MySQL 8.0.17 or
35+
later.
36+
37+
Route chosen: (a), accumulate in double on the native faces. The other route, (b), was to make
38+
the rows path add exact decimals and round once. It was rejected because SQLite's native `sum`
39+
adds the stored doubles (`0.30000000000000004`), so the rows path would then disagree with SQLite
40+
for exactly `0.1 + 0.2`.
41+
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.
47+
48+
A consumer that compared `sum` / `avg` over a fractional column with a decimal literal on
49+
PostgreSQL or MySQL (`$eq: 0.3`) now gets the answer SQLite and the rows path already gave:
50+
compare with a range, or with the double the arithmetic produces.

0 commit comments

Comments
 (0)