#
fiddle_dir = ext/fiddle
fiddle_html = $(fiddle_dir)/fiddle.html
-fiddle_generated = $(fiddle_html) \
- $(fiddle_dir)/fiddle.js \
- $(fiddle_dir)/fiddle.wasm
+fiddle_module_js = $(fiddle_dir)/fiddle-module.js
+fiddle_generated = $(fiddle_module_js) \
+ $(fiddle_dir)/fiddle-module.wasm
# fiddle_dummy_exports = ,comma,list of func bound solely for
# experimentation and testing purposes in the WASM build.
fiddle_dummy_exports = ,_fiddle_experiment,_fiddle_the_db,_fiddle_db_arg
emcc_opt = -Oz
emcc_flags = $(emcc_opt) $(SHELL_OPT) \
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap \
- -sEXPORTED_FUNCTIONS=_fiddle_exec$(fiddle_dummy_exports) \
- --pre-js $(fiddle_dir)/module-pre.js \
- --post-js $(fiddle_dir)/module-post.js \
- --shell-file $(fiddle_dir)/fiddle.in.html \
+ -sEXPORTED_FUNCTIONS=_fiddle_exec,_fiddle_interrupt$(fiddle_dummy_exports) \
$(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
+$(fiddle_module_js): Makefile sqlite3.c shell.c
emcc -o $@ $(emcc_flags) sqlite3.c shell.c
-fiddle: $(fiddle_html)
+fiddle: $(fiddle_module_js)
--- /dev/null
+/*
+ 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.
+
+ 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.
+
+ - 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. The status text is mostly in some
+ undocumented(?) format emited by the emscripten generated
+ module-loading code, encoding progress info within it.
+
+ - 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.
+
+ 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.
+
+ - 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.
+*/
+
+const thisWorker = self;
+
+const wMsg = (type,data)=>postMessage({type, data});
+
+self.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');
+ Module.setStatus = function(text) {
+ if(text) console.error('[post-exception status] ' + text);
+ };
+};
+
+self.Module = {
+/* ^^^ cannot declare that const because fiddle-module.js
+ (auto-generated) includes a decl for it and runs in this scope. */
+ preRun: [],
+ postRun: [],
+ //onRuntimeInitialized: function(){},
+ print: function(text){wMsg('stdout', Array.prototype.slice.call(arguments));},
+ printErr: function(text){wMsg('stderr', Array.prototype.slice.call(arguments));},
+ setStatus: function f(text){wMsg('module',{type:'status',data:text});},
+ totalDependencies: 0,
+ monitorRunDependencies: function(left) {
+ this.totalDependencies = Math.max(this.totalDependencies, left);
+ this.setStatus(left
+ ? ('Preparing... (' + (this.totalDependencies-left)
+ + '/' + this.totalDependencies + ')')
+ : 'All downloads complete.');
+ }
+};
+
+const shellExec = function f(sql){
+ if(!f._) f._ = Module.cwrap('fiddle_exec', null, ['string']);
+ if(Module._isDead){
+ wMsg('stderr', "shell module has exit()ed. Cannot run SQL.");
+ return;
+ }
+ wMsg('working','start');
+ try {
+ if(f._running) wMsg('stderr','Cannot run multiple commands concurrently.');
+ else{
+ f._running = true;
+ f._(sql);
+ }
+ } finally {
+ wMsg('working','end');
+ delete f._running;
+ }
+};
+
+self.onmessage = function(ev){
+ ev = ev.data;
+ //console.debug("worker: onmessage.data",ev);
+ switch(ev.type){
+ case 'shellExec': shellExec(ev.data); return;
+ };
+ console.warn("Unknown fiddle-worker message type:",ev);
+};
+self.Module.setStatus('Downloading...');
+importScripts('fiddle-module.js')
+/* loads module and notifies, via Module.setStatus(), when it's done loading. */;
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
- <title>sqlite3 fiddle (experimental!)</title>
- <!-- script src="jqterm/jqterm-bundle.min.js"></script>
- <link rel="stylesheet" href="jqterm/jquery.terminal.min.css"/ -->
+ <title>sqlite3 fiddle</title>
+ <!--script src="jqterm/jqterm-bundle.min.js"></script>
+ <link rel="stylesheet" href="jqterm/jquery.terminal.min.css"/-->
<style>
/* emcscript-related styling, used during the intialization phase... */
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
div.emscripten { text-align: center; }
div.emscripten_border { border: 1px solid black; }
- #spinner { overflow: visible; }
- #spinner > * {
+ #module-spinner { overflow: visible; }
+ #module-spinner > * {
margin-top: 1em;
}
.spinner {
</head>
<body>
<header id='titlebar'><span>sqlite3 fiddle</span></header>
- <figure id="spinner">
+ <figure id="module-spinner">
<div class="spinner"></div>
<div class='center'><strong>Initializing app...</strong></div>
<div class='center'>
failed and the JavaScript console may contain clues as to why.
</div>
</figure>
- <div class="emscripten" id="status">Downloading...</div>
+ <div class="emscripten" id="module-status">Downloading...</div>
<div class="emscripten">
- <progress value="0" max="100" id="progress" hidden='1'></progress>
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
</div>
<div id='view-terminal' class='app-view hidden initially-hidden'>
INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);
SELECT * FROM t;</textarea>
<div class='button-bar'>
- <button id='btn-run'>Run</button>
+ <button id='btn-shell-exec'>Run</button>
<button id='btn-clear'>Clear Input</button>
<button data-cmd='.help'>Help</button>
<select id='select-examples'></select>
</div> <!-- #view-split -->
<!-- Maintenance notes:
- - emscripten module init goes is in module-pre.js and gets
- prepended to the generated script code.
-
- - App-specific code is in module-post.js and gets appended to
- the generated script code.
-
- - The following placeholder (if you're reading this in the
- input template file) gets replaced by a generated
- amalgamation of: module-pre.js, emcc-generated bootstrapping
- code, and module-post.js.
+ ... TODO... currently being refactored...
-->
- {{{ SCRIPT }}}
+ <script src="fiddle.js"></script>
</body>
</html>
--- /dev/null
+/*
+ 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';
+
+ /**
+ 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(/>/g, ">");
+ //text = text.replace('\n', '<br>', '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) window.Module.jqTerm.echo(text);
+ f._.value += text + "\n";
+ if(this.config.autoScrollOutput){
+ f._.scrollTop = f._.scrollHeight;
+ }
+ },
+ _msgMap: {},
+ 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;
+ },
+ 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;
+ },
+ 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;
+ }
+ };
+
+ 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]);
+ };
+
+ const statusElement = E('#module-status');
+ const progressElement = E('#module-progress');
+ const spinnerElement = E('#module-spinner');
+
+ SF.addMsgHandler('module', function f(ev){
+ ev = ev.data;
+ //console.log("Module status:",ev);
+ if('status'!==ev.type) return;
+ /* This weird handling of the ev.data is simply how
+ emscripten's auto-generated code notifies the client of
+ load progress. */
+ let text = ev.data;
+ 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 {
+ console.log("Finalizing status.");
+ statusElement.remove();
+ SF.clearMsgHandlers('module');
+ 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'));
+
+ 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(){
+ const sql = taInput.value.trim();
+ if(sql) SF.dbExec(sql);
+ },false);
+
+ const preStartWork = function f(){
+ if(!f._){
+ const title = E('title');
+ f._ = {
+ btnLabel: btnShellExec.innerText,
+ pageTitle: title,
+ pageTitleOrig: title.innerText
+ };
+ }
+ f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
+ btnShellExec.setAttribute('disabled','disabled');
+ };
+
+ /* Sends the given text to the db module to evaluate as if it
+ had been entered in the sqlite3 CLI 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. */
+ SF.dbExec = function f(sql){
+ if(this.config.autoClearOutput){
+ this.echo._clearPending = true;
+ }
+ preStartWork();
+ this.wMsg('shellExec',sql);
+ };
+
+ SF.addMsgHandler('working',function f(ev){
+ if('start' === ev.data){
+ //btnShellExec.innerText = "Working..."; // forces layout reflow (annoying)
+ }else if('end' === ev.data){
+ preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
+ btnShellExec.innerText = preStartWork._.btnLabel;
+ btnShellExec.removeAttribute('disabled');
+ }
+ });
+
+ /* 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
+ SF.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 = !!SF.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(){
+ SF.config[this.dataset.config] = this.checked;
+ }, false);
+ });
+ /* For each button with data-cmd=X, map a click handler which
+ calls dbExec(X). */
+ const cmdClick = function(){SF.dbExec(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'),
+ E('body > footer')
+ ];
+ 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;
+ //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(dbExec,{
+ 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"));
+ 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()*/;
+})();
+++ /dev/null
-/*
- 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 --post-js file for emcc. It gets appended to the
- generated fiddle.js. It should contain (or load) 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'),
- E('body > footer')
- ];
- 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;
- //doExec(this.value);
- });
- })()/* example queries */;
-
- 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('#view-terminal').empty();
- Module.jqTerm = eTerm.terminal(doExec,{
- 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"));
- 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*/;
- }
- doExec(null/*init the db and output the header*/);
- Module.print('\nThis 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();
-};
+++ /dev/null
-/*
- 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 --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(/>/g, ">");
- //text = text.replace('\n', '<br>', '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);
- };
- };
-})();
-C fiddle:\sadd\sa\sselection\slist\sof\sexample\squeries.
-D 2022-05-21T00:45:46.764
+C fiddle:\srefactor\sinto\smain\sthread\s(UI)\sand\sworker\sthread\s(wasm\smodule).\sAdded\sbits\sneeded\sto\ssupport\striggering\ssqlite3_interrupt()\sbut\sdo\snot\syet\shave\sa\ssecond\sSharedWorker\sto\stest\sit\swith.
+D 2022-05-21T14:19:05.384
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
-F Makefile.in e3327ed68a4fd74f582db211b2762dda06d4a7f87220eaaaf97f63a200fdbefa
+F Makefile.in 91cf7d4ee6e97dd84cd5cdf541df498e2df038dd83f4b30d1ed8b19fbee3bbdd
F Makefile.linux-gcc f609543700659711fbd230eced1f01353117621dccae7b9fb70daa64236c5241
F Makefile.msc b28a8a7a977e7312f6859f560348e1eb110c21bd6cf9fab0d16537c0a514eef3
F README.md 8b8df9ca852aeac4864eb1e400002633ee6db84065bd01b78c33817f97d31f5e
F ext/expert/sqlite3expert.h ca81efc2679a92373a13a3e76a6138d0310e32be53d6c3bfaedabd158ea8969b
F ext/expert/test_expert.c d56c194b769bdc90cf829a14c9ecbc1edca9c850b837a4d0b13be14095c32a72
F ext/fiddle/Makefile b2904d52c10a7c984cfab95c54fb85f33aa8a6b2653faf1527d08ce57114be46
-F ext/fiddle/fiddle.in.html 15e20c0b235425e88dc622623146c8f44005ddd875e4843f1904823ad1ee16a3
+F ext/fiddle/fiddle-worker.js eae61419d5a7fcc9c4fcfda157ca0fe0a36b690b716899e5ec54a6d20c7037a9
+F ext/fiddle/fiddle.html f536878dbaa35ba4d9ad8c87dda7fb2ea5502fdd824577d83b2265d65b8ca4d1 w ext/fiddle/fiddle.in.html
+F ext/fiddle/fiddle.js 76a490d59bf0e09fb8a9610e6fa5c5032ee44680fbf9773b55974cb50d6dc3dc
F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf
-F ext/fiddle/module-post.js ae25e1e0e77f3856d62798fe72aeae65614687ffa2a3d1f2be3949470c8bfe20
-F ext/fiddle/module-pre.js baee03024cff65ea70ae8d67116e9c0b44723217005e04400930ad4b48be51f4
F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e
F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b
F ext/fts1/ft_hash.h 06df7bba40dadd19597aa400a875dbc2fed705ea
F src/resolve.c a4eb3c617027fd049b07432f3b942ea7151fa793a332a11a7d0f58c9539e104f
F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92
F src/select.c 74060a09f66c0c056f3c61627e22cb484af0bbfa29d7d14dcf17c684742c15de
-F src/shell.c.in c2d899a884cb0304cfd12e3d37c8152be5c578f8bed0e3dae53f92130363ccfc
+F src/shell.c.in 55d71bf8c7a8f2a66bc5f99cd76f226790b291599b83415533dad84a553ed806
F src/sqlite.h.in d15c307939039086adca159dd340a94b79b69827e74c6d661f343eeeaefba896
F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8
F src/sqlite3ext.h a988810c9b21c0dc36dc7a62735012339dc76fc7ab448fb0792721d30eacb69d
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P 31706ef851f7d55e2fe865933ec03f42dfa8b49e6d1f12b6c016b5c1d3460504
-R e3bf69409e6eaf22474ba05290f0bc56
+P 74abf03977e1ff8c6043defa38211cdfcfbba1979771b90ed9d3dbc99750fe9f
+R 209182df1b3b6bd2044e6acab7e588b4
U stephan
-Z 4e174f238744adc17a78f8f77017822d
+Z 8cd34b489ca85b418bdc02d766bcd2dc
# Remove this line to create a well-formed Fossil manifest.
-74abf03977e1ff8c6043defa38211cdfcfbba1979771b90ed9d3dbc99750fe9f
\ No newline at end of file
+5ff3326856bc190cee15a5fca5ded89aacc4bf931a8df98726a872b310e2a4fc
\ No newline at end of file
return arg;
}
+/*
+** Intended to be called via a SharedWorker() while a separate
+** SharedWorker() (which manages the wasm module) is performing work
+** which should be interrupted.
+*/
+void fiddle_interrupt(void){
+ if(globalDb) sqlite3_interrupt(globalDb);
+}
+
/*
** Trivial exportable function for emscripten. Needs to be exported using:
**