@vibeorm/sql 4.2.0 → 5.0.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 (37) hide show
  1. package/dist/ast.d.ts +322 -9
  2. package/dist/codecs.d.ts +46 -0
  3. package/dist/dialect.d.ts +155 -5
  4. package/dist/dialects/migration-inventory.d.ts +58 -1
  5. package/dist/dialects/migration-objects.d.ts +36 -0
  6. package/dist/dialects/migration-visibility.d.ts +6 -1
  7. package/dist/dialects/mysql-check-probe.d.ts +1 -0
  8. package/dist/dialects/mysql-generated-columns.d.ts +43 -0
  9. package/dist/dialects/mysql-index-expressions.d.ts +20 -0
  10. package/dist/dialects/mysql-row-rules.d.ts +8 -0
  11. package/dist/dialects/mysql.d.ts +3 -2
  12. package/dist/dialects/postgres-generated-columns.d.ts +30 -0
  13. package/dist/dialects/postgres-index-expressions.d.ts +10 -0
  14. package/dist/dialects/postgres-index-keys.d.ts +15 -0
  15. package/dist/dialects/postgres-rls.d.ts +16 -1
  16. package/dist/dialects/postgres-row-rules.d.ts +25 -0
  17. package/dist/dialects/sqlite-generated-columns.d.ts +18 -0
  18. package/dist/dialects/sqlite-index-expressions.d.ts +23 -0
  19. package/dist/dialects/sqlite-row-rules.d.ts +8 -0
  20. package/dist/dialects/sqlite-table-definition.d.ts +25 -5
  21. package/dist/generated-columns.d.ts +18 -0
  22. package/dist/index-expressions.d.ts +61 -0
  23. package/dist/index.d.ts +23 -9
  24. package/dist/index.js +2662 -1161
  25. package/dist/index.js.map +35 -21
  26. package/dist/key-list.d.ts +83 -0
  27. package/dist/like.d.ts +17 -0
  28. package/dist/local-date.d.ts +9 -0
  29. package/dist/pg-array.d.ts +6 -0
  30. package/dist/predicate.d.ts +21 -7
  31. package/dist/query/index.js +59 -19
  32. package/dist/query/index.js.map +7 -7
  33. package/dist/query/state.d.ts +5 -0
  34. package/dist/query/types.d.ts +2 -0
  35. package/dist/row-rules.d.ts +57 -0
  36. package/dist/timestamp-identity.d.ts +8 -2
  37. package/package.json +2 -2
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;
@@ -22,7 +22,13 @@ export type ColRef = {
22
22
  * `castText` renders a dialect text cast and preserves `alias ?? name`,
23
23
  * including timestamp identity before driver Date truncation. PostgreSQL
24
24
  * uses `"col"::text`; SQLite/MySQL use CAST AS TEXT/CHAR. `encodeBase64`
25
- * is postgres-only and renders `encode("col", 'base64')`. Other users:
25
+ * is postgres-only and renders `encode("col", 'base64')`.
26
+ * `encodeArrayBase64` is postgres-only and renders
27
+ * `encode(array_send("col"), 'base64')` — a `bytea[]` column's binary array
28
+ * form (dimensions, NULL elements, raw element bytes) as base64: faithful
29
+ * whatever the session's `bytea_output`, keeping a NULL column (`NULL`), an
30
+ * empty array and NULL elements distinct (5.0 lane V; the lateral subselect's
31
+ * `Bytes[]` form, decoded by the runtime's JSON-transport plan). Other users:
26
32
  * the lateral subselect (SQL review B3: raw `to_jsonb` would ship BigInt/Decimal as float64 JSON
27
33
  * numbers and Bytes as `\x` hex text), and every row projection of a
28
34
  * `Float[]`/`Json[]` column (bun:sql's array parser refuses float arrays
@@ -30,7 +36,7 @@ export type ColRef = {
30
36
  * the runtime parses the array literal text itself). Never set on
31
37
  * filter/order refs.
32
38
  */
33
- readonly transform?: "castText" | "encodeBase64" | "jsonText";
39
+ readonly transform?: "castText" | "encodeBase64" | "encodeArrayBase64" | "jsonText";
34
40
  /**
35
41
  * Comparison/order/aggregate cast, sqlite Decimal only (SQL review B4):
36
42
  * Decimal stores as exact TEXT there and text comparison is lexicographic —
@@ -88,6 +94,33 @@ export type AggSelect = {
88
94
  */
89
95
  export type JsonFilterOp = "equals" | "string_contains" | "string_starts_with" | "string_ends_with" | "array_contains" | "array_starts_with" | "array_ends_with";
90
96
  export type ComparisonOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte";
97
+ /**
98
+ * The column facts one member of a {@link SqlExpr} `keyList` tuple needs to
99
+ * bind exactly like the equality `"col" = $n` would. Postgres binds each
100
+ * member as ONE typed array (`$n::<element type>[]`), so it needs the
101
+ * column's element type: the declared native type without its modifier
102
+ * (`timestamptz`, never `timestamptz(3)` — a typmod cast would round
103
+ * `.1234` onto a stored `.123` that `=` does not match), `bpchar` for
104
+ * `char(n)`, `citext`, the enum's own type. MySQL needs to know a time-of-day
105
+ * member (`SqlDialect.timeOfDayInStrategy`); SQLite needs nothing.
106
+ */
107
+ export type KeyListMember = {
108
+ /** The column's scalar type; `"Enum"` for an enum column. */
109
+ readonly type: ScalarType | "Enum";
110
+ /** The declared `@db.*` native type, verbatim (`VarChar(191)`, `Uuid`, `Timestamptz(3)`). */
111
+ readonly nativeType?: string;
112
+ /**
113
+ * A database-stored (native) enum's type: `namespace` qualifies it, absence
114
+ * resolves it through the search path like the statement's table. Absent on
115
+ * a string-stored enum, whose column is text.
116
+ */
117
+ readonly enumType?: {
118
+ readonly namespace?: string;
119
+ readonly name: string;
120
+ };
121
+ /** A time-of-day column (`@db.Time(n)` / `@db.Timetz(n)`). */
122
+ readonly timeOfDay?: boolean;
123
+ };
91
124
  export type SqlExpr = {
92
125
  readonly kind: "composed";
93
126
  readonly fragment: SqlFragment;
@@ -133,6 +166,68 @@ export type SqlExpr = {
133
166
  /** Candidate tuples — every row carries exactly `columns.length` values. */
134
167
  readonly rows: readonly (readonly unknown[])[];
135
168
  readonly negate?: boolean;
169
+ }
170
+ /**
171
+ * KEY-LIST membership — the public `where: { IN: [{ a, b }, …] }` filter:
172
+ * rows whose ordered tuple of column values equals one of `rows`, with
173
+ * semi-join semantics (a repeated tuple never repeats a result row). Renders
174
+ * per `SqlDialect.keyListStrategy`: postgres binds one typed ARRAY per
175
+ * member, `("a", "b") IN (SELECT * FROM unnest($1::int8[], $2::text[]))`,
176
+ * so the text and the planning cost stay constant however long the list is
177
+ * (an OR of 1 000 conjunctions planned in 114 ms on PGlite, 2 ms on
178
+ * postgres 18; the unnest form in 0.1–0.2 ms on both). A `DateTime` member
179
+ * without a declared native type switches postgres to the row-constructor
180
+ * list of untyped parameters (`keyListBindsUntypedRows`: the column may be a
181
+ * zone-less `timestamp`). MySQL renders the row-constructor list, SQLite
182
+ * `IN (VALUES …)`. Each member compares under the column's own type and
183
+ * collation, exactly like `"col" = $n`.
184
+ *
185
+ * An empty `rows` renders `1 = 0` (matches nothing, like an empty
186
+ * `inArray`). The runtime emits arity 1 as `inArray` instead, so a
187
+ * one-field key list keeps the scalar `in` SQL.
188
+ */
189
+ | {
190
+ readonly kind: "keyList";
191
+ /** Ordered tuple columns (compare references); `rows[i][j]` binds to `columns[j]`. */
192
+ readonly columns: readonly ColRef[];
193
+ /** How each member binds, in column order — see {@link KeyListMember}. */
194
+ readonly members: readonly KeyListMember[];
195
+ /** Encoded candidate tuples, each exactly `columns.length` long; may repeat. */
196
+ readonly rows: readonly (readonly unknown[])[];
197
+ }
198
+ /**
199
+ * ROW-VALUE comparison: `("a", "b") > ($1, $2)` — the keyset seek (D-01).
200
+ * SQL compares row values left to right, each pair with that column's own
201
+ * operator, affinity and collation, which is exactly the OR expansion
202
+ * `"a" > $1 OR ("a" = $2 AND "b" > $3)` when no member is NULL — and an
203
+ * index on `(a, b)` bounds it as one range (PostgreSQL `Index Cond: ROW(…) >
204
+ * ROW(…)`, SQLite `SEARCH … (a>?)`). A NULL member makes the comparison
205
+ * UNKNOWN as soon as the scan reaches it, so the runtime emits it only over
206
+ * non-null keys and values. Renders on every dialect (each has row
207
+ * constructors); whether the runtime emits it is
208
+ * `SqlDialect.keysetSeekStrategy`.
209
+ */
210
+ | {
211
+ readonly kind: "rowCompare";
212
+ readonly op: "gt" | "gte" | "lt" | "lte";
213
+ /** Ordered row members (compare references); `values[i]` binds to `columns[i]`. */
214
+ readonly columns: readonly ColRef[];
215
+ /** Encoded values, exactly `columns.length` of them. */
216
+ readonly values: readonly unknown[];
217
+ }
218
+ /**
219
+ * SUBQUERY membership: `"id" IN (SELECT …)`, or the row-value form
220
+ * `("a", "b") IN (SELECT …)` for a composite key — the key-first `distinct`
221
+ * page on PostgreSQL (D-04). The subquery projects exactly `columns.length`
222
+ * columns and shares the statement's parameter sequence. MySQL refuses a
223
+ * `LIMIT` directly inside an `IN` subquery (error 1235), so the renderer
224
+ * refuses that pair there with `VIBE_UNSUPPORTED_CAPABILITY`.
225
+ */
226
+ | {
227
+ readonly kind: "inSelect";
228
+ /** The tested columns, in the subquery's projection order. */
229
+ readonly columns: readonly ColRef[];
230
+ readonly select: SelectStatement;
136
231
  } | {
137
232
  readonly kind: "like";
138
233
  readonly column: ColRef;
@@ -367,7 +462,11 @@ export type OrderTerm = OrderByColumn | OrderByAggregate | OrderBySubquery;
367
462
  /**
368
463
  * One `LEFT JOIN LATERAL (…) AS alias ON true` clause aggregating a correlated
369
464
  * subquery into a single JSON value per outer row — the "join" relation-loading
370
- * strategy (postgres/pglite only; gated on `capabilities.lateralJoin`).
465
+ * strategy (postgres/pglite, and MySQL 8.0.14+ since lane J; gated on
466
+ * `capabilities.lateralJoin` and rendered through `SqlDialect.lateralJson`).
467
+ * MySQL renders positional rows only (`JSON_ARRAY`, `JSON_ARRAYAGG`, `ON
468
+ * true`); its aggregate cannot order, so an ordered list level carries a
469
+ * `select.rowNumber` without partition — rendered as the row's last slot.
371
470
  *
372
471
  * Renders (agg `jsonArray`):
373
472
  * `LEFT JOIN LATERAL (SELECT COALESCE(json_agg(to_json("__sub".*)), '[]'::json)
@@ -435,6 +534,37 @@ export type InnerJoin = {
435
534
  readonly right: ColRef;
436
535
  }[];
437
536
  };
537
+ /**
538
+ * One `LEFT JOIN (<select>) AS "alias" ON "alias"."k" = "outer"."c" [AND …]`
539
+ * clause — portable, equality ON pairs only. Built for the to-one relation
540
+ * `orderBy` (`orderBy: { author: { email: "asc" } }`): the target's key and
541
+ * sort columns ride a derived table under RESERVED aliases (`__vibe_ok0`,
542
+ * `__vibe_ov0`), so the outer statement's unqualified column names can never
543
+ * become ambiguous, and the outer ORDER BY names `"alias"."__vibe_ov0"`.
544
+ * Every engine merges such a derived table into a plain join (measured:
545
+ * postgres pulls it up into a hash/nested-loop left join, sqlite flattens it,
546
+ * mysql merges it), so the planner may hash-join instead of evaluating a
547
+ * correlated subquery per candidate row.
548
+ *
549
+ * The builder emits it only for links that match AT MOST ONE target row, so
550
+ * the join keeps the parent row count. The derived select must alias every
551
+ * projected column; its parameters (none today) join the statement's single
552
+ * sequence where the clause renders (after `keyTable`, before
553
+ * `lateralJoins`). A statement with a `rowLock` refuses it: postgres rejects
554
+ * FOR UPDATE on the nullable side of an outer join, and mysql would lock the
555
+ * joined rows too.
556
+ */
557
+ export type LeftJoin = {
558
+ /** Derived-table alias (`__ord1`). */
559
+ readonly alias: string;
560
+ /** The derived table — every projected column carries an `alias`. */
561
+ readonly select: SelectStatement;
562
+ /** Equality pairs ANDed into the ON clause; never empty. */
563
+ readonly on: readonly {
564
+ readonly left: ColRef;
565
+ readonly right: ColRef;
566
+ }[];
567
+ };
438
568
  /**
439
569
  * How one member of a {@link KeyTableJoin} tuple binds inside the derived
440
570
  * table. `value` binds the parameter as it is: it stays COERCIBLE, so a text
@@ -523,6 +653,82 @@ export type RowNumberProjection = {
523
653
  readonly orderBy: readonly OrderTerm[];
524
654
  readonly alias: string;
525
655
  };
656
+ /**
657
+ * A PER-KEY PAGE of one child table as a derived-table FROM
658
+ * (`SelectStatement.keyPage`): for every listed key tuple, the child rows
659
+ * whose key columns equal it, ordered by `select.orderBy` and cut by
660
+ * `select.limit`/`select.offset` — each key's OWN page, plus a rank column
661
+ * (`rankAlias`) that orders the rows inside their key. The paged include's
662
+ * `ROW_NUMBER() OVER (PARTITION BY key …)` window returns the same rows but
663
+ * numbers EVERY child of every listed key first; this form reads one page of
664
+ * primary keys per key through an index on (key columns, order columns),
665
+ * then each page row once by its primary key.
666
+ *
667
+ * Rendered per `SqlDialect.keyPageStrategy`:
668
+ * - `lateral` (postgres): `unnest($1::<type>[], …) AS "__vibe_keys"
669
+ * ("__vibe_k0", …) CROSS JOIN LATERAL (SELECT <identity>, ROW_NUMBER() OVER
670
+ * (PARTITION BY <key> ORDER BY <order>) AS <rank> FROM <table> WHERE <key> =
671
+ * "__vibe_keys"."__vibe_k<i>" … ORDER BY <order> LIMIT … OFFSET …) AS
672
+ * "__vibe_ids" INNER JOIN <table> AS "__vibe_page" ON <identity> pairs`.
673
+ * - `inLimit` (sqlite, which has no LATERAL): the key tuples as `(VALUES …)`
674
+ * (columns `column1`, `column2`, …, as SQLite names a VALUES clause's
675
+ * columns) joined to `<table> AS "__vibe_page"` ON `(<identity>) IN (SELECT
676
+ * <identity> … WHERE <key> = "__vibe_keys"."column<i>" … ORDER BY … LIMIT …)`,
677
+ * the rank re-derived over the fetched page (`ROW_NUMBER() OVER (PARTITION
678
+ * BY <key> ORDER BY <order>)`): its values differ from the window's, its
679
+ * order inside a key does not.
680
+ * - `window` (mysql): refused with `VIBE_UNSUPPORTED_CAPABILITY` — measured
681
+ * no gain there; the runtime keeps the window.
682
+ *
683
+ * The caller guarantees what makes the page equal to the window's: the order
684
+ * is total inside a key (the identity columns are among the key and order
685
+ * columns), the key tuples are distinct and compare exactly (no folding
686
+ * collation). Members bind like the key-list filter's (`KeyListMember`).
687
+ */
688
+ export type KeyPage = {
689
+ /** How each key member binds, in member order (postgres: one typed array each). */
690
+ readonly members: readonly KeyListMember[];
691
+ /** Encoded, distinct key tuples; never empty, each `members.length` long. */
692
+ readonly rows: readonly (readonly unknown[])[];
693
+ /** The child columns each key member equals, in member order (unqualified). */
694
+ readonly keyColumns: readonly ColRef[];
695
+ /** The child's primary-key columns (unqualified) — each page row is read once by them. */
696
+ readonly identity: readonly ColRef[];
697
+ /**
698
+ * ONE key's page: the child `table` (no alias), the projected `columns`
699
+ * (unqualified; transforms and aliases render as usual), an optional extra
700
+ * `where`, the non-empty `orderBy` (plain column terms), `limit`/`offset`.
701
+ * Any other member is refused.
702
+ */
703
+ readonly select: SelectStatement;
704
+ /** Output name of the rank column. */
705
+ readonly rankAlias: string;
706
+ };
707
+ /**
708
+ * Statement-count pass EPIC 4 (round-3 ST-07): bound key tuples as the FROM
709
+ * of a select (`SelectStatement.fromKeys`) — a derived table named
710
+ * `table.name` whose columns are `columns`, one row per tuple. The write
711
+ * follow-up puts it where the parent table would be: the find path's laterals
712
+ * correlate to `"<table.name>"."<column>"` exactly as over the table, but read
713
+ * the RETURNED key values instead of the parent row (a deleted row, or a
714
+ * native-RLS parent whose SELECT policy the per-relation loads never met).
715
+ *
716
+ * Renders where `SqlDialect.keyListStrategy` is `unnest` (postgres) as the
717
+ * typed form of a `VALUES` list — one array per column, typed like the
718
+ * key-list filter's members (`postgresKeyListElementType`), so the text does
719
+ * not depend on the tuple count: `FROM unnest($1::text[], $2::int4[]) AS
720
+ * "Post" ("id", "authorId")`. A `DateTime` member without a declared native
721
+ * type (`keyListBindsUntypedRows`) and every other dialect refuse with
722
+ * `VIBE_UNSUPPORTED_CAPABILITY` — those dialects have no lateral joins either.
723
+ */
724
+ export type KeyRows = {
725
+ /** Output column names of the derived table, in member order. */
726
+ readonly columns: readonly string[];
727
+ /** How each column binds, in column order — see {@link KeyListMember}. */
728
+ readonly members: readonly KeyListMember[];
729
+ /** Encoded tuples; never empty, each exactly `columns.length` long. */
730
+ readonly rows: readonly (readonly unknown[])[];
731
+ };
526
732
  export type SelectStatement = {
527
733
  readonly kind: "select";
528
734
  readonly table: TableRef;
@@ -548,20 +754,56 @@ export type SelectStatement = {
548
754
  * guarantees that arrangement.
549
755
  */
550
756
  readonly distinctOn?: readonly ColRef[];
757
+ /**
758
+ * Plain `SELECT DISTINCT` over the whole projection — every dialect. The
759
+ * query builder sets it only where it returns exactly the rows its
760
+ * `ROW_NUMBER()` distinct emulation would (sqlite/mysql, value-exact keys,
761
+ * projection equal to the distinct fields); never together with
762
+ * `distinctOn` (the renderer refuses the pair).
763
+ */
764
+ readonly distinct?: true;
551
765
  /**
552
766
  * Derived-table FROM: renders `FROM (<fromSelect>) AS "<table.name>"` —
553
767
  * `table.name` is the subquery ALIAS and `table.alias` must be unset. Used
554
768
  * by aggregate-over-a-subset shapes (`aggregate({ take, skip, orderBy })`).
555
769
  */
556
770
  readonly fromSelect?: SelectStatement;
557
- /** Lateral JSON-aggregation joins — postgres-only, see {@link LateralJoin}. */
771
+ /** Lateral JSON-aggregation joins — postgres and MySQL 8.0.14+, see {@link LateralJoin}. */
558
772
  readonly lateralJoins?: readonly LateralJoin[];
559
773
  /** Plain equality INNER JOINs — portable, see {@link InnerJoin}. */
560
774
  readonly innerJoins?: readonly InnerJoin[];
561
775
  /** Key-table join, rendered after `innerJoins` — mysql only, see {@link KeyTableJoin}. */
562
776
  readonly keyTable?: KeyTableJoin;
777
+ /** Fix lane Q2 (D-03): derived-table LEFT JOINs, rendered after `keyTable` — see {@link LeftJoin}. */
778
+ readonly leftJoins?: readonly LeftJoin[];
563
779
  /** ROW_NUMBER window projection — see {@link RowNumberProjection}. */
564
780
  readonly rowNumber?: RowNumberProjection;
781
+ /**
782
+ * Fix lane Q1 (DB-01): derived-table FROM over a per-key page — renders
783
+ * `FROM (<per-key page>) AS "<table.name>"` like `fromSelect` (and never
784
+ * together with it); see {@link KeyPage}.
785
+ */
786
+ readonly keyPage?: KeyPage;
787
+ /**
788
+ * Fix lane Q2 (round 3, DB-07): derived-table FROM over a `UNION ALL` of
789
+ * branch selects — renders `FROM (SELECT * FROM (<branch 0>) AS
790
+ * "__vibe_u0" UNION ALL SELECT * FROM (<branch 1>) AS "__vibe_u1" …) AS
791
+ * "<table.name>"`. Each branch keeps its own ORDER BY and LIMIT: wrapped in
792
+ * its own derived table, a limited branch is valid on all three engines
793
+ * (SQLite refuses ORDER BY/LIMIT on a bare compound member). At least two
794
+ * branches; each a plain `SELECT <columns> FROM <table> [WHERE] [ORDER BY]
795
+ * [LIMIT]` projecting the same output names in the same order; never
796
+ * together with `fromSelect`, `keyPage` or a `table.alias`. Parameters bind
797
+ * branch by branch, in text order.
798
+ */
799
+ readonly fromUnion?: readonly SelectStatement[];
800
+ /**
801
+ * Statement-count pass EPIC 4 (ST-07): derived-table FROM over bound key
802
+ * tuples, named `table.name` with the listed column names — see
803
+ * {@link KeyRows}. Never together with `fromSelect`, `keyPage`, `fromUnion`,
804
+ * a `table.alias` or a `rowLock`.
805
+ */
806
+ readonly fromKeys?: KeyRows;
565
807
  readonly where?: SqlExpr;
566
808
  readonly groupBy?: readonly ColRef[];
567
809
  /** HAVING predicate — grouped selects; may contain `aggCmp` expressions. */
@@ -597,6 +839,19 @@ export type RowLock = {
597
839
  export declare const SQL_DEFAULT_CELL: {
598
840
  readonly __sqlDefault: true;
599
841
  };
842
+ /**
843
+ * Sentinel update-arm cell: the column keeps its current value —
844
+ * `` `col` = `table`.`col` `` (V8, the insert-if-missing upsert). Valid only in
845
+ * an {@link OnConflictUpdate} `update` set, where MySQL's `ON DUPLICATE KEY
846
+ * UPDATE` needs at least one assignment: a self-assignment changes nothing on a
847
+ * duplicate and, unlike `INSERT IGNORE`, leaves every other error an error.
848
+ * Refused in a plain UPDATE's SET list.
849
+ */
850
+ export declare const SQL_KEEP_CELL: {
851
+ readonly __sqlKeep: true;
852
+ };
853
+ /** Is this update cell the {@link SQL_KEEP_CELL} sentinel? */
854
+ export declare function isSqlKeepCell(value: unknown): boolean;
600
855
  /** Is this VALUES cell the {@link SQL_DEFAULT_CELL} sentinel? */
601
856
  export declare function isSqlDefaultCell(value: unknown): boolean;
602
857
  /**
@@ -674,6 +929,26 @@ export type InsertStatement = {
674
929
  readonly onConflict?: OnConflictClause;
675
930
  /** Rendered as RETURNING where the dialect supports it, else omitted+flagged. */
676
931
  readonly returning?: readonly ReturningColumn[];
932
+ /**
933
+ * Bind the rows as ONE typed array per column instead of one parameter per
934
+ * value (`SqlDialect.bulkInsertForm` `"unnest"`, postgres): `INSERT INTO t
935
+ * ("a", "b") SELECT "c1", "c2" FROM unnest($1::text[], $2::int4[]) WITH
936
+ * ORDINALITY AS "__vibe_rows" ("c1", "c2", "ord") ORDER BY "ord"`. The text
937
+ * is the same for 2 rows and for 3 000, and the statement binds one parameter
938
+ * per column. `ORDER BY "ord"` keeps input order (sequence values, the
939
+ * `ON CONFLICT DO NOTHING` winner).
940
+ *
941
+ * One entry per column, in `columns` order: the column's DECLARED type facts
942
+ * (see {@link KeyListMember}; a `Json` entry names its storage through
943
+ * `nativeType`). Each array binds the BASE type (`varchar`, never
944
+ * `varchar(191)` — an explicit modifier cast truncates silently), so the
945
+ * INSERT's own assignment applies the column's length and precision exactly
946
+ * as it does for a VALUES parameter. `rows` stays row-major: the renderer
947
+ * transposes it. Every cell is a plain value, SQL NULL or a
948
+ * {@link JsonParamCell} — never `DEFAULT` or the database clock (the query
949
+ * builder keeps VALUES for those; the renderer refuses them).
950
+ */
951
+ readonly unnest?: readonly KeyListMember[];
677
952
  };
678
953
  /** Conflicting rows are updated with these column values. */
679
954
  export type OnConflictUpdate = {
@@ -705,11 +980,28 @@ export type OnConflictUpdate = {
705
980
  };
706
981
  /**
707
982
  * Conflicting rows are skipped (`createMany({ skipDuplicates: true })`).
708
- * An empty/absent target means "any conflict" — the mysql form (INSERT IGNORE)
709
- * can only express that, so it ignores a bare `columns` target (documented
710
- * degradation) and REFUSES the refined targets below.
983
+ * An empty/absent target means "any conflict" — the mysql form
984
+ * (`ON DUPLICATE KEY UPDATE k = t.k`, dialect `doNothingForm:
985
+ * "duplicateKeyKeep"`) can only express that, so it ignores a bare `columns`
986
+ * target (documented degradation) and REFUSES the refined targets below.
711
987
  */
712
988
  export type OnConflictDoNothing = {
989
+ /**
990
+ * MySQL form only (`duplicateKeyKeep`): the column the duplicate arm assigns
991
+ * to itself (`` `k` = `t`.`k` ``, a no-op). Absent = the first inserted
992
+ * column; an all-default row (no column) must name one.
993
+ */
994
+ readonly keepColumn?: string;
995
+ /**
996
+ * MySQL form only, single-row statements: a decimal literal S (digits only).
997
+ * The keep assignment becomes `` `k` = IF(LAST_INSERT_ID(S), `t`.`k`, `t`.`k`) ``,
998
+ * so a skipped duplicate reports S as the statement's insert id (an inserted
999
+ * row reports its own id, or 0) — the exact skipped verdict that the
1000
+ * affected count cannot give with CLIENT_FOUND_ROWS (an unchanged duplicate
1001
+ * counts 1, like an insert). The builder picks S unequal to the row's
1002
+ * explicit AUTO_INCREMENT value.
1003
+ */
1004
+ readonly duplicateFlag?: string;
713
1005
  readonly columns?: readonly string[];
714
1006
  /**
715
1007
  * Partial-index target predicate, RAW SQL: `ON CONFLICT (cols) WHERE pred
@@ -727,7 +1019,13 @@ export type OnConflictClause = OnConflictUpdate | OnConflictDoNothing;
727
1019
  export type UpdateStatement = {
728
1020
  readonly kind: "update";
729
1021
  readonly table: TableRef;
730
- /** Column → parameter value, or an atomic {@link SetOperation}. */
1022
+ /**
1023
+ * Column → parameter value, or an atomic {@link SetOperation}. As a
1024
+ * {@link ChainStep} only (statement-maxx lane C3), a value may be a
1025
+ * {@link StepColumnRef} to an EARLIER step: `"col" = (SELECT "c" FROM
1026
+ * "step")` — the step must return at most one row (an owner resolved by a
1027
+ * unique where, or an owner INSERT of one row).
1028
+ */
731
1029
  readonly set: Readonly<Record<string, unknown>>;
732
1030
  readonly where?: SqlExpr;
733
1031
  readonly returning?: readonly ReturningColumn[];
@@ -777,6 +1075,12 @@ export type RlsContextStatement = {
777
1075
  readonly kind: "rlsContext";
778
1076
  readonly context: RlsContextIR;
779
1077
  readonly values: Readonly<Record<string, string | null>>;
1078
+ /** PGlite: bind this trusted runtime role first, in the same statement. */
1079
+ readonly role?: string;
1080
+ /** Switch the transaction to READ ONLY in the same statement, after the claims. */
1081
+ readonly readOnly?: boolean;
1082
+ /** Report the backend as `"backendPid"` / `"backendStart"` text columns. */
1083
+ readonly backend?: boolean;
780
1084
  };
781
1085
  /** Trusted pre-existing PGlite execution role; never a request-supplied principal. */
782
1086
  export type RlsRoleStatement = {
@@ -865,6 +1169,15 @@ export type InsertSelectStatement = {
865
1169
  */
866
1170
  readonly source?: ChainRowSource;
867
1171
  readonly where?: SqlExpr;
1172
+ /**
1173
+ * `ON CONFLICT (<columns>) DO NOTHING` (statement-maxx lane C2): a row whose
1174
+ * conflict target already exists is skipped and not returned — the implicit
1175
+ * many-to-many join INSERT, where re-connecting an existing pair is a no-op.
1176
+ * Postgres form only (the chain renders nowhere else); at least one column.
1177
+ */
1178
+ readonly conflictDoNothing?: {
1179
+ readonly columns: readonly string[];
1180
+ };
868
1181
  readonly returning?: readonly ReturningColumn[];
869
1182
  };
870
1183
  /**
package/dist/codecs.d.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  * Database enums ride the String codec. String-backed enums use the field-bound
21
21
  * membership codec below; their wire values are still strings on every dialect.
22
22
  */
23
+ import { VibeError } from "@vibeorm/schema";
23
24
  import type { Dialect, ScalarType } from "@vibeorm/schema";
24
25
  /**
25
26
  * One scalar type's transport rules for one dialect. Both directions see only
@@ -66,6 +67,23 @@ export type WireFidelity = {
66
67
  readonly json?: "parsed" | "text";
67
68
  /** Int/Float columns arrive as JS numbers (`"native"`) or as text (`"text"`). */
68
69
  readonly numbers?: "native" | "text";
70
+ /**
71
+ * Bytes columns arrive as Node `Buffer` instances (`"buffer"`: node-postgres,
72
+ * bun:sql); absent means a plain `Uint8Array` (PGlite). Never prunes a
73
+ * decoder: the runtime's JSON-transport plan reads it, so a `"join"`
74
+ * relation child's bytes (base64 on the wire) materialize in the same class
75
+ * the driver hands the `"query"` strategy — value identity (5.0 lane V).
76
+ */
77
+ readonly bytes?: "buffer";
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).
85
+ */
86
+ readonly zoneLessDateTime?: "utc";
69
87
  };
70
88
  /**
71
89
  * Is this scalar type's DECODE a no-op under the declared wire fidelity?
@@ -113,6 +131,32 @@ export declare function decodeIsNoop(params: {
113
131
  export declare function parseDateTimeText(params: {
114
132
  text: string;
115
133
  }): Date | undefined;
134
+ /**
135
+ * PostgreSQL date/time text with a year of five or more digits or a ` BC` era,
136
+ * in the row text form and the JSON form: `12345-06-07 08:09:10.111+00`,
137
+ * `12345-06-07T08:09:10.111` (zone-less), `0044-03-15T12:00:00.5+00:00 BC`,
138
+ * `0044-03-15 BC` (`date`). ECMA-262 parses none of them (an expanded year
139
+ * needs `+012345`), and a value near the end of year 9999 reaches them
140
+ * through the session zone alone (`9999-12-31 23:59+00` prints as
141
+ * `10000-01-01 00:59+01` east of UTC). Built from its fields, by the rules of
142
+ * {@link parseDateTimeText}: the BC year astronomically (`1 BC` = year 0, the
143
+ * reading of node-postgres), zone-less text as UTC, the fraction truncated to
144
+ * milliseconds, an offset with seconds applied exactly.
145
+ *
146
+ * `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
148
+ * whose zone-less driver parsers fall back to it (one rule, rule 6).
149
+ */
150
+ export declare function parseExtendedDateTimeText(params: {
151
+ text: string;
152
+ }): Date | undefined;
153
+ /**
154
+ * The decode refusal of a non-finite or unparseable DateTime — one error for
155
+ * every DateTime codec. Exported so a statement that refuses such a value
156
+ * itself (the runtime's in-statement value guard, round-trip follow-up D1)
157
+ * reports exactly this error.
158
+ */
159
+ export declare function decodedDateTimeRefusal(): VibeError;
116
160
  /**
117
161
  * The time-of-day codec for a DateTime field's native type, or `undefined`
118
162
  * when the native type is not `time`/`timetz` (the DateTime table row
@@ -122,6 +166,8 @@ export declare function parseDateTimeText(params: {
122
166
  export declare function timeOfDayCodec(params: {
123
167
  nativeType: string | undefined;
124
168
  }): ScalarCodec | undefined;
169
+ /** Canonical calendar-day text only; a driver Date cannot recover the authored day. */
170
+ export declare const localDateCodec: ScalarCodec;
125
171
  /**
126
172
  * Parse stored JSON text at the centralized codec boundary. A supplied context
127
173
  * marks a bound field: its error contains only that context, never the document