diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3
index ae34ccdb19..da14513415 100644
--- a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3
+++ b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3
@@ -40,6 +40,7 @@ _sqlite3_result_int
 _sqlite3_result_null
 _sqlite3_result_text
 _sqlite3_sourceid
+_sqlite3_sql
 _sqlite3_step
 _sqlite3_value_blob
 _sqlite3_value_bytes
diff --git a/ext/fiddle/sqlite3-api.js b/ext/fiddle/sqlite3-api.js
index d4423a2175..52ea1f982c 100644
--- a/ext/fiddle/sqlite3-api.js
+++ b/ext/fiddle/sqlite3-api.js
@@ -42,6 +42,22 @@
   This file installs namespace.sqlite3, where namespace is `self`,
   meaning either the global window or worker, depending on where this
   is loaded from.
+
+  # Goals and Non-goals of this API
+
+  Goals:
+
+  - Except where noted in the non-goals, provide a more-or-less
+    complete wrapper to the sqlite3 C API, insofar as WASM feature
+    parity with C allows for.
+
+  Non-goals:
+
+  - As WASM is a web-based technology and UTF-8 is the King of
+    Encodings in that realm, there are no plans to support the
+    UTF16-related APIs will not be. They would add a complication to
+    the bindings for no appreciable benefit.
+
 */
 (function(namespace){
     /* For reference: sql.js does essentially everything we want and
@@ -184,13 +200,13 @@
         ["sqlite3_result_null",null,["number"]],
         ["sqlite3_result_text",null,["number", "string", "number", "number"]],
         ["sqlite3_sourceid", "string", []],
+        ["sqlite3_sql", "string", ["number"]],
         ["sqlite3_step", "number", ["number"]],
         ["sqlite3_value_blob", "number", ["number"]],
         ["sqlite3_value_bytes","number",["number"]],
         ["sqlite3_value_double","number",["number"]],
         ["sqlite3_value_text", "string", ["number"]],
         ["sqlite3_value_type", "number", ["number"]]
-        //["sqlite3_sql", "string", ["number"]],
         //["sqlite3_normalized_sql", "string", ["number"]]
     ].forEach(function(a){
         const k = (4==a.length) ? a.shift() : a[0];
@@ -201,10 +217,11 @@
     /* What follows is colloquially known as "OO API #1". It is a
        binding of the sqlite3 API which is designed to be run within
        the same thread (main or worker) as the one in which the
-       sqlite3 WASM binding was initialized.  This wrapper cannot use
+       sqlite3 WASM binding was initialized. This wrapper cannot use
        the sqlite3 binding if, e.g., the wrapper is in the main thread
        and the sqlite3 API is in a worker. */
-    /* memory for use in some pointer-passing routines */
+
+    /** Memory for use in some pointer-to-pointer-passing routines. */
     const pPtrArg = stackAlloc(4);
     /** Throws a new error, concatenating all args with a space between
         each. */
@@ -212,8 +229,8 @@
         throw new Error(Array.prototype.join.call(arguments, ' '));
     };
 
-    const sqlite3/*canonical name*/ = S/*convenience alias*/ = api;
-    
+    const S/*convenience alias*/ = api;
+
     /**
        The DB class wraps a sqlite3 db handle.
     */
@@ -222,6 +239,7 @@
         else if('string'!==typeof name){
             toss("TODO: support blob image of db here.");
         }
+        setValue(pPtrArg, 0, "i32");
         this.checkRc(S.sqlite3_open(name, pPtrArg));
         this._pDb = getValue(pPtrArg, "i32");
         this.filename = name;
@@ -249,7 +267,7 @@
        new instances.
     */
     const Stmt = function(){
-        if(BindTypes!=arguments[2]){
+        if(BindTypes!==arguments[2]){
             toss("Do not call the Stmt constructor directly. Use DB.prepare().");
         }
         this.db = arguments[0];
@@ -265,16 +283,64 @@
         return db;
     };
 
+    /**
+       Expects to be passed (arguments) from DB.exec() and
+       DB.execMulti(). Does the argument processing/validation, throws
+       on error, and returns a new object on success:
+
+       { sql: the SQL, obt: optionsObj, cbArg: function}
+
+       cbArg is only set if the opt.callback is set, in which case
+       it's a function which expects to be passed the current Stmt
+       and returns the callback argument of the type indicated by
+       the input arguments.
+    */
+    const parseExecArgs = function(args){
+        const out = {};
+        switch(args.length){
+            case 1:
+                if('string'===typeof args[0]){
+                    out.sql = args[0];
+                    out.opt = {};
+                }else if(args[0] && 'object'===typeof args[0]){
+                    out.opt = args[0];
+                    out.sql = out.opt.sql;
+                }
+            break;
+            case 2:
+                out.sql = args[0];
+                out.opt = args[1];
+            break;
+            default: toss("Invalid argument count for exec().");
+        };
+        if('string'!==typeof out.sql) toss("Missing SQL argument.");
+        if(out.opt.callback){
+            switch((undefined===out.opt.rowMode)
+                    ? 'stmt' : out.opt.rowMode) {
+                case 'object': out.cbArg = (stmt)=>stmt.get({}); break;
+                case 'array': out.cbArg = (stmt)=>stmt.get([]); break;
+                case 'stmt': out.cbArg = (stmt)=>stmt; break;
+                default: toss("Invalid rowMode:",out.opt.rowMode);
+            }
+        }
+        return out;
+    };
+
     DB.prototype = {
         /**
            Expects to be given an sqlite3 API result code. If it is
            falsy, this function returns this object, else it throws an
            exception with an error message from sqlite3_errmsg(),
-           using this object's db handle.
+           using this object's db handle. Note that if it's passed a
+           non-error code like SQLITE_ROW or SQLITE_DONE, it will
+           still throw but the error string might be "Not an error."
+           The various non-0 non-error codes need to be checked for in
+           client code where they are expected.
         */
         checkRc: function(sqliteResultCode){
             if(!sqliteResultCode) return this;
-            toss(S.sqlite3_errmsg(this._pDb) || "Unknown db error.");
+            toss("sqlite result code",sqliteResultCode+":",
+                 S.sqlite3_errmsg(this._pDb) || "Unknown db error.");
         },
         /**
            Finalizes all open statements and closes this database
@@ -317,7 +383,168 @@
             const stmt = new Stmt(this, pStmt, BindTypes);
             this._statements[pStmt] = stmt;
             return stmt;
-        }
+        },
+        /**
+           This function works like execMulti(), and takes the same
+           arguments, but is more efficient (performs much less work)
+           when the input SQL is only a single statement. If passed a
+           multi-statement SQL, it only processes the first one.
+
+           This function supports one additional option not used by
+           execMulti():
+
+           - .multi: if true, this function acts as a proxy for
+             execMulti().
+        */
+        exec: function(/*(sql [,optionsObj]) or (optionsObj)*/){
+            affirmDbOpen(this);
+            const arg = parseExecArgs(arguments);
+            if(!arg.sql) return this;
+            else if(arg.multi){
+                return this.execMulti(arg, undefined, BindTypes);
+            }
+            const opt = arg.opt;
+            let stmt;
+            try {
+                stmt = this.prepare(arg.sql);
+                if(opt.bind) stmt.bind(bind);
+                if(opt.callback){
+                    while(stmt.step()){
+                        stmt._isLocked = true;
+                        opt.callback(arg.cbArg(stmt), stmt);
+                        stmt._isLocked = false;
+                    }
+                }else{
+                    stmt.step();
+                }
+            }finally{
+                if(stmt){
+                    delete stmt._isLocked;
+                    stmt.finalize();
+                }
+            }
+            return this;
+
+        }/*exec()*/,
+        /**
+           Executes one or more SQL statements. Its arguments
+           must be either (sql,optionsObject) or (optionsObject).
+           In the latter case, optionsObject.sql must contain the
+           SQL to execute. Returns this object. Throws on error.
+
+           If no SQL is provided, or a non-string is provided, an
+           exception is triggered. Empty SQL, on the other hand, is
+           simply a no-op.
+
+           The optional options object may contain any of the following
+           properties:
+
+           - .sql = the SQL to run (unless it's provided as the first
+             argument).
+
+           - .bind = a single value valid as an argument for
+             Stmt.bind(). This is ONLY applied to the FIRST non-empty
+             statement in the SQL which has any bindable
+             parameters. (Empty statements are skipped entirely.)
+
+           - .callback = a function which gets called for each row of
+             the FIRST statement in the SQL (if it has any result
+             rows). The second argument passed to the callback is
+             always the current Stmt object (so that the caller
+             may collect column names, or similar). The first
+             argument passed to the callback defaults to the current
+             Stmt object but may be changed with ...
+
+           - .rowMode = a string describing what type of argument
+             should be passed as the first argument to the callback. A
+             value of 'object' causes the results of `stmt.get({})` to
+             be passed to the object. A value of 'array' causes the
+             results of `stmt.get([])` to be passed to the callback.
+             A value of 'stmt' is equivalent to the default, passing
+             the current Stmt to the callback (noting that it's always
+             passed as the 2nd argument). Any other value triggers an
+             exception.
+
+           - saveSql = an optional array. If set, the SQL of each
+             executed statement is appended to this array before the
+             statement is executed (but after it is prepared - we
+             don't have the string until after that). Empty SQL
+             statements are elided.
+
+           ACHTUNG #1: The callback MUST NOT modify the Stmt
+           object. Calling any of the Stmt.get() variants,
+           Stmt.getColumnName(), or simililar, is legal, but calling
+           step() or finalize() is not. Routines which are illegal
+           in this context will trigger an exception.
+
+           ACHTUNG #2: The semantics of the `bind` and `callback`
+           options may well change or those options may be removed
+           altogether for this function (but retained for exec()).
+        */
+        execMulti: function(/*(sql [,obj]) || (obj)*/){
+            affirmDbOpen(this);
+            const arg = (BindTypes===arguments[2]
+                         /* ^^^ Being passed on from exec() */
+                         ? arguments[0] : parseExecArgs(arguments));
+            if(!arg.sql) return this;
+            const opt = arg.opt;
+            const stack = stackSave();
+            let stmt;
+            let bind = opt.bind;
+            let rowMode = (
+                (opt.callback && opt.rowMode)
+                    ? opt.rowMode : false);
+            try{
+                let pSql = allocateUTF8OnStack(arg.sql)
+                const pzTail = stackAlloc(4);
+                while(getValue(pSql, "i8")){
+                    setValue(pPtrArg, 0, "i32");
+                    setValue(pzTail, 0, "i32");
+                    this.checkRc(S.sqlite3_prepare_v2_sqlptr(
+                        this._pDb, pSql, -1, pPtrArg, pzTail
+                    ));
+                    const pStmt = getValue(pPtrArg, "i32");
+                    pSql = getValue(pzTail, "i32");
+                    if(!pStmt) continue;
+                    if(opt.saveSql){
+                        opt.saveSql.push(S.sqlite3_sql(pStmt).trim());
+                    }
+                    stmt = new Stmt(this, pStmt, BindTypes);
+                    if(bind && stmt.parameterCount){
+                        stmt.bind(bind);
+                        bind = null;
+                    }
+                    if(opt.callback && null!==rowMode){
+                        while(stmt.step()){
+                            stmt._isLocked = true;
+                            callback(arg.cbArg(stmt), stmt);
+                            stmt._isLocked = false;
+                        }
+                        rowMode = null;
+                    }else{
+                        // Do we need to while(stmt.step()){} here?
+                        stmt.step();
+                    }
+                    stmt.finalize();
+                    stmt = null;
+                }
+            }finally{
+                if(stmt){
+                    delete stmt._isLocked;
+                    stmt.finalize();
+                }
+                stackRestore(stack);
+            }
+            return this;
+        }/*execMulti()*/
+    }/*DB.prototype*/;
+
+
+    /** Throws if the given Stmt has been finalized, else stmt is
+        returned. */
+    const affirmStmtOpen = function(stmt){
+        if(!stmt._pStmt) toss("Stmt has been closed.");
+        return stmt;
     };
 
     /** Returns an opaque truthy value from the BindTypes
@@ -360,12 +587,17 @@
         if(0===n || (n===key && (n!==(n|0)/*floating point*/))){
             toss("Invalid bind() parameter name: "+key);
         }
-        else if(n<1 || n>=stmt.parameterCount) toss("Bind index",key,"is out of range.");
+        else if(n<1 || n>stmt.parameterCount) toss("Bind index",key,"is out of range.");
         return n;
     };
 
     /** Throws if ndx is not an integer or if it is out of range
-        for stmt.columnCount, else returns stmt. */
+        for stmt.columnCount, else returns stmt.
+
+        Reminder: this will also fail after the statement is finalized
+        but the resulting error will be about an out-of-bounds column
+        index.
+    */
     const affirmColIndex = function(stmt,ndx){
         if((ndx !== (ndx|0)) || ndx<0 || ndx>=stmt.columnCount){
             toss("Column index",ndx,"is out of range.");
@@ -380,7 +612,22 @@
         }
         return stmt;
     };
-    
+
+    /**
+       If stmt._isLocked is truthy, this throws an exception
+       complaining that the 2nd argument (an operation name,
+       e.g. "bind()") is not legal while the statement is "locked".
+       Locking happens before an exec()-like callback is passed a
+       statement, to ensure that the callback does not mutate or
+       finalize the statement. If it does not throw, it returns stmt.
+    */
+    const affirmUnlocked = function(stmt,currentOpName){
+        if(stmt._isLocked){
+            toss("Operation is illegal when statement is locked:",currentOpName);
+        }
+        return stmt;
+    };
+
     /**
        Binds a single bound parameter value on the given stmt at the
        given index (numeric or named) using the given bindType (see
@@ -388,6 +635,7 @@
        success.
     */
     const bindOne = function f(stmt,ndx,bindType,val){
+        affirmUnlocked(stmt, 'bind()');
         if(!f._){
             f._ = {
                 string: function(stmt, ndx, val, asBlob){
@@ -403,14 +651,14 @@
         ndx = affirmParamIndex(stmt,ndx);
         let rc = 0;
         switch((null===val || undefined===val) ? BindTypes.null : bindType){
-            case BindType.null:
+            case BindTypes.null:
                 rc = S.sqlite3_bind_null(stmt._pStmt, ndx);
                 break;
-            case BindType.string:{
+            case BindTypes.string:{
                 rc = f._.string(stmt, ndx, val, false);
                 break;
             }
-            case BindType.number: {
+            case BindTypes.number: {
                 const m = ((val === (val|0))
                            ? ((val & 0x00000000/*>32 bits*/)
                               ? S.sqlite3_bind_int64
@@ -419,10 +667,10 @@
                 rc = m(stmt._pStmt, ndx, val);
                 break;
             }
-            case BindType.boolean:
+            case BindTypes.boolean:
                 rc = S.sqlite3_bind_int(stmt._pStmt, ndx, val ? 1 : 0);
                 break;
-            case BindType.blob: {
+            case BindTypes.blob: {
                 if('string'===typeof val){
                     rc = f._.string(stmt, ndx, val, true);
                 }else{
@@ -442,13 +690,6 @@
         return stmt;
     };
 
-    /** Throws if the given Stmt has been finalized, else
-        it is returned. */
-    const affirmStmtOpen = function(stmt){
-        if(!stmt._pStmt) toss("Stmt has been closed.");
-        return stmt;
-    };
-
     /** Frees any memory explicitly allocated for the given
         Stmt object. Returns stmt. */
     const freeBindMemory = function(stmt){
@@ -468,6 +709,7 @@
         */
         finalize: function(){
             if(this._pStmt){
+                affirmUnlocked(this,'finalize()');
                 freeBindMemory(this);
                 delete this.db._statements[this._pStmt];
                 S.sqlite3_finalize(this._pStmt);
@@ -475,12 +717,15 @@
                 delete this.parameterCount;
                 delete this._pStmt;
                 delete this.db;
+                delete this._isLocked;
             }
         },
         /** Clears all bound values. Returns this object.
             Throws if this statement has been finalized. */
         clearBindings: function(){
-            freeBindMemory(affirmStmtOpen(this));
+            freeBindMemory(
+                affirmUnlocked(affirmStmtOpen(this), 'clearBindings()')
+            );
             S.sqlite3_clear_bindings(this._pStmt);
             this._mayGet = false;
             return this;
@@ -495,6 +740,7 @@
            any memory allocated for them, are retained.
         */
         reset: function(alsoClearBinds){
+            affirmUnlocked(this,'reset()');
             if(alsoClearBinds) this.clearBindings();
             S.sqlite3_reset(affirmStmtOpen(this)._pStmt);
             this._mayGet = false;
@@ -565,7 +811,7 @@
             }
             if(null===arg || undefined===arg){
                 /* bind NULL */
-                return bindOne(this, ndx, BindType.null, arg);
+                return bindOne(this, ndx, BindTypes.null, arg);
             }
             else if(Array.isArray(arg)){
                 /* bind each entry by index */
@@ -611,7 +857,7 @@
                && BindTypes.null !== t){
                 toss("Invalid value type for bindAsBlob()");
             }
-            return bindOne(this, ndx, BindType.blob, arg);
+            return bindOne(this, ndx, BindTypes.blob, arg);
         },
         /**
            Steps the statement one time. If the result indicates that
@@ -619,12 +865,16 @@
            data is available, false is returned.  Throws on error.
         */
         step: function(){
+            affirmUnlocked(this, 'step()');
             const rc = S.sqlite3_step(affirmStmtOpen(this)._pStmt);
             this._mayGet = false;
             switch(rc){
                 case S.SQLITE_DONE: return false;
                 case S.SQLITE_ROW: return this._mayGet = true;
-                default: this.db.checkRc(rc);
+                default:
+                    console.warn("sqlite3_step() rc=",rc,"SQL =",
+                                 S.sqlite3_sql(this._pStmt));
+                    this.db.checkRc(rc);
             };
         },
         /**
@@ -713,7 +963,8 @@
            A convenience wrapper around get() which fetches the value
            as a string and then, if it is not null, passes it to
            JSON.parse(), returning that result. Throws if parsing
-           fails.
+           fails. If the result is null, null is returned. An empty
+           string, on the other hand, will trigger an exception.
         */
         getJSON: function(ndx){
             const s = this.get(ndx, S.SQLITE_STRING);
@@ -725,26 +976,56 @@
            finalized.
         */
         getColumnName: function(ndx){
-            return S.sqlite3_column_name(affirmColIndex(this,ndx)._pStmt, ndx);
+            return S.sqlite3_column_name(
+                affirmColIndex(affirmStmtOpen(this),ndx)._pStmt, ndx
+            );
+        },
+        /**
+           If this statement potentially has result columns, this
+           function returns an array of all such names. If passed an
+           array, it is used as the target and all names are appended
+           to it. Returns the target array. Throws if this statement
+           cannot have result columns. This object's columnCount member
+           holds the number of columns.
+        */
+        getColumnNames: function(tgt){
+            affirmColIndex(affirmStmtOpen(this),0);
+            if(!tgt) tgt = [];
+            for(let i = 0; i < this.columnCount; ++i){
+                tgt.push(S.sqlite3_column_name(this._pStmt, i));
+            }
+            return tgt;
+        },
+        /**
+           If this statement has named bindable parameters and the
+           given name matches one, its 1-based bind index is
+           returned. If no match is found, 0 is returned. If it has no
+           bindable parameters, the undefined value is returned.
+        */
+        getParamIndex: function(name){
+            return (affirmStmtOpen(this).parameterCount
+                    ? S.sqlite3_bind_parameter_index(this._pStmt, name)
+                    : undefined);
         }
-    };
+    }/*Stmt.prototype*/;
 
     /** OO binding's namespace. */
     const SQLite3 = {
         version: {
-            lib: sqlite3.sqlite3_libversion(),
+            lib: S.sqlite3_libversion(),
             ooApi: "0.0.1"
         },
         DB,
         Stmt,
         /**
            Reports whether a given compile-time option, named by the
-           given argument.
+           given argument. It has several distinct uses:
 
            If optName is an array then it is expected to be a list of
            compilation options and this function returns an object
-           which maps each such option to true or false. That object
-           is returned.
+           which maps each such option to true or false, indicating
+           whether or not the given option was included in this
+           build. That object is returned.
 
            If optName is an object, its keys are expected to be
            compilation options and this function sets each entry to
@@ -752,10 +1033,10 @@
 
            If passed no arguments then it returns an object mapping
            all known compilation options to their compile-time values,
-           or true if the are defined with no value.
+           or boolean true if they are defined with no value.
 
-           In all other cases it returns true if the option was active
-           when when compiling the sqlite3 module, else false.
+           In all other cases it returns true if the given option was
+           active when when compiling the sqlite3 module, else false.
 
            Compile-time option names may optionally include their
            "SQLITE_" prefix. When it returns an object of all options,
@@ -798,9 +1079,9 @@
             ) ? !!S.sqlite3_compileoption_used(optName) : false;
         }
     };
-    
+
     namespace.sqlite3 = {
-        api:sqlite3,
+        api: api,
         SQLite3
     };
 })(self/*worker or window*/);
diff --git a/ext/fiddle/testing1.js b/ext/fiddle/testing1.js
index d667a480c5..b7dcfe6b00 100644
--- a/ext/fiddle/testing1.js
+++ b/ext/fiddle/testing1.js
@@ -20,43 +20,77 @@ const mainTest1 = function(namespace){
     console.log("Loaded module:",S.sqlite3_libversion(),
                 S.sqlite3_sourceid());
     const db = new oo.DB();
-    const log = console.log.bind(console);
-    T.assert(db._pDb);
-    log("DB:",db.filename);
-    log("Build options:",oo.compileOptionUsed());
-    let st = db.prepare("select 3 as a");
-    log("statement =",st);
-    T.assert(st._pStmt)
-        .assert(!st._mayGet)
-        .assert('a' === st.getColumnName(0))
-        .assert(st === db._statements[st._pStmt])
-        .assert(1===st.columnCount)
-        .assert(0===st.parameterCount)
-        .mustThrow(()=>st.bind(1,null))
-        .assert(true===st.step())
-        .assert(3 === st.get(0))
-        .mustThrow(()=>st.get(1))
-        .mustThrow(()=>st.get(0,~S.SQLITE_INTEGER))
-        .assert(3 === st.get(0,S.SQLITE_INTEGER))
-        .assert(3 === st.getInt(0))
-        .assert('3' === st.get(0,S.SQLITE_TEXT))
-        .assert('3' === st.getString(0))
-        .assert(3.0 === st.get(0,S.SQLITE_FLOAT))
-        .assert(3.0 === st.getFloat(0))
-        .assert(st.get(0,S.SQLITE_BLOB) instanceof Uint8Array)
-        .assert(st.getBlob(0) instanceof Uint8Array)
-        .assert(3 === st.get([])[0])
-        .assert(3 === st.get({}).a)
-        .assert(3 === st.getJSON(0))
-        .assert(st._mayGet)
-        .assert(false===st.step())
-        .assert(!st._mayGet)
-    ;
-    let pId = st._pStmt;
-    st.finalize();
-    T.assert(!st._pStmt)
-        .assert(!db._statements[pId]);
-    log("Test count:",T.counter);
+    try {
+        const log = console.log.bind(console);
+        T.assert(db._pDb);
+        log("DB:",db.filename);
+        log("Build options:",oo.compileOptionUsed());
+        let st = db.prepare("select 3 as a");
+        log("statement =",st);
+        T.assert(st._pStmt)
+            .assert(!st._mayGet)
+            .assert('a' === st.getColumnName(0))
+            .assert(st === db._statements[st._pStmt])
+            .assert(1===st.columnCount)
+            .assert(0===st.parameterCount)
+            .mustThrow(()=>st.bind(1,null))
+            .assert(true===st.step())
+            .assert(3 === st.get(0))
+            .mustThrow(()=>st.get(1))
+            .mustThrow(()=>st.get(0,~S.SQLITE_INTEGER))
+            .assert(3 === st.get(0,S.SQLITE_INTEGER))
+            .assert(3 === st.getInt(0))
+            .assert('3' === st.get(0,S.SQLITE_TEXT))
+            .assert('3' === st.getString(0))
+            .assert(3.0 === st.get(0,S.SQLITE_FLOAT))
+            .assert(3.0 === st.getFloat(0))
+            .assert(st.get(0,S.SQLITE_BLOB) instanceof Uint8Array)
+            .assert(st.getBlob(0) instanceof Uint8Array)
+            .assert(3 === st.get([])[0])
+            .assert(3 === st.get({}).a)
+            .assert(3 === st.getJSON(0))
+            .assert(st._mayGet)
+            .assert(false===st.step())
+            .assert(!st._mayGet)
+        ;
+        let pId = st._pStmt;
+        st.finalize();
+        T.assert(!st._pStmt)
+            .assert(!db._statements[pId]);
+
+        let list = [];
+        db.execMulti({
+            sql:`CREATE TABLE t(a,b);
+INSERT INTO t(a,b) VALUES(1,2),(3,4),(?,?);`,
+            saveSql: list,
+            bind: [5,6]
+        });
+        log("Exec'd SQL:", list);
+        let counter = 0, colNames = [];
+        db.exec("SELECT a a, b b FROM t",{
+            rowMode: 'object',
+            callback: function(row,stmt){
+                if(!counter) stmt.getColumnNames(colNames);
+                ++counter;
+                T.assert(row.a%2 && row.a<6);
+            }
+        });
+        assert(2 === colNames.length);
+        assert('a' === colNames[0]);
+        T.assert(3 === counter);
+        db.exec("SELECT a a, b b FROM t",{
+            rowMode: 'array',
+            callback: function(row,stmt){
+                ++counter;
+                assert(Array.isArray(row));
+                T.assert(0===row[1]%2 && row[1]<7);
+            }
+        });
+        T.assert(6 === counter);
+        log("Test count:",T.counter);
+    }finally{
+        db.close();
+    }
 };
 
 self/*window or worker*/.Module.onRuntimeInitialized = function(){
diff --git a/manifest b/manifest
index da28b2bacd..885ccb9c86 100644
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Implemented\sStmt.get()\sand\sfriends\sfor\sWASM\sOO\s#1\swrapper.\sAdded\sbasic\stests\sfor\sprepare/step/get.\sRestructured\smodule\sinit\soutput\sto\sintroduce\sonly\s1\sglobal-scope\ssymbol\sinstead\sof\s2.
-D 2022-05-22T19:09:59.198
+C WASM:\sadded\sexec(),\sexecMulti(),\sand\sseveral\sgetters.\sVarious\stouchups\sand\sfixes.
+D 2022-05-22T22:00:39.271
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -56,7 +56,7 @@ F ext/expert/sqlite3expert.c 6ca30d73b9ed75bd56d6e0d7f2c962d2affaa72c505458619d0
 F ext/expert/sqlite3expert.h ca81efc2679a92373a13a3e76a6138d0310e32be53d6c3bfaedabd158ea8969b
 F ext/expert/test_expert.c d56c194b769bdc90cf829a14c9ecbc1edca9c850b837a4d0b13be14095c32a72
 F ext/fiddle/EXPORTED_FUNCTIONS.fiddle 487fc7c83d45c48326f731c89162ed17ab15767e5efede8999d7d6c6e2d04c0f
-F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 bba07bb2b81ce667df7916d4c942f0bfe6de6c77f5fe769d479f01250f92ca01
+F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 5816adc4d4715b410a9df971c70f55fca610d3a240bd85d2ec34e75483cb54bb
 F ext/fiddle/EXPORTED_RUNTIME_METHODS 91d5dcb0168ee056fa1a340cb8ab3c23d922622f8dad39d28919dd8af2b3ade0
 F ext/fiddle/Makefile 9277c73e208b9c8093659256c9f07409c877e366480c7c22ec545ee345451d95
 F ext/fiddle/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f
@@ -64,10 +64,10 @@ F ext/fiddle/fiddle-worker.js c22557b641b47fa1473d3465a4e69fe06b8b09b924955805a4
 F ext/fiddle/fiddle.html 657c6c3f860c322fba3c69fa4f7a1209e2d2ce44b4bc65a3e154e3a97c047a7c
 F ext/fiddle/fiddle.js f9c79164428e96a5909532f18a8bc8f8c8ec4f738bfc09ad3d2a532c2400f9f0
 F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf
-F ext/fiddle/sqlite3-api.js e205ccb758678bab7261f184e816d809a5031e1b4babd7738bed98de534787dd
+F ext/fiddle/sqlite3-api.js 6d088e075fa2910dcad2584d2a518de33c7dd5ce4d94408691c0cdb1c9c4394b
 F ext/fiddle/testing-common.js 53284264504821314f052017b54fa75ab065dcd9cbb754cc8060930498faeee8
 F ext/fiddle/testing1.html 68cec1b1c8646a071717e5979f22e4268e6d36d96ba13ad68333351acdbcf1d1
-F ext/fiddle/testing1.js 8849eaee6d7b31a195b29f9532c16a87a03e1be780a48cbdec54760c39ebf66c
+F ext/fiddle/testing1.js 5e46c8850f826821cb24b13a21e4dabee8dac9ce76845149dac599ab643784ab
 F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e
 F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b
 F ext/fts1/ft_hash.h 06df7bba40dadd19597aa400a875dbc2fed705ea
@@ -1967,8 +1967,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P f3bc0328c87cac7d50513b0f13576d8fe7b411396f19c08fbe7e7c657b33cfbf
-R e0afacc1df156a218ff63194b2d94770
+P 601dc3fa29c2ce2ede5a8320c79050305f3774b6d7bc759247c5021f3b74aaec
+R e2842421067cb498614f974216c18719
 U stephan
-Z 8276de9ecbbfd692683c1010f2d924a3
+Z 9b9e03e3e05307544edc47fc1b8505b5
 # Remove this line to create a well-formed Fossil manifest.
diff --git a/manifest.uuid b/manifest.uuid
index 826df84f44..e601bd2321 100644
--- a/manifest.uuid
+++ b/manifest.uuid
@@ -1 +1 @@
-601dc3fa29c2ce2ede5a8320c79050305f3774b6d7bc759247c5021f3b74aaec
\ No newline at end of file
+b790c91b85e9cf8eecce86ac1717e8ccd2c3b6b98a1ad6a5d64eefc94ee86f9d
\ No newline at end of file