Skip to content

Commit 77cd027

Browse files
ybai08lovasoa
andauthored
fix: call aggregate init when there are no rows to aggregate (#641)
* fix: call aggregate init when there are no rows to aggregate SQLite never calls xStep for an aggregate over an empty input, only xFinal. The JavaScript state was only initialized in the step wrapper, so finalize received undefined instead of the value returned by init: `SELECT json_agg(x) FROM t WHERE 0` returned NULL instead of '[]', an aggregate with `init: () => 0` and no finalize returned NULL instead of 0, and a finalize that reads properties of its state threw a TypeError. Initialize the state in the finalize wrapper as well when it is missing. * refactor: use a Map for aggregate state --------- Co-authored-by: ybai08 <271969568+ybai08@users.noreply.github.com> Co-authored-by: Ophir Lojkine <contact@ophir.dev>
1 parent 4b638f7 commit 77cd027

2 files changed

Lines changed: 48 additions & 13 deletions

File tree

‎src/api.js‎

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,18 +1343,21 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
13431343
throw "An aggregate function must have a step function in " + name;
13441344
}
13451345

1346-
// state is a state object; we'll use the pointer p to serve as the
1347-
// key for where we hold our state so that multiple invocations of
1348-
// this function never step on each other
1349-
var state = {};
1346+
// Keep each aggregate invocation's state under its context pointer.
1347+
var state = new Map();
1348+
1349+
function getState(p) {
1350+
if (!state.has(p)) state.set(p, init());
1351+
return state.get(p);
1352+
}
13501353

13511354
function wrapped_step(cx, argc, argv) {
13521355
// > The first time the sqlite3_aggregate_context(C,N) routine is
13531356
// > called for a particular aggregate function, SQLite allocates N
13541357
// > bytes of memory, zeroes out that memory, and returns a pointer
13551358
// > to the new memory.
13561359
//
1357-
// We're going to use that pointer as a key to our state array,
1360+
// We're going to use that pointer as a key to our state map,
13581361
// since using sqlite3_aggregate_context as it's meant to be used
13591362
// through webassembly seems to be very difficult. Just allocate
13601363
// one byte.
@@ -1363,16 +1366,16 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
13631366
// If this is the first invocation of wrapped_step, call `init`
13641367
//
13651368
// Make sure that every path through the step and finalize
1366-
// functions deletes the value state[p] when it's done so we don't
1369+
// functions delete the state entry when done so we don't
13671370
// leak memory and possibly stomp the init value of future calls
1368-
if (!Object.hasOwnProperty.call(state, p)) state[p] = init();
1371+
var currentState = getState(p);
13691372

13701373
var args = parseFunctionArguments(argc, argv);
1371-
var mergedArgs = [state[p]].concat(args);
1374+
var mergedArgs = [currentState].concat(args);
13721375
try {
1373-
state[p] = step.apply(null, mergedArgs);
1376+
state.set(p, step.apply(null, mergedArgs));
13741377
} catch (error) {
1375-
delete state[p];
1378+
state.delete(p);
13761379
setFunctionError(cx, error);
13771380
}
13781381
}
@@ -1381,14 +1384,16 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
13811384
var result;
13821385
var p = sqlite3_aggregate_context(cx, 1);
13831386
try {
1384-
result = finalize(state[p]);
1387+
// wrapped_step is never called when there are no rows to
1388+
// aggregate, so the state may not have been initialized yet
1389+
result = finalize(getState(p));
13851390
} catch (error) {
1386-
delete state[p];
1391+
state.delete(p);
13871392
setFunctionError(cx, error);
13881393
return;
13891394
}
13901395
setFunctionResult(cx, result);
1391-
delete state[p];
1396+
state.delete(p);
13921397
}
13931398

13941399
if (Object.hasOwnProperty.call(this.functions, name)) {

‎test/test_aggregate_functions.js‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,36 @@ exports.test = function (SQL, assert) {
5353
"Multiple aggregations at once"
5454
);
5555

56+
result = db.exec("SELECT json_agg(col) FROM test2 WHERE 0;");
57+
assert.strictEqual(
58+
result[0].values[0][0],
59+
"[]",
60+
"Aggregate over no rows finalizes the value returned by init"
61+
);
62+
63+
db.create_aggregate("count_rows", {
64+
init: () => 0,
65+
step: (state, value) => state + 1,
66+
});
67+
result = db.exec("SELECT count_rows(col) FROM test2 WHERE 0;");
68+
assert.strictEqual(
69+
result[0].values[0][0],
70+
0,
71+
"Aggregate over no rows without finalize returns the init value"
72+
);
73+
result = db.exec("SELECT count_rows(col) FROM test2;");
74+
assert.strictEqual(result[0].values[0][0], 3, "Aggregate counts rows");
75+
76+
db.create_aggregate("no_init", {
77+
step: (state, value) => value,
78+
});
79+
result = db.exec("SELECT no_init(col) FROM test2 WHERE 0;");
80+
assert.strictEqual(
81+
result[0].values[0][0],
82+
null,
83+
"Aggregate over no rows without init returns null"
84+
);
85+
5686
db.create_aggregate("is_even", {
5787
init: () => true,
5888
step: state => !state

0 commit comments

Comments
 (0)