@vibeorm/sql 4.2.0 → 4.3.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/ast.d.ts CHANGED
@@ -10,7 +10,7 @@ import type { RlsReadinessQuery } from "./dialects/postgres-rls-readiness.ts";
10
10
  * grows with the runtime in P2/P3. Every shape added here needs a catalog case
11
11
  * (tests/catalog/cases.ts) in the same change.
12
12
  */
13
- import type { AdvisoryLockMode, RlsContextIR, RowLockStrength, RowLockWaitPolicy, SchemaIR, SqlFragmentSpec } from "@vibeorm/schema";
13
+ import type { AdvisoryLockMode, RlsContextIR, RowLockStrength, RowLockWaitPolicy, ScalarType, SchemaIR, SqlFragmentSpec } from "@vibeorm/schema";
14
14
  /** A column reference, optionally table-qualified. */
15
15
  export type ColRef = {
16
16
  readonly table?: string;
@@ -88,6 +88,33 @@ export type AggSelect = {
88
88
  */
89
89
  export type JsonFilterOp = "equals" | "string_contains" | "string_starts_with" | "string_ends_with" | "array_contains" | "array_starts_with" | "array_ends_with";
90
90
  export type ComparisonOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte";
91
+ /**
92
+ * The column facts one member of a {@link SqlExpr} `keyList` tuple needs to
93
+ * bind exactly like the equality `"col" = $n` would. Postgres binds each
94
+ * member as ONE typed array (`$n::<element type>[]`), so it needs the
95
+ * column's element type: the declared native type without its modifier
96
+ * (`timestamptz`, never `timestamptz(3)` — a typmod cast would round
97
+ * `.1234` onto a stored `.123` that `=` does not match), `bpchar` for
98
+ * `char(n)`, `citext`, the enum's own type. MySQL needs to know a time-of-day
99
+ * member (`SqlDialect.timeOfDayInStrategy`); SQLite needs nothing.
100
+ */
101
+ export type KeyListMember = {
102
+ /** The column's scalar type; `"Enum"` for an enum column. */
103
+ readonly type: ScalarType | "Enum";
104
+ /** The declared `@db.*` native type, verbatim (`VarChar(191)`, `Uuid`, `Timestamptz(3)`). */
105
+ readonly nativeType?: string;
106
+ /**
107
+ * A database-stored (native) enum's type: `namespace` qualifies it, absence
108
+ * resolves it through the search path like the statement's table. Absent on
109
+ * a string-stored enum, whose column is text.
110
+ */
111
+ readonly enumType?: {
112
+ readonly namespace?: string;
113
+ readonly name: string;
114
+ };
115
+ /** A time-of-day column (`@db.Time(n)` / `@db.Timetz(n)`). */
116
+ readonly timeOfDay?: boolean;
117
+ };
91
118
  export type SqlExpr = {
92
119
  readonly kind: "composed";
93
120
  readonly fragment: SqlFragment;
@@ -133,6 +160,68 @@ export type SqlExpr = {
133
160
  /** Candidate tuples — every row carries exactly `columns.length` values. */
134
161
  readonly rows: readonly (readonly unknown[])[];
135
162
  readonly negate?: boolean;
163
+ }
164
+ /**
165
+ * KEY-LIST membership — the public `where: { IN: [{ a, b }, …] }` filter:
166
+ * rows whose ordered tuple of column values equals one of `rows`, with
167
+ * semi-join semantics (a repeated tuple never repeats a result row). Renders
168
+ * per `SqlDialect.keyListStrategy`: postgres binds one typed ARRAY per
169
+ * member, `("a", "b") IN (SELECT * FROM unnest($1::int8[], $2::text[]))`,
170
+ * so the text and the planning cost stay constant however long the list is
171
+ * (an OR of 1 000 conjunctions planned in 114 ms on PGlite, 2 ms on
172
+ * postgres 18; the unnest form in 0.1–0.2 ms on both). A `DateTime` member
173
+ * without a declared native type switches postgres to the row-constructor
174
+ * list of untyped parameters (`keyListBindsUntypedRows`: the column may be a
175
+ * zone-less `timestamp`). MySQL renders the row-constructor list, SQLite
176
+ * `IN (VALUES …)`. Each member compares under the column's own type and
177
+ * collation, exactly like `"col" = $n`.
178
+ *
179
+ * An empty `rows` renders `1 = 0` (matches nothing, like an empty
180
+ * `inArray`). The runtime emits arity 1 as `inArray` instead, so a
181
+ * one-field key list keeps the scalar `in` SQL.
182
+ */
183
+ | {
184
+ readonly kind: "keyList";
185
+ /** Ordered tuple columns (compare references); `rows[i][j]` binds to `columns[j]`. */
186
+ readonly columns: readonly ColRef[];
187
+ /** How each member binds, in column order — see {@link KeyListMember}. */
188
+ readonly members: readonly KeyListMember[];
189
+ /** Encoded candidate tuples, each exactly `columns.length` long; may repeat. */
190
+ readonly rows: readonly (readonly unknown[])[];
191
+ }
192
+ /**
193
+ * ROW-VALUE comparison: `("a", "b") > ($1, $2)` — the keyset seek (D-01).
194
+ * SQL compares row values left to right, each pair with that column's own
195
+ * operator, affinity and collation, which is exactly the OR expansion
196
+ * `"a" > $1 OR ("a" = $2 AND "b" > $3)` when no member is NULL — and an
197
+ * index on `(a, b)` bounds it as one range (PostgreSQL `Index Cond: ROW(…) >
198
+ * ROW(…)`, SQLite `SEARCH … (a>?)`). A NULL member makes the comparison
199
+ * UNKNOWN as soon as the scan reaches it, so the runtime emits it only over
200
+ * non-null keys and values. Renders on every dialect (each has row
201
+ * constructors); whether the runtime emits it is
202
+ * `SqlDialect.keysetSeekStrategy`.
203
+ */
204
+ | {
205
+ readonly kind: "rowCompare";
206
+ readonly op: "gt" | "gte" | "lt" | "lte";
207
+ /** Ordered row members (compare references); `values[i]` binds to `columns[i]`. */
208
+ readonly columns: readonly ColRef[];
209
+ /** Encoded values, exactly `columns.length` of them. */
210
+ readonly values: readonly unknown[];
211
+ }
212
+ /**
213
+ * SUBQUERY membership: `"id" IN (SELECT …)`, or the row-value form
214
+ * `("a", "b") IN (SELECT …)` for a composite key — the key-first `distinct`
215
+ * page on PostgreSQL (D-04). The subquery projects exactly `columns.length`
216
+ * columns and shares the statement's parameter sequence. MySQL refuses a
217
+ * `LIMIT` directly inside an `IN` subquery (error 1235), so the renderer
218
+ * refuses that pair there with `VIBE_UNSUPPORTED_CAPABILITY`.
219
+ */
220
+ | {
221
+ readonly kind: "inSelect";
222
+ /** The tested columns, in the subquery's projection order. */
223
+ readonly columns: readonly ColRef[];
224
+ readonly select: SelectStatement;
136
225
  } | {
137
226
  readonly kind: "like";
138
227
  readonly column: ColRef;
@@ -435,6 +524,37 @@ export type InnerJoin = {
435
524
  readonly right: ColRef;
436
525
  }[];
437
526
  };
527
+ /**
528
+ * One `LEFT JOIN (<select>) AS "alias" ON "alias"."k" = "outer"."c" [AND …]`
529
+ * clause — portable, equality ON pairs only. Built for the to-one relation
530
+ * `orderBy` (`orderBy: { author: { email: "asc" } }`): the target's key and
531
+ * sort columns ride a derived table under RESERVED aliases (`__vibe_ok0`,
532
+ * `__vibe_ov0`), so the outer statement's unqualified column names can never
533
+ * become ambiguous, and the outer ORDER BY names `"alias"."__vibe_ov0"`.
534
+ * Every engine merges such a derived table into a plain join (measured:
535
+ * postgres pulls it up into a hash/nested-loop left join, sqlite flattens it,
536
+ * mysql merges it), so the planner may hash-join instead of evaluating a
537
+ * correlated subquery per candidate row.
538
+ *
539
+ * The builder emits it only for links that match AT MOST ONE target row, so
540
+ * the join keeps the parent row count. The derived select must alias every
541
+ * projected column; its parameters (none today) join the statement's single
542
+ * sequence where the clause renders (after `keyTable`, before
543
+ * `lateralJoins`). A statement with a `rowLock` refuses it: postgres rejects
544
+ * FOR UPDATE on the nullable side of an outer join, and mysql would lock the
545
+ * joined rows too.
546
+ */
547
+ export type LeftJoin = {
548
+ /** Derived-table alias (`__ord1`). */
549
+ readonly alias: string;
550
+ /** The derived table — every projected column carries an `alias`. */
551
+ readonly select: SelectStatement;
552
+ /** Equality pairs ANDed into the ON clause; never empty. */
553
+ readonly on: readonly {
554
+ readonly left: ColRef;
555
+ readonly right: ColRef;
556
+ }[];
557
+ };
438
558
  /**
439
559
  * How one member of a {@link KeyTableJoin} tuple binds inside the derived
440
560
  * table. `value` binds the parameter as it is: it stays COERCIBLE, so a text
@@ -523,6 +643,57 @@ export type RowNumberProjection = {
523
643
  readonly orderBy: readonly OrderTerm[];
524
644
  readonly alias: string;
525
645
  };
646
+ /**
647
+ * A PER-KEY PAGE of one child table as a derived-table FROM
648
+ * (`SelectStatement.keyPage`): for every listed key tuple, the child rows
649
+ * whose key columns equal it, ordered by `select.orderBy` and cut by
650
+ * `select.limit`/`select.offset` — each key's OWN page, plus a rank column
651
+ * (`rankAlias`) that orders the rows inside their key. The paged include's
652
+ * `ROW_NUMBER() OVER (PARTITION BY key …)` window returns the same rows but
653
+ * numbers EVERY child of every listed key first; this form reads one page of
654
+ * primary keys per key through an index on (key columns, order columns),
655
+ * then each page row once by its primary key.
656
+ *
657
+ * Rendered per `SqlDialect.keyPageStrategy`:
658
+ * - `lateral` (postgres): `unnest($1::<type>[], …) AS "__vibe_keys"
659
+ * ("__vibe_k0", …) CROSS JOIN LATERAL (SELECT <identity>, ROW_NUMBER() OVER
660
+ * (PARTITION BY <key> ORDER BY <order>) AS <rank> FROM <table> WHERE <key> =
661
+ * "__vibe_keys"."__vibe_k<i>" … ORDER BY <order> LIMIT … OFFSET …) AS
662
+ * "__vibe_ids" INNER JOIN <table> AS "__vibe_page" ON <identity> pairs`.
663
+ * - `inLimit` (sqlite, which has no LATERAL): the key tuples as `(VALUES …)`
664
+ * (columns `column1`, `column2`, …, as SQLite names a VALUES clause's
665
+ * columns) joined to `<table> AS "__vibe_page"` ON `(<identity>) IN (SELECT
666
+ * <identity> … WHERE <key> = "__vibe_keys"."column<i>" … ORDER BY … LIMIT …)`,
667
+ * the rank re-derived over the fetched page (`ROW_NUMBER() OVER (PARTITION
668
+ * BY <key> ORDER BY <order>)`): its values differ from the window's, its
669
+ * order inside a key does not.
670
+ * - `window` (mysql): refused with `VIBE_UNSUPPORTED_CAPABILITY` — measured
671
+ * no gain there; the runtime keeps the window.
672
+ *
673
+ * The caller guarantees what makes the page equal to the window's: the order
674
+ * is total inside a key (the identity columns are among the key and order
675
+ * columns), the key tuples are distinct and compare exactly (no folding
676
+ * collation). Members bind like the key-list filter's (`KeyListMember`).
677
+ */
678
+ export type KeyPage = {
679
+ /** How each key member binds, in member order (postgres: one typed array each). */
680
+ readonly members: readonly KeyListMember[];
681
+ /** Encoded, distinct key tuples; never empty, each `members.length` long. */
682
+ readonly rows: readonly (readonly unknown[])[];
683
+ /** The child columns each key member equals, in member order (unqualified). */
684
+ readonly keyColumns: readonly ColRef[];
685
+ /** The child's primary-key columns (unqualified) — each page row is read once by them. */
686
+ readonly identity: readonly ColRef[];
687
+ /**
688
+ * ONE key's page: the child `table` (no alias), the projected `columns`
689
+ * (unqualified; transforms and aliases render as usual), an optional extra
690
+ * `where`, the non-empty `orderBy` (plain column terms), `limit`/`offset`.
691
+ * Any other member is refused.
692
+ */
693
+ readonly select: SelectStatement;
694
+ /** Output name of the rank column. */
695
+ readonly rankAlias: string;
696
+ };
526
697
  export type SelectStatement = {
527
698
  readonly kind: "select";
528
699
  readonly table: TableRef;
@@ -548,6 +719,14 @@ export type SelectStatement = {
548
719
  * guarantees that arrangement.
549
720
  */
550
721
  readonly distinctOn?: readonly ColRef[];
722
+ /**
723
+ * Plain `SELECT DISTINCT` over the whole projection — every dialect. The
724
+ * query builder sets it only where it returns exactly the rows its
725
+ * `ROW_NUMBER()` distinct emulation would (sqlite/mysql, value-exact keys,
726
+ * projection equal to the distinct fields); never together with
727
+ * `distinctOn` (the renderer refuses the pair).
728
+ */
729
+ readonly distinct?: true;
551
730
  /**
552
731
  * Derived-table FROM: renders `FROM (<fromSelect>) AS "<table.name>"` —
553
732
  * `table.name` is the subquery ALIAS and `table.alias` must be unset. Used
@@ -560,8 +739,29 @@ export type SelectStatement = {
560
739
  readonly innerJoins?: readonly InnerJoin[];
561
740
  /** Key-table join, rendered after `innerJoins` — mysql only, see {@link KeyTableJoin}. */
562
741
  readonly keyTable?: KeyTableJoin;
742
+ /** Fix lane Q2 (D-03): derived-table LEFT JOINs, rendered after `keyTable` — see {@link LeftJoin}. */
743
+ readonly leftJoins?: readonly LeftJoin[];
563
744
  /** ROW_NUMBER window projection — see {@link RowNumberProjection}. */
564
745
  readonly rowNumber?: RowNumberProjection;
746
+ /**
747
+ * Fix lane Q1 (DB-01): derived-table FROM over a per-key page — renders
748
+ * `FROM (<per-key page>) AS "<table.name>"` like `fromSelect` (and never
749
+ * together with it); see {@link KeyPage}.
750
+ */
751
+ readonly keyPage?: KeyPage;
752
+ /**
753
+ * Fix lane Q2 (round 3, DB-07): derived-table FROM over a `UNION ALL` of
754
+ * branch selects — renders `FROM (SELECT * FROM (<branch 0>) AS
755
+ * "__vibe_u0" UNION ALL SELECT * FROM (<branch 1>) AS "__vibe_u1" …) AS
756
+ * "<table.name>"`. Each branch keeps its own ORDER BY and LIMIT: wrapped in
757
+ * its own derived table, a limited branch is valid on all three engines
758
+ * (SQLite refuses ORDER BY/LIMIT on a bare compound member). At least two
759
+ * branches; each a plain `SELECT <columns> FROM <table> [WHERE] [ORDER BY]
760
+ * [LIMIT]` projecting the same output names in the same order; never
761
+ * together with `fromSelect`, `keyPage` or a `table.alias`. Parameters bind
762
+ * branch by branch, in text order.
763
+ */
764
+ readonly fromUnion?: readonly SelectStatement[];
565
765
  readonly where?: SqlExpr;
566
766
  readonly groupBy?: readonly ColRef[];
567
767
  /** HAVING predicate — grouped selects; may contain `aggCmp` expressions. */
@@ -674,6 +874,26 @@ export type InsertStatement = {
674
874
  readonly onConflict?: OnConflictClause;
675
875
  /** Rendered as RETURNING where the dialect supports it, else omitted+flagged. */
676
876
  readonly returning?: readonly ReturningColumn[];
877
+ /**
878
+ * Bind the rows as ONE typed array per column instead of one parameter per
879
+ * value (`SqlDialect.bulkInsertForm` `"unnest"`, postgres): `INSERT INTO t
880
+ * ("a", "b") SELECT "c1", "c2" FROM unnest($1::text[], $2::int4[]) WITH
881
+ * ORDINALITY AS "__vibe_rows" ("c1", "c2", "ord") ORDER BY "ord"`. The text
882
+ * is the same for 2 rows and for 3 000, and the statement binds one parameter
883
+ * per column. `ORDER BY "ord"` keeps input order (sequence values, the
884
+ * `ON CONFLICT DO NOTHING` winner).
885
+ *
886
+ * One entry per column, in `columns` order: the column's DECLARED type facts
887
+ * (see {@link KeyListMember}; a `Json` entry names its storage through
888
+ * `nativeType`). Each array binds the BASE type (`varchar`, never
889
+ * `varchar(191)` — an explicit modifier cast truncates silently), so the
890
+ * INSERT's own assignment applies the column's length and precision exactly
891
+ * as it does for a VALUES parameter. `rows` stays row-major: the renderer
892
+ * transposes it. Every cell is a plain value, SQL NULL or a
893
+ * {@link JsonParamCell} — never `DEFAULT` or the database clock (the query
894
+ * builder keeps VALUES for those; the renderer refuses them).
895
+ */
896
+ readonly unnest?: readonly KeyListMember[];
677
897
  };
678
898
  /** Conflicting rows are updated with these column values. */
679
899
  export type OnConflictUpdate = {
package/dist/codecs.d.ts CHANGED
@@ -122,6 +122,8 @@ export declare function parseDateTimeText(params: {
122
122
  export declare function timeOfDayCodec(params: {
123
123
  nativeType: string | undefined;
124
124
  }): ScalarCodec | undefined;
125
+ /** Canonical calendar-day text only; a driver Date cannot recover the authored day. */
126
+ export declare const localDateCodec: ScalarCodec;
125
127
  /**
126
128
  * Parse stored JSON text at the centralized codec boundary. A supplied context
127
129
  * marks a bound field: its error contains only that context, never the document
package/dist/dialect.d.ts CHANGED
@@ -121,10 +121,101 @@ export type SqlDialect = {
121
121
  * member, so composite membership always chunks against `paramBudget`.
122
122
  */
123
123
  readonly tupleInStrategy: "rowValue" | "expand";
124
+ /**
125
+ * Key-list membership (`SqlExpr` `keyList`, the public `where: { IN }`).
126
+ * `unnest` binds ONE typed array per member — `("a", "b") IN (SELECT * FROM
127
+ * unnest($1::int8[], $2::text[]))` — so the text and its planning cost do
128
+ * not grow with the list (postgres 18.6, 1 000 two-column keys: planning
129
+ * 0.09 ms against 2.0 ms for the OR of conjunctions; 5 000 keys: 0.09 ms
130
+ * against 12.6 ms, and 10 ms against 285 ms execution). `rowValue` renders
131
+ * the row-constructor list `(a, b) IN ((?, ?), …)` (mysql: 3.9 ms for 1 000
132
+ * keys through an index range scan, equal to a VALUES derived table and
133
+ * without its numeric casts; every probed type compared exactly like `=`).
134
+ * `values` renders `("a", "b") IN (VALUES (?, ?), …)` (sqlite 3.51: 0.9 ms
135
+ * prepare + run for 1 000 keys against 1.9 ms for the row list; an OR of
136
+ * 1 000 conjunctions is refused there, "Expression tree is too large").
137
+ * The row forms bind one parameter per member, like scalar `IN (?, …)`.
138
+ */
139
+ readonly keyListStrategy: "unnest" | "rowValue" | "values";
140
+ /**
141
+ * The keyset seek (`after`, `findPage`, a compound `cursor`, graph pages;
142
+ * D-01). `rowValue`: where every key sorts in one direction and no key or
143
+ * position value is NULL, one row-value comparison `("a", "b") > ($1, $2)`
144
+ * (`SqlExpr` `rowCompare`) that an index on `(a, b)` bounds as a range;
145
+ * with mixed directions the OR expansion ANDed with the inclusive bound on
146
+ * the leading key (`"a" >= $1 AND (…)`). Measured: PostgreSQL 18.6 at
147
+ * 500 000 rows deep 25.4 ms → 0.013 ms, SQLite 3.51 at 490 000 rows 12.9 →
148
+ * 0.007 ms — the OR alone is a full index walk from the first row. `or`:
149
+ * the OR expansion alone — MySQL's range optimizer already turns it into
150
+ * index ranges (22 rows touched at any depth) and SCANS a row-value
151
+ * inequality (194 ms at 490 000 rows deep). Nullable keys keep the OR on
152
+ * every dialect.
153
+ */
154
+ readonly keysetSeekStrategy: "rowValue" | "or";
155
+ /**
156
+ * Fix lane Q1 (round 3, DB-01): how a paged include (`include: { rel: {
157
+ * orderBy, take, skip } }`) cuts each parent's page when an index proves the
158
+ * per-key form (`SelectStatement.keyPage`, `KeyPage`). `lateral`: one
159
+ * primary-key page per key in a `CROSS JOIN LATERAL` over the unnested key
160
+ * arrays — PostgreSQL 18.6, 1 000 000 children, 50 parents: 59.1 → 1.08 ms
161
+ * (the window sorted every child of every parent); the key-only page keeps
162
+ * an index-only scan the planner prefers at every `skip`, where a lateral
163
+ * fetching whole rows flipped to bitmap + sort and LOST (40 → 64 ms).
164
+ * `inLimit`: SQLite has no LATERAL — `(VALUES …)` keys joined to the child
165
+ * by `IN (SELECT key … LIMIT)`, a correlated list subquery per key (SQLite
166
+ * 3.51, 500 000 children, 50 parents: 75.2 → 0.47 ms). `window`: keep the
167
+ * `ROW_NUMBER()` window (MySQL: a lateral page still read every child).
168
+ */
169
+ readonly keyPageStrategy: "lateral" | "inLimit" | "window";
170
+ /**
171
+ * Fix lane Q1 (round 3, DB-03): how a positive relation filter that pins ONE
172
+ * related row by a unique key renders. `exists`: the correlated `EXISTS` —
173
+ * PostgreSQL and MySQL turn it into a semi-join. `inSelect`: an uncorrelated
174
+ * `key IN (SELECT key …)` — SQLite runs a correlated `EXISTS` once per outer
175
+ * row (`post where author.email = x`: 134 → 0.15 ms, Post 500 000).
176
+ */
177
+ readonly relationFilterStrategy: "exists" | "inSelect";
178
+ /**
179
+ * DB-02: when a find with a TOTAL order (it ends with the full primary key)
180
+ * pages with `skip` ≥ `minSkip`, the runtime cuts the page as primary keys
181
+ * in a derived table (`SELECT pk FROM t WHERE … ORDER BY … LIMIT ? OFFSET ?`)
182
+ * and joins the rows back by primary key (`fromSelect` + `innerJoins`; MySQL
183
+ * refuses LIMIT inside `IN (subquery)`). MySQL 26.7 / 8.4 drop the index from
184
+ * skip ≈ 5 000 and sort the table (500 000 rows, skip 5 000: 116.6 → 1.21 ms;
185
+ * 100 000: 144 → 9.3 ms) — there it applies under that rule alone.
186
+ * PostgreSQL gains only when the key page is an index-only scan (`authorId =
187
+ * 1`, skip 4 000: 4.37 → 0.24 ms) and loses up to 15 % when the filter is not
188
+ * covered, so `requiresCoveringIndex` asks for one declared index that serves
189
+ * the filter, the order and the key. `null`: never (SQLite walks an index
190
+ * plus rowid lookups cheaply — skip 100 000: 1.02 ms).
191
+ */
192
+ readonly deferredJoinPage: {
193
+ readonly minSkip: number;
194
+ readonly requiresCoveringIndex: boolean;
195
+ } | null;
196
+ /**
197
+ * DB-07: where this engine sorts NULL when ORDER BY names no placement —
198
+ * `high`: above every value (postgres: last on ASC, first on DESC), `low`:
199
+ * below (sqlite, mysql: first on ASC, last on DESC). A b-tree stores NULLs in
200
+ * that position, so only the matching `nulls` placement is index order.
201
+ */
202
+ readonly nativeNullsOrder: "high" | "low";
203
+ /**
204
+ * DB-07: whether a paged find whose LEADING order key is nullable and asks
205
+ * for the non-native placement reads through a `UNION ALL` of two
206
+ * index-ordered branches (`SelectStatement.fromUnion`: the key's non-NULL
207
+ * rows, then its NULL rows, each limited to `skip + take`), re-ordered and
208
+ * paged outside. Today's statement sorts the whole table on every page:
209
+ * PostgreSQL 18 `desc nulls last` 34.8 → 0.075 ms, MySQL 26.7 `asc nulls
210
+ * last` 54.5 → 0.85 ms. SQLite 3.51 already walks the index for either
211
+ * placement (0.005 ms today, 0.020 ms as a union) — `false` there.
212
+ */
213
+ readonly nullsUnionPage: boolean;
124
214
  /**
125
215
  * Membership on a time-of-day column (`inArray` with `timeOfDay`): `in`
126
216
  * renders like any other membership; `equalities` renders an OR of `=`
127
- * comparisons. MySQL 8.4 types a `?` inside a multi-value IN list against a
217
+ * comparisons (a key list with a time-of-day member: an OR of complete
218
+ * conjunctions). MySQL 8.4 types a `?` inside a multi-value IN list against a
128
219
  * TIME(n) column as TIME(6) and matches nothing unless n is 6 (live on
129
220
  * 8.4.11; 26.7 matches); `=` converts the parameter to the column's TIME on
130
221
  * every version.
@@ -243,6 +334,17 @@ export type SqlDialect = {
243
334
  * rows are grouped by column set into separate statements there (M4).
244
335
  */
245
336
  readonly rowDefaultKeyword: boolean;
337
+ /**
338
+ * How a multi-row INSERT whose rows all bind plain values may render
339
+ * (`InsertStatement.unnest`). `unnest` binds ONE typed array per column —
340
+ * `INSERT … SELECT … FROM unnest($1::text[], …) WITH ORDINALITY … ORDER BY`
341
+ * — so the text is constant and a statement binds one parameter per column
342
+ * (postgres and PGlite; the type table, efficiency campaign EPIC C, proved
343
+ * equal stored values and errors on adapter-pg, adapter-bun and PGlite).
344
+ * `values` keeps one parameter per value (mysql and sqlite have no typed
345
+ * array parameters); the renderer refuses an `unnest` statement there.
346
+ */
347
+ readonly bulkInsertForm: "unnest" | "values";
246
348
  /** How an INSERT that names no column renders ({@link EmptyInsertSyntax}). */
247
349
  readonly emptyInsert: EmptyInsertSyntax;
248
350
  /**
@@ -5,6 +5,7 @@ export declare function renderMysqlCheckProbe(params: {
5
5
  dialect: SqlDialect;
6
6
  operation: "clone" | "add" | "read" | "drop";
7
7
  probe: string;
8
+ /** Source for clone, or qualified live table for a transient SHOW CREATE comparison. */
8
9
  table?: {
9
10
  readonly namespace: string;
10
11
  readonly name: string;
@@ -0,0 +1,43 @@
1
+ import type { QualifiedSqlName } from "@vibeorm/schema";
2
+ import type { SqlDialect } from "../dialect.ts";
3
+ import type { ExtractedGeneratedColumn, GeneratedColumnDeclaration } from "../generated-columns.ts";
4
+ import type { RenderedQuery } from "../render.ts";
5
+ /** MySQL stored syntax, placed after complete type/charset/collation information. */
6
+ export declare function renderMysqlGeneratedColumnClause({ generated }: {
7
+ readonly generated: GeneratedColumnDeclaration;
8
+ }): string;
9
+ /** Temporary CREATE/DROP operations avoid ALTER's implicit-commit boundary. */
10
+ export type MysqlGeneratedColumnProbe = {
11
+ readonly operation: "context";
12
+ } | {
13
+ readonly operation: "clone";
14
+ readonly probe: string;
15
+ readonly table: QualifiedSqlName;
16
+ } | {
17
+ readonly operation: "create";
18
+ readonly probe: string;
19
+ readonly definition: string;
20
+ readonly column: string;
21
+ readonly columnDefinition: string;
22
+ readonly backslashEscapes: boolean;
23
+ readonly after?: string;
24
+ } | {
25
+ readonly operation: "read";
26
+ readonly probe: string;
27
+ readonly table?: QualifiedSqlName;
28
+ } | {
29
+ readonly operation: "drop";
30
+ readonly probe: string;
31
+ };
32
+ /** Render scratch SQL from independently provisioned table definitions; migrate owns cleanup. */
33
+ export declare function renderMysqlGeneratedColumnProbe({ dialect, probe }: {
34
+ readonly dialect: SqlDialect;
35
+ readonly probe: MysqlGeneratedColumnProbe;
36
+ }): RenderedQuery;
37
+ /** SHOW CREATE's own escaping rules apply; information_schema uses a different spelling channel. */
38
+ export declare function extractMysqlGeneratedColumn({ definition, column, backslashEscapes }: {
39
+ readonly definition: string;
40
+ readonly column: string;
41
+ readonly backslashEscapes: boolean;
42
+ }): ExtractedGeneratedColumn | undefined;
43
+ //# sourceMappingURL=mysql-generated-columns.d.ts.map
@@ -0,0 +1,20 @@
1
+ import type { IndexKeyTermIR } from "@vibeorm/schema";
2
+ import type { SqlDialect } from "../dialect.ts";
3
+ import type { IndexExpressionProbeParams, IndexKeyOperand } from "../index-expressions.ts";
4
+ import type { PredicateModel } from "../predicate.ts";
5
+ import type { RenderedQuery } from "../render.ts";
6
+ /** Qualify only expression-bearing keys through schema's shared width rule; never impose new bounds on ordinary indexes. */
7
+ export declare function assertMysqlIndexKeyStorage({ model, terms }: {
8
+ readonly dialect: SqlDialect;
9
+ readonly model: PredicateModel;
10
+ readonly terms: readonly IndexKeyTermIR[];
11
+ }): void;
12
+ /** Recover SHOW CREATE key interiors, including exact CAST/charset/collation bytes. */
13
+ export declare function extractMysqlIndexKeys({ definition, index, backslashEscapes }: {
14
+ readonly definition: string;
15
+ readonly index: string;
16
+ readonly backslashEscapes: boolean;
17
+ }): readonly IndexKeyOperand[] | undefined;
18
+ /** Reuse E's temporary-clone/read/drop operations, extending only CREATE's key replacement seam. */
19
+ export declare function renderMysqlIndexExpressionProbe({ ...params }: IndexExpressionProbeParams): readonly RenderedQuery[];
20
+ //# sourceMappingURL=mysql-index-expressions.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { SqlDialect } from "../dialect.ts";
2
+ import type { EventRowRuleContext, RenderedRowRule } from "../row-rules.ts";
3
+ /** Emit mode-safe SIGNAL diagnostics and one unfiltered BEFORE trigger per event. */
4
+ export declare function renderMysqlRowRule({ dialect, context }: {
5
+ readonly dialect: SqlDialect;
6
+ readonly context: EventRowRuleContext;
7
+ }): RenderedRowRule;
8
+ //# sourceMappingURL=mysql-row-rules.d.ts.map
@@ -0,0 +1,30 @@
1
+ import type { QualifiedSqlName } from "@vibeorm/schema";
2
+ import type { SqlDialect } from "../dialect.ts";
3
+ import type { GeneratedColumnDeclaration } from "../generated-columns.ts";
4
+ import type { RenderedQuery } from "../render.ts";
5
+ /** PostgreSQL stored syntax; the public dispatcher performs declaration admission. */
6
+ export declare function renderPostgresGeneratedColumnClause({ generated }: {
7
+ readonly generated: GeneratedColumnDeclaration;
8
+ }): string;
9
+ /** The migration owner creates the desired base context inside the private savepoint. */
10
+ export type PostgresGeneratedColumnProbe = {
11
+ readonly operation: "savepoint" | "rollback" | "release";
12
+ readonly savepoint: string;
13
+ } | {
14
+ readonly operation: "add";
15
+ readonly table: QualifiedSqlName;
16
+ readonly column: string;
17
+ readonly definition: string;
18
+ };
19
+ /** Render pinned-session scratch statements; this helper never executes or manages a session. */
20
+ export declare function renderPostgresGeneratedColumnProbe({ dialect, probe }: {
21
+ readonly dialect: SqlDialect;
22
+ readonly probe: PostgresGeneratedColumnProbe;
23
+ }): RenderedQuery;
24
+ /** Approved stored-generated to ordinary conversion, retaining the present stored values. */
25
+ export declare function renderPostgresDropGeneratedExpression({ dialect, table, column }: {
26
+ readonly dialect: SqlDialect;
27
+ readonly table: QualifiedSqlName;
28
+ readonly column: string;
29
+ }): RenderedQuery;
30
+ //# sourceMappingURL=postgres-generated-columns.d.ts.map
@@ -0,0 +1,10 @@
1
+ import type { IndexExpressionProbeParams, IndexKeyOperand } from "../index-expressions.ts";
2
+ import type { RenderedQuery } from "../render.ts";
3
+ /** CREATE INDEX extraction is conservative; structured catalog rows remain authoritative. */
4
+ export declare function extractPostgresIndexKeys({ definition, index }: {
5
+ readonly definition: string;
6
+ readonly index: string;
7
+ }): readonly IndexKeyOperand[] | undefined;
8
+ /** Caller owns a fixed search_path and rollback savepoint; no populated user table is indexed. */
9
+ export declare function renderPostgresIndexExpressionProbe({ ...params }: IndexExpressionProbeParams): readonly RenderedQuery[];
10
+ //# sourceMappingURL=postgres-index-expressions.d.ts.map
@@ -1,10 +1,25 @@
1
1
  import type { QualifiedSqlName } from "@vibeorm/schema";
2
2
  import type { SqlDialect } from "../dialect.ts";
3
3
  import type { RenderedQuery } from "../render.ts";
4
+ /**
5
+ * Turn JIT compilation off for the rest of the current transaction and return
6
+ * the previous `jit` value as `previous` (read before the change). The bulk
7
+ * index-key read costs far above `jit_above_cost`, and compiling it takes longer
8
+ * than running it. Transaction-local: outside a transaction block it changes nothing.
9
+ */
10
+ export declare function renderPostgresJitSuspend(params: {
11
+ dialect: SqlDialect;
12
+ }): RenderedQuery;
13
+ /** Restore the transaction's `jit` value read by {@link renderPostgresJitSuspend}. */
14
+ export declare function renderPostgresJitRestore(params: {
15
+ dialect: SqlDialect;
16
+ previous: string;
17
+ }): RenderedQuery;
4
18
  /** Physical operands and unambiguous server-default evidence for actual type/access-method pairs. */
5
19
  export declare function renderPostgresIndexKeys(params: {
6
20
  dialect: SqlDialect;
7
21
  table?: QualifiedSqlName;
8
22
  namespaces?: readonly string[];
23
+ expressionMetadata?: boolean;
9
24
  }): RenderedQuery;
10
25
  //# sourceMappingURL=postgres-index-keys.d.ts.map
@@ -0,0 +1,25 @@
1
+ import type { FieldIR, QualifiedSqlName, RowRuleIR, SqlObjectIdentity } from "@vibeorm/schema";
2
+ import type { SqlDialect } from "../dialect.ts";
3
+ import type { RenderedRowRule } from "../row-rules.ts";
4
+ /** A finalized PostgreSQL comparison context, supplied by the canonical schema collector. */
5
+ export type PostgresRowRuleContext = {
6
+ readonly model: string;
7
+ readonly table: QualifiedSqlName;
8
+ readonly rule: RowRuleIR;
9
+ readonly functionIdentity: Extract<SqlObjectIdentity, {
10
+ readonly kind: "function";
11
+ }>;
12
+ readonly triggerIdentity: {
13
+ readonly kind: "trigger";
14
+ readonly table: QualifiedSqlName;
15
+ readonly name: string;
16
+ };
17
+ readonly protectedFields: readonly FieldIR[];
18
+ readonly gateField?: FieldIR;
19
+ };
20
+ /** Emit one ordinary invoker function and an unfiltered BEFORE row trigger; no live objects are taken over. */
21
+ export declare function renderPostgresRowRule({ dialect, context }: {
22
+ readonly dialect: SqlDialect;
23
+ readonly context: PostgresRowRuleContext;
24
+ }): RenderedRowRule;
25
+ //# sourceMappingURL=postgres-row-rules.d.ts.map
@@ -0,0 +1,18 @@
1
+ import type { ExtractedGeneratedColumn, GeneratedColumnDeclaration } from "../generated-columns.ts";
2
+ /** SQLite stored syntax; adding a stored column still requires the migrate rebuild path. */
3
+ export declare function renderSqliteGeneratedColumnClause({ generated }: {
4
+ readonly generated: GeneratedColumnDeclaration;
5
+ }): string;
6
+ /** Shared source-slice extraction for SQLite source and MySQL SHOW CREATE, not SQL equivalence. */
7
+ export declare function extractGenerationFromDefinition({ definition, column, backslashEscapes, mysql }: {
8
+ readonly definition: string;
9
+ readonly column: string;
10
+ readonly backslashEscapes?: boolean;
11
+ readonly mysql?: boolean;
12
+ }): ExtractedGeneratedColumn | undefined;
13
+ /** Recover the exact AS interior; omitted storage means virtual, never inferred stored. */
14
+ export declare function extractSqliteGeneratedColumn({ definition, column }: {
15
+ readonly definition: string;
16
+ readonly column: string;
17
+ }): ExtractedGeneratedColumn | undefined;
18
+ //# sourceMappingURL=sqlite-generated-columns.d.ts.map
@@ -0,0 +1,23 @@
1
+ import type { IndexKeyOperand } from "../index-expressions.ts";
2
+ import type { TableDefinitionToken } from "./sqlite-table-definition.ts";
3
+ /** Exact outer-key slices shared by CREATE INDEX and MySQL table-key clauses. */
4
+ export declare function extractIndexKeyList({ sql, open, close, tokens, wrapped, removeWrapper }: {
5
+ readonly sql: string;
6
+ readonly open: TableDefinitionToken;
7
+ readonly close: TableDefinitionToken;
8
+ readonly tokens: readonly TableDefinitionToken[];
9
+ readonly wrapped?: boolean;
10
+ readonly removeWrapper?: boolean;
11
+ }): readonly IndexKeyOperand[] | undefined;
12
+ /** Parse the CREATE INDEX header without interpreting any scalar expression. */
13
+ export declare function extractStandaloneIndexKeys({ definition, index, postgres }: {
14
+ readonly definition: string;
15
+ readonly index: string;
16
+ readonly postgres?: boolean;
17
+ }): readonly IndexKeyOperand[] | undefined;
18
+ /** SQLite source spelling is the evidence; CAST alone never proves a plain Decimal key. */
19
+ export declare function extractSqliteIndexKeys({ definition, index }: {
20
+ readonly definition: string;
21
+ readonly index: string;
22
+ }): readonly IndexKeyOperand[] | undefined;
23
+ //# sourceMappingURL=sqlite-index-expressions.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { SqlDialect } from "../dialect.ts";
2
+ import type { EventRowRuleContext, RenderedRowRule } from "../row-rules.ts";
3
+ /** Emit literal-only ABORT diagnostics and one unfiltered BEFORE trigger per event. */
4
+ export declare function renderSqliteRowRule({ dialect, context }: {
5
+ readonly dialect: SqlDialect;
6
+ readonly context: EventRowRuleContext;
7
+ }): RenderedRowRule;
8
+ //# sourceMappingURL=sqlite-row-rules.d.ts.map