effect-qb 0.14.0 → 0.15.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 (141) hide show
  1. package/dist/mysql.js +61270 -4259
  2. package/dist/postgres/metadata.js +176 -82
  3. package/dist/postgres.js +5903 -3316
  4. package/package.json +14 -1
  5. package/src/internal/aggregation-validation.ts +3 -3
  6. package/src/internal/case-analysis.d.ts +18 -0
  7. package/src/internal/case-analysis.ts +4 -4
  8. package/src/internal/coercion/analysis.d.ts +7 -0
  9. package/src/internal/{coercion-analysis.ts → coercion/analysis.ts} +3 -3
  10. package/src/internal/coercion/errors.d.ts +17 -0
  11. package/src/internal/{coercion-errors.ts → coercion/errors.ts} +1 -1
  12. package/src/internal/coercion/kind.d.ts +4 -0
  13. package/src/internal/{coercion-kind.ts → coercion/kind.ts} +2 -2
  14. package/src/internal/{coercion-normalize.ts → coercion/normalize.ts} +1 -1
  15. package/src/internal/coercion/rules.d.ts +6 -0
  16. package/src/internal/{coercion-rules.ts → coercion/rules.ts} +2 -2
  17. package/src/internal/column-state.d.ts +190 -0
  18. package/src/internal/column-state.ts +36 -47
  19. package/src/internal/column.ts +21 -305
  20. package/src/internal/datatypes/define.d.ts +17 -0
  21. package/src/internal/datatypes/define.ts +18 -4
  22. package/src/internal/datatypes/lookup.d.ts +44 -0
  23. package/src/internal/datatypes/lookup.ts +61 -152
  24. package/src/internal/datatypes/shape.d.ts +16 -0
  25. package/src/internal/datatypes/shape.ts +1 -1
  26. package/src/internal/derived-table.d.ts +4 -0
  27. package/src/internal/derived-table.ts +21 -16
  28. package/src/internal/dsl-mutation-runtime.ts +378 -0
  29. package/src/internal/dsl-plan-runtime.ts +387 -0
  30. package/src/internal/dsl-query-runtime.ts +160 -0
  31. package/src/internal/dsl-transaction-ddl-runtime.ts +263 -0
  32. package/src/internal/executor.ts +132 -31
  33. package/src/internal/expression-ast.ts +15 -5
  34. package/src/internal/grouping-key.d.ts +3 -0
  35. package/src/internal/grouping-key.ts +1 -1
  36. package/src/internal/implication-runtime.d.ts +15 -0
  37. package/src/internal/implication-runtime.ts +4 -4
  38. package/src/internal/json/ast.d.ts +30 -0
  39. package/src/internal/json/ast.ts +1 -1
  40. package/src/internal/json/errors.d.ts +8 -0
  41. package/src/internal/json/path.d.ts +75 -0
  42. package/src/internal/json/path.ts +1 -1
  43. package/src/internal/json/types.d.ts +62 -0
  44. package/src/internal/predicate/analysis.d.ts +20 -0
  45. package/src/internal/{predicate-analysis.ts → predicate/analysis.ts} +3 -3
  46. package/src/internal/predicate/atom.d.ts +28 -0
  47. package/src/internal/{predicate-branches.ts → predicate/branches.ts} +2 -2
  48. package/src/internal/predicate/context.d.ts +67 -0
  49. package/src/internal/{predicate-context.ts → predicate/context.ts} +8 -5
  50. package/src/internal/predicate/formula.d.ts +35 -0
  51. package/src/internal/{predicate-formula.ts → predicate/formula.ts} +1 -1
  52. package/src/internal/predicate/key.d.ts +11 -0
  53. package/src/internal/{predicate-key.ts → predicate/key.ts} +2 -2
  54. package/src/internal/{predicate-nnf.ts → predicate/nnf.ts} +2 -2
  55. package/src/internal/predicate/normalize.d.ts +53 -0
  56. package/src/internal/{predicate-normalize.ts → predicate/normalize.ts} +19 -19
  57. package/src/internal/predicate/runtime.d.ts +31 -0
  58. package/src/internal/{predicate-runtime.ts → predicate/runtime.ts} +9 -6
  59. package/src/internal/projection-alias.d.ts +13 -0
  60. package/src/internal/projections.d.ts +31 -0
  61. package/src/internal/projections.ts +1 -1
  62. package/src/internal/query-ast.d.ts +217 -0
  63. package/src/internal/query-ast.ts +1 -1
  64. package/src/internal/query-requirements.d.ts +20 -0
  65. package/src/internal/query.d.ts +775 -0
  66. package/src/internal/query.ts +389 -313
  67. package/src/internal/renderer.ts +7 -21
  68. package/src/internal/row-set.d.ts +53 -0
  69. package/src/internal/{plan.ts → row-set.ts} +11 -9
  70. package/src/internal/{runtime-normalize.ts → runtime/normalize.ts} +9 -31
  71. package/src/internal/{runtime-schema.ts → runtime/schema.ts} +13 -38
  72. package/src/internal/runtime/value.d.ts +22 -0
  73. package/src/internal/{runtime-value.ts → runtime/value.ts} +2 -2
  74. package/src/internal/scalar.d.ts +107 -0
  75. package/src/internal/scalar.ts +191 -0
  76. package/src/internal/schema-derivation.d.ts +105 -0
  77. package/src/internal/schema-expression.d.ts +18 -0
  78. package/src/internal/schema-expression.ts +38 -7
  79. package/src/internal/table-options.d.ts +94 -0
  80. package/src/internal/table-options.ts +8 -2
  81. package/src/internal/table.d.ts +173 -0
  82. package/src/internal/table.ts +32 -14
  83. package/src/mysql/column.ts +94 -18
  84. package/src/mysql/datatypes/index.ts +47 -7
  85. package/src/mysql/errors/generated.ts +57336 -0
  86. package/src/mysql/errors/index.ts +1 -0
  87. package/src/mysql/errors/normalize.ts +55 -53
  88. package/src/mysql/errors/types.ts +74 -0
  89. package/src/mysql/executor.ts +69 -7
  90. package/src/mysql/function/aggregate.ts +1 -5
  91. package/src/mysql/function/core.ts +1 -4
  92. package/src/mysql/function/index.ts +0 -1
  93. package/src/mysql/function/string.ts +1 -5
  94. package/src/mysql/function/temporal.ts +12 -15
  95. package/src/mysql/function/window.ts +1 -6
  96. package/src/{internal/mysql-dialect.ts → mysql/internal/dialect.ts} +1 -1
  97. package/src/{internal/mysql-query.ts → mysql/internal/dsl.ts} +1024 -2082
  98. package/src/{internal/mysql-renderer.ts → mysql/internal/renderer.ts} +6 -6
  99. package/src/mysql/internal/sql-expression-renderer.ts +1455 -0
  100. package/src/mysql/json.ts +2 -0
  101. package/src/mysql/query.ts +111 -91
  102. package/src/mysql/renderer.ts +1 -1
  103. package/src/mysql/table.ts +1 -1
  104. package/src/mysql.ts +6 -4
  105. package/src/postgres/cast.ts +11 -12
  106. package/src/postgres/column.ts +178 -46
  107. package/src/postgres/datatypes/index.d.ts +515 -0
  108. package/src/postgres/datatypes/index.ts +22 -13
  109. package/src/postgres/datatypes/spec.d.ts +412 -0
  110. package/src/postgres/errors/generated.ts +2636 -0
  111. package/src/postgres/errors/index.ts +1 -0
  112. package/src/postgres/errors/normalize.ts +47 -62
  113. package/src/postgres/errors/types.ts +92 -34
  114. package/src/postgres/executor.ts +37 -5
  115. package/src/postgres/function/aggregate.ts +1 -5
  116. package/src/postgres/function/core.ts +12 -6
  117. package/src/postgres/function/index.ts +0 -1
  118. package/src/postgres/function/string.ts +1 -5
  119. package/src/postgres/function/temporal.ts +12 -15
  120. package/src/postgres/function/window.ts +1 -6
  121. package/src/{internal/postgres-dialect.ts → postgres/internal/dialect.ts} +1 -1
  122. package/src/{internal/postgres-query.ts → postgres/internal/dsl.ts} +1085 -2089
  123. package/src/{internal/postgres-renderer.ts → postgres/internal/renderer.ts} +6 -6
  124. package/src/postgres/internal/schema-ddl.ts +108 -0
  125. package/src/{internal/postgres-schema-model.ts → postgres/internal/schema-model.ts} +12 -6
  126. package/src/{internal → postgres/internal}/sql-expression-renderer.ts +19 -17
  127. package/src/postgres/{function/json.ts → json.ts} +77 -85
  128. package/src/postgres/metadata.ts +2 -2
  129. package/src/postgres/query.ts +113 -89
  130. package/src/postgres/renderer.ts +3 -13
  131. package/src/postgres/schema-expression.ts +2 -1
  132. package/src/postgres/schema-management.ts +1 -1
  133. package/src/postgres/table.ts +12 -4
  134. package/src/postgres/type.ts +29 -2
  135. package/src/postgres.ts +6 -4
  136. package/src/internal/expression.ts +0 -327
  137. package/src/internal/schema-ddl.ts +0 -55
  138. package/src/mysql/function/json.ts +0 -4
  139. package/src/mysql/private/query.ts +0 -1
  140. package/src/postgres/private/query.ts +0 -1
  141. /package/src/internal/{predicate-atom.ts → predicate/atom.ts} +0 -0
@@ -0,0 +1,1455 @@
1
+ import * as Query from "../../internal/query.js"
2
+ import * as Expression from "../../internal/scalar.js"
3
+ import * as Table from "../../internal/table.js"
4
+ import * as QueryAst from "../../internal/query-ast.js"
5
+ import type { RenderState, SqlDialect } from "../../internal/dialect.js"
6
+ import * as ExpressionAst from "../../internal/expression-ast.js"
7
+ import * as JsonPath from "../../internal/json/path.js"
8
+ import { flattenSelection, type Projection } from "../../internal/projections.js"
9
+ import { type SelectionValue, validateAggregationSelection } from "../../internal/aggregation-validation.js"
10
+ import * as SchemaExpression from "../../internal/schema-expression.js"
11
+ import type { DdlExpressionLike } from "../../internal/table-options.js"
12
+
13
+ const renderDbType = (
14
+ dialect: SqlDialect,
15
+ dbType: Expression.DbType.Any
16
+ ): string => {
17
+ if (dialect.name === "mysql" && dbType.dialect === "mysql" && dbType.kind === "uuid") {
18
+ return "char(36)"
19
+ }
20
+ return dbType.kind
21
+ }
22
+
23
+ const renderCastType = (
24
+ dialect: SqlDialect,
25
+ dbType: Expression.DbType.Any
26
+ ): string => {
27
+ if (dialect.name !== "mysql") {
28
+ return dbType.kind
29
+ }
30
+ switch (dbType.kind) {
31
+ case "text":
32
+ return "char"
33
+ case "uuid":
34
+ return "char(36)"
35
+ case "numeric":
36
+ return "decimal"
37
+ case "timestamp":
38
+ return "datetime"
39
+ case "bool":
40
+ case "boolean":
41
+ return "boolean"
42
+ case "json":
43
+ return "json"
44
+ default:
45
+ return dbType.kind
46
+ }
47
+ }
48
+
49
+ const renderDdlExpression = (
50
+ expression: DdlExpressionLike,
51
+ state: RenderState,
52
+ dialect: SqlDialect
53
+ ): string =>
54
+ SchemaExpression.isSchemaExpression(expression)
55
+ ? SchemaExpression.render(expression)
56
+ : renderExpression(expression, state, dialect)
57
+
58
+ const renderMysqlMutationLimit = (
59
+ expression: Expression.Any,
60
+ state: RenderState,
61
+ dialect: SqlDialect
62
+ ): string => {
63
+ const ast = (expression as Expression.Any & {
64
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
65
+ })[ExpressionAst.TypeId]
66
+ if (ast.kind === "literal" && typeof ast.value === "number" && Number.isInteger(ast.value) && ast.value >= 0) {
67
+ return String(ast.value)
68
+ }
69
+ return renderExpression(expression, state, dialect)
70
+ }
71
+
72
+ const renderColumnDefinition = (
73
+ dialect: SqlDialect,
74
+ state: RenderState,
75
+ columnName: string,
76
+ column: Table.AnyTable[typeof Table.TypeId]["fields"][string]
77
+ ): string => {
78
+ const clauses = [
79
+ dialect.quoteIdentifier(columnName),
80
+ column.metadata.ddlType ?? renderDbType(dialect, column.metadata.dbType)
81
+ ]
82
+ if (column.metadata.identity) {
83
+ clauses.push(`generated ${column.metadata.identity.generation === "byDefault" ? "by default" : "always"} as identity`)
84
+ } else if (column.metadata.generatedValue) {
85
+ clauses.push(`generated always as (${renderDdlExpression(column.metadata.generatedValue, state, dialect)}) stored`)
86
+ } else if (column.metadata.defaultValue) {
87
+ clauses.push(`default ${renderDdlExpression(column.metadata.defaultValue, state, dialect)}`)
88
+ }
89
+ if (!column.metadata.nullable) {
90
+ clauses.push("not null")
91
+ }
92
+ return clauses.join(" ")
93
+ }
94
+
95
+ const renderCreateTableSql = (
96
+ targetSource: QueryAst.FromClause,
97
+ state: RenderState,
98
+ dialect: SqlDialect,
99
+ ifNotExists: boolean
100
+ ): string => {
101
+ const table = targetSource.source as Table.AnyTable
102
+ const fields = table[Table.TypeId].fields
103
+ const definitions = Object.entries(fields).map(([columnName, column]) =>
104
+ renderColumnDefinition(dialect, state, columnName, column)
105
+ )
106
+ for (const option of table[Table.OptionsSymbol]) {
107
+ switch (option.kind) {
108
+ case "primaryKey":
109
+ definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}primary key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
110
+ break
111
+ case "unique":
112
+ definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}unique${option.nullsNotDistinct ? " nulls not distinct" : ""} (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
113
+ break
114
+ case "foreignKey": {
115
+ const reference = option.references()
116
+ definitions.push(
117
+ `${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}foreign key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")}) references ${dialect.renderTableReference(reference.tableName, reference.tableName, reference.schemaName)} (${reference.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.onDelete ? ` on delete ${option.onDelete.replace(/[A-Z]/g, (value) => ` ${value.toLowerCase()}`).trim()}` : ""}${option.onUpdate ? ` on update ${option.onUpdate.replace(/[A-Z]/g, (value) => ` ${value.toLowerCase()}`).trim()}` : ""}${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`
118
+ )
119
+ break
120
+ }
121
+ case "check":
122
+ definitions.push(
123
+ `constraint ${dialect.quoteIdentifier(option.name)} check (${renderDdlExpression(option.predicate, state, dialect)})${option.noInherit ? " no inherit" : ""}`
124
+ )
125
+ break
126
+ case "index":
127
+ break
128
+ }
129
+ }
130
+ return `create table${ifNotExists ? " if not exists" : ""} ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${definitions.join(", ")})`
131
+ }
132
+
133
+ const renderCreateIndexSql = (
134
+ targetSource: QueryAst.FromClause,
135
+ ddl: Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
136
+ state: RenderState,
137
+ dialect: SqlDialect
138
+ ): string => {
139
+ const maybeIfNotExists = dialect.name === "postgres" && ddl.ifNotExists ? " if not exists" : ""
140
+ return `create${ddl.unique ? " unique" : ""} index${maybeIfNotExists} ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${ddl.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`
141
+ }
142
+
143
+ const renderDropIndexSql = (
144
+ targetSource: QueryAst.FromClause,
145
+ ddl: Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
146
+ state: RenderState,
147
+ dialect: SqlDialect
148
+ ): string =>
149
+ dialect.name === "postgres"
150
+ ? `drop index${ddl.ifExists ? " if exists" : ""} ${dialect.quoteIdentifier(ddl.name)}`
151
+ : `drop index ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
152
+
153
+ const isExpression = (value: unknown): value is Expression.Any =>
154
+ value !== null && typeof value === "object" && Expression.TypeId in value
155
+
156
+ const isJsonDbType = (dbType: Expression.DbType.Any): boolean =>
157
+ dbType.kind === "jsonb" || dbType.kind === "json" || ("variant" in dbType && dbType.variant === "json")
158
+
159
+ const isJsonExpression = (value: unknown): value is Expression.Any =>
160
+ isExpression(value) && isJsonDbType(value[Expression.TypeId].dbType)
161
+
162
+ const unsupportedJsonFeature = (
163
+ dialect: SqlDialect,
164
+ feature: string
165
+ ): never => {
166
+ const error = new Error(`Unsupported JSON feature for ${dialect.name}: ${feature}`) as Error & {
167
+ readonly tag: string
168
+ readonly dialect: string
169
+ readonly feature: string
170
+ }
171
+ Object.assign(error, {
172
+ tag: `@${dialect.name}/unsupported/json-feature`,
173
+ dialect: dialect.name,
174
+ feature
175
+ })
176
+ throw error
177
+ }
178
+
179
+ const extractJsonBase = (node: Record<string, unknown>): unknown =>
180
+ node.value ?? node.base ?? node.input ?? node.left ?? node.target
181
+
182
+ const isJsonPathValue = (value: unknown): value is JsonPath.Path<any> =>
183
+ value !== null && typeof value === "object" && JsonPath.TypeId in value
184
+
185
+ const extractJsonPathSegments = (node: Record<string, unknown>): ReadonlyArray<JsonPath.AnySegment> => {
186
+ const path = node.path ?? node.segments ?? node.keys
187
+ if (isJsonPathValue(path)) {
188
+ return path.segments
189
+ }
190
+ if (Array.isArray(path)) {
191
+ return path as readonly JsonPath.AnySegment[]
192
+ }
193
+ if ("key" in node) {
194
+ return [JsonPath.key(String(node.key))]
195
+ }
196
+ if ("segment" in node) {
197
+ const segment = node.segment
198
+ if (typeof segment === "string") {
199
+ return [JsonPath.key(segment)]
200
+ }
201
+ if (typeof segment === "number") {
202
+ return [JsonPath.index(segment)]
203
+ }
204
+ if (segment !== null && typeof segment === "object" && JsonPath.SegmentTypeId in segment) {
205
+ return [segment as JsonPath.AnySegment]
206
+ }
207
+ return []
208
+ }
209
+ if ("right" in node && isJsonPathValue(node.right)) {
210
+ return node.right.segments
211
+ }
212
+ return []
213
+ }
214
+
215
+ const extractJsonValue = (node: Record<string, unknown>): unknown =>
216
+ node.newValue ?? node.insert ?? node.right
217
+
218
+ const renderJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
219
+ if (typeof segment === "string") {
220
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)
221
+ ? `.${segment}`
222
+ : `."${segment.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
223
+ }
224
+ if (typeof segment === "number") {
225
+ return `[${segment}]`
226
+ }
227
+ switch (segment.kind) {
228
+ case "key":
229
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment.key)
230
+ ? `.${segment.key}`
231
+ : `."${segment.key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
232
+ case "index":
233
+ return `[${segment.index}]`
234
+ case "wildcard":
235
+ return "[*]"
236
+ case "slice":
237
+ return `[${segment.start ?? 0} to ${segment.end ?? "last"}]`
238
+ case "descend":
239
+ return ".**"
240
+ default:
241
+ throw new Error("Unsupported JSON path segment")
242
+ }
243
+ }
244
+
245
+ const renderJsonPathStringLiteral = (segments: ReadonlyArray<JsonPath.AnySegment | string | number>): string => {
246
+ let path = "$"
247
+ for (const segment of segments) {
248
+ path += renderJsonPathSegment(segment)
249
+ }
250
+ return path
251
+ }
252
+
253
+ const renderMySqlJsonPath = (
254
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
255
+ state: RenderState,
256
+ dialect: SqlDialect
257
+ ): string => dialect.renderLiteral(renderJsonPathStringLiteral(segments), state)
258
+
259
+ const renderPostgresJsonPathArray = (
260
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
261
+ state: RenderState,
262
+ dialect: SqlDialect
263
+ ): string => `array[${segments.map((segment) => {
264
+ if (typeof segment === "string") {
265
+ return dialect.renderLiteral(segment, state)
266
+ }
267
+ if (typeof segment === "number") {
268
+ return dialect.renderLiteral(String(segment), state)
269
+ }
270
+ switch (segment.kind) {
271
+ case "key":
272
+ return dialect.renderLiteral(segment.key, state)
273
+ case "index":
274
+ return dialect.renderLiteral(String(segment.index), state)
275
+ default:
276
+ throw new Error("Postgres JSON traversal requires exact key/index segments")
277
+ }
278
+ }).join(", ")}]`
279
+
280
+ const renderPostgresTextLiteral = (
281
+ value: string,
282
+ state: RenderState,
283
+ dialect: SqlDialect
284
+ ): string => `cast(${dialect.renderLiteral(value, state)} as text)`
285
+
286
+ const renderPostgresJsonAccessStep = (
287
+ segment: JsonPath.AnySegment,
288
+ textMode: boolean,
289
+ state: RenderState,
290
+ dialect: SqlDialect
291
+ ): string => {
292
+ switch (segment.kind) {
293
+ case "key":
294
+ return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(segment.key, state)}`
295
+ case "index":
296
+ return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(String(segment.index), state)}`
297
+ default:
298
+ throw new Error("Postgres exact JSON access requires key/index segments")
299
+ }
300
+ }
301
+
302
+ const renderPostgresJsonValue = (
303
+ value: unknown,
304
+ state: RenderState,
305
+ dialect: SqlDialect
306
+ ): string => {
307
+ if (!isExpression(value)) {
308
+ throw new Error("Expected a JSON expression")
309
+ }
310
+ const rendered = renderExpression(value, state, dialect)
311
+ return value[Expression.TypeId].dbType.kind === "jsonb"
312
+ ? rendered
313
+ : `cast(${rendered} as jsonb)`
314
+ }
315
+
316
+ const renderPostgresJsonKind = (
317
+ value: Expression.Any
318
+ ): "json" | "jsonb" => value[Expression.TypeId].dbType.kind === "jsonb" ? "jsonb" : "json"
319
+
320
+ const renderJsonOpaquePath = (
321
+ value: unknown,
322
+ state: RenderState,
323
+ dialect: SqlDialect
324
+ ): string => {
325
+ if (isJsonPathValue(value)) {
326
+ return dialect.renderLiteral(renderJsonPathStringLiteral(value.segments), state)
327
+ }
328
+ if (typeof value === "string") {
329
+ return dialect.renderLiteral(value, state)
330
+ }
331
+ if (isExpression(value)) {
332
+ return renderExpression(value, state, dialect)
333
+ }
334
+ throw new Error("Unsupported SQL/JSON path input")
335
+ }
336
+
337
+ const renderFunctionCall = (
338
+ name: string,
339
+ args: readonly Expression.Any[],
340
+ state: RenderState,
341
+ dialect: SqlDialect
342
+ ): string => {
343
+ if (name === "array") {
344
+ return `ARRAY[${args.map((arg) => renderExpression(arg, state, dialect)).join(", ")}]`
345
+ }
346
+ if (name === "extract" && args.length === 2) {
347
+ const field = args[0]
348
+ const source = args[1]
349
+ if (field === undefined) {
350
+ throw new Error("Unsupported SQL extract expression")
351
+ }
352
+ if (source === undefined) {
353
+ throw new Error("Unsupported SQL extract expression")
354
+ }
355
+ const fieldRuntime = isExpression(field) && field[Expression.TypeId].dbType.kind === "text" && typeof field[Expression.TypeId].runtime === "string"
356
+ ? field[Expression.TypeId].runtime
357
+ : undefined
358
+ const renderedField = fieldRuntime ?? renderExpression(field, state, dialect)
359
+ return `extract(${renderedField} from ${renderExpression(source, state, dialect)})`
360
+ }
361
+ const renderedArgs = args.map((arg) => renderExpression(arg, state, dialect)).join(", ")
362
+ if (args.length === 0) {
363
+ switch (name) {
364
+ case "current_date":
365
+ case "current_time":
366
+ case "current_timestamp":
367
+ case "localtime":
368
+ case "localtimestamp":
369
+ return name
370
+ default:
371
+ return `${name}()`
372
+ }
373
+ }
374
+ return `${name}(${renderedArgs})`
375
+ }
376
+
377
+ const renderJsonExpression = (
378
+ expression: Expression.Any,
379
+ ast: Record<string, unknown>,
380
+ state: RenderState,
381
+ dialect: SqlDialect
382
+ ): string | undefined => {
383
+ const kind = typeof ast.kind === "string" ? ast.kind : undefined
384
+ if (!kind) {
385
+ return undefined
386
+ }
387
+
388
+ const base = extractJsonBase(ast)
389
+ const segments = extractJsonPathSegments(ast)
390
+ const exact = segments.every((segment) => segment.kind === "key" || segment.kind === "index")
391
+ const postgresExpressionKind = dialect.name === "postgres" && isJsonExpression(expression)
392
+ ? renderPostgresJsonKind(expression)
393
+ : undefined
394
+ const postgresBaseKind = dialect.name === "postgres" && isJsonExpression(base)
395
+ ? renderPostgresJsonKind(base)
396
+ : undefined
397
+
398
+ switch (kind) {
399
+ case "jsonGet":
400
+ case "jsonPath":
401
+ case "jsonAccess":
402
+ case "jsonTraverse":
403
+ case "jsonGetText":
404
+ case "jsonPathText":
405
+ case "jsonAccessText":
406
+ case "jsonTraverseText": {
407
+ if (!isExpression(base) || segments.length === 0) {
408
+ return undefined
409
+ }
410
+ const baseSql = renderExpression(base, state, dialect)
411
+ const textMode = kind.endsWith("Text") || ast.text === true || ast.asText === true
412
+ if (dialect.name === "postgres") {
413
+ if (exact) {
414
+ return segments.length === 1
415
+ ? `(${baseSql} ${renderPostgresJsonAccessStep(segments[0]!, textMode, state, dialect)})`
416
+ : `(${baseSql} ${textMode ? "#>>" : "#>"} ${renderPostgresJsonPathArray(segments, state, dialect)})`
417
+ }
418
+ const jsonPathLiteral = dialect.renderLiteral(renderJsonPathStringLiteral(segments), state)
419
+ const queried = `jsonb_path_query_first(${renderPostgresJsonValue(base, state, dialect)}, ${jsonPathLiteral})`
420
+ return textMode ? `(${queried} #>> '{}')` : queried
421
+ }
422
+ if (dialect.name === "mysql") {
423
+ const extracted = `json_extract(${baseSql}, ${renderMySqlJsonPath(segments, state, dialect)})`
424
+ return textMode ? `json_unquote(${extracted})` : extracted
425
+ }
426
+ return undefined
427
+ }
428
+ case "jsonHasKey":
429
+ case "jsonKeyExists":
430
+ case "jsonHasAnyKeys":
431
+ case "jsonHasAllKeys": {
432
+ if (!isExpression(base)) {
433
+ return undefined
434
+ }
435
+ const baseSql = dialect.name === "postgres"
436
+ ? renderPostgresJsonValue(base, state, dialect)
437
+ : renderExpression(base, state, dialect)
438
+ const keys = segments
439
+ if (keys.length === 0) {
440
+ return undefined
441
+ }
442
+ if (dialect.name === "postgres") {
443
+ if (kind === "jsonHasAnyKeys") {
444
+ return `(${baseSql} ?| array[${keys.map((key) => renderPostgresTextLiteral(String(key), state, dialect)).join(", ")}])`
445
+ }
446
+ if (kind === "jsonHasAllKeys") {
447
+ return `(${baseSql} ?& array[${keys.map((key) => renderPostgresTextLiteral(String(key), state, dialect)).join(", ")}])`
448
+ }
449
+ return `(${baseSql} ? ${renderPostgresTextLiteral(String(keys[0]!), state, dialect)})`
450
+ }
451
+ if (dialect.name === "mysql") {
452
+ const mode = kind === "jsonHasAllKeys" ? "all" : "one"
453
+ const paths = keys.map((segment) => renderMySqlJsonPath([segment], state, dialect)).join(", ")
454
+ return `json_contains_path(${baseSql}, ${dialect.renderLiteral(mode, state)}, ${paths})`
455
+ }
456
+ return undefined
457
+ }
458
+ case "jsonConcat":
459
+ case "jsonMerge": {
460
+ if (!isExpression(ast.left) || !isExpression(ast.right)) {
461
+ return undefined
462
+ }
463
+ if (dialect.name === "postgres") {
464
+ return `(${renderPostgresJsonValue(ast.left, state, dialect)} || ${renderPostgresJsonValue(ast.right, state, dialect)})`
465
+ }
466
+ if (dialect.name === "mysql") {
467
+ return `json_merge_preserve(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
468
+ }
469
+ return undefined
470
+ }
471
+ case "jsonBuildObject": {
472
+ const entries = Array.isArray((ast as { readonly entries?: readonly { readonly key: string; readonly value: Expression.Any }[] }).entries)
473
+ ? (ast as { readonly entries: readonly { readonly key: string; readonly value: Expression.Any }[] }).entries
474
+ : []
475
+ const renderedEntries = entries.flatMap((entry) => [
476
+ dialect.renderLiteral(entry.key, state),
477
+ renderExpression(entry.value, state, dialect)
478
+ ])
479
+ if (dialect.name === "postgres") {
480
+ return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_object(${renderedEntries.join(", ")})`
481
+ }
482
+ if (dialect.name === "mysql") {
483
+ return `json_object(${renderedEntries.join(", ")})`
484
+ }
485
+ return undefined
486
+ }
487
+ case "jsonBuildArray": {
488
+ const values = Array.isArray((ast as { readonly values?: readonly Expression.Any[] }).values)
489
+ ? (ast as { readonly values: readonly Expression.Any[] }).values
490
+ : []
491
+ const renderedValues = values.map((value) => renderExpression(value, state, dialect)).join(", ")
492
+ if (dialect.name === "postgres") {
493
+ return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_array(${renderedValues})`
494
+ }
495
+ if (dialect.name === "mysql") {
496
+ return `json_array(${renderedValues})`
497
+ }
498
+ return undefined
499
+ }
500
+ case "jsonToJson":
501
+ if (!isExpression(base)) {
502
+ return undefined
503
+ }
504
+ if (dialect.name === "postgres") {
505
+ return `to_json(${renderExpression(base, state, dialect)})`
506
+ }
507
+ if (dialect.name === "mysql") {
508
+ return `cast(${renderExpression(base, state, dialect)} as json)`
509
+ }
510
+ return undefined
511
+ case "jsonToJsonb":
512
+ if (!isExpression(base)) {
513
+ return undefined
514
+ }
515
+ if (dialect.name === "postgres") {
516
+ return `to_jsonb(${renderExpression(base, state, dialect)})`
517
+ }
518
+ if (dialect.name === "mysql") {
519
+ return `cast(${renderExpression(base, state, dialect)} as json)`
520
+ }
521
+ return undefined
522
+ case "jsonTypeOf":
523
+ if (!isExpression(base)) {
524
+ return undefined
525
+ }
526
+ if (dialect.name === "postgres") {
527
+ const baseSql = renderExpression(base, state, dialect)
528
+ return `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof(${baseSql})`
529
+ }
530
+ if (dialect.name === "mysql") {
531
+ return `json_type(${renderExpression(base, state, dialect)})`
532
+ }
533
+ return undefined
534
+ case "jsonLength":
535
+ if (!isExpression(base)) {
536
+ return undefined
537
+ }
538
+ if (dialect.name === "postgres") {
539
+ const baseSql = renderExpression(base, state, dialect)
540
+ const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
541
+ const arrayLength = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_array_length`
542
+ const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
543
+ return `(case when ${typeOf}(${baseSql}) = 'array' then ${arrayLength}(${baseSql}) when ${typeOf}(${baseSql}) = 'object' then (select count(*)::int from ${objectKeys}(${baseSql})) else null end)`
544
+ }
545
+ if (dialect.name === "mysql") {
546
+ return `json_length(${renderExpression(base, state, dialect)})`
547
+ }
548
+ return undefined
549
+ case "jsonKeys":
550
+ if (!isExpression(base)) {
551
+ return undefined
552
+ }
553
+ if (dialect.name === "postgres") {
554
+ const baseSql = renderExpression(base, state, dialect)
555
+ const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
556
+ const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
557
+ return `(case when ${typeOf}(${baseSql}) = 'object' then array(select ${objectKeys}(${baseSql})) else null end)`
558
+ }
559
+ if (dialect.name === "mysql") {
560
+ return `json_keys(${renderExpression(base, state, dialect)})`
561
+ }
562
+ return undefined
563
+ case "jsonStripNulls":
564
+ if (!isExpression(base)) {
565
+ return undefined
566
+ }
567
+ if (dialect.name === "postgres") {
568
+ return `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_strip_nulls(${renderExpression(base, state, dialect)})`
569
+ }
570
+ unsupportedJsonFeature(dialect, "jsonStripNulls")
571
+ return undefined
572
+ case "jsonDelete":
573
+ case "jsonDeletePath":
574
+ case "jsonRemove": {
575
+ if (!isExpression(base) || segments.length === 0) {
576
+ return undefined
577
+ }
578
+ if (dialect.name === "postgres") {
579
+ const baseSql = renderPostgresJsonValue(base, state, dialect)
580
+ if (segments.length === 1 && (segments[0]!.kind === "key" || segments[0]!.kind === "index")) {
581
+ const segment = segments[0]!
582
+ return `(${baseSql} - ${segment.kind === "key"
583
+ ? dialect.renderLiteral(segment.key, state)
584
+ : dialect.renderLiteral(String(segment.index), state)})`
585
+ }
586
+ return `(${baseSql} #- ${renderPostgresJsonPathArray(segments, state, dialect)})`
587
+ }
588
+ if (dialect.name === "mysql") {
589
+ return `json_remove(${renderExpression(base, state, dialect)}, ${segments.map((segment) =>
590
+ renderMySqlJsonPath([segment], state, dialect)
591
+ ).join(", ")})`
592
+ }
593
+ return undefined
594
+ }
595
+ case "jsonSet":
596
+ case "jsonInsert": {
597
+ if (!isExpression(base) || segments.length === 0) {
598
+ return undefined
599
+ }
600
+ const nextValue = extractJsonValue(ast)
601
+ if (!isExpression(nextValue)) {
602
+ return undefined
603
+ }
604
+ const createMissing = ast.createMissing === true
605
+ const insertAfter = ast.insertAfter === true
606
+ if (dialect.name === "postgres") {
607
+ const functionName = kind === "jsonInsert" ? "jsonb_insert" : "jsonb_set"
608
+ const extra =
609
+ kind === "jsonInsert"
610
+ ? `, ${insertAfter ? "true" : "false"}`
611
+ : `, ${createMissing ? "true" : "false"}`
612
+ return `${functionName}(${renderPostgresJsonValue(base, state, dialect)}, ${renderPostgresJsonPathArray(segments, state, dialect)}, ${renderPostgresJsonValue(nextValue, state, dialect)}${extra})`
613
+ }
614
+ if (dialect.name === "mysql") {
615
+ const functionName = kind === "jsonInsert" ? "json_insert" : "json_set"
616
+ return `${functionName}(${renderExpression(base, state, dialect)}, ${renderMySqlJsonPath(segments, state, dialect)}, ${renderExpression(nextValue, state, dialect)})`
617
+ }
618
+ return undefined
619
+ }
620
+ case "jsonPathExists": {
621
+ if (!isExpression(base)) {
622
+ return undefined
623
+ }
624
+ const path = ast.path ?? ast.query ?? ast.right
625
+ if (path === undefined) {
626
+ return undefined
627
+ }
628
+ if (dialect.name === "postgres") {
629
+ return `(${renderPostgresJsonValue(base, state, dialect)} @? ${renderJsonOpaquePath(path, state, dialect)})`
630
+ }
631
+ if (dialect.name === "mysql") {
632
+ return `json_contains_path(${renderExpression(base, state, dialect)}, ${dialect.renderLiteral("one", state)}, ${renderJsonOpaquePath(path, state, dialect)})`
633
+ }
634
+ return undefined
635
+ }
636
+ case "jsonPathMatch": {
637
+ if (!isExpression(base)) {
638
+ return undefined
639
+ }
640
+ const path = ast.path ?? ast.query ?? ast.right
641
+ if (path === undefined) {
642
+ return undefined
643
+ }
644
+ if (dialect.name === "postgres") {
645
+ return `(${renderPostgresJsonValue(base, state, dialect)} @@ ${renderJsonOpaquePath(path, state, dialect)})`
646
+ }
647
+ unsupportedJsonFeature(dialect, "jsonPathMatch")
648
+ }
649
+ }
650
+
651
+ return undefined
652
+ }
653
+
654
+ export interface RenderedQueryAst {
655
+ readonly sql: string
656
+ readonly projections: readonly Projection[]
657
+ }
658
+
659
+ const selectionProjections = (selection: Record<string, unknown>): readonly Projection[] =>
660
+ flattenSelection(selection).map(({ path, alias }) => ({
661
+ path,
662
+ alias
663
+ }))
664
+
665
+ const renderMutationAssignment = (
666
+ entry: QueryAst.AssignmentClause,
667
+ state: RenderState,
668
+ dialect: SqlDialect
669
+ ): string => {
670
+ const column = entry.tableName && dialect.name === "mysql"
671
+ ? `${dialect.quoteIdentifier(entry.tableName)}.${dialect.quoteIdentifier(entry.columnName)}`
672
+ : dialect.quoteIdentifier(entry.columnName)
673
+ return `${column} = ${renderExpression(entry.value, state, dialect)}`
674
+ }
675
+
676
+ const renderJoinSourcesForMutation = (
677
+ joins: readonly QueryAst.JoinClause[],
678
+ state: RenderState,
679
+ dialect: SqlDialect
680
+ ): string => joins.map((join) =>
681
+ renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
682
+ ).join(", ")
683
+
684
+ const renderFromSources = (
685
+ sources: readonly QueryAst.FromClause[],
686
+ state: RenderState,
687
+ dialect: SqlDialect
688
+ ): string => sources.map((source) =>
689
+ renderSourceReference(source.source, source.tableName, source.baseTableName, state, dialect)
690
+ ).join(", ")
691
+
692
+ const renderJoinPredicatesForMutation = (
693
+ joins: readonly QueryAst.JoinClause[],
694
+ state: RenderState,
695
+ dialect: SqlDialect
696
+ ): readonly string[] =>
697
+ joins.flatMap((join) =>
698
+ join.kind === "cross" || !join.on
699
+ ? []
700
+ : [renderExpression(join.on, state, dialect)]
701
+ )
702
+
703
+ const renderDeleteTargets = (
704
+ targets: readonly QueryAst.FromClause[],
705
+ dialect: SqlDialect
706
+ ): string => targets.map((target) => dialect.quoteIdentifier(target.tableName)).join(", ")
707
+
708
+ const renderMysqlMutationLock = (
709
+ lock: QueryAst.LockClause | undefined,
710
+ statement: "update" | "delete"
711
+ ): string => {
712
+ if (!lock) {
713
+ return ""
714
+ }
715
+ switch (lock.mode) {
716
+ case "lowPriority":
717
+ return " low_priority"
718
+ case "ignore":
719
+ return " ignore"
720
+ case "quick":
721
+ return statement === "delete" ? " quick" : ""
722
+ default:
723
+ return ""
724
+ }
725
+ }
726
+
727
+ const renderTransactionClause = (
728
+ clause: QueryAst.TransactionClause,
729
+ dialect: SqlDialect
730
+ ): string => {
731
+ switch (clause.kind) {
732
+ case "transaction": {
733
+ const modes: string[] = []
734
+ if (clause.isolationLevel) {
735
+ modes.push(`isolation level ${clause.isolationLevel}`)
736
+ }
737
+ if (clause.readOnly === true) {
738
+ modes.push("read only")
739
+ }
740
+ return modes.length > 0
741
+ ? `start transaction ${modes.join(", ")}`
742
+ : "start transaction"
743
+ }
744
+ case "commit":
745
+ return "commit"
746
+ case "rollback":
747
+ return "rollback"
748
+ case "savepoint":
749
+ return `savepoint ${dialect.quoteIdentifier(clause.name)}`
750
+ case "rollbackTo":
751
+ return `rollback to savepoint ${dialect.quoteIdentifier(clause.name)}`
752
+ case "releaseSavepoint":
753
+ return `release savepoint ${dialect.quoteIdentifier(clause.name)}`
754
+ }
755
+ return ""
756
+ }
757
+
758
+ const renderSelectionList = (
759
+ selection: Record<string, unknown>,
760
+ state: RenderState,
761
+ dialect: SqlDialect,
762
+ validateAggregation: boolean
763
+ ): RenderedQueryAst => {
764
+ if (validateAggregation) {
765
+ validateAggregationSelection(selection as SelectionValue, [])
766
+ }
767
+ const flattened = flattenSelection(selection)
768
+ const projections = selectionProjections(selection)
769
+ const sql = flattened.map(({ expression, alias }) =>
770
+ `${renderExpression(expression, state, dialect)} as ${dialect.quoteIdentifier(alias)}`).join(", ")
771
+ return {
772
+ sql,
773
+ projections
774
+ }
775
+ }
776
+
777
+ export const renderQueryAst = (
778
+ ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
779
+ state: RenderState,
780
+ dialect: SqlDialect
781
+ ): RenderedQueryAst => {
782
+ let sql = ""
783
+ let projections: readonly Projection[] = []
784
+
785
+ switch (ast.kind) {
786
+ case "select": {
787
+ validateAggregationSelection(ast.select as SelectionValue, ast.groupBy)
788
+ const rendered = renderSelectionList(ast.select as Record<string, unknown>, state, dialect, false)
789
+ projections = rendered.projections
790
+ const clauses = [
791
+ ast.distinctOn && ast.distinctOn.length > 0
792
+ ? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")}) ${rendered.sql}`
793
+ : `select${ast.distinct ? " distinct" : ""} ${rendered.sql}`
794
+ ]
795
+ if (ast.from) {
796
+ clauses.push(`from ${renderSourceReference(ast.from.source, ast.from.tableName, ast.from.baseTableName, state, dialect)}`)
797
+ }
798
+ for (const join of ast.joins) {
799
+ const source = renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
800
+ clauses.push(
801
+ join.kind === "cross"
802
+ ? `cross join ${source}`
803
+ : `${join.kind} join ${source} on ${renderExpression(join.on!, state, dialect)}`
804
+ )
805
+ }
806
+ if (ast.where.length > 0) {
807
+ clauses.push(`where ${ast.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect)).join(" and ")}`)
808
+ }
809
+ if (ast.groupBy.length > 0) {
810
+ clauses.push(`group by ${ast.groupBy.map((value: QueryAst.Ast["groupBy"][number]) => renderExpression(value, state, dialect)).join(", ")}`)
811
+ }
812
+ if (ast.having.length > 0) {
813
+ clauses.push(`having ${ast.having.map((entry: QueryAst.HavingClause) => renderExpression(entry.predicate, state, dialect)).join(" and ")}`)
814
+ }
815
+ if (ast.orderBy.length > 0) {
816
+ clauses.push(`order by ${ast.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`)
817
+ }
818
+ if (ast.limit) {
819
+ clauses.push(`limit ${renderExpression(ast.limit, state, dialect)}`)
820
+ }
821
+ if (ast.offset) {
822
+ clauses.push(`offset ${renderExpression(ast.offset, state, dialect)}`)
823
+ }
824
+ if (ast.lock) {
825
+ clauses.push(
826
+ `${ast.lock.mode === "update" ? "for update" : "for share"}${ast.lock.nowait ? " nowait" : ""}${ast.lock.skipLocked ? " skip locked" : ""}`
827
+ )
828
+ }
829
+ sql = clauses.join(" ")
830
+ break
831
+ }
832
+ case "set": {
833
+ const setAst = ast as QueryAst.Ast<Record<string, unknown>, any, "set">
834
+ const base = renderQueryAst(
835
+ Query.getAst(setAst.setBase as Query.Plan.Any) as QueryAst.Ast<
836
+ Record<string, unknown>,
837
+ any,
838
+ QueryAst.QueryStatement
839
+ >,
840
+ state,
841
+ dialect
842
+ )
843
+ projections = selectionProjections(setAst.select as Record<string, unknown>)
844
+ sql = [
845
+ `(${base.sql})`,
846
+ ...(setAst.setOperations ?? []).map((entry) => {
847
+ const rendered = renderQueryAst(
848
+ Query.getAst(entry.query as Query.Plan.Any) as QueryAst.Ast<
849
+ Record<string, unknown>,
850
+ any,
851
+ QueryAst.QueryStatement
852
+ >,
853
+ state,
854
+ dialect
855
+ )
856
+ return `${entry.kind}${entry.all ? " all" : ""} (${rendered.sql})`
857
+ })
858
+ ].join(" ")
859
+ break
860
+ }
861
+ case "insert": {
862
+ const insertAst = ast as QueryAst.Ast<Record<string, unknown>, any, "insert">
863
+ const targetSource = insertAst.into!
864
+ const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
865
+ sql = `insert into ${target}`
866
+ if (insertAst.insertSource?.kind === "values") {
867
+ const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
868
+ const rows = insertAst.insertSource.rows.map((row) =>
869
+ `(${row.values.map((entry) => renderExpression(entry.value, state, dialect)).join(", ")})`
870
+ ).join(", ")
871
+ sql += ` (${columns}) values ${rows}`
872
+ } else if (insertAst.insertSource?.kind === "query") {
873
+ const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
874
+ const renderedQuery = renderQueryAst(
875
+ Query.getAst(insertAst.insertSource.query as Query.Plan.Any) as QueryAst.Ast<
876
+ Record<string, unknown>,
877
+ any,
878
+ QueryAst.QueryStatement
879
+ >,
880
+ state,
881
+ dialect
882
+ )
883
+ sql += ` (${columns}) ${renderedQuery.sql}`
884
+ } else if (insertAst.insertSource?.kind === "unnest") {
885
+ const unnestSource = insertAst.insertSource
886
+ const columns = unnestSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
887
+ if (dialect.name === "postgres") {
888
+ const table = targetSource.source as Table.AnyTable
889
+ const fields = table[Table.TypeId].fields
890
+ const rendered = unnestSource.values.map((entry) =>
891
+ `cast(${dialect.renderLiteral(entry.values, state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
892
+ ).join(", ")
893
+ sql += ` (${columns}) select * from unnest(${rendered})`
894
+ } else {
895
+ const rowCount = unnestSource.values[0]?.values.length ?? 0
896
+ const rows = Array.from({ length: rowCount }, (_, index) =>
897
+ `(${unnestSource.values.map((entry) =>
898
+ dialect.renderLiteral(entry.values[index], state)
899
+ ).join(", ")})`
900
+ ).join(", ")
901
+ sql += ` (${columns}) values ${rows}`
902
+ }
903
+ } else {
904
+ const columns = (insertAst.values ?? []).map((entry) => dialect.quoteIdentifier(entry.columnName)).join(", ")
905
+ const values = (insertAst.values ?? []).map((entry) => renderExpression(entry.value, state, dialect)).join(", ")
906
+ if ((insertAst.values ?? []).length > 0) {
907
+ sql += ` (${columns}) values (${values})`
908
+ } else {
909
+ sql += " default values"
910
+ }
911
+ }
912
+ if (insertAst.conflict) {
913
+ const updateValues = (insertAst.conflict.values ?? []).map((entry) =>
914
+ `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
915
+ ).join(", ")
916
+ if (dialect.name === "postgres") {
917
+ const targetSql = insertAst.conflict.target?.kind === "constraint"
918
+ ? ` on conflict on constraint ${dialect.quoteIdentifier(insertAst.conflict.target.name)}`
919
+ : insertAst.conflict.target?.kind === "columns"
920
+ ? ` on conflict (${insertAst.conflict.target.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${insertAst.conflict.target.where ? ` where ${renderExpression(insertAst.conflict.target.where, state, dialect)}` : ""}`
921
+ : " on conflict"
922
+ sql += targetSql
923
+ sql += insertAst.conflict.action === "doNothing"
924
+ ? " do nothing"
925
+ : ` do update set ${updateValues}${insertAst.conflict.where ? ` where ${renderExpression(insertAst.conflict.where, state, dialect)}` : ""}`
926
+ } else if (insertAst.conflict.action === "doNothing") {
927
+ sql = sql.replace(/^insert/, "insert ignore")
928
+ } else {
929
+ sql += ` on duplicate key update ${updateValues}`
930
+ }
931
+ }
932
+ const returning = renderSelectionList(insertAst.select as Record<string, unknown>, state, dialect, false)
933
+ projections = returning.projections
934
+ if (returning.sql.length > 0) {
935
+ sql += ` returning ${returning.sql}`
936
+ }
937
+ break
938
+ }
939
+ case "update": {
940
+ const updateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "update">
941
+ const targetSource = updateAst.target!
942
+ const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
943
+ const targets = updateAst.targets ?? [targetSource]
944
+ const fromSources = updateAst.fromSources ?? []
945
+ const assignments = updateAst.set!.map((entry) =>
946
+ renderMutationAssignment(entry, state, dialect)).join(", ")
947
+ if (dialect.name === "mysql") {
948
+ const modifiers = renderMysqlMutationLock(updateAst.lock, "update")
949
+ const extraSources = renderFromSources(fromSources, state, dialect)
950
+ const joinSources = updateAst.joins.map((join) =>
951
+ join.kind === "cross"
952
+ ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
953
+ : `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
954
+ ).join(" ")
955
+ const targetList = [
956
+ ...targets.map((entry) =>
957
+ renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
958
+ ),
959
+ ...(extraSources.length > 0 ? [extraSources] : [])
960
+ ].join(", ")
961
+ sql = `update${modifiers} ${targetList}${joinSources.length > 0 ? ` ${joinSources}` : ""} set ${assignments}`
962
+ } else {
963
+ sql = `update ${target} set ${assignments}`
964
+ const mutationSources = [
965
+ ...(fromSources.length > 0 ? [renderFromSources(fromSources, state, dialect)] : []),
966
+ ...(updateAst.joins.length > 0 ? [renderJoinSourcesForMutation(updateAst.joins, state, dialect)] : [])
967
+ ].filter((part) => part.length > 0)
968
+ if (mutationSources.length > 0) {
969
+ sql += ` from ${mutationSources.join(", ")}`
970
+ }
971
+ }
972
+ const whereParts = [
973
+ ...(dialect.name === "postgres" ? renderJoinPredicatesForMutation(updateAst.joins, state, dialect) : []),
974
+ ...updateAst.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect))
975
+ ]
976
+ if (whereParts.length > 0) {
977
+ sql += ` where ${whereParts.join(" and ")}`
978
+ }
979
+ if (dialect.name === "mysql" && updateAst.orderBy.length > 0) {
980
+ sql += ` order by ${updateAst.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`
981
+ }
982
+ if (dialect.name === "mysql" && updateAst.limit) {
983
+ sql += ` limit ${renderMysqlMutationLimit(updateAst.limit, state, dialect)}`
984
+ }
985
+ const returning = renderSelectionList(updateAst.select as Record<string, unknown>, state, dialect, false)
986
+ projections = returning.projections
987
+ if (returning.sql.length > 0) {
988
+ sql += ` returning ${returning.sql}`
989
+ }
990
+ break
991
+ }
992
+ case "delete": {
993
+ const deleteAst = ast as QueryAst.Ast<Record<string, unknown>, any, "delete">
994
+ const targetSource = deleteAst.target!
995
+ const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
996
+ const targets = deleteAst.targets ?? [targetSource]
997
+ if (dialect.name === "mysql") {
998
+ const modifiers = renderMysqlMutationLock(deleteAst.lock, "delete")
999
+ const hasJoinedSources = deleteAst.joins.length > 0 || targets.length > 1
1000
+ const targetList = renderDeleteTargets(targets, dialect)
1001
+ const fromSources = targets.map((entry) =>
1002
+ renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
1003
+ ).join(", ")
1004
+ const joinSources = deleteAst.joins.map((join) =>
1005
+ join.kind === "cross"
1006
+ ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
1007
+ : `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
1008
+ ).join(" ")
1009
+ sql = hasJoinedSources
1010
+ ? `delete${modifiers} ${targetList} from ${fromSources}${joinSources.length > 0 ? ` ${joinSources}` : ""}`
1011
+ : `delete${modifiers} from ${fromSources}`
1012
+ } else {
1013
+ sql = `delete from ${target}`
1014
+ if (deleteAst.joins.length > 0) {
1015
+ sql += ` using ${renderJoinSourcesForMutation(deleteAst.joins, state, dialect)}`
1016
+ }
1017
+ }
1018
+ const whereParts = [
1019
+ ...(dialect.name === "postgres" ? renderJoinPredicatesForMutation(deleteAst.joins, state, dialect) : []),
1020
+ ...deleteAst.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect))
1021
+ ]
1022
+ if (whereParts.length > 0) {
1023
+ sql += ` where ${whereParts.join(" and ")}`
1024
+ }
1025
+ if (dialect.name === "mysql" && deleteAst.orderBy.length > 0) {
1026
+ sql += ` order by ${deleteAst.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`
1027
+ }
1028
+ if (dialect.name === "mysql" && deleteAst.limit) {
1029
+ sql += ` limit ${renderMysqlMutationLimit(deleteAst.limit, state, dialect)}`
1030
+ }
1031
+ const returning = renderSelectionList(deleteAst.select as Record<string, unknown>, state, dialect, false)
1032
+ projections = returning.projections
1033
+ if (returning.sql.length > 0) {
1034
+ sql += ` returning ${returning.sql}`
1035
+ }
1036
+ break
1037
+ }
1038
+ case "truncate": {
1039
+ const truncateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "truncate">
1040
+ const targetSource = truncateAst.target!
1041
+ sql = `truncate table ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
1042
+ if (truncateAst.truncate?.restartIdentity) {
1043
+ sql += " restart identity"
1044
+ }
1045
+ if (truncateAst.truncate?.cascade) {
1046
+ sql += " cascade"
1047
+ }
1048
+ break
1049
+ }
1050
+ case "merge": {
1051
+ if (dialect.name !== "postgres") {
1052
+ throw new Error(`Unsupported merge statement for ${dialect.name}`)
1053
+ }
1054
+ const mergeAst = ast as QueryAst.Ast<Record<string, unknown>, any, "merge">
1055
+ const targetSource = mergeAst.target!
1056
+ const usingSource = mergeAst.using!
1057
+ const merge = mergeAst.merge!
1058
+ sql = `merge into ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} using ${renderSourceReference(usingSource.source, usingSource.tableName, usingSource.baseTableName, state, dialect)} on ${renderExpression(merge.on, state, dialect)}`
1059
+ if (merge.whenMatched) {
1060
+ sql += " when matched"
1061
+ if (merge.whenMatched.predicate) {
1062
+ sql += ` and ${renderExpression(merge.whenMatched.predicate, state, dialect)}`
1063
+ }
1064
+ if (merge.whenMatched.kind === "delete") {
1065
+ sql += " then delete"
1066
+ } else {
1067
+ sql += ` then update set ${merge.whenMatched.values.map((entry) =>
1068
+ `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
1069
+ ).join(", ")}`
1070
+ }
1071
+ }
1072
+ if (merge.whenNotMatched) {
1073
+ sql += " when not matched"
1074
+ if (merge.whenNotMatched.predicate) {
1075
+ sql += ` and ${renderExpression(merge.whenNotMatched.predicate, state, dialect)}`
1076
+ }
1077
+ sql += ` then insert (${merge.whenNotMatched.values.map((entry) => dialect.quoteIdentifier(entry.columnName)).join(", ")}) values (${merge.whenNotMatched.values.map((entry) => renderExpression(entry.value, state, dialect)).join(", ")})`
1078
+ }
1079
+ break
1080
+ }
1081
+ case "transaction":
1082
+ case "commit":
1083
+ case "rollback":
1084
+ case "savepoint":
1085
+ case "rollbackTo":
1086
+ case "releaseSavepoint": {
1087
+ sql = renderTransactionClause(ast.transaction!, dialect)
1088
+ break
1089
+ }
1090
+ case "createTable": {
1091
+ const createTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createTable">
1092
+ sql = renderCreateTableSql(createTableAst.target!, state, dialect, createTableAst.ddl?.kind === "createTable" && createTableAst.ddl.ifNotExists)
1093
+ break
1094
+ }
1095
+ case "dropTable": {
1096
+ const dropTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropTable">
1097
+ const ifExists = dropTableAst.ddl?.kind === "dropTable" && dropTableAst.ddl.ifExists
1098
+ sql = `drop table${ifExists ? " if exists" : ""} ${renderSourceReference(dropTableAst.target!.source, dropTableAst.target!.tableName, dropTableAst.target!.baseTableName, state, dialect)}`
1099
+ break
1100
+ }
1101
+ case "createIndex": {
1102
+ const createIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createIndex">
1103
+ sql = renderCreateIndexSql(
1104
+ createIndexAst.target!,
1105
+ createIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
1106
+ state,
1107
+ dialect
1108
+ )
1109
+ break
1110
+ }
1111
+ case "dropIndex": {
1112
+ const dropIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropIndex">
1113
+ sql = renderDropIndexSql(
1114
+ dropIndexAst.target!,
1115
+ dropIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
1116
+ state,
1117
+ dialect
1118
+ )
1119
+ break
1120
+ }
1121
+ }
1122
+
1123
+ if (state.ctes.length === 0) {
1124
+ return {
1125
+ sql,
1126
+ projections
1127
+ }
1128
+ }
1129
+ return {
1130
+ sql: `with${state.ctes.some((entry) => entry.recursive) ? " recursive" : ""} ${state.ctes.map((entry) => `${dialect.quoteIdentifier(entry.name)} as (${entry.sql})`).join(", ")} ${sql}`,
1131
+ projections
1132
+ }
1133
+ }
1134
+
1135
+ const renderSourceReference = (
1136
+ source: unknown,
1137
+ tableName: string,
1138
+ baseTableName: string,
1139
+ state: RenderState,
1140
+ dialect: SqlDialect
1141
+ ): string => {
1142
+ const renderSelectRows = (
1143
+ rows: readonly Record<string, Expression.Any>[],
1144
+ columnNames: readonly string[]
1145
+ ): string => {
1146
+ const renderedRows = rows.map((row) =>
1147
+ `select ${columnNames.map((columnName) =>
1148
+ `${renderExpression(row[columnName]!, state, dialect)} as ${dialect.quoteIdentifier(columnName)}`
1149
+ ).join(", ")}`
1150
+ )
1151
+ return `(${renderedRows.join(" union all ")}) as ${dialect.quoteIdentifier(tableName)}(${columnNames.map((columnName) => dialect.quoteIdentifier(columnName)).join(", ")})`
1152
+ }
1153
+
1154
+ const renderUnnestRows = (
1155
+ arrays: Readonly<Record<string, readonly Expression.Any[]>>,
1156
+ columnNames: readonly string[]
1157
+ ): string => {
1158
+ const rowCount = arrays[columnNames[0]!]!.length
1159
+ const rows = Array.from({ length: rowCount }, (_, index) =>
1160
+ Object.fromEntries(columnNames.map((columnName) => [columnName, arrays[columnName]![index]!] as const)) as Record<string, Expression.Any>
1161
+ )
1162
+ return renderSelectRows(rows, columnNames)
1163
+ }
1164
+
1165
+ if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "cte") {
1166
+ const cte = source as unknown as {
1167
+ readonly name: string
1168
+ readonly plan: Query.Plan.Any
1169
+ readonly recursive?: boolean
1170
+ }
1171
+ if (!state.cteNames.has(cte.name)) {
1172
+ state.cteNames.add(cte.name)
1173
+ const rendered = renderQueryAst(Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect)
1174
+ state.ctes.push({
1175
+ name: cte.name,
1176
+ sql: rendered.sql,
1177
+ recursive: cte.recursive
1178
+ })
1179
+ }
1180
+ return dialect.quoteIdentifier(cte.name)
1181
+ }
1182
+ if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "derived") {
1183
+ const derived = source as unknown as {
1184
+ readonly name: string
1185
+ readonly plan: Query.Plan.Any
1186
+ }
1187
+ if (!state.cteNames.has(derived.name)) {
1188
+ // derived tables are inlined, so no CTE registration is needed
1189
+ }
1190
+ return `(${renderQueryAst(Query.getAst(derived.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect).sql}) as ${dialect.quoteIdentifier(derived.name)}`
1191
+ }
1192
+ if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "lateral") {
1193
+ const lateral = source as unknown as {
1194
+ readonly name: string
1195
+ readonly plan: Query.Plan.Any
1196
+ }
1197
+ return `lateral (${renderQueryAst(Query.getAst(lateral.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect).sql}) as ${dialect.quoteIdentifier(lateral.name)}`
1198
+ }
1199
+ if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "values") {
1200
+ const values = source as unknown as {
1201
+ readonly columns: Record<string, Expression.Any>
1202
+ readonly rows: readonly Record<string, Expression.Any>[]
1203
+ }
1204
+ return renderSelectRows(values.rows, Object.keys(values.columns))
1205
+ }
1206
+ if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "unnest") {
1207
+ const unnest = source as unknown as {
1208
+ readonly columns: Record<string, Expression.Any>
1209
+ readonly arrays: Readonly<Record<string, readonly Expression.Any[]>>
1210
+ }
1211
+ return renderUnnestRows(unnest.arrays, Object.keys(unnest.columns))
1212
+ }
1213
+ if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "tableFunction") {
1214
+ const tableFunction = source as unknown as {
1215
+ readonly name: string
1216
+ readonly columns: Record<string, Expression.Any>
1217
+ readonly functionName: string
1218
+ readonly args: readonly Expression.Any[]
1219
+ }
1220
+ if (dialect.name !== "postgres") {
1221
+ throw new Error("Unsupported table function source for SQL rendering")
1222
+ }
1223
+ const columnNames = Object.keys(tableFunction.columns)
1224
+ return `${tableFunction.functionName}(${tableFunction.args.map((arg) => renderExpression(arg, state, dialect)).join(", ")}) as ${dialect.quoteIdentifier(tableFunction.name)}(${columnNames.map((columnName) => dialect.quoteIdentifier(columnName)).join(", ")})`
1225
+ }
1226
+ const schemaName = typeof source === "object" && source !== null && Table.TypeId in source
1227
+ ? (source as Table.AnyTable)[Table.TypeId].schemaName
1228
+ : undefined
1229
+ return dialect.renderTableReference(tableName, baseTableName, schemaName)
1230
+ }
1231
+
1232
+ /**
1233
+ * Renders a scalar expression AST into SQL text.
1234
+ *
1235
+ * This is parameterized by a runtime dialect so the same expression walker can
1236
+ * be reused across dialect-specific renderers while still delegating quoting
1237
+ * and literal serialization to the concrete dialect implementation.
1238
+ */
1239
+ export const renderExpression = (
1240
+ expression: Expression.Any,
1241
+ state: RenderState,
1242
+ dialect: SqlDialect
1243
+ ): string => {
1244
+ const rawAst = (expression as Expression.Any & {
1245
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
1246
+ })[ExpressionAst.TypeId] as ExpressionAst.Any | Record<string, unknown>
1247
+ const jsonSql = renderJsonExpression(expression, rawAst as Record<string, unknown>, state, dialect)
1248
+ if (jsonSql !== undefined) {
1249
+ return jsonSql
1250
+ }
1251
+ const ast = rawAst as ExpressionAst.Any
1252
+ const renderComparisonOperator = (operator: "eq" | "neq" | "lt" | "lte" | "gt" | "gte"): "=" | "<>" | "<" | "<=" | ">" | ">=" =>
1253
+ operator === "eq"
1254
+ ? "="
1255
+ : operator === "neq"
1256
+ ? "<>"
1257
+ : operator === "lt"
1258
+ ? "<"
1259
+ : operator === "lte"
1260
+ ? "<="
1261
+ : operator === "gt"
1262
+ ? ">"
1263
+ : ">="
1264
+ switch (ast.kind) {
1265
+ case "column":
1266
+ return ast.tableName.length === 0
1267
+ ? dialect.quoteIdentifier(ast.columnName)
1268
+ : `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
1269
+ case "literal":
1270
+ return dialect.renderLiteral(ast.value, state)
1271
+ case "excluded":
1272
+ return dialect.name === "mysql"
1273
+ ? `values(${dialect.quoteIdentifier(ast.columnName)})`
1274
+ : `excluded.${dialect.quoteIdentifier(ast.columnName)}`
1275
+ case "cast":
1276
+ return `cast(${renderExpression(ast.value, state, dialect)} as ${renderCastType(dialect, ast.target)})`
1277
+ case "function":
1278
+ return renderFunctionCall(ast.name, Array.isArray(ast.args) ? ast.args : [], state, dialect)
1279
+ case "eq":
1280
+ return `(${renderExpression(ast.left, state, dialect)} = ${renderExpression(ast.right, state, dialect)})`
1281
+ case "neq":
1282
+ return `(${renderExpression(ast.left, state, dialect)} <> ${renderExpression(ast.right, state, dialect)})`
1283
+ case "lt":
1284
+ return `(${renderExpression(ast.left, state, dialect)} < ${renderExpression(ast.right, state, dialect)})`
1285
+ case "lte":
1286
+ return `(${renderExpression(ast.left, state, dialect)} <= ${renderExpression(ast.right, state, dialect)})`
1287
+ case "gt":
1288
+ return `(${renderExpression(ast.left, state, dialect)} > ${renderExpression(ast.right, state, dialect)})`
1289
+ case "gte":
1290
+ return `(${renderExpression(ast.left, state, dialect)} >= ${renderExpression(ast.right, state, dialect)})`
1291
+ case "like":
1292
+ return `(${renderExpression(ast.left, state, dialect)} like ${renderExpression(ast.right, state, dialect)})`
1293
+ case "ilike":
1294
+ return dialect.name === "postgres"
1295
+ ? `(${renderExpression(ast.left, state, dialect)} ilike ${renderExpression(ast.right, state, dialect)})`
1296
+ : `(lower(${renderExpression(ast.left, state, dialect)}) like lower(${renderExpression(ast.right, state, dialect)}))`
1297
+ case "regexMatch":
1298
+ return dialect.name === "postgres"
1299
+ ? `(${renderExpression(ast.left, state, dialect)} ~ ${renderExpression(ast.right, state, dialect)})`
1300
+ : `(${renderExpression(ast.left, state, dialect)} regexp ${renderExpression(ast.right, state, dialect)})`
1301
+ case "regexIMatch":
1302
+ return dialect.name === "postgres"
1303
+ ? `(${renderExpression(ast.left, state, dialect)} ~* ${renderExpression(ast.right, state, dialect)})`
1304
+ : `(${renderExpression(ast.left, state, dialect)} regexp ${renderExpression(ast.right, state, dialect)})`
1305
+ case "regexNotMatch":
1306
+ return dialect.name === "postgres"
1307
+ ? `(${renderExpression(ast.left, state, dialect)} !~ ${renderExpression(ast.right, state, dialect)})`
1308
+ : `(${renderExpression(ast.left, state, dialect)} not regexp ${renderExpression(ast.right, state, dialect)})`
1309
+ case "regexNotIMatch":
1310
+ return dialect.name === "postgres"
1311
+ ? `(${renderExpression(ast.left, state, dialect)} !~* ${renderExpression(ast.right, state, dialect)})`
1312
+ : `(${renderExpression(ast.left, state, dialect)} not regexp ${renderExpression(ast.right, state, dialect)})`
1313
+ case "isDistinctFrom":
1314
+ return dialect.name === "mysql"
1315
+ ? `(not (${renderExpression(ast.left, state, dialect)} <=> ${renderExpression(ast.right, state, dialect)}))`
1316
+ : `(${renderExpression(ast.left, state, dialect)} is distinct from ${renderExpression(ast.right, state, dialect)})`
1317
+ case "isNotDistinctFrom":
1318
+ return dialect.name === "mysql"
1319
+ ? `(${renderExpression(ast.left, state, dialect)} <=> ${renderExpression(ast.right, state, dialect)})`
1320
+ : `(${renderExpression(ast.left, state, dialect)} is not distinct from ${renderExpression(ast.right, state, dialect)})`
1321
+ case "contains":
1322
+ if (dialect.name === "postgres") {
1323
+ const left = isJsonExpression(ast.left)
1324
+ ? renderPostgresJsonValue(ast.left, state, dialect)
1325
+ : renderExpression(ast.left, state, dialect)
1326
+ const right = isJsonExpression(ast.right)
1327
+ ? renderPostgresJsonValue(ast.right, state, dialect)
1328
+ : renderExpression(ast.right, state, dialect)
1329
+ return `(${left} @> ${right})`
1330
+ }
1331
+ if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1332
+ return `json_contains(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
1333
+ }
1334
+ throw new Error("Unsupported container operator for SQL rendering")
1335
+ case "containedBy":
1336
+ if (dialect.name === "postgres") {
1337
+ const left = isJsonExpression(ast.left)
1338
+ ? renderPostgresJsonValue(ast.left, state, dialect)
1339
+ : renderExpression(ast.left, state, dialect)
1340
+ const right = isJsonExpression(ast.right)
1341
+ ? renderPostgresJsonValue(ast.right, state, dialect)
1342
+ : renderExpression(ast.right, state, dialect)
1343
+ return `(${left} <@ ${right})`
1344
+ }
1345
+ if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1346
+ return `json_contains(${renderExpression(ast.right, state, dialect)}, ${renderExpression(ast.left, state, dialect)})`
1347
+ }
1348
+ throw new Error("Unsupported container operator for SQL rendering")
1349
+ case "overlaps":
1350
+ if (dialect.name === "postgres") {
1351
+ const left = isJsonExpression(ast.left)
1352
+ ? renderPostgresJsonValue(ast.left, state, dialect)
1353
+ : renderExpression(ast.left, state, dialect)
1354
+ const right = isJsonExpression(ast.right)
1355
+ ? renderPostgresJsonValue(ast.right, state, dialect)
1356
+ : renderExpression(ast.right, state, dialect)
1357
+ return `(${left} && ${right})`
1358
+ }
1359
+ if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1360
+ return `json_overlaps(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
1361
+ }
1362
+ throw new Error("Unsupported container operator for SQL rendering")
1363
+ case "isNull":
1364
+ return `(${renderExpression(ast.value, state, dialect)} is null)`
1365
+ case "isNotNull":
1366
+ return `(${renderExpression(ast.value, state, dialect)} is not null)`
1367
+ case "not":
1368
+ return `(not ${renderExpression(ast.value, state, dialect)})`
1369
+ case "upper":
1370
+ return `upper(${renderExpression(ast.value, state, dialect)})`
1371
+ case "lower":
1372
+ return `lower(${renderExpression(ast.value, state, dialect)})`
1373
+ case "count":
1374
+ return `count(${renderExpression(ast.value, state, dialect)})`
1375
+ case "max":
1376
+ return `max(${renderExpression(ast.value, state, dialect)})`
1377
+ case "min":
1378
+ return `min(${renderExpression(ast.value, state, dialect)})`
1379
+ case "and":
1380
+ return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" and ")})`
1381
+ case "or":
1382
+ return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" or ")})`
1383
+ case "coalesce":
1384
+ return `coalesce(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")})`
1385
+ case "in":
1386
+ return `(${renderExpression(ast.values[0]!, state, dialect)} in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1387
+ case "notIn":
1388
+ return `(${renderExpression(ast.values[0]!, state, dialect)} not in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1389
+ case "between":
1390
+ return `(${renderExpression(ast.values[0]!, state, dialect)} between ${renderExpression(ast.values[1]!, state, dialect)} and ${renderExpression(ast.values[2]!, state, dialect)})`
1391
+ case "concat":
1392
+ return dialect.renderConcat(ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)))
1393
+ case "case":
1394
+ return `case ${ast.branches.map((branch) =>
1395
+ `when ${renderExpression(branch.when, state, dialect)} then ${renderExpression(branch.then, state, dialect)}`
1396
+ ).join(" ")} else ${renderExpression(ast.else, state, dialect)} end`
1397
+ case "exists":
1398
+ return `exists (${renderQueryAst(
1399
+ Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1400
+ state,
1401
+ dialect
1402
+ ).sql})`
1403
+ case "scalarSubquery":
1404
+ return `(${renderQueryAst(
1405
+ Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1406
+ state,
1407
+ dialect
1408
+ ).sql})`
1409
+ case "inSubquery":
1410
+ return `(${renderExpression(ast.left, state, dialect)} in (${renderQueryAst(
1411
+ Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1412
+ state,
1413
+ dialect
1414
+ ).sql}))`
1415
+ case "comparisonAny":
1416
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderQueryAst(
1417
+ Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1418
+ state,
1419
+ dialect
1420
+ ).sql}))`
1421
+ case "comparisonAll":
1422
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderQueryAst(
1423
+ Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1424
+ state,
1425
+ dialect
1426
+ ).sql}))`
1427
+ case "window": {
1428
+ if (!Array.isArray(ast.partitionBy) || !Array.isArray(ast.orderBy) || typeof ast.function !== "string") {
1429
+ break
1430
+ }
1431
+ const clauses: string[] = []
1432
+ if (ast.partitionBy.length > 0) {
1433
+ clauses.push(`partition by ${ast.partitionBy.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}`)
1434
+ }
1435
+ if (ast.orderBy.length > 0) {
1436
+ clauses.push(`order by ${ast.orderBy.map((entry) =>
1437
+ `${renderExpression(entry.value, state, dialect)} ${entry.direction}`
1438
+ ).join(", ")}`)
1439
+ }
1440
+ const specification = clauses.join(" ")
1441
+ switch (ast.function) {
1442
+ case "rowNumber":
1443
+ return `row_number() over (${specification})`
1444
+ case "rank":
1445
+ return `rank() over (${specification})`
1446
+ case "denseRank":
1447
+ return `dense_rank() over (${specification})`
1448
+ case "over":
1449
+ return `${renderExpression(ast.value!, state, dialect)} over (${specification})`
1450
+ }
1451
+ break
1452
+ }
1453
+ }
1454
+ throw new Error("Unsupported expression for SQL rendering")
1455
+ }