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.
package/dist/index.d.ts CHANGED
@@ -1,28 +1,386 @@
1
+ /** One joined table. */
2
+ interface JoinClause {
3
+ readonly kind: "inner" | "left";
4
+ readonly table: string;
5
+ readonly alias: string;
6
+ /** Equality pairs of qualified columns: `["user.id", "order.userId"]`. */
7
+ readonly on: readonly (readonly [string, string])[];
8
+ }
9
+ /** A selected column, qualified by source alias. */
10
+ interface JoinSelection {
11
+ readonly alias: string;
12
+ readonly column: string;
13
+ }
14
+ /** Collapse a union of object types into their intersection. */
15
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
16
+ /**
17
+ * `where` filter for a join. Keys are `alias.column` refs; each value accepts a
18
+ * bare value (shorthand for `eq`) or an operator object restricted to the
19
+ * operators valid for that column's type — exactly like the single-table
20
+ * {@link WhereInput}, but qualified per source. `like` on a numeric join column,
21
+ * or `gt` on a string one, is a compile error.
22
+ */
23
+ type JoinWhereInput<S extends Sources> = Partial<UnionToIntersection<{
24
+ [A in keyof S]: {
25
+ [C in keyof NonNullable<S[A]> & string as `${A & string}.${C}`]: NonNullable<S[A]>[C] | OperatorsFor<NonNullable<NonNullable<S[A]>[C]>>;
26
+ };
27
+ }[keyof S]>>;
28
+ /** Serializable AST for a multi-table SELECT. */
29
+ interface JoinNode {
30
+ readonly kind: "join_select";
31
+ readonly base: {
32
+ readonly table: string;
33
+ readonly alias: string;
34
+ };
35
+ readonly joins: readonly JoinClause[];
36
+ readonly selections: readonly JoinSelection[];
37
+ readonly where: CondNode | undefined;
38
+ readonly orderBy: readonly {
39
+ readonly ref: string;
40
+ readonly direction: SortDirection;
41
+ }[];
42
+ readonly limit: number | undefined;
43
+ readonly offset: number | undefined;
44
+ /** Per-alias property → column maps, for the sources that rename columns. */
45
+ readonly names?: Readonly<Record<string, NameMap>> | undefined;
46
+ /**
47
+ * Project only this source, with **bare** column names instead of the
48
+ * `alias.column` labels a composite row needs.
49
+ *
50
+ * Set by {@link JoinBuilder.pick}. It is what lets a join stand where a plain
51
+ * `SELECT` of one table would — the recursive branch of a `WITH RECURSIVE`, for
52
+ * instance, whose columns have to line up with the CTE's.
53
+ */
54
+ readonly pick?: string | undefined;
55
+ /** `WITH` entries this statement carries, in order. */
56
+ readonly with?: readonly CteNode[] | undefined;
57
+ }
58
+ /** A map of source alias → its (possibly nullable) row type. */
59
+ type Sources = Record<string, object | null>;
60
+ /** Every `alias.column` reference across the current sources. */
61
+ type ColRef<S extends Sources> = {
62
+ [A in keyof S]: `${A & string}.${keyof NonNullable<S[A]> & string}`;
63
+ }[keyof S];
64
+ /** Valid `alias.column` references for a newly joined model. */
65
+ type RightRef<A extends string, C extends ModelClass> = `${A}.${keyof InferModel<C> & string}`;
66
+ /** The `on` condition: map existing-source refs to new-table refs (equality). */
67
+ type JoinOn<S extends Sources, A extends string, C extends ModelClass> = Partial<Record<ColRef<S>, RightRef<A, C>>>;
68
+ /**
69
+ * Immutable, chainable multi-table SELECT builder.
70
+ *
71
+ * @typeParam S - the accumulated sources (alias → row type; nullable for left joins).
72
+ */
73
+ declare class JoinBuilder<S extends Sources> {
74
+ readonly node: JoinNode;
75
+ /** Source models keyed by alias, for SQL expansion and row coercion. */
76
+ readonly sources: Readonly<Record<string, ModelClass>>;
77
+ /** Phantom: the composite result row type. */
78
+ readonly __row: {
79
+ [A in keyof S]: S[A];
80
+ };
81
+ constructor(node: JoinNode,
82
+ /** Source models keyed by alias, for SQL expansion and row coercion. */
83
+ sources: Readonly<Record<string, ModelClass>>);
84
+ private add;
85
+ private clause;
86
+ /** Inner join another model under `alias`. */
87
+ innerJoin<C extends ModelClass, A extends string>(model: C, alias: A, on: JoinOn<S, A, C>): JoinBuilder<S & {
88
+ [K in A]: InferModel<C>;
89
+ }>;
90
+ /** Left (outer) join another model under `alias` — its side becomes nullable. */
91
+ leftJoin<C extends ModelClass, A extends string>(model: C, alias: A, on: JoinOn<S, A, C>): JoinBuilder<S & {
92
+ [K in A]: InferModel<C> | null;
93
+ }>;
94
+ /**
95
+ * Project **one** source, flat, instead of the composite row.
96
+ *
97
+ * The join still happens — it just stops being what comes back. Needed wherever
98
+ * the result has to match a single table's shape: the recursive branch of a
99
+ * CTE, an `INSERT ... SELECT`, a `UNION` branch.
100
+ *
101
+ * @param alias The source to project.
102
+ * @returns A builder whose rows are that source's rows.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * join(Category, "c").innerJoin(subtree, "s", { "c.parentId": "s.id" }).pick("c");
107
+ * // SELECT "c"."id" AS "id", "c"."name" AS "name" ... — not "c.id"
108
+ * ```
109
+ */
110
+ pick<A extends keyof S & string>(alias: A): JoinBuilder<S> & {
111
+ readonly __row: NonNullable<S[A]>;
112
+ };
113
+ /** Filter by `alias.column` references (object form) or an `and`/`or`/`not`. */
114
+ where(input: JoinWhereInput<S> | Condition): JoinBuilder<S>;
115
+ /** Order by an `alias.column` reference. */
116
+ orderBy(ref: ColRef<S>, direction?: SortDirection): JoinBuilder<S>;
117
+ limit(n: number): JoinBuilder<S>;
118
+ offset(n: number): JoinBuilder<S>;
119
+ }
120
+ /**
121
+ * Start a multi-table query from a base model under `alias`.
122
+ *
123
+ * @param model The base model.
124
+ * @param alias The alias to key the base table's rows under in the result.
125
+ * @returns A `JoinBuilder` with the base source registered.
126
+ */
127
+ declare function join<C extends ModelClass, A extends string>(model: C, alias: A): JoinBuilder<{
128
+ [K in A]: InferModel<C>;
129
+ }>;
130
+
131
+ /**
132
+ * tempest-db-js — set operations (`UNION`, `INTERSECT`, `EXCEPT`).
133
+ *
134
+ * Combining two queries in any way other than a join meant dropping to
135
+ * `session.raw`, losing the row type, the coercion and the composition. A set
136
+ * operation is also the one place a typed builder can catch a mistake the
137
+ * database only reports at runtime: branches whose projections do not line up.
138
+ */
139
+
140
+ /** Which set operation combines the branches. */
141
+ type SetOperator = "union" | "unionAll" | "intersect" | "except";
142
+ /** Serializable AST for a set operation. */
143
+ interface SetNode {
144
+ readonly kind: "set_op";
145
+ readonly op: SetOperator;
146
+ /** The combined SELECTs, in order. A branch may be a join projecting one source. */
147
+ readonly branches: readonly (SelectNode | JoinNode)[];
148
+ /** Ordering applied to the **result**, not to a branch. */
149
+ readonly orderBy: readonly OrderTerm[];
150
+ readonly limit: number | undefined;
151
+ readonly offset: number | undefined;
152
+ }
153
+ /**
154
+ * A combined query, executable like a `SelectBuilder`.
155
+ *
156
+ * `orderBy` / `limit` / `offset` here apply to the **combined** result, which is
157
+ * what SQL does: a branch that needs its own limit keeps it, and the dialect
158
+ * parenthesizes that branch.
159
+ *
160
+ * @typeParam Row - the row type every branch projects.
161
+ */
162
+ declare class SetBuilder<Row> {
163
+ readonly node: SetNode;
164
+ /** The first branch's model, used to coerce the returned rows. */
165
+ readonly source: ModelClass;
166
+ /** Phantom: the result element type, read only by the type system. */
167
+ readonly __row: Row;
168
+ constructor(node: SetNode,
169
+ /** The first branch's model, used to coerce the returned rows. */
170
+ source: ModelClass);
171
+ private with;
172
+ /**
173
+ * Order the combined result.
174
+ *
175
+ * @param column A column of the projected row.
176
+ * @param direction Sort direction (default ascending).
177
+ * @returns A builder carrying the ordering.
178
+ */
179
+ orderBy(column: keyof Row & string, direction?: SortDirection): SetBuilder<Row>;
180
+ /** Limit the combined result. */
181
+ limit(count: number): SetBuilder<Row>;
182
+ /** Skip rows of the combined result. */
183
+ offset(count: number): SetBuilder<Row>;
184
+ }
185
+ /**
186
+ * What a set operation combines: anything that compiles to a SELECT and yields
187
+ * `Row` — a `select()`, or a join narrowed to one source with `.pick()`.
188
+ */
189
+ interface Branch<Row> {
190
+ /** The branch's AST. */
191
+ readonly node: SelectNode | JoinNode;
192
+ /** Phantom: the row type this branch yields. */
193
+ readonly __row: Row;
194
+ /** The model rows are coerced through, when the branch has a single source. */
195
+ readonly source?: ModelClass;
196
+ /** Source models by alias, for a join branch. */
197
+ readonly sources?: Readonly<Record<string, ModelClass>>;
198
+ }
199
+ /**
200
+ * `UNION` — every row of either branch, **duplicates removed**.
201
+ *
202
+ * Every branch must project the same shape; that is checked at compile time here,
203
+ * where the database would only report it at runtime.
204
+ *
205
+ * @param branches The queries to combine.
206
+ * @returns The combined builder.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * union(
211
+ * select(Post, ["id", "createdAt"]).where({ authorId: me }),
212
+ * select(Comment, ["id", "createdAt"]).where({ authorId: me }),
213
+ * ).orderBy("createdAt", "desc").limit(50);
214
+ * ```
215
+ */
216
+ declare function union<Row>(...branches: Branch<Row>[]): SetBuilder<Row>;
217
+ /**
218
+ * `UNION ALL` — every row of either branch, duplicates **kept**.
219
+ *
220
+ * Cheaper than `UNION`, which has to sort or hash to deduplicate. Prefer it
221
+ * whenever the branches cannot overlap.
222
+ *
223
+ * @param branches The queries to combine.
224
+ * @returns The combined builder.
225
+ */
226
+ declare function unionAll<Row>(...branches: Branch<Row>[]): SetBuilder<Row>;
227
+ /**
228
+ * `INTERSECT` — only the rows present in every branch.
229
+ *
230
+ * @param branches The queries to combine.
231
+ * @returns The combined builder.
232
+ */
233
+ declare function intersect<Row>(...branches: Branch<Row>[]): SetBuilder<Row>;
234
+ /**
235
+ * `EXCEPT` — the rows of the first branch that are not in the others.
236
+ *
237
+ * @param branches The queries to combine.
238
+ * @returns The combined builder.
239
+ */
240
+ declare function except<Row>(...branches: Branch<Row>[]): SetBuilder<Row>;
241
+
242
+ /**
243
+ * tempest-db-js — common table expressions (`WITH`).
244
+ *
245
+ * A CTE names a query so the rest of the statement can select from it, and —
246
+ * when it is recursive — so it can select from **itself**. That second form is
247
+ * the only way to walk a tree (a category hierarchy, a reply chain, a dependency
248
+ * graph) in one statement instead of one query per level.
249
+ *
250
+ * The alias is a real model class with the CTE's name as its table, so everything
251
+ * that already takes a model — `select`, `join`, `where` — takes a CTE with no
252
+ * special casing anywhere else.
253
+ */
254
+
255
+ /** A query that can be the body of a CTE. */
256
+ type CteBody<Row> = SelectBuilder<any, Row, any> | SetBuilder<Row>;
257
+ /** One `WITH` entry, as the AST carries it. */
258
+ interface CteNode {
259
+ /** The name the rest of the statement refers to. */
260
+ readonly name: string;
261
+ /** True for `WITH RECURSIVE`. */
262
+ readonly recursive: boolean;
263
+ /** The body query. */
264
+ readonly body: SelectNode | SetNode;
265
+ /**
266
+ * `MATERIALIZED` / `NOT MATERIALIZED`, or `null` to let the planner decide.
267
+ * PostgreSQL 12+ only.
268
+ */
269
+ readonly materialized: boolean | null;
270
+ }
271
+ /** Options for {@link cte} and {@link cteRecursive}. */
272
+ interface CteOptions {
273
+ /**
274
+ * Force (or forbid) materialization of the CTE.
275
+ *
276
+ * PostgreSQL 12+ inlines a CTE used once, which is usually what you want;
277
+ * `true` pins the old behavior when the body is expensive and used twice.
278
+ * Ignored where the dialect has no such syntax.
279
+ */
280
+ readonly materialized?: boolean;
281
+ }
282
+ /**
283
+ * A named query, usable anywhere a model is.
284
+ *
285
+ * @typeParam C - the model class whose rows the CTE yields.
286
+ */
287
+ declare class Cte<C extends ModelClass> {
288
+ /** The alias model: the same columns, under the CTE's name. */
289
+ readonly model: C;
290
+ /** The `WITH` entry this attaches to a statement. */
291
+ readonly node: CteNode;
292
+ constructor(
293
+ /** The alias model: the same columns, under the CTE's name. */
294
+ model: C,
295
+ /** The `WITH` entry this attaches to a statement. */
296
+ node: CteNode);
297
+ /**
298
+ * Select from the CTE, with the `WITH` clause attached.
299
+ *
300
+ * @param columns Optional projection.
301
+ * @returns A builder over the CTE.
302
+ */
303
+ select(): SelectBuilder<InferModel<C>, InferModel<C>>;
304
+ select<K extends keyof InferModel<C> & string>(columns: readonly K[]): SelectBuilder<InferModel<C>, Pick<InferModel<C>, K>>;
305
+ }
306
+ /**
307
+ * Attach a `WITH` entry to a builder that reads from the CTE.
308
+ *
309
+ * Use it when the outer query is not a plain `select(cte.model)` — a join, for
310
+ * instance, whose builder was assembled elsewhere.
311
+ *
312
+ * @param builder The outer query.
313
+ * @param node The `WITH` entry.
314
+ * @returns The same builder shape, carrying the clause.
315
+ */
316
+ declare function attach<B extends {
317
+ node: object;
318
+ }>(builder: B, node: CteNode): B;
319
+ /**
320
+ * A named query the rest of the statement can select from.
321
+ *
322
+ * @param name The CTE's name.
323
+ * @param model The model whose row shape the body yields.
324
+ * @param body The query to name.
325
+ * @param options Materialization hint.
326
+ * @returns The CTE, whose `.model` is usable anywhere a model is.
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * const recent = cte("recent", Order, select(Order).where({ createdAt: { gte: since } }));
331
+ * const rows = await session.execute(recent.select().where({ status: "open" })).all();
332
+ * // WITH "recent" AS (SELECT * FROM "orders" WHERE ...) SELECT * FROM "recent" WHERE ...
333
+ * ```
334
+ */
335
+ declare function cte<C extends ModelClass>(name: string, model: C, body: CteBody<InferModel<C>>, options?: CteOptions): Cte<C>;
1
336
  /**
2
- * tempest-db-js — Phase 2 feasibility spike: the typed query builder.
337
+ * A CTE whose body refers to itself — the way to walk a tree in one statement.
3
338
  *
4
- * The builder is PURE AST + phantom types. It does not touch a database — that is
5
- * Phase 4 (`session.execute`). This file proves that:
6
- * - `select(User)` infers the full row type,
7
- * - `select(User, ["id", "name"])` infers a `Pick` projection,
8
- * - `.where(...)` / `.orderBy(...)` reject keys that are not columns,
9
- * all at compile time.
339
+ * The builder receives the CTE's own alias model, so the recursive branch can
340
+ * join against it.
10
341
  *
11
- * This is a SPIKE, not the final API.
342
+ * @param name The CTE's name.
343
+ * @param model The model whose row shape the body yields.
344
+ * @param build Receives the self-reference and returns the body (a `union`/
345
+ * `unionAll` of the seed and the step).
346
+ * @param options Materialization hint.
347
+ * @returns The CTE.
348
+ *
349
+ * @example
350
+ * ```ts
351
+ * const subtree = cteRecursive("subtree", Category, (self) =>
352
+ * unionAll(
353
+ * select(Category).where({ id: rootId }),
354
+ * join(Category, "c").innerJoin(self, "s", { "c.parentId": "s.id" }).select("c"),
355
+ * ),
356
+ * );
357
+ * ```
12
358
  */
359
+ declare function cteRecursive<C extends ModelClass>(name: string, model: C, build: (self: C) => CteBody<InferModel<C>>, options?: CteOptions): Cte<C>;
13
360
 
361
+ /** The value type an expression produces, for `compute`'s row type. */
362
+ type ExpressionValue<E> = E extends Expression<infer T> ? T : unknown;
363
+ /** Flatten an intersection into one object literal, for readable inference. */
364
+ type Simplify$1<T> = {
365
+ [K in keyof T]: T[K];
366
+ } & {};
14
367
  /** Sort direction for ORDER BY. */
15
368
  type SortDirection = "asc" | "desc";
16
369
  /** One ORDER BY term. */
17
370
  interface OrderTerm {
18
- readonly column: string;
371
+ /** The column to sort by, or an expression (a full-text rank, for instance). */
372
+ readonly column: string | ExprNode;
19
373
  readonly direction: SortDirection;
20
374
  }
21
375
  /** One aggregate expression in a grouped SELECT (`COUNT(*) AS "n"`). */
22
376
  interface AggregateTerm {
23
377
  readonly fn: "count" | "sum" | "avg" | "min" | "max";
24
- /** The column to aggregate, or `"*"` (only valid for `count`). */
25
- readonly column: string | "*";
378
+ /**
379
+ * What is aggregated: a column name, `"*"` (only valid for `count`), or an
380
+ * expression — which is how a conditional aggregate
381
+ * (`SUM(CASE WHEN ... END)`) is expressed.
382
+ */
383
+ readonly column: string | "*" | ExprNode;
26
384
  /** The result alias. */
27
385
  readonly alias: string;
28
386
  }
@@ -52,6 +410,8 @@ interface LockOptions {
52
410
  interface SelectNode {
53
411
  readonly kind: "select";
54
412
  readonly table: string;
413
+ /** The name the table is read under (`FROM "users" AS "sub"`), when aliased. */
414
+ readonly alias?: string | undefined;
55
415
  /** Projected columns, or "*" for the whole row. */
56
416
  readonly columns: readonly string[] | "*";
57
417
  /** Emit `SELECT DISTINCT` when true. */
@@ -64,6 +424,12 @@ interface SelectNode {
64
424
  /** `HAVING` condition, keyed by aggregate alias or grouped column. */
65
425
  readonly having?: CondNode | undefined;
66
426
  readonly orderBy: readonly OrderTerm[];
427
+ /** Per-column codecs for custom types, by property name, when the model has any. */
428
+ readonly codecs?: Readonly<Record<string, ColumnCodec>> | undefined;
429
+ /** `WITH` entries this statement carries, in order. */
430
+ readonly with?: readonly CteNode[] | undefined;
431
+ /** Extra projected expressions, by result alias (window functions, CASE, …). */
432
+ readonly computed?: Readonly<Record<string, ExprNode>> | undefined;
67
433
  readonly limit: number | undefined;
68
434
  readonly offset: number | undefined;
69
435
  /** Row-level locking clause, or `undefined` for none. */
@@ -84,14 +450,70 @@ interface Subquery<T> {
84
450
  /** The AST the outer statement embeds. */
85
451
  readonly node: SelectNode;
86
452
  }
453
+ /**
454
+ * `EXISTS (subquery)` — true when the subquery returns at least one row.
455
+ *
456
+ * The right shape for "is there any…" questions: the database can stop at the
457
+ * first match, which an `IN` over a materialized list cannot. Correlate it by
458
+ * referencing an outer column with a qualified `col("users.id")`.
459
+ *
460
+ * @param subquery The inner SELECT, as a builder or a `.asSubquery()` result.
461
+ * @returns A condition, composable with `and`/`or`/`not`.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * select(User).where(
466
+ * exists(select(Order).where({ userId: col("users.id"), status: "open" })),
467
+ * );
468
+ * // WHERE EXISTS (SELECT * FROM "orders" WHERE "userId" = "users"."id" AND ...)
469
+ * ```
470
+ */
471
+ declare function exists(subquery: SubqueryLike): Condition;
472
+ /**
473
+ * `NOT EXISTS (subquery)` — true when the subquery returns no row.
474
+ *
475
+ * @param subquery The inner SELECT.
476
+ * @returns The condition.
477
+ */
478
+ declare function notExists(subquery: SubqueryLike): Condition;
479
+ /**
480
+ * A scalar subquery — a `SELECT` of one column used where a value goes.
481
+ *
482
+ * Takes the result of `.asSubquery(column)` rather than a bare builder, because
483
+ * that is what pins the projection to exactly **one** column: a scalar subquery
484
+ * returning two columns is a runtime error in every database, and this makes it a
485
+ * compile error instead.
486
+ *
487
+ * @param subquery A single-column subquery.
488
+ * @returns An expression usable in `where`, `orderBy` or an aggregate.
489
+ *
490
+ * @example
491
+ * ```ts
492
+ * select(User).where(
493
+ * scalar(select(Order, ["total"]).where({ userId: col("users.id") })
494
+ * .orderBy("createdAt", "desc").limit(1).asSubquery("total")).gt(100),
495
+ * );
496
+ * ```
497
+ */
498
+ declare function scalar<T>(subquery: Subquery<T>): Expression;
499
+ /** What the subquery helpers accept: a builder, or a narrowed subquery. */
500
+ type SubqueryLike = SelectBuilder<any, any, any> | Subquery<unknown>;
87
501
  /** Runtime guard: is this `in`/`notIn` operand a subquery rather than a list? */
88
502
  declare function isSubquery(value: unknown): value is Subquery<unknown>;
503
+ /**
504
+ * A comparison operand: a value of the column's type, or another expression.
505
+ *
506
+ * Accepting an {@link Expression} is what makes a correlated subquery readable —
507
+ * `where({ userId: col("users.id") })` compares two columns instead of binding
508
+ * the string `"users.id"` as a parameter.
509
+ */
510
+ type Operand<T> = T | Expression;
89
511
  /** Operators valid on every column type. */
90
512
  interface BaseOperators<T> {
91
513
  /** Equal to. */
92
- eq?: T;
514
+ eq?: Operand<T>;
93
515
  /** Not equal to. */
94
- ne?: T;
516
+ ne?: Operand<T>;
95
517
  /**
96
518
  * One of the given values (`IN`) — a list, or a single-column
97
519
  * {@link Subquery} built with `.asSubquery(column)`.
@@ -105,13 +527,13 @@ interface BaseOperators<T> {
105
527
  /** Extra operators for ordered types (numbers, bigint, dates). */
106
528
  interface OrderedOperators<T> extends BaseOperators<T> {
107
529
  /** Greater than. */
108
- gt?: T;
530
+ gt?: Operand<T>;
109
531
  /** Greater than or equal. */
110
- gte?: T;
532
+ gte?: Operand<T>;
111
533
  /** Less than. */
112
- lt?: T;
534
+ lt?: Operand<T>;
113
535
  /** Less than or equal. */
114
- lte?: T;
536
+ lte?: Operand<T>;
115
537
  /** Inclusive range `BETWEEN lo AND hi`. */
116
538
  between?: readonly [T, T];
117
539
  }
@@ -132,7 +554,16 @@ interface StringOperators<T> extends BaseOperators<T> {
132
554
  * wildcards. The safe operator for a case-insensitive lookup (login, email),
133
555
  * and the one that matches a `lower(col)` functional index.
134
556
  */
135
- ieq?: T;
557
+ ieq?: Operand<T>;
558
+ /**
559
+ * Case-insensitive **substring** match of a literal — the safe operator for a
560
+ * search box.
561
+ *
562
+ * The operand is text the user typed, not a pattern: `%` and `_` in it are
563
+ * escaped, so searching for `100%` matches `100%` instead of every row. It
564
+ * compiles to `ILIKE '%…%' ESCAPE '\\'` (`LIKE` where there is no `ILIKE`).
565
+ */
566
+ iContains?: string;
136
567
  }
137
568
  /** Extra operators for array columns (PostgreSQL). */
138
569
  interface ArrayOperators<T> extends BaseOperators<T> {
@@ -159,29 +590,29 @@ type OperatorsFor<T> = [T] extends [readonly unknown[]] ? ArrayOperators<T> : [T
159
590
  * `string`, is a compile error.
160
591
  */
161
592
  type WhereInput<Row = Record<string, unknown>> = {
162
- [K in keyof Row]?: Row[K] | OperatorsFor<NonNullable<Row[K]>>;
593
+ [K in keyof Row]?: Row[K] | Expression | OperatorsFor<NonNullable<Row[K]>>;
163
594
  };
164
595
  /** The full set of operator keys, for the dialect compiler to recognize. */
165
- declare const OPERATORS: readonly ["eq", "ne", "gt", "gte", "lt", "lte", "like", "ilike", "ieq", "in", "notIn", "between", "isNull", "contains", "containedBy", "overlaps"];
596
+ declare const OPERATORS: readonly ["eq", "ne", "gt", "gte", "lt", "lte", "like", "ilike", "ieq", "iContains", "in", "notIn", "between", "isNull", "contains", "containedBy", "overlaps"];
166
597
  /** One supported operator name. */
167
598
  type Operator = (typeof OPERATORS)[number];
168
599
  /** An aggregate expression carrying its result type `T` as a phantom. */
169
600
  declare class Agg<T> {
170
601
  readonly fn: AggregateTerm["fn"];
171
- readonly column: string | "*";
602
+ readonly column: string | "*" | ExprNode;
172
603
  readonly __t: T;
173
- constructor(fn: AggregateTerm["fn"], column: string | "*");
604
+ constructor(fn: AggregateTerm["fn"], column: string | "*" | ExprNode);
174
605
  }
175
606
  /** `COUNT(*)` — the number of rows in the group (never null). */
176
607
  declare function count(): Agg<number>;
177
608
  /** `SUM(column)` — null when the group has no non-null values. */
178
- declare function sum(column: string): Agg<number | null>;
609
+ declare function sum(column: string | Expression): Agg<number | null>;
179
610
  /** `AVG(column)` — null when the group has no non-null values. */
180
- declare function avg(column: string): Agg<number | null>;
611
+ declare function avg(column: string | Expression): Agg<number | null>;
181
612
  /** `MIN(column)` — numeric columns; null on an empty group. */
182
- declare function min(column: string): Agg<number | null>;
613
+ declare function min(column: string | Expression): Agg<number | null>;
183
614
  /** `MAX(column)` — numeric columns; null on an empty group. */
184
- declare function max(column: string): Agg<number | null>;
615
+ declare function max(column: string | Expression): Agg<number | null>;
185
616
  /** Extract the phantom result type of an aggregate expression. */
186
617
  type AggResult<A> = A extends Agg<infer T> ? T : never;
187
618
  /** Flatten an intersection into a single object literal. */
@@ -259,7 +690,26 @@ declare class SelectBuilder<Full, Proj = Full, Grouped extends boolean = false>
259
690
  * @param direction `"asc"` (default) or `"desc"`.
260
691
  * @returns A builder carrying the ordering term.
261
692
  */
262
- orderBy(column: (keyof Full & string) | (keyof Proj & string), direction?: SortDirection): SelectBuilder<Full, Proj, Grouped>;
693
+ /**
694
+ * Project extra expressions alongside the columns, by alias.
695
+ *
696
+ * This is where a window function lands: unlike `aggregate()`, it does **not**
697
+ * group — every row stays, with the computed value attached.
698
+ *
699
+ * @param map Alias → expression.
700
+ * @returns A builder whose row type carries the aliases.
701
+ *
702
+ * @example
703
+ * ```ts
704
+ * select(Sale).compute({
705
+ * rank: over(rowNumber(), { partitionBy: ["region"], orderBy: [["total", "desc"]] }),
706
+ * });
707
+ * ```
708
+ */
709
+ compute<M extends Record<string, Expression<unknown>>>(map: M): SelectBuilder<Full, Simplify$1<Proj & {
710
+ [K in keyof M]: ExpressionValue<M[K]>;
711
+ }>, Grouped>;
712
+ orderBy(column: (keyof Full & string) | (keyof Proj & string) | Expression, direction?: SortDirection): SelectBuilder<Full, Proj, Grouped>;
263
713
  /** Limit the number of rows. */
264
714
  limit(n: number): SelectBuilder<Full, Proj, Grouped>;
265
715
  /** Skip the first `n` rows. */
@@ -369,7 +819,49 @@ type ExprNode = {
369
819
  readonly kind: "fn";
370
820
  readonly name: string;
371
821
  readonly args: readonly ExprNode[];
822
+ } | {
823
+ readonly kind: "case";
824
+ readonly branches: readonly {
825
+ readonly when: CondNode;
826
+ readonly result: ExprNode;
827
+ }[];
828
+ readonly fallback: ExprNode | null;
829
+ } | {
830
+ readonly kind: "cast";
831
+ readonly operand: ExprNode;
832
+ readonly to: CastType;
833
+ } | {
834
+ readonly kind: "scalar";
835
+ readonly select: unknown;
836
+ } | {
837
+ readonly kind: "star";
838
+ } | {
839
+ readonly kind: "window";
840
+ /** The function being windowed (`row_number()`, `sum(total)`, …). */
841
+ readonly fn: ExprNode;
842
+ /** `PARTITION BY` columns, by property name. */
843
+ readonly partitionBy: readonly string[];
844
+ /** `ORDER BY` inside the window. */
845
+ readonly orderBy: readonly {
846
+ readonly column: string;
847
+ readonly direction: "asc" | "desc";
848
+ }[];
849
+ /** An explicit frame clause (`ROWS BETWEEN …`), or `null` for the default. */
850
+ readonly frame: string | null;
851
+ } | {
852
+ readonly kind: "rank";
853
+ readonly columns: readonly string[];
854
+ readonly term: string;
855
+ readonly language: string;
372
856
  };
857
+ /**
858
+ * A target type for {@link cast}.
859
+ *
860
+ * Kept as a portable vocabulary rather than raw SQL: each dialect maps it to the
861
+ * name it actually accepts (`integer` is `INTEGER` on PostgreSQL and `SIGNED` on
862
+ * MySQL), so the same model does not need a different cast per database.
863
+ */
864
+ type CastType = "integer" | "bigint" | "real" | "numeric" | "text" | "boolean" | "date" | "datetime" | "timestamp" | "uuid" | "json" | "jsonb" | "blob";
373
865
  /** Logical condition nodes. */
374
866
  type CondNode = CondFields | {
375
867
  readonly kind: "and";
@@ -385,6 +877,22 @@ type CondNode = CondFields | {
385
877
  readonly left: ExprNode;
386
878
  readonly op: Operator;
387
879
  readonly right: ExprNode;
880
+ } | {
881
+ readonly kind: "exists";
882
+ /** The subquery's SELECT node. */
883
+ readonly select: unknown;
884
+ /** True for `NOT EXISTS`. */
885
+ readonly negate: boolean;
886
+ } | {
887
+ readonly kind: "fullText";
888
+ readonly columns: readonly string[];
889
+ readonly term: string;
890
+ readonly language: string;
891
+ /**
892
+ * The substring condition to compile where there is no text-search engine.
893
+ * Built once, here, so the dialects do not each reimplement the fallback.
894
+ */
895
+ readonly fallback: CondNode;
388
896
  };
389
897
  declare const CONDITION: unique symbol;
390
898
  /** A composed condition produced by `and`/`or`/`not`. */
@@ -415,9 +923,11 @@ declare function isExpression(value: unknown): value is Expression;
415
923
  * expressible at all. An operand that is not an `Expression` is bound as a
416
924
  * parameter, so `.eq(probe)` stays safe by default.
417
925
  */
418
- declare class Expression {
926
+ declare class Expression<T = unknown> {
419
927
  /** The expression AST the dialect renders. */
420
928
  readonly node: ExprNode;
929
+ /** Phantom: the value type this expression produces, read only by the types. */
930
+ readonly __t?: T;
421
931
  constructor(
422
932
  /** The expression AST the dialect renders. */
423
933
  node: ExprNode);
@@ -497,18 +1007,57 @@ declare function col<Row = Record<string, unknown>>(name: keyof Row & string): E
497
1007
  */
498
1008
  declare function val(value: unknown): Expression;
499
1009
  /**
500
- * Build a call to a SQL function.
1010
+ * A `CASE WHEN ... THEN ... ELSE ... END` expression.
501
1011
  *
502
- * A bare string argument is a **column name** — that is the useful default here
503
- * (`fn.lower("username")`), and it is why a literal has to be wrapped in
504
- * {@link val}. The function name is interpolated into the statement, so it is
505
- * validated as a plain identifier and must never come from user input; the
506
- * arguments are always rendered through the expression compiler.
1012
+ * The branch conditions are the same `where` language used everywhere else — the
1013
+ * object form or a `Condition` — so no second grammar shows up just for `CASE`.
1014
+ * The branch results are expressions, and a bare value there is **bound**, not
1015
+ * interpolated.
507
1016
  *
508
- * @param name The SQL function name.
509
- * @param args Column names or expressions.
510
- * @returns An expression for the call.
511
- * @throws Error When `name` is not a plain SQL identifier.
1017
+ * The classic use is a conditional aggregate: summing only the rows that match,
1018
+ * in one pass over the table instead of one query per bucket.
1019
+ *
1020
+ * @param branches `[condition, result]` pairs, evaluated in order.
1021
+ * @param fallback The `ELSE` result. Omitted, a row matching no branch is `NULL`.
1022
+ * @returns An expression usable anywhere an expression is.
1023
+ * @throws Error When no branch is given — `CASE END` is not valid SQL.
1024
+ *
1025
+ * @example
1026
+ * ```ts
1027
+ * select(Order).aggregate(["customer"], {
1028
+ * paid: sum(caseWhen([[{ status: "paid" }, col("total")]], val(0))),
1029
+ * });
1030
+ * // SUM(CASE WHEN "status" = $1 THEN "total" ELSE $2 END) AS "paid"
1031
+ * ```
1032
+ */
1033
+ declare function caseWhen<Row = Record<string, unknown>>(branches: readonly (readonly [WhereArg<Row>, unknown])[], fallback?: unknown): Expression;
1034
+ /**
1035
+ * A `CAST(x AS type)` expression.
1036
+ *
1037
+ * @param operand The expression to convert; a bare string is a column name.
1038
+ * @param to The target type, from the portable {@link CastType} vocabulary.
1039
+ * @returns An expression for the cast.
1040
+ *
1041
+ * @example
1042
+ * ```ts
1043
+ * select(Event).where(cast("externalId", "integer").eq(42));
1044
+ * // WHERE CAST("externalId" AS INTEGER) = $1
1045
+ * ```
1046
+ */
1047
+ declare function cast(operand: Expression | string, to: CastType): Expression;
1048
+ /**
1049
+ * Build a call to a SQL function.
1050
+ *
1051
+ * A bare string argument is a **column name** — that is the useful default here
1052
+ * (`fn.lower("username")`), and it is why a literal has to be wrapped in
1053
+ * {@link val}. The function name is interpolated into the statement, so it is
1054
+ * validated as a plain identifier and must never come from user input; the
1055
+ * arguments are always rendered through the expression compiler.
1056
+ *
1057
+ * @param name The SQL function name.
1058
+ * @param args Column names or expressions.
1059
+ * @returns An expression for the call.
1060
+ * @throws Error When `name` is not a plain SQL identifier.
512
1061
  */
513
1062
  declare function call(name: string, ...args: (Expression | string)[]): Expression;
514
1063
  /**
@@ -550,6 +1099,90 @@ declare function or<Row = Record<string, unknown>>(...inputs: WhereArg<NoInfer<R
550
1099
  /** Negate a condition with `NOT`. */
551
1100
  declare function not<Row = Record<string, unknown>>(input: WhereArg<NoInfer<Row>>): Condition;
552
1101
 
1102
+ /**
1103
+ * tempest-db-js — window functions.
1104
+ *
1105
+ * The questions a `GROUP BY` cannot answer without a second query: the position
1106
+ * of a row inside its group, a running total, the difference to the previous row.
1107
+ * Without them, "top 3 per region" becomes one query per region — the N+1 of
1108
+ * reporting.
1109
+ */
1110
+
1111
+ /**
1112
+ * A function that is only legal **inside** a window.
1113
+ *
1114
+ * `row_number()` and `lag()` are errors without an `OVER` clause — SQLite says
1115
+ * "misuse of window function", PostgreSQL "window function ... requires an OVER
1116
+ * clause". Making them their own type means {@link over} is the only place they
1117
+ * can go, so the mistake does not compile instead of failing at runtime.
1118
+ */
1119
+ interface WindowFn<T = unknown> {
1120
+ /** The call node this wraps. */
1121
+ readonly call: ExprNode;
1122
+ /** Phantom: the value the function produces. */
1123
+ readonly __t?: T;
1124
+ }
1125
+ /** How a window is framed around the current row. */
1126
+ interface WindowSpec {
1127
+ /** Restart the window for each distinct value of these columns. */
1128
+ readonly partitionBy?: readonly string[];
1129
+ /** Order inside the window — what `rank` and a running total are computed over. */
1130
+ readonly orderBy?: readonly (string | readonly [string, "asc" | "desc"])[];
1131
+ /**
1132
+ * An explicit frame (`ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`).
1133
+ *
1134
+ * Omitted, the database's default applies — and the default is `RANGE`, which
1135
+ * for a running total lumps **every peer** of the current row together. When
1136
+ * rows share an `orderBy` value and that matters, say `ROWS` explicitly.
1137
+ */
1138
+ readonly frame?: string;
1139
+ }
1140
+ /**
1141
+ * Apply a function `OVER` a window.
1142
+ *
1143
+ * @param fn The window function ({@link rowNumber}, {@link lag}, …) or an
1144
+ * aggregate (`sum("total")`) used as one.
1145
+ * @param spec Partitioning, ordering and framing.
1146
+ * @returns An expression usable in `compute`, `where` and `orderBy`.
1147
+ *
1148
+ * @example
1149
+ * ```ts
1150
+ * select(Sale).compute({
1151
+ * rank: over(rowNumber(), { partitionBy: ["region"], orderBy: [["total", "desc"]] }),
1152
+ * running: over(sum("total"), { partitionBy: ["region"], orderBy: ["date"] }),
1153
+ * });
1154
+ * ```
1155
+ */
1156
+ declare function over<T = unknown>(fn: WindowFn<T> | Expression<T> | Agg<T>, spec?: WindowSpec): Expression<T>;
1157
+ /** `row_number()` — 1, 2, 3 … within the window, with no ties. */
1158
+ declare function rowNumber(): WindowFn<number>;
1159
+ /** `rank()` — ties share a position, and the next one skips (1, 1, 3). */
1160
+ declare function rank(): WindowFn<number>;
1161
+ /** `dense_rank()` — ties share a position, and the next one does not skip (1, 1, 2). */
1162
+ declare function denseRank(): WindowFn<number>;
1163
+ /** `percent_rank()` — the rank as a fraction between 0 and 1. */
1164
+ declare function percentRank(): WindowFn<number>;
1165
+ /**
1166
+ * `lag(column, offset)` — the value from a row **behind** the current one.
1167
+ *
1168
+ * @param column The column to read.
1169
+ * @param offset How many rows back (default 1).
1170
+ * @returns The expression.
1171
+ */
1172
+ declare function lag<T = unknown>(column: string, offset?: number): WindowFn<T | null>;
1173
+ /**
1174
+ * `lead(column, offset)` — the value from a row **ahead** of the current one.
1175
+ *
1176
+ * @param column The column to read.
1177
+ * @param offset How many rows forward (default 1).
1178
+ * @returns The expression.
1179
+ */
1180
+ declare function lead<T = unknown>(column: string, offset?: number): WindowFn<T | null>;
1181
+ /** `first_value(column)` — the first value in the window. */
1182
+ declare function firstValue<T = unknown>(column: string): WindowFn<T | null>;
1183
+ /** `last_value(column)` — the last value in the window (mind the default frame). */
1184
+ declare function lastValue<T = unknown>(column: string): WindowFn<T | null>;
1185
+
553
1186
  /**
554
1187
  * tempest-db-js — Phase 2: typed INSERT / UPDATE / DELETE builders.
555
1188
  *
@@ -571,11 +1204,11 @@ type Returning = readonly string[] | "*" | null;
571
1204
  * optional.
572
1205
  */
573
1206
  type WriteValues<Row> = {
574
- [K in keyof Row]: Row[K] | SqlExpression;
1207
+ [K in keyof Row]: Row[K] | SqlExpression | Expression;
575
1208
  };
576
1209
  /** A partial write shape — the `SET` clause of an UPDATE or a `DO UPDATE`. */
577
1210
  type WritePatch<Row> = {
578
- [K in keyof Row]?: Row[K] | SqlExpression;
1211
+ [K in keyof Row]?: Row[K] | SqlExpression | Expression;
579
1212
  };
580
1213
  /**
581
1214
  * Conflict-resolution clause for an INSERT (`ON CONFLICT`). `target` is the
@@ -611,8 +1244,19 @@ interface OnConflictUpdateOptions<Full> {
611
1244
  /** Serializable AST for an INSERT. */
612
1245
  interface InsertNode {
613
1246
  readonly kind: "insert";
1247
+ /** Per-column codecs for custom types, by property name. */
1248
+ readonly codecs?: Readonly<Record<string, ColumnCodec>> | undefined;
614
1249
  readonly table: string;
615
1250
  readonly values: readonly Record<string, unknown>[];
1251
+ /**
1252
+ * A `SELECT` feeding the insert, with the target columns it fills.
1253
+ *
1254
+ * Set by {@link InsertBuilder.fromSelect}; mutually exclusive with `values`.
1255
+ */
1256
+ readonly fromSelect?: {
1257
+ readonly columns: readonly string[];
1258
+ readonly select: unknown;
1259
+ } | undefined;
616
1260
  readonly returning: Returning;
617
1261
  /** Conflict handling (`ON CONFLICT ...`), or `undefined` for none. */
618
1262
  readonly onConflict?: OnConflict;
@@ -676,6 +1320,31 @@ declare class InsertBuilder<Full, Ins, Ret = number> {
676
1320
  * @throws ValidationError When a `set` value cannot be bound.
677
1321
  */
678
1322
  onConflictDoUpdate(target: readonly (keyof Full & string)[], set: WritePatch<Full>, options?: OnConflictUpdateOptions<Full>): InsertBuilder<Full, Ins, Ret>;
1323
+ /**
1324
+ * Fill the table from another query — `INSERT INTO t (a, b) SELECT …`.
1325
+ *
1326
+ * The rows never leave the database, which is the point: archiving or copying a
1327
+ * million rows should not become a million round trips through this process.
1328
+ *
1329
+ * @param columns The target columns, in the order the query projects them.
1330
+ * @param query The query producing the rows.
1331
+ * @returns A builder ready to execute.
1332
+ * @throws Error When no target column is given.
1333
+ *
1334
+ * @example
1335
+ * ```ts
1336
+ * insert(ArchivedOrder).fromSelect(
1337
+ * ["id", "total"],
1338
+ * select(Order, ["id", "total"]).where({ createdAt: { lt: cutoff } }),
1339
+ * );
1340
+ * ```
1341
+ */
1342
+ fromSelect<K extends keyof Ins & string>(columns: readonly K[], query: {
1343
+ readonly node: unknown;
1344
+ readonly __row: {
1345
+ [P in K]: unknown;
1346
+ };
1347
+ }): InsertBuilder<Full, Ins, Ret>;
679
1348
  /** Return the full inserted row(s). */
680
1349
  returning(): InsertBuilder<Full, Ins, Full>;
681
1350
  /** Return only the given columns of the inserted row(s). */
@@ -686,7 +1355,14 @@ declare function insert<C extends ModelClass>(model: C): InsertBuilder<InferMode
686
1355
  /** Serializable AST for an UPDATE. */
687
1356
  interface UpdateNode {
688
1357
  readonly kind: "update";
1358
+ /** Per-column codecs for custom types, by property name. */
1359
+ readonly codecs?: Readonly<Record<string, ColumnCodec>> | undefined;
689
1360
  readonly table: string;
1361
+ /** Extra source tables (`UPDATE ... FROM other`), by alias. */
1362
+ readonly from?: readonly {
1363
+ readonly table: string;
1364
+ readonly alias: string;
1365
+ }[];
690
1366
  readonly set: Record<string, unknown>;
691
1367
  readonly where: CondNode | undefined;
692
1368
  /** True once a where-clause or explicit opt-in makes the write safe. */
@@ -732,6 +1408,21 @@ declare class UpdateBuilder<Full, Guarded extends boolean, Ret = number> {
732
1408
  * ```
733
1409
  */
734
1410
  set(values: WritePatch<Full>): UpdateBuilder<Full, Guarded, Ret>;
1411
+ /**
1412
+ * Read from another table while updating — `UPDATE t SET … FROM other WHERE …`.
1413
+ *
1414
+ * The join condition goes in `where`, where SQL wants it:
1415
+ * `.where({ customerId: col("c.id") })`.
1416
+ *
1417
+ * PostgreSQL and SQLite (3.33+) only. MySQL spells this as a multi-table
1418
+ * `UPDATE a JOIN b`, which is out of this project's active scope, so it throws
1419
+ * there rather than emitting something the server rejects.
1420
+ *
1421
+ * @param model The extra source.
1422
+ * @param alias The name to reference it by.
1423
+ * @returns A builder carrying the extra source.
1424
+ */
1425
+ from<C extends ModelClass>(model: C, alias: string): UpdateBuilder<Full, Guarded, Ret>;
735
1426
  /** Restrict the rows to update. Marks the builder safe to execute. */
736
1427
  where(input: WhereInput<Full> | Condition): UpdateBuilder<Full, true, Ret>;
737
1428
  /** Explicit opt-in to update EVERY row. Use deliberately. */
@@ -746,7 +1437,14 @@ declare function update<C extends ModelClass>(model: C): UpdateBuilder<InferMode
746
1437
  /** Serializable AST for a DELETE. */
747
1438
  interface DeleteNode {
748
1439
  readonly kind: "delete";
1440
+ /** Per-column codecs for custom types, by property name. */
1441
+ readonly codecs?: Readonly<Record<string, ColumnCodec>> | undefined;
749
1442
  readonly table: string;
1443
+ /** Extra source tables (`DELETE ... USING other`), by alias. */
1444
+ readonly using?: readonly {
1445
+ readonly table: string;
1446
+ readonly alias: string;
1447
+ }[];
750
1448
  readonly where: CondNode | undefined;
751
1449
  readonly guarded: boolean;
752
1450
  readonly returning: Returning;
@@ -770,6 +1468,18 @@ declare class DeleteBuilder<Full, Guarded extends boolean, Ret = number> {
770
1468
  /** The source model, used to coerce returned rows on execution. */
771
1469
  source: ModelClass);
772
1470
  private with;
1471
+ /**
1472
+ * Delete by matching another table — `DELETE FROM t USING other WHERE …`.
1473
+ *
1474
+ * PostgreSQL only. SQLite and MySQL have no `USING` here; there the portable
1475
+ * form is `where({ id: { in: select(Other, ["id"]).where(...).asSubquery("id") } })`,
1476
+ * and this throws rather than pretending.
1477
+ *
1478
+ * @param model The extra source.
1479
+ * @param alias The name to reference it by.
1480
+ * @returns A builder carrying the extra source.
1481
+ */
1482
+ using<C extends ModelClass>(model: C, alias: string): DeleteBuilder<Full, Guarded, Ret>;
773
1483
  /** Restrict the rows to delete. Marks the builder safe to execute. */
774
1484
  where(input: WhereInput<Full> | Condition): DeleteBuilder<Full, true, Ret>;
775
1485
  /** Explicit opt-in to delete EVERY row. Use deliberately. */
@@ -855,6 +1565,10 @@ declare class ValidationError extends Error {
855
1565
  readonly issues: readonly string[];
856
1566
  constructor(table: string, issues: readonly string[]);
857
1567
  }
1568
+ /** Encode one native row value to its JSON-safe form, by column kind. */
1569
+ declare function encodeValue(column: Column<unknown>, value: unknown): unknown;
1570
+ /** Decode one dict value to its native row form, by column kind. */
1571
+ declare function decodeValue(column: Column<unknown>, value: unknown): unknown;
858
1572
  /**
859
1573
  * Convert a row to a plain dict of native values, restricted to known columns.
860
1574
  * Strips any non-column properties; keeps `Date`/`bigint`/`Uint8Array` as-is.
@@ -893,400 +1607,166 @@ declare function fromDict<C extends ModelClass>(model: C, data: Record<string, u
893
1607
  declare function parse<C extends ModelClass>(model: C, json: string): InferModel<C>;
894
1608
 
895
1609
  /**
896
- * tempest-db-js — Phase 5: typed joins with composite result types.
1610
+ * tempest-db-js — capture the query plans of everything a block runs.
897
1611
  *
898
- * `join(User, "user").innerJoin(Order, "order", { "user.id": "order.userId" })`
899
- * yields rows shaped `{ user: UserRow; order: OrderRow }`. A `leftJoin` makes the
900
- * joined side nullable (`OrderRow | null`), matching SQL outer-join semantics.
901
- *
902
- * Columns are aliased in SQL (`"user"."id" AS "user.id"`) so a flat driver row is
903
- * split back into one nested object per source, each coerced by its model.
1612
+ * A development tool for "why is this endpoint slow?", where the honest answer
1613
+ * usually needs the database's own opinion rather than a wall-clock number.
1614
+ * Wrapping the code beats copying SQL out of a log by hand, because the
1615
+ * parameters that go into the plan are the ones the code actually used.
904
1616
  */
905
1617
 
906
- /** One joined table. */
907
- interface JoinClause {
908
- readonly kind: "inner" | "left";
909
- readonly table: string;
910
- readonly alias: string;
911
- /** Equality pairs of qualified columns: `["user.id", "order.userId"]`. */
912
- readonly on: readonly (readonly [string, string])[];
913
- }
914
- /** A selected column, qualified by source alias. */
915
- interface JoinSelection {
916
- readonly alias: string;
917
- readonly column: string;
1618
+ /** One statement's plan. */
1619
+ interface QueryPlan {
1620
+ /** The statement that was explained. */
1621
+ readonly sql: string;
1622
+ /** The parameters it ran with. */
1623
+ readonly params: readonly unknown[];
1624
+ /** The database's plan, in its own shape (JSON on PostgreSQL, rows on SQLite). */
1625
+ readonly plan: unknown;
1626
+ /** A one-line, human-readable digest of the plan. */
1627
+ summary(): string;
918
1628
  }
919
- /** Collapse a union of object types into their intersection. */
920
- type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
921
- /**
922
- * `where` filter for a join. Keys are `alias.column` refs; each value accepts a
923
- * bare value (shorthand for `eq`) or an operator object restricted to the
924
- * operators valid for that column's type — exactly like the single-table
925
- * {@link WhereInput}, but qualified per source. `like` on a numeric join column,
926
- * or `gt` on a string one, is a compile error.
927
- */
928
- type JoinWhereInput<S extends Sources> = Partial<UnionToIntersection<{
929
- [A in keyof S]: {
930
- [C in keyof NonNullable<S[A]> & string as `${A & string}.${C}`]: NonNullable<S[A]>[C] | OperatorsFor<NonNullable<NonNullable<S[A]>[C]>>;
931
- };
932
- }[keyof S]>>;
933
- /** Serializable AST for a multi-table SELECT. */
934
- interface JoinNode {
935
- readonly kind: "join_select";
936
- readonly base: {
937
- readonly table: string;
938
- readonly alias: string;
939
- };
940
- readonly joins: readonly JoinClause[];
941
- readonly selections: readonly JoinSelection[];
942
- readonly where: CondNode | undefined;
943
- readonly orderBy: readonly {
944
- readonly ref: string;
945
- readonly direction: SortDirection;
946
- }[];
947
- readonly limit: number | undefined;
948
- readonly offset: number | undefined;
949
- /** Per-alias property → column maps, for the sources that rename columns. */
950
- readonly names?: Readonly<Record<string, NameMap>> | undefined;
1629
+ /** Everything a block ran, with a plan each. */
1630
+ interface ExplainReport {
1631
+ /** One entry per statement, in execution order. */
1632
+ readonly plans: readonly QueryPlan[];
1633
+ /** A multi-line digest of every plan, for a test failure message or a log. */
1634
+ summary(): string;
951
1635
  }
952
- /** A map of source alias → its (possibly nullable) row type. */
953
- type Sources = Record<string, object | null>;
954
- /** Every `alias.column` reference across the current sources. */
955
- type ColRef<S extends Sources> = {
956
- [A in keyof S]: `${A & string}.${keyof NonNullable<S[A]> & string}`;
957
- }[keyof S];
958
- /** Valid `alias.column` references for a newly joined model. */
959
- type RightRef<A extends string, C extends ModelClass> = `${A}.${keyof InferModel<C> & string}`;
960
- /** The `on` condition: map existing-source refs to new-table refs (equality). */
961
- type JoinOn<S extends Sources, A extends string, C extends ModelClass> = Partial<Record<ColRef<S>, RightRef<A, C>>>;
962
- /**
963
- * Immutable, chainable multi-table SELECT builder.
964
- *
965
- * @typeParam S - the accumulated sources (alias → row type; nullable for left joins).
966
- */
967
- declare class JoinBuilder<S extends Sources> {
968
- readonly node: JoinNode;
969
- /** Source models keyed by alias, for SQL expansion and row coercion. */
970
- readonly sources: Readonly<Record<string, ModelClass>>;
971
- /** Phantom: the composite result row type. */
972
- readonly __row: {
973
- [A in keyof S]: S[A];
974
- };
975
- constructor(node: JoinNode,
976
- /** Source models keyed by alias, for SQL expansion and row coercion. */
977
- sources: Readonly<Record<string, ModelClass>>);
978
- private add;
979
- private clause;
980
- /** Inner join another model under `alias`. */
981
- innerJoin<C extends ModelClass, A extends string>(model: C, alias: A, on: JoinOn<S, A, C>): JoinBuilder<S & {
982
- [K in A]: InferModel<C>;
983
- }>;
984
- /** Left (outer) join another model under `alias` — its side becomes nullable. */
985
- leftJoin<C extends ModelClass, A extends string>(model: C, alias: A, on: JoinOn<S, A, C>): JoinBuilder<S & {
986
- [K in A]: InferModel<C> | null;
987
- }>;
988
- /** Filter by `alias.column` references (object form) or an `and`/`or`/`not`. */
989
- where(input: JoinWhereInput<S> | Condition): JoinBuilder<S>;
990
- /** Order by an `alias.column` reference. */
991
- orderBy(ref: ColRef<S>, direction?: SortDirection): JoinBuilder<S>;
992
- limit(n: number): JoinBuilder<S>;
993
- offset(n: number): JoinBuilder<S>;
1636
+ /** Options for {@link explainQueries}. */
1637
+ interface ExplainOptions {
1638
+ /**
1639
+ * Run `EXPLAIN ANALYZE`, which **executes** the statement to measure it.
1640
+ *
1641
+ * Refused for anything that is not a `SELECT`: analyzing an `UPDATE` would
1642
+ * apply it a second time.
1643
+ */
1644
+ readonly analyze?: boolean;
1645
+ /** Explain only the statements matching this predicate. */
1646
+ readonly filter?: (sql: string) => boolean;
994
1647
  }
1648
+ /** True for a statement that only reads. */
1649
+ declare function isReadOnlyStatement(sql: string): boolean;
995
1650
  /**
996
- * Start a multi-table query from a base model under `alias`.
1651
+ * Build the digest of a PostgreSQL JSON plan or a SQLite plan row set.
997
1652
  *
998
- * @param model The base model.
999
- * @param alias The alias to key the base table's rows under in the result.
1000
- * @returns A `JoinBuilder` with the base source registered.
1653
+ * @param plan The raw plan.
1654
+ * @returns One line describing it.
1001
1655
  */
1002
- declare function join<C extends ModelClass, A extends string>(model: C, alias: A): JoinBuilder<{
1003
- [K in A]: InferModel<C>;
1004
- }>;
1656
+ declare function summarizePlan(plan: unknown): string;
1005
1657
 
1006
1658
  /**
1007
- * tempest-db-js — Phase 4a: dialect SQL compilation.
1659
+ * tempest-db-js — an opt-in unit of work with an identity map.
1008
1660
  *
1009
- * Turns the dialect-neutral AST (`SelectNode`, `InsertNode`, `UpdateNode`,
1010
- * `DeleteNode` from Phases 1-2) into `{ sql, params }`. This is the ONLY place
1011
- * SQL is produced — always parameterized (`?` for SQLite, `$1` for PostgreSQL),
1012
- * never string interpolation, so it is injection-safe by construction.
1661
+ * The default stays what it has always been: a row is a plain object, and a write
1662
+ * happens when you ask for it. This adds the other model, for the code that wants
1663
+ * it — load a row once, mutate it, and let one `flush()` work out the statements.
1013
1664
  *
1014
- * It does NOT touch a database — execution is Phase 4b (`session.execute`).
1665
+ * Two things it buys, both of which the plain path cannot:
1666
+ *
1667
+ * - **Identity.** Loading the same row twice returns the **same object**, so two
1668
+ * references cannot drift apart in memory while both believe they are the row.
1669
+ * - **Batching.** Ten mutations become the statements the changes actually
1670
+ * require, in one transaction, instead of ten round trips.
1015
1671
  */
1016
1672
 
1017
- /** A compiled, parameterized statement ready to hand to a driver. */
1018
- interface CompiledQuery {
1019
- readonly sql: string;
1020
- readonly params: readonly unknown[];
1021
- }
1022
- /** Any compilable AST node. */
1023
- type QueryNode = SelectNode | InsertNode | UpdateNode | DeleteNode | JoinNode;
1673
+ /** Phantom brand distinguishing a tracked row from a plain one. */
1674
+ declare const TRACKED: unique symbol;
1024
1675
  /**
1025
- * Collects bound parameters and renders placeholders in dialect style. Exposed
1026
- * because dialect subclasses receive it when overriding clause rendering.
1676
+ * A row the unit of work is watching.
1677
+ *
1678
+ * The brand is what makes "tracked" visible in the type: a function taking a
1679
+ * `Tracked<Row>` cannot be handed a plain object that nothing will ever flush.
1027
1680
  */
1028
- declare class Params {
1029
- private readonly placeholder;
1030
- readonly values: unknown[];
1031
- constructor(placeholder: (index: number) => string);
1032
- bind(value: unknown): string;
1681
+ type Tracked<Row> = Row & {
1682
+ readonly [TRACKED]?: true;
1683
+ };
1684
+ /** What a flush did. */
1685
+ interface FlushResult {
1686
+ /** Rows inserted. */
1687
+ readonly inserted: number;
1688
+ /** Rows updated. */
1689
+ readonly updated: number;
1690
+ /** Rows deleted. */
1691
+ readonly deleted: number;
1033
1692
  }
1034
1693
  /**
1035
- * Base SQL compiler shared by every dialect. Subclasses customize only what
1036
- * actually differs between databases (placeholder syntax, `ILIKE` support).
1694
+ * An identity map plus a change log, flushed as one transaction.
1695
+ *
1696
+ * Scope it explicitly — one per request, one per job — and flush it before it
1697
+ * goes away. Nothing here is global.
1037
1698
  */
1038
- declare abstract class BaseDialect {
1039
- abstract readonly name: Dialect;
1040
- /**
1041
- * INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
1042
- * returning). Shared across dialect instances — the key namespaces by dialect
1043
- * name, and the placeholder text is dialect-specific but structure-determined.
1044
- */
1045
- private static readonly insertTemplates;
1046
- /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
1047
- private static readonly quotedIds;
1048
- /** Render the n-th (1-based) placeholder. */
1049
- protected abstract placeholder(index: number): string;
1050
- /** Render a case-insensitive LIKE for the active dialect. */
1051
- protected abstract ilike(column: string, param: string): string;
1699
+ declare class UnitOfWork {
1700
+ private readonly session;
1701
+ private readonly entries;
1702
+ constructor(session: AsyncSession);
1703
+ /** How many rows are being tracked. */
1704
+ get size(): number;
1052
1705
  /**
1053
- * Validate a subquery operand before it is rendered, for dialects that restrict
1054
- * what an `IN (SELECT ...)` may contain. The default accepts everything.
1706
+ * Load a row by primary key, or return the one already loaded.
1055
1707
  *
1056
- * @param _node The subquery's AST.
1057
- * @throws Error When the dialect cannot execute this subquery.
1708
+ * The second call for the same key does **not** hit the database and returns
1709
+ * the **same object** as the first.
1710
+ *
1711
+ * @param model The model class.
1712
+ * @param key The primary key — a value, or an object for a composite key.
1713
+ * @returns The tracked row, or `null` when there is none.
1058
1714
  */
1059
- protected checkSubquery(_node: SelectNode): void;
1715
+ get<C extends ModelClass>(model: C, key: unknown): Promise<Tracked<InferModel<C>> | null>;
1060
1716
  /**
1061
- * The SQL operator for an array containment/overlap test.
1717
+ * Track an existing row that was loaded elsewhere.
1062
1718
  *
1063
- * Only PostgreSQL has native arrays; the other dialects throw rather than
1064
- * emitting an operator that means something else there.
1065
- *
1066
- * @param op The array operator name.
1067
- * @returns The SQL operator text.
1068
- * @throws Error On a dialect without native array support.
1719
+ * @param model The model class.
1720
+ * @param row The row, as loaded.
1721
+ * @returns The tracked object — the same one, if this row is already known.
1069
1722
  */
1070
- protected arrayOperator(op: "contains" | "containedBy" | "overlaps"): string;
1723
+ track<C extends ModelClass>(model: C, row: InferModel<C>): Tracked<InferModel<C>>;
1071
1724
  /**
1072
- * Quote an identifier (column/table) for the active dialect.
1725
+ * Schedule an insert.
1073
1726
  *
1074
- * Memoized: identifiers form a small, stable set (column/table names), but this
1075
- * runs for every identifier on every compile. Caching the quoted form removes a
1076
- * regex-replace + string allocation from the hot path. The standard double-quote
1077
- * form is identical across both dialects, so one shared cache is correct.
1727
+ * @param model The model class.
1728
+ * @param data The row to insert; it must carry the primary key, since the
1729
+ * identity map is keyed by it and a database-generated id is not known yet.
1730
+ * @returns The tracked row.
1731
+ * @throws Error When the key is incomplete.
1078
1732
  */
1079
- protected quoteId(name: string): string;
1080
- /** Compile any node to `{ sql, params }`. */
1081
- compile(node: QueryNode): CompiledQuery;
1733
+ add<C extends ModelClass>(model: C, data: InferInsert<C>): Tracked<InferModel<C>>;
1082
1734
  /**
1083
- * Render a qualified `alias.column` ref as `"alias"."column"`, translating the
1084
- * property name to the real column name for that alias's model.
1735
+ * Schedule a delete.
1085
1736
  *
1086
- * @param ref The `alias.property` reference (a bare name is left unqualified).
1087
- * @param names The node's per-alias name maps, if any source renames columns.
1088
- * @returns The quoted, qualified identifier.
1737
+ * A row added and then removed before the flush simply disappears — no
1738
+ * statement is emitted for it.
1739
+ *
1740
+ * @param model The model class.
1741
+ * @param row The row to delete.
1089
1742
  */
1090
- private qualify;
1743
+ remove<C extends ModelClass>(model: C, row: InferModel<C>): void;
1091
1744
  /**
1092
- * Quote a column identifier, translating the model property name to the real
1093
- * database column name first.
1745
+ * Write every pending change, in one transaction.
1094
1746
  *
1095
- * `names` is `undefined` for a model that renames nothing — the overwhelmingly
1096
- * common case — so this stays a single lookup plus the memoized quote.
1747
+ * Order is inserts, then updates, then deletes — the order that keeps a
1748
+ * foreign key satisfied when a new parent and its children are flushed
1749
+ * together. It is **not** a topological sort: a graph that needs one should be
1750
+ * flushed in stages.
1097
1751
  *
1098
- * @param prop The model property name as written in the builder.
1099
- * @param names The node's property → column map, if any.
1100
- * @returns The quoted database identifier.
1752
+ * A statement that fails takes the whole flush with it, since it all runs in
1753
+ * one transaction. The tracked state is left untouched in that case, so the
1754
+ * caller can fix and flush again.
1755
+ *
1756
+ * @returns How many rows were inserted, updated and deleted.
1101
1757
  */
1102
- protected columnId(prop: string, names: NameMap | undefined): string;
1758
+ flush(): Promise<FlushResult>;
1759
+ /** Forget everything tracked, without writing. */
1760
+ clear(): void;
1103
1761
  /**
1104
- * Render a {@link SqlExpression} inline, binding the parameters it carries.
1762
+ * The identity-map key for a row or a primary key.
1105
1763
  *
1106
- * This is what keeps `set({ attempts: sql.raw("attempts + 1") })` an expression
1107
- * instead of a bound object: the fragment goes into the statement text, and
1108
- * only a `sql.expr` template's interpolations become parameters.
1109
- *
1110
- * @param expr The branded expression.
1111
- * @param params The parameter collector for the statement being compiled.
1112
- * @returns The SQL text of the expression.
1113
- */
1114
- protected renderExpression(expr: SqlExpression, params: Params): string;
1115
- /** Render one write value: a SQL expression inline, anything else as a parameter. */
1116
- protected renderValue(value: unknown, params: Params): string;
1117
- /**
1118
- * Render a row-level locking clause (`FOR UPDATE ...`).
1119
- *
1120
- * Standard on PostgreSQL and MySQL 8.0+; SQLite overrides it to throw.
1121
- *
1122
- * @param lock The locking clause from the node.
1123
- * @returns The SQL text, leading space included.
1124
- */
1125
- protected renderLock(lock: LockClause): string;
1126
- /**
1127
- * Compile a SELECT.
1128
- *
1129
- * Two alias rules differ between clauses and are handled here: PostgreSQL does
1130
- * NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
1131
- * its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
1132
- * by contrast, accepts the output alias everywhere, so it is emitted as
1133
- * written.
1134
- *
1135
- * @param node The select AST.
1136
- * @param params The parameter collector.
1137
- * @returns The SQL text.
1138
- */
1139
- private compileSelect;
1140
- /**
1141
- * Compile an INSERT.
1142
- *
1143
- * Takes the cached fast path only when the statement text is a pure function of
1144
- * its structure. A SQL expression among the values, or a conflict predicate,
1145
- * makes the text depend on the values themselves — those compile uncached, in
1146
- * SQL order, so placeholder positions stay correct.
1147
- */
1148
- private compileInsert;
1149
- /**
1150
- * Compile an INSERT without the template cache, rendering clauses in statement
1151
- * order so every parameter is bound at the position it appears.
1152
- *
1153
- * @param node The insert node.
1154
- * @param columns The column keys shared by every row.
1155
- * @param params The parameter collector.
1156
- * @returns The SQL text.
1157
- */
1158
- private compileInsertDirect;
1159
- /**
1160
- * The INSERT SQL template for a given structure, cached across calls.
1161
- *
1162
- * The text depends only on (dialect, table, columns, row count, returning,
1163
- * conflict shape) — never on the bound values — and placeholder positions are
1164
- * deterministic from the counts (a fresh statement always starts binding at 1).
1165
- * So a per-row insert loop compiles the string once and reuses it every row.
1166
- */
1167
- private insertTemplate;
1168
- /**
1169
- * Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
1170
- * `ON CONFLICT (...) [WHERE predicate] DO NOTHING | DO UPDATE SET ... [WHERE ...]`;
1171
- * MySQL overrides this.
1172
- *
1173
- * The index predicate is rendered before the `DO UPDATE` assignments because
1174
- * that is where it sits in the statement, so its parameters bind first.
1175
- *
1176
- * @param onConflict The conflict clause from the node.
1177
- * @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
1178
- * @param nextValue Yields the SQL for the next `DO UPDATE` assignment value.
1179
- * @param names The node's property → column map, if any.
1180
- * @param params The parameter collector, for the predicates.
1181
- * @returns The SQL text, leading space included.
1182
- */
1183
- protected renderConflict(onConflict: NonNullable<InsertNode["onConflict"]>, conflictCols: readonly string[], nextValue: () => string, names: NameMap | undefined, params: Params): string;
1184
- private compileUpdate;
1185
- private compileDelete;
1186
- private compileJoin;
1187
- protected compileReturning(returning: readonly string[] | "*" | null, names?: NameMap | undefined): string;
1188
- /**
1189
- * Compile a condition tree (fields / and / or / not) to SQL. `idFor` renders a
1190
- * key to a quoted identifier — `quoteId` for single-table, `qualify` for joins —
1191
- * so select/update/delete/join all share this one compiler.
1192
- */
1193
- private compileCondition;
1194
- /**
1195
- * Render one side of a comparison.
1196
- *
1197
- * A column reference goes through `idFor`, so an explicit `.name()` mapping and
1198
- * join qualification apply here exactly as they do in the object form of
1199
- * `where` — `col()` is not a way around them. Only a `value` node binds.
1200
- *
1201
- * @param node The expression AST.
1202
- * @param params The parameter collector.
1203
- * @param idFor The identifier resolver for the enclosing statement.
1204
- * @returns The SQL text of the expression.
1205
- */
1206
- private renderExpr;
1207
- /**
1208
- * Compile a comparison whose right-hand side is another expression rather than
1209
- * a bound value (`total > paid`, `lower(a) = lower(b)`).
1210
- *
1211
- * The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
1212
- * a value operand, and accepting an expression there would silently compile to
1213
- * something else.
1214
- *
1215
- * @param left The rendered left-hand side.
1216
- * @param op The operator name.
1217
- * @param right The rendered right-hand side.
1218
- * @returns The SQL text of the predicate.
1219
- * @throws Error When the operator needs a value operand.
1220
- */
1221
- private compileExprOperator;
1222
- private compileOperator;
1223
- /**
1224
- * Compile `IN` / `NOT IN`, whose operand is either a value list or a
1225
- * single-column subquery.
1226
- *
1227
- * The subquery is rendered at the position it appears in the outer statement
1228
- * and shares the same parameter collector, so its own placeholders land in the
1229
- * right order — and it keeps its own `names` map, since the inner model may use
1230
- * a different naming convention than the outer one.
1231
- *
1232
- * @param id The quoted column identifier being tested.
1233
- * @param operand A list of values, or a {@link Subquery}.
1234
- * @param params The parameter collector for the statement being compiled.
1235
- * @param negate True for `NOT IN`.
1236
- * @returns The SQL text of the predicate.
1237
- */
1238
- private compileIn;
1239
- }
1240
- /** SQLite dialect: `?` placeholders; `ILIKE` falls back to `LIKE` (ASCII-insensitive). */
1241
- declare class SqliteDialect extends BaseDialect {
1242
- readonly name: "sqlite";
1243
- protected placeholder(): string;
1244
- protected ilike(column: string, param: string): string;
1245
- /**
1246
- * SQLite has no row-level locking, so a lock request is an error rather than a
1247
- * silently unlocked `SELECT` — a lock that does not exist only shows up as
1248
- * duplicated work under production concurrency.
1249
- */
1250
- protected renderLock(): string;
1251
- }
1252
- /** PostgreSQL dialect: `$1` placeholders; native `ILIKE`; native array operators. */
1253
- declare class PostgresDialect extends BaseDialect {
1254
- readonly name: "postgresql";
1255
- protected placeholder(index: number): string;
1256
- protected ilike(column: string, param: string): string;
1257
- protected arrayOperator(op: "contains" | "containedBy" | "overlaps"): string;
1258
- }
1259
- /**
1260
- * MySQL dialect: `?` placeholders, backtick identifiers, `ON DUPLICATE KEY
1261
- * UPDATE` for upsert, and case-insensitive `LIKE` (default collation). MySQL has
1262
- * no `RETURNING`, so requesting it throws.
1263
- */
1264
- declare class MysqlDialect extends BaseDialect {
1265
- readonly name: "mysql";
1266
- protected placeholder(): string;
1267
- protected ilike(column: string, param: string): string;
1268
- protected quoteId(name: string): string;
1269
- /**
1270
- * MySQL rejects `LIMIT` inside an `IN` subquery with
1271
- * `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
1272
- * 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
1273
- * instead of surfacing that error from the driver at runtime.
1274
- */
1275
- protected checkSubquery(node: SelectNode): void;
1276
- protected renderConflict(onConflict: NonNullable<InsertNode["onConflict"]>, conflictCols: readonly string[], nextValue: () => string, names: NameMap | undefined): string;
1277
- /**
1278
- * MySQL has no `RETURNING`, so it cannot be compiled into a statement.
1279
- *
1280
- * `session.execute()` still honors `.returning()` on a **single-row INSERT** by
1281
- * running the insert and reading the row back by key on the same connection —
1282
- * that is execution, not compilation, so it never reaches here. Compiling a
1283
- * node with `returning` directly is an error, rather than SQL that silently
1284
- * returns nothing.
1764
+ * @param model The model class.
1765
+ * @param key The row, or the key.
1766
+ * @returns A stable string key.
1285
1767
  */
1286
- protected compileReturning(returning: readonly string[] | "*" | null): string;
1768
+ private identity;
1287
1769
  }
1288
- /** Get a dialect instance by name. */
1289
- declare function getDialect(name: Dialect): BaseDialect;
1290
1770
 
1291
1771
  /** The outcome of running one statement. */
1292
1772
  interface DriverResult {
@@ -1346,17 +1826,51 @@ declare class NodeSqliteDriver implements SyncDriver {
1346
1826
  iterate(sql: string, params: readonly unknown[]): IterableIterator<Record<string, unknown>>;
1347
1827
  close(): void;
1348
1828
  }
1829
+ /**
1830
+ * SQLite driver backed by the `better-sqlite3` peer dependency.
1831
+ *
1832
+ * Selected with `{ driver: "better-sqlite3" }` or the URL suffix
1833
+ * `sqlite+better-sqlite3://…`; the built-in `node:sqlite` stays the default, so
1834
+ * nothing has to be installed to use SQLite. Pick this one when the service
1835
+ * already runs on better-sqlite3, or needs what it exposes and `node:sqlite`
1836
+ * does not — `pragma()`, loadable extensions, its own WAL helpers.
1837
+ *
1838
+ * Row shape matches {@link NodeSqliteDriver}: plain objects, BLOBs as `Buffer`
1839
+ * (a `Uint8Array` subclass), which is what `coerceRow` already expects.
1840
+ */
1841
+ declare class BetterSqliteDriver implements SyncDriver {
1842
+ private readonly db;
1843
+ /** Prepared-statement cache keyed by SQL text — see {@link NodeSqliteDriver}. */
1844
+ private readonly statements;
1845
+ constructor(database: any);
1846
+ /**
1847
+ * Open a `better-sqlite3` database at the given path (or `":memory:"`).
1848
+ *
1849
+ * @param path The database file, or `":memory:"`.
1850
+ * @param options Passed straight to `new Database()` (`readonly`, `timeout`, …).
1851
+ * @returns A driver over the open handle.
1852
+ * @throws If `better-sqlite3` is not installed — it is an optional peer
1853
+ * dependency, so the error names the package to install.
1854
+ */
1855
+ static open(path: string, options?: Readonly<Record<string, unknown>>): BetterSqliteDriver;
1856
+ /** Return the cached prepared statement for `sql`, preparing it on first use. */
1857
+ private prepare;
1858
+ execute(sql: string, params: readonly unknown[]): DriverResult;
1859
+ iterate(sql: string, params: readonly unknown[]): IterableIterator<Record<string, unknown>>;
1860
+ close(): void;
1861
+ }
1349
1862
  type AnySelect = SelectBuilder<any, any, any>;
1350
1863
  type AnyInsert = InsertBuilder<any, any, any>;
1351
1864
  type GuardedUpdate = UpdateBuilder<any, true, any>;
1352
1865
  type GuardedDelete = DeleteBuilder<any, true, any>;
1353
1866
  type AnyJoin = JoinBuilder<any>;
1867
+ type AnySet = SetBuilder<any>;
1354
1868
  /**
1355
1869
  * A builder that is safe to execute. UPDATE/DELETE are accepted only once
1356
1870
  * guarded (after `.where()` or `.unguarded()`) — an unguarded full-table write
1357
1871
  * is a compile error at the execution boundary.
1358
1872
  */
1359
- type Executable = AnySelect | AnyInsert | GuardedUpdate | GuardedDelete | AnyJoin;
1873
+ type Executable = AnySelect | AnyInsert | GuardedUpdate | GuardedDelete | AnyJoin | AnySet;
1360
1874
  /** The element type a builder yields on execution. */
1361
1875
  type RowOf<B> = B extends {
1362
1876
  readonly __row: infer R;
@@ -1393,6 +1907,40 @@ type QueryLogger = (event: {
1393
1907
  readonly sql: string;
1394
1908
  readonly params: readonly unknown[];
1395
1909
  }) => void;
1910
+ /**
1911
+ * What a statement did, reported **after** it ran.
1912
+ *
1913
+ * `onQuery` fires before execution, so it cannot time anything; this is the other
1914
+ * half. It fires on the failure path too, with `error` set — a slow statement that
1915
+ * then fails is exactly the one worth seeing.
1916
+ */
1917
+ interface QueryEndEvent {
1918
+ /** The statement text. */
1919
+ readonly sql: string;
1920
+ /** The bound parameters, in placeholder order. */
1921
+ readonly params: readonly unknown[];
1922
+ /** Wall-clock time the driver took, in milliseconds. */
1923
+ readonly durationMs: number;
1924
+ /** Rows returned (a SELECT or `RETURNING`), or rows affected by a write. */
1925
+ readonly rowCount: number;
1926
+ /** The driver's error, when the statement failed. */
1927
+ readonly error?: unknown;
1928
+ }
1929
+ /**
1930
+ * Called after every statement, with its duration.
1931
+ *
1932
+ * Errors thrown by the hook are ignored, like {@link QueryLogger}.
1933
+ */
1934
+ type QueryEndLogger = (event: QueryEndEvent) => void;
1935
+ /** The per-statement hooks a session carries. */
1936
+ interface QueryHooks {
1937
+ /** Called before a statement runs. */
1938
+ readonly onQuery?: QueryLogger | undefined;
1939
+ /** Called after a statement runs, with its duration. */
1940
+ readonly onQueryEnd?: QueryEndLogger | undefined;
1941
+ /** When set, `onQueryEnd` fires only for statements at least this slow (ms). */
1942
+ readonly slowQueryMs?: number | undefined;
1943
+ }
1396
1944
  /** Synchronous result view over already-fetched rows. */
1397
1945
  declare class SyncResult<Row> {
1398
1946
  private readonly rows;
@@ -1418,16 +1966,41 @@ declare class AsyncResult<Row> {
1418
1966
  scalars(): Promise<unknown[]>;
1419
1967
  rowsAffected(): Promise<number>;
1420
1968
  }
1969
+ /**
1970
+ * A transaction isolation level, in the SQL standard's names.
1971
+ *
1972
+ * The database's default is what you get without asking: `read committed` on
1973
+ * PostgreSQL and MySQL's `repeatable read`. SQLite has only `serializable`.
1974
+ */
1975
+ type IsolationLevel = "read uncommitted" | "read committed" | "repeatable read" | "serializable";
1976
+ /** Characteristics for one transaction block. */
1977
+ interface TransactionOptions {
1978
+ /**
1979
+ * The isolation level for this block.
1980
+ *
1981
+ * Raising it is how the queue and outbox patterns get their invariants: under
1982
+ * `read committed` two workers can both pass the same check before either
1983
+ * commits. Asking for a level a dialect does not implement throws.
1984
+ */
1985
+ readonly isolation?: IsolationLevel;
1986
+ /**
1987
+ * Open the block read-only, so the database itself rejects a write in it.
1988
+ * PostgreSQL and MySQL only; SQLite throws.
1989
+ */
1990
+ readonly readOnly?: boolean;
1991
+ }
1421
1992
  /** A synchronous unit of work (SQLite). */
1422
1993
  declare class SyncSession {
1423
1994
  private readonly driver;
1424
1995
  private readonly dialect;
1425
- /** Optional per-statement logger (query tracing). */
1426
- private readonly logger?;
1996
+ /** Optional per-statement hooks (query tracing and timing). */
1997
+ private readonly hooks?;
1427
1998
  constructor(driver: SyncDriver, dialect: BaseDialect,
1428
- /** Optional per-statement logger (query tracing). */
1429
- logger?: QueryLogger | undefined);
1430
- /** Log, run, and error-wrap one raw statement. */
1999
+ /** Optional per-statement hooks (query tracing and timing). */
2000
+ hooks?: QueryHooks | undefined);
2001
+ /** Open `transaction()` blocks on this session; only the outermost commits. */
2002
+ private depth;
2003
+ /** Log, run, time, and error-wrap one raw statement. */
1431
2004
  private exec;
1432
2005
  /**
1433
2006
  * Run a raw, parameterized SQL statement (synchronous) — the runtime counterpart of the
@@ -1466,7 +2039,29 @@ declare class SyncSession {
1466
2039
  /** Compile, run, and coerce a builder into a result. */
1467
2040
  execute<B extends Executable>(builder: B): SyncResult<RowOf<B>>;
1468
2041
  /** Run `fn` inside a transaction: commit on success, rollback on throw. */
1469
- transaction<T>(fn: (tx: SyncSession) => T): T;
2042
+ /**
2043
+ * How many `transaction()` blocks are open on this session.
2044
+ *
2045
+ * The counter is what makes a service that orchestrates two repositories work:
2046
+ * both hold the same session, so an inner block **joins** the outer one instead
2047
+ * of emitting a second `BEGIN`, and only the outermost exit commits.
2048
+ */
2049
+ get transactionDepth(): number;
2050
+ /** Whether a `transaction()` block is currently open on this session. */
2051
+ get inTransaction(): boolean;
2052
+ /**
2053
+ * Run `fn` inside a transaction, committing on a clean exit and rolling back on
2054
+ * a throw.
2055
+ *
2056
+ * **Re-entrant:** a nested call joins the block already open on this session —
2057
+ * one `BEGIN`, one `COMMIT`, and an inner failure rolls the whole thing back.
2058
+ * To recover from an inner failure without discarding the outer work, use
2059
+ * {@link beginNested}, which is a real savepoint.
2060
+ *
2061
+ * @param fn The body; receives the session to work through.
2062
+ * @returns Whatever `fn` returned.
2063
+ */
2064
+ transaction<T>(fn: (tx: SyncSession) => T, options?: TransactionOptions): T;
1470
2065
  /** Run `fn` inside a SAVEPOINT (nested transaction). */
1471
2066
  beginNested<T>(fn: (sp: SyncSession) => T): T;
1472
2067
  /**
@@ -1482,12 +2077,14 @@ declare class SyncSession {
1482
2077
  declare class AsyncSession {
1483
2078
  private readonly driver;
1484
2079
  private readonly dialect;
1485
- /** Optional per-statement logger (query tracing). */
1486
- private readonly logger?;
2080
+ /** Optional per-statement hooks (query tracing and timing). */
2081
+ private readonly hooks?;
1487
2082
  constructor(driver: AsyncDriver, dialect: BaseDialect,
1488
- /** Optional per-statement logger (query tracing). */
1489
- logger?: QueryLogger | undefined);
1490
- /** Log, run, and error-wrap one raw statement. */
2083
+ /** Optional per-statement hooks (query tracing and timing). */
2084
+ hooks?: QueryHooks | undefined);
2085
+ /** Open `transaction()` blocks on this session; only the outermost commits. */
2086
+ private depth;
2087
+ /** Log, run, time, and error-wrap one raw statement. */
1491
2088
  private exec;
1492
2089
  /**
1493
2090
  * Run a raw, parameterized SQL statement — the runtime counterpart of the
@@ -1543,7 +2140,61 @@ declare class AsyncSession {
1543
2140
  private insertAndReadBack;
1544
2141
  /** Lazily iterate result rows. Uses driver streaming when available. */
1545
2142
  stream<B extends Executable>(builder: B): AsyncIterableIterator<RowOf<B>>;
1546
- transaction<T>(fn: (tx: AsyncSession) => Promise<T>): Promise<T>;
2143
+ /**
2144
+ * How many `transaction()` blocks are open on this session.
2145
+ *
2146
+ * The counter is what makes a service that orchestrates two repositories work:
2147
+ * both hold the same session, so an inner block **joins** the outer one instead
2148
+ * of emitting a second `BEGIN`, and only the outermost exit commits.
2149
+ */
2150
+ get transactionDepth(): number;
2151
+ /** Whether a `transaction()` block is currently open on this session. */
2152
+ get inTransaction(): boolean;
2153
+ /**
2154
+ * Run `fn` inside a transaction, committing on a clean exit and rolling back on
2155
+ * a throw.
2156
+ *
2157
+ * **Re-entrant:** a nested call joins the block already open on this session,
2158
+ * so a service orchestrating several repositories bound to the same session
2159
+ * gets one `BEGIN` and one `COMMIT`, not two of each. An inner failure rolls the
2160
+ * whole block back; use {@link beginNested} for a savepoint that can be
2161
+ * recovered from.
2162
+ *
2163
+ * Pooled drivers (PostgreSQL) pin one connection for the block: `BEGIN`/`COMMIT`
2164
+ * and every statement between them have to run on the same connection, or
2165
+ * postgres.js rejects the raw transaction. Single-connection drivers (SQLite)
2166
+ * skip the reservation.
2167
+ *
2168
+ * @param fn The body; receives the session to work through (the pinned one, on a
2169
+ * pooled driver).
2170
+ * @returns Whatever `fn` returned.
2171
+ */
2172
+ transaction<T>(fn: (tx: AsyncSession) => Promise<T>, options?: TransactionOptions): Promise<T>;
2173
+ /**
2174
+ * Run `fn` inside a `SAVEPOINT` (a nested transaction that can be rolled back on
2175
+ * its own).
2176
+ *
2177
+ * This is the difference from a nested {@link transaction}: a savepoint that
2178
+ * fails discards **only** its own work, so the enclosing block can catch the
2179
+ * error and carry on. A nested `transaction()` joins the outer block, and its
2180
+ * failure takes the whole block down.
2181
+ *
2182
+ * Must run inside an open transaction — PostgreSQL rejects a savepoint outside a
2183
+ * transaction block.
2184
+ *
2185
+ * @param fn The body; receives the same session.
2186
+ * @returns Whatever `fn` returned.
2187
+ */
2188
+ beginNested<T>(fn: (sp: AsyncSession) => Promise<T>): Promise<T>;
2189
+ /**
2190
+ * Open an opt-in {@link UnitOfWork} over this session.
2191
+ *
2192
+ * The default stays a plain object written when you ask; this is the other
2193
+ * model, for code that prefers to mutate and flush once.
2194
+ *
2195
+ * @returns A new, empty unit of work.
2196
+ */
2197
+ unitOfWork(): UnitOfWork;
1547
2198
  close(): Promise<void>;
1548
2199
  /** `await using session = ...` closes the driver when the scope exits. */
1549
2200
  [Symbol.asyncDispose](): Promise<void>;
@@ -1556,178 +2207,1854 @@ interface PoolOptions {
1556
2207
  readonly idleTimeoutMs?: number;
1557
2208
  /** Give up acquiring a connection after this long (ms). */
1558
2209
  readonly connectTimeoutMs?: number;
2210
+ /**
2211
+ * Validate a connection before pinning it for a transaction.
2212
+ *
2213
+ * A pooled connection can die without the pool noticing — a failover, a
2214
+ * pgbouncer restart, a firewall dropping an idle socket. The damage lands on
2215
+ * whoever picks it up next, and it lands worst on a transaction: `BEGIN`
2216
+ * succeeds, a statement mid-block fails, and the block dies halfway.
2217
+ *
2218
+ * With this on, `transaction()` runs `SELECT 1` on the reserved connection
2219
+ * first and reserves another one if that fails. It costs a round trip per
2220
+ * transaction, which is why it is opt-in.
2221
+ *
2222
+ * PostgreSQL only — MySQL throws, and SQLite has no pool.
2223
+ */
2224
+ readonly prePing?: boolean;
2225
+ /**
2226
+ * Close and reopen a connection older than this (ms), regardless of activity.
2227
+ *
2228
+ * The blunt companion to {@link prePing}: it bounds how long a connection can
2229
+ * have been alive, which is what keeps a slow leak (a server-side timeout, a
2230
+ * load balancer's idle cap) from becoming a mystery error hours later.
2231
+ *
2232
+ * PostgreSQL only — MySQL throws, and SQLite has no pool.
2233
+ */
2234
+ readonly recycleMs?: number;
2235
+ }
2236
+ /**
2237
+ * A server-side notice (a PostgreSQL `NOTICE`). The shape is the driver's own —
2238
+ * passed through untouched rather than normalized, since what is useful in it
2239
+ * differs per database.
2240
+ */
2241
+ type NoticeLogger = (notice: Record<string, unknown>) => void;
2242
+ /**
2243
+ * SQLite journal modes accepted by `PRAGMA journal_mode`.
2244
+ *
2245
+ * `"wal"` is the one worth reaching for on a server: readers stop blocking the
2246
+ * writer. It needs a real file — an in-memory database refuses it and stays on
2247
+ * `"memory"`, which this layer reports as an error rather than a silent no-op.
2248
+ */
2249
+ type SqliteJournalMode = "delete" | "truncate" | "persist" | "memory" | "wal" | "off";
2250
+ /** Durability levels accepted by `PRAGMA synchronous`. */
2251
+ type SqliteSynchronous = "off" | "normal" | "full" | "extra";
2252
+ /**
2253
+ * Per-connection SQLite settings, applied right after the handle opens.
2254
+ *
2255
+ * Pragmas are **per connection**, not per database file, so they belong to the
2256
+ * engine rather than to a migration. Only `foreignKeys` has a default that
2257
+ * changes behavior; every other field is emitted only when given, so an existing
2258
+ * database keeps whatever it was configured with.
2259
+ */
2260
+ interface SqliteOptions {
2261
+ /**
2262
+ * Enforce `FOREIGN KEY` constraints. **Defaults to `true`.**
2263
+ *
2264
+ * SQLite ships with enforcement `OFF`, per connection, so a declared foreign
2265
+ * key is decorative until someone turns it on: an orphan `INSERT` is accepted
2266
+ * and `ON DELETE CASCADE` never fires. tempest-db-js turns it on, which makes
2267
+ * the same model behave the same on all three databases.
2268
+ *
2269
+ * Set it to `false` only for the case it exists for — loading a dump whose
2270
+ * insert order does not respect the graph.
2271
+ */
2272
+ readonly foreignKeys?: boolean;
2273
+ /** `PRAGMA journal_mode`. Omitted: the database keeps its current mode. */
2274
+ readonly journalMode?: SqliteJournalMode;
2275
+ /** `PRAGMA busy_timeout`, in milliseconds. How long a writer waits on a lock. */
2276
+ readonly busyTimeoutMs?: number;
2277
+ /** `PRAGMA synchronous`. Durability vs write throughput. */
2278
+ readonly synchronous?: SqliteSynchronous;
2279
+ }
2280
+ /** Options shared by both engine flavors. */
2281
+ interface EngineOptions {
2282
+ /**
2283
+ * Override the driver detected from the URL.
2284
+ *
2285
+ * SQLite ships two: `"node:sqlite"` (the built-in, default — nothing to
2286
+ * install) and `"better-sqlite3"` (the optional peer dependency). PostgreSQL
2287
+ * runs on `"postgres"` (postgres.js) and MySQL on `"mysql2"`; naming those is
2288
+ * a no-op today, kept so the option means the same thing everywhere.
2289
+ *
2290
+ * A name this dialect does not have throws — passing `{ driver: "sqlite3" }`
2291
+ * is a decision that would otherwise be silently ignored.
2292
+ *
2293
+ * The `+suffix` in the URL (`sqlite+better-sqlite3:///app.db`) selects the same
2294
+ * way, with one difference: a suffix naming a driver from another ecosystem
2295
+ * (`sqlite+aiosqlite`, `postgresql+asyncpg`) is ignored rather than rejected,
2296
+ * so a URL copied from a Python service still connects.
2297
+ */
2298
+ readonly driver?: string;
2299
+ /** Connection-pool tuning (PostgreSQL only). */
2300
+ readonly pool?: PoolOptions;
2301
+ /**
2302
+ * Called for every statement a session runs — SQL + bound params. Use for
2303
+ * query logging/tracing. Thrown errors are swallowed so it never breaks a query.
2304
+ */
2305
+ readonly onQuery?: QueryLogger;
2306
+ /**
2307
+ * Called for every server-side notice (`CREATE TABLE IF NOT EXISTS` on an
2308
+ * existing table, `DROP ... IF EXISTS` on a missing one, and so on).
2309
+ *
2310
+ * **Without this, notices are silenced.** postgres.js defaults to printing them
2311
+ * with `console.log`, which drops a nine-line object into the host service's
2312
+ * stdout in the middle of its structured log — on every boot, since a migration
2313
+ * runner is usually the first thing to run. Writing to the host's stdout is the
2314
+ * application's decision, not a library's, so the default is to say nothing and
2315
+ * let you route them:
2316
+ *
2317
+ * ```ts
2318
+ * createEngine(url, { onNotice: (n) => logger.debug({ pg: n }, "postgres notice") });
2319
+ * ```
2320
+ *
2321
+ * Thrown errors are swallowed, like `onQuery`.
2322
+ */
2323
+ readonly onNotice?: NoticeLogger;
2324
+ /**
2325
+ * Options passed straight to the underlying driver, applied **last** so they
2326
+ * win over everything this layer derives (`pool`, `onNotice`).
2327
+ *
2328
+ * The escape hatch for what the typed surface does not model and is not going
2329
+ * to — postgres.js `connection`/`types`/`transform`/`ssl`, mysql2's own
2330
+ * settings, `node:sqlite`'s `readOnly` — so a gap need not become a feature
2331
+ * request.
2332
+ */
2333
+ readonly driverOptions?: Readonly<Record<string, unknown>>;
2334
+ /**
2335
+ * Per-connection SQLite pragmas (`foreign_keys`, `journal_mode`, …), applied
2336
+ * as soon as the handle opens. Passing it to a PostgreSQL or MySQL engine
2337
+ * throws — the settings have no meaning there, and silently ignoring them is
2338
+ * how a durability choice gets lost.
2339
+ */
2340
+ readonly sqlite?: SqliteOptions;
2341
+ /**
2342
+ * Called **after** every statement, with how long the driver took.
2343
+ *
2344
+ * `onQuery` fires before execution, so it cannot time anything; this is the
2345
+ * other half, and it fires on the failure path too (with `error` set). Use it
2346
+ * for latency metrics, tracing spans, and finding the query dragging p99.
2347
+ *
2348
+ * Errors thrown by the hook are swallowed, like `onQuery`.
2349
+ */
2350
+ readonly onQueryEnd?: QueryEndLogger;
2351
+ /**
2352
+ * Only report statements at least this slow (milliseconds) to `onQueryEnd`.
2353
+ *
2354
+ * The cheapest slow-query log there is: set a threshold, log what crosses it.
2355
+ * Without it every statement is reported.
2356
+ */
2357
+ readonly slowQueryMs?: number;
2358
+ }
2359
+ /** A synchronous engine (SQLite only). */
2360
+ declare class SyncEngine {
2361
+ private readonly driver;
2362
+ private readonly hooks?;
2363
+ readonly dialect: Dialect;
2364
+ constructor(driver: SyncDriver, hooks?: QueryHooks | undefined);
2365
+ session(): SyncSession;
2366
+ transaction<T>(fn: (tx: SyncSession) => T, options?: TransactionOptions): T;
2367
+ close(): void;
2368
+ /** `using engine = createSyncEngine(...)` closes the pool when the scope exits. */
2369
+ [Symbol.dispose](): void;
2370
+ }
2371
+ /** An asynchronous engine. */
2372
+ declare class AsyncEngine {
2373
+ private readonly driver;
2374
+ readonly dialect: Dialect;
2375
+ private readonly hooks?;
2376
+ constructor(driver: AsyncDriver, dialect: Dialect, hooks?: QueryHooks | undefined);
2377
+ session(): AsyncSession;
2378
+ transaction<T>(fn: (tx: AsyncSession) => Promise<T>, options?: TransactionOptions): Promise<T>;
2379
+ /**
2380
+ * Run `fn` and return the query plan of every statement it ran.
2381
+ *
2382
+ * The block gets its own session over a **recording** driver, so the plans are
2383
+ * built from the statements and the parameters the code really used — not from
2384
+ * SQL copied out of a log by hand. A development tool: it runs the block once
2385
+ * and then one `EXPLAIN` per statement, so keep it out of the hot path.
2386
+ *
2387
+ * @param fn The code to observe; receives the recording session.
2388
+ * @param options `analyze: true` to measure (refused for writes), `filter` to
2389
+ * explain only some statements.
2390
+ * @returns The report, one plan per statement in execution order.
2391
+ * @throws Error When `analyze` is requested for a statement that writes, or on
2392
+ * a dialect that has no `EXPLAIN ANALYZE`.
2393
+ *
2394
+ * @example
2395
+ * ```ts
2396
+ * const report = await engine.explain(async (session) => {
2397
+ * await new BaseRepository(Order, session).paginate({ page: 3 });
2398
+ * });
2399
+ * console.log(report.summary());
2400
+ * ```
2401
+ */
2402
+ explain(fn: (session: AsyncSession) => Promise<unknown>, options?: ExplainOptions): Promise<ExplainReport>;
2403
+ close(): Promise<void>;
2404
+ /** `await using engine = createEngine(...)` closes the pool when the scope exits. */
2405
+ [Symbol.asyncDispose](): Promise<void>;
2406
+ }
2407
+ /**
2408
+ * Adapt a sync **or** async driver to the async interface.
2409
+ *
2410
+ * `await` normalizes both: a sync driver returns a plain value, an async one a
2411
+ * promise, and awaiting either yields the result. That is what lets the migration
2412
+ * CLI take one code path instead of branching on a difference it cannot detect
2413
+ * from the object's shape.
2414
+ *
2415
+ * @param driver Either driver flavor.
2416
+ * @returns An async driver delegating to it.
2417
+ */
2418
+ declare function toAsyncDriver(driver: SyncDriver | AsyncDriver): AsyncDriver;
2419
+ /**
2420
+ * Create a **synchronous** engine from a database URL. SQLite only — PostgreSQL
2421
+ * has no sane synchronous driver in Node, so a Postgres URL throws, pointing at
2422
+ * the async `createEngine`.
2423
+ *
2424
+ * @param url A SQLite URL, e.g. `"sqlite:///app.db"` or `"sqlite://:memory:"`.
2425
+ * @param options Engine options.
2426
+ * @returns A `SyncEngine`.
2427
+ */
2428
+ declare function createSyncEngine(url: string, options?: EngineOptions): SyncEngine;
2429
+ /**
2430
+ * Create an **asynchronous** engine from a database URL (the default). Works for
2431
+ * both SQLite (sync driver wrapped as async) and PostgreSQL (postgres.js,
2432
+ * lazy-loaded).
2433
+ *
2434
+ * @param url A database URL, e.g. `"postgresql://app@localhost/app"` or
2435
+ * `"sqlite:///app.db"`.
2436
+ * @param options Engine options.
2437
+ * @returns An `AsyncEngine`.
2438
+ */
2439
+ declare function createEngine(url: string, options?: EngineOptions): AsyncEngine;
2440
+
2441
+ /**
2442
+ * tempest-db-js — Phase 4a: dialect SQL compilation.
2443
+ *
2444
+ * Turns the dialect-neutral AST (`SelectNode`, `InsertNode`, `UpdateNode`,
2445
+ * `DeleteNode` from Phases 1-2) into `{ sql, params }`. This is the ONLY place
2446
+ * SQL is produced — always parameterized (`?` for SQLite, `$1` for PostgreSQL),
2447
+ * never string interpolation, so it is injection-safe by construction.
2448
+ *
2449
+ * It does NOT touch a database — execution is Phase 4b (`session.execute`).
2450
+ */
2451
+
2452
+ /** A compiled, parameterized statement ready to hand to a driver. */
2453
+ interface CompiledQuery {
2454
+ readonly sql: string;
2455
+ readonly params: readonly unknown[];
2456
+ }
2457
+ /** Any compilable AST node. */
2458
+ type QueryNode = SelectNode | InsertNode | UpdateNode | DeleteNode | JoinNode | SetNode;
2459
+ /**
2460
+ * Collects bound parameters and renders placeholders in dialect style. Exposed
2461
+ * because dialect subclasses receive it when overriding clause rendering.
2462
+ */
2463
+ declare class Params {
2464
+ private readonly placeholder;
2465
+ readonly values: unknown[];
2466
+ constructor(placeholder: (index: number) => string);
2467
+ bind(value: unknown): string;
2468
+ }
2469
+ /**
2470
+ * Base SQL compiler shared by every dialect. Subclasses customize only what
2471
+ * actually differs between databases (placeholder syntax, `ILIKE` support).
2472
+ */
2473
+ declare abstract class BaseDialect {
2474
+ abstract readonly name: Dialect;
2475
+ /**
2476
+ * INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
2477
+ * returning). Shared across dialect instances — the key namespaces by dialect
2478
+ * name, and the placeholder text is dialect-specific but structure-determined.
2479
+ */
2480
+ private static readonly insertTemplates;
2481
+ /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
2482
+ private static readonly quotedIds;
2483
+ /** Render the n-th (1-based) placeholder. */
2484
+ protected abstract placeholder(index: number): string;
2485
+ /** Render a case-insensitive LIKE for the active dialect. */
2486
+ protected abstract ilike(column: string, param: string): string;
2487
+ /**
2488
+ * Render a case-insensitive LIKE whose pattern carries escaped wildcards.
2489
+ *
2490
+ * The `ESCAPE` clause is not decoration: PostgreSQL treats `\` as the escape
2491
+ * character by default, **SQLite has none at all** until one is declared, so
2492
+ * without this the escaping done on our side would be meaningless there.
2493
+ *
2494
+ * @param column The rendered column.
2495
+ * @param param The bound pattern.
2496
+ * @returns The rendered comparison.
2497
+ */
2498
+ protected ilikeEscaped(column: string, param: string): string;
2499
+ /**
2500
+ * Validate a subquery operand before it is rendered, for dialects that restrict
2501
+ * what an `IN (SELECT ...)` may contain. The default accepts everything.
2502
+ *
2503
+ * @param _node The subquery's AST.
2504
+ * @throws Error When the dialect cannot execute this subquery.
2505
+ */
2506
+ protected checkSubquery(_node: SelectNode): void;
2507
+ /**
2508
+ * The SQL operator for an array containment/overlap test.
2509
+ *
2510
+ * Only PostgreSQL has native arrays; the other dialects throw rather than
2511
+ * emitting an operator that means something else there.
2512
+ *
2513
+ * @param op The array operator name.
2514
+ * @returns The SQL operator text.
2515
+ * @throws Error On a dialect without native array support.
2516
+ */
2517
+ protected arrayOperator(op: "contains" | "containedBy" | "overlaps"): string;
2518
+ /**
2519
+ * Quote an identifier (column/table) for the active dialect.
2520
+ *
2521
+ * Memoized: identifiers form a small, stable set (column/table names), but this
2522
+ * runs for every identifier on every compile. Caching the quoted form removes a
2523
+ * regex-replace + string allocation from the hot path. The standard double-quote
2524
+ * form is identical across both dialects, so one shared cache is correct.
2525
+ */
2526
+ protected quoteId(name: string): string;
2527
+ /** Compile any node to `{ sql, params }`. */
2528
+ compile(node: QueryNode): CompiledQuery;
2529
+ /**
2530
+ * Render a condition as schema SQL, with values inlined.
2531
+ *
2532
+ * Same compiler as a `WHERE`, different parameter strategy — so a `CHECK` and
2533
+ * the query language cannot drift apart in what they mean.
2534
+ *
2535
+ * @param node The condition.
2536
+ * @param params A {@link LiteralParams}.
2537
+ * @returns The rendered predicate.
2538
+ */
2539
+ renderConditionLiteral(node: CondNode, params: Params): string;
2540
+ /**
2541
+ * Render the `WITH` clause of a statement.
2542
+ *
2543
+ * `RECURSIVE` is a property of the **clause**, not of an entry: one recursive
2544
+ * entry makes the whole `WITH` recursive, which is what the SQL standard says
2545
+ * and what PostgreSQL and SQLite both implement.
2546
+ *
2547
+ * @param entries The `WITH` entries, if any.
2548
+ * @param params The parameter collector.
2549
+ * @returns The clause with a trailing space, or an empty string.
2550
+ */
2551
+ protected compileWith(entries: readonly CteNode[] | undefined, params: Params): string;
2552
+ /**
2553
+ * The `EXPLAIN` prefix for this dialect.
2554
+ *
2555
+ * @param analyze Whether to measure by actually running the statement.
2556
+ * @returns The prefix to put in front of the statement.
2557
+ * @throws Error When the dialect cannot do what was asked.
2558
+ */
2559
+ explainPrefix(analyze: boolean): string;
2560
+ /**
2561
+ * The SQL keyword for a set operation.
2562
+ *
2563
+ * @param op The operator.
2564
+ * @returns The keyword.
2565
+ * @throws Error On a dialect that does not implement it.
2566
+ */
2567
+ protected setOperator(op: SetOperator): string;
2568
+ /**
2569
+ * Compile a set operation.
2570
+ *
2571
+ * A branch carrying its own `ORDER BY`/`LIMIT` is parenthesized: without the
2572
+ * parentheses those clauses bind to the **combined** result, which is a
2573
+ * different query and a classic source of silently wrong output.
2574
+ *
2575
+ * @param node The set-operation node.
2576
+ * @param params The parameter collector.
2577
+ * @returns The rendered statement.
2578
+ */
2579
+ protected compileSetOp(node: SetNode, params: Params): string;
2580
+ /**
2581
+ * Render a qualified `alias.column` ref as `"alias"."column"`, translating the
2582
+ * property name to the real column name for that alias's model.
2583
+ *
2584
+ * @param ref The `alias.property` reference (a bare name is left unqualified).
2585
+ * @param names The node's per-alias name maps, if any source renames columns.
2586
+ * @returns The quoted, qualified identifier.
2587
+ */
2588
+ private qualify;
2589
+ /**
2590
+ * Quote a column identifier, translating the model property name to the real
2591
+ * database column name first.
2592
+ *
2593
+ * `names` is `undefined` for a model that renames nothing — the overwhelmingly
2594
+ * common case — so this stays a single lookup plus the memoized quote.
2595
+ *
2596
+ * @param prop The model property name as written in the builder.
2597
+ * @param names The node's property → column map, if any.
2598
+ * @returns The quoted database identifier.
2599
+ */
2600
+ protected columnId(prop: string, names: NameMap | undefined): string;
2601
+ /**
2602
+ * Render a {@link SqlExpression} inline, binding the parameters it carries.
2603
+ *
2604
+ * This is what keeps `set({ attempts: sql.raw("attempts + 1") })` an expression
2605
+ * instead of a bound object: the fragment goes into the statement text, and
2606
+ * only a `sql.expr` template's interpolations become parameters.
2607
+ *
2608
+ * @param expr The branded expression.
2609
+ * @param params The parameter collector for the statement being compiled.
2610
+ * @returns The SQL text of the expression.
2611
+ */
2612
+ protected renderExpression(expr: SqlExpression, params: Params): string;
2613
+ /** Render one write value: a SQL expression inline, anything else as a parameter. */
2614
+ protected renderValue(value: unknown, params: Params): string;
2615
+ /**
2616
+ * The statements that open a transaction with the requested characteristics.
2617
+ *
2618
+ * Returned as a list because the dialects disagree on shape: PostgreSQL takes
2619
+ * everything on the `BEGIN` itself, MySQL needs a separate `SET TRANSACTION`
2620
+ * before it, and SQLite has no syntax at all.
2621
+ *
2622
+ * @param options The requested isolation level and read-only flag.
2623
+ * @returns The statements to run, in order.
2624
+ * @throws Error When the dialect cannot honor what was asked.
2625
+ */
2626
+ beginStatements(options?: TransactionOptions): string[];
2627
+ /**
2628
+ * Render a row-level locking clause (`FOR UPDATE ...`).
2629
+ *
2630
+ * Standard on PostgreSQL and MySQL 8.0+; SQLite overrides it to throw.
2631
+ *
2632
+ * @param lock The locking clause from the node.
2633
+ * @returns The SQL text, leading space included.
2634
+ */
2635
+ protected renderLock(lock: LockClause): string;
2636
+ /**
2637
+ * Compile a SELECT.
2638
+ *
2639
+ * Two alias rules differ between clauses and are handled here: PostgreSQL does
2640
+ * NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
2641
+ * its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
2642
+ * by contrast, accepts the output alias everywhere, so it is emitted as
2643
+ * written.
2644
+ *
2645
+ * @param node The select AST.
2646
+ * @param params The parameter collector.
2647
+ * @returns The SQL text.
2648
+ */
2649
+ private compileSelect;
2650
+ /**
2651
+ * Compile an INSERT.
2652
+ *
2653
+ * Takes the cached fast path only when the statement text is a pure function of
2654
+ * its structure. A SQL expression among the values, or a conflict predicate,
2655
+ * makes the text depend on the values themselves — those compile uncached, in
2656
+ * SQL order, so placeholder positions stay correct.
2657
+ */
2658
+ private compileInsert;
2659
+ /**
2660
+ * Compile an INSERT without the template cache, rendering clauses in statement
2661
+ * order so every parameter is bound at the position it appears.
2662
+ *
2663
+ * @param node The insert node.
2664
+ * @param columns The column keys shared by every row.
2665
+ * @param params The parameter collector.
2666
+ * @returns The SQL text.
2667
+ */
2668
+ private compileInsertDirect;
2669
+ /**
2670
+ * The INSERT SQL template for a given structure, cached across calls.
2671
+ *
2672
+ * The text depends only on (dialect, table, columns, row count, returning,
2673
+ * conflict shape) — never on the bound values — and placeholder positions are
2674
+ * deterministic from the counts (a fresh statement always starts binding at 1).
2675
+ * So a per-row insert loop compiles the string once and reuses it every row.
2676
+ */
2677
+ private insertTemplate;
2678
+ /**
2679
+ * Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
2680
+ * `ON CONFLICT (...) [WHERE predicate] DO NOTHING | DO UPDATE SET ... [WHERE ...]`;
2681
+ * MySQL overrides this.
2682
+ *
2683
+ * The index predicate is rendered before the `DO UPDATE` assignments because
2684
+ * that is where it sits in the statement, so its parameters bind first.
2685
+ *
2686
+ * @param onConflict The conflict clause from the node.
2687
+ * @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
2688
+ * @param nextValue Yields the SQL for the next `DO UPDATE` assignment value.
2689
+ * @param names The node's property → column map, if any.
2690
+ * @param params The parameter collector, for the predicates.
2691
+ * @returns The SQL text, leading space included.
2692
+ */
2693
+ protected renderConflict(onConflict: NonNullable<InsertNode["onConflict"]>, conflictCols: readonly string[], nextValue: () => string, names: NameMap | undefined, params: Params): string;
2694
+ private compileUpdate;
2695
+ private compileDelete;
2696
+ protected compileJoin(node: JoinNode, params: Params): string;
2697
+ /**
2698
+ * Render the extra sources of an `UPDATE ... FROM` / `DELETE ... USING`.
2699
+ *
2700
+ * The dialects that do not have the clause override this and throw: emitting it
2701
+ * anyway would produce a statement the server rejects, and quietly dropping it
2702
+ * would change which rows are written.
2703
+ *
2704
+ * @param keyword `FROM` or `USING`.
2705
+ * @param sources The extra tables, if any.
2706
+ * @returns The clause with a leading space, or an empty string.
2707
+ */
2708
+ protected compileExtraSources(keyword: "FROM" | "USING", sources: readonly {
2709
+ readonly table: string;
2710
+ readonly alias: string;
2711
+ }[] | undefined): string;
2712
+ protected compileReturning(returning: readonly string[] | "*" | null, names?: NameMap | undefined): string;
2713
+ /**
2714
+ * Compile a condition tree (fields / and / or / not) to SQL. `idFor` renders a
2715
+ * key to a quoted identifier — `quoteId` for single-table, `qualify` for joins —
2716
+ * so select/update/delete/join all share this one compiler.
2717
+ */
2718
+ protected compileCondition(node: CondNode | undefined, params: Params, idFor: (key: string) => string, encode?: (key: string, value: unknown) => unknown): string;
2719
+ /**
2720
+ * Compile a full-text condition.
2721
+ *
2722
+ * PostgreSQL gets the real thing (`@@ websearch_to_tsquery`); the dialects with
2723
+ * no text-search engine override this and compile the node's prebuilt substring
2724
+ * fallback instead, so the query still returns the right rows.
2725
+ *
2726
+ * @param node The full-text condition node.
2727
+ * @param params The parameter collector.
2728
+ * @param idFor Column-name resolver.
2729
+ * @returns The rendered condition.
2730
+ */
2731
+ protected compileFullText(node: Extract<CondNode, {
2732
+ kind: "fullText";
2733
+ }>, params: Params, idFor: (key: string) => string): string;
2734
+ /**
2735
+ * Render one side of a comparison.
2736
+ *
2737
+ * A column reference goes through `idFor`, so an explicit `.name()` mapping and
2738
+ * join qualification apply here exactly as they do in the object form of
2739
+ * `where` — `col()` is not a way around them. Only a `value` node binds.
2740
+ *
2741
+ * @param node The expression AST.
2742
+ * @param params The parameter collector.
2743
+ * @param idFor The identifier resolver for the enclosing statement.
2744
+ * @returns The SQL text of the expression.
2745
+ */
2746
+ /**
2747
+ * Render what an aggregate is applied to: `*`, a column, or an expression.
2748
+ *
2749
+ * An expression operand is what makes a conditional aggregate
2750
+ * (`SUM(CASE WHEN ... END)`) expressible — one pass over the table instead of a
2751
+ * query per bucket.
2752
+ *
2753
+ * @param agg The aggregate term.
2754
+ * @param params The parameter collector.
2755
+ * @param names The node's column-name map.
2756
+ * @returns The rendered operand.
2757
+ */
2758
+ private aggregateOperand;
2759
+ private renderExpr;
2760
+ /**
2761
+ * Render a full-text relevance score.
2762
+ *
2763
+ * PostgreSQL has `ts_rank`; the others have nothing equivalent, and they
2764
+ * override this to a constant so that ordering by it is a no-op rather than a
2765
+ * compile error — the fallback keeps returning the right rows, only unranked.
2766
+ *
2767
+ * @param columns The columns making up the document.
2768
+ * @param term The search term.
2769
+ * @param language The text-search configuration.
2770
+ * @param params The parameter collector.
2771
+ * @param idFor Column-name resolver.
2772
+ * @returns The rendered score expression.
2773
+ */
2774
+ protected renderRank(columns: readonly string[], term: string, language: string, params: Params, idFor: (key: string) => string): string;
2775
+ /**
2776
+ * Build the `to_tsvector(...)` document out of the searched columns.
2777
+ *
2778
+ * `coalesce(col, '')` matters: in SQL a `NULL` anywhere in a concatenation makes
2779
+ * the whole document `NULL`, so one empty column would silently exclude the row.
2780
+ *
2781
+ * @param columns The columns making up the document.
2782
+ * @param config The already-bound placeholder for the text-search config.
2783
+ * @param idFor Column-name resolver.
2784
+ * @returns The rendered `to_tsvector(...)` call.
2785
+ */
2786
+ protected tsVector(columns: readonly string[], config: string, idFor: (key: string) => string): string;
2787
+ /**
2788
+ * The SQL type name this dialect accepts in a `CAST`.
2789
+ *
2790
+ * The base mapping is the standard one PostgreSQL takes; SQLite and MySQL
2791
+ * override it, because the names genuinely differ (MySQL's `CAST(x AS SIGNED)`
2792
+ * is not `INTEGER`, and SQLite only has five storage classes to aim at).
2793
+ *
2794
+ * @param to The portable cast target.
2795
+ * @returns The dialect's own type name.
2796
+ */
2797
+ protected castTypeName(to: CastType): string;
2798
+ /**
2799
+ * Compile a comparison whose right-hand side is another expression rather than
2800
+ * a bound value (`total > paid`, `lower(a) = lower(b)`).
2801
+ *
2802
+ * The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
2803
+ * a value operand, and accepting an expression there would silently compile to
2804
+ * something else.
2805
+ *
2806
+ * @param left The rendered left-hand side.
2807
+ * @param op The operator name.
2808
+ * @param right The rendered right-hand side.
2809
+ * @returns The SQL text of the predicate.
2810
+ * @throws Error When the operator needs a value operand.
2811
+ */
2812
+ private compileExprOperator;
2813
+ private compileOperator;
2814
+ /**
2815
+ * Compile `IN` / `NOT IN`, whose operand is either a value list or a
2816
+ * single-column subquery.
2817
+ *
2818
+ * The subquery is rendered at the position it appears in the outer statement
2819
+ * and shares the same parameter collector, so its own placeholders land in the
2820
+ * right order — and it keeps its own `names` map, since the inner model may use
2821
+ * a different naming convention than the outer one.
2822
+ *
2823
+ * @param id The quoted column identifier being tested.
2824
+ * @param operand A list of values, or a {@link Subquery}.
2825
+ * @param params The parameter collector for the statement being compiled.
2826
+ * @param negate True for `NOT IN`.
2827
+ * @returns The SQL text of the predicate.
2828
+ */
2829
+ private compileIn;
2830
+ }
2831
+ /** SQLite dialect: `?` placeholders; `ILIKE` falls back to `LIKE` (ASCII-insensitive). */
2832
+ declare class SqliteDialect extends BaseDialect {
2833
+ readonly name: "sqlite";
2834
+ /**
2835
+ * SQLite explains with `EXPLAIN QUERY PLAN`, and has no `ANALYZE` — the plain
2836
+ * `EXPLAIN` there dumps bytecode, which answers a different question.
2837
+ */
2838
+ explainPrefix(analyze: boolean): string;
2839
+ /**
2840
+ * SQLite has `UPDATE ... FROM` (3.33+) but no `DELETE ... USING`.
2841
+ *
2842
+ * The portable shape there is a subquery — `where({ id: { in: … } })` — so this
2843
+ * throws and says so, rather than emitting a clause SQLite does not parse.
2844
+ */
2845
+ protected compileExtraSources(keyword: "FROM" | "USING", sources: readonly {
2846
+ readonly table: string;
2847
+ readonly alias: string;
2848
+ }[] | undefined): string;
2849
+ /**
2850
+ * SQLite has no text-search engine, so the prebuilt substring fallback is
2851
+ * compiled instead. The rows are right; the ranking is what is missing.
2852
+ */
2853
+ protected compileFullText(node: Extract<CondNode, {
2854
+ kind: "fullText";
2855
+ }>, params: Params, idFor: (key: string) => string): string;
2856
+ /** No text-search engine means no score: a constant, so ordering by it is inert. */
2857
+ protected renderRank(): string;
2858
+ /**
2859
+ * SQLite has five storage classes, so most targets collapse onto `TEXT` or
2860
+ * `INTEGER`. Naming a type it does not know would not fail — SQLite applies the
2861
+ * closest affinity — but it would make the cast mean something different here
2862
+ * than on the other databases, which is what this mapping avoids.
2863
+ */
2864
+ protected castTypeName(to: CastType): string;
2865
+ protected placeholder(): string;
2866
+ protected ilike(column: string, param: string): string;
2867
+ /**
2868
+ * SQLite runs one writer at a time, so its only isolation level **is**
2869
+ * serializable — there is no syntax to ask for another, and no weaker level to
2870
+ * fall back to. Asking for one is an error rather than a silent no-op, since a
2871
+ * caller who wrote `repeatable read` was reasoning about a guarantee.
2872
+ *
2873
+ * `readOnly` likewise has no per-transaction form here (`PRAGMA query_only` is
2874
+ * per connection), so it is refused instead of quietly ignored.
2875
+ */
2876
+ beginStatements(options?: TransactionOptions): string[];
2877
+ /**
2878
+ * SQLite has no row-level locking, so a lock request is an error rather than a
2879
+ * silently unlocked `SELECT` — a lock that does not exist only shows up as
2880
+ * duplicated work under production concurrency.
2881
+ */
2882
+ protected renderLock(): string;
2883
+ }
2884
+ /** PostgreSQL dialect: `$1` placeholders; native `ILIKE`; native array operators. */
2885
+ declare class PostgresDialect extends BaseDialect {
2886
+ readonly name: "postgresql";
2887
+ protected placeholder(index: number): string;
2888
+ protected ilike(column: string, param: string): string;
2889
+ protected arrayOperator(op: "contains" | "containedBy" | "overlaps"): string;
2890
+ }
2891
+ /**
2892
+ * MySQL dialect: `?` placeholders, backtick identifiers, `ON DUPLICATE KEY
2893
+ * UPDATE` for upsert, and case-insensitive `LIKE` (default collation). MySQL has
2894
+ * no `RETURNING`, so requesting it throws.
2895
+ */
2896
+ declare class MysqlDialect extends BaseDialect {
2897
+ readonly name: "mysql";
2898
+ protected placeholder(): string;
2899
+ protected ilike(column: string, param: string): string;
2900
+ /** MySQL's `EXPLAIN FORMAT=JSON` spells the option differently. */
2901
+ explainPrefix(analyze: boolean): string;
2902
+ /**
2903
+ * MySQL only gained `INTERSECT`/`EXCEPT` in 8.0.31, and this project does not
2904
+ * invest in MySQL beyond what already works — so they are refused here rather
2905
+ * than emitted against a server that may reject them.
2906
+ */
2907
+ protected setOperator(op: SetOperator): string;
2908
+ /**
2909
+ * MySQL writes multi-table updates as `UPDATE a JOIN b`, and has no
2910
+ * `DELETE ... USING` in this shape. Both are out of the project's active scope,
2911
+ * so they are refused rather than emitted against a server that rejects them.
2912
+ */
2913
+ protected compileExtraSources(keyword: "FROM" | "USING", sources: readonly {
2914
+ readonly table: string;
2915
+ readonly alias: string;
2916
+ }[] | undefined): string;
2917
+ /**
2918
+ * MySQL's full-text search needs a `FULLTEXT` index and different syntax, and it
2919
+ * is outside this project's active scope — the substring fallback is compiled,
2920
+ * like on SQLite.
2921
+ */
2922
+ protected compileFullText(node: Extract<CondNode, {
2923
+ kind: "fullText";
2924
+ }>, params: Params, idFor: (key: string) => string): string;
2925
+ /** No `ts_rank` equivalent in scope: a constant, so ordering by it is inert. */
2926
+ protected renderRank(): string;
2927
+ /**
2928
+ * MySQL sets the level with a statement **before** the transaction opens, and
2929
+ * spells the read-only flag on `START TRANSACTION` rather than on `BEGIN`.
2930
+ */
2931
+ beginStatements(options?: TransactionOptions): string[];
2932
+ /**
2933
+ * MySQL's `CAST` takes its own vocabulary — `SIGNED`, not `INTEGER`; `CHAR`,
2934
+ * not `TEXT` — and rejects the standard names outright.
2935
+ */
2936
+ protected castTypeName(to: CastType): string;
2937
+ protected quoteId(name: string): string;
2938
+ /**
2939
+ * MySQL rejects `LIMIT` inside an `IN` subquery with
2940
+ * `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
2941
+ * 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
2942
+ * instead of surfacing that error from the driver at runtime.
2943
+ */
2944
+ protected checkSubquery(node: SelectNode): void;
2945
+ protected renderConflict(onConflict: NonNullable<InsertNode["onConflict"]>, conflictCols: readonly string[], nextValue: () => string, names: NameMap | undefined): string;
2946
+ /**
2947
+ * MySQL has no `RETURNING`, so it cannot be compiled into a statement.
2948
+ *
2949
+ * `session.execute()` still honors `.returning()` on a **single-row INSERT** by
2950
+ * running the insert and reading the row back by key on the same connection —
2951
+ * that is execution, not compilation, so it never reaches here. Compiling a
2952
+ * node with `returning` directly is an error, rather than SQL that silently
2953
+ * returns nothing.
2954
+ */
2955
+ protected compileReturning(returning: readonly string[] | "*" | null): string;
2956
+ }
2957
+ /** Get a dialect instance by name. */
2958
+ declare function getDialect(name: Dialect): BaseDialect;
2959
+
2960
+ /**
2961
+ * tempest-db-js — opt-in model mixins.
2962
+ *
2963
+ * The same four or five columns show up on every real table: when the row was
2964
+ * created and last touched, whether it was soft-deleted, who changed it. Writing
2965
+ * them out per model is repetition that drifts — one table gets `updated_at`
2966
+ * without `onUpdate`, another spells it `updatedOn`.
2967
+ *
2968
+ * These mixins are functions that take a base class and return a subclass
2969
+ * carrying the extra columns, mirroring `tempest-fastapi-sdk`'s
2970
+ * `SoftDeleteMixin` / `AuditMixin`. They compose:
2971
+ *
2972
+ * ```ts
2973
+ * class Order extends withSoftDelete(withTimestamps(Model)) {
2974
+ * static override tablename = "orders";
2975
+ * id = column.integer().primaryKey();
2976
+ * }
2977
+ * ```
2978
+ *
2979
+ * The columns are real columns: they appear in `InferModel`, in the migration IR
2980
+ * and in the generated DDL, exactly as if you had typed them.
2981
+ */
2982
+
2983
+ /** Any class usable as a mixin base — `Model` itself, or another mixin's result. */
2984
+ type ModelBase = abstract new (...args: any[]) => Model;
2985
+ /**
2986
+ * Add `createdAt` / `updatedAt` to a model.
2987
+ *
2988
+ * Both default to the database's own clock (`sql.now()`), and `updatedAt` carries
2989
+ * `onUpdate(sql.now())`, so an `UPDATE` refreshes it without the call site
2990
+ * remembering to. Both are `NOT NULL` with a default, so neither shows up as
2991
+ * required in `InferInsert`.
2992
+ *
2993
+ * @param Base The class to extend (`Model`, or another mixin's result).
2994
+ * @returns A subclass carrying the two timestamp columns.
2995
+ */
2996
+ declare function withTimestamps<TBase extends ModelBase>(Base: TBase): (abstract new (...args: any[]) => {
2997
+ /** When the row was inserted. */
2998
+ createdAt: Column<Date, ColumnFlags & {
2999
+ notNull: true;
3000
+ } & {
3001
+ hasDefault: true;
3002
+ }>;
3003
+ /** When the row was last updated — refreshed by every `UPDATE`. */
3004
+ updatedAt: Column<Date, ColumnFlags & {
3005
+ notNull: true;
3006
+ } & {
3007
+ hasDefault: true;
3008
+ }>;
3009
+ }) & TBase;
3010
+ /**
3011
+ * Add `deletedAt` to a model, for non-destructive deletes.
3012
+ *
3013
+ * A row is alive while `deletedAt IS NULL`. **Filtering is the caller's job** —
3014
+ * the mixin only declares the column, so it composes with whatever query strategy
3015
+ * a table needs (a partial index, a repository default, a view). Use
3016
+ * {@link notDeleted} to spell the predicate.
3017
+ *
3018
+ * @param Base The class to extend.
3019
+ * @returns A subclass carrying the `deletedAt` column.
3020
+ */
3021
+ declare function withSoftDelete<TBase extends ModelBase>(Base: TBase): (abstract new (...args: any[]) => {
3022
+ /** When the row was soft-deleted, or `null` while it is alive. */
3023
+ deletedAt: Column<Date, ColumnFlags>;
3024
+ }) & TBase;
3025
+ /**
3026
+ * The `where` fragment matching rows that are **not** soft-deleted.
3027
+ *
3028
+ * @typeParam Row - the row type being filtered.
3029
+ * @returns `{ deletedAt: { isNull: true } }`, typed for the row.
3030
+ */
3031
+ declare function notDeleted<Row>(): WhereInput<Row>;
3032
+ /**
3033
+ * The `where` fragment matching **only** soft-deleted rows.
3034
+ *
3035
+ * @typeParam Row - the row type being filtered.
3036
+ * @returns `{ deletedAt: { isNull: false } }`, typed for the row.
3037
+ */
3038
+ declare function onlyDeleted<Row>(): WhereInput<Row>;
3039
+ /**
3040
+ * Add `createdBy` / `updatedBy` to a model — *who* touched the row.
3041
+ *
3042
+ * The actor column defaults to `column.uuid()`, matching the SDK's user id. A
3043
+ * service whose users are keyed by something else passes a factory:
3044
+ * `withAudit(Model, () => column.integer())`. It must be a factory, not a column:
3045
+ * the two properties need two independent column instances.
3046
+ *
3047
+ * Both columns are nullable — a row created by a background job has no actor, and
3048
+ * forcing a sentinel value there is worse than a `NULL`.
3049
+ *
3050
+ * @param Base The class to extend.
3051
+ * @param actor Factory producing the actor column (default: `column.uuid()`).
3052
+ * @returns A subclass carrying the two actor columns.
3053
+ */
3054
+ declare function withAudit<TBase extends ModelBase, A = string>(Base: TBase, actor?: () => Column<A, ColumnFlags>): (abstract new (...args: any[]) => {
3055
+ /** Who created the row, or `null` when nobody was attributed. */
3056
+ createdBy: Column<A, ColumnFlags>;
3057
+ /** Who last updated the row, or `null`. */
3058
+ updatedBy: Column<A, ColumnFlags>;
3059
+ }) & TBase;
3060
+
3061
+ /**
3062
+ * tempest-db-js — read a constraint violation back out of the driver's error.
3063
+ *
3064
+ * A database says *why* it refused a write, and says it in prose whose shape is
3065
+ * the driver's, not the application's. Answering `409 {"code":"EMAIL_TAKEN"}`
3066
+ * instead of a generic conflict otherwise means every service writing its own
3067
+ * regular expression against whichever dialect the author had running.
3068
+ *
3069
+ * The two dialects in scope say the same five things two different ways:
3070
+ * PostgreSQL names the constraint and lists the columns in a `DETAIL:` line,
3071
+ * while SQLite spells the table and columns into the message and says nothing
3072
+ * about the constraint's name.
3073
+ */
3074
+
3075
+ /** What kind of constraint refused the write. */
3076
+ type IntegrityViolation = "unique" | "foreignKey" | "notNull" | "check" | "exclusion";
3077
+ /** A constraint violation, read out of the driver's error. */
3078
+ interface IntegrityFailure {
3079
+ /** Which kind of constraint refused the write. */
3080
+ readonly violation: IntegrityViolation;
3081
+ /** The constraint's name, when the database reported one (PostgreSQL does). */
3082
+ readonly constraint: string | null;
3083
+ /** The table, when the database reported one. */
3084
+ readonly table: string | null;
3085
+ /**
3086
+ * The columns the constraint covers.
3087
+ *
3088
+ * Empty when the database did not say — a SQLite `FOREIGN KEY constraint
3089
+ * failed` carries no names at all.
3090
+ */
3091
+ readonly columns: readonly string[];
3092
+ /** The driver's own message, kept verbatim for logging. */
3093
+ readonly detail: string;
3094
+ }
3095
+ /**
3096
+ * Read a driver error back into the constraint that refused the write.
3097
+ *
3098
+ * @param error Anything thrown by a write — a `QueryExecutionError` from this
3099
+ * package, or the driver's own error; the cause chain is followed either way.
3100
+ * @param model Optional. When given, database column names are translated back to
3101
+ * the model's property names, so a `snake_case` schema reports `idempotencyKey`
3102
+ * rather than `idempotency_key`.
3103
+ * @returns The violation, or `null` when the error is not an integrity violation
3104
+ * (or came from MySQL, which is out of scope — see the recipe).
3105
+ *
3106
+ * @example
3107
+ * ```ts
3108
+ * try {
3109
+ * await users.create({ email });
3110
+ * } catch (err) {
3111
+ * const failure = parseIntegrityError(err, User);
3112
+ * if (failure?.violation === "unique" && failure.columns.includes("email")) {
3113
+ * throw new EmailTaken();
3114
+ * }
3115
+ * throw err;
3116
+ * }
3117
+ * ```
3118
+ */
3119
+ declare function parseIntegrityError(error: unknown, model?: ModelClass): IntegrityFailure | null;
3120
+
3121
+ /**
3122
+ * tempest-db-js — text search that survives what users actually type.
3123
+ *
3124
+ * Two layers, because they answer different questions:
3125
+ *
3126
+ * - {@link contains} — tokenized, **escaped** substring matching. Identical on
3127
+ * every dialect, needs no extension, no index and no migration. This is what
3128
+ * "find the row whose name contains what the user typed" should use.
3129
+ * - {@link fullText} / {@link fullTextRank} — PostgreSQL's `to_tsvector` /
3130
+ * `websearch_to_tsquery` / `ts_rank`: stemming, stop words and a relevance
3131
+ * score. On any other database they fall back to the first layer, which is the
3132
+ * honest degradation — the query still returns the right rows, just without
3133
+ * stemming.
3134
+ */
3135
+
3136
+ /**
3137
+ * A PostgreSQL text-search configuration (`regconfig`), such as `"english"` or
3138
+ * `"portuguese"`. `"simple"` disables stemming and stop-word removal.
3139
+ */
3140
+ type TextSearchLanguage = string;
3141
+ /** Options shared by the full-text helpers. */
3142
+ interface TextSearchOptions {
3143
+ /** The PostgreSQL text-search configuration. Defaults to `"english"`. */
3144
+ readonly language?: TextSearchLanguage;
3145
+ }
3146
+ /** Options for {@link contains}. */
3147
+ interface ContainsOptions {
3148
+ /**
3149
+ * Whether a row must match **every** token (default) or any one of them.
3150
+ *
3151
+ * `"all"` is what a search box wants: typing more words narrows the result.
3152
+ */
3153
+ readonly match?: "all" | "any";
3154
+ }
3155
+ /**
3156
+ * Escape the `LIKE` wildcards in a literal, for use with `like` / `ilike`.
3157
+ *
3158
+ * `%` and `_` are wildcards, and `\` escapes them — so a user searching for
3159
+ * `100%` matches every row unless the literal is escaped first. The pattern this
3160
+ * produces is meant to be used with an `ESCAPE '\'` clause, which is what the
3161
+ * {@link ContainsOptions} operator emits; PostgreSQL assumes that escape by
3162
+ * default, SQLite has none until it is given one.
3163
+ *
3164
+ * @param value The literal the user typed.
3165
+ * @returns The same text with `\`, `%` and `_` escaped.
3166
+ */
3167
+ declare function escapeLike(value: string): string;
3168
+ /**
3169
+ * Split a search term into the tokens a row has to match.
3170
+ *
3171
+ * @param term What the user typed.
3172
+ * @returns The non-empty whitespace-separated tokens.
3173
+ */
3174
+ declare function tokenize(term: string): string[];
3175
+ /**
3176
+ * Case-insensitive substring search across one or more columns.
3177
+ *
3178
+ * The term is tokenized, and each token is matched — **escaped** — against every
3179
+ * listed column; a row matches when every token appears in at least one of them.
3180
+ * Behaves the same on every dialect, with no index or extension needed.
3181
+ *
3182
+ * @param columns The columns to search, by property name.
3183
+ * @param term What the user typed.
3184
+ * @param options `match: "any"` to require a single token instead of all of them.
3185
+ * @returns A condition for `where`, matching nothing when the term is blank.
3186
+ * @throws Error When no column is given.
3187
+ *
3188
+ * @example
3189
+ * ```ts
3190
+ * select(User).where(contains(["name", "email"], "100% ana"));
3191
+ * // (name ILIKE '%100\%%' ESCAPE '\' OR email ILIKE ...) AND (name ILIKE '%ana%' ...)
3192
+ * ```
3193
+ */
3194
+ declare function contains<Row = Record<string, unknown>>(columns: readonly string[], term: string, options?: ContainsOptions): Condition;
3195
+ /**
3196
+ * PostgreSQL full-text search over one or more columns, with a portable fallback.
3197
+ *
3198
+ * On PostgreSQL this compiles to `to_tsvector(config, col || ' ' || col) @@
3199
+ * websearch_to_tsquery(config, term)`, so the search stems (`buying` matches
3200
+ * `buy`), drops stop words, and understands the quoting and `-exclusion` syntax
3201
+ * users already know from web search boxes.
3202
+ *
3203
+ * On any other database it compiles to {@link contains} instead — the same rows a
3204
+ * substring search would find, without stemming. That is a documented
3205
+ * degradation, not a silent one: a dev SQLite database keeps working, and the
3206
+ * difference is in ranking quality, not correctness.
3207
+ *
3208
+ * @param columns The columns to search, by property name.
3209
+ * @param term What the user typed.
3210
+ * @param options The text-search configuration.
3211
+ * @returns A condition for `where`.
3212
+ * @throws Error When no column is given.
3213
+ */
3214
+ declare function fullText<Row = Record<string, unknown>>(columns: readonly string[], term: string, options?: TextSearchOptions): Condition;
3215
+ /**
3216
+ * The relevance score of a full-text match, for `orderBy`.
3217
+ *
3218
+ * On PostgreSQL this is `ts_rank(...)`, the number that makes "best match first"
3219
+ * mean something. Elsewhere it renders as a constant, so ordering by it is a
3220
+ * no-op and the query falls back to whatever other ordering is given — ranking is
3221
+ * the part that genuinely does not exist without a text-search engine.
3222
+ *
3223
+ * @param columns The same columns passed to {@link fullText}.
3224
+ * @param term The same term.
3225
+ * @param options The same configuration.
3226
+ * @returns An expression usable in `orderBy`.
3227
+ * @throws Error When no column is given.
3228
+ *
3229
+ * @example
3230
+ * ```ts
3231
+ * select(Post)
3232
+ * .where(fullText(["title", "body"], term, { language: "portuguese" }))
3233
+ * .orderBy(fullTextRank(["title", "body"], term, { language: "portuguese" }), "desc");
3234
+ * ```
3235
+ */
3236
+ declare function fullTextRank(columns: readonly string[], term: string, options?: TextSearchOptions): Expression;
3237
+
3238
+ /**
3239
+ * tempest-db-js — lifecycle signals around the repository's write path.
3240
+ *
3241
+ * Reacting to persistence — busting a cache, enqueuing an outbox event, syncing a
3242
+ * search index, writing an audit row — otherwise means wrapping every call site
3243
+ * by hand, and the one call site somebody forgets is the bug.
3244
+ *
3245
+ * Handlers run **inside** whatever transaction is open, because they are handed
3246
+ * the same session the write used: a handler that writes commits with the write,
3247
+ * or rolls back with it.
3248
+ */
3249
+
3250
+ /** The points a repository write is observable at. */
3251
+ type RepositorySignal = "preSave" | "postSave" | "preDelete" | "postDelete";
3252
+ /** What a handler receives. */
3253
+ interface SignalPayload<C extends ModelClass> {
3254
+ /**
3255
+ * The row the signal is about.
3256
+ *
3257
+ * `preSave` on an insert carries what was passed to `create`, so a
3258
+ * database-generated id is not there yet — `postSave` carries the stored row.
3259
+ * `preDelete` carries the row as it was just before it was deleted.
3260
+ */
3261
+ readonly row: InferModel<C>;
3262
+ /** The model being written. */
3263
+ readonly model: C;
3264
+ /** The session the write is running on — the same transaction, if one is open. */
3265
+ readonly session: AsyncSession;
3266
+ /** True when the write that fired this was an insert rather than an update. */
3267
+ readonly isInsert: boolean;
3268
+ }
3269
+ /** A signal handler; may be async, and is awaited. */
3270
+ type SignalHandler<C extends ModelClass> = (payload: SignalPayload<C>) => void | Promise<void>;
3271
+ /**
3272
+ * Register a handler for one signal on one model.
3273
+ *
3274
+ * @param model The model to observe.
3275
+ * @param signal Which point to observe.
3276
+ * @param handler The handler; awaited, and free to use the payload's session.
3277
+ * @returns A function that unregisters this handler.
3278
+ *
3279
+ * @example
3280
+ * ```ts
3281
+ * const off = onSignal(User, "postSave", async ({ row }) => {
3282
+ * await cache.del(`user:${row.id}`);
3283
+ * });
3284
+ * ```
3285
+ */
3286
+ declare function onSignal<C extends ModelClass>(model: C, signal: RepositorySignal, handler: SignalHandler<C>): () => void;
3287
+ /**
3288
+ * Whether anything is listening to a signal on a model.
3289
+ *
3290
+ * The repository asks before doing work only a handler would need — reading the
3291
+ * rows a filter-based `update`/`delete` is about to touch costs a `SELECT`, and
3292
+ * nobody should pay it when no handler exists.
3293
+ *
3294
+ * @param model The model.
3295
+ * @param signal The signal.
3296
+ * @returns True when at least one handler is registered.
3297
+ */
3298
+ declare function hasHandlers(model: ModelClass, signal: RepositorySignal): boolean;
3299
+ /**
3300
+ * Fire a signal, awaiting every handler in registration order.
3301
+ *
3302
+ * A handler that throws **propagates**: on `preSave`/`preDelete` that vetoes the
3303
+ * write, which is the point — a veto that could be swallowed would be a
3304
+ * suggestion.
3305
+ *
3306
+ * @param signal The signal to fire.
3307
+ * @param payload What to hand the handlers.
3308
+ */
3309
+ declare function emitSignal<C extends ModelClass>(signal: RepositorySignal, payload: SignalPayload<C>): Promise<void>;
3310
+ /**
3311
+ * Drop registered handlers — for tests, which otherwise leak them across cases.
3312
+ *
3313
+ * @param model Only this model's handlers; omitted, every model's.
3314
+ */
3315
+ declare function clearSignals(model?: ModelClass): void;
3316
+
3317
+ /**
3318
+ * tempest-db-js — Phase 7: typed repository + pagination.
3319
+ *
3320
+ * `BaseRepository<Model>` mirrors the `tempest-fastapi-sdk` repository: a thin,
3321
+ * fully-typed CRUD + pagination layer over a model and an async session. The
3322
+ * 404-convention is honored — `getById` throws when absent, collection methods
3323
+ * return `[]` (never a "not found" error for an empty list).
3324
+ */
3325
+
3326
+ /** Pagination request — 1-indexed page. */
3327
+ interface PaginationFilter<Row> {
3328
+ readonly page?: number;
3329
+ readonly pageSize?: number;
3330
+ readonly orderBy?: keyof Row & string;
3331
+ readonly ascending?: boolean;
3332
+ readonly filters?: WhereInput<Row>;
3333
+ }
3334
+ /** A page of results plus metadata (mirrors `BasePaginationSchema`). */
3335
+ interface PaginationResult<Row> {
3336
+ readonly items: Row[];
3337
+ readonly total: number;
3338
+ readonly page: number;
3339
+ readonly pageSize: number;
3340
+ readonly pages: number;
3341
+ }
3342
+ /**
3343
+ * Cursor-pagination request.
3344
+ *
3345
+ * Cursor paging trades random access for two things offset paging cannot give on a
3346
+ * large table: no `COUNT(*)`, and a page boundary that does not shift when rows are
3347
+ * inserted while the user is reading.
3348
+ *
3349
+ * @typeParam Row - the row type being paginated.
3350
+ */
3351
+ interface CursorPaginationFilter<Row> {
3352
+ /** Opaque cursor from the previous page. Absent/`null` asks for the first page. */
3353
+ readonly cursor?: string | null;
3354
+ /** Maximum rows to return (default 20). */
3355
+ readonly limit?: number;
3356
+ /** Column to sort by. Defaults to the first primary-key column. */
3357
+ readonly orderBy?: keyof Row & string;
3358
+ /** Sort ascending. Defaults to `false`, so the newest rows come first. */
3359
+ readonly ascending?: boolean;
3360
+ /** Domain filters, applied to every page. */
3361
+ readonly filters?: WhereInput<Row>;
3362
+ }
3363
+ /** One cursor-paginated page. */
3364
+ interface CursorPage<Row> {
3365
+ /** The rows of this page. */
3366
+ readonly items: Row[];
3367
+ /** Cursor for the next page, or `null` when this was the last one. */
3368
+ readonly nextCursor: string | null;
3369
+ }
3370
+ /** Raised when a cursor is not one this repository produced. */
3371
+ declare class InvalidCursor extends Error {
3372
+ constructor(reason: string);
3373
+ }
3374
+ /** Request for {@link BaseRepository.changesSince}. */
3375
+ interface ChangesSinceFilter<Row> {
3376
+ /**
3377
+ * The client's high-water mark. Rows changed **strictly after** it are
3378
+ * returned; `null` (or absent) asks for everything, which is the first sync.
3379
+ */
3380
+ readonly since?: Date | null;
3381
+ /** Cursor from the previous page of this same pull. */
3382
+ readonly cursor?: string | null;
3383
+ /** Maximum rows per page (default 50). */
3384
+ readonly limit?: number;
3385
+ /** Domain filters, applied to every page. */
3386
+ readonly filters?: WhereInput<Row>;
3387
+ }
3388
+ /** One page of changes, plus the watermark to persist for the next pull. */
3389
+ interface ChangesPage<Row> extends CursorPage<Row> {
3390
+ /**
3391
+ * The server's clock, read **before** the query ran.
3392
+ *
3393
+ * This is what a client persists as its next `since` — not the newest
3394
+ * `updatedAt` it saw. A row committed while this page was being built carries
3395
+ * a later timestamp, so it surfaces on the following pull instead of falling
3396
+ * into the gap between the two.
3397
+ */
3398
+ readonly serverTime: Date;
3399
+ }
3400
+ /** Options for {@link BaseRepository.bulkUpsert}. */
3401
+ interface BulkUpsertOptions<Row> {
3402
+ /** The columns forming the conflict target (a unique constraint or index). */
3403
+ readonly conflictColumns: readonly (keyof Row & string)[];
3404
+ /**
3405
+ * Which columns to overwrite on conflict. Omitted, every column present in the
3406
+ * incoming rows except the conflict target is written.
3407
+ */
3408
+ readonly update?: readonly (keyof Row & string)[];
3409
+ }
3410
+ /** Raised by single-record lookups (`getById`) when nothing matches (404). */
3411
+ declare class RecordNotFound extends Error {
3412
+ constructor(table: string, key: unknown);
3413
+ }
3414
+ /**
3415
+ * A fully-typed CRUD + pagination repository over a model and an async session.
3416
+ *
3417
+ * @typeParam C - the model class.
3418
+ */
3419
+ declare class BaseRepository<C extends ModelClass> {
3420
+ protected readonly model: C;
3421
+ protected readonly session: AsyncSession;
3422
+ private readonly pks;
3423
+ constructor(model: C, session: AsyncSession);
3424
+ /**
3425
+ * Narrow every read this repository performs.
3426
+ *
3427
+ * The extension point a scoped repository overrides: returning a filter that
3428
+ * always carries the scope's predicate is what makes it impossible for one
3429
+ * query site to forget it. The base implementation adds nothing.
3430
+ *
3431
+ * @param filters The caller's filters.
3432
+ * @returns The filters actually sent to the database.
3433
+ */
3434
+ protected scopeFilters(filters?: WhereInput<InferModel<C>>): WhereInput<InferModel<C>> | undefined;
3435
+ /**
3436
+ * Stamp every row this repository writes.
3437
+ *
3438
+ * The write-side counterpart of {@link scopeFilters}. The base implementation
3439
+ * writes the row unchanged.
3440
+ *
3441
+ * @param data The row being written.
3442
+ * @returns The row actually written.
3443
+ */
3444
+ protected scopeWrite<T extends Record<string, unknown>>(data: T): T;
3445
+ /** All rows matching `filters` (or everything). Empty list when none match. */
3446
+ list(input?: WhereInput<InferModel<C>>): Promise<InferModel<C>[]>;
3447
+ /** The first row matching `filters`, or `null`. */
3448
+ first(input?: WhereInput<InferModel<C>>): Promise<InferModel<C> | null>;
3449
+ /**
3450
+ * A single row by primary key, or `null`.
3451
+ *
3452
+ * @param id The key — a bare value for a single-column key, an object
3453
+ * (`{ orderId, lineNumber }`) for a composite one.
3454
+ * @returns The row, or `null` when nothing matches.
3455
+ * @throws Error When a scalar is given for a composite key, or the key is
3456
+ * incomplete.
3457
+ */
3458
+ getByIdOrNull(id: unknown): Promise<InferModel<C> | null>;
3459
+ /**
3460
+ * A single row by primary key; throws `RecordNotFound` when absent.
3461
+ *
3462
+ * @param id The key — see {@link getByIdOrNull}.
3463
+ * @returns The row.
3464
+ * @throws RecordNotFound When no row carries that key.
3465
+ */
3466
+ getById(id: unknown): Promise<InferModel<C>>;
3467
+ /** Whether any row matches `filters`. */
3468
+ exists(filters: WhereInput<InferModel<C>>): Promise<boolean>;
3469
+ /** How many rows match `filters` (or the whole table). */
3470
+ count(input?: WhereInput<InferModel<C>>): Promise<number>;
3471
+ /**
3472
+ * Insert one row, returning the created row.
3473
+ *
3474
+ * Fires `preSave` (which can veto by throwing) and then `postSave`.
3475
+ *
3476
+ * @param data The row to insert.
3477
+ * @returns The stored row.
3478
+ */
3479
+ create(data: InferInsert<C>): Promise<InferModel<C>>;
3480
+ /**
3481
+ * Insert many rows, returning the created rows.
3482
+ *
3483
+ * Signals fire per row, so a handler sees one row at a time whether it was
3484
+ * inserted alone or in a batch.
3485
+ *
3486
+ * @param data The rows to insert.
3487
+ * @returns The stored rows.
3488
+ */
3489
+ createMany(data: readonly InferInsert<C>[]): Promise<InferModel<C>[]>;
3490
+ /**
3491
+ * Update rows matching `filters`; returns the number of rows affected.
3492
+ *
3493
+ * With a `preSave`/`postSave` handler registered, the matching rows are read
3494
+ * first so the handler can see them — that extra `SELECT` is skipped entirely
3495
+ * when nothing is listening.
3496
+ *
3497
+ * @param filters Which rows to update.
3498
+ * @param set The columns to change.
3499
+ * @returns The number of rows affected.
3500
+ */
3501
+ update(input: WhereInput<InferModel<C>>, set: Partial<InferModel<C>>): Promise<number>;
3502
+ /**
3503
+ * Delete rows matching `filters`; returns the number of rows affected.
3504
+ *
3505
+ * `preDelete` and `postDelete` receive the row as it was **before** the delete —
3506
+ * the only chance to see it. Reading it costs a `SELECT`, which is skipped when
3507
+ * no handler is registered.
3508
+ *
3509
+ * @param filters Which rows to delete.
3510
+ * @returns The number of rows affected.
3511
+ */
3512
+ delete(input: WhereInput<InferModel<C>>): Promise<number>;
3513
+ /**
3514
+ * Fire one signal for one row on this repository's model and session.
3515
+ *
3516
+ * @param signal Which signal.
3517
+ * @param row The row it is about.
3518
+ * @param isInsert Whether the write was an insert.
3519
+ */
3520
+ private fire;
3521
+ /**
3522
+ * A page of rows plus metadata. `total` counts all matching rows.
3523
+ *
3524
+ * @param filter Page, size, ordering and filters.
3525
+ * @returns The page and pagination metadata.
3526
+ */
3527
+ paginate(filter?: PaginationFilter<InferModel<C>>): Promise<PaginationResult<InferModel<C>>>;
3528
+ /**
3529
+ * A cursor-paginated page: the rows after `cursor`, plus the cursor for the
3530
+ * next page.
3531
+ *
3532
+ * No `COUNT(*)` runs, and the page boundary is stable under concurrent inserts —
3533
+ * the two reasons to reach for this instead of {@link paginate} on a large table.
3534
+ * The trade-off is losing random access: there is no "page 7".
3535
+ *
3536
+ * The primary key is always appended as a tie-break, so rows sharing an
3537
+ * `orderBy` value cannot be skipped or repeated across pages.
3538
+ *
3539
+ * @param filter Cursor, page size, ordering and filters.
3540
+ * @returns The page and the next cursor (`null` on the last page).
3541
+ * @throws InvalidCursor When the cursor is malformed or was built for a
3542
+ * different ordering.
3543
+ */
3544
+ cursorPaginate(filter?: CursorPaginationFilter<InferModel<C>>): Promise<CursorPage<InferModel<C>>>;
3545
+ /**
3546
+ * Whether any **other** row matches `filters`.
3547
+ *
3548
+ * The uniqueness check for an update: "is this e-mail taken by somebody else?"
3549
+ * A plain `exists` would find the row being edited and report a false conflict.
3550
+ *
3551
+ * @param filters What to look for.
3552
+ * @param key The primary key to exclude — the row being updated.
3553
+ * @returns True when a different row matches.
3554
+ */
3555
+ existsExcluding(filters: WhereInput<InferModel<C>>, key: unknown): Promise<boolean>;
3556
+ /**
3557
+ * The rows that changed since a high-water mark — the delta-sync read.
3558
+ *
3559
+ * Rows come back oldest change first, tie-broken by primary key, so a client
3560
+ * can advance its watermark monotonically and resume mid-stream with the
3561
+ * cursor. The filter is **strict** (`updatedAt > since`).
3562
+ *
3563
+ * Soft-deleted rows are included on purpose: they are the tombstones that tell
3564
+ * the client to delete its local copy. Filtering them out would strand deleted
3565
+ * rows on the device forever.
3566
+ *
3567
+ * @param filter The watermark, cursor, page size and domain filters.
3568
+ * @returns The page, plus the `serverTime` to persist as the next watermark.
3569
+ * @throws Error When the model has no `updatedAt` column (see `withTimestamps`).
3570
+ */
3571
+ changesSince(filter?: ChangesSinceFilter<InferModel<C>>): Promise<ChangesPage<InferModel<C>>>;
3572
+ /**
3573
+ * Insert many rows, overwriting the ones that conflict — one statement.
3574
+ *
3575
+ * @param rows The rows to write.
3576
+ * @param options The conflict target, and optionally which columns to overwrite.
3577
+ * @returns The stored rows.
3578
+ * @throws Error When no conflict column is given.
3579
+ */
3580
+ bulkUpsert(rows: readonly InferInsert<C>[], options: BulkUpsertOptions<InferModel<C>>): Promise<InferModel<C>[]>;
3581
+ /**
3582
+ * Mark a row deleted without removing it (`deletedAt = now()`).
3583
+ *
3584
+ * @param key The primary key.
3585
+ * @returns The updated row.
3586
+ * @throws Error When the model has no `deletedAt` column (see `withSoftDelete`).
3587
+ * @throws RecordNotFound When no row carries that key.
3588
+ */
3589
+ softDelete(key: unknown): Promise<InferModel<C>>;
3590
+ /**
3591
+ * Bring a soft-deleted row back (`deletedAt = null`).
3592
+ *
3593
+ * @param key The primary key.
3594
+ * @returns The updated row.
3595
+ * @throws Error When the model has no `deletedAt` column.
3596
+ * @throws RecordNotFound When no row carries that key.
3597
+ */
3598
+ restore(key: unknown): Promise<InferModel<C>>;
3599
+ /**
3600
+ * Delete many rows by primary key, in one statement.
3601
+ *
3602
+ * @param keys The primary keys.
3603
+ * @returns The number of rows actually deleted (keys that matched nothing are
3604
+ * not an error — deleting what is already gone is the desired end state).
3605
+ * @throws Error When the model has a composite primary key: `IN` over a tuple is
3606
+ * not portable, and the caller should loop or build the condition explicitly.
3607
+ */
3608
+ deleteBatch(keys: readonly unknown[]): Promise<number>;
3609
+ /**
3610
+ * Fail loudly when a method needs a column the model does not declare.
3611
+ *
3612
+ * @param column The property name required.
3613
+ * @param method The method asking, for the message.
3614
+ * @throws Error When the column is absent.
3615
+ */
3616
+ private requireColumn;
3617
+ }
3618
+
3619
+ /** Where an outbox row is in its lifecycle. */
3620
+ type OutboxStatus = "pending" | "sending" | "sent" | "failed";
3621
+ /**
3622
+ * Build the base class for a service's outbox table.
3623
+ *
3624
+ * @param tablename The table to store events in.
3625
+ * @returns A model base carrying the outbox columns; extend it to add your own.
3626
+ *
3627
+ * @example
3628
+ * ```ts
3629
+ * class OutboxEvent extends outboxModel("outbox") {}
3630
+ * ```
3631
+ */
3632
+ declare function outboxModel(name: string): (abstract new () => {
3633
+ /** Monotonic id; also the claim order, so events publish in the order written. */
3634
+ id: Column<bigint, ColumnFlags & {
3635
+ primaryKey: true;
3636
+ hasDefault: true;
3637
+ }>;
3638
+ /** The routing key the relay publishes under. */
3639
+ topic: Column<string, ColumnFlags & {
3640
+ notNull: true;
3641
+ }>;
3642
+ /** The event body. */
3643
+ payload: Column<Record<string, unknown>, ColumnFlags & {
3644
+ notNull: true;
3645
+ }>;
3646
+ /** Lifecycle state. */
3647
+ status: Column<"pending" | "sending" | "sent" | "failed", ColumnFlags & {
3648
+ notNull: true;
3649
+ } & {
3650
+ hasDefault: true;
3651
+ }>;
3652
+ /** How many publish attempts have been made. */
3653
+ attempts: Column<number, ColumnFlags & {
3654
+ notNull: true;
3655
+ } & {
3656
+ hasDefault: true;
3657
+ }>;
3658
+ /** Epoch milliseconds before which the row must not be claimed (backoff). */
3659
+ availableAt: Column<bigint, ColumnFlags & {
3660
+ notNull: true;
3661
+ } & {
3662
+ hasDefault: true;
3663
+ }>;
3664
+ /** When the row was written. */
3665
+ createdAt: Column<Date, ColumnFlags & {
3666
+ notNull: true;
3667
+ } & {
3668
+ hasDefault: true;
3669
+ }>;
3670
+ /** When the relay confirmed the publish. */
3671
+ sentAt: Column<Date, ColumnFlags>;
3672
+ /** The last failure's message, kept for triage. */
3673
+ lastError: Column<string, ColumnFlags>;
3674
+ }) & {
3675
+ tablename: string;
3676
+ tableArgs?: () => readonly TableConstraint[];
3677
+ naming?: NamingStrategy;
3678
+ };
3679
+ /** An event to write to the outbox. */
3680
+ interface OutboxEventInput {
3681
+ /** The routing key. */
3682
+ readonly topic: string;
3683
+ /** The event body. */
3684
+ readonly payload: Record<string, unknown>;
3685
+ /** Delay the first attempt by this many milliseconds. */
3686
+ readonly delayMs?: number;
3687
+ }
3688
+ /** Options for {@link OutboxRepository.claim}. */
3689
+ interface ClaimOptions {
3690
+ /** Only claim events on these topics. */
3691
+ readonly topics?: readonly string[];
3692
+ /** Treat the clock as this instant (tests). */
3693
+ readonly now?: number;
3694
+ }
3695
+ /** Options for {@link OutboxRepository.markFailed}. */
3696
+ interface FailOptions {
3697
+ /** Wait this long before the row can be claimed again. */
3698
+ readonly retryInMs?: number;
3699
+ /** Give up permanently instead of scheduling a retry. */
3700
+ readonly permanent?: boolean;
3701
+ }
3702
+ /**
3703
+ * The relay's half of the outbox: claim, confirm, and fail with backoff.
3704
+ *
3705
+ * @typeParam C - the outbox model class.
3706
+ */
3707
+ declare class OutboxRepository<C extends ModelClass> extends BaseRepository<C> {
3708
+ /**
3709
+ * Write events to the outbox.
3710
+ *
3711
+ * Call it inside the same `transaction()` as the business write — that is the
3712
+ * whole point, and it is why this does not open a transaction of its own.
3713
+ *
3714
+ * @param events One event, or many.
3715
+ * @returns The stored rows.
3716
+ */
3717
+ publish(events: OutboxEventInput | readonly OutboxEventInput[]): Promise<InferModel<C>[]>;
3718
+ /**
3719
+ * Claim a batch of due events for this relay, in one statement.
3720
+ *
3721
+ * Uses `FOR UPDATE SKIP LOCKED` over a subquery, so two relays running at once
3722
+ * take **disjoint** batches instead of fighting over the same rows. SQLite has
3723
+ * no row locking and throws — a single-process relay there can claim with
3724
+ * `pending()` plus an update.
3725
+ *
3726
+ * @param limit How many events to take.
3727
+ * @param options Topic filter, and a clock override for tests.
3728
+ * @returns The claimed rows, oldest first.
3729
+ */
3730
+ claim(limit: number, options?: ClaimOptions): Promise<InferModel<C>[]>;
3731
+ /**
3732
+ * The events that are due, without claiming them.
3733
+ *
3734
+ * @param limit How many to read.
3735
+ * @param options Topic filter and clock override.
3736
+ * @returns The due rows, oldest first.
3737
+ */
3738
+ pending(limit: number, options?: ClaimOptions): Promise<InferModel<C>[]>;
3739
+ /**
3740
+ * Confirm that events were published.
3741
+ *
3742
+ * @param ids The claimed ids.
3743
+ * @returns How many rows were marked sent.
3744
+ */
3745
+ markSent(ids: readonly bigint[]): Promise<number>;
3746
+ /**
3747
+ * Record a failed publish, scheduling a retry unless it is permanent.
3748
+ *
3749
+ * The attempt counter was already incremented by {@link claim}, so a row that
3750
+ * keeps failing carries its own history — which is what a dead-letter policy
3751
+ * reads.
3752
+ *
3753
+ * @param id The event's id.
3754
+ * @param error The failure, for triage.
3755
+ * @param options Backoff delay, or `permanent` to stop retrying.
3756
+ * @returns How many rows were updated (0 or 1).
3757
+ */
3758
+ markFailed(id: bigint, error: unknown, options?: FailOptions): Promise<number>;
3759
+ /**
3760
+ * The SELECT of events that may be claimed now.
3761
+ *
3762
+ * @param now The current epoch milliseconds.
3763
+ * @param topics Optional topic filter.
3764
+ * @returns The builder, unordered.
3765
+ */
3766
+ private dueQuery;
3767
+ }
3768
+
3769
+ /**
3770
+ * tempest-db-js — a repository scoped to one tenant.
3771
+ *
3772
+ * In a shared-schema multi-tenant database every tenant's rows live in the same
3773
+ * table, told apart by a column. The danger is plain: forget one
3774
+ * `WHERE tenantId = ?` and tenant A reads — or deletes — tenant B's data. The
3775
+ * footgun is not the missing predicate, it is that **every query site** has to
3776
+ * remember it.
3777
+ *
3778
+ * This binds the tenant once, at construction, and injects the predicate into
3779
+ * every read and every write the repository performs.
3780
+ */
3781
+
3782
+ /** How a repository is bound to one tenant. */
3783
+ interface TenantScope {
3784
+ /** The column carrying the tenant, by property name. */
3785
+ readonly column: string;
3786
+ /** The tenant's identifier. */
3787
+ readonly id: unknown;
1559
3788
  }
1560
3789
  /**
1561
- * A server-side notice (a PostgreSQL `NOTICE`). The shape is the driver's own —
1562
- * passed through untouched rather than normalized, since what is useful in it
1563
- * differs per database.
3790
+ * A repository whose every read and write is confined to one tenant.
3791
+ *
3792
+ * @typeParam C - the model class.
1564
3793
  */
1565
- type NoticeLogger = (notice: Record<string, unknown>) => void;
1566
- /** Options shared by both engine flavors. */
1567
- interface EngineOptions {
1568
- /** Override the driver detected from the URL (e.g. `"better-sqlite3"`). */
1569
- readonly driver?: string;
1570
- /** Connection-pool tuning (PostgreSQL only). */
1571
- readonly pool?: PoolOptions;
3794
+ declare class TenantScopedRepository<C extends ModelClass> extends BaseRepository<C> {
3795
+ private readonly scope;
1572
3796
  /**
1573
- * Called for every statement a session runs — SQL + bound params. Use for
1574
- * query logging/tracing. Thrown errors are swallowed so it never breaks a query.
3797
+ * Bind a repository to one tenant.
3798
+ *
3799
+ * @param model The model class.
3800
+ * @param session The session to run on.
3801
+ * @param scope The tenant column and id.
3802
+ * @throws Error When the model has no such column — a scope that silently
3803
+ * matches nothing is worse than no scope at all.
1575
3804
  */
1576
- readonly onQuery?: QueryLogger;
3805
+ constructor(model: C, session: AsyncSession, scope: TenantScope);
3806
+ /** The tenant this repository is bound to. */
3807
+ get tenantId(): unknown;
1577
3808
  /**
1578
- * Called for every server-side notice (`CREATE TABLE IF NOT EXISTS` on an
1579
- * existing table, `DROP ... IF EXISTS` on a missing one, and so on).
3809
+ * Merge the tenant predicate into every read.
1580
3810
  *
1581
- * **Without this, notices are silenced.** postgres.js defaults to printing them
1582
- * with `console.log`, which drops a nine-line object into the host service's
1583
- * stdout in the middle of its structured log — on every boot, since a migration
1584
- * runner is usually the first thing to run. Writing to the host's stdout is the
1585
- * application's decision, not a library's, so the default is to say nothing and
1586
- * let you route them:
3811
+ * The caller's filters are **added to**, never replaced: passing another
3812
+ * tenant's id produces a contradiction that matches nothing, which is the safe
3813
+ * outcome.
1587
3814
  *
1588
- * ```ts
1589
- * createEngine(url, { onNotice: (n) => logger.debug({ pg: n }, "postgres notice") });
1590
- * ```
3815
+ * @param filters The caller's filters.
3816
+ * @returns The filters plus the tenant predicate.
3817
+ */
3818
+ protected scopeFilters(filters?: WhereInput<InferModel<C>>): WhereInput<InferModel<C>> | undefined;
3819
+ /**
3820
+ * Stamp the tenant onto every row written.
1591
3821
  *
1592
- * Thrown errors are swallowed, like `onQuery`.
3822
+ * A row that arrives carrying a **different** tenant is refused rather than
3823
+ * overwritten: silently rewriting it would turn a caller's bug into data that
3824
+ * looks deliberate.
3825
+ *
3826
+ * @param data The row being written.
3827
+ * @returns The row with the tenant column set.
3828
+ * @throws Error When the row names another tenant.
1593
3829
  */
1594
- readonly onNotice?: NoticeLogger;
3830
+ protected scopeWrite<T extends Record<string, unknown>>(data: T): T;
3831
+ }
3832
+
3833
+ /** What happened to the row. */
3834
+ type AuditAction = "insert" | "update" | "delete";
3835
+ /** A before/after pair per changed column. */
3836
+ type AuditDiff = Record<string, readonly [unknown, unknown]>;
3837
+ /**
3838
+ * Build the base class for a service's audit-log table.
3839
+ *
3840
+ * @param name The table to store entries in.
3841
+ * @returns A model base carrying the audit columns; extend it to add your own.
3842
+ *
3843
+ * @example
3844
+ * ```ts
3845
+ * class AuditLog extends auditLogModel("audit_log") {}
3846
+ * ```
3847
+ */
3848
+ declare function auditLogModel(name: string): (abstract new () => {
3849
+ /** Monotonic id — also the order the changes happened in. */
3850
+ id: Column<bigint, ColumnFlags & {
3851
+ primaryKey: true;
3852
+ hasDefault: true;
3853
+ }>;
3854
+ /** The audited table. */
3855
+ tableName: Column<string, ColumnFlags & {
3856
+ notNull: true;
3857
+ }>;
3858
+ /** The audited row's primary key, as an object. */
3859
+ rowKey: Column<Record<string, unknown>, ColumnFlags & {
3860
+ notNull: true;
3861
+ }>;
3862
+ /** What happened. */
3863
+ action: Column<"insert" | "update" | "delete", ColumnFlags & {
3864
+ notNull: true;
3865
+ }>;
3866
+ /** Who did it, when the caller could say. */
3867
+ actor: Column<string, ColumnFlags>;
3868
+ /** The changed columns as `[before, after]`; the whole row on insert/delete. */
3869
+ changes: Column<AuditDiff, ColumnFlags & {
3870
+ notNull: true;
3871
+ }>;
3872
+ /** When the entry was written. */
3873
+ at: Column<Date, ColumnFlags & {
3874
+ notNull: true;
3875
+ } & {
3876
+ hasDefault: true;
3877
+ }>;
3878
+ }) & {
3879
+ tablename: string;
3880
+ tableArgs?: () => readonly TableConstraint[];
3881
+ naming?: NamingStrategy;
3882
+ };
3883
+ /** How auditing is wired for one model. */
3884
+ interface AuditOptions<L extends ModelClass> {
3885
+ /** The audit-log model to write entries into. */
3886
+ readonly log: L;
1595
3887
  /**
1596
- * Options passed straight to the underlying driver, applied **last** so they
1597
- * win over everything this layer derives (`pool`, `onNotice`).
3888
+ * Who is making the change.
1598
3889
  *
1599
- * The escape hatch for what the typed surface does not model and is not going
1600
- * to — postgres.js `connection`/`types`/`transform`/`ssl`, mysql2's own
1601
- * settings, `node:sqlite`'s `readOnly` — so a gap need not become a feature
1602
- * request.
3890
+ * Called at write time, so it can read whatever request-scoped context the
3891
+ * service keeps. Return `null` for a change with no human behind it.
1603
3892
  */
1604
- readonly driverOptions?: Readonly<Record<string, unknown>>;
3893
+ readonly actor?: () => string | null;
3894
+ /** Columns never worth recording (a password hash, a big blob). */
3895
+ readonly exclude?: readonly string[];
1605
3896
  }
1606
- /** A synchronous engine (SQLite only). */
1607
- declare class SyncEngine {
1608
- private readonly driver;
1609
- private readonly logger?;
1610
- readonly dialect: Dialect;
1611
- constructor(driver: SyncDriver, logger?: QueryLogger | undefined);
1612
- session(): SyncSession;
1613
- transaction<T>(fn: (tx: SyncSession) => T): T;
1614
- close(): void;
1615
- /** `using engine = createSyncEngine(...)` closes the pool when the scope exits. */
1616
- [Symbol.dispose](): void;
3897
+ /**
3898
+ * The columns of a row, encoded for storage in the log's JSON column.
3899
+ *
3900
+ * @param model The audited model.
3901
+ * @param row The row.
3902
+ * @param exclude Columns to leave out.
3903
+ * @returns The row as JSON-safe values.
3904
+ */
3905
+ declare function snapshot(model: ModelClass, row: Record<string, unknown>, exclude?: readonly string[]): Record<string, unknown>;
3906
+ /**
3907
+ * The columns that differ between two snapshots.
3908
+ *
3909
+ * Compared **after** encoding, so a `Date` and its stored string do not read as a
3910
+ * change; a column missing from one side counts as a change to `null`.
3911
+ *
3912
+ * @param before The earlier snapshot.
3913
+ * @param after The later snapshot.
3914
+ * @returns One `[before, after]` pair per changed column.
3915
+ */
3916
+ declare function diffSnapshots(before: Record<string, unknown>, after: Record<string, unknown>): AuditDiff;
3917
+ /**
3918
+ * Record every create, update and delete of a model into an audit log.
3919
+ *
3920
+ * Wired through the repository signals, so entries are written on the same
3921
+ * session — and therefore inside the same transaction — as the change. A change
3922
+ * that rolls back takes its audit entry with it.
3923
+ *
3924
+ * @param model The model to audit.
3925
+ * @param options The log model, the actor resolver and any excluded columns.
3926
+ * @returns A function that turns auditing off again.
3927
+ *
3928
+ * @example
3929
+ * ```ts
3930
+ * class AuditLog extends auditLogModel("audit_log") {}
3931
+ * enableAudit(Order, { log: AuditLog, actor: () => currentUser()?.id ?? null });
3932
+ * ```
3933
+ */
3934
+ declare function enableAudit<C extends ModelClass, L extends ModelClass>(model: C, options: AuditOptions<L>): () => void;
3935
+ /** The row type of an audit-log model, for the reader's convenience. */
3936
+ type AuditEntry<L extends ModelClass> = InferModel<L>;
3937
+
3938
+ /**
3939
+ * tempest-db-js — backup and restore, driven from the CLI.
3940
+ *
3941
+ * The step every runbook asks for before a migration, wrapped so it is the same
3942
+ * command against both databases in scope. It shells out to the canonical tooling
3943
+ * rather than reimplementing a dump format: `pg_dump`/`pg_restore`/`psql` on
3944
+ * PostgreSQL, and SQLite's own `VACUUM INTO` — which, unlike copying the file, is
3945
+ * consistent while the database is being written to.
3946
+ */
3947
+ /** Raised when the tool a dialect needs is not on the PATH. */
3948
+ declare class BackupToolMissing extends Error {
3949
+ constructor(tool: string);
1617
3950
  }
1618
- /** An asynchronous engine. */
1619
- declare class AsyncEngine {
1620
- private readonly driver;
1621
- readonly dialect: Dialect;
1622
- private readonly logger?;
1623
- constructor(driver: AsyncDriver, dialect: Dialect, logger?: QueryLogger | undefined);
1624
- session(): AsyncSession;
1625
- transaction<T>(fn: (tx: AsyncSession) => Promise<T>): Promise<T>;
1626
- close(): Promise<void>;
1627
- /** `await using engine = createEngine(...)` closes the pool when the scope exits. */
1628
- [Symbol.asyncDispose](): Promise<void>;
3951
+ /** Raised for a dialect that has no backup path here. */
3952
+ declare class UnsupportedBackupBackend extends Error {
3953
+ constructor(dialect: string);
3954
+ }
3955
+ /** Options shared by backup and restore. */
3956
+ interface BackupOptions {
3957
+ /** Overwrite the target if it already exists (restore only). */
3958
+ readonly force?: boolean;
3959
+ /** Extra arguments appended to the underlying tool's command line. */
3960
+ readonly extraArgs?: readonly string[];
3961
+ }
3962
+ /** What a backup or restore did. */
3963
+ interface BackupResult {
3964
+ /** The file written (backup) or read (restore). */
3965
+ readonly file: string;
3966
+ /** The dialect it was taken from. */
3967
+ readonly dialect: string;
3968
+ /** The tool used, or `"VACUUM INTO"` / `"copy"` for SQLite. */
3969
+ readonly via: string;
1629
3970
  }
1630
3971
  /**
1631
- * Adapt a sync **or** async driver to the async interface.
3972
+ * The dump format implied by a file's extension.
1632
3973
  *
1633
- * `await` normalizes both: a sync driver returns a plain value, an async one a
1634
- * promise, and awaiting either yields the result. That is what lets the migration
1635
- * CLI take one code path instead of branching on a difference it cannot detect
1636
- * from the object's shape.
3974
+ * `.dump` is `pg_dump`'s custom format, which `pg_restore` reads selectively and
3975
+ * in parallel; `.sql` is plain text, restored by piping it through `psql`. The
3976
+ * extension decides so the two commands cannot disagree about the file.
1637
3977
  *
1638
- * @param driver Either driver flavor.
1639
- * @returns An async driver delegating to it.
3978
+ * @param file The backup file's path.
3979
+ * @returns Which format it is.
1640
3980
  */
1641
- declare function toAsyncDriver(driver: SyncDriver | AsyncDriver): AsyncDriver;
3981
+ declare function backupFormat(file: string): "custom" | "plain";
1642
3982
  /**
1643
- * Create a **synchronous** engine from a database URL. SQLite only — PostgreSQL
1644
- * has no sane synchronous driver in Node, so a Postgres URL throws, pointing at
1645
- * the async `createEngine`.
3983
+ * The connection URL with the driver suffix removed.
1646
3984
  *
1647
- * @param url A SQLite URL, e.g. `"sqlite:///app.db"` or `"sqlite://:memory:"`.
1648
- * @param options Engine options.
1649
- * @returns A `SyncEngine`.
3985
+ * `postgresql+asyncpg://…` is a URL a Python service wrote; `pg_dump` does not
3986
+ * know that scheme.
3987
+ *
3988
+ * @param url The URL as configured.
3989
+ * @returns The URL a client tool accepts.
1650
3990
  */
1651
- declare function createSyncEngine(url: string, options?: EngineOptions): SyncEngine;
3991
+ declare function toolUrl(url: string): string;
1652
3992
  /**
1653
- * Create an **asynchronous** engine from a database URL (the default). Works for
1654
- * both SQLite (sync driver wrapped as async) and PostgreSQL (postgres.js,
1655
- * lazy-loaded).
3993
+ * Write a backup of the database to a file.
1656
3994
  *
1657
- * @param url A database URL, e.g. `"postgresql://app@localhost/app"` or
1658
- * `"sqlite:///app.db"`.
1659
- * @param options Engine options.
1660
- * @returns An `AsyncEngine`.
3995
+ * @param url The database URL.
3996
+ * @param file Where to write. On PostgreSQL the extension picks the format
3997
+ * (`.sql` plain, anything else custom).
3998
+ * @param options Extra arguments for the underlying tool.
3999
+ * @returns What was done.
4000
+ * @throws BackupToolMissing When PostgreSQL's client tools are absent.
4001
+ * @throws UnsupportedBackupBackend On a dialect without a backup path.
1661
4002
  */
1662
- declare function createEngine(url: string, options?: EngineOptions): AsyncEngine;
4003
+ declare function backupDatabase(url: string, file: string, options?: BackupOptions): Promise<BackupResult>;
4004
+ /**
4005
+ * Restore a database from a backup file.
4006
+ *
4007
+ * @param url The database URL to restore **into**.
4008
+ * @param file The backup file.
4009
+ * @param options `force` to overwrite an existing SQLite file; extra tool args.
4010
+ * @returns What was done.
4011
+ * @throws Error When restoring SQLite over an existing file without `force`.
4012
+ * @throws BackupToolMissing When PostgreSQL's client tools are absent.
4013
+ * @throws UnsupportedBackupBackend On a dialect without a restore path.
4014
+ */
4015
+ declare function restoreDatabase(url: string, file: string, options?: BackupOptions): Promise<BackupResult>;
1663
4016
 
1664
4017
  /**
1665
- * tempest-db-js — Phase 7: typed repository + pagination.
4018
+ * tempest-db-js — table aliases outside the join builder.
1666
4019
  *
1667
- * `BaseRepository<Model>` mirrors the `tempest-fastapi-sdk` repository: a thin,
1668
- * fully-typed CRUD + pagination layer over a model and an async session. The
1669
- * 404-convention is honored — `getById` throws when absent, collection methods
1670
- * return `[]` (never a "not found" error for an empty list).
4020
+ * `join(Model, "a")` has always taken an alias, so a self-join works there. A
4021
+ * plain `select()` had no way to name its table, which is what a correlated
4022
+ * subquery over the **same** table needs: without an alias, the inner and outer
4023
+ * `users` are the same name, and the correlation cannot be written at all.
1671
4024
  */
1672
4025
 
1673
- /** Pagination request — 1-indexed page. */
1674
- interface PaginationFilter<Row> {
1675
- readonly page?: number;
1676
- readonly pageSize?: number;
1677
- readonly orderBy?: keyof Row & string;
1678
- readonly ascending?: boolean;
1679
- readonly filters?: WhereInput<Row>;
1680
- }
1681
- /** A page of results plus metadata (mirrors `BasePaginationSchema`). */
1682
- interface PaginationResult<Row> {
1683
- readonly items: Row[];
1684
- readonly total: number;
1685
- readonly page: number;
1686
- readonly pageSize: number;
1687
- readonly pages: number;
1688
- }
1689
- /** Raised by single-record lookups (`getById`) when nothing matches (404). */
1690
- declare class RecordNotFound extends Error {
1691
- constructor(table: string, id: unknown);
1692
- }
1693
4026
  /**
1694
- * A fully-typed CRUD + pagination repository over a model and an async session.
4027
+ * The same model, reading from an alias.
1695
4028
  *
1696
- * @typeParam C - the model class.
4029
+ * The result is a real model class — same columns, same naming strategy, same
4030
+ * codecs — whose `tablename` is the alias. Everything that already takes a model
4031
+ * takes this: `select`, `join`, `col("alias.column")`, row coercion.
4032
+ *
4033
+ * Table constraints are **not** carried over: an alias is a way to read a table,
4034
+ * not a second declaration of it, and reflecting it into a migration would try to
4035
+ * create a table named after the alias.
4036
+ *
4037
+ * @param model The model to alias.
4038
+ * @param alias The name to read it under.
4039
+ * @returns A model class bound to the alias.
4040
+ *
4041
+ * @example
4042
+ * ```ts
4043
+ * const sub = aliased(User, "sub");
4044
+ * select(User).where(
4045
+ * exists(select(sub).where({ managerId: col("users.id") })),
4046
+ * );
4047
+ * // WHERE EXISTS (SELECT * FROM "users" AS "sub" WHERE "managerId" = "users"."id")
4048
+ * ```
1697
4049
  */
1698
- declare class BaseRepository<C extends ModelClass> {
1699
- protected readonly model: C;
1700
- protected readonly session: AsyncSession;
1701
- private readonly pk;
1702
- constructor(model: C, session: AsyncSession);
1703
- /** All rows matching `filters` (or everything). Empty list when none match. */
1704
- list(filters?: WhereInput<InferModel<C>>): Promise<InferModel<C>[]>;
1705
- /** The first row matching `filters`, or `null`. */
1706
- first(filters?: WhereInput<InferModel<C>>): Promise<InferModel<C> | null>;
1707
- /** A single row by primary key, or `null`. */
1708
- getByIdOrNull(id: unknown): Promise<InferModel<C> | null>;
1709
- /** A single row by primary key; throws `RecordNotFound` when absent. */
1710
- getById(id: unknown): Promise<InferModel<C>>;
1711
- /** Whether any row matches `filters`. */
1712
- exists(filters: WhereInput<InferModel<C>>): Promise<boolean>;
1713
- /** How many rows match `filters` (or the whole table). */
1714
- count(filters?: WhereInput<InferModel<C>>): Promise<number>;
1715
- /** Insert one row, returning the created row. */
1716
- create(data: InferInsert<C>): Promise<InferModel<C>>;
1717
- /** Insert many rows, returning the created rows. */
1718
- createMany(data: readonly InferInsert<C>[]): Promise<InferModel<C>[]>;
1719
- /** Update rows matching `filters`; returns the number of rows affected. */
1720
- update(filters: WhereInput<InferModel<C>>, set: Partial<InferModel<C>>): Promise<number>;
1721
- /** Delete rows matching `filters`; returns the number of rows affected. */
1722
- delete(filters: WhereInput<InferModel<C>>): Promise<number>;
1723
- /**
1724
- * A page of rows plus metadata. `total` counts all matching rows.
1725
- *
1726
- * @param filter Page, size, ordering and filters.
1727
- * @returns The page and pagination metadata.
1728
- */
1729
- paginate(filter?: PaginationFilter<InferModel<C>>): Promise<PaginationResult<InferModel<C>>>;
1730
- }
4050
+ declare function aliased<C extends ModelClass>(model: C, alias: string): C;
4051
+ /**
4052
+ * The model an alias was made from, or `null` when it is not an alias.
4053
+ *
4054
+ * @param model Any model class.
4055
+ * @returns The underlying model, or `null`.
4056
+ */
4057
+ declare function aliasOf(model: ModelClass): ModelClass | null;
1731
4058
 
1732
4059
  /**
1733
4060
  * tempest-db-js — opt-in active-record layer.
@@ -1752,11 +4079,16 @@ declare class ActiveRecord<C extends ModelClass> {
1752
4079
  private readonly session;
1753
4080
  /** The current field values (a plain, typed row). */
1754
4081
  data: InferModel<C>;
1755
- private readonly pk;
4082
+ private readonly pks;
1756
4083
  constructor(model: C, session: AsyncSession,
1757
4084
  /** The current field values (a plain, typed row). */
1758
4085
  data: InferModel<C>);
1759
- /** The primary-key value of the wrapped row. */
4086
+ /**
4087
+ * The primary-key values of the wrapped row, as an object.
4088
+ *
4089
+ * Composite keys are the reason this is an object and not a value: identifying
4090
+ * the row by one column of a two-column key hits the wrong row.
4091
+ */
1760
4092
  private pkValue;
1761
4093
  private pkFilter;
1762
4094
  /**
@@ -1878,6 +4210,7 @@ declare function loadRelations<Row extends Record<string, unknown>, Spec extends
1878
4210
  *
1879
4211
  * This is a SPIKE, not the final API. It validates the type machinery only.
1880
4212
  */
4213
+
1881
4214
  /** Phantom marker carrying the static TS type a column maps to. */
1882
4215
  declare const TYPE: unique symbol;
1883
4216
  /** Column flags that influence the inferred row/insert shape. */
@@ -1931,6 +4264,10 @@ type PortableExpression = "now" | "current_date" | "current_time" | "uuidv4" | {
1931
4264
  readonly raw: string;
1932
4265
  } | {
1933
4266
  readonly parts: readonly string[];
4267
+ }
4268
+ /** The incoming value of a column, inside an upsert's `DO UPDATE` clause. */
4269
+ | {
4270
+ readonly excluded: string;
1934
4271
  };
1935
4272
  /**
1936
4273
  * A column default. Either a constant literal value or a server-side expression
@@ -1966,6 +4303,18 @@ interface SqlExpression {
1966
4303
  /** Parameters bound into the fragment's gaps, in order (empty for a token). */
1967
4304
  readonly params: readonly unknown[];
1968
4305
  }
4306
+ /**
4307
+ * Turn a stored column default (or `onUpdate` value) into something the write
4308
+ * path can render.
4309
+ *
4310
+ * A default is kept as a {@link DefaultValue} — the shape the migration IR wants
4311
+ * — while `set()` and `values()` want either a bindable value or a branded
4312
+ * {@link SqlExpression}. This is the one conversion between the two.
4313
+ *
4314
+ * @param value The stored default.
4315
+ * @returns A literal to bind, or a branded expression to render inline.
4316
+ */
4317
+ declare function defaultAsWriteValue(value: DefaultValue): unknown;
1969
4318
  /**
1970
4319
  * Runtime guard: is this value a branded {@link SqlExpression}?
1971
4320
  *
@@ -1988,6 +4337,18 @@ declare const sql: {
1988
4337
  readonly currentTime: () => SqlExpression;
1989
4338
  /** A freshly generated UUID v4 (`gen_random_uuid()` / portable fallback). */
1990
4339
  readonly uuidv4: () => SqlExpression;
4340
+ /**
4341
+ * The **incoming** value of a column, valid only inside an upsert's
4342
+ * `onConflictDoUpdate` patch.
4343
+ *
4344
+ * A multi-row upsert cannot spell the new value as a literal — each row has its
4345
+ * own — so the assignment has to name the row being inserted:
4346
+ * `EXCLUDED."total"` on PostgreSQL and SQLite, `VALUES(total)` on MySQL.
4347
+ *
4348
+ * @param column The column's property name.
4349
+ * @returns The expression, for use as a write value.
4350
+ */
4351
+ readonly excluded: (column: string) => SqlExpression;
1991
4352
  /**
1992
4353
  * Escape hatch: a verbatim SQL expression rendered as-is, with no parameters.
1993
4354
  *
@@ -2055,6 +4416,8 @@ declare class Column<T, F extends ColumnFlags = ColumnFlags> {
2055
4416
  readonly reference: ForeignKeyRef | null;
2056
4417
  /** An explicit database column name overriding the property name, or `null`. */
2057
4418
  readonly dbName: string | null;
4419
+ /** Conversion to and from the stored representation, or `null` for none. */
4420
+ readonly codec: ColumnCodec | null;
2058
4421
  /** Phantom: never read at runtime, only inspected by the type system. */
2059
4422
  readonly [TYPE]: T;
2060
4423
  constructor(type: ColumnType, flags: F,
@@ -2065,7 +4428,9 @@ declare class Column<T, F extends ColumnFlags = ColumnFlags> {
2065
4428
  /** The foreign-key reference this column points to, or `null` for none. */
2066
4429
  reference?: ForeignKeyRef | null,
2067
4430
  /** An explicit database column name overriding the property name, or `null`. */
2068
- dbName?: string | null);
4431
+ dbName?: string | null,
4432
+ /** Conversion to and from the stored representation, or `null` for none. */
4433
+ codec?: ColumnCodec | null);
2069
4434
  /** Clone this column with one facet replaced, carrying every other over. */
2070
4435
  private derive;
2071
4436
  primaryKey(): Column<T, F & {
@@ -2138,6 +4503,60 @@ declare class Column<T, F extends ColumnFlags = ColumnFlags> {
2138
4503
  */
2139
4504
  onUpdate(value: T | DefaultValue): Column<T, F>;
2140
4505
  }
4506
+ /**
4507
+ * Conversion between a domain value and what the database stores.
4508
+ *
4509
+ * The pair is applied on **both** paths — writes and reads — plus on a `where`
4510
+ * operand, so a custom-typed column cannot be compared against the wrong shape by
4511
+ * accident.
4512
+ */
4513
+ interface ColumnCodec {
4514
+ /** Domain value → stored value. */
4515
+ readonly toDb: (value: unknown) => unknown;
4516
+ /** Stored value → domain value. */
4517
+ readonly fromDb: (value: unknown) => unknown;
4518
+ }
4519
+ /**
4520
+ * Declare a column type of your own, over one this package already has.
4521
+ *
4522
+ * Money as integer cents, a `Temporal.Instant`, a branded id, a value object —
4523
+ * the conversion belongs to the column, not to every call site that forgets it.
4524
+ * The DDL and the migration IR keep using the **base** type, so a custom type is
4525
+ * invisible to the schema and cannot cause drift.
4526
+ *
4527
+ * @param spec `base` is a factory for the underlying column; `toDb`/`fromDb`
4528
+ * convert. `base` is a factory, not a column, because each declaration needs
4529
+ * its own instance.
4530
+ * @returns A factory producing columns of the custom type.
4531
+ *
4532
+ * @example
4533
+ * ```ts
4534
+ * const money = customType<Money, bigint>({
4535
+ * base: () => column.bigInteger(),
4536
+ * toDb: (m) => m.cents,
4537
+ * fromDb: (cents) => Money.fromCents(cents),
4538
+ * });
4539
+ *
4540
+ * class Order extends Model {
4541
+ * total = money().notNull();
4542
+ * }
4543
+ * ```
4544
+ */
4545
+ declare function customType<Domain, Stored>(spec: {
4546
+ base: () => Column<Stored, ColumnFlags>;
4547
+ toDb: (value: Domain) => Stored;
4548
+ fromDb: (value: Stored) => Domain;
4549
+ }): () => Column<Domain, ColumnFlags>;
4550
+ /**
4551
+ * The codecs a model declares, by property name.
4552
+ *
4553
+ * `null` when the model has no custom type, which is the common case — the write
4554
+ * and read paths skip the whole conversion step on that answer.
4555
+ *
4556
+ * @param model The model class.
4557
+ * @returns The codec map, or `null`.
4558
+ */
4559
+ declare function codecsOf(model: ModelClass): Record<string, ColumnCodec> | null;
2141
4560
  /**
2142
4561
  * Column factory mirroring SQLAlchemy's typed column constructors. Each entry
2143
4562
  * pairs a distinct SQL type with the TypeScript type it maps to.
@@ -2233,6 +4652,21 @@ type TableConstraint = {
2233
4652
  readonly kind: "unique";
2234
4653
  readonly name?: string | undefined;
2235
4654
  readonly columns: readonly string[];
4655
+ } | {
4656
+ readonly kind: "check";
4657
+ readonly name?: string | undefined;
4658
+ /** The invariant, in the same condition language `where` uses. */
4659
+ readonly expression: CondNode;
4660
+ /** Columns the expression mentions, for the generated name. */
4661
+ readonly columns: readonly string[];
4662
+ } | {
4663
+ readonly kind: "index";
4664
+ readonly name?: string | undefined;
4665
+ readonly columns: readonly string[];
4666
+ /** A unique index rather than a plain one. */
4667
+ readonly unique?: boolean | undefined;
4668
+ /** The predicate of a **partial** index, or `undefined` for a full one. */
4669
+ readonly where?: CondNode | undefined;
2236
4670
  } | {
2237
4671
  readonly kind: "foreignKey";
2238
4672
  readonly name?: string | undefined;
@@ -2262,6 +4696,55 @@ declare function unique(...columns: string[]): TableConstraint;
2262
4696
  * @returns A foreign-key {@link TableConstraint}.
2263
4697
  * @throws Error When the column arrays are empty or mismatched in length.
2264
4698
  */
4699
+ /**
4700
+ * Declare a `CHECK` constraint — an invariant the **database** enforces.
4701
+ *
4702
+ * The expression uses the same condition language as `where`, not a raw string:
4703
+ * a string would have to be compared textually to decide whether the schema
4704
+ * drifted, and two spellings of the same rule would read as a change.
4705
+ *
4706
+ * @param expression The invariant, e.g. `col("total").gte(0)`.
4707
+ * @param options `name` to pin the constraint's name; `columns` to name the
4708
+ * generated one after something other than the expression's own columns.
4709
+ * @returns A check {@link TableConstraint}.
4710
+ *
4711
+ * @example
4712
+ * ```ts
4713
+ * class Order extends Model {
4714
+ * static override tableArgs = () => [check(col("total").gte(0))];
4715
+ * }
4716
+ * ```
4717
+ */
4718
+ declare function check(expression: Condition | Record<string, unknown>, options?: {
4719
+ name?: string;
4720
+ columns?: readonly string[];
4721
+ }): TableConstraint;
4722
+ /**
4723
+ * Declare an index.
4724
+ *
4725
+ * An index the model does not declare is invisible to migrations: it never gets
4726
+ * created, and a table rebuild on SQLite drops it. Declaring it puts it in the
4727
+ * same place as the columns it covers.
4728
+ *
4729
+ * @param columns The indexed columns, in order.
4730
+ * @param options `unique` for a unique index, `where` for a **partial** one, and
4731
+ * `name` to pin the name.
4732
+ * @returns An index {@link TableConstraint}.
4733
+ * @throws Error When no column is given.
4734
+ *
4735
+ * @example
4736
+ * ```ts
4737
+ * static override tableArgs = () => [
4738
+ * index(["customerId", "createdAt"]),
4739
+ * index(["email"], { unique: true, where: { deletedAt: { isNull: true } } }),
4740
+ * ];
4741
+ * ```
4742
+ */
4743
+ declare function index(columns: readonly string[], options?: {
4744
+ name?: string;
4745
+ unique?: boolean;
4746
+ where?: Condition | Record<string, unknown>;
4747
+ }): TableConstraint;
2265
4748
  declare function foreignKey(columns: string[], refTable: string, refColumns: string[], options?: {
2266
4749
  name?: string;
2267
4750
  onDelete?: FkAction;
@@ -2333,6 +4816,32 @@ declare function columnNamesOf(model: ModelClass): NameMap | null;
2333
4816
  declare function columnPropsOf(model: ModelClass): NameMap | null;
2334
4817
  /** Resolve one property name to its database column name. */
2335
4818
  declare function dbColumn(names: NameMap | null | undefined, prop: string): string;
4819
+ /**
4820
+ * Every primary-key column of a model, in declaration order.
4821
+ *
4822
+ * A composite key is a list of more than one name — which is why this returns an
4823
+ * array and not a single name. Reading only the first entry is how a repository
4824
+ * ends up filtering half a key and touching the wrong row.
4825
+ *
4826
+ * @param model The model class.
4827
+ * @returns The primary-key column names (property names, not database names).
4828
+ * @throws Error When the model declares no primary key.
4829
+ */
4830
+ declare function primaryKeysOf(model: ModelClass): string[];
4831
+ /**
4832
+ * Turn a primary-key argument into the filter that identifies exactly one row.
4833
+ *
4834
+ * A single-column key takes the bare value (or an object carrying it); a
4835
+ * composite key **requires** the object, because a scalar cannot say which of the
4836
+ * key columns it is. Passing a scalar for a composite key throws instead of
4837
+ * silently matching on one column.
4838
+ *
4839
+ * @param model The model class.
4840
+ * @param id The key: a scalar, or an object with every key column.
4841
+ * @returns A filter object covering the whole key.
4842
+ * @throws Error When the key is incomplete, or a scalar was given for a composite key.
4843
+ */
4844
+ declare function primaryKeyFilter(model: ModelClass, id: unknown): Record<string, unknown>;
2336
4845
  /** Pull the static type out of a Column. */
2337
4846
  type ColType<C> = C extends Column<infer T, infer _F> ? T : never;
2338
4847
  /** Keys of the model instance whose values are Columns. */
@@ -2396,4 +4905,4 @@ type InferInsert<C extends ModelClass> = Simplify<{
2396
4905
  [K in Exclude<ColumnKeys<InstanceType<C>>, OptionalInsertKeys<InstanceType<C>>>]: ColValue<InstanceType<C>[K]>;
2397
4906
  }>;
2398
4907
 
2399
- export { ActiveRecord, type ActiveRecordManager, Agg, type AggregateTerm, type AsyncDriver, AsyncEngine, AsyncResult, AsyncSession, BaseDialect, BaseRepository, type BelongsTo, type ColRef, type ColType, Column, type ColumnFlags, type ColumnType, type ColumnTypeKind, type ColumnTypeMeta, type CompiledQuery, type CondNode, type Condition, type DefaultValue, DeleteBuilder, type DeleteNode, type Dialect, type DriverResult, type EngineOptions, type Executable, type ExprNode, Expression, type FkAction, type ForeignKeyOptions, type ForeignKeyRef, type HasMany, type InferInsert, type InferModel, InsertBuilder, type InsertNode, InvalidDatabaseUrl, JoinBuilder, type JoinClause, type JoinNode, type JoinOn, type JoinSelection, type JoinWhereInput, type LockClause, type LockOptions, Model, type ModelClass, MysqlDialect, type NameMap, type NamingStrategy, NoResultError, NodeSqliteDriver, type NoticeLogger, OPERATORS, type OnConflict, type OnConflictOptions, type OnConflictUpdateOptions, type Operator, type OperatorsFor, type OrderTerm, type PaginationFilter, type PaginationResult, Params, type ParsedDatabaseUrl, type PoolOptions, type PortableExpression, PostgresDialect, QueryExecutionError, type QueryLogger, type QueryNode, RecordNotFound, type Relation, type RelationValue, type ReservedAsyncDriver, type Returning, type RowOf, SelectBuilder, type SelectNode, type SortDirection, type Sources, type SqlExpression, SqliteDialect, type Subquery, type SyncDriver, SyncEngine, SyncResult, SyncSession, type TableConstraint, UpdateBuilder, type UpdateNode, ValidationError, type WhereArg, type WhereInput, type WithRelations, type WritePatch, type WriteValues, activeRecord, and, avg, belongsTo, col, column, columnNamesOf, columnPropsOf, columnsOf, count, createEngine, createSyncEngine, dbColumn, del, detectDialect, fn, foreignKey, fromDict, getDialect, hasMany, insert, isCondition, isExpression, isSqlExpression, isSubquery, join, loadRelations, max, min, not, or, parse, parseDatabaseUrl, select, sql, stringify, sum, toAsyncDriver, toCondNode, toDict, toJSON, toSnakeCase, unique, update, val };
4908
+ export { ActiveRecord, type ActiveRecordManager, Agg, type AggregateTerm, type AsyncDriver, AsyncEngine, AsyncResult, AsyncSession, type AuditAction, type AuditDiff, type AuditEntry, type AuditOptions, type BackupOptions, type BackupResult, BackupToolMissing, BaseDialect, BaseRepository, type BelongsTo, BetterSqliteDriver, type BulkUpsertOptions, type CastType, type ChangesPage, type ChangesSinceFilter, type ClaimOptions, type ColRef, type ColType, Column, type ColumnCodec, type ColumnFlags, type ColumnType, type ColumnTypeKind, type ColumnTypeMeta, type CompiledQuery, type CondNode, type Condition, type ContainsOptions, Cte, type CteBody, type CteNode, type CteOptions, type CursorPage, type CursorPaginationFilter, type DefaultValue, DeleteBuilder, type DeleteNode, type Dialect, type DriverResult, type EngineOptions, type Executable, type ExplainOptions, type ExplainReport, type ExprNode, Expression, type FailOptions, type FkAction, type FlushResult, type ForeignKeyOptions, type ForeignKeyRef, type HasMany, type InferInsert, type InferModel, InsertBuilder, type InsertNode, type IntegrityFailure, type IntegrityViolation, InvalidCursor, InvalidDatabaseUrl, type IsolationLevel, JoinBuilder, type JoinClause, type JoinNode, type JoinOn, type JoinSelection, type JoinWhereInput, type LockClause, type LockOptions, Model, type ModelBase, type ModelClass, MysqlDialect, type NameMap, type NamingStrategy, NoResultError, NodeSqliteDriver, type NoticeLogger, OPERATORS, type OnConflict, type OnConflictOptions, type OnConflictUpdateOptions, type Operator, type OperatorsFor, type OrderTerm, type OutboxEventInput, OutboxRepository, type OutboxStatus, type PaginationFilter, type PaginationResult, Params, type ParsedDatabaseUrl, type PoolOptions, type PortableExpression, PostgresDialect, type QueryEndEvent, type QueryEndLogger, QueryExecutionError, type QueryHooks, type QueryLogger, type QueryNode, type QueryPlan, RecordNotFound, type Relation, type RelationValue, type RepositorySignal, type ReservedAsyncDriver, type Returning, type RowOf, SelectBuilder, type SelectNode, SetBuilder, type SetNode, type SetOperator, type SignalHandler, type SignalPayload, type SortDirection, type Sources, type SqlExpression, SqliteDialect, type SqliteJournalMode, type SqliteOptions, type SqliteSynchronous, type Subquery, type SubqueryLike, type SyncDriver, SyncEngine, SyncResult, SyncSession, type TableConstraint, type TenantScope, TenantScopedRepository, type TextSearchLanguage, type TextSearchOptions, type Tracked, type TransactionOptions, UnitOfWork, UnsupportedBackupBackend, UpdateBuilder, type UpdateNode, ValidationError, type WhereArg, type WhereInput, type WindowFn, type WindowSpec, type WithRelations, type WritePatch, type WriteValues, activeRecord, aliasOf, aliased, and, attach as attachCte, auditLogModel, avg, backupDatabase, backupFormat, belongsTo, caseWhen, cast, check, clearSignals, codecsOf, col, column, columnNamesOf, columnPropsOf, columnsOf, contains, count, createEngine, createSyncEngine, cte, cteRecursive, customType, dbColumn, decodeValue as decodeColumnValue, defaultAsWriteValue, del, denseRank, detectDialect, diffSnapshots, emitSignal, enableAudit, encodeValue as encodeColumnValue, escapeLike, except, exists, firstValue, fn, foreignKey, fromDict, fullText, fullTextRank, getDialect, hasHandlers, hasMany, index, insert, intersect, isCondition, isExpression, isReadOnlyStatement, isSqlExpression, isSubquery, join, lag, lastValue, lead, loadRelations, max, min, not, notDeleted, notExists, onSignal, onlyDeleted, or, outboxModel, over, parse, parseDatabaseUrl, parseIntegrityError, percentRank, primaryKeyFilter, primaryKeysOf, rank, restoreDatabase, rowNumber, scalar, select, snapshot, sql, stringify, sum, summarizePlan, toAsyncDriver, toCondNode, toDict, toJSON, toSnakeCase, tokenize, toolUrl, union, unionAll, unique, update, val, withAudit, withSoftDelete, withTimestamps };