diff --git a/.eslintrc.js b/.eslintrc.js
index f730a261..02e6047c 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -16,6 +16,7 @@ module.exports = {
ignorePatterns: [
"/dist/",
"/examples/",
+ "/documentation/",
"/node_modules/",
"/out/",
"/src/shell-post.js",
diff --git a/README.md b/README.md
index bfc130b4..4e98ee5e 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,33 @@ db.create_function("add_js", add);
// Run a query in which the function is used
db.run("INSERT INTO hello VALUES (add_js(7, 3), add_js('Hello ', 'world'));"); // Inserts 10 and 'Hello world'
+// You can create custom aggregation functions, by passing a name
+// and a set of functions to `db.create_aggregate`:
+//
+// - an `init` function. This function receives no argument and returns
+// the initial value for the state of the aggregate function.
+// - a `step` function. This function takes two arguments
+// - the current state of the aggregation
+// - a new value to aggregate to the state
+// It should return a new value for the state.
+// - a `finalize` function. This function receives a state object, and
+// returns the final value of the aggregate. It can be omitted, in which case
+// the final value of the state will be returned directly by the aggregate function.
+//
+// Here is an example aggregation function, `json_agg`, which will collect all
+// input values and return them as a JSON array:
+db.create_aggregate(
+ "json_agg",
+ {
+ init: () => [],
+ step: (state, val) => [...state, val],
+ finalize: (state) => JSON.stringify(state),
+ }
+);
+
+db.exec("SELECT json_agg(column1) FROM (VALUES ('hello'), ('world'))");
+// -> The result of the query is the string '["hello","world"]'
+
// Export the database to an Uint8Array containing the SQLite database file
const binaryArray = db.export();
```
diff --git a/src/api.js b/src/api.js
index 854bf32e..ec8c2fe7 100644
--- a/src/api.js
+++ b/src/api.js
@@ -225,6 +225,14 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
"",
["number", "string", "number"]
);
+
+ // https://www.sqlite.org/c3ref/aggregate_context.html
+ // void *sqlite3_aggregate_context(sqlite3_context*, int nBytes)
+ var sqlite3_aggregate_context = cwrap(
+ "sqlite3_aggregate_context",
+ "number",
+ ["number", "number"]
+ );
var registerExtensionFunctions = cwrap(
"RegisterExtensionFunctions",
"number",
@@ -1131,81 +1139,90 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
return sqlite3_changes(this.db);
};
- /** Register a custom function with SQLite
- @example Register a simple function
- db.create_function("addOne", function (x) {return x+1;})
- db.exec("SELECT addOne(1)") // = 2
+ var extract_blob = function extract_blob(ptr) {
+ var size = sqlite3_value_bytes(ptr);
+ var blob_ptr = sqlite3_value_blob(ptr);
+ var blob_arg = new Uint8Array(size);
+ for (var j = 0; j < size; j += 1) {
+ blob_arg[j] = HEAP8[blob_ptr + j];
+ }
+ return blob_arg;
+ };
- @param {string} name the name of the function as referenced in
- SQL statements.
- @param {function} func the actual function to be executed.
- @return {Database} The database object. Useful for method chaining
- */
+ var parseFunctionArguments = function parseFunctionArguments(argc, argv) {
+ var args = [];
+ for (var i = 0; i < argc; i += 1) {
+ var value_ptr = getValue(argv + (4 * i), "i32");
+ var value_type = sqlite3_value_type(value_ptr);
+ var arg;
+ if (
+ value_type === SQLITE_INTEGER
+ || value_type === SQLITE_FLOAT
+ ) {
+ arg = sqlite3_value_double(value_ptr);
+ } else if (value_type === SQLITE_TEXT) {
+ arg = sqlite3_value_text(value_ptr);
+ } else if (value_type === SQLITE_BLOB) {
+ arg = extract_blob(value_ptr);
+ } else arg = null;
+ args.push(arg);
+ }
+ return args;
+ };
+ var setFunctionResult = function setFunctionResult(cx, result) {
+ switch (typeof result) {
+ case "boolean":
+ sqlite3_result_int(cx, result ? 1 : 0);
+ break;
+ case "number":
+ sqlite3_result_double(cx, result);
+ break;
+ case "string":
+ sqlite3_result_text(cx, result, -1, -1);
+ break;
+ case "object":
+ if (result === null) {
+ sqlite3_result_null(cx);
+ } else if (result.length != null) {
+ var blobptr = allocate(result, ALLOC_NORMAL);
+ sqlite3_result_blob(cx, blobptr, result.length, -1);
+ _free(blobptr);
+ } else {
+ sqlite3_result_error(cx, (
+ "Wrong API use : tried to return a value "
+ + "of an unknown type (" + result + ")."
+ ), -1);
+ }
+ break;
+ default:
+ sqlite3_result_null(cx);
+ }
+ };
+
+ /** Register a custom function with SQLite
+ @example
Register a simple function
+ db.create_function("addOne", function (x) {return x+1;})
+ db.exec("SELECT addOne(1)") // = 2
+
+ @param {string} name the name of the function as referenced in
+ SQL statements.
+ @param {function} func the actual function to be executed.
+ @return {Database} The database object. Useful for method chaining
+ */
Database.prototype["create_function"] = function create_function(
name,
func
) {
function wrapped_func(cx, argc, argv) {
+ var args = parseFunctionArguments(argc, argv);
var result;
- function extract_blob(ptr) {
- var size = sqlite3_value_bytes(ptr);
- var blob_ptr = sqlite3_value_blob(ptr);
- var blob_arg = new Uint8Array(size);
- for (var j = 0; j < size; j += 1) {
- blob_arg[j] = HEAP8[blob_ptr + j];
- }
- return blob_arg;
- }
- var args = [];
- for (var i = 0; i < argc; i += 1) {
- var value_ptr = getValue(argv + (4 * i), "i32");
- var value_type = sqlite3_value_type(value_ptr);
- var arg;
- if (
- value_type === SQLITE_INTEGER
- || value_type === SQLITE_FLOAT
- ) {
- arg = sqlite3_value_double(value_ptr);
- } else if (value_type === SQLITE_TEXT) {
- arg = sqlite3_value_text(value_ptr);
- } else if (value_type === SQLITE_BLOB) {
- arg = extract_blob(value_ptr);
- } else arg = null;
- args.push(arg);
- }
try {
result = func.apply(null, args);
} catch (error) {
sqlite3_result_error(cx, error, -1);
return;
}
- switch (typeof result) {
- case "boolean":
- sqlite3_result_int(cx, result ? 1 : 0);
- break;
- case "number":
- sqlite3_result_double(cx, result);
- break;
- case "string":
- sqlite3_result_text(cx, result, -1, -1);
- break;
- case "object":
- if (result === null) {
- sqlite3_result_null(cx);
- } else if (result.length != null) {
- var blobptr = allocate(result, ALLOC_NORMAL);
- sqlite3_result_blob(cx, blobptr, result.length, -1);
- _free(blobptr);
- } else {
- sqlite3_result_error(cx, (
- "Wrong API use : tried to return a value "
- + "of an unknown type (" + result + ")."
- ), -1);
- }
- break;
- default:
- sqlite3_result_null(cx);
- }
+ setFunctionResult(cx, result);
}
if (Object.prototype.hasOwnProperty.call(this.functions, name)) {
removeFunction(this.functions[name]);
@@ -1229,6 +1246,137 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
return this;
};
+ /** Register a custom aggregate with SQLite
+ @example Register a custom sum function
+ db.create_aggregate("js_sum", {
+ init: () => 0,
+ step: (state, value) => state + value,
+ finalize: state => state
+ });
+ db.exec("SELECT js_sum(column1) FROM (VALUES (1), (2))"); // = 3
+
+ @param {string} name the name of the aggregate as referenced in
+ SQL statements.
+ @param {object} aggregateFunctions
+ object containing at least a step function.
+ @param {function(): T} [aggregateFunctions.init = ()=>null]
+ a function receiving no arguments and returning an initial
+ value for the aggregate function. The initial value will be
+ null if this key is omitted.
+ @param {function(T, any) : T} aggregateFunctions.step
+ a function receiving the current state and a value to aggregate
+ and returning a new state.
+ Will receive the value from init for the first step.
+ @param {function(T): any} [aggregateFunctions.finalize = (state)=>state]
+ a function returning the result of the aggregate function
+ given its final state.
+ If omitted, the value returned by the last step
+ will be used as the final value.
+ @return {Database} The database object. Useful for method chaining
+ @template T
+ */
+ Database.prototype["create_aggregate"] = function create_aggregate(
+ name,
+ aggregateFunctions
+ ) {
+ // Default initializer and finalizer
+ var init = aggregateFunctions["init"]
+ || function init() { return null; };
+ var finalize = aggregateFunctions["finalize"]
+ || function finalize(state) { return state; };
+ var step = aggregateFunctions["step"];
+
+ if (!step) {
+ throw "An aggregate function must have a step function in " + name;
+ }
+
+ // state is a state object; we'll use the pointer p to serve as the
+ // key for where we hold our state so that multiple invocations of
+ // this function never step on each other
+ var state = {};
+
+ function wrapped_step(cx, argc, argv) {
+ // > The first time the sqlite3_aggregate_context(C,N) routine is
+ // > called for a particular aggregate function, SQLite allocates N
+ // > bytes of memory, zeroes out that memory, and returns a pointer
+ // > to the new memory.
+ //
+ // We're going to use that pointer as a key to our state array,
+ // since using sqlite3_aggregate_context as it's meant to be used
+ // through webassembly seems to be very difficult. Just allocate
+ // one byte.
+ var p = sqlite3_aggregate_context(cx, 1);
+
+ // If this is the first invocation of wrapped_step, call `init`
+ //
+ // Make sure that every path through the step and finalize
+ // functions deletes the value state[p] when it's done so we don't
+ // leak memory and possibly stomp the init value of future calls
+ if (!Object.hasOwnProperty.call(state, p)) state[p] = init();
+
+ var args = parseFunctionArguments(argc, argv);
+ var mergedArgs = [state[p]].concat(args);
+ try {
+ state[p] = step.apply(null, mergedArgs);
+ } catch (error) {
+ delete state[p];
+ sqlite3_result_error(cx, error, -1);
+ }
+ }
+
+ function wrapped_finalize(cx) {
+ var result;
+ var p = sqlite3_aggregate_context(cx, 1);
+ try {
+ result = finalize(state[p]);
+ } catch (error) {
+ delete state[p];
+ sqlite3_result_error(cx, error, -1);
+ return;
+ }
+ setFunctionResult(cx, result);
+ delete state[p];
+ }
+
+ if (Object.hasOwnProperty.call(this.functions, name)) {
+ removeFunction(this.functions[name]);
+ delete this.functions[name];
+ }
+ var finalize_name = name + "__finalize";
+ if (Object.hasOwnProperty.call(this.functions, finalize_name)) {
+ removeFunction(this.functions[finalize_name]);
+ delete this.functions[finalize_name];
+ }
+ // The signature of the wrapped function is :
+ // void wrapped(sqlite3_context *db, int argc, sqlite3_value **argv)
+ var step_ptr = addFunction(wrapped_step, "viii");
+
+ // The signature of the wrapped function is :
+ // void wrapped(sqlite3_context *db)
+ var finalize_ptr = addFunction(wrapped_finalize, "vi");
+ this.functions[name] = step_ptr;
+ this.functions[finalize_name] = finalize_ptr;
+
+ // passing null to the sixth parameter defines this as an aggregate
+ // function
+ //
+ // > An aggregate SQL function requires an implementation of xStep and
+ // > xFinal and NULL pointer must be passed for xFunc.
+ // - http://www.sqlite.org/c3ref/create_function.html
+ this.handleError(sqlite3_create_function_v2(
+ this.db,
+ name,
+ step.length - 1,
+ SQLITE_UTF8,
+ 0,
+ 0,
+ step_ptr,
+ finalize_ptr,
+ 0
+ ));
+ return this;
+ };
+
// export Database to Module
Module.Database = Database;
};
diff --git a/src/exported_functions.json b/src/exported_functions.json
index b93b07d2..324017ae 100644
--- a/src/exported_functions.json
+++ b/src/exported_functions.json
@@ -41,5 +41,6 @@
"_sqlite3_result_int",
"_sqlite3_result_int64",
"_sqlite3_result_error",
+"_sqlite3_aggregate_context",
"_RegisterExtensionFunctions"
]
diff --git a/test/test_aggregate_functions.js b/test/test_aggregate_functions.js
new file mode 100644
index 00000000..d28c775f
--- /dev/null
+++ b/test/test_aggregate_functions.js
@@ -0,0 +1,132 @@
+exports.test = function (SQL, assert) {
+ function assertFloat(got, expected, message="", sigma=0.001) {
+ assert.ok(got > expected - sigma && got < expected + sigma, message);
+ }
+
+ var db = new SQL.Database();
+
+ db.create_aggregate("sum", {
+ step: function (state, value) { return (state || 0) + value; },
+ });
+
+ db.exec("CREATE TABLE test (col);");
+ db.exec("INSERT INTO test VALUES (1), (2), (3), (null);");
+ var result = db.exec("SELECT sum(col) FROM test;");
+ assert.equal(result[0].values[0][0], 6, "Simple aggregate function.");
+
+ db.create_aggregate("percentile", {
+ init: function() { return { vals: [], pctile: null }},
+ step: function (state, value, pctile) {
+ var typ = typeof value;
+ if (typ == "number" || typ == "bigint") { // ignore nulls
+ state.pctile = pctile;
+ state.vals.push(value);
+ }
+ return state;
+ },
+ finalize: function (state) {
+ return percentile(state.vals, state.pctile);
+ }
+ });
+ result = db.exec("SELECT percentile(col, 80) FROM test;");
+ assertFloat(result[0].values[0][0], 2.6, "Aggregate function with two args");
+
+ db.create_aggregate("json_agg", {
+ init: () => [],
+ step: (state, val) => [...state, val],
+ finalize: (state) => JSON.stringify(state),
+ });
+
+ db.exec("CREATE TABLE test2 (col, col2);");
+ db.exec("INSERT INTO test2 values ('four score', 12), ('and seven', 7), ('years ago', 1);");
+ result = db.exec("SELECT json_agg(col) FROM test2;");
+ assert.deepEqual(
+ JSON.parse(result[0].values[0]),
+ ["four score", "and seven", "years ago"],
+ "Aggregate function that returns JSON"
+ );
+
+ result = db.exec("SELECT json_agg(col), json_agg(col2) FROM test2;");
+ assert.deepEqual(
+ result[0].values[0].map(JSON.parse),
+ [["four score", "and seven", "years ago"], [12, 7, 1]],
+ "Multiple aggregations at once"
+ );
+
+ db.create_aggregate("is_even", {
+ init: () => true,
+ step: state => !state
+ });
+ result = db.exec("SELECT is_even() FROM (VALUES (1),(2),(0));");
+ assert.deepEqual(
+ result[0].values[0][0],
+ 0, // this gets convert from "false" to an int by sqlite
+ "Aggregate functions respect falsy values"
+ );
+
+ db.create_aggregate("sum_non_zero", {
+ init: () => 0,
+ step: (state, value) => {
+ if (!value) throw "bananas";
+ return state + value
+ }
+ });
+ assert.throws(
+ () => db.exec("SELECT sum_non_zero(column1) FROM (VALUES (1),(2),(0));"),
+ "Error: bananas",
+ "Handles exception in a step function"
+ );
+ assert.deepEqual(
+ db.exec("SELECT sum_non_zero(column1) FROM (VALUES (1),(2));")[0].values[0][0],
+ 3,
+ "Aggregate functions work after an exception has been thrown in step"
+ );
+
+ db.create_aggregate("throws_finalize", {
+ step: (state, value) => (state || 0) + value,
+ finalize: (state) => {
+ if (!state) throw "shoes"
+ return state;
+ }
+ });
+ assert.throws(
+ () => db.exec("SELECT throws_finalize(column1) FROM (VALUES (0));"),
+ "Error: shoes",
+ "Handles exception in a finalize function"
+ );
+ assert.deepEqual(
+ db.exec("SELECT throws_finalize(column1) FROM (VALUES (1),(2));")[0].values[0][0],
+ 3,
+ "Aggregate functions work after an exception has been thrown in finalize"
+ );
+}
+
+// helper function to calculate a percentile from an array. Will modify the
+// array in-place.
+function percentile(arr, p) {
+ arr.sort();
+ const pos = (arr.length - 1) * (p / 100);
+ const base = Math.floor(pos);
+ const rest = pos - base;
+ if (arr[base + 1] !== undefined) {
+ return arr[base] + rest * (arr[base + 1] - arr[base]);
+ } else {
+ return arr[base];
+ }
+};
+
+if (module == require.main) {
+ const target_file = process.argv[2];
+ const sql_loader = require('./load_sql_lib');
+ sql_loader(target_file).then((sql)=>{
+ require('test').run({
+ 'test functions': function(assert, done){
+ exports.test(sql, assert, done);
+ }
+ });
+ })
+ .catch((e)=>{
+ console.error(e);
+ assert.fail(e);
+ });
+}
diff --git a/test/test_aggregate_redefinition.js b/test/test_aggregate_redefinition.js
new file mode 100644
index 00000000..05779720
--- /dev/null
+++ b/test/test_aggregate_redefinition.js
@@ -0,0 +1,80 @@
+exports.test = function(sql, assert) {
+ // Test 1: Create a database, Register single function, close database, repeat 1000 times
+ for (var i = 1; i <= 1000; i++)
+ {
+ let lastStep = i == 1000;
+ let db = new sql.Database();
+ try {
+ db.create_aggregate("TestFunction"+i, {step: (state, value) => i})
+ } catch(e) {
+ assert.ok(
+ false,
+ "Test 1: Recreate database "+i+"th times and register aggregate"
+ +" function failed with exception:"+e
+ );
+ db.close();
+ break;
+ }
+ var result = db.exec("SELECT TestFunction"+i+"(1)");
+ var result_str = result[0]["values"][0][0];
+ if(result_str != i || lastStep)
+ {
+ assert.equal(
+ result_str,
+ i,
+ "Test 1: Recreate database "+i+"th times and register aggregate function"
+ );
+ db.close();
+ break;
+ }
+ db.close();
+ }
+
+ // Test 2: Create a database, Register same function 1000 times, close database
+ {
+ let db = new sql.Database();
+ for (var i = 1; i <= 1000; i++)
+ {
+ let lastStep = i == 1000;
+ try {
+ db.create_aggregate("TestFunction", {step: (state, value) => i})
+ } catch(e) {
+ assert.ok(
+ false,
+ "Test 2: Reregister aggregate function "+i+"th times failed with"
+ +" exception:"+e
+ );
+ break;
+ }
+ var result = db.exec("SELECT TestFunction(1)");
+ var result_str = result[0]["values"][0][0];
+ if(result_str != i || lastStep)
+ {
+ assert.equal(
+ result_str,
+ i,
+ "Test 2: Reregister function "+i+"th times"
+ );
+ break;
+ }
+ }
+ db.close();
+ }
+};
+
+
+if (module == require.main) {
+ const target_file = process.argv[2];
+ const sql_loader = require('./load_sql_lib');
+ sql_loader(target_file).then((sql)=>{
+ require('test').run({
+ 'test creating multiple functions': function(assert){
+ exports.test(sql, assert);
+ }
+ });
+ })
+ .catch((e)=>{
+ console.error(e);
+ assert.fail(e);
+ });
+}
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy