@ix-xs/node-comfort 2.1.1 → 2.2.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 (68) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +2 -2
  3. package/package.json +2 -2
  4. package/src/Arr.js +13 -5
  5. package/src/Checker.js +56 -8
  6. package/src/Cli.js +60 -18
  7. package/src/Color.js +32 -6
  8. package/src/Crypto.js +429 -78
  9. package/src/Emitter.js +9 -9
  10. package/src/Env.js +90 -8
  11. package/src/FS.js +366 -56
  12. package/src/Http.js +263 -72
  13. package/src/Id.js +53 -15
  14. package/src/Logger.js +100 -22
  15. package/src/Num.js +2 -1
  16. package/src/Obj.js +225 -36
  17. package/src/SQLite.js +154 -97
  18. package/src/Schema.js +281 -40
  19. package/src/Str.js +77 -29
  20. package/src/Sys.js +123 -22
  21. package/src/Time.js +96 -22
  22. package/src/Utils.js +18 -3
  23. package/src/errors.js +75 -4
  24. package/src/internal/shared.js +68 -14
  25. package/src/internal/terminal.js +49 -0
  26. package/types/index.d.ts +28 -5
  27. package/types/src/Arr.d.ts +4 -2
  28. package/types/src/Arr.d.ts.map +1 -1
  29. package/types/src/Checker.d.ts +29 -3
  30. package/types/src/Checker.d.ts.map +1 -1
  31. package/types/src/Cli.d.ts +6 -4
  32. package/types/src/Cli.d.ts.map +1 -1
  33. package/types/src/Color.d.ts +2 -2
  34. package/types/src/Color.d.ts.map +1 -1
  35. package/types/src/Crypto.d.ts +126 -28
  36. package/types/src/Crypto.d.ts.map +1 -1
  37. package/types/src/Emitter.d.ts +6 -6
  38. package/types/src/Emitter.d.ts.map +1 -1
  39. package/types/src/Env.d.ts +19 -1
  40. package/types/src/Env.d.ts.map +1 -1
  41. package/types/src/FS.d.ts +27 -20
  42. package/types/src/FS.d.ts.map +1 -1
  43. package/types/src/Http.d.ts +42 -5
  44. package/types/src/Http.d.ts.map +1 -1
  45. package/types/src/Id.d.ts.map +1 -1
  46. package/types/src/Logger.d.ts +11 -3
  47. package/types/src/Logger.d.ts.map +1 -1
  48. package/types/src/Num.d.ts.map +1 -1
  49. package/types/src/Obj.d.ts +15 -2
  50. package/types/src/Obj.d.ts.map +1 -1
  51. package/types/src/SQLite.d.ts +104 -12
  52. package/types/src/SQLite.d.ts.map +1 -1
  53. package/types/src/Schema.d.ts +61 -5
  54. package/types/src/Schema.d.ts.map +1 -1
  55. package/types/src/Str.d.ts +32 -7
  56. package/types/src/Str.d.ts.map +1 -1
  57. package/types/src/Sys.d.ts +42 -9
  58. package/types/src/Sys.d.ts.map +1 -1
  59. package/types/src/Time.d.ts +10 -5
  60. package/types/src/Time.d.ts.map +1 -1
  61. package/types/src/Utils.d.ts +16 -6
  62. package/types/src/Utils.d.ts.map +1 -1
  63. package/types/src/errors.d.ts +6 -2
  64. package/types/src/errors.d.ts.map +1 -1
  65. package/types/src/internal/shared.d.ts +7 -0
  66. package/types/src/internal/shared.d.ts.map +1 -1
  67. package/types/src/internal/terminal.d.ts +13 -0
  68. package/types/src/internal/terminal.d.ts.map +1 -0
package/src/SQLite.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * SQLite made pleasant, on the engine built into Node.js (22.13+): CRUD with
4
+ * SQLite made pleasant, on the engine built into Node.js: CRUD with
5
5
  * rich filters, JSON and boolean columns, transactions, migrations and
6
6
  * backups. Methods are synchronous, like the engine, but `await` still
7
7
  * works. Errors are `SQLiteError`s. Values are always bound as parameters
@@ -19,6 +19,10 @@
19
19
  * });
20
20
  * db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
21
21
  * db.getAll("users", { email: { like: "%@example.com" } }, { orderBy: "email", limit: 20 });
22
+ *
23
+ * // Values from a request can't turn into operators:
24
+ * const safe = new nc.SQLite("data/app.sqlite", { operators: "tagged" });
25
+ * safe.getAll("users", { age: nc.SQLite.op.gte(18), role: nc.SQLite.op.in(["admin", "editor"]) });
22
26
  */
23
27
 
24
28
  const nodePath = require("node:path");
@@ -48,14 +52,15 @@ const { toMs } = require("./internal/shared.js");
48
52
  * @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
53
  * @property {string} [defaultExpression] A default computed by SQLite, like `"(unixepoch())"` or `"(lower(hex(randomblob(16))))"`. Written into the table as is: never build it from user input.
50
54
  * @property {unknown[]} [values] Allowed values.
51
- * @property {string} [check] A `CHECK` expression, like `"price >= 0"`.
55
+ * @property {string} [check] A `CHECK` expression, like `"price >= 0"`. Written into the table as is: never build it from user input.
52
56
  * @property {{ table: string, column: string }} [references] Foreign key target.
53
57
  * @property {ForeignKeyAction} [onDelete] When the referenced row is deleted.
54
58
  * @property {ForeignKeyAction} [onUpdate] When the referenced key changes.
55
59
  */
56
60
 
57
61
  /**
58
- * A constraint on several columns.
62
+ * A constraint on several columns. The `expression` of a `check` is written
63
+ * into the table as is: never build it from user input.
59
64
  * @typedef {{ type: "unique", columns: string[] } | { type: "primaryKey", columns: string[] } | { type: "check", expression: string } | { type: "foreignKey", columns: string[], references: { table: string, columns: string[] }, onDelete?: ForeignKeyAction, onUpdate?: ForeignKeyAction }} TableConstraint
60
65
  */
61
66
 
@@ -65,7 +70,7 @@ const { toMs } = require("./internal/shared.js");
65
70
  * @property {string[]} columns Several columns make a composite index.
66
71
  * @property {boolean} [unique]
67
72
  * @property {string} [name] Defaults to `idx_<table>_<columns>`.
68
- * @property {string} [where] Makes it a partial index, like `"deleted_at IS NULL"`.
73
+ * @property {string} [where] Makes it a partial index, like `"deleted_at IS NULL"`. Written into the index as is: never build it from user input.
69
74
  */
70
75
 
71
76
  /**
@@ -101,6 +106,13 @@ const { toMs } = require("./internal/shared.js");
101
106
  * `IS NULL`, an array means "one of", and an object uses operators. Keys
102
107
  * combine with AND; use `$or` and `$and` for the rest.
103
108
  *
109
+ * A value taken from a request must be checked first (parsed JSON like
110
+ * `{ "ne": "" }` would act as an operator), or the database opened with
111
+ * `operators: "tagged"`, where only `SQLite.op` makes operators.
112
+ *
113
+ * On a read, a missing value drops its condition: check required values
114
+ * first (writes, and reads with `operators: "tagged"`, throw instead).
115
+ *
104
116
  * A key with dots looks inside a JSON column: `"settings.theme"` is the
105
117
  * `theme` key of the `settings` column, `"tags.0"` the first item of `tags`.
106
118
  *
@@ -108,6 +120,7 @@ const { toMs } = require("./internal/shared.js");
108
120
  * { status: "active", age: { gte: 18, lt: 65 }, role: ["admin", "editor"], deleted_at: null }
109
121
  * { $or: [{ email: { like: "%@acme.com" } }, { vip: true }] }
110
122
  * { "settings.theme": "dark", "settings.beta": true }
123
+ * { age: nc.SQLite.op.gte(18), role: nc.SQLite.op.in(["admin", "editor"]) }
111
124
  *
112
125
  * @template [R=Record<string, any>] Row type; its keys are suggested as columns.
113
126
  * @typedef {{ [K in keyof R]?: WhereValue } & { [path: `${string}.${string}`]: WhereValue } & { $or?: Where<R>[], $and?: Where<R>[] }} Where
@@ -115,7 +128,33 @@ const { toMs } = require("./internal/shared.js");
115
128
 
116
129
  /**
117
130
  * The condition for one column: a value, `null`, an array, or operators.
118
- * @typedef {null | string | number | bigint | boolean | Date | Uint8Array | readonly unknown[] | WhereOperators} WhereValue
131
+ * @typedef {null | string | number | bigint | boolean | Date | Uint8Array | readonly unknown[] | WhereOperators | TaggedOperator} WhereValue
132
+ */
133
+
134
+ /**
135
+ * An operator made by `SQLite.op`. It carries a mark that JSON can't
136
+ * produce, so it's an operator with `operators: "tagged"` too. Several can
137
+ * be spread into one object: `{ ...op.gte(18), ...op.lt(65) }`.
138
+ * @typedef {Readonly<WhereOperators>} TaggedOperator
139
+ */
140
+
141
+ /**
142
+ * The helpers of `SQLite.op`, one per filter operator.
143
+ * @typedef {object} OperatorHelpers
144
+ * @property {(value: unknown) => TaggedOperator} eq Equal. `null` means `IS NULL`.
145
+ * @property {(value: unknown) => TaggedOperator} ne Not equal. Rows where the column is `NULL` match too.
146
+ * @property {(value: number | string | bigint | Date) => TaggedOperator} gt Greater than.
147
+ * @property {(value: number | string | bigint | Date) => TaggedOperator} gte Greater than or equal.
148
+ * @property {(value: number | string | bigint | Date) => TaggedOperator} lt Less than.
149
+ * @property {(value: number | string | bigint | Date) => TaggedOperator} lte Less than or equal.
150
+ * @property {(values: readonly unknown[]) => TaggedOperator} in One of these values.
151
+ * @property {(values: readonly unknown[]) => TaggedOperator} notIn None of these values.
152
+ * @property {(pattern: string) => TaggedOperator} like A `LIKE` pattern: `%` any text, `_` one character.
153
+ * @property {(pattern: string) => TaggedOperator} notLike
154
+ * @property {(pattern: string) => TaggedOperator} glob A case-sensitive pattern: `*` any text, `?` one character.
155
+ * @property {(min: unknown, max: unknown) => TaggedOperator} between From `min` to `max`, both included.
156
+ * @property {() => TaggedOperator} isNull `IS NULL`.
157
+ * @property {() => TaggedOperator} notNull `IS NOT NULL`.
119
158
  */
120
159
 
121
160
  /**
@@ -144,6 +183,7 @@ const { toMs } = require("./internal/shared.js");
144
183
  * @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
184
  * @property {"OFF" | "NORMAL" | "FULL" | "EXTRA"} [synchronous] How hard SQLite pushes each commit to the disk. With `wal`, `"NORMAL"` writes several times faster and can't corrupt the file; a power cut may undo the last commits. Defaults to SQLite's own setting, `"FULL"`.
146
185
  * @property {boolean} [readOnly]
186
+ * @property {"plain" | "tagged"} [operators] What makes an object in a filter an operator. `"plain"`, the default: any object with an operator key, like `{ gte: 18 }`, and an array means "one of". `"tagged"`: only the operators made by `SQLite.op`, any other object or array is a value compared as JSON text, so a value taken from a request can never become an operator.
147
187
  */
148
188
 
149
189
  /**
@@ -215,7 +255,7 @@ const { toMs } = require("./internal/shared.js");
215
255
 
216
256
  /**
217
257
  * 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
258
+ * @typedef {{ stmt: import("node:sqlite").StatementSync, names: string[], kinds: Uint8Array | undefined }} _Reader
219
259
  */
220
260
 
221
261
  const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -229,6 +269,8 @@ const SOURCE_EXISTS = EXISTS("'table', 'view'");
229
269
  const JSON_KIND = 1;
230
270
  const BOOLEAN_KIND = 2;
231
271
  const DATE_KIND = 3;
272
+ // Marks the operators made by SQLite.op. JSON can't produce a symbol key, so a parsed request never carries it.
273
+ const TAGGED = Symbol("nc.SQLite.op");
232
274
  const OPERATORS = new Set(["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "like", "notLike", "glob", "between", "isNull"]);
233
275
  const COMPARATORS = /** @type {Record<string, string>} */ ({ gt: ">", gte: ">=", lt: "<", lte: "<=", like: "LIKE", notLike: "NOT LIKE", glob: "GLOB" });
234
276
  const ACTIONS = ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"];
@@ -268,7 +310,7 @@ const _engine = () => {
268
310
  try {
269
311
  _DatabaseSync = require("node:sqlite").DatabaseSync;
270
312
  } catch (error) {
271
- throw new SQLiteError(`node:sqlite is not available in Node.js ${process.version}: nc.SQLite requires Node.js 22.13 or newer`, { cause: error });
313
+ throw new SQLiteError(`node:sqlite is not available in Node.js ${process.version}: nc.SQLite requires Node.js 22.23.4, 24.22.1, 26.12.1 or newer`, { cause: error });
272
314
  } finally {
273
315
  process.emitWarning = original;
274
316
  }
@@ -407,6 +449,31 @@ const _insertVerb = (options) => {
407
449
  return mode === "ignore" ? "INSERT OR IGNORE" : "INSERT OR REPLACE";
408
450
  };
409
451
 
452
+ /**
453
+ * @param {string} name
454
+ * @param {unknown} operand
455
+ * @returns {TaggedOperator}
456
+ */
457
+ const _tag = (name, operand) => /** @type {TaggedOperator} */ (Object.freeze({ [name]: operand, [TAGGED]: true }));
458
+
459
+ /** @type {OperatorHelpers} */
460
+ const OP = Object.freeze({
461
+ eq: (value) => _tag("eq", value),
462
+ ne: (value) => _tag("ne", value),
463
+ gt: (value) => _tag("gt", value),
464
+ gte: (value) => _tag("gte", value),
465
+ lt: (value) => _tag("lt", value),
466
+ lte: (value) => _tag("lte", value),
467
+ in: (values) => _tag("in", values),
468
+ notIn: (values) => _tag("notIn", values),
469
+ like: (pattern) => _tag("like", pattern),
470
+ notLike: (pattern) => _tag("notLike", pattern),
471
+ glob: (pattern) => _tag("glob", pattern),
472
+ between: (min, max) => _tag("between", [min, max]),
473
+ isNull: () => _tag("isNull", true),
474
+ notNull: () => _tag("isNull", false),
475
+ });
476
+
410
477
  /**
411
478
  * Removes quoted strings and names, to look for `;` safely.
412
479
  * @param {string} sql
@@ -420,6 +487,23 @@ const _stripQuoted = (sql) => sql.replace(/'(?:''|[^'])*'|"(?:""|[^"])*"/g, "");
420
487
  * @template [R=Record<string, any>] The row type read methods return by default. Each call can override it.
421
488
  */
422
489
  class SQLite {
490
+ /**
491
+ * Filter operators that only code can make. Plain objects like
492
+ * `{ gte: 18 }` still work, unless the database was opened with
493
+ * `operators: "tagged"`: then only these are operators, and a value
494
+ * taken from a request stays a value.
495
+ *
496
+ * @example
497
+ * const { op } = nc.SQLite;
498
+ * const db = new nc.SQLite("data/app.sqlite", { operators: "tagged" });
499
+ * db.getAll("users", { age: op.between(18, 65), role: op.in(["admin", "editor"]), deleted_at: op.isNull() });
500
+ * // { "ne": "" } in the body is compared as text, not run, and a missing token throws
501
+ * db.get("users", { email, token: body.token });
502
+ *
503
+ * @type {OperatorHelpers}
504
+ */
505
+ static op = OP;
506
+
423
507
  /** @type {import("node:sqlite").DatabaseSync} */
424
508
  #db;
425
509
  /** @type {Map<string, import("node:sqlite").StatementSync>} */
@@ -429,14 +513,12 @@ class SQLite {
429
513
  // Tables and views known to exist, so that each call doesn't ask SQLite again.
430
514
  /** @type {Set<string>} */
431
515
  #known = new Set();
432
- /** @type {Map<string, Map<string, number>>} */
433
- #columnKinds = new Map();
434
- /** @type {Map<string, string | undefined>} */
435
- #sources = new Map();
436
516
  /** @type {{ json: boolean, booleans: boolean, dates: boolean }} */
437
517
  #read;
438
518
  #depth = 0;
439
519
  #open = true;
520
+ // With operators: "tagged", only SQLite.op makes operators.
521
+ #tagged = false;
440
522
  /** @type {string} */
441
523
  #path;
442
524
 
@@ -454,6 +536,9 @@ class SQLite {
454
536
  * @throws {SQLiteError} If the file can't be opened.
455
537
  */
456
538
  constructor(path = "db.sqlite", options = {}) {
539
+ const operators = options.operators ?? "plain";
540
+ if (operators !== "plain" && operators !== "tagged") throw new SQLiteError(`Invalid operators "${String(operators)}" (use "plain" or "tagged")`);
541
+ this.#tagged = operators === "tagged";
457
542
  const DatabaseSync = _engine();
458
543
  const memory = path === ":memory:" || path === "";
459
544
  const target = memory ? ":memory:" : nodePath.resolve(process.cwd(), path);
@@ -494,9 +579,9 @@ class SQLite {
494
579
  }
495
580
 
496
581
  /**
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.
582
+ * A prepared query. Rows are fetched as arrays and turned into objects here:
583
+ * it's faster than copying the objects Node returns, and Node says which
584
+ * column each value comes from.
500
585
  * @param {string} sql
501
586
  * @param {boolean} [keep=true] `false` for a statement used once, like an iteration.
502
587
  * @returns {_Reader}
@@ -510,14 +595,9 @@ class SQLite {
510
595
  } catch (error) {
511
596
  throw _wrap(error, sql);
512
597
  }
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
- }
598
+ stmt.setReturnArrays(true);
599
+ reader = { stmt, names: [], kinds: undefined };
600
+ this.#describe(reader);
521
601
  if (keep) {
522
602
  if (this.#readers.size >= 500) this.#readers.delete(/** @type {string} */ (this.#readers.keys().next().value));
523
603
  this.#readers.set(sql, reader);
@@ -530,8 +610,7 @@ class SQLite {
530
610
  * @param {_Reader} reader
531
611
  */
532
612
  #describe(reader) {
533
- /** @type {Array<{ name: string, type: string | null }>} */
534
- const columns = /** @type {any} */ (reader.stmt).columns();
613
+ const columns = reader.stmt.columns();
535
614
  reader.names = columns.map((column) => column.name);
536
615
  reader.kinds = undefined;
537
616
  for (let i = 0; i < columns.length; i++) {
@@ -549,16 +628,6 @@ class SQLite {
549
628
  #row(reader, raw) {
550
629
  /** @type {Record<string, unknown>} */
551
630
  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
631
  // A statement prepared before the table changed can come back with other columns.
563
632
  if (raw.length !== reader.names.length) this.#describe(reader);
564
633
  const names = reader.names;
@@ -621,71 +690,79 @@ class SQLite {
621
690
  * @returns {void}
622
691
  */
623
692
  #invalidate() {
624
- this.#columnKinds.clear();
625
693
  this.#statements.clear();
626
694
  this.#readers.clear();
627
695
  this.#known.clear();
628
- this.#sources.clear();
629
- }
630
-
631
- /**
632
- * The columns of a table that are converted when read. Only used on Node
633
- * versions that don't describe the columns of a query.
634
- * @param {string} table
635
- * @returns {Map<string, number>}
636
- */
637
- #kinds(table) {
638
- let kinds = this.#columnKinds.get(table);
639
- if (!kinds) {
640
- kinds = new Map();
641
- for (const col of /** @type {Array<{ name: string, type: string }>} */ (/** @type {unknown} */ (this.#prepare(`PRAGMA table_info(${_id(table, "table name")})`).all()))) {
642
- const kind = _kindOf(String(col.type).toUpperCase(), this.#read);
643
- if (kind) kinds.set(col.name, kind);
644
- }
645
- this.#columnKinds.set(table, kinds);
646
- }
647
- return kinds;
648
696
  }
649
697
 
650
698
  /**
651
699
  * @param {Where<any> | undefined} where
700
+ * @param {string} [write] The method, for a filter that changes rows: then no condition may be skipped, and the filter must not match every row.
701
+ * @param {string} [read] The method, for a read: with operators: "tagged", no condition may be skipped either.
652
702
  * @returns {{ sql: string, params: any[] }}
653
703
  */
654
- #where(where) {
704
+ #where(where, write, read) {
655
705
  if (!where || !Object.keys(where).length) return { sql: "", params: [] };
706
+ const tagged = this.#tagged;
707
+ // where a missing value throws rather than drops its condition (a token left out would let anyone in)
708
+ const strict = write ?? (tagged ? read ?? "read" : undefined);
656
709
  /** @type {any[]} */
657
710
  const params = [];
658
711
  /**
659
712
  * @param {Where<any>} filter
713
+ * @param {string} at Where this filter is, for error messages.
660
714
  * @returns {string}
661
715
  */
662
- const build = (filter) => {
716
+ const build = (filter, at) => {
663
717
  if (!_isPlain(filter)) throw new SQLiteError("A where filter must be a plain object");
664
718
  /** @type {string[]} */
665
719
  const parts = [];
666
720
  for (const [key, value] of Object.entries(filter)) {
667
- if (value === undefined) continue;
721
+ if (value === undefined) {
722
+ // a missing value would drop its condition, and the query would reach more rows
723
+ if (strict) throw new SQLiteError(`${strict}(): ${at}.${key} is undefined`);
724
+ continue;
725
+ }
668
726
  if (key === "$or" || key === "$and") {
669
727
  if (!Array.isArray(value)) throw new SQLiteError(`"${key}" expects an array of filters`);
670
- const subs = value.map((sub) => build(sub) || "1");
728
+ if (write) {
729
+ if (!value.length) throw new SQLiteError(`${write}(): ${at}.${key} is empty`);
730
+ const subs = value.map((sub, i) => {
731
+ const sql = build(sub, `${at}.${key}[${i}]`);
732
+ if (!sql) throw new SQLiteError(`${write}(): ${at}.${key}[${i}] matches every row`);
733
+ return `(${sql})`;
734
+ });
735
+ parts.push(`(${subs.join(key === "$or" ? " OR " : " AND ")})`);
736
+ continue;
737
+ }
738
+ const subs = value.map((sub, i) => build(sub, strict ? `${at}.${key}[${i}]` : at) || "1");
671
739
  parts.push(subs.length ? `(${subs.map((s) => `(${s})`).join(key === "$or" ? " OR " : " AND ")})` : key === "$or" ? "0" : "1");
672
740
  continue;
673
741
  }
674
742
  const col = _column(key);
675
743
  if (value === null) parts.push(`${col} IS NULL`);
676
- else if (Array.isArray(value)) parts.push(this.#op(col, "in", value, params));
677
- else if (_isPlain(value) && Object.keys(value).some((k) => OPERATORS.has(k))) {
744
+ else if (Array.isArray(value) && !tagged) parts.push(this.#op(col, "in", value, params));
745
+ else if (_isPlain(value) && (/** @type {any} */ (value)[TAGGED] === true || (!tagged && Object.keys(value).some((k) => OPERATORS.has(k))))) {
678
746
  // unknown keys next to operators are typos: #op() reports them
679
- for (const [op, operand] of Object.entries(value)) if (operand !== undefined) parts.push(this.#op(col, op, operand, params));
747
+ for (const [op, operand] of Object.entries(value)) {
748
+ if (operand === undefined) {
749
+ if (strict) throw new SQLiteError(`${strict}(): ${at}.${key}.${op} is undefined`);
750
+ continue;
751
+ }
752
+ const sql = this.#op(col, op, operand, params);
753
+ // "1" (an empty notIn) holds for every row: for a write, it's no condition at all
754
+ if (!write || sql !== "1") parts.push(sql);
755
+ }
680
756
  } else {
681
- // other objects and arrays compare as JSON text
757
+ // other objects and arrays (all of them with operators: "tagged") compare as JSON text
682
758
  parts.push(`${col} = ?`);
683
759
  params.push(_toSQL(value));
684
760
  }
685
761
  }
686
762
  return parts.join(" AND ");
687
763
  };
688
- const sql = build(where);
764
+ const sql = build(where, "where");
765
+ if (write && !sql) throw new SQLiteError(`${write}() refuses a where filter that matches every row${write === "delete" ? " (use clearTable() to delete every row)" : ""}`);
689
766
  return { sql: sql ? ` WHERE ${sql}` : "", params };
690
767
  }
691
768
 
@@ -789,25 +866,6 @@ class SQLite {
789
866
  }
790
867
  }
791
868
 
792
- /**
793
- * The table a simple `SELECT` reads from, for type conversions.
794
- * @param {string} sql
795
- * @returns {string | undefined}
796
- */
797
- #sourceTable(sql) {
798
- if (this.#sources.has(sql)) return this.#sources.get(sql);
799
- /** @type {string | undefined} */
800
- let table;
801
- if (!/\bjoin\b/i.test(sql)) {
802
- const match = /^\s*select\b[\s\S]*?\bfrom\s+"?([A-Za-z_][A-Za-z0-9_]*)"?(?:\s+(?:as\s+)?(?!where\b|order\b|limit\b|group\b)[A-Za-z_]\w*)?\s*(?:where\b|order\b|limit\b|group\b|;|$)/i.exec(sql);
803
- // A view has column types too: SQLite takes them from the tables it reads.
804
- if (match && (this.#known.has(match[1]) || this.#prepare(SOURCE_EXISTS).get(match[1]) !== undefined)) table = match[1];
805
- }
806
- if (this.#sources.size >= 500) this.#sources.clear();
807
- this.#sources.set(sql, table);
808
- return table;
809
- }
810
-
811
869
  /**
812
870
  * Runs SQL that doesn't return rows. Without parameters, you can pass
813
871
  * several statements at once.
@@ -844,8 +902,7 @@ class SQLite {
844
902
 
845
903
  /**
846
904
  * 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.
905
+ * converted like in `get()`, joins and renamed columns included.
849
906
  *
850
907
  * @example
851
908
  * const user = db.queryOne("SELECT * FROM users WHERE email = ?", [email]);
@@ -1126,7 +1183,7 @@ class SQLite {
1126
1183
  #select(tableName, where, options, single) {
1127
1184
  const table = this.#table(tableName);
1128
1185
  const columns = options.columns?.length ? options.columns.map((c) => _id(c, "column name")).join(", ") : "*";
1129
- const clause = this.#where(where);
1186
+ const clause = this.#where(where, undefined, single ? "get" : "getAll");
1130
1187
  let sql = `SELECT ${columns} FROM ${table}${clause.sql}${this.#orderBy(options)}`;
1131
1188
  if (single) {
1132
1189
  sql += " LIMIT 1";
@@ -1176,7 +1233,7 @@ class SQLite {
1176
1233
  * @throws {SQLiteError}
1177
1234
  */
1178
1235
  count(tableName, where = {}) {
1179
- const clause = this.#where(where);
1236
+ const clause = this.#where(where, undefined, "count");
1180
1237
  const sql = `SELECT COUNT(*) AS total FROM ${this.#table(tableName)}${clause.sql}`;
1181
1238
  const stmt = this.#prepare(sql);
1182
1239
  try {
@@ -1198,7 +1255,7 @@ class SQLite {
1198
1255
  * @throws {SQLiteError}
1199
1256
  */
1200
1257
  exists(tableName, where = {}) {
1201
- const clause = this.#where(where);
1258
+ const clause = this.#where(where, undefined, "exists");
1202
1259
  const sql = `SELECT 1 FROM ${this.#table(tableName)}${clause.sql} LIMIT 1`;
1203
1260
  const stmt = this.#prepare(sql);
1204
1261
  try {
@@ -1283,7 +1340,8 @@ class SQLite {
1283
1340
 
1284
1341
  /**
1285
1342
  * Updates the rows that match. The filter is required, so a forgotten
1286
- * condition can't rewrite the whole table.
1343
+ * condition can't rewrite the whole table: an `undefined` value in it, an
1344
+ * empty `$and` or `$or`, or a filter that matches every row throws.
1287
1345
  *
1288
1346
  * @example
1289
1347
  * db.update("users", { active: false }, { last_login: { lt: "2025-01-01" } });
@@ -1299,7 +1357,7 @@ class SQLite {
1299
1357
  if (!_isPlain(where) || !Object.keys(where).length) throw new SQLiteError("update() requires a non-empty where filter");
1300
1358
  const [cols, values] = this.#values(data);
1301
1359
  if (!cols.length) throw new SQLiteError("update() requires at least one column to change");
1302
- const clause = this.#where(where);
1360
+ const clause = this.#where(where, "update");
1303
1361
  const { changes } = this.#run(`UPDATE ${table} SET ${cols.map((c) => `${c} = ?`).join(", ")}${clause.sql}`, [...values, ...clause.params]);
1304
1362
  return { ok: true, changes };
1305
1363
  }
@@ -1351,7 +1409,8 @@ class SQLite {
1351
1409
 
1352
1410
  /**
1353
1411
  * Deletes the rows that match. The filter is required; `clearTable()`
1354
- * deletes everything on purpose.
1412
+ * deletes everything on purpose. An `undefined` value in the filter, an
1413
+ * empty `$and` or `$or`, or a filter that matches every row throws.
1355
1414
  *
1356
1415
  * @example
1357
1416
  * db.delete("sessions", { expires_at: { lt: new Date() } });
@@ -1364,7 +1423,7 @@ class SQLite {
1364
1423
  delete(tableName, where) {
1365
1424
  const table = this.#table(tableName);
1366
1425
  if (!_isPlain(where) || !Object.keys(where).length) throw new SQLiteError("delete() requires a non-empty where filter (use clearTable() to delete every row)");
1367
- const clause = this.#where(where);
1426
+ const clause = this.#where(where, "delete");
1368
1427
  const { changes } = this.#run(`DELETE FROM ${table}${clause.sql}`, clause.params);
1369
1428
  return { ok: true, changes };
1370
1429
  }
@@ -1373,7 +1432,7 @@ class SQLite {
1373
1432
  * Adds to number columns in one statement, so two requests at the same
1374
1433
  * moment both count. Reading the value, adding to it and writing it back
1375
1434
  * would lose one of them. Negative numbers subtract; an empty column
1376
- * counts as 0.
1435
+ * counts as 0. The filter is checked like the one of `update()`.
1377
1436
  *
1378
1437
  * @example
1379
1438
  * db.increment("posts", { views: 1 }, { id: post.id });
@@ -1396,7 +1455,7 @@ class SQLite {
1396
1455
  const col = _id(column, "column name");
1397
1456
  return `${col} = COALESCE(${col}, 0) + ?`;
1398
1457
  });
1399
- const clause = this.#where(where);
1458
+ const clause = this.#where(where, "increment");
1400
1459
  const { changes } = this.#run(`UPDATE ${table} SET ${set.join(", ")}${clause.sql}`, [...entries.map(([, amount]) => amount), ...clause.params]);
1401
1460
  return { ok: true, changes };
1402
1461
  }
@@ -1492,9 +1551,7 @@ class SQLite {
1492
1551
 
1493
1552
  /** Whether a transaction is open. */
1494
1553
  get inTransaction() {
1495
- // `isTransaction` exists since Node 22.16
1496
- const native = /** @type {{ isTransaction?: boolean }} */ (/** @type {unknown} */ (this.#db)).isTransaction;
1497
- return typeof native === "boolean" ? native : this.#depth > 0;
1554
+ return this.#db.isTransaction;
1498
1555
  }
1499
1556
 
1500
1557
  /**