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
);
75 const E
= (s
)=>document
.querySelector(s
);
78 output
: E('#test-output'),
79 selSql
: E('#sql-select'),
80 btnRun
: E('#sql-run'),
81 btnRunNext
: E('#sql-run-next'),
82 btnRunRemaining
: E('#sql-run-remaining'),
83 btnExportMetrics
: E('#export-metrics'),
84 btnClear
: E('#output-clear'),
85 btnReset
: E('#db-reset'),
86 cbReverseLog
: E('#cb-reverse-log-order'),
87 selImpl
: E('#select-impl'),
88 fsToolbar
: E('#toolbar')
90 db
: Object
.create(null),
91 dbs
: Object
.create(null),
102 log
: console
.log
.bind(console
),
103 warn
: console
.warn
.bind(console
),
104 cls: function(){this.e
.output
.innerHTML
= ''},
105 logHtml2: function(cssClass
,...args
){
106 const ln
= document
.createElement('div');
107 if(cssClass
) ln
.classList
.add(cssClass
);
108 ln
.append(document
.createTextNode(args
.join(' ')));
109 this.e
.output
.append(ln
);
110 //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
112 logHtml: function(...args
){
113 console
.log(...args
);
114 if(1) this.logHtml2('', ...args
);
116 logErr: function(...args
){
117 console
.error(...args
);
118 if(1) this.logHtml2('error', ...args
);
121 execSql
: async
function(name
,sql
){
122 const db
= this.getSelectedDb();
123 const banner
= "========================================";
125 "Running",name
,'('+sql
.length
,'bytes) using',db
.id
);
126 const capi
= this.sqlite3
.capi
, wasm
= this.sqlite3
.wasm
;
127 let pStmt
= 0, pSqlBegin
;
128 const metrics
= db
.metrics
= Object
.create(null);
129 metrics
.prepTotal
= metrics
.stepTotal
= 0;
130 metrics
.stmtCount
= 0;
133 this.blockControls(true);
135 this.logErr("Cannot run SQL: error cleanup is pending.");
138 // Run this async so that the UI can be updated for the above header...
140 metrics
.evalSqlEnd
= performance
.now();
141 metrics
.evalTimeTotal
= (metrics
.evalSqlEnd
- metrics
.evalSqlStart
);
142 this.logHtml(db
.id
,"metrics:",JSON
.stringify(metrics
, undefined, ' '));
143 this.logHtml("prepare() count:",metrics
.stmtCount
);
144 this.logHtml("Time in prepare_v2():",metrics
.prepTotal
,"ms",
145 "("+(metrics
.prepTotal
/ metrics
.stmtCount
),"ms per prepare())");
146 this.logHtml("Time in step():",metrics
.stepTotal
,"ms",
147 "("+(metrics
.stepTotal
/ metrics
.stmtCount
),"ms per step())");
148 this.logHtml("Total runtime:",metrics
.evalTimeTotal
,"ms");
149 this.logHtml("Overhead (time - prep - step):",
150 (metrics
.evalTimeTotal
- metrics
.prepTotal
- metrics
.stepTotal
)+"ms");
151 this.logHtml(banner
,"End of",name
);
152 this.metrics
.prepareTimeMs
+= metrics
.prepTotal
;
153 this.metrics
.stepTimeMs
+= metrics
.stepTotal
;
154 this.metrics
.stmtCount
+= metrics
.stmtCount
;
155 this.metrics
.strcpyMs
+= metrics
.strcpy
;
156 this.metrics
.sqlBytes
+= sql
.length
;
160 if('websql'===db
.id
){
162 runner = function(resolve
, reject
){
163 /* WebSQL cannot execute multiple statements, nor can it execute SQL without
164 an explicit transaction. Thus we have to do some fragile surgery on the
165 input SQL. Since we're only expecting carefully curated inputs, the hope is
166 that this will suffice. PS: it also can't run most SQL functions, e.g. even
167 instr() results in "not authorized". */
168 if('string'!==typeof sql
){ // assume TypedArray
169 sql
= new TextDecoder().decode(sql
);
171 sql
= sql
.replace(/-- [^\n]+\n/g,''); // comment lines interfere with our split()
172 const sqls
= sql
.split(/;+\n/);
173 const rxBegin
= /^BEGIN/i, rxCommit
= /^COMMIT/i;
175 const nextSql
= ()=>{
176 let x
= sqls
.shift();
177 while(sqls
.length
&& !x
) x
= sqls
.shift();
178 return x
&& x
.trim();
181 const transaction = function(tx
){
184 /* Try to approximate the spirit of the input scripts
185 by running batches bound by BEGIN/COMMIT statements. */
186 for(s = nextSql(); !!s; s = nextSql()){
187 if(rxBegin.test(s)) continue;
188 else if(rxCommit.test(s)) break;
189 //console.log("websql sql again",sqls.length, s);
191 const t = performance.now();
192 tx.executeSql(s,[], ()=>{}, (t,e)=>{
193 console.error("WebSQL error",e,"SQL =",s);
194 who.logErr(e.message);
198 metrics.stepTotal += performance.now() - t;
201 who.logErr("transaction():",e.message);
205 const n = sqls.length;
206 const nextBatch = function(){
208 console.log("websql sqls.length",sqls.length,'of',n);
209 db.handle.transaction(transaction, (e)=>{
210 who.logErr("Ignoring and contiuing:",e.message)
218 metrics.evalSqlStart = performance.now();
222 console.error("websql error:",e);
223 who.logErr(e.message);
227 }else{/*sqlite3 db...*/
228 runner = function(resolve
, reject
){
229 ++this.metrics
.fileCount
;
230 metrics
.evalSqlStart
= performance
.now();
231 const stack
= wasm
.scopedAllocPush();
234 let sqlByteLen
= sql
.byteLength
;
235 const [ppStmt
, pzTail
] = wasm
.scopedAllocPtr(2);
236 t
= performance
.now();
237 pSqlBegin
= wasm
.scopedAlloc( sqlByteLen
+ 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen
,") failed");
238 metrics
.malloc
= performance
.now() - t
;
239 metrics
.byteLength
= sqlByteLen
;
240 let pSql
= pSqlBegin
;
241 const pSqlEnd
= pSqlBegin
+ sqlByteLen
;
242 t
= performance
.now();
243 wasm
.heap8().set(sql
, pSql
);
244 wasm
.poke(pSql
+ sqlByteLen
, 0);
245 metrics
.strcpy
= performance
.now() - t
;
247 while(pSql
&& wasm
.peek(pSql
,'i8')){
248 wasm
.pokePtr(ppStmt
, 0);
249 wasm
.pokePtr(pzTail
, 0);
250 t
= performance
.now();
251 let rc
= capi
.sqlite3_prepare_v3(
252 db
.handle
, pSql
, sqlByteLen
, 0, ppStmt
, pzTail
254 metrics
.prepTotal
+= performance
.now() - t
;
255 checkSqliteRc(db
.handle
, rc
);
256 pStmt
= wasm
.peekPtr(ppStmt
);
257 pSql
= wasm
.peekPtr(pzTail
);
258 sqlByteLen
= pSqlEnd
- pSql
;
259 if(!pStmt
) continue/*empty statement*/;
261 t
= performance
.now();
262 rc
= capi
.sqlite3_step(pStmt
);
263 capi
.sqlite3_finalize(pStmt
);
265 metrics
.stepTotal
+= performance
.now() - t
;
267 case capi
.SQLITE_ROW
:
268 case capi
.SQLITE_DONE
: break;
269 default: checkSqliteRc(db
.handle
, rc
); toss("Not reached.");
274 if(pStmt
) capi
.sqlite3_finalize(pStmt
);
278 capi
.sqlite3_exec(db
.handle
,"rollback;",0,0,0);
279 wasm
.scopedAllocPop(stack
);
285 p
= new Promise(function(res
,rej
){
286 setTimeout(()=>runner(res
, rej
), 0)/*give UI a chance to output the "running" banner*/;
289 p
= new Promise(runner
);
292 (e
)=>this.logErr("Error via execSql("+name
+",...):",e
.message
)
295 this.blockControls(false);
300 const db
= this.getSelectedDb();
301 if('websql'===db
.id
){
302 this.logErr("TODO: clear websql db.");
305 if(!db
.handle
) return;
306 const capi
= this.sqlite3
, wasm
= this.sqlite3
.wasm
;
307 //const scope = wasm.scopedAllocPush(
308 this.logErr("TODO: clear db");
312 Loads batch-runner.list and populates the selection list from
313 it. Returns a promise which resolves to nothing in particular
314 when it completes. Only intended to be run once at the start
317 loadSqlList
: async
function(){
318 const sel
= this.e
.selSql
;
320 this.blockControls(true);
321 const infile
= 'batch-runner.list';
322 this.logHtml("Loading list of SQL files:", infile
);
325 const r
= await
fetch(infile
);
326 if(404 === r
.status
){
327 toss("Missing file '"+infile
+"'.");
329 if(!r
.ok
) toss("Loading",infile
,"failed:",r
.statusText
);
330 txt
= await r
.text();
331 const warning
= E('#warn-list');
332 if(warning
) warning
.remove();
334 this.logErr(e
.message
);
337 this.blockControls(false);
339 const list
= txt
.split(/\n+/);
342 opt
= document
.createElement('option');
343 opt
.innerText
= "Select file to evaluate...";
347 sel
.appendChild(opt
);
349 list
.forEach(function(fn
){
351 opt
= document
.createElement('option');
353 opt
.innerText
= fn
.split('/').pop();
354 sel
.appendChild(opt
);
356 this.logHtml("Loaded",infile
);
359 /** Fetch ./fn and return its contents as a Uint8Array. */
360 fetchFile: async function(fn, cacheIt=false){
361 if(cacheIt && this.cache[fn]) return this.cache[fn];
362 this.logHtml("Fetching",fn,"...");
365 const r = await fetch(fn);
366 if(!r.ok) toss("Fetch failed:",r.statusText);
367 sql = new Uint8Array(await r.arrayBuffer());
369 this.logErr(e.message);
372 this.logHtml("Fetched",sql.length,"bytes from",fn);
373 if(cacheIt) this.cache[fn] = sql;
377 /** Disable or enable certain UI controls. */
378 blockControls: function(disable
){
379 //document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
380 this.e
.fsToolbar
.disabled
= disable
;
384 Converts this.metrics() to a form which is suitable for easy conversion to
385 CSV. It returns an array of arrays. The first sub-array is the column names.
386 The 2nd and subsequent are the values, one per test file (only the most recent
387 metrics are kept for any given file).
389 metricsToArrays: function(){
391 Object
.keys(this.dbs
).sort().forEach((k
)=>{
392 const d
= this.dbs
[k
];
394 delete m
.evalSqlStart
;
396 const mk
= Object
.keys(m
).sort();
398 rc
.push(['db', ...mk
]);
400 const row
= [k
.split('/').pop()/*remove dir prefix from filename*/];
402 row
.push(...mk
.map((kk
)=>m
[kk
]));
407 metricsToBlob: function(colSeparator
='\t'){
408 const ar
= [], ma
= this.metricsToArrays();
410 this.logErr("Metrics are empty. Run something.");
413 ma
.forEach(function(row
){
414 ar
.push(row
.join(colSeparator
),'\n');
419 downloadMetrics: function(){
420 const b
= this.metricsToBlob();
422 const url
= URL
.createObjectURL(b
);
423 const a
= document
.createElement('a');
425 a
.download
= 'batch-runner-js-'+((new Date().getTime()/1000) | 0)+'.csv';
426 this.logHtml("Triggering download of",a
.download
);
427 document
.body
.appendChild(a
);
430 document
.body
.removeChild(a
);
431 URL
.revokeObjectURL(url
);
436 Fetch file fn and eval it as an SQL blob. This is an async
437 operation and returns a Promise which resolves to this
440 evalFile
: async
function(fn
){
441 const sql
= await
this.fetchFile(fn
);
442 return this.execSql(fn
,sql
);
446 Clears all DB tables in all _opened_ databases. Because of
447 disparities between backends, we cannot simply "unlink" the
448 databases to clean them up.
450 clearStorage: function(onlySelectedDb
=false){
451 const list
= onlySelectedDb
452 ? [('boolean'===typeof onlySelectedDb
)
453 ? this.dbs
[this.e
.selImpl
.value
]
455 : Object
.values(this.dbs
);
458 this.logHtml("Clearing db",db
.id
);
465 Fetches the handle of the db associated with
466 this.e.selImpl.value, opening it if needed.
468 getSelectedDb: function(){
470 for(let opt
of this.e
.selImpl
.options
){
471 const d
= this.dbs
[opt
.value
] = Object
.create(null);
475 d
.filename
= 'file:/virtualfs.sqlite3?vfs=unix-none';
478 d
.filename
= ':memory:';
481 d
.filename
= 'file:'+(
482 this.sqlite3
.capi
.sqlite3_wasmfs_opfs_dir()
483 )+'/wasmfs-opfs.sqlite3b';
486 d
.filename
= 'websql.db';
489 this.logErr("Unhandled db selection option (see details in the console).",opt
);
490 toss("Unhandled db init option");
494 const dbId
= this.e
.selImpl
.value
;
495 const d
= this.dbs
[dbId
];
496 if(d
.handle
) return d
;
497 if('websql' === dbId
){
498 d
.handle
= self
.openDatabase('batch-runner', '0.1', 'foo', 1024 * 1024 * 50);
499 d
.clear
= ()=>clearDbWebSQL(d
);
500 d
.handle
.transaction(function(tx
){
501 tx
.executeSql("PRAGMA cache_size="+cacheSize
);
502 App
.logHtml(dbId
,"cache_size =",cacheSize
);
505 const capi
= this.sqlite3
.capi
, wasm
= this.sqlite3
.wasm
;
506 const stack
= wasm
.scopedAllocPush();
509 const oFlags
= capi
.SQLITE_OPEN_CREATE
| capi
.SQLITE_OPEN_READWRITE
;
510 const ppDb
= wasm
.scopedAllocPtr();
511 const rc
= capi
.sqlite3_open_v2(d
.filename
, ppDb
, oFlags
, null);
512 pDb
= wasm
.peekPtr(ppDb
)
513 if(rc
) toss("sqlite3_open_v2() failed with code",rc
);
514 capi
.sqlite3_exec(pDb
, "PRAGMA cache_size="+cacheSize
, 0, 0, 0);
515 this.logHtml(dbId
,"cache_size =",cacheSize
);
517 if(pDb
) capi
.sqlite3_close_v2(pDb
);
519 wasm
.scopedAllocPop(stack
);
522 d
.clear
= ()=>clearDbSqlite(d
);
525 this.logHtml("Opened db:",dbId
,d
.filename
);
526 console
.log("db =",d
);
530 run: function(sqlite3
){
532 this.sqlite3
= sqlite3
;
533 const capi
= sqlite3
.capi
, wasm
= sqlite3
.wasm
;
534 this.logHtml("Loaded module:",capi
.sqlite3_libversion(), capi
.sqlite3_sourceid());
535 this.logHtml("WASM heap size =",wasm
.heap8().length
);
537 if(capi
.sqlite3_wasmfs_opfs_dir()){
538 E('#warn-opfs').classList
.remove('hidden');
540 E('#warn-opfs').remove();
541 E('option[value=wasmfs-opfs]').disabled
= true;
543 if('function' === typeof self
.openDatabase
){
544 E('#warn-websql').classList
.remove('hidden');
546 E('option[value=websql]').disabled
= true;
547 E('#warn-websql').remove();
550 if(this.e
.cbReverseLog
.checked
){
551 this.e
.output
.classList
.add('reverse');
553 this.e
.cbReverseLog
.addEventListener('change', function(){
554 who
.e
.output
.classList
[this.checked
? 'add' : 'remove']('reverse');
556 this.e
.btnClear
.addEventListener('click', ()=>this.cls(), false);
557 this.e
.btnRun
.addEventListener('click', function(){
558 if(!who
.e
.selSql
.value
) return;
559 who
.evalFile(who
.e
.selSql
.value
);
561 this.e
.btnRunNext
.addEventListener('click', function(){
562 ++who
.e
.selSql
.selectedIndex
;
563 if(!who
.e
.selSql
.value
) return;
564 who
.evalFile(who
.e
.selSql
.value
);
566 this.e
.btnReset
.addEventListener('click', function(){
567 who
.clearStorage(true);
569 this.e
.btnExportMetrics
.addEventListener('click', function(){
570 who
.logHtml2('warning',"Triggering download of metrics CSV. Check your downloads folder.");
571 who
.downloadMetrics();
572 //const m = who.metricsToArrays();
573 //console.log("Metrics:",who.metrics, m);
575 this.e
.selImpl
.addEventListener('change', function(){
578 this.e
.btnRunRemaining
.addEventListener('click', async
function(){
579 let v
= who
.e
.selSql
.value
;
580 const timeStart
= performance
.now();
582 await who
.evalFile(v
);
584 who
.logErr("Error handling script",v
,":",who
.gotError
.message
);
587 ++who
.e
.selSql
.selectedIndex
;
588 v
= who
.e
.selSql
.value
;
590 const timeTotal
= performance
.now() - timeStart
;
591 who
.logHtml("Run-remaining time:",timeTotal
,"ms ("+(timeTotal
/1000/60)+" minute(s))");
593 App
.metrics
.runTimeMs
= timeTotal
;
594 who
.logHtml("Total metrics:",JSON
.stringify(App
.metrics
,undefined,' '));
599 self
.sqlite3TestModule
.initSqlite3().then(function(sqlite3_
){
601 self
.App
= App
/* only to facilitate dev console access */;