@voltro/database 0.4.0 → 0.6.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/index.d.ts CHANGED
@@ -1124,6 +1124,18 @@ export declare interface ColumnDefinition<TsType, Type extends ColumnType = Colu
1124
1124
  readonly __tsType?: TsType;
1125
1125
  }
1126
1126
 
1127
+ /**
1128
+ * The per-column mapping, over a column DEFINITION — what `table.fields` holds.
1129
+ *
1130
+ * Not the builder `text()` returns: its `definition` is private, so a builder
1131
+ * cannot be read from outside. Worth stating because the natural call is
1132
+ * `columnSchema(timestamp())`, and an earlier version accepted it structurally,
1133
+ * found no `type`, and fell through to `Schema.Unknown` — which passes every
1134
+ * value through unchanged, so the mapping looked right in any test that only
1135
+ * checked a round trip. Take definitions from `table.fields`.
1136
+ */
1137
+ export declare const columnSchema: <TsType, C extends ColumnType>(def: ColumnDefinition<TsType, C>) => Schema.Schema.Any;
1138
+
1127
1139
  export declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
1128
1140
 
1129
1141
  /**
@@ -1136,9 +1148,13 @@ export declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal'
1136
1148
  * - A relation name in the eager tree isn't registered (corner case
1137
1149
  * the walker would surface with a clearer error — better to fall
1138
1150
  * back to it).
1139
- * - The dialect doesn't have a compiler module yet.
1140
- * - The compiler hits an explicitly-bailed feature (rare listed
1141
- * in the per-dialect modules' comments).
1151
+ * - An `inferForeignKey` on the eager tree is ambiguous, so there is no
1152
+ * single key to correlate on (the walker raises the explanatory error).
1153
+ *
1154
+ * There is no feature the compiler declines on purpose. `junction: [...]`
1155
+ * projection and the `one`-cardinality check — the two that used to be served
1156
+ * by the walker for correctness reasons — are compiled per dialect and covered
1157
+ * by walker-vs-JSON parity tests against every live engine.
1142
1158
  *
1143
1159
  * Callers (the dialect data stores) treat `null` as "fall back to the
1144
1160
  * walker". This keeps the optimization opt-out-by-failure rather than
@@ -1251,7 +1267,15 @@ export declare type DatabaseHandle<Tables extends Record<string, Table<string, R
1251
1267
  export declare const databaseHandle: <Tables extends Record<string, Table<string, Record<string, ColumnDefinition<unknown>>, boolean, string>>>(tables: Tables) => DatabaseHandle<Tables>;
1252
1268
 
1253
1269
  export declare interface DataStore {
1254
- /** Execute a typed query descriptor and return matching rows. */
1270
+ /**
1271
+ * Execute a typed query descriptor and return matching rows.
1272
+ *
1273
+ * Deliberately NOT generic: `DataStore` is the driver SPI, implemented by
1274
+ * every dialect store and its transactional / namespace views. Threading a
1275
+ * phantom row type through all of them would be pure churn — they genuinely
1276
+ * do return untyped rows off the wire. The row type is re-applied one layer
1277
+ * up, on `FluentStore.query`, which is what handlers actually call.
1278
+ */
1255
1279
  query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row>>;
1256
1280
  /** Insert a row. Emits an insert ChangeEvent (deferred if inside `transactional()`). */
1257
1281
  insert(table: string, row: Row): Promise<Row>;
@@ -1748,6 +1772,22 @@ export declare const drainIdentifierWarnings: () => ReadonlyArray<string>;
1748
1772
  */
1749
1773
  export declare const dropped: () => ColumnBuilder<unknown, "text">;
1750
1774
 
1775
+ /**
1776
+ * Raised when a relation declared `one` matches MORE than one row for the same
1777
+ * parent key — the mis-declaration that silently hands the app an arbitrary
1778
+ * child instead of the parent it asked for.
1779
+ *
1780
+ * It is its own class because BOTH eager paths raise it: the walker while
1781
+ * bucketing, the single-query JSON path while decoding an over-fetched array.
1782
+ * The stores' JSON-agg attempt falls back to the walker on any error; this one
1783
+ * must NOT trigger that fallback — the walker would re-run the same query and
1784
+ * raise the same message, costing a round trip to reach the identical outcome.
1785
+ */
1786
+ export declare class EagerCardinalityError extends Error {
1787
+ readonly _tag = "EagerCardinalityError";
1788
+ constructor(message: string);
1789
+ }
1790
+
1751
1791
  /**
1752
1792
  * Compiled JSON-aggregation plan: a `Statement.Statement` ready to
1753
1793
  * execute via `SqlClient.SqlClient`, plus a `decode()` that restores
@@ -1794,6 +1834,28 @@ export declare interface EagerLoadSpec {
1794
1834
  /** Per-parent offset. Only meaningful with `limit`. */
1795
1835
  readonly offset?: number;
1796
1836
  readonly with?: WithSpec;
1837
+ /**
1838
+ * Project the JUNCTION row's own columns into a `manyToMany` eager result.
1839
+ *
1840
+ * `true` takes every junction column; an array takes only those named. The
1841
+ * values land under `_junction` on each target row:
1842
+ *
1843
+ * projects.with({ teams: { junction: ['role', 'addedAt'] } })
1844
+ * // → team._junction.addedAt
1845
+ *
1846
+ * Nested rather than merged onto the target row, deliberately: a junction and
1847
+ * its target routinely share column names (`createdAt` is the obvious one),
1848
+ * and merging would silently overwrite real target data with membership data.
1849
+ * A name collision that corrupts a row is worse than one extra level.
1850
+ *
1851
+ * Why this exists: junction tables carrying meaningful columns — a role, a
1852
+ * joined-at stamp, a permission tier — are the rule rather than the
1853
+ * exception, and without this an eager load could FILTER on them
1854
+ * (`onJunction`) but never return them, so any relation with real membership
1855
+ * data had to stay a hand-written join. The junction rows were already being
1856
+ * fetched in full to resolve the target ids; this stops throwing them away.
1857
+ */
1858
+ readonly junction?: true | ReadonlyArray<string>;
1797
1859
  /**
1798
1860
  * Filter a `manyToMany` eager branch on the THROUGH-table's columns.
1799
1861
  * The predicate is evaluated against the junction row, not the target
@@ -1818,14 +1880,6 @@ export declare interface EagerLoadSpec {
1818
1880
  */
1819
1881
  export declare type EagerLookup = (descriptor: QueryDescriptor) => Promise<ReadonlyArray<Row_2>>;
1820
1882
 
1821
- /**
1822
- * The compiler walks `descriptor.eager` ONCE to build this tree —
1823
- * each node has the relation's resolved kind, the resolved FK column
1824
- * names (auto-inferred when not explicit), the target table's column
1825
- * shape, and the resolved nested children. Dialects render this
1826
- * structure into their JSON-agg dialect; the decoder walks it to
1827
- * restore types.
1828
- */
1829
1883
  export declare interface EagerRelationNode {
1830
1884
  readonly relationName: string;
1831
1885
  readonly relation: Relation;
@@ -1839,6 +1893,8 @@ export declare interface EagerRelationNode {
1839
1893
  readonly fkSide: 'source' | 'target';
1840
1894
  readonly branch: EagerLoadSpec;
1841
1895
  readonly children: ReadonlyArray<EagerRelationNode>;
1896
+ /** Set only on `manyToMany` branches that asked for junction columns. */
1897
+ readonly junction: JunctionProjection | undefined;
1842
1898
  }
1843
1899
 
1844
1900
  export declare interface EagerRootPlan {
@@ -2489,6 +2545,30 @@ export declare interface JsonIndexPath {
2489
2545
  readonly numeric: boolean;
2490
2546
  }
2491
2547
 
2548
+ /**
2549
+ * The compiler walks `descriptor.eager` ONCE to build this tree —
2550
+ * each node has the relation's resolved kind, the resolved FK column
2551
+ * names (auto-inferred when not explicit), the target table's column
2552
+ * shape, and the resolved nested children. Dialects render this
2553
+ * structure into their JSON-agg dialect; the decoder walks it to
2554
+ * restore types.
2555
+ */
2556
+ /**
2557
+ * A resolved `junction: [...]` / `junction: true` projection on a `manyToMany`
2558
+ * branch. `requested` is what the caller asked for IN ORDER; `present` is the
2559
+ * subset the through table actually has — the only columns SQL can select. The
2560
+ * decoder re-materialises the difference as `undefined`, matching the walker,
2561
+ * which reads a missing key off the junction row and gets `undefined` rather
2562
+ * than raising.
2563
+ */
2564
+ export declare interface JunctionProjection {
2565
+ readonly through: TableLike;
2566
+ readonly requested: ReadonlyArray<string>;
2567
+ readonly present: ReadonlyArray<string>;
2568
+ /** Present columns whose schema type needs ISO-string → Date restoration. */
2569
+ readonly dateColumns: ReadonlyArray<string>;
2570
+ }
2571
+
2492
2572
  export declare const lag: (column: string, offset?: number, alias?: string) => WindowBuilder;
2493
2573
 
2494
2574
  export declare const lead: (column: string, offset?: number, alias?: string) => WindowBuilder;
@@ -2801,6 +2881,19 @@ export declare const one: (target: TableRef, options?: {
2801
2881
  readonly sourceKey?: string;
2802
2882
  }) => OneRelation;
2803
2883
 
2884
+ /**
2885
+ * The single source of the `one`-cardinality message. Both eager paths call it
2886
+ * so a user comparing them can never be shown two different explanations of the
2887
+ * same mistake.
2888
+ */
2889
+ export declare const oneCardinalityMessage: (args: {
2890
+ readonly relationName: string;
2891
+ readonly sourceTableName: string;
2892
+ readonly targetTableName: string;
2893
+ readonly foreignKey: string;
2894
+ readonly sourceKey: string;
2895
+ }) => string;
2896
+
2804
2897
  /** 1:1 — the source table OR the target carries the FK. */
2805
2898
  export declare interface OneRelation {
2806
2899
  readonly kind: 'one';
@@ -2829,6 +2922,29 @@ export declare interface OrPredicate {
2829
2922
  readonly or: ReadonlyArray<Predicate>;
2830
2923
  }
2831
2924
 
2925
+ /**
2926
+ * Keyset pagination on ANY orderable column — the general form of
2927
+ * `paginateById`.
2928
+ *
2929
+ * `paginateById` hardcodes `id`, which is correct for a stable typeid/uuidv7
2930
+ * key and useless for the case real feeds actually need: "the next page by
2931
+ * `createdAt`". Apps hit that immediately and fell back to hand-rolled
2932
+ * `limit + 1` / slice / `hasMore` triples — often on a timestamp cursor the
2933
+ * helper could not express at all.
2934
+ *
2935
+ * The column MUST be unique, or monotonic enough that ties do not straddle a
2936
+ * page boundary; otherwise a keyset cursor can skip or repeat rows at the seam.
2937
+ * For a non-unique column (a timestamp with collisions) pass a composite order
2938
+ * on the descriptor first — `.orderBy('createdAt').orderBy('id')` — and
2939
+ * paginate by the tie-breaker.
2940
+ *
2941
+ * `direction` controls both the comparison and the ORDER BY, so a `desc` feed
2942
+ * ("newest first") pages with `<` instead of `>`. Getting that pairing wrong is
2943
+ * the classic keyset bug: an ascending comparison under a descending sort
2944
+ * silently returns the same page forever.
2945
+ */
2946
+ export declare const paginateBy: (descriptor: QueryDescriptor, column: string, cursor: string | number | Date | undefined, limit: number, direction?: "asc" | "desc") => QueryDescriptor;
2947
+
2832
2948
  /**
2833
2949
  * Cursor pagination by primary key — `WHERE id > cursor ORDER BY id ASC LIMIT n`.
2834
2950
  *
@@ -2968,7 +3084,8 @@ export declare interface Query<RowOf, IxNames extends string = string,
2968
3084
  * `'alias.column'` → value type. Unjoined queries default to `{}`.
2969
3085
  */
2970
3086
  Joins extends JoinsMap = {}> {
2971
- readonly descriptor: QueryDescriptor;
3087
+ /** Carries `RowOf` through to `store.query()` — see `QueryDescriptor<R>`. */
3088
+ readonly descriptor: QueryDescriptor<RowOf>;
2972
3089
  /** Refine the predicate. Multiple `.where(...)` calls combine with AND. */
2973
3090
  where: (predicate: Predicate) => Query<RowOf, IxNames, Joins>;
2974
3091
  orderBy: <K extends keyof RowOf & string>(column: K, direction?: 'asc' | 'desc') => Query<RowOf, IxNames, Joins>;
@@ -3338,9 +3455,30 @@ Joins extends JoinsMap = {}> {
3338
3455
  }, IxNames, Joins>;
3339
3456
  }
3340
3457
 
3341
- export declare interface QueryDescriptor {
3458
+ /**
3459
+ * The runtime-facing materialized query — a plain bag of strings the store and
3460
+ * the matcher consume.
3461
+ *
3462
+ * `R` is a PHANTOM row type. It exists so the row shape the typed builder
3463
+ * already knows survives the trip through `.descriptor` into `store.query()`,
3464
+ * which is where it used to be thrown away: `store.query(database.notes...)`
3465
+ * returned `Row` (`Record<string, unknown>`), so every field read needed a
3466
+ * hand-written cast. One downstream app wrote 2,032 of them. The type was
3467
+ * always available — it was just dropped at this boundary.
3468
+ *
3469
+ * It is an OPTIONAL field, never set at runtime. That matters for two reasons:
3470
+ * a plain object literal still satisfies `QueryDescriptor` (dispatcher, matcher
3471
+ * and store code build and forward these structurally), and `R` stays
3472
+ * covariant, so a `QueryDescriptor<Note>` is still assignable to the bare
3473
+ * `QueryDescriptor` that generic infrastructure passes around. Defaulting to
3474
+ * `Row` keeps every existing annotation meaning exactly what it meant before.
3475
+ */
3476
+ export declare interface QueryDescriptor<R = Row> {
3342
3477
  readonly table: string;
3343
3478
  readonly predicate: Predicate | undefined;
3479
+ /** Phantom — carries the row type through to `store.query()`. Never set at
3480
+ * runtime; erased entirely by the compiler. */
3481
+ readonly __row?: R;
3344
3482
  /**
3345
3483
  * Opt OUT of the runtime's automatic `deletedAt IS NULL` scope on
3346
3484
  * `softDelete()` tables. Set by `.withDeleted()`. The query builder is
@@ -3987,6 +4125,19 @@ declare type Row_3 = Readonly<Record<string, unknown>>;
3987
4125
 
3988
4126
  export declare const rowNumber: (alias?: string) => WindowBuilder;
3989
4127
 
4128
+ /**
4129
+ * Build a row schema for `table`.
4130
+ *
4131
+ * `omit` drops columns from the OUTPUT — the way to keep an `.encrypted()`
4132
+ * token or an internal bookkeeping column off the wire. It is a convenience,
4133
+ * not a security boundary: a column omitted here is simply absent from this
4134
+ * schema, and a handler that returns it under a different schema still sends
4135
+ * it.
4136
+ */
4137
+ export declare const rowSchema: <T extends TableLike>(table: T, options?: {
4138
+ readonly omit?: ReadonlyArray<string>;
4139
+ }) => Schema.Schema.Any;
4140
+
3990
4141
  /**
3991
4142
  * Lower a tenant id to the safe identifier fragment used inside a
3992
4143
  * namespace name. ONLY `[a-z0-9_]` survive; every other character —
@@ -4884,6 +5035,33 @@ export declare const validateColumnName: (tableName: string, columnName: string)
4884
5035
  */
4885
5036
  export declare const validateIndexName: (tableName: string, indexName: string, isAuto: boolean, fields: ReadonlyArray<string>) => void;
4886
5037
 
5038
+ /**
5039
+ * Validate every registered relation's key options against the real tables.
5040
+ *
5041
+ * Deliberately a BOOT SWEEP rather than a check inside `registerRelations`:
5042
+ * a relation's target is a thunk precisely so circular declarations can be
5043
+ * written (`one(() => teams, ...)`), and resolving it at registration time
5044
+ * would force the declaration order the DSL exists to avoid. By boot every
5045
+ * table and every spec is loaded, so the thunks are safe to call.
5046
+ *
5047
+ * Throws on the first bad relation, naming the option that would have been
5048
+ * right — see `validateRelationConfig` for the bug that motivated it. A source
5049
+ * table that is not in the table registry is SKIPPED rather than reported:
5050
+ * that is a different failure with its own clearer message at query time, and
5051
+ * turning it into a relations error here would misdirect.
5052
+ */
5053
+ export declare const validateRegisteredRelations: () => void;
5054
+
5055
+ /**
5056
+ * Reject a relation whose explicit key options cannot possibly resolve.
5057
+ * Pure: takes the already-resolved tables, so it is safe to call from a boot
5058
+ * sweep AND from the join compiler.
5059
+ *
5060
+ * Throws with the corrected option named whenever the column exists on the
5061
+ * OTHER side — that mix-up is the whole reason this function exists.
5062
+ */
5063
+ export declare const validateRelationConfig: (source: TableLike, relationName: string, relation: Relation) => void;
5064
+
4887
5065
  /**
4888
5066
  * Validate a table name supplied to `table('<name>', ...)`. Throws on
4889
5067
  * empty / invalid characters / > 63 bytes. Records a WARN above 50.