@ix-xs/node-comfort 1.1.31 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +190 -0
- package/LICENSE +21 -0
- package/README.md +210 -748
- package/index.js +84 -13
- package/package.json +201 -21
- package/src/Arr.js +901 -0
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +979 -188
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -768
- package/src/Func.js +692 -0
- package/src/Http.js +538 -0
- package/src/Id.js +418 -0
- package/src/Logger.js +739 -307
- package/src/Num.js +849 -0
- package/src/Obj.js +904 -0
- package/src/SQLite.js +1308 -994
- package/src/Schema.js +1586 -0
- package/src/Str.js +999 -0
- package/src/Sys.js +511 -0
- package/src/Time.js +1323 -0
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -120
- package/types/src/Arr.d.ts +562 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +609 -81
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +709 -201
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +455 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +200 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +374 -24
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +578 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +475 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +647 -305
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +696 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +664 -0
- package/types/src/Time.d.ts.map +1 -0
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/tsconfig.json +0 -18
- package/types/index.d.ts.map +0 -1
package/src/SQLite.js
CHANGED
|
@@ -1,994 +1,1308 @@
|
|
|
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
|
-
* @param {
|
|
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
|
-
const
|
|
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
|
-
* @param {
|
|
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
|
-
* @returns {
|
|
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
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* @
|
|
673
|
-
* @param {
|
|
674
|
-
* @param {
|
|
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
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQLite made pleasant, on the engine built into Node.js (22.13+): CRUD with
|
|
5
|
+
* rich filters, JSON and boolean columns, transactions, migrations and
|
|
6
|
+
* backups. Methods are synchronous, like the engine, but `await` still
|
|
7
|
+
* works. Errors are `SQLiteError`s. Values are always bound as parameters
|
|
8
|
+
* and names are checked, so there's no SQL injection through them.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const db = new nc.SQLite("data/app.sqlite");
|
|
12
|
+
* db.createTable({
|
|
13
|
+
* name: "users",
|
|
14
|
+
* columns: {
|
|
15
|
+
* id: { type: "INTEGER", primaryKey: true, autoincrement: true },
|
|
16
|
+
* email: { type: "TEXT", notNull: true, unique: true },
|
|
17
|
+
* settings: { type: "JSON", defaultValue: {} },
|
|
18
|
+
* },
|
|
19
|
+
* });
|
|
20
|
+
* db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
|
|
21
|
+
* db.getAll("users", { email: { like: "%@example.com" } }, { orderBy: "email", limit: 20 });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const nodePath = require("node:path");
|
|
25
|
+
const fs = require("node:fs");
|
|
26
|
+
const { SQLiteError } = require("./errors.js");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A column type. `JSON` and `BOOLEAN` columns give you back objects and
|
|
30
|
+
* `true`/`false`.
|
|
31
|
+
* @typedef {"INTEGER" | "TEXT" | "REAL" | "BLOB" | "NUMERIC" | "JSON" | "BOOLEAN" | "DATETIME" | (string & {})} ColumnType
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What happens to a row when the row it references changes.
|
|
36
|
+
* @typedef {"CASCADE" | "SET NULL" | "SET DEFAULT" | "RESTRICT" | "NO ACTION"} ForeignKeyAction
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A column.
|
|
41
|
+
* @typedef {object} ColumnDefinition
|
|
42
|
+
* @property {ColumnType} type Like `"INTEGER"`, `"TEXT"`, `"JSON"` or `"VARCHAR(255)"`.
|
|
43
|
+
* @property {boolean} [primaryKey]
|
|
44
|
+
* @property {boolean} [autoincrement] With an `INTEGER` primary key: ids are never reused.
|
|
45
|
+
* @property {boolean} [notNull]
|
|
46
|
+
* @property {boolean} [unique]
|
|
47
|
+
* @property {unknown} [defaultValue] Objects work for JSON columns, booleans for BOOLEAN ones.
|
|
48
|
+
* @property {unknown[]} [values] Allowed values.
|
|
49
|
+
* @property {string} [check] A `CHECK` expression, like `"price >= 0"`.
|
|
50
|
+
* @property {{ table: string, column: string }} [references] Foreign key target.
|
|
51
|
+
* @property {ForeignKeyAction} [onDelete] When the referenced row is deleted.
|
|
52
|
+
* @property {ForeignKeyAction} [onUpdate] When the referenced key changes.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A constraint on several columns.
|
|
57
|
+
* @typedef {{ type: "unique", columns: string[] } | { type: "primaryKey", columns: string[] } | { type: "check", expression: string } | { type: "foreignKey", columns: string[], references: { table: string, columns: string[] }, onDelete?: ForeignKeyAction, onUpdate?: ForeignKeyAction }} TableConstraint
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* An index.
|
|
62
|
+
* @typedef {object} IndexDefinition
|
|
63
|
+
* @property {string[]} columns Several columns make a composite index.
|
|
64
|
+
* @property {boolean} [unique]
|
|
65
|
+
* @property {string} [name] Defaults to `idx_<table>_<columns>`.
|
|
66
|
+
* @property {string} [where] Makes it a partial index, like `"deleted_at IS NULL"`.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A table for `createTable()`.
|
|
71
|
+
* @typedef {object} TableDefinition
|
|
72
|
+
* @property {string} name
|
|
73
|
+
* @property {Record<string, ColumnDefinition>} [columns]
|
|
74
|
+
* @property {Record<string, ColumnDefinition>} [options] Old name for `columns`, still accepted.
|
|
75
|
+
* @property {TableConstraint[]} [constraints]
|
|
76
|
+
* @property {IndexDefinition[]} [indexes]
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Operators for a column in a filter.
|
|
81
|
+
* @typedef {object} WhereOperators
|
|
82
|
+
* @property {unknown} [eq] Equal. `null` means `IS NULL`.
|
|
83
|
+
* @property {unknown} [ne] Not equal. Rows where the column is `NULL` match too.
|
|
84
|
+
* @property {number | string | bigint | Date} [gt] Greater than.
|
|
85
|
+
* @property {number | string | bigint | Date} [gte] Greater than or equal.
|
|
86
|
+
* @property {number | string | bigint | Date} [lt] Less than.
|
|
87
|
+
* @property {number | string | bigint | Date} [lte] Less than or equal.
|
|
88
|
+
* @property {unknown[]} [in] One of these values.
|
|
89
|
+
* @property {unknown[]} [notIn] None of these values.
|
|
90
|
+
* @property {string} [like] A `LIKE` pattern: `%` any text, `_` one character.
|
|
91
|
+
* @property {string} [notLike]
|
|
92
|
+
* @property {string} [glob] A case-sensitive pattern: `*` any text, `?` one character.
|
|
93
|
+
* @property {[unknown, unknown]} [between] From `min` to `max`, both included.
|
|
94
|
+
* @property {boolean} [isNull] `true` for `IS NULL`, `false` for `IS NOT NULL`.
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A filter. Each key is a column: a value means "equals", `null` means
|
|
99
|
+
* `IS NULL`, an array means "one of", and an object uses operators. Keys
|
|
100
|
+
* combine with AND; use `$or` and `$and` for the rest.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* { status: "active", age: { gte: 18, lt: 65 }, role: ["admin", "editor"], deleted_at: null }
|
|
104
|
+
* { $or: [{ email: { like: "%@acme.com" } }, { vip: true }] }
|
|
105
|
+
*
|
|
106
|
+
* @template [R=Record<string, any>] Row type; its keys are suggested as columns.
|
|
107
|
+
* @typedef {{ [K in keyof R]?: WhereValue } & { $or?: Where<R>[], $and?: Where<R>[] }} Where
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The condition for one column: a value, `null`, an array, or operators.
|
|
112
|
+
* @typedef {null | string | number | bigint | boolean | Date | Uint8Array | readonly unknown[] | WhereOperators} WhereValue
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sort direction.
|
|
117
|
+
* @typedef {"ASC" | "DESC" | "asc" | "desc"} SortDirection
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Options for `getAll()`.
|
|
122
|
+
* @typedef {object} QueryOptions
|
|
123
|
+
* @property {string | Array<string | [string, SortDirection]>} [orderBy] Like `"created_at"` or `[["age", "DESC"], "name"]`.
|
|
124
|
+
* @property {SortDirection} [direction] For a single `orderBy` column. Defaults to `"ASC"`.
|
|
125
|
+
* @property {number} [limit]
|
|
126
|
+
* @property {number} [offset] Rows to skip, for pagination.
|
|
127
|
+
* @property {string[]} [columns] Columns to return. Defaults to all.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Options for `new SQLite()`.
|
|
132
|
+
* @typedef {object} SQLiteOptions
|
|
133
|
+
* @property {boolean} [json] Parse `JSON` columns. Defaults to `true`.
|
|
134
|
+
* @property {boolean} [booleans] Return `BOOLEAN` columns as `true`/`false`. Defaults to `true`.
|
|
135
|
+
* @property {boolean} [dates] Return `DATE`, `DATETIME` and `TIMESTAMP` columns as `Date`s.
|
|
136
|
+
* @property {boolean} [wal] Write-ahead logging: faster, and reads don't wait for writes. Defaults to `true`.
|
|
137
|
+
* @property {boolean} [foreignKeys] Enforce foreign keys. Defaults to `true`.
|
|
138
|
+
* @property {number} [busyTimeout] How long to wait for a lock held by another connection, in ms. Defaults to `5000`.
|
|
139
|
+
* @property {boolean} [readOnly]
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* What an insert returns.
|
|
144
|
+
* @typedef {{ ok: true, changes: number, lastInsertRowid: number | bigint }} InsertResult
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* What an update or delete returns.
|
|
149
|
+
* @typedef {{ ok: true, changes: number }} ChangeResult
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A column, as returned by `columns()`.
|
|
154
|
+
* @typedef {object} ColumnInfo
|
|
155
|
+
* @property {string} name
|
|
156
|
+
* @property {string} type `""` when there's none.
|
|
157
|
+
* @property {boolean} notNull
|
|
158
|
+
* @property {unknown} defaultValue As SQL text, or `null`.
|
|
159
|
+
* @property {boolean} primaryKey
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A migration: SQL (several statements are fine), or a function that gets
|
|
164
|
+
* the database.
|
|
165
|
+
* @typedef {string | ((db: SQLite<any>) => void)} Migration
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Shortcuts bound to one table, from `table()`.
|
|
170
|
+
* @template [R=Record<string, any>]
|
|
171
|
+
* @typedef {object} TableHandle
|
|
172
|
+
* @property {string} name
|
|
173
|
+
* @property {(where?: Where<R>, options?: Pick<QueryOptions, "columns" | "orderBy" | "direction">) => R | undefined} get The first matching row.
|
|
174
|
+
* @property {(where?: Where<R>, options?: QueryOptions) => R[]} getAll All matching rows.
|
|
175
|
+
* @property {(data: Partial<R>) => InsertResult} insert Inserts a row.
|
|
176
|
+
* @property {(rows: Array<Partial<R>>) => ChangeResult} insertMany Inserts rows in one transaction.
|
|
177
|
+
* @property {(data: Partial<R>, where: Where<R>) => ChangeResult} update Updates matching rows.
|
|
178
|
+
* @property {(data: Partial<R>, conflict: string[]) => InsertResult} upsert Inserts, or updates on conflict.
|
|
179
|
+
* @property {(where: Where<R>) => ChangeResult} delete Deletes matching rows.
|
|
180
|
+
* @property {(where?: Where<R>) => number} count Counts matching rows.
|
|
181
|
+
* @property {(where?: Where<R>) => boolean} exists Does any row match?
|
|
182
|
+
* @property {() => { ok: true }} clear Deletes every row.
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
186
|
+
const OPERATORS = new Set(["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "like", "notLike", "glob", "between", "isNull"]);
|
|
187
|
+
const COMPARATORS = /** @type {Record<string, string>} */ ({ gt: ">", gte: ">=", lt: "<", lte: "<=", like: "LIKE", notLike: "NOT LIKE", glob: "GLOB" });
|
|
188
|
+
const ACTIONS = ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"];
|
|
189
|
+
const SQLITE_CODES = /** @type {Record<number, string>} */ ({
|
|
190
|
+
1: "SQLITE_ERROR", 5: "SQLITE_BUSY", 6: "SQLITE_LOCKED", 8: "SQLITE_READONLY", 11: "SQLITE_CORRUPT", 13: "SQLITE_FULL",
|
|
191
|
+
14: "SQLITE_CANTOPEN", 19: "SQLITE_CONSTRAINT", 20: "SQLITE_MISMATCH", 275: "SQLITE_CONSTRAINT_CHECK",
|
|
192
|
+
787: "SQLITE_CONSTRAINT_FOREIGNKEY", 1299: "SQLITE_CONSTRAINT_NOTNULL", 1555: "SQLITE_CONSTRAINT_PRIMARYKEY",
|
|
193
|
+
2067: "SQLITE_CONSTRAINT_UNIQUE",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/** @type {Array<[RegExp, string]>} */
|
|
197
|
+
const MESSAGE_CODES = [
|
|
198
|
+
[/UNIQUE constraint failed/i, "SQLITE_CONSTRAINT_UNIQUE"],
|
|
199
|
+
[/NOT NULL constraint failed/i, "SQLITE_CONSTRAINT_NOTNULL"],
|
|
200
|
+
[/CHECK constraint failed/i, "SQLITE_CONSTRAINT_CHECK"],
|
|
201
|
+
[/FOREIGN KEY constraint failed/i, "SQLITE_CONSTRAINT_FOREIGNKEY"],
|
|
202
|
+
[/PRIMARY KEY/i, "SQLITE_CONSTRAINT_PRIMARYKEY"],
|
|
203
|
+
[/readonly database/i, "SQLITE_READONLY"],
|
|
204
|
+
[/database is locked/i, "SQLITE_BUSY"],
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
/** @type {typeof import("node:sqlite").DatabaseSync | undefined} */
|
|
208
|
+
let _DatabaseSync;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Loads `node:sqlite` on first use, hiding its "experimental" warning.
|
|
212
|
+
* @returns {typeof import("node:sqlite").DatabaseSync}
|
|
213
|
+
*/
|
|
214
|
+
const _engine = () => {
|
|
215
|
+
if (_DatabaseSync) return _DatabaseSync;
|
|
216
|
+
const original = process.emitWarning;
|
|
217
|
+
process.emitWarning = /** @type {any} */ (function(/** @type {any} */ warning, /** @type {any[]} */ ...args) {
|
|
218
|
+
const text = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
219
|
+
if (/sqlite/i.test(text)) return;
|
|
220
|
+
return /** @type {Function} */ (original).call(process, warning, ...args);
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
_DatabaseSync = require("node:sqlite").DatabaseSync;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw new SQLiteError(`node:sqlite is not available in Node.js ${process.version}: nc.SQLite requires Node.js 22.13 or newer`, { cause: error });
|
|
226
|
+
} finally {
|
|
227
|
+
process.emitWarning = original;
|
|
228
|
+
}
|
|
229
|
+
return _DatabaseSync;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Checks and quotes a table or column name.
|
|
234
|
+
* @param {unknown} name
|
|
235
|
+
* @param {string} [kind="identifier"]
|
|
236
|
+
* @returns {string}
|
|
237
|
+
*/
|
|
238
|
+
const _id = (name, kind = "identifier") => {
|
|
239
|
+
if (typeof name !== "string" || !IDENTIFIER.test(name)) throw new SQLiteError(`Invalid ${kind}: "${String(name)}" (letters, digits and _ only, not starting with a digit)`);
|
|
240
|
+
return `"${name}"`;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {unknown} value
|
|
245
|
+
* @returns {null | number | bigint | string | Uint8Array}
|
|
246
|
+
*/
|
|
247
|
+
const _toSQL = (value) => {
|
|
248
|
+
if (value === undefined || value === null) return null;
|
|
249
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
250
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "string") return value;
|
|
251
|
+
if (value instanceof Date) return value.toISOString();
|
|
252
|
+
if (value instanceof Uint8Array) return value;
|
|
253
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
254
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
255
|
+
return JSON.stringify(value);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A SQL literal, for DEFAULT and CHECK where parameters can't be used.
|
|
260
|
+
* @param {unknown} value
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
const _literal = (value) => {
|
|
264
|
+
const v = _toSQL(value);
|
|
265
|
+
if (v === null) return "NULL";
|
|
266
|
+
if (typeof v === "number" || typeof v === "bigint") return String(v);
|
|
267
|
+
if (typeof v === "string") return `'${v.replace(/'/g, "''")}'`;
|
|
268
|
+
return `X'${Buffer.from(v).toString("hex")}'`;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {unknown} value
|
|
273
|
+
* @returns {value is Record<string, unknown>}
|
|
274
|
+
*/
|
|
275
|
+
const _isPlain = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer);
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {unknown} error
|
|
279
|
+
* @param {string} [sql]
|
|
280
|
+
* @returns {SQLiteError}
|
|
281
|
+
*/
|
|
282
|
+
const _wrap = (error, sql) => {
|
|
283
|
+
if (error instanceof SQLiteError) return error;
|
|
284
|
+
/** @type {any} */
|
|
285
|
+
const native = error;
|
|
286
|
+
const errcode = typeof native?.errcode === "number" ? native.errcode : undefined;
|
|
287
|
+
const message = String(native?.message ?? error);
|
|
288
|
+
// older Node.js versions do not expose `errcode`: deduce the common codes from the message
|
|
289
|
+
const fromMessage = MESSAGE_CODES.find(([pattern]) => pattern.test(message))?.[1];
|
|
290
|
+
const sqliteCode = errcode === undefined ? fromMessage : SQLITE_CODES[errcode] ?? SQLITE_CODES[errcode & 0xff] ?? fromMessage;
|
|
291
|
+
return new SQLiteError(sql ? `${message}\n in: ${sql.replace(/\s+/g, " ").trim()}` : message, { sql, sqliteCode, cause: error });
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Removes quoted strings and names, to look for `;` safely.
|
|
296
|
+
* @param {string} sql
|
|
297
|
+
* @returns {string}
|
|
298
|
+
*/
|
|
299
|
+
const _stripQuoted = (sql) => sql.replace(/'(?:''|[^'])*'|"(?:""|[^"])*"/g, "");
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* A SQLite database connection.
|
|
303
|
+
*
|
|
304
|
+
* @template [R=Record<string, any>] The row type read methods return by default. Each call can override it.
|
|
305
|
+
*/
|
|
306
|
+
class SQLite {
|
|
307
|
+
/** @type {import("node:sqlite").DatabaseSync} */
|
|
308
|
+
#db;
|
|
309
|
+
/** @type {Map<string, import("node:sqlite").StatementSync>} */
|
|
310
|
+
#statements = new Map();
|
|
311
|
+
/** @type {Map<string, Map<string, string>>} */
|
|
312
|
+
#columnTypes = new Map();
|
|
313
|
+
/** @type {{ json: boolean, booleans: boolean, dates: boolean }} */
|
|
314
|
+
#read;
|
|
315
|
+
#depth = 0;
|
|
316
|
+
#open = true;
|
|
317
|
+
/** @type {string} */
|
|
318
|
+
#path;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Opens a database, creating the file and its folders if needed. Use
|
|
322
|
+
* `":memory:"` for a throwaway in-memory database.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* const db = new nc.SQLite(); // ./db.sqlite
|
|
326
|
+
* const app = new nc.SQLite("data/app.sqlite", { dates: true });
|
|
327
|
+
* const mem = new nc.SQLite(":memory:");
|
|
328
|
+
*
|
|
329
|
+
* @param {string} [path="db.sqlite"] Relative to the working directory.
|
|
330
|
+
* @param {SQLiteOptions} [options]
|
|
331
|
+
* @throws {SQLiteError} If the file can't be opened.
|
|
332
|
+
*/
|
|
333
|
+
constructor(path = "db.sqlite", options = {}) {
|
|
334
|
+
const DatabaseSync = _engine();
|
|
335
|
+
const memory = path === ":memory:" || path === "";
|
|
336
|
+
const target = memory ? ":memory:" : nodePath.resolve(process.cwd(), path);
|
|
337
|
+
if (!memory && !options.readOnly) fs.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
338
|
+
try {
|
|
339
|
+
this.#db = new DatabaseSync(target, { readOnly: options.readOnly ?? false });
|
|
340
|
+
this.#db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(Number(options.busyTimeout ?? 5000)) || 0)}`);
|
|
341
|
+
if (!memory && !options.readOnly && (options.wal ?? true)) this.#db.exec("PRAGMA journal_mode = WAL");
|
|
342
|
+
this.#db.exec(`PRAGMA foreign_keys = ${options.foreignKeys === false ? "OFF" : "ON"}`);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw _wrap(error);
|
|
345
|
+
}
|
|
346
|
+
this.#path = target;
|
|
347
|
+
this.#read = { json: options.json ?? true, booleans: options.booleans ?? true, dates: options.dates ?? false };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @param {string} sql
|
|
352
|
+
* @returns {import("node:sqlite").StatementSync}
|
|
353
|
+
*/
|
|
354
|
+
#prepare(sql) {
|
|
355
|
+
let stmt = this.#statements.get(sql);
|
|
356
|
+
if (stmt) return stmt;
|
|
357
|
+
try {
|
|
358
|
+
stmt = this.#db.prepare(sql);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
throw _wrap(error, sql);
|
|
361
|
+
}
|
|
362
|
+
if (this.#statements.size >= 500) this.#statements.delete(/** @type {string} */ (this.#statements.keys().next().value));
|
|
363
|
+
this.#statements.set(sql, stmt);
|
|
364
|
+
return stmt;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @param {unknown} params
|
|
369
|
+
* @returns {any[]}
|
|
370
|
+
*/
|
|
371
|
+
#params(params) {
|
|
372
|
+
if (params === undefined) return [];
|
|
373
|
+
if (Array.isArray(params)) return params.map(_toSQL);
|
|
374
|
+
if (_isPlain(params)) return [Object.fromEntries(Object.entries(params).map(([k, v]) => [k, _toSQL(v)]))];
|
|
375
|
+
return [_toSQL(params)];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @returns {void}
|
|
380
|
+
*/
|
|
381
|
+
#invalidate() {
|
|
382
|
+
this.#columnTypes.clear();
|
|
383
|
+
this.#statements.clear();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Declared column types of a table, by column.
|
|
388
|
+
* @param {string} table
|
|
389
|
+
* @returns {Map<string, string>}
|
|
390
|
+
*/
|
|
391
|
+
#types(table) {
|
|
392
|
+
let types = this.#columnTypes.get(table);
|
|
393
|
+
if (!types) {
|
|
394
|
+
types = new Map();
|
|
395
|
+
for (const col of /** @type {Array<{ name: string, type: string }>} */ (/** @type {unknown} */ (this.#prepare(`PRAGMA table_info(${_id(table, "table name")})`).all()))) {
|
|
396
|
+
types.set(col.name, String(col.type).toUpperCase());
|
|
397
|
+
}
|
|
398
|
+
this.#columnTypes.set(table, types);
|
|
399
|
+
}
|
|
400
|
+
return types;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* @param {unknown} raw
|
|
405
|
+
* @param {string | undefined} table
|
|
406
|
+
* @returns {any}
|
|
407
|
+
*/
|
|
408
|
+
#row(raw, table) {
|
|
409
|
+
if (!raw) return undefined;
|
|
410
|
+
/** @type {Record<string, unknown>} */
|
|
411
|
+
const row = { ...raw };
|
|
412
|
+
if (!table || !(this.#read.json || this.#read.booleans || this.#read.dates)) return row;
|
|
413
|
+
const types = this.#types(table);
|
|
414
|
+
for (const [key, value] of Object.entries(row)) {
|
|
415
|
+
const type = types.get(key);
|
|
416
|
+
if (!type || value === null) continue;
|
|
417
|
+
if (type === "JSON") {
|
|
418
|
+
if (this.#read.json && typeof value === "string") {
|
|
419
|
+
try {
|
|
420
|
+
row[key] = JSON.parse(value);
|
|
421
|
+
} catch {
|
|
422
|
+
// not valid JSON: keep the text
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else if (type === "BOOLEAN" || type === "BOOL") {
|
|
426
|
+
if (this.#read.booleans && (typeof value === "number" || typeof value === "bigint")) row[key] = Number(value) !== 0;
|
|
427
|
+
} else if (this.#read.dates && (type === "DATE" || type === "DATETIME" || type === "TIMESTAMP") && (typeof value === "string" || typeof value === "number")) {
|
|
428
|
+
const date = new Date(value);
|
|
429
|
+
if (!Number.isNaN(date.getTime())) row[key] = date;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return row;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* @param {Where<any> | undefined} where
|
|
437
|
+
* @returns {{ sql: string, params: any[] }}
|
|
438
|
+
*/
|
|
439
|
+
#where(where) {
|
|
440
|
+
if (!where || !Object.keys(where).length) return { sql: "", params: [] };
|
|
441
|
+
/** @type {any[]} */
|
|
442
|
+
const params = [];
|
|
443
|
+
/**
|
|
444
|
+
* @param {Where<any>} filter
|
|
445
|
+
* @returns {string}
|
|
446
|
+
*/
|
|
447
|
+
const build = (filter) => {
|
|
448
|
+
if (!_isPlain(filter)) throw new SQLiteError("A where filter must be a plain object");
|
|
449
|
+
/** @type {string[]} */
|
|
450
|
+
const parts = [];
|
|
451
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
452
|
+
if (value === undefined) continue;
|
|
453
|
+
if (key === "$or" || key === "$and") {
|
|
454
|
+
if (!Array.isArray(value)) throw new SQLiteError(`"${key}" expects an array of filters`);
|
|
455
|
+
const subs = value.map((sub) => build(sub) || "1");
|
|
456
|
+
parts.push(subs.length ? `(${subs.map((s) => `(${s})`).join(key === "$or" ? " OR " : " AND ")})` : key === "$or" ? "0" : "1");
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const col = _id(key, "column name");
|
|
460
|
+
if (value === null) parts.push(`${col} IS NULL`);
|
|
461
|
+
else if (Array.isArray(value)) parts.push(this.#op(col, "in", value, params));
|
|
462
|
+
else if (_isPlain(value) && Object.keys(value).some((k) => OPERATORS.has(k))) {
|
|
463
|
+
// unknown keys next to operators are typos: #op() reports them
|
|
464
|
+
for (const [op, operand] of Object.entries(value)) if (operand !== undefined) parts.push(this.#op(col, op, operand, params));
|
|
465
|
+
} else {
|
|
466
|
+
// other objects and arrays compare as JSON text
|
|
467
|
+
parts.push(`${col} = ?`);
|
|
468
|
+
params.push(_toSQL(value));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return parts.join(" AND ");
|
|
472
|
+
};
|
|
473
|
+
const sql = build(where);
|
|
474
|
+
return { sql: sql ? ` WHERE ${sql}` : "", params };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @param {string} col
|
|
479
|
+
* @param {string} op
|
|
480
|
+
* @param {unknown} operand
|
|
481
|
+
* @param {any[]} params
|
|
482
|
+
* @returns {string}
|
|
483
|
+
*/
|
|
484
|
+
#op(col, op, operand, params) {
|
|
485
|
+
switch (op) {
|
|
486
|
+
case "eq":
|
|
487
|
+
if (operand === null) return `${col} IS NULL`;
|
|
488
|
+
params.push(_toSQL(operand));
|
|
489
|
+
return `${col} = ?`;
|
|
490
|
+
case "ne":
|
|
491
|
+
if (operand === null) return `${col} IS NOT NULL`;
|
|
492
|
+
params.push(_toSQL(operand));
|
|
493
|
+
return `${col} IS NOT ?`;
|
|
494
|
+
case "gt":
|
|
495
|
+
case "gte":
|
|
496
|
+
case "lt":
|
|
497
|
+
case "lte":
|
|
498
|
+
case "like":
|
|
499
|
+
case "notLike":
|
|
500
|
+
case "glob":
|
|
501
|
+
params.push(_toSQL(operand));
|
|
502
|
+
return `${col} ${COMPARATORS[op]} ?`;
|
|
503
|
+
case "in":
|
|
504
|
+
case "notIn": {
|
|
505
|
+
if (!Array.isArray(operand)) throw new SQLiteError(`"${op}" expects an array`);
|
|
506
|
+
if (!operand.length) return op === "in" ? "0" : "1";
|
|
507
|
+
params.push(...operand.map(_toSQL));
|
|
508
|
+
return `${col} ${op === "in" ? "IN" : "NOT IN"} (${operand.map(() => "?").join(", ")})`;
|
|
509
|
+
}
|
|
510
|
+
case "between": {
|
|
511
|
+
if (!Array.isArray(operand) || operand.length !== 2) throw new SQLiteError('"between" expects [min, max]');
|
|
512
|
+
params.push(_toSQL(operand[0]), _toSQL(operand[1]));
|
|
513
|
+
return `${col} BETWEEN ? AND ?`;
|
|
514
|
+
}
|
|
515
|
+
case "isNull":
|
|
516
|
+
return `${col} ${operand ? "IS NULL" : "IS NOT NULL"}`;
|
|
517
|
+
default:
|
|
518
|
+
throw new SQLiteError(`Unknown where operator "${op}"`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @param {QueryOptions} options
|
|
524
|
+
* @returns {string}
|
|
525
|
+
*/
|
|
526
|
+
#orderBy(options) {
|
|
527
|
+
if (!options.orderBy || (Array.isArray(options.orderBy) && !options.orderBy.length)) return "";
|
|
528
|
+
/** @param {unknown} direction */
|
|
529
|
+
const dir = (direction) => {
|
|
530
|
+
const upper = String(direction ?? "ASC").toUpperCase();
|
|
531
|
+
if (upper !== "ASC" && upper !== "DESC") throw new SQLiteError(`Invalid sort direction "${String(direction)}" (use "ASC" or "DESC")`);
|
|
532
|
+
return upper;
|
|
533
|
+
};
|
|
534
|
+
if (!Array.isArray(options.orderBy)) return ` ORDER BY ${_id(options.orderBy, "column name")} ${dir(options.direction)}`;
|
|
535
|
+
return ` ORDER BY ${options.orderBy.map((item) => (Array.isArray(item) ? `${_id(item[0], "column name")} ${dir(item[1])}` : `${_id(item, "column name")} ASC`)).join(", ")}`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Quoted columns and converted values. `undefined` values are skipped.
|
|
540
|
+
* @param {unknown} data
|
|
541
|
+
* @returns {[string[], any[]]}
|
|
542
|
+
*/
|
|
543
|
+
#values(data) {
|
|
544
|
+
if (!_isPlain(data)) throw new SQLiteError("Row data must be a plain object");
|
|
545
|
+
const entries = Object.entries(data).filter(([, v]) => v !== undefined);
|
|
546
|
+
return [entries.map(([k]) => _id(k, "column name")), entries.map(([, v]) => _toSQL(v))];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* @param {string} table
|
|
551
|
+
* @returns {string}
|
|
552
|
+
*/
|
|
553
|
+
#table(table) {
|
|
554
|
+
const quoted = _id(table, "table name");
|
|
555
|
+
if (!this.hasTable(table)) throw new SQLiteError(`Table "${table}" does not exist`);
|
|
556
|
+
return quoted;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* @param {string} sql
|
|
561
|
+
* @param {any[]} params
|
|
562
|
+
* @returns {InsertResult}
|
|
563
|
+
*/
|
|
564
|
+
#run(sql, params) {
|
|
565
|
+
const stmt = this.#prepare(sql);
|
|
566
|
+
try {
|
|
567
|
+
const result = stmt.run(...params);
|
|
568
|
+
return { ok: true, changes: Number(result.changes), lastInsertRowid: result.lastInsertRowid };
|
|
569
|
+
} catch (error) {
|
|
570
|
+
throw _wrap(error, sql);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* The table a simple `SELECT` reads from, for type conversions.
|
|
576
|
+
* @param {string} sql
|
|
577
|
+
* @returns {string | undefined}
|
|
578
|
+
*/
|
|
579
|
+
#sourceTable(sql) {
|
|
580
|
+
if (/\bjoin\b/i.test(sql)) return undefined;
|
|
581
|
+
const match = /^\s*select\b[\s\S]*?\bfrom\s+"?([A-Za-z_][A-Za-z0-9_]*)"?(?:\s+(?:as\s+)?(?!where\b|order\b|limit\b|group\b)[A-Za-z_]\w*)?\s*(?:where\b|order\b|limit\b|group\b|;|$)/i.exec(sql);
|
|
582
|
+
return match && this.hasTable(match[1]) ? match[1] : undefined;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Runs SQL that doesn't return rows. Without parameters, you can pass
|
|
587
|
+
* several statements at once.
|
|
588
|
+
*
|
|
589
|
+
* @example
|
|
590
|
+
* db.exec("UPDATE users SET active = ? WHERE last_login < ?", [false, cutoff]);
|
|
591
|
+
* db.exec("DELETE FROM sessions WHERE user_id = $id", { $id: 42 });
|
|
592
|
+
* db.exec(`
|
|
593
|
+
* CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);
|
|
594
|
+
* CREATE INDEX idx_tags_label ON tags (label);
|
|
595
|
+
* `);
|
|
596
|
+
*
|
|
597
|
+
* @param {string} sql
|
|
598
|
+
* @param {unknown[] | Record<string, unknown> | string | number | bigint | boolean | Date | null} [params] An array, named parameters like `{ $id: 1 }`, or a single value.
|
|
599
|
+
* @returns {{ changes: number, lastInsertRowid: number | bigint }}
|
|
600
|
+
* @throws {SQLiteError}
|
|
601
|
+
*/
|
|
602
|
+
exec(sql, params) {
|
|
603
|
+
const ddl = /^\s*(create|alter|drop)\b/i.test(sql);
|
|
604
|
+
if (params === undefined && /;\s*\S/.test(_stripQuoted(sql))) {
|
|
605
|
+
try {
|
|
606
|
+
this.#db.exec(sql);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
throw _wrap(error, sql);
|
|
609
|
+
} finally {
|
|
610
|
+
this.#invalidate();
|
|
611
|
+
}
|
|
612
|
+
return { changes: 0, lastInsertRowid: 0 };
|
|
613
|
+
}
|
|
614
|
+
const { changes, lastInsertRowid } = this.#run(sql, this.#params(params));
|
|
615
|
+
if (ddl) this.#invalidate();
|
|
616
|
+
return { changes, lastInsertRowid };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Runs a query and returns the first row. When it reads a single table,
|
|
621
|
+
* JSON and boolean columns are converted.
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* const user = db.queryOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
625
|
+
*
|
|
626
|
+
* @template [T=R]
|
|
627
|
+
* @param {string} sql
|
|
628
|
+
* @param {unknown[] | Record<string, unknown> | string | number | bigint | boolean | Date | null} [params]
|
|
629
|
+
* @returns {T | undefined}
|
|
630
|
+
* @throws {SQLiteError}
|
|
631
|
+
*/
|
|
632
|
+
queryOne(sql, params) {
|
|
633
|
+
const stmt = this.#prepare(sql);
|
|
634
|
+
try {
|
|
635
|
+
return this.#row(stmt.get(...this.#params(params)), this.#sourceTable(sql));
|
|
636
|
+
} catch (error) {
|
|
637
|
+
throw _wrap(error, sql);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Runs a query and returns every row.
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* const stats = db.queryAll("SELECT role, COUNT(*) AS n FROM users GROUP BY role");
|
|
646
|
+
*
|
|
647
|
+
* @template [T=R]
|
|
648
|
+
* @param {string} sql
|
|
649
|
+
* @param {unknown[] | Record<string, unknown> | string | number | bigint | boolean | Date | null} [params]
|
|
650
|
+
* @returns {T[]}
|
|
651
|
+
* @throws {SQLiteError}
|
|
652
|
+
*/
|
|
653
|
+
queryAll(sql, params) {
|
|
654
|
+
const stmt = this.#prepare(sql);
|
|
655
|
+
try {
|
|
656
|
+
const table = this.#sourceTable(sql);
|
|
657
|
+
return stmt.all(...this.#params(params)).map((row) => this.#row(row, table));
|
|
658
|
+
} catch (error) {
|
|
659
|
+
throw _wrap(error, sql);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Goes through the rows of a query one at a time, without loading them all.
|
|
665
|
+
* For big exports.
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* for (const row of db.iterate("SELECT * FROM events WHERE day = ?", [day])) {
|
|
669
|
+
* stream.write(JSON.stringify(row) + "\n");
|
|
670
|
+
* }
|
|
671
|
+
*
|
|
672
|
+
* @template [T=R]
|
|
673
|
+
* @param {string} sql
|
|
674
|
+
* @param {unknown[] | Record<string, unknown> | string | number | bigint | boolean | Date | null} [params]
|
|
675
|
+
* @returns {Generator<T, void, undefined>}
|
|
676
|
+
* @throws {SQLiteError}
|
|
677
|
+
*/
|
|
678
|
+
*iterate(sql, params) {
|
|
679
|
+
const table = this.#sourceTable(sql);
|
|
680
|
+
let stmt;
|
|
681
|
+
try {
|
|
682
|
+
stmt = this.#db.prepare(sql);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
throw _wrap(error, sql);
|
|
685
|
+
}
|
|
686
|
+
for (const row of stmt.iterate(...this.#params(params))) yield this.#row(row, table);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Creates a table with its constraints and indexes, unless it already
|
|
691
|
+
* exists. To change an existing table, use `migrate()`.
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* db.createTable({
|
|
695
|
+
* name: "posts",
|
|
696
|
+
* columns: {
|
|
697
|
+
* id: { type: "INTEGER", primaryKey: true, autoincrement: true },
|
|
698
|
+
* user_id: { type: "INTEGER", notNull: true, references: { table: "users", column: "id" }, onDelete: "CASCADE" },
|
|
699
|
+
* status: { type: "TEXT", values: ["draft", "published"], defaultValue: "draft" },
|
|
700
|
+
* tags: { type: "JSON", defaultValue: [] },
|
|
701
|
+
* },
|
|
702
|
+
* indexes: [{ columns: ["user_id", "status"] }],
|
|
703
|
+
* });
|
|
704
|
+
*
|
|
705
|
+
* @param {TableDefinition} table
|
|
706
|
+
* @returns {{ ok: true }}
|
|
707
|
+
* @throws {SQLiteError} If the definition is invalid.
|
|
708
|
+
*/
|
|
709
|
+
createTable(table) {
|
|
710
|
+
const columns = table?.columns ?? table?.options;
|
|
711
|
+
const name = _id(table?.name, "table name");
|
|
712
|
+
if (!_isPlain(columns) || !Object.keys(columns).length) throw new SQLiteError(`Table "${table.name}" needs at least one column in "columns"`);
|
|
713
|
+
const defs = Object.entries(columns).map(([column, def]) => {
|
|
714
|
+
const col = _id(column, "column name");
|
|
715
|
+
if (!def || typeof def.type !== "string") throw new SQLiteError(`Missing "type" for column "${column}"`);
|
|
716
|
+
if (!/^[A-Za-z][A-Za-z0-9_ ]*(\(\s*\d+\s*(,\s*\d+\s*)?\))?$/.test(def.type)) throw new SQLiteError(`Invalid type "${def.type}" for column "${column}"`);
|
|
717
|
+
let sql = `${col} ${def.type}`;
|
|
718
|
+
if (def.primaryKey) sql += ` PRIMARY KEY${def.autoincrement ? " AUTOINCREMENT" : ""}`;
|
|
719
|
+
if (def.notNull) sql += " NOT NULL";
|
|
720
|
+
if (def.unique) sql += " UNIQUE";
|
|
721
|
+
if (def.defaultValue !== undefined) sql += ` DEFAULT ${_literal(def.defaultValue)}`;
|
|
722
|
+
if (def.values?.length) sql += ` CHECK (${col} IN (${def.values.map(_literal).join(", ")}))`;
|
|
723
|
+
if (def.check) sql += ` CHECK (${def.check})`;
|
|
724
|
+
if (def.references) {
|
|
725
|
+
sql += ` REFERENCES ${_id(def.references.table, "referenced table")} (${_id(def.references.column, "referenced column")})`;
|
|
726
|
+
if (def.onDelete) sql += ` ON DELETE ${this.#action(def.onDelete)}`;
|
|
727
|
+
if (def.onUpdate) sql += ` ON UPDATE ${this.#action(def.onUpdate)}`;
|
|
728
|
+
}
|
|
729
|
+
return sql;
|
|
730
|
+
});
|
|
731
|
+
for (const constraint of table.constraints ?? []) defs.push(this.#constraint(constraint));
|
|
732
|
+
this.exec(`CREATE TABLE IF NOT EXISTS ${name} (${defs.join(", ")})`);
|
|
733
|
+
for (const index of table.indexes ?? []) this.createIndex(table.name, index);
|
|
734
|
+
return { ok: true };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* @param {string} action
|
|
739
|
+
* @returns {string}
|
|
740
|
+
*/
|
|
741
|
+
#action(action) {
|
|
742
|
+
const upper = String(action).toUpperCase();
|
|
743
|
+
if (!ACTIONS.includes(upper)) throw new SQLiteError(`Invalid foreign key action "${action}" (expected one of ${ACTIONS.join(", ")})`);
|
|
744
|
+
return upper;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* @param {TableConstraint} constraint
|
|
749
|
+
* @returns {string}
|
|
750
|
+
*/
|
|
751
|
+
#constraint(constraint) {
|
|
752
|
+
/** @param {unknown} list */
|
|
753
|
+
const cols = (list) => {
|
|
754
|
+
if (!Array.isArray(list) || !list.length) throw new SQLiteError(`Constraint "${constraint.type}" requires a non-empty columns array`);
|
|
755
|
+
return list.map((c) => _id(c, "column name")).join(", ");
|
|
756
|
+
};
|
|
757
|
+
switch (constraint?.type) {
|
|
758
|
+
case "unique":
|
|
759
|
+
return `UNIQUE (${cols(constraint.columns)})`;
|
|
760
|
+
case "primaryKey":
|
|
761
|
+
return `PRIMARY KEY (${cols(constraint.columns)})`;
|
|
762
|
+
case "check":
|
|
763
|
+
if (!constraint.expression) throw new SQLiteError("A check constraint requires an expression");
|
|
764
|
+
return `CHECK (${constraint.expression})`;
|
|
765
|
+
case "foreignKey": {
|
|
766
|
+
let sql = `FOREIGN KEY (${cols(constraint.columns)}) REFERENCES ${_id(constraint.references?.table, "referenced table")} (${cols(constraint.references?.columns)})`;
|
|
767
|
+
if (constraint.onDelete) sql += ` ON DELETE ${this.#action(constraint.onDelete)}`;
|
|
768
|
+
if (constraint.onUpdate) sql += ` ON UPDATE ${this.#action(constraint.onUpdate)}`;
|
|
769
|
+
return sql;
|
|
770
|
+
}
|
|
771
|
+
default:
|
|
772
|
+
throw new SQLiteError(`Unsupported constraint type "${String(/** @type {any} */ (constraint)?.type)}"`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Creates an index, unless it already exists.
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* db.createIndex("users", { columns: ["email"], unique: true });
|
|
781
|
+
* db.createIndex("posts", { columns: ["slug"], unique: true, where: "deleted_at IS NULL" });
|
|
782
|
+
*
|
|
783
|
+
* @param {string} tableName
|
|
784
|
+
* @param {IndexDefinition} index
|
|
785
|
+
* @returns {{ ok: true }}
|
|
786
|
+
* @throws {SQLiteError}
|
|
787
|
+
*/
|
|
788
|
+
createIndex(tableName, index) {
|
|
789
|
+
const table = this.#table(tableName);
|
|
790
|
+
if (!Array.isArray(index?.columns) || !index.columns.length) throw new SQLiteError('An index requires a non-empty "columns" array');
|
|
791
|
+
const name = index.name ?? `idx_${tableName}_${index.columns.join("_")}${index.unique ? "_unique" : ""}`;
|
|
792
|
+
this.exec(`CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${_id(name, "index name")} ON ${table} (${index.columns.map((c) => _id(c, "column name")).join(", ")})${index.where ? ` WHERE ${index.where}` : ""}`);
|
|
793
|
+
return { ok: true };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Drops a table and everything in it.
|
|
798
|
+
*
|
|
799
|
+
* @param {string} tableName
|
|
800
|
+
* @returns {{ ok: true }}
|
|
801
|
+
* @throws {SQLiteError} If the table doesn't exist.
|
|
802
|
+
*/
|
|
803
|
+
deleteTable(tableName) {
|
|
804
|
+
this.exec(`DROP TABLE ${this.#table(tableName)}`);
|
|
805
|
+
return { ok: true };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Deletes every row, keeping the table.
|
|
810
|
+
*
|
|
811
|
+
* @param {string} tableName
|
|
812
|
+
* @returns {{ ok: true }}
|
|
813
|
+
* @throws {SQLiteError} If the table doesn't exist.
|
|
814
|
+
*/
|
|
815
|
+
clearTable(tableName) {
|
|
816
|
+
this.#run(`DELETE FROM ${this.#table(tableName)}`, []);
|
|
817
|
+
return { ok: true };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Does the table exist?
|
|
822
|
+
*
|
|
823
|
+
* @param {string} tableName
|
|
824
|
+
* @returns {boolean}
|
|
825
|
+
*/
|
|
826
|
+
hasTable(tableName) {
|
|
827
|
+
if (typeof tableName !== "string" || !IDENTIFIER.test(tableName)) return false;
|
|
828
|
+
return this.#prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName) !== undefined;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* The tables in the database, sorted.
|
|
833
|
+
*
|
|
834
|
+
* @returns {string[]}
|
|
835
|
+
*/
|
|
836
|
+
tables() {
|
|
837
|
+
return this.#prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' ORDER BY name").all().map((row) => String(row.name));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* The columns of a table.
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* db.columns("users");
|
|
845
|
+
* // [{ name: "id", type: "INTEGER", notNull: false, defaultValue: null, primaryKey: true }, ...]
|
|
846
|
+
*
|
|
847
|
+
* @param {string} tableName
|
|
848
|
+
* @returns {ColumnInfo[]}
|
|
849
|
+
* @throws {SQLiteError} If the table doesn't exist.
|
|
850
|
+
*/
|
|
851
|
+
columns(tableName) {
|
|
852
|
+
return this.#prepare(`PRAGMA table_info(${this.#table(tableName)})`).all().map((c) => ({
|
|
853
|
+
name: String(c.name),
|
|
854
|
+
type: String(c.type),
|
|
855
|
+
notNull: c.notnull === 1,
|
|
856
|
+
defaultValue: c.dflt_value,
|
|
857
|
+
primaryKey: Number(c.pk) > 0,
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* The first row that matches.
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* const user = db.get("users", { email: "ada@example.com" });
|
|
866
|
+
* const latest = db.get("posts", { status: "published" }, { orderBy: "created_at", direction: "DESC" });
|
|
867
|
+
*
|
|
868
|
+
* @template [T=R]
|
|
869
|
+
* @param {string} tableName
|
|
870
|
+
* @param {Where<R>} [where]
|
|
871
|
+
* @param {Pick<QueryOptions, "columns" | "orderBy" | "direction">} [options]
|
|
872
|
+
* @returns {T | undefined}
|
|
873
|
+
* @throws {SQLiteError}
|
|
874
|
+
*/
|
|
875
|
+
get(tableName, where = {}, options = {}) {
|
|
876
|
+
return /** @type {T | undefined} */ (this.getAll(tableName, where, { ...options, limit: 1, offset: 0 })[0]);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* The rows that match, with sorting and pagination.
|
|
881
|
+
*
|
|
882
|
+
* @example
|
|
883
|
+
* db.getAll("users");
|
|
884
|
+
* db.getAll("users", { role: ["admin", "editor"], age: { gte: 18 } });
|
|
885
|
+
* db.getAll("posts", { $or: [{ pinned: true }, { views: { gt: 1000 } }] }, {
|
|
886
|
+
* orderBy: [["views", "DESC"], "title"],
|
|
887
|
+
* limit: 20,
|
|
888
|
+
* offset: 40,
|
|
889
|
+
* });
|
|
890
|
+
*
|
|
891
|
+
* @template [T=R]
|
|
892
|
+
* @param {string} tableName
|
|
893
|
+
* @param {Where<R>} [where]
|
|
894
|
+
* @param {QueryOptions} [options]
|
|
895
|
+
* @returns {T[]}
|
|
896
|
+
* @throws {SQLiteError}
|
|
897
|
+
*/
|
|
898
|
+
getAll(tableName, where = {}, options = {}) {
|
|
899
|
+
const table = this.#table(tableName);
|
|
900
|
+
const columns = options.columns?.length ? options.columns.map((c) => _id(c, "column name")).join(", ") : "*";
|
|
901
|
+
const clause = this.#where(where);
|
|
902
|
+
let sql = `SELECT ${columns} FROM ${table}${clause.sql}${this.#orderBy(options)}`;
|
|
903
|
+
const params = clause.params;
|
|
904
|
+
if (options.limit !== undefined || options.offset !== undefined) {
|
|
905
|
+
sql += " LIMIT ? OFFSET ?";
|
|
906
|
+
params.push(options.limit === undefined ? -1 : Math.max(0, Math.floor(options.limit)), Math.max(0, Math.floor(options.offset ?? 0)));
|
|
907
|
+
}
|
|
908
|
+
const stmt = this.#prepare(sql);
|
|
909
|
+
try {
|
|
910
|
+
return stmt.all(...params).map((row) => this.#row(row, tableName));
|
|
911
|
+
} catch (error) {
|
|
912
|
+
throw _wrap(error, sql);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Counts the rows that match.
|
|
918
|
+
*
|
|
919
|
+
* @example
|
|
920
|
+
* db.count("users", { active: true });
|
|
921
|
+
*
|
|
922
|
+
* @param {string} tableName
|
|
923
|
+
* @param {Where<R>} [where]
|
|
924
|
+
* @returns {number}
|
|
925
|
+
* @throws {SQLiteError}
|
|
926
|
+
*/
|
|
927
|
+
count(tableName, where = {}) {
|
|
928
|
+
const clause = this.#where(where);
|
|
929
|
+
const sql = `SELECT COUNT(*) AS total FROM ${this.#table(tableName)}${clause.sql}`;
|
|
930
|
+
const stmt = this.#prepare(sql);
|
|
931
|
+
try {
|
|
932
|
+
return Number(stmt.get(...clause.params)?.total ?? 0);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
throw _wrap(error, sql);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Does any row match?
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* if (db.exists("users", { email })) throw new Error("Email already registered");
|
|
943
|
+
*
|
|
944
|
+
* @param {string} tableName
|
|
945
|
+
* @param {Where<R>} [where]
|
|
946
|
+
* @returns {boolean}
|
|
947
|
+
* @throws {SQLiteError}
|
|
948
|
+
*/
|
|
949
|
+
exists(tableName, where = {}) {
|
|
950
|
+
const clause = this.#where(where);
|
|
951
|
+
const sql = `SELECT 1 FROM ${this.#table(tableName)}${clause.sql} LIMIT 1`;
|
|
952
|
+
const stmt = this.#prepare(sql);
|
|
953
|
+
try {
|
|
954
|
+
return stmt.get(...clause.params) !== undefined;
|
|
955
|
+
} catch (error) {
|
|
956
|
+
throw _wrap(error, sql);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Inserts a row. Objects and arrays are stored as JSON, booleans as 1 and
|
|
962
|
+
* 0, dates as ISO strings. `undefined` values are skipped, so column
|
|
963
|
+
* defaults apply.
|
|
964
|
+
*
|
|
965
|
+
* @example
|
|
966
|
+
* const { lastInsertRowid } = db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
|
|
967
|
+
*
|
|
968
|
+
* @param {string} tableName
|
|
969
|
+
* @param {Partial<R> & Record<string, unknown>} data
|
|
970
|
+
* @returns {InsertResult}
|
|
971
|
+
* @throws {SQLiteError} On a constraint failure; `sqliteCode` tells you which, like `"SQLITE_CONSTRAINT_UNIQUE"`.
|
|
972
|
+
*/
|
|
973
|
+
insert(tableName, data) {
|
|
974
|
+
const table = this.#table(tableName);
|
|
975
|
+
const [cols, values] = this.#values(data);
|
|
976
|
+
const sql = cols.length ? `INSERT INTO ${table} (${cols.join(", ")}) VALUES (${cols.map(() => "?").join(", ")})` : `INSERT INTO ${table} DEFAULT VALUES`;
|
|
977
|
+
return this.#run(sql, values);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Inserts many rows in one transaction: all or nothing, and much faster
|
|
982
|
+
* than one by one.
|
|
983
|
+
*
|
|
984
|
+
* @example
|
|
985
|
+
* db.insertMany("products", rowsFromCsv);
|
|
986
|
+
*
|
|
987
|
+
* @param {string} tableName
|
|
988
|
+
* @param {Array<Partial<R> & Record<string, unknown>>} rows
|
|
989
|
+
* @returns {ChangeResult}
|
|
990
|
+
* @throws {SQLiteError} If a row fails; nothing is inserted then.
|
|
991
|
+
*/
|
|
992
|
+
insertMany(tableName, rows) {
|
|
993
|
+
this.#table(tableName);
|
|
994
|
+
return this.transaction(() => {
|
|
995
|
+
let changes = 0;
|
|
996
|
+
for (const row of rows) changes += this.insert(tableName, row).changes;
|
|
997
|
+
return { ok: /** @type {const} */ (true), changes };
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Updates the rows that match. The filter is required, so a forgotten
|
|
1003
|
+
* condition can't rewrite the whole table.
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* db.update("users", { active: false }, { last_login: { lt: "2025-01-01" } });
|
|
1007
|
+
*
|
|
1008
|
+
* @param {string} tableName
|
|
1009
|
+
* @param {Partial<R> & Record<string, unknown>} data `undefined` values are skipped.
|
|
1010
|
+
* @param {Where<R>} where
|
|
1011
|
+
* @returns {ChangeResult}
|
|
1012
|
+
* @throws {SQLiteError}
|
|
1013
|
+
*/
|
|
1014
|
+
update(tableName, data, where) {
|
|
1015
|
+
const table = this.#table(tableName);
|
|
1016
|
+
if (!_isPlain(where) || !Object.keys(where).length) throw new SQLiteError("update() requires a non-empty where filter");
|
|
1017
|
+
const [cols, values] = this.#values(data);
|
|
1018
|
+
if (!cols.length) throw new SQLiteError("update() requires at least one column to change");
|
|
1019
|
+
const clause = this.#where(where);
|
|
1020
|
+
const { changes } = this.#run(`UPDATE ${table} SET ${cols.map((c) => `${c} = ?`).join(", ")}${clause.sql}`, [...values, ...clause.params]);
|
|
1021
|
+
return { ok: true, changes };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Inserts a row, or updates it in place if it clashes with an existing one
|
|
1026
|
+
* on the `conflict` columns.
|
|
1027
|
+
*
|
|
1028
|
+
* @example
|
|
1029
|
+
* db.upsert("settings", { user_id: 1, key: "theme", value: "dark" }, ["user_id", "key"]);
|
|
1030
|
+
*
|
|
1031
|
+
* @param {string} tableName
|
|
1032
|
+
* @param {Partial<R> & Record<string, unknown>} data
|
|
1033
|
+
* @param {string[]} conflict The unique columns that identify the row.
|
|
1034
|
+
* @returns {InsertResult}
|
|
1035
|
+
* @throws {SQLiteError} If `conflict` doesn't match a unique constraint.
|
|
1036
|
+
*/
|
|
1037
|
+
upsert(tableName, data, conflict) {
|
|
1038
|
+
const table = this.#table(tableName);
|
|
1039
|
+
const [cols, values] = this.#values(data);
|
|
1040
|
+
if (!cols.length) throw new SQLiteError("upsert() requires data");
|
|
1041
|
+
if (!Array.isArray(conflict) || !conflict.length) throw new SQLiteError("upsert() requires at least one conflict column");
|
|
1042
|
+
const targets = conflict.map((c) => _id(c, "column name"));
|
|
1043
|
+
const updates = cols.filter((c) => !targets.includes(c));
|
|
1044
|
+
const set = updates.length ? updates.map((c) => `${c} = excluded.${c}`).join(", ") : `${targets[0]} = ${targets[0]}`;
|
|
1045
|
+
return this.#run(`INSERT INTO ${table} (${cols.join(", ")}) VALUES (${cols.map(() => "?").join(", ")}) ON CONFLICT (${targets.join(", ")}) DO UPDATE SET ${set}`, values);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Updates the row matching `where` (or `{ id: data.id }`), or inserts it.
|
|
1050
|
+
* Kept from 1.x; `upsert()` does the same in one atomic statement.
|
|
1051
|
+
*
|
|
1052
|
+
* @param {string} tableName
|
|
1053
|
+
* @param {Partial<R> & Record<string, unknown>} data
|
|
1054
|
+
* @param {Where<R> | null} [where]
|
|
1055
|
+
* @returns {{ ok: true }}
|
|
1056
|
+
* @throws {SQLiteError}
|
|
1057
|
+
*/
|
|
1058
|
+
set(tableName, data, where = null) {
|
|
1059
|
+
/** @type {Record<string, unknown>} */
|
|
1060
|
+
const record = data;
|
|
1061
|
+
const lookup = where ?? (record.id !== undefined && record.id !== null ? { id: record.id } : null);
|
|
1062
|
+
this.transaction(() => {
|
|
1063
|
+
if (lookup && this.exists(tableName, lookup)) this.update(tableName, data, lookup);
|
|
1064
|
+
else this.insert(tableName, data);
|
|
1065
|
+
});
|
|
1066
|
+
return { ok: true };
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Deletes the rows that match. The filter is required; `clearTable()`
|
|
1071
|
+
* deletes everything on purpose.
|
|
1072
|
+
*
|
|
1073
|
+
* @example
|
|
1074
|
+
* db.delete("sessions", { expires_at: { lt: new Date() } });
|
|
1075
|
+
*
|
|
1076
|
+
* @param {string} tableName
|
|
1077
|
+
* @param {Where<R>} where
|
|
1078
|
+
* @returns {ChangeResult}
|
|
1079
|
+
* @throws {SQLiteError}
|
|
1080
|
+
*/
|
|
1081
|
+
delete(tableName, where) {
|
|
1082
|
+
const table = this.#table(tableName);
|
|
1083
|
+
if (!_isPlain(where) || !Object.keys(where).length) throw new SQLiteError("delete() requires a non-empty where filter (use clearTable() to delete every row)");
|
|
1084
|
+
const clause = this.#where(where);
|
|
1085
|
+
const { changes } = this.#run(`DELETE FROM ${table}${clause.sql}`, clause.params);
|
|
1086
|
+
return { ok: true, changes };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Shortcuts bound to one table, so you don't repeat its name.
|
|
1091
|
+
*
|
|
1092
|
+
* @example
|
|
1093
|
+
* const users = db.table("users"); // in TypeScript: db.table<User>("users")
|
|
1094
|
+
* users.insert({ email: "ada@example.com" });
|
|
1095
|
+
* users.count({ active: true });
|
|
1096
|
+
*
|
|
1097
|
+
* @template [T=R]
|
|
1098
|
+
* @param {string} tableName The table can be created later.
|
|
1099
|
+
* @returns {TableHandle<T>}
|
|
1100
|
+
*/
|
|
1101
|
+
table(tableName) {
|
|
1102
|
+
_id(tableName, "table name");
|
|
1103
|
+
return {
|
|
1104
|
+
name: tableName,
|
|
1105
|
+
get: (where, options) => this.get(tableName, where, options),
|
|
1106
|
+
getAll: (where, options) => this.getAll(tableName, where, options),
|
|
1107
|
+
insert: (data) => this.insert(tableName, /** @type {any} */ (data)),
|
|
1108
|
+
insertMany: (rows) => this.insertMany(tableName, /** @type {any[]} */ (rows)),
|
|
1109
|
+
update: (data, where) => this.update(tableName, /** @type {any} */ (data), where),
|
|
1110
|
+
upsert: (data, conflict) => this.upsert(tableName, /** @type {any} */ (data), conflict),
|
|
1111
|
+
delete: (where) => this.delete(tableName, where),
|
|
1112
|
+
count: (where) => this.count(tableName, where),
|
|
1113
|
+
exists: (where) => this.exists(tableName, where),
|
|
1114
|
+
clear: () => this.clearTable(tableName),
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Runs `callback` in a transaction: all its changes are saved together, or
|
|
1120
|
+
* none are if it throws. The error is then thrown again. Transactions can
|
|
1121
|
+
* nest; an inner failure can be caught without losing the outer work.
|
|
1122
|
+
*
|
|
1123
|
+
* @example
|
|
1124
|
+
* db.transaction(() => {
|
|
1125
|
+
* db.update("accounts", { balance: from.balance - amount }, { id: from.id });
|
|
1126
|
+
* db.update("accounts", { balance: to.balance + amount }, { id: to.id });
|
|
1127
|
+
* });
|
|
1128
|
+
*
|
|
1129
|
+
* @template T
|
|
1130
|
+
* @param {() => T} callback Can be async; the commit then waits for it.
|
|
1131
|
+
* @returns {T}
|
|
1132
|
+
* @throws Whatever `callback` threw, after rolling back.
|
|
1133
|
+
*/
|
|
1134
|
+
transaction(callback) {
|
|
1135
|
+
const savepoint = this.#depth > 0 ? `nc_savepoint_${this.#depth}` : null;
|
|
1136
|
+
this.#db.exec(savepoint ? `SAVEPOINT ${savepoint}` : "BEGIN");
|
|
1137
|
+
this.#depth++;
|
|
1138
|
+
const commit = () => {
|
|
1139
|
+
this.#depth--;
|
|
1140
|
+
this.#db.exec(savepoint ? `RELEASE ${savepoint}` : "COMMIT");
|
|
1141
|
+
};
|
|
1142
|
+
const rollback = () => {
|
|
1143
|
+
this.#depth--;
|
|
1144
|
+
try {
|
|
1145
|
+
this.#db.exec(savepoint ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : "ROLLBACK");
|
|
1146
|
+
} catch {
|
|
1147
|
+
// SQLite already rolled back (for example after a fatal error)
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
let result;
|
|
1151
|
+
try {
|
|
1152
|
+
result = callback();
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
rollback();
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1157
|
+
if (result !== null && typeof result === "object" && typeof (/** @type {any} */ (result).then) === "function") {
|
|
1158
|
+
return /** @type {T} */ (/** @type {unknown} */ (Promise.resolve(result).then(
|
|
1159
|
+
(value) => {
|
|
1160
|
+
commit();
|
|
1161
|
+
return value;
|
|
1162
|
+
},
|
|
1163
|
+
(error) => {
|
|
1164
|
+
rollback();
|
|
1165
|
+
throw error;
|
|
1166
|
+
},
|
|
1167
|
+
)));
|
|
1168
|
+
}
|
|
1169
|
+
commit();
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/** Whether a transaction is open. */
|
|
1174
|
+
get inTransaction() {
|
|
1175
|
+
// `isTransaction` exists since Node 22.16
|
|
1176
|
+
const native = /** @type {{ isTransaction?: boolean }} */ (/** @type {unknown} */ (this.#db)).isTransaction;
|
|
1177
|
+
return typeof native === "boolean" ? native : this.#depth > 0;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Applies migrations in order, each one once. The current version is kept
|
|
1182
|
+
* in the database, and each migration runs in its own transaction. Only
|
|
1183
|
+
* ever add migrations at the end of the list.
|
|
1184
|
+
*
|
|
1185
|
+
* @example
|
|
1186
|
+
* db.migrate([
|
|
1187
|
+
* "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE)",
|
|
1188
|
+
* "ALTER TABLE users ADD COLUMN name TEXT",
|
|
1189
|
+
* (db) => db.update("users", { name: "unknown" }, { name: null }),
|
|
1190
|
+
* ]);
|
|
1191
|
+
*
|
|
1192
|
+
* @param {Migration[]} migrations All of them, from the first.
|
|
1193
|
+
* @returns {{ from: number, to: number }} The version before and after.
|
|
1194
|
+
* @throws {SQLiteError} If a migration fails. It's rolled back; earlier ones stay.
|
|
1195
|
+
*/
|
|
1196
|
+
migrate(migrations) {
|
|
1197
|
+
const from = this.version;
|
|
1198
|
+
for (let i = from; i < migrations.length; i++) {
|
|
1199
|
+
const migration = migrations[i];
|
|
1200
|
+
try {
|
|
1201
|
+
this.transaction(() => {
|
|
1202
|
+
if (typeof migration === "function") migration(this);
|
|
1203
|
+
else this.#db.exec(migration);
|
|
1204
|
+
this.#db.exec(`PRAGMA user_version = ${i + 1}`);
|
|
1205
|
+
});
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
throw new SQLiteError(`Migration ${i + 1} failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error, sql: typeof migration === "string" ? migration : undefined });
|
|
1208
|
+
} finally {
|
|
1209
|
+
this.#invalidate();
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return { from, to: this.version };
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/** The migration version, stored in `PRAGMA user_version`. */
|
|
1216
|
+
get version() {
|
|
1217
|
+
return Number(this.#prepare("PRAGMA user_version").get()?.user_version ?? 0);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Copies the database to a new file. Safe while the app is running.
|
|
1222
|
+
*
|
|
1223
|
+
* @example
|
|
1224
|
+
* db.backup(`backups/app-${nc.time.format(new Date(), "YYYY-MM-DD")}.sqlite`);
|
|
1225
|
+
*
|
|
1226
|
+
* @param {string} path Must not exist yet.
|
|
1227
|
+
* @returns {string} The absolute path of the copy.
|
|
1228
|
+
* @throws {SQLiteError} If the file exists or can't be written.
|
|
1229
|
+
*/
|
|
1230
|
+
backup(path) {
|
|
1231
|
+
const target = nodePath.resolve(process.cwd(), path);
|
|
1232
|
+
if (fs.existsSync(target)) throw new SQLiteError(`Backup destination already exists: ${target}`);
|
|
1233
|
+
fs.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
1234
|
+
try {
|
|
1235
|
+
this.#db.prepare("VACUUM INTO ?").run(target);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
throw _wrap(error, "VACUUM INTO ?");
|
|
1238
|
+
}
|
|
1239
|
+
return target;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Compacts the database file to reclaim unused space.
|
|
1244
|
+
*
|
|
1245
|
+
* @returns {void}
|
|
1246
|
+
*/
|
|
1247
|
+
vacuum() {
|
|
1248
|
+
this.exec("VACUUM");
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Makes a JavaScript function callable from SQL.
|
|
1253
|
+
*
|
|
1254
|
+
* @example
|
|
1255
|
+
* db.fn("slugify", (text) => nc.str.slugify(String(text)));
|
|
1256
|
+
* db.queryAll("SELECT slugify(title) AS slug FROM posts");
|
|
1257
|
+
*
|
|
1258
|
+
* @param {string} name
|
|
1259
|
+
* @param {(...args: any[]) => null | number | bigint | string | Uint8Array} implementation
|
|
1260
|
+
* @param {{ deterministic?: boolean, varargs?: boolean }} [options] `deterministic` (on by default) lets indexes use it; `varargs` accepts any number of arguments.
|
|
1261
|
+
* @returns {this}
|
|
1262
|
+
*/
|
|
1263
|
+
fn(name, implementation, options = {}) {
|
|
1264
|
+
_id(name, "function name");
|
|
1265
|
+
this.#db.function(name, { deterministic: options.deterministic ?? true, varargs: options.varargs ?? false }, implementation);
|
|
1266
|
+
return this;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** The absolute path of the database file, or `":memory:"`. */
|
|
1270
|
+
get path() {
|
|
1271
|
+
return this.#path;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/** Whether the connection is open. */
|
|
1275
|
+
get isOpen() {
|
|
1276
|
+
return this.#open;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* The underlying `node:sqlite` database, for anything not covered here.
|
|
1281
|
+
* @returns {import("node:sqlite").DatabaseSync}
|
|
1282
|
+
*/
|
|
1283
|
+
get native() {
|
|
1284
|
+
return this.#db;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Closes the connection. Calling it twice is fine.
|
|
1289
|
+
*
|
|
1290
|
+
* @returns {void}
|
|
1291
|
+
*/
|
|
1292
|
+
close() {
|
|
1293
|
+
this.#statements.clear();
|
|
1294
|
+
if (!this.#open) return;
|
|
1295
|
+
this.#open = false;
|
|
1296
|
+
this.#db.close();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Closes the connection at the end of a `using` block.
|
|
1301
|
+
* @returns {void}
|
|
1302
|
+
*/
|
|
1303
|
+
[Symbol.dispose]() {
|
|
1304
|
+
this.close();
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
module.exports = SQLite;
|