@vibeorm/sql 5.9.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/ast.d.ts +120 -4
  2. package/dist/codecs.d.ts +106 -12
  3. package/dist/composable.d.ts +44 -4
  4. package/dist/dialect.d.ts +24 -1
  5. package/dist/dialects/migration-inventory.d.ts +0 -11
  6. package/dist/dialects/mysql-generated-columns.d.ts +1 -5
  7. package/dist/dialects/mysql-index-expressions.d.ts +1 -19
  8. package/dist/dialects/mysql-row-rules.d.ts +1 -7
  9. package/dist/dialects/postgres-generated-columns.d.ts +0 -5
  10. package/dist/dialects/postgres-index-expressions.d.ts +1 -11
  11. package/dist/dialects/postgres-rls-readiness.d.ts +0 -2
  12. package/dist/dialects/postgres-rls.d.ts +7 -1
  13. package/dist/dialects/postgres-row-rules.d.ts +1 -24
  14. package/dist/dialects/sqlite-fts.d.ts +2 -2
  15. package/dist/dialects/sqlite-generated-columns.d.ts +1 -12
  16. package/dist/dialects/sqlite-index-expressions.d.ts +1 -24
  17. package/dist/dialects/sqlite-json-equality.d.ts +1 -14
  18. package/dist/dialects/sqlite-row-rules.d.ts +1 -7
  19. package/dist/dialects/sqlite-table-definition.d.ts +0 -19
  20. package/dist/dialects/test-databases.d.ts +96 -0
  21. package/dist/expr-fragment.d.ts +13 -0
  22. package/dist/identifiers.d.ts +1 -4
  23. package/dist/index.d.ts +9 -5
  24. package/dist/index.js +3754 -3052
  25. package/dist/index.js.map +46 -43
  26. package/dist/json-patch.d.ts +0 -14
  27. package/dist/json-path.d.ts +1 -2
  28. package/dist/key-list.d.ts +0 -21
  29. package/dist/native-types.d.ts +3 -1
  30. package/dist/params.d.ts +0 -2
  31. package/dist/query/expressions.d.ts +1 -33
  32. package/dist/query/index.js.map +7 -7
  33. package/dist/query/mutations.d.ts +1 -14
  34. package/dist/query/reusable.d.ts +1 -16
  35. package/dist/query/select.d.ts +1 -10
  36. package/dist/query/sources.d.ts +1 -36
  37. package/dist/query/state.d.ts +1 -77
  38. package/dist/query-plan.d.ts +2 -2
  39. package/dist/result-decoder.d.ts +21 -1
  40. package/dist/row-rules.d.ts +1 -19
  41. package/dist/sql-bindings.d.ts +7 -43
  42. package/dist/sql-handles.d.ts +1 -3
  43. package/dist/sql-input.d.ts +8 -24
  44. package/dist/timestamp-identity.d.ts +6 -12
  45. package/dist/timestamp-text.d.ts +36 -0
  46. package/package.json +2 -2
package/dist/ast.d.ts CHANGED
@@ -307,6 +307,8 @@ export type SqlExpr = {
307
307
  /** The tested columns, in the subquery's projection order. */
308
308
  readonly columns: readonly ColRef[];
309
309
  readonly select: SelectStatement;
310
+ /** The subquery yields at most one row: one column, rendered `"fk" = (SELECT …)`. */
311
+ readonly single?: true;
310
312
  } | {
311
313
  readonly kind: "like";
312
314
  readonly column: ColRef;
@@ -450,12 +452,20 @@ export type SqlExpr = {
450
452
  *
451
453
  * `one` renders the constant `1` — the conventional EXISTS projection; it
452
454
  * cannot be combined with a column list.
455
+ *
456
+ * `exists` asks whether the statement's own FROM and WHERE match a row:
457
+ * `SELECT EXISTS (SELECT 1 FROM … WHERE …) AS "alias"` — one row, one
458
+ * boolean-valued column (PostgreSQL `boolean`, SQLite and MySQL `0`/`1`). It
459
+ * takes no column list, aggregate, grouping, order, paging, lock or join.
453
460
  */
454
461
  export type SelectExpr = {
455
462
  readonly kind: "countAll";
456
463
  readonly alias?: string;
457
464
  } | {
458
465
  readonly kind: "one";
466
+ } | {
467
+ readonly kind: "exists";
468
+ readonly alias: string;
459
469
  };
460
470
  /** Arithmetic applied to a column's current value in an UPDATE ... SET. */
461
471
  export type SetOperator = "increment" | "decrement" | "multiply" | "divide";
@@ -574,10 +584,18 @@ export type OrderTerm = OrderByColumn | OrderByAggregate | OrderBySubquery;
574
584
  export type LateralJoin = {
575
585
  /** Join alias (`__lat_0`) — the outer projection selects `alias.columnAlias`. */
576
586
  readonly alias: string;
577
- /** Column the aggregated JSON value is exposed under (`__rel_posts`). */
587
+ /**
588
+ * Column the aggregated JSON value is exposed under (`__rel_posts`). An
589
+ * `aggregate` lateral exposes one column per entry of its select's
590
+ * `aggregates` instead, under each entry's alias.
591
+ */
578
592
  readonly columnAlias: string;
579
- /** jsonArray: many rows; jsonObject: one row or NULL; count: a scalar COUNT(*) lateral. */
580
- readonly agg: "jsonArray" | "jsonObject" | "count";
593
+ /**
594
+ * jsonArray: many rows; jsonObject: one row or NULL; count: a scalar COUNT(*)
595
+ * lateral; aggregate: one ungrouped row of `aggregates` (no columns), which
596
+ * an aggregate without GROUP BY returns even over zero rows.
597
+ */
598
+ readonly agg: "jsonArray" | "jsonObject" | "count" | "aggregate";
581
599
  /**
582
600
  * Object output is the SQL-layer default. Positional rows follow the exact
583
601
  * select.columns order, using explicit output names (never SELECT *).
@@ -586,6 +604,13 @@ export type LateralJoin = {
586
604
  * slot. Consumers must use the SAME projection to decode the payload.
587
605
  */
588
606
  readonly rowFormat?: "object" | "positional";
607
+ /**
608
+ * Positional rows only: output names of `select.columns` the subselect
609
+ * projects for the statement's own use (an order key the aggregate's ORDER
610
+ * BY reads, a key a nested lateral correlates on) but the JSON row leaves
611
+ * out. The row is the remaining columns in `select.columns` order.
612
+ */
613
+ readonly hidden?: readonly string[];
589
614
  /** The correlated child select — may reference outer columns via table-qualified refs. */
590
615
  readonly select: SelectStatement;
591
616
  };
@@ -1044,7 +1069,18 @@ export type ReturningColumn = string | {
1044
1069
  } | {
1045
1070
  readonly name: string;
1046
1071
  readonly transform: "jsonText";
1047
- } | CaptureReturningColumn | OtherRowReturningColumn;
1072
+ } | CaptureReturningColumn | OtherRowReturningColumn | ServerVersionReturningColumn;
1073
+ /**
1074
+ * The server's `server_version_num` as text, exposed as `alias`:
1075
+ * `pg_catalog.current_setting('server_version_num') AS "alias"`. Rides a write
1076
+ * whose client does not know the server version yet, so the next statement can
1077
+ * take the form that version allows without a read of its own. Dialects with
1078
+ * the `rowCapture` capability only (the version decides whether it applies).
1079
+ */
1080
+ export type ServerVersionReturningColumn = {
1081
+ readonly serverVersion: true;
1082
+ readonly alias: string;
1083
+ };
1048
1084
  /**
1049
1085
  * A RETURNING column read from a row OTHER than the one the statement wrote,
1050
1086
  * exposed as `alias` and projected like the written row's own column
@@ -1201,6 +1237,42 @@ export type OnConflictUpdate = {
1201
1237
  * mysql's ODKU has no WHERE and refuses it (`VIBE_UNSUPPORTED_CAPABILITY`).
1202
1238
  */
1203
1239
  readonly updateWhere?: SqlExpr;
1240
+ /**
1241
+ * Skip-unchanged guard: the conflicting row is updated only when at least
1242
+ * one of these columns' stored value differs, NULL counting as a value,
1243
+ * from the value its `update` assignment would write (each column must have
1244
+ * one). Rendered as the first member of the update arm's `WHERE`, AND-ed
1245
+ * with {@link OnConflictUpdate.updateWhere}; a skipped row writes nothing
1246
+ * and returns nothing. On ON DUPLICATE KEY UPDATE (no WHERE) the columns it
1247
+ * leaves out are assigned first, keeping their stored value when nothing
1248
+ * else would change, and the dialect itself skips the unchanged row.
1249
+ * Needs `upsertSkipUnchanged`.
1250
+ */
1251
+ readonly skipUnchanged?: readonly UnchangedColumn[];
1252
+ /**
1253
+ * The table's native row-security predicate, RAW SQL without parameters
1254
+ * (the managed policy's check, columns qualified by the target table's
1255
+ * name). With a guard on the arm ({@link OnConflictUpdate.updateWhere} or
1256
+ * {@link OnConflictUpdate.skipUnchanged}) the arm's `WHERE` becomes
1257
+ * `(<policy>) IS NOT TRUE OR (<guard>)`: PostgreSQL evaluates that `WHERE`
1258
+ * before its row-security check of the conflicting row, so a row the policy
1259
+ * hides must always take the update path and fail that check, whatever its
1260
+ * values. Ignored when the arm has no guard; refused by a dialect without
1261
+ * `nativeRowLevelSecurity`.
1262
+ */
1263
+ readonly rowPolicy?: string;
1264
+ /**
1265
+ * MySQL only, a single-row statement: the duplicate arm also runs
1266
+ * `` `column` = IF(LAST_INSERT_ID(flag), `t`.`column`, `t`.`column`) `` (a
1267
+ * self-assignment, rendered last), so the statement reports `flag` as its
1268
+ * insert id exactly when the row took the update arm — the one signal that
1269
+ * tells an inserted row from an unchanged one when the driver reports no
1270
+ * `Duplicates:` info. `flag` is a decimal literal.
1271
+ */
1272
+ readonly duplicateFlag?: {
1273
+ readonly column: string;
1274
+ readonly flag: string;
1275
+ };
1204
1276
  /**
1205
1277
  * MySQL only (`upsertForm: "onDuplicateKey"`): append
1206
1278
  * `` `col` = LAST_INSERT_ID(`col`) `` to the update list so the UPDATE arm
@@ -1210,6 +1282,41 @@ export type OnConflictUpdate = {
1210
1282
  */
1211
1283
  readonly lastInsertIdColumn?: string;
1212
1284
  };
1285
+ /**
1286
+ * One column a skip-unchanged conflict arm compares. `json`: a Json column,
1287
+ * compared as `jsonb` on postgres (its `json` type has no equality operator;
1288
+ * `"list"` for a Json list).
1289
+ */
1290
+ export type UnchangedColumn = {
1291
+ readonly column: string;
1292
+ readonly json?: "scalar" | "list";
1293
+ /**
1294
+ * A column whose SQL equality can be coarser than "the stored value would
1295
+ * not change"; the renderer compares it exactly where the dialect needs it.
1296
+ * See {@link UnchangedExactness}.
1297
+ */
1298
+ readonly bytes?: UnchangedExactness;
1299
+ /**
1300
+ * A text column the schema declares with a folding type or collation
1301
+ * (`citext`, a `@collation` that is not byte-ordered): its equality may call
1302
+ * `'x'` and `'X'` equal on any dialect, not only under MySQL's default.
1303
+ */
1304
+ readonly folds?: true;
1305
+ /** The column is a list of `bytes`'s kind (a postgres array). */
1306
+ readonly list?: true;
1307
+ };
1308
+ /**
1309
+ * Why a compared column's own equality may call two different stored values
1310
+ * equal:
1311
+ * - `"text"`: a text column; MySQL's default `_ai_ci` collation folds case
1312
+ * and accents (and see `folds`).
1313
+ * - `"decimal"`: a Decimal column without a declared scale (PostgreSQL's bare
1314
+ * `numeric` keeps `1.50` and `1.5` apart, its equality does not).
1315
+ * - `"float"`: a float column (`-0` equals `0`).
1316
+ * - `"other"`: a Json or list column, or a text-like column of another
1317
+ * declared type, compared by its bytes on MySQL.
1318
+ */
1319
+ export type UnchangedExactness = "text" | "decimal" | "float" | "other";
1213
1320
  /**
1214
1321
  * Conflicting rows are skipped (`createMany({ skipDuplicates: true })`).
1215
1322
  * An empty/absent target means "any conflict" — the mysql form
@@ -1528,6 +1635,15 @@ export type ChainResult = {
1528
1635
  * guard, so it is evaluated exactly once, on the one guaranteed result row.
1529
1636
  */
1530
1637
  readonly veto?: readonly ChainVeto[];
1638
+ /**
1639
+ * A step whose rows every veto depends on: when it returned no row, no veto
1640
+ * is checked and the statement succeeds, answering the one guaranteed row
1641
+ * with NULL columns and the tally (rendered as the first CASE arm, `WHEN NOT
1642
+ * EXISTS (SELECT 1 FROM "step") THEN <tally>`). A nested write whose
1643
+ * missing parent is an answer, not an error, gates its children's vetoes on
1644
+ * the parent step this way. Requires `tally` and at least one veto.
1645
+ */
1646
+ readonly vetoGate?: string;
1531
1647
  /**
1532
1648
  * One column `alias` on every result row: the number of rows the listed
1533
1649
  * data-modifying steps returned (their RETURNING rows = their direct
package/dist/codecs.d.ts CHANGED
@@ -76,15 +76,30 @@ export type WireFidelity = {
76
76
  */
77
77
  readonly bytes?: "buffer";
78
78
  /**
79
- * Zone-less date-time text (MySQL DATETIME / TIMESTAMP / DATE) is read as a
80
- * UTC instant by the driver (`"utc"`: mysql2 with `timezone: "Z"`). Absent
81
- * means another or an unknown zone (another `timezone` option, an injected
82
- * pool). Never prunes a decoder: the runtime reads it to decide whether a
83
- * dialect default of `"join"` keeps value identity — the JSON transport
84
- * decodes that text as UTC (`lateralJson.requiresUtcDateTimeWire`, lane J).
79
+ * The zone the driver reads zone-less date-time text in (MySQL DATETIME /
80
+ * TIMESTAMP / DATE): `"utc"` (mysql2 with `timezone: "Z"`), a fixed offset
81
+ * in minutes east of UTC, or `"local"` (the process zone, read the way
82
+ * mysql2 builds a local `Date`). The MySQL DateTime decoders read the same
83
+ * text in the same zone wherever it arrives as text (the join strategy's
84
+ * JSON, a page key, `exactDates`), so one stored cell is one instant on every
85
+ * path; the adapter writes a bound parameter in it. Absent reads as UTC.
86
+ * Never prunes a decoder. `"utc"` alone also lets a dialect default of
87
+ * `"join"` apply (`lateralJson.requiresUtcDateTimeWire`): it is declared only
88
+ * where the zone is known to be UTC.
85
89
  */
86
- readonly zoneLessDateTime?: "utc";
90
+ readonly zoneLessDateTime?: DateTimeZone;
87
91
  };
92
+ /** A zone zone-less date-time text is read in: UTC, a fixed offset in minutes east of UTC, or the process's local zone. */
93
+ export type DateTimeZone = "utc" | "local" | {
94
+ readonly offsetMinutes: number;
95
+ };
96
+ /**
97
+ * The cache-key fragment of a wire's date-time zone: `""` when zone-less text
98
+ * reads as UTC, so every UTC plan keeps its key; else `"local"` or the offset.
99
+ */
100
+ export declare function dateTimeZoneKey(params: {
101
+ wire: WireFidelity | undefined;
102
+ }): string;
88
103
  /**
89
104
  * Is this scalar type's DECODE a no-op under the declared wire fidelity?
90
105
  * Provable against the decode implementations in this file, identically on
@@ -115,7 +130,8 @@ export declare function decodeIsNoop(params: {
115
130
  wire: WireFidelity | undefined;
116
131
  }): boolean;
117
132
  /**
118
- * Parse date-time text into a `Date`, reading ZONE-LESS text as **UTC**.
133
+ * Parse date-time text into a `Date`, reading ZONE-LESS text as **UTC** — for
134
+ * text a CALLER wrote (a filter or data value, a validator's check).
119
135
  *
120
136
  * ECMA-262 reads `2026-01-15T12:00:00` as process-LOCAL time, so the same
121
137
  * stored value would decode to a different instant on every host time zone,
@@ -124,9 +140,12 @@ export declare function decodeIsNoop(params: {
124
140
  * that yields ONE instant everywhere (MYSQL-2, live-proven; the postgres
125
141
  * `timestamp` case is proven by apps/tests `timestamp-zones.e2e`). Text with
126
142
  * an explicit `Z`/offset and date-only text parse as ECMA-262 already reads
127
- * them (as written / UTC midnight); anything unparseable is `undefined` so the
128
- * caller keeps its own fallback. Fractions beyond milliseconds are truncated —
129
- * `Date` has no finer resolution and engines differ on longer fractions.
143
+ * them (as written / UTC midnight); any other form the engine reads
144
+ * (`Jan 15 2026 12:00 GMT`) is accepted as the engine reads it, and
145
+ * anything unparseable is `undefined` so the caller keeps its own fallback.
146
+ * Fractions beyond milliseconds are truncated — `Date` has no finer
147
+ * resolution and engines differ on longer fractions. Text from the DATABASE
148
+ * never takes that last, guessing step ({@link parseDatabaseDateTime}).
130
149
  */
131
150
  export declare function parseDateTimeText(params: {
132
151
  text: string;
@@ -144,11 +163,14 @@ export declare function parseDateTimeText(params: {
144
163
  * milliseconds, an offset with seconds applied exactly.
145
164
  *
146
165
  * `undefined` for every other text — a 4-digit AD value keeps the parse it
147
- * had — and for an instant outside the `Date` range. Exported for the adapters
166
+ * had, unless `anyYear` (the database-text parser, after its other forms
167
+ * declined: an offset with seconds) — and for an instant outside the `Date`
168
+ * range. Exported for the adapters
148
169
  * whose zone-less driver parsers fall back to it (one rule, rule 6).
149
170
  */
150
171
  export declare function parseExtendedDateTimeText(params: {
151
172
  text: string;
173
+ anyYear?: boolean;
152
174
  }): Date | undefined;
153
175
  /**
154
176
  * The decode refusal of a non-finite or unparseable DateTime — one error for
@@ -157,6 +179,78 @@ export declare function parseExtendedDateTimeText(params: {
157
179
  * reports exactly this error.
158
180
  */
159
181
  export declare function decodedDateTimeRefusal(): VibeError;
182
+ /**
183
+ * The refusal of database date-time text no decoder reads, by its cause —
184
+ * each with its own `meta.reason`, so the message names the right remedy:
185
+ *
186
+ * - `"zero-date"`: a MySQL zero date or a zero month or day, which has no
187
+ * instant (mysql2 renders a binary zero `DATETIME` as `undefined 00:00:00`);
188
+ * - `"datetime-out-of-range"`: ISO-shaped text with no `Date` (a year past
189
+ * ±275760, `294276-12-31` in PostgreSQL, or a day the calendar lacks);
190
+ * - `"datetime-text-layout"`: any other layout; on PostgreSQL its cause is a
191
+ * `DateStyle` other than ISO, and the error names that remedy
192
+ * ({@link dateTimeTextLayoutError}).
193
+ *
194
+ * The text itself is never included.
195
+ */
196
+ export declare function undecodableDateTimeText(params: {
197
+ text: string;
198
+ dialect: Dialect;
199
+ }): VibeError;
200
+ /**
201
+ * The refusal for database DateTime text outside the ISO layouts. On
202
+ * PostgreSQL its cause is a `DateStyle` other than ISO, set by the server,
203
+ * the database or the role; the message and `meta.fix` name the remedies.
204
+ * The text itself is never included.
205
+ */
206
+ export declare function dateTimeTextLayoutError(): VibeError;
207
+ /**
208
+ * The codec of a DateTime field whose native type is a date-only column on
209
+ * `dialect`, or `undefined` (the DateTime table row applies): MySQL's
210
+ * `@db.Date` ({@link mysqlDateOnlyCodec}). PostgreSQL's `date` input and
211
+ * SQLite's text need none.
212
+ */
213
+ export declare function dateOnlyCodec(params: {
214
+ dialect: Dialect;
215
+ nativeType: string | undefined;
216
+ }): ScalarCodec | undefined;
217
+ /**
218
+ * The instant of zone-less date-time text a MySQL server printed (`DATETIME`,
219
+ * `DATE`, a `TIMESTAMP` in the session zone), read in `zone`, the zone mysql2
220
+ * reads the same cell in; the years 0-99 keep their year. `undefined` for any
221
+ * other text. For the adapter's raw path; ORM reads take the codec table.
222
+ */
223
+ export declare function parseZoneLessDateTime(params: {
224
+ text: string;
225
+ zone: DateTimeZone | undefined;
226
+ }): Date | undefined;
227
+ /**
228
+ * `codec` as it reads under the adapter's declared zone
229
+ * (`wire.zoneLessDateTime`): a MySQL DateTime or date-only codec reads
230
+ * zone-less text in that zone when it is not UTC; every other codec, and every
231
+ * codec under UTC, is returned unchanged. Encode is never changed: a `Date`
232
+ * goes to the driver, which writes it in the same zone.
233
+ */
234
+ export declare function zonedCodec(params: {
235
+ dialect: Dialect;
236
+ codec: ScalarCodec;
237
+ wire: WireFidelity | undefined;
238
+ }): ScalarCodec;
239
+ /**
240
+ * The text a MySQL adapter writes for a parameter the DateTime codec bound
241
+ * exactly (a `Date` read from the server's text, which carries more fraction
242
+ * digits than a `Date` holds): the wall clock it was read from when that text,
243
+ * read in `zone`, is this instant (so a wall clock in a daylight-saving gap
244
+ * under `"local"` addresses its own cell); else the instant's wall clock in
245
+ * `zone` (text with an offset, or read in another zone). Every fraction digit
246
+ * is kept, and at least three are written. `undefined` for every other value,
247
+ * which the driver writes as it always did. A keyset position over such a
248
+ * value carries the same text.
249
+ */
250
+ export declare function mysqlBoundTimestampText(params: {
251
+ value: unknown;
252
+ zone: DateTimeZone | undefined;
253
+ }): string | undefined;
160
254
  /**
161
255
  * The time-of-day codec for a DateTime field's native type, or `undefined`
162
256
  * when the native type is not `time`/`timetz` (the DateTime table row
@@ -21,10 +21,22 @@ export type SqlTag = {
21
21
  value: NoInfer<T>;
22
22
  decoder?: ResultDecoder<T>;
23
23
  }) => SqlFragment;
24
- readonly join: (params: {
25
- items: readonly unknown[];
26
- separator?: SqlFragment;
27
- }) => SqlFragment;
24
+ /**
25
+ * A list: fragments spliced, anything else bound as one value each, joined
26
+ * by `separator` (default `, `). `sql.join(items, separator)` and
27
+ * `sql.join({ items, separator })` are the same call.
28
+ */
29
+ readonly join: {
30
+ (items: readonly unknown[], separator?: SqlFragment): SqlFragment;
31
+ (params: {
32
+ items: readonly unknown[];
33
+ separator?: SqlFragment;
34
+ }): SqlFragment;
35
+ };
36
+ /** Every condition must hold: `(a) AND (b)`, each part in parentheses; `undefined` parts are skipped, and none left is `TRUE`. */
37
+ readonly and: (parts: readonly (SqlFragment | undefined)[]) => SqlFragment;
38
+ /** Any condition may hold: `(a) OR (b)`, each part in parentheses; `undefined` parts are skipped, and none left is `FALSE`. */
39
+ readonly or: (parts: readonly (SqlFragment | undefined)[]) => SqlFragment;
28
40
  readonly existing: (params: {
29
41
  column: string;
30
42
  }) => SqlFragment;
@@ -62,6 +74,34 @@ export declare function compileSql(params: {
62
74
  text: string;
63
75
  values: unknown[];
64
76
  };
77
+ /**
78
+ * A tagged template whose every interpolated value binds as one plain
79
+ * parameter (no fragment, no typed array or JSON-list parameter), captured
80
+ * without building a fragment. Its values are snapshotted when captured, as
81
+ * `sql` snapshots them; `compileSqlTemplate` renders it.
82
+ */
83
+ export declare class PlainSqlTemplate {
84
+ readonly strings: TemplateStringsArray;
85
+ readonly values: readonly unknown[];
86
+ constructor(strings: TemplateStringsArray, values: readonly unknown[]);
87
+ }
88
+ /**
89
+ * What `sql` builds for a template, except that a template whose
90
+ * values all bind as plain parameters comes back as a `PlainSqlTemplate`:
91
+ * the same text and values once compiled, without a fragment per call.
92
+ */
93
+ export declare function captureSqlTemplate(params: {
94
+ strings: TemplateStringsArray;
95
+ values: readonly unknown[];
96
+ }): SqlFragment | PlainSqlTemplate;
97
+ /** `compileSql` over what `captureSqlTemplate` returned. */
98
+ export declare function compileSqlTemplate(params: {
99
+ dialect: SqlDialect;
100
+ query: SqlFragment | PlainSqlTemplate;
101
+ }): {
102
+ text: string;
103
+ values: unknown[];
104
+ };
65
105
  /** Whether a nested SQL fragment reads the incoming row (MySQL alias declaration). */
66
106
  export declare function sqlUsesIncoming(params: {
67
107
  fragment: SqlFragment;
package/dist/dialect.d.ts CHANGED
@@ -20,6 +20,13 @@ import type { ParamCollector } from "./params.ts";
20
20
  export type RowLockSyntax = {
21
21
  readonly strengths: Readonly<Partial<Record<RowLockStrength, string>>>;
22
22
  readonly wait: Readonly<Partial<Record<RowLockWaitPolicy, string>>>;
23
+ /**
24
+ * The spelling of a strength when the lock names no `OF` table and no wait
25
+ * policy, where it differs from `strengths`: a form more servers of the
26
+ * family parse (MySQL's `LOCK IN SHARE MODE`, which MariaDB and MySQL before
27
+ * 8.0.1 accept while `FOR SHARE` is a syntax error there).
28
+ */
29
+ readonly plain?: Readonly<Partial<Record<RowLockStrength, string>>>;
23
30
  };
24
31
  /** The blocking and try function names for one advisory-lock mode. */
25
32
  export type AdvisoryLockModeFunctions = {
@@ -218,8 +225,14 @@ export type SqlDialect = {
218
225
  * PostgreSQL and MySQL turn it into a semi-join. `inSelect`: an uncorrelated
219
226
  * `key IN (SELECT key …)` — SQLite runs a correlated `EXISTS` once per outer
220
227
  * row (`post where author.email = x`: 134 → 0.15 ms, Post 500 000).
228
+ * `eqSelect`: a to-one through ONE foreign-key column pinned this way renders
229
+ * `fk = (SELECT key …)`, every other shape the correlated `EXISTS` —
230
+ * PostgreSQL plans the semi-join as a loop into all of the parent's children
231
+ * and sorts them for a page, but reads a page of `fk = (SELECT …)` in index
232
+ * order (newest 20 of 4 000 orders of one person: 889 → 11 buffers,
233
+ * 1.16 → 0.01 ms; an `IN (SELECT …)` keeps the semi-join plan).
221
234
  */
222
- readonly relationFilterStrategy: "exists" | "inSelect";
235
+ readonly relationFilterStrategy: "exists" | "inSelect" | "eqSelect";
223
236
  /**
224
237
  * DB-02: when a find with a TOTAL order (it ends with the full primary key)
225
238
  * pages with `skip` ≥ `minSkip`, the runtime cuts the page as primary keys
@@ -484,5 +497,15 @@ export type SqlDialect = {
484
497
  offsetSeconds: number;
485
498
  collector: ParamCollector;
486
499
  }): string;
500
+ /**
501
+ * True when any pair of the two equally long lists of rendered expressions
502
+ * differs, NULL counting as a value: `IS DISTINCT FROM` on postgres, `IS
503
+ * NOT` on sqlite, `NOT (… <=> …)` on mysql. One pair compares the two
504
+ * expressions; several compare row constructors.
505
+ */
506
+ renderRowsDiffer(params: {
507
+ left: readonly string[];
508
+ right: readonly string[];
509
+ }): string;
487
510
  };
488
511
  //# sourceMappingURL=dialect.d.ts.map
@@ -1,16 +1,5 @@
1
1
  import type { SqlDialect } from "../dialect.ts";
2
2
  import type { RenderedQuery } from "../render.ts";
3
- /**
4
- * Ends a MySQL catalog listing whose row count has no small bound (the
5
- * migration inventory, the visibility proof). `sql_select_limit` (session or
6
- * global, e.g. 1 000 under `mysql --safe-updates`) silently cuts the rows of a
7
- * SELECT without its own LIMIT, and a cut listing reads its last objects (or
8
- * grants) as missing; an explicit LIMIT takes precedence over it. After the
9
- * last `UNION` branch (and its ORDER BY) the LIMIT applies to the whole union.
10
- * The value is MySQL's largest row count, so nothing is ever cut. The batched
11
- * object readers end the same way (`migration-objects.ts`, LEFTOVERS-01).
12
- */
13
- export declare const MYSQL_NO_ROW_LIMIT: string;
14
3
  /** SQLite column metadata includes generated columns omitted by table_info. */
15
4
  export declare function renderSqliteColumnInventory(params: {
16
5
  dialect: SqlDialect;
@@ -1,11 +1,7 @@
1
1
  import type { QualifiedSqlName } from "@vibeorm/schema";
2
2
  import type { SqlDialect } from "../dialect.ts";
3
- import type { ExtractedGeneratedColumn, GeneratedColumnDeclaration } from "../generated-columns.ts";
3
+ import type { ExtractedGeneratedColumn } from "../generated-columns.ts";
4
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
5
  /** Temporary CREATE/DROP operations avoid ALTER's implicit-commit boundary. */
10
6
  export type MysqlGeneratedColumnProbe = {
11
7
  readonly operation: "context";
@@ -1,20 +1,2 @@
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[];
1
+ export {};
20
2
  //# sourceMappingURL=mysql-index-expressions.d.ts.map
@@ -1,8 +1,2 @@
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;
1
+ export {};
8
2
  //# sourceMappingURL=mysql-row-rules.d.ts.map
@@ -1,11 +1,6 @@
1
1
  import type { QualifiedSqlName } from "@vibeorm/schema";
2
2
  import type { SqlDialect } from "../dialect.ts";
3
- import type { GeneratedColumnDeclaration } from "../generated-columns.ts";
4
3
  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
4
  /** The migration owner creates the desired base context inside the private savepoint. */
10
5
  export type PostgresGeneratedColumnProbe = {
11
6
  readonly operation: "savepoint" | "rollback" | "release";
@@ -1,12 +1,2 @@
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, included }: {
5
- readonly definition: string;
6
- readonly index: string;
7
- /** INCLUDE columns the caller read from the catalog; an INCLUDE clause of another length is refused. */
8
- readonly included?: number;
9
- }): readonly IndexKeyOperand[] | undefined;
10
- /** Caller owns a fixed search_path and rollback savepoint; no populated user table is indexed. */
11
- export declare function renderPostgresIndexExpressionProbe({ ...params }: IndexExpressionProbeParams): readonly RenderedQuery[];
1
+ export {};
12
2
  //# sourceMappingURL=postgres-index-expressions.d.ts.map
@@ -2,8 +2,6 @@ import type { SqlDialect } from "../dialect.ts";
2
2
  import type { RenderedQuery } from "../render.ts";
3
3
  /** Throwaway policy identity; never installed on a live table by readiness. */
4
4
  export declare const NATIVE_RLS_PROBE_POLICY: "vibeorm_probe";
5
- /** The reads `catalog` folds, in the order their JSON columns appear. */
6
- export declare const RLS_READINESS_CATALOG_KEYS: readonly ("identity" | "elevatedRoles" | "schemaControl" | "tables" | "policies" | "views" | "routines" | "triggers")[];
7
5
  /** Read-only catalog SQL; exported for exact statement routing in adapter tests. */
8
6
  export declare const RLS_READINESS_SQL: {
9
7
  readonly identity: string;
@@ -43,11 +43,17 @@ export declare function renderRlsColumnType(params: {
43
43
  dialect: SqlDialect;
44
44
  field: FieldIR;
45
45
  }): string;
46
- /** Desired predicate only, for drift probes; it is NOT server-canonical deparse. */
46
+ /**
47
+ * Desired predicate only, for drift probes; it is NOT server-canonical deparse.
48
+ * `qualify` names the table a `where` rule's columns are read from, for a
49
+ * statement with another relation in scope (`excluded` in `ON CONFLICT … DO
50
+ * UPDATE`); the policy itself and the drift probes render them bare.
51
+ */
47
52
  export declare function renderRlsPredicate(params: {
48
53
  dialect: SqlDialect;
49
54
  schema: SchemaIR;
50
55
  model: string;
56
+ qualify?: string;
51
57
  }): string;
52
58
  /** One PERMISSIVE ALL/PUBLIC policy with equal existing/proposed-row checks. */
53
59
  export declare function renderRlsPolicy(params: {
@@ -1,25 +1,2 @@
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;
1
+ export {};
25
2
  //# sourceMappingURL=postgres-row-rules.d.ts.map
@@ -1,4 +1,4 @@
1
- /** @internal SQLite FTS synchronization SQL and its independently verifiable catalog spelling. */
1
+ /** SQLite FTS synchronization SQL and its independently verifiable catalog spelling. */
2
2
  export declare function renderSqliteFtsUpdateTrigger(params: {
3
3
  table: string;
4
4
  ftsTable: string;
@@ -7,7 +7,7 @@ export declare function renderSqliteFtsUpdateTrigger(params: {
7
7
  readonly create: string;
8
8
  readonly definition: string;
9
9
  };
10
- /** @internal Recognize only complete generated legacy/current triggers and their external-content FTS5 table. */
10
+ /** Recognize only complete generated legacy/current triggers and their external-content FTS5 table. */
11
11
  export declare function sqliteFtsUpdateUpgrade(params: {
12
12
  create: string;
13
13
  installed: string;
@@ -1,15 +1,4 @@
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;
1
+ import type { ExtractedGeneratedColumn } from "../generated-columns.ts";
13
2
  /** Recover the exact AS interior; omitted storage means virtual, never inferred stored. */
14
3
  export declare function extractSqliteGeneratedColumn({ definition, column }: {
15
4
  readonly definition: string;