effect-qb 4.0.0-beta.66 → 4.0.0-beta.92

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 (126) hide show
  1. package/README.md +5 -2
  2. package/dist/index.js +9373 -0
  3. package/dist/mysql.js +3917 -2572
  4. package/dist/postgres/metadata.js +2491 -1373
  5. package/dist/postgres.js +5450 -5155
  6. package/dist/sqlite.js +6891 -5528
  7. package/dist/standard.js +9327 -0
  8. package/package.json +9 -2
  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/coercion/rules.ts +13 -1
  13. package/src/internal/column-state.ts +21 -15
  14. package/src/internal/column.ts +44 -7
  15. package/src/internal/datatypes/define.ts +7 -1
  16. package/src/internal/datatypes/enrich.ts +23 -0
  17. package/src/internal/datatypes/lookup.ts +81 -25
  18. package/src/internal/datatypes/matrix.ts +903 -0
  19. package/src/internal/datatypes/shape.ts +2 -0
  20. package/src/internal/derived-table.ts +4 -36
  21. package/src/{mysql/internal/sql-expression-renderer.ts → internal/dialect-renderers/mysql.ts} +552 -361
  22. package/src/{postgres/internal/sql-expression-renderer.ts → internal/dialect-renderers/postgres.ts} +657 -400
  23. package/src/{sqlite/internal/sql-expression-renderer.ts → internal/dialect-renderers/sqlite.ts} +505 -347
  24. package/src/internal/dialect.ts +35 -0
  25. package/src/internal/dsl-mutation-runtime.ts +12 -162
  26. package/src/internal/dsl-plan-runtime.ts +10 -138
  27. package/src/internal/dsl-query-runtime.ts +5 -79
  28. package/src/internal/dsl-transaction-ddl-runtime.ts +41 -65
  29. package/src/internal/executor.ts +14 -16
  30. package/src/internal/grouping-key.ts +87 -20
  31. package/src/internal/implication-runtime.ts +1 -1
  32. package/src/internal/json/path-access.ts +351 -0
  33. package/src/internal/predicate/runtime.ts +3 -0
  34. package/src/internal/query.d.ts +38 -11
  35. package/src/internal/query.ts +64 -25
  36. package/src/internal/renderer.ts +26 -14
  37. package/src/internal/runtime/normalize.ts +12 -5
  38. package/src/internal/scalar.ts +7 -1
  39. package/src/internal/schema-derivation.d.ts +7 -7
  40. package/src/internal/schema-derivation.ts +9 -9
  41. package/src/internal/schema-expression.ts +2 -2
  42. package/src/internal/sql-expression-renderer.ts +19 -0
  43. package/src/internal/standard-dsl.ts +6975 -0
  44. package/src/internal/table-options.ts +126 -66
  45. package/src/internal/table.ts +652 -219
  46. package/src/mysql/column-extension.ts +3 -0
  47. package/src/mysql/column.ts +8 -9
  48. package/src/mysql/datatypes/index.ts +4 -2
  49. package/src/mysql/datatypes/spec.ts +6 -176
  50. package/src/mysql/executor.ts +7 -7
  51. package/src/mysql/internal/dialect.ts +9 -4
  52. package/src/mysql/internal/dsl.ts +309 -154
  53. package/src/mysql/internal/renderer.ts +6 -2
  54. package/src/mysql/json.ts +7 -2
  55. package/src/mysql/query-extension.ts +16 -0
  56. package/src/mysql/renderer.ts +37 -3
  57. package/src/mysql/type.ts +60 -0
  58. package/src/mysql.ts +7 -13
  59. package/src/postgres/check.ts +1 -0
  60. package/src/postgres/column-extension.ts +28 -0
  61. package/src/postgres/column.ts +2 -8
  62. package/src/postgres/datatypes/index.d.ts +2 -1
  63. package/src/postgres/datatypes/index.ts +4 -2
  64. package/src/postgres/datatypes/spec.ts +6 -260
  65. package/src/postgres/executor.ts +7 -7
  66. package/src/postgres/foreign-key.ts +24 -0
  67. package/src/postgres/function/core.ts +1 -3
  68. package/src/postgres/function/index.ts +1 -17
  69. package/src/postgres/index.ts +1 -0
  70. package/src/postgres/internal/dialect.ts +9 -4
  71. package/src/postgres/internal/dsl.ts +303 -163
  72. package/src/postgres/internal/renderer.ts +6 -2
  73. package/src/postgres/internal/schema-ddl.ts +22 -10
  74. package/src/postgres/internal/schema-model.ts +238 -7
  75. package/src/postgres/json-extension.ts +7 -0
  76. package/src/postgres/json.ts +762 -173
  77. package/src/postgres/jsonb.ts +37 -0
  78. package/src/postgres/primary-key.ts +24 -0
  79. package/src/postgres/query-extension.ts +2 -0
  80. package/src/postgres/renderer.ts +37 -3
  81. package/src/postgres/schema-management.ts +12 -11
  82. package/src/postgres/schema.ts +106 -15
  83. package/src/postgres/table.ts +205 -530
  84. package/src/postgres/type.ts +93 -10
  85. package/src/postgres/unique.ts +32 -0
  86. package/src/postgres.ts +20 -16
  87. package/src/sqlite/column-extension.ts +3 -0
  88. package/src/sqlite/column.ts +8 -9
  89. package/src/sqlite/datatypes/index.ts +4 -2
  90. package/src/sqlite/datatypes/spec.ts +6 -94
  91. package/src/sqlite/executor.ts +7 -7
  92. package/src/sqlite/internal/dialect.ts +9 -4
  93. package/src/sqlite/internal/dsl.ts +298 -154
  94. package/src/sqlite/internal/renderer.ts +6 -2
  95. package/src/sqlite/json.ts +8 -2
  96. package/src/sqlite/query-extension.ts +2 -0
  97. package/src/sqlite/renderer.ts +37 -3
  98. package/src/sqlite/type.ts +40 -0
  99. package/src/sqlite.ts +7 -13
  100. package/src/standard/cast.ts +113 -0
  101. package/src/standard/check.ts +17 -0
  102. package/src/standard/column.ts +163 -0
  103. package/src/standard/datatypes/index.ts +83 -0
  104. package/src/standard/datatypes/spec.ts +12 -0
  105. package/src/standard/dialect.ts +40 -0
  106. package/src/standard/foreign-key.ts +37 -0
  107. package/src/standard/function/aggregate.ts +2 -0
  108. package/src/standard/function/core.ts +2 -0
  109. package/src/standard/function/index.ts +18 -0
  110. package/src/standard/function/string.ts +2 -0
  111. package/src/standard/function/temporal.ts +78 -0
  112. package/src/standard/function/window.ts +2 -0
  113. package/src/standard/index.ts +17 -0
  114. package/src/standard/internal/renderer.ts +45 -0
  115. package/src/standard/json.ts +883 -0
  116. package/src/standard/primary-key.ts +17 -0
  117. package/src/standard/query.ts +152 -0
  118. package/src/standard/renderer.ts +49 -0
  119. package/src/standard/table.ts +151 -0
  120. package/src/standard/unique.ts +17 -0
  121. package/src/standard.ts +32 -0
  122. package/src/internal/aggregation-validation.ts +0 -57
  123. package/src/internal/table.d.ts +0 -180
  124. package/src/mysql/table.ts +0 -186
  125. package/src/postgres/cast.ts +0 -45
  126. package/src/sqlite/table.ts +0 -175
@@ -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: [],
@@ -528,24 +528,28 @@ export const fromDriver = <
528
528
  renderer: Renderer.Renderer<Dialect>,
529
529
  sqlDriver: Driver<Dialect, Error, Context>
530
530
  ): Executor<Dialect, Error, Context> => {
531
- const executor = {
531
+ const executor: Executor<Dialect, Error, Context> = {
532
532
  dialect: renderer.dialect,
533
- execute(plan: any) {
533
+ execute<PlanValue extends Query.Plan.Any>(
534
+ plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
535
+ ) {
534
536
  const rendered = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
535
537
  return Effect.map(
536
538
  sqlDriver.execute(rendered),
537
539
  (rows) => remapRows<any>(rendered, rows)
538
- )
540
+ ) as Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
539
541
  },
540
- stream(plan: any) {
542
+ stream<PlanValue extends Query.Plan.Any>(
543
+ plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
544
+ ) {
541
545
  const rendered = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
542
546
  return Stream.mapArray(
543
547
  sqlDriver.stream(rendered),
544
548
  (rows) => remapRows<any>(rendered, rows) as never
545
- )
549
+ ) as Stream.Stream<Query.ResultRow<PlanValue>, Error, Context>
546
550
  }
547
551
  }
548
- return executor as unknown as Executor<Dialect, Error, Context>
552
+ return executor
549
553
  }
550
554
 
551
555
  export const streamFromSqlClient = <Dialect extends string>(
@@ -575,19 +579,13 @@ export const fromSqlClient = <Dialect extends string>(
575
579
  stream: (query) => streamFromSqlClient(query)
576
580
  }))
577
581
 
578
- /** Runs an effect within the ambient `effect/unstable/sql` transaction service. */
579
- export const withTransaction = <A, E, R>(
580
- effect: Effect.Effect<A, E, R>
581
- ): Effect.Effect<A, E | SqlError.SqlError, R | SqlClient.SqlClient> =>
582
- Effect.flatMap(SqlClient.SqlClient, (sql) => sql.withTransaction(effect))
583
-
584
582
  /**
585
- * Runs an effect in a nested transaction scope.
583
+ * Runs an effect within the ambient `effect/unstable/sql` transaction service.
586
584
  *
587
- * When the ambient `effect/unstable/sql` client is already inside a transaction, the
588
- * underlying client implementation uses a savepoint.
585
+ * Nested calls rely on the underlying client transaction implementation for
586
+ * savepoint behavior.
589
587
  */
590
- export const withSavepoint = <A, E, R>(
588
+ export const withTransaction = <A, E, R>(
591
589
  effect: Effect.Effect<A, E, R>
592
590
  ): Effect.Effect<A, E | SqlError.SqlError, R | SqlClient.SqlClient> =>
593
591
  Effect.flatMap(SqlClient.SqlClient, (sql) => sql.withTransaction(effect))
@@ -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))