effect-qb 0.12.3

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