| /* |
| 2022-10-12 |
| |
| 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. |
| |
| *********************************************************************** |
| |
| Main functional and regression tests for the sqlite3 WASM API. |
| |
| This mini-framework works like so: |
| |
| This script adds a series of test groups, each of which contains an |
| arbitrary number of tests, into a queue. After loading of the |
| sqlite3 WASM/JS module is complete, that queue is processed. If any |
| given test fails, the whole thing fails. This script is built such |
| that it can run from the main UI thread or worker thread. Test |
| groups and individual tests can be assigned a predicate function |
| which determines whether to run them or not, and this is |
| specifically intended to be used to toggle certain tests on or off |
| for the main/worker threads or the availability (or not) of |
| optional features such as int64 support. |
| |
| Each test group defines a single state object which gets applied as |
| the test functions' `this` for all tests in that group. Test |
| functions can use that to, e.g., set up a db in an early test and |
| close it in a later test. Each test gets passed the sqlite3 |
| namespace object as its only argument. |
| */ |
| /* |
| This file is intended to be processed by c-pp to inject (or not) |
| code specific to ES6 modules which is illegal in non-module code. |
| |
| Non-ES6 module build and ES6 module for the main-thread: |
| |
| ./c-pp -f tester1.c-pp.js -o tester1.js |
| |
| ES6 worker module build: |
| |
| ./c-pp -f tester1.c-pp.js -o tester1-esm.js -Dtarget=es6-module |
| */ |
| //#if target=es6-module |
| import {default as sqlite3InitModule} from './jswasm/sqlite3.mjs'; |
| globalThis.sqlite3InitModule = sqlite3InitModule; |
| //#else |
| 'use strict'; |
| //#endif |
| (function(self){ |
| /** |
| Set up our output channel differently depending |
| on whether we are running in a worker thread or |
| the main (UI) thread. |
| */ |
| let logClass; |
| /* Predicate for tests/groups. */ |
| const isUIThread = ()=>(globalThis.window===self && globalThis.document); |
| /* Predicate for tests/groups. */ |
| const isWorker = ()=>!isUIThread(); |
| /* Predicate for tests/groups. */ |
| const testIsTodo = ()=>false; |
| const haveWasmCTests = ()=>{ |
| return !!wasm.exports.sqlite3__wasm_test_intptr; |
| }; |
| const hasOpfs = ()=>{ |
| return globalThis.FileSystemHandle |
| && globalThis.FileSystemDirectoryHandle |
| && globalThis.FileSystemFileHandle |
| && globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle |
| && navigator?.storage?.getDirectory; |
| }; |
| |
| { |
| const mapToString = (v)=>{ |
| switch(typeof v){ |
| case 'number': case 'string': case 'boolean': |
| case 'undefined': case 'bigint': |
| return ''+v; |
| default: break; |
| } |
| if(null===v) return 'null'; |
| if(v instanceof Error){ |
| v = { |
| message: v.message, |
| stack: v.stack, |
| errorClass: v.name |
| }; |
| } |
| return JSON.stringify(v,undefined,2); |
| }; |
| const normalizeArgs = (args)=>args.map(mapToString); |
| if( isUIThread() ){ |
| console.log("Running in the UI thread."); |
| const logTarget = document.querySelector('#test-output'); |
| logClass = function(cssClass,...args){ |
| const ln = document.createElement('div'); |
| if(cssClass){ |
| for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){ |
| ln.classList.add(c); |
| } |
| } |
| ln.append(document.createTextNode(normalizeArgs(args).join(' '))); |
| logTarget.append(ln); |
| }; |
| const cbReverse = document.querySelector('#cb-log-reverse'); |
| //cbReverse.setAttribute('checked','checked'); |
| const cbReverseKey = 'tester1:cb-log-reverse'; |
| const cbReverseIt = ()=>{ |
| logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse'); |
| //localStorage.setItem(cbReverseKey, cbReverse.checked ? 1 : 0); |
| }; |
| cbReverse.addEventListener('change', cbReverseIt, true); |
| /*if(localStorage.getItem(cbReverseKey)){ |
| cbReverse.checked = !!(+localStorage.getItem(cbReverseKey)); |
| }*/ |
| cbReverseIt(); |
| }else{ /* Worker thread */ |
| console.log("Running in a Worker thread."); |
| logClass = function(cssClass,...args){ |
| postMessage({ |
| type:'log', |
| payload:{cssClass, args: normalizeArgs(args)} |
| }); |
| }; |
| } |
| } |
| const reportFinalTestStatus = function(pass){ |
| if(isUIThread()){ |
| let e = document.querySelector('#color-target'); |
| e.classList.add(pass ? 'tests-pass' : 'tests-fail'); |
| e = document.querySelector('title'); |
| e.innerText = (pass ? 'PASS' : 'FAIL') + ': ' + e.innerText; |
| }else{ |
| postMessage({type:'test-result', payload:{pass}}); |
| } |
| }; |
| const log = (...args)=>{ |
| //console.log(...args); |
| logClass('',...args); |
| } |
| const warn = (...args)=>{ |
| console.warn(...args); |
| logClass('warning',...args); |
| } |
| const error = (...args)=>{ |
| console.error(...args); |
| logClass('error',...args); |
| }; |
| |
| const toss = (...args)=>{ |
| error(...args); |
| throw new Error(args.join(' ')); |
| }; |
| const tossQuietly = (...args)=>{ |
| throw new Error(args.join(' ')); |
| }; |
| |
| const roundMs = (ms)=>Math.round(ms*100)/100; |
| |
| /** |
| Helpers for writing sqlite3-specific tests. |
| */ |
| const TestUtil = { |
| /** Running total of the number of tests run via |
| this API. */ |
| counter: 0, |
| /** |
| If expr is a function, it is called and its result |
| is returned, coerced to a bool, else expr, coerced to |
| a bool, is returned. |
| */ |
| toBool: function(expr){ |
| return (expr instanceof Function) ? !!expr() : !!expr; |
| }, |
| /** Throws if expr is false. If expr is a function, it is called |
| and its result is evaluated. If passed multiple arguments, |
| those after the first are a message string which get applied |
| as an exception message if the assertion fails. The message |
| arguments are concatenated together with a space between each. |
| */ |
| assert: function f(expr, ...msg){ |
| ++this.counter; |
| if(!this.toBool(expr)){ |
| throw new Error(msg.length ? msg.join(' ') : "Assertion failed."); |
| } |
| return this; |
| }, |
| /** Calls f() and squelches any exception it throws. If it |
| does not throw, this function throws. */ |
| mustThrow: function(f, msg){ |
| ++this.counter; |
| let err; |
| try{ f(); } catch(e){err=e;} |
| if(!err) throw new Error(msg || "Expected exception."); |
| return this; |
| }, |
| /** |
| Works like mustThrow() but expects filter to be a regex, |
| function, or string to match/filter the resulting exception |
| against. If f() does not throw, this test fails and an Error is |
| thrown. If filter is a regex, the test passes if |
| filter.test(error.message) passes. If it's a function, the test |
| passes if filter(error) returns truthy. If it's a string, the |
| test passes if the filter matches the exception message |
| precisely. In all other cases the test fails, throwing an |
| Error. |
| |
| If it throws, msg is used as the error report unless it's falsy, |
| in which case a default is used. |
| */ |
| mustThrowMatching: function(f, filter, msg){ |
| ++this.counter; |
| let err; |
| try{ f(); } catch(e){err=e;} |
| if(!err) throw new Error(msg || "Expected exception."); |
| let pass = false; |
| if(filter instanceof RegExp) pass = filter.test(err.message); |
| else if(filter instanceof Function) pass = filter(err); |
| else if('string' === typeof filter) pass = (err.message === filter); |
| if(!pass){ |
| throw new Error(msg || ("Filter rejected this exception: "+err.message)); |
| } |
| return this; |
| }, |
| /** Throws if expr is truthy or expr is a function and expr() |
| returns truthy. */ |
| throwIf: function(expr, msg){ |
| ++this.counter; |
| if(this.toBool(expr)) throw new Error(msg || "throwIf() failed"); |
| return this; |
| }, |
| /** Throws if expr is falsy or expr is a function and expr() |
| returns falsy. */ |
| throwUnless: function(expr, msg){ |
| ++this.counter; |
| if(!this.toBool(expr)) throw new Error(msg || "throwUnless() failed"); |
| return this; |
| }, |
| eqApprox: (v1,v2,factor=0.05)=>(v1>=(v2-factor) && v1<=(v2+factor)), |
| TestGroup: (function(){ |
| let groupCounter = 0; |
| const TestGroup = function(name, predicate){ |
| this.number = ++groupCounter; |
| this.name = name; |
| this.predicate = predicate; |
| this.tests = []; |
| }; |
| TestGroup.prototype = { |
| addTest: function(testObj){ |
| this.tests.push(testObj); |
| return this; |
| }, |
| run: async function(sqlite3){ |
| logClass('group-start',"Group #"+this.number+':',this.name); |
| if(this.predicate){ |
| const p = this.predicate(sqlite3); |
| if(!p || 'string'===typeof p){ |
| logClass(['warning','skipping-group'], |
| "SKIPPING group:", p ? p : "predicate says to" ); |
| return; |
| } |
| } |
| const assertCount = TestUtil.counter; |
| const groupState = Object.create(null); |
| const skipped = []; |
| let runtime = 0, i = 0; |
| for(const t of this.tests){ |
| ++i; |
| const n = this.number+"."+i; |
| logClass('one-test-line', n+":", t.name); |
| if(t.predicate){ |
| const p = t.predicate(sqlite3); |
| if(!p || 'string'===typeof p){ |
| logClass(['warning','skipping-test'], |
| "SKIPPING:", p ? p : "predicate says to" ); |
| skipped.push( n+': '+t.name ); |
| continue; |
| } |
| } |
| const tc = TestUtil.counter, now = performance.now(); |
| let rc = t.test.call(groupState, sqlite3); |
| /*if(rc instanceof Promise){ |
| rc = rc.catch((e)=>{ |
| error("Test failure:",e); |
| throw e; |
| }); |
| }*/ |
| await rc; |
| const then = performance.now(); |
| runtime += then - now; |
| logClass(['faded','one-test-summary'], |
| TestUtil.counter - tc, 'assertion(s) in', |
| roundMs(then-now),'ms'); |
| } |
| logClass(['green','group-end'], |
| "#"+this.number+":", |
| (TestUtil.counter - assertCount), |
| "assertion(s) in",roundMs(runtime),"ms"); |
| if(0 && skipped.length){ |
| logClass('warning',"SKIPPED test(s) in group",this.number+":",skipped); |
| } |
| } |
| }; |
| return TestGroup; |
| })()/*TestGroup*/, |
| testGroups: [], |
| currentTestGroup: undefined, |
| addGroup: function(name, predicate){ |
| this.testGroups.push( this.currentTestGroup = |
| new this.TestGroup(name, predicate) ); |
| return this; |
| }, |
| addTest: function(name, callback){ |
| let predicate; |
| if(1===arguments.length){ |
| this.currentTestGroup.addTest(arguments[0]); |
| }else{ |
| this.currentTestGroup.addTest({ |
| name, predicate, test: callback |
| }); |
| } |
| return this; |
| }, |
| runTests: async function(sqlite3){ |
| return new Promise(async function(pok,pnok){ |
| try { |
| let runtime = 0; |
| for(let g of this.testGroups){ |
| const now = performance.now(); |
| await g.run(sqlite3); |
| runtime += performance.now() - now; |
| } |
| logClass(['strong','green','full-test-summary'], |
| "Done running tests.",TestUtil.counter,"assertions in", |
| roundMs(runtime),'ms'); |
| pok(); |
| reportFinalTestStatus(true); |
| }catch(e){ |
| error(e); |
| pnok(e); |
| reportFinalTestStatus(false); |
| } |
| }.bind(this)); |
| } |
| }/*TestUtil*/; |
| const T = TestUtil; |
| T.g = T.addGroup; |
| T.t = T.addTest; |
| let capi, wasm/*assigned after module init*/; |
| const sahPoolConfig = { |
| name: 'opfs-sahpool-tester1', |
| clearOnInit: true, |
| initialCapacity: 6 |
| }; |
| //////////////////////////////////////////////////////////////////////// |
| // End of infrastructure setup. Now define the tests... |
| //////////////////////////////////////////////////////////////////////// |
| |
| //////////////////////////////////////////////////////////////////// |
| T.g('Basic sanity checks') |
| .t({ |
| name:'sqlite3_config()', |
| test:function(sqlite3){ |
| for(const k of [ |
| 'SQLITE_CONFIG_GETMALLOC', 'SQLITE_CONFIG_URI' |
| ]){ |
| T.assert(capi[k] > 0); |
| } |
| T.assert(capi.SQLITE_MISUSE===capi.sqlite3_config( |
| capi.SQLITE_CONFIG_URI, 1 |
| ), "MISUSE because the library has already been initialized."); |
| T.assert(capi.SQLITE_MISUSE === capi.sqlite3_config( |
| // not enough args |
| capi.SQLITE_CONFIG_GETMALLOC |
| )); |
| T.assert(capi.SQLITE_NOTFOUND === capi.sqlite3_config( |
| // unhandled-in-JS config option |
| capi.SQLITE_CONFIG_GETMALLOC, 1 |
| )); |
| if(0){ |
| log("We cannot _fully_ test sqlite3_config() after the library", |
| "has been initialized (which it necessarily has been to", |
| "set up various bindings) and we cannot shut it down ", |
| "without losing the VFS registrations."); |
| T.assert(0 === capi.sqlite3_config( |
| capi.SQLITE_CONFIG_URI, 1 |
| )); |
| } |
| } |
| })/*sqlite3_config()*/ |
| |
| //////////////////////////////////////////////////////////////////// |
| .t({ |
| name: "JS wasm-side allocator", |
| test: function(sqlite3){ |
| if(sqlite3.config.useStdAlloc){ |
| warn("Using system allocator. This violates the docs and", |
| "may cause grief with certain APIs", |
| "(e.g. sqlite3_deserialize())."); |
| T.assert(wasm.alloc.impl === wasm.exports.malloc) |
| .assert(wasm.dealloc === wasm.exports.free) |
| .assert(wasm.realloc.impl === wasm.exports.realloc); |
| }else{ |
| T.assert(wasm.alloc.impl === wasm.exports.sqlite3_malloc) |
| .assert(wasm.dealloc === wasm.exports.sqlite3_free) |
| .assert(wasm.realloc.impl === wasm.exports.sqlite3_realloc); |
| } |
| } |
| }) |
| .t('Namespace object checks', function(sqlite3){ |
| const wasmCtypes = wasm.ctype; |
| T.assert(wasmCtypes.structs[0].name==='sqlite3_vfs'). |
| assert(wasmCtypes.structs[0].members.szOsFile.sizeof>=4). |
| assert(wasmCtypes.structs[1/*sqlite3_io_methods*/ |
| ].members.xFileSize.offset>0); |
| [ /* Spot-check a handful of constants to make sure they got installed... */ |
| 'SQLITE_SCHEMA','SQLITE_NULL','SQLITE_UTF8', |
| 'SQLITE_STATIC', 'SQLITE_DIRECTONLY', |
| 'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE' |
| ].forEach((k)=>T.assert('number' === typeof capi[k])); |
| [/* Spot-check a few of the WASM API methods. */ |
| 'alloc', 'dealloc', 'installFunction' |
| ].forEach((k)=>T.assert(wasm[k] instanceof Function)); |
| |
| T.assert(capi.sqlite3_errstr(capi.SQLITE_IOERR_ACCESS).indexOf("I/O")>=0). |
| assert(capi.sqlite3_errstr(capi.SQLITE_CORRUPT).indexOf('malformed')>0). |
| assert(capi.sqlite3_errstr(capi.SQLITE_OK) === 'not an error'); |
| |
| try { |
| throw new sqlite3.WasmAllocError; |
| }catch(e){ |
| T.assert(e instanceof Error) |
| .assert(e instanceof sqlite3.WasmAllocError) |
| .assert("Allocation failed." === e.message); |
| } |
| try { |
| throw new sqlite3.WasmAllocError("test",{ |
| cause: 3 |
| }); |
| }catch(e){ |
| T.assert(3 === e.cause) |
| .assert("test" === e.message); |
| } |
| try {throw new sqlite3.WasmAllocError("test","ing",".")} |
| catch(e){T.assert("test ing ." === e.message)} |
| |
| try{ throw new sqlite3.SQLite3Error(capi.SQLITE_SCHEMA) } |
| catch(e){ |
| T.assert('SQLITE_SCHEMA' === e.message) |
| .assert(capi.SQLITE_SCHEMA === e.resultCode); |
| } |
| try{ sqlite3.SQLite3Error.toss(capi.SQLITE_CORRUPT,{cause: true}) } |
| catch(e){ |
| T.assert('SQLITE_CORRUPT' === e.message) |
| .assert(capi.SQLITE_CORRUPT === e.resultCode) |
| .assert(true===e.cause); |
| } |
| try{ sqlite3.SQLite3Error.toss("resultCode check") } |
| catch(e){ |
| T.assert(capi.SQLITE_ERROR === e.resultCode) |
| .assert('resultCode check' === e.message); |
| } |
| }) |
| //////////////////////////////////////////////////////////////////// |
| .t('strglob/strlike', function(sqlite3){ |
| T.assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")). |
| assert(0!==capi.sqlite3_strglob("*.txt", "foo.xtx")). |
| assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)). |
| assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0)); |
| }) |
| |
| //////////////////////////////////////////////////////////////////// |
| ;/*end of basic sanity checks*/ |
| |
| //////////////////////////////////////////////////////////////////// |
| T.g('C/WASM Utilities') |
| .t('sqlite3.wasm namespace', function(sqlite3){ |
| // TODO: break this into smaller individual test functions. |
| const w = wasm; |
| const chr = (x)=>x.charCodeAt(0); |
| //log("heap getters..."); |
| { |
| const li = [8, 16, 32]; |
| if(w.bigIntEnabled) li.push(64); |
| for(const n of li){ |
| const bpe = n/8; |
| const s = w.heapForSize(n,false); |
| T.assert(bpe===s.BYTES_PER_ELEMENT). |
| assert(w.heapForSize(s.constructor) === s); |
| const u = w.heapForSize(n,true); |
| T.assert(bpe===u.BYTES_PER_ELEMENT). |
| assert(s!==u). |
| assert(w.heapForSize(u.constructor) === u); |
| } |
| } |
| |
| // alloc(), realloc(), allocFromTypedArray() |
| { |
| let m = w.alloc(14); |
| let m2 = w.realloc(m, 16); |
| T.assert(m === m2/* because of alignment */); |
| T.assert(0 === w.realloc(m, 0)); |
| m = m2 = 0; |
| |
| // Check allocation limits and allocator's responses... |
| T.assert('number' === typeof sqlite3.capi.SQLITE_MAX_ALLOCATION_SIZE); |
| if(!sqlite3.config.useStdAlloc){ |
| const tooMuch = sqlite3.capi.SQLITE_MAX_ALLOCATION_SIZE + 1, |
| isAllocErr = (e)=>e instanceof sqlite3.WasmAllocError; |
| T.mustThrowMatching(()=>w.alloc(tooMuch), isAllocErr) |
| .assert(0 === w.alloc.impl(tooMuch)) |
| .mustThrowMatching(()=>w.realloc(0, tooMuch), isAllocErr) |
| .assert(0 === w.realloc.impl(0, tooMuch)); |
| } |
| |
| // Check allocFromTypedArray()... |
| const byteList = [11,22,33] |
| const u = new Uint8Array(byteList); |
| m = w.allocFromTypedArray(u); |
| for(let i = 0; i < u.length; ++i){ |
| T.assert(u[i] === byteList[i]) |
| .assert(u[i] === w.peek8(m + i)); |
| } |
| w.dealloc(m); |
| m = w.allocFromTypedArray(u.buffer); |
| for(let i = 0; i < u.length; ++i){ |
| T.assert(u[i] === byteList[i]) |
| .assert(u[i] === w.peek8(m + i)); |
| } |
| |
| w.dealloc(m); |
| T.mustThrowMatching( |
| ()=>w.allocFromTypedArray(1), |
| 'Value is not of a supported TypedArray type.' |
| ); |
| } |
| |
| { // Test peekXYZ()/pokeXYZ()... |
| const m = w.alloc(8); |
| T.assert( 17 === w.poke8(m,17).peek8(m) ) |
| .assert( 31987 === w.poke16(m,31987).peek16(m) ) |
| .assert( 345678 === w.poke32(m,345678).peek32(m) ) |
| .assert( |
| T.eqApprox( 345678.9, w.poke32f(m,345678.9).peek32f(m) ) |
| ).assert( |
| T.eqApprox( 4567890123.4, w.poke64f(m, 4567890123.4).peek64f(m) ) |
| ); |
| if(w.bigIntEnabled){ |
| T.assert( |
| BigInt(Number.MAX_SAFE_INTEGER) === |
| w.poke64(m, Number.MAX_SAFE_INTEGER).peek64(m) |
| ); |
| } |
| w.dealloc(m); |
| } |
| |
| // isPtr32() |
| { |
| const ip = w.isPtr32; |
| T.assert(ip(0)) |
| .assert(!ip(-1)) |
| .assert(!ip(1.1)) |
| .assert(!ip(0xffffffff)) |
| .assert(ip(0x7fffffff)) |
| .assert(!ip()) |
| .assert(!ip(null)/*might change: under consideration*/) |
| ; |
| } |
| |
| //log("jstrlen()..."); |
| { |
| T.assert(3 === w.jstrlen("abc")).assert(4 === w.jstrlen("äbc")); |
| } |
| |
| //log("jstrcpy()..."); |
| { |
| const fillChar = 10; |
| let ua = new Uint8Array(8), rc, |
| refill = ()=>ua.fill(fillChar); |
| refill(); |
| rc = w.jstrcpy("hello", ua); |
| T.assert(6===rc).assert(0===ua[5]).assert(chr('o')===ua[4]); |
| refill(); |
| ua[5] = chr('!'); |
| rc = w.jstrcpy("HELLO", ua, 0, -1, false); |
| T.assert(5===rc).assert(chr('!')===ua[5]).assert(chr('O')===ua[4]); |
| refill(); |
| rc = w.jstrcpy("the end", ua, 4); |
| //log("rc,ua",rc,ua); |
| T.assert(4===rc).assert(0===ua[7]). |
| assert(chr('e')===ua[6]).assert(chr('t')===ua[4]); |
| refill(); |
| rc = w.jstrcpy("the end", ua, 4, -1, false); |
| T.assert(4===rc).assert(chr(' ')===ua[7]). |
| assert(chr('e')===ua[6]).assert(chr('t')===ua[4]); |
| refill(); |
| rc = w.jstrcpy("", ua, 0, 1, true); |
| //log("rc,ua",rc,ua); |
| T.assert(1===rc).assert(0===ua[0]); |
| refill(); |
| rc = w.jstrcpy("x", ua, 0, 1, true); |
| //log("rc,ua",rc,ua); |
| T.assert(1===rc).assert(0===ua[0]); |
| refill(); |
| rc = w.jstrcpy('äbä', ua, 0, 1, true); |
| T.assert(1===rc, 'Must not write partial multi-byte char.') |
| .assert(0===ua[0]); |
| refill(); |
| rc = w.jstrcpy('äbä', ua, 0, 2, true); |
| T.assert(1===rc, 'Must not write partial multi-byte char.') |
| .assert(0===ua[0]); |
| refill(); |
| rc = w.jstrcpy('äbä', ua, 0, 2, false); |
| T.assert(2===rc).assert(fillChar!==ua[1]).assert(fillChar===ua[2]); |
| }/*jstrcpy()*/ |
| |
| //log("cstrncpy()..."); |
| { |
| const scope = w.scopedAllocPush(); |
| try { |
| let cStr = w.scopedAllocCString("hello"); |
| const n = w.cstrlen(cStr); |
| let cpy = w.scopedAlloc(n+10); |
| let rc = w.cstrncpy(cpy, cStr, n+10); |
| T.assert(n+1 === rc). |
| assert("hello" === w.cstrToJs(cpy)). |
| assert(chr('o') === w.peek8(cpy+n-1)). |
| assert(0 === w.peek8(cpy+n)); |
| let cStr2 = w.scopedAllocCString("HI!!!"); |
| rc = w.cstrncpy(cpy, cStr2, 3); |
| T.assert(3===rc). |
| assert("HI!lo" === w.cstrToJs(cpy)). |
| assert(chr('!') === w.peek8(cpy+2)). |
| assert(chr('l') === w.peek8(cpy+3)); |
| }finally{ |
| w.scopedAllocPop(scope); |
| } |
| } |
| |
| //log("jstrToUintArray()..."); |
| { |
| let a = w.jstrToUintArray("hello", false); |
| T.assert(5===a.byteLength).assert(chr('o')===a[4]); |
| a = w.jstrToUintArray("hello", true); |
| T.assert(6===a.byteLength).assert(chr('o')===a[4]).assert(0===a[5]); |
| a = w.jstrToUintArray("äbä", false); |
| T.assert(5===a.byteLength).assert(chr('b')===a[2]); |
| a = w.jstrToUintArray("äbä", true); |
| T.assert(6===a.byteLength).assert(chr('b')===a[2]).assert(0===a[5]); |
| } |
| |
| //log("allocCString()..."); |
| { |
| const jstr = "hällo, world!"; |
| const [cstr, n] = w.allocCString(jstr, true); |
| T.assert(14 === n) |
| .assert(0===w.peek8(cstr+n)) |
| .assert(chr('!')===w.peek8(cstr+n-1)); |
| w.dealloc(cstr); |
| } |
| |
| //log("scopedAlloc() and friends..."); |
| { |
| const alloc = w.alloc, dealloc = w.dealloc; |
| w.alloc = w.dealloc = null; |
| T.assert(!w.scopedAlloc.level) |
| .mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/) |
| .mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/); |
| w.alloc = alloc; |
| T.mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/); |
| w.dealloc = dealloc; |
| T.mustThrowMatching(()=>w.scopedAllocPop(), /^Invalid state/) |
| .mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/) |
| .mustThrowMatching(()=>w.scopedAlloc.level=0, /read-only/); |
| const asc = w.scopedAllocPush(); |
| let asc2; |
| try { |
| const p1 = w.scopedAlloc(16), |
| p2 = w.scopedAlloc(16); |
| T.assert(1===w.scopedAlloc.level) |
| .assert(Number.isFinite(p1)) |
| .assert(Number.isFinite(p2)) |
| .assert(asc[0] === p1) |
| .assert(asc[1]===p2); |
| asc2 = w.scopedAllocPush(); |
| const p3 = w.scopedAlloc(16); |
| T.assert(2===w.scopedAlloc.level) |
| .assert(Number.isFinite(p3)) |
| .assert(2===asc.length) |
| .assert(p3===asc2[0]); |
| |
| const [z1, z2, z3] = w.scopedAllocPtr(3); |
| T.assert('number'===typeof z1).assert(z2>z1).assert(z3>z2) |
| .assert(0===w.peek32(z1), 'allocPtr() must zero the targets') |
| .assert(0===w.peek32(z3)); |
| }finally{ |
| // Pop them in "incorrect" order to make sure they behave: |
| w.scopedAllocPop(asc); |
| T.assert(0===asc.length); |
| T.mustThrowMatching(()=>w.scopedAllocPop(asc), |
| /^Invalid state object/); |
| if(asc2){ |
| T.assert(2===asc2.length,'Should be p3 and z1'); |
| w.scopedAllocPop(asc2); |
| T.assert(0===asc2.length); |
| T.mustThrowMatching(()=>w.scopedAllocPop(asc2), |
| /^Invalid state object/); |
| } |
| } |
| T.assert(0===w.scopedAlloc.level); |
| w.scopedAllocCall(function(){ |
| T.assert(1===w.scopedAlloc.level); |
| const [cstr, n] = w.scopedAllocCString("hello, world", true); |
| T.assert(12 === n) |
| .assert(0===w.peek8(cstr+n)) |
| .assert(chr('d')===w.peek8(cstr+n-1)); |
| }); |
| }/*scopedAlloc()*/ |
| |
| //log("xCall()..."); |
| { |
| const pJson = w.xCall('sqlite3__wasm_enum_json'); |
| T.assert(Number.isFinite(pJson)).assert(w.cstrlen(pJson)>300); |
| } |
| |
| //log("xWrap()..."); |
| { |
| T.mustThrowMatching(()=>w.xWrap('sqlite3_libversion',null,'i32'), |
| /requires 0 arg/). |
| assert(w.xWrap.resultAdapter('i32') instanceof Function). |
| assert(w.xWrap.argAdapter('i32') instanceof Function); |
| let fw = w.xWrap('sqlite3_libversion','utf8'); |
| T.mustThrowMatching(()=>fw(1), /requires 0 arg/); |
| let rc = fw(); |
| T.assert('string'===typeof rc).assert(rc.length>5); |
| rc = w.xCallWrapped('sqlite3__wasm_enum_json','*'); |
| T.assert(rc>0 && Number.isFinite(rc)); |
| rc = w.xCallWrapped('sqlite3__wasm_enum_json','utf8'); |
| T.assert('string'===typeof rc).assert(rc.length>300); |
| |
| |
| { // 'string:static' argAdapter() sanity checks... |
| let argAd = w.xWrap.argAdapter('string:static'); |
| let p0 = argAd('foo'), p1 = argAd('bar'); |
| T.assert(w.isPtr(p0) && w.isPtr(p1)) |
| .assert(p0 !== p1) |
| .assert(p0 === argAd('foo')) |
| .assert(p1 === argAd('bar')); |
| } |
| |
| // 'string:flexible' argAdapter() sanity checks... |
| w.scopedAllocCall(()=>{ |
| const argAd = w.xWrap.argAdapter('string:flexible'); |
| const cj = (v)=>w.cstrToJs(argAd(v)); |
| T.assert('Hi' === cj('Hi')) |
| .assert('hi' === cj(['h','i'])) |
| .assert('HI' === cj(new Uint8Array([72, 73]))); |
| }); |
| |
| // jsFuncToWasm() |
| { |
| const fsum3 = (x,y,z)=>x+y+z; |
| fw = w.jsFuncToWasm('i(iii)', fsum3); |
| T.assert(fw instanceof Function) |
| .assert( fsum3 !== fw ) |
| .assert( 3 === fw.length ) |
| .assert( 6 === fw(1,2,3) ); |
| T.mustThrowMatching( ()=>w.jsFuncToWasm('x()', function(){}), |
| 'Invalid signature letter: x'); |
| } |
| |
| // xWrap(Function,...) |
| { |
| let fp; |
| try { |
| const fmy = function fmy(i,s,d){ |
| if(fmy.debug) log("fmy(",...arguments,")"); |
| T.assert( 3 === i ) |
| .assert( w.isPtr(s) ) |
| .assert( w.cstrToJs(s) === 'a string' ) |
| .assert( T.eqApprox(1.2, d) ); |
| return w.allocCString("hi"); |
| }; |
| fmy.debug = false; |
| const xwArgs = ['string:dealloc', ['i32', 'string', 'f64']]; |
| fw = w.xWrap(fmy, ...xwArgs); |
| const fmyArgs = [3, 'a string', 1.2]; |
| let rc = fw(...fmyArgs); |
| T.assert( 'hi' === rc ); |
| if(0){ |
| /* Retain this as a "reminder to self"... |
| |
| This extra level of indirection does not work: the |
| string argument is ending up as a null in fmy() but |
| the numeric arguments are making their ways through |
| |
| What's happening is: installFunction() is creating a |
| WASM-compatible function instance. When we pass a JS string |
| into there it's getting coerced into `null` before being passed |
| on to the lower-level wrapper. |
| */ |
| fmy.debug = true; |
| fp = wasm.installFunction('i(isd)', fw); |
| fw = w.functionEntry(fp); |
| rc = fw(...fmyArgs); |
| log("rc =",rc); |
| T.assert( 'hi' === rc ); |
| // Similarly, this does not work: |
| //let fpw = w.xWrap(fp, null, [null,null,null]); |
| //rc = fpw(...fmyArgs); |
| //log("rc =",rc); |
| //T.assert( 'hi' === rc ); |
| } |
| }finally{ |
| wasm.uninstallFunction(fp); |
| } |
| } |
| |
| if(haveWasmCTests()){ |
| if(!sqlite3.config.useStdAlloc){ |
| fw = w.xWrap('sqlite3__wasm_test_str_hello', 'utf8:dealloc',['i32']); |
| rc = fw(0); |
| T.assert('hello'===rc); |
| rc = fw(1); |
| T.assert(null===rc); |
| } |
| |
| if(w.bigIntEnabled){ |
| w.xWrap.resultAdapter('thrice', (v)=>3n*BigInt(v)); |
| w.xWrap.argAdapter('twice', (v)=>2n*BigInt(v)); |
| fw = w.xWrap('sqlite3__wasm_test_int64_times2','thrice','twice'); |
| rc = fw(1); |
| T.assert(12n===rc); |
| |
| w.scopedAllocCall(function(){ |
| const pI1 = w.scopedAlloc(8), pI2 = pI1+4; |
| w.pokePtr([pI1, pI2], 0); |
| const f = w.xWrap('sqlite3__wasm_test_int64_minmax',undefined,['i64*','i64*']); |
| const [r1, r2] = w.peek64([pI1, pI2]); |
| T.assert(!Number.isSafeInteger(r1)).assert(!Number.isSafeInteger(r2)); |
| }); |
| } |
| } |
| }/*xWrap()*/ |
| }/*WhWasmUtil*/) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t({ |
| name: 'sqlite3.StructBinder (jaccwabyt🐇)', |
| predicate: (sqlite3)=>!!sqlite3.wasm.exports.sqlite3__wasm_test_struct |
| || "Built without SQLITE_WASM_ENABLE_C_TESTS", |
| test: function(sqlite3){ |
| const S = sqlite3, W = S.wasm; |
| const MyStructDef = { |
| sizeof: 16, |
| members: { |
| p4: {offset: 0, sizeof: 4, signature: "i"}, |
| pP: {offset: 4, sizeof: 4, signature: "P"}, |
| ro: {offset: 8, sizeof: 4, signature: "i", readOnly: true}, |
| cstr: {offset: 12, sizeof: 4, signature: "s"} |
| } |
| }; |
| if(W.bigIntEnabled){ |
| const m = MyStructDef; |
| m.members.p8 = {offset: m.sizeof, sizeof: 8, signature: "j"}; |
| m.sizeof += m.members.p8.sizeof; |
| } |
| const StructType = S.StructBinder.StructType; |
| const K = S.StructBinder('my_struct',MyStructDef); |
| T.mustThrowMatching(()=>K(), /via 'new'/). |
| mustThrowMatching(()=>new K('hi'), /^Invalid pointer/); |
| const k1 = new K(), k2 = new K(); |
| try { |
| T.assert(k1.constructor === K). |
| assert(K.isA(k1)). |
| assert(k1 instanceof K). |
| assert(K.prototype.lookupMember('p4').key === '$p4'). |
| assert(K.prototype.lookupMember('$p4').name === 'p4'). |
| mustThrowMatching(()=>K.prototype.lookupMember('nope'), /not a mapped/). |
| assert(undefined === K.prototype.lookupMember('nope',false)). |
| assert(k1 instanceof StructType). |
| assert(StructType.isA(k1)). |
| mustThrowMatching(()=>k1.$ro = 1, /read-only/); |
| Object.keys(MyStructDef.members).forEach(function(key){ |
| key = K.memberKey(key); |
| T.assert(0 == k1[key], |
| "Expecting allocation to zero the memory "+ |
| "for "+key+" but got: "+k1[key]+ |
| " from "+k1.memoryDump()); |
| }); |
| T.assert('number' === typeof k1.pointer). |
| mustThrowMatching(()=>k1.pointer = 1, /pointer/); |
| k1.$p4 = 1; k1.$pP = 2; |
| T.assert(1 === k1.$p4).assert(2 === k1.$pP); |
| if(MyStructDef.members.$p8){ |
| k1.$p8 = 1/*must not throw despite not being a BigInt*/; |
| k1.$p8 = BigInt(Number.MAX_SAFE_INTEGER * 2); |
| T.assert(BigInt(2 * Number.MAX_SAFE_INTEGER) === k1.$p8); |
| } |
| T.assert(!k1.ondispose); |
| k1.setMemberCString('cstr', "A C-string."); |
| T.assert(Array.isArray(k1.ondispose)). |
| assert(k1.ondispose[0] === k1.$cstr). |
| assert('number' === typeof k1.$cstr). |
| assert('A C-string.' === k1.memberToJsString('cstr')); |
| k1.$pP = k2; |
| T.assert(k1.$pP === k2.pointer); |
| k1.$pP = null/*null is special-cased to 0.*/; |
| T.assert(0===k1.$pP); |
| let ptr = k1.pointer; |
| k1.dispose(); |
| T.assert(undefined === k1.pointer). |
| mustThrowMatching(()=>{k1.$pP=1}, /disposed instance/); |
| }finally{ |
| k1.dispose(); |
| k2.dispose(); |
| } |
| |
| if(!W.bigIntEnabled){ |
| log("Skipping WasmTestStruct tests: BigInt not enabled."); |
| return; |
| } |
| |
| const WTStructDesc = |
| W.ctype.structs.filter((e)=>'WasmTestStruct'===e.name)[0]; |
| const autoResolvePtr = true /* EXPERIMENTAL */; |
| if(autoResolvePtr){ |
| WTStructDesc.members.ppV.signature = 'P'; |
| } |
| const WTStruct = S.StructBinder(WTStructDesc); |
| //log(WTStruct.structName, WTStruct.structInfo); |
| const wts = new WTStruct(); |
| //log("WTStruct.prototype keys:",Object.keys(WTStruct.prototype)); |
| try{ |
| T.assert(wts.constructor === WTStruct). |
| assert(WTStruct.memberKeys().indexOf('$ppV')>=0). |
| assert(wts.memberKeys().indexOf('$v8')>=0). |
| assert(!K.isA(wts)). |
| assert(WTStruct.isA(wts)). |
| assert(wts instanceof WTStruct). |
| assert(wts instanceof StructType). |
| assert(StructType.isA(wts)). |
| assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8). |
| assert(0===wts.$ppV).assert(0===wts.$xFunc); |
| const testFunc = |
| W.xGet('sqlite3__wasm_test_struct'/*name gets mangled in -O3 builds!*/); |
| let counter = 0; |
| //log("wts.pointer =",wts.pointer); |
| const wtsFunc = function(arg){ |
| /*log("This from a JS function called from C, "+ |
| "which itself was called from JS. arg =",arg);*/ |
| ++counter; |
| if(3===counter){ |
| tossQuietly("Testing exception propagation."); |
| } |
| } |
| wts.$v4 = 10; wts.$v8 = 20; |
| wts.$xFunc = W.installFunction(wtsFunc, wts.memberSignature('xFunc')) |
| T.assert(0===counter).assert(10 === wts.$v4).assert(20n === wts.$v8) |
| .assert(0 === wts.$ppV).assert('number' === typeof wts.$xFunc) |
| .assert(0 === wts.$cstr) |
| .assert(wts.memberIsString('$cstr')) |
| .assert(!wts.memberIsString('$v4')) |
| .assert(null === wts.memberToJsString('$cstr')) |
| .assert(W.functionEntry(wts.$xFunc) instanceof Function); |
| /* It might seem silly to assert that the values match |
| what we just set, but recall that all of those property |
| reads and writes are, via property interceptors, |
| actually marshaling their data to/from a raw memory |
| buffer, so merely reading them back is actually part of |
| testing the struct-wrapping API. */ |
| |
| testFunc(wts.pointer); |
| //log("wts.pointer, wts.$ppV",wts.pointer, wts.$ppV); |
| T.assert(1===counter).assert(20 === wts.$v4).assert(40n === wts.$v8) |
| .assert(wts.$ppV === wts.pointer) |
| .assert('string' === typeof wts.memberToJsString('cstr')) |
| .assert(wts.memberToJsString('cstr') === wts.memberToJsString('$cstr')) |
| .mustThrowMatching(()=>wts.memberToJsString('xFunc'), |
| /Invalid member type signature for C-string/) |
| ; |
| testFunc(wts.pointer); |
| T.assert(2===counter).assert(40 === wts.$v4).assert(80n === wts.$v8) |
| .assert(wts.$ppV === wts.pointer); |
| /** The 3rd call to wtsFunc throw from JS, which is called |
| from C, which is called from JS. Let's ensure that |
| that exception propagates back here... */ |
| T.mustThrowMatching(()=>testFunc(wts.pointer),/^Testing/); |
| W.uninstallFunction(wts.$xFunc); |
| wts.$xFunc = 0; |
| wts.$ppV = 0; |
| T.assert(!wts.$ppV); |
| //WTStruct.debugFlags(0x03); |
| wts.$ppV = wts; |
| T.assert(wts.pointer === wts.$ppV) |
| wts.setMemberCString('cstr', "A C-string."); |
| T.assert(Array.isArray(wts.ondispose)). |
| assert(wts.ondispose[0] === wts.$cstr). |
| assert('A C-string.' === wts.memberToJsString('cstr')); |
| const ptr = wts.pointer; |
| wts.dispose(); |
| T.assert(ptr).assert(undefined === wts.pointer); |
| }finally{ |
| wts.dispose(); |
| } |
| |
| if(1){ // ondispose of other struct instances |
| const s1 = new WTStruct, s2 = new WTStruct, s3 = new WTStruct; |
| T.assert(s1.lookupMember instanceof Function) |
| .assert(s1.addOnDispose instanceof Function); |
| s1.addOnDispose(s2,"testing variadic args"); |
| T.assert(2===s1.ondispose.length); |
| s2.addOnDispose(s3); |
| s1.dispose(); |
| T.assert(!s2.pointer,"Expecting s2 to be ondispose'd by s1."); |
| T.assert(!s3.pointer,"Expecting s3 to be ondispose'd by s2."); |
| } |
| } |
| }/*StructBinder*/) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t('sqlite3.wasm.pstack', function(sqlite3){ |
| const P = wasm.pstack; |
| const isAllocErr = (e)=>e instanceof sqlite3.WasmAllocError; |
| const stack = P.pointer; |
| T.assert(0===stack % 8 /* must be 8-byte aligned */); |
| try{ |
| const remaining = P.remaining; |
| T.assert(P.quota >= 4096) |
| .assert(remaining === P.quota) |
| .mustThrowMatching(()=>P.alloc(0), isAllocErr) |
| .mustThrowMatching(()=>P.alloc(-1), isAllocErr) |
| .mustThrowMatching( |
| ()=>P.alloc('i33'), |
| (e)=>e instanceof sqlite3.WasmAllocError |
| ); |
| ; |
| let p1 = P.alloc(12); |
| T.assert(p1 === stack - 16/*8-byte aligned*/) |
| .assert(P.pointer === p1); |
| let p2 = P.alloc(7); |
| T.assert(p2 === p1-8/*8-byte aligned, stack grows downwards*/) |
| .mustThrowMatching(()=>P.alloc(remaining), isAllocErr) |
| .assert(24 === stack - p2) |
| .assert(P.pointer === p2); |
| let n = remaining - (stack - p2); |
| let p3 = P.alloc(n); |
| T.assert(p3 === stack-remaining) |
| .mustThrowMatching(()=>P.alloc(1), isAllocErr); |
| }finally{ |
| P.restore(stack); |
| } |
| |
| T.assert(P.pointer === stack); |
| try { |
| const [p1, p2, p3] = P.allocChunks(3,'i32'); |
| T.assert(P.pointer === stack-16/*always rounded to multiple of 8*/) |
| .assert(p2 === p1 + 4) |
| .assert(p3 === p2 + 4); |
| T.mustThrowMatching(()=>P.allocChunks(1024, 1024 * 16), |
| (e)=>e instanceof sqlite3.WasmAllocError) |
| }finally{ |
| P.restore(stack); |
| } |
| |
| T.assert(P.pointer === stack); |
| try { |
| let [p1, p2, p3] = P.allocPtr(3,false); |
| let sPos = stack-16/*always rounded to multiple of 8*/; |
| T.assert(P.pointer === sPos) |
| .assert(p2 === p1 + 4) |
| .assert(p3 === p2 + 4); |
| [p1, p2, p3] = P.allocPtr(3); |
| T.assert(P.pointer === sPos-24/*3 x 8 bytes*/) |
| .assert(p2 === p1 + 8) |
| .assert(p3 === p2 + 8); |
| p1 = P.allocPtr(); |
| T.assert('number'===typeof p1); |
| }finally{ |
| P.restore(stack); |
| } |
| }/*pstack tests*/) |
| //////////////////////////////////////////////////////////////////// |
| ;/*end of C/WASM utils checks*/ |
| |
| T.g('sqlite3_randomness()') |
| .t('To memory buffer', function(sqlite3){ |
| const stack = wasm.pstack.pointer; |
| try{ |
| const n = 520; |
| const p = wasm.pstack.alloc(n); |
| T.assert(0===wasm.peek8(p)) |
| .assert(0===wasm.peek8(p+n-1)); |
| T.assert(undefined === capi.sqlite3_randomness(n - 10, p)); |
| let j, check = 0; |
| const heap = wasm.heap8u(); |
| for(j = 0; j < 10 && 0===check; ++j){ |
| check += heap[p + j]; |
| } |
| T.assert(check > 0); |
| check = 0; |
| // Ensure that the trailing bytes were not modified... |
| for(j = n - 10; j < n && 0===check; ++j){ |
| check += heap[p + j]; |
| } |
| T.assert(0===check); |
| }finally{ |
| wasm.pstack.restore(stack); |
| } |
| }) |
| .t('To byte array', function(sqlite3){ |
| const ta = new Uint8Array(117); |
| let i, n = 0; |
| for(i=0; i<ta.byteLength && 0===n; ++i){ |
| n += ta[i]; |
| } |
| T.assert(0===n) |
| .assert(ta === capi.sqlite3_randomness(ta)); |
| for(i=ta.byteLength-10; i<ta.byteLength && 0===n; ++i){ |
| n += ta[i]; |
| } |
| T.assert(n>0); |
| const t0 = new Uint8Array(0); |
| T.assert(t0 === capi.sqlite3_randomness(t0), |
| "0-length array is a special case"); |
| }) |
| ;/*end sqlite3_randomness() checks*/ |
| |
| //////////////////////////////////////////////////////////////////////// |
| T.g('sqlite3.oo1') |
| .t({ |
| name:'Create db', |
| //predicate: (sqlite3)=> |
| test: function(sqlite3){ |
| const dbFile = '/tester1.db'; |
| sqlite3.util.sqlite3__wasm_vfs_unlink(0, dbFile); |
| const db = this.db = new sqlite3.oo1.DB(dbFile, 0 ? 'ct' : 'c'); |
| db.onclose = { |
| disposeAfter: [], |
| disposeBefore: [ |
| (db)=>{ |
| //console.debug("db.onclose.before dropping modules"); |
| //sqlite3.capi.sqlite3_drop_modules(db.pointer, 0); |
| } |
| ], |
| before: function(db){ |
| while(this.disposeBefore.length){ |
| const v = this.disposeBefore.shift(); |
| console.debug("db.onclose.before cleaning up:",v); |
| if(wasm.isPtr(v)) wasm.dealloc(v); |
| else if(v instanceof sqlite3.StructBinder.StructType){ |
| v.dispose(); |
| }else if(v instanceof Function){ |
| try{ v(db) } catch(e){ |
| console.warn("beforeDispose() callback threw:",e); |
| } |
| } |
| } |
| }, |
| after: function(){ |
| while(this.disposeAfter.length){ |
| const v = this.disposeAfter.shift(); |
| console.debug("db.onclose.after cleaning up:",v); |
| if(wasm.isPtr(v)) wasm.dealloc(v); |
| else if(v instanceof sqlite3.StructBinder.StructType){ |
| v.dispose(); |
| }else if(v instanceof Function){ |
| try{v()} catch(e){/*ignored*/} |
| } |
| } |
| } |
| }; |
| |
| T.assert(wasm.isPtr(db.pointer)) |
| .mustThrowMatching(()=>db.pointer=1, /read-only/) |
| .assert(0===sqlite3.capi.sqlite3_extended_result_codes(db.pointer,1)) |
| .assert('main'===db.dbName(0)) |
| .assert('string' === typeof db.dbVfsName()) |
| .assert(db.pointer === wasm.xWrap.testConvertArg('sqlite3*',db)); |
| // Custom db error message handling via sqlite3_prepare_v2/v3() |
| let rc = capi.sqlite3_prepare_v3(db.pointer, {/*invalid*/}, -1, 0, null, null); |
| T.assert(capi.SQLITE_MISUSE === rc) |
| .assert(0 === capi.sqlite3_errmsg(db.pointer).indexOf("Invalid SQL")) |
| .assert(dbFile === db.dbFilename()) |
| .assert(!db.dbFilename('nope')); |
| //Sanity check DB.checkRc()... |
| let ex; |
| try{db.checkRc(rc)} |
| catch(e){ex = e} |
| T.assert(ex instanceof sqlite3.SQLite3Error) |
| .assert(capi.SQLITE_MISUSE===ex.resultCode) |
| .assert(0===ex.message.indexOf("SQLITE_MISUSE: sqlite3 result code")) |
| .assert(ex.message.indexOf("Invalid SQL")>0); |
| T.assert(db === db.checkRc(0)) |
| .assert(db === sqlite3.oo1.DB.checkRc(db,0)) |
| .assert(null === sqlite3.oo1.DB.checkRc(null,0)); |
| this.progressHandlerCount = 0; |
| if( wasm.compileOptionUsed('OMIT_PROGRESS_CALLBACK') ){ |
| T.assert( !capi.sqlite3_progress_handler ); |
| }else{ |
| T.assert( !!capi.sqlite3_progress_handler ); |
| capi.sqlite3_progress_handler(db, 5, (p)=>{ |
| ++this.progressHandlerCount; |
| return 0; |
| }, 0); |
| } |
| } |
| }) |
| //////////////////////////////////////////////////////////////////// |
| .t('sqlite3_db_config() and sqlite3_db_status()', function(sqlite3){ |
| let rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, 0, 0); |
| T.assert(0===rc); |
| rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_MAX+1, 0); |
| T.assert(capi.SQLITE_MISUSE === rc); |
| rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_MAINDBNAME, "main"); |
| T.assert(0 === rc); |
| const stack = wasm.pstack.pointer; |
| try { |
| const [pCur, pHi] = wasm.pstack.allocChunks(2,'i64'); |
| wasm.poke32([pCur, pHi], 0); |
| let [vCur, vHi] = wasm.peek32(pCur, pHi); |
| T.assert(0===vCur).assert(0===vHi); |
| rc = capi.sqlite3_status(capi.SQLITE_STATUS_MEMORY_USED, |
| pCur, pHi, 0); |
| [vCur, vHi] = wasm.peek32(pCur, pHi); |
| //console.warn("i32 vCur,vHi",vCur,vHi); |
| T.assert(0 === rc).assert(vCur > 0).assert(vHi >= vCur); |
| if(wasm.bigIntEnabled){ |
| // Again in 64-bit. Recall that pCur and pHi are allocated |
| // large enough to account for this re-use. |
| wasm.poke64([pCur, pHi], 0); |
| rc = capi.sqlite3_status64(capi.SQLITE_STATUS_MEMORY_USED, |
| pCur, pHi, 0); |
| [vCur, vHi] = wasm.peek64([pCur, pHi]); |
| //console.warn("i64 vCur,vHi",vCur,vHi); |
| T.assert(0 === rc).assert(vCur > 0).assert(vHi >= vCur); |
| } |
| }finally{ |
| wasm.pstack.restore(stack); |
| } |
| }) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t('DB.Stmt', function(sqlite3){ |
| let st = this.db.prepare( |
| new TextEncoder('utf-8').encode("select 3 as a") |
| ); |
| let rc; |
| try { |
| T.assert(wasm.isPtr(st.pointer)) |
| .mustThrowMatching(()=>st.pointer=1, /read-only/) |
| .assert(1===this.db.openStatementCount()) |
| .assert( |
| capi.sqlite3_stmt_status( |
| st, capi.SQLITE_STMTSTATUS_RUN, 0 |
| ) === 0) |
| .assert(!st._mayGet) |
| .assert('a' === st.getColumnName(0)) |
| .mustThrowMatching(()=>st.columnCount=2, |
| /columnCount property is read-only/) |
| .assert(1===st.columnCount) |
| .assert(0===st.parameterCount) |
| .mustThrow(()=>st.bind(1,null)) |
| .assert(true===st.step()) |
| .assert(3 === st.get(0)) |
| .mustThrow(()=>st.get(1)) |
| .mustThrow(()=>st.get(0,~capi.SQLITE_INTEGER)) |
| .assert(3 === st.get(0,capi.SQLITE_INTEGER)) |
| .assert(3 === st.getInt(0)) |
| .assert('3' === st.get(0,capi.SQLITE_TEXT)) |
| .assert('3' === st.getString(0)) |
| .assert(3.0 === st.get(0,capi.SQLITE_FLOAT)) |
| .assert(3.0 === st.getFloat(0)) |
| .assert(3 === st.get({}).a) |
| .assert(3 === st.get([])[0]) |
| .assert(3 === st.getJSON(0)) |
| .assert(st.get(0,capi.SQLITE_BLOB) instanceof Uint8Array) |
| .assert(1===st.get(0,capi.SQLITE_BLOB).length) |
| .assert(st.getBlob(0) instanceof Uint8Array) |
| .assert('3'.charCodeAt(0) === st.getBlob(0)[0]) |
| .assert(st._mayGet) |
| .assert(false===st.step()) |
| .assert(!st._mayGet) |
| .assert( |
| capi.sqlite3_stmt_status( |
| st, capi.SQLITE_STMTSTATUS_RUN, 0 |
| ) > 0); |
| |
| T.assert(this.progressHandlerCount>0 |
| || wasm.compileOptionUsed('OMIT_PROGRESS_CALLBACK'), |
| "Expecting progress callback."). |
| assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")). |
| assert(0!==capi.sqlite3_strglob("*.txt", "foo.xtx")). |
| assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)). |
| assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0)); |
| }finally{ |
| rc = st.finalize(); |
| } |
| T.assert(!st.pointer) |
| .assert(0===this.db.openStatementCount()) |
| .assert(0===rc); |
| |
| T.mustThrowMatching(()=>new sqlite3.oo1.Stmt("hi"), function(err){ |
| return (err instanceof sqlite3.SQLite3Error) |
| && capi.SQLITE_MISUSE === err.resultCode |
| && 0 < err.message.indexOf("Do not call the Stmt constructor directly.") |
| }); |
| }) |
| |
| //////////////////////////////////////////////////////////////////////// |
| .t('sqlite3_js_...()', function(){ |
| const db = this.db; |
| if(1){ |
| const vfsList = capi.sqlite3_js_vfs_list(); |
| T.assert(vfsList.length>1); |
| wasm.scopedAllocCall(()=>{ |
| const vfsArg = (v)=>wasm.xWrap.testConvertArg('sqlite3_vfs*',v); |
| for(const v of vfsList){ |
| T.assert('string' === typeof v); |
| const pVfs = capi.sqlite3_vfs_find(v); |
| T.assert(wasm.isPtr(pVfs)) |
| .assert(pVfs===vfsArg(v)); |
| const vfs = new capi.sqlite3_vfs(pVfs); |
| try { T.assert(vfsArg(vfs)===pVfs) } |
| finally{ vfs.dispose() } |
| } |
| }); |
| } |
| /** |
| Trivia: the magic db name ":memory:" does not actually use the |
| "memdb" VFS unless "memdb" is _explicitly_ provided as the VFS |
| name. Instead, it uses the default VFS with an in-memory btree. |
| Thus this.db's VFS may not be memdb even though it's an in-memory |
| db. |
| */ |
| const pVfsMem = capi.sqlite3_vfs_find('memdb'), |
| pVfsDflt = capi.sqlite3_vfs_find(0), |
| pVfsDb = capi.sqlite3_js_db_vfs(db.pointer); |
| T.assert(pVfsMem > 0) |
| .assert(pVfsDflt > 0) |
| .assert(pVfsDb > 0) |
| .assert(pVfsMem !== pVfsDflt |
| /* memdb lives on top of the default vfs */) |
| .assert(pVfsDb === pVfsDflt || pVfsdb === pVfsMem) |
| ; |
| /*const vMem = new capi.sqlite3_vfs(pVfsMem), |
| vDflt = new capi.sqlite3_vfs(pVfsDflt), |
| vDb = new capi.sqlite3_vfs(pVfsDb);*/ |
| const duv = capi.sqlite3_js_db_uses_vfs; |
| T.assert(pVfsDflt === duv(db.pointer, 0) |
| || pVfsMem === duv(db.pointer,0)) |
| .assert(!duv(db.pointer, "foo")) |
| ; |
| }/*sqlite3_js_...()*/) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t('Table t', function(sqlite3){ |
| const db = this.db; |
| let list = []; |
| this.progressHandlerCount = 0; |
| let rc = db.exec({ |
| sql:['CREATE TABLE t(a,b);', |
| // ^^^ using TEMP TABLE breaks the db export test |
| "INSERT INTO t(a,b) VALUES(1,2),(3,4),", |
| "(?,?)"/*intentionally missing semicolon to test for |
| off-by-one bug in string-to-WASM conversion*/], |
| saveSql: list, |
| bind: [5,6] |
| }); |
| //debug("Exec'd SQL:", list); |
| T.assert(rc === db) |
| .assert(2 === list.length) |
| .assert('string'===typeof list[1]) |
| .assert(3===db.changes()) |
| .assert(this.progressHandlerCount > 0 |
| || wasm.compileOptionUsed('OMIT_PROGRESS_CALLBACK'), |
| "Expecting progress callback.") |
| if(wasm.bigIntEnabled){ |
| T.assert(3n===db.changes(false,true)); |
| } |
| rc = db.exec({ |
| sql: "INSERT INTO t(a,b) values('blob',X'6869') RETURNING 13", |
| rowMode: 0 |
| }); |
| T.assert(Array.isArray(rc)) |
| .assert(1===rc.length) |
| .assert(13 === rc[0]) |
| .assert(1===db.changes()); |
| |
| let vals = db.selectValues('select a from t order by a limit 2'); |
| T.assert( 2 === vals.length ) |
| .assert( 1===vals[0] && 3===vals[1] ); |
| vals = db.selectValues('select a from t order by a limit $L', |
| {$L:2}, capi.SQLITE_TEXT); |
| T.assert( 2 === vals.length ) |
| .assert( '1'===vals[0] && '3'===vals[1] ); |
| vals = undefined; |
| |
| let blob = db.selectValue("select b from t where a='blob'"); |
| T.assert(blob instanceof Uint8Array). |
| assert(0x68===blob[0] && 0x69===blob[1]); |
| blob = null; |
| let counter = 0, colNames = []; |
| list.length = 0; |
| db.exec(new TextEncoder('utf-8').encode("SELECT a a, b b FROM t"),{ |
| rowMode: 'object', |
| resultRows: list, |
| columnNames: colNames, |
| _myState: 3 /* Accessible from the callback */, |
| callback: function(row,stmt){ |
| ++counter; |
| T.assert( |
| 3 === this._myState |
| /* Recall that "this" is the options object. */ |
| ).assert( |
| this.columnNames===colNames |
| ).assert( |
| this.columnNames[0]==='a' && this.columnNames[1]==='b' |
| ).assert( |
| (row.a%2 && row.a<6) || 'blob'===row.a |
| ); |
| } |
| }); |
| T.assert(2 === colNames.length) |
| .assert('a' === colNames[0]) |
| .assert(4 === counter) |
| .assert(4 === list.length); |
| colNames = []; |
| db.exec({ |
| /* Ensure that columnNames is populated for empty result sets. */ |
| sql: "SELECT a a, b B FROM t WHERE 0", |
| columnNames: colNames |
| }); |
| T.assert(2===colNames.length) |
| .assert('a'===colNames[0] && 'B'===colNames[1]); |
| list.length = 0; |
| db.exec("SELECT a a, b b FROM t",{ |
| rowMode: 'array', |
| callback: function(row,stmt){ |
| ++counter; |
| T.assert(Array.isArray(row)) |
| .assert((0===row[1]%2 && row[1]<7) |
| || (row[1] instanceof Uint8Array)); |
| } |
| }); |
| T.assert(8 === counter); |
| T.assert(Number.MIN_SAFE_INTEGER === |
| db.selectValue("SELECT "+Number.MIN_SAFE_INTEGER)). |
| assert(Number.MAX_SAFE_INTEGER === |
| db.selectValue("SELECT "+Number.MAX_SAFE_INTEGER)); |
| counter = 0; |
| let rv = db.exec({ |
| sql: "SELECT a FROM t", |
| callback: ()=>(1===++counter), |
| }); |
| T.assert(db === rv) |
| .assert(2===counter, |
| "Expecting exec step() loop to stop if callback returns false."); |
| /** If exec() is passed neither callback nor returnValue but |
| is passed an explicit rowMode then the default returnValue |
| is the whole result set, as if an empty resultRows option |
| had been passed. */ |
| rv = db.exec({ |
| sql: "SELECT -1 UNION ALL SELECT -2 UNION ALL SELECT -3 ORDER BY 1 DESC", |
| rowMode: 0 |
| }); |
| T.assert(Array.isArray(rv)).assert(3===rv.length) |
| .assert(-1===rv[0]).assert(-3===rv[2]); |
| rv = db.exec("SELECT 1 WHERE 0",{rowMode: 0}); |
| T.assert(Array.isArray(rv)).assert(0===rv.length); |
| if(wasm.bigIntEnabled && haveWasmCTests()){ |
| const mI = wasm.xCall('sqlite3__wasm_test_int64_max'); |
| const b = BigInt(Number.MAX_SAFE_INTEGER * 2); |
| T.assert(b === db.selectValue("SELECT "+b)). |
| assert(b === db.selectValue("SELECT ?", b)). |
| assert(mI == db.selectValue("SELECT $x", {$x:mI})); |
| }else{ |
| /* Curiously, the JS spec seems to be off by one with the definitions |
| of MIN/MAX_SAFE_INTEGER: |
| |
| https://github.com/emscripten-core/emscripten/issues/17391 */ |
| T.mustThrow(()=>db.selectValue("SELECT "+(Number.MAX_SAFE_INTEGER+1))). |
| mustThrow(()=>db.selectValue("SELECT "+(Number.MIN_SAFE_INTEGER-1))); |
| } |
| |
| let st = db.prepare("update t set b=:b where a='blob'"); |
| try { |
| T.assert(0===st.columnCount) |
| .assert( false===st.isReadOnly() ); |
| 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) |
| .assert(0==e.message.indexOf('Cannot prepare empty')); |
| } |
| |
| counter = 0; |
| db.exec({ |
| // Check for https://sqlite.org/forum/forumpost/895425b49a |
| sql: "pragma table_info('t')", |
| rowMode: 'object', |
| callback: function(row){ |
| ++counter; |
| T.assert(row.name==='a' || row.name==='b'); |
| } |
| }); |
| T.assert(2===counter); |
| })/*setup table T*/ |
| |
| //////////////////////////////////////////////////////////////////// |
| .t({ |
| name: "sqlite3_set_authorizer()", |
| predicate: ()=>!!wasm.exports.sqlite3_set_authorizer || "Missing sqlite3_set_authorizer()", |
| test:function(sqlite3){ |
| T.assert(capi.SQLITE_IGNORE>0) |
| .assert(capi.SQLITE_DENY>0); |
| const db = this.db; |
| const ssa = capi.sqlite3_set_authorizer; |
| const n = db.selectValue('select count(*) from t'); |
| T.assert(n>0); |
| let authCount = 0; |
| let rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){ |
| ++authCount; |
| return capi.SQLITE_IGNORE; |
| }, 0); |
| T.assert(0===rc) |
| .assert( |
| undefined === db.selectValue('select count(*) from t') |
| /* Note that the count() never runs, so we get undefined |
| instead of 0. */ |
| ) |
| .assert(authCount>0); |
| authCount = 0; |
| db.exec("update t set a=-9999"); |
| T.assert(authCount>0); |
| /* Reminder: we don't use DELETE because, from the C API docs: |
| |
| "If the action code is [SQLITE_DELETE] and the callback |
| returns [SQLITE_IGNORE] then the [DELETE] operation proceeds |
| but the [truncate optimization] is disabled and all rows are |
| deleted individually." |
| */ |
| rc = ssa(db, null, 0); |
| authCount = 0; |
| T.assert(-9999 != db.selectValue('select a from t')) |
| .assert(0===authCount); |
| rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){ |
| ++authCount; |
| return capi.SQLITE_DENY; |
| }, 0); |
| T.assert(0===rc); |
| let err; |
| try{ db.exec("select 1 from t") } |
| catch(e){ err = e } |
| T.assert(err instanceof sqlite3.SQLite3Error) |
| .assert(err.message.indexOf('not authorized'>0)) |
| .assert(1===authCount); |
| authCount = 0; |
| rc = ssa(db, function(...args){ |
| ++authCount; |
| return capi.SQLITE_OK; |
| }, 0); |
| T.assert(0===rc); |
| T.assert(n === db.selectValue('select count(*) from t')) |
| .assert(authCount>0); |
| authCount = 0; |
| rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){ |
| ++authCount; |
| throw new Error("Testing catching of authorizer."); |
| }, 0); |
| T.assert(0===rc); |
| authCount = 0; |
| err = undefined; |
| try{ db.exec("select 1 from t") } |
| catch(e){err = e} |
| T.assert(err instanceof Error) |
| .assert(err.message.indexOf('not authorized')>0) |
| /* Note that the thrown message is trumped/overwritten |
| by the authorizer process. */ |
| .assert(1===authCount); |
| rc = ssa(db, 0, 0); |
| authCount = 0; |
| T.assert(0===rc); |
| T.assert(n === db.selectValue('select count(*) from t')) |
| .assert(0===authCount); |
| } |
| })/*sqlite3_set_authorizer()*/ |
| |
| //////////////////////////////////////////////////////////////////////// |
| .t("sqlite3_table_column_metadata()", function(sqlite3){ |
| const stack = wasm.pstack.pointer; |
| try{ |
| const [pzDT, pzColl, pNotNull, pPK, pAuto] = |
| wasm.pstack.allocPtr(5); |
| const rc = capi.sqlite3_table_column_metadata( |
| this.db, "main", "t", "rowid", |
| pzDT, pzColl, pNotNull, pPK, pAuto |
| ); |
| T.assert(0===rc) |
| .assert("INTEGER"===wasm.cstrToJs(wasm.peekPtr(pzDT))) |
| .assert("BINARY"===wasm.cstrToJs(wasm.peekPtr(pzColl))) |
| .assert(0===wasm.peek32(pNotNull)) |
| .assert(1===wasm.peek32(pPK)) |
| .assert(0===wasm.peek32(pAuto)) |
| }finally{ |
| wasm.pstack.restore(stack); |
| } |
| }) |
| |
| //////////////////////////////////////////////////////////////////////// |
| .t('selectArray/Object()', function(sqlite3){ |
| const db = this.db; |
| let rc = db.selectArray('select a, b from t where a=?', 5); |
| T.assert(Array.isArray(rc)) |
| .assert(2===rc.length) |
| .assert(5===rc[0] && 6===rc[1]); |
| rc = db.selectArray('select a, b from t where b=-1'); |
| T.assert(undefined === rc); |
| rc = db.selectObject('select a A, b b from t where b=?', 6); |
| T.assert(rc && 'object'===typeof rc) |
| .assert(5===rc.A) |
| .assert(6===rc.b); |
| rc = db.selectArray('select a, b from t where b=-1'); |
| T.assert(undefined === rc); |
| }) |
| //////////////////////////////////////////////////////////////////////// |
| .t('selectArrays/Objects()', function(sqlite3){ |
| const db = this.db; |
| const sql = 'select a, b from t where a=? or b=? order by a'; |
| let rc = db.selectArrays(sql, [1, 4]); |
| T.assert(Array.isArray(rc)) |
| .assert(2===rc.length) |
| .assert(2===rc[0].length) |
| .assert(1===rc[0][0]) |
| .assert(2===rc[0][1]) |
| .assert(3===rc[1][0]) |
| .assert(4===rc[1][1]) |
| rc = db.selectArrays(sql, [99,99]); |
| T.assert(Array.isArray(rc)).assert(0===rc.length); |
| rc = db.selectObjects(sql, [1,4]); |
| T.assert(Array.isArray(rc)) |
| .assert(2===rc.length) |
| .assert('object' === typeof rc[1]) |
| .assert(1===rc[0].a) |
| .assert(2===rc[0].b) |
| .assert(3===rc[1].a) |
| .assert(4===rc[1].b); |
| }) |
| //////////////////////////////////////////////////////////////////////// |
| .t('selectArray/Object/Values() via INSERT/UPDATE...RETURNING', function(sqlite3){ |
| let rc = this.db.selectObject("INSERT INTO t(a,b) VALUES(83,84) RETURNING a as AA"); |
| T.assert(83===rc.AA); |
| rc = this.db.selectArray("UPDATE T set a=85 WHERE a=83 RETURNING b as BB"); |
| T.assert(Array.isArray(rc)).assert(84===rc[0]); |
| //log("select * from t:",this.db.selectObjects("select * from t order by a")); |
| rc = this.db.selectValues("UPDATE T set a=a*1 RETURNING a"); |
| T.assert(Array.isArray(rc)) |
| .assert(5 === rc.length) |
| .assert('number'===typeof rc[0]) |
| .assert(rc[0]|0 === rc[0] /* is small integer */); |
| }) |
| //////////////////////////////////////////////////////////////////////// |
| .t({ |
| name: 'sqlite3_js_db_export()', |
| predicate: ()=>true, |
| test: function(sqlite3){ |
| const db = this.db; |
| const xp = capi.sqlite3_js_db_export(db.pointer); |
| T.assert(xp instanceof Uint8Array) |
| .assert(xp.byteLength>0) |
| .assert(0 === xp.byteLength % 512); |
| this.dbExport = xp; |
| } |
| }/*sqlite3_js_db_export()*/) |
| .t({ |
| name: 'sqlite3_js_posix_create_file()', |
| predicate: ()=>true, |
| test: function(sqlite3){ |
| const db = this.db; |
| const filename = "sqlite3_js_posix_create_file.db"; |
| capi.sqlite3_js_posix_create_file(filename, this.dbExport); |
| delete this.dbExport; |
| const db2 = new sqlite3.oo1.DB(filename,'r'); |
| try { |
| const sql = "select count(*) from t"; |
| const n = db.selectValue(sql); |
| T.assert(n>0 && db2.selectValue(sql) === n); |
| }finally{ |
| db2.close(); |
| sqlite3.util.sqlite3__wasm_vfs_unlink(0, filename); |
| } |
| } |
| }/*sqlite3_js_posix_create_file()*/) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t({ |
| name:'Scalar UDFs', |
| test: function(sqlite3){ |
| const db = this.db; |
| db.createFunction("foo",(pCx,a,b)=>a+b); |
| T.assert(7===db.selectValue("select foo(3,4)")). |
| assert(5===db.selectValue("select foo(3,?)",2)). |
| assert(5===db.selectValue("select foo(?,?2)",[1,4])). |
| assert(5===db.selectValue("select foo($a,$b)",{$a:0,$b:5})); |
| db.createFunction("bar", { |
| arity: -1, |
| xFunc: (pCx,...args)=>{ |
| T.assert(db.pointer === capi.sqlite3_context_db_handle(pCx)); |
| let rc = 0; |
| for(const v of args) rc += v; |
| return rc; |
| } |
| }).createFunction({ |
| name: "asis", |
| xFunc: (pCx,arg)=>arg |
| }); |
| T.assert(0===db.selectValue("select bar()")). |
| assert(1===db.selectValue("select bar(1)")). |
| assert(3===db.selectValue("select bar(1,2)")). |
| assert(-1===db.selectValue("select bar(1,2,-4)")). |
| assert('hi' === db.selectValue("select asis('hi')")). |
| assert('hi' === db.selectValue("select ?",'hi')). |
| assert(null === db.selectValue("select null")). |
| assert(null === db.selectValue("select asis(null)")). |
| assert(1 === db.selectValue("select ?",1)). |
| assert(2 === db.selectValue("select ?",[2])). |
| assert(3 === db.selectValue("select $a",{$a:3})). |
| assert(T.eqApprox(3.1,db.selectValue("select 3.0 + 0.1"))). |
| assert(T.eqApprox(1.3,db.selectValue("select asis(1 + 0.3)"))); |
| |
| let blobArg = new Uint8Array([0x68, 0x69]); |
| let blobRc = db.selectValue( |
| "select asis(?1)", |
| blobArg.buffer/*confirm that ArrayBuffer is handled as a Uint8Array*/ |
| ); |
| T.assert(blobRc instanceof Uint8Array). |
| assert(2 === blobRc.length). |
| assert(0x68==blobRc[0] && 0x69==blobRc[1]); |
| blobRc = db.selectValue("select asis(X'6869')"); |
| T.assert(blobRc instanceof Uint8Array). |
| assert(2 === blobRc.length). |
| assert(0x68==blobRc[0] && 0x69==blobRc[1]); |
| |
| blobArg = new Int8Array([0x68, 0x69]); |
| //debug("blobArg=",blobArg); |
| blobRc = db.selectValue("select asis(?1)", blobArg); |
| T.assert(blobRc instanceof Uint8Array). |
| assert(2 === blobRc.length); |
| //debug("blobRc=",blobRc); |
| T.assert(0x68==blobRc[0] && 0x69==blobRc[1]); |
| |
| let rc = sqlite3.capi.sqlite3_create_function_v2( |
| this.db, "foo", 0, -1, 0, 0, 0, 0, 0 |
| ); |
| T.assert( |
| sqlite3.capi.SQLITE_FORMAT === rc, |
| "For invalid eTextRep argument." |
| ); |
| rc = sqlite3.capi.sqlite3_create_function_v2(this.db, "foo", 0); |
| T.assert( |
| sqlite3.capi.SQLITE_MISUSE === rc, |
| "For invalid arg count." |
| ); |
| |
| /* Confirm that we can map and unmap the same function with |
| multiple arities... */ |
| const fCounts = [0,0]; |
| const fArityCheck = function(pCx){ |
| return ++fCounts[arguments.length-1]; |
| }; |
| //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = true; |
| rc = capi.sqlite3_create_function_v2( |
| db, "nary", 0, capi.SQLITE_UTF8, 0, fArityCheck, 0, 0, 0 |
| ); |
| T.assert( 0===rc ); |
| rc = capi.sqlite3_create_function_v2( |
| db, "nary", 1, capi.SQLITE_UTF8, 0, fArityCheck, 0, 0, 0 |
| ); |
| T.assert( 0===rc ); |
| const sqlFArity0 = "select nary()"; |
| const sqlFArity1 = "select nary(1)"; |
| T.assert( 1 === db.selectValue(sqlFArity0) ) |
| .assert( 1 === fCounts[0] ).assert( 0 === fCounts[1] ); |
| T.assert( 1 === db.selectValue(sqlFArity1) ) |
| .assert( 1 === fCounts[0] ).assert( 1 === fCounts[1] ); |
| capi.sqlite3_create_function_v2( |
| db, "nary", 0, capi.SQLITE_UTF8, 0, 0, 0, 0, 0 |
| ); |
| T.mustThrowMatching((()=>db.selectValue(sqlFArity0)), |
| (e)=>((e instanceof sqlite3.SQLite3Error) |
| && e.message.indexOf("wrong number of arguments")>0), |
| "0-arity variant was uninstalled."); |
| T.assert( 2 === db.selectValue(sqlFArity1) ) |
| .assert( 1 === fCounts[0] ).assert( 2 === fCounts[1] ); |
| capi.sqlite3_create_function_v2( |
| db, "nary", 1, capi.SQLITE_UTF8, 0, 0, 0, 0, 0 |
| ); |
| T.mustThrowMatching((()=>db.selectValue(sqlFArity1)), |
| (e)=>((e instanceof sqlite3.SQLite3Error) |
| && e.message.indexOf("no such function")>0), |
| "1-arity variant was uninstalled."); |
| //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false; |
| } |
| }) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t({ |
| name: 'Aggregate UDFs', |
| //predicate: ()=>false, |
| test: function(sqlite3){ |
| const db = this.db; |
| const sjac = capi.sqlite3_js_aggregate_context; |
| db.createFunction({ |
| name: 'summer', |
| xStep: (pCtx, n)=>{ |
| const ac = sjac(pCtx, 4); |
| wasm.poke32(ac, wasm.peek32(ac) + Number(n)); |
| }, |
| xFinal: (pCtx)=>{ |
| const ac = sjac(pCtx, 0); |
| return ac ? wasm.peek32(ac) : 0; |
| } |
| }); |
| let v = db.selectValue([ |
| "with cte(v) as (", |
| "select 3 union all select 5 union all select 7", |
| ") select summer(v), summer(v+1) from cte" |
| /* ------------------^^^^^^^^^^^ ensures that we're handling |
| sqlite3_aggregate_context() properly. */ |
| ]); |
| T.assert(15===v); |
| T.mustThrowMatching(()=>db.selectValue("select summer(1,2)"), |
| /wrong number of arguments/); |
| |
| db.createFunction({ |
| name: 'summerN', |
| arity: -1, |
| xStep: (pCtx, ...args)=>{ |
| const ac = sjac(pCtx, 4); |
| let sum = wasm.peek32(ac); |
| for(const v of args) sum += Number(v); |
| wasm.poke32(ac, sum); |
| }, |
| xFinal: (pCtx)=>{ |
| const ac = sjac(pCtx, 0); |
| capi.sqlite3_result_int( pCtx, ac ? wasm.peek32(ac) : 0 ); |
| // xFinal() may either return its value directly or call |
| // sqlite3_result_xyz() and return undefined. Both are |
| // functionally equivalent. |
| } |
| }); |
| T.assert(18===db.selectValue('select summerN(1,8,9), summerN(2,3,4)')); |
| T.mustThrowMatching(()=>{ |
| db.createFunction('nope',{ |
| xFunc: ()=>{}, xStep: ()=>{} |
| }); |
| }, /scalar or aggregate\?/); |
| T.mustThrowMatching(()=>{ |
| db.createFunction('nope',{xStep: ()=>{}}); |
| }, /Missing xFinal/); |
| T.mustThrowMatching(()=>{ |
| db.createFunction('nope',{xFinal: ()=>{}}); |
| }, /Missing xStep/); |
| T.mustThrowMatching(()=>{ |
| db.createFunction('nope',{}); |
| }, /Missing function-type properties/); |
| T.mustThrowMatching(()=>{ |
| db.createFunction('nope',{xFunc:()=>{}, xDestroy:'nope'}); |
| }, /xDestroy property must be a function/); |
| T.mustThrowMatching(()=>{ |
| db.createFunction('nope',{xFunc:()=>{}, pApp:'nope'}); |
| }, /Invalid value for pApp/); |
| } |
| }/*aggregate UDFs*/) |
| |
| //////////////////////////////////////////////////////////////////////// |
| .t({ |
| name: 'Aggregate UDFs (64-bit)', |
| predicate: ()=>wasm.bigIntEnabled, |
| //predicate: ()=>false, |
| test: function(sqlite3){ |
| const db = this.db; |
| const sjac = capi.sqlite3_js_aggregate_context; |
| db.createFunction({ |
| name: 'summer64', |
| xStep: (pCtx, n)=>{ |
| const ac = sjac(pCtx, 8); |
| wasm.poke64(ac, wasm.peek64(ac) + BigInt(n)); |
| }, |
| xFinal: (pCtx)=>{ |
| const ac = sjac(pCtx, 0); |
| return ac ? wasm.peek64(ac) : 0n; |
| } |
| }); |
| let v = db.selectValue([ |
| "with cte(v) as (", |
| "select 9007199254740991 union all select 1 union all select 2", |
| ") select summer64(v), summer64(v+1) from cte" |
| ]); |
| T.assert(9007199254740994n===v); |
| } |
| }/*aggregate UDFs*/) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t({ |
| name: 'Window UDFs', |
| predicate: (sqlite3)=>!!sqlite3.wasm.exports.sqlite3_create_window_function |
| /*!sqlite3.wasm.compileOptionUsed('OMIT_WINDOWFUNC')*/ |
| || "Missing window functions", |
| test: function(){ |
| /* Example window function, table, and results taken from: |
| https://sqlite.org/windowfunctions.html#udfwinfunc */ |
| const db = this.db; |
| const sjac = (cx,n=4)=>capi.sqlite3_js_aggregate_context(cx,n); |
| const xValueFinal = (pCtx)=>{ |
| const ac = sjac(pCtx, 0); |
| return ac ? wasm.peek32(ac) : 0; |
| }; |
| const xStepInverse = (pCtx, n)=>{ |
| const ac = sjac(pCtx); |
| wasm.poke32(ac, wasm.peek32(ac) + Number(n)); |
| }; |
| db.createFunction({ |
| name: 'winsumint', |
| xStep: (pCtx, n)=>xStepInverse(pCtx, n), |
| xInverse: (pCtx, n)=>xStepInverse(pCtx, -n), |
| xFinal: xValueFinal, |
| xValue: xValueFinal |
| }); |
| db.exec([ |
| "CREATE TEMP TABLE twin(x, y); INSERT INTO twin VALUES", |
| "('a', 4),('b', 5),('c', 3),('d', 8),('e', 1)" |
| ]); |
| let rc = db.exec({ |
| returnValue: 'resultRows', |
| sql:[ |
| "SELECT x, winsumint(y) OVER (", |
| "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING", |
| ") AS sum_y ", |
| "FROM twin ORDER BY x;" |
| ] |
| }); |
| T.assert(Array.isArray(rc)) |
| .assert(5 === rc.length); |
| let count = 0; |
| for(const row of rc){ |
| switch(++count){ |
| case 1: T.assert('a'===row[0] && 9===row[1]); break; |
| case 2: T.assert('b'===row[0] && 12===row[1]); break; |
| case 3: T.assert('c'===row[0] && 16===row[1]); break; |
| case 4: T.assert('d'===row[0] && 12===row[1]); break; |
| case 5: T.assert('e'===row[0] && 9===row[1]); break; |
| default: toss("Too many rows to window function."); |
| } |
| } |
| const resultRows = []; |
| rc = db.exec({ |
| resultRows, |
| returnValue: 'resultRows', |
| sql:[ |
| "SELECT x, winsumint(y) OVER (", |
| "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING", |
| ") AS sum_y ", |
| "FROM twin ORDER BY x;" |
| ] |
| }); |
| T.assert(rc === resultRows) |
| .assert(5 === rc.length); |
| |
| rc = db.exec({ |
| returnValue: 'saveSql', |
| sql: "select 1; select 2; -- empty\n; select 3" |
| }); |
| T.assert(Array.isArray(rc)) |
| .assert(3===rc.length) |
| .assert('select 1;' === rc[0]) |
| .assert('select 2;' === rc[1]) |
| .assert('-- empty\n; select 3' === rc[2] |
| /* Strange but true. */); |
| T.mustThrowMatching(()=>{ |
| db.exec({sql:'', returnValue: 'nope'}); |
| }, /^Invalid returnValue/); |
| |
| db.exec("DROP TABLE twin"); |
| } |
| }/*window UDFs*/) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t("ATTACH", function(){ |
| const db = this.db; |
| const resultRows = []; |
| db.exec({ |
| sql:new TextEncoder('utf-8').encode([ |
| // ^^^ testing string-vs-typedarray handling in exec() |
| "attach 'session' as foo;", |
| "create table foo.bar(a);", |
| "insert into foo.bar(a) values(1),(2),(3);", |
| "select a from foo.bar order by a;" |
| ].join('')), |
| rowMode: 0, |
| resultRows |
| }); |
| T.assert(3===resultRows.length) |
| .assert(2===resultRows[1]); |
| T.assert(2===db.selectValue('select a from foo.bar where a>1 order by a')); |
| |
| /** Demonstrate the JS-simplified form of the sqlite3_exec() callback... */ |
| let colCount = 0, rowCount = 0; |
| let rc = capi.sqlite3_exec( |
| db, "select a, a*2 from foo.bar", function(aVals, aNames){ |
| //console.warn("execCallback(",arguments,")"); |
| colCount = aVals.length; |
| ++rowCount; |
| T.assert(2===aVals.length) |
| .assert(2===aNames.length) |
| .assert(+(aVals[1]) === 2 * +(aVals[0])); |
| }, 0, 0 |
| ); |
| T.assert(0===rc).assert(3===rowCount).assert(2===colCount); |
| rc = capi.sqlite3_exec( |
| db.pointer, "select a from foo.bar", ()=>{ |
| tossQuietly("Testing throwing from exec() callback."); |
| }, 0, 0 |
| ); |
| T.assert(capi.SQLITE_ABORT === rc); |
| |
| /* Demonstrate how to get access to the "full" callback |
| signature, as opposed to the simplified JS-specific one... */ |
| rowCount = colCount = 0; |
| const pCb = wasm.installFunction('i(pipp)', function(pVoid,nCols,aVals,aCols){ |
| /* Tip: wasm.cArgvToJs() can be used to convert aVals and |
| aCols to arrays: const vals = wasm.cArgvToJs(nCols, |
| aVals); */ |
| ++rowCount; |
| colCount = nCols; |
| T.assert(2 === nCols) |
| .assert(wasm.isPtr(pVoid)) |
| .assert(wasm.isPtr(aVals)) |
| .assert(wasm.isPtr(aCols)) |
| .assert(+wasm.cstrToJs(wasm.peekPtr(aVals + wasm.ptrSizeof)) |
| === 2 * +wasm.cstrToJs(wasm.peekPtr(aVals))); |
| return 0; |
| }); |
| try { |
| T.assert(wasm.isPtr(pCb)); |
| rc = capi.sqlite3_exec( |
| db, new TextEncoder('utf-8').encode("select a, a*2 from foo.bar"), |
| pCb, 0, 0 |
| ); |
| T.assert(0===rc) |
| .assert(3===rowCount) |
| .assert(2===colCount); |
| }finally{ |
| wasm.uninstallFunction(pCb); |
| } |
| |
| // Demonstrate that an OOM result does not propagate through sqlite3_exec()... |
| rc = capi.sqlite3_exec( |
| db, ["select a,"," a*2 from foo.bar"], (aVals, aNames)=>{ |
| sqlite3.WasmAllocError.toss("just testing"); |
| }, 0, 0 |
| ); |
| T.assert(capi.SQLITE_ABORT === rc); |
| |
| db.exec("detach foo"); |
| T.mustThrow(()=>db.exec("select * from foo.bar"), |
| "Because foo is no longer attached."); |
| }) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t("Read-only", function(sqlite3){ |
| T.assert( 0===capi.sqlite3_db_readonly(this.db, "main") ); |
| const db = new sqlite3.oo1.DB('file://'+this.db.filename+'?mode=ro'); |
| T.assert( 1===capi.sqlite3_db_readonly(db, "main") ); |
| T.assert( -1===capi.sqlite3_db_readonly(db, "nope") ); |
| db.close(); |
| }) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t({ |
| name: 'C-side WASM tests', |
| predicate: ()=>(haveWasmCTests() || "Not compiled in."), |
| test: function(){ |
| const w = wasm, db = this.db; |
| const stack = w.scopedAllocPush(); |
| let ptrInt; |
| const origValue = 512; |
| try{ |
| ptrInt = w.scopedAlloc(4); |
| w.poke32(ptrInt,origValue); |
| const cf = w.xGet('sqlite3__wasm_test_intptr'); |
| const oldPtrInt = ptrInt; |
| T.assert(origValue === w.peek32(ptrInt)); |
| const rc = cf(ptrInt); |
| T.assert(2*origValue === rc). |
| assert(rc === w.peek32(ptrInt)). |
| assert(oldPtrInt === ptrInt); |
| const pi64 = w.scopedAlloc(8)/*ptr to 64-bit integer*/; |
| const o64 = 0x010203040506/*>32-bit integer*/; |
| if(w.bigIntEnabled){ |
| w.poke64(pi64, o64); |
| //log("pi64 =",pi64, "o64 = 0x",o64.toString(16), o64); |
| const v64 = ()=>w.peek64(pi64) |
| T.assert(v64() == o64); |
| //T.assert(o64 === w.peek64(pi64)); |
| const cf64w = w.xGet('sqlite3__wasm_test_int64ptr'); |
| cf64w(pi64); |
| T.assert(v64() == BigInt(2 * o64)); |
| cf64w(pi64); |
| T.assert(v64() == BigInt(4 * o64)); |
| |
| const biTimes2 = w.xGet('sqlite3__wasm_test_int64_times2'); |
| T.assert(BigInt(2 * o64) === |
| biTimes2(BigInt(o64)/*explicit conv. required to avoid TypeError |
| in the call :/ */)); |
| |
| const pMin = w.scopedAlloc(16); |
| const pMax = pMin + 8; |
| const g64 = (p)=>w.peek64(p); |
| w.poke64([pMin, pMax], 0); |
| const minMaxI64 = [ |
| w.xCall('sqlite3__wasm_test_int64_min'), |
| w.xCall('sqlite3__wasm_test_int64_max') |
| ]; |
| T.assert(minMaxI64[0] < BigInt(Number.MIN_SAFE_INTEGER)). |
| assert(minMaxI64[1] > BigInt(Number.MAX_SAFE_INTEGER)); |
| //log("int64_min/max() =",minMaxI64, typeof minMaxI64[0]); |
| w.xCall('sqlite3__wasm_test_int64_minmax', pMin, pMax); |
| T.assert(g64(pMin) === minMaxI64[0], "int64 mismatch"). |
| assert(g64(pMax) === minMaxI64[1], "int64 mismatch"); |
| //log("pMin",g64(pMin), "pMax",g64(pMax)); |
| w.poke64(pMin, minMaxI64[0]); |
| T.assert(g64(pMin) === minMaxI64[0]). |
| assert(minMaxI64[0] === db.selectValue("select ?",g64(pMin))). |
| assert(minMaxI64[1] === db.selectValue("select ?",g64(pMax))); |
| const rxRange = /too big/; |
| T.mustThrowMatching(()=>{db.prepare("select ?").bind(minMaxI64[0] - BigInt(1))}, |
| rxRange). |
| mustThrowMatching(()=>{db.prepare("select ?").bind(minMaxI64[1] + BigInt(1))}, |
| (e)=>rxRange.test(e.message)); |
| }else{ |
| log("No BigInt support. Skipping related tests."); |
| log("\"The problem\" here is that we can manipulate, at the byte level,", |
| "heap memory to set 64-bit values, but we can't get those values", |
| "back into JS because of the lack of 64-bit integer support."); |
| } |
| }finally{ |
| const x = w.scopedAlloc(1), y = w.scopedAlloc(1), z = w.scopedAlloc(1); |
| //log("x=",x,"y=",y,"z=",z); // just looking at the alignment |
| w.scopedAllocPop(stack); |
| } |
| } |
| }/* jaccwabyt-specific tests */) |
| |
| //////////////////////////////////////////////////////////////////////// |
| .t({ |
| name: 'virtual table #1: eponymous w/ manual exception handling', |
| predicate: (sqlite3)=>!!sqlite3.capi.sqlite3_vtab || "Missing vtab support", |
| test: function(sqlite3){ |
| const VT = sqlite3.vtab; |
| const tmplCols = Object.assign(Object.create(null),{ |
| A: 0, B: 1 |
| }); |
| /** |
| The vtab demonstrated here is a JS-ification of |
| ext/misc/templatevtab.c. |
| */ |
| const tmplMod = new sqlite3.capi.sqlite3_module(); |
| T.assert(0===tmplMod.$xUpdate); |
| tmplMod.setupModule({ |
| catchExceptions: false, |
| methods: { |
| xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){ |
| try{ |
| const args = wasm.cArgvToJs(argc, argv); |
| T.assert(args.length>=3) |
| .assert(args[0] === 'testvtab') |
| .assert(args[1] === 'main') |
| .assert(args[2] === 'testvtab'); |
| //console.debug("xConnect() args =",args); |
| const rc = capi.sqlite3_declare_vtab( |
| pDb, "CREATE TABLE ignored(a,b)" |
| ); |
| if(0===rc){ |
| const t = VT.xVtab.create(ppVtab); |
| T.assert(t === VT.xVtab.get(wasm.peekPtr(ppVtab))); |
| } |
| return rc; |
| }catch(e){ |
| if(!(e instanceof sqlite3.WasmAllocError)){ |
| wasm.dealloc(wasm.peekPtr, pzErr); |
| wasm.pokePtr(pzErr, wasm.allocCString(e.message)); |
| } |
| return VT.xError('xConnect',e); |
| } |
| }, |
| xCreate: true /* just for testing. Will be removed afterwards. */, |
| xDisconnect: function(pVtab){ |
| try { |
| VT.xVtab.unget(pVtab).dispose(); |
| return 0; |
| }catch(e){ |
| return VT.xError('xDisconnect',e); |
| } |
| }, |
| xOpen: function(pVtab, ppCursor){ |
| try{ |
| const t = VT.xVtab.get(pVtab), |
| c = VT.xCursor.create(ppCursor); |
| T.assert(t instanceof capi.sqlite3_vtab) |
| .assert(c instanceof capi.sqlite3_vtab_cursor); |
| c._rowId = 0; |
| return 0; |
| }catch(e){ |
| return VT.xError('xOpen',e); |
| } |
| }, |
| xClose: function(pCursor){ |
| try{ |
| const c = VT.xCursor.unget(pCursor); |
| T.assert(c instanceof capi.sqlite3_vtab_cursor) |
| .assert(!VT.xCursor.get(pCursor)); |
| c.dispose(); |
| return 0; |
| }catch(e){ |
| return VT.xError('xClose',e); |
| } |
| }, |
| xNext: function(pCursor){ |
| try{ |
| const c = VT.xCursor.get(pCursor); |
| ++c._rowId; |
| return 0; |
| }catch(e){ |
| return VT.xError('xNext',e); |
| } |
| }, |
| xColumn: function(pCursor, pCtx, iCol){ |
| try{ |
| const c = VT.xCursor.get(pCursor); |
| switch(iCol){ |
| case tmplCols.A: |
| capi.sqlite3_result_int(pCtx, 1000 + c._rowId); |
| break; |
| case tmplCols.B: |
| capi.sqlite3_result_int(pCtx, 2000 + c._rowId); |
| break; |
| default: sqlite3.SQLite3Error.toss("Invalid column id",iCol); |
| } |
| return 0; |
| }catch(e){ |
| return VT.xError('xColumn',e); |
| } |
| }, |
| xRowid: function(pCursor, ppRowid64){ |
| try{ |
| const c = VT.xCursor.get(pCursor); |
| VT.xRowid(ppRowid64, c._rowId); |
| return 0; |
| }catch(e){ |
| return VT.xError('xRowid',e); |
| } |
| }, |
| xEof: function(pCursor){ |
| const c = VT.xCursor.get(pCursor), |
| rc = c._rowId>=10; |
| return rc; |
| }, |
| xFilter: function(pCursor, idxNum, idxCStr, |
| argc, argv/* [sqlite3_value* ...] */){ |
| try{ |
| const c = VT.xCursor.get(pCursor); |
| c._rowId = 0; |
| const list = capi.sqlite3_values_to_js(argc, argv); |
| T.assert(argc === list.length); |
| //log(argc,"xFilter value(s):",list); |
| return 0; |
| }catch(e){ |
| return VT.xError('xFilter',e); |
| } |
| }, |
| xBestIndex: function(pVtab, pIdxInfo){ |
| try{ |
| //const t = VT.xVtab.get(pVtab); |
| const sii = capi.sqlite3_index_info; |
| const pii = new sii(pIdxInfo); |
| pii.$estimatedRows = 10; |
| pii.$estimatedCost = 10.0; |
| //log("xBestIndex $nConstraint =",pii.$nConstraint); |
| if(pii.$nConstraint>0){ |
| // Validate nthConstraint() and nthConstraintUsage() |
| const max = pii.$nConstraint; |
| for(let i=0; i < max; ++i ){ |
| let v = pii.nthConstraint(i,true); |
| T.assert(wasm.isPtr(v)); |
| v = pii.nthConstraint(i); |
| T.assert(v instanceof sii.sqlite3_index_constraint) |
| .assert(v.pointer >= pii.$aConstraint); |
| v.dispose(); |
| v = pii.nthConstraintUsage(i,true); |
| T.assert(wasm.isPtr(v)); |
| v = pii.nthConstraintUsage(i); |
| T.assert(v instanceof sii.sqlite3_index_constraint_usage) |
| .assert(v.pointer >= pii.$aConstraintUsage); |
| v.$argvIndex = i;//just to get some values into xFilter |
| v.dispose(); |
| } |
| } |
| //log("xBestIndex $nOrderBy =",pii.$nOrderBy); |
| if(pii.$nOrderBy>0){ |
| // Validate nthOrderBy() |
| const max = pii.$nOrderBy; |
| for(let i=0; i < max; ++i ){ |
| let v = pii.nthOrderBy(i,true); |
| T.assert(wasm.isPtr(v)); |
| v = pii.nthOrderBy(i); |
| T.assert(v instanceof sii.sqlite3_index_orderby) |
| .assert(v.pointer >= pii.$aOrderBy); |
| v.dispose(); |
| } |
| } |
| pii.dispose(); |
| return 0; |
| }catch(e){ |
| return VT.xError('xBestIndex',e); |
| } |
| } |
| } |
| }); |
| this.db.onclose.disposeAfter.push(tmplMod); |
| T.assert(0===tmplMod.$xUpdate) |
| .assert(tmplMod.$xCreate) |
| .assert(tmplMod.$xCreate === tmplMod.$xConnect, |
| "setup() must make these equivalent and "+ |
| "installMethods() must avoid re-compiling identical functions"); |
| tmplMod.$xCreate = 0 /* make tmplMod eponymous-only */; |
| let rc = capi.sqlite3_create_module( |
| this.db, "testvtab", tmplMod, 0 |
| ); |
| this.db.checkRc(rc); |
| const list = this.db.selectArrays( |
| "SELECT a,b FROM testvtab where a<9999 and b>1 order by a, b" |
| /* Query is shaped so that it will ensure that some constraints |
| end up in xBestIndex(). */ |
| ); |
| T.assert(10===list.length) |
| .assert(1000===list[0][0]) |
| .assert(2009===list[list.length-1][1]); |
| } |
| })/*custom vtab #1*/ |
| |
| //////////////////////////////////////////////////////////////////////// |
| .t({ |
| name: 'virtual table #2: non-eponymous w/ automated exception wrapping', |
| predicate: (sqlite3)=>!!sqlite3.capi.sqlite3_vtab || "Missing vtab support", |
| test: function(sqlite3){ |
| const VT = sqlite3.vtab; |
| const tmplCols = Object.assign(Object.create(null),{ |
| A: 0, B: 1 |
| }); |
| /** |
| The vtab demonstrated here is a JS-ification of |
| ext/misc/templatevtab.c. |
| */ |
| let throwOnCreate = 1 ? 0 : capi.SQLITE_CANTOPEN |
| /* ^^^ just for testing exception wrapping. Note that sqlite |
| always translates errors from a vtable to a generic |
| SQLITE_ERROR unless it's from xConnect()/xCreate() and that |
| callback sets an error string. */; |
| const vtabTrace = 1 |
| ? ()=>{} |
| : (methodName,...args)=>console.debug('sqlite3_module::'+methodName+'():',...args); |
| const modConfig = { |
| /* catchExceptions changes how the methods are wrapped */ |
| catchExceptions: true, |
| name: "vtab2test", |
| methods:{ |
| xCreate: function(pDb, pAux, argc, argv, ppVtab, pzErr){ |
| vtabTrace("xCreate",...arguments); |
| if(throwOnCreate){ |
| sqlite3.SQLite3Error.toss( |
| throwOnCreate, |
| "Throwing a test exception." |
| ); |
| } |
| const args = wasm.cArgvToJs(argc, argv); |
| vtabTrace("xCreate","argv:",args); |
| T.assert(args.length>=3); |
| const rc = capi.sqlite3_declare_vtab( |
| pDb, "CREATE TABLE ignored(a,b)" |
| ); |
| if(0===rc){ |
| const t = VT.xVtab.create(ppVtab); |
| T.assert(t === VT.xVtab.get(wasm.peekPtr(ppVtab))); |
| vtabTrace("xCreate",...arguments," ppVtab =",t.pointer); |
| } |
| return rc; |
| }, |
| xConnect: true, |
| xDestroy: function(pVtab){ |
| vtabTrace("xDestroy/xDisconnect",pVtab); |
| VT.xVtab.dispose(pVtab); |
| }, |
| xDisconnect: true, |
| xOpen: function(pVtab, ppCursor){ |
| const t = VT.xVtab.get(pVtab), |
| c = VT.xCursor.create(ppCursor); |
| T.assert(t instanceof capi.sqlite3_vtab) |
| .assert(c instanceof capi.sqlite3_vtab_cursor); |
| vtabTrace("xOpen",...arguments," cursor =",c.pointer); |
| c._rowId = 0; |
| }, |
| xClose: function(pCursor){ |
| vtabTrace("xClose",...arguments); |
| const c = VT.xCursor.unget(pCursor); |
| T.assert(c instanceof capi.sqlite3_vtab_cursor) |
| .assert(!VT.xCursor.get(pCursor)); |
| c.dispose(); |
| }, |
| xNext: function(pCursor){ |
| vtabTrace("xNext",...arguments); |
| const c = VT.xCursor.get(pCursor); |
| ++c._rowId; |
| }, |
| xColumn: function(pCursor, pCtx, iCol){ |
| vtabTrace("xColumn",...arguments); |
| const c = VT.xCursor.get(pCursor); |
| switch(iCol){ |
| case tmplCols.A: |
| capi.sqlite3_result_int(pCtx, 1000 + c._rowId); |
| break; |
| case tmplCols.B: |
| capi.sqlite3_result_int(pCtx, 2000 + c._rowId); |
| break; |
| default: sqlite3.SQLite3Error.toss("Invalid column id",iCol); |
| } |
| }, |
| xRowid: function(pCursor, ppRowid64){ |
| vtabTrace("xRowid",...arguments); |
| const c = VT.xCursor.get(pCursor); |
| VT.xRowid(ppRowid64, c._rowId); |
| }, |
| xEof: function(pCursor){ |
| vtabTrace("xEof",...arguments); |
| return VT.xCursor.get(pCursor)._rowId>=10; |
| }, |
| xFilter: function(pCursor, idxNum, idxCStr, |
| argc, argv/* [sqlite3_value* ...] */){ |
| vtabTrace("xFilter",...arguments); |
| const c = VT.xCursor.get(pCursor); |
| c._rowId = 0; |
| const list = capi.sqlite3_values_to_js(argc, argv); |
| T.assert(argc === list.length); |
| }, |
| xBestIndex: function(pVtab, pIdxInfo){ |
| vtabTrace("xBestIndex",...arguments); |
| //const t = VT.xVtab.get(pVtab); |
| const pii = VT.xIndexInfo(pIdxInfo); |
| pii.$estimatedRows = 10; |
| pii.$estimatedCost = 10.0; |
| pii.dispose(); |
| } |
| }/*methods*/ |
| }; |
| const tmplMod = VT.setupModule(modConfig); |
| T.assert(1===tmplMod.$iVersion); |
| this.db.onclose.disposeAfter.push(tmplMod); |
| this.db.checkRc(capi.sqlite3_create_module( |
| this.db.pointer, modConfig.name, tmplMod.pointer, 0 |
| )); |
| this.db.exec([ |
| "create virtual table testvtab2 using ", |
| modConfig.name, |
| "(arg1 blah, arg2 bloop)" |
| ]); |
| if(0){ |
| /* If we DROP TABLE then xDestroy() is called. If the |
| vtab is instead destroyed when the db is closed, |
| xDisconnect() is called. */ |
| this.db.onclose.disposeBefore.push(function(db){ |
| console.debug("Explicitly dropping testvtab2 via disposeBefore handler..."); |
| db.exec( |
| /** DROP TABLE is the only way to get xDestroy() to be called. */ |
| "DROP TABLE testvtab2" |
| ); |
| }); |
| } |
| let list = this.db.selectArrays( |
| "SELECT a,b FROM testvtab2 where a<9999 and b>1 order by a, b" |
| /* Query is shaped so that it will ensure that some |
| constraints end up in xBestIndex(). */ |
| ); |
| T.assert(10===list.length) |
| .assert(1000===list[0][0]) |
| .assert(2009===list[list.length-1][1]); |
| |
| list = this.db.selectArrays( |
| "SELECT a,b FROM testvtab2 where a<9999 and b>1 order by b, a limit 5" |
| ); |
| T.assert(5===list.length) |
| .assert(1000===list[0][0]) |
| .assert(2004===list[list.length-1][1]); |
| |
| // Call it as a table-valued function... |
| list = this.db.selectArrays([ |
| "SELECT a,b FROM ", modConfig.name, |
| " where a<9999 and b>1 order by b, a limit 1" |
| ]); |
| T.assert(1===list.length) |
| .assert(1000===list[0][0]) |
| .assert(2000===list[0][1]); |
| } |
| })/*custom vtab #2*/ |
| //////////////////////////////////////////////////////////////////////// |
| .t('Custom collation', function(sqlite3){ |
| let collationCounter = 0; |
| let myCmp = function(pArg,n1,p1,n2,p2){ |
| //int (*)(void*,int,const void*,int,const void*) |
| ++collationCounter; |
| const rc = wasm.exports.sqlite3_strnicmp(p1,p2,(n1<n2?n1:n2)); |
| return rc ? rc : (n1 - n2); |
| }; |
| let rc = capi.sqlite3_create_collation_v2(this.db, "mycollation", capi.SQLITE_UTF8, |
| 0, myCmp, 0); |
| this.db.checkRc(rc); |
| rc = this.db.selectValue("select 'hi' = 'HI' collate mycollation"); |
| T.assert(1===rc).assert(1===collationCounter); |
| rc = this.db.selectValue("select 'hii' = 'HI' collate mycollation"); |
| T.assert(0===rc).assert(2===collationCounter); |
| rc = this.db.selectValue("select 'hi' = 'HIi' collate mycollation"); |
| T.assert(0===rc).assert(3===collationCounter); |
| rc = capi.sqlite3_create_collation(this.db,"hi",capi.SQLITE_UTF8/*not enough args*/); |
| T.assert(capi.SQLITE_MISUSE === rc); |
| rc = capi.sqlite3_create_collation_v2(this.db,"hi",capi.SQLITE_UTF8+1/*invalid encoding*/,0,0,0); |
| T.assert(capi.SQLITE_FORMAT === rc) |
| .mustThrowMatching(()=>this.db.checkRc(rc), |
| /SQLITE_UTF8 is the only supported encoding./); |
| /* |
| We need to ensure that replacing that collation function does |
| the right thing. We don't have a handle to the underlying WASM |
| pointer from here, so cannot verify (without digging through |
| internal state) that the old one gets uninstalled, but we can |
| verify that a new one properly replaces it. (That said, |
| console.warn() output has shown that the uninstallation does |
| happen.) |
| */ |
| collationCounter = 0; |
| myCmp = function(pArg,n1,p1,n2,p2){ |
| --collationCounter; |
| return 0; |
| }; |
| rc = capi.sqlite3_create_collation_v2(this.db, "MYCOLLATION", capi.SQLITE_UTF8, |
| 0, myCmp, 0); |
| this.db.checkRc(rc); |
| rc = this.db.selectValue("select 'hi' = 'HI' collate mycollation"); |
| T.assert(rc>0).assert(-1===collationCounter); |
| rc = this.db.selectValue("select 'a' = 'b' collate mycollation"); |
| T.assert(rc>0).assert(-2===collationCounter); |
| rc = capi.sqlite3_create_collation_v2(this.db, "MYCOLLATION", capi.SQLITE_UTF8, |
| 0, null, 0); |
| this.db.checkRc(rc); |
| rc = 0; |
| try { |
| this.db.selectValue("select 'a' = 'b' collate mycollation"); |
| }catch(e){ |
| /* Why is e.resultCode not automatically an extended result |
| code? The DB() class enables those automatically. */ |
| rc = sqlite3.capi.sqlite3_extended_errcode(this.db); |
| } |
| T.assert(capi.SQLITE_ERROR_MISSING_COLLSEQ === rc); |
| })/*custom collation*/ |
| |
| //////////////////////////////////////////////////////////////////////// |
| .t('Close db', function(){ |
| T.assert(this.db).assert(wasm.isPtr(this.db.pointer)); |
| //wasm.sqlite3__wasm_db_reset(this.db); // will leak virtual tables! |
| this.db.close(); |
| T.assert(!this.db.pointer); |
| }) |
| ;/* end of oo1 checks */ |
| |
| //////////////////////////////////////////////////////////////////////// |
| T.g('kvvfs') |
| .t({ |
| name: 'kvvfs is disabled in worker', |
| predicate: ()=>(isWorker() || "test is only valid in a Worker"), |
| test: function(sqlite3){ |
| T.assert( |
| !capi.sqlite3_vfs_find('kvvfs'), |
| "Expecting kvvfs to be unregistered." |
| ); |
| } |
| }) |
| .t({ |
| name: 'kvvfs in main thread', |
| 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; |
| const encOpt1 = 1 |
| ? {textkey: 'foo'} |
| : {key: 'foo'}; |
| const encOpt2 = encOpt1.textkey |
| ? encOpt1 |
| : {hexkey: new Uint8Array([0x66,0x6f,0x6f]/*==>"foo"*/)} |
| try{ |
| db = new this.JDb({ |
| filename: this.kvvfsDbFile, |
| ...encOpt1 |
| }); |
| 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', |
| ...encOpt2 |
| }); |
| 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", |
| test: function(sqlite3){ |
| let countCommit = 0, countRollback = 0;; |
| const db = new sqlite3.oo1.DB(':memory:',1 ? 'c' : 'ct'); |
| let rc = capi.sqlite3_commit_hook(db, (p)=>{ |
| ++countCommit; |
| return (1 === p) ? 0 : capi.SQLITE_ERROR; |
| }, 1); |
| T.assert( 0 === rc /*void pointer*/ ); |
| |
| // Commit hook... |
| T.assert( 0!=capi.sqlite3_get_autocommit(db) ); |
| db.exec("BEGIN; SELECT 1; COMMIT"); |
| T.assert(0 === countCommit, |
| "No-op transactions (mostly) do not trigger commit hook."); |
| db.exec("BEGIN EXCLUSIVE; SELECT 1; COMMIT"); |
| T.assert(1 === countCommit, |
| "But EXCLUSIVE transactions do."); |
| db.transaction((d)=>{ |
| T.assert( 0==capi.sqlite3_get_autocommit(db) ); |
| d.exec("create table t(a)"); |
| }); |
| T.assert(2 === countCommit); |
| |
| // Rollback hook: |
| rc = capi.sqlite3_rollback_hook(db, (p)=>{ |
| ++countRollback; |
| T.assert( 2 === p ); |
| }, 2); |
| T.assert( 0 === rc /*void pointer*/ ); |
| T.mustThrowMatching(()=>{ |
| db.transaction('drop table t',()=>{}) |
| }, (e)=>{ |
| return (capi.SQLITE_MISUSE === e.resultCode) |
| && ( e.message.indexOf('Invalid argument') > 0 ); |
| }); |
| T.assert(0 === countRollback, "Transaction was not started."); |
| T.mustThrowMatching(()=>{ |
| db.transaction('immediate', ()=>{ |
| sqlite3.SQLite3Error.toss(capi.SQLITE_FULL,'testing rollback hook'); |
| }); |
| }, (e)=>{ |
| return capi.SQLITE_FULL === e.resultCode |
| }); |
| T.assert(1 === countRollback); |
| |
| // Update hook... |
| const countUpdate = Object.create(null); |
| capi.sqlite3_update_hook(db, (p,op,dbName,tbl,rowid)=>{ |
| T.assert('main' === dbName.toLowerCase()) |
| .assert('t' === tbl.toLowerCase()) |
| .assert(3===p) |
| .assert('bigint' === typeof rowid); |
| switch(op){ |
| case capi.SQLITE_INSERT: |
| case capi.SQLITE_UPDATE: |
| case capi.SQLITE_DELETE: |
| countUpdate[op] = (countUpdate[op]||0) + 1; |
| break; |
| default: toss("Unexpected hook operator:",op); |
| } |
| }, 3); |
| db.transaction((d)=>{ |
| d.exec([ |
| "insert into t(a) values(1);", |
| "update t set a=2;", |
| "update t set a=3;", |
| "delete from t where a=3" |
| // update hook is not called for an unqualified DELETE |
| ]); |
| }); |
| T.assert(1 === countRollback) |
| .assert(3 === countCommit) |
| .assert(1 === countUpdate[capi.SQLITE_INSERT]) |
| .assert(2 === countUpdate[capi.SQLITE_UPDATE]) |
| .assert(1 === countUpdate[capi.SQLITE_DELETE]); |
| //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = true; |
| T.assert(1 === capi.sqlite3_commit_hook(db, 0, 0)); |
| T.assert(2 === capi.sqlite3_rollback_hook(db, 0, 0)); |
| T.assert(3 === capi.sqlite3_update_hook(db, 0, 0)); |
| //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false; |
| db.close(); |
| } |
| })/* commit/rollback/update hooks */ |
| .t({ |
| name: "sqlite3_preupdate_hook()", |
| predicate: ()=>capi.sqlite3_preupdate_hook || "Missing pre-update hook API", |
| test: function(sqlite3){ |
| const db = new sqlite3.oo1.DB(':memory:', 1 ? 'c' : 'ct'); |
| const countHook = Object.create(null); |
| let rc = capi.sqlite3_preupdate_hook( |
| db, function(p, pDb, op, zDb, zTbl, iKey1, iKey2){ |
| T.assert(9 === p) |
| .assert(db.pointer === pDb) |
| .assert(1 === capi.sqlite3_preupdate_count(pDb)) |
| .assert( 0 > capi.sqlite3_preupdate_blobwrite(pDb) ); |
| countHook[op] = (countHook[op]||0) + 1; |
| switch(op){ |
| case capi.SQLITE_INSERT: |
| case capi.SQLITE_UPDATE: |
| T.assert('number' === typeof capi.sqlite3_preupdate_new_js(pDb, 0)); |
| break; |
| case capi.SQLITE_DELETE: |
| T.assert('number' === typeof capi.sqlite3_preupdate_old_js(pDb, 0)); |
| break; |
| default: toss("Unexpected hook operator:",op); |
| } |
| }, |
| 9 |
| ); |
| db.transaction((d)=>{ |
| d.exec([ |
| "create table t(a);", |
| "insert into t(a) values(1);", |
| "update t set a=2;", |
| "update t set a=3;", |
| "delete from t where a=3" |
| ]); |
| }); |
| T.assert(1 === countHook[capi.SQLITE_INSERT]) |
| .assert(2 === countHook[capi.SQLITE_UPDATE]) |
| .assert(1 === countHook[capi.SQLITE_DELETE]); |
| //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = true; |
| db.close(); |
| //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false; |
| } |
| })/*pre-update hooks*/ |
| ;/*end hook API tests*/ |
| |
| //////////////////////////////////////////////////////////////////////// |
| T.g('Auto-extension API') |
| .t({ |
| name: "Auto-extension sanity checks.", |
| test: function(sqlite3){ |
| let counter = 0; |
| const fp = wasm.installFunction('i(ppp)', function(pDb,pzErr,pApi){ |
| ++counter; |
| return 0; |
| }); |
| (new sqlite3.oo1.DB()).close(); |
| T.assert( 0===counter ); |
| capi.sqlite3_auto_extension(fp); |
| (new sqlite3.oo1.DB()).close(); |
| T.assert( 1===counter ); |
| (new sqlite3.oo1.DB()).close(); |
| T.assert( 2===counter ); |
| capi.sqlite3_cancel_auto_extension(fp); |
| wasm.uninstallFunction(fp); |
| (new sqlite3.oo1.DB()).close(); |
| T.assert( 2===counter ); |
| } |
| }); |
| |
| //////////////////////////////////////////////////////////////////////// |
| T.g('Session API') |
| .t({ |
| name: 'Session API sanity checks', |
| predicate: ()=>!!capi.sqlite3changegroup_add || "Missing session API", |
| test: function(sqlite3){ |
| //warn("The session API tests could use some expansion."); |
| const db1 = new sqlite3.oo1.DB(), db2 = new sqlite3.oo1.DB(); |
| const sqlInit = [ |
| "create table t(rowid INTEGER PRIMARY KEY,a,b); ", |
| "insert into t(rowid,a,b) values", |
| "(1,'a1','b1'),", |
| "(2,'a2','b2'),", |
| "(3,'a3','b3');" |
| ].join(''); |
| db1.exec(sqlInit); |
| db2.exec(sqlInit); |
| T.assert(3 === db1.selectValue("select count(*) from t")) |
| .assert('b3' === db1.selectValue('select b from t where rowid=3')); |
| const stackPtr = wasm.pstack.pointer; |
| try{ |
| let ppOut = wasm.pstack.allocPtr(); |
| let rc = capi.sqlite3session_create(db1, "main", ppOut); |
| T.assert(0===rc); |
| let pSession = wasm.peekPtr(ppOut); |
| T.assert(pSession && wasm.isPtr(pSession)); |
| capi.sqlite3session_table_filter(pSession, (pCtx, tbl)=>{ |
| T.assert('t' === tbl).assert( 99 === pCtx ); |
| return 1; |
| }, 99); |
| db1.exec([ |
| "update t set b='bTwo' where rowid=2;", |
| "update t set a='aThree' where rowid=3;", |
| "delete from t where rowid=1;", |
| "insert into t(rowid,a,b) values(4,'a4','b4')" |
| ]); |
| T.assert('bTwo' === db1.selectValue("select b from t where rowid=2")) |
| .assert(undefined === db1.selectValue('select a from t where rowid=1')) |
| .assert('b4' === db1.selectValue('select b from t where rowid=4')) |
| .assert(3 === db1.selectValue('select count(*) from t')); |
| |
| const testSessionEnable = |
| false /* it's not yet clear whether these test failures are |
| broken tests or broken bindings. */; |
| if(testSessionEnable){ |
| rc = capi.sqlite3session_enable(pSession, 0); |
| T.assert( 0 === rc ) |
| .assert( 0 === capi.sqlite3session_enable(pSession, -1) ); |
| db1.exec("delete from t where rowid=2;"); |
| rc = capi.sqlite3session_enable(pSession, 1); |
| T.assert( rc > 0 ) |
| .assert( capi.sqlite3session_enable(pSession, -1) > 0 ) |
| .assert(undefined === db1.selectValue('select a from t where rowid=2')); |
| }else{ |
| //warn("sqlite3session_enable() tests are currently disabled."); |
| } |
| let db1Count = db1.selectValue("select count(*) from t"); |
| T.assert( db1Count === (testSessionEnable ? 2 : 3) ); |
| |
| /* Capture changeset and destroy session. */ |
| let pnChanges = wasm.pstack.alloc('i32'), |
| ppChanges = wasm.pstack.allocPtr(); |
| rc = capi.sqlite3session_changeset(pSession, pnChanges, ppChanges); |
| T.assert( 0 === rc ); |
| capi.sqlite3session_delete(pSession); |
| pSession = 0; |
| const pChanges = wasm.peekPtr(ppChanges), |
| nChanges = wasm.peek32(pnChanges); |
| T.assert( pChanges && wasm.isPtr( pChanges ) ) |
| .assert( nChanges > 0 ); |
| |
| /* Revert db1 via an inverted changeset, but keep pChanges |
| and nChanges for application to db2. */ |
| rc = capi.sqlite3changeset_invert( nChanges, pChanges, pnChanges, ppChanges ); |
| T.assert( 0 === rc ); |
| rc = capi.sqlite3changeset_apply( |
| db1, wasm.peek32(pnChanges), wasm.peekPtr(ppChanges), 0, (pCtx, eConflict, pIter)=>{ |
| return 1; |
| }, 0 |
| ); |
| T.assert( 0 === rc ); |
| wasm.dealloc( wasm.peekPtr(ppChanges) ); |
| pnChanges = ppChanges = 0; |
| T.assert('b2' === db1.selectValue("select b from t where rowid=2")) |
| .assert('a1' === db1.selectValue('select a from t where rowid=1')) |
| .assert(undefined === db1.selectValue('select b from t where rowid=4')); |
| db1Count = db1.selectValue("select count(*) from t"); |
| T.assert(3 === db1Count); |
| |
| /* Apply pre-reverted changeset (pChanges, nChanges) to |
| db2... */ |
| rc = capi.sqlite3changeset_apply( |
| db2, nChanges, pChanges, 0, (pCtx, eConflict, pIter)=>{ |
| return pCtx ? 1 : 0 |
| }, 1 |
| ); |
| wasm.dealloc( pChanges ); |
| T.assert( 0 === rc ) |
| .assert( 'b4' === db2.selectValue('select b from t where rowid=4') ) |
| .assert( 'aThree' === db2.selectValue('select a from t where rowid=3') ) |
| .assert( undefined === db2.selectValue('select b from t where rowid=1') ); |
| if(testSessionEnable){ |
| T.assert( (undefined === db2.selectValue('select b from t where rowid=2')), |
| "But... the session was disabled when rowid=2 was deleted?" ); |
| log("rowids from db2.t:",db2.selectValues('select rowid from t order by rowid')); |
| T.assert( 3 === db2.selectValue('select count(*) from t') ); |
| }else{ |
| T.assert( 'bTwo' === db2.selectValue('select b from t where rowid=2') ) |
| .assert( 3 === db2.selectValue('select count(*) from t') ); |
| } |
| }finally{ |
| wasm.pstack.restore(stackPtr); |
| db1.close(); |
| db2.close(); |
| } |
| } |
| })/*session API sanity tests*/ |
| ;/*end of session API group*/; |
| |
| //////////////////////////////////////////////////////////////////////// |
| T.g('OPFS: Origin-Private File System', |
| (sqlite3)=>(sqlite3.capi.sqlite3_vfs_find("opfs") |
| || 'requires "opfs" VFS')) |
| .t({ |
| name: 'OPFS db sanity checks', |
| test: async function(sqlite3){ |
| T.assert(capi.sqlite3_vfs_find('opfs')); |
| const opfs = sqlite3.opfs; |
| const filename = this.opfsDbFile = '/dir/sqlite3-tester1.db'; |
| const fileUri = 'file://'+filename+'?delete-before-open=1'; |
| const initSql = [ |
| 'create table p(a);', |
| 'insert into p(a) values(1),(2),(3)' |
| ]; |
| let db = new sqlite3.oo1.OpfsDb(fileUri); |
| try { |
| db.exec(initSql); |
| T.assert(3 === db.selectValue('select count(*) from p')); |
| db.close(); |
| db = new sqlite3.oo1.OpfsDb(filename); |
| db.exec('insert into p(a) values(4),(5),(6)'); |
| T.assert(6 === db.selectValue('select count(*) from p')); |
| this.opfsDbExport = capi.sqlite3_js_db_export(db); |
| T.assert(this.opfsDbExport instanceof Uint8Array) |
| .assert(this.opfsDbExport.byteLength>0 |
| && 0===this.opfsDbExport.byteLength % 512); |
| }finally{ |
| db.close(); |
| } |
| T.assert(await opfs.entryExists(filename)); |
| try { |
| db = new sqlite3.oo1.OpfsDb(fileUri); |
| db.exec(initSql) /* will throw if delete-before-open did not work */; |
| T.assert(3 === db.selectValue('select count(*) from p')); |
| }finally{ |
| if(db) db.close(); |
| } |
| } |
| }/*OPFS db sanity checks*/) |
| .t({ |
| name: 'OPFS import', |
| test: async function(sqlite3){ |
| let db; |
| const filename = this.opfsDbFile; |
| try { |
| const exp = this.opfsDbExport; |
| delete this.opfsDbExport; |
| this.opfsImportSize = await sqlite3.oo1.OpfsDb.importDb(filename, exp); |
| db = new sqlite3.oo1.OpfsDb(this.opfsDbFile); |
| T.assert(6 === db.selectValue('select count(*) from p')). |
| assert( this.opfsImportSize == exp.byteLength ); |
| db.close(); |
| const unlink = this.opfsUnlink = |
| (fn=filename)=>sqlite3.util.sqlite3__wasm_vfs_unlink("opfs",fn); |
| this.opfsUnlink(filename); |
| T.assert(!(await sqlite3.opfs.entryExists(filename))); |
| // Try again with a function as an input source: |
| let cursor = 0; |
| const blockSize = 512, end = exp.byteLength; |
| const reader = async function(){ |
| if(cursor >= exp.byteLength){ |
| return undefined; |
| } |
| const rv = exp.subarray(cursor, cursor+blockSize>end ? end : cursor+blockSize); |
| cursor += blockSize; |
| return rv; |
| }; |
| this.opfsImportSize = await sqlite3.oo1.OpfsDb.importDb(filename, reader); |
| db = new sqlite3.oo1.OpfsDb(this.opfsDbFile); |
| T.assert(6 === db.selectValue('select count(*) from p')). |
| assert( this.opfsImportSize == exp.byteLength ); |
| }finally{ |
| if(db) db.close(); |
| } |
| } |
| }/*OPFS export/import*/) |
| .t({ |
| name: '(Internal-use) OPFS utility APIs', |
| test: async function(sqlite3){ |
| const filename = this.opfsDbFile; |
| const unlink = this.opfsUnlink; |
| T.assert(filename && !!unlink); |
| delete this.opfsDbFile; |
| delete this.opfsUnlink; |
| /************************************************************** |
| ATTENTION CLIENT-SIDE USERS: sqlite3.opfs is NOT intended |
| for client-side use. It is only for this project's own |
| internal use. Its APIs are subject to change or removal at |
| any time. |
| ***************************************************************/ |
| const opfs = sqlite3.opfs; |
| const fSize = this.opfsImportSize; |
| delete this.opfsImportSize; |
| let sh; |
| try{ |
| T.assert(await opfs.entryExists(filename)); |
| const [dirHandle, filenamePart] = await opfs.getDirForFilename(filename, false); |
| const fh = await dirHandle.getFileHandle(filenamePart); |
| sh = await fh.createSyncAccessHandle(); |
| T.assert(fSize === await sh.getSize()); |
| await sh.close(); |
| sh = undefined; |
| unlink(); |
| T.assert(!(await opfs.entryExists(filename))); |
| }finally{ |
| if(sh) await sh.close(); |
| unlink(); |
| } |
| |
| // Some sanity checks of the opfs utility functions... |
| const testDir = '/sqlite3-opfs-'+opfs.randomFilename(12); |
| const aDir = testDir+'/test/dir'; |
| T.assert(await opfs.mkdir(aDir), "mkdir failed") |
| .assert(await opfs.mkdir(aDir), "mkdir must pass if the dir exists") |
| .assert(!(await opfs.unlink(testDir+'/test')), "delete 1 should have failed (dir not empty)") |
| .assert((await opfs.unlink(testDir+'/test/dir')), "delete 2 failed") |
| .assert(!(await opfs.unlink(testDir+'/test/dir')), |
| "delete 2b should have failed (dir already deleted)") |
| .assert((await opfs.unlink(testDir, true)), "delete 3 failed") |
| .assert(!(await opfs.entryExists(testDir)), |
| "entryExists(",testDir,") should have failed"); |
| } |
| }/*OPFS util sanity checks*/) |
| ;/* end OPFS tests */ |
| |
| //////////////////////////////////////////////////////////////////////// |
| T.g('OPFS SyncAccessHandle Pool VFS', |
| (sqlite3)=>(hasOpfs() || "requires OPFS APIs")) |
| .t({ |
| name: 'SAH sanity checks', |
| test: async function(sqlite3){ |
| T.assert(!sqlite3.capi.sqlite3_vfs_find(sahPoolConfig.name)) |
| .assert(sqlite3.capi.sqlite3_js_vfs_list().indexOf(sahPoolConfig.name) < 0) |
| const inst = sqlite3.installOpfsSAHPoolVfs, |
| catcher = (e)=>{ |
| error("Cannot load SAH pool VFS.", |
| "This might not be a problem,", |
| "depending on the environment."); |
| return false; |
| }; |
| let u1, u2; |
| // Ensure that two immediately-consecutive installations |
| // resolve to the same Promise instead of triggering |
| // a locking error. |
| const P1 = inst(sahPoolConfig).then(u=>u1 = u).catch(catcher), |
| P2 = inst(sahPoolConfig).then(u=>u2 = u).catch(catcher); |
| await Promise.all([P1, P2]); |
| if(!(await P1)) return; |
| T.assert(u1 === u2) |
| .assert(sahPoolConfig.name === u1.vfsName) |
| .assert(sqlite3.capi.sqlite3_vfs_find(sahPoolConfig.name)) |
| .assert(u1.getCapacity() >= sahPoolConfig.initialCapacity |
| /* If a test fails before we get to nuke the VFS, we |
| can have more than the initial capacity on the next |
| run. */) |
| .assert(u1.getCapacity() + 2 === (await u2.addCapacity(2))) |
| .assert(2 === (await u2.reduceCapacity(2))) |
| .assert(sqlite3.capi.sqlite3_js_vfs_list().indexOf(sahPoolConfig.name) >= 0); |
| |
| T.assert(0 === u1.getFileCount()); |
| const dbName = '/foo.db'; |
| let db = new u1.OpfsSAHPoolDb(dbName); |
| T.assert(db instanceof sqlite3.oo1.DB) |
| .assert(1 === u1.getFileCount()); |
| db.exec([ |
| 'pragma locking_mode=exclusive;', |
| 'pragma journal_mode=wal;' |
| /* WAL mode only works in this VFS if locking_mode=exclusive |
| is invoked prior to the first db access, as this build |
| does not have the shared-memory APIs needed for WAL without |
| exclusive-mode locking. See: |
| |
| https://sqlite.org/wal.html#use_of_wal_without_shared_memory |
| |
| Note that WAL mode here DOES NOT add any concurrency capabilities |
| to this VFS, but it MAY provide slightly improved performance |
| over the other journaling modes. |
| */, |
| 'create table t(a);', |
| 'insert into t(a) values(1),(2),(3)' |
| ]); |
| T.assert(2 === u1.getFileCount() /* one is the journal file */) |
| .assert(3 === db.selectValue('select count(*) from t')) |
| .assert( |
| 'wal'===db.selectValue('pragma journal_mode') |
| || wasm.compileOptionUsed('OMIT_WAL') |
| ); |
| db.close(); |
| T.assert(1 === u1.getFileCount()); |
| db = new u2.OpfsSAHPoolDb(dbName); |
| T.assert(1 === u1.getFileCount()); |
| db.close(); |
| const fileNames = u1.getFileNames(); |
| T.assert(1 === fileNames.length) |
| .assert(dbName === fileNames[0]) |
| .assert(1 === u1.getFileCount()) |
| |
| if(1){ // test exportFile() and importDb() |
| const dbytes = u1.exportFile(dbName); |
| T.assert(dbytes.length >= 4096); |
| const dbName2 = '/exported.db'; |
| let nWrote = u1.importDb(dbName2, dbytes); |
| T.assert( 2 == u1.getFileCount() ) |
| .assert( dbytes.byteLength == nWrote ); |
| let db2 = new u1.OpfsSAHPoolDb(dbName2); |
| T.assert(db2 instanceof sqlite3.oo1.DB) |
| .assert('wal' !== db2.selectValue("pragma journal_mode") |
| /* importDb() unsets the WAL-mode header for |
| historical reasons. Because clients must |
| explicitly enable pragma locking_mode=exclusive |
| before using WAL, that behavior is retained. */) |
| .assert(3 === db2.selectValue('select count(*) from t')); |
| db2.close(); |
| T.assert(true === u1.unlink(dbName2)) |
| .assert(false === u1.unlink(dbName2)) |
| .assert(1 === u1.getFileCount()) |
| .assert(1 === u1.getFileNames().length); |
| // Try again with a function as an input source: |
| let cursor = 0; |
| const blockSize = 1024, end = dbytes.byteLength; |
| const reader = async function(){ |
| if(cursor >= dbytes.byteLength){ |
| return undefined; |
| } |
| const rv = dbytes.subarray(cursor, cursor+blockSize>end ? end : cursor+blockSize); |
| cursor += blockSize; |
| return rv; |
| }; |
| nWrote = await u1.importDb(dbName2, reader); |
| T.assert( 2 == u1.getFileCount() ); |
| db2 = new u1.OpfsSAHPoolDb(dbName2); |
| T.assert(db2 instanceof sqlite3.oo1.DB) |
| .assert(3 === db2.selectValue('select count(*) from t')); |
| db2.close(); |
| T.assert(true === u1.unlink(dbName2)) |
| .assert(dbytes.byteLength == nWrote); |
| } |
| |
| T.assert(true === u1.unlink(dbName)) |
| .assert(false === u1.unlink(dbName)) |
| .assert(0 === u1.getFileCount()) |
| .assert(0 === u1.getFileNames().length); |
| |
| // Demonstrate that two SAH pools can coexist so long as |
| // they have different names. |
| const conf2 = JSON.parse(JSON.stringify(sahPoolConfig)); |
| conf2.name += '-test2'; |
| const POther = await inst(conf2); |
| //log("Installed second SAH instance as",conf2.name); |
| T.assert(0 === POther.getFileCount()) |
| .assert(true === await POther.removeVfs()); |
| |
| if(0){ |
| /* Enable this block to inspect vfs's contents via the dev |
| console or OPFS Explorer browser extension. The |
| following bits will remove them. */ |
| return; |
| } |
| T.assert(true === await u2.removeVfs()) |
| .assert(false === await u1.removeVfs()) |
| .assert(!sqlite3.capi.sqlite3_vfs_find(sahPoolConfig.name)); |
| |
| let cErr, u3; |
| conf2.$testThrowPhase2 = new Error("Testing throwing during init."); |
| conf2.name = sahPoolConfig.name+'-err'; |
| const P3 = await inst(conf2).then(u=>u3 = u).catch((e)=>cErr=e); |
| T.assert(P3 === conf2.$testThrowPhase2) |
| .assert(cErr === P3) |
| .assert(undefined === u3) |
| .assert(!sqlite3.capi.sqlite3_vfs_find(conf2.name)); |
| delete conf2.$testThrowPhase2; |
| T.assert(cErr === await inst(conf2).catch(e=>e), |
| "Init result is cached even if it failed"); |
| |
| /* Ensure that the forceReinitIfPreviouslyFailed fallback bypasses |
| the VFS init cache... */ |
| cErr = u3 = undefined; |
| conf2.forceReinitIfPreviouslyFailed = true; |
| conf2.verbosity = 3; |
| const P3b = await inst(conf2).then(u=>u3 = u).catch((e)=>cErr=e); |
| T.assert(undefined === cErr) |
| .assert(P3b === u3) |
| .assert(P3b === await inst(conf2)) |
| .assert(true === await u3.removeVfs()) |
| .assert(false === await P3b.removeVfs()); |
| } |
| }/*OPFS SAH Pool sanity checks*/) |
| |
| //////////////////////////////////////////////////////////////////////// |
| T.g('Misc. APIs') |
| .t('bind_parameter_...', function(sqlite3){ |
| const db = new sqlite3.oo1.DB(); |
| db.exec("create table t(a)"); |
| const stmt = db.prepare("insert into t(a) values($a)"); |
| T.assert( 1===capi.sqlite3_bind_parameter_count(stmt) ) |
| .assert( 1===capi.sqlite3_bind_parameter_index(stmt, "$a") ) |
| .assert( 0===capi.sqlite3_bind_parameter_index(stmt, ":a") ) |
| .assert( 1===stmt.getParamIndex("$a") ) |
| .assert( 0===stmt.getParamIndex(":a") ) |
| .assert( "$a"===capi.sqlite3_bind_parameter_name(stmt, 1) ) |
| .assert( null===capi.sqlite3_bind_parameter_name(stmt, 0) ) |
| .assert( "$a"===stmt.getParamName(1) ) |
| .assert( null===stmt.getParamName(0) ); |
| stmt.finalize(); |
| db.close(); |
| }) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t("Misc. stmt_...", function(sqlite3){ |
| const db = new sqlite3.oo1.DB(); |
| db.exec("create table t(a doggiebiscuits); insert into t(a) values(123)"); |
| const stmt = db.prepare("select a, a+1 from t"); |
| T.assert( stmt.isReadOnly() ) |
| .assert( 0===capi.sqlite3_stmt_isexplain(stmt) ) |
| .assert( 0===capi.sqlite3_stmt_explain(stmt, 1) ) |
| .assert( 0!==capi.sqlite3_stmt_isexplain(stmt) ) |
| .assert( 0===capi.sqlite3_stmt_explain(stmt, 2) ) |
| .assert( 0!==capi.sqlite3_stmt_isexplain(stmt) ) |
| .assert( 0===capi.sqlite3_stmt_explain(stmt, 0) ) |
| .assert( 0===capi.sqlite3_stmt_isexplain(stmt) ); |
| let n = 0; |
| while( capi.SQLITE_ROW === capi.sqlite3_step(stmt) ){ |
| ++n; |
| T.assert( 0!==capi.sqlite3_stmt_explain(stmt, 1), |
| "Because stmt is busy" ) |
| .assert( capi.sqlite3_stmt_busy(stmt) ) |
| .assert( stmt.isBusy() ) |
| .assert( 0!==capi.sqlite3_stmt_readonly(stmt) ) |
| .assert( true===stmt.isReadOnly() ); |
| const sv = capi.sqlite3_column_value(stmt, 0); |
| T.assert( 123===capi.sqlite3_value_int(sv) ) |
| .assert( "doggiebiscuits"===capi.sqlite3_column_decltype(stmt,0) ) |
| .assert( null===capi.sqlite3_column_decltype(stmt,1) ); |
| } |
| T.assert( 1===n ) |
| .assert( 0===capi.sqlite3_stmt_busy(stmt) ) |
| .assert( !stmt.isBusy() ); |
| stmt.finalize(); |
| db.close(); |
| }) |
| |
| //////////////////////////////////////////////////////////////////// |
| .t("interrupt", function(sqlite3){ |
| const db = new sqlite3.oo1.DB(); |
| T.assert( 0===capi.sqlite3_is_interrupted(db) ); |
| capi.sqlite3_interrupt(db); |
| T.assert( 0!==capi.sqlite3_is_interrupted(db) ); |
| db.close(); |
| }) |
| |
| //////////////////////////////////////////////////////////////////////// |
| T.g('Bug Reports') |
| .t({ |
| name: 'Delete via bound parameter in subquery', |
| predicate: ()=>wasm.compileOptionUsed('ENABLE_FTS5') || "Missing FTS5", |
| test: function(sqlite3){ |
| // Testing https://sqlite.org/forum/forumpost/40ce55bdf5 |
| // with the exception that that post uses "external content" |
| // for the FTS index. |
| const db = new sqlite3.oo1.DB();//(':memory:','wt'); |
| db.exec([ |
| "create virtual table f using fts5 (path);", |
| "insert into f(path) values('abc'),('def'),('ghi');" |
| ]); |
| const fetchEm = ()=> db.exec({ |
| sql: "SELECT * FROM f order by path", |
| rowMode: 'array' |
| }); |
| const dump = function(lbl){ |
| let rc = fetchEm(); |
| log((lbl ? (lbl+' results') : ''),rc); |
| }; |
| //dump('Full fts table'); |
| let rc = fetchEm(); |
| T.assert(3===rc.length); |
| db.exec(` |
| delete from f where rowid in ( |
| select rowid from f where path = :path |
| )`, |
| {bind: {":path": "def"}} |
| ); |
| //dump('After deleting one entry via subquery'); |
| rc = fetchEm(); |
| T.assert(2===rc.length) |
| .assert('abcghi'===rc.join('')); |
| //log('rc =',rc); |
| db.close(); |
| } |
| }) |
| .t({ |
| name: 'r/o connection recovery from write op error', |
| predicate: ()=>hasOpfs() || "Requires OPFS to reproduce", |
| //predicate: ()=>false, |
| test: async function(sqlite3){ |
| /* https://sqlite.org/forum/forumpost/cf37d5ff1182c31081 |
| |
| The "opfs" VFS (but not SAHPool) was formerly misbehaving |
| after a write attempt was made on a db opened with |
| mode=ro. This test ensures that that behavior is fixed and |
| compares that behavior with other VFSes. */ |
| const tryOne = function(vfsName,descr){ |
| const uri = 'file:///foo.db'; |
| let db = new sqlite3.oo1.DB(uri + (vfsName ? '?vfs='+vfsName : '')); |
| db.exec([ |
| "drop table if exists t;", |
| "create table t(a);", |
| "insert into t(a) values('abc'),('def'),('ghi');" |
| ]); |
| db.close(); |
| db = new sqlite3.oo1.DB(uri+'?mode=ro'+ |
| (vfsName ? '&vfs='+vfsName : '')); |
| let err; |
| try { |
| db.exec('insert into t(a) values(1)'); |
| }catch(e){ |
| err = e; |
| } |
| T.assert(err && (err.message.indexOf('SQLITE_READONLY')===0)); |
| try{ |
| db.exec('select a from t'); |
| }finally{ |
| db.close(); |
| } |
| }; |
| const poolConfig = JSON.parse(JSON.stringify(sahPoolConfig)); |
| poolConfig.name = 'opfs-sahpool-cf37d5ff11'; |
| let poolUtil; |
| await sqlite3.installOpfsSAHPoolVfs(poolConfig).then(p=>poolUtil=p); |
| T.assert(!!sqlite3.capi.sqlite3_vfs_find(poolConfig.name), "Expecting to find just-registered VFS"); |
| try{ |
| tryOne(false, "Emscripten filesystem"); |
| tryOne(poolConfig.name); |
| tryOne('opfs'); |
| }finally{ |
| await poolUtil.removeVfs(); |
| } |
| } |
| }) |
| ;/*end of Bug Reports group*/; |
| |
| //////////////////////////////////////////////////////////////////////// |
| log("Loading and initializing sqlite3 WASM module..."); |
| if(0){ |
| globalThis.sqlite3ApiConfig = { |
| debug: ()=>{}, |
| log: ()=>{}, |
| warn: ()=>{}, |
| error: ()=>{} |
| } |
| } |
| //#ifnot target=es6-module |
| if(!globalThis.sqlite3InitModule && !isUIThread()){ |
| /* Vanilla worker, as opposed to an ES6 module worker */ |
| /* |
| If sqlite3.js is in a directory other than this script, in order |
| to get sqlite3.js to resolve sqlite3.wasm properly, we have to |
| explicitly tell it where sqlite3.js is being loaded from. We do |
| that by passing the `sqlite3.dir=theDirName` URL argument to |
| _this_ script. That URL argument will be seen by the JS/WASM |
| loader and it will adjust the sqlite3.wasm path accordingly. If |
| sqlite3.js/.wasm are in the same directory as this script then |
| that's not needed. |
| |
| URL arguments passed as part of the filename via importScripts() |
| are simply lost, and such scripts see the globalThis.location of |
| _this_ script. |
| */ |
| let sqlite3Js = 'sqlite3.js'; |
| const urlParams = new URL(globalThis.location.href).searchParams; |
| if(urlParams.has('sqlite3.dir')){ |
| sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js; |
| } |
| importScripts(sqlite3Js); |
| } |
| //#endif |
| globalThis.sqlite3InitModule.__isUnderTest = |
| true /* disables certain API-internal cleanup so that we can |
| test internal APIs from here */; |
| globalThis.sqlite3InitModule({ |
| print: log, |
| printErr: error |
| }).then(async function(sqlite3){ |
| TestUtil.assert(!!sqlite3.util); |
| log("Done initializing WASM/JS bits. Running tests..."); |
| sqlite3.config.warn("Installing sqlite3 bits as global S for local dev/test purposes."); |
| globalThis.S = sqlite3; |
| /*await sqlite3.installOpfsSAHPoolVfs(sahPoolConfig) |
| .then((u)=>log("Loaded",u.vfsName,"VFS")) |
| .catch(e=>{ |
| log("Cannot install OpfsSAHPool.",e); |
| });*/ |
| capi = sqlite3.capi; |
| wasm = sqlite3.wasm; |
| log("sqlite3 version:",capi.sqlite3_libversion(), |
| capi.sqlite3_sourceid()); |
| if(wasm.bigIntEnabled){ |
| log("BigInt/int64 support is enabled."); |
| }else{ |
| logClass('warning',"BigInt/int64 support is disabled."); |
| } |
| if(haveWasmCTests()){ |
| log("sqlite3__wasm_test_...() APIs are available."); |
| }else{ |
| logClass('warning',"sqlite3__wasm_test_...() APIs unavailable."); |
| } |
| log("registered vfs list =",capi.sqlite3_js_vfs_list().join(', ')); |
| TestUtil.runTests(sqlite3); |
| }); |
| })(self); |