@voltro/database 0.27.0 → 0.29.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.
package/dist/sql.d.ts CHANGED
@@ -9,6 +9,49 @@ import { Statement } from '@effect/sql';
9
9
 
10
10
  export declare const acquireMigrationLock: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError_2>;
11
11
 
12
+ declare interface AggregateColumn<TResult = number | Date | null> {
13
+ readonly op: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count-distinct' | 'window-row-number' | 'window-rank' | 'window-dense-rank' | 'window-lag' | 'window-lead' | 'window-sum-over' | 'window-avg-over' | 'column';
14
+ readonly column?: string;
15
+ readonly alias: string;
16
+ readonly window?: WindowSpec;
17
+ /** Used by lag/lead — offset rows (default 1). */
18
+ readonly offset?: number;
19
+ /** Phantom — carries the projected/aggregated result type so
20
+ * `aggregate<Spec>()` can infer a precise result row. Never set at
21
+ * runtime; exists purely for `AggregateColumn<infer T>` inference. */
22
+ readonly _result?: TResult;
23
+ }
24
+
25
+ declare interface AndPredicate {
26
+ readonly and: ReadonlyArray<Predicate>;
27
+ }
28
+
29
+ /**
30
+ * Approximate-nearest-neighbour search clause carried on a
31
+ * {@link QueryDescriptor}. See {@link QueryDescriptor.annClause}.
32
+ */
33
+ declare interface AnnClause {
34
+ /** Vector column to search. */
35
+ readonly column: string;
36
+ /** Literal query vector (array overload). Mutually exclusive with `queryText`. */
37
+ readonly queryVector?: ReadonlyArray<number>;
38
+ /** Raw query string (string overload) — embedded by the runtime before
39
+ * compile. Mutually exclusive with `queryVector`. */
40
+ readonly queryText?: string;
41
+ /** Distance metric — MUST match the index opclass to accelerate. Default 'cosine'. */
42
+ readonly distance: VectorDistance;
43
+ /** Per-query HNSW search effort (pgvector `hnsw.ef_search`). Set by `.efSearch(n)`. */
44
+ readonly ef?: number;
45
+ /**
46
+ * Hybrid-search fusion weight in `[0, 1]`. Set by `hybridSearch(...)`
47
+ * when both an ANN ranking and an FTS filter are present: `0` = pure
48
+ * FTS, `1` = pure vector, `0.5` = balanced. The runtime fuses the two
49
+ * rank signals (Reciprocal-Rank-Fusion) weighted by this. Absent for a
50
+ * plain `nearestNeighbours(...)` query.
51
+ */
52
+ readonly alpha?: number;
53
+ }
54
+
12
55
  declare type AnyMixin = MixinDefinition<Record<string, ColumnDefinition<unknown>>>;
13
56
 
14
57
  declare type AnyTable = Table<string, Record<string, ColumnDefinition<unknown>>, boolean, string>;
@@ -399,6 +442,45 @@ declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType,
399
442
  * COSTS is per command — see `serverOnly()` below for the matrix.
400
443
  */
401
444
  readonly serverOnly?: boolean;
445
+ /**
446
+ * Field-level read scopes, set by `.readableBy(...scopes)`. A NON-EMPTY list
447
+ * of scope strings; the column is stripped from wire OUTPUT for any subject
448
+ * that holds NONE of them, and present for a subject holding at least ONE.
449
+ * This is the graded middle of the exposure axis: `.serverOnly()` hides a
450
+ * column from EVERYONE on the wire, a plain column is visible to everyone,
451
+ * and `.readableBy('billing:read')` is visible only to callers scoped for it.
452
+ *
453
+ * The scope strings are the SAME vocabulary as `guards:` / `ctx.access` — they
454
+ * are checked against the subject's EFFECTIVE scope set (raw subject scopes ∪
455
+ * any rbac role-derived scopes), and the `admin:full` bypass sees every
456
+ * `.readableBy()` column, exactly as it satisfies every guard. Empty here is a
457
+ * declaration error (`.readableBy()` with no scope would mean "nobody" — use
458
+ * `.serverOnly()` for that), so the field is never present as `[]`.
459
+ *
460
+ * Orthogonal to `.encrypted()` (storage-at-rest) and to `.sensitive()` /
461
+ * `.safe()` (export-time masking): this is purely the WIRE-exposure axis.
462
+ */
463
+ readonly readableBy?: ReadonlyArray<string>;
464
+ /**
465
+ * CRDT-managed marker, set by the `crdtText()` column constructor. When `true`
466
+ * the column holds an ENCODED CRDT state (opaque bytes — a Yjs update blob
467
+ * behind `@voltro/local-first`'s swappable backend) rather than a plain value.
468
+ *
469
+ * It changes NOTHING about the column's DDL or how the declarative differ
470
+ * treats it: the storage type is `bytes` (BYTEA / BLOB / LONGBLOB /
471
+ * VARBINARY), a normal storable type, and the planner compares it structurally
472
+ * like any other `bytes` column — introspection never reports this flag, and
473
+ * `sameColumnShape` never reads it, so a `crdtText()` column round-trips
474
+ * through a plan with zero churn (no phantom re-plan).
475
+ *
476
+ * What it DOES drive lives above the DDL, in the runtime write path: the
477
+ * MutationStore reads it (via the SchemaRegistry) to run the AUTHORITATIVE
478
+ * server-side merge on write — an incoming encoded update is folded into the
479
+ * stored state with `mergeCrdtStates` before the row is written, so two
480
+ * concurrent clients converge. Orthogonal to every other flag here; a CRDT
481
+ * column is just `bytes` with server-merge semantics.
482
+ */
483
+ readonly crdtManaged?: boolean;
402
484
  /**
403
485
  * Optimistic-concurrency marker, set by `.version()`.
404
486
  *
@@ -851,6 +933,75 @@ export declare interface DriftReport {
851
933
  }>;
852
934
  }
853
935
 
936
+ /**
937
+ * Per-branch refinement passed to `.with({ relation: { ... } })`.
938
+ *
939
+ * `true` is the zero-config shorthand — equivalent to `{}`. Otherwise
940
+ * each branch can carry its own `where` / `orderBy` / `limit` /
941
+ * `offset` / nested `with`, so `users.with({ posts: { limit: 10,
942
+ * orderBy: 'createdAt' } })` works without dropping into a sub-query
943
+ * builder.
944
+ *
945
+ * Untyped at runtime: the QueryDescriptor is the boundary between the
946
+ * typed builder API and the dialect-agnostic JoinCompiler. End-user
947
+ * types live one layer up in the typed `.with()` method on `Query`.
948
+ */
949
+ declare interface EagerLoadSpec {
950
+ readonly where?: Predicate;
951
+ readonly orderBy?: ReadonlyArray<OrderClause>;
952
+ /**
953
+ * Per-parent cap on the relation. `{ limit: 10 }` returns up to 10
954
+ * rows FOR EACH parent — same semantics as Drizzle / Prisma /
955
+ * Hibernate. This matches what users actually mean when they write
956
+ * "the 10 latest posts" inside `users.with({ posts: { limit: 10 } })`.
957
+ *
958
+ * The SQL stores express this natively as a `LIMIT` clause in a
959
+ * correlated subquery (postgres / sqlite / mysql / mariadb) or
960
+ * via `TOP N` inside `FOR JSON PATH` (mssql) when the JSON-agg path
961
+ * is active. The walker fallback post-buckets the batch fetch and
962
+ * slices each bucket.
963
+ */
964
+ readonly limit?: number;
965
+ /** Per-parent offset. Only meaningful with `limit`. */
966
+ readonly offset?: number;
967
+ readonly with?: WithSpec;
968
+ /**
969
+ * Project the JUNCTION row's own columns into a `manyToMany` eager result.
970
+ *
971
+ * `true` takes every junction column; an array takes only those named. The
972
+ * values land under `_junction` on each target row:
973
+ *
974
+ * projects.with({ teams: { junction: ['role', 'addedAt'] } })
975
+ * // → team._junction.addedAt
976
+ *
977
+ * Nested rather than merged onto the target row, deliberately: a junction and
978
+ * its target routinely share column names (`createdAt` is the obvious one),
979
+ * and merging would silently overwrite real target data with membership data.
980
+ * A name collision that corrupts a row is worse than one extra level.
981
+ *
982
+ * Why this exists: junction tables carrying meaningful columns — a role, a
983
+ * joined-at stamp, a permission tier — are the rule rather than the
984
+ * exception, and without this an eager load could FILTER on them
985
+ * (`onJunction`) but never return them, so any relation with real membership
986
+ * data had to stay a hand-written join. The junction rows were already being
987
+ * fetched in full to resolve the target ids; this stops throwing them away.
988
+ */
989
+ readonly junction?: true | ReadonlyArray<string>;
990
+ /**
991
+ * Filter a `manyToMany` eager branch on the THROUGH-table's columns.
992
+ * The predicate is evaluated against the junction row, not the target
993
+ * row — e.g. for `users.with({ organizations: { onJunction: eq('role',
994
+ * 'admin') } })` it keeps only the memberships where `role = 'admin'`,
995
+ * so the loaded `organizations` are exactly the orgs the user is an
996
+ * admin of.
997
+ *
998
+ * Compiles into the correlated subquery's WHERE alongside the
999
+ * source-key correlation, qualified to the junction-table alias.
1000
+ * Ignored on `one` / `many` branches (no junction table exists).
1001
+ */
1002
+ readonly onJunction?: Predicate;
1003
+ }
1004
+
854
1005
  export declare const emitDropColumnDdl: (op: Extract<MigrationOperation, {
855
1006
  kind: "drop-column";
856
1007
  }>, q: typeof quote) => string;
@@ -936,6 +1087,18 @@ cdcChannel?: string) => string;
936
1087
 
937
1088
  declare type EmptyMerge = unknown;
938
1089
 
1090
+ /**
1091
+ * `EXISTS (SELECT 1 FROM ... WHERE ...)` / `NOT EXISTS (...)`.
1092
+ * Mirrors `SubqueryInPredicate` but doesn't bind to a specific
1093
+ * outer column — the predicate is "the sub-query produces at least
1094
+ * one row" (or doesn't). Non-correlated in v1; the sub-query's
1095
+ * WHERE clause references only its own table.
1096
+ */
1097
+ declare interface ExistsPredicate {
1098
+ readonly op: 'exists' | 'not-exists';
1099
+ readonly subquery: QueryDescriptor;
1100
+ }
1101
+
939
1102
  /**
940
1103
  * Filename pattern the discovery walker matches: timestamp-prefixed
941
1104
  * `.ts` files in any depth under the project's `migrations/` dir.
@@ -1215,6 +1378,8 @@ declare interface JsonIndexPath {
1215
1378
  readonly numeric: boolean;
1216
1379
  }
1217
1380
 
1381
+ declare type LeafOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
1382
+
1218
1383
  export declare const mapPgType: (dataType: string, udtName?: string) => ColumnType;
1219
1384
 
1220
1385
  declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = Mixins extends readonly [] ? EmptyMerge : Mixins extends readonly [infer Head, ...infer Rest] ? (Head extends MixinDefinition<infer F> ? F : EmptyMerge) & (Rest extends ReadonlyArray<AnyMixin> ? MergeMixinFields<Rest> : EmptyMerge) : EmptyMerge;
@@ -1453,6 +1618,13 @@ declare interface NarrowedFromSpec {
1453
1618
  readonly using?: string;
1454
1619
  }
1455
1620
 
1621
+ /** Negation of an arbitrary sub-predicate → `NOT (...)`. Wraps any
1622
+ * leaf/and/or/subquery subtree (distinct from the per-column `neq` and
1623
+ * the narrow `notInSubquery`/`notExists`). */
1624
+ declare interface NotPredicate {
1625
+ readonly not: Predicate;
1626
+ }
1627
+
1456
1628
  /**
1457
1629
  * DDL fragment for a numeric primary key column. Each dialect has its
1458
1630
  * own auto-increment idiom — there's no portable shorthand, so we
@@ -1509,6 +1681,15 @@ export declare type OperationClass =
1509
1681
  */
1510
1682
  export declare type OperationKind = 'create-table' | 'drop-table' | 'add-column' | 'drop-column' | 'rename-column' | 'alter-column-nullability' | 'alter-column-type' | 'alter-column-default' | 'add-index' | 'drop-index' | 'add-unique' | 'drop-unique' | 'add-unique-composite' | 'drop-unique-composite' | 'add-foreign-key' | 'drop-foreign-key' | 'add-check' | 'drop-check';
1511
1683
 
1684
+ declare interface OrderClause {
1685
+ readonly column: string;
1686
+ readonly direction: 'asc' | 'desc';
1687
+ }
1688
+
1689
+ declare interface OrPredicate {
1690
+ readonly or: ReadonlyArray<Predicate>;
1691
+ }
1692
+
1512
1693
  export declare const parseEnumCheck: (clause: string) => {
1513
1694
  readonly column: string;
1514
1695
  readonly values: ReadonlyArray<string>;
@@ -1622,6 +1803,23 @@ declare type PluginRefOrphanPolicy =
1622
1803
  * omitting the option, which is the same behaviour arrived at by accident. */
1623
1804
  | 'keep';
1624
1805
 
1806
+ declare type Predicate = PredicateLeaf | AndPredicate | OrPredicate | NotPredicate | SubqueryInPredicate | ExistsPredicate;
1807
+
1808
+ declare interface PredicateLeaf {
1809
+ readonly column: string;
1810
+ readonly op: LeafOperator;
1811
+ readonly value?: unknown;
1812
+ /**
1813
+ * JSON-path segments INTO a `json()` column (object keys / array indices).
1814
+ * When set, the leaf filters on the extracted value, not the whole column —
1815
+ * built via {@link jsonField}. The sql compiler lowers it to the dialect's
1816
+ * json-extract expression; the reactive matcher treats it as non-indexable
1817
+ * (rides the unindexed bucket → re-checked on any change to the table, like
1818
+ * `contains`). Absent for a normal column leaf.
1819
+ */
1820
+ readonly path?: ReadonlyArray<string | number>;
1821
+ }
1822
+
1625
1823
  /**
1626
1824
  * Imperative `applyNamespacedSchema` against a fresh SqlClient — the
1627
1825
  * entry point the CLI / runtime use to provision a tenant's namespace
@@ -1629,6 +1827,275 @@ declare type PluginRefOrphanPolicy =
1629
1827
  */
1630
1828
  export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>, namespace: string, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId) => Promise<void>;
1631
1829
 
1830
+ /**
1831
+ * The runtime-facing materialized query — a plain bag of strings the store and
1832
+ * the matcher consume.
1833
+ *
1834
+ * `R` is a PHANTOM row type. It exists so the row shape the typed builder
1835
+ * already knows survives the trip through `.descriptor` into `store.query()`,
1836
+ * which is where it used to be thrown away: `store.query(database.notes...)`
1837
+ * returned `Row` (`Record<string, unknown>`), so every field read needed a
1838
+ * hand-written cast. One downstream app wrote 2,032 of them. The type was
1839
+ * always available — it was just dropped at this boundary.
1840
+ *
1841
+ * It is an OPTIONAL field, never set at runtime. That matters for two reasons:
1842
+ * a plain object literal still satisfies `QueryDescriptor` (dispatcher, matcher
1843
+ * and store code build and forward these structurally), and `R` stays
1844
+ * covariant, so a `QueryDescriptor<Note>` is still assignable to the bare
1845
+ * `QueryDescriptor` that generic infrastructure passes around. Defaulting to
1846
+ * `Row` keeps every existing annotation meaning exactly what it meant before.
1847
+ */
1848
+ declare interface QueryDescriptor<R = Row> {
1849
+ readonly table: string;
1850
+ readonly predicate: Predicate | undefined;
1851
+ /** Phantom — carries the row type through to `store.query()`. Never set at
1852
+ * runtime; erased entirely by the compiler. */
1853
+ readonly __row?: R;
1854
+ /**
1855
+ * Opt OUT of the runtime's automatic `deletedAt IS NULL` scope on
1856
+ * `softDelete()` tables. Set by `.withDeleted()`. The query builder is
1857
+ * browser-safe and mixin-unaware, so this is just a flag the RUNTIME
1858
+ * (storeMiddleware read path + reactive-query finalize) honors.
1859
+ */
1860
+ readonly includeDeleted?: boolean;
1861
+ /** Opt OUT of the `expires()` read filter — a deliberate read of expired
1862
+ * rows (an admin view, a grace-period check). Same shape and same posture as
1863
+ * `includeDeleted`. */
1864
+ readonly includeExpired?: boolean;
1865
+ /**
1866
+ * Opt OUT of the runtime's automatic tenant scope on `tenant()` tables.
1867
+ * Set by `.unscoped()`. Honored by the runtime, same as `includeDeleted`.
1868
+ */
1869
+ readonly crossTenant?: boolean;
1870
+ readonly order: ReadonlyArray<OrderClause>;
1871
+ readonly take: number | undefined;
1872
+ readonly skip: number | undefined;
1873
+ readonly projection: ReadonlyArray<string> | undefined;
1874
+ /**
1875
+ * Aggregate projection. When set, `SELECT` emits the aggregates
1876
+ * instead of row columns; the result is always exactly ONE row
1877
+ * (per group when combined with `groupBy`, ONE row total when not).
1878
+ *
1879
+ * Cannot coexist with `projection` (the two are mutually exclusive
1880
+ * select clauses). The builder enforces this at chain time.
1881
+ */
1882
+ readonly aggregations?: ReadonlyArray<AggregateColumn<unknown>>;
1883
+ /**
1884
+ * `GROUP BY` columns. When set, the query returns ONE row per
1885
+ * distinct combination of these columns + the aggregations.
1886
+ * Requires `aggregations` to be set; ungrouped aggregates always
1887
+ * produce exactly one row.
1888
+ */
1889
+ readonly groupBy?: ReadonlyArray<string>;
1890
+ /**
1891
+ * `HAVING` clause — predicate evaluated AFTER GROUP BY. Distinct
1892
+ * from `predicate` (which becomes `WHERE` and runs BEFORE grouping).
1893
+ * Use HAVING to filter groups based on their aggregate values
1894
+ * (e.g. "users with more than 10 posts").
1895
+ *
1896
+ * Stored as a `Predicate` so the same predicate compiler that
1897
+ * handles WHERE handles HAVING — but the column names on the LHS
1898
+ * of HAVING refer to aggregate aliases, not raw rows.
1899
+ */
1900
+ readonly having?: Predicate;
1901
+ /**
1902
+ * Full-text-search predicate (S7). Set by `.matching('indexName',
1903
+ * 'query')`. `.matching()` resolves the index back to its declared
1904
+ * columns + config + weights from the table registry at call time and
1905
+ * stores them here, so the SQL compiler can emit the right per-dialect
1906
+ * predicate (and an optional relevance-ranking expression) without
1907
+ * re-consulting the registry.
1908
+ *
1909
+ * Per-dialect emission:
1910
+ * - postgres → `<tsvCol> @@ plainto_tsquery('<cfg>', ?)`
1911
+ * - mysql / mariadb → `MATCH(col1, col2) AGAINST (? IN NATURAL LANGUAGE MODE)`
1912
+ * - sqlite → `<table>_<idx>_fts MATCH ?` against the FTS5 table
1913
+ * - mssql → ranked LOWER(...) LIKE fallback (no server FTS catalog)
1914
+ *
1915
+ * `rank` carries an optional relevance-ranking request. When set, the
1916
+ * compiler also surfaces a synthetic relevance column aliased `rank.alias`
1917
+ * and (when `rank.order` is set) orders by it DESC. The score expression
1918
+ * is per-dialect: postgres `ts_rank`, mysql/mariadb `MATCH ... AGAINST`
1919
+ * score, sqlite FTS5 `bm25`, mssql a LIKE-hit count.
1920
+ */
1921
+ /**
1922
+ * Full-text indexes declared on the FROM table — captured by
1923
+ * `queryFor()` so `.matching()` can resolve an index's columns/config
1924
+ * without a global table-registry lookup. Pure builder metadata; the
1925
+ * SQL compiler never reads it (it reads the resolved `fullTextSearch`).
1926
+ */
1927
+ readonly fullTextIndexes?: ReadonlyArray<{
1928
+ readonly name: string;
1929
+ readonly columns: ReadonlyArray<string>;
1930
+ readonly config?: string;
1931
+ readonly weights?: Readonly<Record<string, 'A' | 'B' | 'C' | 'D'>>;
1932
+ }>;
1933
+ /**
1934
+ * The SOURCE table object this query was built from (`queryFor(t)` / a
1935
+ * `databaseHandle` entry sets it). The eager-load resolver (`.with(...)`)
1936
+ * needs the source table's shape + relations; it used to re-resolve it
1937
+ * BY NAME via `requireTable(descriptor.table)`, which forces every queried
1938
+ * table to be pre-registered (`databaseHandle(...)` / discovery). A direct
1939
+ * query needs no such registration — it carries the object — so an eager
1940
+ * query that threw "table 'X' is not registered" for a table you can query
1941
+ * directly was an asymmetry. Carrying the object here (same precedent as
1942
+ * `fullTextIndexes`) lets the store use it directly, with `requireTable` as
1943
+ * the fallback for reconstructed descriptors (e.g. `paginateById`). NOT
1944
+ * serialized/fingerprinted — the reactive engine keys on table+predicate.
1945
+ */
1946
+ readonly sourceTable?: TableLike;
1947
+ readonly fullTextSearch?: {
1948
+ readonly indexName: string;
1949
+ readonly query: string;
1950
+ /** Columns the index covers — resolved from the table registry. */
1951
+ readonly columns: ReadonlyArray<string>;
1952
+ /** Postgres tsvector config (default 'english'). */
1953
+ readonly config?: string;
1954
+ /** Per-column weight letters (postgres ts_rank). */
1955
+ readonly weights?: Readonly<Record<string, 'A' | 'B' | 'C' | 'D'>>;
1956
+ /** Relevance-ranking request — set by `.rankBy(alias?)`. */
1957
+ readonly rank?: {
1958
+ readonly alias: string;
1959
+ readonly order: boolean;
1960
+ };
1961
+ };
1962
+ /**
1963
+ * Common-Table-Expression definitions (Q4) — `WITH <name> AS (...)`.
1964
+ * Each entry binds a `name` to a sub-query descriptor; the outer
1965
+ * SELECT can reference the CTE by name in `from`, `inSubquery`,
1966
+ * `exists`, etc.
1967
+ *
1968
+ * `.withCte()` adds a non-recursive CTE. For tree-walks use
1969
+ * `.recursiveCte()`, which sets `recursive: true` and emits
1970
+ * `WITH RECURSIVE` (the inner descriptor self-references the CTE
1971
+ * name). Both are reactive — see `runtime/dependencyGraph.ts`.
1972
+ */
1973
+ readonly ctes?: ReadonlyArray<{
1974
+ readonly name: string;
1975
+ readonly descriptor: QueryDescriptor;
1976
+ /**
1977
+ * Recursive CTE marker (Q4 v2). When true, the compiler emits
1978
+ * `WITH RECURSIVE name AS (...)`. The inner descriptor must be a
1979
+ * UNION (via the `union` / `unionAll` helpers) whose first arm is
1980
+ * the anchor and subsequent arms reference the CTE by name (the
1981
+ * recursion). Cycle-detection per-dialect: postgres uses
1982
+ * `UNION` semantics (implicit dedup); other dialects mirror.
1983
+ */
1984
+ readonly recursive?: boolean;
1985
+ }>;
1986
+ /**
1987
+ * SELECT DISTINCT (Q7). When true, dedup rows by the full result
1988
+ * shape. Cross-dialect.
1989
+ */
1990
+ readonly distinct?: boolean;
1991
+ /**
1992
+ * SELECT DISTINCT ON (cols) — postgres-only (Q7). Picks ONE row
1993
+ * per distinct combination of `distinctOn` columns; the row picked
1994
+ * is the FIRST per the descriptor's `order`. The compiler emits a
1995
+ * windowed-rownumber fallback on other dialects.
1996
+ *
1997
+ * Common use: "latest message per channel".
1998
+ */
1999
+ readonly distinctOn?: ReadonlyArray<string>;
2000
+ /**
2001
+ * Alias for the FROM table — `FROM <table> AS <alias>` (Q8).
2002
+ * Needed for self-joins where the same table appears multiple
2003
+ * times with different aliases. When unset, the FROM clause is
2004
+ * the bare table name.
2005
+ */
2006
+ readonly alias?: string;
2007
+ /**
2008
+ * Joined tables (Q8). Each entry adds an INNER JOIN or LEFT JOIN
2009
+ * with an explicit ON predicate. Self-joins re-use the same
2010
+ * source table with a different alias.
2011
+ *
2012
+ * `on` is a regular `Predicate` — the leaf columns may carry
2013
+ * `alias.column` style qualifiers since the compiler emits the
2014
+ * column string verbatim.
2015
+ */
2016
+ readonly joins?: ReadonlyArray<{
2017
+ readonly kind: 'inner' | 'left';
2018
+ readonly table: string;
2019
+ readonly alias: string;
2020
+ readonly on: Predicate;
2021
+ }>;
2022
+ /**
2023
+ * Aliased projection for joined queries (Q8 v2). When set, the
2024
+ * SELECT emits `alias.col AS outputKey` for each entry, producing
2025
+ * a flat row with the user's chosen keys.
2026
+ *
2027
+ * Mutually exclusive with `projection` (regular select-cols).
2028
+ * Bypassed by `aggregations`.
2029
+ */
2030
+ readonly joinedProjection?: ReadonlyArray<{
2031
+ readonly outputKey: string;
2032
+ readonly source: string;
2033
+ }>;
2034
+ /**
2035
+ * Set-operation composition (Q6). When set, the SELECT shape is
2036
+ * replaced by `(SELECT ...) <OP> (SELECT ...) ...`.
2037
+ *
2038
+ * - `'union'` → dedup-merge (`UNION`)
2039
+ * - `'union-all'` → no dedup (`UNION ALL`)
2040
+ * - `'intersect'` → rows in BOTH (`INTERSECT`)
2041
+ * - `'except'` → rows in LHS not in RHS (`EXCEPT`)
2042
+ *
2043
+ * All four are standard SQL + supported on every dialect we ship.
2044
+ */
2045
+ readonly setOp?: {
2046
+ readonly kind: 'union' | 'union-all' | 'intersect' | 'except';
2047
+ readonly queries: ReadonlyArray<QueryDescriptor>;
2048
+ };
2049
+ /**
2050
+ * Optional index hint. Set by `.using('indexName')` on the query
2051
+ * builder. The framework stores it on the descriptor for two reasons:
2052
+ *
2053
+ * 1. Suppresses the dev-mode "non-indexed query" warning for this
2054
+ * descriptor (the user explicitly signalled they thought about
2055
+ * the access pattern).
2056
+ * 2. The matcher's tuple-routing path keys subscriptions on the
2057
+ * declared index's column set — see `IndexedMatcher` in
2058
+ * `@voltro/runtime/matcher.ts`. Subscriptions that declare a
2059
+ * covering index get O(1) candidate lookup per event instead
2060
+ * of column-union routing.
2061
+ */
2062
+ readonly usingIndex?: string;
2063
+ /**
2064
+ * Eager-load tree declared via `.with({ ... })`. Consumed by the
2065
+ * JoinCompiler (`./joinCompiler.ts`) when present — the SQL stores
2066
+ * dispatch nested-JSON-aggregation queries; the in-memory store
2067
+ * walks it via post-hoc lookups.
2068
+ */
2069
+ readonly eager?: WithSpec;
2070
+ /**
2071
+ * Approximate-nearest-neighbour clause (plan 01). Set by
2072
+ * `.nearestNeighbours(...)`. Drives a synthetic `distance` projection
2073
+ * + `ORDER BY <col> <op> <query>` in the SQL compiler:
2074
+ *
2075
+ * - postgres (pgvector): `<col> <=> $q` (cosine) / `<->` (l2) /
2076
+ * `<#>` (inner).
2077
+ * - mariadb / mysql: `VEC_DISTANCE_COSINE/EUCLIDEAN(<col>, $q)`.
2078
+ * - mssql / sqlite: no portable ANN operator → the compiler warns +
2079
+ * skips the distance ordering (sequential scan; rows still return).
2080
+ *
2081
+ * `queryVector` is the literal float array; `queryText` is the
2082
+ * raw string the runtime embeds to a vector *before* compiling
2083
+ * (only valid when the table carries `vectorEmbedding()`). Exactly
2084
+ * one of the two is set.
2085
+ */
2086
+ readonly annClause?: AnnClause;
2087
+ /**
2088
+ * PostGIS spatial distance clause (`@voltro/plugin-postgis`). Set by the
2089
+ * `withDistance(...)` / `nearestBy(...)` transforms applied via `.use(...)`.
2090
+ * Postgres-only — the SQL compiler lowers it to a `ST_Distance(...)`
2091
+ * projected column and/or a `<->` KNN `ORDER BY`; on any non-postgres
2092
+ * dialect the compiler THROWS (mirrors the spatial predicate + column
2093
+ * guards). Never evaluated in-memory (no PostGIS in JS). Composes with
2094
+ * `.where(...)`, `.limit(k)`, spatial predicates, etc.
2095
+ */
2096
+ readonly spatialClause?: SpatialClause;
2097
+ }
2098
+
1632
2099
  declare const quote: (name: string) => string;
1633
2100
 
1634
2101
  /**
@@ -1750,6 +2217,52 @@ export declare const rollingDeployUnsafeOps: (operations: ReadonlyArray<PlannedO
1750
2217
  readonly remedy: string;
1751
2218
  }>;
1752
2219
 
2220
+ declare type Row = Readonly<Record<string, unknown>>;
2221
+
2222
+ /** The context a rule predicate is evaluated with. */
2223
+ declare interface RuleContext {
2224
+ /** Transactional store — read other tables in the mutation's snapshot. */
2225
+ readonly store: RuleReadStore;
2226
+ /** The calling subject (tenant / user), for tenant-aware rules. */
2227
+ readonly subject: unknown;
2228
+ }
2229
+
2230
+ /**
2231
+ * What a rule predicate may return. `true` / `void` / `undefined` = the rule
2232
+ * HOLDS. `false` = violated with the rule's default detail. A
2233
+ * `RuleViolationDetail` object = violated, carrying per-row i18n params / a
2234
+ * field pointer for THIS specific row.
2235
+ */
2236
+ declare type RuleOutcome = boolean | void | RuleViolationDetail;
2237
+
2238
+ /**
2239
+ * A rule predicate over the POST-WRITE row. Runs inside the mutation
2240
+ * transaction. Return `true`/nothing when the invariant holds; return `false`
2241
+ * or a `RuleViolationDetail` to signal a violation. May be async (cross-table
2242
+ * reads).
2243
+ */
2244
+ declare type RulePredicate = (row: Record<string, unknown>, context: RuleContext) => RuleOutcome | Promise<RuleOutcome>;
2245
+
2246
+ /** Minimal READ surface (a subset of `DataStore`) handed to a rule predicate —
2247
+ * the SAME transactional store the mutation writes through, so cross-table
2248
+ * reads share its MVCC snapshot and cannot race the write the rule guards. */
2249
+ declare interface RuleReadStore {
2250
+ query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Record<string, unknown>>>;
2251
+ }
2252
+
2253
+ /** Severity of a business rule. `error` rolls the mutation back; `warning`
2254
+ * logs + audits but lets the write commit (useful during data migrations). */
2255
+ declare type RuleSeverity = 'error' | 'warning';
2256
+
2257
+ declare interface RuleViolationDetail {
2258
+ /** i18n params for the violation message (per this row). */
2259
+ readonly params?: Record<string, unknown>;
2260
+ /** Field path the violation pinpoints, if any. */
2261
+ readonly field?: string;
2262
+ /** Human-readable message (fallback / dev aid). */
2263
+ readonly message?: string;
2264
+ }
2265
+
1753
2266
  export declare const runFileBasedMigrations: (sql: SqlClient.SqlClient, ctx: RunFileBasedMigrationsCtx) => Effect.Effect<FileMigrationRunResult, unknown>;
1754
2267
 
1755
2268
  export declare interface RunFileBasedMigrationsCtx {
@@ -1819,6 +2332,41 @@ export declare const shortFingerprint: (fp: string) => string;
1819
2332
  */
1820
2333
  export declare const snapshotColumn: (name: string, def: ColumnDefinition<unknown>) => ColumnSnapshot;
1821
2334
 
2335
+ /**
2336
+ * PostGIS spatial-distance clause carried on a {@link QueryDescriptor}.
2337
+ * Drives a `ST_Distance` projected column (distance-as-a-value) and/or a
2338
+ * `<->` nearest-neighbour `ORDER BY` (KNN). Built by `@voltro/plugin-postgis`;
2339
+ * postgres-only (the compiler throws on other dialects).
2340
+ */
2341
+ declare interface SpatialClause {
2342
+ /** The spatial column to measure from. */
2343
+ readonly column: string;
2344
+ /** The comparison geometry as WKT/EWKT (e.g. `SRID=4326;POINT(8 50)`). Bound as a parameter. */
2345
+ readonly geom: string;
2346
+ /**
2347
+ * Measure metric metres via a `::geography` cast on both operands
2348
+ * (great-circle distance). When false, distance is in the SRID's own
2349
+ * planar units (`::geometry`). Default true — the "distance in metres"
2350
+ * users expect for lat/lon data.
2351
+ */
2352
+ readonly useGeography: boolean;
2353
+ /**
2354
+ * When set, surface the distance as a projected column aliased this name
2355
+ * (`ST_Distance(col, geom) AS "<projectAs>"`). The row type gains a
2356
+ * `number` field. Absent → no distance projection (KNN-order-only).
2357
+ */
2358
+ readonly projectAs?: string;
2359
+ /**
2360
+ * When set, add a KNN `ORDER BY col <-> geom <dir>` clause (indexed
2361
+ * nearest-neighbour when a GiST index covers the column). Leads any
2362
+ * explicit `.orderBy(...)`. Absent → no spatial ordering. `<->` is always
2363
+ * a planar operator (postgres has no geography `<->`), so KNN ordering
2364
+ * ranks by planar distance even when `useGeography` is true for the
2365
+ * projected metric distance.
2366
+ */
2367
+ readonly order?: 'asc' | 'desc';
2368
+ }
2369
+
1822
2370
  /**
1823
2371
  * The `sql` template tag — captures a tagged-template literal into a
1824
2372
  * {@link RawSqlFragment} descriptor without binding it to any client.
@@ -1888,6 +2436,25 @@ export declare interface SquashResult {
1888
2436
  readonly snapshotId: string;
1889
2437
  }
1890
2438
 
2439
+ /**
2440
+ * `col IN (SELECT col FROM ...)` / `col NOT IN (SELECT ...)` — the
2441
+ * RHS is a sub-query descriptor. The sub-query MUST project a
2442
+ * single column (via `.select('col')`); the SQL compiler emits the
2443
+ * full inner SELECT inline, and the in-memory store evaluates the
2444
+ * sub-query first + then runs the IN check against the materialized
2445
+ * set.
2446
+ *
2447
+ * The sub-query is non-correlated in v1 — it can't reference outer
2448
+ * row columns. Correlated sub-queries (`WHERE EXISTS (... WHERE
2449
+ * inner.userId = outer.id)`) would require a table-alias mechanism;
2450
+ * tracked separately.
2451
+ */
2452
+ declare interface SubqueryInPredicate {
2453
+ readonly column: string;
2454
+ readonly op: 'subquery-in' | 'subquery-not-in';
2455
+ readonly subquery: QueryDescriptor;
2456
+ }
2457
+
1891
2458
  declare interface Table<Name extends string, Fields extends Record<string, ColumnDefinition<unknown>>, Reactive extends boolean = true, IxNames extends string = never> extends TableLike {
1892
2459
  readonly tableName: Name;
1893
2460
  readonly fields: Fields;
@@ -1911,6 +2478,12 @@ declare interface Table<Name extends string, Fields extends Record<string, Colum
1911
2478
  readonly appliedUniques: ReadonlyArray<TableUnique>;
1912
2479
  readonly appliedFullText: ReadonlyArray<TableFullTextIndex>;
1913
2480
  readonly appliedChecks: ReadonlyArray<TableCheck>;
2481
+ /**
2482
+ * The cross-table business rules DECLARED on this table, set by
2483
+ * `.rule(name, predicate)`. Evaluated by the runtime inside the mutation
2484
+ * transaction (see `TableRule`); they emit no DDL.
2485
+ */
2486
+ readonly appliedRules: ReadonlyArray<TableRule>;
1914
2487
  /**
1915
2488
  * The name this table used to have, set by `.renamedFrom('old_name')`.
1916
2489
  * Named apart from the METHOD that sets it — one interface cannot carry both.
@@ -2172,6 +2745,38 @@ declare interface Table<Name extends string, Fields extends Record<string, Colum
2172
2745
  * For a single-column check, `column.check(expr)` is the terser form.
2173
2746
  */
2174
2747
  check: <const CkName extends string>(name: CkName, expr: string) => Table<Name, Fields, Reactive, IxNames>;
2748
+ /**
2749
+ * Declare a CROSS-TABLE business rule (S3). Unlike `.check(name, expr)` — a
2750
+ * single-row SQL `CHECK` the database enforces — a rule is a PREDICATE the
2751
+ * runtime evaluates INSIDE the mutation's transaction, after the write and
2752
+ * before commit. It can read OTHER tables in the same snapshot and rolls the
2753
+ * mutation back with a typed `BusinessRuleViolation` on failure.
2754
+ *
2755
+ * The predicate receives the POST-WRITE row and a `context` whose `store` is
2756
+ * the SAME transactional store the mutation wrote through. Return `true`
2757
+ * (or nothing) when the invariant holds; return `false` or a
2758
+ * `RuleViolationDetail` (i18n `params` / a `field` pointer) to signal a
2759
+ * violation.
2760
+ *
2761
+ * ```ts
2762
+ * table('invoices', { id: id(), total: integer() }).rule(
2763
+ * 'totalMatchesLineItems',
2764
+ * async (row, { store }) => {
2765
+ * const items = await store.query(
2766
+ * lineItems.where(eq('invoiceId', row.id)).descriptor,
2767
+ * )
2768
+ * const sum = items.reduce((a, l) => a + (l.amount as number), 0)
2769
+ * return sum === row.total || { params: { computed: sum, declared: row.total } }
2770
+ * },
2771
+ * )
2772
+ * ```
2773
+ *
2774
+ * `severity: 'warning'` logs + audits the violation but lets the write
2775
+ * commit (useful during data migration); the default `'error'` rolls back.
2776
+ */
2777
+ rule: <const RuleName extends string>(name: RuleName, predicate: RulePredicate, options?: {
2778
+ readonly severity?: RuleSeverity;
2779
+ }) => Table<Name, Fields, Reactive, IxNames>;
2175
2780
  /**
2176
2781
  * Declare a full-text-search index (S7). One method, three back-
2177
2782
  * ends:
@@ -2383,6 +2988,21 @@ declare interface TableLike {
2383
2988
  readonly fields: Record<string, ColumnDefinition<unknown>>;
2384
2989
  }
2385
2990
 
2991
+ /**
2992
+ * A named cross-table business rule, attached by `.rule(name, predicate)`.
2993
+ *
2994
+ * UNLIKE `.check(name, expr)` — a single-row SQL `CHECK` emitted as DDL and
2995
+ * enforced by the database — a rule is a PREDICATE the runtime evaluates INSIDE
2996
+ * the mutation's transaction, AFTER the write and BEFORE commit. It can read
2997
+ * OTHER tables in the same MVCC snapshot and rolls the whole mutation back
2998
+ * (typed `BusinessRuleViolation`) on violation. It emits no DDL.
2999
+ */
3000
+ declare interface TableRule {
3001
+ readonly name: string;
3002
+ readonly predicate: RulePredicate;
3003
+ readonly severity: RuleSeverity;
3004
+ }
3005
+
2386
3006
  export declare interface TableSnapshot {
2387
3007
  readonly name: string;
2388
3008
  readonly columns: ReadonlyArray<ColumnSnapshot>;
@@ -2477,6 +3097,32 @@ declare interface View<Name extends string, Fields extends Record<string, Column
2477
3097
  */
2478
3098
  export declare const VOLTRO_MIGRATION_LOCK_KEY = 6322741009312437n;
2479
3099
 
3100
+ /**
3101
+ * One aggregate column in a `SELECT` projection — `COUNT(*)`,
3102
+ * `SUM(amount) AS total`, etc. Built via the `count() / sum() /
3103
+ * avg() / min() / max() / countDistinct()` helpers exported from
3104
+ * `@voltro/database`.
3105
+ *
3106
+ * `column` is undefined for `count()` (which compiles to `COUNT(*)`)
3107
+ * and required for every other op. `alias` is the column name on the
3108
+ * result row.
3109
+ */
3110
+ /**
3111
+ * Window-function spec (Q3). When set on an AggregateColumn, the
3112
+ * compiler emits `<OP>(...) OVER (PARTITION BY ... ORDER BY ...)`
3113
+ * instead of a plain aggregate. The query is reactive coarsely — the
3114
+ * matcher can't bucket on a window-function result, so the engine
3115
+ * widens the dependency to the whole source table and re-queries on
3116
+ * any write to it (see `runtime/relevantFields.ts`).
3117
+ */
3118
+ declare interface WindowSpec {
3119
+ readonly partitionBy?: ReadonlyArray<string>;
3120
+ readonly orderBy?: ReadonlyArray<{
3121
+ column: string;
3122
+ direction: 'asc' | 'desc';
3123
+ }>;
3124
+ }
3125
+
2480
3126
  /**
2481
3127
  * Convenience wrapper: acquire → run `work` → release. Releases even
2482
3128
  * on failure via `Effect.ensuring`, but a process crash mid-work
@@ -2485,4 +3131,7 @@ export declare const VOLTRO_MIGRATION_LOCK_KEY = 6322741009312437n;
2485
3131
  */
2486
3132
  export declare const withMigrationLock: <A, E, R = never>(sql: SqlClient.SqlClient, work: Effect.Effect<A, E, R>) => Effect.Effect<A, E | SqlError_2, R>;
2487
3133
 
3134
+ /** Tree of relations to eager-load. */
3135
+ declare type WithSpec = Readonly<Record<string, true | EagerLoadSpec>>;
3136
+
2488
3137
  export { }