xref: /sqlite-3.40.0/ext/wasm/batch-runner.js (revision 8948fbee)
12cae138fSstephan/*
22cae138fSstephan  2022-08-29
32cae138fSstephan
42cae138fSstephan  The author disclaims copyright to this source code.  In place of a
52cae138fSstephan  legal notice, here is a blessing:
62cae138fSstephan
72cae138fSstephan  *   May you do good and not evil.
82cae138fSstephan  *   May you find forgiveness for yourself and forgive others.
92cae138fSstephan  *   May you share freely, never taking more than you give.
102cae138fSstephan
112cae138fSstephan  ***********************************************************************
122cae138fSstephan
13cdefd5d0Sstephan  A basic batch SQL runner for sqlite3-api.js. This file must be run in
142cae138fSstephan  main JS thread and sqlite3.js must have been loaded before it.
152cae138fSstephan*/
162cae138fSstephan'use strict';
172cae138fSstephan(function(){
182cae138fSstephan  const toss = function(...args){throw new Error(args.join(' '))};
192f06bf25Sstephan  const warn = console.warn.bind(console);
205b915007Sstephan  let sqlite3;
21d234902bSstephan  const urlParams = new URL(self.location.href).searchParams;
22d234902bSstephan  const cacheSize = (()=>{
23d234902bSstephan    if(urlParams.has('cachesize')) return +urlParams.get('cachesize');
24d234902bSstephan    return 200;
25d234902bSstephan  })();
262cae138fSstephan
275ad36319Sstephan  /** Throws if the given sqlite3 result code is not 0. */
285ad36319Sstephan  const checkSqliteRc = (dbh,rc)=>{
295ad36319Sstephan    if(rc) toss("Prepare failed:",sqlite3.capi.sqlite3_errmsg(dbh));
305ad36319Sstephan  };
315ad36319Sstephan
325ad36319Sstephan  const sqlToDrop = [
335ad36319Sstephan    "SELECT type,name FROM sqlite_schema ",
345ad36319Sstephan    "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
355ad36319Sstephan    "AND name NOT LIKE '\\_%' escape '\\'"
365ad36319Sstephan  ].join('');
375ad36319Sstephan
385ad36319Sstephan  const clearDbWebSQL = function(db){
395ad36319Sstephan    db.handle.transaction(function(tx){
405ad36319Sstephan      const onErr = (e)=>console.error(e);
415ad36319Sstephan      const callback = function(tx, result){
425ad36319Sstephan        const rows = result.rows;
435ad36319Sstephan        let i, n;
445ad36319Sstephan        i = n = rows.length;
455ad36319Sstephan        while(i--){
465ad36319Sstephan          const row = rows.item(i);
475ad36319Sstephan          const name = JSON.stringify(row.name);
485ad36319Sstephan          const type = row.type;
495ad36319Sstephan          switch(type){
50cb22bd80Sstephan              case 'index': case 'table':
51cb22bd80Sstephan              case 'trigger': case 'view': {
525ad36319Sstephan                const sql2 = 'DROP '+type+' '+name;
53cb22bd80Sstephan                tx.executeSql(sql2, [], ()=>{}, onErr);
545ad36319Sstephan                break;
555ad36319Sstephan              }
565ad36319Sstephan              default:
57cb22bd80Sstephan                warn("Unhandled db entry type:",type,'name =',name);
585ad36319Sstephan                break;
595ad36319Sstephan          }
605ad36319Sstephan        }
615ad36319Sstephan      };
625ad36319Sstephan      tx.executeSql(sqlToDrop, [], callback, onErr);
635ad36319Sstephan      db.handle.changeVersion(db.handle.version, "", ()=>{}, onErr, ()=>{});
645ad36319Sstephan    });
655ad36319Sstephan  };
665ad36319Sstephan
675ad36319Sstephan  const clearDbSqlite = function(db){
685ad36319Sstephan    // This would be SO much easier with the oo1 API, but we specifically want to
695ad36319Sstephan    // inject metrics we can't get via that API, and we cannot reliably (OPFS)
705ad36319Sstephan    // open the same DB twice to clear it using that API, so...
71*8948fbeeSstephan    const rc = sqlite3.wasm.exports.sqlite3_wasm_db_reset(db.handle);
72d234902bSstephan    App.logHtml("reset db rc =",rc,db.id, db.filename);
735ad36319Sstephan  };
745ad36319Sstephan
755ad36319Sstephan
765ad36319Sstephan  const E = (s)=>document.querySelector(s);
772cae138fSstephan  const App = {
782cae138fSstephan    e: {
795ad36319Sstephan      output: E('#test-output'),
805ad36319Sstephan      selSql: E('#sql-select'),
815ad36319Sstephan      btnRun: E('#sql-run'),
825ad36319Sstephan      btnRunNext: E('#sql-run-next'),
835ad36319Sstephan      btnRunRemaining: E('#sql-run-remaining'),
845ad36319Sstephan      btnExportMetrics: E('#export-metrics'),
855ad36319Sstephan      btnClear: E('#output-clear'),
865ad36319Sstephan      btnReset: E('#db-reset'),
875ad36319Sstephan      cbReverseLog: E('#cb-reverse-log-order'),
88cb22bd80Sstephan      selImpl: E('#select-impl'),
89cb22bd80Sstephan      fsToolbar: E('#toolbar')
902cae138fSstephan    },
915b915007Sstephan    db: Object.create(null),
925ad36319Sstephan    dbs: Object.create(null),
932f06bf25Sstephan    cache:{},
942cae138fSstephan    log: console.log.bind(console),
952cae138fSstephan    warn: console.warn.bind(console),
962cae138fSstephan    cls: function(){this.e.output.innerHTML = ''},
972cae138fSstephan    logHtml2: function(cssClass,...args){
982cae138fSstephan      const ln = document.createElement('div');
992cae138fSstephan      if(cssClass) ln.classList.add(cssClass);
1002cae138fSstephan      ln.append(document.createTextNode(args.join(' ')));
1012cae138fSstephan      this.e.output.append(ln);
1022f06bf25Sstephan      //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
1032cae138fSstephan    },
1042cae138fSstephan    logHtml: function(...args){
1052cae138fSstephan      console.log(...args);
1062cae138fSstephan      if(1) this.logHtml2('', ...args);
1072cae138fSstephan    },
1082cae138fSstephan    logErr: function(...args){
1092cae138fSstephan      console.error(...args);
1102cae138fSstephan      if(1) this.logHtml2('error', ...args);
1112cae138fSstephan    },
1122cae138fSstephan
1135ad36319Sstephan    execSql: async function(name,sql){
1145ad36319Sstephan      const db = this.getSelectedDb();
1155ad36319Sstephan      const banner = "========================================";
1165ad36319Sstephan      this.logHtml(banner,
1175ad36319Sstephan                   "Running",name,'('+sql.length,'bytes) using',db.id);
118*8948fbeeSstephan      const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
1195ad36319Sstephan      let pStmt = 0, pSqlBegin;
1202cae138fSstephan      const stack = wasm.scopedAllocPush();
1215ad36319Sstephan      const metrics = db.metrics = Object.create(null);
1225ad36319Sstephan      metrics.prepTotal = metrics.stepTotal = 0;
1235ad36319Sstephan      metrics.stmtCount = 0;
1245ad36319Sstephan      metrics.malloc = 0;
1255ad36319Sstephan      metrics.strcpy = 0;
1265ad36319Sstephan      this.blockControls(true);
1275ad36319Sstephan      if(this.gotErr){
1285ad36319Sstephan        this.logErr("Cannot run SQL: error cleanup is pending.");
1295ad36319Sstephan        return;
1305ad36319Sstephan      }
1315ad36319Sstephan      // Run this async so that the UI can be updated for the above header...
132cb22bd80Sstephan      const endRun = ()=>{
1335ad36319Sstephan        metrics.evalSqlEnd = performance.now();
1345ad36319Sstephan        metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
135cb22bd80Sstephan        this.logHtml(db.id,"metrics:",JSON.stringify(metrics, undefined, ' '));
1365ad36319Sstephan        this.logHtml("prepare() count:",metrics.stmtCount);
1375ad36319Sstephan        this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms",
1385ad36319Sstephan                     "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
1395ad36319Sstephan        this.logHtml("Time in step():",metrics.stepTotal,"ms",
1405ad36319Sstephan                     "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())");
1415ad36319Sstephan        this.logHtml("Total runtime:",metrics.evalTimeTotal,"ms");
1425ad36319Sstephan        this.logHtml("Overhead (time - prep - step):",
1435ad36319Sstephan                     (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms");
1445ad36319Sstephan        this.logHtml(banner,"End of",name);
1455ad36319Sstephan      };
1465ad36319Sstephan
1475ad36319Sstephan      let runner;
1485ad36319Sstephan      if('websql'===db.id){
149359d6239Sstephan        const who = this;
1505ad36319Sstephan        runner = function(resolve, reject){
1515ad36319Sstephan          /* WebSQL cannot execute multiple statements, nor can it execute SQL without
1525ad36319Sstephan             an explicit transaction. Thus we have to do some fragile surgery on the
1535ad36319Sstephan             input SQL. Since we're only expecting carefully curated inputs, the hope is
154cb22bd80Sstephan             that this will suffice. PS: it also can't run most SQL functions, e.g. even
155cb22bd80Sstephan             instr() results in "not authorized". */
156cb22bd80Sstephan          if('string'!==typeof sql){ // assume TypedArray
157cb22bd80Sstephan            sql = new TextDecoder().decode(sql);
158cb22bd80Sstephan          }
159cb22bd80Sstephan          sql = sql.replace(/-- [^\n]+\n/g,''); // comment lines interfere with our split()
1605ad36319Sstephan          const sqls = sql.split(/;+\n/);
161cb22bd80Sstephan          const rxBegin = /^BEGIN/i, rxCommit = /^COMMIT/i;
1622cae138fSstephan          try {
1635ad36319Sstephan            const nextSql = ()=>{
1645ad36319Sstephan              let x = sqls.shift();
165cb22bd80Sstephan              while(sqls.length && !x) x = sqls.shift();
1665ad36319Sstephan              return x && x.trim();
1675ad36319Sstephan            };
168cb22bd80Sstephan            const who = this;
1695ad36319Sstephan            const transaction = function(tx){
170cb22bd80Sstephan              try {
1715ad36319Sstephan                let s;
172cb22bd80Sstephan                /* Try to approximate the spirit of the input scripts
173cb22bd80Sstephan                   by running batches bound by BEGIN/COMMIT statements. */
174cb22bd80Sstephan                for(s = nextSql(); !!s; s = nextSql()){
1755ad36319Sstephan                  if(rxBegin.test(s)) continue;
1765ad36319Sstephan                  else if(rxCommit.test(s)) break;
177cb22bd80Sstephan                  //console.log("websql sql again",sqls.length, s);
1785ad36319Sstephan                  ++metrics.stmtCount;
1795ad36319Sstephan                  const t = performance.now();
180cb22bd80Sstephan                  tx.executeSql(s,[], ()=>{}, (t,e)=>{
181cb22bd80Sstephan                    console.error("WebSQL error",e,"SQL =",s);
182cb22bd80Sstephan                    who.logErr(e.message);
183359d6239Sstephan                    //throw e;
184359d6239Sstephan                    return false;
185cb22bd80Sstephan                  });
1865ad36319Sstephan                  metrics.stepTotal += performance.now() - t;
187ffc0cbb0Sstephan                }
1885ad36319Sstephan              }catch(e){
189cb22bd80Sstephan                who.logErr("transaction():",e.message);
190cb22bd80Sstephan                throw e;
191cb22bd80Sstephan              }
192cb22bd80Sstephan            };
193cb22bd80Sstephan            const n = sqls.length;
194cb22bd80Sstephan            const nextBatch = function(){
195cb22bd80Sstephan              if(sqls.length){
196cb22bd80Sstephan                console.log("websql sqls.length",sqls.length,'of',n);
197359d6239Sstephan                db.handle.transaction(transaction, (e)=>{
198359d6239Sstephan                  who.logErr("Ignoring and contiuing:",e.message)
199359d6239Sstephan                  //reject(e);
200359d6239Sstephan                  return false;
201359d6239Sstephan                }, nextBatch);
202cb22bd80Sstephan              }else{
203cb22bd80Sstephan                resolve(who);
204cb22bd80Sstephan              }
205cb22bd80Sstephan            };
206cb22bd80Sstephan            metrics.evalSqlStart = performance.now();
207cb22bd80Sstephan            nextBatch();
208cb22bd80Sstephan          }catch(e){
209cb22bd80Sstephan            //this.gotErr = e;
210cb22bd80Sstephan            console.error("websql error:",e);
211359d6239Sstephan            who.logErr(e.message);
212359d6239Sstephan            //reject(e);
2135ad36319Sstephan          }
2145ad36319Sstephan        }.bind(this);
2155ad36319Sstephan      }else{/*sqlite3 db...*/
2165ad36319Sstephan        runner = function(resolve, reject){
2175ad36319Sstephan          metrics.evalSqlStart = performance.now();
2185ad36319Sstephan          try {
2195ad36319Sstephan            let t;
2205ad36319Sstephan            let sqlByteLen = sql.byteLength;
2215ad36319Sstephan            const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
2225ad36319Sstephan            t = performance.now();
2235ad36319Sstephan            pSqlBegin = wasm.scopedAlloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
2245ad36319Sstephan            metrics.malloc = performance.now() - t;
2255ad36319Sstephan            metrics.byteLength = sqlByteLen;
2265ad36319Sstephan            let pSql = pSqlBegin;
2275ad36319Sstephan            const pSqlEnd = pSqlBegin + sqlByteLen;
2285ad36319Sstephan            t = performance.now();
2295ad36319Sstephan            wasm.heap8().set(sql, pSql);
2305ad36319Sstephan            wasm.setMemValue(pSql + sqlByteLen, 0);
2315ad36319Sstephan            metrics.strcpy = performance.now() - t;
2325ad36319Sstephan            let breaker = 0;
2335ad36319Sstephan            while(pSql && wasm.getMemValue(pSql,'i8')){
2345ad36319Sstephan              wasm.setPtrValue(ppStmt, 0);
2355ad36319Sstephan              wasm.setPtrValue(pzTail, 0);
2365ad36319Sstephan              t = performance.now();
2375ad36319Sstephan              let rc = capi.sqlite3_prepare_v3(
2385ad36319Sstephan                db.handle, pSql, sqlByteLen, 0, ppStmt, pzTail
2395ad36319Sstephan              );
2405ad36319Sstephan              metrics.prepTotal += performance.now() - t;
2415ad36319Sstephan              checkSqliteRc(db.handle, rc);
2425ad36319Sstephan              pStmt = wasm.getPtrValue(ppStmt);
2435ad36319Sstephan              pSql = wasm.getPtrValue(pzTail);
2445ad36319Sstephan              sqlByteLen = pSqlEnd - pSql;
2455ad36319Sstephan              if(!pStmt) continue/*empty statement*/;
2465ad36319Sstephan              ++metrics.stmtCount;
2475ad36319Sstephan              t = performance.now();
2485ad36319Sstephan              rc = capi.sqlite3_step(pStmt);
2495ad36319Sstephan              capi.sqlite3_finalize(pStmt);
2505ad36319Sstephan              pStmt = 0;
2515ad36319Sstephan              metrics.stepTotal += performance.now() - t;
2525ad36319Sstephan              switch(rc){
2535ad36319Sstephan                  case capi.SQLITE_ROW:
2545ad36319Sstephan                  case capi.SQLITE_DONE: break;
2555ad36319Sstephan                  default: checkSqliteRc(db.handle, rc); toss("Not reached.");
2565ad36319Sstephan              }
2575ad36319Sstephan            }
2585ad36319Sstephan            resolve(this);
2595ad36319Sstephan          }catch(e){
2605ad36319Sstephan            if(pStmt) capi.sqlite3_finalize(pStmt);
261cb22bd80Sstephan            //this.gotErr = e;
2625ad36319Sstephan            reject(e);
2632cae138fSstephan          }finally{
264359d6239Sstephan            capi.sqlite3_exec(db.handle,"rollback;",0,0,0);
2652cae138fSstephan            wasm.scopedAllocPop(stack);
2662cae138fSstephan          }
2675ad36319Sstephan        }.bind(this);
2685ad36319Sstephan      }
2695ad36319Sstephan      let p;
2705ad36319Sstephan      if(1){
2715ad36319Sstephan        p = new Promise(function(res,rej){
2725ad36319Sstephan          setTimeout(()=>runner(res, rej), 50)/*give UI a chance to output the "running" banner*/;
2735ad36319Sstephan        });
2745ad36319Sstephan      }else{
2755ad36319Sstephan        p = new Promise(runner);
2765ad36319Sstephan      }
2775ad36319Sstephan      return p.catch(
2785ad36319Sstephan        (e)=>this.logErr("Error via execSql("+name+",...):",e.message)
2795ad36319Sstephan      ).finally(()=>{
280cb22bd80Sstephan        endRun();
2815ad36319Sstephan        this.blockControls(false);
2825ad36319Sstephan      });
2832cae138fSstephan    },
2842cae138fSstephan
2855ad36319Sstephan    clearDb: function(){
2865ad36319Sstephan      const db = this.getSelectedDb();
2875ad36319Sstephan      if('websql'===db.id){
2885ad36319Sstephan        this.logErr("TODO: clear websql db.");
2895ad36319Sstephan        return;
2905b915007Sstephan      }
2915ad36319Sstephan      if(!db.handle) return;
292*8948fbeeSstephan      const capi = this.sqlite3, wasm = this.sqlite3.wasm;
2935ad36319Sstephan      //const scope = wasm.scopedAllocPush(
2945ad36319Sstephan      this.logErr("TODO: clear db");
2952cae138fSstephan    },
2962cae138fSstephan
29753f635dfSstephan    /**
29853f635dfSstephan       Loads batch-runner.list and populates the selection list from
29953f635dfSstephan       it. Returns a promise which resolves to nothing in particular
30053f635dfSstephan       when it completes. Only intended to be run once at the start
30153f635dfSstephan       of the app.
30253f635dfSstephan     */
3032cae138fSstephan    loadSqlList: async function(){
3042cae138fSstephan      const sel = this.e.selSql;
3052cae138fSstephan      sel.innerHTML = '';
3062cae138fSstephan      this.blockControls(true);
3072cae138fSstephan      const infile = 'batch-runner.list';
3082cae138fSstephan      this.logHtml("Loading list of SQL files:", infile);
3092cae138fSstephan      let txt;
3102cae138fSstephan      try{
3112cae138fSstephan        const r = await fetch(infile);
3122cae138fSstephan        if(404 === r.status){
3132cae138fSstephan          toss("Missing file '"+infile+"'.");
3142cae138fSstephan        }
3152cae138fSstephan        if(!r.ok) toss("Loading",infile,"failed:",r.statusText);
3162cae138fSstephan        txt = await r.text();
3175ad36319Sstephan        const warning = E('#warn-list');
3182f06bf25Sstephan        if(warning) warning.remove();
3192cae138fSstephan      }catch(e){
3202cae138fSstephan        this.logErr(e.message);
3212cae138fSstephan        throw e;
3222cae138fSstephan      }finally{
3232cae138fSstephan        this.blockControls(false);
3242cae138fSstephan      }
3252f06bf25Sstephan      const list = txt.split(/\n+/);
3262cae138fSstephan      let opt;
3272cae138fSstephan      if(0){
3282cae138fSstephan        opt = document.createElement('option');
3292cae138fSstephan        opt.innerText = "Select file to evaluate...";
3302cae138fSstephan        opt.value = '';
3312cae138fSstephan        opt.disabled = true;
3322cae138fSstephan        opt.selected = true;
3332cae138fSstephan        sel.appendChild(opt);
3342cae138fSstephan      }
3352cae138fSstephan      list.forEach(function(fn){
3362f06bf25Sstephan        if(!fn) return;
3372cae138fSstephan        opt = document.createElement('option');
338fbf0f488Sstephan        opt.value = fn;
339fbf0f488Sstephan        opt.innerText = fn.split('/').pop();
3402cae138fSstephan        sel.appendChild(opt);
3412cae138fSstephan      });
3422cae138fSstephan      this.logHtml("Loaded",infile);
3432cae138fSstephan    },
3442cae138fSstephan
3452cae138fSstephan    /** Fetch ./fn and return its contents as a Uint8Array. */
3462f06bf25Sstephan    fetchFile: async function(fn, cacheIt=false){
3472f06bf25Sstephan      if(cacheIt && this.cache[fn]) return this.cache[fn];
3482cae138fSstephan      this.logHtml("Fetching",fn,"...");
3492cae138fSstephan      let sql;
3502cae138fSstephan      try {
3512cae138fSstephan        const r = await fetch(fn);
3522cae138fSstephan        if(!r.ok) toss("Fetch failed:",r.statusText);
3532cae138fSstephan        sql = new Uint8Array(await r.arrayBuffer());
3542cae138fSstephan      }catch(e){
3552cae138fSstephan        this.logErr(e.message);
3562cae138fSstephan        throw e;
3572cae138fSstephan      }
3582cae138fSstephan      this.logHtml("Fetched",sql.length,"bytes from",fn);
3592f06bf25Sstephan      if(cacheIt) this.cache[fn] = sql;
3602cae138fSstephan      return sql;
3612f06bf25Sstephan    }/*fetchFile()*/,
3622cae138fSstephan
3632f06bf25Sstephan    /** Disable or enable certain UI controls. */
3642f06bf25Sstephan    blockControls: function(disable){
365cb22bd80Sstephan      //document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
366cb22bd80Sstephan      this.e.fsToolbar.disabled = disable;
3672cae138fSstephan    },
3682cae138fSstephan
36953f635dfSstephan    /**
37053f635dfSstephan       Converts this.metrics() to a form which is suitable for easy conversion to
37153f635dfSstephan       CSV. It returns an array of arrays. The first sub-array is the column names.
37253f635dfSstephan       The 2nd and subsequent are the values, one per test file (only the most recent
37353f635dfSstephan       metrics are kept for any given file).
37453f635dfSstephan    */
37553f635dfSstephan    metricsToArrays: function(){
37653f635dfSstephan      const rc = [];
377cb22bd80Sstephan      Object.keys(this.dbs).sort().forEach((k)=>{
378cb22bd80Sstephan        const d = this.dbs[k];
379cb22bd80Sstephan        const m = d.metrics;
3805ad36319Sstephan        delete m.evalSqlStart;
3815ad36319Sstephan        delete m.evalSqlEnd;
38253f635dfSstephan        const mk = Object.keys(m).sort();
38353f635dfSstephan        if(!rc.length){
3845ad36319Sstephan          rc.push(['db', ...mk]);
38553f635dfSstephan        }
38653f635dfSstephan        const row = [k.split('/').pop()/*remove dir prefix from filename*/];
38753f635dfSstephan        rc.push(row);
388cb22bd80Sstephan        row.push(...mk.map((kk)=>m[kk]));
38953f635dfSstephan      });
39053f635dfSstephan      return rc;
39153f635dfSstephan    },
39253f635dfSstephan
39353f635dfSstephan    metricsToBlob: function(colSeparator='\t'){
39453f635dfSstephan      const ar = [], ma = this.metricsToArrays();
39553f635dfSstephan      if(!ma.length){
39653f635dfSstephan        this.logErr("Metrics are empty. Run something.");
39753f635dfSstephan        return;
39853f635dfSstephan      }
39953f635dfSstephan      ma.forEach(function(row){
40053f635dfSstephan        ar.push(row.join(colSeparator),'\n');
40153f635dfSstephan      });
40253f635dfSstephan      return new Blob(ar);
40353f635dfSstephan    },
40453f635dfSstephan
40553f635dfSstephan    downloadMetrics: function(){
40653f635dfSstephan      const b = this.metricsToBlob();
40753f635dfSstephan      if(!b) return;
40853f635dfSstephan      const url = URL.createObjectURL(b);
40953f635dfSstephan      const a = document.createElement('a');
41053f635dfSstephan      a.href = url;
411cdefd5d0Sstephan      a.download = 'batch-runner-js-'+((new Date().getTime()/1000) | 0)+'.csv';
41253f635dfSstephan      this.logHtml("Triggering download of",a.download);
41353f635dfSstephan      document.body.appendChild(a);
41453f635dfSstephan      a.click();
41553f635dfSstephan      setTimeout(()=>{
41653f635dfSstephan        document.body.removeChild(a);
41753f635dfSstephan        URL.revokeObjectURL(url);
41853f635dfSstephan      }, 500);
41953f635dfSstephan    },
42053f635dfSstephan
42153f635dfSstephan    /**
42253f635dfSstephan       Fetch file fn and eval it as an SQL blob. This is an async
42353f635dfSstephan       operation and returns a Promise which resolves to this
42453f635dfSstephan       object on success.
42553f635dfSstephan    */
4262cae138fSstephan    evalFile: async function(fn){
4272cae138fSstephan      const sql = await this.fetchFile(fn);
4285ad36319Sstephan      return this.execSql(fn,sql);
4292f06bf25Sstephan    }/*evalFile()*/,
4302cae138fSstephan
4315ad36319Sstephan    /**
4325ad36319Sstephan       Clears all DB tables in all _opened_ databases. Because of
4335ad36319Sstephan       disparities between backends, we cannot simply "unlink" the
4345ad36319Sstephan       databases to clean them up.
4355ad36319Sstephan    */
4365ad36319Sstephan    clearStorage: function(onlySelectedDb=false){
437cb22bd80Sstephan      const list = onlySelectedDb
438cb22bd80Sstephan            ? [('boolean'===typeof onlySelectedDb)
4395ad36319Sstephan                ? this.dbs[this.e.selImpl.value]
440cb22bd80Sstephan                : onlySelectedDb]
4415ad36319Sstephan            : Object.values(this.dbs);
4425ad36319Sstephan      for(let db of list){
4435ad36319Sstephan        if(db && db.handle){
4445ad36319Sstephan          this.logHtml("Clearing db",db.id);
445cb22bd80Sstephan          db.clear();
4465ad36319Sstephan        }
4475ad36319Sstephan      }
4485b915007Sstephan    },
4495b915007Sstephan
4505ad36319Sstephan    /**
4515ad36319Sstephan       Fetches the handle of the db associated with
4525ad36319Sstephan       this.e.selImpl.value, opening it if needed.
4535ad36319Sstephan    */
4545ad36319Sstephan    getSelectedDb: function(){
4555ad36319Sstephan      if(!this.dbs.memdb){
4565ad36319Sstephan        for(let opt of this.e.selImpl.options){
4575ad36319Sstephan          const d = this.dbs[opt.value] = Object.create(null);
4585ad36319Sstephan          d.id = opt.value;
4595ad36319Sstephan          switch(d.id){
4605ad36319Sstephan              case 'virtualfs':
4615ad36319Sstephan                d.filename = 'file:/virtualfs.sqlite3?vfs=unix-none';
4625ad36319Sstephan                break;
4635ad36319Sstephan              case 'memdb':
4645ad36319Sstephan                d.filename = ':memory:';
4655ad36319Sstephan                break;
4665ad36319Sstephan              case 'wasmfs-opfs':
467cb22bd80Sstephan                d.filename = 'file:'+(
468cb22bd80Sstephan                  this.sqlite3.capi.sqlite3_wasmfs_opfs_dir()
469cb22bd80Sstephan                )+'/wasmfs-opfs.sqlite3b';
4705ad36319Sstephan                break;
4715ad36319Sstephan              case 'websql':
4725ad36319Sstephan                d.filename = 'websql.db';
4735ad36319Sstephan                break;
4745ad36319Sstephan              default:
4755ad36319Sstephan                this.logErr("Unhandled db selection option (see details in the console).",opt);
4765ad36319Sstephan                toss("Unhandled db init option");
4775b915007Sstephan          }
4785ad36319Sstephan        }
4795ad36319Sstephan      }/*first-time init*/
4805ad36319Sstephan      const dbId = this.e.selImpl.value;
4815ad36319Sstephan      const d = this.dbs[dbId];
4825ad36319Sstephan      if(d.handle) return d;
4835ad36319Sstephan      if('websql' === dbId){
4845ad36319Sstephan        d.handle = self.openDatabase('batch-runner', '0.1', 'foo', 1024 * 1024 * 50);
4855ad36319Sstephan        d.clear = ()=>clearDbWebSQL(d);
486d234902bSstephan        d.handle.transaction(function(tx){
487d234902bSstephan          tx.executeSql("PRAGMA cache_size="+cacheSize);
488d234902bSstephan          App.logHtml(dbId,"cache_size =",cacheSize);
489d234902bSstephan        });
4905ad36319Sstephan      }else{
491*8948fbeeSstephan        const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
4925ad36319Sstephan        const stack = wasm.scopedAllocPush();
4935ad36319Sstephan        let pDb = 0;
4945ad36319Sstephan        try{
4955ad36319Sstephan          const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
4965ad36319Sstephan          const ppDb = wasm.scopedAllocPtr();
4975ad36319Sstephan          const rc = capi.sqlite3_open_v2(d.filename, ppDb, oFlags, null);
4985ad36319Sstephan          pDb = wasm.getPtrValue(ppDb)
4995ad36319Sstephan          if(rc) toss("sqlite3_open_v2() failed with code",rc);
500d234902bSstephan          capi.sqlite3_exec(pDb, "PRAGMA cache_size="+cacheSize, 0, 0, 0);
501d234902bSstephan          this.logHtml(dbId,"cache_size =",cacheSize);
5025ad36319Sstephan        }catch(e){
5035ad36319Sstephan          if(pDb) capi.sqlite3_close_v2(pDb);
5045ad36319Sstephan        }finally{
5055ad36319Sstephan          wasm.scopedAllocPop(stack);
5065ad36319Sstephan        }
5075ad36319Sstephan        d.handle = pDb;
5085ad36319Sstephan        d.clear = ()=>clearDbSqlite(d);
5095ad36319Sstephan      }
5105ad36319Sstephan      d.clear();
511d234902bSstephan      this.logHtml("Opened db:",dbId,d.filename);
5125ad36319Sstephan      console.log("db =",d);
5135ad36319Sstephan      return d;
5145b915007Sstephan    },
5155b915007Sstephan
5162cae138fSstephan    run: function(sqlite3){
5172f06bf25Sstephan      delete this.run;
5182cae138fSstephan      this.sqlite3 = sqlite3;
519*8948fbeeSstephan      const capi = sqlite3.capi, wasm = sqlite3.wasm;
5202cae138fSstephan      this.logHtml("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
5212cae138fSstephan      this.logHtml("WASM heap size =",wasm.heap8().length);
5222f06bf25Sstephan      this.loadSqlList();
5235ad36319Sstephan      if(capi.sqlite3_wasmfs_opfs_dir()){
5245ad36319Sstephan        E('#warn-opfs').classList.remove('hidden');
5255b915007Sstephan      }else{
5265ad36319Sstephan        E('#warn-opfs').remove();
5275ad36319Sstephan        E('option[value=wasmfs-opfs]').disabled = true;
5285ad36319Sstephan      }
5295ad36319Sstephan      if('function' === typeof self.openDatabase){
5305ad36319Sstephan        E('#warn-websql').classList.remove('hidden');
53157db2174Sstephan      }else{
5325ad36319Sstephan        E('option[value=websql]').disabled = true;
5335ad36319Sstephan        E('#warn-websql').remove();
53457db2174Sstephan      }
5352cae138fSstephan      const who = this;
53653f635dfSstephan      if(this.e.cbReverseLog.checked){
53753f635dfSstephan        this.e.output.classList.add('reverse');
53853f635dfSstephan      }
53953f635dfSstephan      this.e.cbReverseLog.addEventListener('change', function(){
5405ad36319Sstephan        who.e.output.classList[this.checked ? 'add' : 'remove']('reverse');
54153f635dfSstephan      }, false);
5422cae138fSstephan      this.e.btnClear.addEventListener('click', ()=>this.cls(), false);
5432cae138fSstephan      this.e.btnRun.addEventListener('click', function(){
5442cae138fSstephan        if(!who.e.selSql.value) return;
5452cae138fSstephan        who.evalFile(who.e.selSql.value);
5462cae138fSstephan      }, false);
5472f06bf25Sstephan      this.e.btnRunNext.addEventListener('click', function(){
5482f06bf25Sstephan        ++who.e.selSql.selectedIndex;
5492f06bf25Sstephan        if(!who.e.selSql.value) return;
5502f06bf25Sstephan        who.evalFile(who.e.selSql.value);
5512f06bf25Sstephan      }, false);
5522f06bf25Sstephan      this.e.btnReset.addEventListener('click', function(){
5535ad36319Sstephan        who.clearStorage(true);
5542f06bf25Sstephan      }, false);
55553f635dfSstephan      this.e.btnExportMetrics.addEventListener('click', function(){
556cdefd5d0Sstephan        who.logHtml2('warning',"Triggering download of metrics CSV. Check your downloads folder.");
55753f635dfSstephan        who.downloadMetrics();
558cdefd5d0Sstephan        //const m = who.metricsToArrays();
559cdefd5d0Sstephan        //console.log("Metrics:",who.metrics, m);
56053f635dfSstephan      });
5615ad36319Sstephan      this.e.selImpl.addEventListener('change', function(){
5625ad36319Sstephan        who.getSelectedDb();
5635ad36319Sstephan      });
5642f06bf25Sstephan      this.e.btnRunRemaining.addEventListener('click', async function(){
5652f06bf25Sstephan        let v = who.e.selSql.value;
5662f06bf25Sstephan        const timeStart = performance.now();
5672f06bf25Sstephan        while(v){
5682f06bf25Sstephan          await who.evalFile(v);
5692f06bf25Sstephan          if(who.gotError){
5702f06bf25Sstephan            who.logErr("Error handling script",v,":",who.gotError.message);
5712f06bf25Sstephan            break;
5722f06bf25Sstephan          }
5732f06bf25Sstephan          ++who.e.selSql.selectedIndex;
5742f06bf25Sstephan          v = who.e.selSql.value;
5752f06bf25Sstephan        }
5762f06bf25Sstephan        const timeTotal = performance.now() - timeStart;
5772f06bf25Sstephan        who.logHtml("Run-remaining time:",timeTotal,"ms ("+(timeTotal/1000/60)+" minute(s))");
5785b915007Sstephan        who.clearStorage();
5792f06bf25Sstephan      }, false);
5802f06bf25Sstephan    }/*run()*/
5812cae138fSstephan  }/*App*/;
5822cae138fSstephan
583359d6239Sstephan  self.sqlite3TestModule.initSqlite3().then(function(sqlite3_){
584359d6239Sstephan    sqlite3 = sqlite3_;
585b94a9860Sstephan    self.App = App /* only to facilitate dev console access */;
586b94a9860Sstephan    App.run(sqlite3);
5872cae138fSstephan  });
5882cae138fSstephan})();
589