Initial version of an sqlfiddle-style application using a WASM build of the sqlite3 shell.

FossilOrigin-Name: af9c21c9e0caf05adac7a9fcde39a9164c89f1c78b767b6fdd74a1405a3d373f
This commit is contained in:
stephan
2022-05-18 17:14:24 +00:00
parent b8b2d9c5e1
commit f8cd3d2b5c
7 changed files with 443 additions and 16 deletions
+21
View File
@@ -1512,3 +1512,24 @@ 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_tmpl = $(fiddle_dir)/fiddle.in.html
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_flags = -sEXPORTED_RUNTIME_METHODS=ccall,cwrap \
-sEXPORTED_FUNCTIONS=_fiddle_exec \
--shell-file $(fiddle_tmpl)
$(fiddle_html): Makefile sqlite3.c shell.c $(fiddle_tmpl)
emcc -o $@ $(emcc_flags) sqlite3.c shell.c
fiddle: $(fiddle_html)
+5
View File
@@ -0,0 +1,5 @@
default:
make -C ../.. fiddle
clean:
make -C ../../ clean-fiddle
+208
View File
@@ -0,0 +1,208 @@
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>sqlite3 fiddle</title>
<style>
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
textarea { font-family: monospace; }
div.emscripten { text-align: center; }
div.emscripten_border { border: 1px solid black; }
.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);}
}
body > header {
font-size: 130%;
font-weight: bold;
}
#main-wrapper {
display: flex;
flex-direction: column;
}
#main-wrapper.side-by-side {
flex-direction: row;
}
.ta-wrapper{
display: flex;
flex-direction: column;
align-items: stretch;
flex: 1 1 auto;
}
.button-bar {
display: flex;
justify-content: center;
}
.button-bar button {
margin: 0.25em 1em;
}
</style>
</head>
<body>
<header>sqlite3 fiddle</header>
<figure style="overflow:visible;" id="spinner"><div class="spinner"></div><center style="margin-top:0.5em"><strong>emscripten</strong></center></figure>
<div class="emscripten" id="status">Downloading...</div>
<div class="emscripten">
<progress value="0" max="100" id="progress" hidden='1'></progress>
</div>
<div id='main-wrapper'>
<div class='ta-wrapper'>
<textarea id="input" rows="8">-- Use ctrl-enter or shift-enter to execute SQL
.nullvalue THIS_IS_NULL
.mode box
create table t(a,b);
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' disabled>Run</button>
<button id='btn-clear' disabled>Clear</button>
</div>
</div>
<div class='ta-wrapper'>
<textarea id="output" readonly rows="8"></textarea>
<div class='button-bar'>
<button id='btn-clear-output' disabled>Clear</button>
</div>
</div>
</div>
<fieldset>
<legend>Options</legend>
<div class=''>
<input type='checkbox' id='opt-cb-sbs'>
<label for='opt-cb-sbs'>Side-by-side</label>
</div>
</fieldset>
<hr>
<script type='text/javascript'>
(function(){
/**
Callback for the emscripten module init process, gets
passed the module object after all parts of the module
have been loaded and initialized.
*/
const doAppSetup = function(Module) {
const taInput = document.querySelector('#input');
const btnClearIn = document.querySelector('#btn-clear');
document.querySelectorAll('button').forEach(function(e){
e.removeAttribute('disabled');
});
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 = document.querySelector('#output');
const btnClearOut = document.querySelector('#btn-clear-output');
btnClearOut.addEventListener('click',function(){
taOutput.value = '';
},false);
const doExec = Module.cwrap('fiddle_exec', null, ['string']);
const btnRun = document.querySelector('#btn-run');
btnRun.addEventListener('click',function(){
const sql = taInput.value.trim();
if(sql){
doExec(sql);
}
},false);
doExec()/*sets up the db and outputs the header*/;
let e = document.querySelector('#opt-cb-sbs');
const mainWrapper = document.querySelector('#main-wrapper');
e.addEventListener('change', function(){
mainWrapper.classList[this.checked ? 'add' : 'remove']('side-by-side');
}, false);
};
/**
What follow is part of the emscripten core setup. Do not modify without
understanding what it's doing...
*/
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var spinnerElement = document.getElementById('spinner');
window.Module = {
preRun: [],
postRun: [],
onRuntimeInitialized: function(){
doAppSetup(this);
},
print: (function f() {
if(!f._){
f._ = document.getElementById('output');
}
f._.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, "&amp;");
//text = text.replace(/</g, "&lt;");
//text = text.replace(/>/g, "&gt;");
//text = text.replace('\n', '<br>', 'g');
//console.log("arguments",arguments);
console.log(text);
f._.value += text + "\n";
f._.scrollTop = f._.scrollHeight; // focus on bottom
};
})(),
setStatus: function(text) {
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
if (text === Module.setStatus.last.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
Module.setStatus.last.time = now;
Module.setStatus.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.value = null;
progressElement.max = null;
progressElement.hidden = true;
if (!text) spinnerElement.hidden = true;
}
statusElement.innerHTML = text;
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
}
};
Module.printErr = Module.print/*redirect stderr*/;
Module.setStatus('Downloading...');
window.onerror = function() {
Module.setStatus('Exception thrown, see JavaScript console');
spinnerElement.style.display = 'none';
Module.setStatus = function(text) {
if (text) console.error('[post-exception status] ' + text);
};
};
})();
</script>
{{{ SCRIPT }}}
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
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.
[emscripten]: https://emscripten.org
[althttpd]: https://sqlite.org/althttpd
+14 -8
View File
@@ -1,9 +1,9 @@
C Fix\sharmless\scompiler\swarnings\sin\sthe\snew\sunixFullPathname\simplementation.
D 2022-05-17T15:11:57.632
C Initial\sversion\sof\san\ssqlfiddle-style\sapplication\susing\sa\sWASM\sbuild\sof\sthe\ssqlite3\sshell.
D 2022-05-18T17:14:24.622
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
F Makefile.in b210ad2733317f1a4353085dfb9d385ceec30b0e6a61d20a5accabecac6b1949
F Makefile.in ff32504cde350caaf4d52abfdca5dd1fa88b21e53071a0f9cc3539b6789c3606
F Makefile.linux-gcc f609543700659711fbd230eced1f01353117621dccae7b9fb70daa64236c5241
F Makefile.msc b28a8a7a977e7312f6859f560348e1eb110c21bd6cf9fab0d16537c0a514eef3
F README.md 8b8df9ca852aeac4864eb1e400002633ee6db84065bd01b78c33817f97d31f5e
@@ -55,6 +55,9 @@ 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 ea647919e6ac4b50edde1490f60ee87e8ccd75141e4aa650718c6f28eb323bbc
F ext/fiddle/fiddle.in.html 85db5e736f82fd2cae1c4f61b5af62239cc5db363b9eb6f4e383e0d97c59b322
F ext/fiddle/index.md 08d25ec6fe2a56923e8ea6e5d6c80907bf3a60f9c40a6841a8f402e402dd5f22
F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e
F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b
F ext/fts1/ft_hash.h 06df7bba40dadd19597aa400a875dbc2fed705ea
@@ -554,7 +557,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 1892f21aafee8eca543881ef429f6166386ca1ae0051252e91d1235bd6f4217b
F src/sqlite.h.in d15c307939039086adca159dd340a94b79b69827e74c6d661f343eeeaefba896
F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8
F src/sqlite3ext.h a988810c9b21c0dc36dc7a62735012339dc76fc7ab448fb0792721d30eacb69d
@@ -1954,8 +1957,11 @@ 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
U drh
Z 99cc78493b68b2d09cacc03a9e60389c
P f7e1ceb5b59a876cfd04a8aac0ee2b322c970555b9c361b4953d711ef6596e37
R f9ce230cf39758aaeb408ee00f09f455
T *branch * fiddle
T *sym-fiddle *
T -sym-trunk *
U stephan
Z 33db0432b22b9541a19d442c2dec2c8e
# Remove this line to create a well-formed Fossil manifest.
+1 -1
View File
@@ -1 +1 @@
f7e1ceb5b59a876cfd04a8aac0ee2b322c970555b9c361b4953d711ef6596e37
af9c21c9e0caf05adac7a9fcde39a9164c89f1c78b767b6fdd74a1405a3d373f
+123 -7
View File
@@ -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
@@ -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 */
char const * zPos; /* Cursor pos into zInput */
} wasm;
#endif
};
#ifdef SQLITE_SHELL_WASM_MODE
static ShellState shellState;
#endif
/* Allowed values for ShellState.autoEQP
*/
@@ -4991,14 +5014,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
@@ -11467,6 +11492,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 +11906,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 +11920,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 +11940,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 +12202,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 +12470,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 +12505,45 @@ 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(char const * 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 */