effect-qb 0.12.3

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 (81) hide show
  1. package/README.md +1294 -0
  2. package/dist/mysql.js +57575 -0
  3. package/dist/postgres.js +6303 -0
  4. package/package.json +42 -0
  5. package/src/internal/aggregation-validation.ts +57 -0
  6. package/src/internal/case-analysis.ts +50 -0
  7. package/src/internal/coercion-analysis.ts +30 -0
  8. package/src/internal/coercion-errors.ts +29 -0
  9. package/src/internal/coercion-kind.ts +32 -0
  10. package/src/internal/coercion-normalize.ts +7 -0
  11. package/src/internal/coercion-rules.ts +25 -0
  12. package/src/internal/column-state.ts +453 -0
  13. package/src/internal/column.ts +417 -0
  14. package/src/internal/datatypes/define.ts +44 -0
  15. package/src/internal/datatypes/lookup.ts +280 -0
  16. package/src/internal/datatypes/shape.ts +72 -0
  17. package/src/internal/derived-table.ts +149 -0
  18. package/src/internal/dialect.ts +30 -0
  19. package/src/internal/executor.ts +390 -0
  20. package/src/internal/expression-ast.ts +349 -0
  21. package/src/internal/expression.ts +325 -0
  22. package/src/internal/grouping-key.ts +82 -0
  23. package/src/internal/json/ast.ts +63 -0
  24. package/src/internal/json/errors.ts +13 -0
  25. package/src/internal/json/path.ts +227 -0
  26. package/src/internal/json/shape.ts +1 -0
  27. package/src/internal/json/types.ts +386 -0
  28. package/src/internal/mysql-dialect.ts +39 -0
  29. package/src/internal/mysql-renderer.ts +37 -0
  30. package/src/internal/plan.ts +64 -0
  31. package/src/internal/postgres-dialect.ts +34 -0
  32. package/src/internal/postgres-renderer.ts +40 -0
  33. package/src/internal/predicate-analysis.ts +71 -0
  34. package/src/internal/predicate-atom.ts +43 -0
  35. package/src/internal/predicate-branches.ts +40 -0
  36. package/src/internal/predicate-context.ts +279 -0
  37. package/src/internal/predicate-formula.ts +100 -0
  38. package/src/internal/predicate-key.ts +28 -0
  39. package/src/internal/predicate-nnf.ts +12 -0
  40. package/src/internal/predicate-normalize.ts +202 -0
  41. package/src/internal/projection-alias.ts +15 -0
  42. package/src/internal/projections.ts +101 -0
  43. package/src/internal/query-ast.ts +297 -0
  44. package/src/internal/query-factory.ts +6757 -0
  45. package/src/internal/query-requirements.ts +40 -0
  46. package/src/internal/query.ts +1590 -0
  47. package/src/internal/renderer.ts +102 -0
  48. package/src/internal/runtime-normalize.ts +344 -0
  49. package/src/internal/runtime-schema.ts +428 -0
  50. package/src/internal/runtime-value.ts +85 -0
  51. package/src/internal/schema-derivation.ts +131 -0
  52. package/src/internal/sql-expression-renderer.ts +1353 -0
  53. package/src/internal/table-options.ts +225 -0
  54. package/src/internal/table.ts +674 -0
  55. package/src/mysql/column.ts +30 -0
  56. package/src/mysql/datatypes/index.ts +6 -0
  57. package/src/mysql/datatypes/spec.ts +180 -0
  58. package/src/mysql/errors/catalog.ts +51662 -0
  59. package/src/mysql/errors/fields.ts +21 -0
  60. package/src/mysql/errors/index.ts +18 -0
  61. package/src/mysql/errors/normalize.ts +232 -0
  62. package/src/mysql/errors/requirements.ts +73 -0
  63. package/src/mysql/executor.ts +134 -0
  64. package/src/mysql/query.ts +189 -0
  65. package/src/mysql/renderer.ts +19 -0
  66. package/src/mysql/table.ts +157 -0
  67. package/src/mysql.ts +18 -0
  68. package/src/postgres/column.ts +20 -0
  69. package/src/postgres/datatypes/index.ts +8 -0
  70. package/src/postgres/datatypes/spec.ts +264 -0
  71. package/src/postgres/errors/catalog.ts +452 -0
  72. package/src/postgres/errors/fields.ts +48 -0
  73. package/src/postgres/errors/index.ts +4 -0
  74. package/src/postgres/errors/normalize.ts +209 -0
  75. package/src/postgres/errors/requirements.ts +65 -0
  76. package/src/postgres/errors/types.ts +38 -0
  77. package/src/postgres/executor.ts +131 -0
  78. package/src/postgres/query.ts +189 -0
  79. package/src/postgres/renderer.ts +29 -0
  80. package/src/postgres/table.ts +157 -0
  81. package/src/postgres.ts +18 -0
@@ -0,0 +1,428 @@
1
+ import * as Schema from "effect/Schema"
2
+ import * as SchemaAST from "effect/SchemaAST"
3
+
4
+ import * as Expression from "./expression.js"
5
+ import * as ExpressionAst from "./expression-ast.js"
6
+ import * as Query from "./query.js"
7
+ import * as JsonPath from "./json/path.js"
8
+ import { flattenSelection } from "./projections.js"
9
+ import {
10
+ BigIntStringSchema,
11
+ DecimalStringSchema,
12
+ InstantStringSchema,
13
+ JsonValueSchema,
14
+ LocalDateStringSchema,
15
+ LocalDateTimeStringSchema,
16
+ LocalTimeStringSchema,
17
+ OffsetTimeStringSchema,
18
+ YearStringSchema
19
+ } from "./runtime-value.js"
20
+ import { mysqlDatatypeKinds } from "../mysql/datatypes/spec.js"
21
+ import { postgresDatatypeKinds } from "../postgres/datatypes/spec.js"
22
+ import type { RuntimeTag } from "./datatypes/shape.js"
23
+
24
+ export type RuntimeSchema = Schema.Schema<any, any, any>
25
+
26
+ const schemaCache = new WeakMap<Expression.Any, RuntimeSchema | undefined>()
27
+
28
+ const stripParameterizedKind = (kind: string): string => {
29
+ const openParen = kind.indexOf("(")
30
+ return openParen === -1 ? kind : kind.slice(0, openParen)
31
+ }
32
+
33
+ const stripArrayKind = (kind: string): string => {
34
+ let current = kind
35
+ while (current.endsWith("[]")) {
36
+ current = current.slice(0, -2)
37
+ }
38
+ return current
39
+ }
40
+
41
+ const baseKind = (kind: string): string => stripArrayKind(stripParameterizedKind(kind))
42
+
43
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
44
+ typeof value === "object" && value !== null && !Array.isArray(value)
45
+
46
+ const runtimeSchemaForTag = (tag: RuntimeTag): RuntimeSchema | undefined => {
47
+ switch (tag) {
48
+ case "string":
49
+ return Schema.String
50
+ case "number":
51
+ return Schema.Number
52
+ case "bigintString":
53
+ return BigIntStringSchema
54
+ case "boolean":
55
+ return Schema.Boolean
56
+ case "json":
57
+ return JsonValueSchema
58
+ case "localDate":
59
+ return LocalDateStringSchema
60
+ case "localTime":
61
+ return LocalTimeStringSchema
62
+ case "offsetTime":
63
+ return OffsetTimeStringSchema
64
+ case "localDateTime":
65
+ return LocalDateTimeStringSchema
66
+ case "instant":
67
+ return InstantStringSchema
68
+ case "year":
69
+ return YearStringSchema
70
+ case "decimalString":
71
+ return DecimalStringSchema
72
+ case "bytes":
73
+ return Schema.Uint8ArrayFromSelf
74
+ case "array":
75
+ return Schema.Array(Schema.Unknown)
76
+ case "record":
77
+ return Schema.Record({
78
+ key: Schema.String,
79
+ value: Schema.Unknown
80
+ })
81
+ case "null":
82
+ return Schema.Null
83
+ case "unknown":
84
+ return undefined
85
+ }
86
+ }
87
+
88
+ const runtimeTagOfBaseDbType = (
89
+ dialect: string,
90
+ kind: string
91
+ ): RuntimeTag | undefined => {
92
+ const normalizedKind = baseKind(kind)
93
+ if (dialect === "postgres") {
94
+ return postgresDatatypeKinds[normalizedKind as keyof typeof postgresDatatypeKinds]?.runtime
95
+ }
96
+ if (dialect === "mysql") {
97
+ return mysqlDatatypeKinds[normalizedKind as keyof typeof mysqlDatatypeKinds]?.runtime
98
+ }
99
+ return undefined
100
+ }
101
+
102
+ export const runtimeSchemaForDbType = (
103
+ dbType: Expression.DbType.Any
104
+ ): RuntimeSchema | undefined => {
105
+ if ("base" in dbType) {
106
+ return runtimeSchemaForDbType(dbType.base)
107
+ }
108
+ if ("element" in dbType) {
109
+ return Schema.Array(runtimeSchemaForDbType(dbType.element) ?? Schema.Unknown)
110
+ }
111
+ if ("fields" in dbType) {
112
+ const fields = Object.fromEntries(
113
+ Object.entries(dbType.fields).map(([key, field]) => [key, runtimeSchemaForDbType(field) ?? Schema.Unknown])
114
+ )
115
+ return Schema.Struct(fields as Record<string, RuntimeSchema>)
116
+ }
117
+ if ("variant" in dbType && dbType.variant === "json") {
118
+ return JsonValueSchema
119
+ }
120
+ if ("variant" in dbType && (dbType.variant === "enum" || dbType.variant === "set")) {
121
+ return Schema.String
122
+ }
123
+ const runtimeTag = runtimeTagOfBaseDbType(dbType.dialect, dbType.kind)
124
+ return runtimeTag === undefined ? undefined : runtimeSchemaForTag(runtimeTag)
125
+ }
126
+
127
+ const makeSchemaFromAst = (ast: SchemaAST.AST): RuntimeSchema =>
128
+ Schema.make(ast)
129
+
130
+ const unionAst = (asts: ReadonlyArray<SchemaAST.AST>): SchemaAST.AST | undefined => {
131
+ if (asts.length === 0) {
132
+ return undefined
133
+ }
134
+ if (asts.length === 1) {
135
+ return asts[0]
136
+ }
137
+ return SchemaAST.Union.make(asts)
138
+ }
139
+
140
+ const propertyAstOf = (
141
+ ast: SchemaAST.AST,
142
+ key: string
143
+ ): SchemaAST.AST | undefined => {
144
+ switch (ast._tag) {
145
+ case "Transformation":
146
+ return propertyAstOf(SchemaAST.typeAST(ast), key)
147
+ case "Refinement":
148
+ return propertyAstOf(ast.from, key)
149
+ case "Suspend":
150
+ return propertyAstOf(ast.f(), key)
151
+ case "TypeLiteral": {
152
+ const property = ast.propertySignatures.find((entry) => entry.name === key)
153
+ if (property !== undefined) {
154
+ return property.type
155
+ }
156
+ const index = ast.indexSignatures.find((entry) => entry.parameter._tag === "StringKeyword")
157
+ return index?.type
158
+ }
159
+ case "Union": {
160
+ const values = ast.types.flatMap((member) => {
161
+ const next = propertyAstOf(member, key)
162
+ return next === undefined ? [] : [next]
163
+ })
164
+ return unionAst(values)
165
+ }
166
+ default:
167
+ return undefined
168
+ }
169
+ }
170
+
171
+ const numberAstOf = (
172
+ ast: SchemaAST.AST,
173
+ index: number
174
+ ): SchemaAST.AST | undefined => {
175
+ switch (ast._tag) {
176
+ case "Transformation":
177
+ return numberAstOf(SchemaAST.typeAST(ast), index)
178
+ case "Refinement":
179
+ return numberAstOf(ast.from, index)
180
+ case "Suspend":
181
+ return numberAstOf(ast.f(), index)
182
+ case "TupleType": {
183
+ const element = ast.elements[index]
184
+ if (element !== undefined) {
185
+ return element.type
186
+ }
187
+ if (ast.rest.length === 0) {
188
+ return undefined
189
+ }
190
+ return unionAst(ast.rest.map((entry) => entry.type))
191
+ }
192
+ case "Union": {
193
+ const values = ast.types.flatMap((member) => {
194
+ const next = numberAstOf(member, index)
195
+ return next === undefined ? [] : [next]
196
+ })
197
+ return unionAst(values)
198
+ }
199
+ default:
200
+ return undefined
201
+ }
202
+ }
203
+
204
+ const exactJsonSegments = (
205
+ segments: readonly JsonPath.CanonicalSegment[]
206
+ ): segments is readonly (JsonPath.KeySegment | JsonPath.IndexSegment)[] =>
207
+ segments.every((segment) => segment.kind === "key" || segment.kind === "index")
208
+
209
+ const schemaAstAtExactJsonPath = (
210
+ schema: RuntimeSchema,
211
+ segments: readonly JsonPath.CanonicalSegment[]
212
+ ): SchemaAST.AST | undefined => {
213
+ let current: SchemaAST.AST = SchemaAST.typeAST(schema.ast)
214
+ for (const segment of segments) {
215
+ if (segment.kind === "key") {
216
+ const property = propertyAstOf(current, segment.key)
217
+ if (property === undefined) {
218
+ return undefined
219
+ }
220
+ current = property
221
+ continue
222
+ }
223
+ if (segment.kind === "index") {
224
+ const next = numberAstOf(current, segment.index)
225
+ if (next === undefined) {
226
+ return undefined
227
+ }
228
+ current = next
229
+ continue
230
+ }
231
+ return undefined
232
+ }
233
+ return current
234
+ }
235
+
236
+ const unionSchemas = (schemas: ReadonlyArray<RuntimeSchema | undefined>): RuntimeSchema | undefined => {
237
+ const resolved = schemas.filter((schema): schema is RuntimeSchema => schema !== undefined)
238
+ if (resolved.length === 0) {
239
+ return undefined
240
+ }
241
+ if (resolved.length === 1) {
242
+ return resolved[0]
243
+ }
244
+ return Schema.Union(...resolved)
245
+ }
246
+
247
+ const firstSelectedExpression = (
248
+ plan: Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>
249
+ ): Expression.Any | undefined => {
250
+ const selection = Query.getAst(plan).select
251
+ return flattenSelection(selection as Record<string, unknown>)[0]?.expression
252
+ }
253
+
254
+ const isJsonCompatibleAst = (ast: SchemaAST.AST): boolean => {
255
+ switch (ast._tag) {
256
+ case "StringKeyword":
257
+ case "NumberKeyword":
258
+ case "BooleanKeyword":
259
+ case "TupleType":
260
+ case "TypeLiteral":
261
+ return true
262
+ case "Literal":
263
+ return ast.literal === null ||
264
+ typeof ast.literal === "string" ||
265
+ typeof ast.literal === "number" ||
266
+ typeof ast.literal === "boolean"
267
+ case "Union":
268
+ return ast.types.every(isJsonCompatibleAst)
269
+ case "Transformation":
270
+ return isJsonCompatibleAst(SchemaAST.typeAST(ast))
271
+ case "Suspend":
272
+ return isJsonCompatibleAst(ast.f())
273
+ default:
274
+ return false
275
+ }
276
+ }
277
+
278
+ const jsonCompatibleSchema = (schema: RuntimeSchema | undefined): RuntimeSchema | undefined => {
279
+ if (schema === undefined) {
280
+ return undefined
281
+ }
282
+ const ast = SchemaAST.typeAST(schema.ast)
283
+ return isJsonCompatibleAst(ast) ? schema : JsonValueSchema
284
+ }
285
+
286
+ const buildStructSchema = (
287
+ entries: readonly { readonly key: string; readonly value: Expression.Any }[]
288
+ ): RuntimeSchema => {
289
+ const fields = Object.fromEntries(
290
+ entries.map((entry) => [entry.key, expressionRuntimeSchema(entry.value) ?? JsonValueSchema])
291
+ )
292
+ return Schema.Struct(fields as Record<string, RuntimeSchema>)
293
+ }
294
+
295
+ const buildTupleSchema = (values: readonly Expression.Any[]): RuntimeSchema =>
296
+ Schema.Tuple(...values.map((value) => expressionRuntimeSchema(value) ?? JsonValueSchema))
297
+
298
+ const deriveRuntimeSchema = (expression: Expression.Any): RuntimeSchema | undefined => {
299
+ const state = expression[Expression.TypeId]
300
+ if (state.runtimeSchema !== undefined) {
301
+ return state.runtimeSchema
302
+ }
303
+ const ast = (expression as Expression.Any & {
304
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
305
+ })[ExpressionAst.TypeId]
306
+ switch (ast.kind) {
307
+ case "column":
308
+ case "excluded":
309
+ return state.runtimeSchema
310
+ case "literal":
311
+ if (ast.value === null) {
312
+ return Schema.Null
313
+ }
314
+ if (typeof ast.value === "string" || typeof ast.value === "number" || typeof ast.value === "boolean") {
315
+ return Schema.Literal(ast.value)
316
+ }
317
+ return runtimeSchemaForDbType(state.dbType)
318
+ case "cast":
319
+ return runtimeSchemaForDbType(ast.target)
320
+ case "isNull":
321
+ case "isNotNull":
322
+ case "not":
323
+ case "eq":
324
+ case "neq":
325
+ case "lt":
326
+ case "lte":
327
+ case "gt":
328
+ case "gte":
329
+ case "like":
330
+ case "ilike":
331
+ case "isDistinctFrom":
332
+ case "isNotDistinctFrom":
333
+ case "contains":
334
+ case "containedBy":
335
+ case "overlaps":
336
+ case "and":
337
+ case "or":
338
+ case "in":
339
+ case "notIn":
340
+ case "between":
341
+ case "exists":
342
+ case "inSubquery":
343
+ case "comparisonAny":
344
+ case "comparisonAll":
345
+ case "jsonHasKey":
346
+ case "jsonKeyExists":
347
+ case "jsonHasAnyKeys":
348
+ case "jsonHasAllKeys":
349
+ case "jsonPathExists":
350
+ case "jsonPathMatch":
351
+ return Schema.Boolean
352
+ case "upper":
353
+ case "lower":
354
+ case "concat":
355
+ case "jsonGetText":
356
+ case "jsonPathText":
357
+ case "jsonAccessText":
358
+ case "jsonTraverseText":
359
+ case "jsonTypeOf":
360
+ return Schema.String
361
+ case "count":
362
+ case "jsonLength":
363
+ return Schema.Number
364
+ case "max":
365
+ case "min":
366
+ return expressionRuntimeSchema(ast.value)
367
+ case "case":
368
+ return unionSchemas([
369
+ ...ast.branches.map((branch) => expressionRuntimeSchema(branch.then)),
370
+ expressionRuntimeSchema(ast.else)
371
+ ])
372
+ case "coalesce":
373
+ return unionSchemas(ast.values.map(expressionRuntimeSchema))
374
+ case "scalarSubquery":
375
+ {
376
+ const selection = firstSelectedExpression(ast.plan)
377
+ return selection === undefined ? undefined : expressionRuntimeSchema(selection)
378
+ }
379
+ case "window":
380
+ return ast.function === "over" && ast.value !== undefined
381
+ ? expressionRuntimeSchema(ast.value)
382
+ : Schema.Number
383
+ case "jsonGet":
384
+ case "jsonPath":
385
+ case "jsonAccess":
386
+ case "jsonTraverse": {
387
+ const baseSchema = expressionRuntimeSchema(ast.base!)
388
+ const segments = ast.segments
389
+ if (baseSchema === undefined || segments === undefined || !exactJsonSegments(segments)) {
390
+ return JsonValueSchema
391
+ }
392
+ const subAst = schemaAstAtExactJsonPath(baseSchema, segments)
393
+ return subAst === undefined ? JsonValueSchema : makeSchemaFromAst(subAst)
394
+ }
395
+ case "jsonDelete":
396
+ case "jsonDeletePath":
397
+ case "jsonRemove":
398
+ case "jsonSet":
399
+ case "jsonInsert":
400
+ return expressionRuntimeSchema(ast.base!)
401
+ case "jsonStripNulls":
402
+ return expressionRuntimeSchema(ast.value!)
403
+ case "jsonConcat":
404
+ case "jsonMerge":
405
+ return JsonValueSchema
406
+ case "jsonBuildObject":
407
+ return buildStructSchema(ast.entries ?? [])
408
+ case "jsonBuildArray":
409
+ return buildTupleSchema(ast.values ?? [])
410
+ case "jsonToJson":
411
+ case "jsonToJsonb":
412
+ return jsonCompatibleSchema(expressionRuntimeSchema(ast.value!))
413
+ case "jsonKeys":
414
+ return Schema.Array(Schema.String)
415
+ }
416
+ }
417
+
418
+ export const expressionRuntimeSchema = (
419
+ expression: Expression.Any
420
+ ): RuntimeSchema | undefined => {
421
+ const cached = schemaCache.get(expression)
422
+ if (cached !== undefined || schemaCache.has(expression)) {
423
+ return cached
424
+ }
425
+ const resolved = deriveRuntimeSchema(expression) ?? runtimeSchemaForDbType(expression[Expression.TypeId].dbType)
426
+ schemaCache.set(expression, resolved)
427
+ return resolved
428
+ }
@@ -0,0 +1,85 @@
1
+ import type * as Brand from "effect/Brand"
2
+ import * as Schema from "effect/Schema"
3
+
4
+ import type { JsonPrimitive, JsonValue } from "./json/types.js"
5
+
6
+ export type { JsonPrimitive, JsonValue } from "./json/types.js"
7
+
8
+ export type LocalDateString = string & Brand.Brand<"LocalDateString">
9
+ export type LocalTimeString = string & Brand.Brand<"LocalTimeString">
10
+ export type OffsetTimeString = string & Brand.Brand<"OffsetTimeString">
11
+ export type LocalDateTimeString = string & Brand.Brand<"LocalDateTimeString">
12
+ export type InstantString = string & Brand.Brand<"InstantString">
13
+ export type YearString = string & Brand.Brand<"YearString">
14
+ export type BigIntString = string & Brand.Brand<"BigIntString">
15
+ export type DecimalString = string & Brand.Brand<"DecimalString">
16
+
17
+ const brandString = <BrandName extends string>(
18
+ pattern: RegExp,
19
+ brand: BrandName
20
+ ): Schema.Schema<string & Brand.Brand<BrandName>> =>
21
+ Schema.String.pipe(
22
+ Schema.pattern(pattern),
23
+ Schema.brand(brand)
24
+ ) as unknown as Schema.Schema<string & Brand.Brand<BrandName>>
25
+
26
+ export const LocalDateStringSchema = brandString(
27
+ /^\d{4}-\d{2}-\d{2}$/,
28
+ "LocalDateString"
29
+ )
30
+
31
+ export const LocalTimeStringSchema = brandString(
32
+ /^\d{2}:\d{2}:\d{2}(?:\.\d+)?$/,
33
+ "LocalTimeString"
34
+ )
35
+
36
+ export const OffsetTimeStringSchema = brandString(
37
+ /^\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/,
38
+ "OffsetTimeString"
39
+ )
40
+
41
+ export const LocalDateTimeStringSchema = brandString(
42
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/,
43
+ "LocalDateTimeString"
44
+ )
45
+
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
+ )
50
+
51
+ export const YearStringSchema = brandString(
52
+ /^\d{4}$/,
53
+ "YearString"
54
+ )
55
+
56
+ export const BigIntStringSchema = brandString(
57
+ /^-?\d+$/,
58
+ "BigIntString"
59
+ )
60
+
61
+ export const DecimalStringSchema = brandString(
62
+ /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/,
63
+ "DecimalString"
64
+ )
65
+
66
+ export const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
67
+ Schema.Union(
68
+ Schema.String,
69
+ Schema.Number,
70
+ Schema.Boolean,
71
+ Schema.Null,
72
+ Schema.Array(JsonValueSchema),
73
+ Schema.Record({
74
+ key: Schema.String,
75
+ value: JsonValueSchema
76
+ })
77
+ )
78
+ )
79
+
80
+ export const JsonPrimitiveSchema: Schema.Schema<JsonPrimitive> = Schema.Union(
81
+ Schema.String,
82
+ Schema.Number,
83
+ Schema.Boolean,
84
+ Schema.Null
85
+ )
@@ -0,0 +1,131 @@
1
+ import * as VariantSchema from "@effect/experimental/VariantSchema"
2
+ import * as Schema from "effect/Schema"
3
+
4
+ import {
5
+ type AnyColumnDefinition,
6
+ type HasDefault,
7
+ type InsertType,
8
+ type IsGenerated,
9
+ type IsNullable,
10
+ type SelectType,
11
+ type UpdateType
12
+ } from "./column-state.js"
13
+
14
+ /** Variant-schema helper used to derive select / insert / update schemas. */
15
+ export const TableSchema = VariantSchema.make({
16
+ variants: ["select", "insert", "update"] as const,
17
+ defaultVariant: "select"
18
+ })
19
+
20
+ type Variants = "select" | "insert" | "update"
21
+
22
+ /** Normalized field map used by table definitions. */
23
+ export type TableFieldMap = Record<string, AnyColumnDefinition>
24
+
25
+ type GeneratedKeys<Fields extends TableFieldMap> = {
26
+ [K in keyof Fields]: IsGenerated<Fields[K]> extends true ? K : never
27
+ }[keyof Fields]
28
+
29
+ type OptionalInsertKeys<Fields extends TableFieldMap> = {
30
+ [K in keyof Fields]:
31
+ IsGenerated<Fields[K]> extends true ? never :
32
+ IsNullable<Fields[K]> extends true ? K :
33
+ HasDefault<Fields[K]> extends true ? K :
34
+ never
35
+ }[keyof Fields]
36
+
37
+ type RequiredInsertKeys<Fields extends TableFieldMap> = Exclude<keyof Fields, GeneratedKeys<Fields> | OptionalInsertKeys<Fields>>
38
+
39
+ type UpdateKeys<Fields extends TableFieldMap, PrimaryKey extends keyof Fields> = Exclude<
40
+ keyof Fields,
41
+ GeneratedKeys<Fields> | PrimaryKey
42
+ >
43
+
44
+ type Simplify<T> = { [K in keyof T]: T[K] } & {}
45
+
46
+ /** Row shape returned by selecting from a table. */
47
+ export type SelectRow<Fields extends TableFieldMap> = Simplify<{
48
+ [K in keyof Fields]: SelectType<Fields[K]>
49
+ }>
50
+
51
+ /** Insert payload derived from a table field map. */
52
+ export type InsertRow<Fields extends TableFieldMap> = Simplify<
53
+ { [K in RequiredInsertKeys<Fields>]: InsertType<Fields[K]> } &
54
+ { [K in OptionalInsertKeys<Fields>]?: InsertType<Fields[K]> }
55
+ >
56
+
57
+ /** Update payload derived from a table field map and primary key. */
58
+ export type UpdateRow<Fields extends TableFieldMap, PrimaryKey extends keyof Fields> = Simplify<
59
+ Partial<{
60
+ [K in UpdateKeys<Fields, PrimaryKey>]: UpdateType<Fields[K]>
61
+ }>
62
+ >
63
+
64
+ const selectSchema = (column: AnyColumnDefinition): Schema.Schema.Any =>
65
+ column.metadata.nullable ? Schema.NullOr(column.schema) : column.schema
66
+
67
+ const insertSchema = (column: AnyColumnDefinition): any | undefined => {
68
+ if (column.metadata.generated) {
69
+ return undefined
70
+ }
71
+ const base = column.metadata.nullable ? Schema.NullOr(column.schema) : column.schema
72
+ return column.metadata.nullable || column.metadata.hasDefault ? Schema.optional(base) : base
73
+ }
74
+
75
+ const updateSchema = (
76
+ column: AnyColumnDefinition,
77
+ isPrimaryKey: boolean
78
+ ): any | undefined => {
79
+ if (column.metadata.generated || isPrimaryKey) {
80
+ return undefined
81
+ }
82
+ const base = column.metadata.nullable ? Schema.NullOr(column.schema) : column.schema
83
+ return Schema.optional(base)
84
+ }
85
+
86
+ /**
87
+ * Derives the `select`, `insert`, and `update` schemas for a table.
88
+ *
89
+ * This is the central place where the column capability flags are turned into
90
+ * real runtime schemas.
91
+ */
92
+ export const deriveSchemas = <
93
+ Fields extends TableFieldMap,
94
+ PrimaryKeyColumns extends keyof Fields & string
95
+ >(
96
+ fields: Fields,
97
+ primaryKeyColumns: readonly PrimaryKeyColumns[]
98
+ ): {
99
+ readonly select: Schema.Schema<SelectRow<Fields>>
100
+ readonly insert: Schema.Schema<InsertRow<Fields>>
101
+ readonly update: Schema.Schema<UpdateRow<Fields, PrimaryKeyColumns>>
102
+ } => {
103
+ const primaryKeySet = new Set<string>(primaryKeyColumns)
104
+ const variants: Record<string, VariantSchema.Field<any>> = {}
105
+ for (const [key, column] of Object.entries(fields)) {
106
+ const config: Record<Variants, any> = {
107
+ select: selectSchema(column),
108
+ insert: undefined,
109
+ update: undefined
110
+ }
111
+ const insert = insertSchema(column)
112
+ const update = updateSchema(column, primaryKeySet.has(key))
113
+ if (insert !== undefined) {
114
+ config.insert = insert
115
+ } else {
116
+ delete config.insert
117
+ }
118
+ if (update !== undefined) {
119
+ config.update = update
120
+ } else {
121
+ delete config.update
122
+ }
123
+ variants[key] = TableSchema.Field(config)
124
+ }
125
+ const struct = TableSchema.Struct(variants as any)
126
+ return {
127
+ select: TableSchema.extract(struct, "select") as unknown as Schema.Schema<SelectRow<Fields>>,
128
+ insert: TableSchema.extract(struct, "insert") as unknown as Schema.Schema<InsertRow<Fields>>,
129
+ update: TableSchema.extract(struct, "update") as unknown as Schema.Schema<UpdateRow<Fields, PrimaryKeyColumns>>
130
+ }
131
+ }