summaryrefslogtreecommitdiffstats
path: root/ext/wasm/SQLTester
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 14:07:11 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 14:07:11 +0000
commit63847496f14c813a5d80efd5b7de0f1294ffe1e3 (patch)
tree01c7571c7c762ceee70638549a99834fdd7c411b /ext/wasm/SQLTester
parentInitial commit. (diff)
downloadsqlite3-63847496f14c813a5d80efd5b7de0f1294ffe1e3.tar.xz
sqlite3-63847496f14c813a5d80efd5b7de0f1294ffe1e3.zip
Adding upstream version 3.45.1.upstream/3.45.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ext/wasm/SQLTester')
-rw-r--r--ext/wasm/SQLTester/GNUmakefile55
-rw-r--r--ext/wasm/SQLTester/SQLTester.mjs1339
-rw-r--r--ext/wasm/SQLTester/SQLTester.run.mjs148
-rw-r--r--ext/wasm/SQLTester/index.html127
-rw-r--r--ext/wasm/SQLTester/touint8array.c29
5 files changed, 1698 insertions, 0 deletions
diff --git a/ext/wasm/SQLTester/GNUmakefile b/ext/wasm/SQLTester/GNUmakefile
new file mode 100644
index 0000000..8fa1247
--- /dev/null
+++ b/ext/wasm/SQLTester/GNUmakefile
@@ -0,0 +1,55 @@
+#!/this/is/make
+#
+# This makefile compiles SQLTester test files into something
+# we can readily import into JavaScript.
+all:
+
+SHELL := $(shell which bash 2>/dev/null)
+MAKEFILE := $(lastword $(MAKEFILE_LIST))
+CLEAN_FILES :=
+DISTCLEAN_FILES := ./--dummy-- *~
+
+test-list.mjs := test-list.mjs
+test-list.mjs.gz := $(test-list.mjs).gz
+CLEAN_FILES += $(test-list.mjs)
+
+tests.dir := $(firstword $(wildcard tests ../../jni/src/tests))
+$(info test script dir=$(tests.dir))
+
+tests.all := $(wildcard $(tests.dir)/*.test)
+
+bin.touint8array := ./touint8array
+$(bin.touint8array): $(bin.touint8array).c $(MAKEFILE)
+ $(CC) -o $@ $<
+CLEAN_FILES += $(bin.touint8array)
+
+ifneq (,$(tests.all))
+$(test-list.mjs): $(bin.touint8array) $(tests.all) $(MAKEFILE)
+ @{\
+ echo 'export default ['; \
+ sep=''; \
+ for f in $(sort $(tests.all)); do \
+ echo -en $$sep'{"name": "'$${f##*/}'", "content":'; \
+ $(bin.touint8array) < $$f; \
+ echo -n '}'; \
+ sep=',\n'; \
+ done; \
+ echo '];'; \
+ } > $@
+ @echo "Created $@"
+$(test-list.mjs.gz): $(test-list.mjs)
+ gzip -c $< > $@
+CLEAN_FILES += $(test-list.mjs.gz)
+all: $(test-list.mjs.gz)
+else
+ @echo "Cannot build $(test-list.mjs) for lack of input test files."; \
+ echo "Symlink ./tests to a directory containing SQLTester-format "; \
+ echo "test scripts named *.test, then try again"; \
+ exit 1
+endif
+
+.PHONY: clean distclean
+clean:
+ -rm -f $(CLEAN_FILES)
+distclean: clean
+ -rm -f $(DISTCLEAN_FILES)
diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs
new file mode 100644
index 0000000..a72399a
--- /dev/null
+++ b/ext/wasm/SQLTester/SQLTester.mjs
@@ -0,0 +1,1339 @@
+/*
+** 2023-08-29
+**
+** 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.
+**
+*************************************************************************
+** This file contains the main application entry pointer for the JS
+** implementation of the SQLTester framework.
+**
+** This version is not well-documented because it's a direct port of
+** the Java immplementation, which is documented: in the main SQLite3
+** source tree, see ext/jni/src/org/sqlite/jni/tester/SQLite3Tester.java.
+*/
+
+import sqlite3ApiInit from '/jswasm/sqlite3.mjs';
+
+const sqlite3 = await sqlite3ApiInit();
+
+const log = (...args)=>{
+ console.log('SQLTester:',...args);
+};
+
+/**
+ Try to install vfsName as the new default VFS. Once this succeeds
+ (returns true) then it becomes a no-op on future calls. Throws if
+ vfs registration as the default VFS fails but has no side effects
+ if vfsName is not currently registered.
+*/
+const tryInstallVfs = function f(vfsName){
+ if(f.vfsName) return false;
+ const pVfs = sqlite3.capi.sqlite3_vfs_find(vfsName);
+ if(pVfs){
+ log("Installing",'"'+vfsName+'"',"as default VFS.");
+ const rc = sqlite3.capi.sqlite3_vfs_register(pVfs, 1);
+ if(rc){
+ sqlite3.SQLite3Error.toss(rc,"While trying to register",vfsName,"vfs.");
+ }
+ f.vfsName = vfsName;
+ }
+ return !!pVfs;
+};
+tryInstallVfs.vfsName = undefined;
+
+if( 0 && globalThis.WorkerGlobalScope ){
+ // Try OPFS storage, if available...
+ if( 0 && sqlite3.oo1.OpfsDb ){
+ /* Really slow with these tests */
+ tryInstallVfs("opfs");
+ }
+ if( sqlite3.installOpfsSAHPoolVfs ){
+ await sqlite3.installOpfsSAHPoolVfs({
+ clearOnInit: true,
+ initialCapacity: 15,
+ name: 'opfs-SQLTester'
+ }).then(pool=>{
+ tryInstallVfs(pool.vfsName);
+ }).catch(e=>{
+ log("OpfsSAHPool could not load:",e);
+ });
+ }
+}
+
+const wPost = (function(){
+ return (('undefined'===typeof WorkerGlobalScope)
+ ? ()=>{}
+ : (type, payload)=>{
+ postMessage({type, payload});
+ });
+})();
+//log("WorkerGlobalScope",globalThis.WorkerGlobalScope);
+
+// Return a new enum entry value
+const newE = ()=>Object.create(null);
+
+const newObj = (props)=>Object.assign(newE(), props);
+
+/**
+ Modes for how to escape (or not) column values and names from
+ SQLTester.execSql() to the result buffer output.
+*/
+const ResultBufferMode = Object.assign(Object.create(null),{
+ //! Do not append to result buffer
+ NONE: newE(),
+ //! Append output escaped.
+ ESCAPED: newE(),
+ //! Append output as-is
+ ASIS: newE()
+});
+
+/**
+ Modes to specify how to emit multi-row output from
+ SQLTester.execSql() to the result buffer.
+*/
+const ResultRowMode = newObj({
+ //! Keep all result rows on one line, space-separated.
+ ONLINE: newE(),
+ //! Add a newline between each result row.
+ NEWLINE: newE()
+});
+
+class SQLTesterException extends globalThis.Error {
+ constructor(testScript, ...args){
+ if(testScript){
+ super( [testScript.getOutputPrefix()+": ", ...args].join('') );
+ }else{
+ super( args.join('') );
+ }
+ this.name = 'SQLTesterException';
+ }
+ isFatal() { return false; }
+}
+
+SQLTesterException.toss = (...args)=>{
+ throw new SQLTesterException(...args);
+}
+
+class DbException extends SQLTesterException {
+ constructor(testScript, pDb, rc, closeDb=false){
+ super(testScript, "DB error #"+rc+": "+sqlite3.capi.sqlite3_errmsg(pDb));
+ this.name = 'DbException';
+ if( closeDb ) sqlite3.capi.sqlite3_close_v2(pDb);
+ }
+ isFatal() { return true; }
+}
+
+class TestScriptFailed extends SQLTesterException {
+ constructor(testScript, ...args){
+ super(testScript,...args);
+ this.name = 'TestScriptFailed';
+ }
+ isFatal() { return true; }
+}
+
+class UnknownCommand extends SQLTesterException {
+ constructor(testScript, cmdName){
+ super(testScript, cmdName);
+ this.name = 'UnknownCommand';
+ }
+ isFatal() { return true; }
+}
+
+class IncompatibleDirective extends SQLTesterException {
+ constructor(testScript, ...args){
+ super(testScript,...args);
+ this.name = 'IncompatibleDirective';
+ }
+}
+
+//! For throwing where an expression is required.
+const toss = (errType, ...args)=>{
+ throw new errType(...args);
+};
+
+const __utf8Decoder = new TextDecoder();
+const __utf8Encoder = new TextEncoder('utf-8');
+//! Workaround for Util.utf8Decode()
+const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer)
+ ? function(){} : globalThis.SharedArrayBuffer;
+
+
+/* Frequently-reused regexes. */
+const Rx = newObj({
+ requiredProperties: / REQUIRED_PROPERTIES:[ \t]*(\S.*)\s*$/,
+ scriptModuleName: / SCRIPT_MODULE_NAME:[ \t]*(\S+)\s*$/,
+ mixedModuleName: / ((MIXED_)?MODULE_NAME):[ \t]*(\S+)\s*$/,
+ command: /^--(([a-z-]+)( .*)?)$/,
+ //! "Special" characters - we have to escape output if it contains any.
+ special: /[\x00-\x20\x22\x5c\x7b\x7d]/,
+ squiggly: /[{}]/
+});
+
+const Util = newObj({
+ toss,
+
+ unlink: function(fn){
+ return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn);
+ },
+
+ argvToString: (list)=>{
+ const m = [...list];
+ m.shift() /* strip command name */;
+ return m.join(" ")
+ },
+
+ utf8Decode: function(arrayBuffer, begin, end){
+ return __utf8Decoder.decode(
+ (arrayBuffer.buffer instanceof __SAB)
+ ? arrayBuffer.slice(begin, end)
+ : arrayBuffer.subarray(begin, end)
+ );
+ },
+
+ utf8Encode: (str)=>__utf8Encoder.encode(str),
+
+ strglob: sqlite3.wasm.xWrap('sqlite3_wasm_SQLTester_strglob','int',
+ ['string','string'])
+})/*Util*/;
+
+class Outer {
+ #lnBuf = [];
+ #verbosity = 0;
+ #logger = console.log.bind(console);
+
+ constructor(func){
+ if(func) this.setFunc(func);
+ }
+
+ logger(...args){
+ if(args.length){
+ this.#logger = args[0];
+ return this;
+ }
+ return this.#logger;
+ }
+
+ out(...args){
+ if( this.getOutputPrefix && !this.#lnBuf.length ){
+ this.#lnBuf.push(this.getOutputPrefix());
+ }
+ this.#lnBuf.push(...args);
+ return this;
+ }
+
+ #outlnImpl(vLevel, ...args){
+ if( this.getOutputPrefix && !this.#lnBuf.length ){
+ this.#lnBuf.push(this.getOutputPrefix());
+ }
+ this.#lnBuf.push(...args,'\n');
+ const msg = this.#lnBuf.join('');
+ this.#lnBuf.length = 0;
+ this.#logger(msg);
+ return this;
+ }
+
+ outln(...args){
+ return this.#outlnImpl(0,...args);
+ }
+
+ outputPrefix(){
+ if( 0==arguments.length ){
+ return (this.getOutputPrefix
+ ? (this.getOutputPrefix() ?? '') : '');
+ }else{
+ this.getOutputPrefix = arguments[0];
+ return this;
+ }
+ }
+
+ static #verboseLabel = ["πŸ”ˆ",/*"πŸ”‰",*/"πŸ”Š","πŸ“’"];
+ verboseN(lvl, args){
+ if( this.#verbosity>=lvl ){
+ this.#outlnImpl(lvl, Outer.#verboseLabel[lvl-1],': ',...args);
+ }
+ }
+ verbose1(...args){ return this.verboseN(1,args); }
+ verbose2(...args){ return this.verboseN(2,args); }
+ verbose3(...args){ return this.verboseN(3,args); }
+
+ verbosity(){
+ const rc = this.#verbosity;
+ if(arguments.length) this.#verbosity = +arguments[0];
+ return rc;
+ }
+
+}/*Outer*/
+
+class SQLTester {
+
+ //! Console output utility.
+ #outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
+ //! List of input scripts.
+ #aScripts = [];
+ //! Test input buffer.
+ #inputBuffer = [];
+ //! Test result buffer.
+ #resultBuffer = [];
+ //! Output representation of SQL NULL.
+ #nullView;
+ metrics = newObj({
+ //! Total tests run
+ nTotalTest: 0,
+ //! Total test script files run
+ nTestFile: 0,
+ //! Test-case count for to the current TestScript
+ nTest: 0,
+ //! Names of scripts which were aborted.
+ failedScripts: []
+ });
+ #emitColNames = false;
+ //! True to keep going regardless of how a test fails.
+ #keepGoing = false;
+ #db = newObj({
+ //! The list of available db handles.
+ list: new Array(7),
+ //! Index into this.list of the current db.
+ iCurrentDb: 0,
+ //! Name of the default db, re-created for each script.
+ initialDbName: "test.db",
+ //! Buffer for REQUIRED_PROPERTIES pragmas.
+ initSql: ['select 1;'],
+ //! (sqlite3*) to the current db.
+ currentDb: function(){
+ return this.list[this.iCurrentDb];
+ }
+ });
+
+ constructor(){
+ this.reset();
+ }
+
+ outln(...args){ return this.#outer.outln(...args); }
+ out(...args){ return this.#outer.out(...args); }
+ outer(...args){
+ if(args.length){
+ this.#outer = args[0];
+ return this;
+ }
+ return this.#outer;
+ }
+ verbose1(...args){ return this.#outer.verboseN(1,args); }
+ verbose2(...args){ return this.#outer.verboseN(2,args); }
+ verbose3(...args){ return this.#outer.verboseN(3,args); }
+ verbosity(...args){
+ const rc = this.#outer.verbosity(...args);
+ return args.length ? this : rc;
+ }
+ setLogger(func){
+ this.#outer.logger(func);
+ return this;
+ }
+
+ incrementTestCounter(){
+ ++this.metrics.nTotalTest;
+ ++this.metrics.nTest;
+ }
+
+ reset(){
+ this.clearInputBuffer();
+ this.clearResultBuffer();
+ this.#clearBuffer(this.#db.initSql);
+ this.closeAllDbs();
+ this.metrics.nTest = 0;
+ this.#nullView = "nil";
+ this.emitColNames = false;
+ this.#db.iCurrentDb = 0;
+ //this.#db.initSql.push("SELECT 1;");
+ }
+
+ appendInput(line, addNL){
+ this.#inputBuffer.push(line);
+ if( addNL ) this.#inputBuffer.push('\n');
+ }
+ appendResult(line, addNL){
+ this.#resultBuffer.push(line);
+ if( addNL ) this.#resultBuffer.push('\n');
+ }
+ appendDbInitSql(sql){
+ this.#db.initSql.push(sql);
+ if( this.currentDb() ){
+ this.execSql(null, true, ResultBufferMode.NONE, null, sql);
+ }
+ }
+
+ #runInitSql(pDb){
+ let rc = 0;
+ for(const sql of this.#db.initSql){
+ this.#outer.verbose2("RUNNING DB INIT CODE: ",sql);
+ rc = this.execSql(pDb, false, ResultBufferMode.NONE, null, sql);
+ if( rc ) break;
+ }
+ return rc;
+ }
+
+#clearBuffer(buffer){
+ buffer.length = 0;
+ return buffer;
+ }
+
+ clearInputBuffer(){ return this.#clearBuffer(this.#inputBuffer); }
+ clearResultBuffer(){return this.#clearBuffer(this.#resultBuffer); }
+
+ getInputText(){ return this.#inputBuffer.join(''); }
+ getResultText(){ return this.#resultBuffer.join(''); }
+
+ #takeBuffer(buffer){
+ const s = buffer.join('');
+ buffer.length = 0;
+ return s;
+ }
+
+ takeInputBuffer(){
+ return this.#takeBuffer(this.#inputBuffer);
+ }
+ takeResultBuffer(){
+ return this.#takeBuffer(this.#resultBuffer);
+ }
+
+ nullValue(){
+ return (0==arguments.length)
+ ? this.#nullView
+ : (this.#nullView = ''+arguments[0]);
+ }
+
+ outputColumnNames(){
+ return (0==arguments.length)
+ ? this.#emitColNames
+ : (this.#emitColNames = !!arguments[0]);
+ }
+
+ currentDbId(){
+ return (0==arguments.length)
+ ? this.#db.iCurrentDb
+ : (this.#affirmDbId(arguments[0]).#db.iCurrentDb = arguments[0]);
+ }
+
+ #affirmDbId(id){
+ if(id<0 || id>=this.#db.list.length){
+ toss(SQLTesterException, "Database index ",id," is out of range.");
+ }
+ return this;
+ }
+
+ currentDb(...args){
+ if( 0!=args.length ){
+ this.#affirmDbId(id).#db.iCurrentDb = id;
+ }
+ return this.#db.currentDb();
+ }
+
+ getDbById(id){
+ return this.#affirmDbId(id).#db.list[id];
+ }
+
+ getCurrentDb(){ return this.#db.list[this.#db.iCurrentDb]; }
+
+
+ closeDb(id) {
+ if( 0==arguments.length ){
+ id = this.#db.iCurrentDb;
+ }
+ const pDb = this.#affirmDbId(id).#db.list[id];
+ if( pDb ){
+ sqlite3.capi.sqlite3_close_v2(pDb);
+ this.#db.list[id] = null;
+ }
+ }
+
+ closeAllDbs(){
+ for(let i = 0; i<this.#db.list.length; ++i){
+ if(this.#db.list[i]){
+ sqlite3.capi.sqlite3_close_v2(this.#db.list[i]);
+ this.#db.list[i] = null;
+ }
+ }
+ this.#db.iCurrentDb = 0;
+ }
+
+ openDb(name, createIfNeeded){
+ if( 3===arguments.length ){
+ const slot = arguments[0];
+ this.#affirmDbId(slot).#db.iCurrentDb = slot;
+ name = arguments[1];
+ createIfNeeded = arguments[2];
+ }
+ this.closeDb();
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ let pDb = 0;
+ let flags = capi.SQLITE_OPEN_READWRITE;
+ if( createIfNeeded ) flags |= capi.SQLITE_OPEN_CREATE;
+ try{
+ let rc;
+ wasm.pstack.call(function(){
+ let ppOut = wasm.pstack.allocPtr();
+ rc = sqlite3.capi.sqlite3_open_v2(name, ppOut, flags, null);
+ pDb = wasm.peekPtr(ppOut);
+ });
+ let sql;
+ if( 0==rc && this.#db.initSql.length > 0){
+ rc = this.#runInitSql(pDb);
+ }
+ if( 0!=rc ){
+ sqlite3.SQLite3Error.toss(
+ rc,
+ "sqlite3 result code",rc+":",
+ (pDb ? sqlite3.capi.sqlite3_errmsg(pDb)
+ : sqlite3.capi.sqlite3_errstr(rc))
+ );
+ }
+ return this.#db.list[this.#db.iCurrentDb] = pDb;
+ }catch(e){
+ sqlite3.capi.sqlite3_close_v2(pDb);
+ throw e;
+ }
+ }
+
+ addTestScript(ts){
+ if( 2===arguments.length ){
+ ts = new TestScript(arguments[0], arguments[1]);
+ }else if(ts instanceof Uint8Array){
+ ts = new TestScript('<unnamed>', ts);
+ }else if('string' === typeof arguments[1]){
+ ts = new TestScript('<unnamed>', Util.utf8Encode(arguments[1]));
+ }
+ if( !(ts instanceof TestScript) ){
+ Util.toss(SQLTesterException, "Invalid argument type for addTestScript()");
+ }
+ this.#aScripts.push(ts);
+ return this;
+ }
+
+ runTests(){
+ const tStart = (new Date()).getTime();
+ let isVerbose = this.verbosity();
+ this.metrics.failedScripts.length = 0;
+ this.metrics.nTotalTest = 0;
+ this.metrics.nTestFile = 0;
+ for(const ts of this.#aScripts){
+ this.reset();
+ ++this.metrics.nTestFile;
+ let threw = false;
+ const timeStart = (new Date()).getTime();
+ let msgTail = '';
+ try{
+ ts.run(this);
+ }catch(e){
+ if(e instanceof SQLTesterException){
+ threw = true;
+ this.outln("πŸ”₯EXCEPTION: ",e);
+ this.metrics.failedScripts.push({script: ts.filename(), message:e.toString()});
+ if( this.#keepGoing ){
+ this.outln("Continuing anyway because of the keep-going option.");
+ }else if( e.isFatal() ){
+ throw e;
+ }
+ }else{
+ throw e;
+ }
+ }finally{
+ const timeEnd = (new Date()).getTime();
+ this.out("🏁", (threw ? "❌" : "βœ…"), " ",
+ this.metrics.nTest, " test(s) in ",
+ (timeEnd-timeStart),"ms. ");
+ const mod = ts.moduleName();
+ if( mod ){
+ this.out( "[",mod,"] " );
+ }
+ this.outln(ts.filename());
+ }
+ }
+ const tEnd = (new Date()).getTime();
+ Util.unlink(this.#db.initialDbName);
+ this.outln("Took ",(tEnd-tStart),"ms. Test count = ",
+ this.metrics.nTotalTest,", script count = ",
+ this.#aScripts.length,(
+ this.metrics.failedScripts.length
+ ? ", failed scripts = "+this.metrics.failedScripts.length
+ : ""
+ )
+ );
+ return this;
+ }
+
+ #setupInitialDb(){
+ if( !this.#db.list[0] ){
+ Util.unlink(this.#db.initialDbName);
+ this.openDb(0, this.#db.initialDbName, true);
+ }else{
+ this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
+ "triggered while it is opened.");
+ }
+ }
+
+ #escapeSqlValue(v){
+ if( !v ) return "{}";
+ if( !Rx.special.test(v) ){
+ return v /* no escaping needed */;
+ }
+ if( !Rx.squiggly.test(v) ){
+ return "{"+v+"}";
+ }
+ const sb = ["\""];
+ const n = v.length;
+ for(let i = 0; i < n; ++i){
+ const ch = v.charAt(i);
+ switch(ch){
+ case '\\': sb.push("\\\\"); break;
+ case '"': sb.push("\\\""); break;
+ default:{
+ //verbose("CHAR ",(int)ch," ",ch," octal=",String.format("\\%03o", (int)ch));
+ const ccode = ch.charCodeAt(i);
+ if( ccode < 32 ) sb.push('\\',ccode.toString(8),'o');
+ else sb.push(ch);
+ break;
+ }
+ }
+ }
+ sb.append("\"");
+ return sb.join('');
+ }
+
+ #appendDbErr(pDb, sb, rc){
+ sb.push(sqlite3.capi.sqlite3_js_rc_str(rc), ' ');
+ const msg = this.#escapeSqlValue(sqlite3.capi.sqlite3_errmsg(pDb));
+ if( '{' === msg.charAt(0) ){
+ sb.push(msg);
+ }else{
+ sb.push('{', msg, '}');
+ }
+ }
+
+ #checkDbRc(pDb,rc){
+ sqlite3.oo1.DB.checkRc(pDb, rc);
+ }
+
+ execSql(pDb, throwOnError, appendMode, rowMode, sql){
+ if( !pDb && !this.#db.list[0] ){
+ this.#setupInitialDb();
+ }
+ if( !pDb ) pDb = this.#db.currentDb();
+ const wasm = sqlite3.wasm, capi = sqlite3.capi;
+ sql = (sql instanceof Uint8Array)
+ ? sql
+ : Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql));
+ const self = this;
+ const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer;
+ let rc = 0;
+ wasm.scopedAllocCall(function(){
+ let sqlByteLen = sql.byteLength;
+ const ppStmt = wasm.scopedAlloc(
+ /* output (sqlite3_stmt**) arg and pzTail */
+ (2 * wasm.ptrSizeof) + (sqlByteLen + 1/* SQL + NUL */)
+ );
+ const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
+ let pSql = pzTail + wasm.ptrSizeof;
+ const pSqlEnd = pSql + sqlByteLen;
+ wasm.heap8().set(sql, pSql);
+ wasm.poke8(pSql + sqlByteLen, 0/*NUL terminator*/);
+ let pos = 0, n = 1, spacing = 0;
+ while( pSql && wasm.peek8(pSql) ){
+ wasm.pokePtr([ppStmt, pzTail], 0);
+ rc = capi.sqlite3_prepare_v3(
+ pDb, pSql, sqlByteLen, 0, ppStmt, pzTail
+ );
+ if( 0!==rc ){
+ if(throwOnError){
+ throw new DbException(self, pDb, rc);
+ }else if( sb ){
+ self.#appendDbErr(pDb, sb, rc);
+ }
+ break;
+ }
+ const pStmt = wasm.peekPtr(ppStmt);
+ pSql = wasm.peekPtr(pzTail);
+ sqlByteLen = pSqlEnd - pSql;
+ if(!pStmt) continue /* only whitespace or comments */;
+ if( sb ){
+ const nCol = capi.sqlite3_column_count(pStmt);
+ let colName, val;
+ while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {
+ for( let i=0; i < nCol; ++i ){
+ if( spacing++ > 0 ) sb.push(' ');
+ if( self.#emitColNames ){
+ colName = capi.sqlite3_column_name(pStmt, i);
+ switch(appendMode){
+ case ResultBufferMode.ASIS: sb.push( colName ); break;
+ case ResultBufferMode.ESCAPED:
+ sb.push( self.#escapeSqlValue(colName) );
+ break;
+ default:
+ self.toss("Unhandled ResultBufferMode.");
+ }
+ sb.push(' ');
+ }
+ val = capi.sqlite3_column_text(pStmt, i);
+ if( null===val ){
+ sb.push( self.#nullView );
+ continue;
+ }
+ switch(appendMode){
+ case ResultBufferMode.ASIS: sb.push( val ); break;
+ case ResultBufferMode.ESCAPED:
+ sb.push( self.#escapeSqlValue(val) );
+ break;
+ }
+ }/* column loop */
+ }/* row loop */
+ if( ResultRowMode.NEWLINE === rowMode ){
+ spacing = 0;
+ sb.push('\n');
+ }
+ }else{ // no output but possibly other side effects
+ while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {}
+ }
+ capi.sqlite3_finalize(pStmt);
+ if( capi.SQLITE_ROW===rc || capi.SQLITE_DONE===rc) rc = 0;
+ else if( rc!=0 ){
+ if( sb ){
+ self.#appendDbErr(db, sb, rc);
+ }
+ break;
+ }
+ }/* SQL script loop */;
+ })/*scopedAllocCall()*/;
+ return rc;
+ }
+
+}/*SQLTester*/
+
+class Command {
+ constructor(){
+ }
+
+ process(sqlTester,testScript,argv){
+ SQLTesterException.toss("process() must be overridden");
+ }
+
+ argcCheck(testScript,argv,min,max){
+ const argc = argv.length-1;
+ if(argc<min || (max>=0 && argc>max)){
+ if( min==max ){
+ testScript.toss(argv[0]," requires exactly ",min," argument(s)");
+ }else if(max>0){
+ testScript.toss(argv[0]," requires ",min,"-",max," arguments.");
+ }else{
+ testScript.toss(argv[0]," requires at least ",min," arguments.");
+ }
+ }
+ }
+}
+
+class Cursor {
+ src;
+ sb = [];
+ pos = 0;
+ //! Current line number. Starts at 0 for internal reasons and will
+ // line up with 1-based reality once parsing starts.
+ lineNo = 0 /* yes, zero */;
+ //! Putback value for this.pos.
+ putbackPos = 0;
+ //! Putback line number
+ putbackLineNo = 0;
+ //! Peeked-to pos, used by peekLine() and consumePeeked().
+ peekedPos = 0;
+ //! Peeked-to line number.
+ peekedLineNo = 0;
+
+ constructor(){
+ }
+
+ //! Restore parsing state to the start of the stream.
+ rewind(){
+ this.sb.length = this.pos = this.lineNo
+ = this.putbackPos = this.putbackLineNo
+ = this.peekedPos = this.peekedLineNo = 0;
+ }
+}
+
+class TestScript {
+ #cursor = new Cursor();
+ #moduleName = null;
+ #filename = null;
+ #testCaseName = null;
+ #outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' );
+
+ constructor(...args){
+ let content, filename;
+ if( 2 == args.length ){
+ filename = args[0];
+ content = args[1];
+ }else if( 1 == args.length ){
+ if(args[0] instanceof Object){
+ const o = args[0];
+ filename = o.name;
+ content = o.content;
+ }else{
+ content = args[0];
+ }
+ }
+ if(!(content instanceof Uint8Array)){
+ if('string' === typeof content){
+ content = Util.utf8Encode(content);
+ }else if((content instanceof ArrayBuffer)
+ ||(content instanceof Array)){
+ content = new Uint8Array(content);
+ }else{
+ toss(Error, "Invalid content type for TestScript constructor.");
+ }
+ }
+ this.#filename = filename;
+ this.#cursor.src = content;
+ }
+
+ moduleName(){
+ return (0==arguments.length)
+ ? this.#moduleName : (this.#moduleName = arguments[0]);
+ }
+
+ testCaseName(){
+ return (0==arguments.length)
+ ? this.#testCaseName : (this.#testCaseName = arguments[0]);
+ }
+ filename(){
+ return (0==arguments.length)
+ ? this.#filename : (this.#filename = arguments[0]);
+ }
+
+ getOutputPrefix() {
+ let rc = "["+(this.#moduleName || '<unnamed>')+"]";
+ if( this.#testCaseName ) rc += "["+this.#testCaseName+"]";
+ if( this.#filename ) rc += '['+this.#filename+']';
+ return rc + " line "+ this.#cursor.lineNo;
+ }
+
+ reset(){
+ this.#testCaseName = null;
+ this.#cursor.rewind();
+ return this;
+ }
+
+ toss(...args){
+ throw new TestScriptFailed(this,...args);
+ }
+
+ verbose1(...args){ return this.#outer.verboseN(1,args); }
+ verbose2(...args){ return this.#outer.verboseN(2,args); }
+ verbose3(...args){ return this.#outer.verboseN(3,args); }
+ verbosity(...args){
+ const rc = this.#outer.verbosity(...args);
+ return args.length ? this : rc;
+ }
+
+ #checkRequiredProperties(tester, props){
+ if(true) return false;
+ let nOk = 0;
+ for(const rp of props){
+ this.verbose2("REQUIRED_PROPERTIES: ",rp);
+ switch(rp){
+ case "RECURSIVE_TRIGGERS":
+ tester.appendDbInitSql("pragma recursive_triggers=on;");
+ ++nOk;
+ break;
+ case "TEMPSTORE_FILE":
+ /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
+ which we just happen to know is the case */
+ tester.appendDbInitSql("pragma temp_store=1;");
+ ++nOk;
+ break;
+ case "TEMPSTORE_MEM":
+ /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
+ which we just happen to know is the case */
+ tester.appendDbInitSql("pragma temp_store=0;");
+ ++nOk;
+ break;
+ case "AUTOVACUUM":
+ tester.appendDbInitSql("pragma auto_vacuum=full;");
+ ++nOk;
+ break;
+ case "INCRVACUUM":
+ tester.appendDbInitSql("pragma auto_vacuum=incremental;");
+ ++nOk;
+ default:
+ break;
+ }
+ }
+ return props.length == nOk;
+ }
+
+ #checkForDirective(tester,line){
+ if(line.startsWith("#")){
+ throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
+ }else if(line.startsWith("---")){
+ throw new IncompatibleDirective(this, "triple-dash: ",line);
+ }
+ let m = Rx.scriptModuleName.exec(line);
+ if( m ){
+ this.#moduleName = m[1];
+ return;
+ }
+ m = Rx.requiredProperties.exec(line);
+ if( m ){
+ const rp = m[1];
+ if( !this.#checkRequiredProperties( tester, rp.split(/\s+/).filter(v=>!!v) ) ){
+ throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
+ }
+ }
+
+ m = Rx.mixedModuleName.exec(line);
+ if( m ){
+ throw new IncompatibleDirective(this, m[1]+": "+m[3]);
+ }
+ if( line.indexOf("\n|")>=0 ){
+ throw new IncompatibleDirective(this, "newline-pipe combination.");
+ }
+
+ }
+
+ #getCommandArgv(line){
+ const m = Rx.command.exec(line);
+ return m ? m[1].trim().split(/\s+/) : null;
+ }
+
+
+ #isCommandLine(line, checkForImpl){
+ let m = Rx.command.exec(line);
+ if( m && checkForImpl ){
+ m = !!CommandDispatcher.getCommandByName(m[2]);
+ }
+ return !!m;
+ }
+
+ fetchCommandBody(tester){
+ const sb = [];
+ let line;
+ while( (null !== (line = this.peekLine())) ){
+ this.#checkForDirective(tester, line);
+ if( this.#isCommandLine(line, true) ) break;
+ sb.push(line,"\n");
+ this.consumePeeked();
+ }
+ line = sb.join('');
+ return !!line.trim() ? line : null;
+ }
+
+ run(tester){
+ this.reset();
+ this.#outer.verbosity( tester.verbosity() );
+ this.#outer.logger( tester.outer().logger() );
+ let line, directive, argv = [];
+ while( null != (line = this.getLine()) ){
+ this.verbose3("run() input line: ",line);
+ this.#checkForDirective(tester, line);
+ argv = this.#getCommandArgv(line);
+ if( argv ){
+ this.#processCommand(tester, argv);
+ continue;
+ }
+ tester.appendInput(line,true);
+ }
+ return true;
+ }
+
+ #processCommand(tester, argv){
+ this.verbose2("processCommand(): ",argv[0], " ", Util.argvToString(argv));
+ if(this.#outer.verbosity()>1){
+ const input = tester.getInputText();
+ this.verbose3("processCommand() input buffer = ",input);
+ }
+ CommandDispatcher.dispatch(tester, this, argv);
+ }
+
+ getLine(){
+ const cur = this.#cursor;
+ if( cur.pos==cur.src.byteLength ){
+ return null/*EOF*/;
+ }
+ cur.putbackPos = cur.pos;
+ cur.putbackLineNo = cur.lineNo;
+ cur.sb.length = 0;
+ let b = 0, prevB = 0, i = cur.pos;
+ let doBreak = false;
+ let nChar = 0 /* number of bytes in the aChar char */;
+ const end = cur.src.byteLength;
+ for(; i < end && !doBreak; ++i){
+ b = cur.src[i];
+ switch( b ){
+ case 13/*CR*/: continue;
+ case 10/*NL*/:
+ ++cur.lineNo;
+ if(cur.sb.length>0) doBreak = true;
+ // Else it's an empty string
+ break;
+ default:{
+ /* Multi-byte chars need to be gathered up and appended at
+ one time so that we can get them as string objects. */
+ nChar = 1;
+ switch( b & 0xF0 ){
+ case 0xC0: nChar = 2; break;
+ case 0xE0: nChar = 3; break;
+ case 0xF0: nChar = 4; break;
+ default:
+ if( b > 127 ) this.toss("Invalid character (#"+b+").");
+ break;
+ }
+ if( 1==nChar ){
+ cur.sb.push(String.fromCharCode(b));
+ }else{
+ const aChar = [] /* multi-byte char buffer */;
+ for(let x = 0; (x < nChar) && (i+x < end); ++x) aChar[x] = cur.src[i+x];
+ cur.sb.push(
+ Util.utf8Decode( new Uint8Array(aChar) )
+ );
+ i += nChar-1;
+ }
+ break;
+ }
+ }
+ }
+ cur.pos = i;
+ const rv = cur.sb.join('');
+ if( i==cur.src.byteLength && 0==rv.length ){
+ return null /* EOF */;
+ }
+ return rv;
+ }/*getLine()*/
+
+ /**
+ Fetches the next line then resets the cursor to its pre-call
+ state. consumePeeked() can be used to consume this peeked line
+ without having to re-parse it.
+ */
+ peekLine(){
+ const cur = this.#cursor;
+ const oldPos = cur.pos;
+ const oldPB = cur.putbackPos;
+ const oldPBL = cur.putbackLineNo;
+ const oldLine = cur.lineNo;
+ try {
+ return this.getLine();
+ }finally{
+ cur.peekedPos = cur.pos;
+ cur.peekedLineNo = cur.lineNo;
+ cur.pos = oldPos;
+ cur.lineNo = oldLine;
+ cur.putbackPos = oldPB;
+ cur.putbackLineNo = oldPBL;
+ }
+ }
+
+
+ /**
+ Only valid after calling peekLine() and before calling getLine().
+ This places the cursor to the position it would have been at had
+ the peekLine() had been fetched with getLine().
+ */
+ consumePeeked(){
+ const cur = this.#cursor;
+ cur.pos = cur.peekedPos;
+ cur.lineNo = cur.peekedLineNo;
+ }
+
+ /**
+ Restores the cursor to the position it had before the previous
+ call to getLine().
+ */
+ putbackLine(){
+ const cur = this.#cursor;
+ cur.pos = cur.putbackPos;
+ cur.lineNo = cur.putbackLineNo;
+ }
+
+}/*TestScript*/;
+
+//! --close command
+class CloseDbCommand extends Command {
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,0,1);
+ let id;
+ if(argv.length>1){
+ const arg = argv[1];
+ if( "all" === arg ){
+ t.closeAllDbs();
+ return;
+ }
+ else{
+ id = parseInt(arg);
+ }
+ }else{
+ id = t.currentDbId();
+ }
+ t.closeDb(id);
+ }
+}
+
+//! --column-names command
+class ColumnNamesCommand extends Command {
+ process( st, ts, argv ){
+ this.argcCheck(ts,argv,1);
+ st.outputColumnNames( !!parseInt(argv[1]) );
+ }
+}
+
+//! --db command
+class DbCommand extends Command {
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,1);
+ t.currentDbId( parseInt(argv[1]) );
+ }
+}
+
+//! --glob command
+class GlobCommand extends Command {
+ #negate = false;
+ constructor(negate=false){
+ super();
+ this.#negate = negate;
+ }
+
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,1,-1);
+ t.incrementTestCounter();
+ const sql = t.takeInputBuffer();
+ let rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
+ ResultRowMode.ONELINE, sql);
+ const result = t.getResultText();
+ const sArgs = Util.argvToString(argv);
+ //t2.verbose2(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
+ const glob = Util.argvToString(argv);
+ rc = Util.strglob(glob, result);
+ if( (this.#negate && 0===rc) || (!this.#negate && 0!==rc) ){
+ ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
+ }
+ }
+}
+
+//! --notglob command
+class NotGlobCommand extends GlobCommand {
+ constructor(){super(true);}
+}
+
+//! --open command
+class OpenDbCommand extends Command {
+ #createIfNeeded = false;
+ constructor(createIfNeeded=false){
+ super();
+ this.#createIfNeeded = createIfNeeded;
+ }
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,1);
+ t.openDb(argv[1], this.#createIfNeeded);
+ }
+}
+
+//! --new command
+class NewDbCommand extends OpenDbCommand {
+ constructor(){ super(true); }
+}
+
+//! Placeholder dummy/no-op commands
+class NoopCommand extends Command {
+ process(t, ts, argv){}
+}
+
+//! --null command
+class NullCommand extends Command {
+ process(st, ts, argv){
+ this.argcCheck(ts,argv,1);
+ st.nullValue( argv[1] );
+ }
+}
+
+//! --print command
+class PrintCommand extends Command {
+ process(st, ts, argv){
+ st.out(ts.getOutputPrefix(),': ');
+ if( 1==argv.length ){
+ st.out( st.getInputText() );
+ }else{
+ st.outln( Util.argvToString(argv) );
+ }
+ }
+}
+
+//! --result command
+class ResultCommand extends Command {
+ #bufferMode;
+ constructor(resultBufferMode = ResultBufferMode.ESCAPED){
+ super();
+ this.#bufferMode = resultBufferMode;
+ }
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,0,-1);
+ t.incrementTestCounter();
+ const sql = t.takeInputBuffer();
+ //ts.verbose2(argv[0]," SQL =\n",sql);
+ t.execSql(null, false, this.#bufferMode, ResultRowMode.ONELINE, sql);
+ const result = t.getResultText().trim();
+ const sArgs = argv.length>1 ? Util.argvToString(argv) : "";
+ if( result !== sArgs ){
+ t.outln(argv[0]," FAILED comparison. Result buffer:\n",
+ result,"\nExpected result:\n",sArgs);
+ ts.toss(argv[0]+" comparison failed.");
+ }
+ }
+}
+
+//! --json command
+class JsonCommand extends ResultCommand {
+ constructor(){ super(ResultBufferMode.ASIS); }
+}
+
+//! --run command
+class RunCommand extends Command {
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,0,1);
+ const pDb = (1==argv.length)
+ ? t.currentDb() : t.getDbById( parseInt(argv[1]) );
+ const sql = t.takeInputBuffer();
+ const rc = t.execSql(pDb, false, ResultBufferMode.NONE,
+ ResultRowMode.ONELINE, sql);
+ if( 0!==rc && t.verbosity()>0 ){
+ const msg = sqlite3.capi.sqlite3_errmsg(pDb);
+ ts.verbose2(argv[0]," non-fatal command error #",rc,": ",
+ msg,"\nfor SQL:\n",sql);
+ }
+ }
+}
+
+//! --tableresult command
+class TableResultCommand extends Command {
+ #jsonMode;
+ constructor(jsonMode=false){
+ super();
+ this.#jsonMode = jsonMode;
+ }
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,0);
+ t.incrementTestCounter();
+ let body = ts.fetchCommandBody(t);
+ if( null===body ) ts.toss("Missing ",argv[0]," body.");
+ body = body.trim();
+ if( !body.endsWith("\n--end") ){
+ ts.toss(argv[0], " must be terminated with --end\\n");
+ }else{
+ body = body.substring(0, body.length-6);
+ }
+ const globs = body.split(/\s*\n\s*/);
+ if( globs.length < 1 ){
+ ts.toss(argv[0], " requires 1 or more ",
+ (this.#jsonMode ? "json snippets" : "globs"),".");
+ }
+ const sql = t.takeInputBuffer();
+ t.execSql(null, true,
+ this.#jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
+ ResultRowMode.NEWLINE, sql);
+ const rbuf = t.getResultText().trim();
+ const res = rbuf.split(/\r?\n/);
+ if( res.length !== globs.length ){
+ ts.toss(argv[0], " failure: input has ", res.length,
+ " row(s) but expecting ",globs.length);
+ }
+ for(let i = 0; i < res.length; ++i){
+ const glob = globs[i].replaceAll(/\s+/g," ").trim();
+ //ts.verbose2(argv[0]," <<",glob,">> vs <<",res[i],">>");
+ if( this.#jsonMode ){
+ if( glob!==res[i] ){
+ ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
+ res[i],">>");
+ }
+ }else if( 0!=Util.strglob(glob, res[i]) ){
+ ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
+ }
+ }
+ }
+}
+
+//! --json-block command
+class JsonBlockCommand extends TableResultCommand {
+ constructor(){ super(true); }
+}
+
+//! --testcase command
+class TestCaseCommand extends Command {
+ process(tester, script, argv){
+ this.argcCheck(script, argv,1);
+ script.testCaseName(argv[1]);
+ tester.clearResultBuffer();
+ tester.clearInputBuffer();
+ }
+}
+
+
+//! --verbosity command
+class VerbosityCommand extends Command {
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,1);
+ ts.verbosity( parseInt(argv[1]) );
+ }
+}
+
+class CommandDispatcher {
+ static map = newObj();
+
+ static getCommandByName(name){
+ let rv = CommandDispatcher.map[name];
+ if( rv ) return rv;
+ switch(name){
+ case "close": rv = new CloseDbCommand(); break;
+ case "column-names": rv = new ColumnNamesCommand(); break;
+ case "db": rv = new DbCommand(); break;
+ case "glob": rv = new GlobCommand(); break;
+ case "json": rv = new JsonCommand(); break;
+ case "json-block": rv = new JsonBlockCommand(); break;
+ case "new": rv = new NewDbCommand(); break;
+ case "notglob": rv = new NotGlobCommand(); break;
+ case "null": rv = new NullCommand(); break;
+ case "oom": rv = new NoopCommand(); break;
+ case "open": rv = new OpenDbCommand(); break;
+ case "print": rv = new PrintCommand(); break;
+ case "result": rv = new ResultCommand(); break;
+ case "run": rv = new RunCommand(); break;
+ case "tableresult": rv = new TableResultCommand(); break;
+ case "testcase": rv = new TestCaseCommand(); break;
+ case "verbosity": rv = new VerbosityCommand(); break;
+ }
+ if( rv ){
+ CommandDispatcher.map[name] = rv;
+ }
+ return rv;
+ }
+
+ static dispatch(tester, testScript, argv){
+ const cmd = CommandDispatcher.getCommandByName(argv[0]);
+ if( !cmd ){
+ toss(UnknownCommand,testScript,argv[0]);
+ }
+ cmd.process(tester, testScript, argv);
+ }
+}/*CommandDispatcher*/
+
+const namespace = newObj({
+ Command,
+ DbException,
+ IncompatibleDirective,
+ Outer,
+ SQLTester,
+ SQLTesterException,
+ TestScript,
+ TestScriptFailed,
+ UnknownCommand,
+ Util,
+ sqlite3
+});
+
+export {namespace as default};
diff --git a/ext/wasm/SQLTester/SQLTester.run.mjs b/ext/wasm/SQLTester/SQLTester.run.mjs
new file mode 100644
index 0000000..735fe4d
--- /dev/null
+++ b/ext/wasm/SQLTester/SQLTester.run.mjs
@@ -0,0 +1,148 @@
+/*
+** 2023-08-29
+**
+** 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.
+**
+*************************************************************************
+** This file contains a test application for SQLTester.js.
+*/
+import {default as ns} from './SQLTester.mjs';
+import {default as allTests} from './test-list.mjs';
+
+globalThis.sqlite3 = ns.sqlite3;
+const log = function f(...args){
+ console.log('SQLTester.run:',...args);
+ return f;
+};
+
+const out = function f(...args){ return f.outer.out(...args) };
+out.outer = new ns.Outer();
+out.outer.getOutputPrefix = ()=>'SQLTester.run: ';
+const outln = (...args)=>{ return out.outer.outln(...args) };
+
+const affirm = function(expr, msg){
+ if( !expr ){
+ throw new Error(arguments[1]
+ ? ("Assertion failed: "+arguments[1])
+ : "Assertion failed");
+ }
+}
+
+let ts = new ns.TestScript('/foo.test',`
+/*
+** This is a comment. There are many like it but this one is mine.
+**
+** SCRIPT_MODULE_NAME: sanity-check-0
+** xMIXED_MODULE_NAME: mixed-module
+** xMODULE_NAME: module-name
+** xREQUIRED_PROPERTIES: small fast reliable
+** xREQUIRED_PROPERTIES: RECURSIVE_TRIGGERS
+** xREQUIRED_PROPERTIES: TEMPSTORE_FILE TEMPSTORE_MEM
+** xREQUIRED_PROPERTIES: AUTOVACUUM INCRVACUUM
+**
+*/
+/* --verbosity 3 */
+/* ---must-fail */
+/* # must fail */
+/* --verbosity 0 */
+--print Hello, world.
+--close all
+--oom
+--db 0
+--new my.db
+--null zilch
+--testcase 1.0
+SELECT 1, null;
+--result 1 zilch
+--glob *zil*
+--notglob *ZIL*
+SELECT 1, 2;
+intentional error;
+--run
+/* ---intentional-failure */
+--testcase json-1
+SELECT json_array(1,2,3)
+--json [1,2,3]
+--testcase tableresult-1
+ select 1, 'a';
+ select 2, 'b';
+--tableresult
+ # [a-z]
+ 2 b
+--end
+--testcase json-block-1
+ select json_array(1,2,3);
+ select json_object('a',1,'b',2);
+--json-block
+ [1,2,3]
+ {"a":1,"b":2}
+--end
+--testcase col-names-on
+--column-names 1
+ select 1 as 'a', 2 as 'b';
+--result a 1 b 2
+--testcase col-names-off
+--column-names 0
+ select 1 as 'a', 2 as 'b';
+--result 1 2
+--close
+--print Until next time
+`);
+
+const sqt = new ns.SQLTester()
+ .setLogger(console.log.bind(console))
+ .verbosity(1)
+ .addTestScript(ts);
+sqt.outer().outputPrefix('');
+
+const runTests = function(){
+ try{
+ if( 0 ){
+ affirm( !sqt.getCurrentDb(), 'sqt.getCurrentDb()' );
+ sqt.openDb('/foo.db', true);
+ affirm( !!sqt.getCurrentDb(),'sqt.getCurrentDb()' );
+ affirm( 'zilch' !== sqt.nullValue() );
+ ts.run(sqt);
+ affirm( 'zilch' === sqt.nullValue() );
+ sqt.addTestScript(ts);
+ sqt.runTests();
+ }else{
+ for(const t of allTests){
+ sqt.addTestScript( new ns.TestScript(t) );
+ }
+ allTests.length = 0;
+ sqt.runTests();
+ }
+ }finally{
+ //log( "Metrics:", sqt.metrics );
+ sqt.reset();
+ }
+};
+
+if( globalThis.WorkerGlobalScope ){
+ const wPost = (type,payload)=>globalThis.postMessage({type, payload});
+ globalThis.onmessage = function({data}){
+ switch(data.type){
+ case 'run-tests':{
+ try{ runTests(); }
+ finally{ wPost('tests-end', sqt.metrics); }
+ break;
+ }
+ default:
+ log("unhandled onmessage: ",data);
+ break;
+ }
+ };
+ sqt.setLogger((msg)=>{
+ wPost('stdout', {message: msg});
+ });
+ wPost('is-ready');
+ //globalThis.onmessage({data:{type:'run-tests'}});
+}else{
+ runTests();
+}
diff --git a/ext/wasm/SQLTester/index.html b/ext/wasm/SQLTester/index.html
new file mode 100644
index 0000000..1dffad6
--- /dev/null
+++ b/ext/wasm/SQLTester/index.html
@@ -0,0 +1,127 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <!--link rel="stylesheet" href="../common/emscripten.css"/-->
+ <link rel="stylesheet" href="../common/testing.css"/>
+ <title>SQLTester</title>
+ </head>
+ <style>
+ fieldset {
+ display: flex;
+ flex-direction: row;
+ padding-right: 1em;
+ }
+ fieldset > :not(.legend) {
+ display: flex;
+ flex-direction: row;
+ padding-right: 1em;
+ }
+ </style>
+ <body>
+ <h1>SQLTester for JS/WASM</h1>
+ <p>This app reads in a build-time-defined set of SQLTester test
+ scripts and runs them through the test suite.
+ </p>
+ <fieldset>
+ <legend>Options</legend>
+ <span class='input-wrapper'>
+ <input type='checkbox' id='cb-log-reverse' checked>
+ <label for='cb-log-reverse'>Reverse log order?</label>
+ </span>
+ <input type='button' id='btn-run-tests' value='Run tests'/>
+ </fieldset>
+ <div id='test-output'>Test output will go here.</div>
+ <!--script src='SQLTester.run.mjs' type='module'></script-->
+ <script>
+ (async function(){
+ const W = new Worker('SQLTester.run.mjs',{
+ type: 'module'
+ });
+ const wPost = (type,payload)=>W.postMessage({type,payload});
+ const mapToString = (v)=>{
+ switch(typeof v){
+ case 'string': return v;
+ case 'number': 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);
+ const logTarget = document.querySelector('#test-output');
+ const 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 = 'SQLTester:cb-log-reverse';
+ const cbReverseIt = ()=>{
+ logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse');
+ };
+ cbReverse.addEventListener('change', cbReverseIt, true);
+ cbReverseIt();
+ }
+
+ const btnRun = document.querySelector('#btn-run-tests');
+ const runTests = ()=>{
+ btnRun.setAttribute('disabled','disabled');
+ wPost('run-tests');
+ logTarget.innerText = 'Running tests...';
+ }
+ btnRun.addEventListener('click', runTests);
+ const log2 = function f(...args){
+ logClass('', ...args);
+ return f;
+ };
+ const log = function f(...args){
+ logClass('','index.html:',...args);
+ return f;
+ };
+
+ const timerId = setTimeout( ()=>{
+ logClass('error',"The SQLTester module is taking an unusually ",
+ "long time to load. More information may be available",
+ "in the dev console.");
+ }, 3000 /* assuming localhost */ );
+
+ W.onmessage = function({data}){
+ switch(data.type){
+ case 'stdout': log2(data.payload.message); break;
+ case 'tests-end':
+ btnRun.removeAttribute('disabled');
+ delete data.payload.nTest;
+ log("test results:",data.payload);
+ break;
+ case 'is-ready':
+ clearTimeout(timerId);
+ runTests(); break;
+ default:
+ log("unhandled onmessage",data);
+ break;
+ }
+ };
+ //runTests()
+ /* Inexplicably, */
+ })();
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/SQLTester/touint8array.c b/ext/wasm/SQLTester/touint8array.c
new file mode 100644
index 0000000..b03ad42
--- /dev/null
+++ b/ext/wasm/SQLTester/touint8array.c
@@ -0,0 +1,29 @@
+/*
+** 2023-08-29
+**
+** 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.
+**
+*************************************************************************
+** This file contains a tool for writing out the contents of stdin as
+** a comma-separated list of numbers, one per byte.
+*/
+
+#include <stdio.h>
+int main(int argc, char const **argv){
+ int i;
+ int rc = 0, colWidth = 30;
+ int ch;
+ printf("[");
+ for( i=0; EOF!=(ch = fgetc(stdin)); ++i ){
+ if( 0!=i ) printf(",");
+ if( i && 0==(i%colWidth) ) puts("");
+ printf("%d",ch);
+ }
+ printf("]");
+ return rc;
+}