diff --git a/Makefile.in b/Makefile.in index e5d30d030b..977f0da953 100644 --- a/Makefile.in +++ b/Makefile.in @@ -1512,3 +1512,36 @@ sqlite3.def: $(REAL_LIBOBJ) sqlite3.dll: $(REAL_LIBOBJ) sqlite3.def $(TCC) -shared -o $@ sqlite3.def \ -Wl,"--strip-all" $(REAL_LIBOBJ) + + +# +# fiddle section +# +fiddle_dir = ext/fiddle +fiddle_html = $(fiddle_dir)/fiddle.html +fiddle_generated = $(fiddle_html) \ + $(fiddle_dir)/fiddle.js \ + $(fiddle_dir)/fiddle.wasm +clean-fiddle: + rm -f $(fiddle_generated) +clean: clean-fiddle +#emcc_opt = -O0 +#emcc_opt = -O1 +#emcc_opt = -O2 +#emcc_opt = -O3 +emcc_opt = -Oz +emcc_flags = $(emcc_opt) $(SHELL_OPT) \ + -sEXPORTED_RUNTIME_METHODS=ccall,cwrap \ + -sEXPORTED_FUNCTIONS=_fiddle_exec \ + -sEXIT_RUNTIME=1 \ + --pre-js $(fiddle_dir)/module-pre.js \ + --post-js $(fiddle_dir)/module-post.js \ + --shell-file $(fiddle_dir)/fiddle.in.html \ + $(fiddle_cflags) +# $(fiddle_cflags) is intended to be passed to make via the CLI in +# order to override, e.g., -Ox for one-off builds. +$(fiddle_html): Makefile sqlite3.c shell.c \ + $(fiddle_dir)/fiddle.in.html \ + $(fiddle_dir)/module-pre.js $(fiddle_dir)/module-post.js + emcc -o $@ $(emcc_flags) sqlite3.c shell.c +fiddle: $(fiddle_html) diff --git a/ext/fiddle/Makefile b/ext/fiddle/Makefile new file mode 100644 index 0000000000..0d4119247b --- /dev/null +++ b/ext/fiddle/Makefile @@ -0,0 +1,7 @@ +# This makefile exists primarily to simplify/speed up development from +# emacs. It is not part of the canonical build process. +default: + make -C ../.. fiddle -e emcc_opt=-O0 + +clean: + make -C ../../ clean-fiddle diff --git a/ext/fiddle/fiddle.in.html b/ext/fiddle/fiddle.in.html new file mode 100644 index 0000000000..6dac369976 --- /dev/null +++ b/ext/fiddle/fiddle.in.html @@ -0,0 +1,239 @@ + + + + + + sqlite3 fiddle + + + + +
sqlite3 fiddle
+
+
+
Initializing app...
+
+ 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...
+
+ +
+ + + +
+
+ Options +
+ + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + + +
+
+
+ +
+ +
+
+
+
+ + {{{ SCRIPT }}} + + 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/module-post.js b/ext/fiddle/module-post.js new file mode 100644 index 0000000000..8e13ab1d3c --- /dev/null +++ b/ext/fiddle/module-post.js @@ -0,0 +1,233 @@ +/* This is the --post-js file for emcc. It gets appended to the + generated fiddle.js. It should contain all app-level code. + + Maintenance achtung: do not call any wasm-bound functions from + outside of the onRuntimeInitialized() function. They are not + permitted to be called until after the module init is complete, + which does not happen until after this file is processed. Once that + init is finished, Module.onRuntimeInitialized() will be + triggered. All app-level init code should go into that callback or + be triggered via it. Calling wasm-bound functions before that + callback is run will trigger an assertion in the wasm environment. +*/ +window.Module.onRuntimeInitialized = function(){ + 'use strict'; + const Module = window.Module /* wasm module as set up by emscripten */; + delete Module.onRuntimeInitialized; + + /* 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]); + }; + + // Unhide all elements which start out hidden + EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden')); + + 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(); + btnRun.click(); + } + }, false); + const taOutput = E('#output'); + const btnClearOut = E('#btn-clear-output'); + btnClearOut.addEventListener('click',function(){ + taOutput.value = ''; + if(Module.jqTerm) Module.jqTerm.clear(); + },false); + /* Sends the given text to the shell. If it's null or empty, this + is a no-op except that the very first call will initialize the + db and output an informational header. */ + const doExec = function f(sql){ + if(!f._) f._ = Module.cwrap('fiddle_exec', null, ['string']); + if(Module._isDead){ + Module.printErr("shell module has exit()ed. Cannot run SQL."); + return; + } + if(Module.config.autoClearOutput) taOutput.value=''; + f._(sql); + }; + const btnRun = E('#btn-run'); + btnRun.addEventListener('click',function(){ + const sql = taInput.value.trim(); + if(sql){ + doExec(sql); + } + },false); + + const mainWrapper = E('#main-wrapper'); + /* For each checkboxes with data-csstgt, set up a handler which + toggles the given CSS class on the element matching + E(data-csstgt). */ + EAll('input[type=checkbox][data-csstgt]') + .forEach(function(e){ + const tgt = E(e.dataset.csstgt); + const cssClass = e.dataset.cssclass || 'error'; + e.checked = tgt.classList.contains(cssClass); + e.addEventListener('change', function(){ + tgt.classList[ + this.checked ? 'add' : 'remove' + ](cssClass) + }, false); + }); + /* For each checkbox with data-config=X, set up a binding to + Module.config[X]. These must be set up AFTER data-csstgt + checkboxes so that those two states can be synced properly. */ + EAll('input[type=checkbox][data-config]') + .forEach(function(e){ + const confVal = !!Module.config[e.dataset.config]; + if(e.checked !== confVal){ + /* Ensure that data-csstgt mappings (if any) get + synced properly. */ + e.checked = confVal; + e.dispatchEvent(new Event('change')); + } + e.addEventListener('change', function(){ + Module.config[this.dataset.config] = this.checked; + }, false); + }); + /* For each button with data-cmd=X, map a click handler which + calls doExec(X). */ + const cmdClick = function(){doExec(this.dataset.cmd);}; + EAll('button[data-cmd]').forEach( + e => e.addEventListener('click', cmdClick, 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 terminal area 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 bcl = document.body.classList; + const appViews = EAll('.app-view'); + const resized = function f(){ + if(f.$disabled) return; + const wh = window.innerHeight; + var ht; + var extra = 0; + const elemsToCount = [ + E('body > header') + ]; + 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", + " - 3em"/*fudge value*/,")" + /* ^^^^ hypothetically not needed, but both Chrome/FF on + Linux will force scrollbars on the body if this value is + too small (<0.75em in my tests). */ + ].join(''); + }); + }; + resized.$disabled = true/*gets deleted when setup is finished*/; + window.addEventListener('resize', debounce(resized, 250), false); + return resized; + })(); + + Module.print(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('#jqterminal').empty(); + Module.jqTerm = eTerm.terminal(doExec,{ + prompt: 'sqlite> ', + greetings: false /* note that the docs incorrectly call this 'greeting' */ + }); + //Module.jqTerm.clear(/*remove the "greeting"*/); + /* Set up a button to toggle the views... */ + const head = E('header#titlebar'); + const btnToggleView = jQuery("")[0]; + 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(); + } + doExec(null/*init the db and output the header*/); + delete ForceResizeKludge.$disabled; + ForceResizeKludge(); +}; diff --git a/ext/fiddle/module-pre.js b/ext/fiddle/module-pre.js new file mode 100644 index 0000000000..ebd812ef99 --- /dev/null +++ b/ext/fiddle/module-pre.js @@ -0,0 +1,110 @@ +/* This is the --pre-js file for emcc. It gets prepended to the + generated fiddle.js. It should contain only code which is relevant + to the setup and initialization of the wasm module. */ +(function(){ + 'use strict'; + + /** + What follows is part of the emscripten core setup. Do not + modify it without understanding what it's doing. + */ + const statusElement = document.getElementById('status'); + const progressElement = document.getElementById('progress'); + const spinnerElement = document.getElementById('spinner'); + const Module = window.Module = { + /* Config object. Referenced by certain Module methods and + app-level code. */ + config: { + /* If true, the Module.print() impl 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, Module.print() will echo its output to + the console, in addition to its normal output widget. */ + printToConsole: true, + /* If true, display input/output areas side-by-side. */ + sideBySide: false, + /* If true, swap positions of the input/output areas. */ + swapInOut: false + }, + preRun: [], + postRun: [], + //onRuntimeInitialized: function(){}, + print: (function f() { + /* 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. */ + const outputElem = document.getElementById('output'); + outputElem.value = ''; // clear browser cache + return function(text) { + if(arguments.length > 1) text = Array.prototype.slice.call(arguments).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*/ + outputElem.value = ''; + return; + } + if(window.Module.config.printToConsole) console.log(text); + if(window.Module.jqTerm) window.Module.jqTerm.echo(text); + outputElem.value += text + "\n"; + if(window.Module.config.autoScrollOutput){ + outputElem.scrollTop = outputElem.scrollHeight; + } + }; + })(), + setStatus: function f(text) { + if(!f.last) f.last = { time: Date.now(), text: '' }; + if(text === f.last.text) return; + const m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/); + const now = Date.now(); + if(m && now - f.last.time < 30) return; // if this is a progress update, skip it if too soon + f.last.time = now; + f.last.text = text; + if(m) { + text = m[1]; + progressElement.value = parseInt(m[2])*100; + progressElement.max = parseInt(m[4])*100; + progressElement.hidden = false; + spinnerElement.hidden = false; + } else { + progressElement.remove(); + if(!text) spinnerElement.remove(); + } + if(text) statusElement.innerText = text; + else statusElement.remove(); + }, + totalDependencies: 0, + monitorRunDependencies: function(left) { + this.totalDependencies = Math.max(this.totalDependencies, left); + this.setStatus(left + ? ('Preparing... (' + (this.totalDependencies-left) + + '/' + this.totalDependencies + ')') + : 'All downloads complete.'); + } + }; + Module.printErr = Module.print/*capture stderr output*/; + Module.setStatus('Downloading...'); + window.onerror = function(/*message, source, lineno, colno, error*/) { + const err = arguments[4]; + if(err && 'ExitStatus'==err.name){ + Module._isDead = true; + Module.printErr("FATAL ERROR:", err.message); + Module.printErr("Restarting the app requires reloading the page."); + const taOutput = document.querySelector('#output'); + if(taOutput) taOutput.classList.add('error'); + } + Module.setStatus('Exception thrown, see JavaScript console'); + spinnerElement.style.display = 'none'; + Module.setStatus = function(text) { + if(text) console.error('[post-exception status] ' + text); + }; + }; +})(); diff --git a/manifest b/manifest index 7f1d327e7e..aa0145d73b 100644 --- a/manifest +++ b/manifest @@ -1,9 +1,9 @@ -C Fix\sharmless\scompiler\swarnings\sin\sthe\snew\sunixFullPathname\simplementation. -D 2022-05-17T15:11:57.632 +C Merge\sthe\schanges\sto\ssupport\sthe\s"fiddle"\sextension. +D 2022-05-19T16:59:46.843 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 -F Makefile.in b210ad2733317f1a4353085dfb9d385ceec30b0e6a61d20a5accabecac6b1949 +F Makefile.in 5dbc61c076215a580d59d1f21b5e62955d2e570321b63f00a31b188f1f5089a6 F Makefile.linux-gcc f609543700659711fbd230eced1f01353117621dccae7b9fb70daa64236c5241 F Makefile.msc b28a8a7a977e7312f6859f560348e1eb110c21bd6cf9fab0d16537c0a514eef3 F README.md 8b8df9ca852aeac4864eb1e400002633ee6db84065bd01b78c33817f97d31f5e @@ -55,6 +55,11 @@ F ext/expert/expert1.test 3c642a4e7bbb14f21ddab595436fb465a4733f47a0fe5b2855e1d5 F ext/expert/sqlite3expert.c 6ca30d73b9ed75bd56d6e0d7f2c962d2affaa72c505458619d0ff5d9cdfac204 F ext/expert/sqlite3expert.h ca81efc2679a92373a13a3e76a6138d0310e32be53d6c3bfaedabd158ea8969b F ext/expert/test_expert.c d56c194b769bdc90cf829a14c9ecbc1edca9c850b837a4d0b13be14095c32a72 +F ext/fiddle/Makefile b2904d52c10a7c984cfab95c54fb85f33aa8a6b2653faf1527d08ce57114be46 +F ext/fiddle/fiddle.in.html ca27f4b0f0477096e78d8b9b44109c234d9305531ab63ecd559a739bdea0b11c +F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf +F ext/fiddle/module-post.js 3d1a368312c598f73eb5d1d715c464ca473d491ad5df4d0636fbcf91a74817a9 +F ext/fiddle/module-pre.js a7b046c0f764b100a5bedd3880bece8e6fb5908fb73cb91fb7a9b692bc938862 F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b F ext/fts1/ft_hash.h 06df7bba40dadd19597aa400a875dbc2fed705ea @@ -554,7 +559,7 @@ F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c F src/resolve.c a4eb3c617027fd049b07432f3b942ea7151fa793a332a11a7d0f58c9539e104f F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 F src/select.c 74060a09f66c0c056f3c61627e22cb484af0bbfa29d7d14dcf17c684742c15de -F src/shell.c.in 176cad562152cbbafe7ecc9c83c82850e2c3d0cf33ec0a52d67341d35c842f22 +F src/shell.c.in cc3e19b2d2eefbadc4139b016c097d6478eae01d14eca993368ee5cff8820fff F src/sqlite.h.in d15c307939039086adca159dd340a94b79b69827e74c6d661f343eeeaefba896 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h a988810c9b21c0dc36dc7a62735012339dc76fc7ab448fb0792721d30eacb69d @@ -1954,8 +1959,9 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P d8b249e8cdf0babe1427d0587dbdc27a52ec06a5ef3a20dfb05a0ea4adb85858 -R 56c8d067b3c475439a951ae630953e32 +P f7e1ceb5b59a876cfd04a8aac0ee2b322c970555b9c361b4953d711ef6596e37 56b82ae806c61b95e62042ca70ed952ce01832b02da55c2b315f9201989514ab +R f9801562283af03ef889fbffa87ba13d +T +closed 56b82ae806c61b95e62042ca70ed952ce01832b02da55c2b315f9201989514ab U drh -Z 99cc78493b68b2d09cacc03a9e60389c +Z 5ac79eb27823f3c1a240f3c2dd67e98a # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index e1be3622c7..4e55b2ea8e 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -f7e1ceb5b59a876cfd04a8aac0ee2b322c970555b9c361b4953d711ef6596e37 \ No newline at end of file +58585f01aa4747d3a09771fb462066bd037914f435ff04fa16ed9b0571e7912a \ No newline at end of file diff --git a/src/shell.c.in b/src/shell.c.in index cace8bf2f4..9a0859293b 100644 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -229,6 +229,16 @@ static void setTextMode(FILE *file, int isOutput){ # define setTextMode(X,Y) #endif +/* +** When compiling with emcc (a.k.a. emscripten), we're building a +** WebAssembly (WASM) bundle and need to disable and rewire a few +** things. +*/ +#ifdef __EMSCRIPTEN__ +#define SQLITE_SHELL_WASM_MODE +#else +#undef SQLITE_SHELL_WASM_MODE +#endif /* True if the timer is enabled */ static int enableTimer = 0; @@ -691,6 +701,7 @@ static char *local_getline(char *zLine, FILE *in){ ** be freed by the caller or else passed back into this routine via the ** zPrior argument for reuse. */ +#ifndef SQLITE_SHELL_WASM_MODE static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ char *zPrompt; char *zResult; @@ -710,7 +721,7 @@ static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ } return zResult; } - +#endif /* !SQLITE_SHELL_WASM_MODE */ /* ** Return the value of a hexadecimal digit. Return -1 if the input @@ -798,7 +809,7 @@ static void freeText(ShellText *p){ ** If the third argument, quote, is not '\0', then it is used as a ** quote character for zAppend. */ -static void appendText(ShellText *p, char const *zAppend, char quote){ +static void appendText(ShellText *p, const char *zAppend, char quote){ int len; int i; int nAppend = strlen30(zAppend); @@ -1009,16 +1020,18 @@ INCLUDE test_windirent.h INCLUDE test_windirent.c #define dirent DIRENT #endif -INCLUDE ../ext/misc/shathree.c -INCLUDE ../ext/misc/fileio.c -INCLUDE ../ext/misc/completion.c -INCLUDE ../ext/misc/appendvfs.c INCLUDE ../ext/misc/memtrace.c +INCLUDE ../ext/misc/shathree.c INCLUDE ../ext/misc/uint.c INCLUDE ../ext/misc/decimal.c INCLUDE ../ext/misc/ieee754.c INCLUDE ../ext/misc/series.c INCLUDE ../ext/misc/regexp.c +#ifndef SQLITE_SHELL_WASM_MODE +INCLUDE ../ext/misc/fileio.c +INCLUDE ../ext/misc/completion.c +INCLUDE ../ext/misc/appendvfs.c +#endif #ifdef SQLITE_HAVE_ZLIB INCLUDE ../ext/misc/zipfile.c INCLUDE ../ext/misc/sqlar.c @@ -1149,8 +1162,18 @@ struct ShellState { char *zNonce; /* Nonce for temporary safe-mode excapes */ EQPGraph sGraph; /* Information for the graphical EXPLAIN QUERY PLAN */ ExpertInfo expert; /* Valid if previous command was ".expert OPT..." */ +#ifdef SQLITE_SHELL_WASM_MODE + struct { + const char * zInput; /* Input string from wasm/JS proxy */ + const char * zPos; /* Cursor pos into zInput */ + } wasm; +#endif }; +#ifdef SQLITE_SHELL_WASM_MODE +static ShellState shellState; +#endif + /* Allowed values for ShellState.autoEQP */ @@ -4218,13 +4241,14 @@ static int run_schema_dump_query( ** Text of help messages. ** ** The help text for each individual command begins with a line that starts -** with ".". Subsequent lines are supplimental information. +** with ".". Subsequent lines are supplemental information. ** ** There must be two or more spaces between the end of the command and the ** start of the description of what that command does. */ static const char *(azHelp[]) = { -#if defined(SQLITE_HAVE_ZLIB) && !defined(SQLITE_OMIT_VIRTUALTABLE) +#if defined(SQLITE_HAVE_ZLIB) && !defined(SQLITE_OMIT_VIRTUALTABLE) \ + && !defined(SQLITE_SHELL_WASM_MODE) ".archive ... Manage SQL archives", " Each command must have exactly one of the following options:", " -c, --create Create a new archive", @@ -4250,10 +4274,12 @@ static const char *(azHelp[]) = { #ifndef SQLITE_OMIT_AUTHORIZATION ".auth ON|OFF Show authorizer callbacks", #endif +#ifndef SQLITE_SHELL_WASM_MODE ".backup ?DB? FILE Backup DB (default \"main\") to FILE", " Options:", " --append Use the appendvfs", " --async Write to FILE without journal and fsync()", +#endif ".bail on|off Stop after hitting an error. Default OFF", ".binary on|off Turn binary output on or off. Default OFF", ".cd DIRECTORY Change the working directory to DIRECTORY", @@ -4280,9 +4306,13 @@ static const char *(azHelp[]) = { " trace Like \"full\" but enable \"PRAGMA vdbe_trace\"", #endif " trigger Like \"full\" but also show trigger bytecode", +#ifndef SQLITE_SHELL_WASM_MODE ".excel Display the output of next command in spreadsheet", " --bom Put a UTF8 byte-order mark on intermediate file", +#endif +#ifndef SQLITE_SHELL_WASM_MODE ".exit ?CODE? Exit this program with return-code CODE", +#endif ".expert EXPERIMENTAL. Suggest indexes for queries", ".explain ?on|off|auto? Change the EXPLAIN formatting mode. Default: auto", ".filectrl CMD ... Run various sqlite3_file_control() operations", @@ -4291,6 +4321,7 @@ static const char *(azHelp[]) = { ".fullschema ?--indent? Show schema and the content of sqlite_stat tables", ".headers on|off Turn display of headers on or off", ".help ?-all? ?PATTERN? Show help text for PATTERN", +#ifndef SQLITE_SHELL_WASM_MODE ".import FILE TABLE Import data from FILE into TABLE", " Options:", " --ascii Use \\037 and \\036 as column and row separators", @@ -4305,6 +4336,7 @@ static const char *(azHelp[]) = { " from the \".mode\" output mode", " * If FILE begins with \"|\" then it is a command that generates the", " input text.", +#endif #ifndef SQLITE_OMIT_TEST_CONTROL ".imposter INDEX TABLE Create imposter table TABLE on index INDEX", #endif @@ -4318,10 +4350,12 @@ static const char *(azHelp[]) = { ".lint OPTIONS Report potential schema issues.", " Options:", " fkey-indexes Find missing foreign key indexes", -#ifndef SQLITE_OMIT_LOAD_EXTENSION +#if !defined(SQLITE_OMIT_LOAD_EXTENSION) && !defined(SQLITE_SHELL_WASM_MODE) ".load FILE ?ENTRY? Load an extension library", #endif +#ifndef SQLITE_SHELL_WASM_MODE ".log FILE|off Turn logging on or off. FILE can be stderr/stdout", +#endif ".mode MODE ?OPTIONS? Set output mode", " MODE is one of:", " ascii Columns/rows delimited by 0x1F and 0x1E", @@ -4348,6 +4382,7 @@ static const char *(azHelp[]) = { " TABLE The name of SQL table used for \"insert\" mode", ".nonce STRING Suspend safe mode for one command if nonce matches", ".nullvalue STRING Use STRING in place of NULL values", +#ifndef SQLITE_SHELL_WASM_MODE ".once ?OPTIONS? ?FILE? Output for the next SQL command only to FILE", " If FILE begins with '|' then open as a pipe", " --bom Put a UTF8 byte-order mark at the beginning", @@ -4356,6 +4391,7 @@ static const char *(azHelp[]) = { ".open ?OPTIONS? ?FILE? Close existing database and reopen FILE", " Options:", " --append Use appendvfs to append database to the end of FILE", +#endif #ifndef SQLITE_OMIT_DESERIALIZE " --deserialize Load into memory using sqlite3_deserialize()", " --hexdb Load the output of \"dbtotxt\" as an in-memory db", @@ -4387,9 +4423,11 @@ static const char *(azHelp[]) = { " --reset Reset the count for each input and interrupt", #endif ".prompt MAIN CONTINUE Replace the standard prompts", +#ifndef SQLITE_SHELL_WASM_MODE ".quit Exit this program", ".read FILE Read input from FILE or command output", " If FILE begins with \"|\", it is a command that generates the input.", +#endif #if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) ".recover Recover as much data as possible from corrupt db.", " --freelist-corrupt Assume the freelist is corrupt", @@ -4398,8 +4436,10 @@ static const char *(azHelp[]) = { " --no-rowids Do not attempt to recover rowid values", " that are not also INTEGER PRIMARY KEYs", #endif +#ifndef SQLITE_SHELL_WASM_MODE ".restore ?DB? FILE Restore content of DB (default \"main\") from FILE", ".save ?OPTIONS? FILE Write database to FILE (an alias for .backup ...)", +#endif ".scanstats on|off Turn sqlite3_stmt_scanstatus() metrics on or off", ".schema ?PATTERN? Show the CREATE statements matching PATTERN", " Options:", @@ -4433,7 +4473,7 @@ static const char *(azHelp[]) = { " --sha3-384 Use the sha3-384 algorithm", " --sha3-512 Use the sha3-512 algorithm", " Any other argument is a LIKE pattern for tables to hash", -#ifndef SQLITE_NOHAVE_SYSTEM +#if !defined(SQLITE_NOHAVE_SYSTEM) && !defined(SQLITE_SHELL_WASM_MODE) ".shell CMD ARGS... Run CMD ARGS... in a system shell", #endif ".show Show the current values for various settings", @@ -4442,11 +4482,13 @@ static const char *(azHelp[]) = { " on Turn on automatic stat display", " stmt Show statement stats", " vmstep Show the virtual machine step count only", -#ifndef SQLITE_NOHAVE_SYSTEM +#if !defined(SQLITE_NOHAVE_SYSTEM) && !defined(SQLITE_SHELL_WASM_MODE) ".system CMD ARGS... Run CMD ARGS... in a system shell", #endif ".tables ?TABLE? List names of tables matching LIKE pattern TABLE", +#ifndef SQLITE_SHELL_WASM_MODE ".testcase NAME Begin redirecting output to 'testcase-out.txt'", +#endif ".testctrl CMD ... Run various sqlite3_test_control() operations", " Run \".testctrl\" with no arguments for details", ".timeout MS Try opening locked tables for MS milliseconds", @@ -4991,14 +5033,16 @@ static void open_db(ShellState *p, int openFlags){ #ifndef SQLITE_OMIT_LOAD_EXTENSION sqlite3_enable_load_extension(p->db, 1); #endif - sqlite3_fileio_init(p->db, 0, 0); sqlite3_shathree_init(p->db, 0, 0); - sqlite3_completion_init(p->db, 0, 0); sqlite3_uint_init(p->db, 0, 0); sqlite3_decimal_init(p->db, 0, 0); sqlite3_regexp_init(p->db, 0, 0); sqlite3_ieee_init(p->db, 0, 0); sqlite3_series_init(p->db, 0, 0); +#ifndef SQLITE_SHELL_WASM_MODE + sqlite3_fileio_init(p->db, 0, 0); + sqlite3_completion_init(p->db, 0, 0); +#endif #if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) sqlite3_dbdata_init(p->db, 0, 0); #endif @@ -8121,7 +8165,8 @@ static int do_meta_command(char *zLine, ShellState *p){ }else #endif -#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB) +#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB) \ + && !defined(SQLITE_SHELL_WASM_MODE) if( c=='a' && strncmp(azArg[0], "archive", n)==0 ){ open_db(p, 0); failIfSafeMode(p, "cannot run .archive in safe mode"); @@ -8129,6 +8174,7 @@ static int do_meta_command(char *zLine, ShellState *p){ }else #endif +#ifndef SQLITE_SHELL_WASM_MODE if( (c=='b' && n>=3 && strncmp(azArg[0], "backup", n)==0) || (c=='s' && n>=3 && strncmp(azArg[0], "save", n)==0) ){ @@ -8197,6 +8243,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } close_db(pDest); }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='b' && n>=3 && strncmp(azArg[0], "bail", n)==0 ){ if( nArg==2 ){ @@ -8578,10 +8625,12 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else +#ifndef SQLITE_SHELL_WASM_MODE if( c=='e' && strncmp(azArg[0], "exit", n)==0 ){ if( nArg>1 && (rc = (int)integerValue(azArg[1]))!=0 ) exit(rc); rc = 2; }else +#endif /* The ".explain" command is automatic now. It is largely pointless. It ** retained purely for backwards compatibility */ @@ -8836,6 +8885,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else +#ifndef SQLITE_SHELL_WASM_MODE if( c=='i' && strncmp(azArg[0], "import", n)==0 ){ char *zTable = 0; /* Insert data into this table */ char *zSchema = 0; /* within this schema (may default to "main") */ @@ -9126,6 +9176,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sCtx.nRow, sCtx.nErr, sCtx.nLine-1); } }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ #ifndef SQLITE_UNTESTABLE if( c=='i' && strncmp(azArg[0], "imposter", n)==0 ){ @@ -9315,7 +9366,7 @@ static int do_meta_command(char *zLine, ShellState *p){ lintDotCommand(p, azArg, nArg); }else -#ifndef SQLITE_OMIT_LOAD_EXTENSION +#if !defined(SQLITE_OMIT_LOAD_EXTENSION) && !defined(SQLITE_SHELL_WASM_MODE) if( c=='l' && strncmp(azArg[0], "load", n)==0 ){ const char *zFile, *zProc; char *zErrMsg = 0; @@ -9337,6 +9388,7 @@ static int do_meta_command(char *zLine, ShellState *p){ }else #endif +#ifndef SQLITE_SHELL_WASM_MODE if( c=='l' && strncmp(azArg[0], "log", n)==0 ){ failIfSafeMode(p, "cannot run .log in safe mode"); if( nArg!=2 ){ @@ -9348,6 +9400,7 @@ static int do_meta_command(char *zLine, ShellState *p){ p->pLog = output_file_open(zFile, 0); } }else +#endif if( c=='m' && strncmp(azArg[0], "mode", n)==0 ){ const char *zMode = 0; @@ -9583,6 +9636,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else +#ifndef SQLITE_SHELL_WASM_MODE if( (c=='o' && (strncmp(azArg[0], "output", n)==0||strncmp(azArg[0], "once", n)==0)) || (c=='e' && n==5 && strcmp(azArg[0],"excel")==0) @@ -9698,6 +9752,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } sqlite3_free(zFile); }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='p' && n>=3 && strncmp(azArg[0], "parameter", n)==0 ){ open_db(p,0); @@ -9867,10 +9922,13 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else +#ifndef SQLITE_SHELL_WASM_MODE if( c=='q' && strncmp(azArg[0], "quit", n)==0 ){ rc = 2; }else +#endif +#ifndef SQLITE_SHELL_WASM_MODE if( c=='r' && n>=3 && strncmp(azArg[0], "read", n)==0 ){ FILE *inSaved = p->in; int savedLineno = p->lineno; @@ -9905,7 +9963,9 @@ static int do_meta_command(char *zLine, ShellState *p){ p->in = inSaved; p->lineno = savedLineno; }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ +#ifndef SQLITE_SHELL_WASM_MODE if( c=='r' && n>=3 && strncmp(azArg[0], "restore", n)==0 ){ const char *zSrcFile; const char *zDb; @@ -9957,6 +10017,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } close_db(pSrc); }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='s' && strncmp(azArg[0], "scanstats", n)==0 ){ if( nArg==2 ){ @@ -10582,7 +10643,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_free(zSql); }else -#ifndef SQLITE_NOHAVE_SYSTEM +#if !defined(SQLITE_NOHAVE_SYSTEM) && !defined(SQLITE_SHELL_WASM_MODE) if( c=='s' && (strncmp(azArg[0], "shell", n)==0 || strncmp(azArg[0],"system",n)==0) ){ @@ -10603,7 +10664,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_free(zCmd); if( x ) raw_printf(stderr, "System command returns %d\n", x); }else -#endif /* !defined(SQLITE_NOHAVE_SYSTEM) */ +#endif /* !defined(SQLITE_NOHAVE_SYSTEM) && !defined(SQLITE_SHELL_WASM_MODE) */ if( c=='s' && strncmp(azArg[0], "show", n)==0 ){ static const char *azBool[] = { "off", "on", "trigger", "full"}; @@ -10783,6 +10844,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_free(azResult); }else +#ifndef SQLITE_SHELL_WASM_MODE /* Begin redirecting output to the file "testcase-out.txt" */ if( c=='t' && strcmp(azArg[0],"testcase")==0 ){ output_reset(p); @@ -10796,6 +10858,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "?"); } }else +#endif /* !defined(SQLITE_SHELL_WASM_MODE) */ #ifndef SQLITE_UNTESTABLE if( c=='t' && n>=8 && strncmp(azArg[0], "testctrl", n)==0 ){ @@ -11467,6 +11530,39 @@ static void echo_group_input(ShellState *p, const char *zDo){ if( ShellHasFlag(p, SHFLG_Echo) ) utf8_printf(p->out, "%s\n", zDo); } +#ifdef SQLITE_SHELL_WASM_MODE +/* +** Alternate one_input_line() impl for wasm mode. This is not in the primary impl +** because we need the global shellState and cannot access it from that function +** without moving lots of code around (creating a larger/messier diff). +*/ +static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ + /* Parse the next line from shellState.wasm.zInput. */ + const char *zBegin = shellState.wasm.zPos; + const char *z = zBegin; + char *zLine = 0; + int nZ = 0; + + UNUSED_PARAMETER(in); + UNUSED_PARAMETER(isContinuation); + if(!z || !*z){ + return 0; + } + while(*z && isspace(*z)) ++z; + zBegin = z; + for(; *z && '\n'!=*z; ++nZ, ++z){} + if(nZ>0 && '\r'==zBegin[nZ-1]){ + --nZ; + } + shellState.wasm.zPos = z; + zLine = realloc(zPrior, nZ+1); + shell_check_oom(zLine); + memcpy(zLine, zBegin, (size_t)nZ); + zLine[nZ] = 0; + return zLine; +} +#endif /* SQLITE_SHELL_WASM_MODE */ + /* ** Read input from *in and process it. If *in==0 then input ** is interactive - the user is typing it it. Otherwise, input @@ -11848,6 +11944,10 @@ static char *cmdline_option_value(int argc, char **argv, int i){ # endif #endif +#ifdef SQLITE_SHELL_WASM_MODE +# define main fiddle_main +#endif + #if SQLITE_SHELL_IS_UTF8 int SQLITE_CDECL main(int argc, char **argv){ #else @@ -11858,7 +11958,11 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ sqlite3_uint64 mem_main_enter = sqlite3_memory_used(); #endif char *zErrMsg = 0; +#ifdef SQLITE_SHELL_WASM_MODE +# define data shellState +#else ShellState data; +#endif const char *zInitFile = 0; int i; int rc = 0; @@ -11874,8 +11978,13 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ setBinaryMode(stdin, 0); setvbuf(stderr, 0, _IONBF, 0); /* Make sure stderr is unbuffered */ +#ifdef SQLITE_SHELL_WASM_MODE + stdin_is_interactive = 0; + stdout_is_console = 1; +#else stdin_is_interactive = isatty(0); stdout_is_console = isatty(1); +#endif #if !defined(_WIN32_WCE) if( getenv("SQLITE_DEBUG_BREAK") ){ @@ -12131,7 +12240,9 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ #endif } data.out = stdout; +#ifndef SQLITE_SHELL_WASM_MODE sqlite3_appendvfs_init(0,0,0); +#endif /* Go ahead and open the database file if it already exists. If the ** file does not exist, delay opening it. This prevents empty database @@ -12397,6 +12508,9 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ rc = process_input(&data); } } +#ifndef SQLITE_SHELL_WASM_MODE + /* In WASM mode we have to leave the db state in place so that + ** client code can "push" SQL into it after this call returns. */ free(azCmd); set_table_name(&data, 0); if( data.db ){ @@ -12429,5 +12543,47 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ (unsigned int)(sqlite3_memory_used()-mem_main_enter)); } #endif +#endif /* !SQLITE_SHELL_WASM_MODE */ return rc; } + + +#ifdef SQLITE_SHELL_WASM_MODE +/* +** Trivial exportable function for emscripten. Needs to be exported using: +** +** emcc ..flags... -sEXPORTED_FUNCTIONS=_fiddle_exec -sEXPORTED_RUNTIME_METHODS=ccall,cwrap +** +** (Note the underscore before the function name.) It processes zSql +** as if it were input to the sqlite3 shell and redirects all output +** to the wasm binding. +*/ +void fiddle_exec(const char * zSql){ + static int once = 0; + int rc = 0; + if(!once){ + /* Simulate an argv array for main() */ + static char * argv[] = {"fiddle", + "-bail", + "-safe"}; + rc = fiddle_main((int)(sizeof(argv)/sizeof(argv[0])), argv); + once = rc ? -1 : 1; + memset(&shellState.wasm, 0, sizeof(shellState.wasm)); + printf( + "SQLite version %s %.19s\n" /*extra-version-info*/, + sqlite3_libversion(), sqlite3_sourceid() + ); + puts("WASM shell"); + puts("Enter \".help\" for usage hints."); + puts("Connected to a transient in-memory database."); + } + if(once<0){ + puts("DB init failed. Not executing SQL."); + }else if(zSql && *zSql){ + shellState.wasm.zInput = zSql; + shellState.wasm.zPos = zSql; + process_input(&shellState); + memset(&shellState.wasm, 0, sizeof(shellState.wasm)); + } +} +#endif /* SQLITE_SHELL_WASM_MODE */