@vibeorm/sql 5.9.0 → 5.11.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 (47) hide show
  1. package/dist/ast.d.ts +337 -4
  2. package/dist/codecs.d.ts +106 -12
  3. package/dist/composable.d.ts +44 -4
  4. package/dist/dialect.d.ts +73 -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-json-keys.d.ts +26 -0
  9. package/dist/dialects/mysql-row-rules.d.ts +1 -7
  10. package/dist/dialects/postgres-generated-columns.d.ts +0 -5
  11. package/dist/dialects/postgres-index-expressions.d.ts +1 -11
  12. package/dist/dialects/postgres-rls-readiness.d.ts +0 -2
  13. package/dist/dialects/postgres-rls.d.ts +7 -1
  14. package/dist/dialects/postgres-row-rules.d.ts +1 -24
  15. package/dist/dialects/sqlite-fts.d.ts +2 -2
  16. package/dist/dialects/sqlite-generated-columns.d.ts +1 -12
  17. package/dist/dialects/sqlite-index-expressions.d.ts +1 -24
  18. package/dist/dialects/sqlite-json-equality.d.ts +1 -14
  19. package/dist/dialects/sqlite-row-rules.d.ts +1 -7
  20. package/dist/dialects/sqlite-table-definition.d.ts +0 -19
  21. package/dist/dialects/test-databases.d.ts +96 -0
  22. package/dist/expr-fragment.d.ts +13 -0
  23. package/dist/identifiers.d.ts +1 -4
  24. package/dist/index.d.ts +11 -6
  25. package/dist/index.js +3023 -2064
  26. package/dist/index.js.map +47 -43
  27. package/dist/json-patch.d.ts +0 -14
  28. package/dist/json-path.d.ts +1 -2
  29. package/dist/key-list.d.ts +38 -21
  30. package/dist/native-types.d.ts +3 -1
  31. package/dist/params.d.ts +0 -2
  32. package/dist/query/expressions.d.ts +1 -33
  33. package/dist/query/index.js.map +7 -7
  34. package/dist/query/mutations.d.ts +1 -14
  35. package/dist/query/reusable.d.ts +1 -16
  36. package/dist/query/select.d.ts +1 -10
  37. package/dist/query/sources.d.ts +1 -36
  38. package/dist/query/state.d.ts +1 -77
  39. package/dist/query-plan.d.ts +2 -2
  40. package/dist/result-decoder.d.ts +21 -1
  41. package/dist/row-rules.d.ts +1 -19
  42. package/dist/sql-bindings.d.ts +7 -43
  43. package/dist/sql-handles.d.ts +1 -3
  44. package/dist/sql-input.d.ts +8 -24
  45. package/dist/timestamp-identity.d.ts +6 -12
  46. package/dist/timestamp-text.d.ts +36 -0
  47. 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,20 @@ 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[];
614
+ /**
615
+ * `jsonObject` laterals only: output names of `select.columns` the lateral
616
+ * exposes as typed columns of its own beside the JSON value (`"alias"."name"`
617
+ * in the outer scope). The outer statement reads a key of the row from them
618
+ * without parsing the JSON; they are SQL NULL when the lateral finds no row.
619
+ */
620
+ readonly expose?: readonly string[];
589
621
  /** The correlated child select — may reference outer columns via table-qualified refs. */
590
622
  readonly select: SelectStatement;
591
623
  };
@@ -752,6 +784,175 @@ export type RowNumberProjection = {
752
784
  readonly orderBy: readonly OrderTerm[];
753
785
  readonly alias: string;
754
786
  };
787
+ /**
788
+ * One window column of a select, rendered after `rowNumber`
789
+ * (`SelectStatement.windows`). Together the two kinds let a statement say
790
+ * which of its rows is the first to hold a key:
791
+ *
792
+ * - `ordinal`: `ROW_NUMBER() OVER ([ORDER BY …]) AS "alias"`, the row's
793
+ * position among the rows of its query level, in `orderBy`, or in the order
794
+ * the rows arrive when `orderBy` is empty (`OVER ()`).
795
+ * - `firstOrdinal`: `MIN(<ordinal>) OVER (PARTITION BY …) AS "alias"`, the
796
+ * smallest `ordinal` among the rows that share the partition columns. The
797
+ * database's own equality groups them (NULLs together), whatever the
798
+ * column's type or collation.
799
+ *
800
+ * A window is computed before the `ORDER BY` and `LIMIT` of its own query
801
+ * level: number a page one level outside the select that cuts it.
802
+ */
803
+ export type WindowColumn = {
804
+ readonly kind: "ordinal";
805
+ readonly alias: string;
806
+ readonly orderBy: readonly OrderTerm[];
807
+ } | {
808
+ readonly kind: "firstOrdinal";
809
+ readonly alias: string;
810
+ readonly ordinal: ColRef;
811
+ readonly partitionBy: readonly ColRef[];
812
+ };
813
+ /**
814
+ * One column of a `SelectStatement.fromRow`: an outer column under the name
815
+ * the row gives it.
816
+ */
817
+ export type RowSourceColumn = {
818
+ readonly column: ColRef;
819
+ readonly alias: string;
820
+ };
821
+ /**
822
+ * One common table expression of a select (`SelectStatement.with`):
823
+ * `"name" AS [MATERIALIZED] (<select>)`. `materialized` pins the table to one
824
+ * evaluation whatever the number of references: required when two readers
825
+ * must see the same rows of a select whose rows are numbered in arrival
826
+ * order.
827
+ */
828
+ export type CommonTable = {
829
+ readonly name: string;
830
+ readonly select: SelectStatement;
831
+ readonly materialized?: true;
832
+ };
833
+ /**
834
+ * The place of one value in a positional JSON row ({@link LateralJoin}
835
+ * `rowFormat: "positional"`): its index among the row's `width` values. A row
836
+ * wider than `JSON_TRANSPORT.chunkSize` is an array of chunks, so the renderer
837
+ * needs both numbers to address the value.
838
+ */
839
+ export type JsonSlot = {
840
+ readonly index: number;
841
+ readonly width: number;
842
+ };
843
+ /**
844
+ * One step of a walk through JSON a statement built: into one value of a
845
+ * positional row, or over every element of a list.
846
+ */
847
+ export type JsonStep = {
848
+ readonly kind: "slot";
849
+ readonly slot: JsonSlot;
850
+ } | {
851
+ readonly kind: "elements";
852
+ };
853
+ /**
854
+ * One key column of a {@link JsonKeys} source: `token` is the output name of
855
+ * the key's text as the JSON rows hold it (at `slot` of the row the walk
856
+ * ends at), `alias` the output name of the same key cast back to the
857
+ * column's own type (`member`, typed like a key-list member).
858
+ */
859
+ export type JsonKeyColumn = {
860
+ readonly slot: JsonSlot;
861
+ readonly token: string;
862
+ readonly alias: string;
863
+ readonly member: KeyListMember;
864
+ };
865
+ /**
866
+ * The distinct keys of the rows a JSON walk reaches, as a derived-table FROM
867
+ * (`SelectStatement.fromJsonKeys`). `source` is a JSON column of a common
868
+ * table of the statement (`source.table` names it); `steps` walk from that
869
+ * value to positional rows, each of which holds the text of its key columns
870
+ * in the slots `keys` name. Renders on PostgreSQL as
871
+ *
872
+ * `(SELECT "__vibe_k"."<token>", CAST("__vibe_k"."<token>" AS <type>) AS "<alias>"
873
+ * FROM (SELECT DISTINCT <row> ->> <slot> AS "<token>"
874
+ * FROM json_array_elements((SELECT json_agg(<list>) FROM "<table>" AS "__vibe_s" WHERE <list> IS NOT NULL))
875
+ * AS "__vibe_d0"("value")
876
+ * CROSS JOIN LATERAL json_array_elements("__vibe_d0"."value") AS "__vibe_e0"("value")) AS "__vibe_k")
877
+ * AS "<table.name>"`
878
+ *
879
+ * Each list on the way is read through ONE value that holds that level's
880
+ * lists of every row (an uncorrelated `json_agg`, planned once), and a
881
+ * second list on the way wraps the first walk the same way. PostgreSQL
882
+ * estimates every `json_array_elements` call at 100 rows whatever it reads:
883
+ * walked row by row, the estimate is rows x 100 per list on the way, and two
884
+ * lists over 2 000 rows were priced past the cost where the server's JIT
885
+ * compiler optimizes (measured: 249 ms against 94 ms with JIT off). Read
886
+ * through one value, each list on the way is 100 x 100 rows at any depth and
887
+ * any row count. The price: that one value holds a level's lists of the whole
888
+ * statement, so it is bound by PostgreSQL's 1 GB limit on a single value.
889
+ *
890
+ * The database groups the keys by their text and types each one from that
891
+ * text with the key-list's base types (`postgresKeyListElementType`), so the
892
+ * laterals joined to the derived table compare `child."fk" = "<table.name>"."<alias>"`
893
+ * under the column's own operator and index. A row the walk does not reach
894
+ * (a missing to-one, an empty list) contributes nothing or a NULL key.
895
+ * Needs the `sharedListDictionary` capability; never together with another
896
+ * FROM form, a `table.alias` or a `rowLock`.
897
+ */
898
+ export type JsonKeys = {
899
+ readonly source: ColRef;
900
+ readonly steps: readonly JsonStep[];
901
+ readonly keys: readonly JsonKeyColumn[];
902
+ };
903
+ /**
904
+ * The rows of a common table, read back through arrays, as a FROM
905
+ * (`SelectStatement.fromTableArrays`): `columns` of the table `source`, each
906
+ * under the output name `alias`, in the order of the column `orderBy`.
907
+ * Renders on PostgreSQL as
908
+ *
909
+ * `(SELECT array_agg("<source>"."<name>" ORDER BY "<source>"."<orderBy>") AS "__vibe_a0", …
910
+ * FROM "<source>") AS "__vibe_a"
911
+ * CROSS JOIN LATERAL unnest("__vibe_a"."__vibe_a0", …) AS "<table.name>"("<alias>", …)`
912
+ *
913
+ * It returns the rows a plain read of the table would, typed as the table's
914
+ * columns (no text on the way), and exists for two things the plain read
915
+ * does not give. The estimate: PostgreSQL expects 10 rows from `unnest` of an
916
+ * array it cannot see, while it expects one row per input row from a table
917
+ * grouped by a key it can trace to a unique column, and every lateral joined
918
+ * to the rows is priced once per expected row (measured: 20 000 rows of 10
919
+ * keys priced at 3.1 million against 129 000, which decides whether the
920
+ * server's JIT compiler runs). The order: laterals joined to the rows read
921
+ * their tables in `orderBy` order, not in a hash table's (measured: the same
922
+ * 20 000 index lookups in 440 ms against 1 066 ms). Needs the `carrierRows`
923
+ * capability on a dialect whose `lateralJson.carriedLists` is `"firstRow"`;
924
+ * never together with another FROM form, a `table.alias` or a `rowLock`.
925
+ */
926
+ export type TableArrays = {
927
+ readonly source: string;
928
+ readonly columns: readonly {
929
+ readonly name: string;
930
+ readonly alias: string;
931
+ }[];
932
+ readonly orderBy: string;
933
+ };
934
+ /**
935
+ * A JSON list a statement returns on its first row only
936
+ * (`SelectStatement.firstRowLists`): `list` describes the list as a
937
+ * `jsonArray` lateral does (its select, row format and column alias; the join
938
+ * alias is unused), `ordinal` is the statement's row ordinal (see
939
+ * {@link WindowColumn} `ordinal`). Renders as a projection:
940
+ *
941
+ * `CASE WHEN <ordinal> = 1 THEN (SELECT COALESCE(json_agg(<row>), '[]'::json)
942
+ * FROM (<list.select>) AS "__sub") END AS "<list.columnAlias>"`
943
+ *
944
+ * The select must not read the statement's own row: an uncorrelated scalar
945
+ * subquery is planned and costed once and runs once, when the first row asks
946
+ * for it. A lateral gated on the ordinal returns the same rows, but
947
+ * PostgreSQL prices it once per row of the statement (measured: 500 rows
948
+ * priced the list 500 times, the estimate passed `jit_above_cost` and the
949
+ * statement spent 100 ms compiling against 5 ms running). Every other row
950
+ * holds SQL NULL. Needs the `sharedListDictionary` capability.
951
+ */
952
+ export type FirstRowList = {
953
+ readonly ordinal: ColRef;
954
+ readonly list: LateralJoin;
955
+ };
755
956
  /**
756
957
  * A PER-KEY PAGE of one child table as a derived-table FROM
757
958
  * (`SelectStatement.keyPage`): for every listed key tuple, the child rows
@@ -844,6 +1045,14 @@ export type GroupingSets = {
844
1045
  };
845
1046
  export type SelectStatement = {
846
1047
  readonly kind: "select";
1048
+ /**
1049
+ * Common table expressions, rendered ahead of the select (`WITH a AS (…),
1050
+ * b AS (…) SELECT …`) and bound first, in order. The statement and the
1051
+ * tables after it read a table by its name as a plain `table`. Only on the
1052
+ * outermost select of a statement; needs the `sharedListDictionary`
1053
+ * capability. See {@link CommonTable}.
1054
+ */
1055
+ readonly with?: readonly CommonTable[];
847
1056
  readonly table: TableRef;
848
1057
  /** Empty array renders as `SELECT *` (unless `selectExpr` replaces it). */
849
1058
  readonly columns: readonly ColRef[];
@@ -893,6 +1102,39 @@ export type SelectStatement = {
893
1102
  readonly leftJoins?: readonly LeftJoin[];
894
1103
  /** ROW_NUMBER window projection — see {@link RowNumberProjection}. */
895
1104
  readonly rowNumber?: RowNumberProjection;
1105
+ /** Window columns, rendered after `rowNumber` — see {@link WindowColumn}. */
1106
+ readonly windows?: readonly WindowColumn[];
1107
+ /** JSON lists of the first row, rendered after `windows` — see {@link FirstRowList}. */
1108
+ readonly firstRowLists?: readonly FirstRowList[];
1109
+ /**
1110
+ * Every column of the statement's FROM, ahead of the other projections:
1111
+ * `SELECT "<from>".*, …`. With `fromSelect` and an empty `columns` list (a
1112
+ * level that adds window columns to the level below it re-exposes that
1113
+ * level's columns without naming each one again), or over a common table
1114
+ * of the statement's own `with`, read under `table.alias ?? table.name`,
1115
+ * where the other projections follow it.
1116
+ */
1117
+ readonly allColumns?: true;
1118
+ /**
1119
+ * Derived-table FROM of exactly ONE row built from outer columns: renders
1120
+ * `FROM (SELECT <column> AS "<alias>", …) AS "<table.name>"`. Inside a
1121
+ * lateral it gives outer values the column names of a table row, so the
1122
+ * laterals joined to it correlate as they would to that table's row
1123
+ * (`child."fk" = "<table.name>"."id"`) and the table is not read again.
1124
+ * Needs the `carrierRows` capability; never together with `fromSelect`,
1125
+ * `keyPage`, `fromUnion`, `fromKeys`, a `table.alias` or a `rowLock`.
1126
+ */
1127
+ readonly fromRow?: readonly RowSourceColumn[];
1128
+ /**
1129
+ * Derived-table FROM over the distinct, typed keys of a JSON walk, named
1130
+ * `table.name` — see {@link JsonKeys}.
1131
+ */
1132
+ readonly fromJsonKeys?: JsonKeys;
1133
+ /**
1134
+ * FROM over the rows of a common table read back through arrays, named
1135
+ * `table.name` — see {@link TableArrays}.
1136
+ */
1137
+ readonly fromTableArrays?: TableArrays;
896
1138
  /**
897
1139
  * Fix lane Q1 (DB-01): derived-table FROM over a per-key page — renders
898
1140
  * `FROM (<per-key page>) AS "<table.name>"` like `fromSelect` (and never
@@ -1044,7 +1286,18 @@ export type ReturningColumn = string | {
1044
1286
  } | {
1045
1287
  readonly name: string;
1046
1288
  readonly transform: "jsonText";
1047
- } | CaptureReturningColumn | OtherRowReturningColumn;
1289
+ } | CaptureReturningColumn | OtherRowReturningColumn | ServerVersionReturningColumn;
1290
+ /**
1291
+ * The server's `server_version_num` as text, exposed as `alias`:
1292
+ * `pg_catalog.current_setting('server_version_num') AS "alias"`. Rides a write
1293
+ * whose client does not know the server version yet, so the next statement can
1294
+ * take the form that version allows without a read of its own. Dialects with
1295
+ * the `rowCapture` capability only (the version decides whether it applies).
1296
+ */
1297
+ export type ServerVersionReturningColumn = {
1298
+ readonly serverVersion: true;
1299
+ readonly alias: string;
1300
+ };
1048
1301
  /**
1049
1302
  * A RETURNING column read from a row OTHER than the one the statement wrote,
1050
1303
  * exposed as `alias` and projected like the written row's own column
@@ -1201,6 +1454,42 @@ export type OnConflictUpdate = {
1201
1454
  * mysql's ODKU has no WHERE and refuses it (`VIBE_UNSUPPORTED_CAPABILITY`).
1202
1455
  */
1203
1456
  readonly updateWhere?: SqlExpr;
1457
+ /**
1458
+ * Skip-unchanged guard: the conflicting row is updated only when at least
1459
+ * one of these columns' stored value differs, NULL counting as a value,
1460
+ * from the value its `update` assignment would write (each column must have
1461
+ * one). Rendered as the first member of the update arm's `WHERE`, AND-ed
1462
+ * with {@link OnConflictUpdate.updateWhere}; a skipped row writes nothing
1463
+ * and returns nothing. On ON DUPLICATE KEY UPDATE (no WHERE) the columns it
1464
+ * leaves out are assigned first, keeping their stored value when nothing
1465
+ * else would change, and the dialect itself skips the unchanged row.
1466
+ * Needs `upsertSkipUnchanged`.
1467
+ */
1468
+ readonly skipUnchanged?: readonly UnchangedColumn[];
1469
+ /**
1470
+ * The table's native row-security predicate, RAW SQL without parameters
1471
+ * (the managed policy's check, columns qualified by the target table's
1472
+ * name). With a guard on the arm ({@link OnConflictUpdate.updateWhere} or
1473
+ * {@link OnConflictUpdate.skipUnchanged}) the arm's `WHERE` becomes
1474
+ * `(<policy>) IS NOT TRUE OR (<guard>)`: PostgreSQL evaluates that `WHERE`
1475
+ * before its row-security check of the conflicting row, so a row the policy
1476
+ * hides must always take the update path and fail that check, whatever its
1477
+ * values. Ignored when the arm has no guard; refused by a dialect without
1478
+ * `nativeRowLevelSecurity`.
1479
+ */
1480
+ readonly rowPolicy?: string;
1481
+ /**
1482
+ * MySQL only, a single-row statement: the duplicate arm also runs
1483
+ * `` `column` = IF(LAST_INSERT_ID(flag), `t`.`column`, `t`.`column`) `` (a
1484
+ * self-assignment, rendered last), so the statement reports `flag` as its
1485
+ * insert id exactly when the row took the update arm — the one signal that
1486
+ * tells an inserted row from an unchanged one when the driver reports no
1487
+ * `Duplicates:` info. `flag` is a decimal literal.
1488
+ */
1489
+ readonly duplicateFlag?: {
1490
+ readonly column: string;
1491
+ readonly flag: string;
1492
+ };
1204
1493
  /**
1205
1494
  * MySQL only (`upsertForm: "onDuplicateKey"`): append
1206
1495
  * `` `col` = LAST_INSERT_ID(`col`) `` to the update list so the UPDATE arm
@@ -1210,6 +1499,41 @@ export type OnConflictUpdate = {
1210
1499
  */
1211
1500
  readonly lastInsertIdColumn?: string;
1212
1501
  };
1502
+ /**
1503
+ * One column a skip-unchanged conflict arm compares. `json`: a Json column,
1504
+ * compared as `jsonb` on postgres (its `json` type has no equality operator;
1505
+ * `"list"` for a Json list).
1506
+ */
1507
+ export type UnchangedColumn = {
1508
+ readonly column: string;
1509
+ readonly json?: "scalar" | "list";
1510
+ /**
1511
+ * A column whose SQL equality can be coarser than "the stored value would
1512
+ * not change"; the renderer compares it exactly where the dialect needs it.
1513
+ * See {@link UnchangedExactness}.
1514
+ */
1515
+ readonly bytes?: UnchangedExactness;
1516
+ /**
1517
+ * A text column the schema declares with a folding type or collation
1518
+ * (`citext`, a `@collation` that is not byte-ordered): its equality may call
1519
+ * `'x'` and `'X'` equal on any dialect, not only under MySQL's default.
1520
+ */
1521
+ readonly folds?: true;
1522
+ /** The column is a list of `bytes`'s kind (a postgres array). */
1523
+ readonly list?: true;
1524
+ };
1525
+ /**
1526
+ * Why a compared column's own equality may call two different stored values
1527
+ * equal:
1528
+ * - `"text"`: a text column; MySQL's default `_ai_ci` collation folds case
1529
+ * and accents (and see `folds`).
1530
+ * - `"decimal"`: a Decimal column without a declared scale (PostgreSQL's bare
1531
+ * `numeric` keeps `1.50` and `1.5` apart, its equality does not).
1532
+ * - `"float"`: a float column (`-0` equals `0`).
1533
+ * - `"other"`: a Json or list column, or a text-like column of another
1534
+ * declared type, compared by its bytes on MySQL.
1535
+ */
1536
+ export type UnchangedExactness = "text" | "decimal" | "float" | "other";
1213
1537
  /**
1214
1538
  * Conflicting rows are skipped (`createMany({ skipDuplicates: true })`).
1215
1539
  * An empty/absent target means "any conflict" — the mysql form
@@ -1528,6 +1852,15 @@ export type ChainResult = {
1528
1852
  * guard, so it is evaluated exactly once, on the one guaranteed result row.
1529
1853
  */
1530
1854
  readonly veto?: readonly ChainVeto[];
1855
+ /**
1856
+ * A step whose rows every veto depends on: when it returned no row, no veto
1857
+ * is checked and the statement succeeds, answering the one guaranteed row
1858
+ * with NULL columns and the tally (rendered as the first CASE arm, `WHEN NOT
1859
+ * EXISTS (SELECT 1 FROM "step") THEN <tally>`). A nested write whose
1860
+ * missing parent is an answer, not an error, gates its children's vetoes on
1861
+ * the parent step this way. Requires `tally` and at least one veto.
1862
+ */
1863
+ readonly vetoGate?: string;
1531
1864
  /**
1532
1865
  * One column `alias` on every result row: the number of rows the listed
1533
1866
  * 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;