@vibeorm/sql 5.8.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 (51) hide show
  1. package/README.md +1 -0
  2. package/dist/ast.d.ts +293 -9
  3. package/dist/codecs.d.ts +106 -12
  4. package/dist/composable.d.ts +44 -4
  5. package/dist/dialect.d.ts +30 -2
  6. package/dist/dialects/diagnostics.d.ts +1 -1
  7. package/dist/dialects/migration-inventory.d.ts +0 -11
  8. package/dist/dialects/migration-shadow-marker.d.ts +65 -0
  9. package/dist/dialects/mysql-generated-columns.d.ts +1 -5
  10. package/dist/dialects/mysql-index-expressions.d.ts +1 -19
  11. package/dist/dialects/mysql-row-rules.d.ts +1 -7
  12. package/dist/dialects/postgres-generated-columns.d.ts +0 -5
  13. package/dist/dialects/postgres-index-expressions.d.ts +1 -9
  14. package/dist/dialects/postgres-rls-readiness.d.ts +0 -2
  15. package/dist/dialects/postgres-rls.d.ts +7 -1
  16. package/dist/dialects/postgres-row-rules.d.ts +1 -24
  17. package/dist/dialects/sqlite-fts.d.ts +2 -2
  18. package/dist/dialects/sqlite-generated-columns.d.ts +1 -12
  19. package/dist/dialects/sqlite-index-expressions.d.ts +1 -22
  20. package/dist/dialects/sqlite-json-equality.d.ts +1 -14
  21. package/dist/dialects/sqlite-row-rules.d.ts +1 -7
  22. package/dist/dialects/sqlite-table-definition.d.ts +0 -19
  23. package/dist/dialects/test-databases.d.ts +96 -0
  24. package/dist/expr-fragment.d.ts +13 -0
  25. package/dist/identifiers.d.ts +1 -4
  26. package/dist/index-expressions.d.ts +8 -2
  27. package/dist/index.d.ts +16 -7
  28. package/dist/index.js +4148 -2762
  29. package/dist/index.js.map +49 -42
  30. package/dist/json-patch.d.ts +82 -0
  31. package/dist/json-path.d.ts +2 -0
  32. package/dist/key-list.d.ts +0 -21
  33. package/dist/native-types.d.ts +72 -0
  34. package/dist/params.d.ts +0 -2
  35. package/dist/query/expressions.d.ts +1 -33
  36. package/dist/query/index.js.map +7 -7
  37. package/dist/query/mutations.d.ts +1 -14
  38. package/dist/query/reusable.d.ts +1 -16
  39. package/dist/query/select.d.ts +1 -10
  40. package/dist/query/sources.d.ts +1 -36
  41. package/dist/query/state.d.ts +1 -77
  42. package/dist/query-plan.d.ts +2 -2
  43. package/dist/render.d.ts +9 -0
  44. package/dist/result-decoder.d.ts +21 -1
  45. package/dist/row-rules.d.ts +1 -19
  46. package/dist/sql-bindings.d.ts +7 -43
  47. package/dist/sql-handles.d.ts +1 -3
  48. package/dist/sql-input.d.ts +8 -24
  49. package/dist/timestamp-identity.d.ts +6 -12
  50. package/dist/timestamp-text.d.ts +36 -0
  51. package/package.json +2 -2
package/README.md CHANGED
@@ -18,6 +18,7 @@ Most users never install this directly — it ships as a dependency of [`vibeorm
18
18
  - **Dialects**: `postgresDialect`, `sqliteDialect`, `mysqlDialect`, the `SqlDialect` type, and the registry `SQL_DIALECTS` / `getSqlDialect`. A dialect carries `quoteIdent` plus the strategy switches: `= ANY` versus `IN` expansion, `ILIKE` versus `LOWER(...) LIKE LOWER(...)`, the upsert form, `ESCAPE` clause presence, `RETURNING` availability.
19
19
  - **Capability tables**: `DIALECT_CAPABILITIES`, `POSTGRES_CAPABILITIES`, `SQLITE_CAPABILITIES`, `MYSQL_CAPABILITIES` — frozen objects consumed by the runtime, migrate, the generator and the E2E harness. A feature a dialect cannot express throws `VIBE_UNSUPPORTED_CAPABILITY` rather than degrading.
20
20
  - **Codec tables**: `DIALECT_CODECS`, `POSTGRES_CODECS`, `SQLITE_CODECS`, `MYSQL_CODECS`, `decodeIsNoop`, and the `CodecTable` / `ScalarCodec` / `WireFidelity` types. All per-dialect value encode and decode lives here, in one place, instead of scattered through query builders.
21
+ - **Column types**: `canonicalColumnType({ dialect, scalar, nativeType })` names the column type migrate creates for a field, in the one spelling migrate compares column types by (`"integer"` for `Int`, `@db.Int4` and `@db.Integer` on PostgreSQL); `normalizePostgresNativeType` / `normalizeMysqlNativeType`, the tables `POSTGRES_SCALAR_COLUMN_TYPES`, `MYSQL_SCALAR_COLUMN_TYPES`, `SQLITE_SCALAR_STORAGE_CLASSES` and `POSTGRES_EXACT_EQUIVALENTS` are the pieces. `describeSchema` (`@vibeorm/runtime`) reports each field's `nativeType` with it.
21
22
  - **Rendering**: `renderStatement({ dialect, statement })` returns `RenderedQuery` (`{ text, values, returning }`); `renderExpr` and `renderFragment` render sub-parts against a shared `ParamCollector`. Placeholders always come from `ParamCollector`, never from hand-written `$1` or `?`.
22
23
  - **LIKE helpers**: `escapeLikePattern`, `escapeGlobPattern`, `containsPattern`, `startsWithPattern`, `endsWithPattern`, `strictMatchPattern`.
23
24
 
package/dist/ast.d.ts CHANGED
@@ -66,6 +66,25 @@ export type ColRef = {
66
66
  * without the `dateTruncation` capability refuse it when rendering.
67
67
  */
68
68
  readonly truncate?: DateTruncation;
69
+ /**
70
+ * A `groupBy` expression key read from the column ({@link GroupKeyExpression}):
71
+ * the text at a JSON path, or a string's leading characters. Like
72
+ * {@link ColRef.truncate} it is set on the projection (with `alias`, the
73
+ * key's result name), GROUP BY and ORDER BY refs of one key, which repeat
74
+ * the identical text: every operand is a validated literal, never a bound
75
+ * parameter, so PostgreSQL and MySQL match the GROUP BY expression.
76
+ */
77
+ readonly groupKey?: GroupKeyExpression;
78
+ /**
79
+ * The first non-NULL of these references — `COALESCE("a", "b")` — instead
80
+ * of the column `name`: the value of an `orderBy: { _coalesce: … }` term.
81
+ * Each member renders as its own reference, casts included (a sqlite
82
+ * Decimal member as `CAST(… AS REAL)`, a mysql native enum as its member
83
+ * index). Set on ORDER BY and comparison refs only. `name` is then
84
+ * {@link COALESCE_REF_NAME}, never a column of a table, so a reader that
85
+ * looks a column up by name finds none.
86
+ */
87
+ readonly coalesce?: readonly ColRef[];
69
88
  /**
70
89
  * Projection alias: renders `ref AS "alias"`. PROJECTION-ONLY — filter,
71
90
  * order and correlation refs never set it (the renderer ignores it outside
@@ -74,6 +93,15 @@ export type ColRef = {
74
93
  */
75
94
  readonly alias?: string;
76
95
  };
96
+ /**
97
+ * The `name` of a {@link ColRef.coalesce} reference. The `__vibe_` column
98
+ * prefix is reserved by schema validation, so no model column carries it.
99
+ */
100
+ export declare const COALESCE_REF_NAME: string;
101
+ /** A {@link ColRef.coalesce} reference over `members`. */
102
+ export declare function coalesceRef(params: {
103
+ members: readonly ColRef[];
104
+ }): ColRef;
77
105
  /** A {@link ColRef.truncate} request: the calendar unit, the zone that defines the calendar, and the column's storage form. */
78
106
  export type DateTruncation = {
79
107
  readonly unit: DateTruncationUnit;
@@ -81,6 +109,24 @@ export type DateTruncation = {
81
109
  readonly timeZone: string;
82
110
  readonly storage: DateTruncationStorage;
83
111
  };
112
+ /**
113
+ * A {@link ColRef.groupKey} expression.
114
+ *
115
+ * - `jsonPathText`: the text at an object-key path of a JSON column — a JSON
116
+ * string unquoted, a number or boolean in its JSON spelling, JSON `null` and
117
+ * a missing path SQL NULL, an object or array its JSON text. Each step reads
118
+ * an object member only. `storage: "json"` marks a postgres `json` column,
119
+ * read through jsonb like the JSON filters.
120
+ * - `prefix`: the first `length` characters of a string column.
121
+ */
122
+ export type GroupKeyExpression = {
123
+ readonly kind: "jsonPathText";
124
+ readonly path: readonly string[];
125
+ readonly storage?: "json";
126
+ } | {
127
+ readonly kind: "prefix";
128
+ readonly length: number;
129
+ };
84
130
  export type TableRef = {
85
131
  readonly name: string;
86
132
  /** Explicit fixed PostgreSQL placement; absence preserves existing SQL. */
@@ -92,11 +138,19 @@ export type AggFn = "count" | "avg" | "sum" | "min" | "max";
92
138
  /** Reference to an aggregate expression: `COUNT(*)` or `FN("col")`. */
93
139
  export type AggRef = {
94
140
  readonly fn: "countAll";
141
+ readonly filter?: SqlExpr;
95
142
  } | {
96
143
  readonly fn: AggFn;
97
144
  readonly column: ColRef;
98
145
  /** Native enum MIN/MAX: projections and HAVING return the database label; order keys keep its ordinal. */
99
146
  readonly enumOrder?: "value" | "ordinal";
147
+ /**
148
+ * Aggregate only the rows the predicate admits: `AGG(x) FILTER (WHERE …)`
149
+ * or `AGG(CASE WHEN … THEN x END)` per the dialect's
150
+ * `filteredAggregates` capability. Its parameters bind each time the
151
+ * aggregate renders (projection, HAVING, ORDER BY), in text order.
152
+ */
153
+ readonly filter?: SqlExpr;
100
154
  };
101
155
  /**
102
156
  * One aggregate projection: `FN("col") AS "alias"`. The alias is part of the
@@ -111,10 +165,13 @@ export type AggSelect = {
111
165
  };
112
166
  /**
113
167
  * JSON path predicates (Prisma's Json filter surface). `not` is `equals` with
114
- * `negate` — there is no separate op. Which ops a dialect can express is
168
+ * `negate` — there is no separate op. `exists` takes a boolean `value`: the
169
+ * member at the path (object keys only) is present (its value may be JSON
170
+ * `null`) or, for `false`, the document is present and the member is not; a
171
+ * SQL NULL column is unknown for both. Which ops a dialect can express is
115
172
  * declared on `SqlDialect.supportedJsonOps` and enforced at render time too.
116
173
  */
117
- export type JsonFilterOp = "equals" | "string_contains" | "string_starts_with" | "string_ends_with" | "array_contains" | "array_starts_with" | "array_ends_with";
174
+ export type JsonFilterOp = "equals" | "exists" | "string_contains" | "string_starts_with" | "string_ends_with" | "array_contains" | "array_starts_with" | "array_ends_with";
118
175
  export type ComparisonOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte";
119
176
  /**
120
177
  * The column facts one member of a {@link SqlExpr} `keyList` tuple needs to
@@ -250,6 +307,8 @@ export type SqlExpr = {
250
307
  /** The tested columns, in the subquery's projection order. */
251
308
  readonly columns: readonly ColRef[];
252
309
  readonly select: SelectStatement;
310
+ /** The subquery yields at most one row: one column, rendered `"fk" = (SELECT …)`. */
311
+ readonly single?: true;
253
312
  } | {
254
313
  readonly kind: "like";
255
314
  readonly column: ColRef;
@@ -393,12 +452,20 @@ export type SqlExpr = {
393
452
  *
394
453
  * `one` renders the constant `1` — the conventional EXISTS projection; it
395
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.
396
460
  */
397
461
  export type SelectExpr = {
398
462
  readonly kind: "countAll";
399
463
  readonly alias?: string;
400
464
  } | {
401
465
  readonly kind: "one";
466
+ } | {
467
+ readonly kind: "exists";
468
+ readonly alias: string;
402
469
  };
403
470
  /** Arithmetic applied to a column's current value in an UPDATE ... SET. */
404
471
  export type SetOperator = "increment" | "decrement" | "multiply" | "divide";
@@ -517,10 +584,18 @@ export type OrderTerm = OrderByColumn | OrderByAggregate | OrderBySubquery;
517
584
  export type LateralJoin = {
518
585
  /** Join alias (`__lat_0`) — the outer projection selects `alias.columnAlias`. */
519
586
  readonly alias: string;
520
- /** 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
+ */
521
592
  readonly columnAlias: string;
522
- /** jsonArray: many rows; jsonObject: one row or NULL; count: a scalar COUNT(*) lateral. */
523
- 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";
524
599
  /**
525
600
  * Object output is the SQL-layer default. Positional rows follow the exact
526
601
  * select.columns order, using explicit output names (never SELECT *).
@@ -529,6 +604,13 @@ export type LateralJoin = {
529
604
  * slot. Consumers must use the SAME projection to decode the payload.
530
605
  */
531
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[];
532
614
  /** The correlated child select — may reference outer columns via table-qualified refs. */
533
615
  readonly select: SelectStatement;
534
616
  };
@@ -771,6 +853,20 @@ export type KeyRows = {
771
853
  /** Encoded tuples; never empty, each exactly `columns.length` long. */
772
854
  readonly rows: readonly (readonly unknown[])[];
773
855
  };
856
+ /**
857
+ * `GROUP BY GROUPING SETS (…)` over a select's `groupBy` columns. Each set
858
+ * lists indexes into `groupBy`; `()` is the grand total. The projection gains
859
+ * `CASE GROUPING(<every groupBy column>) WHEN <mask> THEN <set index> … END AS
860
+ * "<indexAlias>"` after the aggregates, so a row names the set that produced
861
+ * it, and an ORDER BY term may name `indexAlias` as a bare column (an output
862
+ * column). Dialect `groupingSets` capability: `"groupingSets"` renders any
863
+ * list, `"rollup"` only the rollup chain of `groupBy` in order (`GROUP BY a,
864
+ * b WITH ROLLUP`), `"none"` refuses.
865
+ */
866
+ export type GroupingSets = {
867
+ readonly sets: readonly (readonly number[])[];
868
+ readonly indexAlias: string;
869
+ };
774
870
  export type SelectStatement = {
775
871
  readonly kind: "select";
776
872
  readonly table: TableRef;
@@ -850,6 +946,8 @@ export type SelectStatement = {
850
946
  readonly fromKeys?: KeyRows;
851
947
  readonly where?: SqlExpr;
852
948
  readonly groupBy?: readonly ColRef[];
949
+ /** Grouping sets over `groupBy` — see {@link GroupingSets}. */
950
+ readonly groupingSets?: GroupingSets;
853
951
  /** HAVING predicate — grouped selects; may contain `aggCmp` expressions. */
854
952
  readonly having?: SqlExpr;
855
953
  readonly orderBy?: readonly OrderTerm[];
@@ -971,7 +1069,37 @@ export type ReturningColumn = string | {
971
1069
  } | {
972
1070
  readonly name: string;
973
1071
  readonly transform: "jsonText";
974
- } | CaptureReturningColumn;
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
+ };
1084
+ /**
1085
+ * A RETURNING column read from a row OTHER than the one the statement wrote,
1086
+ * exposed as `alias` and projected like the written row's own column
1087
+ * (`projection`), so one codec decodes both:
1088
+ *
1089
+ * - `from: "old"` — the row as it was before the statement (capability
1090
+ * `rowCapture`, PostgreSQL 18): `old."col" AS "alias"`, NULL in every column
1091
+ * for a row the statement inserted;
1092
+ * - `from: { source }` — the joined source of an `UPDATE … FROM` by its alias
1093
+ * (capability `returningReadsJoinedSource`): `"source"."col" AS "alias"`.
1094
+ */
1095
+ export type OtherRowReturningColumn = {
1096
+ readonly from: "old" | {
1097
+ readonly source: string;
1098
+ };
1099
+ readonly name: string;
1100
+ readonly alias: string;
1101
+ readonly projection?: "castText" | "jsonText";
1102
+ };
975
1103
  /**
976
1104
  * A row-capture RETURNING column (`$transaction(…, { capture })`, dialects with
977
1105
  * the `rowCapture` capability: PostgreSQL 18, where RETURNING names the row
@@ -982,15 +1110,71 @@ export type ReturningColumn = string | {
982
1110
  * inserted row, `new` for a deleted one);
983
1111
  * - `"inserted"`: `(old."col" IS NULL) AS "alias"` — the arm of an `INSERT …
984
1112
  * ON CONFLICT DO UPDATE` row, true when it was inserted (`col` must be a
985
- * column no stored row holds NULL in: a primary-key column).
1113
+ * column no stored row holds NULL in: a primary-key column);
1114
+ * - `"old"` / `"new"` with `hop`: the related row's column, read in the same
1115
+ * statement — `(SELECT "__vibe_h"."col"::text FROM "Related" AS "__vibe_h"
1116
+ * WHERE "__vibe_h"."id" = new."relatedId") AS "alias"` (see {@link CaptureHop});
1117
+ * - `"key"`: `old."col" AS "alias"` in the column's own type, for a chain's
1118
+ * {@link CaptureCascade} to match child rows on;
1119
+ * - `"cascade"` ({@link CaptureCascadeReturningColumn}): the images of the
1120
+ * child rows a referential action of this row changes.
986
1121
  */
987
1122
  export type CaptureReturningColumn = {
988
- readonly capture: "old" | "new" | "inserted";
1123
+ readonly capture: "old" | "new" | "inserted" | "key";
989
1124
  readonly name: string;
990
1125
  readonly alias: string;
1126
+ /** With `"old"` / `"new"`: read `name` from the related row instead of this one. */
1127
+ readonly hop?: CaptureHop;
1128
+ } | CaptureCascadeReturningColumn;
1129
+ /**
1130
+ * A to-one hop of a capture cell: the row of `table` whose `on[i].target`
1131
+ * columns equal the reading row's `on[i].source` columns. The target columns
1132
+ * must be a unique key (a scalar subquery that found two rows would fail the
1133
+ * statement); no row, or a NULL source column, reads NULL.
1134
+ */
1135
+ export type CaptureHop = {
1136
+ readonly table: TableRef;
1137
+ readonly on: readonly {
1138
+ readonly target: string;
1139
+ readonly source: string;
1140
+ }[];
1141
+ };
1142
+ /** One cell of a {@link CaptureCascade} image: a child column, or a column of its related row. */
1143
+ export type CaptureCell = {
1144
+ readonly name: string;
1145
+ readonly hop?: CaptureHop;
1146
+ };
1147
+ /**
1148
+ * The distinct images of the child rows of `table` whose `on[i].child` columns
1149
+ * equal a written parent row's `on[i].parent` columns: `(SELECT
1150
+ * array_agg("__vibe_i")::text FROM (SELECT DISTINCT ARRAY[<cells>] AS "__vibe_i"
1151
+ * FROM "Child" AS "__vibe_c" WHERE <match> LIMIT $n) AS "__vibe_d")`, NULL when
1152
+ * no child matched. Read inside the parent's statement, whose snapshot still
1153
+ * holds the rows its foreign keys' referential actions remove or rewrite at
1154
+ * the statement's end. `limit` is bound as a parameter.
1155
+ */
1156
+ export type CaptureCascade = {
1157
+ readonly table: TableRef;
1158
+ readonly on: readonly {
1159
+ readonly child: string;
1160
+ readonly parent: string;
1161
+ }[];
1162
+ readonly cells: readonly CaptureCell[];
1163
+ readonly limit: number;
1164
+ };
1165
+ /**
1166
+ * A per-row {@link CaptureCascade} in RETURNING: `parent` columns are read from
1167
+ * `old.` (`"__vibe_c"."child" = old."parent"`).
1168
+ */
1169
+ export type CaptureCascadeReturningColumn = {
1170
+ readonly capture: "cascade";
1171
+ readonly alias: string;
1172
+ readonly cascade: CaptureCascade;
991
1173
  };
992
1174
  /** The column name a {@link ReturningColumn} exposes in the result row. */
993
1175
  export declare function returningColumnName(column: ReturningColumn): string;
1176
+ /** Is this RETURNING column read from another row ({@link OtherRowReturningColumn})? */
1177
+ export declare function isOtherRowReturningColumn(column: ReturningColumn): column is OtherRowReturningColumn;
994
1178
  /** Is this RETURNING column a row-capture column ({@link CaptureReturningColumn})? */
995
1179
  export declare function isCaptureReturningColumn(column: ReturningColumn): column is CaptureReturningColumn;
996
1180
  export type InsertStatement = {
@@ -1053,6 +1237,42 @@ export type OnConflictUpdate = {
1053
1237
  * mysql's ODKU has no WHERE and refuses it (`VIBE_UNSUPPORTED_CAPABILITY`).
1054
1238
  */
1055
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
+ };
1056
1276
  /**
1057
1277
  * MySQL only (`upsertForm: "onDuplicateKey"`): append
1058
1278
  * `` `col` = LAST_INSERT_ID(`col`) `` to the update list so the UPDATE arm
@@ -1062,6 +1282,41 @@ export type OnConflictUpdate = {
1062
1282
  */
1063
1283
  readonly lastInsertIdColumn?: string;
1064
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";
1065
1320
  /**
1066
1321
  * Conflicting rows are skipped (`createMany({ skipDuplicates: true })`).
1067
1322
  * An empty/absent target means "any conflict" — the mysql form
@@ -1236,11 +1491,24 @@ export declare function stepColumnRef(params: {
1236
1491
  column: string;
1237
1492
  }): StepColumnRef;
1238
1493
  /** The joined relation of an {@link UpdateStatement}: its table, alias and join condition. */
1239
- export type UpdateSource = {
1494
+ export type UpdateSource = UpdateTableSource | UpdateSelectSource;
1495
+ /** A joined table: `FROM "table" AS alias` (`updateMany({ from })`). */
1496
+ export type UpdateTableSource = {
1240
1497
  readonly table: TableRef;
1241
1498
  readonly alias: string;
1242
1499
  readonly on: SqlExpr;
1243
1500
  };
1501
+ /**
1502
+ * A joined subquery: `FROM (SELECT …) AS alias`, PostgreSQL only. With a
1503
+ * locking read of the target table it is the row as it was before the write,
1504
+ * and an {@link OtherRowReturningColumn} `from: { source: alias }` returns its
1505
+ * values (`update({ previous })`, capability `returningReadsJoinedSource`).
1506
+ */
1507
+ export type UpdateSelectSource = {
1508
+ readonly select: SelectStatement;
1509
+ readonly alias: string;
1510
+ readonly on: SqlExpr;
1511
+ };
1244
1512
  /** An UPDATE set value read from the joined source's `column` (`UPDATE … FROM`). */
1245
1513
  export type SourceColumnRef = {
1246
1514
  readonly __sourceColumn: true;
@@ -1367,6 +1635,15 @@ export type ChainResult = {
1367
1635
  * guard, so it is evaluated exactly once, on the one guaranteed result row.
1368
1636
  */
1369
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;
1370
1647
  /**
1371
1648
  * One column `alias` on every result row: the number of rows the listed
1372
1649
  * data-modifying steps returned (their RETURNING rows = their direct
@@ -1412,6 +1689,13 @@ export type ChainCapture = {
1412
1689
  readonly after?: readonly string[];
1413
1690
  /** The most distinct images one image column returns: a positive safe integer, bound as a parameter. */
1414
1691
  readonly limit: number;
1692
+ /**
1693
+ * Child images of the step's rows, one result column `<alias>_c<j>` each: a
1694
+ * {@link CaptureCascade} whose `parent` columns name columns the step
1695
+ * RETURNS, matched by `EXISTS (SELECT 1 FROM "step" AS "__vibe_k" WHERE
1696
+ * "__vibe_k"."parent" = "__vibe_c"."child")`.
1697
+ */
1698
+ readonly cascades?: readonly CaptureCascade[];
1415
1699
  };
1416
1700
  /**
1417
1701
  * Fail the statement when `step` returned no row; `token` names the veto in the
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