effect-qb 0.17.0 → 0.19.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 (103) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +8065 -0
  3. package/dist/mysql.js +3053 -2505
  4. package/dist/postgres/metadata.js +1366 -1250
  5. package/dist/postgres.js +2020 -2719
  6. package/dist/sqlite.js +3226 -2732
  7. package/dist/standard.js +8019 -0
  8. package/package.json +10 -3
  9. package/src/casing.ts +71 -0
  10. package/src/index.ts +2 -0
  11. package/src/internal/casing.ts +89 -0
  12. package/src/internal/column-state.ts +11 -6
  13. package/src/internal/column.ts +44 -7
  14. package/src/internal/datatypes/define.ts +2 -1
  15. package/src/internal/datatypes/enrich.ts +23 -0
  16. package/src/internal/datatypes/lookup.ts +14 -7
  17. package/src/internal/derived-table.ts +4 -36
  18. package/src/{mysql/internal/sql-expression-renderer.ts → internal/dialect-renderers/mysql.ts} +548 -359
  19. package/src/{postgres/internal/sql-expression-renderer.ts → internal/dialect-renderers/postgres.ts} +654 -399
  20. package/src/{sqlite/internal/sql-expression-renderer.ts → internal/dialect-renderers/sqlite.ts} +501 -345
  21. package/src/internal/dialect.ts +35 -0
  22. package/src/internal/dsl-mutation-runtime.ts +12 -162
  23. package/src/internal/dsl-plan-runtime.ts +10 -138
  24. package/src/internal/dsl-query-runtime.ts +5 -79
  25. package/src/internal/dsl-transaction-ddl-runtime.ts +41 -65
  26. package/src/internal/executor.ts +10 -6
  27. package/src/internal/grouping-key.ts +87 -20
  28. package/src/internal/implication-runtime.ts +1 -1
  29. package/src/internal/predicate/runtime.ts +3 -0
  30. package/src/internal/query.d.ts +38 -11
  31. package/src/internal/query.ts +64 -25
  32. package/src/internal/renderer.ts +26 -14
  33. package/src/internal/runtime/normalize.ts +12 -5
  34. package/src/internal/scalar.ts +6 -1
  35. package/src/internal/schema-derivation.d.ts +12 -61
  36. package/src/internal/schema-derivation.ts +90 -38
  37. package/src/internal/schema-expression.ts +2 -2
  38. package/src/internal/sql-expression-renderer.ts +19 -0
  39. package/src/internal/standard-dsl.ts +6885 -0
  40. package/src/internal/table-options.ts +126 -66
  41. package/src/internal/table.d.ts +33 -32
  42. package/src/internal/table.ts +406 -155
  43. package/src/mysql/column-extension.ts +3 -0
  44. package/src/mysql/column.ts +10 -11
  45. package/src/mysql/datatypes/index.ts +3 -2
  46. package/src/mysql/executor.ts +7 -5
  47. package/src/mysql/internal/dialect.ts +9 -4
  48. package/src/mysql/internal/dsl.ts +219 -155
  49. package/src/mysql/internal/renderer.ts +6 -2
  50. package/src/mysql/json.ts +37 -0
  51. package/src/mysql/query-extension.ts +16 -0
  52. package/src/mysql/renderer.ts +31 -4
  53. package/src/mysql.ts +4 -12
  54. package/src/postgres/column-extension.ts +28 -0
  55. package/src/postgres/column.ts +5 -11
  56. package/src/postgres/datatypes/index.d.ts +2 -1
  57. package/src/postgres/datatypes/index.ts +3 -2
  58. package/src/postgres/executor.ts +7 -5
  59. package/src/postgres/function/core.ts +1 -3
  60. package/src/postgres/function/index.ts +1 -17
  61. package/src/postgres/internal/dialect.ts +9 -4
  62. package/src/postgres/internal/dsl.ts +208 -160
  63. package/src/postgres/internal/renderer.ts +6 -2
  64. package/src/postgres/internal/schema-ddl.ts +22 -10
  65. package/src/postgres/internal/schema-model.ts +238 -7
  66. package/src/postgres/json.ts +43 -7
  67. package/src/postgres/jsonb.ts +38 -0
  68. package/src/postgres/query-extension.ts +2 -0
  69. package/src/postgres/renderer.ts +31 -4
  70. package/src/postgres/schema-management.ts +17 -12
  71. package/src/postgres/schema.ts +98 -15
  72. package/src/postgres/table.ts +193 -524
  73. package/src/postgres/type.ts +8 -7
  74. package/src/postgres.ts +9 -11
  75. package/src/sqlite/column-extension.ts +3 -0
  76. package/src/sqlite/column.ts +10 -11
  77. package/src/sqlite/datatypes/index.ts +3 -2
  78. package/src/sqlite/executor.ts +7 -5
  79. package/src/sqlite/internal/dialect.ts +9 -4
  80. package/src/sqlite/internal/dsl.ts +208 -155
  81. package/src/sqlite/internal/renderer.ts +6 -2
  82. package/src/sqlite/json.ts +37 -0
  83. package/src/sqlite/query-extension.ts +2 -0
  84. package/src/sqlite/renderer.ts +31 -4
  85. package/src/sqlite.ts +4 -12
  86. package/src/standard/column.ts +163 -0
  87. package/src/standard/datatypes/index.ts +83 -0
  88. package/src/standard/datatypes/spec.ts +98 -0
  89. package/src/standard/dialect.ts +40 -0
  90. package/src/standard/function/aggregate.ts +2 -0
  91. package/src/standard/function/core.ts +2 -0
  92. package/src/standard/function/index.ts +18 -0
  93. package/src/standard/function/string.ts +2 -0
  94. package/src/standard/function/temporal.ts +78 -0
  95. package/src/standard/function/window.ts +2 -0
  96. package/src/standard/internal/renderer.ts +45 -0
  97. package/src/standard/query.ts +152 -0
  98. package/src/standard/renderer.ts +21 -0
  99. package/src/standard/table.ts +147 -0
  100. package/src/standard.ts +18 -0
  101. package/src/internal/aggregation-validation.ts +0 -57
  102. package/src/mysql/table.ts +0 -183
  103. package/src/sqlite/table.ts +0 -183
@@ -1,5 +1,4 @@
1
1
  import * as Plan from "./row-set.js"
2
- import * as Table from "./table.js"
3
2
 
4
3
  type DslTransactionDdlRuntimeContext = {
5
4
  readonly profile: {
@@ -11,16 +10,13 @@ type DslTransactionDdlRuntimeContext = {
11
10
  readonly defaultIndexName: (tableName: string, columns: readonly string[], unique: boolean) => string
12
11
  }
13
12
 
14
- const allowedIsolationLevels = new Set(["read committed", "repeatable read", "serializable"])
15
-
16
- export const renderTransactionIsolationLevel = (isolationLevel: unknown): string => {
13
+ export const renderTransactionIsolationLevel = (
14
+ isolationLevel: unknown
15
+ ): string => {
17
16
  if (isolationLevel === undefined) {
18
17
  return ""
19
18
  }
20
- if (typeof isolationLevel !== "string" || !allowedIsolationLevels.has(isolationLevel)) {
21
- throw new Error("Unsupported transaction isolation level")
22
- }
23
- return `isolation level ${isolationLevel}`
19
+ return `isolation level ${isolationLevel as string}`
24
20
  }
25
21
 
26
22
  export const expectDdlClauseKind = <
@@ -28,53 +24,29 @@ export const expectDdlClauseKind = <
28
24
  Kind extends Ddl["kind"]
29
25
  >(
30
26
  ddl: Ddl | undefined,
31
- kind: Kind
32
- ): Extract<Ddl, { readonly kind: Kind }> => {
33
- if (ddl === undefined || ddl.kind !== kind) {
34
- throw new Error("Unsupported DDL statement kind")
35
- }
36
- return ddl as Extract<Ddl, { readonly kind: Kind }>
37
- }
27
+ _kind: Kind
28
+ ): Extract<Ddl, { readonly kind: Kind }> =>
29
+ ddl as Extract<Ddl, { readonly kind: Kind }>
38
30
 
39
31
  export const expectTruncateClause = <
40
32
  Truncate extends { readonly kind: string }
41
33
  >(
42
34
  truncate: Truncate | undefined
43
- ): Extract<Truncate, { readonly kind: "truncate" }> => {
44
- if (truncate === undefined || truncate.kind !== "truncate") {
45
- throw new Error("Unsupported truncate statement kind")
46
- }
47
- return truncate as Extract<Truncate, { readonly kind: "truncate" }>
48
- }
35
+ ): Extract<Truncate, { readonly kind: "truncate" }> =>
36
+ truncate as Extract<Truncate, { readonly kind: "truncate" }>
49
37
 
50
- const validateIsolationLevel = (isolationLevel: unknown): void => {
51
- renderTransactionIsolationLevel(isolationLevel)
52
- }
53
-
54
- export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContext) => {
55
- const isRecord = (value: unknown): value is Record<PropertyKey, unknown> =>
56
- typeof value === "object" && value !== null
57
-
58
- const assertTableTarget = (target: unknown, apiName: string): void => {
59
- if (!isRecord(target) || !(Table.TypeId in target) || !(Plan.TypeId in target)) {
60
- throw new Error(`${apiName}(...) requires a table target`)
61
- }
62
- }
38
+ export const normalizeStatementFlag = (value: unknown): boolean =>
39
+ (value as boolean | undefined) ?? false
63
40
 
64
- const validateIndexColumns = (target: any, columns: readonly string[]): void => {
65
- const fields = target[Table.TypeId]?.fields as Record<string, unknown> | undefined
66
- if (fields === undefined) {
67
- return
68
- }
69
- for (const columnName of columns) {
70
- if (!(columnName in fields)) {
71
- throw new Error(`effect-qb: unknown index column '${columnName}'`)
72
- }
73
- }
74
- }
41
+ export const normalizeStatementIdentifier = (
42
+ _apiName: string,
43
+ _identifierName: string,
44
+ value: unknown
45
+ ): string =>
46
+ value as string
75
47
 
48
+ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContext) => {
76
49
  const transaction = (options: { readonly isolationLevel?: any; readonly readOnly?: boolean } = {}) => {
77
- validateIsolationLevel(options.isolationLevel)
78
50
  return ctx.makePlan({
79
51
  selection: {},
80
52
  required: [],
@@ -134,8 +106,8 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
134
106
  orderBy: []
135
107
  }, undefined, "transaction", "rollback")
136
108
 
137
- const savepoint = (name: string) =>
138
- ctx.makePlan({
109
+ const savepoint = (name: string) => {
110
+ return ctx.makePlan({
139
111
  selection: {},
140
112
  required: [],
141
113
  available: {},
@@ -153,9 +125,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
153
125
  groupBy: [],
154
126
  orderBy: []
155
127
  }, undefined, "transaction", "savepoint")
128
+ }
156
129
 
157
- const rollbackTo = (name: string) =>
158
- ctx.makePlan({
130
+ const rollbackTo = (name: string) => {
131
+ return ctx.makePlan({
159
132
  selection: {},
160
133
  required: [],
161
134
  available: {},
@@ -173,9 +146,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
173
146
  groupBy: [],
174
147
  orderBy: []
175
148
  }, undefined, "transaction", "rollbackTo")
149
+ }
176
150
 
177
- const releaseSavepoint = (name: string) =>
178
- ctx.makePlan({
151
+ const releaseSavepoint = (name: string) => {
152
+ return ctx.makePlan({
179
153
  selection: {},
180
154
  required: [],
181
155
  available: {},
@@ -193,9 +167,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
193
167
  groupBy: [],
194
168
  orderBy: []
195
169
  }, undefined, "transaction", "releaseSavepoint")
170
+ }
196
171
 
197
172
  const createTable = (target: any, options: { readonly ifNotExists?: boolean } = {}) => {
198
- assertTableTarget(target, "createTable")
173
+ const ifNotExists = normalizeStatementFlag(options.ifNotExists)
199
174
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
200
175
  return ctx.makePlan({
201
176
  selection: {},
@@ -213,7 +188,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
213
188
  },
214
189
  ddl: {
215
190
  kind: "createTable",
216
- ifNotExists: options.ifNotExists ?? false
191
+ ifNotExists
217
192
  },
218
193
  where: [],
219
194
  having: [],
@@ -224,7 +199,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
224
199
  }
225
200
 
226
201
  const dropTable = (target: any, options: { readonly ifExists?: boolean } = {}) => {
227
- assertTableTarget(target, "dropTable")
202
+ const ifExists = normalizeStatementFlag(options.ifExists)
228
203
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
229
204
  return ctx.makePlan({
230
205
  selection: {},
@@ -242,7 +217,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
242
217
  },
243
218
  ddl: {
244
219
  kind: "dropTable",
245
- ifExists: options.ifExists ?? false
220
+ ifExists
246
221
  },
247
222
  where: [],
248
223
  having: [],
@@ -253,9 +228,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
253
228
  }
254
229
 
255
230
  const createIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly unique?: boolean; readonly ifNotExists?: boolean } = {}) => {
256
- assertTableTarget(target, "createIndex")
257
231
  const normalizedColumns = ctx.normalizeColumnList(columns)
258
- validateIndexColumns(target, normalizedColumns)
232
+ const unique = normalizeStatementFlag(options.unique)
233
+ const ifNotExists = normalizeStatementFlag(options.ifNotExists)
234
+ const name = options.name
259
235
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
260
236
  return ctx.makePlan({
261
237
  selection: {},
@@ -273,10 +249,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
273
249
  },
274
250
  ddl: {
275
251
  kind: "createIndex",
276
- name: options.name ?? ctx.defaultIndexName(sourceBaseName, normalizedColumns, options.unique ?? false),
252
+ name: name ?? ctx.defaultIndexName(sourceBaseName, normalizedColumns, unique),
277
253
  columns: normalizedColumns,
278
- unique: options.unique ?? false,
279
- ifNotExists: options.ifNotExists ?? false
254
+ unique,
255
+ ifNotExists
280
256
  },
281
257
  where: [],
282
258
  having: [],
@@ -287,9 +263,9 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
287
263
  }
288
264
 
289
265
  const dropIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly ifExists?: boolean } = {}) => {
290
- assertTableTarget(target, "dropIndex")
291
266
  const normalizedColumns = ctx.normalizeColumnList(columns)
292
- validateIndexColumns(target, normalizedColumns)
267
+ const ifExists = normalizeStatementFlag(options.ifExists)
268
+ const name = options.name
293
269
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
294
270
  return ctx.makePlan({
295
271
  selection: {},
@@ -307,8 +283,8 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
307
283
  },
308
284
  ddl: {
309
285
  kind: "dropIndex",
310
- name: options.name ?? ctx.defaultIndexName(sourceBaseName, normalizedColumns, false),
311
- ifExists: options.ifExists ?? false
286
+ name: name ?? ctx.defaultIndexName(sourceBaseName, normalizedColumns, false),
287
+ ifExists
312
288
  },
313
289
  where: [],
314
290
  having: [],
@@ -509,24 +509,28 @@ export const fromDriver = <
509
509
  renderer: Renderer.Renderer<Dialect>,
510
510
  sqlDriver: Driver<Dialect, Error, Context>
511
511
  ): Executor<Dialect, Error, Context> => {
512
- const executor = {
512
+ const executor: Executor<Dialect, Error, Context> = {
513
513
  dialect: renderer.dialect,
514
- execute(plan: any) {
514
+ execute<PlanValue extends Query.Plan.Any>(
515
+ plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
516
+ ) {
515
517
  const rendered = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
516
518
  return Effect.map(
517
519
  sqlDriver.execute(rendered),
518
520
  (rows) => remapRows<any>(rendered, rows)
519
- )
521
+ ) as Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
520
522
  },
521
- stream(plan: any) {
523
+ stream<PlanValue extends Query.Plan.Any>(
524
+ plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
525
+ ) {
522
526
  const rendered = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
523
527
  return Stream.mapChunks(
524
528
  sqlDriver.stream(rendered),
525
529
  (rows) => Chunk.unsafeFromArray(remapRows<any>(rendered, Chunk.toReadonlyArray(rows)))
526
- )
530
+ ) as Stream.Stream<Query.ResultRow<PlanValue>, Error, Context>
527
531
  }
528
532
  }
529
- return executor as unknown as Executor<Dialect, Error, Context>
533
+ return executor
530
534
  }
531
535
 
532
536
  export const streamFromSqlClient = <Dialect extends string>(
@@ -21,7 +21,9 @@ const subqueryPlanGroupingKey = (plan: unknown): string => {
21
21
 
22
22
  const literalGroupingKey = (value: unknown): string => {
23
23
  if (value instanceof Date) {
24
- return `date:${value.toISOString()}`
24
+ return Number.isNaN(value.getTime())
25
+ ? "date:invalid"
26
+ : `date:${value.toISOString()}`
25
27
  }
26
28
  if (value === null) {
27
29
  return "null"
@@ -44,6 +46,30 @@ const isExpression = (value: unknown): value is Expression.Any =>
44
46
  const expressionGroupingKey = (value: unknown): string =>
45
47
  isExpression(value) ? groupingKeyOfExpression(value) : "missing"
46
48
 
49
+ const requiredExpressionGroupingKey = (
50
+ _functionName: string,
51
+ value: unknown
52
+ ): string => groupingKeyOfExpression(value as Expression.Any)
53
+
54
+ const requiredBinaryExpressionGroupingKey = (
55
+ _functionName: string,
56
+ left: unknown,
57
+ right: unknown
58
+ ): string => `${groupingKeyOfExpression(left as Expression.Any)},${groupingKeyOfExpression(right as Expression.Any)}`
59
+
60
+ const functionCallArgsGroupingKey = (args: unknown): string =>
61
+ (args as readonly Expression.Any[]).map(groupingKeyOfExpression).join(",")
62
+
63
+ const variadicGroupingKey = (
64
+ _functionName: string,
65
+ values: unknown
66
+ ): string => (values as readonly Expression.Any[]).map(groupingKeyOfExpression).join(",")
67
+
68
+ const castTargetGroupingKey = (target: unknown): string => {
69
+ const typed = target as { readonly dialect?: string; readonly kind?: string } | null | undefined
70
+ return `${typed?.dialect}:${typed?.kind}`
71
+ }
72
+
47
73
  const escapeGroupingText = (value: string): string =>
48
74
  value
49
75
  .replace(/\\/g, "\\\\")
@@ -52,6 +78,30 @@ const escapeGroupingText = (value: string): string =>
52
78
  .replace(/=/g, "\\=")
53
79
  .replace(/>/g, "\\>")
54
80
 
81
+ const functionCallNameGroupingKey = (name: unknown): string =>
82
+ escapeGroupingText(name as string)
83
+
84
+ const quantifiedComparisonGroupingName = (
85
+ kind: "comparisonAny" | "comparisonAll"
86
+ ): "compareAny" | "compareAll" =>
87
+ kind === "comparisonAny" ? "compareAny" : "compareAll"
88
+
89
+ const caseGroupingKey = (
90
+ branches: unknown,
91
+ fallback: unknown
92
+ ): string => {
93
+ const typedBranches = branches as readonly ExpressionAst.CaseBranchNode[]
94
+ return `case(${typedBranches.map((branch) =>
95
+ `when:${groupingKeyOfExpression(branch.when)}=>${groupingKeyOfExpression(branch.then)}`).join("|")};else:${groupingKeyOfExpression(fallback as Expression.Any)})`
96
+ }
97
+
98
+ const collationGroupingKey = (collation: unknown): string => {
99
+ if (Array.isArray(collation)) {
100
+ return collation.map((part) => escapeGroupingText(String(part))).join(",")
101
+ }
102
+ return escapeGroupingText(String(collation))
103
+ }
104
+
55
105
  const jsonSegmentGroupingKey = (segment: unknown): string => {
56
106
  if (segment !== null && typeof segment === "object" && "kind" in segment) {
57
107
  switch ((segment as { readonly kind: string }).kind) {
@@ -78,8 +128,15 @@ const jsonSegmentGroupingKey = (segment: unknown): string => {
78
128
  return "unknown"
79
129
  }
80
130
 
81
- const jsonPathGroupingKey = (segments: readonly unknown[] | undefined): string =>
82
- (segments ?? []).map(jsonSegmentGroupingKey).join(",")
131
+ const jsonPathGroupingKey = (segments: unknown): string => {
132
+ if (segments === undefined) {
133
+ return ""
134
+ }
135
+ if (!Array.isArray(segments)) {
136
+ return "unknown"
137
+ }
138
+ return segments.map(jsonSegmentGroupingKey).join(",")
139
+ }
83
140
 
84
141
  const isJsonPath = (value: unknown): value is JsonPath.Path =>
85
142
  value !== null && typeof value === "object" && JsonPath.TypeId in value
@@ -97,9 +154,20 @@ const jsonOpaquePathGroupingKey = (value: unknown): string => {
97
154
  return "jsonpath:unknown"
98
155
  }
99
156
 
100
- const jsonEntryGroupingKey = (
101
- entry: { readonly key: string; readonly value: Expression.Any }
102
- ): string => `${escapeGroupingText(entry.key)}=>${groupingKeyOfExpression(entry.value)}`
157
+ const jsonKeysGroupingKey = (keys: unknown): string => {
158
+ if (!Array.isArray(keys) || keys.length === 0) {
159
+ return ""
160
+ }
161
+ return keys.map((key) => escapeGroupingText(String(key))).join(",")
162
+ }
163
+
164
+ const jsonBuildObjectGroupingKey = (entries: unknown): string => {
165
+ return (entries as readonly { readonly key: string; readonly value: Expression.Any }[]).map((entry) =>
166
+ `${escapeGroupingText(entry.key)}=>${groupingKeyOfExpression(entry.value)}`).join("|")
167
+ }
168
+
169
+ const jsonBuildArrayGroupingKey = (values: unknown): string =>
170
+ (values as readonly Expression.Any[]).map(groupingKeyOfExpression).join(",")
103
171
 
104
172
  export const groupingKeyOfExpression = (expression: Expression.Any): string => {
105
173
  const ast = (expression as Expression.Any & {
@@ -111,11 +179,11 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
111
179
  case "literal":
112
180
  return `literal:${literalGroupingKey(ast.value)}`
113
181
  case "cast":
114
- return `cast(${groupingKeyOfExpression(ast.value)} as ${ast.target.dialect}:${ast.target.kind})`
182
+ return `cast(${requiredExpressionGroupingKey("cast", ast.value)} as ${castTargetGroupingKey(ast.target)})`
115
183
  case "collate":
116
- return `collate(${groupingKeyOfExpression(ast.value)},${ast.collation.map(escapeGroupingText).join(",")})`
184
+ return `collate(${requiredExpressionGroupingKey("collate", ast.value)},${collationGroupingKey(ast.collation)})`
117
185
  case "function":
118
- return `function(${escapeGroupingText(ast.name)},${ast.args.map(groupingKeyOfExpression).join(",")})`
186
+ return `function(${functionCallNameGroupingKey(ast.name)},${functionCallArgsGroupingKey(ast.args)})`
119
187
  case "isNull":
120
188
  case "isNotNull":
121
189
  case "not":
@@ -124,7 +192,7 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
124
192
  case "count":
125
193
  case "max":
126
194
  case "min":
127
- return `${ast.kind}(${groupingKeyOfExpression(ast.value)})`
195
+ return `${ast.kind}(${requiredExpressionGroupingKey(ast.kind, ast.value)})`
128
196
  case "eq":
129
197
  case "neq":
130
198
  case "lt":
@@ -142,7 +210,7 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
142
210
  case "contains":
143
211
  case "containedBy":
144
212
  case "overlaps":
145
- return `${ast.kind}(${groupingKeyOfExpression(ast.left)},${groupingKeyOfExpression(ast.right)})`
213
+ return `${ast.kind}(${requiredBinaryExpressionGroupingKey(ast.kind, ast.left, ast.right)})`
146
214
  case "and":
147
215
  case "or":
148
216
  case "coalesce":
@@ -150,19 +218,18 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
150
218
  case "in":
151
219
  case "notIn":
152
220
  case "between":
153
- return `${ast.kind}(${ast.values.map(groupingKeyOfExpression).join(",")})`
221
+ return `${ast.kind}(${variadicGroupingKey(ast.kind, ast.values)})`
154
222
  case "case":
155
- return `case(${ast.branches.map((branch: ExpressionAst.CaseBranchNode) =>
156
- `when:${groupingKeyOfExpression(branch.when)}=>${groupingKeyOfExpression(branch.then)}`).join("|")};else:${groupingKeyOfExpression(ast.else)})`
223
+ return caseGroupingKey(ast.branches, ast.else)
157
224
  case "exists":
158
225
  return `exists(${subqueryPlanGroupingKey(ast.plan)})`
159
226
  case "scalarSubquery":
160
227
  return `scalarSubquery(${subqueryPlanGroupingKey(ast.plan)})`
161
228
  case "inSubquery":
162
- return `inSubquery(${groupingKeyOfExpression(ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
229
+ return `inSubquery(${requiredExpressionGroupingKey("inSubquery", ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
163
230
  case "comparisonAny":
164
231
  case "comparisonAll":
165
- return `${ast.kind}(${ast.operator},${groupingKeyOfExpression(ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
232
+ return `${ast.kind}(${ast.operator},${requiredExpressionGroupingKey(quantifiedComparisonGroupingName(ast.kind), ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
166
233
  case "jsonGet":
167
234
  case "jsonPath":
168
235
  case "jsonAccess":
@@ -176,7 +243,7 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
176
243
  case "jsonKeyExists":
177
244
  case "jsonHasAnyKeys":
178
245
  case "jsonHasAllKeys":
179
- return `json(${ast.kind},${expressionGroupingKey(ast.base)},${(ast.keys ?? []).map(escapeGroupingText).join(",")})`
246
+ return `json(${ast.kind},${expressionGroupingKey(ast.base)},${jsonKeysGroupingKey(ast.keys)})`
180
247
  case "jsonConcat":
181
248
  case "jsonMerge":
182
249
  return `json(${ast.kind},${expressionGroupingKey(ast.left)},${expressionGroupingKey(ast.right)},)`
@@ -192,9 +259,9 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
192
259
  case "jsonPathMatch":
193
260
  return `json(${ast.kind},${expressionGroupingKey(ast.base)},${jsonOpaquePathGroupingKey(ast.query)})`
194
261
  case "jsonBuildObject":
195
- return `json(${ast.kind},${(ast.entries ?? []).map(jsonEntryGroupingKey).join("|")})`
262
+ return `json(${ast.kind},${jsonBuildObjectGroupingKey(ast.entries)})`
196
263
  case "jsonBuildArray":
197
- return `json(${ast.kind},${(ast.values ?? []).map(groupingKeyOfExpression).join(",")})`
264
+ return `json(${ast.kind},${jsonBuildArrayGroupingKey(ast.values)})`
198
265
  case "jsonToJson":
199
266
  case "jsonToJsonb":
200
267
  case "jsonTypeOf":
@@ -203,7 +270,7 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
203
270
  case "jsonStripNulls":
204
271
  return `json(${ast.kind},${expressionGroupingKey(ast.value)})`
205
272
  default:
206
- throw new Error("Unsupported expression for grouping key generation")
273
+ return `unknown:${typeof ast === "object" && ast !== null && "kind" in ast ? String((ast as { readonly kind: unknown }).kind) : "ast"}`
207
274
  }
208
275
  }
209
276
 
@@ -41,7 +41,7 @@ const collectPresenceWitnesses = (
41
41
  return
42
42
  }
43
43
  if (Expression.TypeId in selection && ExpressionAst.TypeId in selection) {
44
- const expression = selection as unknown as AstBackedExpression
44
+ const expression = selection as AstBackedExpression
45
45
  const ast = expression[ExpressionAst.TypeId]
46
46
  if (ast.kind === "column" && expression[Expression.TypeId].nullability === "never") {
47
47
  output.add(columnPredicateKey(ast.tableName, ast.columnName))
@@ -474,6 +474,9 @@ const valueKeyOfLiteral = (value: unknown): string => {
474
474
  return "null"
475
475
  }
476
476
  if (value instanceof Date) {
477
+ if (Number.isNaN(value.getTime())) {
478
+ throw new Error("Expected a valid Date value")
479
+ }
477
480
  return `date:${value.toISOString()}`
478
481
  }
479
482
  return "unknown"
@@ -37,6 +37,31 @@ export interface QueryState<Outstanding extends string, AvailableNames extends s
37
37
  }
38
38
  /** Effective SQL dialect carried by an expression. */
39
39
  export type DialectOf<Value extends Expression.Any> = Value[typeof Expression.TypeId]["dialect"];
40
+ type ConcreteDialect<D> = D extends "standard" ? never : D;
41
+ type IsDialectUnion<Union, All = Union> = Union extends any ? [
42
+ All
43
+ ] extends [Union] ? false : true : false;
44
+ export type DialectConflictError<A extends string, B extends string> = string & {
45
+ readonly _tag: "DialectConflict";
46
+ readonly left: A;
47
+ readonly right: B;
48
+ };
49
+ export type MergeDialect<A extends string, B extends string> = [
50
+ ConcreteDialect<A>,
51
+ ConcreteDialect<B>
52
+ ] extends [never, never] ? "standard" : [
53
+ ConcreteDialect<A>,
54
+ ConcreteDialect<B>
55
+ ] extends [never, infer C extends string] ? C : [
56
+ ConcreteDialect<A>,
57
+ ConcreteDialect<B>
58
+ ] extends [infer C extends string, never] ? C : ConcreteDialect<A> extends ConcreteDialect<B> ? ConcreteDialect<B> extends ConcreteDialect<A> ? A : DialectConflictError<A, B> : DialectConflictError<A, B>;
59
+ /** Collapses the portable standard tag out of a dialect union. */
60
+ export type NormalizeDialect<Dialect extends string> = [
61
+ Dialect
62
+ ] extends [never] ? never : [
63
+ Exclude<Dialect, "standard">
64
+ ] extends [never] ? "standard" : IsDialectUnion<Exclude<Dialect, "standard">> extends true ? DialectConflictError<Exclude<Dialect, "standard">, Exclude<Dialect, "standard">> : Exclude<Dialect, "standard">;
40
65
  /** Source dependency union carried by an expression. */
41
66
  export type DependenciesOf<Value extends Expression.Any> = Expression.DependenciesOf<Value>;
42
67
  /** Aggregation kind carried by an expression. */
@@ -135,10 +160,11 @@ export type SelectionShape = Expression.Any | {
135
160
  export type ExtractRequired<Selection> = Selection extends Expression.Any ? RequiredFromDependencies<DependenciesOf<Selection>> : Selection extends Record<string, any> ? {
136
161
  [K in keyof Selection]: ExtractRequired<Selection[K]>;
137
162
  }[keyof Selection] : never;
138
- /** Walks a selection tree and unions the dialects referenced by its leaves. */
139
- export type ExtractDialect<Selection> = Selection extends Expression.Any ? DialectOf<Selection> : Selection extends Record<string, any> ? {
140
- [K in keyof Selection]: ExtractDialect<Selection[K]>;
163
+ type ExtractDialectRaw<Selection> = Selection extends Expression.Any ? DialectOf<Selection> : Selection extends Record<string, any> ? {
164
+ [K in keyof Selection]: ExtractDialectRaw<Selection[K]>;
141
165
  }[keyof Selection] : never;
166
+ /** Walks a selection tree and extracts the effective dialects referenced by its leaves. */
167
+ export type ExtractDialect<Selection> = NormalizeDialect<Extract<ExtractDialectRaw<Selection>, string>>;
142
168
  /**
143
169
  * Minimal table-like shape required by `from(...)` and joins.
144
170
  *
@@ -419,8 +445,7 @@ export type MutationTargetNamesOf<Target extends MutationTargetInput> = Target e
419
445
  export type UpdateInputOfTarget<Target extends MutationTargetInput> = Target extends MutationTargetLike ? MutationInputOf<Table.UpdateOf<Target>> : Target extends MutationTargetTuple ? Simplify<{
420
446
  readonly [K in MutationTargetNamesOf<Target>]?: MutationInputOf<Table.UpdateOf<MutationTargetByName<Target, K>>>;
421
447
  }> : never;
422
- /** Extracts the effective dialect from a source. */
423
- export type SourceDialectOf<Source extends SourceLike> = Source extends TableLike<any, infer Dialect> ? Dialect : Source extends {
448
+ type SourceDialectOfRaw<Source extends SourceLike> = Source extends TableLike<any, infer Dialect> ? Dialect : Source extends {
424
449
  readonly kind: "derived";
425
450
  readonly plan: infer PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>;
426
451
  } ? PlanDialectOf<PlanValue> : Source extends {
@@ -432,6 +457,8 @@ export type SourceDialectOf<Source extends SourceLike> = Source extends TableLik
432
457
  } ? PlanDialectOf<PlanValue> : Source extends {
433
458
  readonly dialect: infer Dialect extends string;
434
459
  } ? Dialect : never;
460
+ /** Extracts the effective dialect from a source. */
461
+ export type SourceDialectOf<Source extends SourceLike> = NormalizeDialect<SourceDialectOfRaw<Source>>;
435
462
  /** Extracts the base table name from a source. */
436
463
  export type SourceBaseNameOf<Source extends SourceLike> = Source extends TableLike<any, any> ? Source[typeof Table.TypeId]["baseName"] : Source extends {
437
464
  readonly kind: "derived";
@@ -493,7 +520,7 @@ export type RequiredOfPlan<PlanValue extends QueryPlan<any, any, any, any, any,
493
520
  /** Extracts the available-source scope carried by a query plan. */
494
521
  export type AvailableOfPlan<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any, any, any>> = QueryPlanParts<PlanValue>["available"];
495
522
  /** Extracts the effective dialect carried by a query plan. */
496
- export type PlanDialectOf<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any, any, any>> = QueryPlanParts<PlanValue>["dialect"];
523
+ export type PlanDialectOf<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any, any, any>> = NormalizeDialect<QueryPlanParts<PlanValue>["dialect"]>;
497
524
  /** Extracts the grouped-source phantom carried by a query plan. */
498
525
  export type GroupedOfPlan<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any, any, any>> = QueryPlanState<PlanValue>["grouped"];
499
526
  /** Extracts the available-name phantom carried by a query plan. */
@@ -562,7 +589,7 @@ type MergeNullability<Left extends Expression.Nullability, Right extends Express
562
589
  /** Folds nullability across a tuple for null-propagating scalar operators. */
563
590
  export type MergeNullabilityTuple<Values extends readonly Expression.Any[], Current extends Expression.Nullability = "never"> = Values extends readonly [infer Head extends Expression.Any, ...infer Tail extends readonly Expression.Any[]] ? MergeNullabilityTuple<Tail, MergeNullability<Current, Expression.NullabilityOf<Head>>> : Current;
564
591
  /** Dialect union across a tuple of expressions. */
565
- export type TupleDialect<Values extends readonly Expression.Any[]> = Values[number] extends never ? never : DialectOf<Values[number]>;
592
+ export type TupleDialect<Values extends readonly Expression.Any[]> = Values[number] extends never ? never : NormalizeDialect<DialectOf<Values[number]>>;
566
593
  /** Dependency union across a tuple of scalar expressions. */
567
594
  export type TupleDependencies<Values extends readonly Expression.Any[]> = Values[number] extends never ? never : DependenciesOf<Values[number]>;
568
595
  /** Builds the canonical dependency union from a string union of table names. */
@@ -661,7 +688,7 @@ type AggregationCompatibilityError<PlanValue extends QueryPlan<any, any, any, an
661
688
  };
662
689
  type DialectCompatibilityError<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>, EngineDialect extends string> = PlanValue & {
663
690
  readonly __effect_qb_error__: "effect-qb: plan dialect is not compatible with the target renderer or executor";
664
- readonly __effect_qb_plan_dialect__: PlanValue[typeof RowSet.TypeId]["dialect"];
691
+ readonly __effect_qb_plan_dialect__: PlanDialectOf<PlanValue>;
665
692
  readonly __effect_qb_target_dialect__: EngineDialect;
666
693
  readonly __effect_qb_hint__: "Use the matching dialect module or renderer/executor";
667
694
  };
@@ -678,11 +705,11 @@ export type AggregationCompatiblePlan<PlanValue extends QueryPlan<any, any, any,
678
705
  /** Narrows a query plan to aggregate-compatible, source-complete plans. */
679
706
  export type CompletePlan<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>> = StatementOfPlan<PlanValue> extends "insert" ? InsertSourceStateOfPlan<PlanValue> extends "missing" ? InsertHasOptionalDefaults<PlanValue> extends true ? PlanValue : InsertSourceCompletenessError<PlanValue> : HasKnownOutstanding<RequiredOfPlan<PlanValue>> extends true ? SourceCompletenessError<PlanValue, Extract<RequiredOfPlan<PlanValue>, string>> : IsAggregationCompatibleSelection<SelectionOfPlan<PlanValue>, GroupedOfPlan<PlanValue>> extends true ? PlanValue : AggregationCompatibilityError<PlanValue> : HasKnownOutstanding<RequiredOfPlan<PlanValue>> extends true ? SourceCompletenessError<PlanValue, Extract<RequiredOfPlan<PlanValue>, string>> : IsAggregationCompatibleSelection<SelectionOfPlan<PlanValue>, GroupedOfPlan<PlanValue>> extends true ? PlanValue : AggregationCompatibilityError<PlanValue>;
680
707
  /** Whether a plan dialect is compatible with a target engine dialect. */
681
- type IsDialectCompatible<PlanDialect extends string, EngineDialect extends string> = [PlanDialect] extends [never] ? true : Extract<PlanDialect, EngineDialect> extends never ? false : true;
708
+ type IsDialectCompatible<PlanDialect extends string, EngineDialect extends string> = [PlanDialect] extends [never] ? true : Exclude<PlanDialect, EngineDialect | "standard"> extends never ? true : false;
682
709
  /** Narrows a complete plan to those compatible with a target engine dialect. */
683
- export type DialectCompatiblePlan<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>, EngineDialect extends string> = IsDialectCompatible<PlanValue[typeof RowSet.TypeId]["dialect"], EngineDialect> extends true ? CompletePlan<PlanValue> : DialectCompatibilityError<PlanValue, EngineDialect>;
710
+ export type DialectCompatiblePlan<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>, EngineDialect extends string> = IsDialectCompatible<PlanDialectOf<PlanValue>, EngineDialect> extends true ? CompletePlan<PlanValue> : DialectCompatibilityError<PlanValue, EngineDialect>;
684
711
  /** Nested-plan compatibility used by subquery expressions such as `exists(...)`. */
685
- export type DialectCompatibleNestedPlan<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>, EngineDialect extends string> = IsDialectCompatible<PlanValue[typeof RowSet.TypeId]["dialect"], EngineDialect> extends true ? AggregationCompatiblePlan<PlanValue> : DialectCompatibilityError<PlanValue, EngineDialect>;
712
+ export type DialectCompatibleNestedPlan<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>, EngineDialect extends string> = IsDialectCompatible<PlanDialectOf<PlanValue>, EngineDialect> extends true ? AggregationCompatiblePlan<PlanValue> : DialectCompatibilityError<PlanValue, EngineDialect>;
686
713
  type SetOperandStatement = "select" | "set";
687
714
  type IsUnion<Value, All = Value> = Value extends any ? ([All] extends [Value] ? false : true) : never;
688
715
  type SingleSelectedExpressionError<PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>> = PlanValue & {