diff --git a/Makefile.in b/Makefile.in
index 29fcaf75c0..2c899a1f4a 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -1513,3 +1513,99 @@ sqlite3.def: $(REAL_LIBOBJ)
sqlite3.dll: $(REAL_LIBOBJ) sqlite3.def
$(TCC) -shared -o $@ sqlite3.def \
-Wl,"--strip-all" $(REAL_LIBOBJ)
+
+
+#
+# fiddle/wasm section
+#
+fiddle_dir = ext/fiddle
+fiddle_dir_abs = $(TOP)/$(fiddle_dir)
+# ^^^ some emcc opts require absolute paths
+fiddle_html = $(fiddle_dir)/fiddle.html
+fiddle_module_js = $(fiddle_dir)/fiddle-module.js
+fiddle_generated = $(fiddle_module_js) \
+ $(fiddle_dir)/fiddle-module.wasm
+sqlite3_wasm_js = $(fiddle_dir)/sqlite3.js
+sqlite3_wasm = $(fiddle_dir)/sqlite3.wasm
+sqlite3_wasm_generated = $(sqlite3_wasm) $(sqlite3_wasm_js)
+clean-wasm:
+ rm -f $(fiddle_generated) $(sqlite3_wasm_generated)
+clean: clean-wasm
+#emcc_opt = -O0
+#emcc_opt = -O1
+#emcc_opt = -O2
+#emcc_opt = -O3
+emcc_opt = -Oz
+emcc_flags = $(emcc_opt) -sALLOW_TABLE_GROWTH -I. $(SHELL_OPT)
+$(fiddle_module_js): Makefile sqlite3.c shell.c \
+ $(fiddle_dir)/EXPORTED_RUNTIME_METHODS \
+ $(fiddle_dir)/EXPORTED_FUNCTIONS.fiddle
+ emcc -o $@ $(emcc_flags) \
+ -sENVIRONMENT=web \
+ -sMODULARIZE \
+ -sEXPORT_NAME=initFiddleModule \
+ -sEXPORTED_RUNTIME_METHODS=@$(fiddle_dir_abs)/EXPORTED_RUNTIME_METHODS \
+ -sEXPORTED_FUNCTIONS=@$(fiddle_dir_abs)/EXPORTED_FUNCTIONS.fiddle \
+ sqlite3.c shell.c
+$(sqlite3_wasm_js): Makefile sqlite3.c \
+ $(fiddle_dir)/sqlite3-api.js \
+ $(fiddle_dir)/EXPORTED_RUNTIME_METHODS \
+ $(fiddle_dir)/EXPORTED_FUNCTIONS.sqlite3-api
+ emcc -o $@ $(emcc_flags) \
+ -sENVIRONMENT=web \
+ -sMODULARIZE \
+ -sEXPORT_NAME=initSqlite3Module \
+ -sEXPORTED_RUNTIME_METHODS=@$(fiddle_dir_abs)/EXPORTED_RUNTIME_METHODS \
+ -sEXPORTED_FUNCTIONS=@$(fiddle_dir_abs)/EXPORTED_FUNCTIONS.sqlite3-api \
+ --post-js=$(fiddle_dir)/sqlite3-api.js \
+ --no-entry \
+ sqlite3.c
+fiddle: $(fiddle_module_js)
+sqlite3-wasm: $(sqlite3_wasm_js)
+wasm: fiddle sqlite3-wasm
+########################################################################
+# Explanation of the emcc build flags:
+#
+# -sENVIRONMENT=web: elides bootstrap code related to non-web JS
+# environments like node.js. Removing this makes the output a tiny
+# tick larger but hypothetically makes it more portable to
+# non-browser JS environments.
+#
+# -sMODULARIZE: changes how the generated code is structured to avoid
+# declaring a global Module object and instead installing a function
+# which loads and initialized the module. The function is named...
+#
+# -sEXPORT_NAME=jsFunctionName (see -sMODULARIZE)
+#
+# -sEXPORTED_RUNTIME_METHODS=@/absolute/path/to/file: a file
+# containing a list of emscripten-supplied APIs, one per line, which
+# must be exported into the generated JS. Must be an absolute path!
+#
+# -sEXPORTED_FUNCTIONS=@/absolute/path/to/file: a file containing a
+# list of C functions, one per line, which must be exported via wasm
+# so they're visible to JS. C symbols names in that file must all
+# start with an underscore for reasons known only to the emcc
+# developers. e.g., _sqlite3_open_v2 and _sqlite3_finalize. Must be
+# an absolute path!
+#
+# --no-entry: for compiling library code with no main(). If this is
+# not supplied and the code has a main(), it is called as part of the
+# module init process. Note that main() is #if'd out of shell.c
+# (renamed) when building in wasm mode.
+#
+# --pre-js/--post-js=FILE relative or absolute paths to JS files to
+# prepend/append to the emcc-generated bootstrapping JS. It's
+# easier/faster to develop with separate JS files (reduces rebuilding
+# requirements) but certain configurations, namely -sMODULARIZE, may
+# require using at least a --pre-js file. They can be used
+# individually and need not be paired.
+#
+# -O0..-O3 and -Oz: optimization levels affect not only C-style
+# optimization but whether or not the resulting generated JS code
+# gets minified. -O0 compiles _much_ more quickly than -O3 or -Oz,
+# and doesn't minimize any JS code, so is recommended for
+# development. -O3 or -Oz are recommended for deployment, but
+# primarily because -Oz will shrink the wasm file notably. JS-side
+# minification makes little difference in terms of overall
+# distributable size.
+########################################################################
diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.fiddle b/ext/fiddle/EXPORTED_FUNCTIONS.fiddle
new file mode 100644
index 0000000000..b96ce4e67c
--- /dev/null
+++ b/ext/fiddle/EXPORTED_FUNCTIONS.fiddle
@@ -0,0 +1,7 @@
+_fiddle_exec
+_fiddle_interrupt
+_fiddle_experiment
+_fiddle_the_db
+_fiddle_db_arg
+_fiddle_db_filename
+_fiddle_reset_db
diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api
new file mode 100644
index 0000000000..3127b294d9
--- /dev/null
+++ b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api
@@ -0,0 +1,51 @@
+_sqlite3_bind_blob
+_sqlite3_bind_double
+_sqlite3_bind_int
+_sqlite3_bind_int64
+_sqlite3_bind_null
+_sqlite3_bind_parameter_count
+_sqlite3_bind_parameter_index
+_sqlite3_bind_text
+_sqlite3_changes
+_sqlite3_clear_bindings
+_sqlite3_close_v2
+_sqlite3_column_blob
+_sqlite3_column_bytes
+_sqlite3_column_count
+_sqlite3_column_count
+_sqlite3_column_double
+_sqlite3_column_int
+_sqlite3_column_int64
+_sqlite3_column_name
+_sqlite3_column_text
+_sqlite3_column_type
+_sqlite3_compileoption_get
+_sqlite3_compileoption_used
+_sqlite3_create_function_v2
+_sqlite3_data_count
+_sqlite3_db_filename
+_sqlite3_errmsg
+_sqlite3_exec
+_sqlite3_finalize
+_sqlite3_interrupt
+_sqlite3_libversion
+_sqlite3_open
+_sqlite3_open_v2
+_sqlite3_prepare_v2
+_sqlite3_prepare_v2
+_sqlite3_reset
+_sqlite3_result_blob
+_sqlite3_result_double
+_sqlite3_result_error
+_sqlite3_result_int
+_sqlite3_result_null
+_sqlite3_result_text
+_sqlite3_sourceid
+_sqlite3_sql
+_sqlite3_step
+_sqlite3_value_blob
+_sqlite3_value_bytes
+_sqlite3_value_double
+_sqlite3_value_text
+_sqlite3_value_type
+_free
diff --git a/ext/fiddle/EXPORTED_RUNTIME_METHODS b/ext/fiddle/EXPORTED_RUNTIME_METHODS
new file mode 100644
index 0000000000..763722d236
--- /dev/null
+++ b/ext/fiddle/EXPORTED_RUNTIME_METHODS
@@ -0,0 +1,14 @@
+ALLOC_NORMAL
+FS
+UTF8ToString
+addFunction
+allocate
+allocateUTF8OnStack
+ccall
+cwrap
+getValue
+removeFunction
+setValue
+stackAlloc
+stackRestore
+stackSave
diff --git a/ext/fiddle/Makefile b/ext/fiddle/Makefile
new file mode 100644
index 0000000000..6f3279b61a
--- /dev/null
+++ b/ext/fiddle/Makefile
@@ -0,0 +1,33 @@
+# This GNU makefile exists primarily to simplify/speed up development
+# from emacs. It is not part of the canonical build process.
+default:
+ $(MAKE) -C ../.. wasm -e emcc_opt=-O0
+
+clean:
+ $(MAKE) -C ../../ clean-wasm
+
+fiddle_files = emscripten.css fiddle.html \
+ fiddle.js fiddle-module.js \
+ fiddle-module.wasm fiddle-worker.js
+
+# fiddle_remote is the remote destination for the fiddle app. It
+# must be a [user@]HOST:/path for rsync.
+# Note that the target "should probably" contain a symlink of
+# index.html -> fiddle.html.
+fiddle_remote ?=
+ifeq (,$(fiddle_remote))
+ifneq (,$(wildcard /home/stephan))
+ fiddle_remote = wh2:www/wh/sqlite3/.
+else ifneq (,$(wildcard /home/drh))
+ #fiddle_remote = if appropriate, add that user@host:/path here
+endif
+endif
+
+$(fiddle_files): default
+
+push-fiddle: $(fiddle_files)
+ @if [ x = "x$(fiddle_remote)" ]; then \
+ echo "fiddle_remote must be a [user@]HOST:/path for rsync"; \
+ exit 1; \
+ fi
+ rsync -va $(fiddle_files) $(fiddle_remote)
diff --git a/ext/fiddle/SqliteTestUtil.js b/ext/fiddle/SqliteTestUtil.js
new file mode 100644
index 0000000000..cf78946dc7
--- /dev/null
+++ b/ext/fiddle/SqliteTestUtil.js
@@ -0,0 +1,144 @@
+/*
+ 2022-05-22
+
+ 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 bootstrapping code used by various test scripts
+ which live in this file's directory.
+*/
+(function(){
+ /* querySelectorAll() proxy */
+ const EAll = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelectorAll(arguments[arguments.length-1]);
+ };
+ /* querySelector() proxy */
+ const E = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelector(arguments[arguments.length-1]);
+ };
+
+ /**
+ Helpers for writing sqlite3-specific tests.
+ */
+ self/*window or worker*/.SqliteTestUtil = {
+ /** 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;
+ },
+ /** abort() if expr is false. If expr is a function, it
+ is called and its result is evaluated.
+ */
+ assert: function f(expr, msg){
+ if(!f._){
+ f._ = ('undefined'===typeof abort
+ ? (msg)=>{throw new Error(msg)}
+ : abort);
+ }
+ ++this.counter;
+ if(!this.toBool(expr)){
+ f._(msg || "Assertion failed.");
+ }
+ return this;
+ },
+ /** Identical to assert() but throws instead of calling
+ abort(). */
+ affirm: function(expr, msg){
+ ++this.counter;
+ if(!this.toBool(expr)) throw new Error(msg || "Affirmation 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;
+ },
+ /** 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;
+ }
+ };
+
+
+ /**
+ This is a module object for use with the emscripten-installed
+ initSqlite3Module() factory function.
+ */
+ self.sqlite3TestModule = {
+ postRun: [
+ /* function(theModule){...} */
+ ],
+ //onRuntimeInitialized: function(){},
+ /* Proxy for C-side stdout output. */
+ print: function(){
+ console.log.apply(console, Array.prototype.slice.call(arguments));
+ },
+ /* Proxy for C-side stderr output. */
+ printErr: function(){
+ console.error.apply(console, Array.prototype.slice.call(arguments));
+ },
+ /**
+ Called by the module init bits to report loading
+ progress. It gets passed an empty argument when loading is
+ done (after onRuntimeInitialized() and any this.postRun
+ callbacks have been run).
+ */
+ setStatus: function f(text){
+ if(!f.last){
+ f.last = { text: '', step: 0 };
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ if(text === f.last.text) return;
+ f.last.text = text;
+ if(f.ui.progress){
+ f.ui.progress.value = f.last.step;
+ f.ui.progress.max = f.last.step + 1;
+ }
+ ++f.last.step;
+ if(text) {
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ }
+ }
+ };
+})(self/*window or worker*/);
diff --git a/ext/fiddle/emscripten.css b/ext/fiddle/emscripten.css
new file mode 100644
index 0000000000..7e3dc811d0
--- /dev/null
+++ b/ext/fiddle/emscripten.css
@@ -0,0 +1,24 @@
+/* emcscript-related styling, used during the module load/intialization processes... */
+.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+div.emscripten { text-align: center; }
+div.emscripten_border { border: 1px solid black; }
+#module-spinner { overflow: visible; }
+#module-spinner > * {
+ margin-top: 1em;
+}
+.spinner {
+ height: 50px;
+ width: 50px;
+ margin: 0px auto;
+ animation: rotation 0.8s linear infinite;
+ border-left: 10px solid rgb(0,150,240);
+ border-right: 10px solid rgb(0,150,240);
+ border-bottom: 10px solid rgb(0,150,240);
+ border-top: 10px solid rgb(100,0,200);
+ border-radius: 100%;
+ background-color: rgb(200,100,250);
+}
+@keyframes rotation {
+ from {transform: rotate(0deg);}
+ to {transform: rotate(360deg);}
+}
diff --git a/ext/fiddle/fiddle-worker.js b/ext/fiddle/fiddle-worker.js
new file mode 100644
index 0000000000..e71955a8a8
--- /dev/null
+++ b/ext/fiddle/fiddle-worker.js
@@ -0,0 +1,300 @@
+/*
+ 2022-05-20
+
+ 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 is the JS Worker file for the sqlite3 fiddle app. It loads the
+ sqlite3 wasm module and offers access to the db via the Worker
+ message-passing interface.
+
+ Forewarning: this API is still very much Under Construction and
+ subject to any number of changes as experience reveals what those
+ need to be.
+
+ Because we can have only a single message handler, as opposed to an
+ arbitrary number of discrete event listeners like with DOM elements,
+ we have to define a lower-level message API. Messages abstractly
+ look like:
+
+ { type: string, data: type-specific value }
+
+ Where 'type' is used for dispatching and 'data' is a
+ 'type'-dependent value.
+
+ The 'type' values expected by each side of the main/worker
+ connection vary. The types are described below but subject to
+ change at any time as this experiment evolves.
+
+ Workers-to-Main types
+
+ - stdout, stderr: indicate stdout/stderr output from the wasm
+ layer. The data property is the string of the output, noting
+ that the emscripten binding emits these one line at a time. Thus,
+ if a C-side puts() emits multiple lines in a single call, the JS
+ side will see that as multiple calls. Example:
+
+ {type:'stdout', data: 'Hi, world.'}
+
+ - module: Status text. This is intended to alert the main thread
+ about module loading status so that, e.g., the main thread can
+ update a progress widget and DTRT when the module is finished
+ loading and available for work. Status messages come in the form
+
+ {type:'module', data:{
+ type:'status',
+ data: {text:string|null, step:1-based-integer}
+ }
+
+ with an incrementing step value for each subsequent message. When
+ the module loading is complete, a message with a text value of
+ null is posted.
+
+ - working: data='start'|'end'. Indicates that work is about to be
+ sent to the module or has just completed. This can be used, e.g.,
+ to disable UI elements which should not be activated while work
+ is pending. Example:
+
+ {type:'working', data:'start'}
+
+ Main-to-Worker types:
+
+ - shellExec: data=text to execute as if it had been entered in the
+ sqlite3 CLI shell app (as opposed to sqlite3_exec()). This event
+ causes the worker to emit a 'working' event (data='start') before
+ it starts and a 'working' event (data='end') when it finished. If
+ called while work is currently being executed it emits stderr
+ message instead of doing actual work, as the underlying db cannot
+ handle concurrent tasks. Example:
+
+ {type:'shellExec', data: 'select * from sqlite_master'}
+
+ - More TBD as the higher-level db layer develops.
+*/
+
+/*
+ Apparent browser(s) bug: console messages emitted may be duplicated
+ in the console, even though they're provably only run once. See:
+
+ https://stackoverflow.com/questions/49659464
+
+ Noting that it happens in Firefox as well as Chrome. Harmless but
+ annoying.
+*/
+"use strict";
+(function(){
+ /**
+ Posts a message in the form {type,data} unless passed more than 2
+ args, in which case it posts {type, data:[arg1...argN]}.
+ */
+ const wMsg = function(type,data){
+ postMessage({
+ type,
+ data: arguments.length<3
+ ? data
+ : Array.prototype.slice.call(arguments,1)
+ });
+ };
+
+ const stdout = function(){wMsg('stdout', Array.prototype.slice.call(arguments));};
+ const stderr = function(){wMsg('stderr', Array.prototype.slice.call(arguments));};
+
+ self.onerror = function(/*message, source, lineno, colno, error*/) {
+ const err = arguments[4];
+ if(err && 'ExitStatus'==err.name){
+ /* This is relevant for the sqlite3 shell binding but not the
+ lower-level binding. */
+ fiddleModule.isDead = true;
+ stderr("FATAL ERROR:", err.message);
+ stderr("Restarting the app requires reloading the page.");
+ wMsg('error', err);
+ }
+ console.error(err);
+ fiddleModule.setStatus('Exception thrown, see JavaScript console: '+err);
+ };
+
+ const Sqlite3Shell = {
+ /** Returns the name of the currently-opened db. */
+ dbFilename: function f(){
+ if(!f._) f._ = fiddleModule.cwrap('fiddle_db_filename', "string", ['string']);
+ return f._();
+ },
+ /**
+ Runs the given text through the shell as if it had been typed
+ in by a user. Fires a working/start event before it starts and
+ working/end event when it finishes.
+ */
+ exec: function f(sql){
+ if(!f._) f._ = fiddleModule.cwrap('fiddle_exec', null, ['string']);
+ if(fiddleModule.isDead){
+ stderr("shell module has exit()ed. Cannot run SQL.");
+ return;
+ }
+ wMsg('working','start');
+ try {
+ if(f._running){
+ stderr('Cannot run multiple commands concurrently.');
+ }else{
+ f._running = true;
+ f._(sql);
+ }
+ } finally {
+ delete f._running;
+ wMsg('working','end');
+ }
+ },
+ resetDb: function f(){
+ if(!f._) f._ = fiddleModule.cwrap('fiddle_reset_db', null);
+ stdout("Resetting database.");
+ f._();
+ stdout("Reset",this.dbFilename());
+ },
+ /* Interrupt can't work: this Worker is tied up working, so won't get the
+ interrupt event which would be needed to perform the interrupt. */
+ interrupt: function f(){
+ if(!f._) f._ = fiddleModule.cwrap('fiddle_interrupt', null);
+ stdout("Requesting interrupt.");
+ f._();
+ }
+ };
+
+ self.onmessage = function f(ev){
+ ev = ev.data;
+ if(!f.cache){
+ f.cache = {
+ prevFilename: null
+ };
+ }
+ //console.debug("worker: onmessage.data",ev);
+ switch(ev.type){
+ case 'shellExec': Sqlite3Shell.exec(ev.data); return;
+ case 'db-reset': Sqlite3Shell.resetDb(); return;
+ case 'interrupt': Sqlite3Shell.interrupt(); return;
+ /** Triggers the export of the current db. Fires an
+ event in the form:
+
+ {type:'db-export',
+ data:{
+ filename: name of db,
+ buffer: contents of the db file (Uint8Array),
+ error: on error, a message string and no buffer property.
+ }
+ }
+ */
+ case 'db-export': {
+ const fn = Sqlite3Shell.dbFilename();
+ stdout("Exporting",fn+".");
+ const fn2 = fn ? fn.split(/[/\\]/).pop() : null;
+ try{
+ if(!fn2) throw new Error("DB appears to be closed.");
+ wMsg('db-export',{
+ filename: fn2,
+ buffer: fiddleModule.FS.readFile(fn, {encoding:"binary"})
+ });
+ }catch(e){
+ /* Post a failure message so that UI elements disabled
+ during the export can be re-enabled. */
+ wMsg('db-export',{
+ filename: fn,
+ error: e.message
+ });
+ }
+ return;
+ }
+ case 'open': {
+ /* Expects: {
+ buffer: ArrayBuffer | Uint8Array,
+ filename: for logging/informational purposes only
+ } */
+ const opt = ev.data;
+ let buffer = opt.buffer;
+ if(buffer instanceof Uint8Array){
+ }else if(buffer instanceof ArrayBuffer){
+ buffer = new Uint8Array(buffer);
+ }else{
+ stderr("'open' expects {buffer:Uint8Array} containing an uploaded db.");
+ return;
+ }
+ const fn = (
+ opt.filename
+ ? opt.filename.split(/[/\\]/).pop().replace('"','_')
+ : ("db-"+((Math.random() * 10000000) | 0)+
+ "-"+((Math.random() * 10000000) | 0)+".sqlite3")
+ );
+ /* We cannot delete the existing db file until the new one
+ is installed, which means that we risk overflowing our
+ quota (if any) by having both the previous and current
+ db briefly installed in the virtual filesystem. */
+ fiddleModule.FS.createDataFile("/", fn, buffer, true, true);
+ const oldName = Sqlite3Shell.dbFilename();
+ Sqlite3Shell.exec('.open "/'+fn+'"');
+ if(oldName !== fn){
+ fiddleModule.FS.unlink(oldName);
+ }
+ stdout("Replaced DB with",fn+".");
+ return;
+ }
+ };
+ console.warn("Unknown fiddle-worker message type:",ev);
+ };
+
+ /**
+ emscripten module for use with build mode -sMODULARIZE.
+ */
+ const fiddleModule = {
+ print: stdout,
+ printErr: stderr,
+ /**
+ Intercepts status updates from the emscripting module init
+ and fires worker events with a type of 'status' and a
+ payload of:
+
+ {
+ text: string | null, // null at end of load process
+ step: integer // starts at 1, increments 1 per call
+ }
+
+ We have no way of knowing in advance how many steps will
+ be processed/posted, so creating a "percentage done" view is
+ not really practical. One can be approximated by giving it a
+ current value of message.step and max value of message.step+1,
+ though.
+
+ When work is finished, a message with a text value of null is
+ submitted.
+
+ After a message with text==null is posted, the module may later
+ post messages about fatal problems, e.g. an exit() being
+ triggered, so it is recommended that UI elements for posting
+ status messages not be outright removed from the DOM when
+ text==null, and that they instead be hidden until/unless
+ text!=null.
+ */
+ setStatus: function f(text){
+ if(!f.last) f.last = { step: 0, text: '' };
+ else if(text === f.last.text) return;
+ f.last.text = text;
+ wMsg('module',{
+ type:'status',
+ data:{step: ++f.last.step, text: text||null}
+ });
+ }
+ };
+
+ importScripts('fiddle-module.js');
+ /**
+ initFiddleModule() is installed via fiddle-module.js due to
+ building with:
+
+ emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule
+ */
+ initFiddleModule(fiddleModule).then(function(thisModule){
+ wMsg('fiddle-ready');
+ });
+})();
diff --git a/ext/fiddle/fiddle.html b/ext/fiddle/fiddle.html
new file mode 100644
index 0000000000..e461f65220
--- /dev/null
+++ b/ext/fiddle/fiddle.html
@@ -0,0 +1,264 @@
+
+
+
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+
+
+
Downloading...
+
+
+
+
+
+ This is a placeholder for a terminal-like view.
+
+
+
+
+
diff --git a/ext/fiddle/fiddle.js b/ext/fiddle/fiddle.js
new file mode 100644
index 0000000000..c03a09af11
--- /dev/null
+++ b/ext/fiddle/fiddle.js
@@ -0,0 +1,783 @@
+/*
+ 2022-05-20
+
+ 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 is the main entry point for the sqlite3 fiddle app. It sets up the
+ various UI bits, loads a Worker for the db connection, and manages the
+ communication between the UI and worker.
+*/
+(function(){
+ 'use strict';
+ /* Recall that the 'self' symbol, except where locally
+ overwritten, refers to the global window or worker object. */
+
+ const storage = (function(NS/*namespace object in which to store this module*/){
+ /* Pedantic licensing note: this code originated in the Fossil SCM
+ source tree, where it has a different license, but the person who
+ ported it into sqlite is the same one who wrote it for fossil. */
+ 'use strict';
+ NS = NS||{};
+
+ /**
+ This module provides a basic wrapper around localStorage
+ or sessionStorage or a dummy proxy object if neither
+ of those are available.
+ */
+ const tryStorage = function f(obj){
+ if(!f.key) f.key = 'storage.access.check';
+ try{
+ obj.setItem(f.key, 'f');
+ const x = obj.getItem(f.key);
+ obj.removeItem(f.key);
+ if(x!=='f') throw new Error(f.key+" failed")
+ return obj;
+ }catch(e){
+ return undefined;
+ }
+ };
+
+ /** Internal storage impl for this module. */
+ const $storage =
+ tryStorage(window.localStorage)
+ || tryStorage(window.sessionStorage)
+ || tryStorage({
+ // A basic dummy xyzStorage stand-in
+ $$$:{},
+ setItem: function(k,v){this.$$$[k]=v},
+ getItem: function(k){
+ return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
+ },
+ removeItem: function(k){delete this.$$$[k]},
+ clear: function(){this.$$$={}}
+ });
+
+ /**
+ For the dummy storage we need to differentiate between
+ $storage and its real property storage for hasOwnProperty()
+ to work properly...
+ */
+ const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
+
+ /**
+ A prefix which gets internally applied to all storage module
+ property keys so that localStorage and sessionStorage across the
+ same browser profile instance do not "leak" across multiple apps
+ being hosted by the same origin server. Such cross-polination is
+ still there but, with this key prefix applied, it won't be
+ immediately visible via the storage API.
+
+ With this in place we can justify using localStorage instead of
+ sessionStorage.
+
+ One implication of using localStorage and sessionStorage is that
+ their scope (the same "origin" and client application/profile)
+ allows multiple apps on the same origin to use the same
+ storage. Thus /appA/foo could then see changes made via
+ /appB/foo. The data do not cross user- or browser boundaries,
+ though, so it "might" arguably be called a
+ feature. storageKeyPrefix was added so that we can sandbox that
+ state for each separate app which shares an origin.
+
+ See: https://fossil-scm.org/forum/forumpost/4afc4d34de
+
+ Sidebar: it might seem odd to provide a key prefix and stick all
+ properties in the topmost level of the storage object. We do that
+ because adding a layer of object to sandbox each app would mean
+ (de)serializing that whole tree on every storage property change.
+ e.g. instead of storageObject.projectName.foo we have
+ storageObject[storageKeyPrefix+'foo']. That's soley for
+ efficiency's sake (in terms of battery life and
+ environment-internal storage-level effort).
+ */
+ const storageKeyPrefix = (
+ $storageHolder===$storage/*localStorage or sessionStorage*/
+ ? (
+ (NS.config ?
+ (NS.config.projectCode || NS.config.projectName
+ || NS.config.shortProjectName)
+ : false)
+ || window.location.pathname
+ )+'::' : (
+ '' /* transient storage */
+ )
+ );
+
+ /**
+ A proxy for localStorage or sessionStorage or a
+ page-instance-local proxy, if neither one is availble.
+
+ Which exact storage implementation is uses is unspecified, and
+ apps must not rely on it.
+ */
+ NS.storage = {
+ storageKeyPrefix: storageKeyPrefix,
+ /** Sets the storage key k to value v, implicitly converting
+ it to a string. */
+ set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
+ /** Sets storage key k to JSON.stringify(v). */
+ setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
+ /** Returns the value for the given storage key, or
+ dflt if the key is not found in the storage. */
+ get: (k,dflt)=>$storageHolder.hasOwnProperty(
+ storageKeyPrefix+k
+ ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
+ /** Returns true if the given key has a value of "true". If the
+ key is not found, it returns true if the boolean value of dflt
+ is "true". (Remember that JS persistent storage values are all
+ strings.) */
+ getBool: function(k,dflt){
+ return 'true'===this.get(k,''+(!!dflt));
+ },
+ /** Returns the JSON.parse()'d value of the given
+ storage key's value, or dflt is the key is not
+ found or JSON.parse() fails. */
+ getJSON: function f(k,dflt){
+ try {
+ const x = this.get(k,f);
+ return x===f ? dflt : JSON.parse(x);
+ }
+ catch(e){return dflt}
+ },
+ /** Returns true if the storage contains the given key,
+ else false. */
+ contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
+ /** Removes the given key from the storage. Returns this. */
+ remove: function(k){
+ $storage.removeItem(storageKeyPrefix+k);
+ return this;
+ },
+ /** Clears ALL keys from the storage. Returns this. */
+ clear: function(){
+ this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
+ return this;
+ },
+ /** Returns an array of all keys currently in the storage. */
+ keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
+ /** Returns true if this storage is transient (only available
+ until the page is reloaded), indicating that fileStorage
+ and sessionStorage are unavailable. */
+ isTransient: ()=>$storageHolder!==$storage,
+ /** Returns a symbolic name for the current storage mechanism. */
+ storageImplName: function(){
+ if($storage===window.localStorage) return 'localStorage';
+ else if($storage===window.sessionStorage) return 'sessionStorage';
+ else return 'transient';
+ },
+
+ /**
+ Returns a brief help text string for the currently-selected
+ storage type.
+ */
+ storageHelpDescription: function(){
+ return {
+ localStorage: "Browser-local persistent storage with an "+
+ "unspecified long-term lifetime (survives closing the browser, "+
+ "but maybe not a browser upgrade).",
+ sessionStorage: "Storage local to this browser tab, "+
+ "lost if this tab is closed.",
+ "transient": "Transient storage local to this invocation of this page."
+ }[this.storageImplName()];
+ }
+ };
+ return NS.storage;
+ })({})/*storage API setup*/;
+
+
+ /** Name of the stored copy of SqliteFiddle.config. */
+ const configStorageKey = 'sqlite3-fiddle-config';
+
+ /**
+ The SqliteFiddle object is intended to be the primary
+ app-level object for the main-thread side of the sqlite
+ fiddle application. It uses a worker thread to load the
+ sqlite WASM module and communicate with it.
+ */
+ const SF/*local convenience alias*/
+ = window.SqliteFiddle/*canonical name*/ = {
+ /* Config options. */
+ config: {
+ /* If true, SqliteFiddle.echo() will auto-scroll the
+ output widget to the bottom when it receives output,
+ else it won't. */
+ autoScrollOutput: true,
+ /* If true, the output area will be cleared before each
+ command is run, else it will not. */
+ autoClearOutput: false,
+ /* If true, SqliteFiddle.echo() will echo its output to
+ the console, in addition to its normal output widget.
+ That slows it down but is useful for testing. */
+ echoToConsole: false,
+ /* If true, display input/output areas side-by-side. */
+ sideBySide: false,
+ /* If true, swap positions of the input/output areas. */
+ swapInOut: false
+ },
+ /**
+ Emits the given text, followed by a line break, to the
+ output widget. If given more than one argument, they are
+ join()'d together with a space between each. As a special
+ case, if passed a single array, that array is used in place
+ of the arguments array (this is to facilitate receiving
+ lists of arguments via worker events).
+ */
+ echo: function f(text) {
+ /* Maintenance reminder: we currently require/expect a textarea
+ output element. It might be nice to extend this to behave
+ differently if the output element is a non-textarea element,
+ in which case it would need to append the given text as a TEXT
+ node and add a line break. */
+ if(!f._){
+ f._ = document.getElementById('output');
+ f._.value = ''; // clear browser cache
+ }
+ if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+ else if(1===arguments.length && Array.isArray(text)) text = text.join(' ');
+ // These replacements are necessary if you render to raw HTML
+ //text = text.replace(/&/g, "&");
+ //text = text.replace(//g, ">");
+ //text = text.replace('\n', ' ', 'g');
+ if(null===text){/*special case: clear output*/
+ f._.value = '';
+ return;
+ }else if(this.echo._clearPending){
+ delete this.echo._clearPending;
+ f._.value = '';
+ }
+ if(this.config.echoToConsole) console.log(text);
+ if(this.jqTerm) this.jqTerm.echo(text);
+ f._.value += text + "\n";
+ if(this.config.autoScrollOutput){
+ f._.scrollTop = f._.scrollHeight;
+ }
+ },
+ _msgMap: {},
+ /** Adds a worker message handler for messages of the given
+ type. */
+ addMsgHandler: function f(type,callback){
+ if(Array.isArray(type)){
+ type.forEach((t)=>this.addMsgHandler(t, callback));
+ return this;
+ }
+ (this._msgMap.hasOwnProperty(type)
+ ? this._msgMap[type]
+ : (this._msgMap[type] = [])).push(callback);
+ return this;
+ },
+ /** Given a worker message, runs all handlers for msg.type. */
+ runMsgHandlers: function(msg){
+ const list = (this._msgMap.hasOwnProperty(msg.type)
+ ? this._msgMap[msg.type] : false);
+ if(!list){
+ console.warn("No handlers found for message type:",msg);
+ return false;
+ }
+ //console.debug("runMsgHandlers",msg);
+ list.forEach((f)=>f(msg));
+ return true;
+ },
+ /** Removes all message handlers for the given message type. */
+ clearMsgHandlers: function(type){
+ delete this._msgMap[type];
+ return this;
+ },
+ /* Posts a message in the form {type, data} to the db worker. Returns this. */
+ wMsg: function(type,data){
+ this.worker.postMessage({type, data});
+ return this;
+ },
+ /**
+ Prompts for confirmation and, if accepted, deletes
+ all content and tables in the (transient) database.
+ */
+ resetDb: function(){
+ if(window.confirm("Really destroy all content and tables "
+ +"in the (transient) db?")){
+ this.wMsg('db-reset');
+ }
+ return this;
+ },
+ /** Stores this object's config in the browser's storage. */
+ storeConfig: function(){
+ storage.setJSON(configStorageKey,this.config);
+ }
+ };
+
+ if(1){ /* Restore SF.config */
+ const storedConfig = storage.getJSON(configStorageKey);
+ if(storedConfig){
+ /* Copy all properties to SF.config which are currently in
+ storedConfig. We don't bother copying any other
+ properties: those have been removed from the app in the
+ meantime. */
+ Object.keys(SF.config).forEach(function(k){
+ if(storedConfig.hasOwnProperty(k)){
+ SF.config[k] = storedConfig[k];
+ }
+ });
+ }
+ }
+
+ SF.worker = new Worker('fiddle-worker.js');
+ SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data);
+ SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data));
+
+ /* querySelectorAll() proxy */
+ const EAll = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelectorAll(arguments[arguments.length-1]);
+ };
+ /* querySelector() proxy */
+ const E = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelector(arguments[arguments.length-1]);
+ };
+
+ /** Handles status updates from the Module object. */
+ SF.addMsgHandler('module', function f(ev){
+ ev = ev.data;
+ if('status'!==ev.type){
+ console.warn("Unexpected module-type message:",ev);
+ return;
+ }
+ if(!f.ui){
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ const msg = ev.data;
+ if(f.ui.progres){
+ progress.value = msg.step;
+ progress.max = msg.step + 1/*we don't know how many steps to expect*/;
+ }
+ if(1==msg.step){
+ f.ui.progress.classList.remove('hidden');
+ f.ui.spinner.classList.remove('hidden');
+ }
+ if(msg.text){
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = msg.text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ /* The module can post messages about fatal problems,
+ e.g. an exit() being triggered or assertion failure,
+ after the last "load" message has arrived, so
+ leave f.ui.status and message listener intact. */
+ }
+ });
+
+ /**
+ The 'fiddle-ready' event is fired (with no payload) when the
+ wasm module has finished loading. Interestingly, that happens
+ _before_ the final module:status event */
+ SF.addMsgHandler('fiddle-ready', function(){
+ SF.clearMsgHandlers('fiddle-ready');
+ self.onSFLoaded();
+ });
+
+ /**
+ Performs all app initialization which must wait until after the
+ worker module is loaded. This function removes itself when it's
+ called.
+ */
+ self.onSFLoaded = function(){
+ delete this.onSFLoaded;
+ // Unhide all elements which start out hidden
+ EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
+ E('#btn-reset').addEventListener('click',()=>SF.resetDb());
+ const taInput = E('#input');
+ const btnClearIn = E('#btn-clear');
+ btnClearIn.addEventListener('click',function(){
+ taInput.value = '';
+ },false);
+ // Ctrl-enter and shift-enter both run the current SQL.
+ taInput.addEventListener('keydown',function(ev){
+ if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
+ ev.preventDefault();
+ ev.stopPropagation();
+ btnShellExec.click();
+ }
+ }, false);
+ const taOutput = E('#output');
+ const btnClearOut = E('#btn-clear-output');
+ btnClearOut.addEventListener('click',function(){
+ taOutput.value = '';
+ if(SF.jqTerm) SF.jqTerm.clear();
+ },false);
+ const btnShellExec = E('#btn-shell-exec');
+ btnShellExec.addEventListener('click',function(ev){
+ let sql;
+ ev.preventDefault();
+ if(taInput.selectionStart e.addEventListener('click', cmdClick, false)
+ );
+
+ btnInterrupt.addEventListener('click',function(){
+ SF.wMsg('interrupt');
+ });
+
+ /** Initiate a download of the db. */
+ const btnExport = E('#btn-export');
+ const eDisableDuringExport = [
+ /* UI elements to disable while export is running. Normally
+ the export is fast enough that this won't matter, but we
+ really don't want to be reading (from outside of sqlite)
+ the db when the user taps btnShellExec. */
+ btnShellExec, btnExport
+ ];
+ btnExport.addEventListener('click',function(){
+ eDisableDuringExport.forEach(e=>e.setAttribute('disabled','disabled'));
+ SF.wMsg('db-export');
+ });
+ SF.addMsgHandler('db-export', function(ev){
+ eDisableDuringExport.forEach(e=>e.removeAttribute('disabled'));
+ ev = ev.data;
+ if(ev.error){
+ SF.echo("Export failed:",ev.error);
+ return;
+ }
+ const blob = new Blob([ev.buffer], {type:"application/x-sqlite3"});
+ const a = document.createElement('a');
+ document.body.appendChild(a);
+ a.href = window.URL.createObjectURL(blob);
+ a.download = ev.filename;
+ a.addEventListener('click',function(){
+ setTimeout(function(){
+ SF.echo("Exported (possibly auto-downloaded):",ev.filename);
+ window.URL.revokeObjectURL(a.href);
+ a.remove();
+ },500);
+ });
+ a.click();
+ });
+ /**
+ Handle load/import of an external db file.
+ */
+ E('#load-db').addEventListener('change',function(){
+ const f = this.files[0];
+ const r = new FileReader();
+ const status = {loaded: 0, total: 0};
+ this.setAttribute('disabled','disabled');
+ r.addEventListener('loadstart', function(){
+ SF.echo("Loading",f.name,"...");
+ });
+ r.addEventListener('progress', function(ev){
+ SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes.");
+ });
+ const that = this;
+ r.addEventListener('load', function(){
+ that.removeAttribute('disabled');
+ SF.echo("Loaded",f.name+". Opening db...");
+ SF.wMsg('open',{
+ filename: f.name,
+ buffer: this.result
+ });
+ });
+ r.addEventListener('error',function(){
+ that.removeAttribute('disabled');
+ SF.echo("Loading",f.name,"failed for unknown reasons.");
+ });
+ r.addEventListener('abort',function(){
+ that.removeAttribute('disabled');
+ SF.echo("Cancelled loading of",f.name+".");
+ });
+ r.readAsArrayBuffer(f);
+ });
+
+ EAll('.fieldset.collapsible').forEach(function(fs){
+ const legend = E(fs,'span.legend'),
+ content = EAll(fs,':scope > div');
+ legend.addEventListener('click', function(){
+ fs.classList.toggle('collapsed');
+ content.forEach((d)=>d.classList.toggle('hidden'));
+ }, false);
+ });
+
+ /**
+ Given a DOM element, this routine measures its "effective
+ height", which is the bounding top/bottom range of this element
+ and all of its children, recursively. For some DOM structure
+ cases, a parent may have a reported height of 0 even though
+ children have non-0 sizes.
+
+ Returns 0 if !e or if the element really has no height.
+ */
+ const effectiveHeight = function f(e){
+ if(!e) return 0;
+ if(!f.measure){
+ f.measure = function callee(e, depth){
+ if(!e) return;
+ const m = e.getBoundingClientRect();
+ if(0===depth){
+ callee.top = m.top;
+ callee.bottom = m.bottom;
+ }else{
+ callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
+ callee.bottom = Math.max(callee.bottom, m.bottom);
+ }
+ Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
+ if(0===depth){
+ //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
+ f.extra += callee.bottom - callee.top;
+ }
+ return f.extra;
+ };
+ }
+ f.extra = 0;
+ f.measure(e,0);
+ return f.extra;
+ };
+
+ /**
+ Returns a function, that, as long as it continues to be invoked,
+ will not be triggered. The function will be called after it stops
+ being called for N milliseconds. If `immediate` is passed, call
+ the callback immediately and hinder future invocations until at
+ least the given time has passed.
+
+ If passed only 1 argument, or passed a falsy 2nd argument,
+ the default wait time set in this function's $defaultDelay
+ property is used.
+
+ Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
+ */
+ const debounce = function f(func, wait, immediate) {
+ var timeout;
+ if(!wait) wait = f.$defaultDelay;
+ return function() {
+ const context = this, args = Array.prototype.slice.call(arguments);
+ const later = function() {
+ timeout = undefined;
+ if(!immediate) func.apply(context, args);
+ };
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if(callNow) func.apply(context, args);
+ };
+ };
+ debounce.$defaultDelay = 500 /*arbitrary*/;
+
+ const ForceResizeKludge = (function(){
+ /* Workaround for Safari mayhem regarding use of vh CSS
+ units.... We cannot use vh units to set the main view
+ size because Safari chokes on that, so we calculate
+ that height here. Larger than ~95% is too big for
+ Firefox on Android, causing the input area to move
+ off-screen. */
+ const appViews = EAll('.app-view');
+ const elemsToCount = [
+ /* Elements which we need to always count in the
+ visible body size. */
+ E('body > header'),
+ E('body > footer')
+ ];
+ const resized = function f(){
+ if(f.$disabled) return;
+ const wh = window.innerHeight;
+ var ht;
+ var extra = 0;
+ elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false);
+ ht = wh - extra;
+ appViews.forEach(function(e){
+ e.style.height =
+ e.style.maxHeight = [
+ "calc(", (ht>=100 ? ht : 100), "px",
+ " - 2em"/*fudge value*/,")"
+ /* ^^^^ hypothetically not needed, but both
+ Chrome/FF on Linux will force scrollbars on the
+ body if this value is too small. */
+ ].join('');
+ });
+ };
+ resized.$disabled = true/*gets deleted when setup is finished*/;
+ window.addEventListener('resize', debounce(resized, 250), false);
+ return resized;
+ })();
+
+ /** Set up a selection list of examples */
+ (function(){
+ const xElem = E('#select-examples');
+ const examples = [
+ {name: "Timer on", sql: ".timer on"},
+ {name: "Setup table T", sql:`.nullvalue NULL
+CREATE TABLE t(a,b);
+INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);
+SELECT * FROM t;`},
+ {name: "Table list", sql: ".tables"},
+ {name: "Box Mode", sql: ".mode box"},
+ {name: "JSON Mode", sql: ".mode json"},
+ {name: "Mandlebrot", sql: `WITH RECURSIVE
+ xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),
+ yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),
+ m(iter, cx, cy, x, y) AS (
+ SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis
+ UNION ALL
+ SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m
+ WHERE (x*x + y*y) < 4.0 AND iter<28
+ ),
+ m2(iter, cx, cy) AS (
+ SELECT max(iter), cx, cy FROM m GROUP BY cx, cy
+ ),
+ a(t) AS (
+ SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '')
+ FROM m2 GROUP BY cy
+ )
+SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;`}
+ ];
+ const newOpt = function(lbl,val){
+ const o = document.createElement('option');
+ o.value = val;
+ if(!val) o.setAttribute('disabled',true);
+ o.appendChild(document.createTextNode(lbl));
+ xElem.appendChild(o);
+ };
+ newOpt("Examples (replaces input!)");
+ examples.forEach((o)=>newOpt(o.name, o.sql));
+ //xElem.setAttribute('disabled',true);
+ xElem.selectedIndex = 0;
+ xElem.addEventListener('change', function(){
+ taInput.value = '-- ' +
+ this.selectedOptions[0].innerText +
+ '\n' + this.value;
+ SF.dbExec(this.value);
+ });
+ })()/* example queries */;
+
+ SF.echo(null/*clear any output generated by the init process*/);
+ if(window.jQuery && window.jQuery.terminal){
+ /* Set up the terminal-style view... */
+ const eTerm = window.jQuery('#view-terminal').empty();
+ SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{
+ prompt: 'sqlite> ',
+ greetings: false /* note that the docs incorrectly call this 'greeting' */
+ });
+ /* Set up a button to toggle the views... */
+ const head = E('header#titlebar');
+ const btnToggleView = document.createElement('button');
+ btnToggleView.appendChild(document.createTextNode("Toggle View"));
+ head.appendChild(btnToggleView);
+ btnToggleView.addEventListener('click',function f(){
+ EAll('.app-view').forEach(e=>e.classList.toggle('hidden'));
+ if(document.body.classList.toggle('terminal-mode')){
+ ForceResizeKludge();
+ }
+ }, false);
+ btnToggleView.click()/*default to terminal view*/;
+ }
+ SF.dbExec(null/*init the db and output the header*/);
+ SF.echo('This experimental app is provided in the hope that it',
+ 'may prove interesting or useful but is not an officially',
+ 'supported deliverable of the sqlite project. It is subject to',
+ 'any number of changes or outright removal at any time.\n');
+ delete ForceResizeKludge.$disabled;
+ ForceResizeKludge();
+ }/*onSFLoaded()*/;
+})();
diff --git a/ext/fiddle/index.md b/ext/fiddle/index.md
new file mode 100644
index 0000000000..9d1f8d83ea
--- /dev/null
+++ b/ext/fiddle/index.md
@@ -0,0 +1,94 @@
+This directory houses a "fiddle"-style application which embeds a
+[Web Assembly (WASM)](https://en.wikipedia.org/wiki/WebAssembly)
+build of the sqlite3 shell app into an HTML page, effectively running
+the shell in a client-side browser.
+
+It requires [emscripten][] and that the build environment be set up for
+emscripten. A mini-HOWTO for setting that up follows...
+
+First, install the Emscripten SDK, as documented
+[here](https://emscripten.org/docs/getting_started/downloads.html) and summarized
+below for Linux environments:
+
+```
+# Clone the emscripten repository:
+$ git clone https://github.com/emscripten-core/emsdk.git
+$ cd emsdk
+
+# Download and install the latest SDK tools:
+$ ./emsdk install latest
+
+# Make the "latest" SDK "active" for the current user:
+$ ./emsdk activate latest
+```
+
+Those parts only need to be run once. The following needs to be run for each
+shell instance which needs the `emcc` compiler:
+
+```
+# Activate PATH and other environment variables in the current terminal:
+$ source ./emsdk_env.sh
+
+$ which emcc
+/path/to/emsdk/upstream/emscripten/emcc
+```
+
+That `env` script needs to be sourced for building this application from the
+top of the sqlite3 build tree:
+
+```
+$ make fiddle
+```
+
+Or:
+
+```
+$ cd ext/fiddle
+$ make
+```
+
+That will generate the fiddle application under
+[ext/fiddle](/dir/ext/fiddle), as `fiddle.html`. That application
+cannot, due to XMLHttpRequest security limitations, run if the HTML
+file is opened directly in the browser (i.e. if it is opened using a
+`file://` URL), so it needs to be served via an HTTP server. For
+example, using [althttpd][]:
+
+```
+$ cd ext/fiddle
+$ althttpd -debug 1 -jail 0 -port 9090 -root .
+```
+
+Then browse to `http://localhost:9090/fiddle.html`.
+
+Note that when serving this app via [althttpd][], it must be a version
+from 2022-05-17 or newer so that it recognizes the `.wasm` file
+extension and responds with the mimetype `application/wasm`, as the
+WASM loader is pedantic about that detail.
+
+# Known Quirks and Limitations
+
+Some "impedence mismatch" between C and WASM/JavaScript is to be
+expected.
+
+## No I/O
+
+sqlite3 shell commands which require file I/O or pipes are disabled in
+the WASM build.
+
+## `exit()` Triggered from C
+
+When C code calls `exit()`, as happens (for example) when running an
+"unsafe" command when safe mode is active, WASM's connection to the
+sqlite3 shell environment has no sensible choice but to shut down
+because `exit()` leaves it in a state we can no longer recover
+from. The JavaScript-side application attempts to recognize this and
+warn the user that restarting the application is necessary. Currently
+the only way to restart it is to reload the page. Restructuring the
+shell code such that it could be "rebooted" without restarting the
+JS app would require some invasive changes which are not currently
+on any TODO list but have not been entirely ruled out long-term.
+
+
+[emscripten]: https://emscripten.org
+[althttpd]: https://sqlite.org/althttpd
diff --git a/ext/fiddle/sqlite3-api.js b/ext/fiddle/sqlite3-api.js
new file mode 100644
index 0000000000..d88ec9a8b8
--- /dev/null
+++ b/ext/fiddle/sqlite3-api.js
@@ -0,0 +1,1396 @@
+/*
+ 2022-05-22
+
+ 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 is intended to be appended to the emcc-generated
+ sqlite3.js via emcc:
+
+ emcc ... -sMODULARIZE -sEXPORT_NAME=initSqlite3Module --post-js=THIS_FILE
+
+ It is loaded by importing the emcc-generated sqlite3.js, then:
+
+ initSqlite3Module({module object}).then(
+ function(theModule){
+ theModule.sqlite3 == an object containing this file's
+ deliverables:
+ {
+ api: bindings for much of the core sqlite3 APIs,
+ SQLite3: high-level OO API wrapper
+ }
+ });
+
+ It is up to the caller to provide a module object compatible with
+ emcc, but it can be a plain empty object. The object passed to
+ initSqlite3Module() will get populated by the emscripten-generated
+ bits and, in part, by the code from this file. Specifically, this file
+ installs the `theModule.sqlite3` part shown above.
+
+ The resulting sqlite3.api object wraps the standard sqlite3 C API in
+ a way as close to its native form as JS allows for. The
+ sqlite3.SQLite3 object provides a higher-level wrapper more
+ appropriate for general client-side use in JS.
+
+ Because using certain parts of the low-level API properly requires
+ some degree of WASM-related magic, it is not recommended that that
+ API be used as-is in client-level code. Rather, client code should
+ use the higher-level OO API or write a custom wrapper on top of the
+ lower-level API. In short, most of the C-style API is used in an
+ intuitive manner from JS but any C-style APIs which take
+ pointers-to-pointer arguments require WASM-specific interfaces
+ installed by emcscripten-generated code. Those which take or return
+ only integers, doubles, strings, or "plain" pointers to db or
+ statement objects can be used in "as normal," noting that "pointers"
+ in wasm are simply 32-bit integers.
+
+ # 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. In fact, provide at least 3...
+
+ - (1) The aforementioned C-style API. (2) An OO-style API on
+ top of that, designed to run in the same thread (main window or
+ Web Worker) as the C API. (3) A less-capable wrapper which can
+ work across the main window/worker boundary, where the sqlite3 API
+ is one of those and this wrapper is in the other. That
+ constellation places some considerable limitations on how the API
+ can be interacted with, but keeping the DB operations out of the
+ UI thread is generally desirable.
+
+ - Insofar as possible, support client-side storage using JS
+ filesystem APIs. As of this writing, such things are still very
+ much TODO.
+
+ Non-goals:
+
+ - As WASM is a web-centric technology and UTF-8 is the King of
+ Encodings in that realm, there are no current plans to support the
+ UTF16-related APIs. They would add a complication to the bindings
+ for no appreciable benefit.
+
+ - Supporting old or niche-market platforms. WASM is built for a
+ modern web and requires modern platforms.
+
+*/
+if(!Module.postRun) Module.postRun = [];
+/* ^^^^ the name Module is, in this setup, scope-local in the generated
+ file sqlite3.js, with which this file gets combined at build-time. */
+Module.postRun.push(function(namespace){
+ 'use strict';
+ /* For reference: sql.js does essentially everything we want and
+ it solves much of the wasm-related voodoo, but we'll need a
+ different structure because we want the db connection to run in
+ a worker thread and feed data back into the main
+ thread. Regardless of those differences, it makes a great point
+ of reference:
+
+ https://github.com/sql-js/sql.js
+
+ Some of the specific design goals here:
+
+ - Bind a low-level sqlite3 API which is close to the native one
+ in terms of usage.
+
+ - Create a higher-level one, more akin to sql.js and
+ node.js-style implementations. This one would speak directly
+ to the low-level API. This API could be used by clients who
+ import the low-level API directly into their main thread
+ (which we don't want to recommend but also don't want to
+ outright forbid).
+
+ - Create a second higher-level one which speaks to the
+ low-level API via worker messages. This one would be intended
+ for use in the main thread, talking to the low-level UI via
+ worker messages. Because workers have only a single message
+ channel, some acrobatics will be needed here to feed async
+ work results back into client-side callbacks (as those
+ callbacks cannot simply be passed to the worker). Exactly
+ what those acrobatics should look like is not yet entirely
+ clear and much experimentation is pending.
+ */
+
+ const SQM = namespace/*the sqlite module object */;
+
+ /**
+ Set up the main sqlite3 binding API here, mimicking the C API as
+ closely as we can.
+
+ Attribution: though not a direct copy/paste, much of what
+ follows is strongly influenced by the sql.js implementation.
+ */
+ const api = {
+ /* It is important that the following integer values match
+ those from the C code. Ideally we could fetch them from the
+ C API, e.g., in the form of a JSON object, but getting that
+ JSON string constructed within our current confines is
+ currently not worth the effort.
+
+ Reminder to self: we could probably do so by adding the
+ proverbial level of indirection, calling in to C to get it,
+ and having that C func call an
+ emscripten-installed/JS-implemented library function which
+ builds the result object:
+
+ const obj = {};
+ sqlite3__get_enum(function(key,val){
+ obj[key] = val;
+ });
+
+ but whether or not we can pass a function that way, via a
+ (void*) is as yet unknown.
+ */
+ /* Minimum subset of sqlite result codes we'll need. */
+ SQLITE_OK: 0,
+ SQLITE_ROW: 100,
+ SQLITE_DONE: 101,
+ /* sqlite data types */
+ SQLITE_INTEGER: 1,
+ SQLITE_FLOAT: 2,
+ SQLITE_TEXT: 3,
+ SQLITE_BLOB: 4,
+ SQLITE_NULL: 5,
+ /* create_function() flags */
+ SQLITE_DETERMINISTIC: 0x000000800,
+ SQLITE_DIRECTONLY: 0x000080000,
+ SQLITE_INNOCUOUS: 0x000200000,
+ /* sqlite encodings, used for creating UDFs, noting that we
+ will only support UTF8. */
+ SQLITE_UTF8: 1
+ };
+ const cwrap = SQM.cwrap;
+ [/* C-side functions to bind. Each entry is an array with 3 or 4
+ elements:
+
+ ["c-side name",
+ "result type" (cwrap() syntax),
+ [arg types in cwrap() syntax]
+ ]
+
+ If it has 4 elements, the first one is an alternate name to
+ use for the JS-side binding. That's required when overloading
+ a binding for two different uses.
+ */
+ ["sqlite3_bind_blob","number",["number", "number", "number", "number", "number"]],
+ ["sqlite3_bind_double","number",["number", "number", "number"]],
+ ["sqlite3_bind_int","number",["number", "number", "number"]],
+ /*Noting that JS/wasm combo does not currently support 64-bit integers:
+ ["sqlite3_bind_int64","number",["number", "number", "number"]],*/
+ ["sqlite3_bind_null","void",["number"]],
+ ["sqlite3_bind_parameter_count", "number", ["number"]],
+ ["sqlite3_bind_parameter_index","number",["number", "string"]],
+ ["sqlite3_bind_text","number",["number", "number", "number", "number", "number"]],
+ ["sqlite3_changes", "number", ["number"]],
+ ["sqlite3_clear_bindings","number",["number"]],
+ ["sqlite3_close_v2", "number", ["number"]],
+ ["sqlite3_column_blob","number", ["number", "number"]],
+ ["sqlite3_column_bytes","number",["number", "number"]],
+ ["sqlite3_column_count", "number", ["number"]],
+ ["sqlite3_column_count","number",["number"]],
+ ["sqlite3_column_double","number",["number", "number"]],
+ ["sqlite3_column_int","number",["number", "number"]],
+ /*Noting that JS/wasm combo does not currently support 64-bit integers:
+ ["sqlite3_column_int64","number",["number", "number"]],*/
+ ["sqlite3_column_name","string",["number", "number"]],
+ ["sqlite3_column_text","string",["number", "number"]],
+ ["sqlite3_column_type","number",["number", "number"]],
+ ["sqlite3_compileoption_get", "string", ["number"]],
+ ["sqlite3_compileoption_used", "number", ["string"]],
+ ["sqlite3_create_function_v2", "number",
+ ["number", "string", "number", "number","number",
+ "number", "number", "number", "number"]],
+ ["sqlite3_data_count", "number", ["number"]],
+ ["sqlite3_db_filename", "string", ["number", "string"]],
+ ["sqlite3_errmsg", "string", ["number"]],
+ ["sqlite3_exec", "number", ["number", "string", "number", "number", "number"]],
+ ["sqlite3_finalize", "number", ["number"]],
+ ["sqlite3_interrupt", "void", ["number"]],
+ ["sqlite3_libversion", "string", []],
+ ["sqlite3_open", "number", ["string", "number"]],
+ //["sqlite3_open_v2", "number", ["string", "number", "number", "string"]],
+ //^^^^ TODO: add the flags needed for the 3rd arg
+ ["sqlite3_prepare_v2", "number", ["number", "string", "number", "number", "number"]],
+ ["sqlite3_prepare_v2_sqlptr", "sqlite3_prepare_v2",
+ /* Impl which requires that the 2nd argument be a pointer to
+ the SQL, instead of a string. This is used for cases where
+ we require a non-NULL value for the final argument. We may
+ or may not need this, depending on how our higher-level
+ API shapes up, but this code's spiritual guide (sql.js)
+ uses it we we'll include it. */
+ "number", ["number", "number", "number", "number", "number"]],
+ ["sqlite3_reset", "number", ["number"]],
+ ["sqlite3_result_blob",null,["number", "number", "number", "number"]],
+ ["sqlite3_result_double",null,["number", "number"]],
+ ["sqlite3_result_error",null,["number", "string", "number"]],
+ ["sqlite3_result_int",null,["number", "number"]],
+ ["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_normalized_sql", "string", ["number"]]
+ ].forEach(function(a){
+ const k = (4==a.length) ? a.shift() : a[0];
+ api[k] = cwrap.apply(this, a);
+ });
+
+ /* 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
+ 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-to-pointer-passing routines. */
+ const pPtrArg = stackAlloc(4);
+ /** Throws a new error, concatenating all args with a space between
+ each. */
+ const toss = function(){
+ throw new Error(Array.prototype.join.call(arguments, ' '));
+ };
+
+ /**
+ The DB class wraps a sqlite3 db handle.
+
+ It accepts the following argument signatures:
+
+ - ()
+ - (undefined) (same effect as ())
+ - (Uint8Array holding an sqlite3 db image)
+
+ It always generates a random filename and sets is to
+ the `filename` property of this object.
+
+ Developer's note: the reason it does not (any longer) support
+ ":memory:" as a name is because we can apparently only export
+ images of DBs which are stored in the pseudo-filesystem
+ provided by the JS APIs. Since exporting and importing images
+ is an important usability feature for this class, ":memory:"
+ DBs are not supported (until/unless we can find a way to export
+ those as well). The naming semantics will certainly evolve as
+ this API does.
+ */
+ const DB = function(arg){
+ const fn = "db-"+((Math.random() * 10000000) | 0)+
+ "-"+((Math.random() * 10000000) | 0)+".sqlite3";
+ let buffer;
+ if(name instanceof Uint8Array){
+ buffer = arg;
+ arg = undefined;
+ }else if(arguments.length && undefined!==arg){
+ toss("Invalid arguments to DB constructor.",
+ "Expecting no args, undefined, or a",
+ "sqlite3 file as a Uint8Array.");
+ }
+ if(buffer){
+ FS.createDataFile("/", fn, buffer, true, true);
+ }
+ setValue(pPtrArg, 0, "i32");
+ this.checkRc(api.sqlite3_open(fn, pPtrArg));
+ this._pDb = getValue(pPtrArg, "i32");
+ this.filename = fn;
+ this._statements = {/*map of open Stmt _pointers_ to Stmt*/};
+ this._udfs = {/*map of UDF names to wasm function _pointers_*/};
+ };
+
+ /**
+ Internal-use enum for mapping JS types to DB-bindable types.
+ These do not (and need not) line up with the SQLITE_type
+ values. All values in this enum must be truthy and distinct
+ but they need not be numbers.
+ */
+ const BindTypes = {
+ null: 1,
+ number: 2,
+ string: 3,
+ boolean: 4,
+ blob: 5
+ };
+ BindTypes['undefined'] == BindTypes.null;
+
+ /**
+ This class wraps sqlite3_stmt. Calling this constructor
+ directly will trigger an exception. Use DB.prepare() to create
+ new instances.
+ */
+ const Stmt = function(){
+ if(BindTypes!==arguments[2]){
+ toss("Do not call the Stmt constructor directly. Use DB.prepare().");
+ }
+ this.db = arguments[0];
+ this._pStmt = arguments[1];
+ this.columnCount = api.sqlite3_column_count(this._pStmt);
+ this.parameterCount = api.sqlite3_bind_parameter_count(this._pStmt);
+ this._allocs = [/*list of alloc'd memory blocks for bind() values*/]
+ };
+
+ /** Throws if the given DB has been closed, else it is returned. */
+ const affirmDbOpen = function(db){
+ if(!db._pDb) toss("DB has been closed.");
+ return db;
+ };
+
+ /** Returns true if n is a 32-bit (signed) integer,
+ else false. */
+ const isInt32 = function(n){
+ return (n===n|0 && n<0xFFFFFFFF) ? true : undefined;
+ };
+
+ /**
+ 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;
+ };
+
+ /** If object opts has _its own_ property named p then that
+ property's value is returned, else dflt is returned. */
+ const getOwnOption = (opts, p, dflt)=>
+ opts.hasOwnProperty(p) ? opts[p] : dflt;
+
+ 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. 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("sqlite result code",sqliteResultCode+":",
+ api.sqlite3_errmsg(this._pDb) || "Unknown db error.");
+ },
+ /**
+ Finalizes all open statements and closes this database
+ connection. This is a no-op if the db has already been
+ closed.
+ */
+ close: function(){
+ if(this._pDb){
+ let s;
+ const that = this;
+ Object.keys(this._statements).forEach(function(k,s){
+ delete that._statements[k];
+ if(s && s._pStmt) s.finalize();
+ });
+ Object.values(this._udfs).forEach(SQM.removeFunction);
+ delete this._udfs;
+ delete this._statements;
+ delete this.filename;
+ api.sqlite3_close_v2(this._pDb);
+ delete this._pDb;
+ }
+ },
+ /**
+ Similar to this.filename but will return NULL for
+ special names like ":memory:". Not of much use until
+ we have filesystem support. Throws if the DB has
+ been closed. If passed an argument it then it will return
+ the filename of the ATTACHEd db with that name, else it assumes
+ a name of `main`.
+ */
+ fileName: function(dbName){
+ return api.sqlite3_db_filename(affirmDbOpen(this)._pDb, dbName||"main");
+ },
+ /**
+ Compiles the given SQL and returns a prepared Stmt. This is
+ the only way to create new Stmt objects. Throws on error.
+ */
+ prepare: function(sql){
+ affirmDbOpen(this);
+ setValue(pPtrArg,0,"i32");
+ this.checkRc(api.sqlite3_prepare_v2(this._pDb, sql, -1, pPtrArg, null));
+ const pStmt = getValue(pPtrArg, "i32");
+ if(!pStmt) toss("Empty SQL is not permitted.");
+ 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.opt.multi){
+ return this.execMulti(arg, undefined, BindTypes);
+ }
+ const opt = arg.opt;
+ let stmt;
+ try {
+ stmt = this.prepare(arg.sql);
+ if(opt.bind) stmt.bind(opt.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 = SQM.allocateUTF8OnStack(arg.sql)
+ const pzTail = stackAlloc(4);
+ while(getValue(pSql, "i8")){
+ setValue(pPtrArg, 0, "i32");
+ setValue(pzTail, 0, "i32");
+ this.checkRc(api.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(api.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()*/,
+ /**
+ Creates a new scalar UDF (User-Defined Function) which is
+ accessible via SQL code. This function may be called in any
+ of the following forms:
+
+ - (name, function)
+ - (name, function, optionsObject)
+ - (name, optionsObject)
+ - (optionsObject)
+
+ In the final two cases, the function must be defined as the
+ 'callback' property of the options object. In the final
+ case, the function's name must be the 'name' property.
+
+ This can only be used to create scalar functions, not
+ aggregate or window functions. UDFs cannot be removed from
+ a DB handle after they're added.
+
+ On success, returns this object. Throws on error.
+
+ When called from SQL, arguments to the UDF, and its result,
+ will be converted between JS and SQL with as much fidelity
+ as is feasible, triggering an exception if a type
+ conversion cannot be determined. Some freedom is afforded
+ to numeric conversions due to friction between the JS and C
+ worlds: integers which are larger than 32 bits will be
+ treated as doubles, as JS does not support 64-bit integers
+ and it is (as of this writing) illegal to use WASM
+ functions which take or return 64-bit integers from JS.
+
+ The optional options object may contain flags to modify how
+ the function is defined:
+
+ - .arity: the number of arguments which SQL calls to this
+ function expect or require. The default value is the
+ callback's length property (i.e. the number of declared
+ parameters it has). A value of -1 means that the function
+ is variadic and may accept any number of arguments, up to
+ sqlite3's compile-time limits. sqlite3 will enforce the
+ argument count if is zero or greater.
+
+ The following properties correspond to flags documented at:
+
+ https://sqlite.org/c3ref/create_function.html
+
+ - .deterministic = SQLITE_DETERMINISTIC
+ - .directOnly = SQLITE_DIRECTONLY
+ - .innocuous = SQLITE_INNOCUOUS
+
+
+ Maintenance reminder: the ability to add new
+ WASM-accessible functions to the runtime requires that the
+ WASM build is compiled with emcc's `-sALLOW_TABLE_GROWTH`
+ flag.
+ */
+ createFunction: function f(name, callback,opt){
+ switch(arguments.length){
+ case 1: /* (optionsObject) */
+ opt = name;
+ name = opt.name;
+ callback = opt.callback;
+ break;
+ case 2: /* (name, callback|optionsObject) */
+ if(!(callback instanceof Function)){
+ opt = callback;
+ callback = opt.callback;
+ }
+ break;
+ default: break;
+ }
+ if(!opt) opt = {};
+ if(!(callback instanceof Function)){
+ toss("Invalid arguments: expecting a callback function.");
+ }else if('string' !== typeof name){
+ toss("Invalid arguments: missing function name.");
+ }
+ if(!f._extractArgs){
+ /* Static init */
+ f._extractArgs = function(argc, pArgv){
+ let i, pVal, valType, arg;
+ const tgt = [];
+ for(i = 0; i < argc; ++i){
+ pVal = getValue(pArgv + (4 * i), "i32");
+ valType = api.sqlite3_value_type(pVal);
+ switch(valType){
+ case api.SQLITE_INTEGER:
+ case api.SQLITE_FLOAT:
+ arg = api.sqlite3_value_double(pVal);
+ break;
+ case SQLITE_TEXT:
+ arg = api.sqlite3_value_text(pVal);
+ break;
+ case SQLITE_BLOB:{
+ const n = api.sqlite3_value_bytes(ptr);
+ const pBlob = api.sqlite3_value_blob(ptr);
+ arg = new Uint8Array(n);
+ let i;
+ for(i = 0; i < n; ++i) arg[i] = HEAP8[pBlob+i];
+ break;
+ }
+ default:
+ arg = null; break;
+ }
+ tgt.push(arg);
+ }
+ return tgt;
+ }/*_extractArgs()*/;
+ f._setResult = function(pCx, val){
+ switch(typeof val) {
+ case 'boolean':
+ api.sqlite3_result_int(pCx, val ? 1 : 0);
+ break;
+ case 'number': {
+ (isInt32(val)
+ ? api.sqlite3_result_int
+ : api.sqlite3_result_double)(pCx, val);
+ break;
+ }
+ case 'string':
+ api.sqlite3_result_text(pCx, val, -1,
+ -1/*==SQLITE_TRANSIENT*/);
+ break;
+ case 'object':
+ if(null===val) {
+ api.sqlite3_result_null(pCx);
+ break;
+ }else if(undefined!==val.length){
+ const pBlob =
+ SQM.allocate(val, SQM.ALLOC_NORMAL);
+ api.sqlite3_result_blob(pCx, pBlob, val.length, -1/*==SQLITE_TRANSIENT*/);
+ SQM._free(blobptr);
+ break;
+ }
+ // else fall through
+ default:
+ toss("Don't not how to handle this UDF result value:",val);
+ };
+ }/*_setResult()*/;
+ }/*static init*/
+ const wrapper = function(pCx, argc, pArgv){
+ try{
+ f._setResult(pCx, callback.apply(null, f._extractArgs(argc, pArgv)));
+ }catch(e){
+ api.sqlite3_result_error(pCx, e.message, -1);
+ }
+ };
+ const pUdf = SQM.addFunction(wrapper, "viii");
+ let fFlags = 0;
+ if(getOwnOption(opt, 'deterministic')) fFlags |= api.SQLITE_DETERMINISTIC;
+ if(getOwnOption(opt, 'directOnly')) fFlags |= api.SQLITE_DIRECTONLY;
+ if(getOwnOption(opt, 'innocuous')) fFlags |= api.SQLITE_INNOCUOUS;
+ name = name.toLowerCase();
+ try {
+ this.checkRc(api.sqlite3_create_function_v2(
+ this._pDb, name,
+ (opt.hasOwnProperty('arity') ? +opt.arity : callback.length),
+ api.SQLITE_UTF8 | fFlags, null/*pApp*/, pUdf,
+ null/*xStep*/, null/*xFinal*/, null/*xDestroy*/));
+ }catch(e){
+ SQM.removeFunction(pUdf);
+ throw e;
+ }
+ if(this._udfs.hasOwnProperty(name)){
+ SQM.removeFunction(this._udfs[name]);
+ }
+ this._udfs[name] = pUdf;
+ return this;
+ }/*createFunction()*/,
+ /**
+ Prepares the given SQL, step()s it one time, and returns
+ the value of the first result column. If it has no results,
+ undefined is returned. If passed a second argument, it is
+ treated like an argument to Stmt.bind(), so may be any type
+ supported by that function. Throws on error (e.g. malformed
+ SQL).
+ */
+ selectValue: function(sql,bind){
+ let stmt, rc;
+ try {
+ stmt = this.prepare(sql).bind(bind);
+ if(stmt.step()) rc = stmt.get(0);
+ }finally{
+ if(stmt) stmt.finalize();
+ }
+ return rc;
+ },
+
+ /**
+ Exports a copy of this db's file as a Uint8Array and
+ returns it. It is technically not legal to call this while
+ any prepared statement are currently active. Throws if this
+ db is not open.
+
+ Maintenance reminder: the corresponding sql.js impl of this
+ feature closes the current db, finalizing any active
+ statements and (seemingly unnecessarily) destroys any UDFs,
+ copies the file, and then re-opens it (without restoring
+ the UDFs). Those gymnastics are not necessary on the tested
+ platform but might be necessary on others. Because of that
+ eventuality, this interface currently enforces that no
+ statements are active when this is run. It will throw if
+ any are.
+ */
+ exportBinaryImage: function(){
+ affirmDbOpen(this);
+ if(Object.keys(this._statements).length){
+ toss("Cannot export with prepared statements active!",
+ "finalize() all statements and try again.");
+ }
+ const img = FS.readFile(this.filename, {encoding:"binary"});
+ return img;
+ }
+ }/*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
+ enum if v's type is a valid bindable type, else
+ returns a falsy value. As a special case, a value of
+ undefined is treated as a bind type of null. */
+ const isSupportedBindType = function(v){
+ let t = BindTypes[(null===v||undefined===v) ? 'null' : typeof v];
+ switch(t){
+ case BindTypes.boolean:
+ case BindTypes.null:
+ case BindTypes.number:
+ case BindTypes.string:
+ return t;
+ default:
+ if(v instanceof Uint8Array) return BindTypes.blob;
+ return undefined;
+ }
+ };
+
+ /**
+ If isSupportedBindType(v) returns a truthy value, this
+ function returns that value, else it throws.
+ */
+ const affirmSupportedBindType = function(v){
+ return isSupportedBindType(v) || toss("Unsupport bind() argument type.");
+ };
+
+ /**
+ If key is a number and within range of stmt's bound parameter
+ count, key is returned.
+
+ If key is not a number then it is checked against named
+ parameters. If a match is found, its index is returned.
+
+ Else it throws.
+ */
+ const affirmParamIndex = function(stmt,key){
+ const n = ('number'===typeof key)
+ ? key : api.sqlite3_bind_parameter_index(stmt._pStmt, key);
+ 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.");
+ return n;
+ };
+
+ /** Throws if ndx is not an integer or if it is out of range
+ 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.");
+ }
+ 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
+ the BindTypes enum) and value. Throws on error. Returns stmt on
+ success.
+ */
+ const bindOne = function f(stmt,ndx,bindType,val){
+ affirmUnlocked(stmt, 'bind()');
+ if(!f._){
+ f._ = {
+ string: function(stmt, ndx, val, asBlob){
+ const bytes = intArrayFromString(val,true);
+ const pStr = SQM.allocate(bytes, ALLOC_NORMAL);
+ stmt._allocs.push(pStr);
+ const func = asBlob ? api.sqlite3_bind_blob : api.sqlite3_bind_text;
+ return func(stmt._pStmt, ndx, pStr, bytes.length, 0);
+ }
+ };
+ }
+ affirmSupportedBindType(val);
+ ndx = affirmParamIndex(stmt,ndx);
+ let rc = 0;
+ switch((null===val || undefined===val) ? BindTypes.null : bindType){
+ case BindTypes.null:
+ rc = api.sqlite3_bind_null(stmt._pStmt, ndx);
+ break;
+ case BindTypes.string:{
+ rc = f._.string(stmt, ndx, val, false);
+ break;
+ }
+ case BindTypes.number: {
+ const m = (isInt32(val)
+ ? api.sqlite3_bind_int
+ /*It's illegal to bind a 64-bit int
+ from here*/
+ : api.sqlite3_bind_double);
+ rc = m(stmt._pStmt, ndx, val);
+ break;
+ }
+ case BindTypes.boolean:
+ rc = api.sqlite3_bind_int(stmt._pStmt, ndx, val ? 1 : 0);
+ break;
+ case BindTypes.blob: {
+ if('string'===typeof val){
+ rc = f._.string(stmt, ndx, val, true);
+ }else{
+ const len = val.length;
+ if(undefined===len){
+ toss("Binding a value as a blob requires",
+ "that it have a length member.");
+ }
+ const pBlob = SQM.allocate(val, ALLOC_NORMAL);
+ stmt._allocs.push(pBlob);
+ rc = api.sqlite3_bind_blob(stmt._pStmt, ndx, pBlob, len, 0);
+ }
+ }
+ default: toss("Unsupported bind() argument type.");
+ }
+ if(rc) stmt.db.checkRc(rc);
+ return stmt;
+ };
+
+ /** Frees any memory explicitly allocated for the given
+ Stmt object. Returns stmt. */
+ const freeBindMemory = function(stmt){
+ let m;
+ while(undefined !== (m = stmt._allocs.pop())){
+ SQM._free(m);
+ }
+ return stmt;
+ };
+
+ Stmt.prototype = {
+ /**
+ "Finalizes" this statement. This is a no-op if the
+ statement has already been finalizes. Returns
+ undefined. Most methods in this class will throw if called
+ after this is.
+ */
+ finalize: function(){
+ if(this._pStmt){
+ affirmUnlocked(this,'finalize()');
+ freeBindMemory(this);
+ delete this.db._statements[this._pStmt];
+ api.sqlite3_finalize(this._pStmt);
+ delete this.columnCount;
+ 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(
+ affirmUnlocked(affirmStmtOpen(this), 'clearBindings()')
+ );
+ api.sqlite3_clear_bindings(this._pStmt);
+ this._mayGet = false;
+ return this;
+ },
+ /**
+ Resets this statement so that it may be step()ed again
+ from the beginning. Returns this object. Throws if this
+ statement has been finalized.
+
+ If passed a truthy argument then this.clearBindings() is
+ also called, otherwise any existing bindings, along with
+ any memory allocated for them, are retained.
+ */
+ reset: function(alsoClearBinds){
+ affirmUnlocked(this,'reset()');
+ if(alsoClearBinds) this.clearBindings();
+ api.sqlite3_reset(affirmStmtOpen(this)._pStmt);
+ this._mayGet = false;
+ return this;
+ },
+ /**
+ Binds one or more values to its bindable parameters. It
+ accepts 1 or 2 arguments:
+
+ If passed a single argument, it must be either an array, an
+ object, or a value of a bindable type (see below).
+
+ If passed 2 arguments, the first one is the 1-based bind
+ index or bindable parameter name and the second one must be
+ a value of a bindable type.
+
+ Bindable value types:
+
+ - null is bound as NULL.
+
+ - undefined as a standalone value is a no-op intended to
+ simplify certain client-side use cases: passing undefined
+ as a value to this function will not actually bind
+ anything and this function will skip confirmation that
+ binding is even legal. (Those semantics simplify certain
+ client-side uses.) Conversely, a value of undefined as an
+ array or object property when binding an array/object
+ (see below) is treated the same as null.
+
+ - Numbers are bound as either doubles or integers: doubles
+ if they are larger than 32 bits, else double or int32,
+ depending on whether they have a fractional part. (It is,
+ as of this writing, illegal to call (from JS) a WASM
+ function which either takes or returns an int64.)
+ Booleans are bound as integer 0 or 1. It is not expected
+ the distinction of binding doubles which have no
+ fractional parts is integers is significant for the
+ majority of clients due to sqlite3's data typing
+ model. This API does not currently support the BigInt
+ type.
+
+ - Strings are bound as strings (use bindAsBlob() to force
+ blob binding).
+
+ - Uint8Array instances are bound as blobs.
+
+ If passed an array, each element of the array is bound at
+ the parameter index equal to the array index plus 1
+ (because arrays are 0-based but binding is 1-based).
+
+ If passed an object, each object key is treated as a
+ bindable parameter name. The object keys _must_ match any
+ bindable parameter names, including any `$`, `@`, or `:`
+ prefix. Because `$` is a legal identifier chararacter in
+ JavaScript, that is the suggested prefix for bindable
+ parameters.
+
+ It returns this object on success and throws on
+ error. Errors include:
+
+ - Any bind index is out of range, a named bind parameter
+ does not match, or this statement has no bindable
+ parameters.
+
+ - Any value to bind is of an unsupported type.
+
+ - Passed no arguments or more than two.
+
+ - The statement has been finalized.
+ */
+ bind: function(/*[ndx,] arg*/){
+ affirmStmtOpen(this);
+ let ndx, arg;
+ switch(arguments.length){
+ case 1: ndx = 1; arg = arguments[0]; break;
+ case 2: ndx = arguments[0]; arg = arguments[1]; break;
+ default: toss("Invalid bind() arguments.");
+ }
+ if(undefined===arg){
+ /* It might seem intuitive to bind undefined as NULL
+ but this approach simplifies certain client-side
+ uses when passing on arguments between 2+ levels of
+ functions. */
+ return this;
+ }else if(!this.parameterCount){
+ toss("This statement has no bindable parameters.");
+ }
+ this._mayGet = false;
+ if(null===arg){
+ /* bind NULL */
+ return bindOne(this, ndx, BindTypes.null, arg);
+ }
+ else if(Array.isArray(arg)){
+ /* bind each entry by index */
+ if(1!==arguments.length){
+ toss("When binding an array, an index argument is not permitted.");
+ }
+ arg.forEach((v,i)=>bindOne(this, i+1, affirmSupportedBindType(v), v));
+ return this;
+ }
+ else if('object'===typeof arg/*null was checked above*/){
+ /* bind by name */
+ if(1!==arguments.length){
+ toss("When binding an object, an index argument is not permitted.");
+ }
+ Object.keys(arg)
+ .forEach(k=>bindOne(this, k,
+ affirmSupportedBindType(arg[k]),
+ arg[k]));
+ return this;
+ }else{
+ return bindOne(this, ndx,
+ affirmSupportedBindType(arg), arg);
+ }
+ toss("Should not reach this point.");
+ },
+ /**
+ Special case of bind() which binds the given value
+ using the BLOB binding mechanism instead of the default
+ selected one for the value. The ndx may be a numbered
+ or named bind index. The value must be of type string,
+ Uint8Array, or null/undefined (both treated as null).
+
+ If passed a single argument, a bind index of 1 is assumed.
+ */
+ bindAsBlob: function(ndx,arg){
+ affirmStmtOpen(this);
+ if(1===arguments.length){
+ ndx = 1;
+ arg = arguments[0];
+ }
+ const t = affirmSupportedBindType(arg);
+ if(BindTypes.string !== t && BindTypes.blob !== t
+ && BindTypes.null !== t){
+ toss("Invalid value type for bindAsBlob()");
+ }
+ this._mayGet = false;
+ return bindOne(this, ndx, BindTypes.blob, arg);
+ },
+ /**
+ Steps the statement one time. If the result indicates that
+ a row of data is available, true is returned. If no row of
+ data is available, false is returned. Throws on error.
+ */
+ step: function(){
+ affirmUnlocked(this, 'step()');
+ const rc = api.sqlite3_step(affirmStmtOpen(this)._pStmt);
+ switch(rc){
+ case api.SQLITE_DONE: return this._mayGet = false;
+ case api.SQLITE_ROW: return this._mayGet = true;
+ default:
+ this._mayGet = false;
+ console.warn("sqlite3_step() rc=",rc,"SQL =",
+ api.sqlite3_sql(this._pStmt));
+ this.db.checkRc(rc);
+ };
+ },
+ /**
+ Fetches the value from the given 0-based column index of
+ the current data row, throwing if index is out of range.
+
+ Requires that step() has just returned a truthy value, else
+ an exception is thrown.
+
+ By default it will determine the data type of the result
+ automatically. If passed a second arugment, it must be one
+ of the enumeration values for sqlite3 types, which are
+ defined as members of the sqlite3 module: SQLITE_INTEGER,
+ SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB. Any other value,
+ except for undefined, will trigger an exception. Passing
+ undefined is the same as not passing a value. It is legal
+ to, e.g., fetch an integer value as a string, in which case
+ sqlite3 will convert the value to a string.
+
+ If ndx is an array, this function behaves a differently: it
+ assigns the indexes of the array, from 0 to the number of
+ result columns, to the values of the corresponding column,
+ and returns that array.
+
+ If ndx is a plain object, this function behaves even
+ differentlier: it assigns the properties of the object to
+ the values of their corresponding result columns.
+
+ Blobs are returned as Uint8Array instances.
+
+ Potential TODO: add type ID SQLITE_JSON, which fetches the
+ result as a string and passes it (if it's not null) to
+ JSON.parse(), returning the result of that. Until then,
+ getJSON() can be used for that.
+ */
+ get: function(ndx,asType){
+ if(!affirmStmtOpen(this)._mayGet){
+ toss("Stmt.step() has not (recently) returned true.");
+ }
+ if(Array.isArray(ndx)){
+ let i = 0;
+ while(i32bits */
+ }
+ case api.SQLITE_FLOAT:
+ return api.sqlite3_column_double(this._pStmt, ndx);
+ case api.SQLITE_TEXT:
+ return api.sqlite3_column_text(this._pStmt, ndx);
+ case api.SQLITE_BLOB: {
+ const n = api.sqlite3_column_bytes(this._pStmt, ndx);
+ const ptr = api.sqlite3_column_blob(this._pStmt, ndx);
+ const rc = new Uint8Array(n);
+ for(let i = 0; i < n; ++i) rc[i] = HEAP8[ptr + i];
+ return rc;
+ }
+ default: toss("Don't know how to translate",
+ "type of result column #"+ndx+".");
+ }
+ abort("Not reached.");
+ },
+ /** Equivalent to get(ndx) but coerces the result to an
+ integer. */
+ getInt: function(ndx){return this.get(ndx,api.SQLITE_INTEGER)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ float. */
+ getFloat: function(ndx){return this.get(ndx,api.SQLITE_FLOAT)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ string. */
+ getString: function(ndx){return this.get(ndx,api.SQLITE_TEXT)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ Uint8Array. */
+ getBlob: function(ndx){return this.get(ndx,api.SQLITE_BLOB)},
+ /**
+ 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. 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, api.SQLITE_STRING);
+ return null===s ? s : JSON.parse(s);
+ },
+ /**
+ Returns the result column name of the given index, or
+ throws if index is out of bounds or this statement has been
+ finalized. This can be used without having run step()
+ first.
+ */
+ getColumnName: function(ndx){
+ return api.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(api.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
+ ? api.sqlite3_bind_parameter_index(this._pStmt, name)
+ : undefined);
+ }
+ }/*Stmt.prototype*/;
+
+ /** OO binding's namespace. */
+ const SQLite3 = {
+ version: {
+ lib: api.sqlite3_libversion(),
+ ooApi: "0.0.1"
+ },
+ DB,
+ Stmt,
+ /**
+ Reports whether a given compile-time option, named by the
+ 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, 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
+ true or false. That object is returned.
+
+ If passed no arguments then it returns an object mapping
+ all known compilation options to their compile-time values,
+ or boolean true if they are defined with no value.
+
+ 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,
+ the prefix is elided.
+ */
+ compileOptionUsed: function f(optName){
+ if(!arguments.length){
+ if(!f._opt){
+ f._rx = /^([^=]+)=(.+)/;
+ f._rxInt = /^-?\d+$/;
+ f._opt = function(opt, rv){
+ const m = f._rx.exec(opt);
+ rv[0] = (m ? m[1] : opt);
+ rv[1] = m ? (f._rxInt.test(m[2]) ? +m[2] : m[2]) : true;
+ };
+ }
+ const rc = {}, ov = [0,0];
+ let i = 0, k;
+ while((k = api.sqlite3_compileoption_get(i++))){
+ f._opt(k,ov);
+ rc[ov[0]] = ov[1];
+ }
+ return rc;
+ }
+ else if(Array.isArray(optName)){
+ const rc = {};
+ optName.forEach((v)=>{
+ rc[v] = api.sqlite3_compileoption_used(v);
+ });
+ return rc;
+ }
+ else if('object' === typeof optName){
+ Object.keys(optName).forEach((k)=> {
+ optName[k] = api.sqlite3_compileoption_used(k);
+ });
+ return optName;
+ }
+ return (
+ 'string'===typeof optName
+ ) ? !!api.sqlite3_compileoption_used(optName) : false;
+ }
+ };
+
+ namespace.sqlite3 = {
+ api: api,
+ SQLite3
+ };
+});
diff --git a/ext/fiddle/testing.css b/ext/fiddle/testing.css
new file mode 100644
index 0000000000..f87dbd2cf1
--- /dev/null
+++ b/ext/fiddle/testing.css
@@ -0,0 +1,31 @@
+textarea {
+ font-family: monospace;
+}
+header {
+ font-size: 130%;
+ font-weight: bold;
+}
+.hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+}
+fieldset.options {
+ font-size: 75%;
+}
+fieldset > legend {
+ padding: 0 0.5em;
+}
+span.labeled-input {
+ padding: 0.25em;
+ margin: 0.25em 0.5em;
+ border-radius: 0.25em;
+ white-space: nowrap;
+ background: #0002;
+}
+.center { text-align: center; }
+.error {
+ color: red;
+ background-color: yellow;
+}
diff --git a/ext/fiddle/testing1.html b/ext/fiddle/testing1.html
new file mode 100644
index 0000000000..bf22f30ff3
--- /dev/null
+++ b/ext/fiddle/testing1.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+ sqlite3-api.js tests
+
+
+
+ sqlite3-api.js tests
+
+
+
+