Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Extend the JS/WASM SEE build support by (A) filtering SEE-related bits out of the JS when not building with SEE and (B) accepting an optional key/textkey/hexkey option to the sqlite3.oo1.DB and subclass constructors to create/open SEE-encrypted databases with. Demonstrate SEE in the test app using the kvvfs. This obviates the changes made in [5c505ee8a7]. |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
8fbda563d2f56f8dd3f695a5711e4356 |
User & Date: | stephan 2024-04-22 16:46:37 |
References
2024-04-22
| ||
17:03 | Minor cleanups to [8fbda563d2f5]. (check-in: 5ee2594b user: stephan tags: trunk) | |
Context
2024-04-22
| ||
17:03 | Minor cleanups to [8fbda563d2f5]. (check-in: 5ee2594b user: stephan tags: trunk) | |
16:46 | Extend the JS/WASM SEE build support by (A) filtering SEE-related bits out of the JS when not building with SEE and (B) accepting an optional key/textkey/hexkey option to the sqlite3.oo1.DB and subclass constructors to create/open SEE-encrypted databases with. Demonstrate SEE in the test app using the kvvfs. This obviates the changes made in [5c505ee8a7]. (check-in: 8fbda563 user: stephan tags: trunk) | |
13:31 | Extra robustness in the code that causes cursors to return NULL when they are participating in an OUTER JOIN. (check-in: 672c2869 user: drh tags: trunk) | |
Changes
Changes to ext/wasm/GNUmakefile.
︙ | ︙ | |||
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 | # # c-pp.c was written specifically for the sqlite project's JavaScript # builds but is maintained as a standalone project: # https://fossil.wanderinghorse.net/r/c-pp # # Note that the SQLITE_... build flags used here have NO EFFECT on the # JS/WASM build. They are solely for use with $(bin.c-pp) itself. bin.c-pp := ./c-pp $(bin.c-pp): c-pp.c $(sqlite3.c) $(MAKEFILE) $(CC) -O0 -o $@ c-pp.c $(sqlite3.c) '-DCMPP_DEFAULT_DELIM="//#"' -I$(dir.top) \ -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_UTF16 \ -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_WAL -DSQLITE_THREADSAFE=0 \ -DSQLITE_TEMP_STORE=3 define C-PP.FILTER # Create $2 from $1 using $(bin.c-pp) # $1 = Input file: c-pp -f $(1).js # $2 = Output file: c-pp -o $(2).js # $3 = optional c-pp -D... flags $(2): $(1) $$(MAKEFILE) $$(bin.c-pp) | > > > > > > > | | 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 | # # c-pp.c was written specifically for the sqlite project's JavaScript # builds but is maintained as a standalone project: # https://fossil.wanderinghorse.net/r/c-pp # # Note that the SQLITE_... build flags used here have NO EFFECT on the # JS/WASM build. They are solely for use with $(bin.c-pp) itself. # # -D... flags which should be included in all invocations should be # appended to $(C-PP.FILTER.global). bin.c-pp := ./c-pp $(bin.c-pp): c-pp.c $(sqlite3.c) $(MAKEFILE) $(CC) -O0 -o $@ c-pp.c $(sqlite3.c) '-DCMPP_DEFAULT_DELIM="//#"' -I$(dir.top) \ -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_UTF16 \ -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_WAL -DSQLITE_THREADSAFE=0 \ -DSQLITE_TEMP_STORE=3 C-PP.FILTER.global ?= ifeq (1,$(SQLITE_C_IS_SEE)) C-PP.FILTER.global += -Denable-see endif define C-PP.FILTER # Create $2 from $1 using $(bin.c-pp) # $1 = Input file: c-pp -f $(1).js # $2 = Output file: c-pp -o $(2).js # $3 = optional c-pp -D... flags $(2): $(1) $$(MAKEFILE) $$(bin.c-pp) $$(bin.c-pp) -f $(1) -o $$@ $(3) $(C-PP.FILTER.global) CLEAN_FILES += $(2) endef # /end C-PP.FILTER ######################################################################## # cflags.common = C compiler flags for all builds cflags.common := -I. -I$(dir $(sqlite3.c)) |
︙ | ︙ |
Changes to ext/wasm/api/sqlite3-api-glue.js.
︙ | ︙ | |||
325 326 327 328 329 330 331 | if(false && wasm.compileOptionUsed('SQLITE_ENABLE_NORMALIZE')){ /* ^^^ "the problem" is that this is an option feature and the build-time function-export list does not currently take optional features into account. */ wasm.bindingSignatures.push(["sqlite3_normalized_sql", "string", "sqlite3_stmt*"]); } | > | > > | 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | if(false && wasm.compileOptionUsed('SQLITE_ENABLE_NORMALIZE')){ /* ^^^ "the problem" is that this is an option feature and the build-time function-export list does not currently take optional features into account. */ wasm.bindingSignatures.push(["sqlite3_normalized_sql", "string", "sqlite3_stmt*"]); } //#if enable-see if(wasm.exports.sqlite3_key_v2 instanceof Function){ /** This code is capable of using an SEE build but note that an SEE WASM build is generally incompatible with SEE's license conditions. It is permitted for use internally in organizations which have licensed SEE, but not for public sites because exposing an SEE build of sqlite3.wasm effectively provides all clients with a working copy of the commercial SEE code. */ wasm.bindingSignatures.push( ["sqlite3_key", "int", "sqlite3*", "string", "int"], ["sqlite3_key_v2","int","sqlite3*","string","*","int"], ["sqlite3_rekey", "int", "sqlite3*", "string", "int"], ["sqlite3_rekey_v2", "int", "sqlite3*", "string", "*", "int"], ["sqlite3_activate_see", undefined, "string"] ); } //#endif enable-see /** Functions which require BigInt (int64) support are separated from the others because we need to conditionally bind them or apply dummy impls, depending on the capabilities of the environment. (That said: we never actually build without BigInt support, and such builds are untested.) |
︙ | ︙ | |||
623 624 625 626 627 628 629 | */ wasm.bindingSignatures.wasmInternal = [ ["sqlite3__wasm_db_reset", "int", "sqlite3*"], ["sqlite3__wasm_db_vfs", "sqlite3_vfs*", "sqlite3*","string"], ["sqlite3__wasm_vfs_create_file", "int", "sqlite3_vfs*","string","*", "int"], ["sqlite3__wasm_posix_create_file", "int", "string","*", "int"], | | > | 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 | */ wasm.bindingSignatures.wasmInternal = [ ["sqlite3__wasm_db_reset", "int", "sqlite3*"], ["sqlite3__wasm_db_vfs", "sqlite3_vfs*", "sqlite3*","string"], ["sqlite3__wasm_vfs_create_file", "int", "sqlite3_vfs*","string","*", "int"], ["sqlite3__wasm_posix_create_file", "int", "string","*", "int"], ["sqlite3__wasm_vfs_unlink", "int", "sqlite3_vfs*","string"], ["sqlite3__wasm_qfmt_token","string:dealloc", "string","int"] ]; /** Install JS<->C struct bindings for the non-opaque struct types we need... */ sqlite3.StructBinder = globalThis.Jaccwabyt({ heap: 0 ? wasm.memory : wasm.heap8u, |
︙ | ︙ |
Changes to ext/wasm/api/sqlite3-api-oo1.js.
︙ | ︙ | |||
83 84 85 86 87 88 89 90 91 92 93 94 95 96 | A map of sqlite3_vfs pointers to SQL code or a callback function to run when the DB constructor opens a database with the given VFS. In the latter case, the call signature is (theDbObject,sqlite3Namespace) and the callback is expected to throw on error. */ const __vfsPostOpenSql = Object.create(null); /** A proxy for DB class constructors. It must be called with the being-construct DB object as its "this". See the DB constructor for the argument docs. This is split into a separate function in order to enable simple creation of special-case DB constructors, e.g. JsStorageDb and OpfsDb. | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | A map of sqlite3_vfs pointers to SQL code or a callback function to run when the DB constructor opens a database with the given VFS. In the latter case, the call signature is (theDbObject,sqlite3Namespace) and the callback is expected to throw on error. */ const __vfsPostOpenSql = Object.create(null); /** Converts ArrayBuffer or Uint8Array ba into a string of hex digits. */ const byteArrayToHex = function(ba){ if( ba instanceof ArrayBuffer ){ ba = new Uint8Array(ba); } const li = []; const digits = "0123456789abcdef"; for( const d of ba ){ li.push( digits[(d & 0xf0) >> 4], digits[d & 0x0f] ); } return li.join(''); }; //#if enable-see /** Internal helper to apply an SEE key to a just-opened database. Requires that db be-a DB object which has just been opened, opt be the options object processed by its ctor, and opt must have either the key, hexkey, or textkey properties, either as a string, an ArrayBuffer, or a Uint8Array. This is a no-op in non-SEE builds. It throws on error and returns without side effects if its key/textkey options are not of valid types. Returns true if it applies the key, else a falsy value. */ const dbCtorApplySEEKey = function(db,opt){ if( !capi.sqlite3_key_v2 ) return; let keytype; let key; const check = (opt.key ? 1 : 0) + (opt.hexkey ? 1 : 0) + (opt.textkey ? 1 : 0); if( !check ) return; else if( check>1 ) toss3("Only ONE of (key, hexkey, textkey) may be provided."); if( opt.key ){ /* It is not legal to bind an argument to PRAGMA key=?, so we convert it to a hexkey... */ keytype = 'key'; key = opt.key; if('string'===typeof key){ key = new TextEncoder('utf-8').encode(key); } if((key instanceof ArrayBuffer) || (key instanceof Uint8Array)){ key = byteArrayToHex(key); keytype = 'hexkey'; }else{ toss3("Invalid value for the 'key' option. Expecting a string, ArrayBuffer, or Uint8Array."); return; } }else if( opt.textkey ){ /* For textkey we need it to be in string form, so convert it to a string if it's a byte array... */ keytype = 'textkey'; key = opt.textkey; if(key instanceof ArrayBuffer){ key = new Uint8Array(key); } if(key instanceof Uint8Array){ key = new TextDecoder('utf-8').decode(key); }else if('string'!==typeof key){ toss3("Invalid value for the 'textkey' option. Expecting a string, ArrayBuffer, or Uint8Array."); } }else if( opt.hexkey ){ keytype = 'hexkey'; key = opt.hexkey; if((key instanceof ArrayBuffer) || (key instanceof Uint8Array)){ key = byteArrayToHex(key); }else if('string'!==typeof key){ toss3("Invalid value for the 'hexkey' option. Expecting a string, ArrayBuffer, or Uint8Array."); } /* else assume it's valid hex codes */; }else{ return; } let stmt; try{ stmt = db.prepare("PRAGMA "+keytype+"="+util.sqlite3__wasm_qfmt_token(key, 1)); stmt.step(); }finally{ if(stmt) stmt.finalize(); } return true; }; //#endif enable-see /** A proxy for DB class constructors. It must be called with the being-construct DB object as its "this". See the DB constructor for the argument docs. This is split into a separate function in order to enable simple creation of special-case DB constructors, e.g. JsStorageDb and OpfsDb. |
︙ | ︙ | |||
171 172 173 174 175 176 177 178 | }finally{ wasm.pstack.restore(stack); } this.filename = fnJs; __ptrMap.set(this, pDb); __stmtMap.set(this, Object.create(null)); try{ // Check for per-VFS post-open SQL/callback... | > > > | | < | < | < | < < | < > | | < < < < | | 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | }finally{ wasm.pstack.restore(stack); } this.filename = fnJs; __ptrMap.set(this, pDb); __stmtMap.set(this, Object.create(null)); try{ //#if enable-see dbCtorApplySEEKey(this,opt); //#endif // Check for per-VFS post-open SQL/callback... const pVfs = capi.sqlite3_js_db_vfs(pDb) || toss3("Internal error: cannot get VFS for new db handle."); const postInitSql = __vfsPostOpenSql[pVfs]; if(postInitSql){ /** Reminder: if this db is encrypted and the client did _not_ pass in the key, any init code will fail, causing the ctor to throw. We don't actually know whether the db is encrypted, so we cannot sensibly apply any heuristics which skip the init code only for encrypted databases for which no key has yet been supplied. */ if(postInitSql instanceof Function){ postInitSql(this, sqlite3); }else{ checkSqlite3Rc( pDb, capi.sqlite3_exec(pDb, postInitSql, 0, 0, 0) ); } } |
︙ | ︙ | |||
294 295 296 297 298 299 300 301 302 303 304 305 306 307 | in the form of a single configuration object with the following properties: - `filename`: database file name - `flags`: open-mode flags - `vfs`: the VFS fname The `filename` and `vfs` arguments may be either JS strings or C-strings allocated via WASM. `flags` is required to be a JS string (because it's specific to this API, which is specific to JS). For purposes of passing a DB instance to C-style sqlite3 functions, the DB object's read-only `pointer` property holds its | > > > > > > > > > > > > > > | 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | in the form of a single configuration object with the following properties: - `filename`: database file name - `flags`: open-mode flags - `vfs`: the VFS fname //#if enable-see And, for SEE-capable builds, optionally ONE of the following: - `key`, `hexkey`, or `textkey`: encryption key as a string, ArrayBuffer, or Uint8Array. These flags function as documented for the SEE pragmas of the same names. In non-SEE builds, these options are ignored. In SEE builds, `PRAGMA key/textkey/hexkey=X` is executed immediately after opening the db. If more than one of the options is provided, or any option has an invalid argument type, an exception is thrown. //#endif enable-see The `filename` and `vfs` arguments may be either JS strings or C-strings allocated via WASM. `flags` is required to be a JS string (because it's specific to this API, which is specific to JS). For purposes of passing a DB instance to C-style sqlite3 functions, the DB object's read-only `pointer` property holds its |
︙ | ︙ | |||
1558 1559 1560 1561 1562 1563 1564 | 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. Booleans are bound as integer 0 or 1. It is not expected the distinction of binding | | | 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 | 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. Booleans are bound as integer 0 or 1. It is not expected the distinction of binding doubles which have no fractional parts and integers is significant for the majority of clients due to sqlite3's data typing model. If [BigInt] support is enabled then this routine will bind BigInt values as 64-bit integers if they'll fit in 64 bits. If that support disabled, it will store the BigInt as an int32 or a double if it can do so without loss of precision. If the BigInt is _too BigInt_ then it will throw. |
︙ | ︙ | |||
1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 | }/*oo1 object*/; if(util.isUIThread()){ /** Functionally equivalent to DB(storageName,'c','kvvfs') except that it throws if the given storage name is not one of 'local' or 'session'. */ sqlite3.oo1.JsStorageDb = function(storageName='session'){ if('session'!==storageName && 'local'!==storageName){ toss3("JsStorageDb db name must be one of 'session' or 'local'."); } | > > > > > > > > > > > > > > | < < < < | 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 | }/*oo1 object*/; if(util.isUIThread()){ /** Functionally equivalent to DB(storageName,'c','kvvfs') except that it throws if the given storage name is not one of 'local' or 'session'. As of version 3.46, the argument may optionally be an options object in the form: { filename: 'session'|'local', ... etc. (all options supported by the DB ctor) } noting that the 'vfs' option supported by main DB constructor is ignored here: the vfs is always 'kvvfs'. */ sqlite3.oo1.JsStorageDb = function(storageName='session'){ const opt = dbCtorHelper.normalizeArgs(...arguments); storageName = opt.filename; if('session'!==storageName && 'local'!==storageName){ toss3("JsStorageDb db name must be one of 'session' or 'local'."); } opt.vfs = 'kvvfs'; dbCtorHelper.call(this, opt); }; const jdb = sqlite3.oo1.JsStorageDb; jdb.prototype = Object.create(DB.prototype); /** Equivalent to sqlite3_js_kvvfs_clear(). */ jdb.clearStorage = capi.sqlite3_js_kvvfs_clear; /** Clears this database instance's storage or throws if this |
︙ | ︙ |
Changes to ext/wasm/api/sqlite3-wasm.c.
︙ | ︙ | |||
1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 | ** Binding for combinations of sqlite3_config() arguments which take ** a single i64 argument. */ SQLITE_WASM_EXPORT int sqlite3__wasm_config_j(int op, sqlite3_int64 arg){ return sqlite3_config(op, arg); } #if 0 // Pending removal after verification of a workaround discussed in the // forum post linked to below. /* ** This function is NOT part of the sqlite3 public API. It is strictly ** for use by the sqlite project's own JS/WASM bindings. | > > > > > > > > > > > > > > > > > > > | 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 | ** Binding for combinations of sqlite3_config() arguments which take ** a single i64 argument. */ SQLITE_WASM_EXPORT int sqlite3__wasm_config_j(int op, sqlite3_int64 arg){ return sqlite3_config(op, arg); } /* ** This function is NOT part of the sqlite3 public API. It is strictly ** for use by the sqlite project's own JS/WASM bindings. ** ** If z is not NULL, returns the result of passing z to ** sqlite3_mprintf()'s %Q modifier (if addQuotes is true) or %q (if ** addQuotes is 0). Returns NULL if z is NULL or on OOM. */ SQLITE_WASM_EXPORT char * sqlite3__wasm_qfmt_token(char *z, int addQuotes){ char * rc = 0; if( z ){ rc = addQuotes ? sqlite3_mprintf("%Q", z) : sqlite3_mprintf("%q", z); } return rc; } #if 0 // Pending removal after verification of a workaround discussed in the // forum post linked to below. /* ** This function is NOT part of the sqlite3 public API. It is strictly ** for use by the sqlite project's own JS/WASM bindings. |
︙ | ︙ |
Changes to ext/wasm/tester1.c-pp.js.
︙ | ︙ | |||
1478 1479 1480 1481 1482 1483 1484 | T.assert(0===st.columnCount); const ndx = st.getParamIndex(':b'); T.assert(1===ndx); st.bindAsBlob(ndx, "ima blob") /*step() skipped intentionally*/.reset(true); } finally { T.assert(0===st.finalize()) | | | 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 | T.assert(0===st.columnCount); const ndx = st.getParamIndex(':b'); T.assert(1===ndx); st.bindAsBlob(ndx, "ima blob") /*step() skipped intentionally*/.reset(true); } finally { T.assert(0===st.finalize()) .assert(undefined===st.finalize()); } try { db.prepare("/*empty SQL*/"); toss("Must not be reached."); }catch(e){ T.assert(e instanceof sqlite3.SQLite3Error) |
︙ | ︙ | |||
2583 2584 2585 2586 2587 2588 2589 | predicate: ()=>(isUIThread() || "local/sessionStorage are unavailable in a Worker"), test: function(sqlite3){ const filename = this.kvvfsDbFile = 'session'; const pVfs = capi.sqlite3_vfs_find('kvvfs'); T.assert(pVfs); const JDb = this.JDb = sqlite3.oo1.JsStorageDb; | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 | predicate: ()=>(isUIThread() || "local/sessionStorage are unavailable in a Worker"), test: function(sqlite3){ const filename = this.kvvfsDbFile = 'session'; const pVfs = capi.sqlite3_vfs_find('kvvfs'); T.assert(pVfs); const JDb = this.JDb = sqlite3.oo1.JsStorageDb; const unlink = this.kvvfsUnlink = ()=>JDb.clearStorage(this.kvvfsDbFile); unlink(); let db = new JDb(filename); try { db.exec([ 'create table kvvfs(a);', 'insert into kvvfs(a) values(1),(2),(3)' ]); T.assert(3 === db.selectValue('select count(*) from kvvfs')); db.close(); db = new JDb(filename); db.exec('insert into kvvfs(a) values(4),(5),(6)'); T.assert(6 === db.selectValue('select count(*) from kvvfs')); }finally{ db.close(); } } }/*kvvfs sanity checks*/) //#if enable-see .t({ name: 'kvvfs with SEE encryption', predicate: ()=>(isUIThread() || "Only available in main thread."), test: function(sqlite3){ this.kvvfsUnlink(); let db; try{ db = new this.JDb({ filename: this.kvvfsDbFile, key: 'foo' }); db.exec([ "create table t(a,b);", "insert into t(a,b) values(1,2),(3,4)" ]); db.close(); let err; try{ db = new this.JDb({ filename: this.kvvfsDbFile, flags: 'ct' }); T.assert(db) /* opening is fine, but... */; db.exec("select 1 from sqlite_schema"); console.warn("sessionStorage =",sessionStorage); }catch(e){ err = e; }finally{ db.close(); } T.assert(err,"Expecting an exception") .assert(sqlite3.capi.SQLITE_NOTADB==err.resultCode, "Expecting NOTADB"); db = new sqlite3.oo1.DB({ filename: this.kvvfsDbFile, vfs: 'kvvfs', hexkey: new Uint8Array([0x66,0x6f,0x6f]) // equivalent: '666f6f' }); T.assert( 4===db.selectValue('select sum(a) from t') ); }finally{ if( db ) db.close(); this.kvvfsUnlink(); } } })/*kvvfs with SEE*/ //#endif enable-see ;/* end kvvfs tests */ //////////////////////////////////////////////////////////////////////// T.g('Hook APIs') .t({ name: "sqlite3_commit/rollback/update_hook()", predicate: ()=>wasm.bigIntEnabled || "Update hook requires int64", |
︙ | ︙ |