tempest-db-js 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -5,14 +5,127 @@ var fs = require('fs');
5
5
  var path = require('path');
6
6
  var promises = require('readline/promises');
7
7
  var url = require('url');
8
+ var child_process = require('child_process');
9
+ var promises$1 = require('fs/promises');
10
+ var util = require('util');
8
11
  var module$1 = require('module');
9
12
 
10
13
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
14
+ // src/conditions.ts
15
+ var CONDITION = /* @__PURE__ */ Symbol.for("tempest-db-js.condition");
16
+ function wrap(node) {
17
+ return { [CONDITION]: true, node };
18
+ }
19
+ function toExprNode(operand) {
20
+ return isExpression(operand) ? operand.node : { kind: "value", value: operand };
21
+ }
22
+ function assertValueOperands(op, operands) {
23
+ if (operands.some(isExpression)) {
24
+ throw new Error(
25
+ `The "${op}" operator binds its operands, so it takes values, not expressions.`
26
+ );
27
+ }
28
+ }
29
+ function isExpression(value) {
30
+ return value instanceof Expression;
31
+ }
32
+ var Expression = class {
33
+ constructor(node) {
34
+ this.node = node;
35
+ }
36
+ node;
37
+ /** Compare this expression against another expression or a bound value. */
38
+ compare(op, operand) {
39
+ return wrap({ kind: "compare", left: this.node, op, right: toExprNode(operand) });
40
+ }
41
+ /** `=` (or `IS NULL` for a null value). */
42
+ eq(operand) {
43
+ return this.compare("eq", operand);
44
+ }
45
+ /** `<>` (or `IS NOT NULL` for a null value). */
46
+ ne(operand) {
47
+ return this.compare("ne", operand);
48
+ }
49
+ /** `>`. */
50
+ gt(operand) {
51
+ return this.compare("gt", operand);
52
+ }
53
+ /** `>=`. */
54
+ gte(operand) {
55
+ return this.compare("gte", operand);
56
+ }
57
+ /** `<`. */
58
+ lt(operand) {
59
+ return this.compare("lt", operand);
60
+ }
61
+ /** `<=`. */
62
+ lte(operand) {
63
+ return this.compare("lte", operand);
64
+ }
65
+ /** `LIKE` — `%` and `_` in the operand are wildcards. */
66
+ like(pattern) {
67
+ return this.compare("like", pattern);
68
+ }
69
+ /** `ILIKE` — case-insensitive **pattern** matching, wildcards included. */
70
+ ilike(pattern) {
71
+ return this.compare("ilike", pattern);
72
+ }
73
+ /** Case-insensitive equality (`lower(a) = lower(b)`), with no wildcards. */
74
+ ieq(operand) {
75
+ return this.compare("ieq", operand);
76
+ }
77
+ /**
78
+ * `IN (...)` over a list of values.
79
+ *
80
+ * @param values The values to test against.
81
+ * @returns The condition.
82
+ * @throws Error When an entry is an {@link Expression} — a list operand is
83
+ * bound, so an expression there would be serialized as a parameter instead of
84
+ * rendered as SQL.
85
+ */
86
+ in(values) {
87
+ assertValueOperands("in", values);
88
+ return this.compare("in", values);
89
+ }
90
+ /**
91
+ * `NOT IN (...)` over a list of values.
92
+ *
93
+ * @param values The values to exclude.
94
+ * @returns The condition.
95
+ * @throws Error When an entry is an {@link Expression} (see {@link Expression.in}).
96
+ */
97
+ notIn(values) {
98
+ assertValueOperands("notIn", values);
99
+ return this.compare("notIn", values);
100
+ }
101
+ /**
102
+ * `BETWEEN lo AND hi` (inclusive).
103
+ *
104
+ * @param lo The lower bound.
105
+ * @param hi The upper bound.
106
+ * @returns The condition.
107
+ * @throws Error When a bound is an {@link Expression} (see {@link Expression.in}).
108
+ */
109
+ between(lo, hi) {
110
+ assertValueOperands("between", [lo, hi]);
111
+ return this.compare("between", [lo, hi]);
112
+ }
113
+ /** `IS NULL` (true) / `IS NOT NULL` (false). */
114
+ isNull(value = true) {
115
+ return this.compare("isNull", value);
116
+ }
117
+ };
118
+
11
119
  // src/expressions.ts
120
+ function renderExcluded(column, bare, dialect) {
121
+ return dialect === "mysql" ? `VALUES(${bare})` : `excluded.${column}`;
122
+ }
12
123
  function renderPortableToken(token, dialect) {
13
124
  switch (token) {
14
125
  case "now":
15
- return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
126
+ if (dialect === "postgresql") return "now()";
127
+ if (dialect === "sqlite") return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
128
+ return "CURRENT_TIMESTAMP";
16
129
  case "current_date":
17
130
  return "CURRENT_DATE";
18
131
  case "current_time":
@@ -24,7 +137,134 @@ function renderPortableToken(token, dialect) {
24
137
  }
25
138
  }
26
139
 
140
+ // src/query.ts
141
+ function isSubquery(value) {
142
+ return typeof value === "object" && value !== null && value.node?.kind === "select";
143
+ }
144
+ var OPERATORS = [
145
+ "eq",
146
+ "ne",
147
+ "gt",
148
+ "gte",
149
+ "lt",
150
+ "lte",
151
+ "like",
152
+ "ilike",
153
+ "ieq",
154
+ "iContains",
155
+ "in",
156
+ "notIn",
157
+ "between",
158
+ "isNull",
159
+ "contains",
160
+ "containedBy",
161
+ "overlaps"
162
+ ];
163
+
164
+ // src/url.ts
165
+ var InvalidDatabaseUrl = class extends Error {
166
+ constructor(url, reason) {
167
+ super(`Invalid database URL ${JSON.stringify(url)}: ${reason}`);
168
+ this.name = "InvalidDatabaseUrl";
169
+ }
170
+ };
171
+ var DIALECT_ALIASES = {
172
+ sqlite: "sqlite",
173
+ sqlite3: "sqlite",
174
+ postgresql: "postgresql",
175
+ postgres: "postgresql",
176
+ pg: "postgresql",
177
+ mysql: "mysql",
178
+ mariadb: "mysql"
179
+ };
180
+ function splitScheme(scheme) {
181
+ const plus = scheme.indexOf("+");
182
+ if (plus === -1) return { base: scheme.toLowerCase(), driver: null };
183
+ return {
184
+ base: scheme.slice(0, plus).toLowerCase(),
185
+ driver: scheme.slice(plus + 1) || null
186
+ };
187
+ }
188
+ function decode(value) {
189
+ try {
190
+ return decodeURIComponent(value);
191
+ } catch {
192
+ return value;
193
+ }
194
+ }
195
+ function parseSqlite(raw, driver, rest) {
196
+ let database;
197
+ if (rest.endsWith(":memory:")) {
198
+ database = ":memory:";
199
+ } else if (rest.startsWith("///")) {
200
+ database = rest.slice(3) || ":memory:";
201
+ } else if (rest.startsWith("//")) {
202
+ database = rest.slice(2) || ":memory:";
203
+ } else {
204
+ database = rest || ":memory:";
205
+ }
206
+ return {
207
+ dialect: "sqlite",
208
+ driver,
209
+ host: null,
210
+ port: null,
211
+ user: null,
212
+ password: null,
213
+ database: decode(database),
214
+ options: {},
215
+ raw
216
+ };
217
+ }
218
+ function parseNetworkUrl(raw, driver, rest, dialect) {
219
+ let parsed;
220
+ try {
221
+ parsed = new URL(`${dialect}:${rest}`);
222
+ } catch {
223
+ throw new InvalidDatabaseUrl(raw, "could not parse host/credentials");
224
+ }
225
+ const database = decode(parsed.pathname.replace(/^\//, "")) || null;
226
+ const options = {};
227
+ for (const [key, value] of parsed.searchParams) options[key] = value;
228
+ return {
229
+ dialect,
230
+ driver,
231
+ host: parsed.hostname || null,
232
+ port: parsed.port ? Number(parsed.port) : null,
233
+ user: parsed.username ? decode(parsed.username) : null,
234
+ password: parsed.password ? decode(parsed.password) : null,
235
+ database,
236
+ options,
237
+ raw
238
+ };
239
+ }
240
+ function parseDatabaseUrl(url) {
241
+ const schemeEnd = url.indexOf(":");
242
+ if (schemeEnd === -1) {
243
+ throw new InvalidDatabaseUrl(
244
+ url,
245
+ "missing scheme (expected e.g. sqlite:// or postgresql://)"
246
+ );
247
+ }
248
+ const { base, driver } = splitScheme(url.slice(0, schemeEnd));
249
+ const dialect = DIALECT_ALIASES[base];
250
+ if (!dialect) {
251
+ throw new InvalidDatabaseUrl(url, `unknown dialect ${JSON.stringify(base)}`);
252
+ }
253
+ const rest = url.slice(schemeEnd + 1);
254
+ if (dialect === "sqlite") return parseSqlite(url, driver, rest);
255
+ return parseNetworkUrl(url, driver, rest, dialect);
256
+ }
257
+
258
+ // src/search.ts
259
+ function escapeLike(value) {
260
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
261
+ }
262
+
27
263
  // src/index.ts
264
+ var EXPRESSION = /* @__PURE__ */ Symbol.for("tempest-db-js.expression");
265
+ function isSqlExpression(value) {
266
+ return typeof value === "object" && value !== null && value[EXPRESSION] === true;
267
+ }
28
268
  function isDefaultValue(value) {
29
269
  return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
30
270
  }
@@ -44,13 +284,14 @@ function parseReference(ref, options) {
44
284
  };
45
285
  }
46
286
  var Column = class _Column {
47
- constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
287
+ constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null, codec = null) {
48
288
  this.type = type;
49
289
  this.flags = flags;
50
290
  this.defaultValue = defaultValue;
51
291
  this.onUpdateValue = onUpdateValue;
52
292
  this.reference = reference;
53
293
  this.dbName = dbName;
294
+ this.codec = codec;
54
295
  }
55
296
  type;
56
297
  flags;
@@ -58,6 +299,7 @@ var Column = class _Column {
58
299
  onUpdateValue;
59
300
  reference;
60
301
  dbName;
302
+ codec;
61
303
  /** Clone this column with one facet replaced, carrying every other over. */
62
304
  derive(patch) {
63
305
  return new _Column(
@@ -66,7 +308,8 @@ var Column = class _Column {
66
308
  patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
67
309
  patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
68
310
  patch.reference !== void 0 ? patch.reference : this.reference,
69
- patch.dbName !== void 0 ? patch.dbName : this.dbName
311
+ patch.dbName !== void 0 ? patch.dbName : this.dbName,
312
+ patch.codec !== void 0 ? patch.codec : this.codec
70
313
  );
71
314
  }
72
315
  primaryKey() {
@@ -204,8 +447,1328 @@ function columnNamesOf(model) {
204
447
  return result;
205
448
  }
206
449
 
450
+ // src/dialect.ts
451
+ var OPERATOR_SET = new Set(OPERATORS);
452
+ var MULTI_VALUE_OPERATORS = /* @__PURE__ */ new Set(["in", "notIn", "between"]);
453
+ function encodeOperand(op, operand, encode) {
454
+ if (op === "isNull") return operand;
455
+ if (MULTI_VALUE_OPERATORS.has(op)) {
456
+ return Array.isArray(operand) ? operand.map(encode) : operand;
457
+ }
458
+ return encode(operand);
459
+ }
460
+ function codecEncoder(codecs) {
461
+ if (!codecs) return (_key, value) => value;
462
+ return (key, value) => {
463
+ const codec = codecs[key];
464
+ return codec ? codec.toDb(value) : value;
465
+ };
466
+ }
467
+ function isOperatorObject(value) {
468
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array) {
469
+ return false;
470
+ }
471
+ const keys = Object.keys(value);
472
+ return keys.length > 0 && keys.every((k) => OPERATOR_SET.has(k));
473
+ }
474
+ var Params = class {
475
+ constructor(placeholder2) {
476
+ this.placeholder = placeholder2;
477
+ }
478
+ placeholder;
479
+ values = [];
480
+ bind(value) {
481
+ this.values.push(value);
482
+ return this.placeholder(this.values.length);
483
+ }
484
+ };
485
+ var LiteralParams = class extends Params {
486
+ constructor() {
487
+ super(() => "");
488
+ }
489
+ bind(value) {
490
+ if (value === null || value === void 0) return "NULL";
491
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
492
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
493
+ if (value instanceof Date) return `'${value.toISOString()}'`;
494
+ return `'${String(value).replace(/'/g, "''")}'`;
495
+ }
496
+ };
497
+ function insertColumns(rows) {
498
+ const columns = [];
499
+ const seen = /* @__PURE__ */ new Set();
500
+ for (const row of rows) {
501
+ for (const key of Object.keys(row)) {
502
+ if (seen.has(key)) continue;
503
+ seen.add(key);
504
+ columns.push(key);
505
+ }
506
+ }
507
+ return columns;
508
+ }
509
+ function insertHasExpression(node) {
510
+ for (const row of node.values) {
511
+ for (const value of Object.values(row)) {
512
+ if (isSqlExpression(value)) return true;
513
+ }
514
+ }
515
+ const update = node.onConflict?.update;
516
+ if (update && update !== "nothing") {
517
+ for (const value of Object.values(update)) {
518
+ if (isSqlExpression(value)) return true;
519
+ }
520
+ }
521
+ return false;
522
+ }
523
+ var BaseDialect = class _BaseDialect {
524
+ /**
525
+ * INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
526
+ * returning). Shared across dialect instances — the key namespaces by dialect
527
+ * name, and the placeholder text is dialect-specific but structure-determined.
528
+ */
529
+ static insertTemplates = /* @__PURE__ */ new Map();
530
+ /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
531
+ static quotedIds = /* @__PURE__ */ new Map();
532
+ /**
533
+ * Render a case-insensitive LIKE whose pattern carries escaped wildcards.
534
+ *
535
+ * The `ESCAPE` clause is not decoration: PostgreSQL treats `\` as the escape
536
+ * character by default, **SQLite has none at all** until one is declared, so
537
+ * without this the escaping done on our side would be meaningless there.
538
+ *
539
+ * @param column The rendered column.
540
+ * @param param The bound pattern.
541
+ * @returns The rendered comparison.
542
+ */
543
+ ilikeEscaped(column, param) {
544
+ return `${this.ilike(column, param)} ESCAPE '\\'`;
545
+ }
546
+ /**
547
+ * Validate a subquery operand before it is rendered, for dialects that restrict
548
+ * what an `IN (SELECT ...)` may contain. The default accepts everything.
549
+ *
550
+ * @param _node The subquery's AST.
551
+ * @throws Error When the dialect cannot execute this subquery.
552
+ */
553
+ checkSubquery(_node) {
554
+ }
555
+ /**
556
+ * The SQL operator for an array containment/overlap test.
557
+ *
558
+ * Only PostgreSQL has native arrays; the other dialects throw rather than
559
+ * emitting an operator that means something else there.
560
+ *
561
+ * @param op The array operator name.
562
+ * @returns The SQL operator text.
563
+ * @throws Error On a dialect without native array support.
564
+ */
565
+ arrayOperator(op) {
566
+ throw new Error(
567
+ `The "${op}" operator needs native array support, which ${this.name} does not have.`
568
+ );
569
+ }
570
+ /**
571
+ * Quote an identifier (column/table) for the active dialect.
572
+ *
573
+ * Memoized: identifiers form a small, stable set (column/table names), but this
574
+ * runs for every identifier on every compile. Caching the quoted form removes a
575
+ * regex-replace + string allocation from the hot path. The standard double-quote
576
+ * form is identical across both dialects, so one shared cache is correct.
577
+ */
578
+ quoteId(name) {
579
+ const cached = _BaseDialect.quotedIds.get(name);
580
+ if (cached !== void 0) return cached;
581
+ const quoted = `"${name.replace(/"/g, '""')}"`;
582
+ _BaseDialect.quotedIds.set(name, quoted);
583
+ return quoted;
584
+ }
585
+ /** Compile any node to `{ sql, params }`. */
586
+ compile(node) {
587
+ const params = new Params((i) => this.placeholder(i));
588
+ let sql;
589
+ switch (node.kind) {
590
+ case "select":
591
+ sql = this.compileSelect(node, params);
592
+ break;
593
+ case "insert":
594
+ sql = this.compileInsert(node, params);
595
+ break;
596
+ case "update":
597
+ sql = this.compileUpdate(node, params);
598
+ break;
599
+ case "delete":
600
+ sql = this.compileDelete(node, params);
601
+ break;
602
+ case "join_select":
603
+ sql = this.compileJoin(node, params);
604
+ break;
605
+ case "set_op":
606
+ sql = this.compileSetOp(node, params);
607
+ break;
608
+ }
609
+ return { sql, params: params.values };
610
+ }
611
+ /**
612
+ * Render a condition as schema SQL, with values inlined.
613
+ *
614
+ * Same compiler as a `WHERE`, different parameter strategy — so a `CHECK` and
615
+ * the query language cannot drift apart in what they mean.
616
+ *
617
+ * @param node The condition.
618
+ * @param params A {@link LiteralParams}.
619
+ * @returns The rendered predicate.
620
+ */
621
+ renderConditionLiteral(node, params) {
622
+ return this.compileCondition(node, params, (key) => this.columnId(key, void 0));
623
+ }
624
+ /**
625
+ * Render the `WITH` clause of a statement.
626
+ *
627
+ * `RECURSIVE` is a property of the **clause**, not of an entry: one recursive
628
+ * entry makes the whole `WITH` recursive, which is what the SQL standard says
629
+ * and what PostgreSQL and SQLite both implement.
630
+ *
631
+ * @param entries The `WITH` entries, if any.
632
+ * @param params The parameter collector.
633
+ * @returns The clause with a trailing space, or an empty string.
634
+ */
635
+ compileWith(entries, params) {
636
+ if (!entries || entries.length === 0) return "";
637
+ const recursive = entries.some((entry) => entry.recursive) ? "RECURSIVE " : "";
638
+ const rendered = entries.map((entry) => {
639
+ const body = entry.body.kind === "set_op" ? this.compileSetOp(entry.body, params) : this.compileSelect(entry.body, params);
640
+ const hint = entry.materialized === null ? "" : entry.materialized ? " MATERIALIZED" : " NOT MATERIALIZED";
641
+ return `${this.quoteId(entry.name)} AS${hint} (${body})`;
642
+ });
643
+ return `WITH ${recursive}${rendered.join(", ")} `;
644
+ }
645
+ /**
646
+ * The `EXPLAIN` prefix for this dialect.
647
+ *
648
+ * @param analyze Whether to measure by actually running the statement.
649
+ * @returns The prefix to put in front of the statement.
650
+ * @throws Error When the dialect cannot do what was asked.
651
+ */
652
+ explainPrefix(analyze) {
653
+ return analyze ? "EXPLAIN (FORMAT JSON, ANALYZE)" : "EXPLAIN (FORMAT JSON)";
654
+ }
655
+ /**
656
+ * The SQL keyword for a set operation.
657
+ *
658
+ * @param op The operator.
659
+ * @returns The keyword.
660
+ * @throws Error On a dialect that does not implement it.
661
+ */
662
+ setOperator(op) {
663
+ switch (op) {
664
+ case "union":
665
+ return "UNION";
666
+ case "unionAll":
667
+ return "UNION ALL";
668
+ case "intersect":
669
+ return "INTERSECT";
670
+ case "except":
671
+ return "EXCEPT";
672
+ }
673
+ }
674
+ /**
675
+ * Compile a set operation.
676
+ *
677
+ * A branch carrying its own `ORDER BY`/`LIMIT` is parenthesized: without the
678
+ * parentheses those clauses bind to the **combined** result, which is a
679
+ * different query and a classic source of silently wrong output.
680
+ *
681
+ * @param node The set-operation node.
682
+ * @param params The parameter collector.
683
+ * @returns The rendered statement.
684
+ */
685
+ compileSetOp(node, params) {
686
+ const keyword = ` ${this.setOperator(node.op)} `;
687
+ const branches = node.branches.map((branch) => {
688
+ const sql2 = branch.kind === "join_select" ? this.compileJoin(branch, params) : this.compileSelect(branch, params);
689
+ const scoped = branch.orderBy.length > 0 || branch.limit !== void 0;
690
+ return scoped ? `(${sql2})` : sql2;
691
+ });
692
+ let sql = branches.join(keyword);
693
+ if (node.orderBy.length > 0) {
694
+ const terms = node.orderBy.map((t) => {
695
+ const id = typeof t.column === "string" ? this.quoteId(t.column) : this.renderExpr(t.column, params, (k) => this.quoteId(k));
696
+ return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
697
+ }).join(", ");
698
+ sql += ` ORDER BY ${terms}`;
699
+ }
700
+ if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
701
+ if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
702
+ return sql;
703
+ }
704
+ /**
705
+ * Render a qualified `alias.column` ref as `"alias"."column"`, translating the
706
+ * property name to the real column name for that alias's model.
707
+ *
708
+ * @param ref The `alias.property` reference (a bare name is left unqualified).
709
+ * @param names The node's per-alias name maps, if any source renames columns.
710
+ * @returns The quoted, qualified identifier.
711
+ */
712
+ qualify(ref, names) {
713
+ const dot = ref.indexOf(".");
714
+ if (dot === -1) return this.quoteId(ref);
715
+ const alias = ref.slice(0, dot);
716
+ const prop = ref.slice(dot + 1);
717
+ return `${this.quoteId(alias)}.${this.columnId(prop, names?.[alias])}`;
718
+ }
719
+ /**
720
+ * Quote a column identifier, translating the model property name to the real
721
+ * database column name first.
722
+ *
723
+ * `names` is `undefined` for a model that renames nothing — the overwhelmingly
724
+ * common case — so this stays a single lookup plus the memoized quote.
725
+ *
726
+ * @param prop The model property name as written in the builder.
727
+ * @param names The node's property → column map, if any.
728
+ * @returns The quoted database identifier.
729
+ */
730
+ columnId(prop, names) {
731
+ const mapped = names?.[prop];
732
+ if (mapped !== void 0) return this.quoteId(mapped);
733
+ const dot = prop.indexOf(".");
734
+ if (dot > 0 && dot < prop.length - 1) {
735
+ return `${this.quoteId(prop.slice(0, dot))}.${this.quoteId(prop.slice(dot + 1))}`;
736
+ }
737
+ return this.quoteId(prop);
738
+ }
739
+ /**
740
+ * Render a {@link SqlExpression} inline, binding the parameters it carries.
741
+ *
742
+ * This is what keeps `set({ attempts: sql.raw("attempts + 1") })` an expression
743
+ * instead of a bound object: the fragment goes into the statement text, and
744
+ * only a `sql.expr` template's interpolations become parameters.
745
+ *
746
+ * @param expr The branded expression.
747
+ * @param params The parameter collector for the statement being compiled.
748
+ * @returns The SQL text of the expression.
749
+ */
750
+ renderExpression(expr, params) {
751
+ const token = expr.expression;
752
+ if (typeof token === "string") return renderPortableToken(token, this.name);
753
+ if ("raw" in token) return token.raw;
754
+ if ("excluded" in token) {
755
+ return renderExcluded(this.quoteId(token.excluded), token.excluded, this.name);
756
+ }
757
+ const parts = token.parts;
758
+ let sql = parts[0] ?? "";
759
+ for (let i = 1; i < parts.length; i++) {
760
+ sql += `${params.bind(expr.params[i - 1])}${parts[i]}`;
761
+ }
762
+ return sql;
763
+ }
764
+ /** Render one write value: a SQL expression inline, anything else as a parameter. */
765
+ renderValue(value, params) {
766
+ if (isSqlExpression(value)) return this.renderExpression(value, params);
767
+ if (isExpression(value)) {
768
+ return this.renderExpr(value.node, params, (k) => this.columnId(k, void 0));
769
+ }
770
+ return params.bind(value);
771
+ }
772
+ /**
773
+ * The statements that open a transaction with the requested characteristics.
774
+ *
775
+ * Returned as a list because the dialects disagree on shape: PostgreSQL takes
776
+ * everything on the `BEGIN` itself, MySQL needs a separate `SET TRANSACTION`
777
+ * before it, and SQLite has no syntax at all.
778
+ *
779
+ * @param options The requested isolation level and read-only flag.
780
+ * @returns The statements to run, in order.
781
+ * @throws Error When the dialect cannot honor what was asked.
782
+ */
783
+ beginStatements(options) {
784
+ const parts = ["BEGIN"];
785
+ if (options?.isolation)
786
+ parts.push(`ISOLATION LEVEL ${options.isolation.toUpperCase()}`);
787
+ if (options?.readOnly) parts.push("READ ONLY");
788
+ return [parts.join(" ")];
789
+ }
790
+ /**
791
+ * Render a row-level locking clause (`FOR UPDATE ...`).
792
+ *
793
+ * Standard on PostgreSQL and MySQL 8.0+; SQLite overrides it to throw.
794
+ *
795
+ * @param lock The locking clause from the node.
796
+ * @returns The SQL text, leading space included.
797
+ */
798
+ renderLock(lock) {
799
+ const strength = lock.strength === "update" ? "FOR UPDATE" : "FOR SHARE";
800
+ const of = lock.of.length > 0 ? ` OF ${lock.of.map((t) => this.quoteId(t)).join(", ")}` : "";
801
+ const wait = lock.wait === "skipLocked" ? " SKIP LOCKED" : lock.wait === "noWait" ? " NOWAIT" : "";
802
+ return ` ${strength}${of}${wait}`;
803
+ }
804
+ // ---- statements -------------------------------------------------------
805
+ /**
806
+ * Compile a SELECT.
807
+ *
808
+ * Two alias rules differ between clauses and are handled here: PostgreSQL does
809
+ * NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
810
+ * its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
811
+ * by contrast, accepts the output alias everywhere, so it is emitted as
812
+ * written.
813
+ *
814
+ * @param node The select AST.
815
+ * @param params The parameter collector.
816
+ * @returns The SQL text.
817
+ */
818
+ compileSelect(node, params) {
819
+ const names = node.names;
820
+ let cols;
821
+ if (node.aggregates.length > 0) {
822
+ const groupSel = node.groupBy.map((c) => this.columnId(c, names));
823
+ const aggSel = node.aggregates.map((a) => {
824
+ const inner = this.aggregateOperand(a, params, names);
825
+ return `${a.fn.toUpperCase()}(${inner}) AS ${this.quoteId(a.alias)}`;
826
+ });
827
+ cols = [...groupSel, ...aggSel].join(", ");
828
+ } else {
829
+ cols = node.columns === "*" ? "*" : node.columns.map((c) => this.columnId(c, names)).join(", ");
830
+ }
831
+ const computed = Object.entries(node.computed ?? {}).map(
832
+ ([alias, expr]) => `${this.renderExpr(expr, params, (k) => this.columnId(k, names))} AS ${this.quoteId(alias)}`
833
+ );
834
+ if (computed.length > 0) cols = [cols, ...computed].join(", ");
835
+ const from = node.alias ? `${this.quoteId(node.table)} AS ${this.quoteId(node.alias)}` : this.quoteId(node.table);
836
+ let sql = `${this.compileWith(node.with, params)}SELECT ${node.distinct ? "DISTINCT " : ""}${cols} FROM ${from}`;
837
+ const where = this.compileCondition(
838
+ node.where,
839
+ params,
840
+ (k) => this.columnId(k, names),
841
+ codecEncoder(node.codecs)
842
+ );
843
+ if (where) sql += ` WHERE ${where}`;
844
+ if (node.groupBy.length > 0) {
845
+ sql += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
846
+ }
847
+ const aggByAlias = new Map(node.aggregates.map((a) => [a.alias, a]));
848
+ if (node.having) {
849
+ const having = this.compileCondition(node.having, params, (key) => {
850
+ const agg = aggByAlias.get(key);
851
+ if (!agg) return this.columnId(key, names);
852
+ const inner = this.aggregateOperand(agg, params, names);
853
+ return `${agg.fn.toUpperCase()}(${inner})`;
854
+ });
855
+ if (having) sql += ` HAVING ${having}`;
856
+ }
857
+ if (node.orderBy.length > 0) {
858
+ const terms = node.orderBy.map((t) => {
859
+ const id = typeof t.column !== "string" ? this.renderExpr(t.column, params, (k) => this.columnId(k, names)) : aggByAlias.has(t.column) ? this.quoteId(t.column) : this.columnId(t.column, names);
860
+ return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
861
+ }).join(", ");
862
+ sql += ` ORDER BY ${terms}`;
863
+ }
864
+ if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
865
+ if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
866
+ if (node.lock) {
867
+ if (node.distinct || node.groupBy.length > 0 || node.aggregates.length > 0) {
868
+ throw new Error(
869
+ "FOR UPDATE / FOR SHARE cannot be combined with DISTINCT or an aggregate query \u2014 lock the underlying rows in a separate SELECT."
870
+ );
871
+ }
872
+ sql += this.renderLock(node.lock);
873
+ }
874
+ return sql;
875
+ }
876
+ /**
877
+ * Compile an INSERT.
878
+ *
879
+ * Takes the cached fast path only when the statement text is a pure function of
880
+ * its structure. A SQL expression among the values, or a conflict predicate,
881
+ * makes the text depend on the values themselves — those compile uncached, in
882
+ * SQL order, so placeholder positions stay correct.
883
+ */
884
+ compileInsert(node, params) {
885
+ if (node.fromSelect) {
886
+ const target = node.fromSelect.columns.map((c) => this.columnId(c, node.names)).join(", ");
887
+ const source = node.fromSelect.select;
888
+ const query = source.kind === "join_select" ? this.compileJoin(source, params) : source.kind === "set_op" ? this.compileSetOp(source, params) : this.compileSelect(source, params);
889
+ const returning = this.compileReturning(node.returning, node.names);
890
+ return `INSERT INTO ${this.quoteId(node.table)} (${target}) ${query}${returning}`;
891
+ }
892
+ const columns = insertColumns(node.values);
893
+ const conflict = node.onConflict;
894
+ const cacheable = conflict?.targetWhere === void 0 && conflict?.updateWhere === void 0 && !insertHasExpression(node);
895
+ if (!cacheable) return this.compileInsertDirect(node, columns, params);
896
+ for (const row of node.values) {
897
+ for (const c of columns) params.bind(row[c] ?? null);
898
+ }
899
+ const conflictCols = conflict && conflict.update !== "nothing" ? Object.keys(conflict.update) : [];
900
+ for (const c of conflictCols) {
901
+ params.bind((conflict?.update)[c]);
902
+ }
903
+ return this.insertTemplate(node, columns, conflictCols, params);
904
+ }
905
+ /**
906
+ * Compile an INSERT without the template cache, rendering clauses in statement
907
+ * order so every parameter is bound at the position it appears.
908
+ *
909
+ * @param node The insert node.
910
+ * @param columns The column keys shared by every row.
911
+ * @param params The parameter collector.
912
+ * @returns The SQL text.
913
+ */
914
+ compileInsertDirect(node, columns, params) {
915
+ const names = node.names;
916
+ const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
917
+ const rowsSql = node.values.map((row) => {
918
+ const cells = columns.map(
919
+ (c) => this.renderValue(row[c] ?? null, params)
920
+ );
921
+ return `(${cells.join(", ")})`;
922
+ }).join(", ");
923
+ let sql = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
924
+ if (node.onConflict) {
925
+ const update = node.onConflict.update;
926
+ const conflictCols = update === "nothing" ? [] : Object.keys(update);
927
+ let cursor = 0;
928
+ sql += this.renderConflict(
929
+ node.onConflict,
930
+ conflictCols,
931
+ () => {
932
+ const key = conflictCols[cursor++];
933
+ return this.renderValue(update[key], params);
934
+ },
935
+ names,
936
+ params
937
+ );
938
+ }
939
+ sql += this.compileReturning(node.returning, names);
940
+ return sql;
941
+ }
942
+ /**
943
+ * The INSERT SQL template for a given structure, cached across calls.
944
+ *
945
+ * The text depends only on (dialect, table, columns, row count, returning,
946
+ * conflict shape) — never on the bound values — and placeholder positions are
947
+ * deterministic from the counts (a fresh statement always starts binding at 1).
948
+ * So a per-row insert loop compiles the string once and reuses it every row.
949
+ */
950
+ insertTemplate(node, columns, conflictCols, params) {
951
+ const returningKey = node.returning === null ? "" : node.returning === "*" ? "*" : node.returning.join(",");
952
+ const conflictKey = node.onConflict ? `${node.onConflict.target.join(",")}>${node.onConflict.update === "nothing" ? "nothing" : conflictCols.join(",")}` : "";
953
+ const key = `${this.name}|${node.table}|${columns.join(",")}|${node.values.length}|${returningKey}|${conflictKey}`;
954
+ const cached = _BaseDialect.insertTemplates.get(key);
955
+ if (cached !== void 0) return cached;
956
+ const names = node.names;
957
+ const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
958
+ let position = 0;
959
+ const rowsSql = node.values.map(() => `(${columns.map(() => this.placeholder(++position)).join(", ")})`).join(", ");
960
+ let sql = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
961
+ if (node.onConflict) {
962
+ sql += this.renderConflict(
963
+ node.onConflict,
964
+ conflictCols,
965
+ () => this.placeholder(++position),
966
+ names,
967
+ params
968
+ );
969
+ }
970
+ sql += this.compileReturning(node.returning, names);
971
+ _BaseDialect.insertTemplates.set(key, sql);
972
+ return sql;
973
+ }
974
+ /**
975
+ * Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
976
+ * `ON CONFLICT (...) [WHERE predicate] DO NOTHING | DO UPDATE SET ... [WHERE ...]`;
977
+ * MySQL overrides this.
978
+ *
979
+ * The index predicate is rendered before the `DO UPDATE` assignments because
980
+ * that is where it sits in the statement, so its parameters bind first.
981
+ *
982
+ * @param onConflict The conflict clause from the node.
983
+ * @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
984
+ * @param nextValue Yields the SQL for the next `DO UPDATE` assignment value.
985
+ * @param names The node's property → column map, if any.
986
+ * @param params The parameter collector, for the predicates.
987
+ * @returns The SQL text, leading space included.
988
+ */
989
+ renderConflict(onConflict, conflictCols, nextValue, names, params) {
990
+ const idFor = (key) => this.columnId(key, names);
991
+ const target = onConflict.target.map(idFor).join(", ");
992
+ const indexWhere = this.compileCondition(onConflict.targetWhere, params, idFor);
993
+ const targetSql = indexWhere ? `(${target}) WHERE ${indexWhere}` : `(${target})`;
994
+ if (onConflict.update === "nothing") return ` ON CONFLICT ${targetSql} DO NOTHING`;
995
+ const assignments = conflictCols.map((c) => `${idFor(c)} = ${nextValue()}`).join(", ");
996
+ let sql = ` ON CONFLICT ${targetSql} DO UPDATE SET ${assignments}`;
997
+ const updateWhere = this.compileCondition(onConflict.updateWhere, params, idFor);
998
+ if (updateWhere) sql += ` WHERE ${updateWhere}`;
999
+ return sql;
1000
+ }
1001
+ compileUpdate(node, params) {
1002
+ const names = node.names;
1003
+ const sets = Object.entries(node.set).map(
1004
+ ([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
1005
+ ).join(", ");
1006
+ let sql = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
1007
+ sql += this.compileExtraSources("FROM", node.from);
1008
+ const where = this.compileCondition(
1009
+ node.where,
1010
+ params,
1011
+ (k) => this.columnId(k, names),
1012
+ codecEncoder(node.codecs)
1013
+ );
1014
+ if (where) sql += ` WHERE ${where}`;
1015
+ sql += this.compileReturning(node.returning, names);
1016
+ return sql;
1017
+ }
1018
+ compileDelete(node, params) {
1019
+ const names = node.names;
1020
+ let sql = `DELETE FROM ${this.quoteId(node.table)}`;
1021
+ sql += this.compileExtraSources("USING", node.using);
1022
+ const where = this.compileCondition(
1023
+ node.where,
1024
+ params,
1025
+ (k) => this.columnId(k, names),
1026
+ codecEncoder(node.codecs)
1027
+ );
1028
+ if (where) sql += ` WHERE ${where}`;
1029
+ sql += this.compileReturning(node.returning, names);
1030
+ return sql;
1031
+ }
1032
+ compileJoin(node, params) {
1033
+ const names = node.names;
1034
+ const cols = node.selections.map((s) => {
1035
+ const ref = `${s.alias}.${s.column}`;
1036
+ const label = node.pick ? s.column : ref;
1037
+ return `${this.qualify(ref, names)} AS ${this.quoteId(label)}`;
1038
+ }).join(", ");
1039
+ let sql = `${this.compileWith(node.with, params)}SELECT ${cols} FROM ${this.quoteId(node.base.table)} AS ${this.quoteId(node.base.alias)}`;
1040
+ for (const j of node.joins) {
1041
+ const kw = j.kind === "left" ? "LEFT JOIN" : "INNER JOIN";
1042
+ const on = j.on.map(([l, r]) => `${this.qualify(l, names)} = ${this.qualify(r, names)}`).join(" AND ");
1043
+ sql += ` ${kw} ${this.quoteId(j.table)} AS ${this.quoteId(j.alias)} ON ${on}`;
1044
+ }
1045
+ const where = this.compileCondition(
1046
+ node.where,
1047
+ params,
1048
+ (k) => this.qualify(k, names)
1049
+ );
1050
+ if (where) sql += ` WHERE ${where}`;
1051
+ if (node.orderBy.length > 0) {
1052
+ const terms = node.orderBy.map(
1053
+ (t) => `${this.qualify(t.ref, names)} ${t.direction === "desc" ? "DESC" : "ASC"}`
1054
+ ).join(", ");
1055
+ sql += ` ORDER BY ${terms}`;
1056
+ }
1057
+ if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
1058
+ if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
1059
+ return sql;
1060
+ }
1061
+ // ---- clauses ----------------------------------------------------------
1062
+ /**
1063
+ * Render the extra sources of an `UPDATE ... FROM` / `DELETE ... USING`.
1064
+ *
1065
+ * The dialects that do not have the clause override this and throw: emitting it
1066
+ * anyway would produce a statement the server rejects, and quietly dropping it
1067
+ * would change which rows are written.
1068
+ *
1069
+ * @param keyword `FROM` or `USING`.
1070
+ * @param sources The extra tables, if any.
1071
+ * @returns The clause with a leading space, or an empty string.
1072
+ */
1073
+ compileExtraSources(keyword, sources) {
1074
+ if (!sources || sources.length === 0) return "";
1075
+ const list = sources.map((s) => `${this.quoteId(s.table)} AS ${this.quoteId(s.alias)}`).join(", ");
1076
+ return ` ${keyword} ${list}`;
1077
+ }
1078
+ compileReturning(returning, names) {
1079
+ if (returning === null) return "";
1080
+ if (returning === "*") return " RETURNING *";
1081
+ return ` RETURNING ${returning.map((c) => this.columnId(c, names)).join(", ")}`;
1082
+ }
1083
+ /**
1084
+ * Compile a condition tree (fields / and / or / not) to SQL. `idFor` renders a
1085
+ * key to a quoted identifier — `quoteId` for single-table, `qualify` for joins —
1086
+ * so select/update/delete/join all share this one compiler.
1087
+ */
1088
+ compileCondition(node, params, idFor, encode = (_key, value) => value) {
1089
+ if (!node) return "";
1090
+ switch (node.kind) {
1091
+ case "fields": {
1092
+ const conditions = [];
1093
+ for (const [key, value] of Object.entries(node.fields)) {
1094
+ const id = idFor(key);
1095
+ if (isExpression(value)) {
1096
+ conditions.push(
1097
+ this.compileExprOperator(
1098
+ id,
1099
+ "eq",
1100
+ this.renderExpr(value.node, params, idFor)
1101
+ )
1102
+ );
1103
+ } else if (isOperatorObject(value)) {
1104
+ for (const [op, operand] of Object.entries(value)) {
1105
+ conditions.push(
1106
+ isExpression(operand) ? this.compileExprOperator(
1107
+ id,
1108
+ op,
1109
+ this.renderExpr(operand.node, params, idFor)
1110
+ ) : this.compileOperator(
1111
+ id,
1112
+ op,
1113
+ encodeOperand(op, operand, (v) => encode(key, v)),
1114
+ params
1115
+ )
1116
+ );
1117
+ }
1118
+ } else {
1119
+ const operand = encode(key, value);
1120
+ conditions.push(
1121
+ operand === null ? `${id} IS NULL` : `${id} = ${params.bind(operand)}`
1122
+ );
1123
+ }
1124
+ }
1125
+ return conditions.join(" AND ");
1126
+ }
1127
+ case "and":
1128
+ case "or": {
1129
+ const parts = node.parts.map((p) => this.compileCondition(p, params, idFor, encode)).filter((s) => s.length > 0);
1130
+ if (parts.length === 0) return "";
1131
+ const sep = node.kind === "and" ? " AND " : " OR ";
1132
+ return parts.map((p) => `(${p})`).join(sep);
1133
+ }
1134
+ case "not": {
1135
+ const inner = this.compileCondition(node.part, params, idFor, encode);
1136
+ return inner ? `NOT (${inner})` : "";
1137
+ }
1138
+ case "exists": {
1139
+ const select = node.select;
1140
+ this.checkSubquery(select);
1141
+ const keyword = node.negate ? "NOT EXISTS" : "EXISTS";
1142
+ return `${keyword} (${this.compileSelect(select, params)})`;
1143
+ }
1144
+ case "fullText":
1145
+ return this.compileFullText(node, params, idFor);
1146
+ case "compare": {
1147
+ const left = this.renderExpr(node.left, params, idFor);
1148
+ if (node.right.kind === "value") {
1149
+ return this.compileOperator(left, node.op, node.right.value, params);
1150
+ }
1151
+ return this.compileExprOperator(
1152
+ left,
1153
+ node.op,
1154
+ this.renderExpr(node.right, params, idFor)
1155
+ );
1156
+ }
1157
+ }
1158
+ }
1159
+ /**
1160
+ * Compile a full-text condition.
1161
+ *
1162
+ * PostgreSQL gets the real thing (`@@ websearch_to_tsquery`); the dialects with
1163
+ * no text-search engine override this and compile the node's prebuilt substring
1164
+ * fallback instead, so the query still returns the right rows.
1165
+ *
1166
+ * @param node The full-text condition node.
1167
+ * @param params The parameter collector.
1168
+ * @param idFor Column-name resolver.
1169
+ * @returns The rendered condition.
1170
+ */
1171
+ compileFullText(node, params, idFor) {
1172
+ const config = params.bind(node.language);
1173
+ return `${this.tsVector(node.columns, config, idFor)} @@ websearch_to_tsquery(${config}::regconfig, ${params.bind(node.term)})`;
1174
+ }
1175
+ /**
1176
+ * Render one side of a comparison.
1177
+ *
1178
+ * A column reference goes through `idFor`, so an explicit `.name()` mapping and
1179
+ * join qualification apply here exactly as they do in the object form of
1180
+ * `where` — `col()` is not a way around them. Only a `value` node binds.
1181
+ *
1182
+ * @param node The expression AST.
1183
+ * @param params The parameter collector.
1184
+ * @param idFor The identifier resolver for the enclosing statement.
1185
+ * @returns The SQL text of the expression.
1186
+ */
1187
+ /**
1188
+ * Render what an aggregate is applied to: `*`, a column, or an expression.
1189
+ *
1190
+ * An expression operand is what makes a conditional aggregate
1191
+ * (`SUM(CASE WHEN ... END)`) expressible — one pass over the table instead of a
1192
+ * query per bucket.
1193
+ *
1194
+ * @param agg The aggregate term.
1195
+ * @param params The parameter collector.
1196
+ * @param names The node's column-name map.
1197
+ * @returns The rendered operand.
1198
+ */
1199
+ aggregateOperand(agg, params, names) {
1200
+ if (agg.column === "*") return "*";
1201
+ if (typeof agg.column === "string") return this.columnId(agg.column, names);
1202
+ return this.renderExpr(agg.column, params, (k) => this.columnId(k, names));
1203
+ }
1204
+ renderExpr(node, params, idFor) {
1205
+ switch (node.kind) {
1206
+ case "column":
1207
+ return idFor(node.name);
1208
+ case "value":
1209
+ return params.bind(node.value);
1210
+ case "fn": {
1211
+ const args = node.args.map((a) => this.renderExpr(a, params, idFor)).join(", ");
1212
+ return `${node.name}(${args})`;
1213
+ }
1214
+ case "case": {
1215
+ const branches = node.branches.map(
1216
+ (b) => `WHEN ${this.compileCondition(b.when, params, idFor)} THEN ${this.renderExpr(b.result, params, idFor)}`
1217
+ ).join(" ");
1218
+ const fallback = node.fallback === null ? "" : ` ELSE ${this.renderExpr(node.fallback, params, idFor)}`;
1219
+ return `CASE ${branches}${fallback} END`;
1220
+ }
1221
+ case "cast":
1222
+ return `CAST(${this.renderExpr(node.operand, params, idFor)} AS ${this.castTypeName(node.to)})`;
1223
+ case "scalar": {
1224
+ const select = node.select;
1225
+ this.checkSubquery(select);
1226
+ return `(${this.compileSelect(select, params)})`;
1227
+ }
1228
+ case "rank":
1229
+ return this.renderRank(node.columns, node.term, node.language, params, idFor);
1230
+ case "star":
1231
+ return "*";
1232
+ case "window": {
1233
+ const call = this.renderExpr(node.fn, params, idFor);
1234
+ const parts = [];
1235
+ if (node.partitionBy.length > 0) {
1236
+ parts.push(`PARTITION BY ${node.partitionBy.map(idFor).join(", ")}`);
1237
+ }
1238
+ if (node.orderBy.length > 0) {
1239
+ const terms = node.orderBy.map((t) => `${idFor(t.column)} ${t.direction === "desc" ? "DESC" : "ASC"}`).join(", ");
1240
+ parts.push(`ORDER BY ${terms}`);
1241
+ }
1242
+ if (node.frame) parts.push(node.frame);
1243
+ return `${call} OVER (${parts.join(" ")})`;
1244
+ }
1245
+ }
1246
+ }
1247
+ /**
1248
+ * Render a full-text relevance score.
1249
+ *
1250
+ * PostgreSQL has `ts_rank`; the others have nothing equivalent, and they
1251
+ * override this to a constant so that ordering by it is a no-op rather than a
1252
+ * compile error — the fallback keeps returning the right rows, only unranked.
1253
+ *
1254
+ * @param columns The columns making up the document.
1255
+ * @param term The search term.
1256
+ * @param language The text-search configuration.
1257
+ * @param params The parameter collector.
1258
+ * @param idFor Column-name resolver.
1259
+ * @returns The rendered score expression.
1260
+ */
1261
+ renderRank(columns, term, language, params, idFor) {
1262
+ const config = params.bind(language);
1263
+ return `ts_rank(${this.tsVector(columns, config, idFor)}, websearch_to_tsquery(${config}::regconfig, ${params.bind(term)}))`;
1264
+ }
1265
+ /**
1266
+ * Build the `to_tsvector(...)` document out of the searched columns.
1267
+ *
1268
+ * `coalesce(col, '')` matters: in SQL a `NULL` anywhere in a concatenation makes
1269
+ * the whole document `NULL`, so one empty column would silently exclude the row.
1270
+ *
1271
+ * @param columns The columns making up the document.
1272
+ * @param config The already-bound placeholder for the text-search config.
1273
+ * @param idFor Column-name resolver.
1274
+ * @returns The rendered `to_tsvector(...)` call.
1275
+ */
1276
+ tsVector(columns, config, idFor) {
1277
+ const document = columns.map((c) => `coalesce(${idFor(c)}, '')`).join(" || ' ' || ");
1278
+ return `to_tsvector(${config}::regconfig, ${document})`;
1279
+ }
1280
+ /**
1281
+ * The SQL type name this dialect accepts in a `CAST`.
1282
+ *
1283
+ * The base mapping is the standard one PostgreSQL takes; SQLite and MySQL
1284
+ * override it, because the names genuinely differ (MySQL's `CAST(x AS SIGNED)`
1285
+ * is not `INTEGER`, and SQLite only has five storage classes to aim at).
1286
+ *
1287
+ * @param to The portable cast target.
1288
+ * @returns The dialect's own type name.
1289
+ */
1290
+ castTypeName(to) {
1291
+ switch (to) {
1292
+ case "integer":
1293
+ return "INTEGER";
1294
+ case "bigint":
1295
+ return "BIGINT";
1296
+ case "real":
1297
+ return "DOUBLE PRECISION";
1298
+ case "numeric":
1299
+ return "NUMERIC";
1300
+ case "text":
1301
+ return "TEXT";
1302
+ case "boolean":
1303
+ return "BOOLEAN";
1304
+ case "date":
1305
+ return "DATE";
1306
+ case "datetime":
1307
+ case "timestamp":
1308
+ return "TIMESTAMP";
1309
+ case "uuid":
1310
+ return "UUID";
1311
+ case "json":
1312
+ return "JSON";
1313
+ case "jsonb":
1314
+ return "JSONB";
1315
+ case "blob":
1316
+ return "BYTEA";
1317
+ }
1318
+ }
1319
+ /**
1320
+ * Compile a comparison whose right-hand side is another expression rather than
1321
+ * a bound value (`total > paid`, `lower(a) = lower(b)`).
1322
+ *
1323
+ * The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
1324
+ * a value operand, and accepting an expression there would silently compile to
1325
+ * something else.
1326
+ *
1327
+ * @param left The rendered left-hand side.
1328
+ * @param op The operator name.
1329
+ * @param right The rendered right-hand side.
1330
+ * @returns The SQL text of the predicate.
1331
+ * @throws Error When the operator needs a value operand.
1332
+ */
1333
+ compileExprOperator(left, op, right) {
1334
+ switch (op) {
1335
+ case "eq":
1336
+ return `${left} = ${right}`;
1337
+ case "ne":
1338
+ return `${left} <> ${right}`;
1339
+ case "gt":
1340
+ return `${left} > ${right}`;
1341
+ case "gte":
1342
+ return `${left} >= ${right}`;
1343
+ case "lt":
1344
+ return `${left} < ${right}`;
1345
+ case "lte":
1346
+ return `${left} <= ${right}`;
1347
+ case "like":
1348
+ return `${left} LIKE ${right}`;
1349
+ case "ilike":
1350
+ return this.ilike(left, right);
1351
+ case "ieq":
1352
+ return `lower(${left}) = lower(${right})`;
1353
+ case "iContains":
1354
+ throw new Error(
1355
+ 'The "iContains" operator matches a literal, so it takes a value, not an expression.'
1356
+ );
1357
+ case "contains":
1358
+ case "containedBy":
1359
+ case "overlaps":
1360
+ return `${left} ${this.arrayOperator(op)} ${right}`;
1361
+ default:
1362
+ throw new Error(`The "${op}" operator takes a value operand, not an expression.`);
1363
+ }
1364
+ }
1365
+ compileOperator(id, op, operand, params) {
1366
+ switch (op) {
1367
+ case "eq":
1368
+ return operand === null ? `${id} IS NULL` : `${id} = ${params.bind(operand)}`;
1369
+ case "ne":
1370
+ return operand === null ? `${id} IS NOT NULL` : `${id} <> ${params.bind(operand)}`;
1371
+ case "gt":
1372
+ return `${id} > ${params.bind(operand)}`;
1373
+ case "gte":
1374
+ return `${id} >= ${params.bind(operand)}`;
1375
+ case "lt":
1376
+ return `${id} < ${params.bind(operand)}`;
1377
+ case "lte":
1378
+ return `${id} <= ${params.bind(operand)}`;
1379
+ case "like":
1380
+ return `${id} LIKE ${params.bind(operand)}`;
1381
+ case "ilike":
1382
+ return this.ilike(id, params.bind(operand));
1383
+ case "ieq":
1384
+ return operand === null ? `${id} IS NULL` : `lower(${id}) = lower(${params.bind(operand)})`;
1385
+ case "iContains":
1386
+ return this.ilikeEscaped(id, params.bind(`%${escapeLike(String(operand))}%`));
1387
+ case "contains":
1388
+ return `${id} ${this.arrayOperator("contains")} ${params.bind(operand)}`;
1389
+ case "containedBy":
1390
+ return `${id} ${this.arrayOperator("containedBy")} ${params.bind(operand)}`;
1391
+ case "overlaps":
1392
+ return `${id} ${this.arrayOperator("overlaps")} ${params.bind(operand)}`;
1393
+ case "in":
1394
+ return this.compileIn(id, operand, params, false);
1395
+ case "notIn":
1396
+ return this.compileIn(id, operand, params, true);
1397
+ case "between": {
1398
+ const [lo, hi] = operand;
1399
+ return `${id} BETWEEN ${params.bind(lo)} AND ${params.bind(hi)}`;
1400
+ }
1401
+ case "isNull":
1402
+ return operand ? `${id} IS NULL` : `${id} IS NOT NULL`;
1403
+ default:
1404
+ throw new Error(`Unknown operator ${JSON.stringify(op)}`);
1405
+ }
1406
+ }
1407
+ /**
1408
+ * Compile `IN` / `NOT IN`, whose operand is either a value list or a
1409
+ * single-column subquery.
1410
+ *
1411
+ * The subquery is rendered at the position it appears in the outer statement
1412
+ * and shares the same parameter collector, so its own placeholders land in the
1413
+ * right order — and it keeps its own `names` map, since the inner model may use
1414
+ * a different naming convention than the outer one.
1415
+ *
1416
+ * @param id The quoted column identifier being tested.
1417
+ * @param operand A list of values, or a {@link Subquery}.
1418
+ * @param params The parameter collector for the statement being compiled.
1419
+ * @param negate True for `NOT IN`.
1420
+ * @returns The SQL text of the predicate.
1421
+ */
1422
+ compileIn(id, operand, params, negate) {
1423
+ const keyword = negate ? "NOT IN" : "IN";
1424
+ if (isSubquery(operand)) {
1425
+ this.checkSubquery(operand.node);
1426
+ return `${id} ${keyword} (${this.compileSelect(operand.node, params)})`;
1427
+ }
1428
+ const values = operand;
1429
+ if (values.length === 0) {
1430
+ return negate ? "1 = 1" : "1 = 0";
1431
+ }
1432
+ const list = values.map((v) => params.bind(v)).join(", ");
1433
+ return `${id} ${keyword} (${list})`;
1434
+ }
1435
+ };
1436
+ var SqliteDialect = class extends BaseDialect {
1437
+ name = "sqlite";
1438
+ /**
1439
+ * SQLite explains with `EXPLAIN QUERY PLAN`, and has no `ANALYZE` — the plain
1440
+ * `EXPLAIN` there dumps bytecode, which answers a different question.
1441
+ */
1442
+ explainPrefix(analyze) {
1443
+ if (analyze) {
1444
+ throw new Error(
1445
+ "SQLite has no EXPLAIN ANALYZE; use EXPLAIN QUERY PLAN (analyze: false)."
1446
+ );
1447
+ }
1448
+ return "EXPLAIN QUERY PLAN";
1449
+ }
1450
+ /**
1451
+ * SQLite has `UPDATE ... FROM` (3.33+) but no `DELETE ... USING`.
1452
+ *
1453
+ * The portable shape there is a subquery — `where({ id: { in: … } })` — so this
1454
+ * throws and says so, rather than emitting a clause SQLite does not parse.
1455
+ */
1456
+ compileExtraSources(keyword, sources) {
1457
+ if (keyword === "USING" && sources && sources.length > 0) {
1458
+ throw new Error(
1459
+ 'SQLite has no DELETE ... USING; filter with a subquery instead: where({ id: { in: select(Other, ["id"]).asSubquery("id") } }).'
1460
+ );
1461
+ }
1462
+ return super.compileExtraSources(keyword, sources);
1463
+ }
1464
+ /**
1465
+ * SQLite has no text-search engine, so the prebuilt substring fallback is
1466
+ * compiled instead. The rows are right; the ranking is what is missing.
1467
+ */
1468
+ compileFullText(node, params, idFor) {
1469
+ return this.compileCondition(node.fallback, params, idFor);
1470
+ }
1471
+ /** No text-search engine means no score: a constant, so ordering by it is inert. */
1472
+ renderRank() {
1473
+ return "0";
1474
+ }
1475
+ /**
1476
+ * SQLite has five storage classes, so most targets collapse onto `TEXT` or
1477
+ * `INTEGER`. Naming a type it does not know would not fail — SQLite applies the
1478
+ * closest affinity — but it would make the cast mean something different here
1479
+ * than on the other databases, which is what this mapping avoids.
1480
+ */
1481
+ castTypeName(to) {
1482
+ switch (to) {
1483
+ case "integer":
1484
+ case "bigint":
1485
+ case "boolean":
1486
+ return "INTEGER";
1487
+ case "real":
1488
+ return "REAL";
1489
+ case "numeric":
1490
+ return "NUMERIC";
1491
+ case "blob":
1492
+ return "BLOB";
1493
+ default:
1494
+ return "TEXT";
1495
+ }
1496
+ }
1497
+ placeholder() {
1498
+ return "?";
1499
+ }
1500
+ ilike(column, param) {
1501
+ return `${column} LIKE ${param}`;
1502
+ }
1503
+ /**
1504
+ * SQLite runs one writer at a time, so its only isolation level **is**
1505
+ * serializable — there is no syntax to ask for another, and no weaker level to
1506
+ * fall back to. Asking for one is an error rather than a silent no-op, since a
1507
+ * caller who wrote `repeatable read` was reasoning about a guarantee.
1508
+ *
1509
+ * `readOnly` likewise has no per-transaction form here (`PRAGMA query_only` is
1510
+ * per connection), so it is refused instead of quietly ignored.
1511
+ */
1512
+ beginStatements(options) {
1513
+ if (options?.isolation && options.isolation !== "serializable") {
1514
+ throw new Error(
1515
+ `SQLite only implements the "serializable" isolation level; ${JSON.stringify(options.isolation)} has no equivalent here.`
1516
+ );
1517
+ }
1518
+ if (options?.readOnly) {
1519
+ throw new Error(
1520
+ "SQLite has no read-only transaction; open the engine with { sqlite: { ... } } on a read-only connection instead."
1521
+ );
1522
+ }
1523
+ return ["BEGIN"];
1524
+ }
1525
+ /**
1526
+ * SQLite has no row-level locking, so a lock request is an error rather than a
1527
+ * silently unlocked `SELECT` — a lock that does not exist only shows up as
1528
+ * duplicated work under production concurrency.
1529
+ */
1530
+ renderLock() {
1531
+ throw new Error(
1532
+ "SQLite has no row-level locking \u2014 FOR UPDATE / FOR SHARE is unsupported. Serialize the claim inside a transaction instead."
1533
+ );
1534
+ }
1535
+ };
1536
+ var PostgresDialect = class extends BaseDialect {
1537
+ name = "postgresql";
1538
+ placeholder(index) {
1539
+ return `$${index}`;
1540
+ }
1541
+ ilike(column, param) {
1542
+ return `${column} ILIKE ${param}`;
1543
+ }
1544
+ arrayOperator(op) {
1545
+ if (op === "contains") return "@>";
1546
+ if (op === "containedBy") return "<@";
1547
+ return "&&";
1548
+ }
1549
+ };
1550
+ var mysqlQuotedIds = /* @__PURE__ */ new Map();
1551
+ var MysqlDialect = class extends BaseDialect {
1552
+ name = "mysql";
1553
+ placeholder() {
1554
+ return "?";
1555
+ }
1556
+ ilike(column, param) {
1557
+ return `${column} LIKE ${param}`;
1558
+ }
1559
+ /** MySQL's `EXPLAIN FORMAT=JSON` spells the option differently. */
1560
+ explainPrefix(analyze) {
1561
+ return analyze ? "EXPLAIN ANALYZE" : "EXPLAIN FORMAT=JSON";
1562
+ }
1563
+ /**
1564
+ * MySQL only gained `INTERSECT`/`EXCEPT` in 8.0.31, and this project does not
1565
+ * invest in MySQL beyond what already works — so they are refused here rather
1566
+ * than emitted against a server that may reject them.
1567
+ */
1568
+ setOperator(op) {
1569
+ if (op === "intersect" || op === "except") {
1570
+ throw new Error(
1571
+ `MySQL support for ${op.toUpperCase()} is out of scope for tempest-db-js; express it with a join or NOT EXISTS.`
1572
+ );
1573
+ }
1574
+ return super.setOperator(op);
1575
+ }
1576
+ /**
1577
+ * MySQL writes multi-table updates as `UPDATE a JOIN b`, and has no
1578
+ * `DELETE ... USING` in this shape. Both are out of the project's active scope,
1579
+ * so they are refused rather than emitted against a server that rejects them.
1580
+ */
1581
+ compileExtraSources(keyword, sources) {
1582
+ if (sources && sources.length > 0) {
1583
+ throw new Error(
1584
+ `MySQL does not take ${keyword} on a write in this form; it spells multi-table writes as UPDATE a JOIN b, which is out of scope for tempest-db-js.`
1585
+ );
1586
+ }
1587
+ return "";
1588
+ }
1589
+ /**
1590
+ * MySQL's full-text search needs a `FULLTEXT` index and different syntax, and it
1591
+ * is outside this project's active scope — the substring fallback is compiled,
1592
+ * like on SQLite.
1593
+ */
1594
+ compileFullText(node, params, idFor) {
1595
+ return this.compileCondition(node.fallback, params, idFor);
1596
+ }
1597
+ /** No `ts_rank` equivalent in scope: a constant, so ordering by it is inert. */
1598
+ renderRank() {
1599
+ return "0";
1600
+ }
1601
+ /**
1602
+ * MySQL sets the level with a statement **before** the transaction opens, and
1603
+ * spells the read-only flag on `START TRANSACTION` rather than on `BEGIN`.
1604
+ */
1605
+ beginStatements(options) {
1606
+ const statements = [];
1607
+ if (options?.isolation) {
1608
+ statements.push(
1609
+ `SET TRANSACTION ISOLATION LEVEL ${options.isolation.toUpperCase()}`
1610
+ );
1611
+ }
1612
+ statements.push(options?.readOnly ? "START TRANSACTION READ ONLY" : "BEGIN");
1613
+ return statements;
1614
+ }
1615
+ /**
1616
+ * MySQL's `CAST` takes its own vocabulary — `SIGNED`, not `INTEGER`; `CHAR`,
1617
+ * not `TEXT` — and rejects the standard names outright.
1618
+ */
1619
+ castTypeName(to) {
1620
+ switch (to) {
1621
+ case "integer":
1622
+ case "bigint":
1623
+ case "boolean":
1624
+ return "SIGNED";
1625
+ case "real":
1626
+ case "numeric":
1627
+ return "DECIMAL";
1628
+ case "text":
1629
+ case "uuid":
1630
+ return "CHAR";
1631
+ case "date":
1632
+ return "DATE";
1633
+ case "datetime":
1634
+ case "timestamp":
1635
+ return "DATETIME";
1636
+ case "json":
1637
+ case "jsonb":
1638
+ return "JSON";
1639
+ case "blob":
1640
+ return "BINARY";
1641
+ }
1642
+ }
1643
+ quoteId(name) {
1644
+ const cached = mysqlQuotedIds.get(name);
1645
+ if (cached !== void 0) return cached;
1646
+ const quoted = `\`${name.replace(/`/g, "``")}\``;
1647
+ mysqlQuotedIds.set(name, quoted);
1648
+ return quoted;
1649
+ }
1650
+ /**
1651
+ * MySQL rejects `LIMIT` inside an `IN` subquery with
1652
+ * `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
1653
+ * 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
1654
+ * instead of surfacing that error from the driver at runtime.
1655
+ */
1656
+ checkSubquery(node) {
1657
+ if (node.limit !== void 0 || node.offset !== void 0) {
1658
+ throw new Error(
1659
+ "MySQL does not support LIMIT/OFFSET inside an IN subquery. Select the ids first and pass them as a list, or wrap the subquery in a derived table."
1660
+ );
1661
+ }
1662
+ }
1663
+ renderConflict(onConflict, conflictCols, nextValue, names) {
1664
+ if (onConflict.targetWhere || onConflict.updateWhere) {
1665
+ throw new Error(
1666
+ "MySQL's ON DUPLICATE KEY UPDATE has no conflict-target predicate \u2014 a partial unique index is PostgreSQL/SQLite only."
1667
+ );
1668
+ }
1669
+ if (onConflict.update === "nothing") {
1670
+ const col = this.columnId(onConflict.target[0] ?? "id", names);
1671
+ return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
1672
+ }
1673
+ const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
1674
+ return ` ON DUPLICATE KEY UPDATE ${assignments}`;
1675
+ }
1676
+ /**
1677
+ * MySQL has no `RETURNING`, so it cannot be compiled into a statement.
1678
+ *
1679
+ * `session.execute()` still honors `.returning()` on a **single-row INSERT** by
1680
+ * running the insert and reading the row back by key on the same connection —
1681
+ * that is execution, not compilation, so it never reaches here. Compiling a
1682
+ * node with `returning` directly is an error, rather than SQL that silently
1683
+ * returns nothing.
1684
+ */
1685
+ compileReturning(returning) {
1686
+ if (returning === null) return "";
1687
+ throw new Error(
1688
+ "RETURNING cannot be compiled for MySQL. session.execute() reads a single-row INSERT back by key (LAST_INSERT_ID()); UPDATE/DELETE have no equivalent \u2014 run a SELECT yourself."
1689
+ );
1690
+ }
1691
+ };
1692
+ function getDialect(name) {
1693
+ switch (name) {
1694
+ case "sqlite":
1695
+ return new SqliteDialect();
1696
+ case "postgresql":
1697
+ return new PostgresDialect();
1698
+ case "mysql":
1699
+ return new MysqlDialect();
1700
+ }
1701
+ }
1702
+
207
1703
  // src/engine.ts
208
- module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bin.cjs', document.baseURI).href)));
1704
+ var nodeRequire = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bin.cjs', document.baseURI).href)));
1705
+ function encodeSqliteParam(value) {
1706
+ if (value === void 0 || value === null) return null;
1707
+ if (typeof value === "boolean") return value ? 1 : 0;
1708
+ if (value instanceof Date) return value.toISOString();
1709
+ if (value instanceof Uint8Array) return value;
1710
+ if (typeof value === "object") return JSON.stringify(value);
1711
+ return value;
1712
+ }
1713
+ var NodeSqliteDriver = class _NodeSqliteDriver {
1714
+ // biome-ignore lint/suspicious/noExplicitAny: node:sqlite DatabaseSync has no shipped types here.
1715
+ db;
1716
+ /**
1717
+ * Prepared-statement cache keyed by SQL text. tempest-db-js always
1718
+ * parameterizes, so a query shape maps to one stable SQL string — reusing the
1719
+ * compiled statement avoids re-`prepare()` on every call (the dominant cost of
1720
+ * per-row inserts and point lookups).
1721
+ */
1722
+ // biome-ignore lint/suspicious/noExplicitAny: node:sqlite StatementSync has no shipped types here.
1723
+ statements = /* @__PURE__ */ new Map();
1724
+ // biome-ignore lint/suspicious/noExplicitAny: accept an already-open DatabaseSync handle.
1725
+ constructor(database) {
1726
+ this.db = database;
1727
+ }
1728
+ /**
1729
+ * Open a `node:sqlite` database at the given path (or `:memory:`).
1730
+ *
1731
+ * @param path The database file, or `":memory:"`.
1732
+ * @param options Passed straight to `DatabaseSync` (`readOnly`, `timeout`, …).
1733
+ * @returns A driver over the open handle.
1734
+ */
1735
+ static open(path, options) {
1736
+ const { DatabaseSync } = nodeRequire("node:sqlite");
1737
+ return new _NodeSqliteDriver(
1738
+ options ? new DatabaseSync(path, { ...options }) : new DatabaseSync(path)
1739
+ );
1740
+ }
1741
+ /** Return the cached prepared statement for `sql`, preparing it on first use. */
1742
+ // biome-ignore lint/suspicious/noExplicitAny: statement type is unavailable here.
1743
+ prepare(sql) {
1744
+ const cached = this.statements.get(sql);
1745
+ if (cached) return cached;
1746
+ const stmt = this.db.prepare(sql);
1747
+ this.statements.set(sql, stmt);
1748
+ return stmt;
1749
+ }
1750
+ execute(sql, params) {
1751
+ const stmt = this.prepare(sql);
1752
+ const bound = params.map(encodeSqliteParam);
1753
+ if (returnsRows(sql)) {
1754
+ return { rows: stmt.all(...bound), changes: 0 };
1755
+ }
1756
+ const info = stmt.run(...bound);
1757
+ return { rows: [], changes: Number(info.changes ?? 0) };
1758
+ }
1759
+ *iterate(sql, params) {
1760
+ const stmt = this.prepare(sql);
1761
+ const bound = params.map(encodeSqliteParam);
1762
+ yield* stmt.iterate(...bound);
1763
+ }
1764
+ close() {
1765
+ this.statements.clear();
1766
+ this.db.close();
1767
+ }
1768
+ };
1769
+ function returnsRows(sql) {
1770
+ return /^\s*(select|with|values|table|pragma|explain)\b/i.test(sql) || /\breturning\b/i.test(sql);
1771
+ }
209
1772
  function toAsyncDriver(driver) {
210
1773
  return {
211
1774
  async execute(sql, params) {
@@ -217,6 +1780,104 @@ function toAsyncDriver(driver) {
217
1780
  };
218
1781
  }
219
1782
 
1783
+ // src/backup.ts
1784
+ var run = util.promisify(child_process.execFile);
1785
+ var BackupToolMissing = class extends Error {
1786
+ constructor(tool) {
1787
+ super(`${tool} is not on the PATH; install the PostgreSQL client tools to back up.`);
1788
+ this.name = "BackupToolMissing";
1789
+ }
1790
+ };
1791
+ var UnsupportedBackupBackend = class extends Error {
1792
+ constructor(dialect) {
1793
+ super(
1794
+ `Backups are implemented for PostgreSQL and SQLite; ${dialect} is not covered.`
1795
+ );
1796
+ this.name = "UnsupportedBackupBackend";
1797
+ }
1798
+ };
1799
+ function backupFormat(file) {
1800
+ return file.endsWith(".sql") ? "plain" : "custom";
1801
+ }
1802
+ function postgresEnv(password) {
1803
+ return password ? { ...process.env, PGPASSWORD: password } : { ...process.env };
1804
+ }
1805
+ function toolUrl(url) {
1806
+ return url.replace(/^([a-z0-9]+)\+[a-z0-9_-]+:/i, "$1:");
1807
+ }
1808
+ async function spawn(tool, args, env) {
1809
+ try {
1810
+ await run(tool, [...args], { env, maxBuffer: 1024 * 1024 * 64 });
1811
+ } catch (error) {
1812
+ if (error.code === "ENOENT") {
1813
+ throw new BackupToolMissing(tool);
1814
+ }
1815
+ throw error;
1816
+ }
1817
+ }
1818
+ async function backupDatabase(url, file, options) {
1819
+ const parsed = parseDatabaseUrl(url);
1820
+ if (parsed.dialect === "sqlite") {
1821
+ const source = parsed.database ?? ":memory:";
1822
+ const driver = NodeSqliteDriver.open(source);
1823
+ try {
1824
+ driver.execute("VACUUM INTO ?", [file]);
1825
+ } finally {
1826
+ driver.close();
1827
+ }
1828
+ return { file, dialect: "sqlite", via: "VACUUM INTO" };
1829
+ }
1830
+ if (parsed.dialect !== "postgresql") {
1831
+ throw new UnsupportedBackupBackend(parsed.dialect);
1832
+ }
1833
+ const format = backupFormat(file);
1834
+ await spawn(
1835
+ "pg_dump",
1836
+ [
1837
+ "--dbname",
1838
+ toolUrl(url),
1839
+ ...format === "custom" ? ["--format=custom"] : [],
1840
+ "--file",
1841
+ file,
1842
+ ...options?.extraArgs ?? []
1843
+ ],
1844
+ postgresEnv(parsed.password)
1845
+ );
1846
+ return { file, dialect: "postgresql", via: "pg_dump" };
1847
+ }
1848
+ async function restoreDatabase(url, file, options) {
1849
+ const parsed = parseDatabaseUrl(url);
1850
+ if (parsed.dialect === "sqlite") {
1851
+ const target = parsed.database ?? ":memory:";
1852
+ await promises$1.copyFile(file, target);
1853
+ return { file, dialect: "sqlite", via: "copy" };
1854
+ }
1855
+ if (parsed.dialect !== "postgresql") {
1856
+ throw new UnsupportedBackupBackend(parsed.dialect);
1857
+ }
1858
+ const env = postgresEnv(parsed.password);
1859
+ if (backupFormat(file) === "plain") {
1860
+ await spawn(
1861
+ "psql",
1862
+ ["--dbname", toolUrl(url), "--file", file, ...options?.extraArgs ?? []],
1863
+ env
1864
+ );
1865
+ return { file, dialect: "postgresql", via: "psql" };
1866
+ }
1867
+ await spawn(
1868
+ "pg_restore",
1869
+ [
1870
+ "--dbname",
1871
+ toolUrl(url),
1872
+ ...options?.force ? ["--clean", "--if-exists"] : [],
1873
+ file,
1874
+ ...options?.extraArgs ?? []
1875
+ ],
1876
+ env
1877
+ );
1878
+ return { file, dialect: "postgresql", via: "pg_restore" };
1879
+ }
1880
+
220
1881
  // src/migrations/operations.ts
221
1882
  var IrreversibleMigration = class extends Error {
222
1883
  constructor(message) {
@@ -250,6 +1911,10 @@ function invert(op) {
250
1911
  return { kind: "recreate_table", from: op.to, to: op.from };
251
1912
  case "add_constraint":
252
1913
  return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
1914
+ case "create_index":
1915
+ return { kind: "drop_index", table: op.table, index: op.index };
1916
+ case "drop_index":
1917
+ return { kind: "create_index", table: op.table, index: op.index };
253
1918
  case "drop_constraint":
254
1919
  return { kind: "add_constraint", table: op.table, constraint: op.constraint };
255
1920
  case "execute":
@@ -440,7 +2105,8 @@ function renderDefault(def, dialect, type) {
440
2105
  "sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
441
2106
  );
442
2107
  }
443
- return renderPortableToken(expr, dialect);
2108
+ const rendered = renderPortableToken(expr, dialect);
2109
+ return dialect === "sqlite" && rendered.includes("(") ? `(${rendered})` : rendered;
444
2110
  }
445
2111
  const value = def.value;
446
2112
  if (value === null) return "NULL";
@@ -488,9 +2154,31 @@ function renderForeignKeyConstraint(fk, dialect) {
488
2154
  function tableConstraintClauses(table, dialect) {
489
2155
  return [
490
2156
  ...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
491
- ...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
2157
+ ...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect)),
2158
+ ...table.checks.map((ck) => renderCheckConstraint(ck, dialect))
492
2159
  ];
493
2160
  }
2161
+ function renderPredicate(node, dialect) {
2162
+ const literals = new LiteralParams();
2163
+ return getDialect(dialect).renderConditionLiteral(node, literals);
2164
+ }
2165
+ function renderCheckConstraint(ck, dialect) {
2166
+ return `CONSTRAINT ${quoteId(ck.name, dialect)} CHECK (${renderPredicate(ck.expression, dialect)})`;
2167
+ }
2168
+ function renderCreateIndex(table, ix, dialect) {
2169
+ if (ix.where && dialect === "mysql") {
2170
+ throw new Error(
2171
+ `MySQL has no partial index; ${ix.name} on ${table} declares a WHERE predicate.`
2172
+ );
2173
+ }
2174
+ const unique = ix.unique ? "UNIQUE " : "";
2175
+ const cols = ix.columns.map((c) => quoteId(c, dialect)).join(", ");
2176
+ const where = ix.where ? ` WHERE ${renderPredicate(ix.where, dialect)}` : "";
2177
+ return `CREATE ${unique}INDEX ${quoteId(ix.name, dialect)} ON ${quoteId(table, dialect)} (${cols})${where}`;
2178
+ }
2179
+ function renderDropIndex(table, ix, dialect) {
2180
+ return dialect === "mysql" ? `DROP INDEX ${quoteId(ix.name, dialect)} ON ${quoteId(table, dialect)}` : `DROP INDEX ${quoteId(ix.name, dialect)}`;
2181
+ }
494
2182
  function renderColumnDef(col, dialect) {
495
2183
  let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
496
2184
  if (col.notNull) sql += " NOT NULL";
@@ -543,7 +2231,10 @@ function renderCreateTable(table, dialect) {
543
2231
  ...typeStmts,
544
2232
  `CREATE TABLE ${quoteId(table.name, dialect)} (
545
2233
  ${cols.join(",\n ")}
546
- )`
2234
+ )`,
2235
+ // Indexes are separate statements, not table clauses — which is also why a
2236
+ // SQLite table rebuild has to recreate them.
2237
+ ...table.indexes.map((ix) => renderCreateIndex(table.name, ix, dialect))
547
2238
  ];
548
2239
  }
549
2240
  function renderOperation(op, dialect) {
@@ -576,6 +2267,10 @@ function renderOperation(op, dialect) {
576
2267
  return renderAddConstraint(op.table, op.constraint, dialect);
577
2268
  case "drop_constraint":
578
2269
  return renderDropConstraint(op.table, op.constraint, dialect);
2270
+ case "create_index":
2271
+ return [renderCreateIndex(op.table, op.index, dialect)];
2272
+ case "drop_index":
2273
+ return [renderDropIndex(op.table, op.index, dialect)];
579
2274
  case "execute":
580
2275
  return [op.up];
581
2276
  }
@@ -586,7 +2281,7 @@ function renderAddConstraint(table, constraint, dialect) {
586
2281
  `add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
587
2282
  );
588
2283
  }
589
- const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
2284
+ const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : constraint.type === "check" ? renderCheckConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
590
2285
  return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
591
2286
  }
592
2287
  function renderDropConstraint(table, constraint, dialect) {
@@ -623,6 +2318,10 @@ function renderSqliteRebuild(from, to) {
623
2318
  common.length > 0 ? `INSERT INTO ${quoteId(tmp, "sqlite")} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name, "sqlite")}` : `-- no common columns to copy from ${from.name}`,
624
2319
  `DROP TABLE ${quoteId(from.name, "sqlite")}`,
625
2320
  `ALTER TABLE ${quoteId(tmp, "sqlite")} RENAME TO ${quoteId(to.name, "sqlite")}`,
2321
+ // The rebuild dropped the old table, and its indexes with it: SQLite ties an
2322
+ // index to the table it was created on. Recreating them is part of the
2323
+ // rebuild, not a follow-up somebody has to remember.
2324
+ ...to.indexes.map((ix) => renderCreateIndex(to.name, ix, "sqlite")),
626
2325
  "PRAGMA foreign_keys=on"
627
2326
  ];
628
2327
  }
@@ -725,8 +2424,49 @@ function diffConstraints(current, target) {
725
2424
  ops.push(addForeignKey(table, tgt));
726
2425
  }
727
2426
  }
2427
+ const currentCk = new Map(current.checks.map((c) => [c.name, c]));
2428
+ const targetCk = new Map(target.checks.map((c) => [c.name, c]));
2429
+ for (const [name, cur] of currentCk) {
2430
+ const tgt = targetCk.get(name);
2431
+ if (!tgt || checkSignature(cur) !== checkSignature(tgt)) {
2432
+ ops.push({ kind: "drop_constraint", table, constraint: checkNamed(cur) });
2433
+ }
2434
+ }
2435
+ for (const [name, tgt] of targetCk) {
2436
+ const cur = currentCk.get(name);
2437
+ if (!cur || checkSignature(cur) !== checkSignature(tgt)) {
2438
+ ops.push({ kind: "add_constraint", table, constraint: checkNamed(tgt) });
2439
+ }
2440
+ }
2441
+ const currentIx = new Map(current.indexes.map((i) => [i.name, i]));
2442
+ const targetIx = new Map(target.indexes.map((i) => [i.name, i]));
2443
+ for (const [name, cur] of currentIx) {
2444
+ const tgt = targetIx.get(name);
2445
+ if (!tgt || indexSignature(cur) !== indexSignature(tgt)) {
2446
+ ops.push({ kind: "drop_index", table, index: cur });
2447
+ }
2448
+ }
2449
+ for (const [name, tgt] of targetIx) {
2450
+ const cur = currentIx.get(name);
2451
+ if (!cur || indexSignature(cur) !== indexSignature(tgt)) {
2452
+ ops.push({ kind: "create_index", table, index: tgt });
2453
+ }
2454
+ }
728
2455
  return ops;
729
2456
  }
2457
+ function checkSignature(ck) {
2458
+ return JSON.stringify(ck.expression);
2459
+ }
2460
+ function indexSignature(ix) {
2461
+ return JSON.stringify({
2462
+ columns: ix.columns,
2463
+ unique: ix.unique,
2464
+ where: ix.where
2465
+ });
2466
+ }
2467
+ function checkNamed(ck) {
2468
+ return { type: "check", constraint: ck };
2469
+ }
730
2470
  function uniqueNamed(uc) {
731
2471
  return { type: "unique", constraint: uc };
732
2472
  }
@@ -841,6 +2581,41 @@ function heads(migrations) {
841
2581
  function constraintName(prefix, table, columns) {
842
2582
  return `${prefix}_${table}_${columns.join("_")}`;
843
2583
  }
2584
+ function renameConditionColumns(node, toColumn) {
2585
+ const expr = (current) => {
2586
+ switch (current.kind) {
2587
+ case "column":
2588
+ return { kind: "column", name: toColumn(current.name) };
2589
+ case "fn":
2590
+ return { ...current, args: current.args.map(expr) };
2591
+ case "cast":
2592
+ return { ...current, operand: expr(current.operand) };
2593
+ default:
2594
+ return current;
2595
+ }
2596
+ };
2597
+ switch (node.kind) {
2598
+ case "fields": {
2599
+ const fields = {};
2600
+ for (const [key, value] of Object.entries(node.fields)) {
2601
+ fields[toColumn(key)] = value;
2602
+ }
2603
+ return { kind: "fields", fields };
2604
+ }
2605
+ case "and":
2606
+ case "or":
2607
+ return {
2608
+ ...node,
2609
+ parts: node.parts.map((part) => renameConditionColumns(part, toColumn))
2610
+ };
2611
+ case "not":
2612
+ return { kind: "not", part: renameConditionColumns(node.part, toColumn) };
2613
+ case "compare":
2614
+ return { ...node, left: expr(node.left), right: expr(node.right) };
2615
+ default:
2616
+ return node;
2617
+ }
2618
+ }
844
2619
  function reflectTable(model) {
845
2620
  const names = columnNamesOf(model);
846
2621
  const toColumn = (prop) => names?.[prop] ?? prop;
@@ -862,6 +2637,8 @@ function reflectTable(model) {
862
2637
  }
863
2638
  const uniqueConstraints = [];
864
2639
  const foreignKeys = [];
2640
+ const checks = [];
2641
+ const indexes = [];
865
2642
  for (const c of model.tableArgs?.() ?? []) {
866
2643
  const cols = c.columns.map(toColumn);
867
2644
  if (c.kind === "unique") {
@@ -869,6 +2646,18 @@ function reflectTable(model) {
869
2646
  name: c.name ?? constraintName("uq", model.tablename, cols),
870
2647
  columns: cols
871
2648
  });
2649
+ } else if (c.kind === "check") {
2650
+ checks.push({
2651
+ name: c.name ?? constraintName("ck", model.tablename, cols),
2652
+ expression: renameConditionColumns(c.expression, toColumn)
2653
+ });
2654
+ } else if (c.kind === "index") {
2655
+ indexes.push({
2656
+ name: c.name ?? constraintName("ix", model.tablename, cols),
2657
+ columns: cols,
2658
+ unique: c.unique === true,
2659
+ where: c.where ? renameConditionColumns(c.where, toColumn) : null
2660
+ });
872
2661
  } else {
873
2662
  foreignKeys.push({
874
2663
  name: c.name ?? constraintName("fk", model.tablename, cols),
@@ -880,7 +2669,15 @@ function reflectTable(model) {
880
2669
  });
881
2670
  }
882
2671
  }
883
- return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
2672
+ return {
2673
+ name: model.tablename,
2674
+ columns,
2675
+ primaryKey,
2676
+ uniqueConstraints,
2677
+ foreignKeys,
2678
+ checks,
2679
+ indexes
2680
+ };
884
2681
  }
885
2682
  function reflectSchema(models) {
886
2683
  const tables = {};
@@ -997,6 +2794,7 @@ function compareSqliteSchemas(actual, expected) {
997
2794
  );
998
2795
  }
999
2796
  }
2797
+ issues.push(...indexDrift(tableName, actualTable, expectedTable));
1000
2798
  }
1001
2799
  for (const tableName of Object.keys(actual.tables)) {
1002
2800
  if (!expected.tables[tableName]) {
@@ -1050,6 +2848,32 @@ function sqliteUniqueFromPragma(table, rows) {
1050
2848
  function isUniqueIndex(idx) {
1051
2849
  return Number(idx.unique) === 1 && idx.origin !== "pk";
1052
2850
  }
2851
+ function indexDrift(tableName, actual, expected) {
2852
+ const issues = [];
2853
+ const signature = (ix) => `${ix.columns.join(",")}${ix.unique ? " unique" : ""}`;
2854
+ const actualByName = new Map(actual.indexes.map((ix) => [ix.name, ix]));
2855
+ const expectedByName = new Map(expected.indexes.map((ix) => [ix.name, ix]));
2856
+ for (const [name, exp] of expectedByName) {
2857
+ const act = actualByName.get(name);
2858
+ if (!act) {
2859
+ issues.push(`index "${tableName}.${name}" is missing from the database`);
2860
+ continue;
2861
+ }
2862
+ if (signature(act) !== signature(exp)) {
2863
+ issues.push(
2864
+ `index "${tableName}.${name}" differs: model (${signature(exp)}), db (${signature(act)})`
2865
+ );
2866
+ }
2867
+ }
2868
+ for (const name of actualByName.keys()) {
2869
+ if (!expectedByName.has(name)) {
2870
+ issues.push(
2871
+ `index "${tableName}.${name}" exists in the database but not in the model`
2872
+ );
2873
+ }
2874
+ }
2875
+ return issues;
2876
+ }
1053
2877
  var SQLITE_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'";
1054
2878
  async function introspectSqliteAsync(driver) {
1055
2879
  const tablesRows = (await driver.execute(SQLITE_TABLES_SQL, [])).rows;
@@ -1067,12 +2891,27 @@ async function introspectSqliteAsync(driver) {
1067
2891
  const unique = sqliteUniqueFromPragma(tableName, cols);
1068
2892
  if (unique) uniqueConstraints.push(unique);
1069
2893
  }
2894
+ const explicitIndexes = [];
2895
+ for (const idx of indexes) {
2896
+ if (idx.origin !== "c" || idx.partial === 1) continue;
2897
+ const cols = (await driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, [])).rows;
2898
+ const sorted = [...cols].sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
2899
+ if (sorted.length === 0) continue;
2900
+ explicitIndexes.push({
2901
+ name: idx.name,
2902
+ columns: sorted,
2903
+ unique: Number(idx.unique) === 1,
2904
+ where: null
2905
+ });
2906
+ }
1070
2907
  tables[tableName] = {
1071
2908
  name: tableName,
1072
2909
  columns,
1073
2910
  primaryKey,
1074
2911
  uniqueConstraints,
1075
- foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows)
2912
+ foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows),
2913
+ checks: [],
2914
+ indexes: explicitIndexes
1076
2915
  };
1077
2916
  }
1078
2917
  return { tables };
@@ -1198,6 +3037,8 @@ async function introspectPostgres(driver) {
1198
3037
  columns,
1199
3038
  primaryKey,
1200
3039
  uniqueConstraints: await postgresUniques(driver, tableName),
3040
+ checks: [],
3041
+ indexes: await postgresIndexes(driver, tableName),
1201
3042
  foreignKeys: await postgresForeignKeys(driver, tableName)
1202
3043
  };
1203
3044
  }
@@ -1240,6 +3081,29 @@ async function postgresUniques(driver, table) {
1240
3081
  columns: r.cols ?? []
1241
3082
  }));
1242
3083
  }
3084
+ async function postgresIndexes(driver, table) {
3085
+ const { rows } = await driver.execute(
3086
+ `SELECT i.relname AS name,
3087
+ ix.indisunique AS is_unique,
3088
+ ix.indpred IS NOT NULL AS is_partial,
3089
+ array_to_string(array_agg(a.attname ORDER BY k.ord), ',') AS columns
3090
+ FROM pg_index ix
3091
+ JOIN pg_class i ON i.oid = ix.indexrelid
3092
+ JOIN pg_class t ON t.oid = ix.indrelid
3093
+ JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON true
3094
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
3095
+ WHERE t.relname = $1
3096
+ AND NOT EXISTS (SELECT 1 FROM pg_constraint c WHERE c.conindid = ix.indexrelid)
3097
+ GROUP BY i.relname, ix.indisunique, ix.indpred`,
3098
+ [table]
3099
+ );
3100
+ return rows.filter((row) => !row.is_partial).map((row) => ({
3101
+ name: row.name,
3102
+ columns: row.columns.split(","),
3103
+ unique: row.is_unique === true,
3104
+ where: null
3105
+ }));
3106
+ }
1243
3107
  function describeKind(type) {
1244
3108
  if (type.kind !== "array") return type.kind;
1245
3109
  return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
@@ -1290,6 +3154,7 @@ async function checkDriftPostgres(driver, models) {
1290
3154
  );
1291
3155
  }
1292
3156
  }
3157
+ issues.push(...indexDrift(tableName, actualTable, expectedTable));
1293
3158
  }
1294
3159
  for (const tableName of Object.keys(actual.tables)) {
1295
3160
  if (!expected.tables[tableName]) {
@@ -1615,7 +3480,22 @@ function applyOperation(schema, op) {
1615
3480
  tables[op.table] = op.constraint.type === "unique" ? {
1616
3481
  ...t,
1617
3482
  uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
1618
- } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
3483
+ } : op.constraint.type === "check" ? { ...t, checks: [...t.checks, op.constraint.constraint] } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
3484
+ }
3485
+ break;
3486
+ }
3487
+ case "create_index": {
3488
+ const t = tables[op.table];
3489
+ if (t) tables[op.table] = { ...t, indexes: [...t.indexes, op.index] };
3490
+ break;
3491
+ }
3492
+ case "drop_index": {
3493
+ const t = tables[op.table];
3494
+ if (t) {
3495
+ tables[op.table] = {
3496
+ ...t,
3497
+ indexes: t.indexes.filter((i) => i.name !== op.index.name)
3498
+ };
1619
3499
  }
1620
3500
  break;
1621
3501
  }
@@ -1626,7 +3506,7 @@ function applyOperation(schema, op) {
1626
3506
  tables[op.table] = op.constraint.type === "unique" ? {
1627
3507
  ...t,
1628
3508
  uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
1629
- } : {
3509
+ } : op.constraint.type === "check" ? { ...t, checks: t.checks.filter((c) => c.name !== dropName) } : {
1630
3510
  ...t,
1631
3511
  foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
1632
3512
  };
@@ -1831,8 +3711,37 @@ async function promptRenames(config, rest) {
1831
3711
  }
1832
3712
  return flags;
1833
3713
  }
3714
+ async function runBackupCommand(argv) {
3715
+ const [command, file, ...rest] = argv;
3716
+ if (command !== "backup" && command !== "restore") return false;
3717
+ const urlFlag = rest.indexOf("--url");
3718
+ const url = (urlFlag >= 0 ? rest[urlFlag + 1] : void 0) ?? rest.find((arg) => arg.startsWith("--url="))?.slice("--url=".length) ?? process.env.DATABASE_URL;
3719
+ if (!file || !url) {
3720
+ process.stderr.write(
3721
+ `tempest-db: usage: tempest-db ${command} <file> --url <database-url>
3722
+ (or set DATABASE_URL)
3723
+ `
3724
+ );
3725
+ process.exitCode = 1;
3726
+ return true;
3727
+ }
3728
+ const force = rest.includes("--force");
3729
+ try {
3730
+ const result = command === "backup" ? await backupDatabase(url, file, { force }) : await restoreDatabase(url, file, { force });
3731
+ process.stdout.write(
3732
+ `${command === "backup" ? "backed up" : "restored"} ${result.dialect} via ${result.via}: ${result.file}
3733
+ `
3734
+ );
3735
+ } catch (error) {
3736
+ process.stderr.write(`tempest-db: ${error.message}
3737
+ `);
3738
+ process.exitCode = 1;
3739
+ }
3740
+ return true;
3741
+ }
1834
3742
  async function main(argv) {
1835
3743
  const { configPath, rest } = extractConfigFlag(argv);
3744
+ if (await runBackupCommand(rest)) return;
1836
3745
  const resolved = resolveConfigPath(configPath);
1837
3746
  if (!resolved) {
1838
3747
  process.stderr.write(