effect-qb 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/mysql.js +1661 -591
  2. package/dist/postgres/metadata.js +1930 -135
  3. package/dist/postgres.js +7808 -6718
  4. package/dist/sqlite.js +8360 -0
  5. package/package.json +6 -1
  6. package/src/internal/derived-table.ts +29 -3
  7. package/src/internal/dialect.ts +2 -0
  8. package/src/internal/dsl-mutation-runtime.ts +173 -4
  9. package/src/internal/dsl-plan-runtime.ts +165 -20
  10. package/src/internal/dsl-query-runtime.ts +60 -6
  11. package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
  12. package/src/internal/executor.ts +47 -9
  13. package/src/internal/expression-ast.ts +3 -2
  14. package/src/internal/grouping-key.ts +141 -1
  15. package/src/internal/implication-runtime.ts +2 -1
  16. package/src/internal/json/types.ts +155 -40
  17. package/src/internal/predicate/context.ts +14 -1
  18. package/src/internal/predicate/key.ts +19 -2
  19. package/src/internal/predicate/runtime.ts +27 -3
  20. package/src/internal/query.ts +252 -30
  21. package/src/internal/renderer.ts +35 -2
  22. package/src/internal/runtime/driver-value-mapping.ts +58 -0
  23. package/src/internal/runtime/normalize.ts +62 -38
  24. package/src/internal/runtime/schema.ts +5 -3
  25. package/src/internal/runtime/value.ts +153 -30
  26. package/src/internal/table-options.ts +108 -1
  27. package/src/internal/table.ts +87 -29
  28. package/src/mysql/column.ts +18 -2
  29. package/src/mysql/datatypes/index.ts +21 -0
  30. package/src/mysql/errors/catalog.ts +5 -5
  31. package/src/mysql/errors/normalize.ts +2 -2
  32. package/src/mysql/internal/dsl.ts +736 -218
  33. package/src/mysql/internal/renderer.ts +2 -1
  34. package/src/mysql/internal/sql-expression-renderer.ts +486 -130
  35. package/src/mysql/query.ts +9 -2
  36. package/src/mysql/table.ts +38 -12
  37. package/src/postgres/column.ts +4 -2
  38. package/src/postgres/errors/normalize.ts +2 -2
  39. package/src/postgres/executor.ts +48 -5
  40. package/src/postgres/function/core.ts +19 -1
  41. package/src/postgres/internal/dsl.ts +683 -240
  42. package/src/postgres/internal/renderer.ts +2 -1
  43. package/src/postgres/internal/schema-ddl.ts +2 -1
  44. package/src/postgres/internal/schema-model.ts +6 -3
  45. package/src/postgres/internal/sql-expression-renderer.ts +420 -91
  46. package/src/postgres/json.ts +57 -17
  47. package/src/postgres/query.ts +9 -2
  48. package/src/postgres/schema-management.ts +91 -4
  49. package/src/postgres/schema.ts +1 -1
  50. package/src/postgres/table.ts +189 -53
  51. package/src/sqlite/column.ts +128 -0
  52. package/src/sqlite/datatypes/index.ts +79 -0
  53. package/src/sqlite/datatypes/spec.ts +98 -0
  54. package/src/sqlite/errors/catalog.ts +103 -0
  55. package/src/sqlite/errors/fields.ts +19 -0
  56. package/src/sqlite/errors/index.ts +19 -0
  57. package/src/sqlite/errors/normalize.ts +229 -0
  58. package/src/sqlite/errors/requirements.ts +71 -0
  59. package/src/sqlite/errors/types.ts +29 -0
  60. package/src/sqlite/executor.ts +227 -0
  61. package/src/sqlite/function/aggregate.ts +2 -0
  62. package/src/sqlite/function/core.ts +2 -0
  63. package/src/sqlite/function/index.ts +19 -0
  64. package/src/sqlite/function/string.ts +2 -0
  65. package/src/sqlite/function/temporal.ts +100 -0
  66. package/src/sqlite/function/window.ts +2 -0
  67. package/src/sqlite/internal/dialect.ts +37 -0
  68. package/src/sqlite/internal/dsl.ts +6926 -0
  69. package/src/sqlite/internal/renderer.ts +47 -0
  70. package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
  71. package/src/sqlite/json.ts +2 -0
  72. package/src/sqlite/query.ts +196 -0
  73. package/src/sqlite/renderer.ts +24 -0
  74. package/src/sqlite/table.ts +183 -0
  75. package/src/sqlite.ts +22 -0
@@ -1,5 +1,6 @@
1
1
  import * as Expression from "./scalar.js"
2
2
  import * as Plan from "./row-set.js"
3
+ import { flattenSelection } from "./projections.js"
3
4
 
4
5
  type DslQueryRuntimeContext = {
5
6
  readonly profile: {
@@ -19,6 +20,36 @@ type DslQueryRuntimeContext = {
19
20
  }
20
21
 
21
22
  export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
23
+ const assertSelectionObject = (apiName: string, selection: any): void => {
24
+ if (
25
+ selection === null ||
26
+ typeof selection !== "object" ||
27
+ Array.isArray(selection) ||
28
+ Expression.TypeId in selection
29
+ ) {
30
+ throw new Error(`${apiName}(...) expects a projection object`)
31
+ }
32
+ }
33
+
34
+ const assertSelectionTree = (apiName: string, selection: any): void => {
35
+ const visit = (value: any, isRoot: boolean): void => {
36
+ if (value !== null && typeof value === "object" && Expression.TypeId in value) {
37
+ return
38
+ }
39
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
40
+ throw new Error(`${apiName}(...) selection leaves must be expressions`)
41
+ }
42
+ const nested = Object.values(value)
43
+ if (!isRoot && nested.length === 0) {
44
+ throw new Error(`${apiName}(...) projection objects cannot contain empty nested selections`)
45
+ }
46
+ for (const item of nested) {
47
+ visit(item, false)
48
+ }
49
+ }
50
+ visit(selection, true)
51
+ }
52
+
22
53
  const values = (rows: readonly [Record<string, any>, ...Record<string, any>[]]) => {
23
54
  if (rows.length === 0) {
24
55
  throw new Error("values(...) requires at least one row")
@@ -28,10 +59,14 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
28
59
  ...Record<string, Expression.Any>[]
29
60
  ]
30
61
  const columnNames = Object.keys(normalizedRows[0]!)
62
+ if (columnNames.length === 0) {
63
+ throw new Error("values(...) rows must specify at least one column")
64
+ }
65
+ const columnNameSet = new Set(columnNames)
31
66
  for (const row of normalizedRows) {
32
67
  const rowKeys = Object.keys(row)
33
- if (rowKeys.length !== columnNames.length || !rowKeys.every((key, index) => key === columnNames[index])) {
34
- throw new Error("values(...) rows must project the same columns in the same order")
68
+ if (rowKeys.length !== columnNames.length || !rowKeys.every((key) => columnNameSet.has(key))) {
69
+ throw new Error("values(...) rows must project the same columns")
35
70
  }
36
71
  }
37
72
  return Object.assign(Object.create(ctx.ValuesInputProto), {
@@ -97,8 +132,10 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
97
132
  return Object.assign(source, columns)
98
133
  }
99
134
 
100
- const select = (selection: any) =>
101
- ctx.makePlan({
135
+ const select = (selection: any = {}) => {
136
+ assertSelectionObject("select", selection)
137
+ assertSelectionTree("select", selection)
138
+ return ctx.makePlan({
102
139
  selection,
103
140
  required: ctx.extractRequiredRuntime(selection),
104
141
  available: {},
@@ -112,9 +149,13 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
112
149
  groupBy: [],
113
150
  orderBy: []
114
151
  }, undefined, "read", "select")
152
+ }
115
153
 
116
154
  const groupBy = (...values: readonly Expression.Any[]) =>
117
155
  (plan: any) => {
156
+ if (values.length === 0) {
157
+ throw new Error("groupBy(...) requires at least one expression")
158
+ }
118
159
  const current = plan[Plan.TypeId]
119
160
  const currentAst = ctx.getAst(plan)
120
161
  const currentQuery = ctx.getQueryState(plan)
@@ -132,11 +173,23 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
132
173
  }, currentQuery.assumptions, currentQuery.capabilities, currentQuery.statement)
133
174
  }
134
175
 
135
- const returning = (selection: any) =>
136
- (plan: any) => {
176
+ const returning = (selection: any) => {
177
+ assertSelectionObject("returning", selection)
178
+ assertSelectionTree("returning", selection)
179
+ if (flattenSelection(selection as Record<string, unknown>).length === 0) {
180
+ throw new Error("returning(...) requires at least one selected expression")
181
+ }
182
+ return (plan: any) => {
137
183
  const current = plan[Plan.TypeId]
138
184
  const currentAst = ctx.getAst(plan)
139
185
  const currentQuery = ctx.getQueryState(plan)
186
+ if (
187
+ currentQuery.statement !== "insert" &&
188
+ currentQuery.statement !== "update" &&
189
+ currentQuery.statement !== "delete"
190
+ ) {
191
+ throw new Error(`returning(...) is not supported for ${currentQuery.statement} statements`)
192
+ }
140
193
  return ctx.makePlan({
141
194
  selection,
142
195
  required: [...ctx.currentRequiredList(current.required), ...ctx.extractRequiredRuntime(selection)].filter((name, index, list) =>
@@ -148,6 +201,7 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
148
201
  select: selection
149
202
  }, currentQuery.assumptions, currentQuery.capabilities, currentQuery.statement, currentQuery.target, currentQuery.insertSource)
150
203
  }
204
+ }
151
205
 
152
206
  return {
153
207
  values,
@@ -1,4 +1,5 @@
1
1
  import * as Plan from "./row-set.js"
2
+ import * as Table from "./table.js"
2
3
 
3
4
  type DslTransactionDdlRuntimeContext = {
4
5
  readonly profile: {
@@ -10,9 +11,71 @@ type DslTransactionDdlRuntimeContext = {
10
11
  readonly defaultIndexName: (tableName: string, columns: readonly string[], unique: boolean) => string
11
12
  }
12
13
 
14
+ const allowedIsolationLevels = new Set(["read committed", "repeatable read", "serializable"])
15
+
16
+ export const renderTransactionIsolationLevel = (isolationLevel: unknown): string => {
17
+ if (isolationLevel === undefined) {
18
+ return ""
19
+ }
20
+ if (typeof isolationLevel !== "string" || !allowedIsolationLevels.has(isolationLevel)) {
21
+ throw new Error("Unsupported transaction isolation level")
22
+ }
23
+ return `isolation level ${isolationLevel}`
24
+ }
25
+
26
+ export const expectDdlClauseKind = <
27
+ Ddl extends { readonly kind: string },
28
+ Kind extends Ddl["kind"]
29
+ >(
30
+ 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
+ }
38
+
39
+ export const expectTruncateClause = <
40
+ Truncate extends { readonly kind: string }
41
+ >(
42
+ 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
+ }
49
+
50
+ const validateIsolationLevel = (isolationLevel: unknown): void => {
51
+ renderTransactionIsolationLevel(isolationLevel)
52
+ }
53
+
13
54
  export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContext) => {
14
- const transaction = (options: { readonly isolationLevel?: any; readonly readOnly?: boolean } = {}) =>
15
- ctx.makePlan({
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
+ }
63
+
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
+ }
75
+
76
+ const transaction = (options: { readonly isolationLevel?: any; readonly readOnly?: boolean } = {}) => {
77
+ validateIsolationLevel(options.isolationLevel)
78
+ return ctx.makePlan({
16
79
  selection: {},
17
80
  required: [],
18
81
  available: {},
@@ -31,6 +94,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
31
94
  groupBy: [],
32
95
  orderBy: []
33
96
  }, undefined, "transaction", "transaction")
97
+ }
34
98
 
35
99
  const commit = () =>
36
100
  ctx.makePlan({
@@ -131,6 +195,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
131
195
  }, undefined, "transaction", "releaseSavepoint")
132
196
 
133
197
  const createTable = (target: any, options: { readonly ifNotExists?: boolean } = {}) => {
198
+ assertTableTarget(target, "createTable")
134
199
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
135
200
  return ctx.makePlan({
136
201
  selection: {},
@@ -159,6 +224,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
159
224
  }
160
225
 
161
226
  const dropTable = (target: any, options: { readonly ifExists?: boolean } = {}) => {
227
+ assertTableTarget(target, "dropTable")
162
228
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
163
229
  return ctx.makePlan({
164
230
  selection: {},
@@ -187,7 +253,9 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
187
253
  }
188
254
 
189
255
  const createIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly unique?: boolean; readonly ifNotExists?: boolean } = {}) => {
256
+ assertTableTarget(target, "createIndex")
190
257
  const normalizedColumns = ctx.normalizeColumnList(columns)
258
+ validateIndexColumns(target, normalizedColumns)
191
259
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
192
260
  return ctx.makePlan({
193
261
  selection: {},
@@ -219,7 +287,9 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
219
287
  }
220
288
 
221
289
  const dropIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly ifExists?: boolean } = {}) => {
290
+ assertTableTarget(target, "dropIndex")
222
291
  const normalizedColumns = ctx.normalizeColumnList(columns)
292
+ validateIndexColumns(target, normalizedColumns)
223
293
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
224
294
  return ctx.makePlan({
225
295
  selection: {},
@@ -16,6 +16,8 @@ import * as Query from "./query.js"
16
16
  import * as QueryAst from "./query-ast.js"
17
17
  import * as Renderer from "./renderer.js"
18
18
  import * as Plan from "./row-set.js"
19
+ import { columnPredicateKey } from "./predicate/runtime.js"
20
+ import { isJsonValue } from "./runtime/normalize.js"
19
21
 
20
22
  /** Flat database row keyed by rendered projection aliases. */
21
23
  export type FlatRow = Readonly<Record<string, unknown>>
@@ -221,7 +223,7 @@ const effectiveRuntimeNullability = (
221
223
  return "always"
222
224
  }
223
225
  if (ast.kind === "column") {
224
- const key = `${ast.tableName}.${ast.columnName}`
226
+ const key = columnPredicateKey(ast.tableName, ast.columnName)
225
227
  if (scope.absentSourceNames.has(ast.tableName) || scope.nullKeys.has(key)) {
226
228
  return "always"
227
229
  }
@@ -237,6 +239,20 @@ const effectiveRuntimeNullability = (
237
239
  : nullability
238
240
  }
239
241
 
242
+ const dbTypeAllowsTopLevelJsonNull = (
243
+ dbType: Expression.DbType.Any
244
+ ): boolean => {
245
+ if ("base" in dbType) {
246
+ return dbTypeAllowsTopLevelJsonNull(dbType.base)
247
+ }
248
+ return ("variant" in dbType && dbType.variant === "json") || dbType.runtime === "json"
249
+ }
250
+
251
+ const schemaAcceptsNull = (
252
+ schema: Schema.Schema.Any | undefined
253
+ ): boolean =>
254
+ schema !== undefined && (Schema.is(schema) as (value: unknown) => boolean)(null)
255
+
240
256
  const decodeProjectionValue = (
241
257
  rendered: Renderer.RenderedQuery<any, any>,
242
258
  projection: Renderer.RenderedQuery<any, any>["projections"][number],
@@ -262,8 +278,12 @@ const decodeProjectionValue = (
262
278
  }
263
279
 
264
280
  const nullability = effectiveRuntimeNullability(expression, scope)
281
+ const schema = expressionRuntimeSchema(expression, { assumptions: scope.assumptions })
265
282
  if (normalized === null) {
266
283
  if (nullability === "never") {
284
+ if (dbTypeAllowsTopLevelJsonNull(expression[Expression.TypeId].dbType) && schemaAcceptsNull(schema)) {
285
+ return null
286
+ }
267
287
  throw makeRowDecodeError(
268
288
  rendered,
269
289
  projection,
@@ -289,7 +309,18 @@ const decodeProjectionValue = (
289
309
  )
290
310
  }
291
311
 
292
- const schema = expressionRuntimeSchema(expression, { assumptions: scope.assumptions })
312
+ if (dbTypeAllowsTopLevelJsonNull(expression[Expression.TypeId].dbType) && !isJsonValue(normalized)) {
313
+ throw makeRowDecodeError(
314
+ rendered,
315
+ projection,
316
+ expression,
317
+ raw,
318
+ "schema",
319
+ new Error("Expected a JSON value"),
320
+ normalized
321
+ )
322
+ }
323
+
293
324
  if (schema === undefined) {
294
325
  return normalized
295
326
  }
@@ -316,8 +347,8 @@ export const makeRowDecoder = (
316
347
  const projections = flattenSelection(
317
348
  Query.getAst(plan).select as Record<string, unknown>
318
349
  )
319
- const byAlias = new Map(
320
- projections.map((projection) => [projection.alias, projection.expression] as const)
350
+ const byPath = new Map(
351
+ projections.map((projection) => [JSON.stringify(projection.path), projection.expression] as const)
321
352
  )
322
353
  const driverMode = options.driverMode ?? "raw"
323
354
  const valueMappings = options.valueMappings ?? rendered.valueMappings
@@ -325,12 +356,19 @@ export const makeRowDecoder = (
325
356
  return (row) => {
326
357
  const decoded: Record<string, unknown> = {}
327
358
  for (const projection of rendered.projections) {
328
- if (!(projection.alias in row)) {
329
- continue
330
- }
331
- const expression = byAlias.get(projection.alias)
359
+ const expression = byPath.get(JSON.stringify(projection.path))
332
360
  if (expression === undefined) {
333
- continue
361
+ throw new Error(`Rendered projection path '${projection.path.join(".")}' does not exist in the query selection`)
362
+ }
363
+ if (!(projection.alias in row)) {
364
+ throw makeRowDecodeError(
365
+ rendered,
366
+ projection,
367
+ expression,
368
+ undefined,
369
+ "schema",
370
+ new Error(`Missing required projection alias '${projection.alias}'`)
371
+ )
334
372
  }
335
373
  setPath(
336
374
  decoded,
@@ -36,11 +36,12 @@ export interface CastNode<
36
36
 
37
37
  /** Explicit collation captured by the internal expression AST. */
38
38
  export interface CollateNode<
39
- Value extends Expression.Any = Expression.Any
39
+ Value extends Expression.Any = Expression.Any,
40
+ Collation extends readonly [string, ...string[]] = readonly [string, ...string[]]
40
41
  > {
41
42
  readonly kind: "collate"
42
43
  readonly value: Value
43
- readonly collation: readonly [string, ...string[]]
44
+ readonly collation: Collation
44
45
  }
45
46
 
46
47
  /** General SQL function call captured by the internal expression AST. */
@@ -1,5 +1,23 @@
1
1
  import * as Expression from "./scalar.js"
2
2
  import * as ExpressionAst from "./expression-ast.js"
3
+ import * as JsonPath from "./json/path.js"
4
+ import { columnPredicateKey } from "./predicate/runtime.js"
5
+
6
+ const subqueryPlanIds = new WeakMap<object, string>()
7
+ let nextSubqueryPlanId = 0
8
+
9
+ const subqueryPlanGroupingKey = (plan: unknown): string => {
10
+ if (plan === null || typeof plan !== "object") {
11
+ return "unknown"
12
+ }
13
+ const existing = subqueryPlanIds.get(plan)
14
+ if (existing !== undefined) {
15
+ return existing
16
+ }
17
+ const next = `${nextSubqueryPlanId++}`
18
+ subqueryPlanIds.set(plan, next)
19
+ return next
20
+ }
3
21
 
4
22
  const literalGroupingKey = (value: unknown): string => {
5
23
  if (value instanceof Date) {
@@ -20,17 +38,84 @@ const literalGroupingKey = (value: unknown): string => {
20
38
  }
21
39
  }
22
40
 
41
+ const isExpression = (value: unknown): value is Expression.Any =>
42
+ value !== null && typeof value === "object" && Expression.TypeId in value
43
+
44
+ const expressionGroupingKey = (value: unknown): string =>
45
+ isExpression(value) ? groupingKeyOfExpression(value) : "missing"
46
+
47
+ const escapeGroupingText = (value: string): string =>
48
+ value
49
+ .replace(/\\/g, "\\\\")
50
+ .replace(/,/g, "\\,")
51
+ .replace(/\|/g, "\\|")
52
+ .replace(/=/g, "\\=")
53
+ .replace(/>/g, "\\>")
54
+
55
+ const jsonSegmentGroupingKey = (segment: unknown): string => {
56
+ if (segment !== null && typeof segment === "object" && "kind" in segment) {
57
+ switch ((segment as { readonly kind: string }).kind) {
58
+ case "key":
59
+ return `key:${escapeGroupingText((segment as JsonPath.KeySegment).key)}`
60
+ case "index":
61
+ return `index:${(segment as JsonPath.IndexSegment).index}`
62
+ case "wildcard":
63
+ return "wildcard"
64
+ case "slice": {
65
+ const slice = segment as JsonPath.SliceSegment
66
+ return `slice:${slice.start ?? ""}:${slice.end ?? ""}`
67
+ }
68
+ case "descend":
69
+ return "descend"
70
+ }
71
+ }
72
+ if (typeof segment === "string") {
73
+ return `key:${escapeGroupingText(segment)}`
74
+ }
75
+ if (typeof segment === "number") {
76
+ return `index:${segment}`
77
+ }
78
+ return "unknown"
79
+ }
80
+
81
+ const jsonPathGroupingKey = (segments: readonly unknown[] | undefined): string =>
82
+ (segments ?? []).map(jsonSegmentGroupingKey).join(",")
83
+
84
+ const isJsonPath = (value: unknown): value is JsonPath.Path =>
85
+ value !== null && typeof value === "object" && JsonPath.TypeId in value
86
+
87
+ const jsonOpaquePathGroupingKey = (value: unknown): string => {
88
+ if (isJsonPath(value)) {
89
+ return `jsonpath:${jsonPathGroupingKey(value.segments)}`
90
+ }
91
+ if (typeof value === "string") {
92
+ return `jsonpath:${escapeGroupingText(value)}`
93
+ }
94
+ if (isExpression(value)) {
95
+ return `jsonpath:${groupingKeyOfExpression(value)}`
96
+ }
97
+ return "jsonpath:unknown"
98
+ }
99
+
100
+ const jsonEntryGroupingKey = (
101
+ entry: { readonly key: string; readonly value: Expression.Any }
102
+ ): string => `${escapeGroupingText(entry.key)}=>${groupingKeyOfExpression(entry.value)}`
103
+
23
104
  export const groupingKeyOfExpression = (expression: Expression.Any): string => {
24
105
  const ast = (expression as Expression.Any & {
25
106
  readonly [ExpressionAst.TypeId]: ExpressionAst.Any
26
107
  })[ExpressionAst.TypeId]
27
108
  switch (ast.kind) {
28
109
  case "column":
29
- return `column:${ast.tableName}.${ast.columnName}`
110
+ return `column:${columnPredicateKey(ast.tableName, ast.columnName)}`
30
111
  case "literal":
31
112
  return `literal:${literalGroupingKey(ast.value)}`
32
113
  case "cast":
33
114
  return `cast(${groupingKeyOfExpression(ast.value)} as ${ast.target.dialect}:${ast.target.kind})`
115
+ case "collate":
116
+ return `collate(${groupingKeyOfExpression(ast.value)},${ast.collation.map(escapeGroupingText).join(",")})`
117
+ case "function":
118
+ return `function(${escapeGroupingText(ast.name)},${ast.args.map(groupingKeyOfExpression).join(",")})`
34
119
  case "isNull":
35
120
  case "isNotNull":
36
121
  case "not":
@@ -48,8 +133,15 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
48
133
  case "gte":
49
134
  case "like":
50
135
  case "ilike":
136
+ case "regexMatch":
137
+ case "regexIMatch":
138
+ case "regexNotMatch":
139
+ case "regexNotIMatch":
51
140
  case "isDistinctFrom":
52
141
  case "isNotDistinctFrom":
142
+ case "contains":
143
+ case "containedBy":
144
+ case "overlaps":
53
145
  return `${ast.kind}(${groupingKeyOfExpression(ast.left)},${groupingKeyOfExpression(ast.right)})`
54
146
  case "and":
55
147
  case "or":
@@ -62,6 +154,54 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
62
154
  case "case":
63
155
  return `case(${ast.branches.map((branch: ExpressionAst.CaseBranchNode) =>
64
156
  `when:${groupingKeyOfExpression(branch.when)}=>${groupingKeyOfExpression(branch.then)}`).join("|")};else:${groupingKeyOfExpression(ast.else)})`
157
+ case "exists":
158
+ return `exists(${subqueryPlanGroupingKey(ast.plan)})`
159
+ case "scalarSubquery":
160
+ return `scalarSubquery(${subqueryPlanGroupingKey(ast.plan)})`
161
+ case "inSubquery":
162
+ return `inSubquery(${groupingKeyOfExpression(ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
163
+ case "comparisonAny":
164
+ case "comparisonAll":
165
+ return `${ast.kind}(${ast.operator},${groupingKeyOfExpression(ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
166
+ case "jsonGet":
167
+ case "jsonPath":
168
+ case "jsonAccess":
169
+ case "jsonTraverse":
170
+ case "jsonGetText":
171
+ case "jsonPathText":
172
+ case "jsonAccessText":
173
+ case "jsonTraverseText":
174
+ return `json(${ast.kind},${expressionGroupingKey(ast.base)},${jsonPathGroupingKey(ast.segments)})`
175
+ case "jsonHasKey":
176
+ case "jsonKeyExists":
177
+ case "jsonHasAnyKeys":
178
+ case "jsonHasAllKeys":
179
+ return `json(${ast.kind},${expressionGroupingKey(ast.base)},${(ast.keys ?? []).map(escapeGroupingText).join(",")})`
180
+ case "jsonConcat":
181
+ case "jsonMerge":
182
+ return `json(${ast.kind},${expressionGroupingKey(ast.left)},${expressionGroupingKey(ast.right)},)`
183
+ case "jsonDelete":
184
+ case "jsonDeletePath":
185
+ case "jsonRemove":
186
+ return `json(${ast.kind},${expressionGroupingKey(ast.base)},${expressionGroupingKey(undefined)},${jsonPathGroupingKey(ast.segments)})`
187
+ case "jsonSet":
188
+ return `json(${ast.kind},${expressionGroupingKey(ast.base)},${expressionGroupingKey(ast.newValue)},${jsonPathGroupingKey(ast.segments)})`
189
+ case "jsonInsert":
190
+ return `json(${ast.kind},${expressionGroupingKey(ast.base)},${expressionGroupingKey(ast.insert)},${jsonPathGroupingKey(ast.segments)})`
191
+ case "jsonPathExists":
192
+ case "jsonPathMatch":
193
+ return `json(${ast.kind},${expressionGroupingKey(ast.base)},${jsonOpaquePathGroupingKey(ast.query)})`
194
+ case "jsonBuildObject":
195
+ return `json(${ast.kind},${(ast.entries ?? []).map(jsonEntryGroupingKey).join("|")})`
196
+ case "jsonBuildArray":
197
+ return `json(${ast.kind},${(ast.values ?? []).map(groupingKeyOfExpression).join(",")})`
198
+ case "jsonToJson":
199
+ case "jsonToJsonb":
200
+ case "jsonTypeOf":
201
+ case "jsonLength":
202
+ case "jsonKeys":
203
+ case "jsonStripNulls":
204
+ return `json(${ast.kind},${expressionGroupingKey(ast.value)})`
65
205
  default:
66
206
  throw new Error("Unsupported expression for grouping key generation")
67
207
  }
@@ -5,6 +5,7 @@ import * as Table from "./table.js"
5
5
  import type { PredicateFormula } from "./predicate/formula.js"
6
6
  import {
7
7
  assumeFormulaTrue,
8
+ columnPredicateKey,
8
9
  contradictsFormula,
9
10
  guaranteedNonNullKeys,
10
11
  guaranteedNullKeys,
@@ -43,7 +44,7 @@ const collectPresenceWitnesses = (
43
44
  const expression = selection as unknown as AstBackedExpression
44
45
  const ast = expression[ExpressionAst.TypeId]
45
46
  if (ast.kind === "column" && expression[Expression.TypeId].nullability === "never") {
46
- output.add(`${ast.tableName}.${ast.columnName}`)
47
+ output.add(columnPredicateKey(ast.tableName, ast.columnName))
47
48
  }
48
49
  return
49
50
  }