tempest-db-js 0.7.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.
@@ -3,11 +3,145 @@
3
3
  var module$1 = require('module');
4
4
 
5
5
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
6
+ // src/conditions.ts
7
+ var CONDITION = /* @__PURE__ */ Symbol.for("tempest-db-js.condition");
8
+ function wrap(node) {
9
+ return { [CONDITION]: true, node };
10
+ }
11
+ function toExprNode(operand) {
12
+ return isExpression(operand) ? operand.node : { kind: "value", value: operand };
13
+ }
14
+ function assertValueOperands(op, operands) {
15
+ if (operands.some(isExpression)) {
16
+ throw new Error(
17
+ `The "${op}" operator binds its operands, so it takes values, not expressions.`
18
+ );
19
+ }
20
+ }
21
+ function isExpression(value) {
22
+ return value instanceof Expression;
23
+ }
24
+ var Expression = class {
25
+ constructor(node) {
26
+ this.node = node;
27
+ }
28
+ node;
29
+ /** Compare this expression against another expression or a bound value. */
30
+ compare(op, operand) {
31
+ return wrap({ kind: "compare", left: this.node, op, right: toExprNode(operand) });
32
+ }
33
+ /** `=` (or `IS NULL` for a null value). */
34
+ eq(operand) {
35
+ return this.compare("eq", operand);
36
+ }
37
+ /** `<>` (or `IS NOT NULL` for a null value). */
38
+ ne(operand) {
39
+ return this.compare("ne", operand);
40
+ }
41
+ /** `>`. */
42
+ gt(operand) {
43
+ return this.compare("gt", operand);
44
+ }
45
+ /** `>=`. */
46
+ gte(operand) {
47
+ return this.compare("gte", operand);
48
+ }
49
+ /** `<`. */
50
+ lt(operand) {
51
+ return this.compare("lt", operand);
52
+ }
53
+ /** `<=`. */
54
+ lte(operand) {
55
+ return this.compare("lte", operand);
56
+ }
57
+ /** `LIKE` — `%` and `_` in the operand are wildcards. */
58
+ like(pattern) {
59
+ return this.compare("like", pattern);
60
+ }
61
+ /** `ILIKE` — case-insensitive **pattern** matching, wildcards included. */
62
+ ilike(pattern) {
63
+ return this.compare("ilike", pattern);
64
+ }
65
+ /** Case-insensitive equality (`lower(a) = lower(b)`), with no wildcards. */
66
+ ieq(operand) {
67
+ return this.compare("ieq", operand);
68
+ }
69
+ /**
70
+ * `IN (...)` over a list of values.
71
+ *
72
+ * @param values The values to test against.
73
+ * @returns The condition.
74
+ * @throws Error When an entry is an {@link Expression} — a list operand is
75
+ * bound, so an expression there would be serialized as a parameter instead of
76
+ * rendered as SQL.
77
+ */
78
+ in(values) {
79
+ assertValueOperands("in", values);
80
+ return this.compare("in", values);
81
+ }
82
+ /**
83
+ * `NOT IN (...)` over a list of values.
84
+ *
85
+ * @param values The values to exclude.
86
+ * @returns The condition.
87
+ * @throws Error When an entry is an {@link Expression} (see {@link Expression.in}).
88
+ */
89
+ notIn(values) {
90
+ assertValueOperands("notIn", values);
91
+ return this.compare("notIn", values);
92
+ }
93
+ /**
94
+ * `BETWEEN lo AND hi` (inclusive).
95
+ *
96
+ * @param lo The lower bound.
97
+ * @param hi The upper bound.
98
+ * @returns The condition.
99
+ * @throws Error When a bound is an {@link Expression} (see {@link Expression.in}).
100
+ */
101
+ between(lo, hi) {
102
+ assertValueOperands("between", [lo, hi]);
103
+ return this.compare("between", [lo, hi]);
104
+ }
105
+ /** `IS NULL` (true) / `IS NOT NULL` (false). */
106
+ isNull(value = true) {
107
+ return this.compare("isNull", value);
108
+ }
109
+ };
110
+
111
+ // src/query.ts
112
+ function isSubquery(value) {
113
+ return typeof value === "object" && value !== null && value.node?.kind === "select";
114
+ }
115
+ var OPERATORS = [
116
+ "eq",
117
+ "ne",
118
+ "gt",
119
+ "gte",
120
+ "lt",
121
+ "lte",
122
+ "like",
123
+ "ilike",
124
+ "ieq",
125
+ "iContains",
126
+ "in",
127
+ "notIn",
128
+ "between",
129
+ "isNull",
130
+ "contains",
131
+ "containedBy",
132
+ "overlaps"
133
+ ];
134
+
6
135
  // src/expressions.ts
136
+ function renderExcluded(column, bare, dialect) {
137
+ return dialect === "mysql" ? `VALUES(${bare})` : `excluded.${column}`;
138
+ }
7
139
  function renderPortableToken(token, dialect) {
8
140
  switch (token) {
9
141
  case "now":
10
- return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
142
+ if (dialect === "postgresql") return "now()";
143
+ if (dialect === "sqlite") return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
144
+ return "CURRENT_TIMESTAMP";
11
145
  case "current_date":
12
146
  return "CURRENT_DATE";
13
147
  case "current_time":
@@ -18,6 +152,1264 @@ function renderPortableToken(token, dialect) {
18
152
  return "(lower(hex(randomblob(16))))";
19
153
  }
20
154
  }
155
+
156
+ // src/search.ts
157
+ function escapeLike(value) {
158
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
159
+ }
160
+
161
+ // src/dialect.ts
162
+ var OPERATOR_SET = new Set(OPERATORS);
163
+ var MULTI_VALUE_OPERATORS = /* @__PURE__ */ new Set(["in", "notIn", "between"]);
164
+ function encodeOperand(op, operand, encode) {
165
+ if (op === "isNull") return operand;
166
+ if (MULTI_VALUE_OPERATORS.has(op)) {
167
+ return Array.isArray(operand) ? operand.map(encode) : operand;
168
+ }
169
+ return encode(operand);
170
+ }
171
+ function codecEncoder(codecs) {
172
+ if (!codecs) return (_key, value) => value;
173
+ return (key, value) => {
174
+ const codec = codecs[key];
175
+ return codec ? codec.toDb(value) : value;
176
+ };
177
+ }
178
+ function isOperatorObject(value) {
179
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array) {
180
+ return false;
181
+ }
182
+ const keys = Object.keys(value);
183
+ return keys.length > 0 && keys.every((k) => OPERATOR_SET.has(k));
184
+ }
185
+ var Params = class {
186
+ constructor(placeholder2) {
187
+ this.placeholder = placeholder2;
188
+ }
189
+ placeholder;
190
+ values = [];
191
+ bind(value) {
192
+ this.values.push(value);
193
+ return this.placeholder(this.values.length);
194
+ }
195
+ };
196
+ var LiteralParams = class extends Params {
197
+ constructor() {
198
+ super(() => "");
199
+ }
200
+ bind(value) {
201
+ if (value === null || value === void 0) return "NULL";
202
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
203
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
204
+ if (value instanceof Date) return `'${value.toISOString()}'`;
205
+ return `'${String(value).replace(/'/g, "''")}'`;
206
+ }
207
+ };
208
+ function insertColumns(rows) {
209
+ const columns = [];
210
+ const seen = /* @__PURE__ */ new Set();
211
+ for (const row of rows) {
212
+ for (const key of Object.keys(row)) {
213
+ if (seen.has(key)) continue;
214
+ seen.add(key);
215
+ columns.push(key);
216
+ }
217
+ }
218
+ return columns;
219
+ }
220
+ function insertHasExpression(node) {
221
+ for (const row of node.values) {
222
+ for (const value of Object.values(row)) {
223
+ if (isSqlExpression(value)) return true;
224
+ }
225
+ }
226
+ const update = node.onConflict?.update;
227
+ if (update && update !== "nothing") {
228
+ for (const value of Object.values(update)) {
229
+ if (isSqlExpression(value)) return true;
230
+ }
231
+ }
232
+ return false;
233
+ }
234
+ var BaseDialect = class _BaseDialect {
235
+ /**
236
+ * INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
237
+ * returning). Shared across dialect instances — the key namespaces by dialect
238
+ * name, and the placeholder text is dialect-specific but structure-determined.
239
+ */
240
+ static insertTemplates = /* @__PURE__ */ new Map();
241
+ /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
242
+ static quotedIds = /* @__PURE__ */ new Map();
243
+ /**
244
+ * Render a case-insensitive LIKE whose pattern carries escaped wildcards.
245
+ *
246
+ * The `ESCAPE` clause is not decoration: PostgreSQL treats `\` as the escape
247
+ * character by default, **SQLite has none at all** until one is declared, so
248
+ * without this the escaping done on our side would be meaningless there.
249
+ *
250
+ * @param column The rendered column.
251
+ * @param param The bound pattern.
252
+ * @returns The rendered comparison.
253
+ */
254
+ ilikeEscaped(column, param) {
255
+ return `${this.ilike(column, param)} ESCAPE '\\'`;
256
+ }
257
+ /**
258
+ * Validate a subquery operand before it is rendered, for dialects that restrict
259
+ * what an `IN (SELECT ...)` may contain. The default accepts everything.
260
+ *
261
+ * @param _node The subquery's AST.
262
+ * @throws Error When the dialect cannot execute this subquery.
263
+ */
264
+ checkSubquery(_node) {
265
+ }
266
+ /**
267
+ * The SQL operator for an array containment/overlap test.
268
+ *
269
+ * Only PostgreSQL has native arrays; the other dialects throw rather than
270
+ * emitting an operator that means something else there.
271
+ *
272
+ * @param op The array operator name.
273
+ * @returns The SQL operator text.
274
+ * @throws Error On a dialect without native array support.
275
+ */
276
+ arrayOperator(op) {
277
+ throw new Error(
278
+ `The "${op}" operator needs native array support, which ${this.name} does not have.`
279
+ );
280
+ }
281
+ /**
282
+ * Quote an identifier (column/table) for the active dialect.
283
+ *
284
+ * Memoized: identifiers form a small, stable set (column/table names), but this
285
+ * runs for every identifier on every compile. Caching the quoted form removes a
286
+ * regex-replace + string allocation from the hot path. The standard double-quote
287
+ * form is identical across both dialects, so one shared cache is correct.
288
+ */
289
+ quoteId(name) {
290
+ const cached = _BaseDialect.quotedIds.get(name);
291
+ if (cached !== void 0) return cached;
292
+ const quoted = `"${name.replace(/"/g, '""')}"`;
293
+ _BaseDialect.quotedIds.set(name, quoted);
294
+ return quoted;
295
+ }
296
+ /** Compile any node to `{ sql, params }`. */
297
+ compile(node) {
298
+ const params = new Params((i) => this.placeholder(i));
299
+ let sql;
300
+ switch (node.kind) {
301
+ case "select":
302
+ sql = this.compileSelect(node, params);
303
+ break;
304
+ case "insert":
305
+ sql = this.compileInsert(node, params);
306
+ break;
307
+ case "update":
308
+ sql = this.compileUpdate(node, params);
309
+ break;
310
+ case "delete":
311
+ sql = this.compileDelete(node, params);
312
+ break;
313
+ case "join_select":
314
+ sql = this.compileJoin(node, params);
315
+ break;
316
+ case "set_op":
317
+ sql = this.compileSetOp(node, params);
318
+ break;
319
+ }
320
+ return { sql, params: params.values };
321
+ }
322
+ /**
323
+ * Render a condition as schema SQL, with values inlined.
324
+ *
325
+ * Same compiler as a `WHERE`, different parameter strategy — so a `CHECK` and
326
+ * the query language cannot drift apart in what they mean.
327
+ *
328
+ * @param node The condition.
329
+ * @param params A {@link LiteralParams}.
330
+ * @returns The rendered predicate.
331
+ */
332
+ renderConditionLiteral(node, params) {
333
+ return this.compileCondition(node, params, (key) => this.columnId(key, void 0));
334
+ }
335
+ /**
336
+ * Render the `WITH` clause of a statement.
337
+ *
338
+ * `RECURSIVE` is a property of the **clause**, not of an entry: one recursive
339
+ * entry makes the whole `WITH` recursive, which is what the SQL standard says
340
+ * and what PostgreSQL and SQLite both implement.
341
+ *
342
+ * @param entries The `WITH` entries, if any.
343
+ * @param params The parameter collector.
344
+ * @returns The clause with a trailing space, or an empty string.
345
+ */
346
+ compileWith(entries, params) {
347
+ if (!entries || entries.length === 0) return "";
348
+ const recursive = entries.some((entry) => entry.recursive) ? "RECURSIVE " : "";
349
+ const rendered = entries.map((entry) => {
350
+ const body = entry.body.kind === "set_op" ? this.compileSetOp(entry.body, params) : this.compileSelect(entry.body, params);
351
+ const hint = entry.materialized === null ? "" : entry.materialized ? " MATERIALIZED" : " NOT MATERIALIZED";
352
+ return `${this.quoteId(entry.name)} AS${hint} (${body})`;
353
+ });
354
+ return `WITH ${recursive}${rendered.join(", ")} `;
355
+ }
356
+ /**
357
+ * The `EXPLAIN` prefix for this dialect.
358
+ *
359
+ * @param analyze Whether to measure by actually running the statement.
360
+ * @returns The prefix to put in front of the statement.
361
+ * @throws Error When the dialect cannot do what was asked.
362
+ */
363
+ explainPrefix(analyze) {
364
+ return analyze ? "EXPLAIN (FORMAT JSON, ANALYZE)" : "EXPLAIN (FORMAT JSON)";
365
+ }
366
+ /**
367
+ * The SQL keyword for a set operation.
368
+ *
369
+ * @param op The operator.
370
+ * @returns The keyword.
371
+ * @throws Error On a dialect that does not implement it.
372
+ */
373
+ setOperator(op) {
374
+ switch (op) {
375
+ case "union":
376
+ return "UNION";
377
+ case "unionAll":
378
+ return "UNION ALL";
379
+ case "intersect":
380
+ return "INTERSECT";
381
+ case "except":
382
+ return "EXCEPT";
383
+ }
384
+ }
385
+ /**
386
+ * Compile a set operation.
387
+ *
388
+ * A branch carrying its own `ORDER BY`/`LIMIT` is parenthesized: without the
389
+ * parentheses those clauses bind to the **combined** result, which is a
390
+ * different query and a classic source of silently wrong output.
391
+ *
392
+ * @param node The set-operation node.
393
+ * @param params The parameter collector.
394
+ * @returns The rendered statement.
395
+ */
396
+ compileSetOp(node, params) {
397
+ const keyword = ` ${this.setOperator(node.op)} `;
398
+ const branches = node.branches.map((branch) => {
399
+ const sql2 = branch.kind === "join_select" ? this.compileJoin(branch, params) : this.compileSelect(branch, params);
400
+ const scoped = branch.orderBy.length > 0 || branch.limit !== void 0;
401
+ return scoped ? `(${sql2})` : sql2;
402
+ });
403
+ let sql = branches.join(keyword);
404
+ if (node.orderBy.length > 0) {
405
+ const terms = node.orderBy.map((t) => {
406
+ const id = typeof t.column === "string" ? this.quoteId(t.column) : this.renderExpr(t.column, params, (k) => this.quoteId(k));
407
+ return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
408
+ }).join(", ");
409
+ sql += ` ORDER BY ${terms}`;
410
+ }
411
+ if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
412
+ if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
413
+ return sql;
414
+ }
415
+ /**
416
+ * Render a qualified `alias.column` ref as `"alias"."column"`, translating the
417
+ * property name to the real column name for that alias's model.
418
+ *
419
+ * @param ref The `alias.property` reference (a bare name is left unqualified).
420
+ * @param names The node's per-alias name maps, if any source renames columns.
421
+ * @returns The quoted, qualified identifier.
422
+ */
423
+ qualify(ref, names) {
424
+ const dot = ref.indexOf(".");
425
+ if (dot === -1) return this.quoteId(ref);
426
+ const alias = ref.slice(0, dot);
427
+ const prop = ref.slice(dot + 1);
428
+ return `${this.quoteId(alias)}.${this.columnId(prop, names?.[alias])}`;
429
+ }
430
+ /**
431
+ * Quote a column identifier, translating the model property name to the real
432
+ * database column name first.
433
+ *
434
+ * `names` is `undefined` for a model that renames nothing — the overwhelmingly
435
+ * common case — so this stays a single lookup plus the memoized quote.
436
+ *
437
+ * @param prop The model property name as written in the builder.
438
+ * @param names The node's property → column map, if any.
439
+ * @returns The quoted database identifier.
440
+ */
441
+ columnId(prop, names) {
442
+ const mapped = names?.[prop];
443
+ if (mapped !== void 0) return this.quoteId(mapped);
444
+ const dot = prop.indexOf(".");
445
+ if (dot > 0 && dot < prop.length - 1) {
446
+ return `${this.quoteId(prop.slice(0, dot))}.${this.quoteId(prop.slice(dot + 1))}`;
447
+ }
448
+ return this.quoteId(prop);
449
+ }
450
+ /**
451
+ * Render a {@link SqlExpression} inline, binding the parameters it carries.
452
+ *
453
+ * This is what keeps `set({ attempts: sql.raw("attempts + 1") })` an expression
454
+ * instead of a bound object: the fragment goes into the statement text, and
455
+ * only a `sql.expr` template's interpolations become parameters.
456
+ *
457
+ * @param expr The branded expression.
458
+ * @param params The parameter collector for the statement being compiled.
459
+ * @returns The SQL text of the expression.
460
+ */
461
+ renderExpression(expr, params) {
462
+ const token = expr.expression;
463
+ if (typeof token === "string") return renderPortableToken(token, this.name);
464
+ if ("raw" in token) return token.raw;
465
+ if ("excluded" in token) {
466
+ return renderExcluded(this.quoteId(token.excluded), token.excluded, this.name);
467
+ }
468
+ const parts = token.parts;
469
+ let sql = parts[0] ?? "";
470
+ for (let i = 1; i < parts.length; i++) {
471
+ sql += `${params.bind(expr.params[i - 1])}${parts[i]}`;
472
+ }
473
+ return sql;
474
+ }
475
+ /** Render one write value: a SQL expression inline, anything else as a parameter. */
476
+ renderValue(value, params) {
477
+ if (isSqlExpression(value)) return this.renderExpression(value, params);
478
+ if (isExpression(value)) {
479
+ return this.renderExpr(value.node, params, (k) => this.columnId(k, void 0));
480
+ }
481
+ return params.bind(value);
482
+ }
483
+ /**
484
+ * The statements that open a transaction with the requested characteristics.
485
+ *
486
+ * Returned as a list because the dialects disagree on shape: PostgreSQL takes
487
+ * everything on the `BEGIN` itself, MySQL needs a separate `SET TRANSACTION`
488
+ * before it, and SQLite has no syntax at all.
489
+ *
490
+ * @param options The requested isolation level and read-only flag.
491
+ * @returns The statements to run, in order.
492
+ * @throws Error When the dialect cannot honor what was asked.
493
+ */
494
+ beginStatements(options) {
495
+ const parts = ["BEGIN"];
496
+ if (options?.isolation)
497
+ parts.push(`ISOLATION LEVEL ${options.isolation.toUpperCase()}`);
498
+ if (options?.readOnly) parts.push("READ ONLY");
499
+ return [parts.join(" ")];
500
+ }
501
+ /**
502
+ * Render a row-level locking clause (`FOR UPDATE ...`).
503
+ *
504
+ * Standard on PostgreSQL and MySQL 8.0+; SQLite overrides it to throw.
505
+ *
506
+ * @param lock The locking clause from the node.
507
+ * @returns The SQL text, leading space included.
508
+ */
509
+ renderLock(lock) {
510
+ const strength = lock.strength === "update" ? "FOR UPDATE" : "FOR SHARE";
511
+ const of = lock.of.length > 0 ? ` OF ${lock.of.map((t) => this.quoteId(t)).join(", ")}` : "";
512
+ const wait = lock.wait === "skipLocked" ? " SKIP LOCKED" : lock.wait === "noWait" ? " NOWAIT" : "";
513
+ return ` ${strength}${of}${wait}`;
514
+ }
515
+ // ---- statements -------------------------------------------------------
516
+ /**
517
+ * Compile a SELECT.
518
+ *
519
+ * Two alias rules differ between clauses and are handled here: PostgreSQL does
520
+ * NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
521
+ * its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
522
+ * by contrast, accepts the output alias everywhere, so it is emitted as
523
+ * written.
524
+ *
525
+ * @param node The select AST.
526
+ * @param params The parameter collector.
527
+ * @returns The SQL text.
528
+ */
529
+ compileSelect(node, params) {
530
+ const names = node.names;
531
+ let cols;
532
+ if (node.aggregates.length > 0) {
533
+ const groupSel = node.groupBy.map((c) => this.columnId(c, names));
534
+ const aggSel = node.aggregates.map((a) => {
535
+ const inner = this.aggregateOperand(a, params, names);
536
+ return `${a.fn.toUpperCase()}(${inner}) AS ${this.quoteId(a.alias)}`;
537
+ });
538
+ cols = [...groupSel, ...aggSel].join(", ");
539
+ } else {
540
+ cols = node.columns === "*" ? "*" : node.columns.map((c) => this.columnId(c, names)).join(", ");
541
+ }
542
+ const computed = Object.entries(node.computed ?? {}).map(
543
+ ([alias, expr]) => `${this.renderExpr(expr, params, (k) => this.columnId(k, names))} AS ${this.quoteId(alias)}`
544
+ );
545
+ if (computed.length > 0) cols = [cols, ...computed].join(", ");
546
+ const from = node.alias ? `${this.quoteId(node.table)} AS ${this.quoteId(node.alias)}` : this.quoteId(node.table);
547
+ let sql = `${this.compileWith(node.with, params)}SELECT ${node.distinct ? "DISTINCT " : ""}${cols} FROM ${from}`;
548
+ const where = this.compileCondition(
549
+ node.where,
550
+ params,
551
+ (k) => this.columnId(k, names),
552
+ codecEncoder(node.codecs)
553
+ );
554
+ if (where) sql += ` WHERE ${where}`;
555
+ if (node.groupBy.length > 0) {
556
+ sql += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
557
+ }
558
+ const aggByAlias = new Map(node.aggregates.map((a) => [a.alias, a]));
559
+ if (node.having) {
560
+ const having = this.compileCondition(node.having, params, (key) => {
561
+ const agg = aggByAlias.get(key);
562
+ if (!agg) return this.columnId(key, names);
563
+ const inner = this.aggregateOperand(agg, params, names);
564
+ return `${agg.fn.toUpperCase()}(${inner})`;
565
+ });
566
+ if (having) sql += ` HAVING ${having}`;
567
+ }
568
+ if (node.orderBy.length > 0) {
569
+ const terms = node.orderBy.map((t) => {
570
+ 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);
571
+ return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
572
+ }).join(", ");
573
+ sql += ` ORDER BY ${terms}`;
574
+ }
575
+ if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
576
+ if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
577
+ if (node.lock) {
578
+ if (node.distinct || node.groupBy.length > 0 || node.aggregates.length > 0) {
579
+ throw new Error(
580
+ "FOR UPDATE / FOR SHARE cannot be combined with DISTINCT or an aggregate query \u2014 lock the underlying rows in a separate SELECT."
581
+ );
582
+ }
583
+ sql += this.renderLock(node.lock);
584
+ }
585
+ return sql;
586
+ }
587
+ /**
588
+ * Compile an INSERT.
589
+ *
590
+ * Takes the cached fast path only when the statement text is a pure function of
591
+ * its structure. A SQL expression among the values, or a conflict predicate,
592
+ * makes the text depend on the values themselves — those compile uncached, in
593
+ * SQL order, so placeholder positions stay correct.
594
+ */
595
+ compileInsert(node, params) {
596
+ if (node.fromSelect) {
597
+ const target = node.fromSelect.columns.map((c) => this.columnId(c, node.names)).join(", ");
598
+ const source = node.fromSelect.select;
599
+ const query = source.kind === "join_select" ? this.compileJoin(source, params) : source.kind === "set_op" ? this.compileSetOp(source, params) : this.compileSelect(source, params);
600
+ const returning = this.compileReturning(node.returning, node.names);
601
+ return `INSERT INTO ${this.quoteId(node.table)} (${target}) ${query}${returning}`;
602
+ }
603
+ const columns = insertColumns(node.values);
604
+ const conflict = node.onConflict;
605
+ const cacheable = conflict?.targetWhere === void 0 && conflict?.updateWhere === void 0 && !insertHasExpression(node);
606
+ if (!cacheable) return this.compileInsertDirect(node, columns, params);
607
+ for (const row of node.values) {
608
+ for (const c of columns) params.bind(row[c] ?? null);
609
+ }
610
+ const conflictCols = conflict && conflict.update !== "nothing" ? Object.keys(conflict.update) : [];
611
+ for (const c of conflictCols) {
612
+ params.bind((conflict?.update)[c]);
613
+ }
614
+ return this.insertTemplate(node, columns, conflictCols, params);
615
+ }
616
+ /**
617
+ * Compile an INSERT without the template cache, rendering clauses in statement
618
+ * order so every parameter is bound at the position it appears.
619
+ *
620
+ * @param node The insert node.
621
+ * @param columns The column keys shared by every row.
622
+ * @param params The parameter collector.
623
+ * @returns The SQL text.
624
+ */
625
+ compileInsertDirect(node, columns, params) {
626
+ const names = node.names;
627
+ const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
628
+ const rowsSql = node.values.map((row) => {
629
+ const cells = columns.map(
630
+ (c) => this.renderValue(row[c] ?? null, params)
631
+ );
632
+ return `(${cells.join(", ")})`;
633
+ }).join(", ");
634
+ let sql = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
635
+ if (node.onConflict) {
636
+ const update = node.onConflict.update;
637
+ const conflictCols = update === "nothing" ? [] : Object.keys(update);
638
+ let cursor = 0;
639
+ sql += this.renderConflict(
640
+ node.onConflict,
641
+ conflictCols,
642
+ () => {
643
+ const key = conflictCols[cursor++];
644
+ return this.renderValue(update[key], params);
645
+ },
646
+ names,
647
+ params
648
+ );
649
+ }
650
+ sql += this.compileReturning(node.returning, names);
651
+ return sql;
652
+ }
653
+ /**
654
+ * The INSERT SQL template for a given structure, cached across calls.
655
+ *
656
+ * The text depends only on (dialect, table, columns, row count, returning,
657
+ * conflict shape) — never on the bound values — and placeholder positions are
658
+ * deterministic from the counts (a fresh statement always starts binding at 1).
659
+ * So a per-row insert loop compiles the string once and reuses it every row.
660
+ */
661
+ insertTemplate(node, columns, conflictCols, params) {
662
+ const returningKey = node.returning === null ? "" : node.returning === "*" ? "*" : node.returning.join(",");
663
+ const conflictKey = node.onConflict ? `${node.onConflict.target.join(",")}>${node.onConflict.update === "nothing" ? "nothing" : conflictCols.join(",")}` : "";
664
+ const key = `${this.name}|${node.table}|${columns.join(",")}|${node.values.length}|${returningKey}|${conflictKey}`;
665
+ const cached = _BaseDialect.insertTemplates.get(key);
666
+ if (cached !== void 0) return cached;
667
+ const names = node.names;
668
+ const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
669
+ let position = 0;
670
+ const rowsSql = node.values.map(() => `(${columns.map(() => this.placeholder(++position)).join(", ")})`).join(", ");
671
+ let sql = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
672
+ if (node.onConflict) {
673
+ sql += this.renderConflict(
674
+ node.onConflict,
675
+ conflictCols,
676
+ () => this.placeholder(++position),
677
+ names,
678
+ params
679
+ );
680
+ }
681
+ sql += this.compileReturning(node.returning, names);
682
+ _BaseDialect.insertTemplates.set(key, sql);
683
+ return sql;
684
+ }
685
+ /**
686
+ * Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
687
+ * `ON CONFLICT (...) [WHERE predicate] DO NOTHING | DO UPDATE SET ... [WHERE ...]`;
688
+ * MySQL overrides this.
689
+ *
690
+ * The index predicate is rendered before the `DO UPDATE` assignments because
691
+ * that is where it sits in the statement, so its parameters bind first.
692
+ *
693
+ * @param onConflict The conflict clause from the node.
694
+ * @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
695
+ * @param nextValue Yields the SQL for the next `DO UPDATE` assignment value.
696
+ * @param names The node's property → column map, if any.
697
+ * @param params The parameter collector, for the predicates.
698
+ * @returns The SQL text, leading space included.
699
+ */
700
+ renderConflict(onConflict, conflictCols, nextValue, names, params) {
701
+ const idFor = (key) => this.columnId(key, names);
702
+ const target = onConflict.target.map(idFor).join(", ");
703
+ const indexWhere = this.compileCondition(onConflict.targetWhere, params, idFor);
704
+ const targetSql = indexWhere ? `(${target}) WHERE ${indexWhere}` : `(${target})`;
705
+ if (onConflict.update === "nothing") return ` ON CONFLICT ${targetSql} DO NOTHING`;
706
+ const assignments = conflictCols.map((c) => `${idFor(c)} = ${nextValue()}`).join(", ");
707
+ let sql = ` ON CONFLICT ${targetSql} DO UPDATE SET ${assignments}`;
708
+ const updateWhere = this.compileCondition(onConflict.updateWhere, params, idFor);
709
+ if (updateWhere) sql += ` WHERE ${updateWhere}`;
710
+ return sql;
711
+ }
712
+ compileUpdate(node, params) {
713
+ const names = node.names;
714
+ const sets = Object.entries(node.set).map(
715
+ ([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
716
+ ).join(", ");
717
+ let sql = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
718
+ sql += this.compileExtraSources("FROM", node.from);
719
+ const where = this.compileCondition(
720
+ node.where,
721
+ params,
722
+ (k) => this.columnId(k, names),
723
+ codecEncoder(node.codecs)
724
+ );
725
+ if (where) sql += ` WHERE ${where}`;
726
+ sql += this.compileReturning(node.returning, names);
727
+ return sql;
728
+ }
729
+ compileDelete(node, params) {
730
+ const names = node.names;
731
+ let sql = `DELETE FROM ${this.quoteId(node.table)}`;
732
+ sql += this.compileExtraSources("USING", node.using);
733
+ const where = this.compileCondition(
734
+ node.where,
735
+ params,
736
+ (k) => this.columnId(k, names),
737
+ codecEncoder(node.codecs)
738
+ );
739
+ if (where) sql += ` WHERE ${where}`;
740
+ sql += this.compileReturning(node.returning, names);
741
+ return sql;
742
+ }
743
+ compileJoin(node, params) {
744
+ const names = node.names;
745
+ const cols = node.selections.map((s) => {
746
+ const ref = `${s.alias}.${s.column}`;
747
+ const label = node.pick ? s.column : ref;
748
+ return `${this.qualify(ref, names)} AS ${this.quoteId(label)}`;
749
+ }).join(", ");
750
+ let sql = `${this.compileWith(node.with, params)}SELECT ${cols} FROM ${this.quoteId(node.base.table)} AS ${this.quoteId(node.base.alias)}`;
751
+ for (const j of node.joins) {
752
+ const kw = j.kind === "left" ? "LEFT JOIN" : "INNER JOIN";
753
+ const on = j.on.map(([l, r]) => `${this.qualify(l, names)} = ${this.qualify(r, names)}`).join(" AND ");
754
+ sql += ` ${kw} ${this.quoteId(j.table)} AS ${this.quoteId(j.alias)} ON ${on}`;
755
+ }
756
+ const where = this.compileCondition(
757
+ node.where,
758
+ params,
759
+ (k) => this.qualify(k, names)
760
+ );
761
+ if (where) sql += ` WHERE ${where}`;
762
+ if (node.orderBy.length > 0) {
763
+ const terms = node.orderBy.map(
764
+ (t) => `${this.qualify(t.ref, names)} ${t.direction === "desc" ? "DESC" : "ASC"}`
765
+ ).join(", ");
766
+ sql += ` ORDER BY ${terms}`;
767
+ }
768
+ if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
769
+ if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
770
+ return sql;
771
+ }
772
+ // ---- clauses ----------------------------------------------------------
773
+ /**
774
+ * Render the extra sources of an `UPDATE ... FROM` / `DELETE ... USING`.
775
+ *
776
+ * The dialects that do not have the clause override this and throw: emitting it
777
+ * anyway would produce a statement the server rejects, and quietly dropping it
778
+ * would change which rows are written.
779
+ *
780
+ * @param keyword `FROM` or `USING`.
781
+ * @param sources The extra tables, if any.
782
+ * @returns The clause with a leading space, or an empty string.
783
+ */
784
+ compileExtraSources(keyword, sources) {
785
+ if (!sources || sources.length === 0) return "";
786
+ const list = sources.map((s) => `${this.quoteId(s.table)} AS ${this.quoteId(s.alias)}`).join(", ");
787
+ return ` ${keyword} ${list}`;
788
+ }
789
+ compileReturning(returning, names) {
790
+ if (returning === null) return "";
791
+ if (returning === "*") return " RETURNING *";
792
+ return ` RETURNING ${returning.map((c) => this.columnId(c, names)).join(", ")}`;
793
+ }
794
+ /**
795
+ * Compile a condition tree (fields / and / or / not) to SQL. `idFor` renders a
796
+ * key to a quoted identifier — `quoteId` for single-table, `qualify` for joins —
797
+ * so select/update/delete/join all share this one compiler.
798
+ */
799
+ compileCondition(node, params, idFor, encode = (_key, value) => value) {
800
+ if (!node) return "";
801
+ switch (node.kind) {
802
+ case "fields": {
803
+ const conditions = [];
804
+ for (const [key, value] of Object.entries(node.fields)) {
805
+ const id = idFor(key);
806
+ if (isExpression(value)) {
807
+ conditions.push(
808
+ this.compileExprOperator(
809
+ id,
810
+ "eq",
811
+ this.renderExpr(value.node, params, idFor)
812
+ )
813
+ );
814
+ } else if (isOperatorObject(value)) {
815
+ for (const [op, operand] of Object.entries(value)) {
816
+ conditions.push(
817
+ isExpression(operand) ? this.compileExprOperator(
818
+ id,
819
+ op,
820
+ this.renderExpr(operand.node, params, idFor)
821
+ ) : this.compileOperator(
822
+ id,
823
+ op,
824
+ encodeOperand(op, operand, (v) => encode(key, v)),
825
+ params
826
+ )
827
+ );
828
+ }
829
+ } else {
830
+ const operand = encode(key, value);
831
+ conditions.push(
832
+ operand === null ? `${id} IS NULL` : `${id} = ${params.bind(operand)}`
833
+ );
834
+ }
835
+ }
836
+ return conditions.join(" AND ");
837
+ }
838
+ case "and":
839
+ case "or": {
840
+ const parts = node.parts.map((p) => this.compileCondition(p, params, idFor, encode)).filter((s) => s.length > 0);
841
+ if (parts.length === 0) return "";
842
+ const sep = node.kind === "and" ? " AND " : " OR ";
843
+ return parts.map((p) => `(${p})`).join(sep);
844
+ }
845
+ case "not": {
846
+ const inner = this.compileCondition(node.part, params, idFor, encode);
847
+ return inner ? `NOT (${inner})` : "";
848
+ }
849
+ case "exists": {
850
+ const select = node.select;
851
+ this.checkSubquery(select);
852
+ const keyword = node.negate ? "NOT EXISTS" : "EXISTS";
853
+ return `${keyword} (${this.compileSelect(select, params)})`;
854
+ }
855
+ case "fullText":
856
+ return this.compileFullText(node, params, idFor);
857
+ case "compare": {
858
+ const left = this.renderExpr(node.left, params, idFor);
859
+ if (node.right.kind === "value") {
860
+ return this.compileOperator(left, node.op, node.right.value, params);
861
+ }
862
+ return this.compileExprOperator(
863
+ left,
864
+ node.op,
865
+ this.renderExpr(node.right, params, idFor)
866
+ );
867
+ }
868
+ }
869
+ }
870
+ /**
871
+ * Compile a full-text condition.
872
+ *
873
+ * PostgreSQL gets the real thing (`@@ websearch_to_tsquery`); the dialects with
874
+ * no text-search engine override this and compile the node's prebuilt substring
875
+ * fallback instead, so the query still returns the right rows.
876
+ *
877
+ * @param node The full-text condition node.
878
+ * @param params The parameter collector.
879
+ * @param idFor Column-name resolver.
880
+ * @returns The rendered condition.
881
+ */
882
+ compileFullText(node, params, idFor) {
883
+ const config = params.bind(node.language);
884
+ return `${this.tsVector(node.columns, config, idFor)} @@ websearch_to_tsquery(${config}::regconfig, ${params.bind(node.term)})`;
885
+ }
886
+ /**
887
+ * Render one side of a comparison.
888
+ *
889
+ * A column reference goes through `idFor`, so an explicit `.name()` mapping and
890
+ * join qualification apply here exactly as they do in the object form of
891
+ * `where` — `col()` is not a way around them. Only a `value` node binds.
892
+ *
893
+ * @param node The expression AST.
894
+ * @param params The parameter collector.
895
+ * @param idFor The identifier resolver for the enclosing statement.
896
+ * @returns The SQL text of the expression.
897
+ */
898
+ /**
899
+ * Render what an aggregate is applied to: `*`, a column, or an expression.
900
+ *
901
+ * An expression operand is what makes a conditional aggregate
902
+ * (`SUM(CASE WHEN ... END)`) expressible — one pass over the table instead of a
903
+ * query per bucket.
904
+ *
905
+ * @param agg The aggregate term.
906
+ * @param params The parameter collector.
907
+ * @param names The node's column-name map.
908
+ * @returns The rendered operand.
909
+ */
910
+ aggregateOperand(agg, params, names) {
911
+ if (agg.column === "*") return "*";
912
+ if (typeof agg.column === "string") return this.columnId(agg.column, names);
913
+ return this.renderExpr(agg.column, params, (k) => this.columnId(k, names));
914
+ }
915
+ renderExpr(node, params, idFor) {
916
+ switch (node.kind) {
917
+ case "column":
918
+ return idFor(node.name);
919
+ case "value":
920
+ return params.bind(node.value);
921
+ case "fn": {
922
+ const args = node.args.map((a) => this.renderExpr(a, params, idFor)).join(", ");
923
+ return `${node.name}(${args})`;
924
+ }
925
+ case "case": {
926
+ const branches = node.branches.map(
927
+ (b) => `WHEN ${this.compileCondition(b.when, params, idFor)} THEN ${this.renderExpr(b.result, params, idFor)}`
928
+ ).join(" ");
929
+ const fallback = node.fallback === null ? "" : ` ELSE ${this.renderExpr(node.fallback, params, idFor)}`;
930
+ return `CASE ${branches}${fallback} END`;
931
+ }
932
+ case "cast":
933
+ return `CAST(${this.renderExpr(node.operand, params, idFor)} AS ${this.castTypeName(node.to)})`;
934
+ case "scalar": {
935
+ const select = node.select;
936
+ this.checkSubquery(select);
937
+ return `(${this.compileSelect(select, params)})`;
938
+ }
939
+ case "rank":
940
+ return this.renderRank(node.columns, node.term, node.language, params, idFor);
941
+ case "star":
942
+ return "*";
943
+ case "window": {
944
+ const call = this.renderExpr(node.fn, params, idFor);
945
+ const parts = [];
946
+ if (node.partitionBy.length > 0) {
947
+ parts.push(`PARTITION BY ${node.partitionBy.map(idFor).join(", ")}`);
948
+ }
949
+ if (node.orderBy.length > 0) {
950
+ const terms = node.orderBy.map((t) => `${idFor(t.column)} ${t.direction === "desc" ? "DESC" : "ASC"}`).join(", ");
951
+ parts.push(`ORDER BY ${terms}`);
952
+ }
953
+ if (node.frame) parts.push(node.frame);
954
+ return `${call} OVER (${parts.join(" ")})`;
955
+ }
956
+ }
957
+ }
958
+ /**
959
+ * Render a full-text relevance score.
960
+ *
961
+ * PostgreSQL has `ts_rank`; the others have nothing equivalent, and they
962
+ * override this to a constant so that ordering by it is a no-op rather than a
963
+ * compile error — the fallback keeps returning the right rows, only unranked.
964
+ *
965
+ * @param columns The columns making up the document.
966
+ * @param term The search term.
967
+ * @param language The text-search configuration.
968
+ * @param params The parameter collector.
969
+ * @param idFor Column-name resolver.
970
+ * @returns The rendered score expression.
971
+ */
972
+ renderRank(columns, term, language, params, idFor) {
973
+ const config = params.bind(language);
974
+ return `ts_rank(${this.tsVector(columns, config, idFor)}, websearch_to_tsquery(${config}::regconfig, ${params.bind(term)}))`;
975
+ }
976
+ /**
977
+ * Build the `to_tsvector(...)` document out of the searched columns.
978
+ *
979
+ * `coalesce(col, '')` matters: in SQL a `NULL` anywhere in a concatenation makes
980
+ * the whole document `NULL`, so one empty column would silently exclude the row.
981
+ *
982
+ * @param columns The columns making up the document.
983
+ * @param config The already-bound placeholder for the text-search config.
984
+ * @param idFor Column-name resolver.
985
+ * @returns The rendered `to_tsvector(...)` call.
986
+ */
987
+ tsVector(columns, config, idFor) {
988
+ const document = columns.map((c) => `coalesce(${idFor(c)}, '')`).join(" || ' ' || ");
989
+ return `to_tsvector(${config}::regconfig, ${document})`;
990
+ }
991
+ /**
992
+ * The SQL type name this dialect accepts in a `CAST`.
993
+ *
994
+ * The base mapping is the standard one PostgreSQL takes; SQLite and MySQL
995
+ * override it, because the names genuinely differ (MySQL's `CAST(x AS SIGNED)`
996
+ * is not `INTEGER`, and SQLite only has five storage classes to aim at).
997
+ *
998
+ * @param to The portable cast target.
999
+ * @returns The dialect's own type name.
1000
+ */
1001
+ castTypeName(to) {
1002
+ switch (to) {
1003
+ case "integer":
1004
+ return "INTEGER";
1005
+ case "bigint":
1006
+ return "BIGINT";
1007
+ case "real":
1008
+ return "DOUBLE PRECISION";
1009
+ case "numeric":
1010
+ return "NUMERIC";
1011
+ case "text":
1012
+ return "TEXT";
1013
+ case "boolean":
1014
+ return "BOOLEAN";
1015
+ case "date":
1016
+ return "DATE";
1017
+ case "datetime":
1018
+ case "timestamp":
1019
+ return "TIMESTAMP";
1020
+ case "uuid":
1021
+ return "UUID";
1022
+ case "json":
1023
+ return "JSON";
1024
+ case "jsonb":
1025
+ return "JSONB";
1026
+ case "blob":
1027
+ return "BYTEA";
1028
+ }
1029
+ }
1030
+ /**
1031
+ * Compile a comparison whose right-hand side is another expression rather than
1032
+ * a bound value (`total > paid`, `lower(a) = lower(b)`).
1033
+ *
1034
+ * The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
1035
+ * a value operand, and accepting an expression there would silently compile to
1036
+ * something else.
1037
+ *
1038
+ * @param left The rendered left-hand side.
1039
+ * @param op The operator name.
1040
+ * @param right The rendered right-hand side.
1041
+ * @returns The SQL text of the predicate.
1042
+ * @throws Error When the operator needs a value operand.
1043
+ */
1044
+ compileExprOperator(left, op, right) {
1045
+ switch (op) {
1046
+ case "eq":
1047
+ return `${left} = ${right}`;
1048
+ case "ne":
1049
+ return `${left} <> ${right}`;
1050
+ case "gt":
1051
+ return `${left} > ${right}`;
1052
+ case "gte":
1053
+ return `${left} >= ${right}`;
1054
+ case "lt":
1055
+ return `${left} < ${right}`;
1056
+ case "lte":
1057
+ return `${left} <= ${right}`;
1058
+ case "like":
1059
+ return `${left} LIKE ${right}`;
1060
+ case "ilike":
1061
+ return this.ilike(left, right);
1062
+ case "ieq":
1063
+ return `lower(${left}) = lower(${right})`;
1064
+ case "iContains":
1065
+ throw new Error(
1066
+ 'The "iContains" operator matches a literal, so it takes a value, not an expression.'
1067
+ );
1068
+ case "contains":
1069
+ case "containedBy":
1070
+ case "overlaps":
1071
+ return `${left} ${this.arrayOperator(op)} ${right}`;
1072
+ default:
1073
+ throw new Error(`The "${op}" operator takes a value operand, not an expression.`);
1074
+ }
1075
+ }
1076
+ compileOperator(id, op, operand, params) {
1077
+ switch (op) {
1078
+ case "eq":
1079
+ return operand === null ? `${id} IS NULL` : `${id} = ${params.bind(operand)}`;
1080
+ case "ne":
1081
+ return operand === null ? `${id} IS NOT NULL` : `${id} <> ${params.bind(operand)}`;
1082
+ case "gt":
1083
+ return `${id} > ${params.bind(operand)}`;
1084
+ case "gte":
1085
+ return `${id} >= ${params.bind(operand)}`;
1086
+ case "lt":
1087
+ return `${id} < ${params.bind(operand)}`;
1088
+ case "lte":
1089
+ return `${id} <= ${params.bind(operand)}`;
1090
+ case "like":
1091
+ return `${id} LIKE ${params.bind(operand)}`;
1092
+ case "ilike":
1093
+ return this.ilike(id, params.bind(operand));
1094
+ case "ieq":
1095
+ return operand === null ? `${id} IS NULL` : `lower(${id}) = lower(${params.bind(operand)})`;
1096
+ case "iContains":
1097
+ return this.ilikeEscaped(id, params.bind(`%${escapeLike(String(operand))}%`));
1098
+ case "contains":
1099
+ return `${id} ${this.arrayOperator("contains")} ${params.bind(operand)}`;
1100
+ case "containedBy":
1101
+ return `${id} ${this.arrayOperator("containedBy")} ${params.bind(operand)}`;
1102
+ case "overlaps":
1103
+ return `${id} ${this.arrayOperator("overlaps")} ${params.bind(operand)}`;
1104
+ case "in":
1105
+ return this.compileIn(id, operand, params, false);
1106
+ case "notIn":
1107
+ return this.compileIn(id, operand, params, true);
1108
+ case "between": {
1109
+ const [lo, hi] = operand;
1110
+ return `${id} BETWEEN ${params.bind(lo)} AND ${params.bind(hi)}`;
1111
+ }
1112
+ case "isNull":
1113
+ return operand ? `${id} IS NULL` : `${id} IS NOT NULL`;
1114
+ default:
1115
+ throw new Error(`Unknown operator ${JSON.stringify(op)}`);
1116
+ }
1117
+ }
1118
+ /**
1119
+ * Compile `IN` / `NOT IN`, whose operand is either a value list or a
1120
+ * single-column subquery.
1121
+ *
1122
+ * The subquery is rendered at the position it appears in the outer statement
1123
+ * and shares the same parameter collector, so its own placeholders land in the
1124
+ * right order — and it keeps its own `names` map, since the inner model may use
1125
+ * a different naming convention than the outer one.
1126
+ *
1127
+ * @param id The quoted column identifier being tested.
1128
+ * @param operand A list of values, or a {@link Subquery}.
1129
+ * @param params The parameter collector for the statement being compiled.
1130
+ * @param negate True for `NOT IN`.
1131
+ * @returns The SQL text of the predicate.
1132
+ */
1133
+ compileIn(id, operand, params, negate) {
1134
+ const keyword = negate ? "NOT IN" : "IN";
1135
+ if (isSubquery(operand)) {
1136
+ this.checkSubquery(operand.node);
1137
+ return `${id} ${keyword} (${this.compileSelect(operand.node, params)})`;
1138
+ }
1139
+ const values = operand;
1140
+ if (values.length === 0) {
1141
+ return negate ? "1 = 1" : "1 = 0";
1142
+ }
1143
+ const list = values.map((v) => params.bind(v)).join(", ");
1144
+ return `${id} ${keyword} (${list})`;
1145
+ }
1146
+ };
1147
+ var SqliteDialect = class extends BaseDialect {
1148
+ name = "sqlite";
1149
+ /**
1150
+ * SQLite explains with `EXPLAIN QUERY PLAN`, and has no `ANALYZE` — the plain
1151
+ * `EXPLAIN` there dumps bytecode, which answers a different question.
1152
+ */
1153
+ explainPrefix(analyze) {
1154
+ if (analyze) {
1155
+ throw new Error(
1156
+ "SQLite has no EXPLAIN ANALYZE; use EXPLAIN QUERY PLAN (analyze: false)."
1157
+ );
1158
+ }
1159
+ return "EXPLAIN QUERY PLAN";
1160
+ }
1161
+ /**
1162
+ * SQLite has `UPDATE ... FROM` (3.33+) but no `DELETE ... USING`.
1163
+ *
1164
+ * The portable shape there is a subquery — `where({ id: { in: … } })` — so this
1165
+ * throws and says so, rather than emitting a clause SQLite does not parse.
1166
+ */
1167
+ compileExtraSources(keyword, sources) {
1168
+ if (keyword === "USING" && sources && sources.length > 0) {
1169
+ throw new Error(
1170
+ 'SQLite has no DELETE ... USING; filter with a subquery instead: where({ id: { in: select(Other, ["id"]).asSubquery("id") } }).'
1171
+ );
1172
+ }
1173
+ return super.compileExtraSources(keyword, sources);
1174
+ }
1175
+ /**
1176
+ * SQLite has no text-search engine, so the prebuilt substring fallback is
1177
+ * compiled instead. The rows are right; the ranking is what is missing.
1178
+ */
1179
+ compileFullText(node, params, idFor) {
1180
+ return this.compileCondition(node.fallback, params, idFor);
1181
+ }
1182
+ /** No text-search engine means no score: a constant, so ordering by it is inert. */
1183
+ renderRank() {
1184
+ return "0";
1185
+ }
1186
+ /**
1187
+ * SQLite has five storage classes, so most targets collapse onto `TEXT` or
1188
+ * `INTEGER`. Naming a type it does not know would not fail — SQLite applies the
1189
+ * closest affinity — but it would make the cast mean something different here
1190
+ * than on the other databases, which is what this mapping avoids.
1191
+ */
1192
+ castTypeName(to) {
1193
+ switch (to) {
1194
+ case "integer":
1195
+ case "bigint":
1196
+ case "boolean":
1197
+ return "INTEGER";
1198
+ case "real":
1199
+ return "REAL";
1200
+ case "numeric":
1201
+ return "NUMERIC";
1202
+ case "blob":
1203
+ return "BLOB";
1204
+ default:
1205
+ return "TEXT";
1206
+ }
1207
+ }
1208
+ placeholder() {
1209
+ return "?";
1210
+ }
1211
+ ilike(column, param) {
1212
+ return `${column} LIKE ${param}`;
1213
+ }
1214
+ /**
1215
+ * SQLite runs one writer at a time, so its only isolation level **is**
1216
+ * serializable — there is no syntax to ask for another, and no weaker level to
1217
+ * fall back to. Asking for one is an error rather than a silent no-op, since a
1218
+ * caller who wrote `repeatable read` was reasoning about a guarantee.
1219
+ *
1220
+ * `readOnly` likewise has no per-transaction form here (`PRAGMA query_only` is
1221
+ * per connection), so it is refused instead of quietly ignored.
1222
+ */
1223
+ beginStatements(options) {
1224
+ if (options?.isolation && options.isolation !== "serializable") {
1225
+ throw new Error(
1226
+ `SQLite only implements the "serializable" isolation level; ${JSON.stringify(options.isolation)} has no equivalent here.`
1227
+ );
1228
+ }
1229
+ if (options?.readOnly) {
1230
+ throw new Error(
1231
+ "SQLite has no read-only transaction; open the engine with { sqlite: { ... } } on a read-only connection instead."
1232
+ );
1233
+ }
1234
+ return ["BEGIN"];
1235
+ }
1236
+ /**
1237
+ * SQLite has no row-level locking, so a lock request is an error rather than a
1238
+ * silently unlocked `SELECT` — a lock that does not exist only shows up as
1239
+ * duplicated work under production concurrency.
1240
+ */
1241
+ renderLock() {
1242
+ throw new Error(
1243
+ "SQLite has no row-level locking \u2014 FOR UPDATE / FOR SHARE is unsupported. Serialize the claim inside a transaction instead."
1244
+ );
1245
+ }
1246
+ };
1247
+ var PostgresDialect = class extends BaseDialect {
1248
+ name = "postgresql";
1249
+ placeholder(index) {
1250
+ return `$${index}`;
1251
+ }
1252
+ ilike(column, param) {
1253
+ return `${column} ILIKE ${param}`;
1254
+ }
1255
+ arrayOperator(op) {
1256
+ if (op === "contains") return "@>";
1257
+ if (op === "containedBy") return "<@";
1258
+ return "&&";
1259
+ }
1260
+ };
1261
+ var mysqlQuotedIds = /* @__PURE__ */ new Map();
1262
+ var MysqlDialect = class extends BaseDialect {
1263
+ name = "mysql";
1264
+ placeholder() {
1265
+ return "?";
1266
+ }
1267
+ ilike(column, param) {
1268
+ return `${column} LIKE ${param}`;
1269
+ }
1270
+ /** MySQL's `EXPLAIN FORMAT=JSON` spells the option differently. */
1271
+ explainPrefix(analyze) {
1272
+ return analyze ? "EXPLAIN ANALYZE" : "EXPLAIN FORMAT=JSON";
1273
+ }
1274
+ /**
1275
+ * MySQL only gained `INTERSECT`/`EXCEPT` in 8.0.31, and this project does not
1276
+ * invest in MySQL beyond what already works — so they are refused here rather
1277
+ * than emitted against a server that may reject them.
1278
+ */
1279
+ setOperator(op) {
1280
+ if (op === "intersect" || op === "except") {
1281
+ throw new Error(
1282
+ `MySQL support for ${op.toUpperCase()} is out of scope for tempest-db-js; express it with a join or NOT EXISTS.`
1283
+ );
1284
+ }
1285
+ return super.setOperator(op);
1286
+ }
1287
+ /**
1288
+ * MySQL writes multi-table updates as `UPDATE a JOIN b`, and has no
1289
+ * `DELETE ... USING` in this shape. Both are out of the project's active scope,
1290
+ * so they are refused rather than emitted against a server that rejects them.
1291
+ */
1292
+ compileExtraSources(keyword, sources) {
1293
+ if (sources && sources.length > 0) {
1294
+ throw new Error(
1295
+ `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.`
1296
+ );
1297
+ }
1298
+ return "";
1299
+ }
1300
+ /**
1301
+ * MySQL's full-text search needs a `FULLTEXT` index and different syntax, and it
1302
+ * is outside this project's active scope — the substring fallback is compiled,
1303
+ * like on SQLite.
1304
+ */
1305
+ compileFullText(node, params, idFor) {
1306
+ return this.compileCondition(node.fallback, params, idFor);
1307
+ }
1308
+ /** No `ts_rank` equivalent in scope: a constant, so ordering by it is inert. */
1309
+ renderRank() {
1310
+ return "0";
1311
+ }
1312
+ /**
1313
+ * MySQL sets the level with a statement **before** the transaction opens, and
1314
+ * spells the read-only flag on `START TRANSACTION` rather than on `BEGIN`.
1315
+ */
1316
+ beginStatements(options) {
1317
+ const statements = [];
1318
+ if (options?.isolation) {
1319
+ statements.push(
1320
+ `SET TRANSACTION ISOLATION LEVEL ${options.isolation.toUpperCase()}`
1321
+ );
1322
+ }
1323
+ statements.push(options?.readOnly ? "START TRANSACTION READ ONLY" : "BEGIN");
1324
+ return statements;
1325
+ }
1326
+ /**
1327
+ * MySQL's `CAST` takes its own vocabulary — `SIGNED`, not `INTEGER`; `CHAR`,
1328
+ * not `TEXT` — and rejects the standard names outright.
1329
+ */
1330
+ castTypeName(to) {
1331
+ switch (to) {
1332
+ case "integer":
1333
+ case "bigint":
1334
+ case "boolean":
1335
+ return "SIGNED";
1336
+ case "real":
1337
+ case "numeric":
1338
+ return "DECIMAL";
1339
+ case "text":
1340
+ case "uuid":
1341
+ return "CHAR";
1342
+ case "date":
1343
+ return "DATE";
1344
+ case "datetime":
1345
+ case "timestamp":
1346
+ return "DATETIME";
1347
+ case "json":
1348
+ case "jsonb":
1349
+ return "JSON";
1350
+ case "blob":
1351
+ return "BINARY";
1352
+ }
1353
+ }
1354
+ quoteId(name) {
1355
+ const cached = mysqlQuotedIds.get(name);
1356
+ if (cached !== void 0) return cached;
1357
+ const quoted = `\`${name.replace(/`/g, "``")}\``;
1358
+ mysqlQuotedIds.set(name, quoted);
1359
+ return quoted;
1360
+ }
1361
+ /**
1362
+ * MySQL rejects `LIMIT` inside an `IN` subquery with
1363
+ * `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
1364
+ * 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
1365
+ * instead of surfacing that error from the driver at runtime.
1366
+ */
1367
+ checkSubquery(node) {
1368
+ if (node.limit !== void 0 || node.offset !== void 0) {
1369
+ throw new Error(
1370
+ "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."
1371
+ );
1372
+ }
1373
+ }
1374
+ renderConflict(onConflict, conflictCols, nextValue, names) {
1375
+ if (onConflict.targetWhere || onConflict.updateWhere) {
1376
+ throw new Error(
1377
+ "MySQL's ON DUPLICATE KEY UPDATE has no conflict-target predicate \u2014 a partial unique index is PostgreSQL/SQLite only."
1378
+ );
1379
+ }
1380
+ if (onConflict.update === "nothing") {
1381
+ const col = this.columnId(onConflict.target[0] ?? "id", names);
1382
+ return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
1383
+ }
1384
+ const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
1385
+ return ` ON DUPLICATE KEY UPDATE ${assignments}`;
1386
+ }
1387
+ /**
1388
+ * MySQL has no `RETURNING`, so it cannot be compiled into a statement.
1389
+ *
1390
+ * `session.execute()` still honors `.returning()` on a **single-row INSERT** by
1391
+ * running the insert and reading the row back by key on the same connection —
1392
+ * that is execution, not compilation, so it never reaches here. Compiling a
1393
+ * node with `returning` directly is an error, rather than SQL that silently
1394
+ * returns nothing.
1395
+ */
1396
+ compileReturning(returning) {
1397
+ if (returning === null) return "";
1398
+ throw new Error(
1399
+ "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."
1400
+ );
1401
+ }
1402
+ };
1403
+ function getDialect(name) {
1404
+ switch (name) {
1405
+ case "sqlite":
1406
+ return new SqliteDialect();
1407
+ case "postgresql":
1408
+ return new PostgresDialect();
1409
+ case "mysql":
1410
+ return new MysqlDialect();
1411
+ }
1412
+ }
21
1413
  module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
22
1414
  function toAsyncDriver(driver) {
23
1415
  return {
@@ -31,6 +1423,10 @@ function toAsyncDriver(driver) {
31
1423
  }
32
1424
 
33
1425
  // src/index.ts
1426
+ var EXPRESSION = /* @__PURE__ */ Symbol.for("tempest-db-js.expression");
1427
+ function isSqlExpression(value) {
1428
+ return typeof value === "object" && value !== null && value[EXPRESSION] === true;
1429
+ }
34
1430
  function isDefaultValue(value) {
35
1431
  return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
36
1432
  }
@@ -50,13 +1446,14 @@ function parseReference(ref, options) {
50
1446
  };
51
1447
  }
52
1448
  var Column = class _Column {
53
- constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
1449
+ constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null, codec = null) {
54
1450
  this.type = type;
55
1451
  this.flags = flags;
56
1452
  this.defaultValue = defaultValue;
57
1453
  this.onUpdateValue = onUpdateValue;
58
1454
  this.reference = reference;
59
1455
  this.dbName = dbName;
1456
+ this.codec = codec;
60
1457
  }
61
1458
  type;
62
1459
  flags;
@@ -64,6 +1461,7 @@ var Column = class _Column {
64
1461
  onUpdateValue;
65
1462
  reference;
66
1463
  dbName;
1464
+ codec;
67
1465
  /** Clone this column with one facet replaced, carrying every other over. */
68
1466
  derive(patch) {
69
1467
  return new _Column(
@@ -72,7 +1470,8 @@ var Column = class _Column {
72
1470
  patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
73
1471
  patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
74
1472
  patch.reference !== void 0 ? patch.reference : this.reference,
75
- patch.dbName !== void 0 ? patch.dbName : this.dbName
1473
+ patch.dbName !== void 0 ? patch.dbName : this.dbName,
1474
+ patch.codec !== void 0 ? patch.codec : this.codec
76
1475
  );
77
1476
  }
78
1477
  primaryKey() {
@@ -214,6 +1613,41 @@ function columnNamesOf(model) {
214
1613
  function constraintName(prefix, table, columns) {
215
1614
  return `${prefix}_${table}_${columns.join("_")}`;
216
1615
  }
1616
+ function renameConditionColumns(node, toColumn) {
1617
+ const expr = (current) => {
1618
+ switch (current.kind) {
1619
+ case "column":
1620
+ return { kind: "column", name: toColumn(current.name) };
1621
+ case "fn":
1622
+ return { ...current, args: current.args.map(expr) };
1623
+ case "cast":
1624
+ return { ...current, operand: expr(current.operand) };
1625
+ default:
1626
+ return current;
1627
+ }
1628
+ };
1629
+ switch (node.kind) {
1630
+ case "fields": {
1631
+ const fields = {};
1632
+ for (const [key, value] of Object.entries(node.fields)) {
1633
+ fields[toColumn(key)] = value;
1634
+ }
1635
+ return { kind: "fields", fields };
1636
+ }
1637
+ case "and":
1638
+ case "or":
1639
+ return {
1640
+ ...node,
1641
+ parts: node.parts.map((part) => renameConditionColumns(part, toColumn))
1642
+ };
1643
+ case "not":
1644
+ return { kind: "not", part: renameConditionColumns(node.part, toColumn) };
1645
+ case "compare":
1646
+ return { ...node, left: expr(node.left), right: expr(node.right) };
1647
+ default:
1648
+ return node;
1649
+ }
1650
+ }
217
1651
  function reflectTable(model) {
218
1652
  const names = columnNamesOf(model);
219
1653
  const toColumn = (prop) => names?.[prop] ?? prop;
@@ -235,6 +1669,8 @@ function reflectTable(model) {
235
1669
  }
236
1670
  const uniqueConstraints = [];
237
1671
  const foreignKeys = [];
1672
+ const checks = [];
1673
+ const indexes = [];
238
1674
  for (const c of model.tableArgs?.() ?? []) {
239
1675
  const cols = c.columns.map(toColumn);
240
1676
  if (c.kind === "unique") {
@@ -242,6 +1678,18 @@ function reflectTable(model) {
242
1678
  name: c.name ?? constraintName("uq", model.tablename, cols),
243
1679
  columns: cols
244
1680
  });
1681
+ } else if (c.kind === "check") {
1682
+ checks.push({
1683
+ name: c.name ?? constraintName("ck", model.tablename, cols),
1684
+ expression: renameConditionColumns(c.expression, toColumn)
1685
+ });
1686
+ } else if (c.kind === "index") {
1687
+ indexes.push({
1688
+ name: c.name ?? constraintName("ix", model.tablename, cols),
1689
+ columns: cols,
1690
+ unique: c.unique === true,
1691
+ where: c.where ? renameConditionColumns(c.where, toColumn) : null
1692
+ });
245
1693
  } else {
246
1694
  foreignKeys.push({
247
1695
  name: c.name ?? constraintName("fk", model.tablename, cols),
@@ -253,7 +1701,15 @@ function reflectTable(model) {
253
1701
  });
254
1702
  }
255
1703
  }
256
- return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
1704
+ return {
1705
+ name: model.tablename,
1706
+ columns,
1707
+ primaryKey,
1708
+ uniqueConstraints,
1709
+ foreignKeys,
1710
+ checks,
1711
+ indexes
1712
+ };
257
1713
  }
258
1714
  function reflectSchema(models) {
259
1715
  const tables = {};
@@ -300,6 +1756,10 @@ function invert(op) {
300
1756
  return { kind: "recreate_table", from: op.to, to: op.from };
301
1757
  case "add_constraint":
302
1758
  return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
1759
+ case "create_index":
1760
+ return { kind: "drop_index", table: op.table, index: op.index };
1761
+ case "drop_index":
1762
+ return { kind: "create_index", table: op.table, index: op.index };
303
1763
  case "drop_constraint":
304
1764
  return { kind: "add_constraint", table: op.table, constraint: op.constraint };
305
1765
  case "execute":
@@ -443,7 +1903,8 @@ function renderDefault(def, dialect, type) {
443
1903
  "sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
444
1904
  );
445
1905
  }
446
- return renderPortableToken(expr, dialect);
1906
+ const rendered = renderPortableToken(expr, dialect);
1907
+ return dialect === "sqlite" && rendered.includes("(") ? `(${rendered})` : rendered;
447
1908
  }
448
1909
  const value = def.value;
449
1910
  if (value === null) return "NULL";
@@ -491,9 +1952,31 @@ function renderForeignKeyConstraint(fk, dialect) {
491
1952
  function tableConstraintClauses(table, dialect) {
492
1953
  return [
493
1954
  ...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
494
- ...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
1955
+ ...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect)),
1956
+ ...table.checks.map((ck) => renderCheckConstraint(ck, dialect))
495
1957
  ];
496
1958
  }
1959
+ function renderPredicate(node, dialect) {
1960
+ const literals = new LiteralParams();
1961
+ return getDialect(dialect).renderConditionLiteral(node, literals);
1962
+ }
1963
+ function renderCheckConstraint(ck, dialect) {
1964
+ return `CONSTRAINT ${quoteId(ck.name, dialect)} CHECK (${renderPredicate(ck.expression, dialect)})`;
1965
+ }
1966
+ function renderCreateIndex(table, ix, dialect) {
1967
+ if (ix.where && dialect === "mysql") {
1968
+ throw new Error(
1969
+ `MySQL has no partial index; ${ix.name} on ${table} declares a WHERE predicate.`
1970
+ );
1971
+ }
1972
+ const unique = ix.unique ? "UNIQUE " : "";
1973
+ const cols = ix.columns.map((c) => quoteId(c, dialect)).join(", ");
1974
+ const where = ix.where ? ` WHERE ${renderPredicate(ix.where, dialect)}` : "";
1975
+ return `CREATE ${unique}INDEX ${quoteId(ix.name, dialect)} ON ${quoteId(table, dialect)} (${cols})${where}`;
1976
+ }
1977
+ function renderDropIndex(table, ix, dialect) {
1978
+ return dialect === "mysql" ? `DROP INDEX ${quoteId(ix.name, dialect)} ON ${quoteId(table, dialect)}` : `DROP INDEX ${quoteId(ix.name, dialect)}`;
1979
+ }
497
1980
  function renderColumnDef(col, dialect) {
498
1981
  let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
499
1982
  if (col.notNull) sql += " NOT NULL";
@@ -546,7 +2029,10 @@ function renderCreateTable(table, dialect) {
546
2029
  ...typeStmts,
547
2030
  `CREATE TABLE ${quoteId(table.name, dialect)} (
548
2031
  ${cols.join(",\n ")}
549
- )`
2032
+ )`,
2033
+ // Indexes are separate statements, not table clauses — which is also why a
2034
+ // SQLite table rebuild has to recreate them.
2035
+ ...table.indexes.map((ix) => renderCreateIndex(table.name, ix, dialect))
550
2036
  ];
551
2037
  }
552
2038
  function renderOperation(op, dialect) {
@@ -579,6 +2065,10 @@ function renderOperation(op, dialect) {
579
2065
  return renderAddConstraint(op.table, op.constraint, dialect);
580
2066
  case "drop_constraint":
581
2067
  return renderDropConstraint(op.table, op.constraint, dialect);
2068
+ case "create_index":
2069
+ return [renderCreateIndex(op.table, op.index, dialect)];
2070
+ case "drop_index":
2071
+ return [renderDropIndex(op.table, op.index, dialect)];
582
2072
  case "execute":
583
2073
  return [op.up];
584
2074
  }
@@ -589,7 +2079,7 @@ function renderAddConstraint(table, constraint, dialect) {
589
2079
  `add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
590
2080
  );
591
2081
  }
592
- const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
2082
+ const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : constraint.type === "check" ? renderCheckConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
593
2083
  return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
594
2084
  }
595
2085
  function renderDropConstraint(table, constraint, dialect) {
@@ -626,6 +2116,10 @@ function renderSqliteRebuild(from, to) {
626
2116
  common.length > 0 ? `INSERT INTO ${quoteId(tmp, "sqlite")} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name, "sqlite")}` : `-- no common columns to copy from ${from.name}`,
627
2117
  `DROP TABLE ${quoteId(from.name, "sqlite")}`,
628
2118
  `ALTER TABLE ${quoteId(tmp, "sqlite")} RENAME TO ${quoteId(to.name, "sqlite")}`,
2119
+ // The rebuild dropped the old table, and its indexes with it: SQLite ties an
2120
+ // index to the table it was created on. Recreating them is part of the
2121
+ // rebuild, not a follow-up somebody has to remember.
2122
+ ...to.indexes.map((ix) => renderCreateIndex(to.name, ix, "sqlite")),
629
2123
  "PRAGMA foreign_keys=on"
630
2124
  ];
631
2125
  }
@@ -728,8 +2222,49 @@ function diffConstraints(current, target) {
728
2222
  ops.push(addForeignKey(table, tgt));
729
2223
  }
730
2224
  }
2225
+ const currentCk = new Map(current.checks.map((c) => [c.name, c]));
2226
+ const targetCk = new Map(target.checks.map((c) => [c.name, c]));
2227
+ for (const [name, cur] of currentCk) {
2228
+ const tgt = targetCk.get(name);
2229
+ if (!tgt || checkSignature(cur) !== checkSignature(tgt)) {
2230
+ ops.push({ kind: "drop_constraint", table, constraint: checkNamed(cur) });
2231
+ }
2232
+ }
2233
+ for (const [name, tgt] of targetCk) {
2234
+ const cur = currentCk.get(name);
2235
+ if (!cur || checkSignature(cur) !== checkSignature(tgt)) {
2236
+ ops.push({ kind: "add_constraint", table, constraint: checkNamed(tgt) });
2237
+ }
2238
+ }
2239
+ const currentIx = new Map(current.indexes.map((i) => [i.name, i]));
2240
+ const targetIx = new Map(target.indexes.map((i) => [i.name, i]));
2241
+ for (const [name, cur] of currentIx) {
2242
+ const tgt = targetIx.get(name);
2243
+ if (!tgt || indexSignature(cur) !== indexSignature(tgt)) {
2244
+ ops.push({ kind: "drop_index", table, index: cur });
2245
+ }
2246
+ }
2247
+ for (const [name, tgt] of targetIx) {
2248
+ const cur = currentIx.get(name);
2249
+ if (!cur || indexSignature(cur) !== indexSignature(tgt)) {
2250
+ ops.push({ kind: "create_index", table, index: tgt });
2251
+ }
2252
+ }
731
2253
  return ops;
732
2254
  }
2255
+ function checkSignature(ck) {
2256
+ return JSON.stringify(ck.expression);
2257
+ }
2258
+ function indexSignature(ix) {
2259
+ return JSON.stringify({
2260
+ columns: ix.columns,
2261
+ unique: ix.unique,
2262
+ where: ix.where
2263
+ });
2264
+ }
2265
+ function checkNamed(ck) {
2266
+ return { type: "check", constraint: ck };
2267
+ }
733
2268
  function uniqueNamed(uc) {
734
2269
  return { type: "unique", constraint: uc };
735
2270
  }
@@ -1183,6 +2718,8 @@ function introspectSqlite(driver) {
1183
2718
  columns,
1184
2719
  primaryKey,
1185
2720
  uniqueConstraints: sqliteUniques(driver, tableName),
2721
+ checks: [],
2722
+ indexes: sqliteIndexes(driver, tableName),
1186
2723
  foreignKeys: sqliteForeignKeys(driver, tableName)
1187
2724
  };
1188
2725
  }
@@ -1271,6 +2808,7 @@ function compareSqliteSchemas(actual, expected) {
1271
2808
  );
1272
2809
  }
1273
2810
  }
2811
+ issues.push(...indexDrift(tableName, actualTable, expectedTable));
1274
2812
  }
1275
2813
  for (const tableName of Object.keys(actual.tables)) {
1276
2814
  if (!expected.tables[tableName]) {
@@ -1321,9 +2859,58 @@ function sqliteUniqueFromPragma(table, rows) {
1321
2859
  if (cols.length === 0) return null;
1322
2860
  return { name: `uq_${table}_${cols.join("_")}`, columns: cols };
1323
2861
  }
2862
+ function indexesFromPragma(rows, columnsOfIndex) {
2863
+ const indexes = [];
2864
+ for (const idx of rows) {
2865
+ if (idx.origin !== "c") continue;
2866
+ if (idx.partial === 1) continue;
2867
+ const cols = [...columnsOfIndex(idx.name)].sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
2868
+ if (cols.length === 0) continue;
2869
+ indexes.push({
2870
+ name: idx.name,
2871
+ columns: cols,
2872
+ unique: Number(idx.unique) === 1,
2873
+ where: null
2874
+ });
2875
+ }
2876
+ return indexes;
2877
+ }
2878
+ function sqliteIndexes(driver, table) {
2879
+ const rows = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
2880
+ return indexesFromPragma(
2881
+ rows,
2882
+ (name) => driver.execute(`PRAGMA index_info(${JSON.stringify(name)})`, []).rows
2883
+ );
2884
+ }
1324
2885
  function isUniqueIndex(idx) {
1325
2886
  return Number(idx.unique) === 1 && idx.origin !== "pk";
1326
2887
  }
2888
+ function indexDrift(tableName, actual, expected) {
2889
+ const issues = [];
2890
+ const signature = (ix) => `${ix.columns.join(",")}${ix.unique ? " unique" : ""}`;
2891
+ const actualByName = new Map(actual.indexes.map((ix) => [ix.name, ix]));
2892
+ const expectedByName = new Map(expected.indexes.map((ix) => [ix.name, ix]));
2893
+ for (const [name, exp] of expectedByName) {
2894
+ const act = actualByName.get(name);
2895
+ if (!act) {
2896
+ issues.push(`index "${tableName}.${name}" is missing from the database`);
2897
+ continue;
2898
+ }
2899
+ if (signature(act) !== signature(exp)) {
2900
+ issues.push(
2901
+ `index "${tableName}.${name}" differs: model (${signature(exp)}), db (${signature(act)})`
2902
+ );
2903
+ }
2904
+ }
2905
+ for (const name of actualByName.keys()) {
2906
+ if (!expectedByName.has(name)) {
2907
+ issues.push(
2908
+ `index "${tableName}.${name}" exists in the database but not in the model`
2909
+ );
2910
+ }
2911
+ }
2912
+ return issues;
2913
+ }
1327
2914
  var SQLITE_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'";
1328
2915
  async function introspectSqliteAsync(driver) {
1329
2916
  const tablesRows = (await driver.execute(SQLITE_TABLES_SQL, [])).rows;
@@ -1341,12 +2928,27 @@ async function introspectSqliteAsync(driver) {
1341
2928
  const unique = sqliteUniqueFromPragma(tableName, cols);
1342
2929
  if (unique) uniqueConstraints.push(unique);
1343
2930
  }
2931
+ const explicitIndexes = [];
2932
+ for (const idx of indexes) {
2933
+ if (idx.origin !== "c" || idx.partial === 1) continue;
2934
+ const cols = (await driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, [])).rows;
2935
+ const sorted = [...cols].sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
2936
+ if (sorted.length === 0) continue;
2937
+ explicitIndexes.push({
2938
+ name: idx.name,
2939
+ columns: sorted,
2940
+ unique: Number(idx.unique) === 1,
2941
+ where: null
2942
+ });
2943
+ }
1344
2944
  tables[tableName] = {
1345
2945
  name: tableName,
1346
2946
  columns,
1347
2947
  primaryKey,
1348
2948
  uniqueConstraints,
1349
- foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows)
2949
+ foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows),
2950
+ checks: [],
2951
+ indexes: explicitIndexes
1350
2952
  };
1351
2953
  }
1352
2954
  return { tables };
@@ -1472,6 +3074,8 @@ async function introspectPostgres(driver) {
1472
3074
  columns,
1473
3075
  primaryKey,
1474
3076
  uniqueConstraints: await postgresUniques(driver, tableName),
3077
+ checks: [],
3078
+ indexes: await postgresIndexes(driver, tableName),
1475
3079
  foreignKeys: await postgresForeignKeys(driver, tableName)
1476
3080
  };
1477
3081
  }
@@ -1514,6 +3118,29 @@ async function postgresUniques(driver, table) {
1514
3118
  columns: r.cols ?? []
1515
3119
  }));
1516
3120
  }
3121
+ async function postgresIndexes(driver, table) {
3122
+ const { rows } = await driver.execute(
3123
+ `SELECT i.relname AS name,
3124
+ ix.indisunique AS is_unique,
3125
+ ix.indpred IS NOT NULL AS is_partial,
3126
+ array_to_string(array_agg(a.attname ORDER BY k.ord), ',') AS columns
3127
+ FROM pg_index ix
3128
+ JOIN pg_class i ON i.oid = ix.indexrelid
3129
+ JOIN pg_class t ON t.oid = ix.indrelid
3130
+ JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON true
3131
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
3132
+ WHERE t.relname = $1
3133
+ AND NOT EXISTS (SELECT 1 FROM pg_constraint c WHERE c.conindid = ix.indexrelid)
3134
+ GROUP BY i.relname, ix.indisunique, ix.indpred`,
3135
+ [table]
3136
+ );
3137
+ return rows.filter((row) => !row.is_partial).map((row) => ({
3138
+ name: row.name,
3139
+ columns: row.columns.split(","),
3140
+ unique: row.is_unique === true,
3141
+ where: null
3142
+ }));
3143
+ }
1517
3144
  function describeKind(type) {
1518
3145
  if (type.kind !== "array") return type.kind;
1519
3146
  return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
@@ -1564,6 +3191,7 @@ async function checkDriftPostgres(driver, models) {
1564
3191
  );
1565
3192
  }
1566
3193
  }
3194
+ issues.push(...indexDrift(tableName, actualTable, expectedTable));
1567
3195
  }
1568
3196
  for (const tableName of Object.keys(actual.tables)) {
1569
3197
  if (!expected.tables[tableName]) {
@@ -1641,7 +3269,22 @@ function applyOperation(schema, op) {
1641
3269
  tables[op.table] = op.constraint.type === "unique" ? {
1642
3270
  ...t,
1643
3271
  uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
1644
- } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
3272
+ } : op.constraint.type === "check" ? { ...t, checks: [...t.checks, op.constraint.constraint] } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
3273
+ }
3274
+ break;
3275
+ }
3276
+ case "create_index": {
3277
+ const t = tables[op.table];
3278
+ if (t) tables[op.table] = { ...t, indexes: [...t.indexes, op.index] };
3279
+ break;
3280
+ }
3281
+ case "drop_index": {
3282
+ const t = tables[op.table];
3283
+ if (t) {
3284
+ tables[op.table] = {
3285
+ ...t,
3286
+ indexes: t.indexes.filter((i) => i.name !== op.index.name)
3287
+ };
1645
3288
  }
1646
3289
  break;
1647
3290
  }
@@ -1652,7 +3295,7 @@ function applyOperation(schema, op) {
1652
3295
  tables[op.table] = op.constraint.type === "unique" ? {
1653
3296
  ...t,
1654
3297
  uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
1655
- } : {
3298
+ } : op.constraint.type === "check" ? { ...t, checks: t.checks.filter((c) => c.name !== dropName) } : {
1656
3299
  ...t,
1657
3300
  foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
1658
3301
  };