From: stephan Date: Tue, 24 May 2022 22:16:12 +0000 (+0000) Subject: fiddle: added support for exporting (downloading) the current db file. To do this... X-Git-Tag: version-3.39.0~102^2~2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=80bf86967f291019ab4bb686dd92b3a3c5a402a0;p=thirdparty%2Fsqlite.git fiddle: added support for exporting (downloading) the current db file. To do this we had to fall back to named dbs, instead of defaulting to an in-memory one, but the virtual filesystem is an in-memory FS, so the end effect is the same. FossilOrigin-Name: 7c7fd34c8a05832a3973aaffe696250cb4d2a0b1646c9bfbe83970daf33cd817 --- diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.fiddle b/ext/fiddle/EXPORTED_FUNCTIONS.fiddle index 7ec5df3e47..6356910f3b 100644 --- a/ext/fiddle/EXPORTED_FUNCTIONS.fiddle +++ b/ext/fiddle/EXPORTED_FUNCTIONS.fiddle @@ -3,3 +3,4 @@ _fiddle_interrupt _fiddle_experiment _fiddle_the_db _fiddle_db_arg +_fiddle_db_filename diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 index 45f9266a38..3127b294d9 100644 --- a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 +++ b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 @@ -30,6 +30,7 @@ _sqlite3_finalize _sqlite3_interrupt _sqlite3_libversion _sqlite3_open +_sqlite3_open_v2 _sqlite3_prepare_v2 _sqlite3_prepare_v2 _sqlite3_reset diff --git a/ext/fiddle/fiddle-worker.js b/ext/fiddle/fiddle-worker.js index 6a687e42cb..8d331bff15 100644 --- a/ext/fiddle/fiddle-worker.js +++ b/ext/fiddle/fiddle-worker.js @@ -89,7 +89,18 @@ */ "use strict"; -const wMsg = (type,data)=>postMessage({type, data}); +/** + Posts a message in the form {type,data} unless passed more than 2 + args, in which case it posts {type, data:[arg1...argN]}. +*/ +const wMsg = function(type,data){ + postMessage({ + type, + data: arguments.length<3 + ? data + : Array.prototype.slice.call(arguments,1) + }); +}; self.onerror = function(/*message, source, lineno, colno, error*/) { const err = arguments[4]; @@ -160,6 +171,16 @@ self.Module = { }; const Sqlite3Shell = { + /** Returns the name of the currently-opened db. */ + dbFilename: function f(){ + if(!f._) f._ = Module.cwrap('fiddle_db_filename', "string", ['string']); + return f._(); + }, + /** + Runs the given text through the shell as if it had been typed + in by a user. Fires a working/start event before it starts and + working/end event when it finishes. + */ exec: function f(sql){ if(!f._) f._ = Module.cwrap('fiddle_exec', null, ['string']); if(Module._isDead){ @@ -168,14 +189,15 @@ const Sqlite3Shell = { } wMsg('working','start'); try { - if(f._running) wMsg('stderr','Cannot run multiple commands concurrently.'); - else{ + if(f._running){ + wMsg('stderr','Cannot run multiple commands concurrently.'); + }else{ f._running = true; f._(sql); } } finally { - wMsg('working','end'); delete f._running; + wMsg('working','end'); } }, /* Interrupt can't work: this Worker is tied up working, so won't get the @@ -198,6 +220,37 @@ self.onmessage = function f(ev){ switch(ev.type){ case 'shellExec': Sqlite3Shell.exec(ev.data); return; case 'interrupt': Sqlite3Shell.interrupt(); return; + /** Triggers the export of the current db. Fires an + event in the form: + + {type:'db-export', + data:{ + filename: name of db, + buffer: contents of the db file (Uint8Array), + error: on error, a message string and no buffer property. + } + } + */ + case 'db-export': { + const fn = Sqlite3Shell.dbFilename(); + wMsg('stdout',"Exporting",fn+"."); + const fn2 = fn ? fn.split(/[/\\]/).pop() : null; + try{ + if(!fn2) throw new Error("DB appears to be closed."); + wMsg('db-export',{ + filename: fn2, + buffer: FS.readFile(fn, {encoding:"binary"}) + }); + }catch(e){ + /* Post a failure message so that UI elements disabled + during the export can be re-enabled. */ + wMsg('db-export',{ + filename: fn, + error: e.message + }); + } + return; + } case 'open': { /* Expects: { buffer: ArrayBuffer | Uint8Array, @@ -212,18 +265,23 @@ self.onmessage = function f(ev){ wMsg('stderr',"'open' expects {buffer:Uint8Array} containing an uploaded db."); return; } - if(f.cache.prevFilename){ - FS.unlink(f.cache.prevFilename); - /* Noting that it might not actually be removed until - the current db handle closes it. */ - f.cache.prevFilename = null; - } - const fn = "db-"+((Math.random() * 10000000) | 0)+ - "-"+((Math.random() * 10000000) | 0)+".sqlite3"; + const fn = ( + opt.filename + ? opt.filename.split(/[/\\]/).pop().replace('"','_') + : ("db-"+((Math.random() * 10000000) | 0)+ + "-"+((Math.random() * 10000000) | 0)+".sqlite3") + ); + /* We cannot delete the existing db file until the new one + is installed, which means that we risk overflowing our + quota (if any) by having both the previous and current + db briefly installed in the virtual filesystem. */ FS.createDataFile("/", fn, buffer, true, true); - f.cache.prevFilename = fn; - Sqlite3Shell.exec(".open /"+fn); - wMsg('stdout',"Replaced DB with "+(opt.filename || fn)+"."); + const oldName = Sqlite3Shell.dbFilename(); + Sqlite3Shell.exec('.open "/'+fn+'"'); + if(oldName !== fn){ + FS.unlink(oldName); + } + wMsg('stdout',"Replaced DB with",fn+"."); return; } }; diff --git a/ext/fiddle/fiddle.html b/ext/fiddle/fiddle.html index 8db780e4d7..3f00e8eb7c 100644 --- a/ext/fiddle/fiddle.html +++ b/ext/fiddle/fiddle.html @@ -169,6 +169,9 @@ + + +
diff --git a/ext/fiddle/fiddle.js b/ext/fiddle/fiddle.js index 796c6bcedb..54d2be99c3 100644 --- a/ext/fiddle/fiddle.js +++ b/ext/fiddle/fiddle.js @@ -219,7 +219,7 @@ },false); const btnInterrupt = E("#btn-interrupt"); - btnInterrupt.classList.add('hidden'); + //btnInterrupt.classList.add('hidden'); /** To be called immediately before work is sent to the worker. Updates some UI elements. The 'working'/'end' event will apply the inverse, undoing the bits this @@ -308,20 +308,55 @@ SF.wMsg('interrupt'); }); - const fileSelector = E('#load-db'); - fileSelector.addEventListener('change',function(){ + /** Initiate a download of the db. */ + const btnExport = E('#btn-export'); + const eDisableDuringExport = [ + /* UI elements to disable while export is running. Normally + the export is fast enough that this won't matter, but we + really don't want to be reading (from outside of sqlite) + the db when the user taps btnShellExec. */ + btnShellExec, btnExport + ]; + btnExport.addEventListener('click',function(){ + eDisableDuringExport.forEach(e=>e.setAttribute('disabled','disabled')); + SF.wMsg('db-export'); + }); + SF.addMsgHandler('db-export', function(ev){ + eDisableDuringExport.forEach(e=>e.removeAttribute('disabled')); + ev = ev.data; + if(ev.error){ + SF.echo("Export failed:",ev.error); + return; + } + const blob = new Blob([ev.buffer], {type:"application/x-sqlite3"}); + const a = document.createElement('a'); + document.body.appendChild(a); + a.href = window.URL.createObjectURL(blob); + a.download = ev.filename; + a.addEventListener('click',function(){ + setTimeout(function(){ + SF.echo("Exported (possibly auto-downloaded):",ev.filename); + window.URL.revokeObjectURL(a.href); + a.remove(); + },0); + }); + a.click(); + }); + + E('#load-db').addEventListener('change',function(){ const f = this.files[0]; const r = new FileReader(); const status = {loaded: 0, total: 0}; - fileSelector.setAttribute('disabled','disabled'); + this.setAttribute('disabled','disabled'); r.addEventListener('loadstart', function(){ SF.echo("Loading",f.name,"..."); }); r.addEventListener('progress', function(ev){ SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes."); }); + const that = this; r.addEventListener('load', function(){ - fileSelector.removeAttribute('disabled'); + that.removeAttribute('disabled'); SF.echo("Loaded",f.name+". Opening db..."); SF.wMsg('open',{ filename: f.name, @@ -329,11 +364,11 @@ }); }); r.addEventListener('error',function(){ - fileSelector.removeAttribute('disabled'); + that.removeAttribute('disabled'); SF.echo("Loading",f.name,"failed for unknown reason."); }); r.addEventListener('abort',function(){ - fileSelector.removeAttribute('disabled'); + that.removeAttribute('disabled'); SF.echo("Cancelled loading of",f.name+"."); }); r.readAsArrayBuffer(f); diff --git a/ext/fiddle/sqlite3-api.js b/ext/fiddle/sqlite3-api.js index 1bf1629003..7dff49980b 100644 --- a/ext/fiddle/sqlite3-api.js +++ b/ext/fiddle/sqlite3-api.js @@ -206,6 +206,8 @@ ["sqlite3_interrupt", "void", ["number"]], ["sqlite3_libversion", "string", []], ["sqlite3_open", "number", ["string", "number"]], + //["sqlite3_open_v2", "number", ["string", "number", "number", "string"]], + //^^^^ TODO: add the flags needed for the 3rd arg ["sqlite3_prepare_v2", "number", ["number", "string", "number", "number", "number"]], ["sqlite3_prepare_v2_sqlptr", "sqlite3_prepare_v2", /* Impl which requires that the 2nd argument be a pointer to @@ -253,34 +255,40 @@ }; /** - The DB class wraps a sqlite3 db handle. Its argument may either - be a db name or a Uint8Array containing a binary image of a - database. If the name is not provided or is an empty string, - ":memory:" is used. A string name other than ":memory:" or "" - will currently fail to open, for lack of a filesystem to - load it from. If given a blob, a random name is generated. - - Achtung: all arguments other than those specifying an - in-memory db are currently untested for lack of an app - to test them in. + The DB class wraps a sqlite3 db handle. + + It accepts the following argument signatures: + + - () + - (undefined) (same effect as ()) + - (Uint8Array holding an sqlite3 db image) + + It always generates a random filename and sets is to + the `filename` property of this object. + + Developer's note: the reason it does not (any longer) support + ":memory:" as a name is because we can apparently only export + images of DBs which are stored in the pseudo-filesystem + provided by the JS APIs. Since exporting and importing images + is an important usability feature for this class, ":memory:" + DBs are not supported (until/unless we can find a way to export + those as well). The naming semantics will certainly evolve as + this API does. */ - const DB = function(name/*TODO? openMode flags*/){ - let fn, buff; + const DB = function(arg){ + const fn = "db-"+((Math.random() * 10000000) | 0)+ + "-"+((Math.random() * 10000000) | 0)+".sqlite3"; + let buffer; if(name instanceof Uint8Array){ - buff = name; - name = undefined; - fn = "db-"+((Math.random() * 10000000) | 0)+ - "-"+((Math.random() * 10000000) | 0)+".sqlite3"; - }else if(":memory:" === name || "" === name){ - fn = name || ":memory:"; - name = undefined; - }else if('string'!==typeof name){ - toss("TODO: support blob image of db here."); - }else{ - fn = name; + buffer = arg; + arg = undefined; + }else if(arguments.length && undefined!==arg){ + toss("Invalid arguments to DB constructor.", + "Expecting no args, undefined, or a", + "sqlite3 file as a Uint8Array."); } - if(buff){ - FS.createDataFile("/", fn, buff, true, true); + if(buffer){ + FS.createDataFile("/", fn, buffer, true, true); } setValue(pPtrArg, 0, "i32"); this.checkRc(api.sqlite3_open(fn, pPtrArg)); @@ -413,6 +421,7 @@ Object.values(this._udfs).forEach(Module.removeFunction); delete this._udfs; delete this._statements; + delete this.filename; api.sqlite3_close_v2(this._pDb); delete this._pDb; } @@ -779,6 +788,32 @@ if(stmt) stmt.finalize(); } return rc; + }, + + /** + Exports a copy of this db's file as a Uint8Array and + returns it. It is technically not legal to call this while + any prepared statement are currently active. Throws if this + db is not open. + + Maintenance reminder: the corresponding sql.js impl of this + feature closes the current db, finalizing any active + statements and (seemingly unnecessarily) destroys any UDFs, + copies the file, and then re-opens it (without restoring + the UDFs). Those gymnastics are not necessary on the tested + platform but might be necessary on others. Because of that + eventuality, this interface currently enforces that no + statements are active when this is run. It will throw if + any are. + */ + exportBinaryImage: function(){ + affirmDbOpen(this); + if(Object.keys(this._statements).length){ + toss("Cannot export with prepared statements active!", + "finalize() all statements and try again."); + } + const img = FS.readFile(this.filename, {encoding:"binary"}); + return img; } }/*DB.prototype*/; diff --git a/manifest b/manifest index 85d3e696ed..803bf329b9 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C fiddle:\sinitial\swork\son\sloading\sa\sclient-side\sdb\sfile.\sWorks\sbut\srequires\ssome\scleanup.\sExport\sis\snot\syet\simplemented. -D 2022-05-24T19:01:21.099 +C fiddle:\sadded\ssupport\sfor\sexporting\s(downloading)\sthe\scurrent\sdb\sfile.\sTo\sdo\sthis\swe\shad\sto\sfall\sback\sto\snamed\sdbs,\sinstead\sof\sdefaulting\sto\san\sin-memory\sone,\sbut\sthe\svirtual\sfilesystem\sis\san\sin-memory\sFS,\sso\sthe\send\seffect\sis\sthe\ssame. +D 2022-05-24T22:16:12.220 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -55,17 +55,17 @@ 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/EXPORTED_FUNCTIONS.fiddle 487fc7c83d45c48326f731c89162ed17ab15767e5efede8999d7d6c6e2d04c0f -F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 07b573a1830cb2d38ed347cf2a4139ec3b9c0f69748da6a2d8356b426c807694 +F ext/fiddle/EXPORTED_FUNCTIONS.fiddle 2f7c561af85e6d711fb42f395bc0074b6e6fcf16bc57d495ce4e1c3d0484c5d2 +F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 540b9dec63a3a62a256e2f030827848a92e9b9d9b6fa5c0188295a4a1c5382cd F ext/fiddle/EXPORTED_RUNTIME_METHODS ff64aea52779b0d4a838268275fe02adf6f2fdf4d9ce21c22d104bf3d7597398 F ext/fiddle/Makefile 2608fe0c56fa8f9cdf17e28d2be6def550a2fe987db5f7fc06d0210bfc868258 F ext/fiddle/SqliteTestUtil.js e3094833660a6ddd40766b802901b5861b37f0b89c6c577ee0ce4c9d36399e61 F ext/fiddle/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f -F ext/fiddle/fiddle-worker.js 12b51133e47d1bfaf3e09bb0fa956c0f24bada42d25d4d67322ed86ca94ff634 -F ext/fiddle/fiddle.html caee127caba2e2a797954ae911f071a6fd224c07e108171d24b01940058b4564 -F ext/fiddle/fiddle.js e4b10f2b7c325060d0c2ff49408cf5ca1439f8bac2df97781fe7213eaac1d6a3 +F ext/fiddle/fiddle-worker.js 2c4e323ad5c94b863271d7779436a6e5291d3cc9dd2f5e5d067a22c2a539ff22 +F ext/fiddle/fiddle.html 468723b7a0bbdc92e24990c72e4b8bffdc1a8d605f91b595e36bcd8a6c840ff8 +F ext/fiddle/fiddle.js bc08897ceee8b4f32bb32dabab9c28d5aa1f252a88ac14b51933d6fa36e1c34c F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf -F ext/fiddle/sqlite3-api.js 5b47e19d2e34cf0d6f09e1ea845f4e151879aaa37dc06eb88b21865efb94bd8a +F ext/fiddle/sqlite3-api.js ce08520b8117e4fbbbeb02d8d047defd4e8507d687e76d20a39f12401bad0219 F ext/fiddle/testing-common.js a2527fd8dfb500bad9b434ae2645bb91489792115ee1e1b4b53cac4e9198992a F ext/fiddle/testing.css 750572dded671d2cf142bbcb27af5542522ac08db128245d0b9fe410aa1d7f2a F ext/fiddle/testing1.html c00236d71b7f7523b722ae2f79cb2b734e6ed4ff16102fa69974145f6e2bfc95 @@ -569,7 +569,7 @@ F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c F src/resolve.c a4eb3c617027fd049b07432f3b942ea7151fa793a332a11a7d0f58c9539e104f F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 F src/select.c 4f13d01141caae4f982125fecf50703c746ab621c57ca0aa747fab2b28e88c1e -F src/shell.c.in b0adbcaaa7941284194b1e3ca2b89fb8a3cb96d395ea2f3be46b85f166716b09 +F src/shell.c.in 20932a2f318c77ae4540427dab75de9e91b5d2b9f8201c5953931e84c0d3cc5d F src/sqlite.h.in d15c307939039086adca159dd340a94b79b69827e74c6d661f343eeeaefba896 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h a988810c9b21c0dc36dc7a62735012339dc76fc7ab448fb0792721d30eacb69d @@ -1969,11 +1969,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 2b6ebba26d936ae7b9acf7d4bd15e82cbfabda22e1044b3dd838c7b07095100e -R 62090175f25fd90e2c5311e4c96dd351 -T *branch * fiddle-local-db -T *sym-fiddle-local-db * -T -sym-trunk * Cancelled\sby\sbranch. +P 0fa8378c006fcf2311772d36cf2e3c2cd8e8648f671de89ee9832e2e1a06ef49 +R 39035a17b33485a10b3b4dd052e35f61 U stephan -Z 3bd69b4520f16e04c7fb41fc28b99798 +Z 0fc30b16e829c9b67855feacd792a751 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 4689d8f38c..bb0697fefc 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -0fa8378c006fcf2311772d36cf2e3c2cd8e8648f671de89ee9832e2e1a06ef49 \ No newline at end of file +7c7fd34c8a05832a3973aaffe696250cb4d2a0b1646c9bfbe83970daf33cd817 \ No newline at end of file diff --git a/src/shell.c.in b/src/shell.c.in index 06a4b81137..f6c24ebdbd 100644 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -12618,6 +12618,17 @@ void fiddle_interrupt(void){ if(globalDb) sqlite3_interrupt(globalDb); } +/* +** Returns the filename of the given db name, assuming +** "main" if zDbName is NULL. Returns NULL globalDb is +** not opened. +*/ +const char * fiddle_db_filename(const char * zDbName){ + return globalDb + ? sqlite3_db_filename(globalDb, zDbName ? zDbName : "main") + : NULL; +} + /* ** Trivial exportable function for emscripten. Needs to be exported using: ** @@ -12644,9 +12655,12 @@ void fiddle_exec(const char * zSql){ ); puts("WASM shell"); puts("Enter \".help\" for usage hints."); - if(once>0) open_db(&shellState, 0); + if(once>0){ + shellState.pAuxDb->zDbFilename = "/fiddle.sqlite3"; + open_db(&shellState, 0); + } if(shellState.db){ - puts("Connected to a transient in-memory database."); + printf("Connected to %s.\n", fiddle_db_filename(NULL)); }else{ fprintf(stderr,"ERROR initializing db!\n"); return;