effect-qb 0.16.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +8065 -0
  3. package/dist/mysql.js +4036 -2418
  4. package/dist/postgres/metadata.js +2536 -625
  5. package/dist/postgres.js +8248 -7857
  6. package/dist/sqlite.js +8854 -0
  7. package/dist/standard.js +8019 -0
  8. package/package.json +15 -3
  9. package/src/casing.ts +71 -0
  10. package/src/index.ts +2 -0
  11. package/src/internal/casing.ts +89 -0
  12. package/src/internal/column-state.ts +11 -6
  13. package/src/internal/column.ts +44 -7
  14. package/src/internal/datatypes/define.ts +2 -1
  15. package/src/internal/datatypes/enrich.ts +23 -0
  16. package/src/internal/datatypes/lookup.ts +14 -7
  17. package/src/internal/derived-table.ts +7 -13
  18. package/src/internal/dialect-renderers/mysql.ts +2046 -0
  19. package/src/{postgres/internal/sql-expression-renderer.ts → internal/dialect-renderers/postgres.ts} +867 -283
  20. package/src/{mysql/internal/sql-expression-renderer.ts → internal/dialect-renderers/sqlite.ts} +834 -358
  21. package/src/internal/dialect.ts +37 -0
  22. package/src/internal/dsl-mutation-runtime.ts +29 -10
  23. package/src/internal/dsl-plan-runtime.ts +41 -24
  24. package/src/internal/dsl-query-runtime.ts +11 -31
  25. package/src/internal/dsl-transaction-ddl-runtime.ts +61 -15
  26. package/src/internal/executor.ts +57 -15
  27. package/src/internal/expression-ast.ts +3 -2
  28. package/src/internal/grouping-key.ts +216 -9
  29. package/src/internal/implication-runtime.ts +3 -2
  30. package/src/internal/json/types.ts +155 -40
  31. package/src/internal/predicate/context.ts +14 -1
  32. package/src/internal/predicate/key.ts +19 -2
  33. package/src/internal/predicate/runtime.ts +30 -3
  34. package/src/internal/query.d.ts +38 -11
  35. package/src/internal/query.ts +315 -54
  36. package/src/internal/renderer.ts +51 -6
  37. package/src/internal/runtime/driver-value-mapping.ts +58 -0
  38. package/src/internal/runtime/normalize.ts +74 -43
  39. package/src/internal/runtime/schema.ts +5 -3
  40. package/src/internal/runtime/value.ts +153 -30
  41. package/src/internal/scalar.ts +6 -1
  42. package/src/internal/schema-derivation.d.ts +12 -61
  43. package/src/internal/schema-derivation.ts +90 -38
  44. package/src/internal/schema-expression.ts +2 -2
  45. package/src/internal/sql-expression-renderer.ts +19 -0
  46. package/src/internal/standard-dsl.ts +6885 -0
  47. package/src/internal/table-options.ts +229 -62
  48. package/src/internal/table.d.ts +33 -32
  49. package/src/internal/table.ts +469 -160
  50. package/src/mysql/column-extension.ts +3 -0
  51. package/src/mysql/column.ts +27 -12
  52. package/src/mysql/datatypes/index.ts +24 -2
  53. package/src/mysql/errors/catalog.ts +5 -5
  54. package/src/mysql/errors/normalize.ts +2 -2
  55. package/src/mysql/executor.ts +7 -5
  56. package/src/mysql/internal/dialect.ts +9 -4
  57. package/src/mysql/internal/dsl.ts +906 -324
  58. package/src/mysql/internal/renderer.ts +7 -2
  59. package/src/mysql/json.ts +37 -0
  60. package/src/mysql/query-extension.ts +16 -0
  61. package/src/mysql/query.ts +9 -2
  62. package/src/mysql/renderer.ts +31 -4
  63. package/src/mysql.ts +4 -12
  64. package/src/postgres/column-extension.ts +28 -0
  65. package/src/postgres/column.ts +9 -13
  66. package/src/postgres/datatypes/index.d.ts +2 -1
  67. package/src/postgres/datatypes/index.ts +3 -2
  68. package/src/postgres/errors/normalize.ts +2 -2
  69. package/src/postgres/executor.ts +55 -10
  70. package/src/postgres/function/core.ts +20 -4
  71. package/src/postgres/function/index.ts +1 -17
  72. package/src/postgres/internal/dialect.ts +9 -4
  73. package/src/postgres/internal/dsl.ts +850 -359
  74. package/src/postgres/internal/renderer.ts +7 -2
  75. package/src/postgres/internal/schema-ddl.ts +22 -9
  76. package/src/postgres/internal/schema-model.ts +244 -10
  77. package/src/postgres/json.ts +100 -24
  78. package/src/postgres/jsonb.ts +38 -0
  79. package/src/postgres/query-extension.ts +2 -0
  80. package/src/postgres/query.ts +9 -2
  81. package/src/postgres/renderer.ts +31 -4
  82. package/src/postgres/schema-management.ts +108 -16
  83. package/src/postgres/schema.ts +98 -15
  84. package/src/postgres/table.ts +203 -398
  85. package/src/postgres/type.ts +8 -7
  86. package/src/postgres.ts +9 -11
  87. package/src/sqlite/column-extension.ts +3 -0
  88. package/src/sqlite/column.ts +127 -0
  89. package/src/sqlite/datatypes/index.ts +80 -0
  90. package/src/sqlite/datatypes/spec.ts +98 -0
  91. package/src/sqlite/errors/catalog.ts +103 -0
  92. package/src/sqlite/errors/fields.ts +19 -0
  93. package/src/sqlite/errors/index.ts +19 -0
  94. package/src/sqlite/errors/normalize.ts +229 -0
  95. package/src/sqlite/errors/requirements.ts +71 -0
  96. package/src/sqlite/errors/types.ts +29 -0
  97. package/src/sqlite/executor.ts +229 -0
  98. package/src/sqlite/function/aggregate.ts +2 -0
  99. package/src/sqlite/function/core.ts +2 -0
  100. package/src/sqlite/function/index.ts +19 -0
  101. package/src/sqlite/function/string.ts +2 -0
  102. package/src/sqlite/function/temporal.ts +100 -0
  103. package/src/sqlite/function/window.ts +2 -0
  104. package/src/sqlite/internal/dialect.ts +42 -0
  105. package/src/sqlite/internal/dsl.ts +6979 -0
  106. package/src/sqlite/internal/renderer.ts +51 -0
  107. package/src/sqlite/json.ts +39 -0
  108. package/src/sqlite/query-extension.ts +2 -0
  109. package/src/sqlite/query.ts +196 -0
  110. package/src/sqlite/renderer.ts +51 -0
  111. package/src/sqlite.ts +14 -0
  112. package/src/standard/column.ts +163 -0
  113. package/src/standard/datatypes/index.ts +83 -0
  114. package/src/standard/datatypes/spec.ts +98 -0
  115. package/src/standard/dialect.ts +40 -0
  116. package/src/standard/function/aggregate.ts +2 -0
  117. package/src/standard/function/core.ts +2 -0
  118. package/src/standard/function/index.ts +18 -0
  119. package/src/standard/function/string.ts +2 -0
  120. package/src/standard/function/temporal.ts +78 -0
  121. package/src/standard/function/window.ts +2 -0
  122. package/src/standard/internal/renderer.ts +45 -0
  123. package/src/standard/query.ts +152 -0
  124. package/src/standard/renderer.ts +21 -0
  125. package/src/standard/table.ts +147 -0
  126. package/src/standard.ts +18 -0
  127. package/src/internal/aggregation-validation.ts +0 -57
  128. package/src/mysql/table.ts +0 -157
@@ -0,0 +1,2046 @@
1
+ import * as Schema from "effect/Schema"
2
+
3
+ import * as Query from "../query.js"
4
+ import * as Expression from "../scalar.js"
5
+ import * as Table from "../table.js"
6
+ import * as QueryAst from "../query-ast.js"
7
+ import { renderDbTypeName, type RenderState, type RenderValueContext, type SqlDialect } from "../dialect.js"
8
+ import * as ExpressionAst from "../expression-ast.js"
9
+ import * as JsonPath from "../json/path.js"
10
+ import { renderMysqlMutationLockMode, renderSelectLockMode } from "../dsl-plan-runtime.js"
11
+ import { expectConflictClause } from "../dsl-mutation-runtime.js"
12
+ import { expectDdlClauseKind, expectTruncateClause, normalizeStatementFlag, normalizeStatementIdentifier, renderTransactionIsolationLevel } from "../dsl-transaction-ddl-runtime.js"
13
+ import {
14
+ renderJsonSelectSql,
15
+ renderSelectSql,
16
+ toDriverValue
17
+ } from "../runtime/driver-value-mapping.js"
18
+ import { normalizeDbValue } from "../runtime/normalize.js"
19
+ import { flattenSelection, type Projection } from "../projections.js"
20
+ import * as SchemaExpression from "../schema-expression.js"
21
+ import { renderReferentialAction, validateOptions, type DdlExpressionLike, type TableOptionSpec } from "../table-options.js"
22
+ import * as Casing from "../casing.js"
23
+
24
+ const renderDbType = (
25
+ dialect: SqlDialect,
26
+ dbType: Expression.DbType.Any
27
+ ): string => {
28
+ if (dialect.name === "mysql" && dbType.kind === "uuid") {
29
+ return "char(36)"
30
+ }
31
+ return renderDbTypeName(dbType.kind)
32
+ }
33
+
34
+ const isArrayDbType = (dbType: Expression.DbType.Any): boolean =>
35
+ "element" in dbType
36
+
37
+ const renderCastType = (
38
+ dialect: SqlDialect,
39
+ dbType: unknown
40
+ ): string => {
41
+ const kind = (dbType as { readonly kind?: string } | undefined)?.kind as string
42
+ if (dialect.name !== "mysql") {
43
+ return renderDbTypeName(kind)
44
+ }
45
+ switch (kind) {
46
+ case "text":
47
+ return "char"
48
+ case "uuid":
49
+ return "char(36)"
50
+ case "numeric":
51
+ return "decimal"
52
+ case "timestamp":
53
+ return "datetime"
54
+ case "bool":
55
+ case "boolean":
56
+ return "boolean"
57
+ case "json":
58
+ return "json"
59
+ default:
60
+ return renderDbTypeName(kind)
61
+ }
62
+ }
63
+
64
+ const renderMysqlDdlString = (value: string): string =>
65
+ `'${value.replaceAll("'", "''")}'`
66
+
67
+ const renderMysqlDdlBytes = (value: Uint8Array): string =>
68
+ `x'${Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("")}'`
69
+
70
+ const renderMysqlDdlLiteral = (
71
+ value: unknown,
72
+ state: RenderState,
73
+ context: RenderValueContext = {}
74
+ ): string => {
75
+ const driverValue = toDriverValue(value, {
76
+ dialect: "mysql",
77
+ valueMappings: state.valueMappings,
78
+ ...context
79
+ })
80
+ if (driverValue === null) {
81
+ return "null"
82
+ }
83
+ switch (typeof driverValue) {
84
+ case "boolean":
85
+ return driverValue ? "true" : "false"
86
+ case "number":
87
+ if (!Number.isFinite(driverValue)) {
88
+ throw new Error("Expected a finite numeric value")
89
+ }
90
+ return String(driverValue)
91
+ case "bigint":
92
+ return driverValue.toString()
93
+ case "string":
94
+ return renderMysqlDdlString(driverValue)
95
+ case "object":
96
+ if (driverValue instanceof Uint8Array) {
97
+ return renderMysqlDdlBytes(driverValue)
98
+ }
99
+ break
100
+ }
101
+ throw new Error("Unsupported mysql DDL literal value")
102
+ }
103
+
104
+ const renderDdlExpression = (
105
+ expression: DdlExpressionLike,
106
+ state: RenderState,
107
+ dialect: SqlDialect
108
+ ): string => {
109
+ if (SchemaExpression.isSchemaExpression(expression)) {
110
+ return SchemaExpression.render(expression)
111
+ }
112
+ return renderExpression(expression, state, {
113
+ ...dialect,
114
+ renderLiteral: renderMysqlDdlLiteral
115
+ })
116
+ }
117
+
118
+ const renderMysqlMutationLimit = (
119
+ expression: Expression.Any,
120
+ state: RenderState,
121
+ dialect: SqlDialect
122
+ ): string => {
123
+ const ast = (expression as Expression.Any & {
124
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
125
+ })[ExpressionAst.TypeId]
126
+ if (ast.kind === "literal" && typeof ast.value === "number" && Number.isInteger(ast.value) && ast.value >= 0) {
127
+ return String(ast.value)
128
+ }
129
+ return renderExpression(expression, state, dialect)
130
+ }
131
+
132
+ const casingForTable = (
133
+ table: Table.AnyTable,
134
+ state: RenderState
135
+ ): Casing.Options | undefined =>
136
+ Casing.merge(state.casing, table[Table.TypeId].casing)
137
+
138
+ const casedColumnName = (
139
+ columnName: string,
140
+ state: RenderState,
141
+ tableName?: string
142
+ ): string => {
143
+ if (tableName !== undefined) {
144
+ const mapped = state.sourceNames?.get(tableName)?.columns.get(columnName)
145
+ if (mapped !== undefined) {
146
+ return mapped
147
+ }
148
+ }
149
+ return Casing.applyCategory(state.casing, "columns", columnName)
150
+ }
151
+
152
+ const casedTableReferenceName = (
153
+ tableName: string,
154
+ state: RenderState
155
+ ): string =>
156
+ state.sourceNames?.get(tableName)?.tableName ?? Casing.applyCategory(state.casing, "tables", tableName)
157
+
158
+ const quoteColumn = (
159
+ columnName: string,
160
+ state: RenderState,
161
+ dialect: SqlDialect,
162
+ tableName?: string
163
+ ): string => dialect.quoteIdentifier(casedColumnName(columnName, state, tableName))
164
+
165
+ const stateWithTableCasing = (
166
+ state: RenderState,
167
+ source: unknown
168
+ ): RenderState =>
169
+ typeof source === "object" && source !== null && Table.TypeId in source
170
+ ? { ...state, casing: casingForTable(source as Table.AnyTable, state) }
171
+ : state
172
+
173
+ const referenceCasing = (
174
+ reference: { readonly casing?: Casing.Options },
175
+ state: RenderState
176
+ ): Casing.Options | undefined =>
177
+ Casing.merge(state.casing, reference.casing)
178
+
179
+ const renderReferenceTable = (
180
+ reference: {
181
+ readonly tableName: string
182
+ readonly schemaName?: string
183
+ readonly casing?: Casing.Options
184
+ },
185
+ state: RenderState,
186
+ dialect: SqlDialect
187
+ ): string => {
188
+ const casing = referenceCasing(reference, state)
189
+ const tableName = Casing.applyCategory(casing, "tables", reference.tableName)
190
+ const schemaName = reference.schemaName === undefined
191
+ ? undefined
192
+ : Casing.applyCategory(casing, "schemas", reference.schemaName)
193
+ return dialect.renderTableReference(tableName, tableName, schemaName)
194
+ }
195
+
196
+ const quoteReferenceColumn = (
197
+ columnName: string,
198
+ reference: { readonly casing?: Casing.Options },
199
+ state: RenderState,
200
+ dialect: SqlDialect
201
+ ): string =>
202
+ dialect.quoteIdentifier(Casing.applyCategory(referenceCasing(reference, state), "columns", columnName))
203
+
204
+ const registerSourceReference = (
205
+ source: unknown,
206
+ tableName: string,
207
+ state: RenderState
208
+ ): void => {
209
+ if (typeof source !== "object" || source === null) {
210
+ return
211
+ }
212
+ if (Table.TypeId in source) {
213
+ const table = source as Table.AnyTable
214
+ const tableState = table[Table.TypeId]
215
+ const casing = casingForTable(table, state)
216
+ const renderedTableName = tableState.kind === "alias"
217
+ ? tableName
218
+ : Casing.applyCategory(casing, "tables", tableState.baseName)
219
+ const columns = new Map(
220
+ Object.keys(tableState.fields).map((columnName) => [
221
+ columnName,
222
+ Casing.applyCategory(casing, "columns", columnName)
223
+ ] as const)
224
+ )
225
+ state.sourceNames?.set(tableName, {
226
+ tableName: renderedTableName,
227
+ columns
228
+ })
229
+ return
230
+ }
231
+ if ("columns" in source && typeof source.columns === "object" && source.columns !== null) {
232
+ state.sourceNames?.set(tableName, {
233
+ tableName,
234
+ columns: new Map(Object.keys(source.columns).map((columnName) => [columnName, columnName] as const))
235
+ })
236
+ }
237
+ }
238
+
239
+ const registerQuerySources = (
240
+ ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
241
+ state: RenderState
242
+ ): void => {
243
+ if (ast.from !== undefined) {
244
+ registerSourceReference(ast.from.source, ast.from.tableName, state)
245
+ }
246
+ for (const source of ast.fromSources ?? []) {
247
+ registerSourceReference(source.source, source.tableName, state)
248
+ }
249
+ for (const join of ast.joins) {
250
+ registerSourceReference(join.source, join.tableName, state)
251
+ }
252
+ if (ast.into !== undefined) {
253
+ registerSourceReference(ast.into.source, ast.into.tableName, state)
254
+ }
255
+ if (ast.target !== undefined) {
256
+ registerSourceReference(ast.target.source, ast.target.tableName, state)
257
+ }
258
+ for (const target of ast.targets ?? []) {
259
+ registerSourceReference(target.source, target.tableName, state)
260
+ }
261
+ if (ast.using !== undefined) {
262
+ registerSourceReference(ast.using.source, ast.using.tableName, state)
263
+ }
264
+ }
265
+
266
+ const renderColumnDefinition = (
267
+ dialect: SqlDialect,
268
+ state: RenderState,
269
+ columnName: string,
270
+ column: Table.AnyTable[typeof Table.TypeId]["fields"][string],
271
+ tableName?: string,
272
+ casing?: Casing.Options
273
+ ): string => {
274
+ const expressionState = { ...state, casing, rowLocalColumns: true }
275
+ if (isArrayDbType(column.metadata.dbType)) {
276
+ throw new Error("Unsupported mysql array column options")
277
+ }
278
+ const clauses = [
279
+ quoteColumn(columnName, state, dialect, tableName),
280
+ column.metadata.ddlType === undefined
281
+ ? renderDbType(dialect, column.metadata.dbType)
282
+ : renderDbTypeName(column.metadata.ddlType)
283
+ ]
284
+ if (column.metadata.identity) {
285
+ throw new Error("Unsupported mysql identity column options")
286
+ } else if (column.metadata.generatedValue) {
287
+ clauses.push(`generated always as (${renderDdlExpression(column.metadata.generatedValue, expressionState, dialect)}) stored`)
288
+ } else if (column.metadata.defaultValue) {
289
+ clauses.push(`default ${renderDdlExpression(column.metadata.defaultValue, expressionState, dialect)}`)
290
+ }
291
+ if (!column.metadata.nullable) {
292
+ clauses.push("not null")
293
+ }
294
+ return clauses.join(" ")
295
+ }
296
+
297
+ const renderCreateTableSql = (
298
+ targetSource: QueryAst.FromClause,
299
+ state: RenderState,
300
+ dialect: SqlDialect,
301
+ ifNotExists: unknown
302
+ ): string => {
303
+ const normalizedIfNotExists = normalizeStatementFlag(ifNotExists)
304
+ const table = targetSource.source as Table.AnyTable
305
+ const tableCasing = casingForTable(table, state)
306
+ const fields = table[Table.TypeId].fields
307
+ const definitions = Object.entries(fields).map(([columnName, column]) =>
308
+ renderColumnDefinition(dialect, state, columnName, column, targetSource.tableName, tableCasing)
309
+ )
310
+ const options = table[Table.OptionsSymbol] as unknown
311
+ const tableOptions = (Array.isArray(options) ? options : [options]) as readonly TableOptionSpec[]
312
+ validateOptions(table[Table.TypeId].name, fields, tableOptions)
313
+ for (const option of tableOptions) {
314
+ if (typeof option !== "object" || option === null || !("kind" in option)) {
315
+ continue
316
+ }
317
+ switch (option.kind) {
318
+ case "primaryKey":
319
+ if (option.deferrable || option.initiallyDeferred) {
320
+ throw new Error("Unsupported mysql primary key constraint options")
321
+ }
322
+ definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "constraints", option.name))} ` : ""}primary key (${option.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
323
+ break
324
+ case "unique":
325
+ if (option.nullsNotDistinct || option.deferrable || option.initiallyDeferred) {
326
+ throw new Error("Unsupported mysql unique constraint options")
327
+ }
328
+ definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "constraints", option.name))} ` : ""}unique${option.nullsNotDistinct ? " nulls not distinct" : ""} (${option.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
329
+ break
330
+ case "foreignKey": {
331
+ if (option.deferrable || option.initiallyDeferred) {
332
+ throw new Error("Unsupported mysql foreign key constraint options")
333
+ }
334
+ const reference = typeof option.references === "function"
335
+ ? option.references()
336
+ : option.references
337
+ definitions.push(
338
+ `${option.name ? `constraint ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "constraints", option.name))} ` : ""}foreign key (${option.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")}) references ${renderReferenceTable(reference, state, dialect)} (${reference.columns.map((column) => quoteReferenceColumn(column, reference, state, dialect)).join(", ")})${option.onDelete !== undefined ? ` on delete ${renderReferentialAction(option.onDelete)}` : ""}${option.onUpdate !== undefined ? ` on update ${renderReferentialAction(option.onUpdate)}` : ""}${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`
339
+ )
340
+ break
341
+ }
342
+ case "check":
343
+ if (option.noInherit) {
344
+ throw new Error("Unsupported mysql check constraint options")
345
+ }
346
+ definitions.push(
347
+ `constraint ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "constraints", option.name))} check (${renderDdlExpression(option.predicate, { ...state, casing: tableCasing, rowLocalColumns: true }, dialect)})${option.noInherit ? " no inherit" : ""}`
348
+ )
349
+ break
350
+ case "index":
351
+ break
352
+ default:
353
+ throw new Error("Unsupported table option kind")
354
+ }
355
+ }
356
+ return `create table${normalizedIfNotExists ? " if not exists" : ""} ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${definitions.join(", ")})`
357
+ }
358
+
359
+ const renderCreateIndexSql = (
360
+ targetSource: QueryAst.FromClause,
361
+ ddl: Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
362
+ state: RenderState,
363
+ dialect: SqlDialect
364
+ ): string => {
365
+ const unique = normalizeStatementFlag(ddl.unique)
366
+ const ifNotExists = normalizeStatementFlag(ddl.ifNotExists)
367
+ const name = normalizeStatementIdentifier("createIndex", "option 'name'", ddl.name)
368
+ if (ifNotExists) {
369
+ throw new Error("Unsupported mysql create index options")
370
+ }
371
+ const maybeIfNotExists = dialect.name === "postgres" && ifNotExists ? " if not exists" : ""
372
+ const table = targetSource.source as Table.AnyTable
373
+ const tableCasing = casingForTable(table, state)
374
+ return `create${unique ? " unique" : ""} index${maybeIfNotExists} ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "indexes", name))} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${ddl.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")})`
375
+ }
376
+
377
+ const renderDropIndexSql = (
378
+ targetSource: QueryAst.FromClause,
379
+ ddl: Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
380
+ state: RenderState,
381
+ dialect: SqlDialect
382
+ ): string => {
383
+ const ifExists = normalizeStatementFlag(ddl.ifExists)
384
+ const name = normalizeStatementIdentifier("dropIndex", "option 'name'", ddl.name)
385
+ if (ifExists) {
386
+ throw new Error("Unsupported mysql drop index options")
387
+ }
388
+ const table = targetSource.source as Table.AnyTable
389
+ const tableCasing = casingForTable(table, state)
390
+ return dialect.name === "postgres"
391
+ ? `drop index${ifExists ? " if exists" : ""} ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "indexes", name))}`
392
+ : `drop index ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "indexes", name))} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
393
+ }
394
+
395
+ const isExpression = (value: unknown): value is Expression.Any =>
396
+ value !== null && typeof value === "object" && Expression.TypeId in value
397
+
398
+ const isJsonDbType = (dbType: Expression.DbType.Any): boolean => {
399
+ if (dbType.kind === "jsonb" || dbType.kind === "json") {
400
+ return true
401
+ }
402
+ if (!("variant" in dbType)) {
403
+ return false
404
+ }
405
+ const variant = dbType.variant as string
406
+ return variant === "json" || variant === "jsonb"
407
+ }
408
+
409
+ const isJsonExpression = (value: unknown): value is Expression.Any =>
410
+ isExpression(value) && isJsonDbType(value[Expression.TypeId].dbType)
411
+
412
+ const expectValueExpression = (
413
+ _functionName: string,
414
+ value: unknown
415
+ ): Expression.Any => value as Expression.Any
416
+
417
+ const expectBinaryExpressions = (
418
+ _functionName: string,
419
+ left: unknown,
420
+ right: unknown
421
+ ): readonly [Expression.Any, Expression.Any] => [left as Expression.Any, right as Expression.Any]
422
+
423
+ const renderBinaryExpression = (
424
+ functionName: string,
425
+ operator: string,
426
+ left: unknown,
427
+ right: unknown,
428
+ state: RenderState,
429
+ dialect: SqlDialect
430
+ ): string => {
431
+ const [leftExpression, rightExpression] = expectBinaryExpressions(functionName, left, right)
432
+ return `(${renderExpression(leftExpression, state, dialect)} ${operator} ${renderExpression(rightExpression, state, dialect)})`
433
+ }
434
+
435
+ const unsupportedJsonFeature = (
436
+ dialect: SqlDialect,
437
+ feature: string
438
+ ): never => {
439
+ const error = new Error(`Unsupported JSON feature for ${dialect.name}: ${feature}`) as Error & {
440
+ readonly tag: string
441
+ readonly dialect: string
442
+ readonly feature: string
443
+ }
444
+ Object.assign(error, {
445
+ tag: `@${dialect.name}/unsupported/json-feature`,
446
+ dialect: dialect.name,
447
+ feature
448
+ })
449
+ throw error
450
+ }
451
+
452
+ const extractJsonBase = (node: Record<string, unknown>): unknown =>
453
+ node.value ?? node.base ?? node.input ?? node.left ?? node.target
454
+
455
+ const isJsonPathValue = (value: unknown): value is JsonPath.Path<any> =>
456
+ value !== null && typeof value === "object" && JsonPath.TypeId in value
457
+
458
+ const isOptionalJsonPathNumber = (value: unknown): boolean =>
459
+ value === undefined || (typeof value === "number" && Number.isFinite(value))
460
+
461
+ const isJsonPathSegment = (segment: unknown): boolean => {
462
+ if (typeof segment === "string") {
463
+ return true
464
+ }
465
+ if (typeof segment === "number") {
466
+ return Number.isFinite(segment)
467
+ }
468
+ if (segment === null || typeof segment !== "object" || !("kind" in segment)) {
469
+ return false
470
+ }
471
+ switch ((segment as { readonly kind?: unknown }).kind) {
472
+ case "key":
473
+ return typeof (segment as { readonly key?: unknown }).key === "string"
474
+ case "index": {
475
+ const index = (segment as { readonly index?: unknown }).index
476
+ return typeof index === "number" && Number.isFinite(index)
477
+ }
478
+ case "wildcard":
479
+ case "descend":
480
+ return true
481
+ case "slice":
482
+ return isOptionalJsonPathNumber((segment as { readonly start?: unknown }).start) &&
483
+ isOptionalJsonPathNumber((segment as { readonly end?: unknown }).end)
484
+ default:
485
+ return false
486
+ }
487
+ }
488
+
489
+ const validateJsonPathSegments = (segments: unknown): ReadonlyArray<JsonPath.AnySegment> => {
490
+ if (!Array.isArray(segments)) {
491
+ throw new Error("JSON path expressions require a segment array")
492
+ }
493
+ if (segments.some((segment) => !isJsonPathSegment(segment))) {
494
+ throw new Error("JSON path segments require string, number, or path segment objects")
495
+ }
496
+ return segments as ReadonlyArray<JsonPath.AnySegment>
497
+ }
498
+
499
+ const extractJsonPathSegments = (node: Record<string, unknown>): ReadonlyArray<JsonPath.AnySegment> => {
500
+ const path = node.path ?? node.segments ?? node.keys
501
+ if (isJsonPathValue(path)) {
502
+ return validateJsonPathSegments(path.segments)
503
+ }
504
+ if (Array.isArray(path)) {
505
+ return validateJsonPathSegments(path)
506
+ }
507
+ if (node.segments !== undefined) {
508
+ return validateJsonPathSegments(node.segments)
509
+ }
510
+ if ("key" in node) {
511
+ return [JsonPath.key(String(node.key))]
512
+ }
513
+ if ("segment" in node) {
514
+ const segment = node.segment
515
+ if (typeof segment === "string") {
516
+ return [JsonPath.key(segment)]
517
+ }
518
+ if (typeof segment === "number") {
519
+ return [JsonPath.index(segment)]
520
+ }
521
+ if (segment !== null && typeof segment === "object" && JsonPath.SegmentTypeId in segment) {
522
+ return [segment as JsonPath.AnySegment]
523
+ }
524
+ return []
525
+ }
526
+ if ("right" in node && isJsonPathValue(node.right)) {
527
+ return validateJsonPathSegments(node.right.segments)
528
+ }
529
+ return []
530
+ }
531
+
532
+ const extractJsonKeys = (
533
+ node: Record<string, unknown>,
534
+ segments: ReadonlyArray<JsonPath.AnySegment>
535
+ ): readonly unknown[] =>
536
+ Array.isArray(node.keys)
537
+ ? node.keys
538
+ : segments.map((segment) =>
539
+ typeof segment === "object" && segment !== null && segment.kind === "key"
540
+ ? segment.key
541
+ : segment
542
+ )
543
+
544
+ const extractJsonValue = (node: Record<string, unknown>): unknown =>
545
+ node.newValue ?? node.insert ?? node.right
546
+
547
+ const renderJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
548
+ const renderKey = (value: string): string =>
549
+ /^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
550
+ ? `.${value}`
551
+ : `.${JSON.stringify(value)}`
552
+ if (typeof segment === "string") {
553
+ return renderKey(segment)
554
+ }
555
+ if (typeof segment === "number") {
556
+ return `[${segment}]`
557
+ }
558
+ switch (segment.kind) {
559
+ case "key":
560
+ return renderKey(segment.key)
561
+ case "index":
562
+ return `[${segment.index}]`
563
+ case "wildcard":
564
+ return "[*]"
565
+ case "slice":
566
+ return `[${segment.start ?? 0} to ${segment.end ?? "last"}]`
567
+ case "descend":
568
+ return ".**"
569
+ default:
570
+ throw new Error("Unsupported JSON path segment")
571
+ }
572
+ }
573
+
574
+ const renderMySqlJsonIndex = (index: number): string =>
575
+ index >= 0 ? String(index) : index === -1 ? "last" : `last-${Math.abs(index) - 1}`
576
+
577
+ const renderMySqlJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
578
+ if (typeof segment === "number") {
579
+ return `[${renderMySqlJsonIndex(segment)}]`
580
+ }
581
+ if (typeof segment === "object" && segment !== null && segment.kind === "index") {
582
+ return `[${renderMySqlJsonIndex(segment.index)}]`
583
+ }
584
+ if (typeof segment === "object" && segment !== null && segment.kind === "slice") {
585
+ return `[${renderMySqlJsonIndex(segment.start ?? 0)} to ${segment.end === undefined ? "last" : renderMySqlJsonIndex(segment.end)}]`
586
+ }
587
+ if (typeof segment === "object" && segment !== null && segment.kind === "descend") {
588
+ return "**"
589
+ }
590
+ return renderJsonPathSegment(segment)
591
+ }
592
+
593
+ const renderJsonPathStringLiteral = (
594
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
595
+ renderSegment: (segment: JsonPath.AnySegment | string | number) => string = renderJsonPathSegment
596
+ ): string => {
597
+ let path = "$"
598
+ for (const segment of segments) {
599
+ path += renderSegment(segment)
600
+ }
601
+ return path
602
+ }
603
+
604
+ const renderMySqlJsonPath = (
605
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
606
+ state: RenderState,
607
+ dialect: SqlDialect
608
+ ): string => dialect.renderLiteral(renderJsonPathStringLiteral(segments, renderMySqlJsonPathSegment), state)
609
+
610
+ const isJsonArrayIndexSegment = (segment: JsonPath.AnySegment | string | number | undefined): boolean =>
611
+ typeof segment === "number" || (typeof segment === "object" && segment !== null && segment.kind === "index")
612
+
613
+ const isExactJsonPathSegment = (segment: JsonPath.AnySegment | string | number): boolean =>
614
+ typeof segment === "string" ||
615
+ typeof segment === "number" ||
616
+ (typeof segment === "object" && segment !== null && (segment.kind === "key" || segment.kind === "index"))
617
+
618
+ const assertMySqlJsonMutationPath = (
619
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>
620
+ ): void => {
621
+ if (!segments.every(isExactJsonPathSegment)) {
622
+ throw new Error("MySQL JSON mutation paths require key/index segments")
623
+ }
624
+ }
625
+
626
+ const renderMySqlJsonInsertPath = (
627
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
628
+ insertAfter: boolean,
629
+ state: RenderState,
630
+ dialect: SqlDialect
631
+ ): string => {
632
+ if (!insertAfter || segments.length === 0) {
633
+ return renderMySqlJsonPath(segments, state, dialect)
634
+ }
635
+ const last = segments[segments.length - 1]
636
+ const nextSegments = segments.slice(0, -1)
637
+ if (typeof last === "number") {
638
+ return renderMySqlJsonPath([...nextSegments, last + 1], state, dialect)
639
+ }
640
+ if (typeof last === "object" && last !== null && last.kind === "index") {
641
+ return renderMySqlJsonPath([...nextSegments, { ...last, index: last.index + 1 }], state, dialect)
642
+ }
643
+ return renderMySqlJsonPath(segments, state, dialect)
644
+ }
645
+
646
+ const renderPostgresJsonPathArray = (
647
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
648
+ state: RenderState,
649
+ dialect: SqlDialect
650
+ ): string => `array[${segments.map((segment) => {
651
+ if (typeof segment === "string") {
652
+ return dialect.renderLiteral(segment, state)
653
+ }
654
+ if (typeof segment === "number") {
655
+ return dialect.renderLiteral(String(segment), state)
656
+ }
657
+ switch (segment.kind) {
658
+ case "key":
659
+ return dialect.renderLiteral(segment.key, state)
660
+ case "index":
661
+ return dialect.renderLiteral(String(segment.index), state)
662
+ default:
663
+ throw new Error("Postgres JSON traversal requires exact key/index segments")
664
+ }
665
+ }).join(", ")}]`
666
+
667
+ const renderPostgresTextLiteral = (
668
+ value: string,
669
+ state: RenderState,
670
+ dialect: SqlDialect
671
+ ): string => `cast(${dialect.renderLiteral(value, state)} as text)`
672
+
673
+ const renderPostgresJsonAccessStep = (
674
+ segment: JsonPath.AnySegment,
675
+ textMode: boolean,
676
+ state: RenderState,
677
+ dialect: SqlDialect
678
+ ): string => {
679
+ switch (segment.kind) {
680
+ case "key":
681
+ return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(segment.key, state)}`
682
+ case "index":
683
+ return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(String(segment.index), state)}`
684
+ default:
685
+ throw new Error("Postgres exact JSON access requires key/index segments")
686
+ }
687
+ }
688
+
689
+ const renderPostgresJsonValue = (
690
+ value: unknown,
691
+ state: RenderState,
692
+ dialect: SqlDialect
693
+ ): string => {
694
+ if (!isExpression(value)) {
695
+ throw new Error("Expected a JSON expression")
696
+ }
697
+ const rendered = renderExpression(value, state, dialect)
698
+ return value[Expression.TypeId].dbType.kind === "jsonb"
699
+ ? rendered
700
+ : `cast(${rendered} as jsonb)`
701
+ }
702
+
703
+ const expressionDriverContext = (
704
+ expression: Expression.Any,
705
+ state: RenderState,
706
+ dialect: SqlDialect
707
+ ) => ({
708
+ dialect: dialect.name,
709
+ valueMappings: state.valueMappings,
710
+ dbType: expression[Expression.TypeId].dbType,
711
+ runtimeSchema: expression[Expression.TypeId].runtimeSchema,
712
+ driverValueMapping: expression[Expression.TypeId].driverValueMapping
713
+ })
714
+
715
+ const renderMySqlStructuredJsonLiteral = (
716
+ expression: Expression.Any,
717
+ state: RenderState
718
+ ): string | undefined => {
719
+ const ast = (expression as Expression.Any & {
720
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
721
+ })[ExpressionAst.TypeId]
722
+ if (ast.kind !== "literal" || ast.value === null || typeof ast.value !== "object") {
723
+ return undefined
724
+ }
725
+ state.params.push(JSON.stringify(ast.value))
726
+ return "cast(? as json)"
727
+ }
728
+
729
+ const renderJsonInputExpression = (
730
+ expression: Expression.Any,
731
+ state: RenderState,
732
+ dialect: SqlDialect
733
+ ): string => {
734
+ if (dialect.name === "mysql") {
735
+ const jsonLiteral = renderMySqlStructuredJsonLiteral(expression, state)
736
+ if (jsonLiteral !== undefined) {
737
+ return jsonLiteral
738
+ }
739
+ }
740
+ return renderJsonSelectSql(
741
+ renderExpression(expression, state, dialect),
742
+ expressionDriverContext(expression, state, dialect)
743
+ )
744
+ }
745
+
746
+ const encodeArrayValues = (
747
+ values: readonly unknown[],
748
+ column: Table.AnyTable[typeof Table.TypeId]["fields"][string],
749
+ state: RenderState,
750
+ dialect: SqlDialect
751
+ ): readonly unknown[] =>
752
+ values.map((value) => {
753
+ if (value === null && column.metadata.nullable) {
754
+ return null
755
+ }
756
+ const runtimeSchemaAccepts = column.schema !== undefined &&
757
+ (Schema.is(column.schema) as (candidate: unknown) => boolean)(value)
758
+ const normalizedValue = runtimeSchemaAccepts
759
+ ? value
760
+ : normalizeDbValue(column.metadata.dbType, value)
761
+ const encodedValue = column.schema === undefined || runtimeSchemaAccepts
762
+ ? normalizedValue
763
+ : (Schema.decodeUnknownSync as any)(column.schema)(normalizedValue)
764
+ return toDriverValue(encodedValue, {
765
+ dialect: dialect.name,
766
+ valueMappings: state.valueMappings,
767
+ dbType: column.metadata.dbType,
768
+ runtimeSchema: column.schema,
769
+ driverValueMapping: column.metadata.driverValueMapping
770
+ })
771
+ })
772
+
773
+ const renderPostgresJsonKind = (
774
+ value: Expression.Any
775
+ ): "json" | "jsonb" => value[Expression.TypeId].dbType.kind === "jsonb" ? "jsonb" : "json"
776
+
777
+ const renderJsonOpaquePath = (
778
+ value: unknown,
779
+ state: RenderState,
780
+ dialect: SqlDialect
781
+ ): string => {
782
+ if (isJsonPathValue(value)) {
783
+ const renderSegment = dialect.name === "mysql"
784
+ ? renderMySqlJsonPathSegment
785
+ : renderJsonPathSegment
786
+ return dialect.renderLiteral(renderJsonPathStringLiteral(value.segments, renderSegment), state)
787
+ }
788
+ if (typeof value === "string") {
789
+ if (value.trim().length === 0) {
790
+ throw new Error("SQL/JSON path input must be a non-empty string")
791
+ }
792
+ return dialect.renderLiteral(value, state)
793
+ }
794
+ if (isExpression(value)) {
795
+ const ast = (value as Expression.Any & {
796
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
797
+ })[ExpressionAst.TypeId]
798
+ if (ast.kind === "literal" && typeof ast.value === "string" && ast.value.trim().length === 0) {
799
+ throw new Error("SQL/JSON path input must be a non-empty string")
800
+ }
801
+ return renderExpression(value, state, dialect)
802
+ }
803
+ throw new Error("Unsupported SQL/JSON path input")
804
+ }
805
+
806
+ const renderFunctionName = (name: unknown): string => {
807
+ return name as string
808
+ }
809
+
810
+ const renderExtractField = (field: Expression.Any): string => {
811
+ const ast = (field as Expression.Any & {
812
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
813
+ })[ExpressionAst.TypeId] as ExpressionAst.LiteralNode<string>
814
+ return ast.value
815
+ }
816
+
817
+ const renderFunctionCall = (
818
+ name: unknown,
819
+ args: unknown,
820
+ state: RenderState,
821
+ dialect: SqlDialect
822
+ ): string => {
823
+ const functionName = renderFunctionName(name)
824
+ const functionArgs = args as readonly Expression.Any[]
825
+ if (functionName === "array") {
826
+ return `ARRAY[${functionArgs.map((arg) => renderExpression(arg, state, dialect)).join(", ")}]`
827
+ }
828
+ if (functionName === "extract") {
829
+ const field = functionArgs[0]!
830
+ const source = functionArgs[1]!
831
+ return `extract(${renderExtractField(field)} from ${renderExpression(source, state, dialect)})`
832
+ }
833
+ const renderedArgs = functionArgs.map((arg) => renderExpression(arg, state, dialect)).join(", ")
834
+ if (functionArgs.length === 0) {
835
+ switch (functionName) {
836
+ case "current_date":
837
+ case "current_time":
838
+ case "current_timestamp":
839
+ case "localtime":
840
+ case "localtimestamp":
841
+ return functionName
842
+ default:
843
+ return `${functionName}()`
844
+ }
845
+ }
846
+ return `${functionName}(${renderedArgs})`
847
+ }
848
+
849
+ const renderJsonExpression = (
850
+ expression: Expression.Any,
851
+ ast: Record<string, unknown>,
852
+ state: RenderState,
853
+ dialect: SqlDialect
854
+ ): string | undefined => {
855
+ const kind = typeof ast.kind === "string" ? ast.kind : undefined
856
+ if (!kind) {
857
+ return undefined
858
+ }
859
+
860
+ const base = extractJsonBase(ast)
861
+ const segments = extractJsonPathSegments(ast)
862
+ const exact = segments.every((segment) => segment.kind === "key" || segment.kind === "index")
863
+ const postgresExpressionKind = dialect.name === "postgres" && isJsonExpression(expression)
864
+ ? renderPostgresJsonKind(expression)
865
+ : undefined
866
+ const postgresBaseKind = dialect.name === "postgres" && isJsonExpression(base)
867
+ ? renderPostgresJsonKind(base)
868
+ : undefined
869
+
870
+ switch (kind) {
871
+ case "jsonGet":
872
+ case "jsonPath":
873
+ case "jsonAccess":
874
+ case "jsonTraverse":
875
+ case "jsonGetText":
876
+ case "jsonPathText":
877
+ case "jsonAccessText":
878
+ case "jsonTraverseText": {
879
+ if (!isExpression(base) || segments.length === 0) {
880
+ return undefined
881
+ }
882
+ const baseSql = renderExpression(base, state, dialect)
883
+ const textMode = kind.endsWith("Text") || ast.text === true || ast.asText === true
884
+ if (dialect.name === "postgres") {
885
+ if (exact) {
886
+ return segments.length === 1
887
+ ? `(${baseSql} ${renderPostgresJsonAccessStep(segments[0]!, textMode, state, dialect)})`
888
+ : `(${baseSql} ${textMode ? "#>>" : "#>"} ${renderPostgresJsonPathArray(segments, state, dialect)})`
889
+ }
890
+ const jsonPathLiteral = dialect.renderLiteral(renderJsonPathStringLiteral(segments), state)
891
+ const queried = `jsonb_path_query_first(${renderPostgresJsonValue(base, state, dialect)}, ${jsonPathLiteral})`
892
+ return textMode ? `(${queried} #>> '{}')` : queried
893
+ }
894
+ if (dialect.name === "mysql") {
895
+ const extracted = `json_extract(${baseSql}, ${renderMySqlJsonPath(segments, state, dialect)})`
896
+ return textMode ? `json_unquote(${extracted})` : extracted
897
+ }
898
+ return undefined
899
+ }
900
+ case "jsonHasKey":
901
+ case "jsonKeyExists":
902
+ case "jsonHasAnyKeys":
903
+ case "jsonHasAllKeys": {
904
+ if (!isExpression(base)) {
905
+ return undefined
906
+ }
907
+ const baseSql = dialect.name === "postgres"
908
+ ? renderPostgresJsonValue(base, state, dialect)
909
+ : renderExpression(base, state, dialect)
910
+ const keys = extractJsonKeys(ast, segments)
911
+ if (keys.length === 0) {
912
+ return undefined
913
+ }
914
+ if (keys.some((key) => typeof key !== "string" || key.length === 0)) {
915
+ throw new Error("json key predicates require string keys")
916
+ }
917
+ const keyNames = keys as readonly string[]
918
+ if (dialect.name === "postgres") {
919
+ if (kind === "jsonHasAnyKeys") {
920
+ return `(${baseSql} ?| array[${keyNames.map((key) => renderPostgresTextLiteral(key, state, dialect)).join(", ")}])`
921
+ }
922
+ if (kind === "jsonHasAllKeys") {
923
+ return `(${baseSql} ?& array[${keyNames.map((key) => renderPostgresTextLiteral(key, state, dialect)).join(", ")}])`
924
+ }
925
+ return `(${baseSql} ? ${renderPostgresTextLiteral(keyNames[0]!, state, dialect)})`
926
+ }
927
+ if (dialect.name === "mysql") {
928
+ const mode = kind === "jsonHasAllKeys" ? "all" : "one"
929
+ const modeSql = dialect.renderLiteral(mode, state)
930
+ const paths = keyNames.map((segment) => renderMySqlJsonPath([segment], state, dialect)).join(", ")
931
+ return `json_contains_path(${baseSql}, ${modeSql}, ${paths})`
932
+ }
933
+ return undefined
934
+ }
935
+ case "jsonConcat":
936
+ case "jsonMerge": {
937
+ if (!isExpression(ast.left) || !isExpression(ast.right)) {
938
+ return undefined
939
+ }
940
+ if (dialect.name === "postgres") {
941
+ return `(${renderPostgresJsonValue(ast.left, state, dialect)} || ${renderPostgresJsonValue(ast.right, state, dialect)})`
942
+ }
943
+ if (dialect.name === "mysql") {
944
+ return `json_merge_preserve(${renderJsonInputExpression(ast.left, state, dialect)}, ${renderJsonInputExpression(ast.right, state, dialect)})`
945
+ }
946
+ return undefined
947
+ }
948
+ case "jsonBuildObject": {
949
+ const entries = (ast as { readonly entries: readonly { readonly key: string; readonly value: Expression.Any }[] }).entries
950
+ const renderedEntries = entries.flatMap((entry) => [
951
+ dialect.renderLiteral(entry.key, state),
952
+ renderJsonInputExpression(entry.value, state, dialect)
953
+ ])
954
+ if (dialect.name === "postgres") {
955
+ return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_object(${renderedEntries.join(", ")})`
956
+ }
957
+ if (dialect.name === "mysql") {
958
+ return `json_object(${renderedEntries.join(", ")})`
959
+ }
960
+ return undefined
961
+ }
962
+ case "jsonBuildArray": {
963
+ const values = (ast as { readonly values: readonly Expression.Any[] }).values
964
+ const renderedValues = values.map((value) => renderJsonInputExpression(value, state, dialect)).join(", ")
965
+ if (dialect.name === "postgres") {
966
+ return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_array(${renderedValues})`
967
+ }
968
+ if (dialect.name === "mysql") {
969
+ return `json_array(${renderedValues})`
970
+ }
971
+ return undefined
972
+ }
973
+ case "jsonToJson":
974
+ if (!isExpression(base)) {
975
+ return undefined
976
+ }
977
+ if (dialect.name === "postgres") {
978
+ return `to_json(${renderJsonInputExpression(base, state, dialect)})`
979
+ }
980
+ if (dialect.name === "mysql") {
981
+ return renderMySqlStructuredJsonLiteral(base, state) ?? `cast(${renderExpression(base, state, dialect)} as json)`
982
+ }
983
+ return undefined
984
+ case "jsonToJsonb":
985
+ if (!isExpression(base)) {
986
+ return undefined
987
+ }
988
+ if (dialect.name === "postgres") {
989
+ return `to_jsonb(${renderJsonInputExpression(base, state, dialect)})`
990
+ }
991
+ if (dialect.name === "mysql") {
992
+ return renderMySqlStructuredJsonLiteral(base, state) ?? `cast(${renderExpression(base, state, dialect)} as json)`
993
+ }
994
+ return undefined
995
+ case "jsonTypeOf":
996
+ if (!isExpression(base)) {
997
+ return undefined
998
+ }
999
+ if (dialect.name === "postgres") {
1000
+ const baseSql = renderExpression(base, state, dialect)
1001
+ return `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof(${baseSql})`
1002
+ }
1003
+ if (dialect.name === "mysql") {
1004
+ return `json_type(${renderExpression(base, state, dialect)})`
1005
+ }
1006
+ return undefined
1007
+ case "jsonLength":
1008
+ if (!isExpression(base)) {
1009
+ return undefined
1010
+ }
1011
+ if (dialect.name === "postgres") {
1012
+ const baseSql = renderExpression(base, state, dialect)
1013
+ const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
1014
+ const arrayLength = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_array_length`
1015
+ const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
1016
+ return `(case when ${typeOf}(${baseSql}) = 'array' then ${arrayLength}(${baseSql}) when ${typeOf}(${baseSql}) = 'object' then (select count(*)::int from ${objectKeys}(${baseSql})) else null end)`
1017
+ }
1018
+ if (dialect.name === "mysql") {
1019
+ return `json_length(${renderExpression(base, state, dialect)})`
1020
+ }
1021
+ return undefined
1022
+ case "jsonKeys":
1023
+ if (!isExpression(base)) {
1024
+ return undefined
1025
+ }
1026
+ if (dialect.name === "postgres") {
1027
+ const baseSql = renderExpression(base, state, dialect)
1028
+ const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
1029
+ const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
1030
+ return `(case when ${typeOf}(${baseSql}) = 'object' then array(select ${objectKeys}(${baseSql})) else null end)`
1031
+ }
1032
+ if (dialect.name === "mysql") {
1033
+ return `json_keys(${renderExpression(base, state, dialect)})`
1034
+ }
1035
+ return undefined
1036
+ case "jsonStripNulls":
1037
+ if (!isExpression(base)) {
1038
+ return undefined
1039
+ }
1040
+ if (dialect.name === "postgres") {
1041
+ return `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_strip_nulls(${renderExpression(base, state, dialect)})`
1042
+ }
1043
+ unsupportedJsonFeature(dialect, "jsonStripNulls")
1044
+ return undefined
1045
+ case "jsonDelete":
1046
+ case "jsonDeletePath":
1047
+ case "jsonRemove": {
1048
+ if (!isExpression(base) || segments.length === 0) {
1049
+ return undefined
1050
+ }
1051
+ if (dialect.name === "postgres") {
1052
+ const baseSql = renderPostgresJsonValue(base, state, dialect)
1053
+ if (segments.length === 1 && (segments[0]!.kind === "key" || segments[0]!.kind === "index")) {
1054
+ const segment = segments[0]!
1055
+ return `(${baseSql} - ${segment.kind === "key"
1056
+ ? dialect.renderLiteral(segment.key, state)
1057
+ : dialect.renderLiteral(String(segment.index), state)})`
1058
+ }
1059
+ return `(${baseSql} #- ${renderPostgresJsonPathArray(segments, state, dialect)})`
1060
+ }
1061
+ if (dialect.name === "mysql") {
1062
+ assertMySqlJsonMutationPath(segments)
1063
+ return `json_remove(${renderExpression(base, state, dialect)}, ${renderMySqlJsonPath(segments, state, dialect)})`
1064
+ }
1065
+ return undefined
1066
+ }
1067
+ case "jsonSet":
1068
+ case "jsonInsert": {
1069
+ if (!isExpression(base) || segments.length === 0) {
1070
+ return undefined
1071
+ }
1072
+ const nextValue = extractJsonValue(ast)
1073
+ if (!isExpression(nextValue)) {
1074
+ return undefined
1075
+ }
1076
+ const createMissing = ast.createMissing === true
1077
+ const insertAfter = ast.insertAfter === true
1078
+ if (dialect.name === "postgres") {
1079
+ const functionName = kind === "jsonInsert" ? "jsonb_insert" : "jsonb_set"
1080
+ const extra =
1081
+ kind === "jsonInsert"
1082
+ ? `, ${insertAfter ? "true" : "false"}`
1083
+ : `, ${createMissing ? "true" : "false"}`
1084
+ return `${functionName}(${renderPostgresJsonValue(base, state, dialect)}, ${renderPostgresJsonPathArray(segments, state, dialect)}, ${renderPostgresJsonValue(nextValue, state, dialect)}${extra})`
1085
+ }
1086
+ if (dialect.name === "mysql") {
1087
+ assertMySqlJsonMutationPath(segments)
1088
+ const renderedBase = renderExpression(base, state, dialect)
1089
+ if (kind === "jsonInsert" && isJsonArrayIndexSegment(segments[segments.length - 1])) {
1090
+ const renderedPath = renderMySqlJsonInsertPath(segments, insertAfter, state, dialect)
1091
+ const renderedValue = renderJsonInputExpression(nextValue, state, dialect)
1092
+ return `json_array_insert(${renderedBase}, ${renderedPath}, ${renderedValue})`
1093
+ }
1094
+ const functionName = kind === "jsonInsert" ? "json_insert" : createMissing ? "json_set" : "json_replace"
1095
+ const renderedPath = renderMySqlJsonPath(segments, state, dialect)
1096
+ const renderedValue = renderJsonInputExpression(nextValue, state, dialect)
1097
+ return `${functionName}(${renderedBase}, ${renderedPath}, ${renderedValue})`
1098
+ }
1099
+ return undefined
1100
+ }
1101
+ case "jsonPathExists": {
1102
+ if (!isExpression(base)) {
1103
+ return undefined
1104
+ }
1105
+ const path = ast.path ?? ast.query ?? ast.right
1106
+ if (path === undefined) {
1107
+ return undefined
1108
+ }
1109
+ if (dialect.name === "postgres") {
1110
+ return `(${renderPostgresJsonValue(base, state, dialect)} @? ${renderJsonOpaquePath(path, state, dialect)})`
1111
+ }
1112
+ if (dialect.name === "mysql") {
1113
+ return `json_contains_path(${renderExpression(base, state, dialect)}, ${dialect.renderLiteral("one", state)}, ${renderJsonOpaquePath(path, state, dialect)})`
1114
+ }
1115
+ return undefined
1116
+ }
1117
+ case "jsonPathMatch": {
1118
+ if (!isExpression(base)) {
1119
+ return undefined
1120
+ }
1121
+ const path = ast.path ?? ast.query ?? ast.right
1122
+ if (path === undefined) {
1123
+ return undefined
1124
+ }
1125
+ if (dialect.name === "postgres") {
1126
+ return `(${renderPostgresJsonValue(base, state, dialect)} @@ ${renderJsonOpaquePath(path, state, dialect)})`
1127
+ }
1128
+ unsupportedJsonFeature(dialect, "jsonPathMatch")
1129
+ }
1130
+ }
1131
+
1132
+ return undefined
1133
+ }
1134
+
1135
+ export interface RenderedQueryAst {
1136
+ readonly sql: string
1137
+ readonly projections: readonly Projection[]
1138
+ }
1139
+
1140
+ const selectionProjections = (selection: Record<string, unknown>): readonly Projection[] =>
1141
+ flattenSelection(selection).map(({ path, alias }) => ({
1142
+ path,
1143
+ alias
1144
+ }))
1145
+
1146
+ const renderMutationAssignment = (
1147
+ entry: QueryAst.AssignmentClause,
1148
+ state: RenderState,
1149
+ dialect: SqlDialect,
1150
+ targetTableName?: string
1151
+ ): string => {
1152
+ const column = entry.tableName && dialect.name === "mysql"
1153
+ ? `${dialect.quoteIdentifier(casedTableReferenceName(entry.tableName, state))}.${quoteColumn(entry.columnName, state, dialect, entry.tableName)}`
1154
+ : quoteColumn(entry.columnName, state, dialect, targetTableName)
1155
+ return `${column} = ${renderExpression(entry.value, state, dialect)}`
1156
+ }
1157
+
1158
+ const renderJoinSourcesForMutation = (
1159
+ joins: readonly QueryAst.JoinClause[],
1160
+ state: RenderState,
1161
+ dialect: SqlDialect
1162
+ ): string => joins.map((join) =>
1163
+ renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
1164
+ ).join(", ")
1165
+
1166
+ const renderFromSources = (
1167
+ sources: readonly QueryAst.FromClause[],
1168
+ state: RenderState,
1169
+ dialect: SqlDialect
1170
+ ): string => sources.map((source) =>
1171
+ renderSourceReference(source.source, source.tableName, source.baseTableName, state, dialect)
1172
+ ).join(", ")
1173
+
1174
+ const renderJoinPredicatesForMutation = (
1175
+ joins: readonly QueryAst.JoinClause[],
1176
+ state: RenderState,
1177
+ dialect: SqlDialect
1178
+ ): readonly string[] =>
1179
+ joins.flatMap((join) =>
1180
+ join.kind === "cross" || !join.on
1181
+ ? []
1182
+ : [renderExpression(join.on, state, dialect)]
1183
+ )
1184
+
1185
+ const renderDeleteTargets = (
1186
+ targets: readonly QueryAst.FromClause[],
1187
+ state: RenderState,
1188
+ dialect: SqlDialect
1189
+ ): string => targets.map((target) => dialect.quoteIdentifier(casedTableReferenceName(target.tableName, state))).join(", ")
1190
+
1191
+ const renderMysqlMutationLock = (
1192
+ lock: QueryAst.LockClause | undefined,
1193
+ statement: "update" | "delete"
1194
+ ): string => {
1195
+ if (!lock) {
1196
+ return ""
1197
+ }
1198
+ return renderMysqlMutationLockMode(lock.mode, statement)
1199
+ }
1200
+
1201
+ const renderTransactionClause = (
1202
+ clause: QueryAst.TransactionClause,
1203
+ dialect: SqlDialect
1204
+ ): string => {
1205
+ switch (clause.kind) {
1206
+ case "transaction": {
1207
+ const modes: string[] = []
1208
+ const isolationLevel = renderTransactionIsolationLevel(clause.isolationLevel)
1209
+ if (isolationLevel) {
1210
+ modes.push(isolationLevel)
1211
+ }
1212
+ if (normalizeStatementFlag(clause.readOnly)) {
1213
+ modes.push("read only")
1214
+ }
1215
+ return modes.length > 0
1216
+ ? `start transaction ${modes.join(", ")}`
1217
+ : "start transaction"
1218
+ }
1219
+ case "commit":
1220
+ return "commit"
1221
+ case "rollback":
1222
+ return "rollback"
1223
+ case "savepoint":
1224
+ return `savepoint ${dialect.quoteIdentifier(normalizeStatementIdentifier("savepoint", "name", clause.name))}`
1225
+ case "rollbackTo":
1226
+ return `rollback to savepoint ${dialect.quoteIdentifier(normalizeStatementIdentifier("rollbackTo", "name", clause.name))}`
1227
+ case "releaseSavepoint":
1228
+ return `release savepoint ${dialect.quoteIdentifier(normalizeStatementIdentifier("releaseSavepoint", "name", clause.name))}`
1229
+ }
1230
+ return "start transaction"
1231
+ }
1232
+
1233
+ const renderSelectionList = (
1234
+ selection: Record<string, unknown>,
1235
+ state: RenderState,
1236
+ dialect: SqlDialect
1237
+ ): RenderedQueryAst => {
1238
+ const flattened = flattenSelection(selection)
1239
+ const projections = selectionProjections(selection)
1240
+ const sql = flattened.map(({ expression, alias }) =>
1241
+ `${renderSelectSql(renderExpression(expression, state, dialect), expressionDriverContext(expression, state, dialect))} as ${dialect.quoteIdentifier(alias)}`).join(", ")
1242
+ return {
1243
+ sql,
1244
+ projections
1245
+ }
1246
+ }
1247
+
1248
+ const nestedRenderState = (state: RenderState): RenderState => ({
1249
+ params: state.params,
1250
+ valueMappings: state.valueMappings,
1251
+ casing: state.casing,
1252
+ ctes: [],
1253
+ cteNames: new Set(state.cteNames),
1254
+ cteSources: new Map(state.cteSources),
1255
+ sourceNames: new Map(state.sourceNames)
1256
+ })
1257
+
1258
+ export const renderQueryAst = (
1259
+ ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1260
+ state: RenderState,
1261
+ dialect: SqlDialect,
1262
+ options: { readonly emitCtes?: boolean } = {}
1263
+ ): RenderedQueryAst => {
1264
+ registerQuerySources(ast, state)
1265
+ let sql = ""
1266
+ let projections: readonly Projection[] = []
1267
+
1268
+ switch (ast.kind) {
1269
+ case "select": {
1270
+ const rendered = renderSelectionList(ast.select as Record<string, unknown>, state, dialect)
1271
+ projections = rendered.projections
1272
+ const selectList = rendered.sql.length > 0 ? ` ${rendered.sql}` : ""
1273
+ const clauses = [
1274
+ ast.distinctOn && ast.distinctOn.length > 0
1275
+ ? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")})${selectList}`
1276
+ : `select${ast.distinct ? " distinct" : ""}${selectList}`
1277
+ ]
1278
+ if (ast.from) {
1279
+ clauses.push(`from ${renderSourceReference(ast.from.source, ast.from.tableName, ast.from.baseTableName, state, dialect)}`)
1280
+ }
1281
+ for (const join of ast.joins) {
1282
+ if (dialect.name === "mysql" && join.kind === "full") {
1283
+ throw new Error("Unsupported mysql full join")
1284
+ }
1285
+ const source = renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
1286
+ clauses.push(
1287
+ join.kind === "cross"
1288
+ ? `cross join ${source}`
1289
+ : `${join.kind} join ${source} on ${renderExpression(join.on!, state, dialect)}`
1290
+ )
1291
+ }
1292
+ if (ast.where.length > 0) {
1293
+ clauses.push(`where ${ast.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect)).join(" and ")}`)
1294
+ }
1295
+ if (ast.groupBy.length > 0) {
1296
+ clauses.push(`group by ${ast.groupBy.map((value: QueryAst.Ast["groupBy"][number]) => renderExpression(value, state, dialect)).join(", ")}`)
1297
+ }
1298
+ if (ast.having.length > 0) {
1299
+ clauses.push(`having ${ast.having.map((entry: QueryAst.HavingClause) => renderExpression(entry.predicate, state, dialect)).join(" and ")}`)
1300
+ }
1301
+ if (ast.orderBy.length > 0) {
1302
+ clauses.push(`order by ${ast.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`)
1303
+ }
1304
+ if (ast.limit) {
1305
+ clauses.push(`limit ${renderExpression(ast.limit, state, dialect)}`)
1306
+ }
1307
+ if (ast.offset) {
1308
+ clauses.push(`offset ${renderExpression(ast.offset, state, dialect)}`)
1309
+ }
1310
+ if (ast.lock) {
1311
+ clauses.push(
1312
+ `${renderSelectLockMode(ast.lock.mode)}${ast.lock.nowait ? " nowait" : ""}${ast.lock.skipLocked ? " skip locked" : ""}`
1313
+ )
1314
+ }
1315
+ sql = clauses.join(" ")
1316
+ break
1317
+ }
1318
+ case "set": {
1319
+ const setAst = ast as QueryAst.Ast<Record<string, unknown>, any, "set">
1320
+ const base = renderQueryAst(
1321
+ Query.getAst(setAst.setBase as Query.Plan.Any) as QueryAst.Ast<
1322
+ Record<string, unknown>,
1323
+ any,
1324
+ QueryAst.QueryStatement
1325
+ >,
1326
+ state,
1327
+ dialect
1328
+ )
1329
+ projections = selectionProjections(setAst.select as Record<string, unknown>)
1330
+ sql = [
1331
+ `(${base.sql})`,
1332
+ ...(setAst.setOperations ?? []).map((entry) => {
1333
+ const rendered = renderQueryAst(
1334
+ Query.getAst(entry.query as Query.Plan.Any) as QueryAst.Ast<
1335
+ Record<string, unknown>,
1336
+ any,
1337
+ QueryAst.QueryStatement
1338
+ >,
1339
+ state,
1340
+ dialect
1341
+ )
1342
+ return `${entry.kind}${entry.all ? " all" : ""} (${rendered.sql})`
1343
+ })
1344
+ ].join(" ")
1345
+ break
1346
+ }
1347
+ case "insert": {
1348
+ const insertAst = ast as QueryAst.Ast<Record<string, unknown>, any, "insert">
1349
+ const targetSource = insertAst.into!
1350
+ const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1351
+ const targetCasingState = stateWithTableCasing(state, targetSource.source)
1352
+ const insertSource = insertAst.insertSource
1353
+ const conflict = expectConflictClause(insertAst.conflict)
1354
+ sql = `insert into ${target}`
1355
+ if (insertSource?.kind === "values") {
1356
+ const columns = insertSource.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")
1357
+ const rows = insertSource.rows.map((row) =>
1358
+ `(${row.values.map((entry) => renderExpression(entry.value, targetCasingState, dialect)).join(", ")})`
1359
+ ).join(", ")
1360
+ sql += ` (${columns}) values ${rows}`
1361
+ } else if (insertSource?.kind === "query") {
1362
+ const columns = insertSource.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")
1363
+ const renderedQuery = renderQueryAst(
1364
+ Query.getAst(insertSource.query as Query.Plan.Any) as QueryAst.Ast<
1365
+ Record<string, unknown>,
1366
+ any,
1367
+ QueryAst.QueryStatement
1368
+ >,
1369
+ state,
1370
+ dialect
1371
+ )
1372
+ sql += ` (${columns}) ${renderedQuery.sql}`
1373
+ } else if (insertSource?.kind === "unnest") {
1374
+ const columns = insertSource.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")
1375
+ if (dialect.name === "postgres") {
1376
+ const table = targetSource.source as Table.AnyTable
1377
+ const fields = table[Table.TypeId].fields
1378
+ const rendered = insertSource.values.map((entry) =>
1379
+ `cast(${dialect.renderLiteral(encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect), state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
1380
+ ).join(", ")
1381
+ sql += ` (${columns}) select * from unnest(${rendered})`
1382
+ } else {
1383
+ const table = targetSource.source as Table.AnyTable
1384
+ const fields = table[Table.TypeId].fields
1385
+ const encodedValues = insertSource.values.map((entry) => ({
1386
+ columnName: entry.columnName,
1387
+ values: encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect)
1388
+ }))
1389
+ const rowCount = encodedValues[0]?.values.length ?? 0
1390
+ const rows = Array.from({ length: rowCount }, (_, index) =>
1391
+ `(${encodedValues.map((entry) => dialect.renderLiteral(entry.values[index], state)).join(", ")})`
1392
+ ).join(", ")
1393
+ sql += ` (${columns}) values ${rows}`
1394
+ }
1395
+ } else {
1396
+ const insertValues = insertAst.values ?? []
1397
+ const columns = insertValues.map((entry) => quoteColumn(entry.columnName, state, dialect, targetSource.tableName)).join(", ")
1398
+ const values = insertValues.map((entry) => renderExpression(entry.value, targetCasingState, dialect)).join(", ")
1399
+ if (insertValues.length > 0) {
1400
+ sql += ` (${columns}) values (${values})`
1401
+ } else {
1402
+ sql += " () values ()"
1403
+ }
1404
+ }
1405
+ if (conflict) {
1406
+ const conflictValueState = { ...targetCasingState, allowExcluded: true }
1407
+ const updateValues = (conflict.values ?? []).map((entry) =>
1408
+ `${quoteColumn(entry.columnName, state, dialect, targetSource.tableName)} = ${renderExpression(entry.value, conflictValueState, dialect)}`
1409
+ ).join(", ")
1410
+ if (dialect.name === "postgres") {
1411
+ const targetSql = conflict.target?.kind === "constraint"
1412
+ ? ` on conflict on constraint ${dialect.quoteIdentifier(Casing.applyCategory(targetCasingState.casing, "constraints", conflict.target.name))}`
1413
+ : conflict.target?.kind === "columns"
1414
+ ? ` on conflict (${conflict.target.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")})${conflict.target.where ? ` where ${renderExpression(conflict.target.where, targetCasingState, dialect)}` : ""}`
1415
+ : " on conflict"
1416
+ sql += targetSql
1417
+ sql += conflict.action === "doNothing"
1418
+ ? " do nothing"
1419
+ : ` do update set ${updateValues}${conflict.where ? ` where ${renderExpression(conflict.where, conflictValueState, dialect)}` : ""}`
1420
+ } else if (conflict.action === "doNothing") {
1421
+ sql = sql.replace(/^insert/, "insert ignore")
1422
+ } else {
1423
+ sql += ` on duplicate key update ${updateValues}`
1424
+ }
1425
+ }
1426
+ const hasReturning = Object.keys(insertAst.select as Record<string, unknown>).length > 0
1427
+ const returning = hasReturning
1428
+ ? renderSelectionList(insertAst.select as Record<string, unknown>, state, dialect)
1429
+ : { sql: "", projections: [] }
1430
+ if (dialect.name === "mysql" && returning.sql.length > 0) {
1431
+ throw new Error("Unsupported mysql returning")
1432
+ }
1433
+ projections = returning.projections
1434
+ if (returning.sql.length > 0) {
1435
+ sql += ` returning ${returning.sql}`
1436
+ }
1437
+ break
1438
+ }
1439
+ case "update": {
1440
+ const updateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "update">
1441
+ const targetSource = updateAst.target!
1442
+ const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1443
+ const targets = updateAst.targets ?? [targetSource]
1444
+ const fromSources = updateAst.fromSources ?? []
1445
+ const assignments = updateAst.set!.map((entry) =>
1446
+ renderMutationAssignment(entry, state, dialect, targetSource.tableName)).join(", ")
1447
+ if (dialect.name === "mysql") {
1448
+ const modifiers = renderMysqlMutationLock(updateAst.lock, "update")
1449
+ const extraSources = renderFromSources(fromSources, state, dialect)
1450
+ const joinSources = updateAst.joins.map((join) =>
1451
+ join.kind === "full"
1452
+ ? (() => {
1453
+ throw new Error("Unsupported mysql full join")
1454
+ })()
1455
+ : join.kind === "cross"
1456
+ ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
1457
+ : `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
1458
+ ).join(" ")
1459
+ const targetList = [
1460
+ ...targets.map((entry) =>
1461
+ renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
1462
+ ),
1463
+ ...(extraSources.length > 0 ? [extraSources] : [])
1464
+ ].join(", ")
1465
+ sql = `update${modifiers} ${targetList}${joinSources.length > 0 ? ` ${joinSources}` : ""} set ${assignments}`
1466
+ } else {
1467
+ sql = `update ${target} set ${assignments}`
1468
+ const mutationSources = [
1469
+ ...(fromSources.length > 0 ? [renderFromSources(fromSources, state, dialect)] : []),
1470
+ ...(updateAst.joins.length > 0 ? [renderJoinSourcesForMutation(updateAst.joins, state, dialect)] : [])
1471
+ ].filter((part) => part.length > 0)
1472
+ if (mutationSources.length > 0) {
1473
+ sql += ` from ${mutationSources.join(", ")}`
1474
+ }
1475
+ }
1476
+ const whereParts = [
1477
+ ...(dialect.name === "postgres" ? renderJoinPredicatesForMutation(updateAst.joins, state, dialect) : []),
1478
+ ...updateAst.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect))
1479
+ ]
1480
+ if (whereParts.length > 0) {
1481
+ sql += ` where ${whereParts.join(" and ")}`
1482
+ }
1483
+ if (dialect.name === "mysql" && updateAst.orderBy.length > 0) {
1484
+ sql += ` order by ${updateAst.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`
1485
+ }
1486
+ if (dialect.name === "mysql" && updateAst.limit) {
1487
+ sql += ` limit ${renderMysqlMutationLimit(updateAst.limit, state, dialect)}`
1488
+ }
1489
+ const hasReturning = Object.keys(updateAst.select as Record<string, unknown>).length > 0
1490
+ const returning = hasReturning
1491
+ ? renderSelectionList(updateAst.select as Record<string, unknown>, state, dialect)
1492
+ : { sql: "", projections: [] }
1493
+ if (dialect.name === "mysql" && returning.sql.length > 0) {
1494
+ throw new Error("Unsupported mysql returning")
1495
+ }
1496
+ projections = returning.projections
1497
+ if (returning.sql.length > 0) {
1498
+ sql += ` returning ${returning.sql}`
1499
+ }
1500
+ break
1501
+ }
1502
+ case "delete": {
1503
+ const deleteAst = ast as QueryAst.Ast<Record<string, unknown>, any, "delete">
1504
+ const targetSource = deleteAst.target!
1505
+ const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1506
+ const targets = deleteAst.targets ?? [targetSource]
1507
+ if (dialect.name === "mysql") {
1508
+ const modifiers = renderMysqlMutationLock(deleteAst.lock, "delete")
1509
+ const hasJoinedSources = deleteAst.joins.length > 0 || targets.length > 1
1510
+ const targetList = renderDeleteTargets(targets, state, dialect)
1511
+ const fromSources = targets.map((entry) =>
1512
+ renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
1513
+ ).join(", ")
1514
+ const joinSources = deleteAst.joins.map((join) =>
1515
+ join.kind === "full"
1516
+ ? (() => {
1517
+ throw new Error("Unsupported mysql full join")
1518
+ })()
1519
+ : join.kind === "cross"
1520
+ ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
1521
+ : `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
1522
+ ).join(" ")
1523
+ sql = hasJoinedSources
1524
+ ? `delete${modifiers} ${targetList} from ${fromSources}${joinSources.length > 0 ? ` ${joinSources}` : ""}`
1525
+ : `delete${modifiers} from ${fromSources}`
1526
+ } else {
1527
+ sql = `delete from ${target}`
1528
+ if (deleteAst.joins.length > 0) {
1529
+ sql += ` using ${renderJoinSourcesForMutation(deleteAst.joins, state, dialect)}`
1530
+ }
1531
+ }
1532
+ const whereParts = [
1533
+ ...(dialect.name === "postgres" ? renderJoinPredicatesForMutation(deleteAst.joins, state, dialect) : []),
1534
+ ...deleteAst.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect))
1535
+ ]
1536
+ if (whereParts.length > 0) {
1537
+ sql += ` where ${whereParts.join(" and ")}`
1538
+ }
1539
+ if (dialect.name === "mysql" && deleteAst.orderBy.length > 0) {
1540
+ sql += ` order by ${deleteAst.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`
1541
+ }
1542
+ if (dialect.name === "mysql" && deleteAst.limit) {
1543
+ sql += ` limit ${renderMysqlMutationLimit(deleteAst.limit, state, dialect)}`
1544
+ }
1545
+ const hasReturning = Object.keys(deleteAst.select as Record<string, unknown>).length > 0
1546
+ const returning = hasReturning
1547
+ ? renderSelectionList(deleteAst.select as Record<string, unknown>, state, dialect)
1548
+ : { sql: "", projections: [] }
1549
+ if (dialect.name === "mysql" && returning.sql.length > 0) {
1550
+ throw new Error("Unsupported mysql returning")
1551
+ }
1552
+ projections = returning.projections
1553
+ if (returning.sql.length > 0) {
1554
+ sql += ` returning ${returning.sql}`
1555
+ }
1556
+ break
1557
+ }
1558
+ case "truncate": {
1559
+ const truncateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "truncate">
1560
+ const truncate = expectTruncateClause(truncateAst.truncate)
1561
+ const targetSource = truncateAst.target!
1562
+ const restartIdentity = truncate.restartIdentity
1563
+ const cascade = truncate.cascade
1564
+ if (dialect.name === "mysql" && (restartIdentity || cascade)) {
1565
+ throw new Error("Unsupported mysql truncate options")
1566
+ }
1567
+ sql = `truncate table ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
1568
+ if (restartIdentity) {
1569
+ sql += " restart identity"
1570
+ }
1571
+ if (cascade) {
1572
+ sql += " cascade"
1573
+ }
1574
+ break
1575
+ }
1576
+ case "merge": {
1577
+ if (dialect.name !== "postgres") {
1578
+ throw new Error(`Unsupported merge statement for ${dialect.name}`)
1579
+ }
1580
+ const mergeAst = ast as QueryAst.Ast<Record<string, unknown>, any, "merge">
1581
+ const targetSource = mergeAst.target!
1582
+ const usingSource = mergeAst.using!
1583
+ const merge = mergeAst.merge!
1584
+ 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)}`
1585
+ if (merge.whenMatched) {
1586
+ sql += " when matched"
1587
+ if (merge.whenMatched.predicate) {
1588
+ sql += ` and ${renderExpression(merge.whenMatched.predicate, state, dialect)}`
1589
+ }
1590
+ if (merge.whenMatched.kind === "delete") {
1591
+ sql += " then delete"
1592
+ } else {
1593
+ sql += ` then update set ${merge.whenMatched.values.map((entry) =>
1594
+ `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
1595
+ ).join(", ")}`
1596
+ }
1597
+ }
1598
+ if (merge.whenNotMatched) {
1599
+ sql += " when not matched"
1600
+ if (merge.whenNotMatched.predicate) {
1601
+ sql += ` and ${renderExpression(merge.whenNotMatched.predicate, state, dialect)}`
1602
+ }
1603
+ 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(", ")})`
1604
+ }
1605
+ break
1606
+ }
1607
+ case "transaction":
1608
+ case "commit":
1609
+ case "rollback":
1610
+ case "savepoint":
1611
+ case "rollbackTo":
1612
+ case "releaseSavepoint": {
1613
+ sql = renderTransactionClause(ast.transaction!, dialect)
1614
+ break
1615
+ }
1616
+ case "createTable": {
1617
+ const createTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createTable">
1618
+ const ddl = expectDdlClauseKind(createTableAst.ddl, "createTable")
1619
+ sql = renderCreateTableSql(createTableAst.target!, state, dialect, ddl.ifNotExists)
1620
+ break
1621
+ }
1622
+ case "dropTable": {
1623
+ const dropTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropTable">
1624
+ const ddl = expectDdlClauseKind(dropTableAst.ddl, "dropTable")
1625
+ const ifExists = normalizeStatementFlag(ddl.ifExists)
1626
+ sql = `drop table${ifExists ? " if exists" : ""} ${renderSourceReference(dropTableAst.target!.source, dropTableAst.target!.tableName, dropTableAst.target!.baseTableName, state, dialect)}`
1627
+ break
1628
+ }
1629
+ case "createIndex": {
1630
+ const createIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createIndex">
1631
+ sql = renderCreateIndexSql(
1632
+ createIndexAst.target!,
1633
+ expectDdlClauseKind(createIndexAst.ddl, "createIndex"),
1634
+ state,
1635
+ dialect
1636
+ )
1637
+ break
1638
+ }
1639
+ case "dropIndex": {
1640
+ const dropIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropIndex">
1641
+ sql = renderDropIndexSql(
1642
+ dropIndexAst.target!,
1643
+ expectDdlClauseKind(dropIndexAst.ddl, "dropIndex"),
1644
+ state,
1645
+ dialect
1646
+ )
1647
+ break
1648
+ }
1649
+ default: {
1650
+ if (ast.transaction !== undefined) {
1651
+ sql = renderTransactionClause(ast.transaction, dialect)
1652
+ }
1653
+ break
1654
+ }
1655
+ }
1656
+
1657
+ if (state.ctes.length === 0 || options.emitCtes === false) {
1658
+ return {
1659
+ sql,
1660
+ projections
1661
+ }
1662
+ }
1663
+ return {
1664
+ sql: `with${state.ctes.some((entry) => entry.recursive) ? " recursive" : ""} ${state.ctes.map((entry) => `${dialect.quoteIdentifier(entry.name)} as (${entry.sql})`).join(", ")} ${sql}`,
1665
+ projections
1666
+ }
1667
+ }
1668
+
1669
+ const renderSourceReference = (
1670
+ source: unknown,
1671
+ tableName: string,
1672
+ baseTableName: string,
1673
+ state: RenderState,
1674
+ dialect: SqlDialect
1675
+ ): string => {
1676
+ const renderSelectRows = (
1677
+ rows: readonly Record<string, Expression.Any>[],
1678
+ columnNames: readonly string[]
1679
+ ): string => {
1680
+ const renderedRows = rows.map((row) =>
1681
+ `select ${columnNames.map((columnName) =>
1682
+ `${renderExpression(row[columnName]!, state, dialect)} as ${dialect.quoteIdentifier(columnName)}`
1683
+ ).join(", ")}`
1684
+ )
1685
+ return `(${renderedRows.join(" union all ")}) as ${dialect.quoteIdentifier(tableName)}(${columnNames.map((columnName) => dialect.quoteIdentifier(columnName)).join(", ")})`
1686
+ }
1687
+
1688
+ const renderUnnestRows = (
1689
+ arrays: Readonly<Record<string, readonly Expression.Any[]>>,
1690
+ columnNames: readonly string[]
1691
+ ): string => {
1692
+ const rowCount = arrays[columnNames[0]!]!.length
1693
+ const rows = Array.from({ length: rowCount }, (_, index) =>
1694
+ Object.fromEntries(columnNames.map((columnName) => [columnName, arrays[columnName]![index]!] as const)) as Record<string, Expression.Any>
1695
+ )
1696
+ return renderSelectRows(rows, columnNames)
1697
+ }
1698
+
1699
+ if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "cte") {
1700
+ const cte = source as unknown as {
1701
+ readonly name: string
1702
+ readonly plan: Query.Plan.Any
1703
+ readonly recursive?: boolean
1704
+ }
1705
+ const registeredCteSource = state.cteSources.get(cte.name)
1706
+ if (registeredCteSource !== undefined && registeredCteSource !== cte.plan) {
1707
+ throw new Error(`common table expression name is already registered with a different plan: ${cte.name}`)
1708
+ }
1709
+ if (!state.cteNames.has(cte.name)) {
1710
+ state.cteNames.add(cte.name)
1711
+ state.cteSources.set(cte.name, cte.plan)
1712
+ const statement = Query.getQueryState(cte.plan).statement
1713
+ if (statement !== "select" && statement !== "set") {
1714
+ const cteAst = Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>
1715
+ if (Object.keys((cteAst.select ?? {}) as Record<string, unknown>).length > 0) {
1716
+ throw new Error("Unsupported mysql returning")
1717
+ }
1718
+ throw new Error("Unsupported mysql data-modifying cte")
1719
+ }
1720
+ const rendered = renderQueryAst(
1721
+ Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1722
+ state,
1723
+ dialect,
1724
+ { emitCtes: false }
1725
+ )
1726
+ state.ctes.push({
1727
+ name: cte.name,
1728
+ sql: rendered.sql,
1729
+ recursive: cte.recursive
1730
+ })
1731
+ }
1732
+ return dialect.quoteIdentifier(cte.name)
1733
+ }
1734
+ if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "derived") {
1735
+ const derived = source as unknown as {
1736
+ readonly name: string
1737
+ readonly plan: Query.Plan.Any
1738
+ }
1739
+ if (!state.cteNames.has(derived.name)) {
1740
+ // derived tables are inlined, so no CTE registration is needed
1741
+ }
1742
+ return `(${renderQueryAst(Query.getAst(derived.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, nestedRenderState(state), dialect).sql}) as ${dialect.quoteIdentifier(derived.name)}`
1743
+ }
1744
+ if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "lateral") {
1745
+ const lateral = source as unknown as {
1746
+ readonly name: string
1747
+ readonly plan: Query.Plan.Any
1748
+ }
1749
+ return `lateral (${renderQueryAst(Query.getAst(lateral.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, nestedRenderState(state), dialect).sql}) as ${dialect.quoteIdentifier(lateral.name)}`
1750
+ }
1751
+ if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "values") {
1752
+ const values = source as unknown as {
1753
+ readonly columns: Record<string, Expression.Any>
1754
+ readonly rows: readonly Record<string, Expression.Any>[]
1755
+ }
1756
+ return renderSelectRows(values.rows, Object.keys(values.columns))
1757
+ }
1758
+ if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "unnest") {
1759
+ const unnest = source as unknown as {
1760
+ readonly columns: Record<string, Expression.Any>
1761
+ readonly arrays: Readonly<Record<string, readonly Expression.Any[]>>
1762
+ }
1763
+ return renderUnnestRows(unnest.arrays, Object.keys(unnest.columns))
1764
+ }
1765
+ if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "tableFunction") {
1766
+ const tableFunction = source as unknown as {
1767
+ readonly name: string
1768
+ readonly columns: Record<string, Expression.Any>
1769
+ readonly functionName: string
1770
+ readonly args: readonly Expression.Any[]
1771
+ }
1772
+ if (dialect.name !== "postgres") {
1773
+ throw new Error("Unsupported table function source for SQL rendering")
1774
+ }
1775
+ const functionName = renderFunctionName(tableFunction.functionName)
1776
+ const columnNames = Object.keys(tableFunction.columns)
1777
+ return `${functionName}(${tableFunction.args.map((arg) => renderExpression(arg, state, dialect)).join(", ")}) as ${dialect.quoteIdentifier(tableFunction.name)}(${columnNames.map((columnName) => dialect.quoteIdentifier(columnName)).join(", ")})`
1778
+ }
1779
+ const schemaName = typeof source === "object" && source !== null && Table.TypeId in source
1780
+ ? (source as Table.AnyTable)[Table.TypeId].schemaName
1781
+ : undefined
1782
+ if (typeof source === "object" && source !== null && Table.TypeId in source) {
1783
+ const table = source as Table.AnyTable
1784
+ const tableState = table[Table.TypeId]
1785
+ const casing = casingForTable(table, state)
1786
+ const renderedTableName = tableState.kind === "alias"
1787
+ ? tableName
1788
+ : Casing.applyCategory(casing, "tables", baseTableName)
1789
+ const renderedBaseName = Casing.applyCategory(casing, "tables", baseTableName)
1790
+ const renderedSchemaName = schemaName === undefined
1791
+ ? undefined
1792
+ : Casing.applyCategory(casing, "schemas", schemaName)
1793
+ return dialect.renderTableReference(renderedTableName, renderedBaseName, renderedSchemaName)
1794
+ }
1795
+ return dialect.renderTableReference(
1796
+ Casing.applyCategory(state.casing, "tables", tableName),
1797
+ Casing.applyCategory(state.casing, "tables", baseTableName),
1798
+ schemaName === undefined ? undefined : Casing.applyCategory(state.casing, "schemas", schemaName)
1799
+ )
1800
+ }
1801
+
1802
+ const renderSubqueryExpressionPlan = (
1803
+ plan: Query.Plan.Any,
1804
+ state: RenderState,
1805
+ dialect: SqlDialect
1806
+ ): string => {
1807
+ const statement = Query.getQueryState(plan).statement
1808
+ if (statement !== "select" && statement !== "set") {
1809
+ throw new Error("subquery expressions only accept select-like query plans")
1810
+ }
1811
+ return renderQueryAst(
1812
+ Query.getAst(plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1813
+ state,
1814
+ dialect
1815
+ ).sql
1816
+ }
1817
+
1818
+ /**
1819
+ * Renders a scalar expression AST into SQL text.
1820
+ *
1821
+ * This is parameterized by a runtime dialect so the same expression walker can
1822
+ * be reused across dialect-specific renderers while still delegating quoting
1823
+ * and literal serialization to the concrete dialect implementation.
1824
+ */
1825
+ export const renderExpression = (
1826
+ expression: Expression.Any,
1827
+ state: RenderState,
1828
+ dialect: SqlDialect
1829
+ ): string => {
1830
+ const rawAst = (expression as Expression.Any & {
1831
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
1832
+ })[ExpressionAst.TypeId] as ExpressionAst.Any | Record<string, unknown>
1833
+ const jsonSql = renderJsonExpression(expression, rawAst as Record<string, unknown>, state, dialect)
1834
+ if (jsonSql !== undefined) {
1835
+ return jsonSql
1836
+ }
1837
+ const ast = rawAst as ExpressionAst.Any
1838
+ const renderComparisonOperator = (operator: unknown): "=" | "<>" | "<" | "<=" | ">" | ">=" =>
1839
+ ({
1840
+ eq: "=",
1841
+ neq: "<>",
1842
+ lt: "<",
1843
+ lte: "<=",
1844
+ gt: ">",
1845
+ gte: ">="
1846
+ } as const)[operator as "eq" | "neq" | "lt" | "lte" | "gt" | "gte"]!
1847
+ switch (ast.kind) {
1848
+ case "column":
1849
+ return state.rowLocalColumns || ast.tableName.length === 0
1850
+ ? quoteColumn(ast.columnName, state, dialect, ast.tableName)
1851
+ : `${dialect.quoteIdentifier(casedTableReferenceName(ast.tableName, state))}.${quoteColumn(ast.columnName, state, dialect, ast.tableName)}`
1852
+ case "literal":
1853
+ if (typeof ast.value === "number" && !Number.isFinite(ast.value)) {
1854
+ throw new Error("Expected a finite numeric value")
1855
+ }
1856
+ return dialect.renderLiteral(ast.value, state, expression[Expression.TypeId])
1857
+ case "excluded":
1858
+ if (state.allowExcluded !== true) {
1859
+ throw new Error("excluded(...) is only supported inside insert conflict handlers")
1860
+ }
1861
+ return dialect.name === "mysql"
1862
+ ? `values(${quoteColumn(ast.columnName, state, dialect)})`
1863
+ : `excluded.${quoteColumn(ast.columnName, state, dialect)}`
1864
+ case "cast":
1865
+ return `cast(${renderExpression(expectValueExpression("cast", ast.value), state, dialect)} as ${renderCastType(dialect, ast.target)})`
1866
+ case "function":
1867
+ return renderFunctionCall(ast.name, ast.args, state, dialect)
1868
+ case "eq":
1869
+ return renderBinaryExpression("eq", "=", ast.left, ast.right, state, dialect)
1870
+ case "neq":
1871
+ return renderBinaryExpression("neq", "<>", ast.left, ast.right, state, dialect)
1872
+ case "lt":
1873
+ return renderBinaryExpression("lt", "<", ast.left, ast.right, state, dialect)
1874
+ case "lte":
1875
+ return renderBinaryExpression("lte", "<=", ast.left, ast.right, state, dialect)
1876
+ case "gt":
1877
+ return renderBinaryExpression("gt", ">", ast.left, ast.right, state, dialect)
1878
+ case "gte":
1879
+ return renderBinaryExpression("gte", ">=", ast.left, ast.right, state, dialect)
1880
+ case "like":
1881
+ return renderBinaryExpression("like", "like", ast.left, ast.right, state, dialect)
1882
+ case "ilike": {
1883
+ const [left, right] = expectBinaryExpressions("ilike", ast.left, ast.right)
1884
+ return dialect.name === "postgres"
1885
+ ? `(${renderExpression(left, state, dialect)} ilike ${renderExpression(right, state, dialect)})`
1886
+ : `(lower(${renderExpression(left, state, dialect)}) like lower(${renderExpression(right, state, dialect)}))`
1887
+ }
1888
+ case "regexMatch": {
1889
+ const [left, right] = expectBinaryExpressions("regexMatch", ast.left, ast.right)
1890
+ return dialect.name === "postgres"
1891
+ ? `(${renderExpression(left, state, dialect)} ~ ${renderExpression(right, state, dialect)})`
1892
+ : `(${renderExpression(left, state, dialect)} regexp ${renderExpression(right, state, dialect)})`
1893
+ }
1894
+ case "regexIMatch": {
1895
+ const [left, right] = expectBinaryExpressions("regexIMatch", ast.left, ast.right)
1896
+ return dialect.name === "postgres"
1897
+ ? `(${renderExpression(left, state, dialect)} ~* ${renderExpression(right, state, dialect)})`
1898
+ : `(${renderExpression(left, state, dialect)} regexp ${renderExpression(right, state, dialect)})`
1899
+ }
1900
+ case "regexNotMatch": {
1901
+ const [left, right] = expectBinaryExpressions("regexNotMatch", ast.left, ast.right)
1902
+ return dialect.name === "postgres"
1903
+ ? `(${renderExpression(left, state, dialect)} !~ ${renderExpression(right, state, dialect)})`
1904
+ : `(${renderExpression(left, state, dialect)} not regexp ${renderExpression(right, state, dialect)})`
1905
+ }
1906
+ case "regexNotIMatch": {
1907
+ const [left, right] = expectBinaryExpressions("regexNotIMatch", ast.left, ast.right)
1908
+ return dialect.name === "postgres"
1909
+ ? `(${renderExpression(left, state, dialect)} !~* ${renderExpression(right, state, dialect)})`
1910
+ : `(${renderExpression(left, state, dialect)} not regexp ${renderExpression(right, state, dialect)})`
1911
+ }
1912
+ case "isDistinctFrom": {
1913
+ const [left, right] = expectBinaryExpressions("isDistinctFrom", ast.left, ast.right)
1914
+ return dialect.name === "mysql"
1915
+ ? `(not (${renderExpression(left, state, dialect)} <=> ${renderExpression(right, state, dialect)}))`
1916
+ : `(${renderExpression(left, state, dialect)} is distinct from ${renderExpression(right, state, dialect)})`
1917
+ }
1918
+ case "isNotDistinctFrom": {
1919
+ const [left, right] = expectBinaryExpressions("isNotDistinctFrom", ast.left, ast.right)
1920
+ return dialect.name === "mysql"
1921
+ ? `(${renderExpression(left, state, dialect)} <=> ${renderExpression(right, state, dialect)})`
1922
+ : `(${renderExpression(left, state, dialect)} is not distinct from ${renderExpression(right, state, dialect)})`
1923
+ }
1924
+ case "contains": {
1925
+ const [leftExpression, rightExpression] = expectBinaryExpressions("contains", ast.left, ast.right)
1926
+ if (dialect.name === "postgres") {
1927
+ const left = isJsonExpression(leftExpression)
1928
+ ? renderPostgresJsonValue(leftExpression, state, dialect)
1929
+ : renderExpression(leftExpression, state, dialect)
1930
+ const right = isJsonExpression(rightExpression)
1931
+ ? renderPostgresJsonValue(rightExpression, state, dialect)
1932
+ : renderExpression(rightExpression, state, dialect)
1933
+ return `(${left} @> ${right})`
1934
+ }
1935
+ if (dialect.name === "mysql" && isJsonExpression(leftExpression) && isJsonExpression(rightExpression)) {
1936
+ return `json_contains(${renderJsonInputExpression(leftExpression, state, dialect)}, ${renderJsonInputExpression(rightExpression, state, dialect)})`
1937
+ }
1938
+ throw new Error("Unsupported container operator for SQL rendering")
1939
+ }
1940
+ case "containedBy": {
1941
+ const [leftExpression, rightExpression] = expectBinaryExpressions("containedBy", ast.left, ast.right)
1942
+ if (dialect.name === "postgres") {
1943
+ const left = isJsonExpression(leftExpression)
1944
+ ? renderPostgresJsonValue(leftExpression, state, dialect)
1945
+ : renderExpression(leftExpression, state, dialect)
1946
+ const right = isJsonExpression(rightExpression)
1947
+ ? renderPostgresJsonValue(rightExpression, state, dialect)
1948
+ : renderExpression(rightExpression, state, dialect)
1949
+ return `(${left} <@ ${right})`
1950
+ }
1951
+ if (dialect.name === "mysql" && isJsonExpression(leftExpression) && isJsonExpression(rightExpression)) {
1952
+ return `json_contains(${renderJsonInputExpression(rightExpression, state, dialect)}, ${renderJsonInputExpression(leftExpression, state, dialect)})`
1953
+ }
1954
+ throw new Error("Unsupported container operator for SQL rendering")
1955
+ }
1956
+ case "overlaps": {
1957
+ const [leftExpression, rightExpression] = expectBinaryExpressions("overlaps", ast.left, ast.right)
1958
+ if (dialect.name === "postgres") {
1959
+ const left = isJsonExpression(leftExpression)
1960
+ ? renderPostgresJsonValue(leftExpression, state, dialect)
1961
+ : renderExpression(leftExpression, state, dialect)
1962
+ const right = isJsonExpression(rightExpression)
1963
+ ? renderPostgresJsonValue(rightExpression, state, dialect)
1964
+ : renderExpression(rightExpression, state, dialect)
1965
+ return `(${left} && ${right})`
1966
+ }
1967
+ if (dialect.name === "mysql" && isJsonExpression(leftExpression) && isJsonExpression(rightExpression)) {
1968
+ return `json_overlaps(${renderJsonInputExpression(leftExpression, state, dialect)}, ${renderJsonInputExpression(rightExpression, state, dialect)})`
1969
+ }
1970
+ throw new Error("Unsupported container operator for SQL rendering")
1971
+ }
1972
+ case "isNull":
1973
+ return `(${renderExpression(expectValueExpression("isNull", ast.value), state, dialect)} is null)`
1974
+ case "isNotNull":
1975
+ return `(${renderExpression(expectValueExpression("isNotNull", ast.value), state, dialect)} is not null)`
1976
+ case "not":
1977
+ return `(not ${renderExpression(expectValueExpression("not", ast.value), state, dialect)})`
1978
+ case "upper":
1979
+ return `upper(${renderExpression(expectValueExpression("upper", ast.value), state, dialect)})`
1980
+ case "lower":
1981
+ return `lower(${renderExpression(expectValueExpression("lower", ast.value), state, dialect)})`
1982
+ case "count":
1983
+ return `count(${renderExpression(expectValueExpression("count", ast.value), state, dialect)})`
1984
+ case "max":
1985
+ return `max(${renderExpression(expectValueExpression("max", ast.value), state, dialect)})`
1986
+ case "min":
1987
+ return `min(${renderExpression(expectValueExpression("min", ast.value), state, dialect)})`
1988
+ case "and":
1989
+ return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" and ")})`
1990
+ case "or":
1991
+ return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" or ")})`
1992
+ case "coalesce":
1993
+ return `coalesce(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")})`
1994
+ case "in":
1995
+ return `(${renderExpression(ast.values[0]!, state, dialect)} in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1996
+ case "notIn":
1997
+ return `(${renderExpression(ast.values[0]!, state, dialect)} not in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1998
+ case "between":
1999
+ return `(${renderExpression(ast.values[0]!, state, dialect)} between ${renderExpression(ast.values[1]!, state, dialect)} and ${renderExpression(ast.values[2]!, state, dialect)})`
2000
+ case "concat":
2001
+ return dialect.renderConcat(ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)))
2002
+ case "case":
2003
+ return `case ${ast.branches.map((branch) =>
2004
+ `when ${renderExpression(branch.when, state, dialect)} then ${renderExpression(branch.then, state, dialect)}`
2005
+ ).join(" ")} else ${renderExpression(ast.else, state, dialect)} end`
2006
+ case "exists":
2007
+ return `exists (${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
2008
+ case "scalarSubquery":
2009
+ return `(${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
2010
+ case "inSubquery":
2011
+ return `(${renderExpression(expectValueExpression("inSubquery", ast.left), state, dialect)} in (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
2012
+ case "comparisonAny":
2013
+ return `(${renderExpression(expectValueExpression("compareAny", ast.left), state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
2014
+ case "comparisonAll":
2015
+ return `(${renderExpression(expectValueExpression("compareAll", ast.left), state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
2016
+ case "window": {
2017
+ const partitionBy = ast.partitionBy as readonly Expression.Any[]
2018
+ const orderBy = ast.orderBy as readonly {
2019
+ readonly value: Expression.Any
2020
+ readonly direction: string
2021
+ }[]
2022
+ const clauses: string[] = []
2023
+ if (partitionBy.length > 0) {
2024
+ clauses.push(`partition by ${partitionBy.map((value) => renderExpression(value, state, dialect)).join(", ")}`)
2025
+ }
2026
+ if (orderBy.length > 0) {
2027
+ clauses.push(`order by ${orderBy.map((entry) =>
2028
+ `${renderExpression(entry.value, state, dialect)} ${entry.direction}`
2029
+ ).join(", ")}`)
2030
+ }
2031
+ const specification = clauses.join(" ")
2032
+ switch (ast.function) {
2033
+ case "rowNumber":
2034
+ return `row_number() over (${specification})`
2035
+ case "rank":
2036
+ return `rank() over (${specification})`
2037
+ case "denseRank":
2038
+ return `dense_rank() over (${specification})`
2039
+ case "over":
2040
+ return `${renderExpression(ast.value as Expression.Any, state, dialect)} over (${specification})`
2041
+ }
2042
+ break
2043
+ }
2044
+ }
2045
+ throw new Error("Unsupported expression for SQL rendering")
2046
+ }