Skip to content

Commit 678ba3f

Browse files
committed
fix: bigint AVG precision loss for sums exceeding 2^53
Number(bigintSum) in AVG computation lost precision when the running sum exceeded Number.MAX_SAFE_INTEGER. Added bigIntAvg() helpers that divide in BigInt space first (quotient + remainder) to preserve precision. Fixed in partial-agg.ts (GROUP BY AVG) and operators.ts (window AVG, both running and frame paths).
1 parent 0e816f8 commit 678ba3f

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

src/operators.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ import {
4040
type PartialAgg,
4141
} from "./partial-agg.js";
4242

43+
/** Compute AVG from bigint sum without losing precision for sums > 2^53.
44+
* Divides in BigInt space, handles remainder as float. */
45+
function bigIntAvg(sum: bigint, count: number): number {
46+
const bigCount = BigInt(count);
47+
return Number(sum / bigCount) + Number(sum % bigCount) / count;
48+
}
49+
4350
/** A batch of rows flowing through the pipeline. */
4451
export type RowBatch = Row[];
4552

@@ -1109,7 +1116,7 @@ export class WindowOperator implements Operator {
11091116
}
11101117
switch (fn) {
11111118
case "sum": rows[indices[i]][alias] = runCount === 0 ? null : runSum; break;
1112-
case "avg": rows[indices[i]][alias] = runCount === 0 ? null : Number(runSum) / runCount; break;
1119+
case "avg": rows[indices[i]][alias] = runCount === 0 ? null : bigIntAvg(runSum, runCount); break;
11131120
case "min": rows[indices[i]][alias] = runMin ?? null; break;
11141121
case "max": rows[indices[i]][alias] = runMax ?? null; break;
11151122
case "count": rows[indices[i]][alias] = runCount; break;
@@ -1133,7 +1140,7 @@ export class WindowOperator implements Operator {
11331140
}
11341141
switch (fn) {
11351142
case "sum": rows[indices[i]][alias] = count === 0 ? null : sum; break;
1136-
case "avg": rows[indices[i]][alias] = count === 0 ? null : Number(sum) / count; break;
1143+
case "avg": rows[indices[i]][alias] = count === 0 ? null : bigIntAvg(sum, count); break;
11371144
case "min": rows[indices[i]][alias] = min ?? null; break;
11381145
case "max": rows[indices[i]][alias] = max ?? null; break;
11391146
case "count": rows[indices[i]][alias] = count; break;

src/partial-agg.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ export interface PartialAggState {
3434
bigMax?: bigint;
3535
}
3636

37+
/** Compute AVG from bigint sum + number sum without losing precision for large sums. */
38+
function bigAvg(bigSum: bigint, numSum: number, count: number): number {
39+
const bigCount = BigInt(count);
40+
// Integer part from bigint division, remainder handled as float
41+
const quotient = Number(bigSum / bigCount);
42+
const remainder = Number(bigSum % bigCount);
43+
return quotient + (remainder + numSum) / count;
44+
}
45+
3746
export function initPartialAggState(
3847
fn: PartialAggState["fn"],
3948
column: string,
@@ -102,7 +111,7 @@ function resolveValue(state: PartialAggState): number | bigint | string | null {
102111
const hasBig = state.bigSum !== undefined;
103112
switch (state.fn) {
104113
case "sum": return hasBig ? (state.bigSum! + BigInt(Math.trunc(state.sum))) : state.sum;
105-
case "avg": return hasBig ? (Number(state.bigSum!) + state.sum) / state.count : state.sum / state.count;
114+
case "avg": return hasBig ? bigAvg(state.bigSum!, state.sum, state.count) : state.sum / state.count;
106115
case "min": return state.strMin !== undefined ? state.strMin : hasBig ? state.bigMin! : state.min;
107116
case "max": return state.strMax !== undefined ? state.strMax : hasBig ? state.bigMax! : state.max;
108117
case "stddev": return state.count < 2 ? null : Math.sqrt(Math.max(0, (state.m2 ?? 0) / state.count));

0 commit comments

Comments
 (0)