@ix-xs/node-comfort 1.2.0 → 2.0.0

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