-
Notifications
You must be signed in to change notification settings - Fork 1k
/
sqlite3-api-oo1.js
1438 lines (1340 loc) · 55 KB
/
sqlite3-api-oo1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
2022-07-22
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.
***********************************************************************
This file contains the so-called OO #1 API wrapper for the sqlite3
WASM build. It requires that sqlite3-api-glue.js has already run
and it installs its deliverable as self.sqlite3.oo1.
*/
(function(self){
const toss = (...args)=>{throw new Error(args.join(' '))};
const sqlite3 = self.sqlite3 || toss("Missing main sqlite3 object.");
const capi = sqlite3.capi, util = capi.util;
/* What follows is colloquially known as "OO API #1". It is a
binding of the sqlite3 API which is designed to be run within
the same thread (main or worker) as the one in which the
sqlite3 WASM binding was initialized. This wrapper cannot use
the sqlite3 binding if, e.g., the wrapper is in the main thread
and the sqlite3 API is in a worker. */
/**
In order to keep clients from manipulating, perhaps
inadvertently, the underlying pointer values of DB and Stmt
instances, we'll gate access to them via the `pointer` property
accessor and store their real values in this map. Keys = DB/Stmt
objects, values = pointer values. This also unifies how those are
accessed, for potential use downstream via custom
capi.wasm.xWrap() function signatures which know how to extract
it.
*/
const __ptrMap = new WeakMap();
/**
Map of DB instances to objects, each object being a map of UDF
names to wasm function _pointers_ added to that DB handle via
createFunction().
*/
const __udfMap = new WeakMap();
/**
Map of DB instances to objects, each object being a map of Stmt
wasm pointers to Stmt objects.
*/
const __stmtMap = new WeakMap();
/** If object opts has _its own_ property named p then that
property's value is returned, else dflt is returned. */
const getOwnOption = (opts, p, dflt)=>
opts.hasOwnProperty(p) ? opts[p] : dflt;
/**
An Error subclass specifically for reporting DB-level errors and
enabling clients to unambiguously identify such exceptions.
*/
class SQLite3Error extends Error {
constructor(...args){
super(...args);
this.name = 'SQLite3Error';
}
};
const toss3 = (...args)=>{throw new SQLite3Error(args)};
sqlite3.SQLite3Error = SQLite3Error;
/**
The DB class provides a high-level OO wrapper around an sqlite3
db handle.
The given db filename must be resolvable using whatever
filesystem layer (virtual or otherwise) is set up for the default
sqlite3 VFS.
Note that the special sqlite3 db names ":memory:" and ""
(temporary db) have their normal special meanings here and need
not resolve to real filenames, but "" uses an on-storage
temporary database and requires that the VFS support that.
The db is currently opened with a fixed set of flags:
(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
SQLITE_OPEN_EXRESCODE). This API will change in the future
permit the caller to provide those flags via an additional
argument.
For purposes of passing a DB instance to C-style sqlite3
functions, its read-only `pointer` property holds its `sqlite3*`
pointer value. That property can also be used to check whether
this DB instance is still open.
*/
const DB = function ctor(fn=':memory:'){
if('string'!==typeof fn){
toss3("Invalid filename for DB constructor.");
}
const stack = capi.wasm.scopedAllocPush();
let ptr;
try {
const ppDb = capi.wasm.scopedAllocPtr() /* output (sqlite3**) arg */;
const rc = capi.sqlite3_open_v2(fn, ppDb, capi.SQLITE_OPEN_READWRITE
| capi.SQLITE_OPEN_CREATE
| capi.SQLITE_OPEN_EXRESCODE, null);
ptr = capi.wasm.getMemValue(ppDb, '*');
ctor.checkRc(ptr, rc);
}catch(e){
if(ptr) capi.sqlite3_close_v2(ptr);
throw e;
}
finally{capi.wasm.scopedAllocPop(stack);}
this.filename = fn;
__ptrMap.set(this, ptr);
__stmtMap.set(this, Object.create(null));
__udfMap.set(this, Object.create(null));
};
/**
Internal-use enum for mapping JS types to DB-bindable types.
These do not (and need not) line up with the SQLITE_type
values. All values in this enum must be truthy and distinct
but they need not be numbers.
*/
const BindTypes = {
null: 1,
number: 2,
string: 3,
boolean: 4,
blob: 5
};
BindTypes['undefined'] == BindTypes.null;
if(capi.wasm.bigIntEnabled){
BindTypes.bigint = BindTypes.number;
}
/**
This class wraps sqlite3_stmt. Calling this constructor
directly will trigger an exception. Use DB.prepare() to create
new instances.
For purposes of passing a Stmt instance to C-style sqlite3
functions, its read-only `pointer` property holds its `sqlite3_stmt*`
pointer value.
*/
const Stmt = function(){
if(BindTypes!==arguments[2]){
toss3("Do not call the Stmt constructor directly. Use DB.prepare().");
}
this.db = arguments[0];
__ptrMap.set(this, arguments[1]);
this.columnCount = capi.sqlite3_column_count(this.pointer);
this.parameterCount = capi.sqlite3_bind_parameter_count(this.pointer);
};
/** Throws if the given DB has been closed, else it is returned. */
const affirmDbOpen = function(db){
if(!db.pointer) toss3("DB has been closed.");
return db;
};
/** Throws if ndx is not an integer or if it is out of range
for stmt.columnCount, else returns stmt.
Reminder: this will also fail after the statement is finalized
but the resulting error will be about an out-of-bounds column
index.
*/
const affirmColIndex = function(stmt,ndx){
if((ndx !== (ndx|0)) || ndx<0 || ndx>=stmt.columnCount){
toss3("Column index",ndx,"is out of range.");
}
return stmt;
};
/**
Expects to be passed (arguments) from DB.exec() and
DB.execMulti(). Does the argument processing/validation, throws
on error, and returns a new object on success:
{ sql: the SQL, opt: optionsObj, cbArg: function}
cbArg is only set if the opt.callback is set, in which case
it's a function which expects to be passed the current Stmt
and returns the callback argument of the type indicated by
the input arguments.
*/
const parseExecArgs = function(args){
const out = Object.create(null);
out.opt = Object.create(null);
switch(args.length){
case 1:
if('string'===typeof args[0] || util.isSQLableTypedArray(args[0])){
out.sql = args[0];
}else if(args[0] && 'object'===typeof args[0]){
out.opt = args[0];
out.sql = out.opt.sql;
}
break;
case 2:
out.sql = args[0];
out.opt = args[1];
break;
default: toss3("Invalid argument count for exec().");
};
if(util.isSQLableTypedArray(out.sql)){
out.sql = util.typedArrayToString(out.sql);
}else if(Array.isArray(out.sql)){
out.sql = out.sql.join('');
}else if('string'!==typeof out.sql){
toss3("Missing SQL argument.");
}
if(out.opt.callback || out.opt.resultRows){
switch((undefined===out.opt.rowMode)
? 'stmt' : out.opt.rowMode) {
case 'object': out.cbArg = (stmt)=>stmt.get({}); break;
case 'array': out.cbArg = (stmt)=>stmt.get([]); break;
case 'stmt':
if(Array.isArray(out.opt.resultRows)){
toss3("Invalid rowMode for resultRows array: must",
"be one of 'array', 'object',",
"or a result column number.");
}
out.cbArg = (stmt)=>stmt;
break;
default:
if(util.isInt32(out.opt.rowMode)){
out.cbArg = (stmt)=>stmt.get(out.opt.rowMode);
break;
}
toss3("Invalid rowMode:",out.opt.rowMode);
}
}
return out;
};
/**
Expects to be given a DB instance or an `sqlite3*` pointer, and an
sqlite3 API result code. If the result code is not falsy, this
function throws an SQLite3Error with an error message from
sqlite3_errmsg(), using dbPtr as the db handle. Note that if it's
passed a non-error code like SQLITE_ROW or SQLITE_DONE, it will
still throw but the error string might be "Not an error." The
various non-0 non-error codes need to be checked for in client
code where they are expected.
*/
DB.checkRc = function(dbPtr, sqliteResultCode){
if(sqliteResultCode){
if(dbPtr instanceof DB) dbPtr = dbPtr.pointer;
throw new SQLite3Error([
"sqlite result code",sqliteResultCode+":",
capi.sqlite3_errmsg(dbPtr) || "Unknown db error."
].join(' '));
}
};
DB.prototype = {
/**
Finalizes all open statements and closes this database
connection. This is a no-op if the db has already been
closed. After calling close(), `this.pointer` will resolve to
`undefined`, so that can be used to check whether the db
instance is still opened.
*/
close: function(){
if(this.pointer){
const pDb = this.pointer;
let s;
const that = this;
Object.keys(__stmtMap.get(this)).forEach((k,s)=>{
if(s && s.pointer) s.finalize();
});
Object.values(__udfMap.get(this)).forEach(
capi.wasm.uninstallFunction.bind(capi.wasm)
);
__ptrMap.delete(this);
__stmtMap.delete(this);
__udfMap.delete(this);
capi.sqlite3_close_v2(pDb);
delete this.filename;
}
},
/**
Returns the number of changes, as per sqlite3_changes()
(if the first argument is false) or sqlite3_total_changes()
(if it's true). If the 2nd argument is true, it uses
sqlite3_changes64() or sqlite3_total_changes64(), which
will trigger an exception if this build does not have
BigInt support enabled.
*/
changes: function(total=false,sixtyFour=false){
const p = affirmDbOpen(this).pointer;
if(total){
return sixtyFour
? capi.sqlite3_total_changes64(p)
: capi.sqlite3_total_changes(p);
}else{
return sixtyFour
? capi.sqlite3_changes64(p)
: capi.sqlite3_changes(p);
}
},
/**
Similar to this.filename but will return NULL for
special names like ":memory:". Not of much use until
we have filesystem support. Throws if the DB has
been closed. If passed an argument it then it will return
the filename of the ATTACHEd db with that name, else it assumes
a name of `main`.
*/
fileName: function(dbName){
return capi.sqlite3_db_filename(affirmDbOpen(this).pointer, dbName||"main");
},
/**
Returns true if this db instance has a name which resolves to a
file. If the name is "" or ":memory:", it resolves to false.
Note that it is not aware of the peculiarities of URI-style
names and a URI-style name for a ":memory:" db will fool it.
*/
hasFilename: function(){
const fn = this.filename;
if(!fn || ':memory'===fn) return false;
return true;
},
/**
Returns the name of the given 0-based db number, as documented
for sqlite3_db_name().
*/
dbName: function(dbNumber=0){
return capi.sqlite3_db_name(affirmDbOpen(this).pointer, dbNumber);
},
/**
Compiles the given SQL and returns a prepared Stmt. This is
the only way to create new Stmt objects. Throws on error.
The given SQL must be a string, a Uint8Array holding SQL, or a
WASM pointer to memory holding the NUL-terminated SQL string.
If the SQL contains no statements, an SQLite3Error is thrown.
Design note: the C API permits empty SQL, reporting it as a 0
result code and a NULL stmt pointer. Supporting that case here
would cause extra work for all clients: any use of the Stmt API
on such a statement will necessarily throw, so clients would be
required to check `stmt.pointer` after calling `prepare()` in
order to determine whether the Stmt instance is empty or not.
Long-time practice (with other sqlite3 script bindings)
suggests that the empty-prepare case is sufficiently rare (and
useless) that supporting it here would simply hurt overall
usability.
*/
prepare: function(sql){
affirmDbOpen(this);
const stack = capi.wasm.scopedAllocPush();
let ppStmt, pStmt;
try{
ppStmt = capi.wasm.scopedAllocPtr()/* output (sqlite3_stmt**) arg */;
DB.checkRc(this, capi.sqlite3_prepare_v2(this.pointer, sql, -1, ppStmt, null));
pStmt = capi.wasm.getMemValue(ppStmt, '*');
}
finally {capi.wasm.scopedAllocPop(stack)}
if(!pStmt) toss3("Cannot prepare empty SQL.");
const stmt = new Stmt(this, pStmt, BindTypes);
__stmtMap.get(this)[pStmt] = stmt;
return stmt;
},
/**
This function works like execMulti(), and takes most of the
same arguments, but is more efficient (performs much less
work) when the input SQL is only a single statement. If
passed a multi-statement SQL, it only processes the first
one.
This function supports the following additional options not
supported by execMulti():
- .multi: if true, this function acts as a proxy for
execMulti() and behaves identically to that function.
- .columnNames: if this is an array and the query has
result columns, the array is passed to
Stmt.getColumnNames() to append the column names to it
(regardless of whether the query produces any result
rows). If the query has no result columns, this value is
unchanged.
The following options to execMulti() are _not_ supported by
this method (they are simply ignored):
- .saveSql
*/
exec: function(/*(sql [,optionsObj]) or (optionsObj)*/){
affirmDbOpen(this);
const arg = parseExecArgs(arguments);
if(!arg.sql) return this;
else if(arg.opt.multi){
return this.execMulti(arg, undefined, BindTypes);
}
const opt = arg.opt;
let stmt, rowTarget;
try {
if(Array.isArray(opt.resultRows)){
rowTarget = opt.resultRows;
}
stmt = this.prepare(arg.sql);
if(stmt.columnCount && Array.isArray(opt.columnNames)){
stmt.getColumnNames(opt.columnNames);
}
if(opt.bind) stmt.bind(opt.bind);
if(opt.callback || rowTarget){
while(stmt.step()){
const row = arg.cbArg(stmt);
if(rowTarget) rowTarget.push(row);
if(opt.callback){
stmt._isLocked = true;
opt.callback(row, stmt);
stmt._isLocked = false;
}
}
}else{
stmt.step();
}
}finally{
if(stmt){
delete stmt._isLocked;
stmt.finalize();
}
}
return this;
}/*exec()*/,
/**
Executes one or more SQL statements in the form of a single
string. Its arguments must be either (sql,optionsObject) or
(optionsObject). In the latter case, optionsObject.sql
must contain the SQL to execute. Returns this
object. Throws on error.
If no SQL is provided, or a non-string is provided, an
exception is triggered. Empty SQL, on the other hand, is
simply a no-op.
The optional options object may contain any of the following
properties:
- .sql = the SQL to run (unless it's provided as the first
argument). This must be of type string, Uint8Array, or an
array of strings (in which case they're concatenated
together as-is, with no separator between elements,
before evaluation).
- .bind = a single value valid as an argument for
Stmt.bind(). This is ONLY applied to the FIRST non-empty
statement in the SQL which has any bindable
parameters. (Empty statements are skipped entirely.)
- .callback = a function which gets called for each row of
the FIRST statement in the SQL which has result
_columns_, but only if that statement has any result
_rows_. The second argument passed to the callback is
always the current Stmt object (so that the caller may
collect column names, or similar). The first argument
passed to the callback defaults to the current Stmt
object but may be changed with ...
- .rowMode = either a string describing what type of argument
should be passed as the first argument to the callback or an
integer representing a result column index. A `rowMode` of
'object' causes the results of `stmt.get({})` to be passed to
the `callback` and/or appended to `resultRows`. A value of
'array' causes the results of `stmt.get([])` to be passed to
passed on. A value of 'stmt' is equivalent to the default,
passing the current Stmt to the callback (noting that it's
always passed as the 2nd argument), but this mode will trigger
an exception if `resultRows` is an array. If `rowMode` is an
integer, only the single value from that result column will be
passed on. Any other value for the option triggers an
exception.
- .resultRows: if this is an array, it functions similarly to
the `callback` option: each row of the result set (if any) of
the FIRST first statement which has result _columns_ is
appended to the array in the format specified for the `rowMode`
option, with the exception that the only legal values for
`rowMode` in this case are 'array' or 'object', neither of
which is the default. It is legal to use both `resultRows` and
`callback`, but `resultRows` is likely much simpler to use for
small data sets and can be used over a WebWorker-style message
interface. execMulti() throws if `resultRows` is set and
`rowMode` is 'stmt' (which is the default!).
- saveSql = an optional array. If set, the SQL of each
executed statement is appended to this array before the
statement is executed (but after it is prepared - we
don't have the string until after that). Empty SQL
statements are elided.
See also the exec() method, which is a close cousin of this
one.
ACHTUNG #1: The callback MUST NOT modify the Stmt
object. Calling any of the Stmt.get() variants,
Stmt.getColumnName(), or similar, is legal, but calling
step() or finalize() is not. Routines which are illegal
in this context will trigger an exception.
ACHTUNG #2: The semantics of the `bind` and `callback`
options may well change or those options may be removed
altogether for this function (but retained for exec()).
Generally speaking, neither bind parameters nor a callback
are generically useful when executing multi-statement SQL.
*/
execMulti: function(/*(sql [,obj]) || (obj)*/){
affirmDbOpen(this);
const wasm = capi.wasm;
const arg = (BindTypes===arguments[2]
/* ^^^ Being passed on from exec() */
? arguments[0] : parseExecArgs(arguments));
if(!arg.sql) return this;
const opt = arg.opt;
const callback = opt.callback;
const resultRows = (Array.isArray(opt.resultRows)
? opt.resultRows : undefined);
if(resultRows && 'stmt'===opt.rowMode){
toss3("rowMode 'stmt' is not valid in combination",
"with a resultRows array.");
}
let rowMode = (((callback||resultRows) && (undefined!==opt.rowMode))
? opt.rowMode : undefined);
let stmt;
let bind = opt.bind;
const stack = wasm.scopedAllocPush();
try{
const isTA = util.isSQLableTypedArray(arg.sql)
/* Optimization: if the SQL is a TypedArray we can save some string
conversion costs. */;
/* Allocate the two output pointers (ppStmt, pzTail) and heap
space for the SQL (pSql). When prepare_v2() returns, pzTail
will point to somewhere in pSql. */
let sqlByteLen = isTA ? arg.sql.byteLength : wasm.jstrlen(arg.sql);
const ppStmt = wasm.scopedAlloc(/* output (sqlite3_stmt**) arg and pzTail */
(2 * wasm.ptrSizeof)
+ (sqlByteLen + 1/* SQL + NUL */));
const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
let pSql = pzTail + wasm.ptrSizeof;
const pSqlEnd = pSql + sqlByteLen;
if(isTA) wasm.heap8().set(arg.sql, pSql);
else wasm.jstrcpy(arg.sql, wasm.heap8(), pSql, sqlByteLen, false);
wasm.setMemValue(pSql + sqlByteLen, 0/*NUL terminator*/);
while(wasm.getMemValue(pSql, 'i8')
/* Maintenance reminder: ^^^^ _must_ be i8 or else we
will very likely cause an endless loop. What that's
doing is checking for a terminating NUL byte. If we
use i32 or similar then we read 4 bytes, read stuff
around the NUL terminator, and get stuck in and
endless loop at the end of the SQL, endlessly
re-preparing an empty statement. */ ){
wasm.setMemValue(ppStmt, 0, wasm.ptrIR);
wasm.setMemValue(pzTail, 0, wasm.ptrIR);
DB.checkRc(this, capi.sqlite3_prepare_v2(
this.pointer, pSql, sqlByteLen, ppStmt, pzTail
));
const pStmt = wasm.getMemValue(ppStmt, wasm.ptrIR);
pSql = wasm.getMemValue(pzTail, wasm.ptrIR);
sqlByteLen = pSqlEnd - pSql;
if(!pStmt) continue;
if(Array.isArray(opt.saveSql)){
opt.saveSql.push(capi.sqlite3_sql(pStmt).trim());
}
stmt = new Stmt(this, pStmt, BindTypes);
if(bind && stmt.parameterCount){
stmt.bind(bind);
bind = null;
}
if(stmt.columnCount && undefined!==rowMode){
/* Only forward SELECT results for the FIRST query
in the SQL which potentially has them. */
while(stmt.step()){
stmt._isLocked = true;
const row = arg.cbArg(stmt);
if(callback) callback(row, stmt);
if(resultRows) resultRows.push(row);
stmt._isLocked = false;
}
rowMode = undefined;
}else{
// Do we need to while(stmt.step()){} here?
stmt.step();
}
stmt.finalize();
stmt = null;
}
}catch(e){
console.warn("DB.execMulti() is propagating exception",opt,e);
throw e;
}finally{
if(stmt){
delete stmt._isLocked;
stmt.finalize();
}
wasm.scopedAllocPop(stack);
}
return this;
}/*execMulti()*/,
/**
Creates a new scalar UDF (User-Defined Function) which is
accessible via SQL code. This function may be called in any
of the following forms:
- (name, function)
- (name, function, optionsObject)
- (name, optionsObject)
- (optionsObject)
In the final two cases, the function must be defined as the
'callback' property of the options object. In the final
case, the function's name must be the 'name' property.
This can only be used to create scalar functions, not
aggregate or window functions. UDFs cannot be removed from
a DB handle after they're added.
On success, returns this object. Throws on error.
When called from SQL, arguments to the UDF, and its result,
will be converted between JS and SQL with as much fidelity
as is feasible, triggering an exception if a type
conversion cannot be determined. Some freedom is afforded
to numeric conversions due to friction between the JS and C
worlds: integers which are larger than 32 bits will be
treated as doubles, as JS does not support 64-bit integers
and it is (as of this writing) illegal to use WASM
functions which take or return 64-bit integers from JS.
The optional options object may contain flags to modify how
the function is defined:
- .arity: the number of arguments which SQL calls to this
function expect or require. The default value is the
callback's length property (i.e. the number of declared
parameters it has). A value of -1 means that the function
is variadic and may accept any number of arguments, up to
sqlite3's compile-time limits. sqlite3 will enforce the
argument count if is zero or greater.
The following properties correspond to flags documented at:
https://sqlite.org/c3ref/create_function.html
- .deterministic = SQLITE_DETERMINISTIC
- .directOnly = SQLITE_DIRECTONLY
- .innocuous = SQLITE_INNOCUOUS
Maintenance reminder: the ability to add new
WASM-accessible functions to the runtime requires that the
WASM build is compiled with emcc's `-sALLOW_TABLE_GROWTH`
flag.
*/
createFunction: function f(name, callback,opt){
switch(arguments.length){
case 1: /* (optionsObject) */
opt = name;
name = opt.name;
callback = opt.callback;
break;
case 2: /* (name, callback|optionsObject) */
if(!(callback instanceof Function)){
opt = callback;
callback = opt.callback;
}
break;
default: break;
}
if(!opt) opt = {};
if(!(callback instanceof Function)){
toss3("Invalid arguments: expecting a callback function.");
}else if('string' !== typeof name){
toss3("Invalid arguments: missing function name.");
}
if(!f._extractArgs){
/* Static init */
f._extractArgs = function(argc, pArgv){
let i, pVal, valType, arg;
const tgt = [];
for(i = 0; i < argc; ++i){
pVal = capi.wasm.getMemValue(pArgv + (capi.wasm.ptrSizeof * i),
capi.wasm.ptrIR);
/**
Curiously: despite ostensibly requiring 8-byte
alignment, the pArgv array is parcelled into chunks of
4 bytes (1 pointer each). The values those point to
have 8-byte alignment but the individual argv entries
do not.
*/
valType = capi.sqlite3_value_type(pVal);
switch(valType){
case capi.SQLITE_INTEGER:
case capi.SQLITE_FLOAT:
arg = capi.sqlite3_value_double(pVal);
break;
case capi.SQLITE_TEXT:
arg = capi.sqlite3_value_text(pVal);
break;
case capi.SQLITE_BLOB:{
const n = capi.sqlite3_value_bytes(pVal);
const pBlob = capi.sqlite3_value_blob(pVal);
arg = new Uint8Array(n);
let i;
const heap = n ? capi.wasm.heap8() : false;
for(i = 0; i < n; ++i) arg[i] = heap[pBlob+i];
break;
}
case capi.SQLITE_NULL:
arg = null; break;
default:
toss3("Unhandled sqlite3_value_type()",valType,
"is possibly indicative of incorrect",
"pointer size assumption.");
}
tgt.push(arg);
}
return tgt;
}/*_extractArgs()*/;
f._setResult = function(pCx, val){
switch(typeof val) {
case 'boolean':
capi.sqlite3_result_int(pCx, val ? 1 : 0);
break;
case 'number': {
(util.isInt32(val)
? capi.sqlite3_result_int
: capi.sqlite3_result_double)(pCx, val);
break;
}
case 'string':
capi.sqlite3_result_text(pCx, val, -1, capi.SQLITE_TRANSIENT);
break;
case 'object':
if(null===val) {
capi.sqlite3_result_null(pCx);
break;
}else if(util.isBindableTypedArray(val)){
const pBlob = capi.wasm.mallocFromTypedArray(val);
capi.sqlite3_result_blob(pCx, pBlob, val.byteLength,
capi.SQLITE_TRANSIENT);
capi.wasm.dealloc(pBlob);
break;
}
// else fall through
default:
toss3("Don't not how to handle this UDF result value:",val);
};
}/*_setResult()*/;
}/*static init*/
const wrapper = function(pCx, argc, pArgv){
try{
f._setResult(pCx, callback.apply(null, f._extractArgs(argc, pArgv)));
}catch(e){
if(e instanceof capi.WasmAllocError){
capi.sqlite3_result_error_nomem(pCx);
}else{
capi.sqlite3_result_error(pCx, e.message, -1);
}
}
};
const pUdf = capi.wasm.installFunction(wrapper, "v(iii)");
let fFlags = 0 /*flags for sqlite3_create_function_v2()*/;
if(getOwnOption(opt, 'deterministic')) fFlags |= capi.SQLITE_DETERMINISTIC;
if(getOwnOption(opt, 'directOnly')) fFlags |= capi.SQLITE_DIRECTONLY;
if(getOwnOption(opt, 'innocuous')) fFlags |= capi.SQLITE_INNOCUOUS;
name = name.toLowerCase();
try {
DB.checkRc(this, capi.sqlite3_create_function_v2(
this.pointer, name,
(opt.hasOwnProperty('arity') ? +opt.arity : callback.length),
capi.SQLITE_UTF8 | fFlags, null/*pApp*/, pUdf,
null/*xStep*/, null/*xFinal*/, null/*xDestroy*/));
}catch(e){
capi.wasm.uninstallFunction(pUdf);
throw e;
}
const udfMap = __udfMap.get(this);
if(udfMap[name]){
try{capi.wasm.uninstallFunction(udfMap[name])}
catch(e){/*ignore*/}
}
udfMap[name] = pUdf;
return this;
}/*createFunction()*/,
/**
Prepares the given SQL, step()s it one time, and returns
the value of the first result column. If it has no results,
undefined is returned.
If passed a second argument, it is treated like an argument
to Stmt.bind(), so may be any type supported by that
function. Passing the undefined value is the same as passing
no value, which is useful when...
If passed a 3rd argument, it is expected to be one of the
SQLITE_{typename} constants. Passing the undefined value is
the same as not passing a value.
Throws on error (e.g. malformedSQL).
*/
selectValue: function(sql,bind,asType){
let stmt, rc;
try {
stmt = this.prepare(sql).bind(bind);
if(stmt.step()) rc = stmt.get(0,asType);
}finally{
if(stmt) stmt.finalize();
}
return rc;
},
/**
Returns the number of currently-opened Stmt handles for this db
handle, or 0 if this DB instance is closed.
*/
openStatementCount: function(){
return this.pointer ? Object.keys(__stmtMap.get(this)).length : 0;
},
/**
This function currently does nothing and always throws. It
WILL BE REMOVED pending other refactoring, to eliminate a hard
dependency on Emscripten. This feature will be moved into a
higher-level API or a runtime-configurable feature.
That said, what its replacement should eventually do is...
Exports a copy of this db's file as a Uint8Array and
returns it. It is technically not legal to call this while
any prepared statement are currently active because,
depending on the platform, it might not be legal to read
the db while a statement is locking it. Throws if this db
is not open or has any opened statements.
The resulting buffer can be passed to this class's
constructor to restore the DB.
Maintenance reminder: the corresponding sql.js impl of this
feature closes the current db, finalizing any active
statements and (seemingly unnecessarily) destroys any UDFs,
copies the file, and then re-opens it (without restoring
the UDFs). Those gymnastics are not necessary on the tested
platform but might be necessary on others. Because of that
eventuality, this interface currently enforces that no
statements are active when this is run. It will throw if
any are.
*/
exportBinaryImage: function(){
toss3("exportBinaryImage() is slated for removal for portability reasons.");
/***********************
The following is currently kept only for reference when
porting to some other layer, noting that we may well not be
able to implement this, at this level, when using the OPFS
VFS because of its exclusive locking policy.
affirmDbOpen(this);
if(this.openStatementCount()>0){
toss3("Cannot export with prepared statements active!",
"finalize() all statements and try again.");
}
return MODCFG.FS.readFile(this.filename, {encoding:"binary"});
***********************/
}
}/*DB.prototype*/;
/** Throws if the given Stmt has been finalized, else stmt is
returned. */
const affirmStmtOpen = function(stmt){
if(!stmt.pointer) toss3("Stmt has been closed.");
return stmt;
};
/** Returns an opaque truthy value from the BindTypes
enum if v's type is a valid bindable type, else
returns a falsy value. As a special case, a value of
undefined is treated as a bind type of null. */
const isSupportedBindType = function(v){
let t = BindTypes[(null===v||undefined===v) ? 'null' : typeof v];
switch(t){
case BindTypes.boolean:
case BindTypes.null:
case BindTypes.number:
case BindTypes.string:
return t;
case BindTypes.bigint:
if(capi.wasm.bigIntEnabled) return t;
/* else fall through */
default:
//console.log("isSupportedBindType",t,v);
return util.isBindableTypedArray(v) ? BindTypes.blob : undefined;
}
};
/**
If isSupportedBindType(v) returns a truthy value, this
function returns that value, else it throws.
*/
const affirmSupportedBindType = function(v){
//console.log('affirmSupportedBindType',v);
return isSupportedBindType(v) || toss3("Unsupported bind() argument type:",typeof v);
};
/**
If key is a number and within range of stmt's bound parameter
count, key is returned.
If key is not a number then it is checked against named
parameters. If a match is found, its index is returned.
Else it throws.
*/
const affirmParamIndex = function(stmt,key){
const n = ('number'===typeof key)
? key : capi.sqlite3_bind_parameter_index(stmt.pointer, key);
if(0===n || !util.isInt32(n)){
toss3("Invalid bind() parameter name: "+key);
}
else if(n<1 || n>stmt.parameterCount) toss3("Bind index",key,"is out of range.");
return n;
};
/**
If stmt._isLocked is truthy, this throws an exception
complaining that the 2nd argument (an operation name,
e.g. "bind()") is not legal while the statement is "locked".
Locking happens before an exec()-like callback is passed a
statement, to ensure that the callback does not mutate or
finalize the statement. If it does not throw, it returns stmt.
*/
const affirmUnlocked = function(stmt,currentOpName){
if(stmt._isLocked){
toss3("Operation is illegal when statement is locked:",currentOpName);
}
return stmt;
};
/**
Binds a single bound parameter value on the given stmt at the
given index (numeric or named) using the given bindType (see
the BindTypes enum) and value. Throws on error. Returns stmt on
success.
*/
const bindOne = function f(stmt,ndx,bindType,val){
affirmUnlocked(stmt, 'bind()');
if(!f._){
if(capi.wasm.bigIntEnabled){
f._maxInt = BigInt("0x7fffffffffffffff");
f._minInt = ~f._maxInt;
}
/* Reminder: when not in BigInt mode, it's impossible for
JS to represent a number out of the range we can bind,
so we have no range checking. */
f._ = {
string: function(stmt, ndx, val, asBlob){
if(1){
/* _Hypothetically_ more efficient than the impl in the 'else' block. */
const stack = capi.wasm.scopedAllocPush();
try{
const n = capi.wasm.jstrlen(val);
const pStr = capi.wasm.scopedAlloc(n);
capi.wasm.jstrcpy(val, capi.wasm.heap8u(), pStr, n, false);
const f = asBlob ? capi.sqlite3_bind_blob : capi.sqlite3_bind_text;
return f(stmt.pointer, ndx, pStr, n, capi.SQLITE_TRANSIENT);
}finally{
capi.wasm.scopedAllocPop(stack);
}
}else{
const bytes = capi.wasm.jstrToUintArray(val,false);
const pStr = capi.wasm.alloc(bytes.length || 1);
capi.wasm.heap8u().set(bytes.length ? bytes : [0], pStr);
try{
const f = asBlob ? capi.sqlite3_bind_blob : capi.sqlite3_bind_text;
return f(stmt.pointer, ndx, pStr, bytes.length, capi.SQLITE_TRANSIENT);
}finally{
capi.wasm.dealloc(pStr);
}
}
}
};
}
affirmSupportedBindType(val);
ndx = affirmParamIndex(stmt,ndx);
let rc = 0;
switch((null===val || undefined===val) ? BindTypes.null : bindType){
case BindTypes.null:
rc = capi.sqlite3_bind_null(stmt.pointer, ndx);
break;
case BindTypes.string:
rc = f._.string(stmt, ndx, val, false);
break;
case BindTypes.number: {
let m;
if(util.isInt32(val)) m = capi.sqlite3_bind_int;
else if(capi.wasm.bigIntEnabled && ('bigint'===typeof val)){
if(val<f._minInt || val>f._maxInt){
toss3("BigInt value is out of range for int64: "+val);
}