aboutsummaryrefslogtreecommitdiff
path: root/ext/fiddle/sqlite3-api.js
diff options
context:
space:
mode:
authorstephan <stephan@noemail.net>2022-06-25 02:37:57 +0000
committerstephan <stephan@noemail.net>2022-06-25 02:37:57 +0000
commitd45ac3947de2d3b5c3e84667bac22a044c72fa64 (patch)
treeec3a2caea702b9d29d53905076bce1fc6ef5a115 /ext/fiddle/sqlite3-api.js
parent86bc5e41bbe5247b74c41b629257e83618d4a0dc (diff)
downloadsqlite-d45ac3947de2d3b5c3e84667bac22a044c72fa64.tar.gz
sqlite-d45ac3947de2d3b5c3e84667bac22a044c72fa64.zip
wasm binding: consolidated the two sqlite3_prepare_v2() bindings behind a single dispathcer. Various internal cleanups and refactoring. Branched because trunk is in pencils-down mode for pending 3.39 release.
FossilOrigin-Name: ab3e50dab4d71557ab5d179bbd6caf7fb61ab7c51dffc8e4714441c189ce3e5c
Diffstat (limited to 'ext/fiddle/sqlite3-api.js')
-rw-r--r--ext/fiddle/sqlite3-api.js239
1 files changed, 184 insertions, 55 deletions
diff --git a/ext/fiddle/sqlite3-api.js b/ext/fiddle/sqlite3-api.js
index 5057eb53f..3ee57f681 100644
--- a/ext/fiddle/sqlite3-api.js
+++ b/ext/fiddle/sqlite3-api.js
@@ -166,9 +166,29 @@ Module.postRun.push(function(namespace/*the module object, the target for
SQLITE_INNOCUOUS: 0x000200000,
/* sqlite encodings, used for creating UDFs, noting that we
will only support UTF8. */
- SQLITE_UTF8: 1
+ SQLITE_UTF8: 1,
+ /* Values for the final argument of sqlite3_result_blob(),
+ noting that these are interpreted in WASM as pointer
+ values. */
+ SQLITE_TRANSIENT: -1,
+ SQLITE_STATIC: 0,
+
+ /**
+ Holds state which are specific to the WASM-related
+ infrastructure and glue code. It is not expected that client
+ code will normally need these, but they're exposed here in case it
+ does.
+ */
+ wasm: {
+ /**
+ Proxy for SQM.allocate(). TODO: remove deprecated allocate(),
+ use _malloc(). The kicker is that allocate() uses
+ module-init-internal state which isn't normally visible to
+ us.
+ */
+ allocate: (slab, allocator=SQM.ALLOC_NORMAL)=>SQM.allocate(slab, allocator)
+ }
};
- const cwrap = SQM.cwrap;
[/* C-side functions to bind. Each entry is an array with 3 or 4
elements:
@@ -219,14 +239,8 @@ Module.postRun.push(function(namespace/*the module object, the target for
["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_prepare_v2() is handled separately due to us requiring two
+ different sets of semantics for that function. */
["sqlite3_reset", "number", ["number"]],
["sqlite3_result_blob",null,["number", "number", "number", "number"]],
["sqlite3_result_double",null,["number", "number"]],
@@ -245,7 +259,104 @@ Module.postRun.push(function(namespace/*the module object, the target for
//["sqlite3_normalized_sql", "string", ["number"]]
].forEach(function(a){
const k = (4==a.length) ? a.shift() : a[0];
- api[k] = cwrap.apply(this, a);
+ api[k] = SQM.cwrap.apply(this, a);
+ });
+
+ /**
+ Proxies for variants of sqlite3_prepare_v2() which have
+ differing JS/WASM binding semantics.
+ */
+ const prepareMethods = {
+ /**
+ This binding expects a JS string as its 2nd argument and
+ null as its final argument. In order to compile multiple
+ statements from a single string, the "full" impl (see
+ below) must be used.
+ */
+ basic: SQM.cwrap('sqlite3_prepare_v2',
+ "number", ["number", "string", "number"/*MUST always be negative*/,
+ "number", "number"/*MUST be 0 or null or undefined!*/]),
+ /* Impl which requires that the 2nd argument be a pointer to
+ the SQL string, instead of being converted to a
+ string. This variant is necessary for cases where we
+ require a non-NULL value for the final argument
+ (exec()'ing multiple statements from one input
+ string). For simpler cases, where only the first statement
+ in the SQL string is required, the wrapper named
+ sqlite3_prepare_v2() is sufficient and easier to use
+ because it doesn't require dealing with pointers.
+
+ TODO: hide both of these methods behind a single hand-written
+ sqlite3_prepare_v2() wrapper which dispatches to the appropriate impl.
+ */
+ full: SQM.cwrap('sqlite3_prepare_v2',
+ "number", ["number", "number", "number"/*MUST always be negative*/,
+ "number", "number"]),
+ };
+
+ const uint8ToString = (str)=>new TextDecoder('utf-8').decode(str);
+ //const stringToUint8 = (sql)=>new TextEncoder('utf-8').encode(sql);
+
+ /**
+ sqlite3_prepare_v2() binding which handles two different uses
+ with differing JS/WASM semantics:
+
+ 1) sqlite3_prepare_v2(pDb, sqlString, -1, ppStmt [, null])
+
+ 2) sqlite3_prepare_v2(pDb, sqlPointer, -1, ppStmt, sqlPointerToPointer)
+
+ Note that the SQL length argument (the 3rd argument) must
+ always be negative because it must be a byte length and that
+ value is expensive to calculate from JS (where we get the
+ character length of strings). It is retained in this API's
+ interface for code/documentation compatibility reasons but is
+ currently _always_ ignored. When using the 2nd form of this
+ call, it is critical that the custom-allocated string be
+ terminated with a 0 byte. (Potential TODO: if this value is >0,
+ assume the caller knows precisely what they're doing and pass
+ it on as-is. That approach currently seems fraught with peril.)
+
+ In usage (1), the 2nd argument must be of type string or
+ Uint8Array (which is assumed to hold SQL). If it is, this
+ function assumes case (1) and calls the underling C function
+ with:
+
+ (pDb, sql, -1, ppStmt, null)
+
+ If sql is not a string or Uint8Array, it must be a _pointer_ to
+ a string which was allocated via api.wasm.allocateUTF8OnStack()
+ or equivalent (TODO: define "or equivalent"). In that case, the
+ final argument may be 0/null/undefined or must be a pointer to
+ which the "tail" of the compiled SQL is written, as documented
+ for the C-side sqlite3_prepare_v2(). In case (2), the
+ underlying C function is called with:
+
+ (pDb, sql, -1, ppStmt, pzTail)
+
+ It returns its result and compiled statement as documented in
+ the C API. Fetching the output pointer (4th argument) requires using
+ api.wasm.getValue().
+ */
+ api.sqlite3_prepare_v2 = function(pDb, sql, sqlLen, ppStmt, pzTail){
+ if(sql instanceof Uint8Array) sql = uint8ToString(sql);
+ /* ^^^ TODO: confirm whether this conversion is really
+ necessary or whether passing on the array as-is will work
+ as if it were a string. */
+ switch(typeof sql){
+ case 'string': return prepareMethods.basic(pDb, sql, -1, ppStmt, null);
+ case 'number': return prepareMethods.full(pDb, sql, -1, ppStmt, pzTail);
+ default: toss("Invalid SQL argument type for sqlite3_prepare_v2().");
+ }
+ };
+
+ /** Populate api.wasm... */
+ ['getValue','setValue', 'stackSave', 'stackRestore', 'stackAlloc',
+ 'allocateUTF8OnStack', '_malloc', '_free',
+ 'addFunction', 'removeFunction'
+ ].forEach(function(m){
+ if(undefined === (api.wasm[m] = SQM[m])){
+ toss("Internal init error: Module."+m+" not found.");
+ }
});
/* What follows is colloquially known as "OO API #1". It is a
@@ -255,8 +366,6 @@ Module.postRun.push(function(namespace/*the module object, the target for
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(){
@@ -322,9 +431,12 @@ Module.postRun.push(function(namespace/*the module object, the target for
}
FS.createDataFile("/", fn, buffer, true, true);
}
- setValue(pPtrArg, 0, "i32");
- this.checkRc(api.sqlite3_open(fn, pPtrArg));
- this._pDb = getValue(pPtrArg, "i32");
+ const stack = api.wasm.stackSave();
+ const ppDb = api.wasm.stackAlloc(4) /* output (sqlite3**) arg */;
+ api.wasm.setValue(ppDb, 0, "i32");
+ try {this.checkRc(api.sqlite3_open(fn, ppDb));}
+ finally{api.wasm.stackRestore(stack);}
+ this._pDb = api.wasm.getValue(ppDb, "i32");
this.filename = fn;
this._statements = {/*map of open Stmt _pointers_ to Stmt*/};
this._udfs = {/*map of UDF names to wasm function _pointers_*/};
@@ -378,7 +490,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
DB.execMulti(). Does the argument processing/validation, throws
on error, and returns a new object on success:
- { sql: the SQL, obt: optionsObj, cbArg: function}
+ { sql: the SQL, opt: 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
@@ -386,12 +498,13 @@ Module.postRun.push(function(namespace/*the module object, the target for
the input arguments.
*/
const parseExecArgs = function(args){
- const out = {};
+ const out = {opt:{}};
switch(args.length){
case 1:
if('string'===typeof args[0]){
out.sql = args[0];
- out.opt = {};
+ }else if(args[0] instanceof Uint8Array){
+ out.sql = args[0];
}else if(args[0] && 'object'===typeof args[0]){
out.opt = args[0];
out.sql = out.opt.sql;
@@ -403,7 +516,11 @@ Module.postRun.push(function(namespace/*the module object, the target for
break;
default: toss("Invalid argument count for exec().");
};
- if('string'!==typeof out.sql) toss("Missing SQL argument.");
+ if(out.sql instanceof Uint8Array){
+ out.sql = uint8ToString(out.sql);
+ }else 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) {
@@ -453,7 +570,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
delete that._statements[k];
if(s && s._pStmt) s.finalize();
});
- Object.values(this._udfs).forEach(SQM.removeFunction);
+ Object.values(this._udfs).forEach(api.wasm.removeFunction);
delete this._udfs;
delete this._statements;
api.sqlite3_close_v2(this._pDb);
@@ -481,13 +598,21 @@ Module.postRun.push(function(namespace/*the module object, the target for
/**
Compiles the given SQL and returns a prepared Stmt. This is
the only way to create new Stmt objects. Throws on error.
+
+ The given SQL must be a string, a Uint8Array holding SQL,
+ or a WASM pointer to memory allocated using
+ api.wasm.allocateUTF8OnStack() (or equivalent (a term which
+ is yet to be defined precisely)).
*/
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 stack = api.wasm.stackSave();
+ const ppStmt = api.wasm.stackAlloc(4)/* output (sqlite3_stmt**) arg */;
+ api.wasm.setValue(ppStmt, 0, "i32");
+ try {this.checkRc(api.sqlite3_prepare_v2(this._pDb, sql, -1, ppStmt, null));}
+ finally {api.wasm.stackRestore(stack);}
+ const pStmt = api.wasm.getValue(ppStmt, "i32");
+ if(!pStmt) toss("Cannot prepare empty SQL.");
const stmt = new Stmt(this, pStmt, BindTypes);
this._statements[pStmt] = stmt;
return stmt;
@@ -585,7 +710,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
properties:
- .sql = the SQL to run (unless it's provided as the first
- argument).
+ argument). This must be of type string or Uint8Array.
- .bind = a single value valid as an argument for
Stmt.bind(). This is ONLY applied to the FIRST non-empty
@@ -638,23 +763,27 @@ Module.postRun.push(function(namespace/*the module object, the target for
? arguments[0] : parseExecArgs(arguments));
if(!arg.sql) return this;
const opt = arg.opt;
- const stack = stackSave();
+ const stack = api.wasm.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 sql = (arg.sql instanceof Uint8Array)
+ ? uint8ToString(arg.sql)
+ : arg.sql;
+ let pSql = api.wasm.allocateUTF8OnStack(sql)
+ const ppStmt = api.wasm.stackAlloc(8) /* output (sqlite3_stmt**) arg */;
+ const pzTail = ppStmt + 4 /* final arg to sqlite3_prepare_v2_sqlptr() */;
+ while(api.wasm.getValue(pSql, "i8")){
+ api.wasm.setValue(ppStmt, 0, "i32");
+ api.wasm.setValue(pzTail, 0, "i32");
+ this.checkRc(api.sqlite3_prepare_v2(
+ this._pDb, pSql, -1, ppStmt, pzTail
));
- const pStmt = getValue(pPtrArg, "i32");
- pSql = getValue(pzTail, "i32");
+ const pStmt = api.wasm.getValue(ppStmt, "i32");
+ pSql = api.wasm.getValue(pzTail, "i32");
if(!pStmt) continue;
if(opt.saveSql){
opt.saveSql.push(api.sqlite3_sql(pStmt).trim());
@@ -683,7 +812,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
delete stmt._isLocked;
stmt.finalize();
}
- stackRestore(stack);
+ api.wasm.stackRestore(stack);
}
return this;
}/*execMulti()*/,
@@ -736,7 +865,6 @@ Module.postRun.push(function(namespace/*the module object, the target for
- .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`
@@ -769,7 +897,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
let i, pVal, valType, arg;
const tgt = [];
for(i = 0; i < argc; ++i){
- pVal = getValue(pArgv + (4 * i), "i32");
+ pVal = api.wasm.getValue(pArgv + (4 * i), "i32");
valType = api.sqlite3_value_type(pVal);
switch(valType){
case api.SQLITE_INTEGER:
@@ -806,8 +934,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
break;
}
case 'string':
- api.sqlite3_result_text(pCx, val, -1,
- -1/*==SQLITE_TRANSIENT*/);
+ api.sqlite3_result_text(pCx, val, -1, api.SQLITE_TRANSIENT);
break;
case 'object':
if(null===val) {
@@ -815,9 +942,10 @@ Module.postRun.push(function(namespace/*the module object, the target for
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);
+ api.wasm.allocate(val);
+ api.sqlite3_result_blob(pCx, pBlob, val.length,
+ api.SQLITE_TRANSIENT);
+ api.wasm._free(blobptr);
break;
}
// else fall through
@@ -833,7 +961,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
api.sqlite3_result_error(pCx, e.message, -1);
}
};
- const pUdf = SQM.addFunction(wrapper, "viii");
+ const pUdf = api.wasm.addFunction(wrapper, "viii");
let fFlags = 0;
if(getOwnOption(opt, 'deterministic')) fFlags |= api.SQLITE_DETERMINISTIC;
if(getOwnOption(opt, 'directOnly')) fFlags |= api.SQLITE_DIRECTONLY;
@@ -846,11 +974,11 @@ Module.postRun.push(function(namespace/*the module object, the target for
api.SQLITE_UTF8 | fFlags, null/*pApp*/, pUdf,
null/*xStep*/, null/*xFinal*/, null/*xDestroy*/));
}catch(e){
- SQM.removeFunction(pUdf);
+ api.wasm.removeFunction(pUdf);
throw e;
}
if(this._udfs.hasOwnProperty(name)){
- SQM.removeFunction(this._udfs[name]);
+ api.wasm.removeFunction(this._udfs[name]);
}
this._udfs[name] = pUdf;
return this;
@@ -999,7 +1127,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
f._ = {
string: function(stmt, ndx, val, asBlob){
const bytes = intArrayFromString(val,true);
- const pStr = SQM.allocate(bytes, ALLOC_NORMAL);
+ const pStr = api.wasm.allocate(bytes);
stmt._allocs.push(pStr);
const func = asBlob ? api.sqlite3_bind_blob : api.sqlite3_bind_text;
return func(stmt._pStmt, ndx, pStr, bytes.length, 0);
@@ -1038,7 +1166,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
toss("Binding a value as a blob requires",
"that it have a length member.");
}
- const pBlob = SQM.allocate(val, ALLOC_NORMAL);
+ const pBlob = api.wasm.allocate(val);
stmt._allocs.push(pBlob);
rc = api.sqlite3_bind_blob(stmt._pStmt, ndx, pBlob, len, 0);
}
@@ -1054,7 +1182,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
const freeBindMemory = function(stmt){
let m;
while(undefined !== (m = stmt._allocs.pop())){
- SQM._free(m);
+ api.wasm._free(m);
}
return stmt;
};
@@ -1481,7 +1609,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
if(self === self.window){
/* This is running in the main window thread, so we're done. */
- setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
+ postMessage({type:'sqlite3-api',data:'loaded'});
return;
}
/******************************************************************
@@ -1489,14 +1617,15 @@ Module.postRun.push(function(namespace/*the module object, the target for
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.
+ asynchronous message-passing. That requires an asychronous API
+ with a distinctly different shape that the main OO API.
Certain important considerations here include:
@@ -1777,5 +1906,5 @@ Module.postRun.push(function(namespace/*the module object, the target for
wState.post(evType, response, wMsgHandler.xfer);
};
- setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
-});
+ postMessage({type:'sqlite3-api',data:'loaded'});
+})/*postRun.push(...)*/;