Skip to content

Commit 5e5b063

Browse files
llimlliblovasoa
andauthored
Enable custom aggregate functions (take 2) (#529)
* initial commit. * documentation * remove no-longer-valid type * close over state initialization for performance * link documentation in comment * more testing * run tests if they're main * accept a single arg * this kind of works but I'm abandoning this branch Basically it seems that the sqlite extension pattern of 'allocate a struct and stick it in the context pointer' is not going to work for us here. I wonder if using the id of the pointer returned by sqlite3_aggregate_context would be enough? Since no two functions could use the same pointer, per https://www.sqlite.org/c3ref/aggregate_context.html ? * a middle road sqlite3_agg_context solution * try out auto-updating state * improve quantile test, add multiple agg test * add a null to the test * acorn fails to parse ||=, whatever * make eslint happy * make initial_value an argument * test step and finalize exceptions * add memory leak test * update docs to current interface * delete state in exception handlers * remove null state * return init function and document object * more tests and update back to init function * update redefinition test for new interface * update README to match fixed signature * more consistent test formatting * Update README.md Co-authored-by: Ophir LOJKINE <[email protected]> * clarify what exactly the result will contain * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Improve documentation and type annotations * ignore documentation in eslintrc * reduce code size Thanks a lot, @llimllib ! Co-authored-by: dogquery <> Co-authored-by: Ophir LOJKINE <[email protected]>
1 parent 577056b commit 5e5b063

6 files changed

+451
-62
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
ignorePatterns: [
1717
"/dist/",
1818
"/examples/",
19+
"/documentation/",
1920
"/node_modules/",
2021
"/out/",
2122
"/src/shell-post.js",

README.md

+27
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,33 @@ db.create_function("add_js", add);
7575
// Run a query in which the function is used
7676
db.run("INSERT INTO hello VALUES (add_js(7, 3), add_js('Hello ', 'world'));"); // Inserts 10 and 'Hello world'
7777

78+
// You can create custom aggregation functions, by passing a name
79+
// and a set of functions to `db.create_aggregate`:
80+
//
81+
// - an `init` function. This function receives no argument and returns
82+
// the initial value for the state of the aggregate function.
83+
// - a `step` function. This function takes two arguments
84+
// - the current state of the aggregation
85+
// - a new value to aggregate to the state
86+
// It should return a new value for the state.
87+
// - a `finalize` function. This function receives a state object, and
88+
// returns the final value of the aggregate. It can be omitted, in which case
89+
// the final value of the state will be returned directly by the aggregate function.
90+
//
91+
// Here is an example aggregation function, `json_agg`, which will collect all
92+
// input values and return them as a JSON array:
93+
db.create_aggregate(
94+
"json_agg",
95+
{
96+
init: () => [],
97+
step: (state, val) => [...state, val],
98+
finalize: (state) => JSON.stringify(state),
99+
}
100+
);
101+
102+
db.exec("SELECT json_agg(column1) FROM (VALUES ('hello'), ('world'))");
103+
// -> The result of the query is the string '["hello","world"]'
104+
78105
// Export the database to an Uint8Array containing the SQLite database file
79106
const binaryArray = db.export();
80107
```

src/api.js

+210-62
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
225225
"",
226226
["number", "string", "number"]
227227
);
228+
229+
// https://www.sqlite.org/c3ref/aggregate_context.html
230+
// void *sqlite3_aggregate_context(sqlite3_context*, int nBytes)
231+
var sqlite3_aggregate_context = cwrap(
232+
"sqlite3_aggregate_context",
233+
"number",
234+
["number", "number"]
235+
);
228236
var registerExtensionFunctions = cwrap(
229237
"RegisterExtensionFunctions",
230238
"number",
@@ -1131,81 +1139,90 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
11311139
return sqlite3_changes(this.db);
11321140
};
11331141

1134-
/** Register a custom function with SQLite
1135-
@example Register a simple function
1136-
db.create_function("addOne", function (x) {return x+1;})
1137-
db.exec("SELECT addOne(1)") // = 2
1142+
var extract_blob = function extract_blob(ptr) {
1143+
var size = sqlite3_value_bytes(ptr);
1144+
var blob_ptr = sqlite3_value_blob(ptr);
1145+
var blob_arg = new Uint8Array(size);
1146+
for (var j = 0; j < size; j += 1) {
1147+
blob_arg[j] = HEAP8[blob_ptr + j];
1148+
}
1149+
return blob_arg;
1150+
};
11381151

1139-
@param {string} name the name of the function as referenced in
1140-
SQL statements.
1141-
@param {function} func the actual function to be executed.
1142-
@return {Database} The database object. Useful for method chaining
1143-
*/
1152+
var parseFunctionArguments = function parseFunctionArguments(argc, argv) {
1153+
var args = [];
1154+
for (var i = 0; i < argc; i += 1) {
1155+
var value_ptr = getValue(argv + (4 * i), "i32");
1156+
var value_type = sqlite3_value_type(value_ptr);
1157+
var arg;
1158+
if (
1159+
value_type === SQLITE_INTEGER
1160+
|| value_type === SQLITE_FLOAT
1161+
) {
1162+
arg = sqlite3_value_double(value_ptr);
1163+
} else if (value_type === SQLITE_TEXT) {
1164+
arg = sqlite3_value_text(value_ptr);
1165+
} else if (value_type === SQLITE_BLOB) {
1166+
arg = extract_blob(value_ptr);
1167+
} else arg = null;
1168+
args.push(arg);
1169+
}
1170+
return args;
1171+
};
1172+
var setFunctionResult = function setFunctionResult(cx, result) {
1173+
switch (typeof result) {
1174+
case "boolean":
1175+
sqlite3_result_int(cx, result ? 1 : 0);
1176+
break;
1177+
case "number":
1178+
sqlite3_result_double(cx, result);
1179+
break;
1180+
case "string":
1181+
sqlite3_result_text(cx, result, -1, -1);
1182+
break;
1183+
case "object":
1184+
if (result === null) {
1185+
sqlite3_result_null(cx);
1186+
} else if (result.length != null) {
1187+
var blobptr = allocate(result, ALLOC_NORMAL);
1188+
sqlite3_result_blob(cx, blobptr, result.length, -1);
1189+
_free(blobptr);
1190+
} else {
1191+
sqlite3_result_error(cx, (
1192+
"Wrong API use : tried to return a value "
1193+
+ "of an unknown type (" + result + ")."
1194+
), -1);
1195+
}
1196+
break;
1197+
default:
1198+
sqlite3_result_null(cx);
1199+
}
1200+
};
1201+
1202+
/** Register a custom function with SQLite
1203+
@example <caption>Register a simple function</caption>
1204+
db.create_function("addOne", function (x) {return x+1;})
1205+
db.exec("SELECT addOne(1)") // = 2
1206+
1207+
@param {string} name the name of the function as referenced in
1208+
SQL statements.
1209+
@param {function} func the actual function to be executed.
1210+
@return {Database} The database object. Useful for method chaining
1211+
*/
11441212
Database.prototype["create_function"] = function create_function(
11451213
name,
11461214
func
11471215
) {
11481216
function wrapped_func(cx, argc, argv) {
1217+
var args = parseFunctionArguments(argc, argv);
11491218
var result;
1150-
function extract_blob(ptr) {
1151-
var size = sqlite3_value_bytes(ptr);
1152-
var blob_ptr = sqlite3_value_blob(ptr);
1153-
var blob_arg = new Uint8Array(size);
1154-
for (var j = 0; j < size; j += 1) {
1155-
blob_arg[j] = HEAP8[blob_ptr + j];
1156-
}
1157-
return blob_arg;
1158-
}
1159-
var args = [];
1160-
for (var i = 0; i < argc; i += 1) {
1161-
var value_ptr = getValue(argv + (4 * i), "i32");
1162-
var value_type = sqlite3_value_type(value_ptr);
1163-
var arg;
1164-
if (
1165-
value_type === SQLITE_INTEGER
1166-
|| value_type === SQLITE_FLOAT
1167-
) {
1168-
arg = sqlite3_value_double(value_ptr);
1169-
} else if (value_type === SQLITE_TEXT) {
1170-
arg = sqlite3_value_text(value_ptr);
1171-
} else if (value_type === SQLITE_BLOB) {
1172-
arg = extract_blob(value_ptr);
1173-
} else arg = null;
1174-
args.push(arg);
1175-
}
11761219
try {
11771220
result = func.apply(null, args);
11781221
} catch (error) {
11791222
sqlite3_result_error(cx, error, -1);
11801223
return;
11811224
}
1182-
switch (typeof result) {
1183-
case "boolean":
1184-
sqlite3_result_int(cx, result ? 1 : 0);
1185-
break;
1186-
case "number":
1187-
sqlite3_result_double(cx, result);
1188-
break;
1189-
case "string":
1190-
sqlite3_result_text(cx, result, -1, -1);
1191-
break;
1192-
case "object":
1193-
if (result === null) {
1194-
sqlite3_result_null(cx);
1195-
} else if (result.length != null) {
1196-
var blobptr = allocate(result, ALLOC_NORMAL);
1197-
sqlite3_result_blob(cx, blobptr, result.length, -1);
1198-
_free(blobptr);
1199-
} else {
1200-
sqlite3_result_error(cx, (
1201-
"Wrong API use : tried to return a value "
1202-
+ "of an unknown type (" + result + ")."
1203-
), -1);
1204-
}
1205-
break;
1206-
default:
1207-
sqlite3_result_null(cx);
1208-
}
1225+
setFunctionResult(cx, result);
12091226
}
12101227
if (Object.prototype.hasOwnProperty.call(this.functions, name)) {
12111228
removeFunction(this.functions[name]);
@@ -1229,6 +1246,137 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
12291246
return this;
12301247
};
12311248

1249+
/** Register a custom aggregate with SQLite
1250+
@example <caption>Register a custom sum function</caption>
1251+
db.create_aggregate("js_sum", {
1252+
init: () => 0,
1253+
step: (state, value) => state + value,
1254+
finalize: state => state
1255+
});
1256+
db.exec("SELECT js_sum(column1) FROM (VALUES (1), (2))"); // = 3
1257+
1258+
@param {string} name the name of the aggregate as referenced in
1259+
SQL statements.
1260+
@param {object} aggregateFunctions
1261+
object containing at least a step function.
1262+
@param {function(): T} [aggregateFunctions.init = ()=>null]
1263+
a function receiving no arguments and returning an initial
1264+
value for the aggregate function. The initial value will be
1265+
null if this key is omitted.
1266+
@param {function(T, any) : T} aggregateFunctions.step
1267+
a function receiving the current state and a value to aggregate
1268+
and returning a new state.
1269+
Will receive the value from init for the first step.
1270+
@param {function(T): any} [aggregateFunctions.finalize = (state)=>state]
1271+
a function returning the result of the aggregate function
1272+
given its final state.
1273+
If omitted, the value returned by the last step
1274+
will be used as the final value.
1275+
@return {Database} The database object. Useful for method chaining
1276+
@template T
1277+
*/
1278+
Database.prototype["create_aggregate"] = function create_aggregate(
1279+
name,
1280+
aggregateFunctions
1281+
) {
1282+
// Default initializer and finalizer
1283+
var init = aggregateFunctions["init"]
1284+
|| function init() { return null; };
1285+
var finalize = aggregateFunctions["finalize"]
1286+
|| function finalize(state) { return state; };
1287+
var step = aggregateFunctions["step"];
1288+
1289+
if (!step) {
1290+
throw "An aggregate function must have a step function in " + name;
1291+
}
1292+
1293+
// state is a state object; we'll use the pointer p to serve as the
1294+
// key for where we hold our state so that multiple invocations of
1295+
// this function never step on each other
1296+
var state = {};
1297+
1298+
function wrapped_step(cx, argc, argv) {
1299+
// > The first time the sqlite3_aggregate_context(C,N) routine is
1300+
// > called for a particular aggregate function, SQLite allocates N
1301+
// > bytes of memory, zeroes out that memory, and returns a pointer
1302+
// > to the new memory.
1303+
//
1304+
// We're going to use that pointer as a key to our state array,
1305+
// since using sqlite3_aggregate_context as it's meant to be used
1306+
// through webassembly seems to be very difficult. Just allocate
1307+
// one byte.
1308+
var p = sqlite3_aggregate_context(cx, 1);
1309+
1310+
// If this is the first invocation of wrapped_step, call `init`
1311+
//
1312+
// Make sure that every path through the step and finalize
1313+
// functions deletes the value state[p] when it's done so we don't
1314+
// leak memory and possibly stomp the init value of future calls
1315+
if (!Object.hasOwnProperty.call(state, p)) state[p] = init();
1316+
1317+
var args = parseFunctionArguments(argc, argv);
1318+
var mergedArgs = [state[p]].concat(args);
1319+
try {
1320+
state[p] = step.apply(null, mergedArgs);
1321+
} catch (error) {
1322+
delete state[p];
1323+
sqlite3_result_error(cx, error, -1);
1324+
}
1325+
}
1326+
1327+
function wrapped_finalize(cx) {
1328+
var result;
1329+
var p = sqlite3_aggregate_context(cx, 1);
1330+
try {
1331+
result = finalize(state[p]);
1332+
} catch (error) {
1333+
delete state[p];
1334+
sqlite3_result_error(cx, error, -1);
1335+
return;
1336+
}
1337+
setFunctionResult(cx, result);
1338+
delete state[p];
1339+
}
1340+
1341+
if (Object.hasOwnProperty.call(this.functions, name)) {
1342+
removeFunction(this.functions[name]);
1343+
delete this.functions[name];
1344+
}
1345+
var finalize_name = name + "__finalize";
1346+
if (Object.hasOwnProperty.call(this.functions, finalize_name)) {
1347+
removeFunction(this.functions[finalize_name]);
1348+
delete this.functions[finalize_name];
1349+
}
1350+
// The signature of the wrapped function is :
1351+
// void wrapped(sqlite3_context *db, int argc, sqlite3_value **argv)
1352+
var step_ptr = addFunction(wrapped_step, "viii");
1353+
1354+
// The signature of the wrapped function is :
1355+
// void wrapped(sqlite3_context *db)
1356+
var finalize_ptr = addFunction(wrapped_finalize, "vi");
1357+
this.functions[name] = step_ptr;
1358+
this.functions[finalize_name] = finalize_ptr;
1359+
1360+
// passing null to the sixth parameter defines this as an aggregate
1361+
// function
1362+
//
1363+
// > An aggregate SQL function requires an implementation of xStep and
1364+
// > xFinal and NULL pointer must be passed for xFunc.
1365+
// - http://www.sqlite.org/c3ref/create_function.html
1366+
this.handleError(sqlite3_create_function_v2(
1367+
this.db,
1368+
name,
1369+
step.length - 1,
1370+
SQLITE_UTF8,
1371+
0,
1372+
0,
1373+
step_ptr,
1374+
finalize_ptr,
1375+
0
1376+
));
1377+
return this;
1378+
};
1379+
12321380
// export Database to Module
12331381
Module.Database = Database;
12341382
};

src/exported_functions.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@
4141
"_sqlite3_result_int",
4242
"_sqlite3_result_int64",
4343
"_sqlite3_result_error",
44+
"_sqlite3_aggregate_context",
4445
"_RegisterExtensionFunctions"
4546
]

0 commit comments

Comments
 (0)