]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
fiddle: added support for exporting (downloading) the current db file. To do this...
authorstephan <stephan@noemail.net>
Tue, 24 May 2022 22:16:12 +0000 (22:16 +0000)
committerstephan <stephan@noemail.net>
Tue, 24 May 2022 22:16:12 +0000 (22:16 +0000)
FossilOrigin-Name: 7c7fd34c8a05832a3973aaffe696250cb4d2a0b1646c9bfbe83970daf33cd817

ext/fiddle/EXPORTED_FUNCTIONS.fiddle
ext/fiddle/EXPORTED_FUNCTIONS.sqlite3
ext/fiddle/fiddle-worker.js
ext/fiddle/fiddle.html
ext/fiddle/fiddle.js
ext/fiddle/sqlite3-api.js
manifest
manifest.uuid
src/shell.c.in

index 7ec5df3e47ec6fa61cc5e8743caea18448269c30..6356910f3b5586b6059e1b5523fdc96df9858c28 100644 (file)
@@ -3,3 +3,4 @@ _fiddle_interrupt
 _fiddle_experiment
 _fiddle_the_db
 _fiddle_db_arg
+_fiddle_db_filename
index 45f9266a38e15e0bc949cf9fc538b3ac6d31b0d4..3127b294d93124b4ed514bb4e63f3c3861ee559b 100644 (file)
@@ -30,6 +30,7 @@ _sqlite3_finalize
 _sqlite3_interrupt
 _sqlite3_libversion
 _sqlite3_open
+_sqlite3_open_v2
 _sqlite3_prepare_v2
 _sqlite3_prepare_v2
 _sqlite3_reset
index 6a687e42cb63c3343eb9ed4070034e20192f0c63..8d331bff151453eb1ec55bb24801e9390f2d7693 100644 (file)
 */
 "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;
         }
     };
index 8db780e4d75625d9e2f27943969f56cb021785cf..3f00e8eb7c07bf637a78698da242a5417a6d3229 100644 (file)
             <input type='file' id='load-db'/>
             <label>Load DB</label>
           </span>
+          <span class='labeled-input'>
+            <button id='btn-export'>Download DB</button>
+          </span>
         </div>
       </fieldset>
       <div id='main-wrapper' class=''>
index 796c6bcedb8c95ee731bcc2af69ab85565515dd4..54d2be99c30fcc49ec386b56030aa7b77c72e7af 100644 (file)
         },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
             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,
                 });
             });
             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);
index 1bf16290034b56d67906314be93e5107b08d2baa..7dff49980bdb6818759d675946378c9e6cd54642 100644 (file)
         ["sqlite3_interrupt", "void", ["number"]],
         ["sqlite3_libversion", "string", []],
         ["sqlite3_open", "number", ["string", "number"]],
+        //["sqlite3_open_v2", "number", ["string", "number", "number", "string"]],
+        //^^^^ TODO: add the flags needed for the 3rd arg
         ["sqlite3_prepare_v2", "number", ["number", "string", "number", "number", "number"]],
         ["sqlite3_prepare_v2_sqlptr", "sqlite3_prepare_v2",
          /* Impl which requires that the 2nd argument be a pointer to
     };
 
     /**
-       The 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));
                 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;
             }
                 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*/;
 
index 85d3e696eda613e2a7ce5000ff0108fd92b0aa4c..803bf329b989077c86e3c4abe5390ff4ed40bf42 100644 (file)
--- 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.
index 4689d8f38c959729d744c90d6d95dfda0632646a..bb0697fefc4aaf68f9ab0a880e5255e372333171 100644 (file)
@@ -1 +1 @@
-0fa8378c006fcf2311772d36cf2e3c2cd8e8648f671de89ee9832e2e1a06ef49
\ No newline at end of file
+7c7fd34c8a05832a3973aaffe696250cb4d2a0b1646c9bfbe83970daf33cd817
\ No newline at end of file
index 06a4b8113727d8562d97711ec5d626fdb3c858dd..f6c24ebdbd7c3565640a358c9ef48df2734fa856 100644 (file)
@@ -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;