123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- /*
- 2022-08-29
- 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.
- ***********************************************************************
- A basic batch SQL runner for sqlite3-api.js. This file must be run in
- main JS thread and sqlite3.js must have been loaded before it.
- */
- 'use strict';
- (function(){
- const toss = function(...args){throw new Error(args.join(' '))};
- const warn = console.warn.bind(console);
- let sqlite3;
- const urlParams = new URL(self.location.href).searchParams;
- const cacheSize = (()=>{
- if(urlParams.has('cachesize')) return +urlParams.get('cachesize');
- return 200;
- })();
- /** Throws if the given sqlite3 result code is not 0. */
- const checkSqliteRc = (dbh,rc)=>{
- if(rc) toss("Prepare failed:",sqlite3.capi.sqlite3_errmsg(dbh));
- };
- const sqlToDrop = [
- "SELECT type,name FROM sqlite_schema ",
- "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
- "AND name NOT LIKE '\\_%' escape '\\'"
- ].join('');
-
- const clearDbWebSQL = function(db){
- db.handle.transaction(function(tx){
- const onErr = (e)=>console.error(e);
- const callback = function(tx, result){
- const rows = result.rows;
- let i, n;
- i = n = rows.length;
- while(i--){
- const row = rows.item(i);
- const name = JSON.stringify(row.name);
- const type = row.type;
- switch(type){
- case 'index': case 'table':
- case 'trigger': case 'view': {
- const sql2 = 'DROP '+type+' '+name;
- tx.executeSql(sql2, [], ()=>{}, onErr);
- break;
- }
- default:
- warn("Unhandled db entry type:",type,'name =',name);
- break;
- }
- }
- };
- tx.executeSql(sqlToDrop, [], callback, onErr);
- db.handle.changeVersion(db.handle.version, "", ()=>{}, onErr, ()=>{});
- });
- };
- const clearDbSqlite = function(db){
- // This would be SO much easier with the oo1 API, but we specifically want to
- // inject metrics we can't get via that API, and we cannot reliably (OPFS)
- // open the same DB twice to clear it using that API, so...
- const rc = sqlite3.wasm.exports.sqlite3_wasm_db_reset(db.handle);
- App.logHtml("reset db rc =",rc,db.id, db.filename);
- };
- const E = (s)=>document.querySelector(s);
- const App = {
- e: {
- output: E('#test-output'),
- selSql: E('#sql-select'),
- btnRun: E('#sql-run'),
- btnRunNext: E('#sql-run-next'),
- btnRunRemaining: E('#sql-run-remaining'),
- btnExportMetrics: E('#export-metrics'),
- btnClear: E('#output-clear'),
- btnReset: E('#db-reset'),
- cbReverseLog: E('#cb-reverse-log-order'),
- selImpl: E('#select-impl'),
- fsToolbar: E('#toolbar')
- },
- db: Object.create(null),
- dbs: Object.create(null),
- cache:{},
- metrics: {
- fileCount: 0,
- runTimeMs: 0,
- prepareTimeMs: 0,
- stepTimeMs: 0,
- stmtCount: 0,
- strcpyMs: 0,
- sqlBytes: 0
- },
- log: console.log.bind(console),
- warn: console.warn.bind(console),
- cls: function(){this.e.output.innerHTML = ''},
- logHtml2: function(cssClass,...args){
- const ln = document.createElement('div');
- if(cssClass) ln.classList.add(cssClass);
- ln.append(document.createTextNode(args.join(' ')));
- this.e.output.append(ln);
- //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
- },
- logHtml: function(...args){
- console.log(...args);
- if(1) this.logHtml2('', ...args);
- },
- logErr: function(...args){
- console.error(...args);
- if(1) this.logHtml2('error', ...args);
- },
- execSql: async function(name,sql){
- const db = this.getSelectedDb();
- const banner = "========================================";
- this.logHtml(banner,
- "Running",name,'('+sql.length,'bytes) using',db.id);
- const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
- let pStmt = 0, pSqlBegin;
- const metrics = db.metrics = Object.create(null);
- metrics.prepTotal = metrics.stepTotal = 0;
- metrics.stmtCount = 0;
- metrics.malloc = 0;
- metrics.strcpy = 0;
- this.blockControls(true);
- if(this.gotErr){
- this.logErr("Cannot run SQL: error cleanup is pending.");
- return;
- }
- // Run this async so that the UI can be updated for the above header...
- const endRun = ()=>{
- metrics.evalSqlEnd = performance.now();
- metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
- this.logHtml(db.id,"metrics:",JSON.stringify(metrics, undefined, ' '));
- this.logHtml("prepare() count:",metrics.stmtCount);
- this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms",
- "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
- this.logHtml("Time in step():",metrics.stepTotal,"ms",
- "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())");
- this.logHtml("Total runtime:",metrics.evalTimeTotal,"ms");
- this.logHtml("Overhead (time - prep - step):",
- (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms");
- this.logHtml(banner,"End of",name);
- this.metrics.prepareTimeMs += metrics.prepTotal;
- this.metrics.stepTimeMs += metrics.stepTotal;
- this.metrics.stmtCount += metrics.stmtCount;
- this.metrics.strcpyMs += metrics.strcpy;
- this.metrics.sqlBytes += sql.length;
- };
- let runner;
- if('websql'===db.id){
- const who = this;
- runner = function(resolve, reject){
- /* WebSQL cannot execute multiple statements, nor can it execute SQL without
- an explicit transaction. Thus we have to do some fragile surgery on the
- input SQL. Since we're only expecting carefully curated inputs, the hope is
- that this will suffice. PS: it also can't run most SQL functions, e.g. even
- instr() results in "not authorized". */
- if('string'!==typeof sql){ // assume TypedArray
- sql = new TextDecoder().decode(sql);
- }
- sql = sql.replace(/-- [^\n]+\n/g,''); // comment lines interfere with our split()
- const sqls = sql.split(/;+\n/);
- const rxBegin = /^BEGIN/i, rxCommit = /^COMMIT/i;
- try {
- const nextSql = ()=>{
- let x = sqls.shift();
- while(sqls.length && !x) x = sqls.shift();
- return x && x.trim();
- };
- const who = this;
- const transaction = function(tx){
- try {
- let s;
- /* Try to approximate the spirit of the input scripts
- by running batches bound by BEGIN/COMMIT statements. */
- for(s = nextSql(); !!s; s = nextSql()){
- if(rxBegin.test(s)) continue;
- else if(rxCommit.test(s)) break;
- //console.log("websql sql again",sqls.length, s);
- ++metrics.stmtCount;
- const t = performance.now();
- tx.executeSql(s,[], ()=>{}, (t,e)=>{
- console.error("WebSQL error",e,"SQL =",s);
- who.logErr(e.message);
- //throw e;
- return false;
- });
- metrics.stepTotal += performance.now() - t;
- }
- }catch(e){
- who.logErr("transaction():",e.message);
- throw e;
- }
- };
- const n = sqls.length;
- const nextBatch = function(){
- if(sqls.length){
- console.log("websql sqls.length",sqls.length,'of',n);
- db.handle.transaction(transaction, (e)=>{
- who.logErr("Ignoring and contiuing:",e.message)
- //reject(e);
- return false;
- }, nextBatch);
- }else{
- resolve(who);
- }
- };
- metrics.evalSqlStart = performance.now();
- nextBatch();
- }catch(e){
- //this.gotErr = e;
- console.error("websql error:",e);
- who.logErr(e.message);
- //reject(e);
- }
- }.bind(this);
- }else{/*sqlite3 db...*/
- runner = function(resolve, reject){
- ++this.metrics.fileCount;
- metrics.evalSqlStart = performance.now();
- const stack = wasm.scopedAllocPush();
- try {
- let t;
- let sqlByteLen = sql.byteLength;
- const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
- t = performance.now();
- pSqlBegin = wasm.scopedAlloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
- metrics.malloc = performance.now() - t;
- metrics.byteLength = sqlByteLen;
- let pSql = pSqlBegin;
- const pSqlEnd = pSqlBegin + sqlByteLen;
- t = performance.now();
- wasm.heap8().set(sql, pSql);
- wasm.poke(pSql + sqlByteLen, 0);
- metrics.strcpy = performance.now() - t;
- let breaker = 0;
- while(pSql && wasm.peek(pSql,'i8')){
- wasm.pokePtr(ppStmt, 0);
- wasm.pokePtr(pzTail, 0);
- t = performance.now();
- let rc = capi.sqlite3_prepare_v3(
- db.handle, pSql, sqlByteLen, 0, ppStmt, pzTail
- );
- metrics.prepTotal += performance.now() - t;
- checkSqliteRc(db.handle, rc);
- pStmt = wasm.peekPtr(ppStmt);
- pSql = wasm.peekPtr(pzTail);
- sqlByteLen = pSqlEnd - pSql;
- if(!pStmt) continue/*empty statement*/;
- ++metrics.stmtCount;
- t = performance.now();
- rc = capi.sqlite3_step(pStmt);
- capi.sqlite3_finalize(pStmt);
- pStmt = 0;
- metrics.stepTotal += performance.now() - t;
- switch(rc){
- case capi.SQLITE_ROW:
- case capi.SQLITE_DONE: break;
- default: checkSqliteRc(db.handle, rc); toss("Not reached.");
- }
- }
- resolve(this);
- }catch(e){
- if(pStmt) capi.sqlite3_finalize(pStmt);
- //this.gotErr = e;
- reject(e);
- }finally{
- capi.sqlite3_exec(db.handle,"rollback;",0,0,0);
- wasm.scopedAllocPop(stack);
- }
- }.bind(this);
- }
- let p;
- if(1){
- p = new Promise(function(res,rej){
- setTimeout(()=>runner(res, rej), 0)/*give UI a chance to output the "running" banner*/;
- });
- }else{
- p = new Promise(runner);
- }
- return p.catch(
- (e)=>this.logErr("Error via execSql("+name+",...):",e.message)
- ).finally(()=>{
- endRun();
- this.blockControls(false);
- });
- },
-
- clearDb: function(){
- const db = this.getSelectedDb();
- if('websql'===db.id){
- this.logErr("TODO: clear websql db.");
- return;
- }
- if(!db.handle) return;
- const capi = this.sqlite3, wasm = this.sqlite3.wasm;
- //const scope = wasm.scopedAllocPush(
- this.logErr("TODO: clear db");
- },
-
- /**
- Loads batch-runner.list and populates the selection list from
- it. Returns a promise which resolves to nothing in particular
- when it completes. Only intended to be run once at the start
- of the app.
- */
- loadSqlList: async function(){
- const sel = this.e.selSql;
- sel.innerHTML = '';
- this.blockControls(true);
- const infile = 'batch-runner.list';
- this.logHtml("Loading list of SQL files:", infile);
- let txt;
- try{
- const r = await fetch(infile);
- if(404 === r.status){
- toss("Missing file '"+infile+"'.");
- }
- if(!r.ok) toss("Loading",infile,"failed:",r.statusText);
- txt = await r.text();
- const warning = E('#warn-list');
- if(warning) warning.remove();
- }catch(e){
- this.logErr(e.message);
- throw e;
- }finally{
- this.blockControls(false);
- }
- const list = txt.split(/\n+/);
- let opt;
- if(0){
- opt = document.createElement('option');
- opt.innerText = "Select file to evaluate...";
- opt.value = '';
- opt.disabled = true;
- opt.selected = true;
- sel.appendChild(opt);
- }
- list.forEach(function(fn){
- if(!fn) return;
- opt = document.createElement('option');
- opt.value = fn;
- opt.innerText = fn.split('/').pop();
- sel.appendChild(opt);
- });
- this.logHtml("Loaded",infile);
- },
- /** Fetch ./fn and return its contents as a Uint8Array. */
- fetchFile: async function(fn, cacheIt=false){
- if(cacheIt && this.cache[fn]) return this.cache[fn];
- this.logHtml("Fetching",fn,"...");
- let sql;
- try {
- const r = await fetch(fn);
- if(!r.ok) toss("Fetch failed:",r.statusText);
- sql = new Uint8Array(await r.arrayBuffer());
- }catch(e){
- this.logErr(e.message);
- throw e;
- }
- this.logHtml("Fetched",sql.length,"bytes from",fn);
- if(cacheIt) this.cache[fn] = sql;
- return sql;
- }/*fetchFile()*/,
- /** Disable or enable certain UI controls. */
- blockControls: function(disable){
- //document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
- this.e.fsToolbar.disabled = disable;
- },
- /**
- Converts this.metrics() to a form which is suitable for easy conversion to
- CSV. It returns an array of arrays. The first sub-array is the column names.
- The 2nd and subsequent are the values, one per test file (only the most recent
- metrics are kept for any given file).
- */
- metricsToArrays: function(){
- const rc = [];
- Object.keys(this.dbs).sort().forEach((k)=>{
- const d = this.dbs[k];
- const m = d.metrics;
- delete m.evalSqlStart;
- delete m.evalSqlEnd;
- const mk = Object.keys(m).sort();
- if(!rc.length){
- rc.push(['db', ...mk]);
- }
- const row = [k.split('/').pop()/*remove dir prefix from filename*/];
- rc.push(row);
- row.push(...mk.map((kk)=>m[kk]));
- });
- return rc;
- },
- metricsToBlob: function(colSeparator='\t'){
- const ar = [], ma = this.metricsToArrays();
- if(!ma.length){
- this.logErr("Metrics are empty. Run something.");
- return;
- }
- ma.forEach(function(row){
- ar.push(row.join(colSeparator),'\n');
- });
- return new Blob(ar);
- },
- downloadMetrics: function(){
- const b = this.metricsToBlob();
- if(!b) return;
- const url = URL.createObjectURL(b);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'batch-runner-js-'+((new Date().getTime()/1000) | 0)+'.csv';
- this.logHtml("Triggering download of",a.download);
- document.body.appendChild(a);
- a.click();
- setTimeout(()=>{
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }, 500);
- },
- /**
- Fetch file fn and eval it as an SQL blob. This is an async
- operation and returns a Promise which resolves to this
- object on success.
- */
- evalFile: async function(fn){
- const sql = await this.fetchFile(fn);
- return this.execSql(fn,sql);
- }/*evalFile()*/,
- /**
- Clears all DB tables in all _opened_ databases. Because of
- disparities between backends, we cannot simply "unlink" the
- databases to clean them up.
- */
- clearStorage: function(onlySelectedDb=false){
- const list = onlySelectedDb
- ? [('boolean'===typeof onlySelectedDb)
- ? this.dbs[this.e.selImpl.value]
- : onlySelectedDb]
- : Object.values(this.dbs);
- for(let db of list){
- if(db && db.handle){
- this.logHtml("Clearing db",db.id);
- db.clear();
- }
- }
- },
- /**
- Fetches the handle of the db associated with
- this.e.selImpl.value, opening it if needed.
- */
- getSelectedDb: function(){
- if(!this.dbs.memdb){
- for(let opt of this.e.selImpl.options){
- const d = this.dbs[opt.value] = Object.create(null);
- d.id = opt.value;
- switch(d.id){
- case 'virtualfs':
- d.filename = 'file:/virtualfs.sqlite3?vfs=unix-none';
- break;
- case 'memdb':
- d.filename = ':memory:';
- break;
- case 'wasmfs-opfs':
- d.filename = 'file:'+(
- this.sqlite3.capi.sqlite3_wasmfs_opfs_dir()
- )+'/wasmfs-opfs.sqlite3b';
- break;
- case 'websql':
- d.filename = 'websql.db';
- break;
- default:
- this.logErr("Unhandled db selection option (see details in the console).",opt);
- toss("Unhandled db init option");
- }
- }
- }/*first-time init*/
- const dbId = this.e.selImpl.value;
- const d = this.dbs[dbId];
- if(d.handle) return d;
- if('websql' === dbId){
- d.handle = self.openDatabase('batch-runner', '0.1', 'foo', 1024 * 1024 * 50);
- d.clear = ()=>clearDbWebSQL(d);
- d.handle.transaction(function(tx){
- tx.executeSql("PRAGMA cache_size="+cacheSize);
- App.logHtml(dbId,"cache_size =",cacheSize);
- });
- }else{
- const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
- const stack = wasm.scopedAllocPush();
- let pDb = 0;
- try{
- const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
- const ppDb = wasm.scopedAllocPtr();
- const rc = capi.sqlite3_open_v2(d.filename, ppDb, oFlags, null);
- pDb = wasm.peekPtr(ppDb)
- if(rc) toss("sqlite3_open_v2() failed with code",rc);
- capi.sqlite3_exec(pDb, "PRAGMA cache_size="+cacheSize, 0, 0, 0);
- this.logHtml(dbId,"cache_size =",cacheSize);
- }catch(e){
- if(pDb) capi.sqlite3_close_v2(pDb);
- }finally{
- wasm.scopedAllocPop(stack);
- }
- d.handle = pDb;
- d.clear = ()=>clearDbSqlite(d);
- }
- d.clear();
- this.logHtml("Opened db:",dbId,d.filename);
- console.log("db =",d);
- return d;
- },
- run: function(sqlite3){
- delete this.run;
- this.sqlite3 = sqlite3;
- const capi = sqlite3.capi, wasm = sqlite3.wasm;
- this.logHtml("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
- this.logHtml("WASM heap size =",wasm.heap8().length);
- this.loadSqlList();
- if(capi.sqlite3_wasmfs_opfs_dir()){
- E('#warn-opfs').classList.remove('hidden');
- }else{
- E('#warn-opfs').remove();
- E('option[value=wasmfs-opfs]').disabled = true;
- }
- if('function' === typeof self.openDatabase){
- E('#warn-websql').classList.remove('hidden');
- }else{
- E('option[value=websql]').disabled = true;
- E('#warn-websql').remove();
- }
- const who = this;
- if(this.e.cbReverseLog.checked){
- this.e.output.classList.add('reverse');
- }
- this.e.cbReverseLog.addEventListener('change', function(){
- who.e.output.classList[this.checked ? 'add' : 'remove']('reverse');
- }, false);
- this.e.btnClear.addEventListener('click', ()=>this.cls(), false);
- this.e.btnRun.addEventListener('click', function(){
- if(!who.e.selSql.value) return;
- who.evalFile(who.e.selSql.value);
- }, false);
- this.e.btnRunNext.addEventListener('click', function(){
- ++who.e.selSql.selectedIndex;
- if(!who.e.selSql.value) return;
- who.evalFile(who.e.selSql.value);
- }, false);
- this.e.btnReset.addEventListener('click', function(){
- who.clearStorage(true);
- }, false);
- this.e.btnExportMetrics.addEventListener('click', function(){
- who.logHtml2('warning',"Triggering download of metrics CSV. Check your downloads folder.");
- who.downloadMetrics();
- //const m = who.metricsToArrays();
- //console.log("Metrics:",who.metrics, m);
- });
- this.e.selImpl.addEventListener('change', function(){
- who.getSelectedDb();
- });
- this.e.btnRunRemaining.addEventListener('click', async function(){
- let v = who.e.selSql.value;
- const timeStart = performance.now();
- while(v){
- await who.evalFile(v);
- if(who.gotError){
- who.logErr("Error handling script",v,":",who.gotError.message);
- break;
- }
- ++who.e.selSql.selectedIndex;
- v = who.e.selSql.value;
- }
- const timeTotal = performance.now() - timeStart;
- who.logHtml("Run-remaining time:",timeTotal,"ms ("+(timeTotal/1000/60)+" minute(s))");
- who.clearStorage();
- App.metrics.runTimeMs = timeTotal;
- who.logHtml("Total metrics:",JSON.stringify(App.metrics,undefined,' '));
- }, false);
- }/*run()*/
- }/*App*/;
- self.sqlite3TestModule.initSqlite3().then(function(sqlite3_){
- sqlite3 = sqlite3_;
- self.App = App /* only to facilitate dev console access */;
- App.run(sqlite3);
- });
- })();
|