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