diff --git a/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api index aead79e50d..f03478b17c 100644 --- a/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api +++ b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api @@ -25,6 +25,7 @@ _sqlite3_compileoption_used _sqlite3_create_function_v2 _sqlite3_data_count _sqlite3_db_filename +_sqlite3_db_handle _sqlite3_db_name _sqlite3_errmsg _sqlite3_error_offset @@ -33,6 +34,7 @@ _sqlite3_exec _sqlite3_expanded_sql _sqlite3_extended_errcode _sqlite3_extended_result_codes +_sqlite3_file_control _sqlite3_finalize _sqlite3_initialize _sqlite3_interrupt diff --git a/ext/wasm/api/sqlite3-api-cleanup.js b/ext/wasm/api/sqlite3-api-cleanup.js index 1b57cdc5de..0e99edf508 100644 --- a/ext/wasm/api/sqlite3-api-cleanup.js +++ b/ext/wasm/api/sqlite3-api-cleanup.js @@ -20,11 +20,17 @@ if('undefined' !== typeof Module){ // presumably an Emscripten build /** Install a suitable default configuration for sqlite3ApiBootstrap(). */ - const SABC = self.sqlite3ApiBootstrap.defaultConfig; - SABC.Module = Module /* ==> Currently needs to be exposed here for test code. NOT part - of the public API. */; - SABC.exports = Module['asm']; - SABC.memory = Module.wasmMemory /* gets set if built with -sIMPORT_MEMORY */; + const SABC = self.sqlite3ApiConfig || Object.create(null); + if(undefined===SABC.Module){ + SABC.Module = Module /* ==> Currently needs to be exposed here for + test code. NOT part of the public API. */; + } + if(undefined===SABC.exports){ + SABC.exports = Module['asm']; + } + if(undefined===SABC.memory){ + SABC.memory = Module.wasmMemory /* gets set if built with -sIMPORT_MEMORY */; + } /** For current (2022-08-22) purposes, automatically call @@ -35,8 +41,15 @@ if('undefined' !== typeof Module){ // presumably an Emscripten build configuration used by a no-args call to sqlite3ApiBootstrap(). */ //console.warn("self.sqlite3ApiConfig = ",self.sqlite3ApiConfig); - const sqlite3 = self.sqlite3ApiBootstrap(); - delete self.sqlite3ApiBootstrap; + const rmApiConfig = (SABC !== self.sqlite3ApiConfig); + self.sqlite3ApiConfig = SABC; + let sqlite3; + try{ + sqlite3 = self.sqlite3ApiBootstrap(); + }finally{ + delete self.sqlite3ApiBootstrap; + if(rmApiConfig) delete self.sqlite3ApiConfig; + } if(self.location && +self.location.port > 1024){ console.warn("Installing sqlite3 bits as global S for local dev/test purposes."); diff --git a/ext/wasm/api/sqlite3-api-glue.js b/ext/wasm/api/sqlite3-api-glue.js index 3a9e8803cb..67f9403548 100644 --- a/ext/wasm/api/sqlite3-api-glue.js +++ b/ext/wasm/api/sqlite3-api-glue.js @@ -55,6 +55,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ */ const aPtr = wasm.xWrap.argAdapter('*'); wasm.xWrap.argAdapter('sqlite3*', aPtr)('sqlite3_stmt*', aPtr); + wasm.xWrap.resultAdapter('sqlite3*', aPtr)('sqlite3_stmt*', aPtr); /** Populate api object with sqlite3_...() by binding the "raw" wasm @@ -174,7 +175,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ wasm.ctype = JSON.parse(wasm.cstringToJs(cJson)); //console.debug('wasm.ctype length =',wasm.cstrlen(cJson)); for(const t of ['access', 'blobFinalizers', 'dataTypes', - 'encodings', 'flock', 'ioCap', + 'encodings', 'fcntl', 'flock', 'ioCap', 'openFlags', 'prepareFlags', 'resultCodes', 'syncFlags', 'udfFlags', 'version' ]){ diff --git a/ext/wasm/api/sqlite3-api-oo1.js b/ext/wasm/api/sqlite3-api-oo1.js index 3dfe5bfb05..8280204d62 100644 --- a/ext/wasm/api/sqlite3-api-oo1.js +++ b/ext/wasm/api/sqlite3-api-oo1.js @@ -496,6 +496,13 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ the statement actually produces any result rows. ================================================================== + - `.columnNames`: if this is an array, the column names of the + result set are stored in this array before the callback (if + any) is triggered (regardless of whether the query produces any + result rows). If no statement has result columns, this value is + unchanged. Achtung: an SQL result may have multiple columns + with identical names. + - `.callback` = a function which gets called for each row of the result set, but only if that statement has any result _rows_. The callback's "this" is the options object. The second @@ -523,8 +530,8 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ as the first argument to the callback: A.1) `'array'` (the default) causes the results of - `stmt.get([])` to be passed to passed on and/or appended to - `resultRows`. + `stmt.get([])` to be passed to the `callback` and/or appended + to `resultRows`. A.2) `'object'` causes the results of `stmt.get(Object.create(null))` to be passed to the @@ -536,7 +543,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ A.3) `'stmt'` causes the current Stmt to be passed to the callback, but this mode will trigger an exception if `resultRows` is an array because appending the statement to - the array would be unhelpful. + the array would be downright unhelpful. B) An integer, indicating a zero-based column in the result row. Only that one single value will be passed on. @@ -545,10 +552,14 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ ':', '$', or '@' will fetch the row as an object, extract that one field, and pass that field's value to the callback. Note that these keys are case-sensitive so must match the case used - in the SQL. e.g. `"select a A from t"` with a `rowMode` of '$A' - would work but '$a' would not. A reference to a column not in - the result set will trigger an exception on the first row (as - the check is not performed until rows are fetched). + in the SQL. e.g. `"select a A from t"` with a `rowMode` of + `'$A'` would work but `'$a'` would not. A reference to a column + not in the result set will trigger an exception on the first + row (as the check is not performed until rows are fetched). + Note also that `$` is a legal identifier character in JS so + need not be quoted. (Design note: those 3 characters were + chosen because they are the characters support for naming bound + parameters.) Any other `rowMode` value triggers an exception. @@ -560,12 +571,17 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ and can be used over a WebWorker-style message interface. exec() throws if `resultRows` is set and `rowMode` is 'stmt'. - - `.columnNames`: if this is an array, the column names of the - result set are stored in this array before the callback (if - any) is triggered (regardless of whether the query produces any - result rows). If no statement has result columns, this value is - unchanged. Achtung: an SQL result may have multiple columns - with identical names. + + Potential TODOs: + + - `.bind`: permit an array of arrays/objects to bind. The first + sub-array would act on the first statement which has bindable + parameters (as it does now). The 2nd would act on the next such + statement, etc. + + - `.callback` and `.resultRows`: permit an array entries with + semantics similar to those described for `.bind` above. + */ exec: function(/*(sql [,obj]) || (obj)*/){ affirmDbOpen(this); @@ -1580,31 +1596,5 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ Stmt }/*oo1 object*/; - if( self.window===self && 0!==capi.sqlite3_vfs_find('kvvfs') ){ - /* Features specific to kvvfs... */ - /** - Clears all storage used by the kvvfs DB backend, deleting any - DB(s) stored there. Its argument must be either 'session', - 'local', or ''. In the first two cases, only sessionStorage - resp. localStorage is cleared. If it's an empty string (the - default) then both are cleared. Only storage keys which match - the pattern used by kvvfs are cleared: any other client-side - data are retained. - */ - DB.clearKvvfsStorage = function(which=''){ - const prefix = 'kvvfs-'+which; - const stores = []; - if('session'===which || ''===which) stores.push(sessionStorage); - if('local'===which || ''===which) stores.push(localStorage); - stores.forEach(function(s){ - const toRm = []; - let i = 0, k; - for( i = 0; (k = s.key(i)); ++i ){ - if(k.startsWith(prefix)) toRm.push(k); - } - toRm.forEach((kk)=>s.removeItem(kk)); - }); - }; - }/* main-window-only bits */ }); diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index 17dcd42289..1c22e9ea21 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -43,10 +43,7 @@ - Insofar as possible, support client-side storage using JS filesystem APIs. As of this writing, such things are still very - much TODO. Initial testing with using IndexedDB as backing storage - showed it to work reasonably well, but it's also too easy to - corrupt by using a web page in two browser tabs because IndexedDB - lacks the locking features needed to support that. + much under development. Specific non-goals of this project: @@ -54,8 +51,10 @@ Encodings in that realm, there are no currently plans to support the UTF16-related sqlite3 APIs. They would add a complication to the bindings for no appreciable benefit. Though web-related - implementation details take priority, the lower-level WASM module - "should" work in non-web WASM environments. + implementation details take priority, and the JavaScript + components of the API specifically focus on browser clients, the + lower-level WASM module "should" work in non-web WASM + environments. - Supporting old or niche-market platforms. WASM is built for a modern web and requires modern platforms. @@ -78,17 +77,18 @@ */ /** - sqlite3ApiBootstrap() is the only global symbol exposed by this - API. It is intended to be called one time at the end of the API - amalgamation process, passed configuration details for the current - environment, and then optionally be removed from the global object - using `delete self.sqlite3ApiBootstrap`. + sqlite3ApiBootstrap() is the only global symbol persistently + exposed by this API. It is intended to be called one time at the + end of the API amalgamation process, passed configuration details + for the current environment, and then optionally be removed from + the global object using `delete self.sqlite3ApiBootstrap`. This function expects a configuration object, intended to abstract away details specific to any given WASM environment, primarily so that it can be used without any _direct_ dependency on - Emscripten. The config object is only honored the first time this - is called. Subsequent calls ignore the argument and return the same + Emscripten. (Note the default values for the config object!) The + config object is only honored the first time this is + called. Subsequent calls ignore the argument and return the same (configured) object which gets initialized by the first call. The config object properties include: @@ -133,7 +133,7 @@ */ 'use strict'; self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( - apiConfig = (sqlite3ApiBootstrap.defaultConfig || self.sqlite3ApiConfig) + apiConfig = (self.sqlite3ApiConfig || sqlite3ApiBootstrap.defaultConfig) ){ if(sqlite3ApiBootstrap.sqlite3){ /* already initalized */ console.warn("sqlite3ApiBootstrap() called multiple times.", @@ -567,18 +567,22 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( ) ? !!capi.sqlite3_compileoption_used(optName) : false; }/*compileOptionUsed()*/; - capi.wasm.bindingSignatures = [ - /** - Signatures for the WASM-exported C-side functions. Each entry - is an array with 2+ elements: + /** + Signatures for the WASM-exported C-side functions. Each entry + is an array with 2+ elements: - ["c-side name", - "result type" (capi.wasm.xWrap() syntax), - [arg types in xWrap() syntax] - // ^^^ this needn't strictly be an array: it can be subsequent - // elements instead: [x,y,z] is equivalent to x,y,z - ] - */ + [ "c-side name", + "result type" (capi.wasm.xWrap() syntax), + [arg types in xWrap() syntax] + // ^^^ this needn't strictly be an array: it can be subsequent + // elements instead: [x,y,z] is equivalent to x,y,z + ] + + Note that support for the API-specific data types in the + result/argument type strings gets plugged in at a later phase in + the API initialization process. + */ + capi.wasm.bindingSignatures = [ // Please keep these sorted by function name! ["sqlite3_bind_blob","int", "sqlite3_stmt*", "int", "*", "int", "*"], ["sqlite3_bind_double","int", "sqlite3_stmt*", "int", "f64"], @@ -604,6 +608,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( "sqlite3*", "string", "int", "int", "*", "*", "*", "*", "*"], ["sqlite3_data_count", "int", "sqlite3_stmt*"], ["sqlite3_db_filename", "string", "sqlite3*", "string"], + ["sqlite3_db_handle", "sqlite3*", "sqlite3_stmt*"], ["sqlite3_db_name", "string", "sqlite3*", "int"], ["sqlite3_errmsg", "string", "sqlite3*"], ["sqlite3_error_offset", "int", "sqlite3*"], @@ -614,6 +619,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( ["sqlite3_expanded_sql", "string", "sqlite3_stmt*"], ["sqlite3_extended_errcode", "int", "sqlite3*"], ["sqlite3_extended_result_codes", "int", "sqlite3*", "int"], + ["sqlite3_file_control", "int", "sqlite3*", "string", "int", "*"], ["sqlite3_finalize", "int", "sqlite3_stmt*"], ["sqlite3_initialize", undefined], ["sqlite3_interrupt", undefined, "sqlite3*" @@ -740,20 +746,132 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( /** Returns true if sqlite3.capi.sqlite3_web_persistent_dir() is a - non-empty string and the given name has that string as its - prefix, else returns false. + non-empty string and the given name starts with (that string + + '/'), else returns false. + + Potential (but arguable) TODO: return true if the name is one of + (":localStorage:", "local", ":sessionStorage:", "session") and + kvvfs is available. */ capi.sqlite3_web_filename_is_persistent = function(name){ const p = capi.sqlite3_web_persistent_dir(); - return (p && name) ? name.startsWith(p) : false; + return (p && name) ? name.startsWith(p+'/') : false; }; - + if(0===capi.wasm.exports.sqlite3_vfs_find(0)){ /* Assume that sqlite3_initialize() has not yet been called. This will be the case in an SQLITE_OS_KV build. */ capi.wasm.exports.sqlite3_initialize(); } + if( self.window===self ){ + /* Features specific to the main window thread... */ + + /** + Internal helper for sqlite3_web_kvvfs_clear() and friends. + Its argument should be one of ('local','session',''). + */ + const __kvvfsInfo = function(which){ + const rc = Object.create(null); + rc.prefix = 'kvvfs-'+which; + rc.stores = []; + if('session'===which || ''===which) rc.stores.push(self.sessionStorage); + if('local'===which || ''===which) rc.stores.push(self.localStorage); + return rc; + }; + + /** + Clears all storage used by the kvvfs DB backend, deleting any + DB(s) stored there. Its argument must be either 'session', + 'local', or ''. In the first two cases, only sessionStorage + resp. localStorage is cleared. If it's an empty string (the + default) then both are cleared. Only storage keys which match + the pattern used by kvvfs are cleared: any other client-side + data are retained. + + This function is only available in the main window thread. + + Returns the number of entries cleared. + */ + capi.sqlite3_web_kvvfs_clear = function(which=''){ + let rc = 0; + const kvinfo = __kvvfsInfo(which); + kvinfo.stores.forEach((s)=>{ + const toRm = [] /* keys to remove */; + let i; + for( i = 0; i < s.length; ++i ){ + const k = s.key(i); + if(k.startsWith(kvinfo.prefix)) toRm.push(k); + } + toRm.forEach((kk)=>s.removeItem(kk)); + rc += toRm.length; + }); + return rc; + }; + + /** + This routine guesses the approximate amount of + window.localStorage and/or window.sessionStorage in use by the + kvvfs database backend. Its argument must be one of + ('session', 'local', ''). In the first two cases, only + sessionStorage resp. localStorage is counted. If it's an empty + string (the default) then both are counted. Only storage keys + which match the pattern used by kvvfs are counted. The returned + value is the "length" value of every matching key and value, + noting that the kvvf uses only ASCII keys and values. + + Note that the returned size is not authoritative from the + perspective of how much data can fit into localStorage and + sessionStorage, as the precise algorithms for determining + those limits are unspecified and may include per-entry + overhead invisible to clients. + */ + capi.sqlite3_web_kvvfs_size = function(which=''){ + let sz = 0; + const kvinfo = __kvvfsInfo(which); + kvinfo.stores.forEach((s)=>{ + let i; + for(i = 0; i < s.length; ++i){ + const k = s.key(i); + if(k.startsWith(kvinfo.prefix)){ + sz += k.length; + sz += s.getItem(k).length; + } + } + }); + return sz; + }; + + /** + Given an `sqlite3*`, returns a truthy value (see below) if that + db handle uses the "kvvfs" VFS, else returns false. If pDb is + NULL then this function returns true if the default VFS is + "kvvfs". Results are undefined if pDb is truthy but refers to + an invalid pointer. + + The truthy value it returns is a pointer to the kvvfs + `sqlite3_vfs` object. + */ + capi.sqlite3_web_db_is_kvvfs = function(pDb){ + const pK = capi.sqlite3_vfs_find("kvvfs"); + if(!pK) return false; + else if(!pDb){ + return capi.sqlite3_vfs_find(0) && pK; + } + const scope = capi.wasm.scopedAllocPush(); + try{ + const ppVfs = capi.wasm.scopedAllocPtr(); + return ( + (0===capi.sqlite3_file_control( + pDb, "main", capi.SQLITE_FCNTL_VFS_POINTER, ppVfs + )) && (capi.wasm.getPtrValue(ppVfs) === pK) + ) ? pK : false; + }finally{ + capi.wasm.scopedAllocPop(scope); + } + }; + }/* main-window-only bits */ + /* The remainder of the API will be set up in later steps. */ const sqlite3 = { WasmAllocError: WasmAllocError, @@ -790,6 +908,11 @@ self.sqlite3ApiBootstrap.initializers = []; global-scope symbol. */ self.sqlite3ApiBootstrap.defaultConfig = Object.create(null); -/** Placeholder: gets installed by the first call to - self.sqlite3ApiBootstrap(). */ +/** + Placeholder: gets installed by the first call to + self.sqlite3ApiBootstrap(). However, it is recommended that the + caller of sqlite3ApiBootstrap() capture its return value and delete + self.sqlite3ApiBootstrap after calling it. It returns the same + value which will be stored here. +*/ self.sqlite3ApiBootstrap.sqlite3 = undefined; diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c index 2a505f19ab..c072e8c9d4 100644 --- a/ext/wasm/api/sqlite3-wasm.c +++ b/ext/wasm/api/sqlite3-wasm.c @@ -76,7 +76,7 @@ int sqlite3_wasm_db_error(sqlite3*db, int err_code, const char *zMsg){ */ WASM_KEEP const char * sqlite3_wasm_enum_json(void){ - static char strBuf[1024 * 8] = {0} /* where the JSON goes */; + static char strBuf[1024 * 12] = {0} /* where the JSON goes */; int n = 0, childCount = 0, structCount = 0 /* output counters for figuring out where commas go */; char * pos = &strBuf[1] /* skip first byte for now to help protect @@ -259,7 +259,8 @@ const char * sqlite3_wasm_enum_json(void){ } _DefGroup; DefGroup(openFlags) { - /* Noting that not all of these will have any effect in WASM-space. */ + /* Noting that not all of these will have any effect in + ** WASM-space. */ DefInt(SQLITE_OPEN_READONLY); DefInt(SQLITE_OPEN_READWRITE); DefInt(SQLITE_OPEN_CREATE); @@ -322,6 +323,49 @@ const char * sqlite3_wasm_enum_json(void){ DefInt(SQLITE_IOCAP_BATCH_ATOMIC); } _DefGroup; + DefGroup(fcntl) { + DefInt(SQLITE_FCNTL_LOCKSTATE); + DefInt(SQLITE_FCNTL_GET_LOCKPROXYFILE); + DefInt(SQLITE_FCNTL_SET_LOCKPROXYFILE); + DefInt(SQLITE_FCNTL_LAST_ERRNO); + DefInt(SQLITE_FCNTL_SIZE_HINT); + DefInt(SQLITE_FCNTL_CHUNK_SIZE); + DefInt(SQLITE_FCNTL_FILE_POINTER); + DefInt(SQLITE_FCNTL_SYNC_OMITTED); + DefInt(SQLITE_FCNTL_WIN32_AV_RETRY); + DefInt(SQLITE_FCNTL_PERSIST_WAL); + DefInt(SQLITE_FCNTL_OVERWRITE); + DefInt(SQLITE_FCNTL_VFSNAME); + DefInt(SQLITE_FCNTL_POWERSAFE_OVERWRITE); + DefInt(SQLITE_FCNTL_PRAGMA); + DefInt(SQLITE_FCNTL_BUSYHANDLER); + DefInt(SQLITE_FCNTL_TEMPFILENAME); + DefInt(SQLITE_FCNTL_MMAP_SIZE); + DefInt(SQLITE_FCNTL_TRACE); + DefInt(SQLITE_FCNTL_HAS_MOVED); + DefInt(SQLITE_FCNTL_SYNC); + DefInt(SQLITE_FCNTL_COMMIT_PHASETWO); + DefInt(SQLITE_FCNTL_WIN32_SET_HANDLE); + DefInt(SQLITE_FCNTL_WAL_BLOCK); + DefInt(SQLITE_FCNTL_ZIPVFS); + DefInt(SQLITE_FCNTL_RBU); + DefInt(SQLITE_FCNTL_VFS_POINTER); + DefInt(SQLITE_FCNTL_JOURNAL_POINTER); + DefInt(SQLITE_FCNTL_WIN32_GET_HANDLE); + DefInt(SQLITE_FCNTL_PDB); + DefInt(SQLITE_FCNTL_BEGIN_ATOMIC_WRITE); + DefInt(SQLITE_FCNTL_COMMIT_ATOMIC_WRITE); + DefInt(SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE); + DefInt(SQLITE_FCNTL_LOCK_TIMEOUT); + DefInt(SQLITE_FCNTL_DATA_VERSION); + DefInt(SQLITE_FCNTL_SIZE_LIMIT); + DefInt(SQLITE_FCNTL_CKPT_DONE); + DefInt(SQLITE_FCNTL_RESERVE_BYTES); + DefInt(SQLITE_FCNTL_CKPT_START); + DefInt(SQLITE_FCNTL_EXTERNAL_READER); + DefInt(SQLITE_FCNTL_CKSM_FILE); + } _DefGroup; + DefGroup(access){ DefInt(SQLITE_ACCESS_EXISTS); DefInt(SQLITE_ACCESS_READWRITE); diff --git a/ext/wasm/batch-runner.html b/ext/wasm/batch-runner.html index 38f38070c0..2a6c1405cf 100644 --- a/ext/wasm/batch-runner.html +++ b/ext/wasm/batch-runner.html @@ -55,7 +55,9 @@