/*
  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.mjs -Dtarget:es6-module
*/
//#@ policy error
//#if target:es6-module
import {default as sqlite3InitModule} from "@sqlite3.js@";
globalThis.sqlite3InitModule = sqlite3InitModule;
//#else
'use strict';
//#/if
(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;
  };

  let SQLite3 /* populated after module load */;
  const hasOpfs = ()=>!!SQLite3?.oo1?.OpfsDb;

  {
    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');
      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 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 debug = (...args)=>{
    console.debug('tester1',...args);
  };

  const reportFinalTestStatus = function(pass){
    debug("Final test status:",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}});
    }
    TestUtil.checkHeapSize(true);
  };

  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;

  const looksLikePtr = (v,positive=true)=> positive ? v>0 : v>=0;

  /**
     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. If filter is a number then it is compared against
       the resultCode property of the exception. 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);
      else if('number' === typeof filter) pass = (err.resultCode === filter);
      if(!pass){
        console.error("Filter",filter,"rejected exception",err);
        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');
            TestUtil.checkHeapSize();
          }
          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));
    },

    checkHeapSize: function(force=false){
      const heapSize = SQLite3.wasm.heap8().byteLength;
      if( force || heapSize !== TestUtil.lastHeapSize ){
        TestUtil.lastHeapSize = heapSize;
        log('WASM heap size:', heapSize,'bytes');
      }
    }
  }/*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
  };

//#if enable-see
  /**
     Code consolidator for SEE sanity checks for various VFSes. ctor
     is the VFS's oo1.DB-type constructor.  ctorOptFunc(bool) is a
     function which must return a constructor args object for ctor. It
     is passed true if the db needs to be cleaned up/unlinked before
     opening it (OPFS) and false if not (how that is done is
     VFS-dependent).  dbUnlink is a function which is expected to
     unlink() the db file if the ctorOpfFunc does not do so when
     passed true (kvvfs).

     This function initializes the db described by ctorOptFunc(...),
     writes some secret info into it, and re-opens it twice to
     confirm that it can be read with an SEE key and cannot be read
     without one.
  */
  T.seeBaseCheck = function(ctor, ctorOptFunc, dbUnlink){
    let initDb = true;
    const tryKey = function(keyKey, key, expectCount){
      let db;
      //console.debug('tryKey()',arguments);
      try {
        if (initDb) {
          const ctoropt = ctorOptFunc(initDb);
          initDb = false;
          db = new ctor({
            ...ctoropt,
            [keyKey]: key
          });
          db.exec([
            "drop table if exists t;",
            "create table t(a);"
          ]);
          db.close();
          db = null;
          // Ensure that it's actually encrypted...
          let err;
          try {
            db = new ctor(ctorOptFunc(false));
            T.assert(db, 'db opened') /* opening is fine, but... */;
            db.exec("select 1 from sqlite_schema");
            console.warn("(should not be reached) sessionStorage =", sessionStorage);
          } catch (e) {
            err = e;
          } finally {
            db.close()
            db = null;
          }
          T.assert(err, "Expecting an exception")
            .assert(capi.SQLITE_NOTADB == err.resultCode,
                    "Expecting NOTADB");
        }/*initDb*/
        db = new ctor({
          ...ctorOptFunc(false),
          [keyKey]: key
        });
        db.exec("insert into t(a) values (1),(2)");
        T.assert(expectCount === db.selectValue('select sum(a) from t'));
      } finally {
        if (db) db.close();
      }
    };
    const hexFoo = new Uint8Array([0x66,0x6f,0x6f]/*=="foo"*/);
    dbUnlink();
    tryKey('textkey', 'foo', 3);
    T.assert( !initDb );
    tryKey('textkey', 'foo', 6);
    dbUnlink();
    initDb = true;
    tryKey('key', 'foo', 3);
    T.assert( !initDb );
    tryKey('key', hexFoo, 6);
    dbUnlink();
    initDb = true;
    tryKey('hexkey', hexFoo, 3);
    T.assert( !initDb );
    tryKey('hexkey', hexFoo, 6);
    dbUnlink();
  };
//#/if enable-see

  /* Tests common to "opfs" and "opfs-wl".  These tests manipulate
     "this" and must run in order.
  */
  T.opfsCommon = {
    sanityChecks: async function(vfsName, oo1Ctor, sqlite3){
      T.assert(capi.sqlite3_vfs_find(vfsName));
      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 oo1Ctor(fileUri);
      try {
        db.exec(initSql);
        T.assert(3 === db.selectValue('select count(*) from p'));
        db.close();
        db = new oo1Ctor(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();
        db = null;
      }
      T.assert(await opfs.entryExists(filename));
      try {
        db = new oo1Ctor(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();
      }
    },

    importer: async function(vfsName, oo1Ctor, sqlite3){
      let db;
      const filename = this.opfsDbFile;
      try {
        const exp = this.opfsDbExport;
        delete this.opfsDbExport;
        this.opfsImportSize = await oo1Ctor.importDb(filename, exp);
        db = new oo1Ctor(this.opfsDbFile);
        T.assert(6 === db.selectValue('select count(*) from p')).
          assert( this.opfsImportSize == exp.byteLength );
        db.close();
        db = null;
        this.opfsUnlink =
          (fn=filename)=>sqlite3.util.sqlite3__wasm_vfs_unlink(vfsName, 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 oo1Ctor.importDb(filename, reader);
        db = new oo1Ctor(this.opfsDbFile);
        T.assert(6 === db.selectValue('select count(*) from p')).
          assert( this.opfsImportSize == exp.byteLength );
      }finally{
        if(db) db.close();
      }
    },

    opfsUtil: async function(vfsName, oo1Ctor, 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. The sqlite3.opfs namespace is REMOVED from the
        sqlite3 namespace in non-test runs of the library.
      ***************************************************************/
      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");
    }
  }/*T.opfsCommon*/;

  ////////////////////////////////////////////////////////////////////////
  // 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.impl === 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_UTF8_ZT',
        '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 */);
        let x = w.realloc(m, 0);
        T.assert(w.ptr.null === x);
        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(w.ptr.null === w.alloc.impl(tooMuch))
            .mustThrowMatching(()=>w.realloc(0, tooMuch), isAllocErr)
            .assert(w.ptr.null === w.realloc.impl(wasm.ptr.null, tooMuch));
        }

        // Check allocFromTypedArray()...
        const byteList = [11,22,33]
        const u = new Uint8Array(byteList);
        m = w.allocFromTypedArray(u);
        let mAsNumber = Number(m);
        for(let i = 0; i < u.length; ++i){
          T.assert(u[i] === byteList[i])
            .assert(u[i] === w.peek8(mAsNumber + i));
        }
        w.dealloc(m);
        m = w.allocFromTypedArray(u.buffer);
        mAsNumber = Number(m);
        for(let i = 0; i < u.length; ++i){
          T.assert(u[i] === byteList[i])
            .assert(u[i] === w.peek8(mAsNumber + 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);
          const nPtr = w.ptr.coerce(n);
          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( w.ptr.add(cpy,nPtr, -1))).
            assert(0 === w.peek8( w.ptr.add(cpy,nPtr) ) );
          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( w.ptr.add(cpy, 2) )).
            assert(chr('l') === w.peek8( w.ptr.add(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(w.ptr.add(cstr,n)))
          .assert(chr('!')===w.peek8(w.ptr.add(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(looksLikePtr(p1))
            .assert(looksLikePtr(p2))
            .assert(asc[0] === p1)
            .assert(asc[1]===p2);
          asc2 = w.scopedAllocPush();
          const p3 = w.scopedAlloc(16);
          T.assert(2===w.scopedAlloc.level)
            .assert(looksLikePtr(p3))
            .assert(2===asc.length)
            .assert(p3===asc2[0]);

          const [z1, z2, z3] = w.scopedAllocPtr(3);
          T.assert(typeof w.ptr.null===typeof z1).assert(z2>z1).assert(z3>z2)
            .assert(w.ptr.null===w.peekPtr(z1), 'allocPtr() must zero the targets')
            .assert(w.ptr.null===w.peekPtr(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( w.ptr.add(cstr,n) ))
            .assert(chr('d')===w.peek8( w.ptr.add(cstr, n, -1) ));
        });
      }/*scopedAlloc()*/

      //log("xCall()...");
      {
        const pJson = w.xCall('sqlite3__wasm_enum_json');
        T.assert(looksLikePtr(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 && looksLikePtr(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 toFlexStr = w.xWrap.argAdapter('string:flexible');
          const cj = (v)=>w.cstrToJs(toFlexStr(v));
          //console.debug("toFlexStr(new Uint8Array([72, 73]))",toFlexStr(new Uint8Array([72, 73])));
          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(w.ptr.size), pI2 = w.ptr.add(pI1, w.ptr.size);
              w.pokePtr([pI1, pI2], w.ptr.null);
              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: 0, members: {}
        };
        const addMember = function(tgt, name, member){
          member.offset = tgt.sizeof;
          tgt.sizeof += member.sizeof;
          tgt.members[name] = member;
        };
        const msd = MyStructDef;
        addMember(msd, 'p4', {sizeof: 4, signature: "i"});
        addMember(msd, 'pP', {sizeof: wasm.ptr.size, signature: "P"});
        addMember(msd, 'ro', {
          sizeof: 4,
          signature: "i",
          readOnly: true
        });
        addMember(msd, 'cstr', {
          sizeof: wasm.ptr.size,
          signature: "s"
        });
        if(W.bigIntEnabled){
          addMember(msd, 'p8', {sizeof: 8, signature: "j"});
        }
        const StructType = S.StructBinder.StructType;
        const K = S.StructBinder('my_struct',MyStructDef);
        //K.debugFlags(0x03);
        T.mustThrowMatching(()=>K(), /via 'new'/).
          mustThrowMatching(()=>new K('hi'), (err)=>{
            return /^Invalid pointer/.test(err.message)/*32-bit*/
              || /.*bigint.*/i.test(err.message)/*64-bit*/;
          });
        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(looksLikePtr(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(looksLikePtr(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/);
        }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(looksLikePtr(wts.pointer)).assert(0==wts.$v4).assert(0n===wts.$v8).
            assert(0==wts.$ppV).assert(0==wts.$xFunc);
          const testFunc = 1
                ? W.xGet('sqlite3__wasm_test_struct'/*name gets mangled in -O3 builds!*/)
                : W.xWrap('sqlite3__wasm_test_struct', undefined, '*');
          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'))
          //console.debug("wts.memberSignature('xFunc')",wts.memberSignature('xFunc'));
          //console.debug("wts.$xFunc",wts.$xFunc, W.functionEntry(wts.$xFunc));
          T.assert(0===counter).assert(10 === wts.$v4).assert(20n === wts.$v8)
            .assert(0 == wts.$ppV).assert(looksLikePtr(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. */

          if( 0 ){
            console.debug("wts",wts,"wts.pointer",wts.pointer,
                          "testFunc",testFunc/*FF v142 emits the wrong function here!*/);
          }
          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===Number(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 == Number(stack) - 16/*8-byte aligned*/)
          .assert(P.pointer === p1);
        let p2 = P.alloc(7);
        T.assert(p2 == Number(p1)-8/*8-byte aligned, stack grows downwards*/)
          .mustThrowMatching(()=>P.alloc(remaining), isAllocErr)
          .assert(24 == Number(stack) - Number(p2))
          .assert(P.pointer === p2);
        let n = remaining - (Number(stack) - Number(p2));
        let p3 = P.alloc(n);
        T.assert(p3 == Number(stack)-Number(remaining))
          .mustThrowMatching(()=>P.alloc(1), isAllocErr);
      }finally{
        P.restore(stack);
      }

      T.assert(P.pointer === stack);
      try {
        const [p1, p2, p3] = P.allocChunks(3,'i32');
        let sPos = wasm.ptr.add(stack,-16)/*pstack alloc always rounds to multiple of 8*/;
        T.assert(P.pointer === sPos)
          .assert(p1 === sPos)
          .assert(p2 == Number(p1) + 4)
          .assert(p3 == Number(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 = wasm.ptr.add(stack,
                               -(4===wasm.ptr.size
                                 ? 16/*pstack alloc always rounds to multiple of 8*/
                                 : 24));
        T.assert(P.pointer === p1)
          .assert(p1 === sPos)
          .assert(p2 == Number(p1) + wasm.ptr.size)
          .assert(p3 == Number(p2) + wasm.ptr.size);
        [p1, p2, p3] = P.allocPtr(3);
        T.assert(P.pointer === wasm.ptr.add(sPos, -24)/*3 x 8 bytes*/)
          .assert(p2 == Number(p1) + 8)
          .assert(p3 == Number(p2) + 8);
        p1 = P.allocPtr();
        T.assert(looksLikePtr(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(wasm.ptr.add(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[wasm.ptr.add(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[wasm.ptr.add(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({
      name: "oo1.DB/Stmt.wrapDbHandle()",
      test: function(sqlite3){
        /* Maintenance reminder: this function is early in the list to
           demonstrate that the wrappers for this.db created by this
           function do not interfere with downstream tests, e.g. by
           closing this.db.pointer. */
        //sqlite3.config.debug("Proxying",this.db);
        const misuseMsg = "SQLITE_MISUSE: Argument must be a WASM sqlite3 pointer";
        T.mustThrowMatching(()=>sqlite3.oo1.DB.wrapHandle(this.db), misuseMsg)
          .mustThrowMatching(()=>sqlite3.oo1.DB.wrapHandle(0), misuseMsg);
        let dw = sqlite3.oo1.DB.wrapHandle(this.db.pointer);
        //sqlite3.config.debug('dw',dw);
        T.assert( dw, '!!dw' )
          .assert( dw instanceof sqlite3.oo1.DB, 'dw is-a oo1.DB' )
          .assert( dw.pointer, 'dw.pointer' )
          .assert( dw.pointer === this.db.pointer, 'dw.pointer===db.pointer' )
          .assert( dw.filename === this.db.filename, 'dw.filename===db.filename' );

        T.assert( dw === dw.exec("select 1") );
        let q;
        try {
          q = dw.prepare("select 1");
          T.assert( q.step() )
            .assert( !q.step() );
        }finally{
          if( q ) q.finalize();
        }
        dw.close();
        T.assert( !dw.pointer )
          .assert( this.db === this.db.exec("select 1") );
        dw = undefined;

        let pDb = 0, pStmt = 0;
        const stack = wasm.pstack.pointer;
        try {
          const ppOut = wasm.pstack.allocPtr();
          T.assert( 0==wasm.peekPtr(ppOut) );
          let rc = capi.sqlite3_open_v2( ":memory:", ppOut,
                                         capi.SQLITE_OPEN_CREATE
                                         | capi.SQLITE_OPEN_READWRITE,
                                         0);
          T.assert( 0===rc, 'open_v2()' );
          pDb = wasm.peekPtr(ppOut);
          wasm.pokePtr(ppOut, 0);
          T.assert( pDb>0, 'pDb>0' );
          const pTmp = pDb;
          dw = sqlite3.oo1.DB.wrapHandle(pDb, true);
          pDb = 0;
          //sqlite3.config.debug("dw",dw);
          T.assert( pTmp===dw.pointer, 'pTmp===dw.pointer' );
          T.assert( dw.filename === "", "dw.filename == "+dw.filename );
          let q = dw.prepare("select 1");
          try {
            T.assert( q.step(), "step()" );
            T.assert( !q.step(), "!step()" );
          }finally{
            q.finalize();
            q = undefined;
          }
          T.assert( dw===dw.exec("select 1") );
          dw.affirmOpen();
          const select1 = "select 1";
          rc = capi.sqlite3_prepare_v2( dw, select1, -1, ppOut, 0 );
          T.assert( 0===rc, 'prepare_v2() rc='+rc );
          pStmt = wasm.peekPtr(ppOut);
          T.assert( pStmt && wasm.isPtr(pStmt), 'pStmt is valid?' );
          try {
            //log( "capi.sqlite3_sql() =",capi.sqlite3_sql(pStmt));
            T.assert( select1 === capi.sqlite3_sql(pStmt), 'SQL mismatch' );
            q = sqlite3.oo1.Stmt.wrapHandle(dw, pStmt, false);
            //log("q@"+pStmt+" does not own handle");
            T.assert( q.step(), "step()" )
              .assert( !q.step(), "!step()" );
            q.finalize();
            q = undefined;
            T.assert( select1 === capi.sqlite3_sql(pStmt), 'SQL mismatch'
                    /* This will fail if we've mismanaged pStmt's lifetime */);
            q = sqlite3.oo1.Stmt.wrapHandle(dw, pStmt, true);
            pStmt = 0;
            q.reset();
            T.assert( q.step(), "step()" )
              .assert( !q.step(), "!step()" );
          }finally{
            if( pStmt ) capi.sqlite3_finalize(pStmt)
            if( q ) q.finalize();
          }

        }finally{
          wasm.pstack.restore(stack);
          if( pDb ){ capi.sqlite3_close_v2(pDb); }
          else if( dw ){ dw.close(); }
        }
      }
    })/*oo1.DB/Stmt.wrapHandle()*/

  ////////////////////////////////////////////////////////////////////
    .t('sqlite3_db_config() and sqlite3_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);
      }

      capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_ENABLE_COMMENTS, 0, null);
      T.mustThrow(()=>this.db.exec("select 1 /* with comments */"), "SQL comments are disallowed");
      capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_ENABLE_COMMENTS, 1, null);
      this.db.exec("select 1 /* with comments */");
      /* SQLITE_DBCONFIG_ENABLE_ATTACH_... are in the ATTACH-specific tests */
    })

  ////////////////////////////////////////////////////////////////////
    .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('a' === st.getColumnName(0))
          .mustThrowMatching(()=>st.columnCount=2,
                             /columnCount property is read-only/)
          .assert(1===st.columnCount)
          .assert(0===st.parameterCount)
          .assert(0===capi.sqlite3_bind_parameter_count(st))
          .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(false===st.step())
          .mustThrowMatching(()=>st.get(0),
                             "Stmt.step() has not (recently) returned true.")
          .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.");
      }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;
      //wasm.xWrap.debug = true;
      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;
      blob = db.selectValue("select ?1", new Uint8Array([97,0,98,0,99]),
                            sqlite3.capi.SQLITE_TEXT);
      T.assert("a\0b\0c"===blob, "Something is amiss with embedded NULs");
      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(1===st.parameterCount)
          .assert(1===capi.sqlite3_bind_parameter_count(st))
          .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",
          1 ? (pCx,a,b)=>a+b
            : (pCx,a,b)=>{
              /*return sqlite3.capi.sqlite3_result_error_js(
                db, sqlite3.capi.SQLITE_ERROR, "foo???"
              );*/
              console.debug("foo UDF", pCx, a, b);
              return Number(a)+Number(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(sqlite3){
      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(wasm.ptr.add(aVals, wasm.ptr.size)))
                  === 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.");

      /* SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE/WRITE... */
      const db2 = new sqlite3.oo1.DB();
      try{
        capi.sqlite3_db_config(db2, capi.SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE, 0, null);
        T.mustThrow(()=>db2.exec("attach 'attached.db' as foo"),
                    "Cannot create a new db via ATTACH");
        capi.sqlite3_db_config(db2, capi.SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE, 1, null);
        db2.exec([
          "attach 'attached.db' as foo;",
          "create table foo.t(a);",
          "insert into foo.t(a) values(1);",
          "detach foo;"
          ]);
        capi.sqlite3_db_config(db2, capi.SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE, 0, null);
        db2.exec("attach 'attached.db' as foo");
        T.mustThrow(()=>db2.exec("insert into foo.t(a) values(2)"),
                   "ATTACH_WRITE is false");
        capi.sqlite3_db_config(db2, capi.SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE, 1, null);
        db2.exec([
          "detach foo;",
          "attach 'attached.db' as foo;",
          "insert into foo.t(a) values(2);",
          "drop table foo.t;",
          "detach foo"
        ]);
      }finally{
        db2.close();
      }
    })/*ATTACH tests*/

  ////////////////////////////////////////////////////////////////////
    .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 = w.ptr.add(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(!tmplMod.$xUpdate);
        const dbg = 1 ? ()=>{} : sqlite3.config.debug;
        //tmplMod.debugFlags(0x03);
        tmplMod.setupModule({
          catchExceptions: false,
          methods: {
            xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){
              dbg("xConnect",...arguments);
              try{
                const args = wasm.cArgvToJs(argc, argv);
                T.assert(args.length>=3)
                  .assert(args[0] === 'testvtab')
                  .assert(args[1] === 'main')
                  .assert(args[2] === 'testvtab');
                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){
              dbg("xDisconnect",...arguments);
              try {
                VT.xVtab.unget(pVtab).dispose();
                return 0;
              }catch(e){
                return VT.xError('xDisconnect',e);
              }
            },
            xOpen: function(pVtab, ppCursor){
              dbg("xOpen",...arguments);
              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){
              dbg("xClose",...arguments);
              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){
              dbg("xNext",...arguments);
              try{
                const c = VT.xCursor.get(pCursor);
                ++c._rowId;
                return 0;
              }catch(e){
                return VT.xError('xNext',e);
              }
            },
            xColumn: function(pCursor, pCtx, iCol){
              dbg("xColumn",...arguments);
              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){
              dbg("xRowid",...arguments);
              try{
                const c = VT.xCursor.get(pCursor);
                VT.xRowid(ppRowid64, c._rowId);
                return 0;
              }catch(e){
                return VT.xError('xRowid',e);
              }
            },
            xEof: function(pCursor){
              dbg("xEof",...arguments);
              const c = VT.xCursor.get(pCursor),
                    rc = c._rowId>=10;
              return rc;
            },
            xFilter: function(pCursor, idxNum, idxCStr,
                              argc, argv/* [sqlite3_value* ...] */){
              dbg("xFilter",...arguments);
              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){
              dbg("xBestIndex",...arguments);
              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(!tmplMod.$xUpdate)
          .assert(wasm.isPtr(tmplMod.$xRowid))
          .assert(wasm.isPtr(tmplMod.$xCreate))
          .assert(tmplMod.$xCreate === tmplMod.$xConnect,
                  "setupModule() must make these equivalent and "+
                  "installMethods() must avoid re-compiling identical functions");
        tmplMod.$xCreate = wasm.ptr.null /* 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 v1 API availability',
      test: function(sqlite3){
        const capi = sqlite3.capi;
        if( isUIThread() ){
          T.assert( !!capi.sqlite3_js_kvvfs_size )
            .assert( !!capi.sqlite3_js_kvvfs_clear );
        }else{
          /* Historical behaviour retained not for compatibility but
             to help avoid some confusion between the v1 and v2 kvvfs
             APIs (namely in how the v1 variants handle empty
             strings). */
          T.assert( !capi.sqlite3_js_kvvfs_clear )
            .assert( !capi.sqlite3_js_kvvfs_size );
        }
        const k = sqlite3.kvvfs;
        T.assert( k && 'object'===typeof k );
        for(const n of ['reserve', 'import', 'export',
                        'unlink', 'listen', 'unlisten',
                        'exists',
                        'estimateSize', 'clear'] ){
          T.assert( k[n] instanceof Function );
        }

        if( 0 ){
          const scope = wasm.scopedAllocPush();
          try{
            const pg = [
              "53514C69746520666F726D61742033b20b0101b402020d02d02l01d0",
              "4l01jb02b2E91E00Dd011FD3b1FD3dxl2B010617171701377461626C",
              "656B767666736B7676667302435245415445205441424C45206B76766673286129"
            ].join('');
            const n = pg.length;
            const pI = wasm.scopedAlloc( n+1 );
            const nO = 8192 * 2;
            const pO = wasm.scopedAlloc( nO );
            const heap = wasm.heap8u();
            let i;
            for( i=0; i<n; ++i ){
              heap[wasm.ptr.add(pI, i)] = pg.codePointAt(i) & 0xff;
            }
            heap[wasm.ptr.add(pI, i)] = 0;
            const rc = wasm.exports.sqlite3__wasm_kvvfs_decode(pI, pO, nO);
            const u = heap.slice(pO, wasm.ptr.add(pO,rc));
            debug("decode rc=", rc, u);
          }finally{
            wasm.scopedAllocPop(scope);
          }
        }
      }
    }/*kvvfs API availability*/)
    .t({
      name: 'kvvfs sessionStorage',
      predicate: ()=>(globalThis.sessionStorage || "sessionStorage is unavailable"),
      test: function(sqlite3){
        const JDb = sqlite3.oo1.JsStorageDb;
        const pVfs = capi.sqlite3_vfs_find('kvvfs');
        T.assert(looksLikePtr(pVfs));
        const pStorage = sqlite3.kvvfs.internal.storageForZClass('session');
        T.assert( 0 === pStorage.files.length )
          .assert( globalThis.sessionStorage===pStorage.storage )
          .assert( 'kvvfs-session-' === pStorage.keyPrefix );
        const filename = this.kvvfsDbFile = 'session';
        const unlink = this.kvvfsUnlink = ()=>sqlite3.kvvfs.clear(filename);
        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'))
            .assert( db.storageSize() > 0, "Db size counting is broken" );
          db.close();
          db = undefined;
          db = new JDb(filename);
          db.exec('insert into kvvfs(a) values(4),(5),(6)');
          T.assert(6 === db.selectValue('select count(*) from kvvfs'));
        }finally{
          if( db ){
            db.close();
            db = null;
          }
        }
        //console.debug("sessionStorage",globalThis.sessionStorage);
        const corruptJrnl = [
          /* Test that it recovers properly from a bad journal:
             https://sqlite.org/bugs/forumpost/20e208fe172cae4f */
          pStorage.keyPrefix+'jrnl',
          'cb d9d505f920a163d7ffffffffdeadbeef000000010000020000001000'
        ];
        sessionStorage.setItem(...corruptJrnl);
        //console.debug("sessionStorage",globalThis.sessionStorage);
        T.assert( corruptJrnl[1] === sessionStorage.getItem(corruptJrnl[0]) );
        try{
          db = new JDb(filename);
          T.assert(6 === db.selectValue('select count(*) from kvvfs'));
          db.exec('insert into kvvfs(a) values(7),(8),(9)');
          T.assert(9 === db.selectValue('select count(*) from kvvfs'));
          T.assert( corruptJrnl[1] !== sessionStorage.getItem(corruptJrnl[0]) );
        }finally{
          if( db ) db.close();
        }
      }
    }/*kvvfs sanity checks*/)
    .t({
      name: 'transient kvvfs',
      //predicate: ()=>false,
      test: function(sqlite3){
        const filename = '.' /* preinstalled instance */;
        const JDb = sqlite3.oo1.JsStorageDb;
        const DB = sqlite3.oo1.DB;
        T.mustThrowMatching(()=>new JDb(""), capi.SQLITE_MISUSE);
        T.mustThrowMatching(()=>{
          new JDb("this\ns an illegal - contains control characters");
          /* We don't have a way to get error strings from xOpen()
             to this point? xOpen() does not have a handle to the
             db and SQLite is not calling xGetLastError() to fetch
             the error string. */
        }, capi.SQLITE_RANGE);
        T.mustThrowMatching(()=>{new JDb("foo-journal");},
                            capi.SQLITE_MISUSE);
        T.mustThrowMatching(()=>{new JDb("foo-wal");},
                            capi.SQLITE_MISUSE);
        T.mustThrowMatching(()=>{new JDb("foo-shm");},
                            capi.SQLITE_MISUSE);
        T.mustThrowMatching(()=>{
          new JDb("01234567890123456789"+
                  "01234567890123456789"+
                  "01234567890123456789"+
                  "01234567890123456789"+
                  "01234567890123456789"+
                  "01234567890123456789"+
                  "0"/*too long*/);
        }, capi.SQLITE_RANGE);
        {
          const name = "01234567890123456789012" /* max name length */;
          (new JDb(name)).close();
          T.assert( sqlite3.kvvfs.unlink(name) );
        }

        sqlite3.kvvfs.clear(filename);
        let db = new JDb(filename);
        const sqlSetup = [
          'create table kvvfs(a);',
          'insert into kvvfs(a) values(1),(2),(3)'
        ];
        try {
          T.assert( 0===db.storageSize(), "expecting 0 storage size" );
          T.mustThrowMatching(()=>db.clearStorage(), /in-use/);
          //db.clearStorage();
          T.assert( 0===db.storageSize(), "expecting 0 storage size" );
          db.exec(sqlSetup);
          T.assert( 0<db.storageSize(), "expecting non-0 db size" );
          T.mustThrowMatching(()=>db.clearStorage(), /in-use/);
          //db.clearStorage(/*wiping everything out from under it*/);
          T.assert( 0<db.storageSize(), "expecting non-0 storage size" );
          //db.exec(sqlSetup/*that actually worked*/);
          /* Clearing the storage out from under the db does actually
             work so long as we re-initialize it before reading.
             It is tentatively disallowed for sanity's sake rather
             than it not working.
           */
          T.assert( 0<db.storageSize(), "expecting non-0 db size" );
          const close = ()=>{
            db.close();
            db = undefined;
          };
          T.assert(3 === db.selectValue('select count(*) from kvvfs'));
          close();

          const exportDb = sqlite3.kvvfs.export;
          db = new JDb(filename);
          db.exec('insert into kvvfs(a) values(4),(5),(6)');
          T.assert(6 === db.selectValue('select count(*) from kvvfs'));
          const exp = exportDb({name:filename,includeJournal:true});
          T.assert( filename===exp.name, "Broken export filename" )
            .assert( exp?.size > 0, "Missing db size" )
            .assert( exp?.pages?.length > 0, "Missing db pages" );
          console.debug("kvvfs to Object:",exp);
          close();

          const dbFileRaw = 'file:new-storage?vfs=kvvfs&delete-on-close=1';
          db = new DB({
            filename: dbFileRaw,
            //flags: 'ct'
          });
          db.exec(sqlSetup);
          const dbFilename = db.dbFilename();
          //console.warn("db.dbFilename() =",dbFilename);
          T.assert(3 === db.selectValue('select count(*) from kvvfs'));
          debug("kvvfs to Object:",exportDb(dbFilename));
          const n = sqlite3.kvvfs.estimateSize( dbFilename );
          T.assert( n>0, "Db size count failed" );

          if( 1 ){
            // Concurrent open of that same name uses the same storage
            const x = new JDb(dbFilename);
            T.assert(3 === db.selectValue('select count(*) from kvvfs'));
            x.close();
          }
          close();
          // When the final instance of a name is closed, the storage
          // disappears...
          T.mustThrowMatching(function(){
            /* Ensure that 'new-storage' was deleted when its refcount
               went to 0, because of its 'transient' flag. By default
               the objects are retained, like a filesystem would. */
            let ddb = new JDb(dbFilename);
            try{ddb.selectValue('select a from kvvfs')}
            finally{ddb.close()}
          }, /no such table: kvvfs/);
        }finally{
          if( db ) db.close();
        }
      }
    }/*transient kvvfs*/)
    .t({
      name: 'concurrent transient kvvfs',
      //predicate: ()=>false,
      test: function(sqlite3){
        const filename = 'myStorage';
        const kvvfs = sqlite3.kvvfs;
        const DB = sqlite3.oo1.DB;
        const JDb = sqlite3.oo1.JsStorageDb;
        let db;
        let duo;
        let q1, q2;
        const sqlSetup = [
          'create table kvvfs(a);',
          'insert into kvvfs(a) values(1),(2),(3)'
        ];
        const sqlCount = 'select count(*) from kvvfs';

        try {
          const exportDb = sqlite3.kvvfs.export;
          const dbFileRaw = 'file:'+filename+'?vfs=kvvfs&delete-on-close=1';
          sqlite3.kvvfs.clear(filename);
          db = new DB(dbFileRaw);
          db.exec(sqlSetup);
          T.assert(3 === db.selectValue(sqlCount));

          duo = new JDb(filename);
          duo.exec('insert into kvvfs(a) values(4),(5),(6)');
          T.assert(6 === db.selectValue(sqlCount));
          const expOpt = {
            name: filename,
            decodePages: true
          };
          let exp = exportDb(expOpt);
          let expectRows = 6;
          debug("exported db",exp);
          db.close();
          T.assert(expectRows === duo.selectValue(sqlCount));
          duo.close();
          T.mustThrowMatching(function(){
            let ddb = new JDb(filename);
            try{ddb.selectValue('select a from kvvfs')}
            finally{ddb.close()}
          }, /.*no such table: kvvfs.*/);

          T.assert( kvvfs.unlink(filename) )
            .assert( !kvvfs.exists(filename) );

          const importDb = sqlite3.kvvfs.import;
          duo = new JDb(dbFileRaw);
          T.mustThrowMatching(()=>importDb(exp,true), /.*in use.*/);
          duo.close();
          importDb(exp, true);
          duo = new JDb(dbFileRaw);
          T.assert(expectRows === duo.selectValue(sqlCount));
          let newCount;
          try{
            duo.transaction(()=>{
              duo.exec("insert into kvvfs(a) values(7)");
              newCount = duo.selectValue(sqlCount);
              T.assert(false, "rolling back");
            });
          }catch(e){/*ignored*/}
          T.assert(7===newCount, "Unexpected row count before rollback")
            .assert(expectRows === duo.selectValue(sqlCount),
                    "Unexpected row count after rollback");
          duo.close();

          T.assert( kvvfs.unlink(filename) )
            .assert( !kvvfs.exists(filename) );

          importDb(exp, true);
          db = new JDb({
            filename,
            flags: 'c'
            /* BUG: without the 'c' flag, the db works until we try to
               vacuum, at which point it fails with "read only db". */
          });
          duo = new JDb(filename);
          T.assert(expectRows === duo.selectValue(sqlCount));
          const sqlIns1 = "insert into kvvfs(a) values(?)";
          q1 = db.prepare(sqlIns1);
          q2 = duo.prepare(sqlIns1);
          if( 0 ){
            q1.bind('from q1').stepFinalize();
            ++expectRows;
            T.assert(expectRows === duo.selectValue(sqlCount),
                     "Unexpected record count.");
            q2.bind('from q1').stepFinalize();
            ++expectRows;
          }else{
            q1.bind('from q1');
            T.assert(capi.SQLITE_DONE===capi.sqlite3_step(q1),
                     "Unexpected step result");
            ++expectRows;
            T.assert(expectRows === duo.selectValue(sqlCount),
                     "Unexpected record count.");
            q2.bind('from q1').step();
            ++expectRows;
          }
          T.assert(expectRows === db.selectValue(sqlCount),
                   "Unexpected record count.");
          q1.finalize();
          q2.finalize();

          if( 1 ){
            debug("Begin vacuum/page size test...");
            const defaultPageSize = 1024 * 8 /* build-time default */;
            const pageSize = 0
                  ? defaultPageSize
                  : 1024 * 16 /* any valid value other than the default */;
            if( 0 ){
              debug("Export before vacuum", exportDb(expOpt));
              debug("page size before vacuum",
                    db.selectArray(
                      "select page_size from pragma_page_size()"
                    ));
            }
            //kvvfs.log.xFileControl = true;
            //kvvfs.log.xAccess = true;
            db.exec([
              "BEGIN;",
              "insert into kvvfs(a) values(randomblob(16000/*>pg size*/));",
              "COMMIT;",
              "delete from kvvfs where octet_length(a)>100;",
              "pragma page_size="+pageSize+";",
              "vacuum;",
              "select 1;"
            ]);
            const expectPageSize = kvvfs.internal.disablePageSizeChange
                  ? defaultPageSize
                  : pageSize;
            const gotPageSize = db.selectValue(
              "select page_size from pragma_page_size()"
            );
            T.assert(+gotPageSize === expectPageSize,
                     "Expecting page size",expectPageSize,
                     "got",gotPageSize);
            T.assert(expectRows === duo.selectValue(sqlCount),
                     "Unexpected record count.");
            kvvfs.log.xAccess = kvvfs.log.xFileControl = false;
            T.assert(expectRows === duo.selectValue(sqlCount),
                     "Unexpected record count.");
            exp = exportDb(expOpt);
            debug("Exported page-expanded db",exp);
            if( 0 ){
              debug("vacuumed export",exp);
            }
            debug("End vacuum/page size test.");
          }else{
            expectRows = 6;
          }

          db.close();
          duo.close();
          T.assert( kvvfs.unlink(exp.name) )
            .assert( !kvvfs.exists(exp.name) );
          importDb(exp);
          T.assert( kvvfs.exists(exp.name) );
          db = new JDb(exp.name);
          //debug("column count after export",db.selectValue(sqlCount));
          T.assert(expectRows === db.selectValue(sqlCount),
                   "Unexpected record count.");

          /*
            TODO: more advanced concurrent use tests, e.g. looping
            over a query in one connection while writing from
            another. Currently that will probably corrupt the db, and
            kvvfs's journaling does not support multiple journals per
            storage unit. We need to test the locking and fix it as
            appropriate.
          */
        }finally{
          q1?.finalize?.();
          q2?.finalize?.();
          db?.close?.();
          duo?.close?.();
        }
      }
    }/*concurrent transient kvvfs*/)

    .t({
      name: 'kvvfs listeners (experiment)',
      test: function(sqlite3){
        const kvvfs = sqlite3.kvvfs;
        const filename = 'listen';
        let db;
        try {
          const DB = sqlite3.oo1.DB;
          const sqlSetup = [
            'create table kvvfs(a);',
            'insert into kvvfs(a) values(1),(2),(3)'
          ];
          const sqlCount = "select count(*) from kvvfs";
          const sqlSelectSchema = "select * from sqlite_schema";
          const counts = Object.create(null);
          const incr = (key)=>counts[key] = 1 + (counts[key] ?? 0);
          const pglog = Object.assign(Object.create(null),{
            pages: [],
            jrnl: undefined,
            size: undefined,
            includeJournal: false,
            decodePages: true,
            exception: new Error("Testing that exceptions from listeners do not interfere") 
         });
          const toss = ()=>{
            if( pglog.exception ){
              const e = pglog.exception;
              delete pglog.exception;
              throw e;
            }
          };

          const listener = {
            storage: filename,
            reserve: true,
            includeJournal: pglog.includeJournal,
            decodePages: pglog.decodePages,
            events: {
              /**
                 These may be async but must not be in this case
                 because we can't test their result without a lot of
                 hoop-jumping if they are. Kvvfs calls these
                 asynchronously, though.
               */
              'open':   (ev)=>{
                //console.warn('open',ev);
                incr(ev.type);
                T.assert(filename===ev.storageName)
                  .assert('number'===typeof ev.data);
              },
              'close': (ev)=>{
                //console.warn('close',ev);
                incr(ev.type);
                T.assert('number'===typeof ev.data);
                toss();
              },
              'delete': (ev)=>{
                //console.warn('delete',ev);
                incr(ev.type);
                T.assert('string'===typeof ev.data);
                switch(ev.data){
                  case 'jrnl':
                    T.assert(pglog.includeJournal);
                    pglog.jrnl = null;
                    break;
                  default:{
                    const n = +ev.data;
                    T.assert( n>0, "Expecting positive db page number" );
                    if( n < pglog.pages.length ){
                      pglog.size = undefined;
                    }
                    pglog.pages[n] = undefined;
                    break;
                  }
                }
              },
              'sync': (ev)=>{
                incr(ev.data ? 'xSync' : 'xFileControlSync');
              },
              'write':  (ev)=>{
                //console.warn('write',ev);
                incr(ev.type);
                T.assert(Array.isArray(ev.data));
                const key = ev.data[0], val = ev.data[1];
                T.assert('string'===typeof key);
                switch( key ){
                  case 'jrnl':
                    T.assert(pglog.includeJournal);
                    pglog.jrnl = val;
                    break;
                  case 'sz':{
                    const sz = +val;
                    T.assert( sz>0, "Expecting a db page number" );
                    if( sz < pglog.sz ){
                      pglog.pages.length = sz / pglog.pages.length;
                    }
                    pglog.size = sz;
                    break;
                  }
                  default:
                    T.assert( +key>0, "Expecting a positive db page number" );
                    pglog.pages[+key] = val;
                    if( pglog.decodePages ){
                      T.assert( val instanceof Uint8Array );
                    }else{
                      T.assert( 'string'===typeof val );
                    }
                    break;
                }
              }
            }
          };

          kvvfs.listen(listener);
          const dbFileRaw = 'file:'+filename+'?vfs=kvvfs&delete-on-close=1';
          const expOpt = {
            name: filename,
            //decodePages: true
          };
          db = new DB(dbFileRaw);
          db.exec(sqlSetup);
          T.assert(db.selectObjects(sqlSelectSchema)?.length>0,
                   "Unexpected empty schema");
          db.close();
          debug("kvvfs listener counts:",counts);
          T.assert( counts.open );
          T.assert( counts.close );
          T.assert( listener.includeJournal ? counts.delete : !counts.delete );
          T.assert( counts.write );
          T.assert( counts.xSync );
          T.assert( counts.xFileControlSync>=counts.xSync );
          T.assert( counts.open===counts.close );
          T.assert( pglog.includeJournal
                    ? (null===pglog.jrnl)
                    : (undefined===pglog.jrnl),
                     "Unexpected pglog.jrnl value: "+pglog.jrnl );
          if( 1 ){
            T.assert(undefined===pglog.pages[0], "Expecting empty slot 0");
            pglog.pages.shift();
            //debug("kvvfs listener pageLog", pglog);
          }
          const before = JSON.stringify(counts);
          T.assert( kvvfs.unlisten(listener) );
          T.assert( !kvvfs.unlisten(listener) );
          db = new DB(dbFileRaw);
          T.assert( db.selectObjects(sqlSelectSchema)?.length>0 );
          const exp = kvvfs.export(expOpt);
          const expectRows = db.selectValue(sqlCount);
          db.exec("delete from kvvfs");
          db.close();
          const after = JSON.stringify(counts);
          T.assert( before===after, "Expecting no events after unlistening." );
          if( 0 ){
            exp = kvvfs.export(expOpt);
            debug("Post-delete export:",exp);
          }
          if( 1 ){
            // Replace the storage with the pglog state...
            const bogoExp = {
              name: filename,
              size: pglog.size,
              timestamp: Date.now(),
              pages: pglog.pages
            };
            //debug("exp",exp);
            //debug("bogoExp",bogoExp);
            kvvfs.import(bogoExp, true);
            //debug("Re-exported", kvvfs.export(expOpt));
            db = new DB(dbFileRaw);
            // Failing on the next line despite exports looking good
            T.assert(db.selectObjects(sqlSelectSchema)?.length>0,
                     "Empty schema on imported db");
            T.assert(expectRows===db.selectValue(sqlCount));
            db.close();
          }
        }finally{
          db?.close?.();
          kvvfs.unlink(filename);
        }
      }
    })/*kvvfs listeners */

    .t({
      name: 'kvvfs vtab',
      predicate: (sqlite3)=>!!sqlite3.kvvfs.create_module,
      test: function(sqlite3){
        const kvvfs = sqlite3.kvvfs;
        const db = new sqlite3.oo1.DB();
        const db2 = new sqlite3.oo1.DB('file:foo?vfs=kvvfs&delete-on-close=1');
        try{
          kvvfs.create_module(db);
          let rc = db.selectObjects("select * from sqlite_kvvfs order by name");
          debug("sqlite_kvvfs vtab:", rc);
          const nDb = rc.length;
          rc = db.selectObject("select * from sqlite_kvvfs where name='foo'");
          T.assert(rc, "Expecting foo storage record")
            .assert('foo'===rc.name, "Unexpected name")
            .assert(1===rc.nRef, "Unexpected refcount");
          db2.close();
          rc = db.selectObjects("select * from sqlite_kvvfs");
          T.assert( !rc.filter((o)=>o.name==='foo').length,
                    "Expecting foo storage to be gone");
          debug("sqlite_kvvfs vtab:", rc);
          T.assert( rc.length+1 === nDb,
                    "Unexpected storage count: got",rc.length,"expected",nDb);
        }finally{
          db.close();
          db2.close();
        }
      }
    })/* kvvfs vtab */

//#if enable-see
    .t({
      name: 'kvvfs SEE encryption in sessionStorage',
      predicate: ()=>(!!globalThis.sessionStorage
                      || "sessionStorage is not available"),
      test: function(sqlite3){
        const JDb = sqlite3.oo1.JsStorageDb;
        T.seeBaseCheck(JDb,
                       (isInit)=>{
                         return {filename: "session"};
                       },
                       ()=>JDb.clearStorage('session'));
      }
    })/*kvvfs with SEE*/
//#/if 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)=>{
          //debug("commit hook",arguments);
          ++countCommit;
          return (17 == p) ? 0 : capi.SQLITE_ERROR;
        }, 17);
        T.assert( wasm.ptr.null === rc );

        // 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);
        T.assert(17 == capi.sqlite3_commit_hook(db, 0, 0));

        // Rollback hook:
        rc = capi.sqlite3_rollback_hook(db, (p)=>{
          ++countRollback;
          T.assert( 21 == p );
        }, 21);
        T.assert( wasm.ptr.null===rc );
        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)=>{
          //console.error("transaction error:",e);
          return capi.SQLITE_FULL === e.resultCode
        });
        T.assert(1 === countRollback);
        T.assert(21 == capi.sqlite3_rollback_hook(db, 0, 0));

        // 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(33==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);
          }
        }, 33);
        db.transaction((d)=>{
          db.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(2 === 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(33 == 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
        );
        T.assert( 0==rc );
        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;
        T.assert( !!capi.sqlite3_preupdate_hook(db, 0, 0) );
        //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false;
        T.assert( !capi.sqlite3_preupdate_hook(db, 0, 0) );
        db.close();
      }
    })/*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*/;

  ////////////////////////////////////////////////////////////////////////
  // Tests for "opfs" and "opfs-wl" are essentially identical, so...
//#query {
  select 'opfs' vfsName, 'OPFS: Origin-Private File System' label, 'OpfsDb' oo1Ctor
  UNION ALL
  select 'opfs-wl', 'OPFS with Web Locks', 'OpfsWlDb'
}
   T.g('@vfsName@: @label@',
      (sqlite3)=>(!!capi.sqlite3_vfs_find("@vfsName@") || 'requires "@vfsName@" VFS'))
    .t({
      name: '@vfsName@ db sanity checks',
      test: async (sqlite3)=>{
        await T.opfsCommon.sanityChecks('@vfsName@', sqlite3.oo1.@oo1Ctor@, sqlite3);
      }
    })
    .t({
      name: '@vfsName@ import/export',
      test: async (sqlite3)=>{
        await T.opfsCommon.importer('@vfsName@', sqlite3.oo1.@oo1Ctor@, sqlite3);
      }
    })
//#if vfsName = "opfs"
//#// This is independent of the VFS, so only test this once
    .t({
      name: '(Internal-use) OPFS utility APIs',
      test: async (sqlite3)=>{
        await T.opfsCommon.opfsUtil("@vfsName@", sqlite3.oo1.@oo1Ctor@, sqlite3);
      }
    })
//#/if
//#if enable-see
    .t({
      name: '@vfsName@ with SEE encryption',
      predicate: (sqlite3)=>!!sqlite3.oo1.@oo1Ctor@,
      test: function(sqlite3){
        T.seeBaseCheck(
          sqlite3.oo1.@oo1Ctor@,
          function(isInit){
            const opt = {filename: 'file:///sqlite3-see.edb'};
            if( isInit ) opt.filename += '?delete-before-open=1';
            return opt;
          },
          ()=>{}
        );
      }
    })
//#/if enable-see
  ;/* end OPFS tests */
//#/query
  ////////////////////////////////////////////////////////////////////////
  T.g('OPFS SyncAccessHandle Pool VFS',
      (sqlite3)=>(isWorker() && !!sqlite3.installOpfsSAHPoolVfs || "requires OPFS SAH Pool 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';
        //wasm.xWrap.debug = true;
        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())
          .mustThrowMatching(
            ()=>u1.pauseVfs(),
            (err)=>{
              return capi.SQLITE_MISUSE===err.resultCode
                && /^SQLITE_MISUSE: Cannot pause VFS /.test(err.message);
            },
            "Cannot pause VFS with opened db."
          );
        db.close();
        T.assert( u2===u2.pauseVfs() )
          .assert( u2.isPaused() )
          .assert( !capi.sqlite3_vfs_find(u2.vfsName) )
          .mustThrowMatching(()=>new u2.OpfsSAHPoolDb(dbName),
                             /.+no such vfs: .+/,
                             "VFS is not available")
          .assert( u2===await u2.unpauseVfs() )
          .assert( u2===await u1.unpauseVfs(), "unpause is a no-op if the VFS is not paused" )
          .assert( !!capi.sqlite3_vfs_find(u2.vfsName) );
        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*/)
//#if enable-see
    .t({
      name: 'OPFS SAHPool with SEE encryption',
      test: async function(sqlite3){
        const inst = sqlite3.installOpfsSAHPoolVfs,
              catcher = (e)=>{
                error("Cannot load SAH pool VFS.",
                      "This might not be a problem,",
                      "depending on the environment.");
                return false;
              };
        const poolConfig = {
          name: 'opfs-sahpool-see',
          clearOnInit: true,
          initialCapacity: 6
        }
        let poolUtil;
        const P1 = await inst(poolConfig).then(u=>poolUtil = u).catch(catcher);
        const dbFile = '/sqlite3-see.edb';
        T.seeBaseCheck(
          poolUtil.OpfsSAHPoolDb,
          (isInit)=>{return {filename: dbFile}},
          ()=>poolUtil.unlink(dbFile)
        );
        poolUtil.removeVfs();
      }
    })/*opfs-sahpool with SEE*/
//#/if enable-see
  ;

  ////////////////////////////////////////////////////////////////////////
  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===stmt.parameterCount )
        .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();
    })

    /**
       Ensure that certain Stmt members throw when called
       via DB.exec().
    */
    .t('locked-by-exec() APIs', function(sqlite3){
      const db = new sqlite3.oo1.DB();
      db.exec("create table t(a);insert into t(a) values(1);");
      let checkCount = 0;
      const checkOp = function(op){
        ++checkCount;
        T.mustThrowMatching(() => {
          db.exec({
            sql: "select ?1",
            bind: op,
            callback: (row, stmt) => {
              switch (row[0]) {
                case 'bind': stmt.bind(1); break;
                case 'finalize':
                case 'clearBindings':
                case 'reset':
                case 'step': stmt[op](); break;
              }
            }
          });
        }, /^Operation is illegal when statement is locked.*/)
      };
      try{
        checkOp('bind');
        checkOp('finalize');
        checkOp('clearBindings');
        checkOp('reset');
        checkOp('step');
        T.assert(5===checkCount);
      }finally{
        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)");
      let stmt;
      try{
        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() );

        if( wasm.exports.sqlite3_column_origin_name ){
          log("Column metadata APIs enabled");
          T.assert( "t" === capi.sqlite3_column_table_name(stmt, 0))
            .assert("a" === capi.sqlite3_column_origin_name(stmt, 0))
            .assert("main" === capi.sqlite3_column_database_name(stmt, 0))
        }else{
          log("Column metadata APIs not enabled");
        } // column metadata APIs
        stmt.finalize(); stmt = null;
        stmt = db.prepare("select ?1").bind(new Uint8Array([97,0,98,0,99]));
        stmt.step();
        const sv = capi.sqlite3_column_value(stmt,0);
        T.assert("a\0b\0c"===capi.sqlite3_value_text(sv),
                 "Expecting NULs to have survived.");
        stmt.finalize(); stmt = null;

        /* sqlite3_bind_zeroblob() (added in 3.53) */
        stmt = db.prepare("select ?1");
        T.assert( 0===capi.sqlite3_bind_zeroblob(stmt, 1, 53) );
        T.assert( stmt.step() );
        const b = stmt.get(0);
        stmt.finalize(); stmt = null;
        T.assert( b instanceof Uint8Array )
          .assert( 53===b.length );

      }finally{
        if(stmt) 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("sqlite3_set_errmsg()", function(sqlite3){
      /* Added in 3.51.0 */
      const db = new sqlite3.oo1.DB();//(':memory:','wt');
      try{
        const capi = sqlite3.capi;
        const sse = capi.sqlite3_set_errmsg,
              sec = capi.sqlite3_errcode,
              sem = capi.sqlite3_errmsg;
        T.assert( 0===sec(db) )
          .assert( "not an error"===sem(db) );
        let rc = sse(db, capi.SQLITE_RANGE, "nope");
        T.assert( 0==rc )
          .assert( capi.SQLITE_RANGE===sec(db) )
          .assert( "nope"===sem(db) );
        rc = sse(0, 0, 0);
        T.assert( capi.SQLITE_MISUSE===rc );
        rc = sse(db, 0, 0);
        T.assert( 0===rc )
          .assert( 0===sec(db) )
          .assert( "not an error"===sem(db) );
      }finally{
        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. This isn't testing a fix, just confirming
           that the bug report is not really a bug.
        */
        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();
        }
      }
    })
    .t({
      /* https://github.com/sqlite/sqlite-wasm/issues/92 */
      name: 'sqlite3_set_auxdata() binding signature',
      test: function(sqlite3){
        const db = new sqlite3.oo1.DB();
        const stack = wasm.pstack.pointer;
        const pAux = wasm.pstack.alloc(4);
        let pAuxDestructed = 0;
        const pAuxDtor = wasm.installFunction('v(p)', function(ptr){
          //log("freeing auxdata");
          ++pAuxDestructed;
        });
        let pAuxDtorDestructed = false;
        db.onclose = {
          after: ()=>{
            pAuxDtorDestructed = true;
            wasm.uninstallFunction(pAuxDtor);
          }
        };
        let nAuxSet = 0 /* how many times we set aux data */;
        let nAuxReused = 0 /* how many times we reused aux data */;
        try{
          db.createFunction("auxtest",{
            xFunc: function(pCx, x, y){
              T.assert(wasm.isPtr(pCx));
              const localAux = capi.sqlite3_get_auxdata(pCx, 0);
              if( !localAux ){
                //log("setting auxdata");
                /**
                   We do not currently an automated way to clean up
                   auxdata finalizer functions (the 4th argument to
                   sqlite3_set_auxdata()) which get automatically
                   converted from JS to WASM. Because of that, enabling
                   automated conversions here would lead to leaks more
                   often than not. Instead, follow the pattern show in
                   this function: use wasm.installFunction() to create
                   the function, then pass the resulting function
                   pointer this function, and cleanup (at some point)
                   using wasm.uninstallFunction().
                */
                ++nAuxSet;
                capi.sqlite3_set_auxdata(pCx, 0, pAux, pAuxDtor);
              }else{
                //log("reusing auxdata",localAux);
                T.assert(pAux===localAux);
                ++nAuxReused;
              }
              return x;
            }
          });
          db.exec([
            "create table t(a);",
            "insert into t(a) values(1),(2),(1);",
            "select auxtest(1,a), auxtest(1,a) from t order by a"
          ]);
        }finally{
          db.close();
          wasm.pstack.restore(stack);
        }
        T.assert(nAuxSet>0).assert(nAuxReused>0)
          .assert(6===nAuxReused+nAuxSet);
        T.assert(pAuxDestructed>0);
        T.assert(pAuxDtorDestructed);
      }
    })
  ;/*end of Bug Reports group*/;

  ////////////////////////////////////////////////////////////////////////
  log("Loading and initializing sqlite3 WASM module...");
  if(1){
    globalThis.sqlite3ApiConfig = {
      //debug: ()=>{}, log: ()=>{}, warn: ()=>{}, error: ()=>{},
      disable: {
        vfs: {
          kvvfs: false,
          opfs: false,
          "opfs-sahpool": false,
          "opfs-wl": false
        }
      }
    }
  }
//#if not 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@'
        .split('/').pop()/*the build-injected name has a dir part and
                           we specifically want to test the following
                           support for locating the wasm, so remove
                           that dir part. */;
    const urlParams = new URL(globalThis.location.href).searchParams;
    if(urlParams.has('sqlite3.dir')){
      sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
    }
    importScripts(sqlite3Js);
  }
//#/if
  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(', '));
    SQLite3 = sqlite3;
    log("WASM pointer size:",wasm.ptr.size,"bytes.");
    TestUtil.checkHeapSize();
    TestUtil.runTests(sqlite3);
  });
})(self);
