turbine-orm 0.27.0 → 0.28.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 (52) hide show
  1. package/README.md +17 -13
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/destructive.js +47 -31
  4. package/dist/cjs/cli/index.js +273 -71
  5. package/dist/cjs/cli/mcp.js +788 -0
  6. package/dist/cjs/cli/migrate.js +95 -20
  7. package/dist/cjs/cli/studio.js +3 -2
  8. package/dist/cjs/client.js +267 -34
  9. package/dist/cjs/dialect.js +2 -0
  10. package/dist/cjs/generate.js +171 -7
  11. package/dist/cjs/index.js +4 -1
  12. package/dist/cjs/introspect.js +177 -4
  13. package/dist/cjs/query/batched-loader.js +148 -0
  14. package/dist/cjs/query/builder.js +714 -133
  15. package/dist/cjs/schema-builder.js +59 -4
  16. package/dist/cjs/schema-sql.js +315 -6
  17. package/dist/cjs/seed.js +66 -0
  18. package/dist/cli/config.d.ts +9 -2
  19. package/dist/cli/config.js +19 -3
  20. package/dist/cli/destructive.js +47 -31
  21. package/dist/cli/index.d.ts +52 -1
  22. package/dist/cli/index.js +272 -74
  23. package/dist/cli/mcp.d.ts +17 -0
  24. package/dist/cli/mcp.js +781 -0
  25. package/dist/cli/migrate.d.ts +37 -0
  26. package/dist/cli/migrate.js +92 -20
  27. package/dist/cli/studio.d.ts +3 -2
  28. package/dist/cli/studio.js +3 -2
  29. package/dist/client.d.ts +136 -1
  30. package/dist/client.js +267 -34
  31. package/dist/dialect.d.ts +17 -0
  32. package/dist/dialect.js +2 -0
  33. package/dist/generate.d.ts +17 -0
  34. package/dist/generate.js +171 -10
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +2 -0
  37. package/dist/introspect.d.ts +20 -1
  38. package/dist/introspect.js +175 -4
  39. package/dist/query/batched-loader.d.ts +29 -2
  40. package/dist/query/batched-loader.js +148 -1
  41. package/dist/query/builder.d.ts +156 -8
  42. package/dist/query/builder.js +715 -134
  43. package/dist/query/index.d.ts +1 -1
  44. package/dist/query/types.d.ts +113 -8
  45. package/dist/schema-builder.d.ts +73 -8
  46. package/dist/schema-builder.js +59 -4
  47. package/dist/schema-sql.d.ts +67 -0
  48. package/dist/schema-sql.js +310 -6
  49. package/dist/schema.d.ts +53 -0
  50. package/dist/seed.d.ts +4 -0
  51. package/dist/seed.js +63 -0
  52. package/package.json +2 -3
@@ -5,7 +5,7 @@
5
5
  * `import { … } from './query/index.js'` is a drop-in replacement for the
6
6
  * former monolithic `import { … } from './query.js'`.
7
7
  */
8
- export type { AggregateArgs, AggregateResult, ArrayFilter, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, HavingClause, JsonFilter, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, SelectResult, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
8
+ export type { AggregateArgs, AggregateResult, ArrayFilter, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByArgs, HavingClause, JsonFilter, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
9
9
  export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
10
10
  export { postgresDialect } from '../dialect.js';
11
11
  export type { SqlCacheEntry } from './utils.js';
@@ -79,9 +79,41 @@ export type WhereClause<T> = {
79
79
  /** Relation filters — keyed by relation name, value is { some, every, none } */
80
80
  [relationName: string]: unknown;
81
81
  };
82
+ /**
83
+ * Client-level automatic WHERE filters, keyed by table accessor (the name used
84
+ * in `db[name]` / `client.table(name)`). Each value is AND-merged into the
85
+ * compiled WHERE of every read and mutation on that table, and into every
86
+ * relation subquery that targets it — the mechanism behind soft-delete and
87
+ * multi-tenancy. A function value is evaluated at query-build time (per query),
88
+ * so a closure over per-request state (e.g. the current tenant id) enables
89
+ * request-scoped filters. `create`/`createMany` are never filtered.
90
+ */
91
+ export type GlobalFilters = {
92
+ [tableAccessor: string]: WhereClause<any> | (() => WhereClause<any>);
93
+ };
94
+ /**
95
+ * Per-query opt-out of the configured {@link GlobalFilters}. `true` skips the
96
+ * global filter on the query's own table AND on every relation target it
97
+ * touches; an array skips only the named table accessors (own table and/or
98
+ * relation targets). Global filters never satisfy the empty-`where` guard for
99
+ * `update`/`delete` — that guard always checks the user-supplied `where`.
100
+ */
101
+ export type SkipGlobalFilters = true | readonly string[];
102
+ /**
103
+ * Reserved key in a `with` clause that requests correlated relation counts.
104
+ * `_count: true` counts every to-many relation of the table; a record form
105
+ * (`_count: { posts: true }`) counts only the named to-many relations. Each
106
+ * selected relation resolves to a number on the result row's `_count` object.
107
+ */
108
+ export type WithCount = true | Record<string, true>;
82
109
  /**
83
110
  * Unparameterized with clause — accepts any relation name.
84
111
  * Used internally by the query builder at runtime.
112
+ *
113
+ * The reserved `_count` key (see {@link WithCount}) is also accepted at runtime;
114
+ * the builder reads it via a cast so the narrow `true | WithOptions` element
115
+ * type is preserved for the relation-subquery machinery. Typed callers get a
116
+ * fully-typed `_count` through {@link TypedWithClause}.
85
117
  */
86
118
  export interface WithClause {
87
119
  [relation: string]: true | WithOptions;
@@ -98,6 +130,11 @@ export interface WithClause {
98
130
  */
99
131
  export type TypedWithClause<R extends object = {}> = [keyof R] extends [never] ? WithClause : {
100
132
  [K in keyof R]?: true | WithOptions<RelationRelations<R[K]> & object>;
133
+ } & {
134
+ /** Reserved: correlated relation counts. `true` counts all to-many relations. */
135
+ _count?: true | {
136
+ [K in keyof R]?: true;
137
+ };
101
138
  };
102
139
  /**
103
140
  * Options for an included relation.
@@ -111,7 +148,7 @@ export type TypedWithClause<R extends object = {}> = [keyof R] extends [never] ?
111
148
  export interface WithOptions<NestedR extends object = {}> {
112
149
  with?: TypedWithClause<NestedR>;
113
150
  where?: Record<string, unknown>;
114
- orderBy?: Record<string, OrderDirection>;
151
+ orderBy?: Record<string, OrderDirection | OrderBySpec>;
115
152
  limit?: number;
116
153
  /** Only include these fields from the relation */
117
154
  select?: Record<string, boolean>;
@@ -194,11 +231,31 @@ type ApplyCardinality<Rel, Resolved> = Rel extends RelationDescriptor<infer _T,
194
231
  * @typeParam W - The `with` clause the user passed (e.g. `{ posts: true }` or
195
232
  * `{ posts: { with: { comments: true } } }`).
196
233
  */
197
- export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T : W extends object ? [keyof W & keyof R] extends [never] ? T : T & {
234
+ export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T : W extends object ? W extends {
235
+ _count: infer C;
236
+ } ? // `_count` requested — add the typed count object alongside any relations.
237
+ WithRelationAdditions<T, R, W> & {
238
+ _count: CountResult<C>;
239
+ } : WithRelationAdditions<T, R, W> : T;
240
+ /**
241
+ * The relation additions grafted onto `T` by a `with` clause (the `_count`
242
+ * reserved key is handled separately by {@link WithResult}). Kept as its own
243
+ * alias so the no-`_count` path stays byte-identical to the pre-`_count` type.
244
+ */
245
+ type WithRelationAdditions<T, R extends object, W> = [keyof W & keyof R] extends [never] ? T : T & {
198
246
  [K in keyof W & keyof R]: W[K] extends true ? ApplyCardinality<R[K], RelationTarget<R[K]>> : W[K] extends {
199
247
  with?: infer NestedW;
200
248
  } ? NestedW extends object ? ApplyCardinality<R[K], WithResult<RelationTarget<R[K]>, RelationRelations<R[K]> & object, NestedW>> : ApplyCardinality<R[K], RelationTarget<R[K]>> : ApplyCardinality<R[K], RelationTarget<R[K]>>;
201
- } : T;
249
+ };
250
+ /**
251
+ * Compute the shape of the `_count` object on a result row. `_count: true`
252
+ * counts every to-many relation (keys unknown at the type level → an open
253
+ * `Record<string, number>`); the record form (`_count: { posts: true }`) yields
254
+ * `{ [K in selected]: number }`.
255
+ */
256
+ type CountResult<C> = C extends true ? Record<string, number> : C extends object ? {
257
+ [K in keyof C]: number;
258
+ } : Record<string, number>;
202
259
  /** Extract keys from a boolean record where the value is `true`. */
203
260
  type TrueKeys<S extends Record<string, boolean>> = {
204
261
  [K in keyof S]: S[K] extends true ? K : never;
@@ -230,6 +287,8 @@ export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithCla
230
287
  timeout?: number;
231
288
  /** Override the client's relation-loading strategy for this query. See {@link RelationLoadStrategy}. */
232
289
  relationLoadStrategy?: RelationLoadStrategy;
290
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
291
+ skipGlobalFilters?: SkipGlobalFilters;
233
292
  }
234
293
  export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
235
294
  where?: WhereClause<T>;
@@ -249,6 +308,8 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
249
308
  timeout?: number;
250
309
  /** Override the client's relation-loading strategy for this query. See {@link RelationLoadStrategy}. */
251
310
  relationLoadStrategy?: RelationLoadStrategy;
311
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
312
+ skipGlobalFilters?: SkipGlobalFilters;
252
313
  }
253
314
  export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> extends FindManyArgs<T, R, W, S, O> {
254
315
  /**
@@ -349,6 +410,8 @@ export interface UpdateArgs<T, R extends object = {}> {
349
410
  field: keyof T & string;
350
411
  expected: number;
351
412
  };
413
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
414
+ skipGlobalFilters?: SkipGlobalFilters;
352
415
  }
353
416
  export interface UpdateManyArgs<T> {
354
417
  where: WhereClause<T>;
@@ -357,6 +420,8 @@ export interface UpdateManyArgs<T> {
357
420
  timeout?: number;
358
421
  /** See {@link UpdateArgs.allowFullTableScan}. */
359
422
  allowFullTableScan?: boolean;
423
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
424
+ skipGlobalFilters?: SkipGlobalFilters;
360
425
  }
361
426
  export interface DeleteArgs<T> {
362
427
  where: WhereClause<T>;
@@ -364,6 +429,8 @@ export interface DeleteArgs<T> {
364
429
  timeout?: number;
365
430
  /** See {@link UpdateArgs.allowFullTableScan}. */
366
431
  allowFullTableScan?: boolean;
432
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
433
+ skipGlobalFilters?: SkipGlobalFilters;
367
434
  }
368
435
  export interface DeleteManyArgs<T> {
369
436
  where: WhereClause<T>;
@@ -371,6 +438,8 @@ export interface DeleteManyArgs<T> {
371
438
  timeout?: number;
372
439
  /** See {@link UpdateArgs.allowFullTableScan}. */
373
440
  allowFullTableScan?: boolean;
441
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
442
+ skipGlobalFilters?: SkipGlobalFilters;
374
443
  }
375
444
  export interface UpsertArgs<T> {
376
445
  where: WhereClause<T>;
@@ -378,6 +447,8 @@ export interface UpsertArgs<T> {
378
447
  update: Partial<T>;
379
448
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
380
449
  timeout?: number;
450
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
451
+ skipGlobalFilters?: SkipGlobalFilters;
381
452
  }
382
453
  /** `connectOrCreate` op: connect to the row matching `where`, or create it. */
383
454
  export interface ConnectOrCreateOp<T, TR extends object = {}> {
@@ -434,6 +505,8 @@ export interface CountArgs<T> {
434
505
  where?: WhereClause<T>;
435
506
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
436
507
  timeout?: number;
508
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
509
+ skipGlobalFilters?: SkipGlobalFilters;
437
510
  }
438
511
  /**
439
512
  * Numeric comparison operators usable inside a `having` filter. A bare number
@@ -503,10 +576,12 @@ export interface GroupByArgs<T> {
503
576
  _max?: Partial<Record<keyof T & string, boolean>>;
504
577
  /** Filter whole groups by their aggregate values (SQL HAVING). */
505
578
  having?: HavingClause<T>;
506
- /** Order groups */
507
- orderBy?: Record<string, OrderDirection>;
579
+ /** Order groups (supports {@link OrderBySpec} for NULLS placement). */
580
+ orderBy?: Record<string, OrderDirection | OrderBySpec>;
508
581
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
509
582
  timeout?: number;
583
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
584
+ skipGlobalFilters?: SkipGlobalFilters;
510
585
  }
511
586
  /** Arguments for the standalone aggregate method */
512
587
  export interface AggregateArgs<T> {
@@ -523,6 +598,8 @@ export interface AggregateArgs<T> {
523
598
  _max?: Partial<Record<keyof T & string, boolean>>;
524
599
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
525
600
  timeout?: number;
601
+ /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
602
+ skipGlobalFilters?: SkipGlobalFilters;
526
603
  }
527
604
  /** Result type for aggregate queries */
528
605
  export interface AggregateResult<T> {
@@ -634,8 +711,36 @@ export interface VectorOrderBy {
634
711
  distance: VectorOrderByDistance;
635
712
  }
636
713
  /**
637
- * An orderBy clause maps each column to either a plain direction (`'asc'` /
638
- * `'desc'`) or, for pgvector columns, a KNN distance ordering object.
714
+ * Explicit ordering spec for a column: a direction plus an optional NULLS
715
+ * placement. `{ sort: 'desc', nulls: 'last' }` compiles to
716
+ * `ORDER BY "col" DESC NULLS LAST`. The plain `'asc' | 'desc'` shorthand is
717
+ * still accepted and unchanged. `nulls` is only emitted on engines that
718
+ * support the `NULLS FIRST/LAST` grammar (PostgreSQL, SQLite); requesting it
719
+ * elsewhere throws {@link UnsupportedFeatureError} (E017).
720
+ */
721
+ export interface OrderBySpec {
722
+ sort: OrderDirection;
723
+ nulls?: 'first' | 'last';
724
+ }
725
+ /**
726
+ * Ordering by a relation, keyed by the relation name in an {@link OrderByClause}:
727
+ *
728
+ * - to-many (hasMany / manyToMany): `{ posts: { _count: 'desc' } }` — orders by
729
+ * a correlated `COUNT(*)` of the related rows.
730
+ * - to-one (belongsTo / hasOne): `{ author: { name: 'asc' } }` — orders by a
731
+ * correlated scalar subquery on the target column (an {@link OrderBySpec} with
732
+ * `nulls` is accepted too).
733
+ */
734
+ export type RelationOrderBy = {
735
+ _count: OrderDirection;
736
+ } | Record<string, OrderDirection | OrderBySpec>;
737
+ /**
738
+ * An orderBy clause maps each key to one of:
739
+ * - a plain direction (`'asc'` / `'desc'`),
740
+ * - an {@link OrderBySpec} (`{ sort, nulls }`) for NULLS placement,
741
+ * - for pgvector columns, a KNN distance ordering ({@link VectorOrderBy}),
742
+ * - for a relation name, a {@link RelationOrderBy} (`_count` for to-many, a
743
+ * target column for to-one).
639
744
  */
640
- export type OrderByClause = Record<string, OrderDirection | VectorOrderBy>;
745
+ export type OrderByClause = Record<string, OrderDirection | OrderBySpec | VectorOrderBy | RelationOrderBy>;
641
746
  export {};
@@ -22,9 +22,19 @@
22
22
  * });
23
23
  * ```
24
24
  */
25
- import type { SchemaMetadata } from './schema.js';
25
+ import type { ReferentialAction, SchemaMetadata } from './schema.js';
26
+ export type { ReferentialAction } from './schema.js';
26
27
  /** Shorthand type names that map to Postgres column types */
27
- export type ColumnTypeName = 'serial' | 'bigserial' | 'bigint' | 'integer' | 'smallint' | 'text' | 'varchar' | 'boolean' | 'timestamp' | 'timestamptz' | 'date' | 'json' | 'jsonb' | 'uuid' | 'real' | 'double' | 'numeric' | 'bytea';
28
+ export type ColumnTypeName = 'serial' | 'bigserial' | 'bigint' | 'integer' | 'smallint' | 'text' | 'varchar' | 'boolean' | 'timestamp' | 'timestamptz' | 'date' | 'json' | 'jsonb' | 'uuid' | 'real' | 'double' | 'numeric' | 'bytea' | 'enum' | 'vector';
29
+ /** Foreign key reference with optional referential actions. */
30
+ export interface ReferenceDef {
31
+ /** REFERENCES target in "table.column" form. */
32
+ target: string;
33
+ /** `ON DELETE` action. Omit for the Postgres default (`NO ACTION`). */
34
+ onDelete?: ReferentialAction;
35
+ /** `ON UPDATE` action. Omit for the Postgres default (`NO ACTION`). */
36
+ onUpdate?: ReferentialAction;
37
+ }
28
38
  /** Column definition as a plain object. This is what users write. */
29
39
  export interface ColumnDef {
30
40
  /** Column type (required) */
@@ -39,13 +49,24 @@ export interface ColumnDef {
39
49
  unique?: boolean;
40
50
  /** DEFAULT expression (raw SQL, e.g. 'now()' or "'active'") */
41
51
  default?: string;
42
- /** REFERENCES target in "table.column" form */
43
- references?: string;
52
+ /**
53
+ * REFERENCES target. Either the "table.column" string form or an object with
54
+ * referential actions ({@link ReferenceDef}).
55
+ */
56
+ references?: string | ReferenceDef;
44
57
  /** Max length for varchar columns */
45
58
  maxLength?: number;
59
+ /** Enum type name — required when `type: 'enum'`. */
60
+ enumName?: string;
61
+ /** pgvector dimension count — required when `type: 'vector'`. */
62
+ dimensions?: number;
63
+ /** When true, the column is an array of `type` (e.g. `text[]`). */
64
+ array?: boolean;
65
+ /** Column-level `CHECK` expression (raw SQL, e.g. `price >= 0`). */
66
+ check?: string;
46
67
  }
47
68
  /** Postgres-level column type (uppercase, as used in DDL) */
48
- export type ColumnType = 'SERIAL' | 'BIGSERIAL' | 'BIGINT' | 'INTEGER' | 'SMALLINT' | 'TEXT' | 'BOOLEAN' | 'TIMESTAMPTZ' | 'JSONB' | 'UUID' | 'REAL' | 'DOUBLE PRECISION' | 'NUMERIC' | 'BYTEA' | 'DATE' | 'VARCHAR';
69
+ export type ColumnType = 'SERIAL' | 'BIGSERIAL' | 'BIGINT' | 'INTEGER' | 'SMALLINT' | 'TEXT' | 'BOOLEAN' | 'TIMESTAMPTZ' | 'JSONB' | 'UUID' | 'REAL' | 'DOUBLE PRECISION' | 'NUMERIC' | 'BYTEA' | 'DATE' | 'VARCHAR' | 'ENUM' | 'VECTOR';
49
70
  export interface ColumnConfig {
50
71
  type: ColumnType;
51
72
  isPrimaryKey: boolean;
@@ -55,6 +76,18 @@ export interface ColumnConfig {
55
76
  defaultValue: string | null;
56
77
  referencesTarget: string | null;
57
78
  maxLength: number | null;
79
+ /** FK `ON DELETE` action, or null for the Postgres default. */
80
+ onDelete: ReferentialAction | null;
81
+ /** FK `ON UPDATE` action, or null for the Postgres default. */
82
+ onUpdate: ReferentialAction | null;
83
+ /** Enum type name when `type === 'ENUM'`, else null. */
84
+ enumName: string | null;
85
+ /** pgvector dimensions when `type === 'VECTOR'`, else null. */
86
+ vectorDimensions: number | null;
87
+ /** Whether the column is an array of its base type. */
88
+ isArray: boolean;
89
+ /** Column-level `CHECK` expression, or null. */
90
+ check: string | null;
58
91
  }
59
92
  /**
60
93
  * Explicit many-to-many relation declaration for the code-first schema.
@@ -86,6 +119,13 @@ export interface ManyToManyDef {
86
119
  */
87
120
  references?: string | readonly string[];
88
121
  }
122
+ /** A table-level named (or unnamed) `CHECK` constraint. */
123
+ export interface CheckDef {
124
+ /** Optional constraint name → `CONSTRAINT "name" CHECK (...)`. */
125
+ name?: string;
126
+ /** Raw SQL boolean expression, e.g. `price > cost`. */
127
+ expression: string;
128
+ }
89
129
  export interface TableDef {
90
130
  /**
91
131
  * DDL-facing table name (snake_case). This is the name used when generating
@@ -117,6 +157,8 @@ export interface TableDef {
117
157
  * {@link SchemaMetadata} with `manyToMany` {@link RelationDef}s.
118
158
  */
119
159
  manyToMany?: readonly ManyToManyDef[];
160
+ /** Table-level `CHECK` constraints. */
161
+ checks?: readonly CheckDef[];
120
162
  }
121
163
  /**
122
164
  * User-facing input shape for a single table when using the object format.
@@ -127,8 +169,10 @@ export interface TableInput {
127
169
  primaryKey?: readonly string[];
128
170
  /** Optional explicit many-to-many relations on this table */
129
171
  manyToMany?: readonly ManyToManyDef[];
172
+ /** Optional table-level CHECK constraints */
173
+ checks?: readonly CheckDef[];
130
174
  /** Column definitions keyed by camelCase field name */
131
- [columnName: string]: ColumnDef | readonly string[] | readonly ManyToManyDef[] | undefined;
175
+ [columnName: string]: ColumnDef | readonly string[] | readonly ManyToManyDef[] | readonly CheckDef[] | undefined;
132
176
  }
133
177
  export interface SchemaDef {
134
178
  /**
@@ -137,6 +181,22 @@ export interface SchemaDef {
137
181
  * name is available as `tables[key].name`.
138
182
  */
139
183
  tables: Record<string, TableDef>;
184
+ /**
185
+ * Schema-level enum declarations (`CREATE TYPE "<name>" AS ENUM (...)`),
186
+ * keyed by DDL enum type name → ordered labels. Consumed by `schema-sql.ts`
187
+ * to emit `CREATE TYPE` before the tables that reference them and by
188
+ * `generate.ts` for string-literal union codegen. Omitted when no enums are
189
+ * declared (back-compat with `{ tables }`-only consumers).
190
+ */
191
+ enums?: Record<string, readonly string[]>;
192
+ }
193
+ /** Options accepted by {@link defineSchema}. */
194
+ export interface DefineSchemaOptions {
195
+ /**
196
+ * Schema-level enum declarations, keyed by DDL enum type name → ordered
197
+ * labels. Columns opt in via `{ type: 'enum', enumName: '<name>' }`.
198
+ */
199
+ enums?: Record<string, readonly string[]>;
140
200
  }
141
201
  /** Input format: table name -> column defs (object format) or TableDef (legacy builder) */
142
202
  type SchemaInput = Record<string, Record<string, ColumnDef> | TableDef | TableInput>;
@@ -159,7 +219,7 @@ type SchemaInput = Record<string, Record<string, ColumnDef> | TableDef | TableIn
159
219
  * });
160
220
  * ```
161
221
  */
162
- export declare function defineSchema(input: SchemaInput): SchemaDef;
222
+ export declare function defineSchema(input: SchemaInput, options?: DefineSchemaOptions): SchemaDef;
163
223
  export declare class ColumnBuilder {
164
224
  private _config;
165
225
  constructor();
@@ -186,7 +246,12 @@ export declare class ColumnBuilder {
186
246
  nullable(): this;
187
247
  unique(): this;
188
248
  default(val: string): this;
189
- references(target: string): this;
249
+ references(target: string, opts?: {
250
+ onDelete?: ReferentialAction;
251
+ onUpdate?: ReferentialAction;
252
+ }): this;
253
+ check(expression: string): this;
254
+ array(): this;
190
255
  build(): ColumnConfig;
191
256
  }
192
257
  /** @deprecated Use defineSchema() with plain objects instead */
@@ -51,12 +51,34 @@ const TYPE_MAP = {
51
51
  double: 'DOUBLE PRECISION',
52
52
  numeric: 'NUMERIC',
53
53
  bytea: 'BYTEA',
54
+ // Sentinels — the real DDL type is derived from `enumName` / `dimensions`
55
+ // in schema-sql.ts, never from these placeholders.
56
+ enum: 'ENUM',
57
+ vector: 'VECTOR',
54
58
  };
55
59
  /** Convert a user-facing ColumnDef to the internal ColumnConfig */
56
60
  function resolveColumn(def) {
57
61
  if (!(def.type in TYPE_MAP)) {
58
62
  throw new Error(`Invalid column type "${def.type}". Valid types: ${Object.keys(TYPE_MAP).join(', ')}`);
59
63
  }
64
+ if (def.type === 'enum' && !def.enumName) {
65
+ throw new Error(`Column of type "enum" requires an "enumName" (the CREATE TYPE name).`);
66
+ }
67
+ if (def.type === 'vector' && (def.dimensions == null || def.dimensions <= 0)) {
68
+ throw new Error(`Column of type "vector" requires a positive "dimensions" count.`);
69
+ }
70
+ // `references` is either the "table.column" string or a { target, onDelete, onUpdate } object.
71
+ let referencesTarget = null;
72
+ let onDelete = null;
73
+ let onUpdate = null;
74
+ if (typeof def.references === 'string') {
75
+ referencesTarget = def.references;
76
+ }
77
+ else if (def.references) {
78
+ referencesTarget = def.references.target;
79
+ onDelete = def.references.onDelete ?? null;
80
+ onUpdate = def.references.onUpdate ?? null;
81
+ }
60
82
  return {
61
83
  type: TYPE_MAP[def.type],
62
84
  isPrimaryKey: def.primaryKey ?? false,
@@ -64,8 +86,14 @@ function resolveColumn(def) {
64
86
  isNullable: def.nullable ?? false,
65
87
  isUnique: def.unique ?? false,
66
88
  defaultValue: def.default ?? null,
67
- referencesTarget: def.references ?? null,
89
+ referencesTarget,
68
90
  maxLength: def.maxLength ?? null,
91
+ onDelete,
92
+ onUpdate,
93
+ enumName: def.enumName ?? null,
94
+ vectorDimensions: def.dimensions ?? null,
95
+ isArray: def.array ?? false,
96
+ check: def.check ?? null,
69
97
  };
70
98
  }
71
99
  /** Check if a value is a TableDef (from legacy table() builder) */
@@ -91,7 +119,7 @@ function isTableDef(v) {
91
119
  * });
92
120
  * ```
93
121
  */
94
- export function defineSchema(input) {
122
+ export function defineSchema(input, options) {
95
123
  const tables = {};
96
124
  for (const [accessor, value] of Object.entries(input)) {
97
125
  // The user-facing key is the camelCase JS accessor; the DDL-facing
@@ -110,6 +138,7 @@ export function defineSchema(input) {
110
138
  const columns = {};
111
139
  let pk;
112
140
  let m2m;
141
+ let checks;
113
142
  for (const [fieldName, def] of Object.entries(raw)) {
114
143
  if (fieldName === 'manyToMany') {
115
144
  if (def !== undefined) {
@@ -120,6 +149,15 @@ export function defineSchema(input) {
120
149
  }
121
150
  continue;
122
151
  }
152
+ if (fieldName === 'checks') {
153
+ if (def !== undefined) {
154
+ if (!Array.isArray(def)) {
155
+ throw new Error(`Table "${accessor}": "checks" must be an array of { name?, expression } objects`);
156
+ }
157
+ checks = def;
158
+ }
159
+ continue;
160
+ }
123
161
  if (fieldName === 'primaryKey') {
124
162
  // Top-level composite primary key declaration
125
163
  if (def !== undefined) {
@@ -160,10 +198,11 @@ export function defineSchema(input) {
160
198
  columns,
161
199
  ...(pk && pk.length > 0 ? { primaryKey: pk } : {}),
162
200
  ...(m2m && m2m.length > 0 ? { manyToMany: m2m } : {}),
201
+ ...(checks && checks.length > 0 ? { checks } : {}),
163
202
  };
164
203
  }
165
204
  }
166
- return { tables };
205
+ return { tables, ...(options?.enums ? { enums: options.enums } : {}) };
167
206
  }
168
207
  /**
169
208
  * Local copy of camelToSnake to avoid a circular import dependency at the
@@ -187,6 +226,12 @@ export class ColumnBuilder {
187
226
  defaultValue: null,
188
227
  referencesTarget: null,
189
228
  maxLength: null,
229
+ onDelete: null,
230
+ onUpdate: null,
231
+ enumName: null,
232
+ vectorDimensions: null,
233
+ isArray: false,
234
+ check: null,
190
235
  };
191
236
  }
192
237
  serial() {
@@ -283,8 +328,18 @@ export class ColumnBuilder {
283
328
  this._config.defaultValue = val;
284
329
  return this;
285
330
  }
286
- references(target) {
331
+ references(target, opts) {
287
332
  this._config.referencesTarget = target;
333
+ this._config.onDelete = opts?.onDelete ?? null;
334
+ this._config.onUpdate = opts?.onUpdate ?? null;
335
+ return this;
336
+ }
337
+ check(expression) {
338
+ this._config.check = expression;
339
+ return this;
340
+ }
341
+ array() {
342
+ this._config.isArray = true;
288
343
  return this;
289
344
  }
290
345
  build() {
@@ -5,11 +5,21 @@
5
5
  * Also provides diff and push commands for syncing schema to a live database.
6
6
  */
7
7
  import { type Dialect } from './dialect.js';
8
+ import { type ReferentialAction } from './schema.js';
8
9
  import type { SchemaDef, TableDef } from './schema-builder.js';
9
10
  export interface SchemaSqlOptions {
10
11
  /** SQL dialect used for DDL generation. Defaults to PostgreSQL. */
11
12
  dialect?: Dialect;
13
+ /**
14
+ * How to handle the pgvector extension when the schema contains a `vector`
15
+ * column. `'auto'` (default) prepends `CREATE EXTENSION IF NOT EXISTS vector;`
16
+ * — appropriate for `push`. `'manual'` emits a leading comment only, so the
17
+ * generated `.sql` migration doesn't silently require superuser privileges.
18
+ */
19
+ extensions?: 'auto' | 'manual';
12
20
  }
21
+ /** Map a {@link ReferentialAction} to its SQL keyword form. */
22
+ export declare function referentialActionToSql(action: ReferentialAction): string;
13
23
  /**
14
24
  * Convert a SchemaDef into an ordered array of SQL DDL statements.
15
25
  *
@@ -44,7 +54,64 @@ export interface DiffResult {
44
54
  statements: string[];
45
55
  /** SQL statements to reverse the diff (DOWN direction, for migrations) */
46
56
  reverseStatements: string[];
57
+ /**
58
+ * Human-readable warnings for changes the diff detected but refuses to apply
59
+ * automatically because they are destructive or otherwise unsafe (enum value
60
+ * removal/reorder, etc.). Never executed — surfaced for the operator.
61
+ */
62
+ warnings?: string[];
47
63
  }
64
+ /** A FK's current referential actions as read from the DB. */
65
+ export interface DbForeignKey {
66
+ constraintName: string;
67
+ column: string;
68
+ targetTable: string;
69
+ targetColumn: string;
70
+ onDelete: ReferentialAction;
71
+ onUpdate: ReferentialAction;
72
+ }
73
+ /**
74
+ * Build the `ADD CONSTRAINT ... FOREIGN KEY` statement for a FK with the given
75
+ * referential actions. Default (`no action`) clauses are omitted, matching how
76
+ * Postgres normalizes them, so re-diffing is stable.
77
+ */
78
+ export declare function buildAddForeignKeyStatement(table: string, constraintName: string, column: string, targetTable: string, targetColumn: string, onDelete: ReferentialAction, onUpdate: ReferentialAction, dialect?: Dialect): string;
79
+ /**
80
+ * Decide whether a FK's referential actions changed. When they differ, returns
81
+ * the DROP + ADD CONSTRAINT statements (and their reverse) — Postgres has no
82
+ * `ALTER CONSTRAINT` for referential actions, so drop-and-recreate is the only
83
+ * path. Returns null when the actions already match.
84
+ */
85
+ export declare function diffReferentialAction(table: string, db: DbForeignKey, schemaOnDelete: ReferentialAction, schemaOnUpdate: ReferentialAction, dialect?: Dialect): {
86
+ statements: string[];
87
+ reverseStatements: string[];
88
+ } | null;
89
+ /**
90
+ * Compute append-only enum value changes. Returns `ALTER TYPE ... ADD VALUE`
91
+ * statements for labels present in the schema but not the DB (in order), plus a
92
+ * destructive warning for any DB label the schema dropped or any reorder —
93
+ * Postgres cannot remove or reorder enum values without recreating the type.
94
+ */
95
+ export declare function diffEnumValues(enumName: string, schemaLabels: readonly string[], dbLabels: readonly string[], dialect?: Dialect): {
96
+ statements: string[];
97
+ warnings: string[];
98
+ };
99
+ /** A check constraint as declared or read from the DB. */
100
+ export interface CheckSpec {
101
+ name: string;
102
+ expression: string;
103
+ }
104
+ /**
105
+ * Diff a table's CHECK constraints (matched by name). Adds constraints missing
106
+ * from the DB, drops DB constraints absent from the schema, and drop+adds when a
107
+ * same-named constraint's expression changed. Expression comparison is a naive
108
+ * whitespace-insensitive match — semantically-equal-but-different-spelled
109
+ * expressions may re-emit (documented; harmless drop+add).
110
+ */
111
+ export declare function diffCheckConstraints(table: string, schemaChecks: readonly CheckSpec[], dbChecks: readonly CheckSpec[], dialect?: Dialect): {
112
+ statements: string[];
113
+ reverseStatements: string[];
114
+ };
48
115
  /**
49
116
  * Compare a SchemaDef against a live Postgres database and return the diff.
50
117
  *