effect-qb 0.12.3 → 0.14.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 (65) hide show
  1. package/README.md +6 -1283
  2. package/dist/mysql.js +6376 -4978
  3. package/dist/postgres/metadata.js +2724 -0
  4. package/dist/postgres.js +5475 -3636
  5. package/package.json +13 -8
  6. package/src/internal/column-state.ts +88 -6
  7. package/src/internal/column.ts +569 -34
  8. package/src/internal/datatypes/define.ts +0 -30
  9. package/src/internal/executor.ts +45 -11
  10. package/src/internal/expression-ast.ts +15 -0
  11. package/src/internal/expression.ts +3 -1
  12. package/src/internal/implication-runtime.ts +171 -0
  13. package/src/internal/mysql-query.ts +7173 -0
  14. package/src/internal/mysql-renderer.ts +2 -2
  15. package/src/internal/plan.ts +14 -4
  16. package/src/internal/{query-factory.ts → postgres-query.ts} +669 -230
  17. package/src/internal/postgres-renderer.ts +2 -2
  18. package/src/internal/postgres-schema-model.ts +144 -0
  19. package/src/internal/predicate-analysis.ts +10 -0
  20. package/src/internal/predicate-context.ts +112 -36
  21. package/src/internal/predicate-formula.ts +31 -19
  22. package/src/internal/predicate-normalize.ts +177 -106
  23. package/src/internal/predicate-runtime.ts +676 -0
  24. package/src/internal/query.ts +471 -41
  25. package/src/internal/renderer.ts +2 -2
  26. package/src/internal/runtime-schema.ts +74 -20
  27. package/src/internal/schema-ddl.ts +55 -0
  28. package/src/internal/schema-derivation.ts +93 -21
  29. package/src/internal/schema-expression.ts +44 -0
  30. package/src/internal/sql-expression-renderer.ts +123 -35
  31. package/src/internal/table-options.ts +88 -7
  32. package/src/internal/table.ts +106 -42
  33. package/src/mysql/column.ts +3 -1
  34. package/src/mysql/datatypes/index.ts +17 -2
  35. package/src/mysql/executor.ts +20 -17
  36. package/src/mysql/function/aggregate.ts +6 -0
  37. package/src/mysql/function/core.ts +5 -0
  38. package/src/mysql/function/index.ts +20 -0
  39. package/src/mysql/function/json.ts +4 -0
  40. package/src/mysql/function/string.ts +6 -0
  41. package/src/mysql/function/temporal.ts +103 -0
  42. package/src/mysql/function/window.ts +7 -0
  43. package/src/mysql/private/query.ts +1 -0
  44. package/src/mysql/query.ts +6 -26
  45. package/src/mysql.ts +2 -0
  46. package/src/postgres/cast.ts +31 -0
  47. package/src/postgres/column.ts +27 -1
  48. package/src/postgres/datatypes/index.ts +40 -5
  49. package/src/postgres/executor.ts +19 -17
  50. package/src/postgres/function/aggregate.ts +6 -0
  51. package/src/postgres/function/core.ts +16 -0
  52. package/src/postgres/function/index.ts +20 -0
  53. package/src/postgres/function/json.ts +501 -0
  54. package/src/postgres/function/string.ts +6 -0
  55. package/src/postgres/function/temporal.ts +107 -0
  56. package/src/postgres/function/window.ts +7 -0
  57. package/src/postgres/metadata.ts +31 -0
  58. package/src/postgres/private/query.ts +1 -0
  59. package/src/postgres/query.ts +6 -28
  60. package/src/postgres/schema-expression.ts +16 -0
  61. package/src/postgres/schema-management.ts +204 -0
  62. package/src/postgres/schema.ts +35 -0
  63. package/src/postgres/table.ts +307 -41
  64. package/src/postgres/type.ts +4 -0
  65. package/src/postgres.ts +16 -0
@@ -7,6 +7,8 @@ import * as ExpressionAst from "./expression-ast.js"
7
7
  import * as JsonPath from "./json/path.js"
8
8
  import { flattenSelection, type Projection } from "./projections.js"
9
9
  import { type SelectionValue, validateAggregationSelection } from "./aggregation-validation.js"
10
+ import * as SchemaExpression from "./schema-expression.js"
11
+ import type { DdlExpressionLike } from "./table-options.js"
10
12
 
11
13
  const renderDbType = (
12
14
  dialect: SqlDialect,
@@ -44,35 +46,38 @@ const renderCastType = (
44
46
  }
45
47
  }
46
48
 
49
+ const renderDdlExpression = (
50
+ expression: DdlExpressionLike,
51
+ state: RenderState,
52
+ dialect: SqlDialect
53
+ ): string =>
54
+ SchemaExpression.isSchemaExpression(expression)
55
+ ? SchemaExpression.render(expression)
56
+ : renderExpression(expression, state, dialect)
57
+
47
58
  const renderColumnDefinition = (
48
59
  dialect: SqlDialect,
60
+ state: RenderState,
49
61
  columnName: string,
50
62
  column: Table.AnyTable[typeof Table.TypeId]["fields"][string]
51
63
  ): string => {
52
64
  const clauses = [
53
65
  dialect.quoteIdentifier(columnName),
54
- renderDbType(dialect, column.metadata.dbType)
66
+ column.metadata.ddlType ?? renderDbType(dialect, column.metadata.dbType)
55
67
  ]
68
+ if (column.metadata.identity) {
69
+ clauses.push(`generated ${column.metadata.identity.generation === "byDefault" ? "by default" : "always"} as identity`)
70
+ } else if (column.metadata.generatedValue) {
71
+ clauses.push(`generated always as (${renderDdlExpression(column.metadata.generatedValue, state, dialect)}) stored`)
72
+ } else if (column.metadata.defaultValue) {
73
+ clauses.push(`default ${renderDdlExpression(column.metadata.defaultValue, state, dialect)}`)
74
+ }
56
75
  if (!column.metadata.nullable) {
57
76
  clauses.push("not null")
58
77
  }
59
78
  return clauses.join(" ")
60
79
  }
61
80
 
62
- const renderCheckPredicate = (
63
- predicate: unknown,
64
- state: RenderState,
65
- dialect: SqlDialect
66
- ): string => {
67
- if (typeof predicate === "string") {
68
- return predicate
69
- }
70
- if (predicate !== null && typeof predicate === "object" && Expression.TypeId in predicate) {
71
- return renderExpression(predicate as Expression.Any, state, dialect)
72
- }
73
- throw new Error("Unsupported check constraint predicate for DDL rendering")
74
- }
75
-
76
81
  const renderCreateTableSql = (
77
82
  targetSource: QueryAst.FromClause,
78
83
  state: RenderState,
@@ -82,26 +87,26 @@ const renderCreateTableSql = (
82
87
  const table = targetSource.source as Table.AnyTable
83
88
  const fields = table[Table.TypeId].fields
84
89
  const definitions = Object.entries(fields).map(([columnName, column]) =>
85
- renderColumnDefinition(dialect, columnName, column)
90
+ renderColumnDefinition(dialect, state, columnName, column)
86
91
  )
87
92
  for (const option of table[Table.OptionsSymbol]) {
88
93
  switch (option.kind) {
89
94
  case "primaryKey":
90
- definitions.push(`primary key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`)
95
+ definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}primary key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
91
96
  break
92
97
  case "unique":
93
- definitions.push(`unique (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`)
98
+ definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}unique${option.nullsNotDistinct ? " nulls not distinct" : ""} (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
94
99
  break
95
100
  case "foreignKey": {
96
101
  const reference = option.references()
97
102
  definitions.push(
98
- `foreign key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")}) references ${dialect.renderTableReference(reference.tableName, reference.tableName, reference.schemaName)} (${reference.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`
103
+ `${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}foreign key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")}) references ${dialect.renderTableReference(reference.tableName, reference.tableName, reference.schemaName)} (${reference.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.onDelete ? ` on delete ${option.onDelete.replace(/[A-Z]/g, (value) => ` ${value.toLowerCase()}`).trim()}` : ""}${option.onUpdate ? ` on update ${option.onUpdate.replace(/[A-Z]/g, (value) => ` ${value.toLowerCase()}`).trim()}` : ""}${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`
99
104
  )
100
105
  break
101
106
  }
102
107
  case "check":
103
108
  definitions.push(
104
- `constraint ${dialect.quoteIdentifier(option.name)} check (${renderCheckPredicate(option.predicate, state, dialect)})`
109
+ `constraint ${dialect.quoteIdentifier(option.name)} check (${renderDdlExpression(option.predicate, state, dialect)})${option.noInherit ? " no inherit" : ""}`
105
110
  )
106
111
  break
107
112
  case "index":
@@ -258,6 +263,12 @@ const renderPostgresJsonPathArray = (
258
263
  }
259
264
  }).join(", ")}]`
260
265
 
266
+ const renderPostgresTextLiteral = (
267
+ value: string,
268
+ state: RenderState,
269
+ dialect: SqlDialect
270
+ ): string => `cast(${dialect.renderLiteral(value, state)} as text)`
271
+
261
272
  const renderPostgresJsonAccessStep = (
262
273
  segment: JsonPath.AnySegment,
263
274
  textMode: boolean,
@@ -288,6 +299,10 @@ const renderPostgresJsonValue = (
288
299
  : `cast(${rendered} as jsonb)`
289
300
  }
290
301
 
302
+ const renderPostgresJsonKind = (
303
+ value: Expression.Any
304
+ ): "json" | "jsonb" => value[Expression.TypeId].dbType.kind === "jsonb" ? "jsonb" : "json"
305
+
291
306
  const renderJsonOpaquePath = (
292
307
  value: unknown,
293
308
  state: RenderState,
@@ -305,7 +320,48 @@ const renderJsonOpaquePath = (
305
320
  throw new Error("Unsupported SQL/JSON path input")
306
321
  }
307
322
 
323
+ const renderFunctionCall = (
324
+ name: string,
325
+ args: readonly Expression.Any[],
326
+ state: RenderState,
327
+ dialect: SqlDialect
328
+ ): string => {
329
+ if (name === "array") {
330
+ return `ARRAY[${args.map((arg) => renderExpression(arg, state, dialect)).join(", ")}]`
331
+ }
332
+ if (name === "extract" && args.length === 2) {
333
+ const field = args[0]
334
+ const source = args[1]
335
+ if (field === undefined) {
336
+ throw new Error("Unsupported SQL extract expression")
337
+ }
338
+ if (source === undefined) {
339
+ throw new Error("Unsupported SQL extract expression")
340
+ }
341
+ const fieldRuntime = isExpression(field) && field[Expression.TypeId].dbType.kind === "text" && typeof field[Expression.TypeId].runtime === "string"
342
+ ? field[Expression.TypeId].runtime
343
+ : undefined
344
+ const renderedField = fieldRuntime ?? renderExpression(field, state, dialect)
345
+ return `extract(${renderedField} from ${renderExpression(source, state, dialect)})`
346
+ }
347
+ const renderedArgs = args.map((arg) => renderExpression(arg, state, dialect)).join(", ")
348
+ if (args.length === 0) {
349
+ switch (name) {
350
+ case "current_date":
351
+ case "current_time":
352
+ case "current_timestamp":
353
+ case "localtime":
354
+ case "localtimestamp":
355
+ return name
356
+ default:
357
+ return `${name}()`
358
+ }
359
+ }
360
+ return `${name}(${renderedArgs})`
361
+ }
362
+
308
363
  const renderJsonExpression = (
364
+ expression: Expression.Any,
309
365
  ast: Record<string, unknown>,
310
366
  state: RenderState,
311
367
  dialect: SqlDialect
@@ -318,6 +374,12 @@ const renderJsonExpression = (
318
374
  const base = extractJsonBase(ast)
319
375
  const segments = extractJsonPathSegments(ast)
320
376
  const exact = segments.every((segment) => segment.kind === "key" || segment.kind === "index")
377
+ const postgresExpressionKind = dialect.name === "postgres" && isJsonExpression(expression)
378
+ ? renderPostgresJsonKind(expression)
379
+ : undefined
380
+ const postgresBaseKind = dialect.name === "postgres" && isJsonExpression(base)
381
+ ? renderPostgresJsonKind(base)
382
+ : undefined
321
383
 
322
384
  switch (kind) {
323
385
  case "jsonGet":
@@ -341,7 +403,7 @@ const renderJsonExpression = (
341
403
  }
342
404
  const jsonPathLiteral = dialect.renderLiteral(renderJsonPathStringLiteral(segments), state)
343
405
  const queried = `jsonb_path_query_first(${renderPostgresJsonValue(base, state, dialect)}, ${jsonPathLiteral})`
344
- return textMode ? `cast(${queried} as text)` : queried
406
+ return textMode ? `(${queried} #>> '{}')` : queried
345
407
  }
346
408
  if (dialect.name === "mysql") {
347
409
  const extracted = `json_extract(${baseSql}, ${renderMySqlJsonPath(segments, state, dialect)})`
@@ -365,12 +427,12 @@ const renderJsonExpression = (
365
427
  }
366
428
  if (dialect.name === "postgres") {
367
429
  if (kind === "jsonHasAnyKeys") {
368
- return `(${baseSql} ?| ${renderPostgresJsonPathArray(keys, state, dialect)})`
430
+ return `(${baseSql} ?| array[${keys.map((key) => renderPostgresTextLiteral(String(key), state, dialect)).join(", ")}])`
369
431
  }
370
432
  if (kind === "jsonHasAllKeys") {
371
- return `(${baseSql} ?& ${renderPostgresJsonPathArray(keys, state, dialect)})`
433
+ return `(${baseSql} ?& array[${keys.map((key) => renderPostgresTextLiteral(String(key), state, dialect)).join(", ")}])`
372
434
  }
373
- return `(${baseSql} ? ${dialect.renderLiteral(keys[0]!, state)})`
435
+ return `(${baseSql} ? ${renderPostgresTextLiteral(String(keys[0]!), state, dialect)})`
374
436
  }
375
437
  if (dialect.name === "mysql") {
376
438
  const mode = kind === "jsonHasAllKeys" ? "all" : "one"
@@ -401,7 +463,7 @@ const renderJsonExpression = (
401
463
  renderExpression(entry.value, state, dialect)
402
464
  ])
403
465
  if (dialect.name === "postgres") {
404
- return `jsonb_build_object(${renderedEntries.join(", ")})`
466
+ return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_object(${renderedEntries.join(", ")})`
405
467
  }
406
468
  if (dialect.name === "mysql") {
407
469
  return `json_object(${renderedEntries.join(", ")})`
@@ -414,7 +476,7 @@ const renderJsonExpression = (
414
476
  : []
415
477
  const renderedValues = values.map((value) => renderExpression(value, state, dialect)).join(", ")
416
478
  if (dialect.name === "postgres") {
417
- return `jsonb_build_array(${renderedValues})`
479
+ return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_array(${renderedValues})`
418
480
  }
419
481
  if (dialect.name === "mysql") {
420
482
  return `json_array(${renderedValues})`
@@ -448,7 +510,8 @@ const renderJsonExpression = (
448
510
  return undefined
449
511
  }
450
512
  if (dialect.name === "postgres") {
451
- return `jsonb_typeof(${renderPostgresJsonValue(base, state, dialect)})`
513
+ const baseSql = renderExpression(base, state, dialect)
514
+ return `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof(${baseSql})`
452
515
  }
453
516
  if (dialect.name === "mysql") {
454
517
  return `json_type(${renderExpression(base, state, dialect)})`
@@ -459,8 +522,11 @@ const renderJsonExpression = (
459
522
  return undefined
460
523
  }
461
524
  if (dialect.name === "postgres") {
462
- const jsonb = renderPostgresJsonValue(base, state, dialect)
463
- return `(case when jsonb_typeof(${jsonb}) = 'array' then jsonb_array_length(${jsonb}) when jsonb_typeof(${jsonb}) = 'object' then jsonb_object_length(${jsonb}) else null end)`
525
+ const baseSql = renderExpression(base, state, dialect)
526
+ const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
527
+ const arrayLength = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_array_length`
528
+ const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
529
+ return `(case when ${typeOf}(${baseSql}) = 'array' then ${arrayLength}(${baseSql}) when ${typeOf}(${baseSql}) = 'object' then (select count(*)::int from ${objectKeys}(${baseSql})) else null end)`
464
530
  }
465
531
  if (dialect.name === "mysql") {
466
532
  return `json_length(${renderExpression(base, state, dialect)})`
@@ -471,8 +537,10 @@ const renderJsonExpression = (
471
537
  return undefined
472
538
  }
473
539
  if (dialect.name === "postgres") {
474
- const jsonb = renderPostgresJsonValue(base, state, dialect)
475
- return `(case when jsonb_typeof(${jsonb}) = 'object' then array(select jsonb_object_keys(${jsonb})) else null end)`
540
+ const baseSql = renderExpression(base, state, dialect)
541
+ const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
542
+ const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
543
+ return `(case when ${typeOf}(${baseSql}) = 'object' then array(select ${objectKeys}(${baseSql})) else null end)`
476
544
  }
477
545
  if (dialect.name === "mysql") {
478
546
  return `json_keys(${renderExpression(base, state, dialect)})`
@@ -483,7 +551,7 @@ const renderJsonExpression = (
483
551
  return undefined
484
552
  }
485
553
  if (dialect.name === "postgres") {
486
- return `jsonb_strip_nulls(${renderPostgresJsonValue(base, state, dialect)})`
554
+ return `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_strip_nulls(${renderExpression(base, state, dialect)})`
487
555
  }
488
556
  unsupportedJsonFeature(dialect, "jsonStripNulls")
489
557
  return undefined
@@ -1162,7 +1230,7 @@ export const renderExpression = (
1162
1230
  const rawAst = (expression as Expression.Any & {
1163
1231
  readonly [ExpressionAst.TypeId]: ExpressionAst.Any
1164
1232
  })[ExpressionAst.TypeId] as ExpressionAst.Any | Record<string, unknown>
1165
- const jsonSql = renderJsonExpression(rawAst as Record<string, unknown>, state, dialect)
1233
+ const jsonSql = renderJsonExpression(expression, rawAst as Record<string, unknown>, state, dialect)
1166
1234
  if (jsonSql !== undefined) {
1167
1235
  return jsonSql
1168
1236
  }
@@ -1179,9 +1247,11 @@ export const renderExpression = (
1179
1247
  : operator === "gt"
1180
1248
  ? ">"
1181
1249
  : ">="
1182
- switch (ast.kind) {
1250
+ switch (ast.kind) {
1183
1251
  case "column":
1184
- return `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
1252
+ return ast.tableName.length === 0
1253
+ ? dialect.quoteIdentifier(ast.columnName)
1254
+ : `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
1185
1255
  case "literal":
1186
1256
  return dialect.renderLiteral(ast.value, state)
1187
1257
  case "excluded":
@@ -1190,6 +1260,8 @@ export const renderExpression = (
1190
1260
  : `excluded.${dialect.quoteIdentifier(ast.columnName)}`
1191
1261
  case "cast":
1192
1262
  return `cast(${renderExpression(ast.value, state, dialect)} as ${renderCastType(dialect, ast.target)})`
1263
+ case "function":
1264
+ return renderFunctionCall(ast.name, Array.isArray(ast.args) ? ast.args : [], state, dialect)
1193
1265
  case "eq":
1194
1266
  return `(${renderExpression(ast.left, state, dialect)} = ${renderExpression(ast.right, state, dialect)})`
1195
1267
  case "neq":
@@ -1208,6 +1280,22 @@ export const renderExpression = (
1208
1280
  return dialect.name === "postgres"
1209
1281
  ? `(${renderExpression(ast.left, state, dialect)} ilike ${renderExpression(ast.right, state, dialect)})`
1210
1282
  : `(lower(${renderExpression(ast.left, state, dialect)}) like lower(${renderExpression(ast.right, state, dialect)}))`
1283
+ case "regexMatch":
1284
+ return dialect.name === "postgres"
1285
+ ? `(${renderExpression(ast.left, state, dialect)} ~ ${renderExpression(ast.right, state, dialect)})`
1286
+ : `(${renderExpression(ast.left, state, dialect)} regexp ${renderExpression(ast.right, state, dialect)})`
1287
+ case "regexIMatch":
1288
+ return dialect.name === "postgres"
1289
+ ? `(${renderExpression(ast.left, state, dialect)} ~* ${renderExpression(ast.right, state, dialect)})`
1290
+ : `(${renderExpression(ast.left, state, dialect)} regexp ${renderExpression(ast.right, state, dialect)})`
1291
+ case "regexNotMatch":
1292
+ return dialect.name === "postgres"
1293
+ ? `(${renderExpression(ast.left, state, dialect)} !~ ${renderExpression(ast.right, state, dialect)})`
1294
+ : `(${renderExpression(ast.left, state, dialect)} not regexp ${renderExpression(ast.right, state, dialect)})`
1295
+ case "regexNotIMatch":
1296
+ return dialect.name === "postgres"
1297
+ ? `(${renderExpression(ast.left, state, dialect)} !~* ${renderExpression(ast.right, state, dialect)})`
1298
+ : `(${renderExpression(ast.left, state, dialect)} not regexp ${renderExpression(ast.right, state, dialect)})`
1211
1299
  case "isDistinctFrom":
1212
1300
  return dialect.name === "mysql"
1213
1301
  ? `(not (${renderExpression(ast.left, state, dialect)} <=> ${renderExpression(ast.right, state, dialect)}))`
@@ -4,39 +4,78 @@ import {
4
4
  type AnyColumnDefinition,
5
5
  type IsNullable
6
6
  } from "./column-state.js"
7
+ import type { Any as AnyExpression } from "./expression.js"
8
+ import type { Any as AnySchemaExpression } from "./schema-expression.js"
7
9
  import type { TableFieldMap } from "./schema-derivation.js"
8
10
 
9
11
  /** Non-empty list of column names. */
10
12
  export type ColumnList = readonly [string, ...string[]]
11
13
 
14
+ export type DdlExpressionLike = AnyExpression | AnySchemaExpression
15
+
16
+ export type ReferentialAction = "noAction" | "restrict" | "cascade" | "setNull" | "setDefault"
17
+
18
+ export type IndexKeySpec =
19
+ | {
20
+ readonly kind: "column"
21
+ readonly column: string
22
+ readonly order?: "asc" | "desc"
23
+ readonly nulls?: "first" | "last"
24
+ }
25
+ | {
26
+ readonly kind: "expression"
27
+ readonly expression: DdlExpressionLike
28
+ readonly order?: "asc" | "desc"
29
+ readonly nulls?: "first" | "last"
30
+ }
31
+
12
32
  /** Normalized table-level option record. */
13
33
  export type TableOptionSpec =
14
34
  | {
15
35
  readonly kind: "index"
16
- readonly columns: ColumnList
36
+ readonly columns?: ColumnList
37
+ readonly name?: string
38
+ readonly unique?: boolean
39
+ readonly method?: string
40
+ readonly include?: readonly string[]
41
+ readonly predicate?: DdlExpressionLike
42
+ readonly keys?: readonly [IndexKeySpec, ...IndexKeySpec[]]
17
43
  }
18
44
  | {
19
45
  readonly kind: "unique"
20
46
  readonly columns: ColumnList
47
+ readonly name?: string
48
+ readonly nullsNotDistinct?: boolean
49
+ readonly deferrable?: boolean
50
+ readonly initiallyDeferred?: boolean
21
51
  }
22
52
  | {
23
53
  readonly kind: "primaryKey"
24
54
  readonly columns: ColumnList
55
+ readonly name?: string
56
+ readonly deferrable?: boolean
57
+ readonly initiallyDeferred?: boolean
25
58
  }
26
59
  | {
27
60
  readonly kind: "foreignKey"
28
61
  readonly columns: ColumnList
62
+ readonly name?: string
29
63
  readonly references: () => {
30
64
  readonly tableName: string
31
65
  readonly schemaName?: string
32
66
  readonly columns: ColumnList
33
67
  readonly knownColumns?: readonly string[]
34
68
  }
69
+ readonly onUpdate?: ReferentialAction
70
+ readonly onDelete?: ReferentialAction
71
+ readonly deferrable?: boolean
72
+ readonly initiallyDeferred?: boolean
35
73
  }
36
74
  | {
37
75
  readonly kind: "check"
38
76
  readonly name: string
39
- readonly predicate: unknown
77
+ readonly predicate: DdlExpressionLike
78
+ readonly noInherit?: boolean
40
79
  }
41
80
 
42
81
  /** Thin wrapper used by the public `Table.*` option builders. */
@@ -108,7 +147,11 @@ export const collectInlineOptions = <Fields extends TableFieldMap>(
108
147
  if (column.metadata.unique && !column.metadata.primaryKey) {
109
148
  options.push({
110
149
  kind: "unique",
111
- columns: [columnName]
150
+ columns: [columnName],
151
+ name: column.metadata.uniqueConstraint?.name,
152
+ nullsNotDistinct: column.metadata.uniqueConstraint?.nullsNotDistinct,
153
+ deferrable: column.metadata.uniqueConstraint?.deferrable,
154
+ initiallyDeferred: column.metadata.uniqueConstraint?.initiallyDeferred
112
155
  })
113
156
  }
114
157
  if (column.metadata.references) {
@@ -124,7 +167,27 @@ export const collectInlineOptions = <Fields extends TableFieldMap>(
124
167
  schemaName: bound.schemaName,
125
168
  columns: [bound.columnName]
126
169
  }
127
- }
170
+ },
171
+ name: column.metadata.references.name,
172
+ onUpdate: column.metadata.references.onUpdate,
173
+ onDelete: column.metadata.references.onDelete,
174
+ deferrable: column.metadata.references.deferrable,
175
+ initiallyDeferred: column.metadata.references.initiallyDeferred
176
+ })
177
+ }
178
+ if (column.metadata.index) {
179
+ options.push({
180
+ kind: "index",
181
+ keys: [{
182
+ kind: "column",
183
+ column: columnName,
184
+ order: column.metadata.index.order,
185
+ nulls: column.metadata.index.nulls
186
+ }],
187
+ name: column.metadata.index.name,
188
+ method: column.metadata.index.method,
189
+ include: column.metadata.index.include,
190
+ predicate: column.metadata.index.predicate
128
191
  })
129
192
  }
130
193
  }
@@ -173,17 +236,20 @@ export const validateOptions = <Fields extends TableFieldMap>(
173
236
  case "primaryKey":
174
237
  case "unique":
175
238
  case "foreignKey": {
176
- if (option.columns.length === 0) {
239
+ const columns = option.kind === "index"
240
+ ? option.columns ?? []
241
+ : option.columns
242
+ if (columns.length === 0 && option.kind !== "index") {
177
243
  throw new Error(`Option '${option.kind}' on table '${tableName}' requires at least one column`)
178
244
  }
179
- for (const column of option.columns) {
245
+ for (const column of columns) {
180
246
  if (!knownColumns.has(column)) {
181
247
  throw new Error(`Unknown column '${column}' on table '${tableName}'`)
182
248
  }
183
249
  }
184
250
  if (option.kind === "foreignKey") {
185
251
  const reference = option.references()
186
- if (reference.columns.length !== option.columns.length) {
252
+ if (reference.columns.length !== columns.length) {
187
253
  throw new Error(`Foreign key on table '${tableName}' must reference the same number of columns`)
188
254
  }
189
255
  if (reference.knownColumns) {
@@ -195,6 +261,21 @@ export const validateOptions = <Fields extends TableFieldMap>(
195
261
  }
196
262
  }
197
263
  }
264
+ if (option.kind === "index") {
265
+ for (const column of option.include ?? []) {
266
+ if (!knownColumns.has(column)) {
267
+ throw new Error(`Unknown included column '${column}' on table '${tableName}'`)
268
+ }
269
+ }
270
+ for (const key of option.keys ?? []) {
271
+ if (key.kind === "column" && !knownColumns.has(key.column)) {
272
+ throw new Error(`Unknown index key column '${key.column}' on table '${tableName}'`)
273
+ }
274
+ }
275
+ if (option.columns === undefined && (option.keys === undefined || option.keys.length === 0)) {
276
+ throw new Error(`Index on table '${tableName}' requires at least one column or key`)
277
+ }
278
+ }
198
279
  break
199
280
  }
200
281
  case "check": {