'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;
})();
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){
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:{},
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);
},
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 stack = wasm.scopedAllocPush();
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;
}
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);
};
let runner;
if('websql'===db.id){
const who = this;
runner = function(resolve, reject){
if('string'!==typeof sql){ sql = new TextDecoder().decode(sql);
}
sql = sql.replace(/-- [^\n]+\n/g,''); 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;
for(s = nextSql(); !!s; s = nextSql()){
if(rxBegin.test(s)) continue;
else if(rxCommit.test(s)) break;
++metrics.stmtCount;
const t = performance.now();
tx.executeSql(s,[], ()=>{}, (t,e)=>{
console.error("WebSQL error",e,"SQL =",s);
who.logErr(e.message);
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)
return false;
}, nextBatch);
}else{
resolve(who);
}
};
metrics.evalSqlStart = performance.now();
nextBatch();
}catch(e){
console.error("websql error:",e);
who.logErr(e.message);
}
}.bind(this);
}else{
runner = function(resolve, reject){
metrics.evalSqlStart = performance.now();
try {
let t;
let sqlByteLen = sql.byteLength;
const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
t = performance.now();
pSqlBegin = wasm.scopedAlloc( sqlByteLen + 1) || 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;
++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);
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), 50);
});
}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;
this.logErr("TODO: clear db");
},
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);
},
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;
},
blockControls: function(disable){
this.e.fsToolbar.disabled = disable;
},
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()];
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);
},
evalFile: async function(fn){
const sql = await this.fetchFile(fn);
return this.execSql(fn,sql);
},
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();
}
}
},
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");
}
}
}
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();
});
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();
}, false);
}
};
self.sqlite3TestModule.initSqlite3().then(function(sqlite3_){
sqlite3 = sqlite3_;
self.App = App ;
App.run(sqlite3);
});
})();