effect-qb 0.16.0 → 0.17.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 (75) hide show
  1. package/dist/mysql.js +1661 -591
  2. package/dist/postgres/metadata.js +1930 -135
  3. package/dist/postgres.js +7808 -6718
  4. package/dist/sqlite.js +8360 -0
  5. package/package.json +6 -1
  6. package/src/internal/derived-table.ts +29 -3
  7. package/src/internal/dialect.ts +2 -0
  8. package/src/internal/dsl-mutation-runtime.ts +173 -4
  9. package/src/internal/dsl-plan-runtime.ts +165 -20
  10. package/src/internal/dsl-query-runtime.ts +60 -6
  11. package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
  12. package/src/internal/executor.ts +47 -9
  13. package/src/internal/expression-ast.ts +3 -2
  14. package/src/internal/grouping-key.ts +141 -1
  15. package/src/internal/implication-runtime.ts +2 -1
  16. package/src/internal/json/types.ts +155 -40
  17. package/src/internal/predicate/context.ts +14 -1
  18. package/src/internal/predicate/key.ts +19 -2
  19. package/src/internal/predicate/runtime.ts +27 -3
  20. package/src/internal/query.ts +252 -30
  21. package/src/internal/renderer.ts +35 -2
  22. package/src/internal/runtime/driver-value-mapping.ts +58 -0
  23. package/src/internal/runtime/normalize.ts +62 -38
  24. package/src/internal/runtime/schema.ts +5 -3
  25. package/src/internal/runtime/value.ts +153 -30
  26. package/src/internal/table-options.ts +108 -1
  27. package/src/internal/table.ts +87 -29
  28. package/src/mysql/column.ts +18 -2
  29. package/src/mysql/datatypes/index.ts +21 -0
  30. package/src/mysql/errors/catalog.ts +5 -5
  31. package/src/mysql/errors/normalize.ts +2 -2
  32. package/src/mysql/internal/dsl.ts +736 -218
  33. package/src/mysql/internal/renderer.ts +2 -1
  34. package/src/mysql/internal/sql-expression-renderer.ts +486 -130
  35. package/src/mysql/query.ts +9 -2
  36. package/src/mysql/table.ts +38 -12
  37. package/src/postgres/column.ts +4 -2
  38. package/src/postgres/errors/normalize.ts +2 -2
  39. package/src/postgres/executor.ts +48 -5
  40. package/src/postgres/function/core.ts +19 -1
  41. package/src/postgres/internal/dsl.ts +683 -240
  42. package/src/postgres/internal/renderer.ts +2 -1
  43. package/src/postgres/internal/schema-ddl.ts +2 -1
  44. package/src/postgres/internal/schema-model.ts +6 -3
  45. package/src/postgres/internal/sql-expression-renderer.ts +420 -91
  46. package/src/postgres/json.ts +57 -17
  47. package/src/postgres/query.ts +9 -2
  48. package/src/postgres/schema-management.ts +91 -4
  49. package/src/postgres/schema.ts +1 -1
  50. package/src/postgres/table.ts +189 -53
  51. package/src/sqlite/column.ts +128 -0
  52. package/src/sqlite/datatypes/index.ts +79 -0
  53. package/src/sqlite/datatypes/spec.ts +98 -0
  54. package/src/sqlite/errors/catalog.ts +103 -0
  55. package/src/sqlite/errors/fields.ts +19 -0
  56. package/src/sqlite/errors/index.ts +19 -0
  57. package/src/sqlite/errors/normalize.ts +229 -0
  58. package/src/sqlite/errors/requirements.ts +71 -0
  59. package/src/sqlite/errors/types.ts +29 -0
  60. package/src/sqlite/executor.ts +227 -0
  61. package/src/sqlite/function/aggregate.ts +2 -0
  62. package/src/sqlite/function/core.ts +2 -0
  63. package/src/sqlite/function/index.ts +19 -0
  64. package/src/sqlite/function/string.ts +2 -0
  65. package/src/sqlite/function/temporal.ts +100 -0
  66. package/src/sqlite/function/window.ts +2 -0
  67. package/src/sqlite/internal/dialect.ts +37 -0
  68. package/src/sqlite/internal/dsl.ts +6926 -0
  69. package/src/sqlite/internal/renderer.ts +47 -0
  70. package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
  71. package/src/sqlite/json.ts +2 -0
  72. package/src/sqlite/query.ts +196 -0
  73. package/src/sqlite/renderer.ts +24 -0
  74. package/src/sqlite/table.ts +183 -0
  75. package/src/sqlite.ts +22 -0
@@ -1,13 +1,30 @@
1
1
  import type * as Expression from "../scalar.js"
2
2
  import type { RuntimeTag } from "../datatypes/shape.js"
3
+ import {
4
+ canonicalizeBigIntString,
5
+ canonicalizeDecimalString,
6
+ isValidInstantString,
7
+ isValidLocalDateString,
8
+ isValidLocalDateTimeString,
9
+ isValidLocalTimeString,
10
+ isValidOffsetTimeString
11
+ } from "./value.js"
3
12
 
4
13
  const isRecord = (value: unknown): value is Record<string, unknown> =>
5
14
  typeof value === "object" && value !== null && !Array.isArray(value)
6
15
 
16
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
17
+ if (!isRecord(value)) {
18
+ return false
19
+ }
20
+ const prototype = Object.getPrototypeOf(value)
21
+ return prototype === Object.prototype || prototype === null
22
+ }
23
+
7
24
  const pad = (value: number, width = 2): string => value.toString().padStart(width, "0")
8
25
 
9
26
  const formatLocalDate = (value: Date): string =>
10
- `${value.getUTCFullYear()}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`
27
+ `${pad(value.getUTCFullYear(), 4)}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`
11
28
 
12
29
  const formatLocalTime = (value: Date): string => {
13
30
  const milliseconds = value.getUTCMilliseconds()
@@ -34,12 +51,17 @@ const expectString = (value: unknown, label: string): string => {
34
51
  throw new Error(`Expected ${label} as string`)
35
52
  }
36
53
 
54
+ const finiteNumberStringPattern = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:[eE][+-]?\d+)?$/
55
+
37
56
  const normalizeNumber = (value: unknown): number => {
38
57
  if (typeof value === "number" && Number.isFinite(value)) {
39
58
  return value
40
59
  }
41
- if (typeof value === "string" && value.trim() !== "") {
42
- const parsed = Number(value)
60
+ if (typeof value === "string") {
61
+ const trimmed = value.trim()
62
+ const parsed = finiteNumberStringPattern.test(trimmed)
63
+ ? Number(trimmed)
64
+ : Number.NaN
43
65
  if (Number.isFinite(parsed)) {
44
66
  return parsed
45
67
  }
@@ -81,27 +103,12 @@ const normalizeBigIntString = (value: unknown): string => {
81
103
  if (typeof value === "number" && Number.isSafeInteger(value)) {
82
104
  return BigInt(value).toString()
83
105
  }
84
- if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
85
- return BigInt(value.trim()).toString()
106
+ if (typeof value === "string") {
107
+ return canonicalizeBigIntString(value)
86
108
  }
87
109
  throw new Error("Expected an integer-like bigint value")
88
110
  }
89
111
 
90
- const canonicalizeDecimalString = (input: string): string => {
91
- const trimmed = input.trim()
92
- const match = /^([+-]?)(\d+)(?:\.(\d+))?$/.exec(trimmed)
93
- if (match === null) {
94
- throw new Error("Expected a decimal string")
95
- }
96
- const sign = match[1] === "-" ? "-" : ""
97
- const integer = match[2]!.replace(/^0+(?=\d)/, "") || "0"
98
- const fraction = (match[3] ?? "").replace(/0+$/, "")
99
- if (fraction.length === 0) {
100
- return `${sign}${integer}`
101
- }
102
- return `${sign}${integer}.${fraction}`
103
- }
104
-
105
112
  const normalizeDecimalString = (value: unknown): string => {
106
113
  if (typeof value === "string") {
107
114
  return canonicalizeDecimalString(value)
@@ -121,12 +128,15 @@ const normalizeLocalDate = (value: unknown): string => {
121
128
  return formatLocalDate(value)
122
129
  }
123
130
  const raw = expectString(value, "local date").trim()
124
- if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
131
+ if (isValidLocalDateString(raw)) {
125
132
  return raw
126
133
  }
127
- const parsed = new Date(raw)
128
- if (!Number.isNaN(parsed.getTime())) {
129
- return formatLocalDate(parsed)
134
+ const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
135
+ if (isValidInstantString(canonicalInstant)) {
136
+ const parsed = new Date(canonicalInstant)
137
+ if (!Number.isNaN(parsed.getTime())) {
138
+ return formatLocalDate(parsed)
139
+ }
130
140
  }
131
141
  throw new Error("Expected a local-date value")
132
142
  }
@@ -136,7 +146,7 @@ const normalizeLocalTime = (value: unknown): string => {
136
146
  return formatLocalTime(value)
137
147
  }
138
148
  const raw = expectString(value, "local time").trim()
139
- if (/^\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(raw)) {
149
+ if (isValidLocalTimeString(raw)) {
140
150
  return raw
141
151
  }
142
152
  throw new Error("Expected a local-time value")
@@ -147,7 +157,7 @@ const normalizeOffsetTime = (value: unknown): string => {
147
157
  return `${formatLocalTime(value)}Z`
148
158
  }
149
159
  const raw = expectString(value, "offset time").trim()
150
- if (/^\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(raw)) {
160
+ if (isValidOffsetTimeString(raw)) {
151
161
  return raw
152
162
  }
153
163
  throw new Error("Expected an offset-time value")
@@ -158,11 +168,13 @@ const normalizeLocalDateTime = (value: unknown): string => {
158
168
  return formatLocalDateTime(value)
159
169
  }
160
170
  const raw = expectString(value, "local datetime").trim()
161
- if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(raw)) {
162
- return raw.replace(" ", "T")
171
+ const canonicalLocalDateTime = raw.replace(" ", "T")
172
+ if (isValidLocalDateTimeString(canonicalLocalDateTime)) {
173
+ return canonicalLocalDateTime
163
174
  }
164
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(raw)) {
165
- const parsed = new Date(raw)
175
+ const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
176
+ if (isValidInstantString(canonicalInstant)) {
177
+ const parsed = new Date(canonicalInstant)
166
178
  if (!Number.isNaN(parsed.getTime())) {
167
179
  return formatLocalDateTime(parsed)
168
180
  }
@@ -178,7 +190,11 @@ const normalizeInstant = (value: unknown): string => {
178
190
  if (!/[zZ]|[+-]\d{2}:\d{2}$/.test(raw)) {
179
191
  throw new Error("Instant values require a timezone offset")
180
192
  }
181
- const parsed = new Date(raw)
193
+ const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
194
+ if (!isValidInstantString(canonicalInstant)) {
195
+ throw new Error("Expected an ISO instant value")
196
+ }
197
+ const parsed = new Date(canonicalInstant)
182
198
  if (Number.isNaN(parsed.getTime())) {
183
199
  throw new Error("Expected an ISO instant value")
184
200
  }
@@ -209,20 +225,21 @@ const normalizeBytes = (value: unknown): Uint8Array => {
209
225
  throw new Error("Expected a byte array value")
210
226
  }
211
227
 
212
- const isJsonValue = (value: unknown): boolean => {
228
+ export const isJsonValue = (value: unknown): boolean => {
213
229
  if (value === null) {
214
230
  return true
215
231
  }
216
232
  switch (typeof value) {
217
233
  case "string":
218
- case "number":
219
234
  case "boolean":
220
235
  return true
236
+ case "number":
237
+ return Number.isFinite(value)
221
238
  case "object":
222
239
  if (Array.isArray(value)) {
223
240
  return value.every(isJsonValue)
224
241
  }
225
- return isRecord(value) && Object.values(value).every(isJsonValue)
242
+ return isPlainRecord(value) && Object.values(value).every(isJsonValue)
226
243
  default:
227
244
  return false
228
245
  }
@@ -230,11 +247,18 @@ const isJsonValue = (value: unknown): boolean => {
230
247
 
231
248
  const normalizeJson = (value: unknown): unknown => {
232
249
  if (typeof value === "string") {
233
- const parsed = JSON.parse(value)
234
- if (isJsonValue(parsed)) {
235
- return parsed
250
+ try {
251
+ const parsed = JSON.parse(value)
252
+ if (isJsonValue(parsed)) {
253
+ return parsed
254
+ }
255
+ throw new Error("Parsed JSON value is not a valid JSON runtime")
256
+ } catch (error) {
257
+ if (error instanceof SyntaxError) {
258
+ return value
259
+ }
260
+ throw error
236
261
  }
237
- throw new Error("Parsed JSON value is not a valid JSON runtime")
238
262
  }
239
263
  if (isJsonValue(value)) {
240
264
  return value
@@ -38,12 +38,14 @@ const schemaCache = new WeakMap<Expression.Any, RuntimeSchema | undefined>()
38
38
  const isRecord = (value: unknown): value is Record<string, unknown> =>
39
39
  typeof value === "object" && value !== null && !Array.isArray(value)
40
40
 
41
+ const FiniteNumberSchema = Schema.Number.pipe(Schema.finite())
42
+
41
43
  const runtimeSchemaForTag = (tag: RuntimeTag): RuntimeSchema | undefined => {
42
44
  switch (tag) {
43
45
  case "string":
44
46
  return Schema.String
45
47
  case "number":
46
- return Schema.Number
48
+ return FiniteNumberSchema
47
49
  case "bigintString":
48
50
  return BigIntStringSchema
49
51
  case "boolean":
@@ -388,7 +390,7 @@ const deriveRuntimeSchema = (
388
390
  return Schema.String
389
391
  case "count":
390
392
  case "jsonLength":
391
- return Schema.Number
393
+ return FiniteNumberSchema
392
394
  case "max":
393
395
  case "min":
394
396
  return expressionRuntimeSchema(ast.value, context)
@@ -404,7 +406,7 @@ const deriveRuntimeSchema = (
404
406
  case "window":
405
407
  return ast.function === "over" && ast.value !== undefined
406
408
  ? expressionRuntimeSchema(ast.value, context)
407
- : Schema.Number
409
+ : FiniteNumberSchema
408
410
  case "jsonGet":
409
411
  case "jsonPath":
410
412
  case "jsonAccess":
@@ -23,50 +23,173 @@ const brandString = <BrandName extends string>(
23
23
  Schema.brand(brand)
24
24
  ) as unknown as Schema.Schema<string & Brand.Brand<BrandName>>
25
25
 
26
- export const LocalDateStringSchema = brandString(
27
- /^\d{4}-\d{2}-\d{2}$/,
28
- "LocalDateString"
29
- )
26
+ export const localDatePattern = /^(\d{4})-(\d{2})-(\d{2})$/
30
27
 
31
- export const LocalTimeStringSchema = brandString(
32
- /^\d{2}:\d{2}:\d{2}(?:\.\d+)?$/,
33
- "LocalTimeString"
34
- )
28
+ export const isValidLocalDateString = (value: string): boolean => {
29
+ const match = localDatePattern.exec(value)
30
+ if (match === null) {
31
+ return false
32
+ }
33
+ const year = Number(match[1])
34
+ const month = Number(match[2])
35
+ const day = Number(match[3])
36
+ const parsed = new Date(Date.UTC(year, month - 1, day))
37
+ parsed.setUTCFullYear(year)
38
+ return parsed.getUTCFullYear() === year &&
39
+ parsed.getUTCMonth() === month - 1 &&
40
+ parsed.getUTCDate() === day
41
+ }
35
42
 
36
- export const OffsetTimeStringSchema = brandString(
37
- /^\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/,
38
- "OffsetTimeString"
39
- )
43
+ export const localTimePattern = /^(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?$/
40
44
 
41
- export const LocalDateTimeStringSchema = brandString(
42
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/,
43
- "LocalDateTimeString"
44
- )
45
+ export const isValidLocalTimeString = (value: string): boolean => {
46
+ const match = localTimePattern.exec(value)
47
+ if (match === null) {
48
+ return false
49
+ }
50
+ const hour = Number(match[1])
51
+ const minute = Number(match[2])
52
+ const second = Number(match[3])
53
+ return hour >= 0 && hour <= 23 &&
54
+ minute >= 0 && minute <= 59 &&
55
+ second >= 0 && second <= 59
56
+ }
45
57
 
46
- export const InstantStringSchema = brandString(
47
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/,
48
- "InstantString"
49
- )
58
+ const offsetPattern = /^(?:Z|[+-](\d{2}):(\d{2}))$/
59
+
60
+ const isValidOffset = (value: string): boolean => {
61
+ const match = offsetPattern.exec(value)
62
+ if (match === null) {
63
+ return false
64
+ }
65
+ if (value === "Z") {
66
+ return true
67
+ }
68
+ const hour = Number(match[1])
69
+ const minute = Number(match[2])
70
+ return hour >= 0 && hour <= 23 &&
71
+ minute >= 0 && minute <= 59
72
+ }
73
+
74
+ export const offsetTimePattern = /^(\d{2}:\d{2}:\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:\d{2})$/
75
+
76
+ export const isValidOffsetTimeString = (value: string): boolean => {
77
+ const match = offsetTimePattern.exec(value)
78
+ return match !== null &&
79
+ isValidLocalTimeString(match[1]!) &&
80
+ isValidOffset(match[2]!)
81
+ }
82
+
83
+ export const localDateTimePattern = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/
84
+
85
+ export const isValidLocalDateTimeString = (value: string): boolean => {
86
+ const match = localDateTimePattern.exec(value)
87
+ return match !== null &&
88
+ isValidLocalDateString(match[1]!) &&
89
+ isValidLocalTimeString(match[2]!)
90
+ }
91
+
92
+ export const instantPattern = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:\d{2})$/
93
+
94
+ export const isValidInstantString = (value: string): boolean => {
95
+ const match = instantPattern.exec(value)
96
+ return match !== null &&
97
+ isValidLocalDateString(match[1]!) &&
98
+ isValidLocalTimeString(match[2]!) &&
99
+ isValidOffset(match[3]!)
100
+ }
101
+
102
+ export const LocalDateStringSchema = Schema.String.pipe(
103
+ Schema.pattern(localDatePattern),
104
+ Schema.filter(isValidLocalDateString),
105
+ Schema.brand("LocalDateString")
106
+ ) as unknown as Schema.Schema<LocalDateString>
107
+
108
+ export const LocalTimeStringSchema = Schema.String.pipe(
109
+ Schema.pattern(localTimePattern),
110
+ Schema.filter(isValidLocalTimeString),
111
+ Schema.brand("LocalTimeString")
112
+ ) as unknown as Schema.Schema<LocalTimeString>
113
+
114
+ export const OffsetTimeStringSchema = Schema.String.pipe(
115
+ Schema.pattern(offsetTimePattern),
116
+ Schema.filter(isValidOffsetTimeString),
117
+ Schema.brand("OffsetTimeString")
118
+ ) as unknown as Schema.Schema<OffsetTimeString>
119
+
120
+ export const LocalDateTimeStringSchema = Schema.String.pipe(
121
+ Schema.pattern(localDateTimePattern),
122
+ Schema.filter(isValidLocalDateTimeString),
123
+ Schema.brand("LocalDateTimeString")
124
+ ) as unknown as Schema.Schema<LocalDateTimeString>
125
+
126
+ export const InstantStringSchema = Schema.String.pipe(
127
+ Schema.pattern(instantPattern),
128
+ Schema.filter(isValidInstantString),
129
+ Schema.brand("InstantString")
130
+ ) as unknown as Schema.Schema<InstantString>
50
131
 
51
132
  export const YearStringSchema = brandString(
52
133
  /^\d{4}$/,
53
134
  "YearString"
54
135
  )
55
136
 
56
- export const BigIntStringSchema = brandString(
57
- /^-?\d+$/,
58
- "BigIntString"
59
- )
137
+ export const canonicalizeBigIntString = (input: string): string => {
138
+ const trimmed = input.trim()
139
+ if (!/^-?\d+$/.test(trimmed)) {
140
+ throw new Error("Expected an integer-like bigint value")
141
+ }
142
+ return BigInt(trimmed).toString()
143
+ }
60
144
 
61
- export const DecimalStringSchema = brandString(
62
- /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/,
63
- "DecimalString"
64
- )
145
+ export const isCanonicalBigIntString = (value: string): boolean => {
146
+ try {
147
+ return canonicalizeBigIntString(value) === value
148
+ } catch {
149
+ return false
150
+ }
151
+ }
152
+
153
+ export const canonicalizeDecimalString = (input: string): string => {
154
+ const trimmed = input.trim()
155
+ const match = /^([+-]?)(\d+)(?:\.(\d+))?$/.exec(trimmed)
156
+ if (match === null) {
157
+ throw new Error("Expected a decimal string")
158
+ }
159
+ const sign = match[1] === "-" ? "-" : ""
160
+ const integer = match[2]!.replace(/^0+(?=\d)/, "") || "0"
161
+ const fraction = (match[3] ?? "").replace(/0+$/, "")
162
+ if (fraction.length === 0) {
163
+ if (integer === "0") {
164
+ return "0"
165
+ }
166
+ return `${sign}${integer}`
167
+ }
168
+ return `${sign}${integer}.${fraction}`
169
+ }
170
+
171
+ export const isCanonicalDecimalString = (value: string): boolean => {
172
+ try {
173
+ return canonicalizeDecimalString(value) === value
174
+ } catch {
175
+ return false
176
+ }
177
+ }
178
+
179
+ export const BigIntStringSchema = Schema.String.pipe(
180
+ Schema.filter(isCanonicalBigIntString),
181
+ Schema.brand("BigIntString")
182
+ ) as unknown as Schema.Schema<BigIntString>
183
+
184
+ export const DecimalStringSchema = Schema.String.pipe(
185
+ Schema.filter(isCanonicalDecimalString),
186
+ Schema.brand("DecimalString")
187
+ ) as unknown as Schema.Schema<DecimalString>
65
188
 
66
189
  export const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
67
190
  Schema.Union(
68
191
  Schema.String,
69
- Schema.Number,
192
+ Schema.Number.pipe(Schema.finite()),
70
193
  Schema.Boolean,
71
194
  Schema.Null,
72
195
  Schema.Array(JsonValueSchema),
@@ -79,7 +202,7 @@ export const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
79
202
 
80
203
  export const JsonPrimitiveSchema: Schema.Schema<JsonPrimitive> = Schema.Union(
81
204
  Schema.String,
82
- Schema.Number,
205
+ Schema.Number.pipe(Schema.finite()),
83
206
  Schema.Boolean,
84
207
  Schema.Null
85
208
  )
@@ -15,6 +15,30 @@ export type DdlExpressionLike = AnyExpression | AnySchemaExpression
15
15
 
16
16
  export type ReferentialAction = "noAction" | "restrict" | "cascade" | "setNull" | "setDefault"
17
17
 
18
+ const referentialActionError = "Foreign key action must be noAction, restrict, cascade, setNull, or setDefault"
19
+
20
+ export const renderReferentialAction = (action: unknown): string => {
21
+ switch (action) {
22
+ case "noAction":
23
+ return "no action"
24
+ case "restrict":
25
+ return "restrict"
26
+ case "cascade":
27
+ return "cascade"
28
+ case "setNull":
29
+ return "set null"
30
+ case "setDefault":
31
+ return "set default"
32
+ }
33
+ throw new Error(referentialActionError)
34
+ }
35
+
36
+ const validateReferentialAction = (action: unknown): void => {
37
+ if (action !== undefined) {
38
+ renderReferentialAction(action)
39
+ }
40
+ }
41
+
18
42
  export type IndexKeySpec =
19
43
  | {
20
44
  readonly kind: "column"
@@ -107,6 +131,18 @@ type TupleFromColumns<Columns> = Columns extends readonly [infer Head extends st
107
131
  ? readonly [Columns]
108
132
  : never
109
133
 
134
+ export type NonEmptyColumnInput<Columns extends string | readonly string[]> =
135
+ TupleFromColumns<Columns> extends never ? never : Columns
136
+
137
+ export type MatchingColumnArityInput<
138
+ Left extends string | readonly string[],
139
+ Right extends string | readonly string[]
140
+ > = TupleFromColumns<Left>["length"] extends TupleFromColumns<Right>["length"]
141
+ ? TupleFromColumns<Right>["length"] extends TupleFromColumns<Left>["length"]
142
+ ? unknown
143
+ : never
144
+ : never
145
+
110
146
  type AssertKnownColumns<Fields extends TableFieldMap, Columns extends readonly string[]> = Exclude<
111
147
  Columns[number],
112
148
  ColumnNameUnion<Fields>
@@ -114,6 +150,47 @@ type AssertKnownColumns<Fields extends TableFieldMap, Columns extends readonly s
114
150
  ? Columns
115
151
  : never
116
152
 
153
+ type IndexKeyColumnNames<Keys> = Keys extends readonly (infer Key)[]
154
+ ? Key extends { readonly kind: "column"; readonly column: infer Column extends string }
155
+ ? Column
156
+ : never
157
+ : never
158
+
159
+ type IndexOptionColumnNames<Spec> =
160
+ | (Spec extends { readonly columns: infer Columns extends readonly string[] } ? Columns[number] : never)
161
+ | (Spec extends { readonly include: infer Include extends readonly string[] } ? Include[number] : never)
162
+ | (Spec extends { readonly keys: infer Keys } ? IndexKeyColumnNames<Keys> : never)
163
+
164
+ type ForeignKeyReferencedColumnNames<Spec> = Spec extends { readonly references: () => infer Reference }
165
+ ? Reference extends { readonly columns: infer Columns extends readonly string[] }
166
+ ? Columns[number]
167
+ : never
168
+ : never
169
+
170
+ type ForeignKeyKnownReferencedColumnNames<Spec> = Spec extends { readonly references: () => infer Reference }
171
+ ? Reference extends { readonly knownColumns: infer KnownColumns extends readonly string[] }
172
+ ? KnownColumns[number]
173
+ : string
174
+ : string
175
+
176
+ type AssertKnownColumnNames<Fields extends TableFieldMap, Columns extends string> = [Columns] extends [never]
177
+ ? true
178
+ : string extends Columns
179
+ ? true
180
+ : Exclude<Columns, ColumnNameUnion<Fields>> extends never
181
+ ? true
182
+ : false
183
+
184
+ type AssertKnownReferenceColumnNames<KnownColumns extends string, Columns extends string> = [Columns] extends [never]
185
+ ? true
186
+ : string extends Columns
187
+ ? true
188
+ : string extends KnownColumns
189
+ ? true
190
+ : Exclude<Columns, KnownColumns> extends never
191
+ ? true
192
+ : false
193
+
117
194
  type AssertPrimaryKeyColumns<
118
195
  Fields extends TableFieldMap,
119
196
  Columns extends readonly string[]
@@ -159,6 +236,8 @@ export const collectInlineOptions = <Fields extends TableFieldMap>(
159
236
  })
160
237
  }
161
238
  if (column.metadata.references) {
239
+ validateReferentialAction(column.metadata.references.onUpdate)
240
+ validateReferentialAction(column.metadata.references.onDelete)
162
241
  const local = [columnName] as ColumnList
163
242
  options.push({
164
243
  kind: "foreignKey",
@@ -254,6 +333,8 @@ export const validateOptions = <Fields extends TableFieldMap>(
254
333
  }
255
334
  }
256
335
  if (option.kind === "foreignKey") {
336
+ validateReferentialAction(option.onUpdate)
337
+ validateReferentialAction(option.onDelete)
257
338
  const reference = option.references()
258
339
  if (reference.columns.length !== columns.length) {
259
340
  throw new Error(`Foreign key on table '${tableName}' must reference the same number of columns`)
@@ -278,7 +359,7 @@ export const validateOptions = <Fields extends TableFieldMap>(
278
359
  throw new Error(`Unknown index key column '${key.column}' on table '${tableName}'`)
279
360
  }
280
361
  }
281
- if (option.columns === undefined && (option.keys === undefined || option.keys.length === 0)) {
362
+ if (columns.length === 0 && (option.keys === undefined || option.keys.length === 0)) {
282
363
  throw new Error(`Index on table '${tableName}' requires at least one column or key`)
283
364
  }
284
365
  }
@@ -308,5 +389,31 @@ export type ValidatePrimaryKeyColumns<
308
389
  Columns extends readonly string[]
309
390
  > = AssertPrimaryKeyColumns<Fields, AssertKnownColumns<Fields, Columns>>
310
391
 
392
+ /** Compile-time validation that index columns, included columns, and column keys exist on the table. */
393
+ export type ValidateIndexOptionColumns<
394
+ Fields extends TableFieldMap,
395
+ Spec
396
+ > = AssertKnownColumnNames<Fields, IndexOptionColumnNames<Spec>> extends true ? Spec : never
397
+
398
+ /** Compile-time validation that foreign keys reference known local and target columns. */
399
+ export type ValidateForeignKeyOptionColumns<
400
+ Fields extends TableFieldMap,
401
+ Spec
402
+ > = Spec extends { readonly columns: infer Columns extends readonly string[] }
403
+ ? AssertKnownColumns<Fields, Columns> extends never
404
+ ? never
405
+ : AssertKnownReferenceColumnNames<
406
+ ForeignKeyKnownReferencedColumnNames<Spec>,
407
+ ForeignKeyReferencedColumnNames<Spec>
408
+ > extends true
409
+ ? Spec
410
+ : never
411
+ : AssertKnownReferenceColumnNames<
412
+ ForeignKeyKnownReferencedColumnNames<Spec>,
413
+ ForeignKeyReferencedColumnNames<Spec>
414
+ > extends true
415
+ ? Spec
416
+ : never
417
+
311
418
  /** Normalizes a public column input into the internal tuple form. */
312
419
  export type NormalizeColumns<Columns extends string | readonly string[]> = TupleFromColumns<Columns>