effect-qb 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/mysql.js +1957 -595
  2. package/dist/postgres/metadata.js +2507 -182
  3. package/dist/postgres.js +9587 -8201
  4. package/dist/sqlite.js +8360 -0
  5. package/package.json +7 -2
  6. package/src/internal/column-state.ts +7 -0
  7. package/src/internal/column.ts +22 -0
  8. package/src/internal/derived-table.ts +29 -3
  9. package/src/internal/dialect.ts +14 -1
  10. package/src/internal/dsl-mutation-runtime.ts +173 -4
  11. package/src/internal/dsl-plan-runtime.ts +165 -20
  12. package/src/internal/dsl-query-runtime.ts +60 -6
  13. package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
  14. package/src/internal/executor.ts +62 -13
  15. package/src/internal/expression-ast.ts +3 -2
  16. package/src/internal/grouping-key.ts +141 -1
  17. package/src/internal/implication-runtime.ts +2 -1
  18. package/src/internal/json/types.ts +155 -40
  19. package/src/internal/predicate/analysis.ts +103 -1
  20. package/src/internal/predicate/atom.ts +7 -0
  21. package/src/internal/predicate/context.ts +170 -17
  22. package/src/internal/predicate/key.ts +64 -2
  23. package/src/internal/predicate/normalize.ts +115 -34
  24. package/src/internal/predicate/runtime.ts +144 -13
  25. package/src/internal/query.ts +563 -103
  26. package/src/internal/renderer.ts +39 -2
  27. package/src/internal/runtime/driver-value-mapping.ts +244 -0
  28. package/src/internal/runtime/normalize.ts +62 -38
  29. package/src/internal/runtime/schema.ts +5 -3
  30. package/src/internal/runtime/value.ts +153 -30
  31. package/src/internal/scalar.ts +11 -0
  32. package/src/internal/table-options.ts +108 -1
  33. package/src/internal/table.ts +87 -29
  34. package/src/mysql/column.ts +19 -2
  35. package/src/mysql/datatypes/index.ts +21 -0
  36. package/src/mysql/errors/catalog.ts +5 -5
  37. package/src/mysql/errors/normalize.ts +2 -2
  38. package/src/mysql/executor.ts +20 -5
  39. package/src/mysql/internal/dialect.ts +12 -6
  40. package/src/mysql/internal/dsl.ts +995 -263
  41. package/src/mysql/internal/renderer.ts +13 -3
  42. package/src/mysql/internal/sql-expression-renderer.ts +530 -128
  43. package/src/mysql/query.ts +9 -2
  44. package/src/mysql/renderer.ts +7 -2
  45. package/src/mysql/table.ts +38 -12
  46. package/src/postgres/cast.ts +22 -7
  47. package/src/postgres/column.ts +5 -2
  48. package/src/postgres/errors/normalize.ts +2 -2
  49. package/src/postgres/executor.ts +68 -10
  50. package/src/postgres/function/core.ts +19 -1
  51. package/src/postgres/internal/dialect.ts +12 -6
  52. package/src/postgres/internal/dsl.ts +958 -288
  53. package/src/postgres/internal/renderer.ts +13 -3
  54. package/src/postgres/internal/schema-ddl.ts +2 -1
  55. package/src/postgres/internal/schema-model.ts +6 -3
  56. package/src/postgres/internal/sql-expression-renderer.ts +477 -96
  57. package/src/postgres/json.ts +57 -17
  58. package/src/postgres/query.ts +9 -2
  59. package/src/postgres/renderer.ts +7 -2
  60. package/src/postgres/schema-management.ts +91 -4
  61. package/src/postgres/schema.ts +1 -1
  62. package/src/postgres/table.ts +189 -53
  63. package/src/postgres/type.ts +4 -0
  64. package/src/sqlite/column.ts +128 -0
  65. package/src/sqlite/datatypes/index.ts +79 -0
  66. package/src/sqlite/datatypes/spec.ts +98 -0
  67. package/src/sqlite/errors/catalog.ts +103 -0
  68. package/src/sqlite/errors/fields.ts +19 -0
  69. package/src/sqlite/errors/index.ts +19 -0
  70. package/src/sqlite/errors/normalize.ts +229 -0
  71. package/src/sqlite/errors/requirements.ts +71 -0
  72. package/src/sqlite/errors/types.ts +29 -0
  73. package/src/sqlite/executor.ts +227 -0
  74. package/src/sqlite/function/aggregate.ts +2 -0
  75. package/src/sqlite/function/core.ts +2 -0
  76. package/src/sqlite/function/index.ts +19 -0
  77. package/src/sqlite/function/string.ts +2 -0
  78. package/src/sqlite/function/temporal.ts +100 -0
  79. package/src/sqlite/function/window.ts +2 -0
  80. package/src/sqlite/internal/dialect.ts +37 -0
  81. package/src/sqlite/internal/dsl.ts +6926 -0
  82. package/src/sqlite/internal/renderer.ts +47 -0
  83. package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
  84. package/src/sqlite/json.ts +2 -0
  85. package/src/sqlite/query.ts +196 -0
  86. package/src/sqlite/renderer.ts +24 -0
  87. package/src/sqlite/table.ts +183 -0
  88. package/src/sqlite.ts +22 -0
@@ -1,14 +1,25 @@
1
+ import * as Schema from "effect/Schema"
2
+
1
3
  import * as Query from "../../internal/query.js"
2
4
  import * as Expression from "../../internal/scalar.js"
3
5
  import * as Table from "../../internal/table.js"
4
6
  import * as QueryAst from "../../internal/query-ast.js"
5
- import type { RenderState, SqlDialect } from "../../internal/dialect.js"
7
+ import type { RenderState, RenderValueContext, SqlDialect } from "../../internal/dialect.js"
6
8
  import * as ExpressionAst from "../../internal/expression-ast.js"
7
9
  import * as JsonPath from "../../internal/json/path.js"
10
+ import { renderSelectLockMode } from "../../internal/dsl-plan-runtime.js"
11
+ import { expectConflictClause, expectInsertSourceKind } from "../../internal/dsl-mutation-runtime.js"
12
+ import { expectDdlClauseKind, expectTruncateClause, renderTransactionIsolationLevel } from "../../internal/dsl-transaction-ddl-runtime.js"
13
+ import {
14
+ renderJsonSelectSql,
15
+ renderSelectSql,
16
+ toDriverValue
17
+ } from "../../internal/runtime/driver-value-mapping.js"
18
+ import { normalizeDbValue } from "../../internal/runtime/normalize.js"
8
19
  import { flattenSelection, type Projection } from "../../internal/projections.js"
9
20
  import { type SelectionValue, validateAggregationSelection } from "../../internal/aggregation-validation.js"
10
21
  import * as SchemaExpression from "../../internal/schema-expression.js"
11
- import type { DdlExpressionLike } from "../../internal/table-options.js"
22
+ import { renderReferentialAction, type DdlExpressionLike } from "../../internal/table-options.js"
12
23
 
13
24
  const renderDbType = (
14
25
  dialect: SqlDialect,
@@ -46,14 +57,59 @@ const renderCastType = (
46
57
  }
47
58
  }
48
59
 
60
+ const renderPostgresDdlString = (value: string): string =>
61
+ `'${value.replaceAll("'", "''")}'`
62
+
63
+ const renderPostgresDdlBytes = (value: Uint8Array): string =>
64
+ `decode('${Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("")}', 'hex')`
65
+
66
+ const renderPostgresDdlLiteral = (
67
+ value: unknown,
68
+ state: RenderState,
69
+ context: RenderValueContext = {}
70
+ ): string => {
71
+ const driverValue = toDriverValue(value, {
72
+ dialect: "postgres",
73
+ valueMappings: state.valueMappings,
74
+ ...context
75
+ })
76
+ if (driverValue === null) {
77
+ return "null"
78
+ }
79
+ switch (typeof driverValue) {
80
+ case "boolean":
81
+ return driverValue ? "true" : "false"
82
+ case "number":
83
+ if (!Number.isFinite(driverValue)) {
84
+ throw new Error("Expected a finite numeric value")
85
+ }
86
+ return String(driverValue)
87
+ case "bigint":
88
+ return driverValue.toString()
89
+ case "string":
90
+ return renderPostgresDdlString(driverValue)
91
+ case "object":
92
+ if (driverValue instanceof Uint8Array) {
93
+ return renderPostgresDdlBytes(driverValue)
94
+ }
95
+ break
96
+ }
97
+ throw new Error("Unsupported postgres DDL literal value")
98
+ }
99
+
49
100
  const renderDdlExpression = (
50
101
  expression: DdlExpressionLike,
51
102
  state: RenderState,
52
103
  dialect: SqlDialect
53
- ): string =>
54
- SchemaExpression.isSchemaExpression(expression)
55
- ? SchemaExpression.render(expression)
56
- : renderExpression(expression, state, dialect)
104
+ ): string => {
105
+ if (SchemaExpression.isSchemaExpression(expression)) {
106
+ return SchemaExpression.render(expression)
107
+ }
108
+ return renderExpression(expression, state, {
109
+ ...dialect,
110
+ renderLiteral: renderPostgresDdlLiteral
111
+ })
112
+ }
57
113
 
58
114
  const renderColumnDefinition = (
59
115
  dialect: SqlDialect,
@@ -100,17 +156,19 @@ const renderCreateTableSql = (
100
156
  case "foreignKey": {
101
157
  const reference = option.references()
102
158
  definitions.push(
103
- `${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}foreign key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")}) references ${dialect.renderTableReference(reference.tableName, reference.tableName, reference.schemaName)} (${reference.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.onDelete ? ` on delete ${option.onDelete.replace(/[A-Z]/g, (value) => ` ${value.toLowerCase()}`).trim()}` : ""}${option.onUpdate ? ` on update ${option.onUpdate.replace(/[A-Z]/g, (value) => ` ${value.toLowerCase()}`).trim()}` : ""}${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`
159
+ `${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}foreign key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")}) references ${dialect.renderTableReference(reference.tableName, reference.tableName, reference.schemaName)} (${reference.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.onDelete !== undefined ? ` on delete ${renderReferentialAction(option.onDelete)}` : ""}${option.onUpdate !== undefined ? ` on update ${renderReferentialAction(option.onUpdate)}` : ""}${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`
104
160
  )
105
161
  break
106
162
  }
107
163
  case "check":
108
164
  definitions.push(
109
- `constraint ${dialect.quoteIdentifier(option.name)} check (${renderDdlExpression(option.predicate, state, dialect)})${option.noInherit ? " no inherit" : ""}`
165
+ `constraint ${dialect.quoteIdentifier(option.name)} check (${renderDdlExpression(option.predicate, { ...state, rowLocalColumns: true }, dialect)})${option.noInherit ? " no inherit" : ""}`
110
166
  )
111
167
  break
112
168
  case "index":
113
169
  break
170
+ default:
171
+ throw new Error("Unsupported table option kind")
114
172
  }
115
173
  }
116
174
  return `create table${ifNotExists ? " if not exists" : ""} ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${definitions.join(", ")})`
@@ -131,20 +189,74 @@ const renderDropIndexSql = (
131
189
  ddl: Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
132
190
  state: RenderState,
133
191
  dialect: SqlDialect
134
- ): string =>
135
- dialect.name === "postgres"
136
- ? `drop index${ddl.ifExists ? " if exists" : ""} ${dialect.quoteIdentifier(ddl.name)}`
137
- : `drop index ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
192
+ ): string => {
193
+ if (dialect.name === "postgres") {
194
+ const schemaName = typeof targetSource.source === "object" &&
195
+ targetSource.source !== null &&
196
+ Table.TypeId in targetSource.source
197
+ ? (targetSource.source as Table.AnyTable)[Table.TypeId].schemaName
198
+ : undefined
199
+ const indexName = schemaName === undefined || schemaName === "public"
200
+ ? dialect.quoteIdentifier(ddl.name)
201
+ : `${dialect.quoteIdentifier(schemaName)}.${dialect.quoteIdentifier(ddl.name)}`
202
+ return `drop index${ddl.ifExists ? " if exists" : ""} ${indexName}`
203
+ }
204
+ return `drop index ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
205
+ }
138
206
 
139
207
  const isExpression = (value: unknown): value is Expression.Any =>
140
208
  value !== null && typeof value === "object" && Expression.TypeId in value
141
209
 
142
- const isJsonDbType = (dbType: Expression.DbType.Any): boolean =>
143
- dbType.kind === "jsonb" || dbType.kind === "json" || ("variant" in dbType && dbType.variant === "json")
210
+ const isJsonDbType = (dbType: Expression.DbType.Any): boolean => {
211
+ if (dbType.kind === "jsonb" || dbType.kind === "json") {
212
+ return true
213
+ }
214
+ if (!("variant" in dbType)) {
215
+ return false
216
+ }
217
+ const variant = dbType.variant as string
218
+ return variant === "json" || variant === "jsonb"
219
+ }
144
220
 
145
221
  const isJsonExpression = (value: unknown): value is Expression.Any =>
146
222
  isExpression(value) && isJsonDbType(value[Expression.TypeId].dbType)
147
223
 
224
+ const postgresRangeSubtypeByKind: Readonly<Record<string, string>> = {
225
+ int4range: "int4",
226
+ int8range: "int8",
227
+ numrange: "numeric",
228
+ tsrange: "timestamp",
229
+ tstzrange: "timestamptz",
230
+ daterange: "date",
231
+ int4multirange: "int4",
232
+ int8multirange: "int8",
233
+ nummultirange: "numeric",
234
+ tsmultirange: "timestamp",
235
+ tstzmultirange: "timestamptz",
236
+ datemultirange: "date"
237
+ }
238
+
239
+ const postgresRangeSubtypeKey = (dbType: Expression.DbType.Any): string | undefined => {
240
+ if ("base" in dbType) {
241
+ return postgresRangeSubtypeKey(dbType.base)
242
+ }
243
+ if ("subtype" in dbType) {
244
+ return postgresRangeSubtypeKey(dbType.subtype) ?? dbType.subtype.kind
245
+ }
246
+ return postgresRangeSubtypeByKind[dbType.kind]
247
+ }
248
+
249
+ const assertCompatiblePostgresRangeOperands = (
250
+ left: Expression.Any,
251
+ right: Expression.Any
252
+ ): void => {
253
+ const leftKey = postgresRangeSubtypeKey(left[Expression.TypeId].dbType)
254
+ const rightKey = postgresRangeSubtypeKey(right[Expression.TypeId].dbType)
255
+ if (leftKey !== undefined && rightKey !== undefined && leftKey !== rightKey) {
256
+ throw new Error("Incompatible postgres range operands")
257
+ }
258
+ }
259
+
148
260
  const unsupportedJsonFeature = (
149
261
  dialect: SqlDialect,
150
262
  feature: string
@@ -202,19 +314,19 @@ const extractJsonValue = (node: Record<string, unknown>): unknown =>
202
314
  node.newValue ?? node.insert ?? node.right
203
315
 
204
316
  const renderJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
317
+ const renderKey = (value: string): string =>
318
+ /^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
319
+ ? `.${value}`
320
+ : `.${JSON.stringify(value)}`
205
321
  if (typeof segment === "string") {
206
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)
207
- ? `.${segment}`
208
- : `."${segment.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
322
+ return renderKey(segment)
209
323
  }
210
324
  if (typeof segment === "number") {
211
325
  return `[${segment}]`
212
326
  }
213
327
  switch (segment.kind) {
214
328
  case "key":
215
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment.key)
216
- ? `.${segment.key}`
217
- : `."${segment.key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
329
+ return renderKey(segment.key)
218
330
  case "index":
219
331
  return `[${segment.index}]`
220
332
  case "wildcard":
@@ -279,7 +391,7 @@ const renderPostgresJsonAccessStep = (
279
391
  case "key":
280
392
  return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(segment.key, state)}`
281
393
  case "index":
282
- return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(String(segment.index), state)}`
394
+ return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(segment.index, state)}`
283
395
  default:
284
396
  throw new Error("Postgres exact JSON access requires key/index segments")
285
397
  }
@@ -294,11 +406,66 @@ const renderPostgresJsonValue = (
294
406
  throw new Error("Expected a JSON expression")
295
407
  }
296
408
  const rendered = renderExpression(value, state, dialect)
409
+ const ast = (value as Expression.Any & {
410
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
411
+ })[ExpressionAst.TypeId]
412
+ if (ast.kind === "literal") {
413
+ return `cast(${rendered} as jsonb)`
414
+ }
297
415
  return value[Expression.TypeId].dbType.kind === "jsonb"
298
416
  ? rendered
299
417
  : `cast(${rendered} as jsonb)`
300
418
  }
301
419
 
420
+ const expressionDriverContext = (
421
+ expression: Expression.Any,
422
+ state: RenderState,
423
+ dialect: SqlDialect
424
+ ) => ({
425
+ dialect: dialect.name,
426
+ valueMappings: state.valueMappings,
427
+ dbType: expression[Expression.TypeId].dbType,
428
+ runtimeSchema: expression[Expression.TypeId].runtimeSchema,
429
+ driverValueMapping: expression[Expression.TypeId].driverValueMapping
430
+ })
431
+
432
+ const renderJsonInputExpression = (
433
+ expression: Expression.Any,
434
+ state: RenderState,
435
+ dialect: SqlDialect
436
+ ): string =>
437
+ renderJsonSelectSql(
438
+ renderExpression(expression, state, dialect),
439
+ expressionDriverContext(expression, state, dialect)
440
+ )
441
+
442
+ const encodeArrayValues = (
443
+ values: readonly unknown[],
444
+ column: Table.AnyTable[typeof Table.TypeId]["fields"][string],
445
+ state: RenderState,
446
+ dialect: SqlDialect
447
+ ): readonly unknown[] =>
448
+ values.map((value) => {
449
+ if (value === null && column.metadata.nullable) {
450
+ return null
451
+ }
452
+ const runtimeSchemaAccepts = column.schema !== undefined &&
453
+ (Schema.is(column.schema) as (candidate: unknown) => boolean)(value)
454
+ const normalizedValue = runtimeSchemaAccepts
455
+ ? value
456
+ : normalizeDbValue(column.metadata.dbType, value)
457
+ const encodedValue = column.schema === undefined || runtimeSchemaAccepts
458
+ ? normalizedValue
459
+ : (Schema.decodeUnknownSync as any)(column.schema)(normalizedValue)
460
+ return toDriverValue(encodedValue, {
461
+ dialect: dialect.name,
462
+ valueMappings: state.valueMappings,
463
+ dbType: column.metadata.dbType,
464
+ runtimeSchema: column.schema,
465
+ driverValueMapping: column.metadata.driverValueMapping
466
+ })
467
+ })
468
+
302
469
  const renderPostgresJsonKind = (
303
470
  value: Expression.Any
304
471
  ): "json" | "jsonb" => value[Expression.TypeId].dbType.kind === "jsonb" ? "jsonb" : "json"
@@ -460,7 +627,7 @@ const renderJsonExpression = (
460
627
  : []
461
628
  const renderedEntries = entries.flatMap((entry) => [
462
629
  dialect.renderLiteral(entry.key, state),
463
- renderExpression(entry.value, state, dialect)
630
+ renderJsonInputExpression(entry.value, state, dialect)
464
631
  ])
465
632
  if (dialect.name === "postgres") {
466
633
  return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_object(${renderedEntries.join(", ")})`
@@ -474,7 +641,7 @@ const renderJsonExpression = (
474
641
  const values = Array.isArray((ast as { readonly values?: readonly Expression.Any[] }).values)
475
642
  ? (ast as { readonly values: readonly Expression.Any[] }).values
476
643
  : []
477
- const renderedValues = values.map((value) => renderExpression(value, state, dialect)).join(", ")
644
+ const renderedValues = values.map((value) => renderJsonInputExpression(value, state, dialect)).join(", ")
478
645
  if (dialect.name === "postgres") {
479
646
  return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_array(${renderedValues})`
480
647
  }
@@ -488,7 +655,7 @@ const renderJsonExpression = (
488
655
  return undefined
489
656
  }
490
657
  if (dialect.name === "postgres") {
491
- return `to_json(${renderExpression(base, state, dialect)})`
658
+ return `to_json(${renderJsonInputExpression(base, state, dialect)})`
492
659
  }
493
660
  if (dialect.name === "mysql") {
494
661
  return `cast(${renderExpression(base, state, dialect)} as json)`
@@ -499,7 +666,7 @@ const renderJsonExpression = (
499
666
  return undefined
500
667
  }
501
668
  if (dialect.name === "postgres") {
502
- return `to_jsonb(${renderExpression(base, state, dialect)})`
669
+ return `to_jsonb(${renderJsonInputExpression(base, state, dialect)})`
503
670
  }
504
671
  if (dialect.name === "mysql") {
505
672
  return `cast(${renderExpression(base, state, dialect)} as json)`
@@ -540,7 +707,7 @@ const renderJsonExpression = (
540
707
  const baseSql = renderExpression(base, state, dialect)
541
708
  const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
542
709
  const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
543
- return `(case when ${typeOf}(${baseSql}) = 'object' then array(select ${objectKeys}(${baseSql})) else null end)`
710
+ return `(case when ${typeOf}(${baseSql}) = 'object' then to_json(array(select ${objectKeys}(${baseSql}))) else null end)`
544
711
  }
545
712
  if (dialect.name === "mysql") {
546
713
  return `json_keys(${renderExpression(base, state, dialect)})`
@@ -567,7 +734,7 @@ const renderJsonExpression = (
567
734
  const segment = segments[0]!
568
735
  return `(${baseSql} - ${segment.kind === "key"
569
736
  ? dialect.renderLiteral(segment.key, state)
570
- : dialect.renderLiteral(String(segment.index), state)})`
737
+ : dialect.renderLiteral(segment.index, state)})`
571
738
  }
572
739
  return `(${baseSql} #- ${renderPostgresJsonPathArray(segments, state, dialect)})`
573
740
  }
@@ -691,6 +858,15 @@ const renderDeleteTargets = (
691
858
  dialect: SqlDialect
692
859
  ): string => targets.map((target) => dialect.quoteIdentifier(target.tableName)).join(", ")
693
860
 
861
+ const assertMergeActionKind = (
862
+ kind: unknown,
863
+ allowed: readonly string[]
864
+ ): void => {
865
+ if (typeof kind !== "string" || !allowed.includes(kind)) {
866
+ throw new Error("Unsupported merge action kind")
867
+ }
868
+ }
869
+
694
870
  const renderMysqlMutationLock = (
695
871
  lock: QueryAst.LockClause | undefined,
696
872
  statement: "update" | "delete"
@@ -717,8 +893,9 @@ const renderTransactionClause = (
717
893
  switch (clause.kind) {
718
894
  case "transaction": {
719
895
  const modes: string[] = []
720
- if (clause.isolationLevel) {
721
- modes.push(`isolation level ${clause.isolationLevel}`)
896
+ const isolationLevel = renderTransactionIsolationLevel(clause.isolationLevel)
897
+ if (isolationLevel) {
898
+ modes.push(isolationLevel)
722
899
  }
723
900
  if (clause.readOnly === true) {
724
901
  modes.push("read only")
@@ -738,7 +915,7 @@ const renderTransactionClause = (
738
915
  case "releaseSavepoint":
739
916
  return `release savepoint ${dialect.quoteIdentifier(clause.name)}`
740
917
  }
741
- return ""
918
+ throw new Error("Unsupported transaction statement kind")
742
919
  }
743
920
 
744
921
  const renderSelectionList = (
@@ -751,19 +928,117 @@ const renderSelectionList = (
751
928
  validateAggregationSelection(selection as SelectionValue, [])
752
929
  }
753
930
  const flattened = flattenSelection(selection)
931
+ if (dialect.name === "mysql" && flattened.length === 0) {
932
+ throw new Error("mysql select statements require at least one selected expression")
933
+ }
754
934
  const projections = selectionProjections(selection)
755
935
  const sql = flattened.map(({ expression, alias }) =>
756
- `${renderExpression(expression, state, dialect)} as ${dialect.quoteIdentifier(alias)}`).join(", ")
936
+ `${renderSelectSql(renderExpression(expression, state, dialect), expressionDriverContext(expression, state, dialect))} as ${dialect.quoteIdentifier(alias)}`).join(", ")
757
937
  return {
758
938
  sql,
759
939
  projections
760
940
  }
761
941
  }
762
942
 
943
+ const nestedRenderState = (state: RenderState): RenderState => ({
944
+ params: state.params,
945
+ valueMappings: state.valueMappings,
946
+ ctes: [],
947
+ cteNames: new Set(state.cteNames),
948
+ cteSources: new Map(state.cteSources)
949
+ })
950
+
951
+ const assertMatchingSetProjections = (
952
+ left: readonly Projection[],
953
+ right: readonly Projection[]
954
+ ): void => {
955
+ const leftKeys = left.map((projection) => JSON.stringify(projection.path))
956
+ const rightKeys = right.map((projection) => JSON.stringify(projection.path))
957
+ if (leftKeys.length !== rightKeys.length || leftKeys.some((key, index) => key !== rightKeys[index])) {
958
+ throw new Error("set operator operands must have matching result rows")
959
+ }
960
+ }
961
+
962
+ const assertNoGroupedMutationClauses = (
963
+ ast: Pick<QueryAst.Ast, "groupBy" | "having">,
964
+ statement: string
965
+ ): void => {
966
+ if (ast.groupBy.length > 0) {
967
+ throw new Error(`groupBy(...) is not supported for ${statement} statements`)
968
+ }
969
+ if (ast.having.length > 0) {
970
+ throw new Error(`having(...) is not supported for ${statement} statements`)
971
+ }
972
+ }
973
+
974
+ const assertNoInsertQueryClauses = (
975
+ ast: Pick<QueryAst.Ast, "where" | "joins" | "orderBy" | "limit" | "offset" | "lock">
976
+ ): void => {
977
+ if (ast.where.length > 0) {
978
+ throw new Error("where(...) is not supported for insert statements")
979
+ }
980
+ if (ast.joins.length > 0) {
981
+ throw new Error("join(...) is not supported for insert statements")
982
+ }
983
+ if (ast.orderBy.length > 0) {
984
+ throw new Error("orderBy(...) is not supported for insert statements")
985
+ }
986
+ if (ast.limit) {
987
+ throw new Error("limit(...) is not supported for insert statements")
988
+ }
989
+ if (ast.offset) {
990
+ throw new Error("offset(...) is not supported for insert statements")
991
+ }
992
+ if (ast.lock) {
993
+ throw new Error("lock(...) is not supported for insert statements")
994
+ }
995
+ }
996
+
997
+ const assertNoStatementQueryClauses = (
998
+ ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
999
+ statement: string,
1000
+ options: { readonly allowSelection?: boolean } = {}
1001
+ ): void => {
1002
+ if (ast.distinct) {
1003
+ throw new Error(`distinct(...) is not supported for ${statement} statements`)
1004
+ }
1005
+ if (ast.where.length > 0) {
1006
+ throw new Error(`where(...) is not supported for ${statement} statements`)
1007
+ }
1008
+ if ((ast.fromSources?.length ?? 0) > 0 || ast.from) {
1009
+ throw new Error(`from(...) is not supported for ${statement} statements`)
1010
+ }
1011
+ if (ast.joins.length > 0) {
1012
+ throw new Error(`join(...) is not supported for ${statement} statements`)
1013
+ }
1014
+ if (ast.groupBy.length > 0) {
1015
+ throw new Error(`groupBy(...) is not supported for ${statement} statements`)
1016
+ }
1017
+ if (ast.having.length > 0) {
1018
+ throw new Error(`having(...) is not supported for ${statement} statements`)
1019
+ }
1020
+ if (ast.orderBy.length > 0) {
1021
+ throw new Error(`orderBy(...) is not supported for ${statement} statements`)
1022
+ }
1023
+ if (ast.limit) {
1024
+ throw new Error(`limit(...) is not supported for ${statement} statements`)
1025
+ }
1026
+ if (ast.offset) {
1027
+ throw new Error(`offset(...) is not supported for ${statement} statements`)
1028
+ }
1029
+ if (ast.lock) {
1030
+ throw new Error(`lock(...) is not supported for ${statement} statements`)
1031
+ }
1032
+ if (options.allowSelection !== true && Object.keys(ast.select).length > 0) {
1033
+ throw new Error(`returning(...) is not supported for ${statement} statements`)
1034
+ }
1035
+ }
1036
+
763
1037
  export const renderQueryAst = (
764
1038
  ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
765
1039
  state: RenderState,
766
- dialect: SqlDialect
1040
+ dialect: SqlDialect,
1041
+ options: { readonly emitCtes?: boolean } = {}
767
1042
  ): RenderedQueryAst => {
768
1043
  let sql = ""
769
1044
  let projections: readonly Projection[] = []
@@ -773,10 +1048,11 @@ export const renderQueryAst = (
773
1048
  validateAggregationSelection(ast.select as SelectionValue, ast.groupBy)
774
1049
  const rendered = renderSelectionList(ast.select as Record<string, unknown>, state, dialect, false)
775
1050
  projections = rendered.projections
1051
+ const selectList = rendered.sql.length > 0 ? ` ${rendered.sql}` : ""
776
1052
  const clauses = [
777
1053
  ast.distinctOn && ast.distinctOn.length > 0
778
- ? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")}) ${rendered.sql}`
779
- : `select${ast.distinct ? " distinct" : ""} ${rendered.sql}`
1054
+ ? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")})${selectList}`
1055
+ : `select${ast.distinct ? " distinct" : ""}${selectList}`
780
1056
  ]
781
1057
  if (ast.from) {
782
1058
  clauses.push(`from ${renderSourceReference(ast.from.source, ast.from.tableName, ast.from.baseTableName, state, dialect)}`)
@@ -808,8 +1084,11 @@ export const renderQueryAst = (
808
1084
  clauses.push(`offset ${renderExpression(ast.offset, state, dialect)}`)
809
1085
  }
810
1086
  if (ast.lock) {
1087
+ if (ast.lock.nowait && ast.lock.skipLocked) {
1088
+ throw new Error("lock(...) cannot specify both nowait and skipLocked")
1089
+ }
811
1090
  clauses.push(
812
- `${ast.lock.mode === "update" ? "for update" : "for share"}${ast.lock.nowait ? " nowait" : ""}${ast.lock.skipLocked ? " skip locked" : ""}`
1091
+ `${renderSelectLockMode(ast.lock.mode)}${ast.lock.nowait ? " nowait" : ""}${ast.lock.skipLocked ? " skip locked" : ""}`
813
1092
  )
814
1093
  }
815
1094
  sql = clauses.join(" ")
@@ -817,6 +1096,7 @@ export const renderQueryAst = (
817
1096
  }
818
1097
  case "set": {
819
1098
  const setAst = ast as QueryAst.Ast<Record<string, unknown>, any, "set">
1099
+ assertNoStatementQueryClauses(setAst, "set", { allowSelection: true })
820
1100
  const base = renderQueryAst(
821
1101
  Query.getAst(setAst.setBase as Query.Plan.Any) as QueryAst.Ast<
822
1102
  Record<string, unknown>,
@@ -827,6 +1107,7 @@ export const renderQueryAst = (
827
1107
  dialect
828
1108
  )
829
1109
  projections = selectionProjections(setAst.select as Record<string, unknown>)
1110
+ assertMatchingSetProjections(projections, base.projections)
830
1111
  sql = [
831
1112
  `(${base.sql})`,
832
1113
  ...(setAst.setOperations ?? []).map((entry) => {
@@ -839,6 +1120,7 @@ export const renderQueryAst = (
839
1120
  state,
840
1121
  dialect
841
1122
  )
1123
+ assertMatchingSetProjections(projections, rendered.projections)
842
1124
  return `${entry.kind}${entry.all ? " all" : ""} (${rendered.sql})`
843
1125
  })
844
1126
  ].join(" ")
@@ -846,19 +1128,26 @@ export const renderQueryAst = (
846
1128
  }
847
1129
  case "insert": {
848
1130
  const insertAst = ast as QueryAst.Ast<Record<string, unknown>, any, "insert">
1131
+ if (insertAst.distinct) {
1132
+ throw new Error("distinct(...) is not supported for insert statements")
1133
+ }
1134
+ assertNoGroupedMutationClauses(insertAst, "insert")
1135
+ assertNoInsertQueryClauses(insertAst)
849
1136
  const targetSource = insertAst.into!
850
1137
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1138
+ const insertSource = expectInsertSourceKind(insertAst.insertSource)
1139
+ const conflict = expectConflictClause(insertAst.conflict)
851
1140
  sql = `insert into ${target}`
852
- if (insertAst.insertSource?.kind === "values") {
853
- const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
854
- const rows = insertAst.insertSource.rows.map((row) =>
1141
+ if (insertSource?.kind === "values") {
1142
+ const columns = insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
1143
+ const rows = insertSource.rows.map((row) =>
855
1144
  `(${row.values.map((entry) => renderExpression(entry.value, state, dialect)).join(", ")})`
856
1145
  ).join(", ")
857
1146
  sql += ` (${columns}) values ${rows}`
858
- } else if (insertAst.insertSource?.kind === "query") {
859
- const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
1147
+ } else if (insertSource?.kind === "query") {
1148
+ const columns = insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
860
1149
  const renderedQuery = renderQueryAst(
861
- Query.getAst(insertAst.insertSource.query as Query.Plan.Any) as QueryAst.Ast<
1150
+ Query.getAst(insertSource.query as Query.Plan.Any) as QueryAst.Ast<
862
1151
  Record<string, unknown>,
863
1152
  any,
864
1153
  QueryAst.QueryStatement
@@ -867,21 +1156,24 @@ export const renderQueryAst = (
867
1156
  dialect
868
1157
  )
869
1158
  sql += ` (${columns}) ${renderedQuery.sql}`
870
- } else if (insertAst.insertSource?.kind === "unnest") {
871
- const unnestSource = insertAst.insertSource
872
- const columns = unnestSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
1159
+ } else if (insertSource?.kind === "unnest") {
1160
+ const columns = insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
873
1161
  if (dialect.name === "postgres") {
874
1162
  const table = targetSource.source as Table.AnyTable
875
1163
  const fields = table[Table.TypeId].fields
876
- const rendered = unnestSource.values.map((entry) =>
877
- `cast(${dialect.renderLiteral(entry.values, state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
1164
+ const rendered = insertSource.values.map((entry) =>
1165
+ `cast(${dialect.renderLiteral(encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect), state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
878
1166
  ).join(", ")
879
1167
  sql += ` (${columns}) select * from unnest(${rendered})`
880
1168
  } else {
881
- const rowCount = unnestSource.values[0]?.values.length ?? 0
1169
+ const rowCount = insertSource.values[0]?.values.length ?? 0
882
1170
  const rows = Array.from({ length: rowCount }, (_, index) =>
883
- `(${unnestSource.values.map((entry) =>
884
- dialect.renderLiteral(entry.values[index], state)
1171
+ `(${insertSource.values.map((entry) =>
1172
+ dialect.renderLiteral(
1173
+ entry.values[index],
1174
+ state,
1175
+ (targetSource.source as Table.AnyTable)[Table.TypeId].fields[entry.columnName]![Expression.TypeId]
1176
+ )
885
1177
  ).join(", ")})`
886
1178
  ).join(", ")
887
1179
  sql += ` (${columns}) values ${rows}`
@@ -895,21 +1187,24 @@ export const renderQueryAst = (
895
1187
  sql += " default values"
896
1188
  }
897
1189
  }
898
- if (insertAst.conflict) {
899
- const updateValues = (insertAst.conflict.values ?? []).map((entry) =>
1190
+ if (conflict) {
1191
+ if (conflict.action === "doNothing" && conflict.where) {
1192
+ throw new Error("conflict action predicates require update assignments")
1193
+ }
1194
+ const updateValues = (conflict.values ?? []).map((entry) =>
900
1195
  `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
901
1196
  ).join(", ")
902
1197
  if (dialect.name === "postgres") {
903
- const targetSql = insertAst.conflict.target?.kind === "constraint"
904
- ? ` on conflict on constraint ${dialect.quoteIdentifier(insertAst.conflict.target.name)}`
905
- : insertAst.conflict.target?.kind === "columns"
906
- ? ` on conflict (${insertAst.conflict.target.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${insertAst.conflict.target.where ? ` where ${renderExpression(insertAst.conflict.target.where, state, dialect)}` : ""}`
1198
+ const targetSql = conflict.target?.kind === "constraint"
1199
+ ? ` on conflict on constraint ${dialect.quoteIdentifier(conflict.target.name)}`
1200
+ : conflict.target?.kind === "columns"
1201
+ ? ` on conflict (${conflict.target.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${conflict.target.where ? ` where ${renderExpression(conflict.target.where, state, dialect)}` : ""}`
907
1202
  : " on conflict"
908
1203
  sql += targetSql
909
- sql += insertAst.conflict.action === "doNothing"
1204
+ sql += conflict.action === "doNothing"
910
1205
  ? " do nothing"
911
- : ` do update set ${updateValues}${insertAst.conflict.where ? ` where ${renderExpression(insertAst.conflict.where, state, dialect)}` : ""}`
912
- } else if (insertAst.conflict.action === "doNothing") {
1206
+ : ` do update set ${updateValues}${conflict.where ? ` where ${renderExpression(conflict.where, state, dialect)}` : ""}`
1207
+ } else if (conflict.action === "doNothing") {
913
1208
  sql = sql.replace(/^insert/, "insert ignore")
914
1209
  } else {
915
1210
  sql += ` on duplicate key update ${updateValues}`
@@ -924,10 +1219,29 @@ export const renderQueryAst = (
924
1219
  }
925
1220
  case "update": {
926
1221
  const updateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "update">
1222
+ if (updateAst.distinct) {
1223
+ throw new Error("distinct(...) is not supported for update statements")
1224
+ }
1225
+ assertNoGroupedMutationClauses(updateAst, "update")
1226
+ if (updateAst.orderBy.length > 0) {
1227
+ throw new Error("orderBy(...) is not supported for update statements")
1228
+ }
1229
+ if (updateAst.limit) {
1230
+ throw new Error("limit(...) is not supported for update statements")
1231
+ }
1232
+ if (updateAst.offset) {
1233
+ throw new Error("offset(...) is not supported for update statements")
1234
+ }
1235
+ if (updateAst.lock) {
1236
+ throw new Error("lock(...) is not supported for update statements")
1237
+ }
927
1238
  const targetSource = updateAst.target!
928
1239
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
929
1240
  const targets = updateAst.targets ?? [targetSource]
930
1241
  const fromSources = updateAst.fromSources ?? []
1242
+ if ((updateAst.set ?? []).length === 0) {
1243
+ throw new Error("update statements require at least one assignment")
1244
+ }
931
1245
  const assignments = updateAst.set!.map((entry) =>
932
1246
  renderMutationAssignment(entry, state, dialect)).join(", ")
933
1247
  if (dialect.name === "mysql") {
@@ -977,6 +1291,22 @@ export const renderQueryAst = (
977
1291
  }
978
1292
  case "delete": {
979
1293
  const deleteAst = ast as QueryAst.Ast<Record<string, unknown>, any, "delete">
1294
+ if (deleteAst.distinct) {
1295
+ throw new Error("distinct(...) is not supported for delete statements")
1296
+ }
1297
+ assertNoGroupedMutationClauses(deleteAst, "delete")
1298
+ if (deleteAst.orderBy.length > 0 && dialect.name === "postgres") {
1299
+ throw new Error("orderBy(...) is not supported for delete statements")
1300
+ }
1301
+ if (deleteAst.limit && dialect.name === "postgres") {
1302
+ throw new Error("limit(...) is not supported for delete statements")
1303
+ }
1304
+ if (deleteAst.offset) {
1305
+ throw new Error("offset(...) is not supported for delete statements")
1306
+ }
1307
+ if (deleteAst.lock) {
1308
+ throw new Error("lock(...) is not supported for delete statements")
1309
+ }
980
1310
  const targetSource = deleteAst.target!
981
1311
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
982
1312
  const targets = deleteAst.targets ?? [targetSource]
@@ -1023,12 +1353,14 @@ export const renderQueryAst = (
1023
1353
  }
1024
1354
  case "truncate": {
1025
1355
  const truncateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "truncate">
1356
+ assertNoStatementQueryClauses(truncateAst, "truncate")
1357
+ const truncate = expectTruncateClause(truncateAst.truncate)
1026
1358
  const targetSource = truncateAst.target!
1027
1359
  sql = `truncate table ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
1028
- if (truncateAst.truncate?.restartIdentity) {
1360
+ if (truncate.restartIdentity) {
1029
1361
  sql += " restart identity"
1030
1362
  }
1031
- if (truncateAst.truncate?.cascade) {
1363
+ if (truncate.cascade) {
1032
1364
  sql += " cascade"
1033
1365
  }
1034
1366
  break
@@ -1041,8 +1373,18 @@ export const renderQueryAst = (
1041
1373
  const targetSource = mergeAst.target!
1042
1374
  const usingSource = mergeAst.using!
1043
1375
  const merge = mergeAst.merge!
1376
+ if (merge.kind !== "merge") {
1377
+ throw new Error("Unsupported merge statement kind")
1378
+ }
1379
+ if (Object.keys(mergeAst.select as Record<string, unknown>).length > 0) {
1380
+ throw new Error("returning(...) is not supported for merge statements")
1381
+ }
1382
+ if (!merge.whenMatched && !merge.whenNotMatched) {
1383
+ throw new Error("merge statements require at least one action")
1384
+ }
1044
1385
  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)}`
1045
1386
  if (merge.whenMatched) {
1387
+ assertMergeActionKind(merge.whenMatched.kind, ["update", "delete"])
1046
1388
  sql += " when matched"
1047
1389
  if (merge.whenMatched.predicate) {
1048
1390
  sql += ` and ${renderExpression(merge.whenMatched.predicate, state, dialect)}`
@@ -1050,16 +1392,23 @@ export const renderQueryAst = (
1050
1392
  if (merge.whenMatched.kind === "delete") {
1051
1393
  sql += " then delete"
1052
1394
  } else {
1395
+ if (merge.whenMatched.values.length === 0) {
1396
+ throw new Error("merge update actions require at least one assignment")
1397
+ }
1053
1398
  sql += ` then update set ${merge.whenMatched.values.map((entry) =>
1054
1399
  `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
1055
1400
  ).join(", ")}`
1056
1401
  }
1057
1402
  }
1058
1403
  if (merge.whenNotMatched) {
1404
+ assertMergeActionKind(merge.whenNotMatched.kind, ["insert"])
1059
1405
  sql += " when not matched"
1060
1406
  if (merge.whenNotMatched.predicate) {
1061
1407
  sql += ` and ${renderExpression(merge.whenNotMatched.predicate, state, dialect)}`
1062
1408
  }
1409
+ if (merge.whenNotMatched.values.length === 0) {
1410
+ throw new Error("merge insert actions require at least one value")
1411
+ }
1063
1412
  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(", ")})`
1064
1413
  }
1065
1414
  break
@@ -1070,25 +1419,30 @@ export const renderQueryAst = (
1070
1419
  case "savepoint":
1071
1420
  case "rollbackTo":
1072
1421
  case "releaseSavepoint": {
1422
+ assertNoStatementQueryClauses(ast, ast.kind)
1073
1423
  sql = renderTransactionClause(ast.transaction!, dialect)
1074
1424
  break
1075
1425
  }
1076
1426
  case "createTable": {
1077
1427
  const createTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createTable">
1078
- sql = renderCreateTableSql(createTableAst.target!, state, dialect, createTableAst.ddl?.kind === "createTable" && createTableAst.ddl.ifNotExists)
1428
+ assertNoStatementQueryClauses(createTableAst, "createTable")
1429
+ const ddl = expectDdlClauseKind(createTableAst.ddl, "createTable")
1430
+ sql = renderCreateTableSql(createTableAst.target!, state, dialect, ddl.ifNotExists)
1079
1431
  break
1080
1432
  }
1081
1433
  case "dropTable": {
1082
1434
  const dropTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropTable">
1083
- const ifExists = dropTableAst.ddl?.kind === "dropTable" && dropTableAst.ddl.ifExists
1084
- sql = `drop table${ifExists ? " if exists" : ""} ${renderSourceReference(dropTableAst.target!.source, dropTableAst.target!.tableName, dropTableAst.target!.baseTableName, state, dialect)}`
1435
+ assertNoStatementQueryClauses(dropTableAst, "dropTable")
1436
+ const ddl = expectDdlClauseKind(dropTableAst.ddl, "dropTable")
1437
+ sql = `drop table${ddl.ifExists ? " if exists" : ""} ${renderSourceReference(dropTableAst.target!.source, dropTableAst.target!.tableName, dropTableAst.target!.baseTableName, state, dialect)}`
1085
1438
  break
1086
1439
  }
1087
1440
  case "createIndex": {
1088
1441
  const createIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createIndex">
1442
+ assertNoStatementQueryClauses(createIndexAst, "createIndex")
1089
1443
  sql = renderCreateIndexSql(
1090
1444
  createIndexAst.target!,
1091
- createIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
1445
+ expectDdlClauseKind(createIndexAst.ddl, "createIndex"),
1092
1446
  state,
1093
1447
  dialect
1094
1448
  )
@@ -1096,17 +1450,20 @@ export const renderQueryAst = (
1096
1450
  }
1097
1451
  case "dropIndex": {
1098
1452
  const dropIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropIndex">
1453
+ assertNoStatementQueryClauses(dropIndexAst, "dropIndex")
1099
1454
  sql = renderDropIndexSql(
1100
1455
  dropIndexAst.target!,
1101
- dropIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
1456
+ expectDdlClauseKind(dropIndexAst.ddl, "dropIndex"),
1102
1457
  state,
1103
1458
  dialect
1104
1459
  )
1105
1460
  break
1106
1461
  }
1462
+ default:
1463
+ throw new Error("Unsupported query statement kind")
1107
1464
  }
1108
1465
 
1109
- if (state.ctes.length === 0) {
1466
+ if (state.ctes.length === 0 || options.emitCtes === false) {
1110
1467
  return {
1111
1468
  sql,
1112
1469
  projections
@@ -1154,9 +1511,19 @@ const renderSourceReference = (
1154
1511
  readonly plan: Query.Plan.Any
1155
1512
  readonly recursive?: boolean
1156
1513
  }
1514
+ const registeredCteSource = state.cteSources.get(cte.name)
1515
+ if (registeredCteSource !== undefined && registeredCteSource !== cte.plan) {
1516
+ throw new Error(`common table expression name is already registered with a different plan: ${cte.name}`)
1517
+ }
1157
1518
  if (!state.cteNames.has(cte.name)) {
1158
1519
  state.cteNames.add(cte.name)
1159
- const rendered = renderQueryAst(Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect)
1520
+ state.cteSources.set(cte.name, cte.plan)
1521
+ const rendered = renderQueryAst(
1522
+ Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1523
+ state,
1524
+ dialect,
1525
+ { emitCtes: false }
1526
+ )
1160
1527
  state.ctes.push({
1161
1528
  name: cte.name,
1162
1529
  sql: rendered.sql,
@@ -1173,14 +1540,14 @@ const renderSourceReference = (
1173
1540
  if (!state.cteNames.has(derived.name)) {
1174
1541
  // derived tables are inlined, so no CTE registration is needed
1175
1542
  }
1176
- return `(${renderQueryAst(Query.getAst(derived.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect).sql}) as ${dialect.quoteIdentifier(derived.name)}`
1543
+ return `(${renderQueryAst(Query.getAst(derived.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, nestedRenderState(state), dialect).sql}) as ${dialect.quoteIdentifier(derived.name)}`
1177
1544
  }
1178
1545
  if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "lateral") {
1179
1546
  const lateral = source as unknown as {
1180
1547
  readonly name: string
1181
1548
  readonly plan: Query.Plan.Any
1182
1549
  }
1183
- return `lateral (${renderQueryAst(Query.getAst(lateral.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect).sql}) as ${dialect.quoteIdentifier(lateral.name)}`
1550
+ 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)}`
1184
1551
  }
1185
1552
  if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "values") {
1186
1553
  const values = source as unknown as {
@@ -1215,6 +1582,22 @@ const renderSourceReference = (
1215
1582
  return dialect.renderTableReference(tableName, baseTableName, schemaName)
1216
1583
  }
1217
1584
 
1585
+ const renderSubqueryExpressionPlan = (
1586
+ plan: Query.Plan.Any,
1587
+ state: RenderState,
1588
+ dialect: SqlDialect
1589
+ ): string => {
1590
+ const statement = Query.getQueryState(plan).statement
1591
+ if (statement !== "select" && statement !== "set") {
1592
+ throw new Error("subquery expressions only accept select-like query plans")
1593
+ }
1594
+ return renderQueryAst(
1595
+ Query.getAst(plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1596
+ state,
1597
+ dialect
1598
+ ).sql
1599
+ }
1600
+
1218
1601
  /**
1219
1602
  * Renders a scalar expression AST into SQL text.
1220
1603
  *
@@ -1249,11 +1632,14 @@ export const renderExpression = (
1249
1632
  : ">="
1250
1633
  switch (ast.kind) {
1251
1634
  case "column":
1252
- return ast.tableName.length === 0
1635
+ return state.rowLocalColumns || ast.tableName.length === 0
1253
1636
  ? dialect.quoteIdentifier(ast.columnName)
1254
1637
  : `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
1255
1638
  case "literal":
1256
- return dialect.renderLiteral(ast.value, state)
1639
+ if (typeof ast.value === "number" && !Number.isFinite(ast.value)) {
1640
+ throw new Error("Expected a finite numeric value")
1641
+ }
1642
+ return dialect.renderLiteral(ast.value, state, expression[Expression.TypeId])
1257
1643
  case "excluded":
1258
1644
  return dialect.name === "mysql"
1259
1645
  ? `values(${dialect.quoteIdentifier(ast.columnName)})`
@@ -1308,6 +1694,7 @@ export const renderExpression = (
1308
1694
  : `(${renderExpression(ast.left, state, dialect)} is not distinct from ${renderExpression(ast.right, state, dialect)})`
1309
1695
  case "contains":
1310
1696
  if (dialect.name === "postgres") {
1697
+ assertCompatiblePostgresRangeOperands(ast.left, ast.right)
1311
1698
  const left = isJsonExpression(ast.left)
1312
1699
  ? renderPostgresJsonValue(ast.left, state, dialect)
1313
1700
  : renderExpression(ast.left, state, dialect)
@@ -1322,6 +1709,7 @@ export const renderExpression = (
1322
1709
  throw new Error("Unsupported container operator for SQL rendering")
1323
1710
  case "containedBy":
1324
1711
  if (dialect.name === "postgres") {
1712
+ assertCompatiblePostgresRangeOperands(ast.left, ast.right)
1325
1713
  const left = isJsonExpression(ast.left)
1326
1714
  ? renderPostgresJsonValue(ast.left, state, dialect)
1327
1715
  : renderExpression(ast.left, state, dialect)
@@ -1336,6 +1724,7 @@ export const renderExpression = (
1336
1724
  throw new Error("Unsupported container operator for SQL rendering")
1337
1725
  case "overlaps":
1338
1726
  if (dialect.name === "postgres") {
1727
+ assertCompatiblePostgresRangeOperands(ast.left, ast.right)
1339
1728
  const left = isJsonExpression(ast.left)
1340
1729
  ? renderPostgresJsonValue(ast.left, state, dialect)
1341
1730
  : renderExpression(ast.left, state, dialect)
@@ -1365,14 +1754,26 @@ export const renderExpression = (
1365
1754
  case "min":
1366
1755
  return `min(${renderExpression(ast.value, state, dialect)})`
1367
1756
  case "and":
1757
+ if (ast.values.length === 0) {
1758
+ throw new Error("and(...) requires at least one predicate")
1759
+ }
1368
1760
  return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" and ")})`
1369
1761
  case "or":
1762
+ if (ast.values.length === 0) {
1763
+ throw new Error("or(...) requires at least one predicate")
1764
+ }
1370
1765
  return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" or ")})`
1371
1766
  case "coalesce":
1372
1767
  return `coalesce(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")})`
1373
1768
  case "in":
1769
+ if (ast.values.length < 2) {
1770
+ throw new Error("in(...) requires at least one candidate value")
1771
+ }
1374
1772
  return `(${renderExpression(ast.values[0]!, state, dialect)} in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1375
1773
  case "notIn":
1774
+ if (ast.values.length < 2) {
1775
+ throw new Error("notIn(...) requires at least one candidate value")
1776
+ }
1376
1777
  return `(${renderExpression(ast.values[0]!, state, dialect)} not in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1377
1778
  case "between":
1378
1779
  return `(${renderExpression(ast.values[0]!, state, dialect)} between ${renderExpression(ast.values[1]!, state, dialect)} and ${renderExpression(ast.values[2]!, state, dialect)})`
@@ -1383,35 +1784,15 @@ export const renderExpression = (
1383
1784
  `when ${renderExpression(branch.when, state, dialect)} then ${renderExpression(branch.then, state, dialect)}`
1384
1785
  ).join(" ")} else ${renderExpression(ast.else, state, dialect)} end`
1385
1786
  case "exists":
1386
- return `exists (${renderQueryAst(
1387
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1388
- state,
1389
- dialect
1390
- ).sql})`
1787
+ return `exists (${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
1391
1788
  case "scalarSubquery":
1392
- return `(${renderQueryAst(
1393
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1394
- state,
1395
- dialect
1396
- ).sql})`
1789
+ return `(${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
1397
1790
  case "inSubquery":
1398
- return `(${renderExpression(ast.left, state, dialect)} in (${renderQueryAst(
1399
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1400
- state,
1401
- dialect
1402
- ).sql}))`
1791
+ return `(${renderExpression(ast.left, state, dialect)} in (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1403
1792
  case "comparisonAny":
1404
- return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderQueryAst(
1405
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1406
- state,
1407
- dialect
1408
- ).sql}))`
1793
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1409
1794
  case "comparisonAll":
1410
- return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderQueryAst(
1411
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1412
- state,
1413
- dialect
1414
- ).sql}))`
1795
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1415
1796
  case "window": {
1416
1797
  if (!Array.isArray(ast.partitionBy) || !Array.isArray(ast.orderBy) || typeof ast.function !== "string") {
1417
1798
  break