@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/SQLite.js CHANGED
@@ -1,1005 +1,1629 @@
1
- const checker = require("./Checker");
2
- const { resolve, dirname, isAbsolute } = require("node:path");
3
- const { existsSync, mkdirSync } = require("node:fs");
4
- const originalEmitWarning = process.emitWarning;
5
-
6
- process.emitWarning = (warning, ...args) => {
7
- if (
8
- (warning &&
9
- warning.name === "ExperimentalWarning" &&
10
- warning.message.includes("SQLite")) ||
11
- (typeof warning === "string" &&
12
- warning.startsWith("SQLite is an experimental feature"))
13
- ) {
14
- return;
15
- }
16
-
17
- return originalEmitWarning.call(process, warning, ...args);
18
- };
19
-
20
- const { DatabaseSync } = require("node:sqlite");
21
-
22
- /**
23
- * A lightweight SQLite wrapper around Node.js built-in `node:sqlite` module.
24
- *
25
- * Goals:
26
- * - Keep the API small and easy to learn.
27
- * - Support common CRUD workflows.
28
- * - Expose raw SQL helpers when advanced control is needed.
29
- * - Support indexes, foreign keys, composite unique constraints, and transactions.
30
- *
31
- * Notes:
32
- * - Values are safely bound with `?` placeholders.
33
- * - Table names, column names, and index names cannot be parameterized in SQLite,
34
- * so this class validates identifiers before interpolating them into SQL.
35
- * - WAL mode and foreign keys are enabled automatically.
36
- *
37
- * @example
38
- * const { SQLite } = require("@ix-xs/node-comfort");
39
- * const db = new SQLite("db/app.sqlite");
40
- *
41
- * await db.createTable({
42
- * name: "User",
43
- * columns: {
44
- * id: { type: "INTEGER", primaryKey: true, autoincrement: true },
45
- * email: { type: "TEXT", unique: true },
46
- * createdAt: { type: "INTEGER", notNull: true }
47
- * },
48
- * indexes: [
49
- * { columns: ["email"], unique: true }
50
- * ]
51
- * });
52
- *
53
- * await db.insert("User", {
54
- * email: "john@example.com",
55
- * createdAt: Date.now()
56
- * });
57
- *
58
- * const user = await db.get("User", { email: "john@example.com" });
59
- */
60
- module.exports = class SQLite {
61
- /** @type {DatabaseSync} */
62
- #$;
63
-
64
- /**
65
- * In-memory schema registry.
66
- * @type {Map<string, {
67
- * name: string,
68
- * columns: Record<string, any>,
69
- * constraints?: any[],
70
- * indexes?: any[]
71
- * }>}
72
- */
73
- #models = new Map();
74
-
75
- /**
76
- * Creates a new SQLite database connection.
77
- *
78
- * The database file and its parent folders are created if necessary.
79
- * WAL journal mode and foreign key support are enabled automatically.
80
- *
81
- * Pass `":memory:"` to create a transient in-memory database.
82
- *
83
- * @param {string} [path="db.sqlite"] - Path to the SQLite database file.
84
- *
85
- * @example
86
- * const db = new SQLite();
87
- *
88
- * @example
89
- * const db = new SQLite("data/my-app.sqlite");
90
- *
91
- * @example
92
- * const db = new SQLite(":memory:");
93
- */
94
- constructor(path = "db.sqlite") {
95
- let target = path;
96
-
97
- if (path !== ":memory:" && path !== "") {
98
- // Resolve to an absolute path so the folder we create and the file we
99
- // open are guaranteed to be the same location (relative to the cwd).
100
- target = isAbsolute(path) ? path : resolve(process.cwd(), path);
101
- const folder = dirname(target);
102
- if (folder && !existsSync(folder)) {
103
- mkdirSync(folder, { recursive: true });
104
- }
105
- }
106
-
107
- this.#$ = new DatabaseSync(target);
108
- this.#run("PRAGMA journal_mode = WAL");
109
- this.#run("PRAGMA foreign_keys = ON");
110
- }
111
-
112
- /**
113
- * Ensures a SQL identifier is safe to interpolate.
114
- *
115
- * SQLite does not allow binding table or column names as parameters,
116
- * so identifiers must be validated before being used in SQL strings.
117
- *
118
- * Allowed characters:
119
- * - letters
120
- * - digits
121
- * - underscore
122
- *
123
- * The first character must be a letter or underscore.
124
- *
125
- * @private
126
- * @param {string} name - Identifier to validate.
127
- * @param {string} [kind="identifier"] - Human-readable identifier type for error messages.
128
- * @returns {string}
129
- * @throws {Error} If the identifier is invalid.
130
- */
131
- #assertIdentifier(name, kind = "identifier") {
132
- if (typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
133
- throw new Error(`Invalid ${kind}: "${name}"`);
134
- }
135
-
136
- return name;
137
- }
138
-
139
- /**
140
- * Converts a JavaScript value into a SQL literal.
141
- *
142
- * This is only used for SQL fragments that cannot be parameter-bound,
143
- * such as `DEFAULT`, `CHECK`, or migration-time SQL generation.
144
- *
145
- * @private
146
- * @param {any} value - Value to convert.
147
- * @returns {string}
148
- */
149
- #toSQLValue(value) {
150
- if (value === null) return "NULL";
151
- if (typeof value === "number") return String(value);
152
- if (typeof value === "bigint") return String(value);
153
- if (typeof value === "boolean") return value ? "1" : "0";
154
- if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
155
-
156
- return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
157
- }
158
-
159
- /**
160
- * Executes a SQL statement that modifies the database.
161
- *
162
- * This is the internal low-level helper used by mutation methods.
163
- *
164
- * @private
165
- * @param {string} sql - SQL statement.
166
- * @param {any[]|any} [params=[]] - Parameters to bind.
167
- * @returns {{ changes: number, lastInsertRowid: bigint|number }}
168
- */
169
- #run(sql, params = []) {
170
- const stmt = this.#$.prepare(sql);
171
- const result = checker.isArray(params) ? stmt.run(...params) : stmt.run(params);
172
-
173
- return {
174
- changes: result.changes,
175
- lastInsertRowid: result.lastInsertRowid,
176
- };
177
- }
178
-
179
- /**
180
- * Builds a `WHERE` clause from a plain object.
181
- *
182
- * Example:
183
- * `{ id: 1, role: "admin" }`
184
- * becomes:
185
- * `WHERE id = ? AND role = ?`
186
- *
187
- * If the object is empty, an empty WHERE clause is returned.
188
- *
189
- * @private
190
- * @param {Record<string, any>} [where={}] - Conditions.
191
- * @returns {{ where: string, params: any[] }}
192
- */
193
- #buildWhere(where = {}) {
194
- if (!where || Object.keys(where).length === 0) {
195
- return { where: "", params: [] };
196
- }
197
-
198
- const conditions = [];
199
- const params = [];
200
-
201
- for (const [key, value] of Object.entries(where)) {
202
- this.#assertIdentifier(key, "column name");
203
-
204
- if (value === null) {
205
- conditions.push(`${key} IS NULL`);
206
- continue;
207
- }
208
-
209
- conditions.push(`${key} = ?`);
210
- params.push(value);
211
- }
212
-
213
- return {
214
- where: `WHERE ${conditions.join(" AND ")}`,
215
- params,
216
- };
217
- }
218
-
219
- /**
220
- * Builds the SQL fragment for a column definition.
221
- *
222
- * Supported column properties:
223
- * - type
224
- * - primaryKey
225
- * - autoincrement
226
- * - notNull
227
- * - unique
228
- * - defaultValue
229
- * - values
230
- * - references
231
- * - onDelete
232
- * - onUpdate
233
- *
234
- * @private
235
- * @param {string} columnName - Column name.
236
- * @param {Record<string, any>} def - Column definition.
237
- * @returns {string}
238
- */
239
- #buildColumnSQL(columnName, def) {
240
- this.#assertIdentifier(columnName, "column name");
241
-
242
- if (!def?.type) {
243
- throw new Error(`Missing "type" for column "${columnName}"`);
244
- }
245
-
246
- let colDef = `${columnName} ${def.type}`;
247
-
248
- if (def.primaryKey) {
249
- colDef += " PRIMARY KEY";
250
- if (def.autoincrement) colDef += " AUTOINCREMENT";
251
- }
252
-
253
- if (def.notNull) colDef += " NOT NULL";
254
- if (def.unique) colDef += " UNIQUE";
255
-
256
- if (def.defaultValue !== undefined) {
257
- colDef += ` DEFAULT ${this.#toSQLValue(def.defaultValue)}`;
258
- }
259
-
260
- if (def.values?.length) {
261
- colDef += ` CHECK(${columnName} IN (${def.values
262
- .map((value) => this.#toSQLValue(value))
263
- .join(", ")}))`;
264
- }
265
-
266
- if (def.references) {
267
- this.#assertIdentifier(def.references.table, "referenced table");
268
- this.#assertIdentifier(def.references.column, "referenced column");
269
-
270
- colDef += ` REFERENCES ${def.references.table}(${def.references.column})`;
271
-
272
- if (def.onDelete) colDef += ` ON DELETE ${def.onDelete}`;
273
- if (def.onUpdate) colDef += ` ON UPDATE ${def.onUpdate}`;
274
- }
275
-
276
- return colDef;
277
- }
278
-
279
- /**
280
- * Builds a table-level constraint SQL fragment.
281
- *
282
- * Supported constraint formats:
283
- * - `{ type: "unique", columns: ["email", "provider"] }`
284
- * - `{ type: "primaryKey", columns: ["a", "b"] }`
285
- * - `{ type: "check", expression: "age >= 0" }`
286
- * - `{ type: "foreignKey", columns: ["userId"], references: { table: "User", columns: ["id"] }, onDelete: "CASCADE" }`
287
- *
288
- * @private
289
- * @param {Record<string, any>} constraint - Constraint definition.
290
- * @returns {string}
291
- */
292
- #buildConstraintSQL(constraint) {
293
- if (!constraint?.type) {
294
- throw new Error("Table constraint requires a \"type\" property");
295
- }
296
-
297
- if (constraint.type === "unique") {
298
- const columns = (constraint.columns || []).map((col) =>
299
- this.#assertIdentifier(col, "column name"),
300
- );
301
- if (!columns.length) throw new Error("UNIQUE constraint requires columns");
302
- return `UNIQUE (${columns.join(", ")})`;
303
- }
304
-
305
- if (constraint.type === "primaryKey") {
306
- const columns = (constraint.columns || []).map((col) =>
307
- this.#assertIdentifier(col, "column name"),
308
- );
309
- if (!columns.length) throw new Error("PRIMARY KEY constraint requires columns");
310
- return `PRIMARY KEY (${columns.join(", ")})`;
311
- }
312
-
313
- if (constraint.type === "check") {
314
- if (!constraint.expression || typeof constraint.expression !== "string") {
315
- throw new Error("CHECK constraint requires an expression");
316
- }
317
- return `CHECK (${constraint.expression})`;
318
- }
319
-
320
- if (constraint.type === "foreignKey") {
321
- const columns = (constraint.columns || []).map((col) =>
322
- this.#assertIdentifier(col, "column name"),
323
- );
324
- const refColumns = (constraint.references?.columns || []).map((col) =>
325
- this.#assertIdentifier(col, "referenced column"),
326
- );
327
-
328
- if (!columns.length) throw new Error("FOREIGN KEY constraint requires columns");
329
- if (!constraint.references?.table) {
330
- throw new Error("FOREIGN KEY constraint requires references.table");
331
- }
332
- if (!refColumns.length) {
333
- throw new Error("FOREIGN KEY constraint requires references.columns");
334
- }
335
-
336
- this.#assertIdentifier(constraint.references.table, "referenced table");
337
-
338
- let sql = `FOREIGN KEY (${columns.join(", ")}) REFERENCES ${constraint.references.table} (${refColumns.join(", ")})`;
339
-
340
- if (constraint.onDelete) sql += ` ON DELETE ${constraint.onDelete}`;
341
- if (constraint.onUpdate) sql += ` ON UPDATE ${constraint.onUpdate}`;
342
-
343
- return sql;
344
- }
345
-
346
- throw new Error(`Unsupported constraint type "${constraint.type}"`);
347
- }
348
-
349
- /**
350
- * Builds a complete `CREATE TABLE` statement.
351
- *
352
- * @private
353
- * @param {string} tableName - Table name.
354
- * @param {Record<string, any>} columns - Column definitions.
355
- * @param {Record<string, any>[]} [constraints=[]] - Table-level constraints.
356
- * @returns {string}
357
- */
358
- #buildCreateSQL(tableName, columns, constraints = []) {
359
- this.#assertIdentifier(tableName, "table name");
360
-
361
- const columnDefs = Object.entries(columns).map(([columnName, def]) =>
362
- this.#buildColumnSQL(columnName, def),
363
- );
364
-
365
- const tableConstraints = constraints.map((constraint) =>
366
- this.#buildConstraintSQL(constraint),
367
- );
368
-
369
- return `CREATE TABLE IF NOT EXISTS ${tableName} (${[
370
- ...columnDefs,
371
- ...tableConstraints,
372
- ].join(", ")})`;
373
- }
374
-
375
- /**
376
- * Returns the default `ORDER BY` clause for a table.
377
- *
378
- * The first declared column is used as default. If the model is unknown,
379
- * `"id DESC"` is used as a fallback.
380
- *
381
- * @private
382
- * @param {string} tableName - Table name.
383
- * @returns {string}
384
- */
385
- #getOrderBy(tableName) {
386
- const model = this.#models.get(tableName);
387
- if (!model) return "id DESC";
388
-
389
- const firstCol = Object.keys(model.columns)[0];
390
- return `${firstCol} DESC`;
391
- }
392
-
393
- /**
394
- * Registers a table model and creates the table if necessary.
395
- *
396
- * If the model already exists in memory, nothing is done.
397
- *
398
- * @private
399
- * @param {{
400
- * name: string,
401
- * columns: Record<string, any>,
402
- * constraints?: Record<string, any>[],
403
- * indexes?: Record<string, any>[]
404
- * }} table - Table definition.
405
- */
406
- #defineModel(table) {
407
- if (this.#models.has(table.name)) return;
408
-
409
- const createSQL = this.#buildCreateSQL(
410
- table.name,
411
- table.columns,
412
- table.constraints || [],
413
- );
414
-
415
- this.#run(createSQL);
416
- this.#models.set(table.name, {
417
- name: table.name,
418
- columns: table.columns,
419
- constraints: table.constraints || [],
420
- indexes: table.indexes || [],
421
- });
422
-
423
- for (const index of table.indexes || []) {
424
- this.createIndex(table.name, index);
425
- }
426
- }
427
-
428
- /**
429
- * Executes a raw SQL statement using `run()`.
430
- *
431
- * Use this for custom SQL that is not covered by the helper methods.
432
- * This method is intended for statements such as:
433
- * - `INSERT`
434
- * - `UPDATE`
435
- * - `DELETE`
436
- * - `CREATE INDEX`
437
- * - PRAGMA writes
438
- *
439
- * @param {string} sql - SQL statement.
440
- * @param {any[]|any} [params=[]] - Parameters to bind.
441
- * @returns {Promise<{ changes: number, lastInsertRowid: bigint|number } | { error: string }>}
442
- */
443
- async exec(sql, params = []) {
444
- try {
445
- return this.#run(sql, params);
446
- } catch (error) {
447
- return { error: error.message };
448
- }
449
- }
450
-
451
- /**
452
- * Executes a raw SQL query and returns the first row.
453
- *
454
- * @param {string} sql - SQL query.
455
- * @param {any[]|any} [params=[]] - Parameters to bind.
456
- * @returns {Promise<object|undefined|{ error: string }>}
457
- */
458
- async queryOne(sql, params = []) {
459
- try {
460
- const stmt = this.#$.prepare(sql);
461
- const row = checker.isArray(params) ? stmt.get(...params) : stmt.get(params);
462
- return row || undefined;
463
- } catch (error) {
464
- return { error: error.message };
465
- }
466
- }
467
-
468
- /**
469
- * Executes a raw SQL query and returns all rows.
470
- *
471
- * @param {string} sql - SQL query.
472
- * @param {any[]|any} [params=[]] - Parameters to bind.
473
- * @returns {Promise<object[]|{ error: string }>}
474
- */
475
- async queryAll(sql, params = []) {
476
- try {
477
- const stmt = this.#$.prepare(sql);
478
- return checker.isArray(params) ? stmt.all(...params) : stmt.all(params);
479
- } catch (error) {
480
- return { error: error.message };
481
- }
482
- }
483
-
484
- /**
485
- * Creates a table and stores its schema in memory.
486
- *
487
- * This method supports both the new `columns` property and the legacy
488
- * `options` property for backward compatibility.
489
- *
490
- * @param {{
491
- * name: string,
492
- * columns?: Record<string, {
493
- * type: "INTEGER"|"TEXT"|"REAL"|"BLOB"|"NUMERIC",
494
- * primaryKey?: boolean,
495
- * autoincrement?: boolean,
496
- * notNull?: boolean,
497
- * unique?: boolean,
498
- * defaultValue?: any,
499
- * values?: any[],
500
- * references?: { table: string, column: string },
501
- * onDelete?: string,
502
- * onUpdate?: string
503
- * }>,
504
- * options?: Record<string, any>,
505
- * constraints?: Array<
506
- * | { type: "unique", columns: string[] }
507
- * | { type: "primaryKey", columns: string[] }
508
- * | { type: "check", expression: string }
509
- * | {
510
- * type: "foreignKey",
511
- * columns: string[],
512
- * references: { table: string, columns: string[] },
513
- * onDelete?: string,
514
- * onUpdate?: string
515
- * }
516
- * >,
517
- * indexes?: Array<{
518
- * name?: string,
519
- * columns: string[],
520
- * unique?: boolean
521
- * }>
522
- * }} table - Table definition.
523
- * @returns {Promise<{ ok: true } | { error: string }>}
524
- *
525
- * @example
526
- * await db.createTable({
527
- * name: "Post",
528
- * columns: {
529
- * id: { type: "INTEGER", primaryKey: true, autoincrement: true },
530
- * userId: {
531
- * type: "INTEGER",
532
- * notNull: true,
533
- * references: { table: "User", column: "id" },
534
- * onDelete: "CASCADE"
535
- * },
536
- * title: { type: "TEXT", notNull: true }
537
- * },
538
- * indexes: [{ columns: ["userId"] }]
539
- * });
540
- */
541
- async createTable(table) {
542
- try {
543
- const columns = table.columns || table.options;
544
-
545
- if (!columns) {
546
- return { error: "Missing \"columns\" (or legacy \"options\") in table definition" };
547
- }
548
-
549
- this.#defineModel({
550
- name: table.name,
551
- columns,
552
- constraints: table.constraints || [],
553
- indexes: table.indexes || [],
554
- });
555
-
556
- return { ok: true };
557
- } catch (error) {
558
- return { error: error.message };
559
- }
560
- }
561
-
562
- /**
563
- * Creates an index on an existing table.
564
- *
565
- * Composite indexes are supported by passing multiple columns.
566
- *
567
- * @param {string} tableName - Table name.
568
- * @param {{
569
- * name?: string,
570
- * columns: string[],
571
- * unique?: boolean
572
- * }} index - Index definition.
573
- * @returns {Promise<{ ok: true } | { error: string }>}
574
- *
575
- * @example
576
- * await db.createIndex("UserAuthMethod", {
577
- * columns: ["provider", "providerUserId"],
578
- * unique: true
579
- * });
580
- */
581
- async createIndex(tableName, index) {
582
- try {
583
- this.#assertIdentifier(tableName, "table name");
584
-
585
- const columns = (index.columns || []).map((column) =>
586
- this.#assertIdentifier(column, "column name"),
587
- );
588
-
589
- if (!columns.length) {
590
- return { error: "Index requires a non-empty \"columns\" array" };
591
- }
592
-
593
- const indexName =
594
- index.name ||
595
- `idx_${tableName}_${columns.join("_")}${index.unique ? "_unique" : ""}`;
596
-
597
- this.#assertIdentifier(indexName, "index name");
598
-
599
- const sql = `CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${columns.join(", ")})`;
600
- this.#run(sql);
601
-
602
- return { ok: true };
603
- } catch (error) {
604
- return { error: error.message };
605
- }
606
- }
607
-
608
- /**
609
- * Drops a table and removes its model definition.
610
- *
611
- * @param {string} tableName - Table name.
612
- * @returns {Promise<{ ok: true } | { error: string }>}
613
- */
614
- async deleteTable(tableName) {
615
- try {
616
- if (!this.#models.has(tableName)) {
617
- return { error: `Table ${tableName} doesn't exist` };
618
- }
619
-
620
- this.#assertIdentifier(tableName, "table name");
621
- this.#run(`DROP TABLE IF EXISTS ${tableName}`);
622
- this.#models.delete(tableName);
623
-
624
- return { ok: true };
625
- } catch (error) {
626
- return { error: error.message };
627
- }
628
- }
629
-
630
- /**
631
- * Deletes all rows from a table.
632
- *
633
- * @param {string} tableName - Table name.
634
- * @returns {Promise<{ ok: true } | { error: string }>}
635
- */
636
- async clearTable(tableName) {
637
- try {
638
- if (!this.#models.has(tableName)) {
639
- return { error: `Table ${tableName} doesn't exist` };
640
- }
641
-
642
- this.#assertIdentifier(tableName, "table name");
643
- this.#run(`DELETE FROM ${tableName}`);
644
-
645
- return { ok: true };
646
- } catch (error) {
647
- return { error: error.message };
648
- }
649
- }
650
-
651
- /**
652
- * Returns a single row matching a plain-object WHERE clause.
653
- *
654
- * @param {string} tableName - Table name.
655
- * @param {Record<string, any>} [where={}] - Conditions.
656
- * @returns {Promise<object|undefined|{ error: string }>}
657
- */
658
- async get(tableName, where = {}) {
659
- try {
660
- if (!this.#models.has(tableName)) return undefined;
661
-
662
- this.#assertIdentifier(tableName, "table name");
663
-
664
- const whereClause = this.#buildWhere(where);
665
- const sql = `SELECT * FROM ${tableName} ${whereClause.where} LIMIT 1`;
666
- const stmt = this.#$.prepare(sql);
667
- const record = stmt.get(...whereClause.params);
668
-
669
- return record || undefined;
670
- } catch (error) {
671
- return { error: error.message };
672
- }
673
- }
674
-
675
- /**
676
- * Returns multiple rows from a table.
677
- *
678
- * Supports:
679
- * - exact-match filters via `where`
680
- * - custom ordering
681
- * - pagination via `limit` and `offset`
682
- *
683
- * @param {string} tableName - Table name.
684
- * @param {Record<string, any>} [where={}] - Exact-match conditions.
685
- * @param {{
686
- * limit?: number,
687
- * offset?: number,
688
- * orderBy?: string,
689
- * direction?: "ASC"|"DESC"
690
- * }} [options={}] - Query options.
691
- * @returns {Promise<object[] | { error: string }>}
692
- *
693
- * @example
694
- * const posts = await db.getAll("Post", { userId: 1 }, {
695
- * limit: 20,
696
- * offset: 0,
697
- * orderBy: "createdAt",
698
- * direction: "DESC"
699
- * });
700
- */
701
- async getAll(tableName, where = {}, options = {}) {
702
- try {
703
- if (!this.#models.has(tableName)) return [];
704
-
705
- this.#assertIdentifier(tableName, "table name");
706
-
707
- const whereClause = this.#buildWhere(where);
708
- const limit = Number.isInteger(options.limit) ? options.limit : 50;
709
- const offset = Number.isInteger(options.offset) ? options.offset : 0;
710
- const direction = options.direction === "ASC" ? "ASC" : "DESC";
711
-
712
- const orderBy = options.orderBy
713
- ? this.#assertIdentifier(options.orderBy, "column name")
714
- : this.#getOrderBy(tableName).split(" ")[0];
715
-
716
- const sql = `SELECT * FROM ${tableName} ${whereClause.where} ORDER BY ${orderBy} ${direction} LIMIT ? OFFSET ?`;
717
- const stmt = this.#$.prepare(sql);
718
- const records = stmt.all(...whereClause.params, limit, offset);
719
-
720
- return records;
721
- } catch (error) {
722
- return { error: error.message };
723
- }
724
- }
725
-
726
- /**
727
- * Inserts a new row into a table.
728
- *
729
- * This method performs a plain `INSERT`.
730
- * If a unique constraint is violated, SQLite will throw an error.
731
- *
732
- * @param {string} tableName - Table name.
733
- * @param {Record<string, any>} data - Row to insert.
734
- * @returns {Promise<{ ok: true, lastInsertRowid: bigint|number, changes: number } | { error: string }>}
735
- */
736
- async insert(tableName, data) {
737
- try {
738
- if (!this.#models.has(tableName)) {
739
- return { error: `Table ${tableName} doesn't exist` };
740
- }
741
-
742
- this.#assertIdentifier(tableName, "table name");
743
-
744
- const columns = Object.keys(data);
745
- if (!columns.length) return { error: "Insert data cannot be empty" };
746
-
747
- columns.forEach((column) => this.#assertIdentifier(column, "column name"));
748
-
749
- const placeholders = columns.map(() => "?").join(", ");
750
- const sql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
751
-
752
- const result = this.#run(sql, Object.values(data));
753
-
754
- return { ok: true, ...result };
755
- } catch (error) {
756
- return { error: error.message };
757
- }
758
- }
759
-
760
- /**
761
- * Updates one or more rows matching a WHERE clause.
762
- *
763
- * This method performs a plain `UPDATE`.
764
- *
765
- * @param {string} tableName - Table name.
766
- * @param {Record<string, any>} data - Fields to update.
767
- * @param {Record<string, any>} where - Conditions used to select rows.
768
- * @returns {Promise<{ ok: true, changes: number } | { error: string }>}
769
- */
770
- async update(tableName, data, where) {
771
- try {
772
- if (!this.#models.has(tableName)) {
773
- return { error: `Table ${tableName} doesn't exist` };
774
- }
775
-
776
- if (!where || Object.keys(where).length === 0) {
777
- return { error: "Update requires a non-empty where clause" };
778
- }
779
-
780
- this.#assertIdentifier(tableName, "table name");
781
-
782
- const columns = Object.keys(data);
783
- if (!columns.length) return { error: "Update data cannot be empty" };
784
-
785
- columns.forEach((column) => this.#assertIdentifier(column, "column name"));
786
-
787
- const setClause = columns.map((column) => `${column} = ?`).join(", ");
788
- const whereClause = this.#buildWhere(where);
789
-
790
- const sql = `UPDATE ${tableName} SET ${setClause} ${whereClause.where}`;
791
- const result = this.#run(sql, [...Object.values(data), ...whereClause.params]);
792
-
793
- return { ok: true, changes: result.changes };
794
- } catch (error) {
795
- return { error: error.message };
796
- }
797
- }
798
-
799
- /**
800
- * Inserts or updates a row using SQLite `ON CONFLICT DO UPDATE`.
801
- *
802
- * This is safer than `INSERT OR REPLACE` because it does not delete
803
- * and recreate the row.
804
- *
805
- * @param {string} tableName - Table name.
806
- * @param {Record<string, any>} data - Row data.
807
- * @param {string[]} conflict - Columns that define the conflict target.
808
- * @returns {Promise<{ ok: true, changes: number, lastInsertRowid: bigint|number } | { error: string }>}
809
- *
810
- * @example
811
- * await db.upsert("UserAuthMethod", {
812
- * userId: 1,
813
- * provider: "google",
814
- * providerUserId: "123"
815
- * }, ["provider", "providerUserId"]);
816
- */
817
- async upsert(tableName, data, conflict = []) {
818
- try {
819
- if (!this.#models.has(tableName)) {
820
- return { error: `Table ${tableName} doesn't exist` };
821
- }
822
-
823
- this.#assertIdentifier(tableName, "table name");
824
-
825
- const columns = Object.keys(data);
826
- if (!columns.length) return { error: "Upsert data cannot be empty" };
827
- if (!conflict.length) return { error: "Upsert requires at least one conflict column" };
828
-
829
- columns.forEach((column) => this.#assertIdentifier(column, "column name"));
830
- conflict.forEach((column) => this.#assertIdentifier(column, "column name"));
831
-
832
- const placeholders = columns.map(() => "?").join(", ");
833
- const updateColumns = columns.filter((column) => !conflict.includes(column));
834
-
835
- const updateClause = updateColumns.length
836
- ? updateColumns.map((column) => `${column} = excluded.${column}`).join(", ")
837
- : `${conflict[0]} = ${conflict[0]}`;
838
-
839
- const sql = `
840
- INSERT INTO ${tableName} (${columns.join(", ")})
841
- VALUES (${placeholders})
842
- ON CONFLICT (${conflict.join(", ")})
843
- DO UPDATE SET ${updateClause}
844
- `.trim();
845
-
846
- const result = this.#run(sql, Object.values(data));
847
- return { ok: true, ...result };
848
- } catch (error) {
849
- return { error: error.message };
850
- }
851
- }
852
-
853
- /**
854
- * Backward-compatible helper that inserts or updates a row.
855
- *
856
- * Behavior:
857
- * - If `where` is provided, the existing row is loaded and merged with `data`,
858
- * then updated if found or inserted if not found.
859
- * - If `where` is not provided but `data.id` exists, lookup is performed on `id`.
860
- * - If no existing row is found, a new row is inserted.
861
- *
862
- * This method is intentionally simple, but for production-grade control,
863
- * prefer using `insert()`, `update()`, or `upsert()`.
864
- *
865
- * @param {string} tableName - Table name.
866
- * @param {Record<string, any>} data - Row data.
867
- * @param {Record<string, any>|null} [where=null] - Lookup condition.
868
- * @returns {Promise<{ ok: true } | { error: string }>}
869
- */
870
- async set(tableName, data, where = null) {
871
- try {
872
- if (!this.#models.has(tableName)) {
873
- return { error: `Table ${tableName} doesn't exist` };
874
- }
875
-
876
- const lookup = where ?? (data.id ? { id: data.id } : null);
877
-
878
- if (!lookup) {
879
- const inserted = await this.insert(tableName, data);
880
- if (inserted.error) return inserted;
881
- return { ok: true };
882
- }
883
-
884
- const existing = await this.get(tableName, lookup);
885
- if (existing?.error) return existing;
886
-
887
- if (existing) {
888
- const merged = { ...existing, ...data };
889
- const updated = await this.update(tableName, merged, lookup);
890
- if (updated.error) return updated;
891
- return { ok: true };
892
- }
893
-
894
- const inserted = await this.insert(tableName, data);
895
- if (inserted.error) return inserted;
896
-
897
- return { ok: true };
898
- } catch (error) {
899
- return { error: error.message };
900
- }
901
- }
902
-
903
- /**
904
- * Deletes rows matching a WHERE clause.
905
- *
906
- * @param {string} tableName - Table name.
907
- * @param {Record<string, any>} where - Conditions.
908
- * @returns {Promise<{ ok: true, changes: number } | { error: string }>}
909
- */
910
- async delete(tableName, where) {
911
- try {
912
- if (!this.#models.has(tableName)) {
913
- return { error: `Table ${tableName} doesn't exist` };
914
- }
915
-
916
- if (!where || Object.keys(where).length === 0) {
917
- return { error: "Delete requires a non-empty where clause" };
918
- }
919
-
920
- this.#assertIdentifier(tableName, "table name");
921
-
922
- const whereClause = this.#buildWhere(where);
923
- const sql = `DELETE FROM ${tableName} ${whereClause.where}`;
924
- const result = this.#run(sql, whereClause.params);
925
-
926
- return { ok: true, changes: result.changes };
927
- } catch (error) {
928
- return { error: error.message };
929
- }
930
- }
931
-
932
- /**
933
- * Counts rows matching a WHERE clause.
934
- *
935
- * @param {string} tableName - Table name.
936
- * @param {Record<string, any>} [where={}] - Conditions.
937
- * @returns {Promise<number | { error: string }>}
938
- */
939
- async count(tableName, where = {}) {
940
- try {
941
- if (!this.#models.has(tableName)) {
942
- return { error: `Table ${tableName} doesn't exist` };
943
- }
944
-
945
- this.#assertIdentifier(tableName, "table name");
946
-
947
- const whereClause = this.#buildWhere(where);
948
- const sql = `SELECT COUNT(*) as total FROM ${tableName} ${whereClause.where}`;
949
- const stmt = this.#$.prepare(sql);
950
- const result = stmt.get(...whereClause.params);
951
-
952
- return result.total;
953
- } catch (error) {
954
- return { error: error.message };
955
- }
956
- }
957
-
958
- /**
959
- * Runs multiple operations inside a transaction.
960
- *
961
- * If the callback throws, the transaction is rolled back.
962
- * If the callback completes successfully, the transaction is committed.
963
- *
964
- * The callback may be synchronous or asynchronous.
965
- *
966
- * @template T
967
- * @param {() => T|Promise<T>} callback - Function to execute inside the transaction.
968
- * @returns {Promise<T | { error: string }>}
969
- *
970
- * @example
971
- * const result = await db.transaction(async () => {
972
- * await db.insert("User", { email: "a@b.com", createdAt: Date.now() });
973
- * await db.insert("Profile", { userId: 1, nickname: "John" });
974
- * return { ok: true };
975
- * });
976
- */
977
- async transaction(callback) {
978
- try {
979
- this.#run("BEGIN");
980
-
981
- const result = await callback();
982
-
983
- this.#run("COMMIT");
984
- return result;
985
- } catch (error) {
986
- try {
987
- this.#run("ROLLBACK");
988
- } catch {}
989
-
990
- return { error: error.message };
991
- }
992
- }
993
-
994
- /**
995
- * Closes the underlying SQLite connection.
996
- *
997
- * @example
998
- * db.close();
999
- */
1000
- close() {
1001
- if (this.#$ && typeof this.#$.close === "function") {
1002
- this.#$.close();
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;