4 The author disclaims copyright to this source code. In place of a
5 legal notice, here is a blessing:
7 * May you do good and not evil.
8 * May you find forgiveness for yourself and forgive others.
9 * May you share freely, never taking more than you give.
11 ***********************************************************************
13 A basic batch SQL runner for sqlite3-api.js. This file must be run in
14 main JS thread and sqlite3.js must have been loaded before it.
18 const toss = function(...args
){throw new Error(args
.join(' '))};
19 const warn
= console
.warn
.bind(console
);
21 const urlParams
= new URL(self
.location
.href
).searchParams
;
22 const cacheSize
= (()=>{
23 if(urlParams
.has('cachesize')) return +urlParams
.get('cachesize');
27 /** Throws if the given sqlite3 result code is not 0. */
28 const checkSqliteRc
= (dbh
,rc
)=>{
29 if(rc
) toss("Prepare failed:",sqlite3
.capi
.sqlite3_errmsg(dbh
));
33 "SELECT type,name FROM sqlite_schema ",
34 "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
35 "AND name NOT LIKE '\\_%' escape '\\'"
38 const clearDbWebSQL = function(db
){
39 db
.handle
.transaction(function(tx
){
40 const onErr
= (e
)=>console
.error(e
);
41 const callback = function(tx
, result
){
42 const rows
= result
.rows
;
46 const row
= rows
.item(i
);
47 const name
= JSON
.stringify(row
.name
);
48 const type
= row
.type
;
50 case 'index': case 'table':
51 case 'trigger': case 'view': {
52 const sql2
= 'DROP '+type
+' '+name
;
53 tx
.executeSql(sql2
, [], ()=>{}, onErr
);
57 warn("Unhandled db entry type:",type
,'name =',name
);
62 tx
.executeSql(sqlToDrop
, [], callback
, onErr
);
63 db
.handle
.changeVersion(db
.handle
.version
, "", ()=>{}, onErr
, ()=>{});
67 const clearDbSqlite = function(db
){
68 // This would be SO much easier with the oo1 API, but we specifically want to
69 // inject metrics we can't get via that API, and we cannot reliably (OPFS)
70 // open the same DB twice to clear it using that API, so...
71 const rc
= sqlite3
.wasm
.exports
.sqlite3_wasm_db_reset(db
.handle
);
72 App
.logHtml("reset db rc =",rc
,db
.id
, db
.filename
);
76 const E
= (s
)=>document
.querySelector(s
);
79 output
: E('#test-output'),
80 selSql
: E('#sql-select'),
81 btnRun
: E('#sql-run'),
82 btnRunNext
: E('#sql-run-next'),
83 btnRunRemaining
: E('#sql-run-remaining'),
84 btnExportMetrics
: E('#export-metrics'),
85 btnClear
: E('#output-clear'),
86 btnReset
: E('#db-reset'),
87 cbReverseLog
: E('#cb-reverse-log-order'),
88 selImpl
: E('#select-impl'),
89 fsToolbar
: E('#toolbar')
91 db
: Object
.create(null),
92 dbs
: Object
.create(null),
94 log
: console
.log
.bind(console
),
95 warn
: console
.warn
.bind(console
),
96 cls: function(){this.e
.output
.innerHTML
= ''},
97 logHtml2: function(cssClass
,...args
){
98 const ln
= document
.createElement('div');
99 if(cssClass
) ln
.classList
.add(cssClass
);
100 ln
.append(document
.createTextNode(args
.join(' ')));
101 this.e
.output
.append(ln
);
102 //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
104 logHtml: function(...args
){
105 console
.log(...args
);
106 if(1) this.logHtml2('', ...args
);
108 logErr: function(...args
){
109 console
.error(...args
);
110 if(1) this.logHtml2('error', ...args
);
113 execSql
: async
function(name
,sql
){
114 const db
= this.getSelectedDb();
115 const banner
= "========================================";
117 "Running",name
,'('+sql
.length
,'bytes) using',db
.id
);
118 const capi
= this.sqlite3
.capi
, wasm
= this.sqlite3
.wasm
;
119 let pStmt
= 0, pSqlBegin
;
120 const stack
= wasm
.scopedAllocPush();
121 const metrics
= db
.metrics
= Object
.create(null);
122 metrics
.prepTotal
= metrics
.stepTotal
= 0;
123 metrics
.stmtCount
= 0;
126 this.blockControls(true);
128 this.logErr("Cannot run SQL: error cleanup is pending.");
131 // Run this async so that the UI can be updated for the above header...
133 metrics
.evalSqlEnd
= performance
.now();
134 metrics
.evalTimeTotal
= (metrics
.evalSqlEnd
- metrics
.evalSqlStart
);
135 this.logHtml(db
.id
,"metrics:",JSON
.stringify(metrics
, undefined, ' '));
136 this.logHtml("prepare() count:",metrics
.stmtCount
);
137 this.logHtml("Time in prepare_v2():",metrics
.prepTotal
,"ms",
138 "("+(metrics
.prepTotal
/ metrics
.stmtCount
),"ms per prepare())");
139 this.logHtml("Time in step():",metrics
.stepTotal
,"ms",
140 "("+(metrics
.stepTotal
/ metrics
.stmtCount
),"ms per step())");
141 this.logHtml("Total runtime:",metrics
.evalTimeTotal
,"ms");
142 this.logHtml("Overhead (time - prep - step):",
143 (metrics
.evalTimeTotal
- metrics
.prepTotal
- metrics
.stepTotal
)+"ms");
144 this.logHtml(banner
,"End of",name
);
148 if('websql'===db
.id
){
150 runner = function(resolve
, reject
){
151 /* WebSQL cannot execute multiple statements, nor can it execute SQL without
152 an explicit transaction. Thus we have to do some fragile surgery on the
153 input SQL. Since we're only expecting carefully curated inputs, the hope is
154 that this will suffice. PS: it also can't run most SQL functions, e.g. even
155 instr() results in "not authorized". */
156 if('string'!==typeof sql
){ // assume TypedArray
157 sql
= new TextDecoder().decode(sql
);
159 sql
= sql
.replace(/-- [^\n]+\n/g,''); // comment lines interfere with our split()
160 const sqls
= sql
.split(/;+\n/);
161 const rxBegin
= /^BEGIN/i, rxCommit
= /^COMMIT/i;
163 const nextSql
= ()=>{
164 let x
= sqls
.shift();
165 while(sqls
.length
&& !x
) x
= sqls
.shift();
166 return x
&& x
.trim();
169 const transaction = function(tx
){
172 /* Try to approximate the spirit of the input scripts
173 by running batches bound by BEGIN/COMMIT statements. */
174 for(s = nextSql(); !!s; s = nextSql()){
175 if(rxBegin.test(s)) continue;
176 else if(rxCommit.test(s)) break;
177 //console.log("websql sql again",sqls.length, s);
179 const t = performance.now();
180 tx.executeSql(s,[], ()=>{}, (t,e)=>{
181 console.error("WebSQL error",e,"SQL =",s);
182 who.logErr(e.message);
186 metrics.stepTotal += performance.now() - t;
189 who.logErr("transaction():",e.message);
193 const n = sqls.length;
194 const nextBatch = function(){
196 console.log("websql sqls.length",sqls.length,'of',n);
197 db.handle.transaction(transaction, (e)=>{
198 who.logErr("Ignoring and contiuing:",e.message)
206 metrics.evalSqlStart = performance.now();
210 console.error("websql error:",e);
211 who.logErr(e.message);
215 }else{/*sqlite3 db...*/
216 runner = function(resolve
, reject
){
217 metrics
.evalSqlStart
= performance
.now();
220 let sqlByteLen
= sql
.byteLength
;
221 const [ppStmt
, pzTail
] = wasm
.scopedAllocPtr(2);
222 t
= performance
.now();
223 pSqlBegin
= wasm
.scopedAlloc( sqlByteLen
+ 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen
,") failed");
224 metrics
.malloc
= performance
.now() - t
;
225 metrics
.byteLength
= sqlByteLen
;
226 let pSql
= pSqlBegin
;
227 const pSqlEnd
= pSqlBegin
+ sqlByteLen
;
228 t
= performance
.now();
229 wasm
.heap8().set(sql
, pSql
);
230 wasm
.poke(pSql
+ sqlByteLen
, 0);
231 metrics
.strcpy
= performance
.now() - t
;
233 while(pSql
&& wasm
.peek(pSql
,'i8')){
234 wasm
.pokePtr(ppStmt
, 0);
235 wasm
.pokePtr(pzTail
, 0);
236 t
= performance
.now();
237 let rc
= capi
.sqlite3_prepare_v3(
238 db
.handle
, pSql
, sqlByteLen
, 0, ppStmt
, pzTail
240 metrics
.prepTotal
+= performance
.now() - t
;
241 checkSqliteRc(db
.handle
, rc
);
242 pStmt
= wasm
.peekPtr(ppStmt
);
243 pSql
= wasm
.peekPtr(pzTail
);
244 sqlByteLen
= pSqlEnd
- pSql
;
245 if(!pStmt
) continue/*empty statement*/;
247 t
= performance
.now();
248 rc
= capi
.sqlite3_step(pStmt
);
249 capi
.sqlite3_finalize(pStmt
);
251 metrics
.stepTotal
+= performance
.now() - t
;
253 case capi
.SQLITE_ROW
:
254 case capi
.SQLITE_DONE
: break;
255 default: checkSqliteRc(db
.handle
, rc
); toss("Not reached.");
260 if(pStmt
) capi
.sqlite3_finalize(pStmt
);
264 capi
.sqlite3_exec(db
.handle
,"rollback;",0,0,0);
265 wasm
.scopedAllocPop(stack
);
271 p
= new Promise(function(res
,rej
){
272 setTimeout(()=>runner(res
, rej
), 50)/*give UI a chance to output the "running" banner*/;
275 p
= new Promise(runner
);
278 (e
)=>this.logErr("Error via execSql("+name
+",...):",e
.message
)
281 this.blockControls(false);
286 const db
= this.getSelectedDb();
287 if('websql'===db
.id
){
288 this.logErr("TODO: clear websql db.");
291 if(!db
.handle
) return;
292 const capi
= this.sqlite3
, wasm
= this.sqlite3
.wasm
;
293 //const scope = wasm.scopedAllocPush(
294 this.logErr("TODO: clear db");
298 Loads batch-runner.list and populates the selection list from
299 it. Returns a promise which resolves to nothing in particular
300 when it completes. Only intended to be run once at the start
303 loadSqlList
: async
function(){
304 const sel
= this.e
.selSql
;
306 this.blockControls(true);
307 const infile
= 'batch-runner.list';
308 this.logHtml("Loading list of SQL files:", infile
);
311 const r
= await
fetch(infile
);
312 if(404 === r
.status
){
313 toss("Missing file '"+infile
+"'.");
315 if(!r
.ok
) toss("Loading",infile
,"failed:",r
.statusText
);
316 txt
= await r
.text();
317 const warning
= E('#warn-list');
318 if(warning
) warning
.remove();
320 this.logErr(e
.message
);
323 this.blockControls(false);
325 const list
= txt
.split(/\n+/);
328 opt
= document
.createElement('option');
329 opt
.innerText
= "Select file to evaluate...";
333 sel
.appendChild(opt
);
335 list
.forEach(function(fn
){
337 opt
= document
.createElement('option');
339 opt
.innerText
= fn
.split('/').pop();
340 sel
.appendChild(opt
);
342 this.logHtml("Loaded",infile
);
345 /** Fetch ./fn and return its contents as a Uint8Array. */
346 fetchFile: async function(fn, cacheIt=false){
347 if(cacheIt && this.cache[fn]) return this.cache[fn];
348 this.logHtml("Fetching",fn,"...");
351 const r = await fetch(fn);
352 if(!r.ok) toss("Fetch failed:",r.statusText);
353 sql = new Uint8Array(await r.arrayBuffer());
355 this.logErr(e.message);
358 this.logHtml("Fetched",sql.length,"bytes from",fn);
359 if(cacheIt) this.cache[fn] = sql;
363 /** Disable or enable certain UI controls. */
364 blockControls: function(disable
){
365 //document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
366 this.e
.fsToolbar
.disabled
= disable
;
370 Converts this.metrics() to a form which is suitable for easy conversion to
371 CSV. It returns an array of arrays. The first sub-array is the column names.
372 The 2nd and subsequent are the values, one per test file (only the most recent
373 metrics are kept for any given file).
375 metricsToArrays: function(){
377 Object
.keys(this.dbs
).sort().forEach((k
)=>{
378 const d
= this.dbs
[k
];
380 delete m
.evalSqlStart
;
382 const mk
= Object
.keys(m
).sort();
384 rc
.push(['db', ...mk
]);
386 const row
= [k
.split('/').pop()/*remove dir prefix from filename*/];
388 row
.push(...mk
.map((kk
)=>m
[kk
]));
393 metricsToBlob: function(colSeparator
='\t'){
394 const ar
= [], ma
= this.metricsToArrays();
396 this.logErr("Metrics are empty. Run something.");
399 ma
.forEach(function(row
){
400 ar
.push(row
.join(colSeparator
),'\n');
405 downloadMetrics: function(){
406 const b
= this.metricsToBlob();
408 const url
= URL
.createObjectURL(b
);
409 const a
= document
.createElement('a');
411 a
.download
= 'batch-runner-js-'+((new Date().getTime()/1000) | 0)+'.csv';
412 this.logHtml("Triggering download of",a
.download
);
413 document
.body
.appendChild(a
);
416 document
.body
.removeChild(a
);
417 URL
.revokeObjectURL(url
);
422 Fetch file fn and eval it as an SQL blob. This is an async
423 operation and returns a Promise which resolves to this
426 evalFile
: async
function(fn
){
427 const sql
= await
this.fetchFile(fn
);
428 return this.execSql(fn
,sql
);
432 Clears all DB tables in all _opened_ databases. Because of
433 disparities between backends, we cannot simply "unlink" the
434 databases to clean them up.
436 clearStorage: function(onlySelectedDb
=false){
437 const list
= onlySelectedDb
438 ? [('boolean'===typeof onlySelectedDb
)
439 ? this.dbs
[this.e
.selImpl
.value
]
441 : Object
.values(this.dbs
);
444 this.logHtml("Clearing db",db
.id
);
451 Fetches the handle of the db associated with
452 this.e.selImpl.value, opening it if needed.
454 getSelectedDb: function(){
456 for(let opt
of this.e
.selImpl
.options
){
457 const d
= this.dbs
[opt
.value
] = Object
.create(null);
461 d
.filename
= 'file:/virtualfs.sqlite3?vfs=unix-none';
464 d
.filename
= ':memory:';
467 d
.filename
= 'file:'+(
468 this.sqlite3
.capi
.sqlite3_wasmfs_opfs_dir()
469 )+'/wasmfs-opfs.sqlite3b';
472 d
.filename
= 'websql.db';
475 this.logErr("Unhandled db selection option (see details in the console).",opt
);
476 toss("Unhandled db init option");
480 const dbId
= this.e
.selImpl
.value
;
481 const d
= this.dbs
[dbId
];
482 if(d
.handle
) return d
;
483 if('websql' === dbId
){
484 d
.handle
= self
.openDatabase('batch-runner', '0.1', 'foo', 1024 * 1024 * 50);
485 d
.clear
= ()=>clearDbWebSQL(d
);
486 d
.handle
.transaction(function(tx
){
487 tx
.executeSql("PRAGMA cache_size="+cacheSize
);
488 App
.logHtml(dbId
,"cache_size =",cacheSize
);
491 const capi
= this.sqlite3
.capi
, wasm
= this.sqlite3
.wasm
;
492 const stack
= wasm
.scopedAllocPush();
495 const oFlags
= capi
.SQLITE_OPEN_CREATE
| capi
.SQLITE_OPEN_READWRITE
;
496 const ppDb
= wasm
.scopedAllocPtr();
497 const rc
= capi
.sqlite3_open_v2(d
.filename
, ppDb
, oFlags
, null);
498 pDb
= wasm
.peekPtr(ppDb
)
499 if(rc
) toss("sqlite3_open_v2() failed with code",rc
);
500 capi
.sqlite3_exec(pDb
, "PRAGMA cache_size="+cacheSize
, 0, 0, 0);
501 this.logHtml(dbId
,"cache_size =",cacheSize
);
503 if(pDb
) capi
.sqlite3_close_v2(pDb
);
505 wasm
.scopedAllocPop(stack
);
508 d
.clear
= ()=>clearDbSqlite(d
);
511 this.logHtml("Opened db:",dbId
,d
.filename
);
512 console
.log("db =",d
);
516 run: function(sqlite3
){
518 this.sqlite3
= sqlite3
;
519 const capi
= sqlite3
.capi
, wasm
= sqlite3
.wasm
;
520 this.logHtml("Loaded module:",capi
.sqlite3_libversion(), capi
.sqlite3_sourceid());
521 this.logHtml("WASM heap size =",wasm
.heap8().length
);
523 if(capi
.sqlite3_wasmfs_opfs_dir()){
524 E('#warn-opfs').classList
.remove('hidden');
526 E('#warn-opfs').remove();
527 E('option[value=wasmfs-opfs]').disabled
= true;
529 if('function' === typeof self
.openDatabase
){
530 E('#warn-websql').classList
.remove('hidden');
532 E('option[value=websql]').disabled
= true;
533 E('#warn-websql').remove();
536 if(this.e
.cbReverseLog
.checked
){
537 this.e
.output
.classList
.add('reverse');
539 this.e
.cbReverseLog
.addEventListener('change', function(){
540 who
.e
.output
.classList
[this.checked
? 'add' : 'remove']('reverse');
542 this.e
.btnClear
.addEventListener('click', ()=>this.cls(), false);
543 this.e
.btnRun
.addEventListener('click', function(){
544 if(!who
.e
.selSql
.value
) return;
545 who
.evalFile(who
.e
.selSql
.value
);
547 this.e
.btnRunNext
.addEventListener('click', function(){
548 ++who
.e
.selSql
.selectedIndex
;
549 if(!who
.e
.selSql
.value
) return;
550 who
.evalFile(who
.e
.selSql
.value
);
552 this.e
.btnReset
.addEventListener('click', function(){
553 who
.clearStorage(true);
555 this.e
.btnExportMetrics
.addEventListener('click', function(){
556 who
.logHtml2('warning',"Triggering download of metrics CSV. Check your downloads folder.");
557 who
.downloadMetrics();
558 //const m = who.metricsToArrays();
559 //console.log("Metrics:",who.metrics, m);
561 this.e
.selImpl
.addEventListener('change', function(){
564 this.e
.btnRunRemaining
.addEventListener('click', async
function(){
565 let v
= who
.e
.selSql
.value
;
566 const timeStart
= performance
.now();
568 await who
.evalFile(v
);
570 who
.logErr("Error handling script",v
,":",who
.gotError
.message
);
573 ++who
.e
.selSql
.selectedIndex
;
574 v
= who
.e
.selSql
.value
;
576 const timeTotal
= performance
.now() - timeStart
;
577 who
.logHtml("Run-remaining time:",timeTotal
,"ms ("+(timeTotal
/1000/60)+" minute(s))");
583 self
.sqlite3TestModule
.initSqlite3().then(function(sqlite3_
){
585 self
.App
= App
/* only to facilitate dev console access */;