@ix-xs/node-comfort 2.0.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 (77) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +32 -30
  3. package/index.js +3 -3
  4. package/package.json +8 -7
  5. package/src/Arr.js +222 -58
  6. package/src/Async.js +322 -90
  7. package/src/Cache.js +148 -44
  8. package/src/Checker.js +257 -26
  9. package/src/Cli.js +404 -145
  10. package/src/Color.js +224 -83
  11. package/src/Crypto.js +481 -106
  12. package/src/Emitter.js +104 -37
  13. package/src/Env.js +176 -42
  14. package/src/FS.js +203 -60
  15. package/src/Func.js +226 -76
  16. package/src/Http.js +311 -149
  17. package/src/Id.js +281 -96
  18. package/src/Logger.js +302 -78
  19. package/src/Num.js +182 -60
  20. package/src/Obj.js +340 -125
  21. package/src/SQLite.js +426 -105
  22. package/src/Schema.js +602 -87
  23. package/src/Str.js +316 -86
  24. package/src/Sys.js +197 -54
  25. package/src/Time.js +572 -317
  26. package/src/Utils.js +62 -41
  27. package/src/errors.js +54 -11
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +59 -17
  30. package/types/src/Arr.d.ts +78 -0
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +52 -1
  33. package/types/src/Async.d.ts.map +1 -1
  34. package/types/src/Cache.d.ts +33 -7
  35. package/types/src/Cache.d.ts.map +1 -1
  36. package/types/src/Checker.d.ts +155 -2
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +97 -15
  39. package/types/src/Cli.d.ts.map +1 -1
  40. package/types/src/Color.d.ts +7 -1
  41. package/types/src/Color.d.ts.map +1 -1
  42. package/types/src/Crypto.d.ts +99 -13
  43. package/types/src/Crypto.d.ts.map +1 -1
  44. package/types/src/Emitter.d.ts +36 -17
  45. package/types/src/Emitter.d.ts.map +1 -1
  46. package/types/src/Env.d.ts +31 -1
  47. package/types/src/Env.d.ts.map +1 -1
  48. package/types/src/FS.d.ts +36 -5
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +147 -19
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +113 -43
  53. package/types/src/Http.d.ts.map +1 -1
  54. package/types/src/Id.d.ts +60 -8
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +53 -2
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +48 -7
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +67 -4
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +123 -14
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +256 -7
  65. package/types/src/Schema.d.ts.map +1 -1
  66. package/types/src/Str.d.ts +49 -1
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +51 -6
  69. package/types/src/Sys.d.ts.map +1 -1
  70. package/types/src/Time.d.ts +130 -34
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +15 -11
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +27 -3
  75. package/types/src/errors.d.ts.map +1 -1
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
package/src/SQLite.js CHANGED
@@ -24,6 +24,7 @@
24
24
  const nodePath = require("node:path");
25
25
  const fs = require("node:fs");
26
26
  const { SQLiteError } = require("./errors.js");
27
+ const { toMs } = require("./internal/shared.js");
27
28
 
28
29
  /**
29
30
  * A column type. `JSON` and `BOOLEAN` columns give you back objects and
@@ -44,7 +45,8 @@ const { SQLiteError } = require("./errors.js");
44
45
  * @property {boolean} [autoincrement] With an `INTEGER` primary key: ids are never reused.
45
46
  * @property {boolean} [notNull]
46
47
  * @property {boolean} [unique]
47
- * @property {unknown} [defaultValue] Objects work for JSON columns, booleans for BOOLEAN ones.
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.
48
50
  * @property {unknown[]} [values] Allowed values.
49
51
  * @property {string} [check] A `CHECK` expression, like `"price >= 0"`.
50
52
  * @property {{ table: string, column: string }} [references] Foreign key target.
@@ -99,12 +101,16 @@ const { SQLiteError } = require("./errors.js");
99
101
  * `IS NULL`, an array means "one of", and an object uses operators. Keys
100
102
  * combine with AND; use `$or` and `$and` for the rest.
101
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
+ *
102
107
  * @example
103
108
  * { status: "active", age: { gte: 18, lt: 65 }, role: ["admin", "editor"], deleted_at: null }
104
109
  * { $or: [{ email: { like: "%@acme.com" } }, { vip: true }] }
110
+ * { "settings.theme": "dark", "settings.beta": true }
105
111
  *
106
112
  * @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
113
+ * @typedef {{ [K in keyof R]?: WhereValue } & { [path: `${string}.${string}`]: WhereValue } & { $or?: Where<R>[], $and?: Where<R>[] }} Where
108
114
  */
109
115
 
110
116
  /**
@@ -135,7 +141,8 @@ const { SQLiteError } = require("./errors.js");
135
141
  * @property {boolean} [dates] Return `DATE`, `DATETIME` and `TIMESTAMP` columns as `Date`s.
136
142
  * @property {boolean} [wal] Write-ahead logging: faster, and reads don't wait for writes. Defaults to `true`.
137
143
  * @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`.
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"`.
139
146
  * @property {boolean} [readOnly]
140
147
  */
141
148
 
@@ -172,9 +179,11 @@ const { SQLiteError } = require("./errors.js");
172
179
  * @property {string} name
173
180
  * @property {(where?: Where<R>, options?: Pick<QueryOptions, "columns" | "orderBy" | "direction">) => R | undefined} get The first matching row.
174
181
  * @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.
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.
177
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.
178
187
  * @property {(data: Partial<R>, conflict: string[]) => InsertResult} upsert Inserts, or updates on conflict.
179
188
  * @property {(where: Where<R>) => ChangeResult} delete Deletes matching rows.
180
189
  * @property {(where?: Where<R>) => number} count Counts matching rows.
@@ -182,7 +191,44 @@ const { SQLiteError } = require("./errors.js");
182
191
  * @property {() => { ok: true }} clear Deletes every row.
183
192
  */
184
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
+
185
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;
186
232
  const OPERATORS = new Set(["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "like", "notLike", "glob", "between", "isNull"]);
187
233
  const COMPARATORS = /** @type {Record<string, string>} */ ({ gt: ">", gte: ">=", lt: "<", lte: "<=", like: "LIKE", notLike: "NOT LIKE", glob: "GLOB" });
188
234
  const ACTIONS = ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"];
@@ -291,6 +337,76 @@ const _wrap = (error, sql) => {
291
337
  return new SQLiteError(sql ? `${message}\n in: ${sql.replace(/\s+/g, " ").trim()}` : message, { sql, sqliteCode, cause: error });
292
338
  };
293
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
+
294
410
  /**
295
411
  * Removes quoted strings and names, to look for `;` safely.
296
412
  * @param {string} sql
@@ -308,8 +424,15 @@ class SQLite {
308
424
  #db;
309
425
  /** @type {Map<string, import("node:sqlite").StatementSync>} */
310
426
  #statements = new Map();
311
- /** @type {Map<string, Map<string, string>>} */
312
- #columnTypes = 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();
313
436
  /** @type {{ json: boolean, booleans: boolean, dates: boolean }} */
314
437
  #read;
315
438
  #depth = 0;
@@ -337,8 +460,13 @@ class SQLite {
337
460
  if (!memory && !options.readOnly) fs.mkdirSync(nodePath.dirname(target), { recursive: true });
338
461
  try {
339
462
  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)}`);
463
+ this.#db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(toMs(options.busyTimeout ?? 5000, "busyTimeout")) || 0)}`);
341
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
+ }
342
470
  this.#db.exec(`PRAGMA foreign_keys = ${options.foreignKeys === false ? "OFF" : "ON"}`);
343
471
  } catch (error) {
344
472
  throw _wrap(error);
@@ -348,6 +476,7 @@ class SQLite {
348
476
  }
349
477
 
350
478
  /**
479
+ * A prepared statement, kept for the next call.
351
480
  * @param {string} sql
352
481
  * @returns {import("node:sqlite").StatementSync}
353
482
  */
@@ -364,6 +493,118 @@ class SQLite {
364
493
  return stmt;
365
494
  }
366
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
+
367
608
  /**
368
609
  * @param {unknown} params
369
610
  * @returns {any[]}
@@ -376,60 +617,34 @@ class SQLite {
376
617
  }
377
618
 
378
619
  /**
620
+ * Forgets what was learned about the schema, after it changed.
379
621
  * @returns {void}
380
622
  */
381
623
  #invalidate() {
382
- this.#columnTypes.clear();
624
+ this.#columnKinds.clear();
383
625
  this.#statements.clear();
626
+ this.#readers.clear();
627
+ this.#known.clear();
628
+ this.#sources.clear();
384
629
  }
385
630
 
386
631
  /**
387
- * Declared column types of a table, by column.
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.
388
634
  * @param {string} table
389
- * @returns {Map<string, string>}
635
+ * @returns {Map<string, number>}
390
636
  */
391
- #types(table) {
392
- let types = this.#columnTypes.get(table);
393
- if (!types) {
394
- types = new Map();
637
+ #kinds(table) {
638
+ let kinds = this.#columnKinds.get(table);
639
+ if (!kinds) {
640
+ kinds = new Map();
395
641
  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;
642
+ const kind = _kindOf(String(col.type).toUpperCase(), this.#read);
643
+ if (kind) kinds.set(col.name, kind);
430
644
  }
645
+ this.#columnKinds.set(table, kinds);
431
646
  }
432
- return row;
647
+ return kinds;
433
648
  }
434
649
 
435
650
  /**
@@ -456,7 +671,7 @@ class SQLite {
456
671
  parts.push(subs.length ? `(${subs.map((s) => `(${s})`).join(key === "$or" ? " OR " : " AND ")})` : key === "$or" ? "0" : "1");
457
672
  continue;
458
673
  }
459
- const col = _id(key, "column name");
674
+ const col = _column(key);
460
675
  if (value === null) parts.push(`${col} IS NULL`);
461
676
  else if (Array.isArray(value)) parts.push(this.#op(col, "in", value, params));
462
677
  else if (_isPlain(value) && Object.keys(value).some((k) => OPERATORS.has(k))) {
@@ -531,8 +746,8 @@ class SQLite {
531
746
  if (upper !== "ASC" && upper !== "DESC") throw new SQLiteError(`Invalid sort direction "${String(direction)}" (use "ASC" or "DESC")`);
532
747
  return upper;
533
748
  };
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(", ")}`;
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(", ")}`;
536
751
  }
537
752
 
538
753
  /**
@@ -552,7 +767,10 @@ class SQLite {
552
767
  */
553
768
  #table(table) {
554
769
  const quoted = _id(table, "table name");
555
- if (!this.hasTable(table)) throw new SQLiteError(`Table "${table}" does not exist`);
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
+ }
556
774
  return quoted;
557
775
  }
558
776
 
@@ -577,9 +795,17 @@ class SQLite {
577
795
  * @returns {string | undefined}
578
796
  */
579
797
  #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;
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;
583
809
  }
584
810
 
585
811
  /**
@@ -617,8 +843,9 @@ class SQLite {
617
843
  }
618
844
 
619
845
  /**
620
- * Runs a query and returns the first row. When it reads a single table,
621
- * JSON and boolean columns are converted.
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.
622
849
  *
623
850
  * @example
624
851
  * const user = db.queryOne("SELECT * FROM users WHERE email = ?", [email]);
@@ -630,12 +857,7 @@ class SQLite {
630
857
  * @throws {SQLiteError}
631
858
  */
632
859
  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
- }
860
+ return this.#one(sql, this.#params(params));
639
861
  }
640
862
 
641
863
  /**
@@ -651,13 +873,7 @@ class SQLite {
651
873
  * @throws {SQLiteError}
652
874
  */
653
875
  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
- }
876
+ return this.#all(sql, this.#params(params));
661
877
  }
662
878
 
663
879
  /**
@@ -676,14 +892,9 @@ class SQLite {
676
892
  * @throws {SQLiteError}
677
893
  */
678
894
  *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);
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);
687
898
  }
688
899
 
689
900
  /**
@@ -718,7 +929,11 @@ class SQLite {
718
929
  if (def.primaryKey) sql += ` PRIMARY KEY${def.autoincrement ? " AUTOINCREMENT" : ""}`;
719
930
  if (def.notNull) sql += " NOT NULL";
720
931
  if (def.unique) sql += " UNIQUE";
721
- if (def.defaultValue !== undefined) sql += ` DEFAULT ${_literal(def.defaultValue)}`;
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
+ }
722
937
  if (def.values?.length) sql += ` CHECK (${col} IN (${def.values.map(_literal).join(", ")}))`;
723
938
  if (def.check) sql += ` CHECK (${def.check})`;
724
939
  if (def.references) {
@@ -825,7 +1040,7 @@ class SQLite {
825
1040
  */
826
1041
  hasTable(tableName) {
827
1042
  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;
1043
+ return this.#prepare(TABLE_EXISTS).get(tableName) !== undefined;
829
1044
  }
830
1045
 
831
1046
  /**
@@ -873,7 +1088,8 @@ class SQLite {
873
1088
  * @throws {SQLiteError}
874
1089
  */
875
1090
  get(tableName, where = {}, options = {}) {
876
- return /** @type {T | undefined} */ (this.getAll(tableName, where, { ...options, limit: 1, offset: 0 })[0]);
1091
+ const query = this.#select(tableName, where, options, true);
1092
+ return this.#one(query.sql, query.params);
877
1093
  }
878
1094
 
879
1095
  /**
@@ -896,21 +1112,56 @@ class SQLite {
896
1112
  * @throws {SQLiteError}
897
1113
  */
898
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) {
899
1127
  const table = this.#table(tableName);
900
1128
  const columns = options.columns?.length ? options.columns.map((c) => _id(c, "column name")).join(", ") : "*";
901
1129
  const clause = this.#where(where);
902
1130
  let sql = `SELECT ${columns} FROM ${table}${clause.sql}${this.#orderBy(options)}`;
903
- const params = clause.params;
904
- if (options.limit !== undefined || options.offset !== undefined) {
1131
+ if (single) {
1132
+ sql += " LIMIT 1";
1133
+ } else if (options.limit !== undefined || options.offset !== undefined) {
905
1134
  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);
1135
+ clause.params.push(options.limit === undefined ? -1 : Math.max(0, Math.floor(options.limit)), Math.max(0, Math.floor(options.offset ?? 0)));
913
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 };
914
1165
  }
915
1166
 
916
1167
  /**
@@ -964,17 +1215,19 @@ class SQLite {
964
1215
  *
965
1216
  * @example
966
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
967
1219
  *
968
1220
  * @param {string} tableName
969
1221
  * @param {Partial<R> & Record<string, unknown>} data
1222
+ * @param {InsertOptions} [options]
970
1223
  * @returns {InsertResult}
971
1224
  * @throws {SQLiteError} On a constraint failure; `sqliteCode` tells you which, like `"SQLITE_CONSTRAINT_UNIQUE"`.
972
1225
  */
973
- insert(tableName, data) {
1226
+ insert(tableName, data, options) {
974
1227
  const table = this.#table(tableName);
975
1228
  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);
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);
978
1231
  }
979
1232
 
980
1233
  /**
@@ -983,17 +1236,47 @@ class SQLite {
983
1236
  *
984
1237
  * @example
985
1238
  * db.insertMany("products", rowsFromCsv);
1239
+ * db.insertMany("tags", labels.map((label) => ({ label })), { onConflict: "ignore" });
986
1240
  *
987
1241
  * @param {string} tableName
988
- * @param {Array<Partial<R> & Record<string, unknown>>} rows
1242
+ * @param {Iterable<Partial<R> & Record<string, unknown>>} rows
1243
+ * @param {InsertOptions} [options]
989
1244
  * @returns {ChangeResult}
990
1245
  * @throws {SQLiteError} If a row fails; nothing is inserted then.
991
1246
  */
992
- insertMany(tableName, rows) {
993
- this.#table(tableName);
1247
+ insertMany(tableName, rows, options) {
1248
+ const table = this.#table(tableName);
1249
+ const verb = _insertVerb(options);
994
1250
  return this.transaction(() => {
995
1251
  let changes = 0;
996
- for (const row of rows) changes += this.insert(tableName, row).changes;
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
+ }
997
1280
  return { ok: /** @type {const} */ (true), changes };
998
1281
  });
999
1282
  }
@@ -1086,6 +1369,38 @@ class SQLite {
1086
1369
  return { ok: true, changes };
1087
1370
  }
1088
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
+
1089
1404
  /**
1090
1405
  * Shortcuts bound to one table, so you don't repeat its name.
1091
1406
  *
@@ -1104,9 +1419,11 @@ class SQLite {
1104
1419
  name: tableName,
1105
1420
  get: (where, options) => this.get(tableName, where, options),
1106
1421
  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)),
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),
1109
1425
  update: (data, where) => this.update(tableName, /** @type {any} */ (data), where),
1426
+ increment: (amounts, where) => this.increment(tableName, /** @type {any} */ (amounts), where),
1110
1427
  upsert: (data, conflict) => this.upsert(tableName, /** @type {any} */ (data), conflict),
1111
1428
  delete: (where) => this.delete(tableName, where),
1112
1429
  count: (where) => this.count(tableName, where),
@@ -1122,18 +1439,19 @@ class SQLite {
1122
1439
  *
1123
1440
  * @example
1124
1441
  * db.transaction(() => {
1125
- * db.update("accounts", { balance: from.balance - amount }, { id: from.id });
1126
- * db.update("accounts", { balance: to.balance + amount }, { id: to.id });
1442
+ * db.increment("accounts", { balance: -amount }, { id: from.id });
1443
+ * db.increment("accounts", { balance: amount }, { id: to.id });
1127
1444
  * });
1128
1445
  *
1129
1446
  * @template T
1130
1447
  * @param {() => T} callback Can be async; the commit then waits for it.
1448
+ * @param {TransactionOptions} [options]
1131
1449
  * @returns {T}
1132
1450
  * @throws Whatever `callback` threw, after rolling back.
1133
1451
  */
1134
- transaction(callback) {
1452
+ transaction(callback, options) {
1135
1453
  const savepoint = this.#depth > 0 ? `nc_savepoint_${this.#depth}` : null;
1136
- this.#db.exec(savepoint ? `SAVEPOINT ${savepoint}` : "BEGIN");
1454
+ this.#db.exec(savepoint ? `SAVEPOINT ${savepoint}` : options?.immediate ? "BEGIN IMMEDIATE" : "BEGIN");
1137
1455
  this.#depth++;
1138
1456
  const commit = () => {
1139
1457
  this.#depth--;
@@ -1141,6 +1459,8 @@ class SQLite {
1141
1459
  };
1142
1460
  const rollback = () => {
1143
1461
  this.#depth--;
1462
+ // A table created in there is gone again.
1463
+ this.#known.clear();
1144
1464
  try {
1145
1465
  this.#db.exec(savepoint ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : "ROLLBACK");
1146
1466
  } catch {
@@ -1291,6 +1611,7 @@ class SQLite {
1291
1611
  */
1292
1612
  close() {
1293
1613
  this.#statements.clear();
1614
+ this.#readers.clear();
1294
1615
  if (!this.#open) return;
1295
1616
  this.#open = false;
1296
1617
  this.#db.close();