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.
- package/dist/mysql.js +1661 -591
- package/dist/postgres/metadata.js +1930 -135
- package/dist/postgres.js +7808 -6718
- package/dist/sqlite.js +8360 -0
- package/package.json +6 -1
- package/src/internal/derived-table.ts +29 -3
- package/src/internal/dialect.ts +2 -0
- package/src/internal/dsl-mutation-runtime.ts +173 -4
- package/src/internal/dsl-plan-runtime.ts +165 -20
- package/src/internal/dsl-query-runtime.ts +60 -6
- package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
- package/src/internal/executor.ts +47 -9
- package/src/internal/expression-ast.ts +3 -2
- package/src/internal/grouping-key.ts +141 -1
- package/src/internal/implication-runtime.ts +2 -1
- package/src/internal/json/types.ts +155 -40
- package/src/internal/predicate/context.ts +14 -1
- package/src/internal/predicate/key.ts +19 -2
- package/src/internal/predicate/runtime.ts +27 -3
- package/src/internal/query.ts +252 -30
- package/src/internal/renderer.ts +35 -2
- package/src/internal/runtime/driver-value-mapping.ts +58 -0
- package/src/internal/runtime/normalize.ts +62 -38
- package/src/internal/runtime/schema.ts +5 -3
- package/src/internal/runtime/value.ts +153 -30
- package/src/internal/table-options.ts +108 -1
- package/src/internal/table.ts +87 -29
- package/src/mysql/column.ts +18 -2
- package/src/mysql/datatypes/index.ts +21 -0
- package/src/mysql/errors/catalog.ts +5 -5
- package/src/mysql/errors/normalize.ts +2 -2
- package/src/mysql/internal/dsl.ts +736 -218
- package/src/mysql/internal/renderer.ts +2 -1
- package/src/mysql/internal/sql-expression-renderer.ts +486 -130
- package/src/mysql/query.ts +9 -2
- package/src/mysql/table.ts +38 -12
- package/src/postgres/column.ts +4 -2
- package/src/postgres/errors/normalize.ts +2 -2
- package/src/postgres/executor.ts +48 -5
- package/src/postgres/function/core.ts +19 -1
- package/src/postgres/internal/dsl.ts +683 -240
- package/src/postgres/internal/renderer.ts +2 -1
- package/src/postgres/internal/schema-ddl.ts +2 -1
- package/src/postgres/internal/schema-model.ts +6 -3
- package/src/postgres/internal/sql-expression-renderer.ts +420 -91
- package/src/postgres/json.ts +57 -17
- package/src/postgres/query.ts +9 -2
- package/src/postgres/schema-management.ts +91 -4
- package/src/postgres/schema.ts +1 -1
- package/src/postgres/table.ts +189 -53
- package/src/sqlite/column.ts +128 -0
- package/src/sqlite/datatypes/index.ts +79 -0
- package/src/sqlite/datatypes/spec.ts +98 -0
- package/src/sqlite/errors/catalog.ts +103 -0
- package/src/sqlite/errors/fields.ts +19 -0
- package/src/sqlite/errors/index.ts +19 -0
- package/src/sqlite/errors/normalize.ts +229 -0
- package/src/sqlite/errors/requirements.ts +71 -0
- package/src/sqlite/errors/types.ts +29 -0
- package/src/sqlite/executor.ts +227 -0
- package/src/sqlite/function/aggregate.ts +2 -0
- package/src/sqlite/function/core.ts +2 -0
- package/src/sqlite/function/index.ts +19 -0
- package/src/sqlite/function/string.ts +2 -0
- package/src/sqlite/function/temporal.ts +100 -0
- package/src/sqlite/function/window.ts +2 -0
- package/src/sqlite/internal/dialect.ts +37 -0
- package/src/sqlite/internal/dsl.ts +6926 -0
- package/src/sqlite/internal/renderer.ts +47 -0
- package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
- package/src/sqlite/json.ts +2 -0
- package/src/sqlite/query.ts +196 -0
- package/src/sqlite/renderer.ts +24 -0
- package/src/sqlite/table.ts +183 -0
- 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"
|
|
42
|
-
const
|
|
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"
|
|
85
|
-
return
|
|
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 (
|
|
131
|
+
if (isValidLocalDateString(raw)) {
|
|
125
132
|
return raw
|
|
126
133
|
}
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
162
|
-
|
|
171
|
+
const canonicalLocalDateTime = raw.replace(" ", "T")
|
|
172
|
+
if (isValidLocalDateTimeString(canonicalLocalDateTime)) {
|
|
173
|
+
return canonicalLocalDateTime
|
|
163
174
|
}
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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 (
|
|
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>
|