relq 1.0.4 → 1.0.6

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 (84) hide show
  1. package/dist/cjs/cli/commands/add.cjs +252 -12
  2. package/dist/cjs/cli/commands/commit.cjs +12 -1
  3. package/dist/cjs/cli/commands/export.cjs +25 -19
  4. package/dist/cjs/cli/commands/import.cjs +219 -100
  5. package/dist/cjs/cli/commands/init.cjs +86 -14
  6. package/dist/cjs/cli/commands/pull.cjs +104 -23
  7. package/dist/cjs/cli/commands/push.cjs +38 -3
  8. package/dist/cjs/cli/index.cjs +9 -1
  9. package/dist/cjs/cli/utils/ast/codegen/builder.cjs +297 -0
  10. package/dist/cjs/cli/utils/ast/codegen/constraints.cjs +185 -0
  11. package/dist/cjs/cli/utils/ast/codegen/defaults.cjs +311 -0
  12. package/dist/cjs/cli/utils/ast/codegen/index.cjs +24 -0
  13. package/dist/cjs/cli/utils/ast/codegen/type-map.cjs +116 -0
  14. package/dist/cjs/cli/utils/ast/codegen/utils.cjs +69 -0
  15. package/dist/cjs/cli/utils/ast/index.cjs +19 -0
  16. package/dist/cjs/cli/utils/ast/transformer/helpers.cjs +154 -0
  17. package/dist/cjs/cli/utils/ast/transformer/index.cjs +25 -0
  18. package/dist/cjs/cli/utils/ast/types.cjs +2 -0
  19. package/dist/cjs/cli/utils/ast-codegen.cjs +949 -0
  20. package/dist/cjs/cli/utils/ast-transformer.cjs +916 -0
  21. package/dist/cjs/cli/utils/change-tracker.cjs +50 -1
  22. package/dist/cjs/cli/utils/cli-utils.cjs +151 -0
  23. package/dist/cjs/cli/utils/fast-introspect.cjs +149 -23
  24. package/dist/cjs/cli/utils/pg-parser.cjs +1 -0
  25. package/dist/cjs/cli/utils/repo-manager.cjs +121 -4
  26. package/dist/cjs/cli/utils/schema-comparator.cjs +98 -14
  27. package/dist/cjs/cli/utils/schema-introspect.cjs +56 -19
  28. package/dist/cjs/cli/utils/snapshot-manager.cjs +0 -1
  29. package/dist/cjs/cli/utils/sql-generator.cjs +353 -64
  30. package/dist/cjs/cli/utils/type-generator.cjs +114 -15
  31. package/dist/cjs/core/relq-client.cjs +22 -6
  32. package/dist/cjs/schema-definition/column-types.cjs +150 -13
  33. package/dist/cjs/schema-definition/defaults.cjs +72 -0
  34. package/dist/cjs/schema-definition/index.cjs +15 -1
  35. package/dist/cjs/schema-definition/introspection.cjs +7 -3
  36. package/dist/cjs/schema-definition/pg-relations.cjs +169 -0
  37. package/dist/cjs/schema-definition/pg-view.cjs +30 -0
  38. package/dist/cjs/schema-definition/table-definition.cjs +110 -4
  39. package/dist/cjs/types/config-types.cjs +13 -4
  40. package/dist/cjs/utils/aws-dsql.cjs +177 -0
  41. package/dist/config.d.ts +146 -1
  42. package/dist/esm/cli/commands/add.js +250 -13
  43. package/dist/esm/cli/commands/commit.js +12 -1
  44. package/dist/esm/cli/commands/export.js +25 -19
  45. package/dist/esm/cli/commands/import.js +221 -102
  46. package/dist/esm/cli/commands/init.js +86 -14
  47. package/dist/esm/cli/commands/pull.js +106 -25
  48. package/dist/esm/cli/commands/push.js +39 -4
  49. package/dist/esm/cli/index.js +9 -1
  50. package/dist/esm/cli/utils/ast/codegen/builder.js +291 -0
  51. package/dist/esm/cli/utils/ast/codegen/constraints.js +176 -0
  52. package/dist/esm/cli/utils/ast/codegen/defaults.js +305 -0
  53. package/dist/esm/cli/utils/ast/codegen/index.js +6 -0
  54. package/dist/esm/cli/utils/ast/codegen/type-map.js +111 -0
  55. package/dist/esm/cli/utils/ast/codegen/utils.js +60 -0
  56. package/dist/esm/cli/utils/ast/index.js +3 -0
  57. package/dist/esm/cli/utils/ast/transformer/helpers.js +141 -0
  58. package/dist/esm/cli/utils/ast/transformer/index.js +2 -0
  59. package/dist/esm/cli/utils/ast/types.js +1 -0
  60. package/dist/esm/cli/utils/ast-codegen.js +945 -0
  61. package/dist/esm/cli/utils/ast-transformer.js +907 -0
  62. package/dist/esm/cli/utils/change-tracker.js +50 -1
  63. package/dist/esm/cli/utils/cli-utils.js +147 -0
  64. package/dist/esm/cli/utils/fast-introspect.js +149 -23
  65. package/dist/esm/cli/utils/pg-parser.js +1 -0
  66. package/dist/esm/cli/utils/repo-manager.js +114 -4
  67. package/dist/esm/cli/utils/schema-comparator.js +98 -14
  68. package/dist/esm/cli/utils/schema-introspect.js +56 -19
  69. package/dist/esm/cli/utils/snapshot-manager.js +0 -1
  70. package/dist/esm/cli/utils/sql-generator.js +353 -64
  71. package/dist/esm/cli/utils/type-generator.js +114 -15
  72. package/dist/esm/core/relq-client.js +23 -7
  73. package/dist/esm/schema-definition/column-types.js +147 -12
  74. package/dist/esm/schema-definition/defaults.js +69 -0
  75. package/dist/esm/schema-definition/index.js +3 -0
  76. package/dist/esm/schema-definition/introspection.js +7 -3
  77. package/dist/esm/schema-definition/pg-relations.js +161 -0
  78. package/dist/esm/schema-definition/pg-view.js +24 -0
  79. package/dist/esm/schema-definition/table-definition.js +110 -4
  80. package/dist/esm/types/config-types.js +12 -4
  81. package/dist/esm/utils/aws-dsql.js +139 -0
  82. package/dist/index.d.ts +159 -1
  83. package/dist/schema-builder.d.ts +1314 -32
  84. package/package.json +1 -1
@@ -1,3 +1,77 @@
1
+ /**
2
+ * Type-safe PostgreSQL DEFAULT value helpers
3
+ * Covers all PostgreSQL default value types with 100% typed output
4
+ */
5
+ export interface DefaultValue {
6
+ readonly $sql: string;
7
+ readonly $isDefault: true;
8
+ }
9
+ export declare const DEFAULT: {
10
+ readonly genRandomUuid: () => DefaultValue;
11
+ readonly uuidGenerateV4: () => DefaultValue;
12
+ readonly uuidGenerateV1: () => DefaultValue;
13
+ readonly uuidGenerateV1mc: () => DefaultValue;
14
+ readonly uuidNil: () => DefaultValue;
15
+ readonly now: () => DefaultValue;
16
+ readonly currentTimestamp: () => DefaultValue;
17
+ readonly currentDate: () => DefaultValue;
18
+ readonly currentTime: () => DefaultValue;
19
+ readonly localTimestamp: () => DefaultValue;
20
+ readonly localTime: () => DefaultValue;
21
+ readonly transactionTimestamp: () => DefaultValue;
22
+ readonly statementTimestamp: () => DefaultValue;
23
+ readonly clockTimestamp: () => DefaultValue;
24
+ readonly timeofday: () => DefaultValue;
25
+ readonly interval: (value: string) => DefaultValue;
26
+ readonly currentUser: () => DefaultValue;
27
+ readonly sessionUser: () => DefaultValue;
28
+ readonly user: () => DefaultValue;
29
+ readonly currentSchema: () => DefaultValue;
30
+ readonly currentDatabase: () => DefaultValue;
31
+ readonly currentCatalog: () => DefaultValue;
32
+ readonly inetClientAddr: () => DefaultValue;
33
+ readonly inetClientPort: () => DefaultValue;
34
+ readonly inetServerAddr: () => DefaultValue;
35
+ readonly inetServerPort: () => DefaultValue;
36
+ readonly pgBackendPid: () => DefaultValue;
37
+ readonly nextval: (sequenceName: string) => DefaultValue;
38
+ readonly currval: (sequenceName: string) => DefaultValue;
39
+ readonly lastval: () => DefaultValue;
40
+ readonly random: () => DefaultValue;
41
+ readonly pi: () => DefaultValue;
42
+ readonly emptyString: () => DefaultValue;
43
+ readonly emptyObject: () => DefaultValue;
44
+ readonly emptyJson: () => DefaultValue;
45
+ readonly emptyJsonb: () => DefaultValue;
46
+ readonly emptyArray: () => DefaultValue;
47
+ readonly emptyArrayOf: (type: string) => DefaultValue;
48
+ readonly true: () => DefaultValue;
49
+ readonly false: () => DefaultValue;
50
+ readonly null: () => DefaultValue;
51
+ readonly zero: () => DefaultValue;
52
+ readonly one: () => DefaultValue;
53
+ readonly negativeOne: () => DefaultValue;
54
+ readonly string: (value: string) => DefaultValue;
55
+ readonly number: (value: number) => DefaultValue;
56
+ readonly integer: (value: number) => DefaultValue;
57
+ readonly decimal: (value: number, precision?: number) => DefaultValue;
58
+ readonly cast: (value: string | number, type: string) => DefaultValue;
59
+ readonly emptyTsvector: () => DefaultValue;
60
+ readonly point: (x: number, y: number) => DefaultValue;
61
+ readonly inet: (address: string) => DefaultValue;
62
+ readonly cidr: (network: string) => DefaultValue;
63
+ readonly macaddr: (address: string) => DefaultValue;
64
+ readonly emptyInt4range: () => DefaultValue;
65
+ readonly emptyInt8range: () => DefaultValue;
66
+ readonly emptyNumrange: () => DefaultValue;
67
+ readonly emptyTsrange: () => DefaultValue;
68
+ readonly emptyTstzrange: () => DefaultValue;
69
+ readonly emptyDaterange: () => DefaultValue;
70
+ readonly emptyHstore: () => DefaultValue;
71
+ readonly emptyBytea: () => DefaultValue;
72
+ readonly money: (value: number | string) => DefaultValue;
73
+ readonly zeroMoney: () => DefaultValue;
74
+ };
1
75
  export declare const EMPTY_OBJECT: unique symbol;
2
76
  export declare const EMPTY_ARRAY: unique symbol;
3
77
  export interface ColumnConfig<T = unknown> {
@@ -5,7 +79,7 @@ export interface ColumnConfig<T = unknown> {
5
79
  $sqlType?: string;
6
80
  $tsType?: T;
7
81
  $nullable?: boolean;
8
- $default?: T | (() => T) | string | object | typeof EMPTY_OBJECT | typeof EMPTY_ARRAY;
82
+ $default?: T | (() => T) | string | object | typeof EMPTY_OBJECT | typeof EMPTY_ARRAY | DefaultValue;
9
83
  $primaryKey?: boolean;
10
84
  $unique?: boolean;
11
85
  $references?: {
@@ -27,6 +101,7 @@ export interface ColumnConfig<T = unknown> {
27
101
  $scale?: number;
28
102
  $withTimezone?: boolean;
29
103
  $columnName?: string;
104
+ $trackingId?: string;
30
105
  }
31
106
  /** Table column references for generated columns - each column becomes a ChainableExpr */
32
107
  export type GeneratedTableRefs<T extends Record<string, ColumnConfig>> = {
@@ -109,13 +184,60 @@ export interface GeneratedFn {
109
184
  extract(field: "year" | "month" | "day" | "hour" | "minute" | "second" | "dow" | "doy" | "week" | "quarter" | "epoch", col: ChainableExpr | GeneratedExpr): ChainableExpr;
110
185
  datePart(field: string, col: ChainableExpr | GeneratedExpr): ChainableExpr;
111
186
  age(col1: ChainableExpr | GeneratedExpr, col2: ChainableExpr | GeneratedExpr): ChainableExpr;
112
- toTsvector(config: string, col: ChainableExpr | GeneratedExpr): ChainableExpr;
187
+ /** Create tsvector from text using specified config (e.g., 'english', 'simple') */
188
+ toTsvector(config: string, col: ChainableExpr | GeneratedExpr | string): ChainableExpr;
189
+ /** Assign weight (A, B, C, or D) to tsvector for ranking */
190
+ setweight(tsvector: ChainableExpr | GeneratedExpr, weight: "A" | "B" | "C" | "D"): ChainableExpr;
191
+ /** Concatenate multiple tsvectors using || operator */
192
+ tsvectorConcat(...vectors: (ChainableExpr | GeneratedExpr)[]): ChainableExpr;
193
+ /** Generic concatenation using || operator (works for text, tsvector, arrays) */
194
+ concat(...args: (ChainableExpr | GeneratedExpr | string)[]): ChainableExpr;
195
+ /** Text-specific concatenation using || operator */
196
+ textConcat(...args: (ChainableExpr | GeneratedExpr | string)[]): ChainableExpr;
197
+ /** Create tsquery from text */
198
+ toTsquery(config: string, col: ChainableExpr | GeneratedExpr | string): ChainableExpr;
199
+ /** Create tsquery from plain text (handles spaces and punctuation) */
200
+ plaintoTsquery(config: string, col: ChainableExpr | GeneratedExpr | string): ChainableExpr;
201
+ /** Create tsquery from web-style search (handles OR, quotes, etc.) */
202
+ websearchToTsquery(config: string, col: ChainableExpr | GeneratedExpr | string): ChainableExpr;
113
203
  similarity(col1: ChainableExpr | GeneratedExpr, col2: ChainableExpr | GeneratedExpr | string): ChainableExpr;
114
204
  point(x: ChainableExpr | GeneratedExpr | number, y: ChainableExpr | GeneratedExpr | number): ChainableExpr;
115
205
  arrayLength(col: ChainableExpr | GeneratedExpr, dim?: number): ChainableExpr;
116
206
  arrayPosition(arr: ChainableExpr | GeneratedExpr, elem: ChainableExpr | GeneratedExpr | string | number): ChainableExpr;
117
207
  md5(col: ChainableExpr | GeneratedExpr): ChainableExpr;
118
208
  sha256(col: ChainableExpr | GeneratedExpr): ChainableExpr;
209
+ /** Reference a column by name (used when importing SQL schemas) */
210
+ col(name: string, alias?: string): ChainableExpr;
211
+ /** Pass raw SQL expression directly - use for complex expressions */
212
+ raw(sql: string): ChainableExpr;
213
+ /** Cast to any PostgreSQL type by name */
214
+ cast(value: ChainableExpr | GeneratedExpr | string | number, typeName: string): ChainableExpr;
215
+ /** Call any SQL function by name */
216
+ func(name: string, ...args: (ChainableExpr | GeneratedExpr | string | number)[]): ChainableExpr;
217
+ /** Embed raw SQL - use with caution */
218
+ sql(expression: string): ChainableExpr;
219
+ /** Apply an operator between two values */
220
+ op(left: ChainableExpr | GeneratedExpr | string | number, operator: string, right: ChainableExpr | GeneratedExpr | string | number): ChainableExpr;
221
+ setWeight(tsvector: ChainableExpr | GeneratedExpr, weight: string): ChainableExpr;
222
+ asVarchar(col: ChainableExpr | GeneratedExpr): ChainableExpr;
223
+ /** Concatenate text or tsvector values using || operator */
224
+ textConcat(left: ChainableExpr | GeneratedExpr | string, right: ChainableExpr | GeneratedExpr | string): ChainableExpr;
225
+ /** Full-text search match: tsvector @@ tsquery */
226
+ tsMatch(left: ChainableExpr | GeneratedExpr, right: ChainableExpr | GeneratedExpr): ChainableExpr;
227
+ /** Compare two values with a comparison operator */
228
+ compare(left: ChainableExpr | GeneratedExpr | string | number, op: "=" | "<>" | "!=" | "<" | ">" | "<=" | ">=", right: ChainableExpr | GeneratedExpr | string | number): ChainableExpr;
229
+ /** Apply regex match operator */
230
+ regex(value: ChainableExpr | GeneratedExpr, op: "~" | "~*" | "!~" | "!~*", pattern: ChainableExpr | GeneratedExpr | string): ChainableExpr;
231
+ /** Logical AND */
232
+ and(...args: (ChainableExpr | GeneratedExpr)[]): ChainableExpr;
233
+ /** Logical OR */
234
+ or(...args: (ChainableExpr | GeneratedExpr)[]): ChainableExpr;
235
+ /** Logical NOT */
236
+ not(arg: ChainableExpr | GeneratedExpr): ChainableExpr;
237
+ /** IS NULL check */
238
+ isNull(col: ChainableExpr | GeneratedExpr): ChainableExpr;
239
+ /** IS NOT NULL check */
240
+ isNotNull(col: ChainableExpr | GeneratedExpr): ChainableExpr;
119
241
  }
120
242
  /** CASE expression builder */
121
243
  export interface CaseBuilder {
@@ -128,6 +250,133 @@ export interface GeneratedExpr {
128
250
  readonly $sql: string;
129
251
  readonly $expr: true;
130
252
  }
253
+ /**
254
+ * Fluent chainable expression for generated columns.
255
+ * Provides a readable, chainable API similar to index WHERE clauses.
256
+ *
257
+ * ## Basic Usage
258
+ *
259
+ * Access columns via the table parameter in generatedAlwaysAs:
260
+ * ```typescript
261
+ * generatedAlwaysAs(table =>
262
+ * table.firstName.concat(' ', table.lastName)
263
+ * )
264
+ * ```
265
+ *
266
+ * ## Chainable Methods
267
+ *
268
+ * ### Null Handling
269
+ * - `.coalesce(fallback)` - Returns first non-null value
270
+ * - `.nullif(value)` - Returns NULL if equal to value
271
+ *
272
+ * ### Type Casting
273
+ * - `.asText()` - Cast to TEXT
274
+ * - `.asInteger()` - Cast to INTEGER
275
+ * - `.asVarchar()` - Cast to VARCHAR
276
+ *
277
+ * ### Text Functions
278
+ * - `.lower()` - Convert to lowercase
279
+ * - `.upper()` - Convert to uppercase
280
+ * - `.trim()` - Remove whitespace
281
+ * - `.concat(...parts)` - Concatenate with other values/columns
282
+ * - `.substring(start, length?)` - Extract substring
283
+ * - `.replace(from, to)` - Replace text
284
+ * - `.length()` - Get string length
285
+ *
286
+ * ### Full-Text Search
287
+ * - `.toTsvector(config)` - Create tsvector (e.g., 'english')
288
+ * - `.setWeight(weight)` - Set weight A/B/C/D for ranking
289
+ * - `.tsvConcat(other)` - Concatenate tsvectors (|| operator)
290
+ *
291
+ * ### JSON/JSONB
292
+ * - `.jsonExtract(key)` - Extract JSON value (->)
293
+ * - `.jsonExtractText(key)` - Extract as text (->>)
294
+ *
295
+ * ### Math
296
+ * - `.add(n)`, `.subtract(n)`, `.multiply(n)`, `.divide(n)`
297
+ * - `.abs()`, `.round(precision?)`, `.floor()`, `.ceil()`
298
+ *
299
+ * @example Full-text search vector
300
+ * ```typescript
301
+ * searchVector: tsvector().generatedAlwaysAs(table =>
302
+ * table.email.coalesce('').asText().toTsvector('english').setWeight('A')
303
+ * .tsvConcat(table.name.coalesce('').toTsvector('english').setWeight('B'))
304
+ * )
305
+ * ```
306
+ *
307
+ * @example Computed full name
308
+ * ```typescript
309
+ * fullName: text().generatedAlwaysAs(table =>
310
+ * table.firstName.concat(' ', table.lastName)
311
+ * )
312
+ * ```
313
+ */
314
+ export interface FluentGenExpr extends GeneratedExpr {
315
+ /** COALESCE - returns first non-null value */
316
+ coalesce(fallback: FluentGenExpr | string | number): FluentGenExpr;
317
+ /** NULLIF - returns NULL if equal to value */
318
+ nullif(value: FluentGenExpr | string | number): FluentGenExpr;
319
+ /** Cast to TEXT */
320
+ asText(): FluentGenExpr;
321
+ /** Cast to INTEGER */
322
+ asInteger(): FluentGenExpr;
323
+ /** Cast to VARCHAR */
324
+ asVarchar(): FluentGenExpr;
325
+ /** Cast to any PostgreSQL type */
326
+ cast(typeName: string): FluentGenExpr;
327
+ /** Convert to lowercase */
328
+ lower(): FluentGenExpr;
329
+ /** Convert to uppercase */
330
+ upper(): FluentGenExpr;
331
+ /** Remove leading/trailing whitespace */
332
+ trim(): FluentGenExpr;
333
+ /** Concatenate with other values using || operator */
334
+ concat(...parts: (FluentGenExpr | string)[]): FluentGenExpr;
335
+ /** Extract substring */
336
+ substring(start: number, length?: number): FluentGenExpr;
337
+ /** Replace text */
338
+ replace(from: string, to: string): FluentGenExpr;
339
+ /** Get string length */
340
+ length(): FluentGenExpr;
341
+ /** Left pad to specified length */
342
+ lpad(length: number, fill?: string): FluentGenExpr;
343
+ /** Right pad to specified length */
344
+ rpad(length: number, fill?: string): FluentGenExpr;
345
+ /** Create tsvector with specified config (e.g., 'english') */
346
+ toTsvector(config: string): FluentGenExpr;
347
+ /** Set weight for tsvector ranking (A/B/C/D) */
348
+ setWeight(weight: "A" | "B" | "C" | "D"): FluentGenExpr;
349
+ /** Concatenate tsvectors using || operator */
350
+ tsvConcat(other: FluentGenExpr): FluentGenExpr;
351
+ /** Extract JSON value (->) */
352
+ jsonExtract(key: string): FluentGenExpr;
353
+ /** Extract JSON value as text (->>) */
354
+ jsonExtractText(key: string): FluentGenExpr;
355
+ /** Add value */
356
+ add(value: FluentGenExpr | number): FluentGenExpr;
357
+ /** Subtract value */
358
+ subtract(value: FluentGenExpr | number): FluentGenExpr;
359
+ /** Multiply by value */
360
+ multiply(value: FluentGenExpr | number): FluentGenExpr;
361
+ /** Divide by value */
362
+ divide(value: FluentGenExpr | number): FluentGenExpr;
363
+ /** Absolute value */
364
+ abs(): FluentGenExpr;
365
+ /** Round to precision */
366
+ round(precision?: number): FluentGenExpr;
367
+ /** Floor */
368
+ floor(): FluentGenExpr;
369
+ /** Ceiling */
370
+ ceil(): FluentGenExpr;
371
+ /** Extract date part */
372
+ extract(field: "year" | "month" | "day" | "hour" | "minute" | "second"): FluentGenExpr;
373
+ /** Get array length */
374
+ arrayLength(dimension?: number): FluentGenExpr;
375
+ }
376
+ /** Table refs for fluent generated column expressions */
377
+ export type FluentGenTableRefs<T extends Record<string, ColumnConfig>> = {
378
+ [K in keyof T]: FluentGenExpr;
379
+ };
131
380
  export type ColumnBuilder<T, Config extends ColumnConfig<T> = ColumnConfig<T>> = Config & {
132
381
  notNull(): ColumnBuilder<T, Config & {
133
382
  $nullable: false;
@@ -135,7 +384,7 @@ export type ColumnBuilder<T, Config extends ColumnConfig<T> = ColumnConfig<T>> =
135
384
  nullable(): ColumnBuilder<T, Config & {
136
385
  $nullable: true;
137
386
  }>;
138
- default<V extends T | (() => T)>(value: V): ColumnBuilder<T, Config & {
387
+ default<V extends T | (() => T) | DefaultValue>(value: V): ColumnBuilder<T, Config & {
139
388
  $default: V;
140
389
  }>;
141
390
  primaryKey(): ColumnBuilder<T, Config & {
@@ -149,28 +398,31 @@ export type ColumnBuilder<T, Config extends ColumnConfig<T> = ColumnConfig<T>> =
149
398
  onUpdate?: string;
150
399
  }): ColumnBuilder<T, Config>;
151
400
  /**
152
- * Add CHECK constraint with enum-like values.
153
- * Narrows the column type to the union of provided values.
154
- * @param values - Allowed string values
401
+ * Add CHECK constraint with enum-like values and explicit constraint name.
402
+ * Narrows the column type to the union of provided values for autocomplete.
403
+ * @param name - Constraint name (preserved for 1:1 SQL parity)
404
+ * @param values - Allowed string values array (literal types auto-inferred)
155
405
  * @example
156
- * .check('active', 'inactive', 'banned')
406
+ * .check('users_status_check', ['active', 'inactive', 'banned'])
157
407
  * // Type becomes 'active' | 'inactive' | 'banned'
158
408
  */
159
- check<V extends string>(...values: V[]): ColumnBuilder<V, ColumnConfig<V> & {
409
+ check<const V extends readonly string[]>(name: string, values: V): ColumnBuilder<V[number], ColumnConfig<V[number]> & {
160
410
  $check: string;
161
- $checkValues: readonly V[];
411
+ $checkName: string;
412
+ $checkValues: V;
162
413
  }>;
163
414
  /**
164
415
  * Add CHECK constraint that excludes specific values.
165
- * Narrows the column type to exclude provided values.
166
- * @param values - Disallowed string values
416
+ * @param name - Constraint name (preserved for 1:1 SQL parity)
417
+ * @param values - Disallowed string values array
167
418
  * @example
168
- * .checkNot('deleted', 'archived')
419
+ * .checkNot('users_status_check', ['deleted', 'archived'])
169
420
  * // Column cannot have these values
170
421
  */
171
- checkNot<V extends string>(...values: V[]): ColumnBuilder<T, Config & {
422
+ checkNot<const V extends readonly string[]>(name: string, values: V): ColumnBuilder<T, Config & {
172
423
  $checkNot: string;
173
- $checkNotValues: readonly V[];
424
+ $checkNotName: string;
425
+ $checkNotValues: V;
174
426
  }>;
175
427
  /** @deprecated Use generatedAlwaysAs with callback instead */
176
428
  generatedAs(expression: string, stored?: boolean): ColumnBuilder<T, Config & {
@@ -199,6 +451,14 @@ export type ColumnBuilder<T, Config extends ColumnConfig<T> = ColumnConfig<T>> =
199
451
  * (table, F) => F.concat(table.firstName, ' ', table.lastName)
200
452
  * )
201
453
  */
454
+ generatedAlwaysAs(callback: (F: GeneratedFn) => GeneratedExpr, options?: {
455
+ stored?: boolean;
456
+ }): ColumnBuilder<T, Config & {
457
+ $generated: {
458
+ expression: string;
459
+ stored?: boolean;
460
+ };
461
+ }>;
202
462
  generatedAlwaysAs<Cols extends Record<string, ColumnConfig>>(callback: (table: GeneratedTableRefs<Cols>, F: GeneratedFn) => GeneratedExpr, options?: {
203
463
  stored?: boolean;
204
464
  }): ColumnBuilder<T, Config & {
@@ -245,7 +505,30 @@ export type ColumnBuilder<T, Config extends ColumnConfig<T> = ColumnConfig<T>> =
245
505
  dimensions(d: number): ColumnBuilder<T, Config & {
246
506
  $dimensions: number;
247
507
  }>;
508
+ /**
509
+ * Add a comment/description to the column.
510
+ * This will be stored in the database schema and used for documentation.
511
+ * @param text - The comment text
512
+ * @example
513
+ * email: varchar(255).notNull().comment('User primary email address')
514
+ */
515
+ comment(text: string): ColumnBuilder<T, Config & {
516
+ $comment: string;
517
+ }>;
518
+ /**
519
+ * Set tracking ID for rename detection.
520
+ * This ID persists across column renames and allows detecting RENAME vs DROP+ADD.
521
+ * Auto-generated during pull/import if not present.
522
+ * @param trackingId - Unique tracking identifier (e.g., 'col_a1b2c3')
523
+ * @example
524
+ * userId: varchar('user_id').notNull().$id('col_abc123')
525
+ */
526
+ $id(trackingId: string): ColumnBuilder<T, Config & {
527
+ $trackingId: string;
528
+ }>;
248
529
  };
530
+ /** Create a fluent chainable expression for generated columns */
531
+ export declare function createFluentGenExpr(sql: string): FluentGenExpr;
249
532
  export declare const integer: (columnName?: string) => ColumnBuilder<number, ColumnConfig<number>>;
250
533
  export declare const int: (columnName?: string) => ColumnBuilder<number, ColumnConfig<number>>;
251
534
  export declare const int4: (columnName?: string) => ColumnBuilder<number, ColumnConfig<number>>;
@@ -764,14 +1047,17 @@ export declare function generateCompositeTypeSQL<T extends Record<string, Column
764
1047
  * data: customType('my_type').notNull().array()
765
1048
  */
766
1049
  export declare const customType: <T = string>(typeName: string, columnName?: string) => ColumnBuilder<T, ColumnConfig<T>>;
767
- export declare const enumType: <T extends string>(name: string, values: readonly T[], columnName?: string) => ColumnConfig<T> & {
1050
+ export declare const enumType: <T extends string>(name: string | {
1051
+ name: string;
1052
+ values: readonly T[];
1053
+ }, values?: readonly T[], columnName?: string) => ColumnConfig<T> & {
768
1054
  notNull(): ColumnBuilder<T, ColumnConfig<T> & {
769
1055
  $nullable: false;
770
1056
  }>;
771
1057
  nullable(): ColumnBuilder<T, ColumnConfig<T> & {
772
1058
  $nullable: true;
773
1059
  }>;
774
- default<V extends T | (() => T)>(value: V): ColumnBuilder<T, ColumnConfig<T> & {
1060
+ default<V extends DefaultValue | T | (() => T)>(value: V): ColumnBuilder<T, ColumnConfig<T> & {
775
1061
  $default: V;
776
1062
  }>;
777
1063
  primaryKey(): ColumnBuilder<T, ColumnConfig<T> & {
@@ -785,28 +1071,31 @@ export declare const enumType: <T extends string>(name: string, values: readonly
785
1071
  onUpdate?: string;
786
1072
  }): ColumnBuilder<T, ColumnConfig<T>>;
787
1073
  /**
788
- * Add CHECK constraint with enum-like values.
789
- * Narrows the column type to the union of provided values.
790
- * @param values - Allowed string values
1074
+ * Add CHECK constraint with enum-like values and explicit constraint name.
1075
+ * Narrows the column type to the union of provided values for autocomplete.
1076
+ * @param name - Constraint name (preserved for 1:1 SQL parity)
1077
+ * @param values - Allowed string values array (literal types auto-inferred)
791
1078
  * @example
792
- * .check('active', 'inactive', 'banned')
1079
+ * .check('users_status_check', ['active', 'inactive', 'banned'])
793
1080
  * // Type becomes 'active' | 'inactive' | 'banned'
794
1081
  */
795
- check<V extends string>(...values: V[]): ColumnBuilder<V, ColumnConfig<V> & {
1082
+ check<const V extends readonly string[]>(name: string, values: V): ColumnBuilder<V[number], ColumnConfig<V[number]> & {
796
1083
  $check: string;
797
- $checkValues: readonly V[];
1084
+ $checkName: string;
1085
+ $checkValues: V;
798
1086
  }>;
799
1087
  /**
800
1088
  * Add CHECK constraint that excludes specific values.
801
- * Narrows the column type to exclude provided values.
802
- * @param values - Disallowed string values
1089
+ * @param name - Constraint name (preserved for 1:1 SQL parity)
1090
+ * @param values - Disallowed string values array
803
1091
  * @example
804
- * .checkNot('deleted', 'archived')
1092
+ * .checkNot('users_status_check', ['deleted', 'archived'])
805
1093
  * // Column cannot have these values
806
1094
  */
807
- checkNot<V extends string>(...values: V[]): ColumnBuilder<T, ColumnConfig<T> & {
1095
+ checkNot<const V extends readonly string[]>(name: string, values: V): ColumnBuilder<T, ColumnConfig<T> & {
808
1096
  $checkNot: string;
809
- $checkNotValues: readonly V[];
1097
+ $checkNotName: string;
1098
+ $checkNotValues: V;
810
1099
  }>;
811
1100
  /** @deprecated Use generatedAlwaysAs with callback instead */
812
1101
  generatedAs(expression: string, stored?: boolean): ColumnBuilder<T, ColumnConfig<T> & {
@@ -835,6 +1124,14 @@ export declare const enumType: <T extends string>(name: string, values: readonly
835
1124
  * (table, F) => F.concat(table.firstName, ' ', table.lastName)
836
1125
  * )
837
1126
  */
1127
+ generatedAlwaysAs(callback: (F: GeneratedFn) => GeneratedExpr, options?: {
1128
+ stored?: boolean;
1129
+ }): ColumnBuilder<T, ColumnConfig<T> & {
1130
+ $generated: {
1131
+ expression: string;
1132
+ stored?: boolean;
1133
+ };
1134
+ }>;
838
1135
  generatedAlwaysAs<Cols extends Record<string, ColumnConfig>>(callback: (table: GeneratedTableRefs<Cols>, F: GeneratedFn) => GeneratedExpr, options?: {
839
1136
  stored?: boolean;
840
1137
  }): ColumnBuilder<T, ColumnConfig<T> & {
@@ -881,9 +1178,43 @@ export declare const enumType: <T extends string>(name: string, values: readonly
881
1178
  dimensions(d: number): ColumnBuilder<T, ColumnConfig<T> & {
882
1179
  $dimensions: number;
883
1180
  }>;
1181
+ /**
1182
+ * Add a comment/description to the column.
1183
+ * This will be stored in the database schema and used for documentation.
1184
+ * @param text - The comment text
1185
+ * @example
1186
+ * email: varchar(255).notNull().comment('User primary email address')
1187
+ */
1188
+ comment(text: string): ColumnBuilder<T, ColumnConfig<T> & {
1189
+ $comment: string;
1190
+ }>;
1191
+ /**
1192
+ * Set tracking ID for rename detection.
1193
+ * This ID persists across column renames and allows detecting RENAME vs DROP+ADD.
1194
+ * Auto-generated during pull/import if not present.
1195
+ * @param trackingId - Unique tracking identifier (e.g., 'col_a1b2c3')
1196
+ * @example
1197
+ * userId: varchar('user_id').notNull().$id('col_abc123')
1198
+ */
1199
+ $id(trackingId: string): ColumnBuilder<T, ColumnConfig<T> & {
1200
+ $trackingId: string;
1201
+ }>;
884
1202
  } & {
885
1203
  $enumValues: readonly T[];
886
1204
  };
1205
+ /**
1206
+ * Create a column that references a domain type
1207
+ * @param domainDef - The domain definition from pgDomain() or just the domain name
1208
+ * @param columnName - Optional explicit column name
1209
+ * @example
1210
+ * // With domain definition
1211
+ * email: domainType(emailDomain)
1212
+ * // With domain name
1213
+ * email: domainType('email_domain')
1214
+ */
1215
+ export declare const domainType: <T = string>(domainDef: string | {
1216
+ $domainName: string;
1217
+ }, columnName?: string) => ColumnBuilder<T, ColumnConfig<T>>;
887
1218
  /** @deprecated Use pgComposite() instead for better type inference */
888
1219
  export declare const compositeType: <T>(typeName: string, columnName?: string) => ColumnBuilder<T, ColumnConfig<T>>;
889
1220
  /** SQL expression brand - marks a value as SQL expression for runtime detection */
@@ -1257,6 +1588,49 @@ export interface IndexDefinition {
1257
1588
  include?: string[];
1258
1589
  /** Expression for expression-based indexes */
1259
1590
  expression?: string;
1591
+ /**
1592
+ * Generate CREATE INDEX IF NOT EXISTS instead of CREATE INDEX.
1593
+ *
1594
+ * When true, the generated SQL will be:
1595
+ * `CREATE INDEX IF NOT EXISTS index_name ON table_name (...)`
1596
+ *
1597
+ * This makes index creation idempotent - if the index already exists,
1598
+ * PostgreSQL will skip creation instead of throwing an error.
1599
+ *
1600
+ * Use cases:
1601
+ * - Idempotent migrations that can be run multiple times safely
1602
+ * - Manual schema management where you want to avoid errors on re-runs
1603
+ * - Incremental schema updates in development environments
1604
+ * - CI/CD pipelines where schema might already exist
1605
+ *
1606
+ * @default false
1607
+ */
1608
+ ifNotExists?: boolean;
1609
+ /**
1610
+ * Use ON ONLY clause for partitioned tables.
1611
+ *
1612
+ * When true, the generated SQL will be:
1613
+ * `CREATE INDEX index_name ON ONLY table_name (...)`
1614
+ *
1615
+ * This creates an index on the parent partitioned table only, without
1616
+ * automatically creating matching indexes on child partitions. Each
1617
+ * partition must have its own index created separately.
1618
+ *
1619
+ * Use cases:
1620
+ * - When you want different index configurations per partition
1621
+ * - When partitions have different access patterns
1622
+ * - When you want to control index creation timing per partition
1623
+ * - For declarative partitioning with custom index strategies
1624
+ * - When some partitions don't need certain indexes (e.g., archive partitions)
1625
+ *
1626
+ * @default false
1627
+ * @see https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-MAINTENANCE
1628
+ */
1629
+ tableOnly?: boolean;
1630
+ /** Index comment/description */
1631
+ comment?: string;
1632
+ /** Tracking ID for rename detection */
1633
+ trackingId?: string;
1260
1634
  }
1261
1635
  export interface IndexBuilder {
1262
1636
  name: string;
@@ -1278,6 +1652,8 @@ export interface IndexBuilder {
1278
1652
  on<T>(callback: (F: SqlFunctions) => SqlExpr): IndexBuilder;
1279
1653
  /** Expression-based index columns */
1280
1654
  on(...columns: string[]): IndexBuilder;
1655
+ /** Add a comment/description to the index */
1656
+ comment(text: string): IndexBuilder;
1281
1657
  }
1282
1658
  /** Where builder for index conditions */
1283
1659
  export interface IndexWhereBuilder<T extends Record<string, unknown>> {
@@ -1348,6 +1724,11 @@ export interface CheckExpr<T = unknown> extends SqlExpr {
1348
1724
  * @example table.role.neq('banned') // role <> 'banned'
1349
1725
  */
1350
1726
  neq(value: T | CheckExpr<T>): WhereCondition;
1727
+ /**
1728
+ * Not equals (alias for neq) - SQL: `column <> value`
1729
+ * @example table.role.ne('banned') // role <> 'banned'
1730
+ */
1731
+ ne(value: T | CheckExpr<T>): WhereCondition;
1351
1732
  /**
1352
1733
  * Is null - SQL: `column IS NULL`
1353
1734
  * @example table.deletedAt.isNull() // deleted_at IS NULL
@@ -1440,25 +1821,136 @@ export interface CheckConstraintBuilder<T extends Record<string, unknown>> {
1440
1821
  col(name: keyof T): CheckExpr;
1441
1822
  /** Create a named check constraint */
1442
1823
  constraint(name: string, condition: WhereCondition): CheckConstraintDef;
1824
+ /** Create a raw SQL expression condition */
1825
+ raw(expression: string): WhereCondition;
1443
1826
  }
1444
- /** Column expression for use in where conditions */
1827
+ /**
1828
+ * Column expression for use in index WHERE clauses and check constraints.
1829
+ *
1830
+ * Provides a fluent, chainable API for building type-safe SQL conditions.
1831
+ * All comparison methods return a {@link WhereCondition} which can be further
1832
+ * chained with `.and()` and `.or()` for compound conditions.
1833
+ *
1834
+ * ## Basic Usage
1835
+ *
1836
+ * In index definitions, access columns via the table parameter:
1837
+ * ```typescript
1838
+ * defineTable('users', { ... }, {
1839
+ * indexes: [{
1840
+ * columns: ['email'],
1841
+ * where: table => table.isDeleted.eq(false)
1842
+ * }]
1843
+ * })
1844
+ * ```
1845
+ *
1846
+ * ## Comparison Operators
1847
+ *
1848
+ * | Method | SQL | Example |
1849
+ * |--------|-----|---------|
1850
+ * | `.eq(value)` | `=` | `table.status.eq('active')` → `"status" = 'active'` |
1851
+ * | `.neq(value)` | `!=` | `table.status.neq('deleted')` → `"status" != 'deleted'` |
1852
+ * | `.ne(value)` | `!=` | Alias for `.neq()` |
1853
+ * | `.gt(value)` | `>` | `table.age.gt(18)` → `"age" > 18` |
1854
+ * | `.gte(value)` | `>=` | `table.age.gte(21)` → `"age" >= 21` |
1855
+ * | `.lt(value)` | `<` | `table.price.lt(100)` → `"price" < 100` |
1856
+ * | `.lte(value)` | `<=` | `table.price.lte(50)` → `"price" <= 50` |
1857
+ *
1858
+ * ## Null Checks
1859
+ *
1860
+ * | Method | SQL | Example |
1861
+ * |--------|-----|---------|
1862
+ * | `.isNull()` | `IS NULL` | `table.deletedAt.isNull()` → `"deleted_at" IS NULL` |
1863
+ * | `.isNotNull()` | `IS NOT NULL` | `table.email.isNotNull()` → `"email" IS NOT NULL` |
1864
+ *
1865
+ * ## Collection Operators
1866
+ *
1867
+ * | Method | SQL | Example |
1868
+ * |--------|-----|---------|
1869
+ * | `.in([...])` | `IN (...)` | `table.status.in(['a', 'b'])` → `"status" IN ('a', 'b')` |
1870
+ * | `.notIn([...])` | `NOT IN (...)` | `table.type.notIn([1, 2])` → `"type" NOT IN (1, 2)` |
1871
+ * | `.between(min, max)` | `BETWEEN ... AND ...` | `table.age.between(18, 65)` → `"age" BETWEEN 18 AND 65` |
1872
+ *
1873
+ * ## Pattern Matching
1874
+ *
1875
+ * | Method | SQL | Example |
1876
+ * |--------|-----|---------|
1877
+ * | `.like(pattern)` | `LIKE` | `table.name.like('John%')` → `"name" LIKE 'John%'` |
1878
+ * | `.ilike(pattern)` | `ILIKE` | `table.email.ilike('%@gmail%')` → `"email" ILIKE '%@gmail%'` |
1879
+ *
1880
+ * ## Compound Conditions
1881
+ *
1882
+ * Chain conditions using `.and()` and `.or()`:
1883
+ *
1884
+ * ```typescript
1885
+ * // Simple AND: status = 'active' AND is_deleted = false
1886
+ * table.status.eq('active').and(table.isDeleted.eq(false))
1887
+ *
1888
+ * // Simple OR: role = 'admin' OR role = 'moderator'
1889
+ * table.role.eq('admin').or(table.role.eq('moderator'))
1890
+ *
1891
+ * // Complex: (a = 3 AND b = 7) OR (a = 9 AND b = 1)
1892
+ * table.a.eq(3).and(table.b.eq(7))
1893
+ * .or(table.a.eq(9).and(table.b.eq(1)))
1894
+ * ```
1895
+ *
1896
+ * Each `.and()` and `.or()` wraps operands in parentheses for correct precedence.
1897
+ *
1898
+ * ## Arithmetic (for computed expressions)
1899
+ *
1900
+ * | Method | SQL | Example |
1901
+ * |--------|-----|---------|
1902
+ * | `.plus(n)` | `+` | `table.price.plus(tax)` → `("price" + "tax")` |
1903
+ * | `.minus(n)` | `-` | `table.total.minus(5)` → `("total" - 5)` |
1904
+ *
1905
+ * @example Partial index on active, non-deleted users
1906
+ * ```typescript
1907
+ * indexes: [{
1908
+ * columns: ['email'],
1909
+ * unique: true,
1910
+ * where: table => table.status.eq('active').and(table.isDeleted.eq(false))
1911
+ * }]
1912
+ * ```
1913
+ *
1914
+ * @example Partial index with null check
1915
+ * ```typescript
1916
+ * indexes: [{
1917
+ * columns: ['verified_at'],
1918
+ * where: table => table.verifiedAt.isNotNull()
1919
+ * }]
1920
+ * ```
1921
+ */
1445
1922
  export interface ColumnExpr extends SqlExpr {
1923
+ /** Equals: `column = value` */
1446
1924
  eq(value: unknown): WhereCondition;
1925
+ /** Not equals (alternative syntax): `column != value` */
1447
1926
  neq(value: unknown): WhereCondition;
1927
+ /** Not equals (SQL standard): `column <> value` */
1928
+ ne(value: unknown): WhereCondition;
1929
+ /** Greater than: `column > value` */
1448
1930
  gt(value: unknown): WhereCondition;
1931
+ /** Greater than or equal: `column >= value` */
1449
1932
  gte(value: unknown): WhereCondition;
1933
+ /** Less than: `column < value` */
1450
1934
  lt(value: unknown): WhereCondition;
1935
+ /** Less than or equal: `column <= value` */
1451
1936
  lte(value: unknown): WhereCondition;
1937
+ /** Is null: `column IS NULL` */
1452
1938
  isNull(): WhereCondition;
1939
+ /** Is not null: `column IS NOT NULL` */
1453
1940
  isNotNull(): WhereCondition;
1941
+ /** In list: `column IN (value1, value2, ...)` */
1454
1942
  in(values: unknown[]): WhereCondition;
1943
+ /** Not in list: `column NOT IN (value1, value2, ...)` */
1455
1944
  notIn(values: unknown[]): WhereCondition;
1945
+ /** Between range: `column BETWEEN min AND max` */
1456
1946
  between(min: unknown, max: unknown): WhereCondition;
1947
+ /** Like pattern (case-sensitive): `column LIKE pattern` */
1457
1948
  like(pattern: string): WhereCondition;
1949
+ /** Like pattern (case-insensitive): `column ILIKE pattern` */
1458
1950
  ilike(pattern: string): WhereCondition;
1459
- /** Add interval to column: col.plus(interval('30 days')) */
1951
+ /** Add value: `(column + value)` - returns ColumnExpr for chaining */
1460
1952
  plus(value: SqlExpr | number): ColumnExpr;
1461
- /** Subtract interval from column: col.minus(interval('1 hour')) */
1953
+ /** Subtract value: `(column - value)` - returns ColumnExpr for chaining */
1462
1954
  minus(value: SqlExpr | number): ColumnExpr;
1463
1955
  }
1464
1956
  export type IndexInput = IndexDefinition | IndexBuilder | IndexBuilderChain | {
@@ -1503,6 +1995,140 @@ export interface IndexBuilderChain {
1503
1995
  desc(): IndexBuilderChain;
1504
1996
  include(...columns: string[]): IndexBuilderChain;
1505
1997
  opclass(opclass: string): IndexBuilderChain;
1998
+ /**
1999
+ * Generate CREATE INDEX IF NOT EXISTS instead of CREATE INDEX.
2000
+ *
2001
+ * This makes the index creation idempotent - if the index already exists,
2002
+ * PostgreSQL will skip creation instead of throwing an error.
2003
+ *
2004
+ * @example
2005
+ * ```typescript
2006
+ * indexes: (table, index) => [
2007
+ * // Idempotent index - safe to run multiple times
2008
+ * index('idx_users_email')
2009
+ * .on(table.email)
2010
+ * .ifNotExists(),
2011
+ * ]
2012
+ * ```
2013
+ *
2014
+ * Generated SQL:
2015
+ * ```sql
2016
+ * CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
2017
+ * ```
2018
+ *
2019
+ * **Use cases:**
2020
+ * - **Idempotent migrations** - Migrations that can be run multiple times
2021
+ * without causing errors, useful for CI/CD pipelines
2022
+ * - **Manual schema management** - When you want to manually run schema
2023
+ * scripts without tracking which statements have already been executed
2024
+ * - **Development environments** - When databases may already have some
2025
+ * indexes and you want to apply incremental changes
2026
+ * - **Multi-tenant systems** - When creating tenant-specific indexes that
2027
+ * may or may not exist
2028
+ *
2029
+ * **Important Notes:**
2030
+ * - When the index already exists, PostgreSQL will NOT verify that the
2031
+ * existing index matches your definition (columns, type, options)
2032
+ * - If you need to modify an existing index, you must DROP and recreate it
2033
+ * - For schema validation, use `relq diff` to compare definitions
2034
+ *
2035
+ * @returns The index builder chain for further method chaining
2036
+ */
2037
+ ifNotExists(): IndexBuilderChain;
2038
+ /**
2039
+ * Use ON ONLY clause to create index on parent partitioned table only.
2040
+ *
2041
+ * When called, generates: `CREATE INDEX name ON ONLY table_name (...)`
2042
+ *
2043
+ * By default, when you create an index on a partitioned table, PostgreSQL
2044
+ * automatically creates matching indexes on all child partitions. Using
2045
+ * `tableOnly()` prevents this automatic propagation.
2046
+ *
2047
+ * @example
2048
+ * ```typescript
2049
+ * indexes: (table, index) => [
2050
+ * // Index only on parent table, not on partitions
2051
+ * index('idx_orders_date')
2052
+ * .on(table.orderDate)
2053
+ * .tableOnly(),
2054
+ * ]
2055
+ * ```
2056
+ *
2057
+ * Generated SQL:
2058
+ * ```sql
2059
+ * CREATE INDEX idx_orders_date ON ONLY orders (order_date);
2060
+ * ```
2061
+ *
2062
+ * Use cases:
2063
+ * - When you want different index configurations per partition
2064
+ * - When partitions have vastly different access patterns
2065
+ * - When you want to control index creation timing per partition
2066
+ * - For declarative partitioning with custom index strategies
2067
+ * - When some partitions don't need certain indexes (e.g., archive partitions)
2068
+ *
2069
+ * Without `tableOnly()`, PostgreSQL would:
2070
+ * 1. Create the index on the parent table
2071
+ * 2. Automatically create matching indexes on ALL child partitions
2072
+ *
2073
+ * With `tableOnly()`, you must manually create indexes on each partition:
2074
+ * ```sql
2075
+ * CREATE INDEX idx_orders_2024_date ON orders_2024 (order_date);
2076
+ * CREATE INDEX idx_orders_2025_date ON orders_2025 (order_date);
2077
+ * ```
2078
+ *
2079
+ * @returns The index builder chain for further method chaining
2080
+ * @see https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-MAINTENANCE
2081
+ */
2082
+ tableOnly(): IndexBuilderChain;
2083
+ /** Add a comment/description to the index */
2084
+ comment(text: string): IndexBuilderChain;
2085
+ /**
2086
+ * Set tracking ID for rename detection.
2087
+ * This ID persists across index renames and allows detecting RENAME vs DROP+ADD.
2088
+ * @param trackingId - Unique tracking identifier (e.g., 'idx_a1b2c3')
2089
+ */
2090
+ $id(trackingId: string): IndexBuilderChain;
2091
+ }
2092
+ /** Options for constraints (name is optional - auto-generated if not provided) */
2093
+ export interface ConstraintOptions {
2094
+ name?: string;
2095
+ columns: ColumnRef$1[];
2096
+ }
2097
+ /** Constraint builder for table-level constraints (composite PKs, etc.) */
2098
+ export interface ConstraintBuilder<T extends Record<string, ColumnConfig>> {
2099
+ /**
2100
+ * Define composite primary key constraint
2101
+ * @example
2102
+ * // Without name (auto-generates name):
2103
+ * constraint.primaryKey(table.id, table.userId)
2104
+ *
2105
+ * // With name:
2106
+ * constraint.primaryKey({ name: 'pk_results', columns: [table.id, table.userId] })
2107
+ */
2108
+ primaryKey(...columns: ColumnRef$1[]): ConstraintDef;
2109
+ primaryKey(options: ConstraintOptions): ConstraintDef;
2110
+ /**
2111
+ * Define unique constraint across multiple columns
2112
+ * @example
2113
+ * // Without name (auto-generates name):
2114
+ * constraint.unique(table.email, table.tenantId)
2115
+ *
2116
+ * // With name:
2117
+ * constraint.unique({ name: 'uq_email_tenant', columns: [table.email, table.tenantId] })
2118
+ */
2119
+ unique(...columns: ColumnRef$1[]): ConstraintDef;
2120
+ unique(options: ConstraintOptions): ConstraintDef;
2121
+ /** Define exclusion constraint with raw SQL expression */
2122
+ exclude(name: string, expression: string): ConstraintDef;
2123
+ }
2124
+ /** Constraint definition result */
2125
+ export interface ConstraintDef {
2126
+ readonly $type: "PRIMARY KEY" | "UNIQUE" | "EXCLUDE";
2127
+ readonly $name: string;
2128
+ readonly $columns: string[];
2129
+ readonly $expression?: string;
2130
+ /** Tracking ID for rename detection */
2131
+ readonly $trackingId?: string;
1506
2132
  }
1507
2133
  export interface TableConfig<T extends Record<string, ColumnConfig>> {
1508
2134
  columns: T;
@@ -1512,6 +2138,15 @@ export interface TableConfig<T extends Record<string, ColumnConfig>> {
1512
2138
  columns: string[];
1513
2139
  name?: string;
1514
2140
  }>;
2141
+ /**
2142
+ * Table-level constraints with typed callback API (composite primary keys, etc.)
2143
+ * @example
2144
+ * constraints: (table, constraint) => [
2145
+ * constraint.primaryKey(table.id, table.userId),
2146
+ * constraint.unique({ name: 'uq_email_date', columns: [table.email, table.date] }),
2147
+ * ]
2148
+ */
2149
+ constraints?: ((table: ColumnRefs<T>, constraint: ConstraintBuilder<T>) => ConstraintDef[]);
1515
2150
  /**
1516
2151
  * CHECK constraints with typed callback API
1517
2152
  * @example
@@ -1531,9 +2166,11 @@ export interface TableConfig<T extends Record<string, ColumnConfig>> {
1531
2166
  onDelete?: "CASCADE" | "SET NULL" | "SET DEFAULT" | "RESTRICT" | "NO ACTION";
1532
2167
  onUpdate?: "CASCADE" | "SET NULL" | "SET DEFAULT" | "RESTRICT" | "NO ACTION";
1533
2168
  name?: string;
2169
+ /** Tracking ID for rename detection */
2170
+ trackingId?: string;
1534
2171
  }>;
1535
- /** Index definitions - use (table, index, F) => [index('name').on(F.lower(table.email))] */
1536
- indexes?: IndexInput[] | ((table: ColumnRefs<T>, index: IndexFactory, F: SqlFunctions) => IndexInput[]);
2172
+ /** Index definitions - use (table, index, F) => [index('name').on(table.email).where(table.isActive.eq(true))] */
2173
+ indexes?: IndexInput[] | ((table: IndexTableRefs<T>, index: IndexFactory, F: SqlFunctions) => IndexInput[]);
1537
2174
  inherits?: string[];
1538
2175
  /** Partition strategy - use (table, p) => p.list(table.col) / p.range(table.col) / p.hash(table.col, modulus) */
1539
2176
  partitionBy?: (table: ColumnRefs<T>, p: PartitionStrategyFactory) => PartitionStrategyDef;
@@ -1541,6 +2178,68 @@ export interface TableConfig<T extends Record<string, ColumnConfig>> {
1541
2178
  partitions?: (partition: PartitionFactory) => ChildPartitionDef[];
1542
2179
  tablespace?: string;
1543
2180
  withOptions?: Record<string, unknown>;
2181
+ /**
2182
+ * Generate CREATE TABLE IF NOT EXISTS instead of CREATE TABLE.
2183
+ *
2184
+ * When `ifNotExists: true`, the generated SQL will be:
2185
+ * `CREATE TABLE IF NOT EXISTS table_name (...)`
2186
+ *
2187
+ * This makes table creation idempotent - if the table already exists,
2188
+ * PostgreSQL will skip creation instead of throwing an error.
2189
+ *
2190
+ * **Use Cases:**
2191
+ * - **Idempotent migrations** - Migrations that can be run multiple times
2192
+ * without causing errors, useful for CI/CD pipelines
2193
+ * - **Manual schema management** - When you want to manually run schema
2194
+ * scripts without tracking which statements have already been executed
2195
+ * - **Development environments** - When databases may already have some
2196
+ * tables and you want to apply incremental changes
2197
+ * - **Multi-tenant systems** - When creating tenant-specific tables that
2198
+ * may or may not exist
2199
+ * - **Bootstrap scripts** - Initial setup scripts that might run on
2200
+ * existing databases
2201
+ *
2202
+ * **Example:**
2203
+ * ```typescript
2204
+ * export const users = defineTable('users', {
2205
+ * id: uuid().primaryKey().default('gen_random_uuid()'),
2206
+ * email: varchar(255).notNull(),
2207
+ * }, {
2208
+ * ifNotExists: true, // Safe to run multiple times
2209
+ * });
2210
+ * ```
2211
+ *
2212
+ * **Generated SQL:**
2213
+ * ```sql
2214
+ * CREATE TABLE IF NOT EXISTS users (
2215
+ * id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
2216
+ * email VARCHAR(255) NOT NULL
2217
+ * );
2218
+ * ```
2219
+ *
2220
+ * **Important Notes:**
2221
+ * - When `ifNotExists: true` and the table already exists, PostgreSQL
2222
+ * will NOT verify that the existing table matches your schema
2223
+ * - Column differences, missing columns, or type mismatches are NOT detected
2224
+ * - For schema validation, use proper migration tools or `relq diff`
2225
+ * - The existing table's structure may differ from your definition
2226
+ *
2227
+ * @default false - Standard CREATE TABLE that throws if table exists
2228
+ */
2229
+ ifNotExists?: boolean;
2230
+ /**
2231
+ * Table comment/description.
2232
+ * This will be stored in the database schema and used for documentation.
2233
+ * @example
2234
+ * defineTable('users', { ... }, { comment: 'Stores user account information' })
2235
+ */
2236
+ comment?: string;
2237
+ /**
2238
+ * Tracking ID for rename detection.
2239
+ * Auto-generated during pull/import if not present.
2240
+ * @example 't1a2b3'
2241
+ */
2242
+ $trackingId?: string;
1544
2243
  }
1545
2244
  type ColumnRef$1 = string & {
1546
2245
  __columnRef: true;
@@ -1548,6 +2247,16 @@ type ColumnRef$1 = string & {
1548
2247
  export type ColumnRefs<T extends Record<string, ColumnConfig>> = {
1549
2248
  [K in keyof T]: ColumnRef$1;
1550
2249
  };
2250
+ /**
2251
+ * Table refs for index WHERE clauses - each column is a ColumnExpr with comparison methods
2252
+ *
2253
+ * Used in: `indexes: (table, index) => [index('name').on(table.col).where(table.isActive.eq(true))]`
2254
+ *
2255
+ * @template T - Record of column names to ColumnConfig types
2256
+ */
2257
+ export type IndexTableRefs<T extends Record<string, ColumnConfig>> = {
2258
+ [K in keyof T]: ColumnExpr;
2259
+ };
1551
2260
  /**
1552
2261
  * Table refs for check constraints - each column is a typed CheckExpr
1553
2262
  * The type is inferred from the column's ColumnConfig type parameter
@@ -1649,6 +2358,8 @@ export interface TableDefinition<T extends Record<string, ColumnConfig>> {
1649
2358
  expression: string;
1650
2359
  name?: string;
1651
2360
  }>;
2361
+ /** Table-level constraints (composite PKs, etc.) */
2362
+ $constraints?: ConstraintDef[];
1652
2363
  $foreignKeys?: Array<{
1653
2364
  columns: string[];
1654
2365
  references: {
@@ -1658,12 +2369,18 @@ export interface TableDefinition<T extends Record<string, ColumnConfig>> {
1658
2369
  onDelete?: string;
1659
2370
  onUpdate?: string;
1660
2371
  name?: string;
2372
+ /** Tracking ID for rename detection */
2373
+ trackingId?: string;
1661
2374
  }>;
1662
2375
  $indexes?: IndexDefinition[];
1663
2376
  $inherits?: string[];
1664
2377
  $partitionBy?: PartitionStrategyDef;
1665
2378
  $tablespace?: string;
1666
2379
  $withOptions?: Record<string, unknown>;
2380
+ /** Whether to use CREATE TABLE IF NOT EXISTS */
2381
+ $ifNotExists?: boolean;
2382
+ /** Tracking ID for rename detection */
2383
+ $trackingId?: string;
1667
2384
  $inferSelect: BuildSelectType<T>;
1668
2385
  $inferInsert: BuildInsertType<T>;
1669
2386
  toSQL(): string;
@@ -1770,6 +2487,523 @@ export declare function manyToMany<TFrom extends TableDefinition<Record<string,
1770
2487
  throughFromKey?: string;
1771
2488
  throughToKey?: string;
1772
2489
  }): ManyToManyRelation<TFrom, TTo, TThrough>;
2490
+ /**
2491
+ * PostgreSQL referential actions for ON DELETE and ON UPDATE clauses.
2492
+ *
2493
+ * | Action | Behavior |
2494
+ * |--------|----------|
2495
+ * | `'NO ACTION'` | Default. Raises error if referenced rows exist. Checked at statement end. |
2496
+ * | `'RESTRICT'` | Like NO ACTION but checked immediately (cannot be deferred). |
2497
+ * | `'CASCADE'` | Deletes/updates child rows when parent is deleted/updated. |
2498
+ * | `'SET NULL'` | Sets FK columns to NULL when parent is deleted/updated. |
2499
+ * | `'SET DEFAULT'` | Sets FK columns to their default values when parent changes. |
2500
+ *
2501
+ * @example
2502
+ * ```typescript
2503
+ * // When an order is deleted, delete all order items
2504
+ * orderId: r.referenceTo.orders(t => ({ onDelete: 'CASCADE' }))
2505
+ *
2506
+ * // When a user is deleted, orphan their comments (set user_id to NULL)
2507
+ * userId: r.referenceTo.users(t => ({ onDelete: 'SET NULL' }))
2508
+ *
2509
+ * // Prevent deleting a category that has products
2510
+ * categoryId: r.referenceTo.categories(t => ({ onDelete: 'RESTRICT' }))
2511
+ * ```
2512
+ */
2513
+ export type ReferentialAction = "NO ACTION" | "RESTRICT" | "CASCADE" | "SET NULL" | "SET DEFAULT";
2514
+ /**
2515
+ * PostgreSQL MATCH types for foreign key constraints.
2516
+ *
2517
+ * | Match Type | Behavior |
2518
+ * |------------|----------|
2519
+ * | `'SIMPLE'` | Default. Any NULL in FK columns satisfies constraint. |
2520
+ * | `'FULL'` | All FK columns must be NULL together, or all must be non-NULL. |
2521
+ *
2522
+ * Note: MATCH PARTIAL is defined in SQL standard but not implemented in PostgreSQL.
2523
+ *
2524
+ * @example
2525
+ * ```typescript
2526
+ * // Composite FK where all columns must match or all be NULL
2527
+ * productWarehouse: r.referenceTo.inventory(t => ({
2528
+ * columns: [r.orderItems.productId, r.orderItems.warehouseId],
2529
+ * references: [t.productId, t.warehouseId],
2530
+ * match: 'FULL',
2531
+ * }))
2532
+ * ```
2533
+ */
2534
+ export type MatchType = "SIMPLE" | "FULL";
2535
+ /**
2536
+ * Column reference with table and column metadata for relations.
2537
+ * Used internally by the builder to track column references.
2538
+ */
2539
+ export interface RelationColumnRef<TTableName extends string = string, TColumnName extends string = string> {
2540
+ /** SQL table name */
2541
+ $table: TTableName;
2542
+ /** SQL column name */
2543
+ $column: TColumnName;
2544
+ /** Schema key (TypeScript property name) */
2545
+ $schemaKey?: string;
2546
+ }
2547
+ /**
2548
+ * Options for a single-column foreign key constraint.
2549
+ *
2550
+ * Maps to PostgreSQL syntax:
2551
+ * ```sql
2552
+ * column_name type REFERENCES target_table(target_column)
2553
+ * [ON DELETE action]
2554
+ * [ON UPDATE action]
2555
+ * [MATCH FULL | MATCH SIMPLE]
2556
+ * [DEFERRABLE | NOT DEFERRABLE]
2557
+ * [INITIALLY DEFERRED | INITIALLY IMMEDIATE]
2558
+ * ```
2559
+ *
2560
+ * @example
2561
+ * ```typescript
2562
+ * // Simple FK to primary key (references defaults to PK)
2563
+ * userId: r.referenceTo.users(t => ({
2564
+ * onDelete: 'CASCADE',
2565
+ * }))
2566
+ *
2567
+ * // FK to non-PK unique column
2568
+ * walletAddress: r.referenceTo.wallets(t => ({
2569
+ * references: t.address, // References wallets.address, not wallets.id
2570
+ * onDelete: 'SET NULL',
2571
+ * }))
2572
+ *
2573
+ * // Full options
2574
+ * orderId: r.referenceTo.orders(t => ({
2575
+ * references: t.id,
2576
+ * onDelete: 'CASCADE',
2577
+ * onUpdate: 'NO ACTION',
2578
+ * match: 'SIMPLE',
2579
+ * deferrable: true,
2580
+ * initiallyDeferred: true,
2581
+ * }))
2582
+ * ```
2583
+ */
2584
+ export interface ForeignKeyOptions<TTargetColumns extends Record<string, RelationColumnRef>> {
2585
+ /**
2586
+ * Target column on the referenced table.
2587
+ *
2588
+ * - If omitted: References the PRIMARY KEY of the target table
2589
+ * - If specified: Must be a PRIMARY KEY or UNIQUE column
2590
+ *
2591
+ * @example
2592
+ * ```typescript
2593
+ * // References users.id (primary key, default)
2594
+ * userId: r.referenceTo.users(t => ({ onDelete: 'CASCADE' }))
2595
+ *
2596
+ * // References users.email (must be UNIQUE)
2597
+ * userEmail: r.referenceTo.users(t => ({
2598
+ * references: t.email,
2599
+ * onDelete: 'SET NULL',
2600
+ * }))
2601
+ * ```
2602
+ */
2603
+ references?: RelationColumnRef;
2604
+ /**
2605
+ * Action when referenced row is deleted.
2606
+ *
2607
+ * | Action | Effect |
2608
+ * |--------|--------|
2609
+ * | `'NO ACTION'` | Raises error (default, checked at statement end) |
2610
+ * | `'RESTRICT'` | Raises error (checked immediately) |
2611
+ * | `'CASCADE'` | Deletes this row too |
2612
+ * | `'SET NULL'` | Sets this column to NULL |
2613
+ * | `'SET DEFAULT'` | Sets this column to its default value |
2614
+ *
2615
+ * @default 'NO ACTION'
2616
+ *
2617
+ * @example
2618
+ * ```typescript
2619
+ * // Delete order items when order is deleted
2620
+ * orderId: r.referenceTo.orders(t => ({ onDelete: 'CASCADE' }))
2621
+ *
2622
+ * // Keep comment but orphan it when user deleted
2623
+ * userId: r.referenceTo.users(t => ({ onDelete: 'SET NULL' }))
2624
+ * ```
2625
+ */
2626
+ onDelete?: ReferentialAction;
2627
+ /**
2628
+ * Action when referenced column value is updated.
2629
+ *
2630
+ * Rarely used since primary keys typically don't change.
2631
+ * Same actions as onDelete.
2632
+ *
2633
+ * @default 'NO ACTION'
2634
+ *
2635
+ * @example
2636
+ * ```typescript
2637
+ * // If order.id changes, update order_items.order_id too
2638
+ * orderId: r.referenceTo.orders(t => ({
2639
+ * onDelete: 'CASCADE',
2640
+ * onUpdate: 'CASCADE',
2641
+ * }))
2642
+ * ```
2643
+ */
2644
+ onUpdate?: ReferentialAction;
2645
+ /**
2646
+ * MATCH type for the foreign key constraint.
2647
+ *
2648
+ * - `'SIMPLE'` (default): Any NULL in FK columns satisfies constraint
2649
+ * - `'FULL'`: All FK columns must be NULL together, or all non-NULL
2650
+ *
2651
+ * Only relevant for composite (multi-column) foreign keys.
2652
+ *
2653
+ * @default 'SIMPLE'
2654
+ */
2655
+ match?: MatchType;
2656
+ /**
2657
+ * Whether the constraint can be deferred to transaction end.
2658
+ *
2659
+ * When `true`, you can use `SET CONSTRAINTS ... DEFERRED` to defer
2660
+ * constraint checking until transaction commit.
2661
+ *
2662
+ * Useful for circular references or bulk operations.
2663
+ *
2664
+ * @default false
2665
+ *
2666
+ * @example
2667
+ * ```typescript
2668
+ * // Allow deferring this constraint
2669
+ * parentId: r.referenceTo.categories(t => ({
2670
+ * onDelete: 'CASCADE',
2671
+ * deferrable: true,
2672
+ * }))
2673
+ *
2674
+ * // Then in a transaction:
2675
+ * // SET CONSTRAINTS category_parent_fk DEFERRED;
2676
+ * // ... insert circular references ...
2677
+ * // COMMIT; -- constraint checked here
2678
+ * ```
2679
+ */
2680
+ deferrable?: boolean;
2681
+ /**
2682
+ * Whether deferred constraints start in deferred mode.
2683
+ *
2684
+ * Only applies when `deferrable: true`.
2685
+ * - `false`: Constraint is checked after each statement (can be deferred manually)
2686
+ * - `true`: Constraint is checked at transaction commit by default
2687
+ *
2688
+ * @default false
2689
+ */
2690
+ initiallyDeferred?: boolean;
2691
+ /**
2692
+ * Tracking ID for rename detection.
2693
+ * Auto-generated during pull/import if not present.
2694
+ * @example 'fk_a1b2c3'
2695
+ */
2696
+ trackingId?: string;
2697
+ }
2698
+ /**
2699
+ * Options for a composite (multi-column) foreign key constraint.
2700
+ *
2701
+ * Maps to PostgreSQL table-level constraint:
2702
+ * ```sql
2703
+ * FOREIGN KEY (col1, col2, ...)
2704
+ * REFERENCES target_table(target_col1, target_col2, ...)
2705
+ * [ON DELETE action]
2706
+ * [ON UPDATE action]
2707
+ * ...
2708
+ * ```
2709
+ *
2710
+ * @example
2711
+ * ```typescript
2712
+ * // Composite FK: (product_id, warehouse_id) → inventory(product_id, warehouse_id)
2713
+ * productWarehouse: r.referenceTo.inventory(t => ({
2714
+ * columns: [r.orderItems.productId, r.orderItems.warehouseId],
2715
+ * references: [t.productId, t.warehouseId],
2716
+ * onDelete: 'CASCADE',
2717
+ * match: 'FULL', // All columns must match or all be NULL
2718
+ * }))
2719
+ * ```
2720
+ */
2721
+ export interface CompositeForeignKeyOptions<TTargetColumns extends Record<string, RelationColumnRef>> {
2722
+ /**
2723
+ * Source columns on THIS table (array for composite FK).
2724
+ * Column count must match `references` array.
2725
+ */
2726
+ columns: RelationColumnRef[];
2727
+ /**
2728
+ * Target columns on the referenced table (array for composite FK).
2729
+ * Must be a PRIMARY KEY or UNIQUE constraint together.
2730
+ */
2731
+ references: RelationColumnRef[];
2732
+ /** Action when referenced row is deleted */
2733
+ onDelete?: ReferentialAction;
2734
+ /** Action when referenced column values are updated */
2735
+ onUpdate?: ReferentialAction;
2736
+ /** MATCH type (FULL recommended for composite FKs) */
2737
+ match?: MatchType;
2738
+ /** Whether constraint can be deferred */
2739
+ deferrable?: boolean;
2740
+ /** Whether deferred constraints start deferred */
2741
+ initiallyDeferred?: boolean;
2742
+ /** Tracking ID for rename detection */
2743
+ trackingId?: string;
2744
+ }
2745
+ /**
2746
+ * Foreign key relation definition (runtime representation).
2747
+ * Stores all metadata about a REFERENCES constraint.
2748
+ */
2749
+ export interface ForeignKeyRelationDef {
2750
+ /** Relation type marker */
2751
+ $type: "foreignKey";
2752
+ /** Target table name (SQL name) */
2753
+ $targetTable: string;
2754
+ /** Source column(s) on this table */
2755
+ $columns: Array<{
2756
+ table: string;
2757
+ column: string;
2758
+ }>;
2759
+ /** Target column(s) on referenced table (undefined = PK) */
2760
+ $references?: Array<{
2761
+ table: string;
2762
+ column: string;
2763
+ }>;
2764
+ /** ON DELETE action */
2765
+ $onDelete?: ReferentialAction;
2766
+ /** ON UPDATE action */
2767
+ $onUpdate?: ReferentialAction;
2768
+ /** MATCH type */
2769
+ $match?: MatchType;
2770
+ /** DEFERRABLE flag */
2771
+ $deferrable?: boolean;
2772
+ /** INITIALLY DEFERRED flag */
2773
+ $initiallyDeferred?: boolean;
2774
+ /** Tracking ID for rename detection */
2775
+ $trackingId?: string;
2776
+ }
2777
+ /** Relation definition type */
2778
+ export type RelationDef = ForeignKeyRelationDef;
2779
+ /** Table's relations map */
2780
+ export type TableRelations = Record<string, RelationDef>;
2781
+ /** All relations for a schema */
2782
+ export type SchemaRelations = Record<string, TableRelations>;
2783
+ /**
2784
+ * Target table column accessor.
2785
+ * Provides autocomplete for target table's columns.
2786
+ */
2787
+ export type TargetTableColumns<TTable extends TableDefinition<any>> = {
2788
+ [K in keyof TTable["$columns"]]: RelationColumnRef<TTable["$name"], K & string>;
2789
+ };
2790
+ /**
2791
+ * Callback function type for referenceTo builder.
2792
+ * Receives target table columns and returns FK options.
2793
+ */
2794
+ export type ReferenceToCallback<TTargetTable extends TableDefinition<any>> = (t: TargetTableColumns<TTargetTable>) => ForeignKeyOptions<TargetTableColumns<TTargetTable>> | CompositeForeignKeyOptions<TargetTableColumns<TTargetTable>>;
2795
+ /**
2796
+ * The referenceTo builder - provides target table selection with autocomplete.
2797
+ *
2798
+ * @example
2799
+ * ```typescript
2800
+ * // r.referenceTo.users gives autocomplete for users table
2801
+ * userId: r.referenceTo.users(t => ({
2802
+ * references: t.id, // Autocomplete shows users columns
2803
+ * onDelete: 'CASCADE',
2804
+ * }))
2805
+ * ```
2806
+ */
2807
+ export type ReferenceToBuilder<TSchema extends Record<string, TableDefinition<any>>> = {
2808
+ [K in keyof TSchema]: (callback: ReferenceToCallback<TSchema[K]>) => ForeignKeyRelationDef;
2809
+ };
2810
+ /**
2811
+ * Source table column accessor.
2812
+ * Used for composite FK columns on the current table.
2813
+ */
2814
+ export type SourceTableColumns<TTable extends TableDefinition<any>> = {
2815
+ [K in keyof TTable["$columns"]]: RelationColumnRef<TTable["$name"], K & string>;
2816
+ };
2817
+ /**
2818
+ * All source table column refs for the schema.
2819
+ * Enables referencing columns on any table for composite FKs.
2820
+ *
2821
+ * @example
2822
+ * ```typescript
2823
+ * // Access columns from any table
2824
+ * columns: [r.orderItems.productId, r.orderItems.warehouseId]
2825
+ * ```
2826
+ */
2827
+ export type SchemaColumnRefs<TSchema extends Record<string, TableDefinition<any>>> = {
2828
+ [K in keyof TSchema]: SourceTableColumns<TSchema[K]>;
2829
+ };
2830
+ /**
2831
+ * Full relation builder passed to pgRelations callback.
2832
+ *
2833
+ * @example
2834
+ * ```typescript
2835
+ * pgRelations(schema, (r) => ({
2836
+ * // r.referenceTo.tableName - define FK to that table
2837
+ * // r.tableName.columnName - reference columns (for composite FKs)
2838
+ * }))
2839
+ * ```
2840
+ */
2841
+ export interface FullRelationBuilder<TSchema extends Record<string, TableDefinition<any>>> {
2842
+ /**
2843
+ * Define a foreign key reference to another table.
2844
+ *
2845
+ * @example
2846
+ * ```typescript
2847
+ * // Single-column FK
2848
+ * userId: r.referenceTo.users(t => ({
2849
+ * references: t.id,
2850
+ * onDelete: 'CASCADE',
2851
+ * }))
2852
+ *
2853
+ * // FK to non-PK column
2854
+ * email: r.referenceTo.users(t => ({
2855
+ * references: t.email, // Must be UNIQUE
2856
+ * onDelete: 'SET NULL',
2857
+ * }))
2858
+ * ```
2859
+ */
2860
+ referenceTo: ReferenceToBuilder<TSchema>;
2861
+ }
2862
+ /** Full builder with table column refs for composite FKs */
2863
+ export type FullRelationBuilderWithRefs<TSchema extends Record<string, TableDefinition<any>>> = FullRelationBuilder<TSchema> & SchemaColumnRefs<TSchema>;
2864
+ /**
2865
+ * Define PostgreSQL foreign key relations between tables.
2866
+ *
2867
+ * Creates a type-safe relations object that mirrors PostgreSQL REFERENCES constraints
2868
+ * exactly, including ON DELETE, ON UPDATE, MATCH, and DEFERRABLE options.
2869
+ *
2870
+ * @param schema - The schema object containing all table definitions (use `as const`)
2871
+ * @param builder - Callback receiving the relation builder for each table
2872
+ * @returns The relations object with full FK metadata
2873
+ *
2874
+ * @example Basic foreign keys
2875
+ * ```typescript
2876
+ * export const schema = { users, posts, comments } as const;
2877
+ *
2878
+ * export const relations = pgRelations(schema, (r) => ({
2879
+ * posts: {
2880
+ * // posts.user_id REFERENCES users ON DELETE CASCADE
2881
+ * userId: r.referenceTo.users(t => ({
2882
+ * onDelete: 'CASCADE',
2883
+ * })),
2884
+ * },
2885
+ * comments: {
2886
+ * postId: r.referenceTo.posts(t => ({ onDelete: 'CASCADE' })),
2887
+ * userId: r.referenceTo.users(t => ({ onDelete: 'SET NULL' })),
2888
+ * },
2889
+ * }));
2890
+ * ```
2891
+ *
2892
+ * @example FK to non-primary key column
2893
+ * ```typescript
2894
+ * export const relations = pgRelations(schema, (r) => ({
2895
+ * orders: {
2896
+ * // orders.wallet_address REFERENCES wallets(address)
2897
+ * walletAddress: r.referenceTo.wallets(t => ({
2898
+ * references: t.address, // Not wallets.id, but wallets.address
2899
+ * onDelete: 'SET NULL',
2900
+ * })),
2901
+ * },
2902
+ * }));
2903
+ * ```
2904
+ *
2905
+ * @example Composite (multi-column) foreign key
2906
+ * ```typescript
2907
+ * export const relations = pgRelations(schema, (r) => ({
2908
+ * orderItems: {
2909
+ * // FOREIGN KEY (product_id, warehouse_id)
2910
+ * // REFERENCES inventory(product_id, warehouse_id)
2911
+ * productWarehouse: r.referenceTo.inventory(t => ({
2912
+ * columns: [r.orderItems.productId, r.orderItems.warehouseId],
2913
+ * references: [t.productId, t.warehouseId],
2914
+ * onDelete: 'CASCADE',
2915
+ * match: 'FULL',
2916
+ * })),
2917
+ * },
2918
+ * }));
2919
+ * ```
2920
+ *
2921
+ * @example Deferrable constraint (for circular references)
2922
+ * ```typescript
2923
+ * export const relations = pgRelations(schema, (r) => ({
2924
+ * employees: {
2925
+ * // Can defer constraint check to transaction commit
2926
+ * managerId: r.referenceTo.employees(t => ({
2927
+ * references: t.id,
2928
+ * onDelete: 'SET NULL',
2929
+ * deferrable: true,
2930
+ * initiallyDeferred: true,
2931
+ * })),
2932
+ * },
2933
+ * }));
2934
+ * ```
2935
+ */
2936
+ export declare function pgRelations<TSchema extends Record<string, TableDefinition<any>>>(schema: TSchema, builder: (tables: {
2937
+ [K in keyof TSchema]: (defineRelations: (r: FullRelationBuilderWithRefs<TSchema>) => TableRelations) => TableRelations;
2938
+ }) => SchemaRelations): SchemaRelations;
2939
+ /**
2940
+ * Simplified pgRelations API - direct table → relations mapping.
2941
+ *
2942
+ * @example
2943
+ * ```typescript
2944
+ * export const relations = defineRelations(schema, {
2945
+ * posts: (r) => ({
2946
+ * userId: r.referenceTo.users(t => ({ onDelete: 'CASCADE' })),
2947
+ * }),
2948
+ * comments: (r) => ({
2949
+ * postId: r.referenceTo.posts(t => ({ onDelete: 'CASCADE' })),
2950
+ * userId: r.referenceTo.users(t => ({ onDelete: 'SET NULL' })),
2951
+ * }),
2952
+ * });
2953
+ * ```
2954
+ */
2955
+ export declare function defineRelations<TSchema extends Record<string, TableDefinition<any>>, TRelations extends {
2956
+ [K in keyof TSchema]?: (r: FullRelationBuilderWithRefs<TSchema>) => TableRelations;
2957
+ }>(schema: TSchema, relationDefs: TRelations): SchemaRelations;
2958
+ /**
2959
+ * Helper type to derive DatabaseSchema type from a schema const object.
2960
+ *
2961
+ * Use this to create a type alias for your database schema, enabling
2962
+ * full type inference across your application.
2963
+ *
2964
+ * @example
2965
+ * ```typescript
2966
+ * // In db/schema.ts
2967
+ * export const schema = { users, posts, comments } as const;
2968
+ * export type DatabaseSchema = RelqDatabaseSchema<typeof schema>;
2969
+ *
2970
+ * // Now you can use DatabaseSchema for typing
2971
+ * function getUser(db: Relq<DatabaseSchema>, id: string) {
2972
+ * return db.table.users.select('*').where(q => q.equal('id', id)).one();
2973
+ * }
2974
+ * ```
2975
+ */
2976
+ export type RelqDatabaseSchema<TSchema extends Record<string, TableDefinition<any>>> = {
2977
+ [K in keyof TSchema]: TSchema[K];
2978
+ };
2979
+ /**
2980
+ * Convert action code from pgsql-parser to ReferentialAction string.
2981
+ *
2982
+ * @internal Used by code generator
2983
+ */
2984
+ export declare function actionCodeToString(code: string): ReferentialAction;
2985
+ /**
2986
+ * Convert ReferentialAction to action code for pgsql-parser.
2987
+ *
2988
+ * @internal Used for SQL generation
2989
+ */
2990
+ export declare function stringToActionCode(action: ReferentialAction): string;
2991
+ /**
2992
+ * Convert match code from pgsql-parser to MatchType string.
2993
+ *
2994
+ * @internal Used by code generator
2995
+ */
2996
+ export declare function matchCodeToString(code: string): MatchType;
2997
+ /**
2998
+ * Generate SQL REFERENCES clause from a ForeignKeyRelationDef.
2999
+ *
3000
+ * @example
3001
+ * ```typescript
3002
+ * const sql = generateReferencesSQL(relationDef, 'user_id');
3003
+ * // "user_id uuid REFERENCES users(id) ON DELETE CASCADE"
3004
+ * ```
3005
+ */
3006
+ export declare function generateReferencesSQL(relation: ForeignKeyRelationDef, columnName: string, columnType?: string): string;
1773
3007
  export interface PgEnumConfig<T extends readonly string[]> {
1774
3008
  /** Enum type name in PostgreSQL */
1775
3009
  $enumName: string;
@@ -2173,6 +3407,54 @@ export declare function dropSequenceSQL(seq: SequenceConfig): string;
2173
3407
  * Type guard to check if a value is a SequenceConfig
2174
3408
  */
2175
3409
  export declare function isSequenceConfig(value: unknown): value is SequenceConfig;
3410
+ /**
3411
+ * PostgreSQL View Definitions
3412
+ *
3413
+ * Provides typed helpers for defining views and materialized views.
3414
+ */
3415
+ export interface ViewConfig {
3416
+ _type: "view";
3417
+ name: string;
3418
+ definition: string;
3419
+ isMaterialized: false;
3420
+ }
3421
+ export interface MaterializedViewConfig {
3422
+ _type: "materialized_view";
3423
+ name: string;
3424
+ definition: string;
3425
+ isMaterialized: true;
3426
+ withData?: boolean;
3427
+ }
3428
+ /**
3429
+ * Define a PostgreSQL view
3430
+ *
3431
+ * @example
3432
+ * export const activeUsers = pgView('active_users', `
3433
+ * SELECT id, name, email FROM users WHERE status = 'active'
3434
+ * `)
3435
+ */
3436
+ export declare function pgView(name: string, definition: string): ViewConfig;
3437
+ /**
3438
+ * Define a PostgreSQL materialized view
3439
+ *
3440
+ * @example
3441
+ * export const userStats = pgMaterializedView('user_stats', `
3442
+ * SELECT user_id, COUNT(*) as order_count, SUM(total) as total_spent
3443
+ * FROM orders
3444
+ * GROUP BY user_id
3445
+ * `)
3446
+ */
3447
+ export declare function pgMaterializedView(name: string, definition: string, options?: {
3448
+ withData?: boolean;
3449
+ }): MaterializedViewConfig;
3450
+ /**
3451
+ * Generate CREATE VIEW SQL
3452
+ */
3453
+ export declare function viewToSQL(view: ViewConfig): string;
3454
+ /**
3455
+ * Generate CREATE MATERIALIZED VIEW SQL
3456
+ */
3457
+ export declare function materializedViewToSQL(view: MaterializedViewConfig): string;
2176
3458
  /**
2177
3459
  * SQL Tagged Template Literal
2178
3460
  *