626aa48c26
FossilOrigin-Name: f28de5b726999b913b442fa51355d020ba1f1662d2f7978380623c16438eb238
1782 lines
74 KiB
JavaScript
1782 lines
74 KiB
JavaScript
/*
|
|
2022-05-22
|
|
|
|
The author disclaims copyright to this source code. In place of a
|
|
legal notice, here is a blessing:
|
|
|
|
* May you do good and not evil.
|
|
* May you find forgiveness for yourself and forgive others.
|
|
* May you share freely, never taking more than you give.
|
|
|
|
***********************************************************************
|
|
|
|
This file is intended to be appended to the emcc-generated
|
|
sqlite3.js via emcc:
|
|
|
|
emcc ... -sMODULARIZE -sEXPORT_NAME=initSqlite3Module --post-js=THIS_FILE
|
|
|
|
It is loaded by importing the emcc-generated sqlite3.js, then:
|
|
|
|
initSqlite3Module({module object}).then(
|
|
function(theModule){
|
|
theModule.sqlite3 == an object containing this file's
|
|
deliverables:
|
|
{
|
|
api: bindings for much of the core sqlite3 APIs,
|
|
SQLite3: high-level OO API wrapper
|
|
}
|
|
});
|
|
|
|
It is up to the caller to provide a module object compatible with
|
|
emcc, but it can be a plain empty object. The object passed to
|
|
initSqlite3Module() will get populated by the emscripten-generated
|
|
bits and, in part, by the code from this file. Specifically, this file
|
|
installs the `theModule.sqlite3` part shown above.
|
|
|
|
The resulting sqlite3.api object wraps the standard sqlite3 C API in
|
|
a way as close to its native form as JS allows for. The
|
|
sqlite3.SQLite3 object provides a higher-level wrapper more
|
|
appropriate for general client-side use in JS.
|
|
|
|
Because using certain parts of the low-level API properly requires
|
|
some degree of WASM-related magic, it is not recommended that that
|
|
API be used as-is in client-level code. Rather, client code should
|
|
use the higher-level OO API or write a custom wrapper on top of the
|
|
lower-level API. In short, most of the C-style API is used in an
|
|
intuitive manner from JS but any C-style APIs which take
|
|
pointers-to-pointer arguments require WASM-specific interfaces
|
|
installed by emcscripten-generated code. Those which take or return
|
|
only integers, doubles, strings, or "plain" pointers to db or
|
|
statement objects can be used in "as normal," noting that "pointers"
|
|
in wasm are simply 32-bit integers.
|
|
|
|
# Goals and Non-goals of this API
|
|
|
|
Goals:
|
|
|
|
- Except where noted in the non-goals, provide a more-or-less
|
|
complete wrapper to the sqlite3 C API, insofar as WASM feature
|
|
parity with C allows for. In fact, provide at least 3...
|
|
|
|
- (1) The aforementioned C-style API. (2) An OO-style API on
|
|
top of that, designed to run in the same thread (main window or
|
|
Web Worker) as the C API. (3) A less-capable wrapper which can
|
|
work across the main window/worker boundary, where the sqlite3 API
|
|
is one of those and this wrapper is in the other. That
|
|
constellation places some considerable limitations on how the API
|
|
can be interacted with, but keeping the DB operations out of the
|
|
UI thread is generally desirable.
|
|
|
|
- Insofar as possible, support client-side storage using JS
|
|
filesystem APIs. As of this writing, such things are still very
|
|
much TODO.
|
|
|
|
Non-goals:
|
|
|
|
- As WASM is a web-centric technology and UTF-8 is the King of
|
|
Encodings in that realm, there are no current plans to support the
|
|
UTF16-related APIs. They would add a complication to the bindings
|
|
for no appreciable benefit.
|
|
|
|
- Supporting old or niche-market platforms. WASM is built for a
|
|
modern web and requires modern platforms.
|
|
|
|
*/
|
|
if(!Module.postRun) Module.postRun = [];
|
|
/* ^^^^ the name Module is, in this setup, scope-local in the generated
|
|
file sqlite3.js, with which this file gets combined at build-time. */
|
|
Module.postRun.push(function(namespace/*the module object, the target for
|
|
installed features*/){
|
|
'use strict';
|
|
/* For reference: sql.js does essentially everything we want and
|
|
it solves much of the wasm-related voodoo, but we'll need a
|
|
different structure because we want the db connection to run in
|
|
a worker thread and feed data back into the main
|
|
thread. Regardless of those differences, it makes a great point
|
|
of reference:
|
|
|
|
https://github.com/sql-js/sql.js
|
|
|
|
Some of the specific design goals here:
|
|
|
|
- Bind a low-level sqlite3 API which is close to the native one
|
|
in terms of usage.
|
|
|
|
- Create a higher-level one, more akin to sql.js and
|
|
node.js-style implementations. This one would speak directly
|
|
to the low-level API. This API could be used by clients who
|
|
import the low-level API directly into their main thread
|
|
(which we don't want to recommend but also don't want to
|
|
outright forbid).
|
|
|
|
- Create a second higher-level one which speaks to the
|
|
low-level API via worker messages. This one would be intended
|
|
for use in the main thread, talking to the low-level UI via
|
|
worker messages. Because workers have only a single message
|
|
channel, some acrobatics will be needed here to feed async
|
|
work results back into client-side callbacks (as those
|
|
callbacks cannot simply be passed to the worker). Exactly
|
|
what those acrobatics should look like is not yet entirely
|
|
clear and much experimentation is pending.
|
|
*/
|
|
|
|
const SQM = namespace/*the sqlite module object */;
|
|
|
|
/**
|
|
Set up the main sqlite3 binding API here, mimicking the C API as
|
|
closely as we can.
|
|
|
|
Attribution: though not a direct copy/paste, much of what
|
|
follows is strongly influenced by the sql.js implementation.
|
|
*/
|
|
const api = {
|
|
/* It is important that the following integer values match
|
|
those from the C code. Ideally we could fetch them from the
|
|
C API, e.g., in the form of a JSON object, but getting that
|
|
JSON string constructed within our current confines is
|
|
currently not worth the effort.
|
|
|
|
Reminder to self: we could probably do so by adding the
|
|
proverbial level of indirection, calling in to C to get it,
|
|
and having that C func call an
|
|
emscripten-installed/JS-implemented library function which
|
|
builds the result object:
|
|
|
|
const obj = {};
|
|
sqlite3__get_enum(function(key,val){
|
|
obj[key] = val;
|
|
});
|
|
|
|
but whether or not we can pass a function that way, via a
|
|
(void*) is as yet unknown.
|
|
*/
|
|
/* Minimum subset of sqlite result codes we'll need. */
|
|
SQLITE_OK: 0,
|
|
SQLITE_ROW: 100,
|
|
SQLITE_DONE: 101,
|
|
/* sqlite data types */
|
|
SQLITE_INTEGER: 1,
|
|
SQLITE_FLOAT: 2,
|
|
SQLITE_TEXT: 3,
|
|
SQLITE_BLOB: 4,
|
|
SQLITE_NULL: 5,
|
|
/* create_function() flags */
|
|
SQLITE_DETERMINISTIC: 0x000000800,
|
|
SQLITE_DIRECTONLY: 0x000080000,
|
|
SQLITE_INNOCUOUS: 0x000200000,
|
|
/* sqlite encodings, used for creating UDFs, noting that we
|
|
will only support UTF8. */
|
|
SQLITE_UTF8: 1
|
|
};
|
|
const cwrap = SQM.cwrap;
|
|
[/* C-side functions to bind. Each entry is an array with 3 or 4
|
|
elements:
|
|
|
|
["c-side name",
|
|
"result type" (cwrap() syntax),
|
|
[arg types in cwrap() syntax]
|
|
]
|
|
|
|
If it has 4 elements, the first one is an alternate name to
|
|
use for the JS-side binding. That's required when overloading
|
|
a binding for two different uses.
|
|
*/
|
|
["sqlite3_bind_blob","number",["number", "number", "number", "number", "number"]],
|
|
["sqlite3_bind_double","number",["number", "number", "number"]],
|
|
["sqlite3_bind_int","number",["number", "number", "number"]],
|
|
/*Noting that JS/wasm combo does not currently support 64-bit integers:
|
|
["sqlite3_bind_int64","number",["number", "number", "number"]],*/
|
|
["sqlite3_bind_null","void",["number"]],
|
|
["sqlite3_bind_parameter_count", "number", ["number"]],
|
|
["sqlite3_bind_parameter_index","number",["number", "string"]],
|
|
["sqlite3_bind_text","number",["number", "number", "number", "number", "number"]],
|
|
["sqlite3_changes", "number", ["number"]],
|
|
["sqlite3_clear_bindings","number",["number"]],
|
|
["sqlite3_close_v2", "number", ["number"]],
|
|
["sqlite3_column_blob","number", ["number", "number"]],
|
|
["sqlite3_column_bytes","number",["number", "number"]],
|
|
["sqlite3_column_count", "number", ["number"]],
|
|
["sqlite3_column_count","number",["number"]],
|
|
["sqlite3_column_double","number",["number", "number"]],
|
|
["sqlite3_column_int","number",["number", "number"]],
|
|
/*Noting that JS/wasm combo does not currently support 64-bit integers:
|
|
["sqlite3_column_int64","number",["number", "number"]],*/
|
|
["sqlite3_column_name","string",["number", "number"]],
|
|
["sqlite3_column_text","string",["number", "number"]],
|
|
["sqlite3_column_type","number",["number", "number"]],
|
|
["sqlite3_compileoption_get", "string", ["number"]],
|
|
["sqlite3_compileoption_used", "number", ["string"]],
|
|
["sqlite3_create_function_v2", "number",
|
|
["number", "string", "number", "number","number",
|
|
"number", "number", "number", "number"]],
|
|
["sqlite3_data_count", "number", ["number"]],
|
|
["sqlite3_db_filename", "string", ["number", "string"]],
|
|
["sqlite3_errmsg", "string", ["number"]],
|
|
["sqlite3_exec", "number", ["number", "string", "number", "number", "number"]],
|
|
["sqlite3_finalize", "number", ["number"]],
|
|
["sqlite3_interrupt", "void", ["number"]],
|
|
["sqlite3_libversion", "string", []],
|
|
["sqlite3_open", "number", ["string", "number"]],
|
|
//["sqlite3_open_v2", "number", ["string", "number", "number", "string"]],
|
|
//^^^^ TODO: add the flags needed for the 3rd arg
|
|
["sqlite3_prepare_v2", "number", ["number", "string", "number", "number", "number"]],
|
|
["sqlite3_prepare_v2_sqlptr", "sqlite3_prepare_v2",
|
|
/* Impl which requires that the 2nd argument be a pointer to
|
|
the SQL string, instead of being converted to a
|
|
string. This is used for cases where we require a non-NULL
|
|
value for the final argument (exec()'ing multiple
|
|
statements from one input string). */
|
|
"number", ["number", "number", "number", "number", "number"]],
|
|
["sqlite3_reset", "number", ["number"]],
|
|
["sqlite3_result_blob",null,["number", "number", "number", "number"]],
|
|
["sqlite3_result_double",null,["number", "number"]],
|
|
["sqlite3_result_error",null,["number", "string", "number"]],
|
|
["sqlite3_result_int",null,["number", "number"]],
|
|
["sqlite3_result_null",null,["number"]],
|
|
["sqlite3_result_text",null,["number", "string", "number", "number"]],
|
|
["sqlite3_sourceid", "string", []],
|
|
["sqlite3_sql", "string", ["number"]],
|
|
["sqlite3_step", "number", ["number"]],
|
|
["sqlite3_value_blob", "number", ["number"]],
|
|
["sqlite3_value_bytes","number",["number"]],
|
|
["sqlite3_value_double","number",["number"]],
|
|
["sqlite3_value_text", "string", ["number"]],
|
|
["sqlite3_value_type", "number", ["number"]]
|
|
//["sqlite3_normalized_sql", "string", ["number"]]
|
|
].forEach(function(a){
|
|
const k = (4==a.length) ? a.shift() : a[0];
|
|
api[k] = cwrap.apply(this, a);
|
|
});
|
|
|
|
/* What follows is colloquially known as "OO API #1". It is a
|
|
binding of the sqlite3 API which is designed to be run within
|
|
the same thread (main or worker) as the one in which the
|
|
sqlite3 WASM binding was initialized. This wrapper cannot use
|
|
the sqlite3 binding if, e.g., the wrapper is in the main thread
|
|
and the sqlite3 API is in a worker. */
|
|
|
|
/** Memory for use in some pointer-to-pointer-passing routines. */
|
|
const pPtrArg = stackAlloc(4);
|
|
/** Throws a new error, concatenating all args with a space between
|
|
each. */
|
|
const toss = function(){
|
|
throw new Error(Array.prototype.join.call(arguments, ' '));
|
|
};
|
|
|
|
/**
|
|
The DB class wraps a sqlite3 db handle.
|
|
|
|
It accepts the following argument signatures:
|
|
|
|
- ()
|
|
- (undefined) (same effect as ())
|
|
- (filename[,buffer])
|
|
- (buffer)
|
|
|
|
Where a buffer indicates a Uint8Array holding an sqlite3 db
|
|
image.
|
|
|
|
If the filename is provided, only the last component of the
|
|
path is used - any path prefix is stripped and certain
|
|
"special" characters are replaced with `_`. If no name is
|
|
provided, a random name is generated. The resulting filename is
|
|
the one used for accessing the db file within root directory of
|
|
the emscripten-supplied virtual filesystem, and is set (with no
|
|
path part) as the DB object's `filename` property.
|
|
|
|
Note that the special sqlite3 db names ":memory:" and ""
|
|
(temporary db) have no special meanings here. We can apparently
|
|
only export images of DBs which are stored in the
|
|
pseudo-filesystem provided by the JS APIs. Since exporting and
|
|
importing images is an important usability feature for this
|
|
class, ":memory:" DBs are not supported (until/unless we can
|
|
find a way to export those as well). The naming semantics will
|
|
certainly evolve as this API does.
|
|
*/
|
|
const DB = function(arg){
|
|
let buffer, fn;
|
|
if(arg instanceof Uint8Array){
|
|
buffer = arg;
|
|
arg = undefined;
|
|
}else if(arguments.length){ /*(filename[,buffer])*/
|
|
if('string'===typeof arg){
|
|
const p = arg.split('/').pop().replace(':','_');
|
|
if(p) fn = p;
|
|
if(arguments.length>1){
|
|
buffer = arguments[1];
|
|
}
|
|
}else if(undefined!==arg){
|
|
toss("Invalid arguments to DB constructor.",
|
|
"Expecting (), (undefined), (name,buffer),",
|
|
"or (buffer), where buffer an sqlite3 db ",
|
|
"as a Uint8Array.");
|
|
}
|
|
}
|
|
if(!fn){
|
|
fn = "db-"+((Math.random() * 10000000) | 0)+
|
|
"-"+((Math.random() * 10000000) | 0)+".sqlite3";
|
|
}
|
|
if(buffer){
|
|
if(!(buffer instanceof Uint8Array)){
|
|
toss("Expecting Uint8Array image of db contents.");
|
|
}
|
|
FS.createDataFile("/", fn, buffer, true, true);
|
|
}
|
|
setValue(pPtrArg, 0, "i32");
|
|
this.checkRc(api.sqlite3_open(fn, pPtrArg));
|
|
this._pDb = getValue(pPtrArg, "i32");
|
|
this.filename = fn;
|
|
this._statements = {/*map of open Stmt _pointers_ to Stmt*/};
|
|
this._udfs = {/*map of UDF names to wasm function _pointers_*/};
|
|
};
|
|
|
|
/**
|
|
Internal-use enum for mapping JS types to DB-bindable types.
|
|
These do not (and need not) line up with the SQLITE_type
|
|
values. All values in this enum must be truthy and distinct
|
|
but they need not be numbers.
|
|
*/
|
|
const BindTypes = {
|
|
null: 1,
|
|
number: 2,
|
|
string: 3,
|
|
boolean: 4,
|
|
blob: 5
|
|
};
|
|
BindTypes['undefined'] == BindTypes.null;
|
|
|
|
/**
|
|
This class wraps sqlite3_stmt. Calling this constructor
|
|
directly will trigger an exception. Use DB.prepare() to create
|
|
new instances.
|
|
*/
|
|
const Stmt = function(){
|
|
if(BindTypes!==arguments[2]){
|
|
toss("Do not call the Stmt constructor directly. Use DB.prepare().");
|
|
}
|
|
this.db = arguments[0];
|
|
this._pStmt = arguments[1];
|
|
this.columnCount = api.sqlite3_column_count(this._pStmt);
|
|
this.parameterCount = api.sqlite3_bind_parameter_count(this._pStmt);
|
|
this._allocs = [/*list of alloc'd memory blocks for bind() values*/]
|
|
};
|
|
|
|
/** Throws if the given DB has been closed, else it is returned. */
|
|
const affirmDbOpen = function(db){
|
|
if(!db._pDb) toss("DB has been closed.");
|
|
return db;
|
|
};
|
|
|
|
/** Returns true if n is a 32-bit (signed) integer,
|
|
else false. */
|
|
const isInt32 = function(n){
|
|
return (n===n|0 && n<0xFFFFFFFF) ? true : undefined;
|
|
};
|
|
|
|
/**
|
|
Expects to be passed (arguments) from DB.exec() and
|
|
DB.execMulti(). Does the argument processing/validation, throws
|
|
on error, and returns a new object on success:
|
|
|
|
{ sql: the SQL, obt: optionsObj, cbArg: function}
|
|
|
|
cbArg is only set if the opt.callback is set, in which case
|
|
it's a function which expects to be passed the current Stmt
|
|
and returns the callback argument of the type indicated by
|
|
the input arguments.
|
|
*/
|
|
const parseExecArgs = function(args){
|
|
const out = {};
|
|
switch(args.length){
|
|
case 1:
|
|
if('string'===typeof args[0]){
|
|
out.sql = args[0];
|
|
out.opt = {};
|
|
}else if(args[0] && 'object'===typeof args[0]){
|
|
out.opt = args[0];
|
|
out.sql = out.opt.sql;
|
|
}
|
|
break;
|
|
case 2:
|
|
out.sql = args[0];
|
|
out.opt = args[1];
|
|
break;
|
|
default: toss("Invalid argument count for exec().");
|
|
};
|
|
if('string'!==typeof out.sql) toss("Missing SQL argument.");
|
|
if(out.opt.callback || out.opt.resultRows){
|
|
switch((undefined===out.opt.rowMode)
|
|
? 'stmt' : out.opt.rowMode) {
|
|
case 'object': out.cbArg = (stmt)=>stmt.get({}); break;
|
|
case 'array': out.cbArg = (stmt)=>stmt.get([]); break;
|
|
case 'stmt': out.cbArg = (stmt)=>stmt; break;
|
|
default: toss("Invalid rowMode:",out.opt.rowMode);
|
|
}
|
|
}
|
|
return out;
|
|
};
|
|
|
|
/** If object opts has _its own_ property named p then that
|
|
property's value is returned, else dflt is returned. */
|
|
const getOwnOption = (opts, p, dflt)=>
|
|
opts.hasOwnProperty(p) ? opts[p] : dflt;
|
|
|
|
DB.prototype = {
|
|
/**
|
|
Expects to be given an sqlite3 API result code. If it is
|
|
falsy, this function returns this object, else it throws an
|
|
exception with an error message from sqlite3_errmsg(),
|
|
using this object's db handle. Note that if it's passed a
|
|
non-error code like SQLITE_ROW or SQLITE_DONE, it will
|
|
still throw but the error string might be "Not an error."
|
|
The various non-0 non-error codes need to be checked for in
|
|
client code where they are expected.
|
|
*/
|
|
checkRc: function(sqliteResultCode){
|
|
if(!sqliteResultCode) return this;
|
|
toss("sqlite result code",sqliteResultCode+":",
|
|
api.sqlite3_errmsg(this._pDb) || "Unknown db error.");
|
|
},
|
|
/**
|
|
Finalizes all open statements and closes this database
|
|
connection. This is a no-op if the db has already been
|
|
closed. If the db is open and alsoUnlink is truthy then the
|
|
this.filename entry in the pseudo-filesystem will also be
|
|
removed (and any error in that attempt is silently
|
|
ignored).
|
|
*/
|
|
close: function(alsoUnlink){
|
|
if(this._pDb){
|
|
let s;
|
|
const that = this;
|
|
Object.keys(this._statements).forEach(function(k,s){
|
|
delete that._statements[k];
|
|
if(s && s._pStmt) s.finalize();
|
|
});
|
|
Object.values(this._udfs).forEach(SQM.removeFunction);
|
|
delete this._udfs;
|
|
delete this._statements;
|
|
api.sqlite3_close_v2(this._pDb);
|
|
delete this._pDb;
|
|
if(this.filename){
|
|
if(alsoUnlink){
|
|
try{SQM.FS.unlink('/'+this.filename);}
|
|
catch(e){/*ignored*/}
|
|
}
|
|
delete this.filename;
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
Similar to this.filename but will return NULL for
|
|
special names like ":memory:". Not of much use until
|
|
we have filesystem support. Throws if the DB has
|
|
been closed. If passed an argument it then it will return
|
|
the filename of the ATTACHEd db with that name, else it assumes
|
|
a name of `main`.
|
|
*/
|
|
fileName: function(dbName){
|
|
return api.sqlite3_db_filename(affirmDbOpen(this)._pDb, dbName||"main");
|
|
},
|
|
/**
|
|
Compiles the given SQL and returns a prepared Stmt. This is
|
|
the only way to create new Stmt objects. Throws on error.
|
|
*/
|
|
prepare: function(sql){
|
|
affirmDbOpen(this);
|
|
setValue(pPtrArg,0,"i32");
|
|
this.checkRc(api.sqlite3_prepare_v2(this._pDb, sql, -1, pPtrArg, null));
|
|
const pStmt = getValue(pPtrArg, "i32");
|
|
if(!pStmt) toss("Empty SQL is not permitted.");
|
|
const stmt = new Stmt(this, pStmt, BindTypes);
|
|
this._statements[pStmt] = stmt;
|
|
return stmt;
|
|
},
|
|
/**
|
|
This function works like execMulti(), and takes most of the
|
|
same arguments, but is more efficient (performs much less
|
|
work) when the input SQL is only a single statement. If
|
|
passed a multi-statement SQL, it only processes the first
|
|
one.
|
|
|
|
This function supports the following additional options not
|
|
supported by execMulti():
|
|
|
|
- .multi: if true, this function acts as a proxy for
|
|
execMulti() and behaves identically to that function.
|
|
|
|
- .resultRows: if this is an array, each row of the result
|
|
set (if any) is appended to it in the format specified
|
|
for the `rowMode` property, with the exception that the
|
|
only legal values for `rowMode` in this case are 'array'
|
|
or 'object', neither of which is the default. It is legal
|
|
to use both `resultRows` and `callback`, but `resultRows`
|
|
is likely much simpler to use for small data sets and can
|
|
be used over a WebWorker-style message interface.
|
|
|
|
- .columnNames: if this is an array and the query has
|
|
result columns, the array is passed to
|
|
Stmt.getColumnNames() to append the column names to it
|
|
(regardless of whether the query produces any result
|
|
rows). If the query has no result columns, this value is
|
|
unchanged.
|
|
|
|
The following options to execMulti() are _not_ supported by
|
|
this method (they are simply ignored):
|
|
|
|
- .saveSql
|
|
*/
|
|
exec: function(/*(sql [,optionsObj]) or (optionsObj)*/){
|
|
affirmDbOpen(this);
|
|
const arg = parseExecArgs(arguments);
|
|
if(!arg.sql) return this;
|
|
else if(arg.opt.multi){
|
|
return this.execMulti(arg, undefined, BindTypes);
|
|
}
|
|
const opt = arg.opt;
|
|
let stmt, rowTarget;
|
|
try {
|
|
if(Array.isArray(opt.resultRows)){
|
|
if(opt.rowMode!=='array' && opt.rowMode!=='object'){
|
|
toss("Invalid rowMode for resultRows array: must",
|
|
"be one of 'array' or 'object'.");
|
|
}
|
|
rowTarget = opt.resultRows;
|
|
}
|
|
stmt = this.prepare(arg.sql);
|
|
if(stmt.columnCount && Array.isArray(opt.columnNames)){
|
|
stmt.getColumnNames(opt.columnNames);
|
|
}
|
|
if(opt.bind) stmt.bind(opt.bind);
|
|
if(opt.callback || rowTarget){
|
|
while(stmt.step()){
|
|
const row = arg.cbArg(stmt);
|
|
if(rowTarget) rowTarget.push(row);
|
|
if(opt.callback){
|
|
stmt._isLocked = true;
|
|
opt.callback(row, stmt);
|
|
stmt._isLocked = false;
|
|
}
|
|
}
|
|
}else{
|
|
stmt.step();
|
|
}
|
|
}finally{
|
|
if(stmt){
|
|
delete stmt._isLocked;
|
|
stmt.finalize();
|
|
}
|
|
}
|
|
return this;
|
|
|
|
}/*exec()*/,
|
|
/**
|
|
Executes one or more SQL statements in the form of a single
|
|
string. Its arguments must be either (sql,optionsObject) or
|
|
(optionsObject). In the latter case, optionsObject.sql
|
|
must contain the SQL to execute. Returns this
|
|
object. Throws on error.
|
|
|
|
If no SQL is provided, or a non-string is provided, an
|
|
exception is triggered. Empty SQL, on the other hand, is
|
|
simply a no-op.
|
|
|
|
The optional options object may contain any of the following
|
|
properties:
|
|
|
|
- .sql = the SQL to run (unless it's provided as the first
|
|
argument).
|
|
|
|
- .bind = a single value valid as an argument for
|
|
Stmt.bind(). This is ONLY applied to the FIRST non-empty
|
|
statement in the SQL which has any bindable
|
|
parameters. (Empty statements are skipped entirely.)
|
|
|
|
- .callback = a function which gets called for each row of
|
|
the FIRST statement in the SQL (if it has any result
|
|
rows). The second argument passed to the callback is
|
|
always the current Stmt object (so that the caller
|
|
may collect column names, or similar). The first
|
|
argument passed to the callback defaults to the current
|
|
Stmt object but may be changed with ...
|
|
|
|
- .rowMode = a string describing what type of argument
|
|
should be passed as the first argument to the callback. A
|
|
value of 'object' causes the results of `stmt.get({})` to
|
|
be passed to the object. A value of 'array' causes the
|
|
results of `stmt.get([])` to be passed to the callback.
|
|
A value of 'stmt' is equivalent to the default, passing
|
|
the current Stmt to the callback (noting that it's always
|
|
passed as the 2nd argument). Any other value triggers an
|
|
exception.
|
|
|
|
- saveSql = an optional array. If set, the SQL of each
|
|
executed statement is appended to this array before the
|
|
statement is executed (but after it is prepared - we
|
|
don't have the string until after that). Empty SQL
|
|
statements are elided.
|
|
|
|
See also the exec() method, which is a close cousin of this
|
|
one.
|
|
|
|
ACHTUNG #1: The callback MUST NOT modify the Stmt
|
|
object. Calling any of the Stmt.get() variants,
|
|
Stmt.getColumnName(), or similar, is legal, but calling
|
|
step() or finalize() is not. Routines which are illegal
|
|
in this context will trigger an exception.
|
|
|
|
ACHTUNG #2: The semantics of the `bind` and `callback`
|
|
options may well change or those options may be removed
|
|
altogether for this function (but retained for exec()).
|
|
Generally speaking, neither bind parameters nor a callback
|
|
are generically useful when executing multi-statement SQL.
|
|
*/
|
|
execMulti: function(/*(sql [,obj]) || (obj)*/){
|
|
affirmDbOpen(this);
|
|
const arg = (BindTypes===arguments[2]
|
|
/* ^^^ Being passed on from exec() */
|
|
? arguments[0] : parseExecArgs(arguments));
|
|
if(!arg.sql) return this;
|
|
const opt = arg.opt;
|
|
const stack = stackSave();
|
|
let stmt;
|
|
let bind = opt.bind;
|
|
let rowMode = (
|
|
(opt.callback && opt.rowMode)
|
|
? opt.rowMode : false);
|
|
try{
|
|
let pSql = SQM.allocateUTF8OnStack(arg.sql)
|
|
const pzTail = stackAlloc(4);
|
|
while(getValue(pSql, "i8")){
|
|
setValue(pPtrArg, 0, "i32");
|
|
setValue(pzTail, 0, "i32");
|
|
this.checkRc(api.sqlite3_prepare_v2_sqlptr(
|
|
this._pDb, pSql, -1, pPtrArg, pzTail
|
|
));
|
|
const pStmt = getValue(pPtrArg, "i32");
|
|
pSql = getValue(pzTail, "i32");
|
|
if(!pStmt) continue;
|
|
if(opt.saveSql){
|
|
opt.saveSql.push(api.sqlite3_sql(pStmt).trim());
|
|
}
|
|
stmt = new Stmt(this, pStmt, BindTypes);
|
|
if(bind && stmt.parameterCount){
|
|
stmt.bind(bind);
|
|
bind = null;
|
|
}
|
|
if(opt.callback && null!==rowMode){
|
|
while(stmt.step()){
|
|
stmt._isLocked = true;
|
|
callback(arg.cbArg(stmt), stmt);
|
|
stmt._isLocked = false;
|
|
}
|
|
rowMode = null;
|
|
}else{
|
|
// Do we need to while(stmt.step()){} here?
|
|
stmt.step();
|
|
}
|
|
stmt.finalize();
|
|
stmt = null;
|
|
}
|
|
}finally{
|
|
if(stmt){
|
|
delete stmt._isLocked;
|
|
stmt.finalize();
|
|
}
|
|
stackRestore(stack);
|
|
}
|
|
return this;
|
|
}/*execMulti()*/,
|
|
/**
|
|
Creates a new scalar UDF (User-Defined Function) which is
|
|
accessible via SQL code. This function may be called in any
|
|
of the following forms:
|
|
|
|
- (name, function)
|
|
- (name, function, optionsObject)
|
|
- (name, optionsObject)
|
|
- (optionsObject)
|
|
|
|
In the final two cases, the function must be defined as the
|
|
'callback' property of the options object. In the final
|
|
case, the function's name must be the 'name' property.
|
|
|
|
This can only be used to create scalar functions, not
|
|
aggregate or window functions. UDFs cannot be removed from
|
|
a DB handle after they're added.
|
|
|
|
On success, returns this object. Throws on error.
|
|
|
|
When called from SQL, arguments to the UDF, and its result,
|
|
will be converted between JS and SQL with as much fidelity
|
|
as is feasible, triggering an exception if a type
|
|
conversion cannot be determined. Some freedom is afforded
|
|
to numeric conversions due to friction between the JS and C
|
|
worlds: integers which are larger than 32 bits will be
|
|
treated as doubles, as JS does not support 64-bit integers
|
|
and it is (as of this writing) illegal to use WASM
|
|
functions which take or return 64-bit integers from JS.
|
|
|
|
The optional options object may contain flags to modify how
|
|
the function is defined:
|
|
|
|
- .arity: the number of arguments which SQL calls to this
|
|
function expect or require. The default value is the
|
|
callback's length property (i.e. the number of declared
|
|
parameters it has). A value of -1 means that the function
|
|
is variadic and may accept any number of arguments, up to
|
|
sqlite3's compile-time limits. sqlite3 will enforce the
|
|
argument count if is zero or greater.
|
|
|
|
The following properties correspond to flags documented at:
|
|
|
|
https://sqlite.org/c3ref/create_function.html
|
|
|
|
- .deterministic = SQLITE_DETERMINISTIC
|
|
- .directOnly = SQLITE_DIRECTONLY
|
|
- .innocuous = SQLITE_INNOCUOUS
|
|
|
|
|
|
Maintenance reminder: the ability to add new
|
|
WASM-accessible functions to the runtime requires that the
|
|
WASM build is compiled with emcc's `-sALLOW_TABLE_GROWTH`
|
|
flag.
|
|
*/
|
|
createFunction: function f(name, callback,opt){
|
|
switch(arguments.length){
|
|
case 1: /* (optionsObject) */
|
|
opt = name;
|
|
name = opt.name;
|
|
callback = opt.callback;
|
|
break;
|
|
case 2: /* (name, callback|optionsObject) */
|
|
if(!(callback instanceof Function)){
|
|
opt = callback;
|
|
callback = opt.callback;
|
|
}
|
|
break;
|
|
default: break;
|
|
}
|
|
if(!opt) opt = {};
|
|
if(!(callback instanceof Function)){
|
|
toss("Invalid arguments: expecting a callback function.");
|
|
}else if('string' !== typeof name){
|
|
toss("Invalid arguments: missing function name.");
|
|
}
|
|
if(!f._extractArgs){
|
|
/* Static init */
|
|
f._extractArgs = function(argc, pArgv){
|
|
let i, pVal, valType, arg;
|
|
const tgt = [];
|
|
for(i = 0; i < argc; ++i){
|
|
pVal = getValue(pArgv + (4 * i), "i32");
|
|
valType = api.sqlite3_value_type(pVal);
|
|
switch(valType){
|
|
case api.SQLITE_INTEGER:
|
|
case api.SQLITE_FLOAT:
|
|
arg = api.sqlite3_value_double(pVal);
|
|
break;
|
|
case SQLITE_TEXT:
|
|
arg = api.sqlite3_value_text(pVal);
|
|
break;
|
|
case SQLITE_BLOB:{
|
|
const n = api.sqlite3_value_bytes(ptr);
|
|
const pBlob = api.sqlite3_value_blob(ptr);
|
|
arg = new Uint8Array(n);
|
|
let i;
|
|
for(i = 0; i < n; ++i) arg[i] = HEAP8[pBlob+i];
|
|
break;
|
|
}
|
|
default:
|
|
arg = null; break;
|
|
}
|
|
tgt.push(arg);
|
|
}
|
|
return tgt;
|
|
}/*_extractArgs()*/;
|
|
f._setResult = function(pCx, val){
|
|
switch(typeof val) {
|
|
case 'boolean':
|
|
api.sqlite3_result_int(pCx, val ? 1 : 0);
|
|
break;
|
|
case 'number': {
|
|
(isInt32(val)
|
|
? api.sqlite3_result_int
|
|
: api.sqlite3_result_double)(pCx, val);
|
|
break;
|
|
}
|
|
case 'string':
|
|
api.sqlite3_result_text(pCx, val, -1,
|
|
-1/*==SQLITE_TRANSIENT*/);
|
|
break;
|
|
case 'object':
|
|
if(null===val) {
|
|
api.sqlite3_result_null(pCx);
|
|
break;
|
|
}else if(undefined!==val.length){
|
|
const pBlob =
|
|
SQM.allocate(val, SQM.ALLOC_NORMAL);
|
|
api.sqlite3_result_blob(pCx, pBlob, val.length, -1/*==SQLITE_TRANSIENT*/);
|
|
SQM._free(blobptr);
|
|
break;
|
|
}
|
|
// else fall through
|
|
default:
|
|
toss("Don't not how to handle this UDF result value:",val);
|
|
};
|
|
}/*_setResult()*/;
|
|
}/*static init*/
|
|
const wrapper = function(pCx, argc, pArgv){
|
|
try{
|
|
f._setResult(pCx, callback.apply(null, f._extractArgs(argc, pArgv)));
|
|
}catch(e){
|
|
api.sqlite3_result_error(pCx, e.message, -1);
|
|
}
|
|
};
|
|
const pUdf = SQM.addFunction(wrapper, "viii");
|
|
let fFlags = 0;
|
|
if(getOwnOption(opt, 'deterministic')) fFlags |= api.SQLITE_DETERMINISTIC;
|
|
if(getOwnOption(opt, 'directOnly')) fFlags |= api.SQLITE_DIRECTONLY;
|
|
if(getOwnOption(opt, 'innocuous')) fFlags |= api.SQLITE_INNOCUOUS;
|
|
name = name.toLowerCase();
|
|
try {
|
|
this.checkRc(api.sqlite3_create_function_v2(
|
|
this._pDb, name,
|
|
(opt.hasOwnProperty('arity') ? +opt.arity : callback.length),
|
|
api.SQLITE_UTF8 | fFlags, null/*pApp*/, pUdf,
|
|
null/*xStep*/, null/*xFinal*/, null/*xDestroy*/));
|
|
}catch(e){
|
|
SQM.removeFunction(pUdf);
|
|
throw e;
|
|
}
|
|
if(this._udfs.hasOwnProperty(name)){
|
|
SQM.removeFunction(this._udfs[name]);
|
|
}
|
|
this._udfs[name] = pUdf;
|
|
return this;
|
|
}/*createFunction()*/,
|
|
/**
|
|
Prepares the given SQL, step()s it one time, and returns
|
|
the value of the first result column. If it has no results,
|
|
undefined is returned. If passed a second argument, it is
|
|
treated like an argument to Stmt.bind(), so may be any type
|
|
supported by that function. Throws on error (e.g. malformed
|
|
SQL).
|
|
*/
|
|
selectValue: function(sql,bind){
|
|
let stmt, rc;
|
|
try {
|
|
stmt = this.prepare(sql).bind(bind);
|
|
if(stmt.step()) rc = stmt.get(0);
|
|
}finally{
|
|
if(stmt) stmt.finalize();
|
|
}
|
|
return rc;
|
|
},
|
|
|
|
/**
|
|
Exports a copy of this db's file as a Uint8Array and
|
|
returns it. It is technically not legal to call this while
|
|
any prepared statement are currently active because,
|
|
depending on the platform, it might not be legal to read
|
|
the db while a statement is locking it. Throws if this db
|
|
is not open or has any opened statements.
|
|
|
|
The resulting buffer can be passed to this class's
|
|
constructor to restore the DB.
|
|
|
|
Maintenance reminder: the corresponding sql.js impl of this
|
|
feature closes the current db, finalizing any active
|
|
statements and (seemingly unnecessarily) destroys any UDFs,
|
|
copies the file, and then re-opens it (without restoring
|
|
the UDFs). Those gymnastics are not necessary on the tested
|
|
platform but might be necessary on others. Because of that
|
|
eventuality, this interface currently enforces that no
|
|
statements are active when this is run. It will throw if
|
|
any are.
|
|
*/
|
|
exportBinaryImage: function(){
|
|
affirmDbOpen(this);
|
|
if(Object.keys(this._statements).length){
|
|
toss("Cannot export with prepared statements active!",
|
|
"finalize() all statements and try again.");
|
|
}
|
|
return FS.readFile(this.filename, {encoding:"binary"});
|
|
}
|
|
}/*DB.prototype*/;
|
|
|
|
|
|
/** Throws if the given Stmt has been finalized, else stmt is
|
|
returned. */
|
|
const affirmStmtOpen = function(stmt){
|
|
if(!stmt._pStmt) toss("Stmt has been closed.");
|
|
return stmt;
|
|
};
|
|
|
|
/** Returns an opaque truthy value from the BindTypes
|
|
enum if v's type is a valid bindable type, else
|
|
returns a falsy value. As a special case, a value of
|
|
undefined is treated as a bind type of null. */
|
|
const isSupportedBindType = function(v){
|
|
let t = BindTypes[(null===v||undefined===v) ? 'null' : typeof v];
|
|
switch(t){
|
|
case BindTypes.boolean:
|
|
case BindTypes.null:
|
|
case BindTypes.number:
|
|
case BindTypes.string:
|
|
return t;
|
|
default:
|
|
if(v instanceof Uint8Array) return BindTypes.blob;
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
/**
|
|
If isSupportedBindType(v) returns a truthy value, this
|
|
function returns that value, else it throws.
|
|
*/
|
|
const affirmSupportedBindType = function(v){
|
|
return isSupportedBindType(v) || toss("Unsupport bind() argument type.");
|
|
};
|
|
|
|
/**
|
|
If key is a number and within range of stmt's bound parameter
|
|
count, key is returned.
|
|
|
|
If key is not a number then it is checked against named
|
|
parameters. If a match is found, its index is returned.
|
|
|
|
Else it throws.
|
|
*/
|
|
const affirmParamIndex = function(stmt,key){
|
|
const n = ('number'===typeof key)
|
|
? key : api.sqlite3_bind_parameter_index(stmt._pStmt, key);
|
|
if(0===n || (n===key && (n!==(n|0)/*floating point*/))){
|
|
toss("Invalid bind() parameter name: "+key);
|
|
}
|
|
else if(n<1 || n>stmt.parameterCount) toss("Bind index",key,"is out of range.");
|
|
return n;
|
|
};
|
|
|
|
/** Throws if ndx is not an integer or if it is out of range
|
|
for stmt.columnCount, else returns stmt.
|
|
|
|
Reminder: this will also fail after the statement is finalized
|
|
but the resulting error will be about an out-of-bounds column
|
|
index.
|
|
*/
|
|
const affirmColIndex = function(stmt,ndx){
|
|
if((ndx !== (ndx|0)) || ndx<0 || ndx>=stmt.columnCount){
|
|
toss("Column index",ndx,"is out of range.");
|
|
}
|
|
return stmt;
|
|
};
|
|
|
|
/**
|
|
If stmt._isLocked is truthy, this throws an exception
|
|
complaining that the 2nd argument (an operation name,
|
|
e.g. "bind()") is not legal while the statement is "locked".
|
|
Locking happens before an exec()-like callback is passed a
|
|
statement, to ensure that the callback does not mutate or
|
|
finalize the statement. If it does not throw, it returns stmt.
|
|
*/
|
|
const affirmUnlocked = function(stmt,currentOpName){
|
|
if(stmt._isLocked){
|
|
toss("Operation is illegal when statement is locked:",currentOpName);
|
|
}
|
|
return stmt;
|
|
};
|
|
|
|
/**
|
|
Binds a single bound parameter value on the given stmt at the
|
|
given index (numeric or named) using the given bindType (see
|
|
the BindTypes enum) and value. Throws on error. Returns stmt on
|
|
success.
|
|
*/
|
|
const bindOne = function f(stmt,ndx,bindType,val){
|
|
affirmUnlocked(stmt, 'bind()');
|
|
if(!f._){
|
|
f._ = {
|
|
string: function(stmt, ndx, val, asBlob){
|
|
const bytes = intArrayFromString(val,true);
|
|
const pStr = SQM.allocate(bytes, ALLOC_NORMAL);
|
|
stmt._allocs.push(pStr);
|
|
const func = asBlob ? api.sqlite3_bind_blob : api.sqlite3_bind_text;
|
|
return func(stmt._pStmt, ndx, pStr, bytes.length, 0);
|
|
}
|
|
};
|
|
}
|
|
affirmSupportedBindType(val);
|
|
ndx = affirmParamIndex(stmt,ndx);
|
|
let rc = 0;
|
|
switch((null===val || undefined===val) ? BindTypes.null : bindType){
|
|
case BindTypes.null:
|
|
rc = api.sqlite3_bind_null(stmt._pStmt, ndx);
|
|
break;
|
|
case BindTypes.string:{
|
|
rc = f._.string(stmt, ndx, val, false);
|
|
break;
|
|
}
|
|
case BindTypes.number: {
|
|
const m = (isInt32(val)
|
|
? api.sqlite3_bind_int
|
|
/*It's illegal to bind a 64-bit int
|
|
from here*/
|
|
: api.sqlite3_bind_double);
|
|
rc = m(stmt._pStmt, ndx, val);
|
|
break;
|
|
}
|
|
case BindTypes.boolean:
|
|
rc = api.sqlite3_bind_int(stmt._pStmt, ndx, val ? 1 : 0);
|
|
break;
|
|
case BindTypes.blob: {
|
|
if('string'===typeof val){
|
|
rc = f._.string(stmt, ndx, val, true);
|
|
}else{
|
|
const len = val.length;
|
|
if(undefined===len){
|
|
toss("Binding a value as a blob requires",
|
|
"that it have a length member.");
|
|
}
|
|
const pBlob = SQM.allocate(val, ALLOC_NORMAL);
|
|
stmt._allocs.push(pBlob);
|
|
rc = api.sqlite3_bind_blob(stmt._pStmt, ndx, pBlob, len, 0);
|
|
}
|
|
}
|
|
default: toss("Unsupported bind() argument type.");
|
|
}
|
|
if(rc) stmt.db.checkRc(rc);
|
|
return stmt;
|
|
};
|
|
|
|
/** Frees any memory explicitly allocated for the given
|
|
Stmt object. Returns stmt. */
|
|
const freeBindMemory = function(stmt){
|
|
let m;
|
|
while(undefined !== (m = stmt._allocs.pop())){
|
|
SQM._free(m);
|
|
}
|
|
return stmt;
|
|
};
|
|
|
|
Stmt.prototype = {
|
|
/**
|
|
"Finalizes" this statement. This is a no-op if the
|
|
statement has already been finalizes. Returns
|
|
undefined. Most methods in this class will throw if called
|
|
after this is.
|
|
*/
|
|
finalize: function(){
|
|
if(this._pStmt){
|
|
affirmUnlocked(this,'finalize()');
|
|
freeBindMemory(this);
|
|
delete this.db._statements[this._pStmt];
|
|
api.sqlite3_finalize(this._pStmt);
|
|
delete this.columnCount;
|
|
delete this.parameterCount;
|
|
delete this._pStmt;
|
|
delete this.db;
|
|
delete this._isLocked;
|
|
}
|
|
},
|
|
/** Clears all bound values. Returns this object.
|
|
Throws if this statement has been finalized. */
|
|
clearBindings: function(){
|
|
freeBindMemory(
|
|
affirmUnlocked(affirmStmtOpen(this), 'clearBindings()')
|
|
);
|
|
api.sqlite3_clear_bindings(this._pStmt);
|
|
this._mayGet = false;
|
|
return this;
|
|
},
|
|
/**
|
|
Resets this statement so that it may be step()ed again
|
|
from the beginning. Returns this object. Throws if this
|
|
statement has been finalized.
|
|
|
|
If passed a truthy argument then this.clearBindings() is
|
|
also called, otherwise any existing bindings, along with
|
|
any memory allocated for them, are retained.
|
|
*/
|
|
reset: function(alsoClearBinds){
|
|
affirmUnlocked(this,'reset()');
|
|
if(alsoClearBinds) this.clearBindings();
|
|
api.sqlite3_reset(affirmStmtOpen(this)._pStmt);
|
|
this._mayGet = false;
|
|
return this;
|
|
},
|
|
/**
|
|
Binds one or more values to its bindable parameters. It
|
|
accepts 1 or 2 arguments:
|
|
|
|
If passed a single argument, it must be either an array, an
|
|
object, or a value of a bindable type (see below).
|
|
|
|
If passed 2 arguments, the first one is the 1-based bind
|
|
index or bindable parameter name and the second one must be
|
|
a value of a bindable type.
|
|
|
|
Bindable value types:
|
|
|
|
- null is bound as NULL.
|
|
|
|
- undefined as a standalone value is a no-op intended to
|
|
simplify certain client-side use cases: passing undefined
|
|
as a value to this function will not actually bind
|
|
anything and this function will skip confirmation that
|
|
binding is even legal. (Those semantics simplify certain
|
|
client-side uses.) Conversely, a value of undefined as an
|
|
array or object property when binding an array/object
|
|
(see below) is treated the same as null.
|
|
|
|
- Numbers are bound as either doubles or integers: doubles
|
|
if they are larger than 32 bits, else double or int32,
|
|
depending on whether they have a fractional part. (It is,
|
|
as of this writing, illegal to call (from JS) a WASM
|
|
function which either takes or returns an int64.)
|
|
Booleans are bound as integer 0 or 1. It is not expected
|
|
the distinction of binding doubles which have no
|
|
fractional parts is integers is significant for the
|
|
majority of clients due to sqlite3's data typing
|
|
model. This API does not currently support the BigInt
|
|
type.
|
|
|
|
- Strings are bound as strings (use bindAsBlob() to force
|
|
blob binding).
|
|
|
|
- Uint8Array instances are bound as blobs.
|
|
|
|
If passed an array, each element of the array is bound at
|
|
the parameter index equal to the array index plus 1
|
|
(because arrays are 0-based but binding is 1-based).
|
|
|
|
If passed an object, each object key is treated as a
|
|
bindable parameter name. The object keys _must_ match any
|
|
bindable parameter names, including any `$`, `@`, or `:`
|
|
prefix. Because `$` is a legal identifier chararacter in
|
|
JavaScript, that is the suggested prefix for bindable
|
|
parameters.
|
|
|
|
It returns this object on success and throws on
|
|
error. Errors include:
|
|
|
|
- Any bind index is out of range, a named bind parameter
|
|
does not match, or this statement has no bindable
|
|
parameters.
|
|
|
|
- Any value to bind is of an unsupported type.
|
|
|
|
- Passed no arguments or more than two.
|
|
|
|
- The statement has been finalized.
|
|
*/
|
|
bind: function(/*[ndx,] arg*/){
|
|
affirmStmtOpen(this);
|
|
let ndx, arg;
|
|
switch(arguments.length){
|
|
case 1: ndx = 1; arg = arguments[0]; break;
|
|
case 2: ndx = arguments[0]; arg = arguments[1]; break;
|
|
default: toss("Invalid bind() arguments.");
|
|
}
|
|
if(undefined===arg){
|
|
/* It might seem intuitive to bind undefined as NULL
|
|
but this approach simplifies certain client-side
|
|
uses when passing on arguments between 2+ levels of
|
|
functions. */
|
|
return this;
|
|
}else if(!this.parameterCount){
|
|
toss("This statement has no bindable parameters.");
|
|
}
|
|
this._mayGet = false;
|
|
if(null===arg){
|
|
/* bind NULL */
|
|
return bindOne(this, ndx, BindTypes.null, arg);
|
|
}
|
|
else if(Array.isArray(arg)){
|
|
/* bind each entry by index */
|
|
if(1!==arguments.length){
|
|
toss("When binding an array, an index argument is not permitted.");
|
|
}
|
|
arg.forEach((v,i)=>bindOne(this, i+1, affirmSupportedBindType(v), v));
|
|
return this;
|
|
}
|
|
else if('object'===typeof arg/*null was checked above*/){
|
|
/* bind by name */
|
|
if(1!==arguments.length){
|
|
toss("When binding an object, an index argument is not permitted.");
|
|
}
|
|
Object.keys(arg)
|
|
.forEach(k=>bindOne(this, k,
|
|
affirmSupportedBindType(arg[k]),
|
|
arg[k]));
|
|
return this;
|
|
}else{
|
|
return bindOne(this, ndx,
|
|
affirmSupportedBindType(arg), arg);
|
|
}
|
|
toss("Should not reach this point.");
|
|
},
|
|
/**
|
|
Special case of bind() which binds the given value
|
|
using the BLOB binding mechanism instead of the default
|
|
selected one for the value. The ndx may be a numbered
|
|
or named bind index. The value must be of type string,
|
|
Uint8Array, or null/undefined (both treated as null).
|
|
|
|
If passed a single argument, a bind index of 1 is assumed.
|
|
*/
|
|
bindAsBlob: function(ndx,arg){
|
|
affirmStmtOpen(this);
|
|
if(1===arguments.length){
|
|
ndx = 1;
|
|
arg = arguments[0];
|
|
}
|
|
const t = affirmSupportedBindType(arg);
|
|
if(BindTypes.string !== t && BindTypes.blob !== t
|
|
&& BindTypes.null !== t){
|
|
toss("Invalid value type for bindAsBlob()");
|
|
}
|
|
this._mayGet = false;
|
|
return bindOne(this, ndx, BindTypes.blob, arg);
|
|
},
|
|
/**
|
|
Steps the statement one time. If the result indicates that
|
|
a row of data is available, true is returned. If no row of
|
|
data is available, false is returned. Throws on error.
|
|
*/
|
|
step: function(){
|
|
affirmUnlocked(this, 'step()');
|
|
const rc = api.sqlite3_step(affirmStmtOpen(this)._pStmt);
|
|
switch(rc){
|
|
case api.SQLITE_DONE: return this._mayGet = false;
|
|
case api.SQLITE_ROW: return this._mayGet = true;
|
|
default:
|
|
this._mayGet = false;
|
|
console.warn("sqlite3_step() rc=",rc,"SQL =",
|
|
api.sqlite3_sql(this._pStmt));
|
|
this.db.checkRc(rc);
|
|
};
|
|
},
|
|
/**
|
|
Fetches the value from the given 0-based column index of
|
|
the current data row, throwing if index is out of range.
|
|
|
|
Requires that step() has just returned a truthy value, else
|
|
an exception is thrown.
|
|
|
|
By default it will determine the data type of the result
|
|
automatically. If passed a second arugment, it must be one
|
|
of the enumeration values for sqlite3 types, which are
|
|
defined as members of the sqlite3 module: SQLITE_INTEGER,
|
|
SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB. Any other value,
|
|
except for undefined, will trigger an exception. Passing
|
|
undefined is the same as not passing a value. It is legal
|
|
to, e.g., fetch an integer value as a string, in which case
|
|
sqlite3 will convert the value to a string.
|
|
|
|
If ndx is an array, this function behaves a differently: it
|
|
assigns the indexes of the array, from 0 to the number of
|
|
result columns, to the values of the corresponding column,
|
|
and returns that array.
|
|
|
|
If ndx is a plain object, this function behaves even
|
|
differentlier: it assigns the properties of the object to
|
|
the values of their corresponding result columns.
|
|
|
|
Blobs are returned as Uint8Array instances.
|
|
|
|
Potential TODO: add type ID SQLITE_JSON, which fetches the
|
|
result as a string and passes it (if it's not null) to
|
|
JSON.parse(), returning the result of that. Until then,
|
|
getJSON() can be used for that.
|
|
*/
|
|
get: function(ndx,asType){
|
|
if(!affirmStmtOpen(this)._mayGet){
|
|
toss("Stmt.step() has not (recently) returned true.");
|
|
}
|
|
if(Array.isArray(ndx)){
|
|
let i = 0;
|
|
while(i<this.columnCount){
|
|
ndx[i] = this.get(i++);
|
|
}
|
|
return ndx;
|
|
}else if(ndx && 'object'===typeof ndx){
|
|
let i = 0;
|
|
while(i<this.columnCount){
|
|
ndx[api.sqlite3_column_name(this._pStmt,i)] = this.get(i++);
|
|
}
|
|
return ndx;
|
|
}
|
|
affirmColIndex(this, ndx);
|
|
switch(undefined===asType
|
|
? api.sqlite3_column_type(this._pStmt, ndx)
|
|
: asType){
|
|
case api.SQLITE_NULL: return null;
|
|
case api.SQLITE_INTEGER:{
|
|
return 0 | api.sqlite3_column_double(this._pStmt, ndx);
|
|
/* ^^^^^^^^ strips any fractional part and handles
|
|
handles >32bits */
|
|
}
|
|
case api.SQLITE_FLOAT:
|
|
return api.sqlite3_column_double(this._pStmt, ndx);
|
|
case api.SQLITE_TEXT:
|
|
return api.sqlite3_column_text(this._pStmt, ndx);
|
|
case api.SQLITE_BLOB: {
|
|
const n = api.sqlite3_column_bytes(this._pStmt, ndx);
|
|
const ptr = api.sqlite3_column_blob(this._pStmt, ndx);
|
|
const rc = new Uint8Array(n);
|
|
for(let i = 0; i < n; ++i) rc[i] = HEAP8[ptr + i];
|
|
if(n && this.db._blobXfer instanceof Array){
|
|
/* This is an optimization soley for the
|
|
Worker-based API. These values will be
|
|
transfered to the main thread directly
|
|
instead of being copied. */
|
|
this.db._blobXfer.push(rc.buffer);
|
|
}
|
|
return rc;
|
|
}
|
|
default: toss("Don't know how to translate",
|
|
"type of result column #"+ndx+".");
|
|
}
|
|
abort("Not reached.");
|
|
},
|
|
/** Equivalent to get(ndx) but coerces the result to an
|
|
integer. */
|
|
getInt: function(ndx){return this.get(ndx,api.SQLITE_INTEGER)},
|
|
/** Equivalent to get(ndx) but coerces the result to a
|
|
float. */
|
|
getFloat: function(ndx){return this.get(ndx,api.SQLITE_FLOAT)},
|
|
/** Equivalent to get(ndx) but coerces the result to a
|
|
string. */
|
|
getString: function(ndx){return this.get(ndx,api.SQLITE_TEXT)},
|
|
/** Equivalent to get(ndx) but coerces the result to a
|
|
Uint8Array. */
|
|
getBlob: function(ndx){return this.get(ndx,api.SQLITE_BLOB)},
|
|
/**
|
|
A convenience wrapper around get() which fetches the value
|
|
as a string and then, if it is not null, passes it to
|
|
JSON.parse(), returning that result. Throws if parsing
|
|
fails. If the result is null, null is returned. An empty
|
|
string, on the other hand, will trigger an exception.
|
|
*/
|
|
getJSON: function(ndx){
|
|
const s = this.get(ndx, api.SQLITE_STRING);
|
|
return null===s ? s : JSON.parse(s);
|
|
},
|
|
/**
|
|
Returns the result column name of the given index, or
|
|
throws if index is out of bounds or this statement has been
|
|
finalized. This can be used without having run step()
|
|
first.
|
|
*/
|
|
getColumnName: function(ndx){
|
|
return api.sqlite3_column_name(
|
|
affirmColIndex(affirmStmtOpen(this),ndx)._pStmt, ndx
|
|
);
|
|
},
|
|
/**
|
|
If this statement potentially has result columns, this
|
|
function returns an array of all such names. If passed an
|
|
array, it is used as the target and all names are appended
|
|
to it. Returns the target array. Throws if this statement
|
|
cannot have result columns. This object's columnCount member
|
|
holds the number of columns.
|
|
*/
|
|
getColumnNames: function(tgt){
|
|
affirmColIndex(affirmStmtOpen(this),0);
|
|
if(!tgt) tgt = [];
|
|
for(let i = 0; i < this.columnCount; ++i){
|
|
tgt.push(api.sqlite3_column_name(this._pStmt, i));
|
|
}
|
|
return tgt;
|
|
},
|
|
/**
|
|
If this statement has named bindable parameters and the
|
|
given name matches one, its 1-based bind index is
|
|
returned. If no match is found, 0 is returned. If it has no
|
|
bindable parameters, the undefined value is returned.
|
|
*/
|
|
getParamIndex: function(name){
|
|
return (affirmStmtOpen(this).parameterCount
|
|
? api.sqlite3_bind_parameter_index(this._pStmt, name)
|
|
: undefined);
|
|
}
|
|
}/*Stmt.prototype*/;
|
|
|
|
/** OO binding's namespace. */
|
|
const SQLite3 = {
|
|
version: {
|
|
lib: api.sqlite3_libversion(),
|
|
ooApi: "0.0.1"
|
|
},
|
|
DB,
|
|
Stmt,
|
|
/**
|
|
Reports info about compile-time options. It has several
|
|
distinct uses:
|
|
|
|
If optName is an array then it is expected to be a list of
|
|
compilation options and this function returns an object
|
|
which maps each such option to true or false, indicating
|
|
whether or not the given option was included in this
|
|
build. That object is returned.
|
|
|
|
If optName is an object, its keys are expected to be
|
|
compilation options and this function sets each entry to
|
|
true or false. That object is returned.
|
|
|
|
If passed no arguments then it returns an object mapping
|
|
all known compilation options to their compile-time values,
|
|
or boolean true if they are defined with no value.
|
|
|
|
In all other cases it returns true if the given option was
|
|
active when when compiling the sqlite3 module, else false.
|
|
|
|
Compile-time option names may optionally include their
|
|
"SQLITE_" prefix. When it returns an object of all options,
|
|
the prefix is elided.
|
|
*/
|
|
compileOptionUsed: function f(optName){
|
|
if(!arguments.length){
|
|
if(!f._opt){
|
|
f._rx = /^([^=]+)=(.+)/;
|
|
f._rxInt = /^-?\d+$/;
|
|
f._opt = function(opt, rv){
|
|
const m = f._rx.exec(opt);
|
|
rv[0] = (m ? m[1] : opt);
|
|
rv[1] = m ? (f._rxInt.test(m[2]) ? +m[2] : m[2]) : true;
|
|
};
|
|
}
|
|
const rc = {}, ov = [0,0];
|
|
let i = 0, k;
|
|
while((k = api.sqlite3_compileoption_get(i++))){
|
|
f._opt(k,ov);
|
|
rc[ov[0]] = ov[1];
|
|
}
|
|
return rc;
|
|
}
|
|
else if(Array.isArray(optName)){
|
|
const rc = {};
|
|
optName.forEach((v)=>{
|
|
rc[v] = api.sqlite3_compileoption_used(v);
|
|
});
|
|
return rc;
|
|
}
|
|
else if('object' === typeof optName){
|
|
Object.keys(optName).forEach((k)=> {
|
|
optName[k] = api.sqlite3_compileoption_used(k);
|
|
});
|
|
return optName;
|
|
}
|
|
return (
|
|
'string'===typeof optName
|
|
) ? !!api.sqlite3_compileoption_used(optName) : false;
|
|
}
|
|
}/*SQLite3 object*/;
|
|
|
|
namespace.sqlite3 = {
|
|
api: api,
|
|
SQLite3
|
|
};
|
|
|
|
if(self === self.window){
|
|
/* This is running in the main window thread, so we're done. */
|
|
setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
|
|
return;
|
|
}
|
|
/******************************************************************
|
|
End of main window thread. What follows is only intended for use
|
|
in Worker threads.
|
|
******************************************************************/
|
|
|
|
/*
|
|
UNDER CONSTRUCTION
|
|
|
|
We need an API which can proxy the DB API via a Worker message
|
|
interface. The primary quirky factor in such an API is that we
|
|
cannot pass callback functions between the window thread and a
|
|
worker thread, so we have to receive all db results via
|
|
asynchronous message-passing.
|
|
|
|
Certain important considerations here include:
|
|
|
|
- Support only one db connection or multiple? The former is far
|
|
easier, but there's always going to be a user out there who
|
|
wants to juggle six database handles at once. Do we add that
|
|
complexity or tell such users to write their own code using
|
|
the provided lower-level APIs?
|
|
|
|
- Fetching multiple results: do we pass them on as a series of
|
|
messages, with start/end messages on either end, or do we
|
|
collect all results and bundle them back in a single message?
|
|
The former is, generically speaking, more memory-efficient but
|
|
the latter far easier to implement in this environment. The
|
|
latter is untennable for large data sets. Despite a web page
|
|
hypothetically being a relatively limited environment, there
|
|
will always be those users who feel that they should/need to
|
|
be able to work with multi-hundred-meg (or larger) blobs, and
|
|
passing around arrays of those may quickly exhaust the JS
|
|
engine's memory.
|
|
|
|
TODOs include, but are not limited to:
|
|
|
|
- The ability to manage multiple DB handles. This can
|
|
potentially be done via a simple mapping of DB.filename or
|
|
DB._pDb (`sqlite3*` handle) to DB objects. The open()
|
|
interface would need to provide an ID (probably DB._pDb) back
|
|
to the user which can optionally be passed as an argument to
|
|
the other APIs (they'd default to the first-opened DB, for
|
|
ease of use). Client-side usability of this feature would
|
|
benefit from making another wrapper class (or a singleton)
|
|
available to the main thread, with that object proxying all(?)
|
|
communication with the worker.
|
|
|
|
- Revisit how virtual files are managed. We currently delete DBs
|
|
from the virtual filesystem when we close them, for the sake
|
|
of saving memory (the VFS lives in RAM). Supporting multiple
|
|
DBs may require that we give up that habit. Similarly, fully
|
|
supporting ATTACH, where a user can upload multiple DBs and
|
|
ATTACH them, also requires the that we manage the VFS entries
|
|
better. As of this writing, ATTACH will fail fatally in the
|
|
fiddle app (but not the lower-level APIs) because it runs in
|
|
safe mode, where ATTACH is disabled.
|
|
*/
|
|
|
|
/**
|
|
Helper for managing Worker-level state.
|
|
*/
|
|
const wState = {
|
|
db: undefined,
|
|
open: function(arg){
|
|
if(!arg && this.db) return this.db;
|
|
else if(this.db) this.db.close();
|
|
return this.db = (Array.isArray(arg) ? new DB(...arg) : new DB(arg));
|
|
},
|
|
close: function(alsoUnlink){
|
|
if(this.db){
|
|
this.db.close(alsoUnlink);
|
|
this.db = undefined;
|
|
}
|
|
},
|
|
affirmOpen: function(){
|
|
return this.db || toss("DB is not opened.");
|
|
},
|
|
post: function(type,data,xferList){
|
|
if(xferList){
|
|
self.postMessage({type, data},xferList);
|
|
xferList.length = 0;
|
|
}else{
|
|
self.postMessage({type, data});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
A level of "organizational abstraction" for the Worker
|
|
API. Each method in this object must map directly to a Worker
|
|
message type key. The onmessage() dispatcher attempts to
|
|
dispatch all inbound messages to a method of this object,
|
|
passing it the event.data part of the inbound event object. All
|
|
methods must return a plain Object containing any response
|
|
state, which the dispatcher may amend. All methods must throw
|
|
on error.
|
|
*/
|
|
const wMsgHandler = {
|
|
xfer: [/*Temp holder for "transferable" postMessage() state.*/],
|
|
/**
|
|
Proxy for DB.exec() which expects a single argument of type
|
|
string (SQL to execute) or an options object in the form
|
|
expected by exec(). The notable differences from exec()
|
|
include:
|
|
|
|
- The default value for options.rowMode is 'array' because
|
|
the normal default cannot cross the window/Worker boundary.
|
|
|
|
- A function-type options.callback property cannot cross
|
|
the window/Worker boundary, so is not useful here. If
|
|
options.callback is a string then it is assumed to be a
|
|
message type key, in which case a callback function will be
|
|
applied which posts each row result via:
|
|
|
|
postMessage({type: thatKeyType, data: theRow})
|
|
|
|
And, at the end of the result set (whether or not any
|
|
result rows were produced), it will post an identical
|
|
message with data:null to alert the caller than the result
|
|
set is completed.
|
|
|
|
The callback proxy must not recurse into this interface, or
|
|
results are undefined. (It hypothetically cannot recurse
|
|
because an exec() call will be tying up the Worker thread,
|
|
causing any recursion attempt to wait until the first
|
|
exec() is completed.)
|
|
|
|
The response is the input options object (or a synthesized
|
|
one if passed only a string), noting that
|
|
options.resultRows and options.columnNames may be populated
|
|
by the call to exec().
|
|
|
|
This opens/creates the Worker's db if needed.
|
|
*/
|
|
exec: function(ev){
|
|
const opt = (
|
|
'string'===typeof ev.data
|
|
) ? {sql: ev.data} : (ev.data || {});
|
|
if(!opt.rowMode){
|
|
/* Since the default rowMode of 'stmt' is not useful
|
|
for the Worker interface, we'll default to
|
|
something else. */
|
|
opt.rowMode = 'array';
|
|
}else if('stmt'===opt.rowMode){
|
|
toss("Invalid rowMode for exec(): stmt mode",
|
|
"does not work in the Worker API.");
|
|
}
|
|
const db = wState.open();
|
|
if(opt.callback || opt.resultRows instanceof Array){
|
|
// Part of a copy-avoidance optimization for blobs
|
|
db._blobXfer = this.xfer;
|
|
}
|
|
const callbackMsgType = opt.callback;
|
|
if('string' === typeof callbackMsgType){
|
|
const that = this;
|
|
opt.callback =
|
|
(row)=>wState.post(callbackMsgType,row,this.xfer);
|
|
}
|
|
try {
|
|
db.exec(opt);
|
|
if(opt.callback instanceof Function){
|
|
opt.callback = callbackMsgType;
|
|
wState.post(callbackMsgType, null);
|
|
}
|
|
}finally{
|
|
delete db._blobXfer;
|
|
if('string'===typeof callbackMsgType){
|
|
opt.callback = callbackMsgType;
|
|
}
|
|
}
|
|
return opt;
|
|
}/*exec()*/,
|
|
/**
|
|
Proxy for DB.exportBinaryImage(). Throws if the db has not
|
|
been opened. Response is an object:
|
|
|
|
{
|
|
buffer: Uint8Array (db file contents),
|
|
filename: the current db filename,
|
|
mimetype: string
|
|
}
|
|
*/
|
|
export: function(ev){
|
|
const db = wState.affirmOpen();
|
|
const response = {
|
|
buffer: db.exportBinaryImage(),
|
|
filename: db.filename,
|
|
mimetype: 'application/x-sqlite3'
|
|
};
|
|
this.xfer.push(response.buffer.buffer);
|
|
return response;
|
|
}/*export()*/,
|
|
/**
|
|
Proxy for the DB constructor. Expects to be passed a single
|
|
object or a falsy value to use defaults. The object may
|
|
have a filename property to name the db file (see the DB
|
|
constructor for peculiarities and transformations) and/or a
|
|
buffer property (a Uint8Array holding a complete database
|
|
file's contents). The response is an object:
|
|
|
|
{
|
|
filename: db filename (possibly differing from the input)
|
|
}
|
|
|
|
If the Worker's db is currently opened, this call closes it
|
|
before proceeding.
|
|
*/
|
|
open: function(ev){
|
|
wState.close(/*true???*/);
|
|
const args = [], data = (ev.data || {});
|
|
if(data.filename) args.push(data.filename);
|
|
if(data.buffer){
|
|
args.push(data.buffer);
|
|
this.xfer.push(data.buffer.buffer);
|
|
}
|
|
const db = wState.open(args);
|
|
return {filename: db.filename};
|
|
},
|
|
/**
|
|
Proxy for DB.close(). If ev.data may either be a boolean or
|
|
an object with an `unlink` property. If that value is
|
|
truthy then the db file (if the db is currently open) will
|
|
be unlinked from the virtual filesystem, else it will be
|
|
kept intact. The response object is:
|
|
|
|
{filename: db filename _if_ the db is is opened when this
|
|
is called, else the undefined value
|
|
}
|
|
*/
|
|
close: function(ev){
|
|
const response = {
|
|
filename: wState.db && wState.db.filename
|
|
};
|
|
if(wState.db){
|
|
wState.close(!!(ev.data && 'object'===typeof ev.data)
|
|
? ev.data.unlink : ev.data);
|
|
}
|
|
return response;
|
|
}
|
|
}/*wMsgHandler*/;
|
|
|
|
/**
|
|
UNDER CONSTRUCTION!
|
|
|
|
A subset of the DB API is accessible via Worker messages in the form:
|
|
|
|
{ type: apiCommand,
|
|
data: apiArguments }
|
|
|
|
As a rule, these commands respond with a postMessage() of their
|
|
own in the same form, but will, if needed, transform the `data`
|
|
member to an object and may add state to it. The responses
|
|
always have an object-format `data` part. If the inbound `data`
|
|
is an object which has a `messageId` property, that property is
|
|
always mirrored in the result object, for use in client-side
|
|
dispatching of these asynchronous results. Exceptions thrown
|
|
during processing result in an `error`-type event with a
|
|
payload in the form:
|
|
|
|
{
|
|
message: error string,
|
|
errorClass: class name of the error type,
|
|
input: ev.data,
|
|
[messageId: if set in the inbound message]
|
|
}
|
|
|
|
The individual APIs are documented in the wMsgHandler object.
|
|
*/
|
|
self.onmessage = function(ev){
|
|
ev = ev.data;
|
|
let response, evType = ev.type;
|
|
try {
|
|
if(wMsgHandler.hasOwnProperty(evType) &&
|
|
wMsgHandler[evType] instanceof Function){
|
|
response = wMsgHandler[evType](ev);
|
|
}else{
|
|
toss("Unknown db worker message type:",ev.type);
|
|
}
|
|
}catch(err){
|
|
evType = 'error';
|
|
response = {
|
|
message: err.message,
|
|
errorClass: err.name,
|
|
input: ev
|
|
};
|
|
}
|
|
if(!response.messageId && ev.data
|
|
&& 'object'===typeof ev.data && ev.data.messageId){
|
|
response.messageId = ev.data.messageId;
|
|
}
|
|
wState.post(evType, response, wMsgHandler.xfer);
|
|
};
|
|
|
|
setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
|
|
});
|