@vibeorm/sql 2.5.0 → 3.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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ast.d.ts +331 -11
  3. package/dist/codecs.d.ts +48 -0
  4. package/dist/dialect.d.ts +44 -0
  5. package/dist/dialects/postgres-index-keys.d.ts +1 -0
  6. package/dist/dialects/postgres-physical-catalog.d.ts +7 -1
  7. package/dist/dialects/sqlite-fts.d.ts +28 -0
  8. package/dist/index.d.ts +9 -5
  9. package/dist/index.js +757 -193
  10. package/dist/index.js.map +19 -17
  11. package/dist/json-list-parameter.d.ts +37 -0
  12. package/dist/pg-array.d.ts +13 -0
  13. package/dist/render.d.ts +11 -1
  14. package/package.json +5 -3
  15. package/dist/ast.d.ts.map +0 -1
  16. package/dist/capabilities.d.ts.map +0 -1
  17. package/dist/codecs.d.ts.map +0 -1
  18. package/dist/composable.d.ts.map +0 -1
  19. package/dist/dialect.d.ts.map +0 -1
  20. package/dist/dialects/constraint-owned-index.d.ts.map +0 -1
  21. package/dist/dialects/diagnostics.d.ts.map +0 -1
  22. package/dist/dialects/external-dependents.d.ts.map +0 -1
  23. package/dist/dialects/external-ownership.d.ts.map +0 -1
  24. package/dist/dialects/migration-history.d.ts.map +0 -1
  25. package/dist/dialects/migration-inventory.d.ts.map +0 -1
  26. package/dist/dialects/migration-objects.d.ts.map +0 -1
  27. package/dist/dialects/migration-replay-coverage.d.ts.map +0 -1
  28. package/dist/dialects/migration-shadow.d.ts.map +0 -1
  29. package/dist/dialects/migration-visibility.d.ts.map +0 -1
  30. package/dist/dialects/module-dependency.d.ts.map +0 -1
  31. package/dist/dialects/mysql-check-probe.d.ts.map +0 -1
  32. package/dist/dialects/mysql.d.ts.map +0 -1
  33. package/dist/dialects/postgres-default-identity.d.ts.map +0 -1
  34. package/dist/dialects/postgres-diagnostics.d.ts.map +0 -1
  35. package/dist/dialects/postgres-index-keys.d.ts.map +0 -1
  36. package/dist/dialects/postgres-migration.d.ts.map +0 -1
  37. package/dist/dialects/postgres-physical-catalog.d.ts.map +0 -1
  38. package/dist/dialects/postgres-rls-readiness.d.ts.map +0 -1
  39. package/dist/dialects/postgres-rls.d.ts.map +0 -1
  40. package/dist/dialects/postgres-type-identity.d.ts.map +0 -1
  41. package/dist/dialects/postgres.d.ts.map +0 -1
  42. package/dist/dialects/registry.d.ts.map +0 -1
  43. package/dist/dialects/sqlite-constraints.d.ts.map +0 -1
  44. package/dist/dialects/sqlite-session.d.ts.map +0 -1
  45. package/dist/dialects/sqlite-table-definition.d.ts.map +0 -1
  46. package/dist/dialects/sqlite.d.ts.map +0 -1
  47. package/dist/exclusion.d.ts.map +0 -1
  48. package/dist/identifiers.d.ts.map +0 -1
  49. package/dist/index-columns.d.ts.map +0 -1
  50. package/dist/index.d.ts.map +0 -1
  51. package/dist/json-transport.d.ts.map +0 -1
  52. package/dist/like.d.ts.map +0 -1
  53. package/dist/locks.d.ts.map +0 -1
  54. package/dist/module-journal.d.ts.map +0 -1
  55. package/dist/native-enum-parameter.d.ts.map +0 -1
  56. package/dist/params.d.ts.map +0 -1
  57. package/dist/pg-array.d.ts.map +0 -1
  58. package/dist/predicate.d.ts.map +0 -1
  59. package/dist/render.d.ts.map +0 -1
  60. package/dist/result-decoder.d.ts.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VibeORM contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/ast.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { NativeEnumArrayParameter } from "./native-enum-parameter.ts";
2
+ import type { JsonListParameter } from "./json-list-parameter.ts";
2
3
  import type { SqlFragment } from "./composable.ts";
3
4
  import type { RlsReadinessQuery } from "./dialects/postgres-rls-readiness.ts";
4
5
  /**
@@ -15,11 +16,15 @@ export type ColRef = {
15
16
  readonly table?: string;
16
17
  readonly name: string;
17
18
  /**
18
- * Projection-only wire transform, postgres lateral joins only (SQL review
19
- * B3): raw `to_jsonb` would ship BigInt/Decimal as float64 JSON numbers and
20
- * Bytes as `\x` hex text, so the subselect casts instead — `castText`
21
- * renders `"col"::text AS "col"`, `encodeBase64` renders
22
- * `encode("col", 'base64') AS "col"`. Never set on filter/order refs.
19
+ * Projection-only wire transform, postgres only. `castText` renders
20
+ * `"col"::text AS "col"`, `encodeBase64` renders
21
+ * `encode("col", 'base64') AS "col"`. Two users: the lateral subselect (SQL
22
+ * review B3 — raw `to_jsonb` would ship BigInt/Decimal as float64 JSON
23
+ * numbers and Bytes as `\x` hex text), and every row projection of a
24
+ * `Float[]`/`Json[]` column (bun:sql's array parser refuses float arrays
25
+ * printed with an exponent and json arrays holding integers beyond int32;
26
+ * the runtime parses the array literal text itself). Never set on
27
+ * filter/order refs.
23
28
  */
24
29
  readonly transform?: "castText" | "encodeBase64";
25
30
  /**
@@ -30,6 +35,14 @@ export type ColRef = {
30
35
  * storage and read-back stay the exact text.
31
36
  */
32
37
  readonly castReal?: boolean;
38
+ /**
39
+ * Order/distinct/compare cast, postgres `json` columns only (`Json @db.Json`,
40
+ * release-3.0 D3): `json` has no ordering or equality operator, so ORDER BY,
41
+ * DISTINCT ON and comparison sites render `"col"::jsonb` (`"col"::jsonb[]`
42
+ * for a `json[]` list column) — the order and distinctness of a jsonb
43
+ * column. Never set on projections, so read-back keeps the stored text.
44
+ */
45
+ readonly castJsonb?: "jsonb" | "jsonb[]";
33
46
  /**
34
47
  * Projection alias: renders `ref AS "alias"`. PROJECTION-ONLY — filter,
35
48
  * order and correlation refs never set it (the renderer ignores it outside
@@ -93,6 +106,11 @@ export type SqlExpr = {
93
106
  readonly column: ColRef;
94
107
  readonly values: readonly unknown[];
95
108
  readonly negate?: boolean;
109
+ /**
110
+ * The column holds a time of day (`@db.Time(n)` / `@db.Timetz(n)`): the
111
+ * dialect renders the membership per `SqlDialect.timeOfDayInStrategy`.
112
+ */
113
+ readonly timeOfDay?: boolean;
96
114
  }
97
115
  /**
98
116
  * ORDERED-TUPLE membership: `("a", "b") IN ((?, ?), (?, ?))`. The composite
@@ -183,13 +201,13 @@ export type SqlExpr = {
183
201
  | {
184
202
  readonly kind: "arrayContainsAll";
185
203
  readonly column: ColRef;
186
- readonly values: readonly unknown[] | string | NativeEnumArrayParameter;
204
+ readonly values: readonly unknown[] | string | NativeEnumArrayParameter | JsonListParameter;
187
205
  }
188
206
  /** Scalar-list overlap: `"col" && $n` (one array param) — Prisma's `hasSome`. Postgres only. */
189
207
  | {
190
208
  readonly kind: "arrayOverlaps";
191
209
  readonly column: ColRef;
192
- readonly values: readonly unknown[] | string | NativeEnumArrayParameter;
210
+ readonly values: readonly unknown[] | string | NativeEnumArrayParameter | JsonListParameter;
193
211
  }
194
212
  /**
195
213
  * Scalar-list emptiness via `array_length("col", 1) IS [NOT] NULL` — the v1
@@ -216,6 +234,15 @@ export type SqlExpr = {
216
234
  readonly value: unknown;
217
235
  readonly insensitive?: boolean;
218
236
  readonly negate?: boolean;
237
+ /**
238
+ * The column's postgres type: absent (or `jsonb`) for the default
239
+ * `jsonb`, `json` for `Json @db.Json`. Postgres defines `=`, `<>` and
240
+ * `@>` for jsonb only, so on a `json` column every op casts the
241
+ * EXTRACTED value to jsonb (the string ops then read it as text), which
242
+ * gives a jsonb column's answer. Sqlite and mysql have one JSON type
243
+ * and ignore it.
244
+ */
245
+ readonly storage?: "jsonb" | "json";
219
246
  }
220
247
  /**
221
248
  * A bound extension fragment: literal text, bound parameters and quoted
@@ -369,6 +396,24 @@ export type LateralJoin = {
369
396
  /** The correlated child select — may reference outer columns via table-qualified refs. */
370
397
  readonly select: SelectStatement;
371
398
  };
399
+ /**
400
+ * One correlated scalar `COUNT(*)` projected into a select list:
401
+ * `(SELECT COUNT(*) FROM … WHERE <correlation>) AS "alias"` — the relation
402
+ * `_count` of the round-trip campaign's R-03b, which saves the separate grouped
403
+ * count statement. Portable: postgres, sqlite and mysql all evaluate a
404
+ * correlated scalar subquery once per output row; the runtime wraps a paged
405
+ * root in a derived table first, so the subqueries run only for the page.
406
+ * The select must be ONE ungrouped `COUNT(*)` (`columns: []`, `selectExpr`
407
+ * countAll, no grouping, paging, distinct or lock) — anything else is refused
408
+ * at render. Its parameters join the statement's single sequence in text order
409
+ * (the select list renders before FROM).
410
+ */
411
+ export type CountSubquery = {
412
+ /** Output column the count is exposed under (`__vibe_count_0`). */
413
+ readonly alias: string;
414
+ /** The correlated count select — references outer columns via table-qualified refs. */
415
+ readonly select: SelectStatement;
416
+ };
372
417
  /**
373
418
  * One `INNER JOIN "table" AS alias ON left = right [AND …]` clause — plain
374
419
  * equality joins only, portable across all dialects. Added for the
@@ -384,6 +429,75 @@ export type InnerJoin = {
384
429
  readonly right: ColRef;
385
430
  }[];
386
431
  };
432
+ /**
433
+ * How one member of a {@link KeyTableJoin} tuple binds inside the derived
434
+ * table. `value` binds the parameter as it is: it stays COERCIBLE, so a text
435
+ * column's OWN collation decides `=` — the equality `col IN (?, …)` uses.
436
+ * `integer` and `decimal` cast the parameter into the column's numeric family:
437
+ * BigInt and Decimal values travel as strings, and a derived-table string
438
+ * compared with a numeric column goes through a double (measured on mysql:
439
+ * `9007199254740993` matched `9007199254740992`).
440
+ *
441
+ * `integer` renders `CAST(? AS SIGNED)`. `decimal` renders
442
+ * `CAST(? AS DECIMAL(65, scale))` with the tested column's own scale: 65 is
443
+ * MySQL's maximum precision, so the cast holds every value a
444
+ * `DECIMAL(p, scale)` column can hold. A fixed `DECIMAL(65,30)` held only 35
445
+ * integer digits and clipped a `Decimal(65, 0)` key onto its maximum, which
446
+ * matched nothing (review finding 1); the column's own precision would clip a
447
+ * wider value onto the column's maximum and pair it with a row it does not
448
+ * equal (measured: `CAST('100000' AS DECIMAL(5,0))` is `99999`). `scale` is
449
+ * 0–30, MySQL's range.
450
+ *
451
+ * `citext` (postgres only) renders `CAST($n AS citext)`: a postgres `citext`
452
+ * key compares case-insensitively, and a bare VALUES parameter would be
453
+ * `text`, whose `=` against a citext column is the case-sensitive text
454
+ * operator. A VALUES cell has no column to infer a type from, so every member
455
+ * of a postgres key table binds typed: `citext`, `text` (`CAST($n AS text)`,
456
+ * a text or varchar column) or `integer` (`CAST($n AS bigint)`, exact against
457
+ * any integer column) — any other member refuses on postgres. `text` is
458
+ * postgres-only; mysql binds text members as `value`.
459
+ */
460
+ export type KeyTableMember = {
461
+ readonly kind: "value";
462
+ } | {
463
+ readonly kind: "integer";
464
+ } | {
465
+ readonly kind: "decimal";
466
+ readonly scale: number;
467
+ } | {
468
+ readonly kind: "citext";
469
+ } | {
470
+ readonly kind: "text";
471
+ };
472
+ /**
473
+ * The batch's key tuples as a derived table, each tagged with its ORDINAL
474
+ * (row `i` carries `i`), INNER JOINed on the tested columns — mysql:
475
+ * `INNER JOIN (VALUES ROW(?, 0), ROW(?, 1)) AS \`alias\` (\`__vibe_k0\`, \`ordinal\`)
476
+ * ON col = \`alias\`.\`__vibe_k0\``; postgres only over `citext` members
477
+ * (`(VALUES (CAST($1 AS citext), 0), …)`, release 3.0 nested writes).
478
+ * Member columns are named `__vibe_k<i>` (a reserved prefix, so unqualified
479
+ * child columns never collide with them).
480
+ *
481
+ * Why it exists (EPIC R): a relation load used to stitch children to parents
482
+ * by comparing decoded keys in JavaScript, while mysql's collations equate
483
+ * spellings (`acme` = `Acme` = `ACME`, `cafe` = `Café`, `x ` = `x`) that
484
+ * JavaScript keeps apart — the foreign key and the `IN` filter accepted the
485
+ * child, the stitch dropped it. Joined this way the DATABASE pairs every child
486
+ * row with every batch key it considers equal, and the projected ordinal names
487
+ * the key; the join is also the membership filter.
488
+ */
489
+ export type KeyTableJoin = {
490
+ /** Derived-table alias (`__vibe_keys`); the ordinal projects as `alias.ordinalAlias`. */
491
+ readonly alias: string;
492
+ /** The statement's columns, compared member by member (`=`), in tuple order. */
493
+ readonly columns: readonly ColRef[];
494
+ /** How each member binds, in tuple order — see {@link KeyTableMember}. */
495
+ readonly members: readonly KeyTableMember[];
496
+ /** Encoded key tuples; never empty, each as long as `columns`. */
497
+ readonly rows: readonly (readonly unknown[])[];
498
+ /** Output name of the ordinal column. */
499
+ readonly ordinalAlias: string;
500
+ };
387
501
  /**
388
502
  * A `ROW_NUMBER() OVER (PARTITION BY … [ORDER BY …]) AS "alias"` projection,
389
503
  * appended after `columns` — the distinct emulation on dialects without
@@ -413,6 +527,8 @@ export type SelectStatement = {
413
527
  * combined with `selectExpr`.
414
528
  */
415
529
  readonly aggregates?: readonly AggSelect[];
530
+ /** Correlated `COUNT(*)` subqueries, rendered after `aggregates` — see {@link CountSubquery}. */
531
+ readonly countSubqueries?: readonly CountSubquery[];
416
532
  /**
417
533
  * `SELECT DISTINCT ON (cols)` — postgres only (`capabilities.distinctOn`;
418
534
  * other dialects refuse at render time and the runtime emulates client-side
@@ -430,6 +546,8 @@ export type SelectStatement = {
430
546
  readonly lateralJoins?: readonly LateralJoin[];
431
547
  /** Plain equality INNER JOINs — portable, see {@link InnerJoin}. */
432
548
  readonly innerJoins?: readonly InnerJoin[];
549
+ /** Key-table join, rendered after `innerJoins` — mysql only, see {@link KeyTableJoin}. */
550
+ readonly keyTable?: KeyTableJoin;
433
551
  /** ROW_NUMBER window projection — see {@link RowNumberProjection}. */
434
552
  readonly rowNumber?: RowNumberProjection;
435
553
  readonly where?: SqlExpr;
@@ -482,14 +600,21 @@ export declare function isSqlDefaultCell(value: unknown): boolean;
482
600
  * Sqlite/mysql render the bare placeholder — their Json transport is already
483
601
  * plain text. The COLLECTED parameter is the plain text either way: this
484
602
  * wrapper never crosses the wire.
603
+ *
604
+ * `storage: "json"` marks a postgres `json` column (`Json @db.Json`): the cell
605
+ * renders `$n::text::json`, so the column stores the text as written (key
606
+ * order and spelling kept) instead of jsonb's normalized text (release-3.0
607
+ * D3). Absent = jsonb; dialects without a param cast ignore it.
485
608
  */
486
609
  export type JsonParamCell = {
487
610
  readonly __jsonParam: true;
488
611
  readonly text: string;
612
+ readonly storage?: "json";
489
613
  };
490
614
  /** Wrap a Json column's encoded (stringified) parameter for cast-site rendering. */
491
615
  export declare function jsonParamCell(params: {
492
616
  text: string;
617
+ storage?: "json";
493
618
  }): JsonParamCell;
494
619
  /** Is this VALUES/SET cell a {@link JsonParamCell}? */
495
620
  export declare function isJsonParamCell(value: unknown): value is JsonParamCell;
@@ -511,6 +636,17 @@ export declare function excludedRef(params: {
511
636
  }): ExcludedRef;
512
637
  /** Is this SET cell an {@link ExcludedRef}? */
513
638
  export declare function isExcludedRef(value: unknown): value is ExcludedRef;
639
+ /**
640
+ * One RETURNING column: a bare column name renders `"col"`; the object form
641
+ * renders `"col"::text AS "col"` (postgres only) — the `Float[]`/`Json[]`
642
+ * projection rule {@link ColRef.transform} documents, applied to RETURNING.
643
+ */
644
+ export type ReturningColumn = string | {
645
+ readonly name: string;
646
+ readonly castText: true;
647
+ };
648
+ /** The column name a {@link ReturningColumn} exposes in the result row. */
649
+ export declare function returningColumnName(column: ReturningColumn): string;
514
650
  export type InsertStatement = {
515
651
  readonly kind: "insert";
516
652
  readonly table: TableRef;
@@ -520,7 +656,7 @@ export type InsertStatement = {
520
656
  /** Upsert clause: ON CONFLICT (pg/sqlite) or ON DUPLICATE KEY (mysql). */
521
657
  readonly onConflict?: OnConflictClause;
522
658
  /** Rendered as RETURNING where the dialect supports it, else omitted+flagged. */
523
- readonly returning?: readonly string[];
659
+ readonly returning?: readonly ReturningColumn[];
524
660
  };
525
661
  /** Conflicting rows are updated with these column values. */
526
662
  export type OnConflictUpdate = {
@@ -577,13 +713,13 @@ export type UpdateStatement = {
577
713
  /** Column → parameter value, or an atomic {@link SetOperation}. */
578
714
  readonly set: Readonly<Record<string, unknown>>;
579
715
  readonly where?: SqlExpr;
580
- readonly returning?: readonly string[];
716
+ readonly returning?: readonly ReturningColumn[];
581
717
  };
582
718
  export type DeleteStatement = {
583
719
  readonly kind: "delete";
584
720
  readonly table: TableRef;
585
721
  readonly where?: SqlExpr;
586
- readonly returning?: readonly string[];
722
+ readonly returning?: readonly ReturningColumn[];
587
723
  };
588
724
  /** Native RLS DDL, kept distinct from ordinary row-query expressions. */
589
725
  export type RlsEnableStatement = {
@@ -663,7 +799,191 @@ export type AdvisoryLockStatement = {
663
799
  };
664
800
  /** The column an advisory try-lock projects its verdict into. */
665
801
  export declare const ADVISORY_LOCK_RESULT_COLUMN: string;
666
- export type SqlStatement = SelectStatement | InsertStatement | UpdateStatement | DeleteStatement | RlsEnableStatement | RlsForceStatement | RlsDisableStatement | RlsNoForceStatement | RlsDropPolicyStatement | RlsPolicyStatement | RlsContextStatement | RlsRoleStatement | AdvisoryLockStatement;
802
+ /**
803
+ * A cell of an {@link InsertSelectStatement} that reads a column of an EARLIER
804
+ * chain step: `"step"."column"`. A column reference, not a parameter — the
805
+ * value comes from the step's rows inside the same statement (a child row
806
+ * taking its parent's key from the parent step, round-trip campaign EPIC F).
807
+ * The step must be named in the insert's `from` list.
808
+ */
809
+ export type StepColumnRef = {
810
+ readonly __stepColumn: true;
811
+ readonly step: string;
812
+ readonly column: string;
813
+ };
814
+ /** Reference `column` of the earlier chain step `step` from an INSERT … SELECT cell. */
815
+ export declare function stepColumnRef(params: {
816
+ step: string;
817
+ column: string;
818
+ }): StepColumnRef;
819
+ /** Is this INSERT … SELECT cell a {@link StepColumnRef}? */
820
+ export declare function isStepColumnRef(value: unknown): value is StepColumnRef;
821
+ /**
822
+ * `INSERT INTO t (cols) SELECT <cells> [FROM "step", …] [WHERE <expr>]
823
+ * [RETURNING …]` — an INSERT whose ONE row is a SELECT list instead of a
824
+ * VALUES row, so a WHERE can veto the write inside the statement and cells can
825
+ * read earlier steps. Valid ONLY as a {@link ChainStep} statement.
826
+ *
827
+ * Cells take the same forms as a VALUES cell — a codec-encoded value, a
828
+ * {@link JsonParamCell}, a {@link DbNowMarker} — plus {@link StepColumnRef}.
829
+ * The {@link SQL_DEFAULT_CELL} is refused: `DEFAULT` is not allowed in a SELECT
830
+ * list; an omitted column takes its default by being absent from `columns`.
831
+ *
832
+ * Parameter typing (O-03, probed on PostgreSQL): a bare parameter in this
833
+ * one-row SELECT list takes the TARGET column's type, exactly as in VALUES.
834
+ */
835
+ export type InsertSelectStatement = {
836
+ readonly kind: "insertSelect";
837
+ readonly table: TableRef;
838
+ readonly columns: readonly string[];
839
+ /** One cell per column, aligned to `columns` — deterministic parameter order. */
840
+ readonly cells: readonly unknown[];
841
+ /** Earlier chain steps the SELECT reads FROM, in order (absent: a one-row SELECT without FROM). */
842
+ readonly from?: readonly string[];
843
+ /**
844
+ * Several source rows (round-trip campaign EPIC F): the SELECT reads them
845
+ * from a derived table `alias`, joined after the `from` steps, and the
846
+ * `cells` read its columns through `stepColumnRef({ step: alias, column })`.
847
+ * See {@link ChainRowSource} for how the rows are typed.
848
+ */
849
+ readonly source?: ChainRowSource;
850
+ readonly where?: SqlExpr;
851
+ readonly returning?: readonly ReturningColumn[];
852
+ };
853
+ /**
854
+ * Rows an {@link InsertSelectStatement} inserts, as ONE derived table:
855
+ * `(SELECT <cols> FROM (VALUES (<typed NULL row>), ($1, $2), …) AS "alias"
856
+ * (<cols>) OFFSET 1) AS "alias"`.
857
+ *
858
+ * Why the typed NULL row (review O-03): a bare parameter outside the INSERT's
859
+ * own VALUES has no type, and PostgreSQL resolves an all-parameter column of a
860
+ * VALUES list or UNION in FROM as `text` ("column is of type integer but
861
+ * expression is of type text"). The first row is one `(SELECT "col" FROM
862
+ * <typedBy> WHERE FALSE)` per column — a NULL of the TARGET column's exact
863
+ * type (enums, `numeric(p,s)`, `varchar(n)`, domains and arrays included) —
864
+ * so every parameter below takes that type, with no cast map anywhere;
865
+ * `OFFSET 1` drops the row. (A `UNION ALL` of one-row SELECTs types the same
866
+ * way but costs ~10× the planning time at 1000 rows — EPIC F shape gate.)
867
+ *
868
+ * Cells take the VALUES forms: a codec-encoded value, a {@link JsonParamCell}
869
+ * or a {@link DbNowMarker}; the DEFAULT cell is refused (a homogeneous row set
870
+ * omits the column instead). A cell carrying its OWN type (the Json cast, the
871
+ * clock) must agree with the column's type under VALUES' common-type rules,
872
+ * which are stricter than INSERT's assignment casts — the builder keeps such
873
+ * columns out (EPIC F eligibility). Rows are read in list order, which is the
874
+ * order the INSERT consumes them.
875
+ */
876
+ export type ChainRowSource = {
877
+ readonly alias: string;
878
+ /** The table whose columns type the rows — normally the INSERT's own table. */
879
+ readonly typedBy: TableRef;
880
+ readonly columns: readonly string[];
881
+ /** At least one row, each aligned to `columns`. */
882
+ readonly rows: readonly (readonly unknown[])[];
883
+ };
884
+ /** What one chain step may be: a read, or a data-modifying statement with RETURNING. */
885
+ export type ChainStepStatement = SelectStatement | InsertStatement | InsertSelectStatement | UpdateStatement | DeleteStatement;
886
+ /**
887
+ * One named step: rendered `"name" AS (<statement>)`. Later steps and the
888
+ * result read it as a table (`FROM "name"`, `EXISTS (SELECT 1 FROM "name" …)`,
889
+ * a {@link StepColumnRef}). A data-modifying step contributes the rows its
890
+ * RETURNING list names.
891
+ */
892
+ export type ChainStep = {
893
+ readonly name: string;
894
+ readonly statement: ChainStepStatement;
895
+ };
896
+ /**
897
+ * The chain's final SELECT: the rows of `rowsFrom` (in that order, `UNION
898
+ * ALL`), each projecting `columns`.
899
+ *
900
+ * `verdict` adds one column `alias` = `expr` to every row, AND guarantees a
901
+ * row: when the `rowsFrom` steps return nothing, the result is exactly ONE
902
+ * verdict row whose `columns` are all NULL. A caller recognizes it by NULL in
903
+ * a column the steps never return as NULL (a primary key). Accounting
904
+ * consequence: such a statement returns one row per written row OR one
905
+ * verdict row — a caller that counts returned rows as row changes must undo a
906
+ * verdict-row outcome (the upsert fold always rolls it back).
907
+ */
908
+ export type ChainResult = {
909
+ readonly rowsFrom: readonly string[];
910
+ readonly columns: readonly string[];
911
+ readonly verdict?: {
912
+ readonly alias: string;
913
+ readonly expr: SqlExpr;
914
+ };
915
+ /**
916
+ * In-statement vetoes (round-trip campaign EPIC F), checked in order: when
917
+ * `step` returned no row (fewer than `minRows`, EPIC W), the STATEMENT FAILS — so the database undoes every
918
+ * effect of every step, trigger writes included — with an error whose
919
+ * message carries `__vibe_veto:<token>:<nonce>:` ({@link CHAIN_VETO_PREFIX},
920
+ * {@link chainVetoToken}). Requires `tally`: the veto is the tally column's
921
+ * guard, so it is evaluated exactly once, on the one guaranteed result row.
922
+ */
923
+ readonly veto?: readonly ChainVeto[];
924
+ /**
925
+ * One column `alias` on every result row: the number of rows the listed
926
+ * data-modifying steps returned (their RETURNING rows = their direct
927
+ * effects). Guarantees one row like `verdict`. Row-change accounting reads
928
+ * it instead of counting result rows (effect `writeTally`).
929
+ */
930
+ readonly tally?: ChainTally;
931
+ };
932
+ /**
933
+ * Fail the statement when `step` returned no row; `token` names the veto in the
934
+ * error. `nonce` is bound as a PARAMETER into the marker (never SQL text): a
935
+ * per-call random value the caller requires back in the error, so a user value
936
+ * that merely looks like a marker is never read as a veto.
937
+ *
938
+ * `minRows` (EPIC W) makes the veto count-sensitive: the statement fails when
939
+ * `step` returned FEWER rows than that (a multi-row INSERT a trigger shortened).
940
+ * Omitted or 1 = "no row". A positive safe integer, rendered as SQL text.
941
+ */
942
+ export type ChainVeto = {
943
+ readonly step: string;
944
+ readonly token: string;
945
+ readonly nonce: string;
946
+ readonly minRows?: number;
947
+ };
948
+ /** Count the RETURNING rows of `steps` into the result column `alias`. */
949
+ export type ChainTally = {
950
+ readonly alias: string;
951
+ readonly steps: readonly string[];
952
+ };
953
+ /** Marker every chain veto error message carries, followed by `<token>:<nonce>:`. */
954
+ export declare const CHAIN_VETO_PREFIX: string;
955
+ /** Allowed veto tokens: lower-case words — they are rendered into SQL text. */
956
+ export declare const CHAIN_VETO_TOKEN_PATTERN: RegExp;
957
+ /**
958
+ * The veto token a database error message carries for this `nonce`, or
959
+ * `undefined`. Matches the marker anywhere in the text, so a localized server
960
+ * message (`lc_messages`) still yields it: the marker is the invalid VALUE the
961
+ * message quotes. A marker without the caller's nonce is not a veto.
962
+ */
963
+ export declare function chainVetoToken(params: {
964
+ message: string;
965
+ nonce: string;
966
+ }): string | undefined;
967
+ /**
968
+ * ONE statement made of an ordered chain of named steps — reads (a locked
969
+ * probe) and data-modifying statements with RETURNING — ending in a final
970
+ * SELECT over them (round-trip campaign EPIC 4, reused by EPIC F). Renders as
971
+ * `WITH "a" AS (…), "b" AS (…) SELECT …` on a dialect whose
972
+ * `statementChain` strategy names the form (postgres); every other dialect
973
+ * refuses at render time (`VIBE_UNSUPPORTED_CAPABILITY`) — callers gate on
974
+ * the strategy first and keep their multi-statement path there.
975
+ *
976
+ * PostgreSQL semantics the builder relies on: every step sees ONE snapshot
977
+ * (a step never sees another step's writes — only its RETURNING rows), steps
978
+ * that reference a step run after it, and the statement is atomic: an error in
979
+ * any step undoes every step's effects, trigger writes included.
980
+ */
981
+ export type ChainStatement = {
982
+ readonly kind: "chain";
983
+ readonly steps: readonly ChainStep[];
984
+ readonly result: ChainResult;
985
+ };
986
+ export type SqlStatement = SelectStatement | InsertStatement | UpdateStatement | DeleteStatement | RlsEnableStatement | RlsForceStatement | RlsDisableStatement | RlsNoForceStatement | RlsDropPolicyStatement | RlsPolicyStatement | RlsContextStatement | RlsRoleStatement | AdvisoryLockStatement | ChainStatement;
667
987
  /** Readiness statements remain distinct from application queries. */
668
988
  export type RlsReadinessStatement = {
669
989
  readonly kind: "rlsReadiness";
package/dist/codecs.d.ts CHANGED
@@ -113,6 +113,54 @@ export declare function decodeIsNoop(params: {
113
113
  export declare function parseDateTimeText(params: {
114
114
  text: string;
115
115
  }): Date | undefined;
116
+ /**
117
+ * The time-of-day codec for a DateTime field's native type, or `undefined`
118
+ * when the native type is not `time`/`timetz` (the DateTime table row
119
+ * applies). The one native-type rule is `timeOfDayType` in @vibeorm/schema,
120
+ * shared with the dialect refusal in schema validation.
121
+ */
122
+ export declare function timeOfDayCodec(params: {
123
+ nativeType: string | undefined;
124
+ }): ScalarCodec | undefined;
125
+ /**
126
+ * The SHORTEST decimal that round-trips through float32 to the same value as
127
+ * `value` — what postgres itself prints for a `real` (`0.1`, `3.4028235e+38`).
128
+ *
129
+ * A driver that reads a `real` in BINARY format (bun:sql with named
130
+ * statements) hands back the float32 widened to float64:
131
+ * `0.10000000149011612` for a stored `0.1`. Text-format drivers already give
132
+ * the short form, and for those this is the identity — the shortest form of a
133
+ * short value is itself — so it is safe on every driver. Smallest `p` in 1..9
134
+ * with `fround(toPrecision(p)) === fround(value)`; nine significant digits
135
+ * always identify a float32. `±0`, NaN and ±Infinity pass through.
136
+ */
137
+ export declare function shortestFloat32(params: {
138
+ value: number;
139
+ }): number;
140
+ /**
141
+ * `Float @db.Real` — a refinement of the Float codec keyed by the native type,
142
+ * not by the scalar type, so it is not a table row: the runtime routes a
143
+ * field with `nativeType: "Real"` here. Decode = {@link numberCodec} then
144
+ * {@link shortestFloat32}. Real aggregates that postgres returns as `real`
145
+ * (`min`/`max`/`sum`) use it too; `avg` returns double precision and must not.
146
+ */
147
+ export declare const float32Codec: ScalarCodec;
148
+ /**
149
+ * Element decoders for a postgres array that arrived as its TEXT literal
150
+ * (`{1e-07,NULL}`, `{"{\"a\": 1}"}`) — the runtime projects `Float[]`/`Json[]`
151
+ * as `::text`, and drivers return arrays whose type OID they do not know
152
+ * (`uuid[]` on bun:sql; `xml[]`, `bit[]`, `citext[]` on node-postgres) as the
153
+ * literal. Each element is postgres's text output for its type:
154
+ *
155
+ * - Boolean `t`/`f`, Bytes `\x` hex and Json text need their own conversion;
156
+ * - every other scalar's regular postgres codec already accepts the text form
157
+ * (`Number("1e-07")`, `Number("NaN")`, `Number("-0")`, BigInt/Decimal text,
158
+ * `parseDateTimeText` for `2024-06-01 12:34:56.789+00`), so `null` means
159
+ * "decode with the field's own codec".
160
+ */
161
+ export declare function postgresArrayElementFromText(params: {
162
+ scalarType: ScalarType;
163
+ }): ((text: string) => unknown) | null;
116
164
  /** postgres (and pglite — same wire behaviour). */
117
165
  export declare const POSTGRES_CODECS: CodecTable;
118
166
  /** sqlite — no boolean, date or json types; everything rides TEXT/INTEGER. */
package/dist/dialect.d.ts CHANGED
@@ -72,6 +72,14 @@ export type ConflictKeyEquality = {
72
72
  * created otherwise, and sqlite's default collating sequence is BINARY);
73
73
  * mysql does not, because its server default `utf8mb4_0900_ai_ci` ignores
74
74
  * case and accents.
75
+ *
76
+ * Consumers: the batched upsert's duplicate-key check
77
+ * (packages/runtime/src/bulk-upsert.ts) and the relation loader
78
+ * (packages/runtime/src/relation-loader.ts, `associatesInDatabase`). Where
79
+ * this is false, a relation key with a `String` member is paired with its
80
+ * parents by the DATABASE through a key table (`SelectStatement.keyTable`,
81
+ * mysql only) instead of by JavaScript string equality, which would drop
82
+ * children whose key the collation equates (`acme` under `Acme`).
75
83
  */
76
84
  readonly textIsBinaryByDefault: boolean;
77
85
  /** Character capacity of the default String column, if bounded. Also consumed by migrate. */
@@ -100,6 +108,15 @@ export type SqlDialect = {
100
108
  * member, so composite membership always chunks against `paramBudget`.
101
109
  */
102
110
  readonly tupleInStrategy: "rowValue" | "expand";
111
+ /**
112
+ * Membership on a time-of-day column (`inArray` with `timeOfDay`): `in`
113
+ * renders like any other membership; `equalities` renders an OR of `=`
114
+ * comparisons. MySQL 8.4 types a `?` inside a multi-value IN list against a
115
+ * TIME(n) column as TIME(6) and matches nothing unless n is 6 (live on
116
+ * 8.4.11; 26.7 matches); `=` converts the parameter to the column's TIME on
117
+ * every version.
118
+ */
119
+ readonly timeOfDayInStrategy: "in" | "equalities";
103
120
  /** Upsert clause form. */
104
121
  readonly upsertForm: "onConflict" | "onDuplicateKey";
105
122
  /** Which equality decides a duplicate conflict key here (batched upsert). */
@@ -136,6 +153,20 @@ export type SqlDialect = {
136
153
  * non-native dialects — documented in docs/dialect-notes.md.
137
154
  */
138
155
  readonly strictMatchStrategy: "native" | "glob" | "binary";
156
+ /**
157
+ * `sql.greatest` (NULL-ignoring two-operand maximum): `native` renders
158
+ * `GREATEST(l, r)`, whose PostgreSQL semantics already ignore NULL; `frame`
159
+ * binds both operands once in a correlated derived table and compares the
160
+ * bound columns with CASE (MySQL's GREATEST returns NULL on any NULL, and
161
+ * SQLite has none). `wrapped-frame` is the frame with each operand in its own
162
+ * scalar subquery: MySQL 8.4 refuses a derived-table select list that holds a
163
+ * nested frame beside a direct outer column reference (ER_ILLEGAL_REFERENCE
164
+ * 1247). In both frames a bare NULL value operand renders as the NULL
165
+ * literal. Only a node with a nested greatest operand uses a frame; a node
166
+ * without one keeps the 2.x CASE form (2.x semantics, types and aggregate
167
+ * scope), which repeats its two operands, so nesting stays linear.
168
+ */
169
+ readonly greatestForm: "native" | "frame" | "wrapped-frame";
139
170
  /** ORDER BY NULLS FIRST/LAST: native clause or `col IS NULL` emulation term. */
140
171
  readonly orderByNullsMode: "native" | "emulate";
141
172
  /** Dialects where OFFSET requires a LIMIT clause declare the filler literal. */
@@ -197,6 +228,19 @@ export type SqlDialect = {
197
228
  * rows are grouped by column set into separate statements there (M4).
198
229
  */
199
230
  readonly rowDefaultKeyword: boolean;
231
+ /**
232
+ * How a {@link ChainStatement} (an ordered chain of read and data-modifying
233
+ * steps ending in one SELECT — round-trip campaign EPIC 4/F) renders, or
234
+ * `null` where the dialect cannot run one as a single statement.
235
+ * `dataModifyingCte` is PostgreSQL's `WITH "step" AS (INSERT/UPDATE/DELETE …
236
+ * RETURNING …) SELECT …` (PGlite included). SQLite and MySQL accept only
237
+ * SELECT inside WITH, so they are `null`: the runtime keeps its
238
+ * multi-statement path there (an optimisation, not a feature — plan D4) and
239
+ * the renderer refuses a chain with `VIBE_UNSUPPORTED_CAPABILITY`. An
240
+ * internal SQL-shape strategy, deliberately NOT in the public capability
241
+ * table (review O-11).
242
+ */
243
+ readonly statementChain: "dataModifyingCte" | null;
200
244
  /**
201
245
  * Conservative bound-parameter budget per statement (M7). Hard limits:
202
246
  * sqlite errors past SQLITE_MAX_VARIABLE_NUMBER (32766 by default), mysql
@@ -5,5 +5,6 @@ import type { RenderedQuery } from "../render.ts";
5
5
  export declare function renderPostgresIndexKeys(params: {
6
6
  dialect: SqlDialect;
7
7
  table?: QualifiedSqlName;
8
+ namespaces?: readonly string[];
8
9
  }): RenderedQuery;
9
10
  //# sourceMappingURL=postgres-index-keys.d.ts.map
@@ -2,7 +2,13 @@ import type { SqlDialect } from "../dialect.ts";
2
2
  import type { RenderedQuery } from "../render.ts";
3
3
  /** Physical schema queries; callers name each namespace without changing search_path. */
4
4
  export type PostgresPhysicalCatalogQuery = "tables" | "views" | "columns" | "constraints" | "checks" | "foreignKeys" | "enums" | "indexes";
5
- /** Read namespace-qualified physical metadata, preserving catalog type and index identities. */
5
+ /**
6
+ * Read namespace-qualified physical metadata, preserving catalog type and index identities.
7
+ * Extension members (`pg_depend.deptype = 'e'`) are left out of `tables`, `views` and `enums`:
8
+ * the extension's own script owns them, so no plan may create or drop them. The other reads hang
9
+ * off a table and reach the schema only through the filtered table list; PostgreSQL never records
10
+ * an index or a constraint as an extension member on its own.
11
+ */
6
12
  export declare function renderPostgresPhysicalCatalog(params: {
7
13
  dialect: SqlDialect;
8
14
  namespace: string;
@@ -0,0 +1,28 @@
1
+ /** @internal SQLite FTS synchronization SQL and its independently verifiable catalog spelling. */
2
+ export declare function renderSqliteFtsUpdateTrigger(params: {
3
+ table: string;
4
+ ftsTable: string;
5
+ columns: readonly string[];
6
+ }): {
7
+ readonly create: string;
8
+ readonly definition: string;
9
+ };
10
+ /** @internal Recognize only complete generated legacy/current triggers and their external-content FTS5 table. */
11
+ export declare function sqliteFtsUpdateUpgrade(params: {
12
+ create: string;
13
+ installed: string;
14
+ table: string;
15
+ name: string;
16
+ tableDefinition?: string;
17
+ }): undefined | {
18
+ readonly kind: "tableRequired";
19
+ readonly ftsTable: string;
20
+ } | {
21
+ readonly kind: "recognized";
22
+ readonly ftsTable: string;
23
+ readonly legacyDefinition: string;
24
+ readonly currentDefinition: string;
25
+ readonly drop: string;
26
+ readonly installed: "legacy" | "current";
27
+ };
28
+ //# sourceMappingURL=sqlite-fts.d.ts.map