effect-qb 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/mysql.js +1661 -591
  2. package/dist/postgres/metadata.js +1930 -135
  3. package/dist/postgres.js +7808 -6718
  4. package/dist/sqlite.js +8360 -0
  5. package/package.json +6 -1
  6. package/src/internal/derived-table.ts +29 -3
  7. package/src/internal/dialect.ts +2 -0
  8. package/src/internal/dsl-mutation-runtime.ts +173 -4
  9. package/src/internal/dsl-plan-runtime.ts +165 -20
  10. package/src/internal/dsl-query-runtime.ts +60 -6
  11. package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
  12. package/src/internal/executor.ts +47 -9
  13. package/src/internal/expression-ast.ts +3 -2
  14. package/src/internal/grouping-key.ts +141 -1
  15. package/src/internal/implication-runtime.ts +2 -1
  16. package/src/internal/json/types.ts +155 -40
  17. package/src/internal/predicate/context.ts +14 -1
  18. package/src/internal/predicate/key.ts +19 -2
  19. package/src/internal/predicate/runtime.ts +27 -3
  20. package/src/internal/query.ts +252 -30
  21. package/src/internal/renderer.ts +35 -2
  22. package/src/internal/runtime/driver-value-mapping.ts +58 -0
  23. package/src/internal/runtime/normalize.ts +62 -38
  24. package/src/internal/runtime/schema.ts +5 -3
  25. package/src/internal/runtime/value.ts +153 -30
  26. package/src/internal/table-options.ts +108 -1
  27. package/src/internal/table.ts +87 -29
  28. package/src/mysql/column.ts +18 -2
  29. package/src/mysql/datatypes/index.ts +21 -0
  30. package/src/mysql/errors/catalog.ts +5 -5
  31. package/src/mysql/errors/normalize.ts +2 -2
  32. package/src/mysql/internal/dsl.ts +736 -218
  33. package/src/mysql/internal/renderer.ts +2 -1
  34. package/src/mysql/internal/sql-expression-renderer.ts +486 -130
  35. package/src/mysql/query.ts +9 -2
  36. package/src/mysql/table.ts +38 -12
  37. package/src/postgres/column.ts +4 -2
  38. package/src/postgres/errors/normalize.ts +2 -2
  39. package/src/postgres/executor.ts +48 -5
  40. package/src/postgres/function/core.ts +19 -1
  41. package/src/postgres/internal/dsl.ts +683 -240
  42. package/src/postgres/internal/renderer.ts +2 -1
  43. package/src/postgres/internal/schema-ddl.ts +2 -1
  44. package/src/postgres/internal/schema-model.ts +6 -3
  45. package/src/postgres/internal/sql-expression-renderer.ts +420 -91
  46. package/src/postgres/json.ts +57 -17
  47. package/src/postgres/query.ts +9 -2
  48. package/src/postgres/schema-management.ts +91 -4
  49. package/src/postgres/schema.ts +1 -1
  50. package/src/postgres/table.ts +189 -53
  51. package/src/sqlite/column.ts +128 -0
  52. package/src/sqlite/datatypes/index.ts +79 -0
  53. package/src/sqlite/datatypes/spec.ts +98 -0
  54. package/src/sqlite/errors/catalog.ts +103 -0
  55. package/src/sqlite/errors/fields.ts +19 -0
  56. package/src/sqlite/errors/index.ts +19 -0
  57. package/src/sqlite/errors/normalize.ts +229 -0
  58. package/src/sqlite/errors/requirements.ts +71 -0
  59. package/src/sqlite/errors/types.ts +29 -0
  60. package/src/sqlite/executor.ts +227 -0
  61. package/src/sqlite/function/aggregate.ts +2 -0
  62. package/src/sqlite/function/core.ts +2 -0
  63. package/src/sqlite/function/index.ts +19 -0
  64. package/src/sqlite/function/string.ts +2 -0
  65. package/src/sqlite/function/temporal.ts +100 -0
  66. package/src/sqlite/function/window.ts +2 -0
  67. package/src/sqlite/internal/dialect.ts +37 -0
  68. package/src/sqlite/internal/dsl.ts +6926 -0
  69. package/src/sqlite/internal/renderer.ts +47 -0
  70. package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
  71. package/src/sqlite/json.ts +2 -0
  72. package/src/sqlite/query.ts +196 -0
  73. package/src/sqlite/renderer.ts +24 -0
  74. package/src/sqlite/table.ts +183 -0
  75. package/src/sqlite.ts +22 -0
@@ -1,19 +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"
8
13
  import {
9
14
  renderJsonSelectSql,
10
15
  renderSelectSql,
11
16
  toDriverValue
12
17
  } from "../../internal/runtime/driver-value-mapping.js"
18
+ import { normalizeDbValue } from "../../internal/runtime/normalize.js"
13
19
  import { flattenSelection, type Projection } from "../../internal/projections.js"
14
20
  import { type SelectionValue, validateAggregationSelection } from "../../internal/aggregation-validation.js"
15
21
  import * as SchemaExpression from "../../internal/schema-expression.js"
16
- import type { DdlExpressionLike } from "../../internal/table-options.js"
22
+ import { renderReferentialAction, type DdlExpressionLike } from "../../internal/table-options.js"
17
23
 
18
24
  const renderDbType = (
19
25
  dialect: SqlDialect,
@@ -51,14 +57,59 @@ const renderCastType = (
51
57
  }
52
58
  }
53
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
+
54
100
  const renderDdlExpression = (
55
101
  expression: DdlExpressionLike,
56
102
  state: RenderState,
57
103
  dialect: SqlDialect
58
- ): string =>
59
- SchemaExpression.isSchemaExpression(expression)
60
- ? SchemaExpression.render(expression)
61
- : 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
+ }
62
113
 
63
114
  const renderColumnDefinition = (
64
115
  dialect: SqlDialect,
@@ -105,17 +156,19 @@ const renderCreateTableSql = (
105
156
  case "foreignKey": {
106
157
  const reference = option.references()
107
158
  definitions.push(
108
- `${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" : ""}` : ""}`
109
160
  )
110
161
  break
111
162
  }
112
163
  case "check":
113
164
  definitions.push(
114
- `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" : ""}`
115
166
  )
116
167
  break
117
168
  case "index":
118
169
  break
170
+ default:
171
+ throw new Error("Unsupported table option kind")
119
172
  }
120
173
  }
121
174
  return `create table${ifNotExists ? " if not exists" : ""} ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${definitions.join(", ")})`
@@ -136,20 +189,74 @@ const renderDropIndexSql = (
136
189
  ddl: Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
137
190
  state: RenderState,
138
191
  dialect: SqlDialect
139
- ): string =>
140
- dialect.name === "postgres"
141
- ? `drop index${ddl.ifExists ? " if exists" : ""} ${dialect.quoteIdentifier(ddl.name)}`
142
- : `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
+ }
143
206
 
144
207
  const isExpression = (value: unknown): value is Expression.Any =>
145
208
  value !== null && typeof value === "object" && Expression.TypeId in value
146
209
 
147
- const isJsonDbType = (dbType: Expression.DbType.Any): boolean =>
148
- 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
+ }
149
220
 
150
221
  const isJsonExpression = (value: unknown): value is Expression.Any =>
151
222
  isExpression(value) && isJsonDbType(value[Expression.TypeId].dbType)
152
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
+
153
260
  const unsupportedJsonFeature = (
154
261
  dialect: SqlDialect,
155
262
  feature: string
@@ -207,19 +314,19 @@ const extractJsonValue = (node: Record<string, unknown>): unknown =>
207
314
  node.newValue ?? node.insert ?? node.right
208
315
 
209
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)}`
210
321
  if (typeof segment === "string") {
211
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)
212
- ? `.${segment}`
213
- : `."${segment.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
322
+ return renderKey(segment)
214
323
  }
215
324
  if (typeof segment === "number") {
216
325
  return `[${segment}]`
217
326
  }
218
327
  switch (segment.kind) {
219
328
  case "key":
220
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment.key)
221
- ? `.${segment.key}`
222
- : `."${segment.key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
329
+ return renderKey(segment.key)
223
330
  case "index":
224
331
  return `[${segment.index}]`
225
332
  case "wildcard":
@@ -284,7 +391,7 @@ const renderPostgresJsonAccessStep = (
284
391
  case "key":
285
392
  return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(segment.key, state)}`
286
393
  case "index":
287
- return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(String(segment.index), state)}`
394
+ return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(segment.index, state)}`
288
395
  default:
289
396
  throw new Error("Postgres exact JSON access requires key/index segments")
290
397
  }
@@ -338,14 +445,26 @@ const encodeArrayValues = (
338
445
  state: RenderState,
339
446
  dialect: SqlDialect
340
447
  ): readonly unknown[] =>
341
- values.map((value) =>
342
- toDriverValue(value, {
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, {
343
461
  dialect: dialect.name,
344
462
  valueMappings: state.valueMappings,
345
463
  dbType: column.metadata.dbType,
346
464
  runtimeSchema: column.schema,
347
465
  driverValueMapping: column.metadata.driverValueMapping
348
- }))
466
+ })
467
+ })
349
468
 
350
469
  const renderPostgresJsonKind = (
351
470
  value: Expression.Any
@@ -588,7 +707,7 @@ const renderJsonExpression = (
588
707
  const baseSql = renderExpression(base, state, dialect)
589
708
  const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
590
709
  const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
591
- 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)`
592
711
  }
593
712
  if (dialect.name === "mysql") {
594
713
  return `json_keys(${renderExpression(base, state, dialect)})`
@@ -615,7 +734,7 @@ const renderJsonExpression = (
615
734
  const segment = segments[0]!
616
735
  return `(${baseSql} - ${segment.kind === "key"
617
736
  ? dialect.renderLiteral(segment.key, state)
618
- : dialect.renderLiteral(String(segment.index), state)})`
737
+ : dialect.renderLiteral(segment.index, state)})`
619
738
  }
620
739
  return `(${baseSql} #- ${renderPostgresJsonPathArray(segments, state, dialect)})`
621
740
  }
@@ -739,6 +858,15 @@ const renderDeleteTargets = (
739
858
  dialect: SqlDialect
740
859
  ): string => targets.map((target) => dialect.quoteIdentifier(target.tableName)).join(", ")
741
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
+
742
870
  const renderMysqlMutationLock = (
743
871
  lock: QueryAst.LockClause | undefined,
744
872
  statement: "update" | "delete"
@@ -765,8 +893,9 @@ const renderTransactionClause = (
765
893
  switch (clause.kind) {
766
894
  case "transaction": {
767
895
  const modes: string[] = []
768
- if (clause.isolationLevel) {
769
- modes.push(`isolation level ${clause.isolationLevel}`)
896
+ const isolationLevel = renderTransactionIsolationLevel(clause.isolationLevel)
897
+ if (isolationLevel) {
898
+ modes.push(isolationLevel)
770
899
  }
771
900
  if (clause.readOnly === true) {
772
901
  modes.push("read only")
@@ -786,7 +915,7 @@ const renderTransactionClause = (
786
915
  case "releaseSavepoint":
787
916
  return `release savepoint ${dialect.quoteIdentifier(clause.name)}`
788
917
  }
789
- return ""
918
+ throw new Error("Unsupported transaction statement kind")
790
919
  }
791
920
 
792
921
  const renderSelectionList = (
@@ -799,6 +928,9 @@ const renderSelectionList = (
799
928
  validateAggregationSelection(selection as SelectionValue, [])
800
929
  }
801
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
+ }
802
934
  const projections = selectionProjections(selection)
803
935
  const sql = flattened.map(({ expression, alias }) =>
804
936
  `${renderSelectSql(renderExpression(expression, state, dialect), expressionDriverContext(expression, state, dialect))} as ${dialect.quoteIdentifier(alias)}`).join(", ")
@@ -808,10 +940,105 @@ const renderSelectionList = (
808
940
  }
809
941
  }
810
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
+
811
1037
  export const renderQueryAst = (
812
1038
  ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
813
1039
  state: RenderState,
814
- dialect: SqlDialect
1040
+ dialect: SqlDialect,
1041
+ options: { readonly emitCtes?: boolean } = {}
815
1042
  ): RenderedQueryAst => {
816
1043
  let sql = ""
817
1044
  let projections: readonly Projection[] = []
@@ -821,10 +1048,11 @@ export const renderQueryAst = (
821
1048
  validateAggregationSelection(ast.select as SelectionValue, ast.groupBy)
822
1049
  const rendered = renderSelectionList(ast.select as Record<string, unknown>, state, dialect, false)
823
1050
  projections = rendered.projections
1051
+ const selectList = rendered.sql.length > 0 ? ` ${rendered.sql}` : ""
824
1052
  const clauses = [
825
1053
  ast.distinctOn && ast.distinctOn.length > 0
826
- ? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")}) ${rendered.sql}`
827
- : `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}`
828
1056
  ]
829
1057
  if (ast.from) {
830
1058
  clauses.push(`from ${renderSourceReference(ast.from.source, ast.from.tableName, ast.from.baseTableName, state, dialect)}`)
@@ -856,8 +1084,11 @@ export const renderQueryAst = (
856
1084
  clauses.push(`offset ${renderExpression(ast.offset, state, dialect)}`)
857
1085
  }
858
1086
  if (ast.lock) {
1087
+ if (ast.lock.nowait && ast.lock.skipLocked) {
1088
+ throw new Error("lock(...) cannot specify both nowait and skipLocked")
1089
+ }
859
1090
  clauses.push(
860
- `${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" : ""}`
861
1092
  )
862
1093
  }
863
1094
  sql = clauses.join(" ")
@@ -865,6 +1096,7 @@ export const renderQueryAst = (
865
1096
  }
866
1097
  case "set": {
867
1098
  const setAst = ast as QueryAst.Ast<Record<string, unknown>, any, "set">
1099
+ assertNoStatementQueryClauses(setAst, "set", { allowSelection: true })
868
1100
  const base = renderQueryAst(
869
1101
  Query.getAst(setAst.setBase as Query.Plan.Any) as QueryAst.Ast<
870
1102
  Record<string, unknown>,
@@ -875,6 +1107,7 @@ export const renderQueryAst = (
875
1107
  dialect
876
1108
  )
877
1109
  projections = selectionProjections(setAst.select as Record<string, unknown>)
1110
+ assertMatchingSetProjections(projections, base.projections)
878
1111
  sql = [
879
1112
  `(${base.sql})`,
880
1113
  ...(setAst.setOperations ?? []).map((entry) => {
@@ -887,6 +1120,7 @@ export const renderQueryAst = (
887
1120
  state,
888
1121
  dialect
889
1122
  )
1123
+ assertMatchingSetProjections(projections, rendered.projections)
890
1124
  return `${entry.kind}${entry.all ? " all" : ""} (${rendered.sql})`
891
1125
  })
892
1126
  ].join(" ")
@@ -894,19 +1128,26 @@ export const renderQueryAst = (
894
1128
  }
895
1129
  case "insert": {
896
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)
897
1136
  const targetSource = insertAst.into!
898
1137
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1138
+ const insertSource = expectInsertSourceKind(insertAst.insertSource)
1139
+ const conflict = expectConflictClause(insertAst.conflict)
899
1140
  sql = `insert into ${target}`
900
- if (insertAst.insertSource?.kind === "values") {
901
- const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
902
- 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) =>
903
1144
  `(${row.values.map((entry) => renderExpression(entry.value, state, dialect)).join(", ")})`
904
1145
  ).join(", ")
905
1146
  sql += ` (${columns}) values ${rows}`
906
- } else if (insertAst.insertSource?.kind === "query") {
907
- 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(", ")
908
1149
  const renderedQuery = renderQueryAst(
909
- Query.getAst(insertAst.insertSource.query as Query.Plan.Any) as QueryAst.Ast<
1150
+ Query.getAst(insertSource.query as Query.Plan.Any) as QueryAst.Ast<
910
1151
  Record<string, unknown>,
911
1152
  any,
912
1153
  QueryAst.QueryStatement
@@ -915,20 +1156,19 @@ export const renderQueryAst = (
915
1156
  dialect
916
1157
  )
917
1158
  sql += ` (${columns}) ${renderedQuery.sql}`
918
- } else if (insertAst.insertSource?.kind === "unnest") {
919
- const unnestSource = insertAst.insertSource
920
- 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(", ")
921
1161
  if (dialect.name === "postgres") {
922
1162
  const table = targetSource.source as Table.AnyTable
923
1163
  const fields = table[Table.TypeId].fields
924
- const rendered = unnestSource.values.map((entry) =>
1164
+ const rendered = insertSource.values.map((entry) =>
925
1165
  `cast(${dialect.renderLiteral(encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect), state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
926
1166
  ).join(", ")
927
1167
  sql += ` (${columns}) select * from unnest(${rendered})`
928
1168
  } else {
929
- const rowCount = unnestSource.values[0]?.values.length ?? 0
1169
+ const rowCount = insertSource.values[0]?.values.length ?? 0
930
1170
  const rows = Array.from({ length: rowCount }, (_, index) =>
931
- `(${unnestSource.values.map((entry) =>
1171
+ `(${insertSource.values.map((entry) =>
932
1172
  dialect.renderLiteral(
933
1173
  entry.values[index],
934
1174
  state,
@@ -947,21 +1187,24 @@ export const renderQueryAst = (
947
1187
  sql += " default values"
948
1188
  }
949
1189
  }
950
- if (insertAst.conflict) {
951
- 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) =>
952
1195
  `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
953
1196
  ).join(", ")
954
1197
  if (dialect.name === "postgres") {
955
- const targetSql = insertAst.conflict.target?.kind === "constraint"
956
- ? ` on conflict on constraint ${dialect.quoteIdentifier(insertAst.conflict.target.name)}`
957
- : insertAst.conflict.target?.kind === "columns"
958
- ? ` 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)}` : ""}`
959
1202
  : " on conflict"
960
1203
  sql += targetSql
961
- sql += insertAst.conflict.action === "doNothing"
1204
+ sql += conflict.action === "doNothing"
962
1205
  ? " do nothing"
963
- : ` do update set ${updateValues}${insertAst.conflict.where ? ` where ${renderExpression(insertAst.conflict.where, state, dialect)}` : ""}`
964
- } 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") {
965
1208
  sql = sql.replace(/^insert/, "insert ignore")
966
1209
  } else {
967
1210
  sql += ` on duplicate key update ${updateValues}`
@@ -976,10 +1219,29 @@ export const renderQueryAst = (
976
1219
  }
977
1220
  case "update": {
978
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
+ }
979
1238
  const targetSource = updateAst.target!
980
1239
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
981
1240
  const targets = updateAst.targets ?? [targetSource]
982
1241
  const fromSources = updateAst.fromSources ?? []
1242
+ if ((updateAst.set ?? []).length === 0) {
1243
+ throw new Error("update statements require at least one assignment")
1244
+ }
983
1245
  const assignments = updateAst.set!.map((entry) =>
984
1246
  renderMutationAssignment(entry, state, dialect)).join(", ")
985
1247
  if (dialect.name === "mysql") {
@@ -1029,6 +1291,22 @@ export const renderQueryAst = (
1029
1291
  }
1030
1292
  case "delete": {
1031
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
+ }
1032
1310
  const targetSource = deleteAst.target!
1033
1311
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1034
1312
  const targets = deleteAst.targets ?? [targetSource]
@@ -1075,12 +1353,14 @@ export const renderQueryAst = (
1075
1353
  }
1076
1354
  case "truncate": {
1077
1355
  const truncateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "truncate">
1356
+ assertNoStatementQueryClauses(truncateAst, "truncate")
1357
+ const truncate = expectTruncateClause(truncateAst.truncate)
1078
1358
  const targetSource = truncateAst.target!
1079
1359
  sql = `truncate table ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
1080
- if (truncateAst.truncate?.restartIdentity) {
1360
+ if (truncate.restartIdentity) {
1081
1361
  sql += " restart identity"
1082
1362
  }
1083
- if (truncateAst.truncate?.cascade) {
1363
+ if (truncate.cascade) {
1084
1364
  sql += " cascade"
1085
1365
  }
1086
1366
  break
@@ -1093,8 +1373,18 @@ export const renderQueryAst = (
1093
1373
  const targetSource = mergeAst.target!
1094
1374
  const usingSource = mergeAst.using!
1095
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
+ }
1096
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)}`
1097
1386
  if (merge.whenMatched) {
1387
+ assertMergeActionKind(merge.whenMatched.kind, ["update", "delete"])
1098
1388
  sql += " when matched"
1099
1389
  if (merge.whenMatched.predicate) {
1100
1390
  sql += ` and ${renderExpression(merge.whenMatched.predicate, state, dialect)}`
@@ -1102,16 +1392,23 @@ export const renderQueryAst = (
1102
1392
  if (merge.whenMatched.kind === "delete") {
1103
1393
  sql += " then delete"
1104
1394
  } else {
1395
+ if (merge.whenMatched.values.length === 0) {
1396
+ throw new Error("merge update actions require at least one assignment")
1397
+ }
1105
1398
  sql += ` then update set ${merge.whenMatched.values.map((entry) =>
1106
1399
  `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
1107
1400
  ).join(", ")}`
1108
1401
  }
1109
1402
  }
1110
1403
  if (merge.whenNotMatched) {
1404
+ assertMergeActionKind(merge.whenNotMatched.kind, ["insert"])
1111
1405
  sql += " when not matched"
1112
1406
  if (merge.whenNotMatched.predicate) {
1113
1407
  sql += ` and ${renderExpression(merge.whenNotMatched.predicate, state, dialect)}`
1114
1408
  }
1409
+ if (merge.whenNotMatched.values.length === 0) {
1410
+ throw new Error("merge insert actions require at least one value")
1411
+ }
1115
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(", ")})`
1116
1413
  }
1117
1414
  break
@@ -1122,25 +1419,30 @@ export const renderQueryAst = (
1122
1419
  case "savepoint":
1123
1420
  case "rollbackTo":
1124
1421
  case "releaseSavepoint": {
1422
+ assertNoStatementQueryClauses(ast, ast.kind)
1125
1423
  sql = renderTransactionClause(ast.transaction!, dialect)
1126
1424
  break
1127
1425
  }
1128
1426
  case "createTable": {
1129
1427
  const createTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createTable">
1130
- 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)
1131
1431
  break
1132
1432
  }
1133
1433
  case "dropTable": {
1134
1434
  const dropTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropTable">
1135
- const ifExists = dropTableAst.ddl?.kind === "dropTable" && dropTableAst.ddl.ifExists
1136
- 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)}`
1137
1438
  break
1138
1439
  }
1139
1440
  case "createIndex": {
1140
1441
  const createIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createIndex">
1442
+ assertNoStatementQueryClauses(createIndexAst, "createIndex")
1141
1443
  sql = renderCreateIndexSql(
1142
1444
  createIndexAst.target!,
1143
- createIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
1445
+ expectDdlClauseKind(createIndexAst.ddl, "createIndex"),
1144
1446
  state,
1145
1447
  dialect
1146
1448
  )
@@ -1148,17 +1450,20 @@ export const renderQueryAst = (
1148
1450
  }
1149
1451
  case "dropIndex": {
1150
1452
  const dropIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropIndex">
1453
+ assertNoStatementQueryClauses(dropIndexAst, "dropIndex")
1151
1454
  sql = renderDropIndexSql(
1152
1455
  dropIndexAst.target!,
1153
- dropIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
1456
+ expectDdlClauseKind(dropIndexAst.ddl, "dropIndex"),
1154
1457
  state,
1155
1458
  dialect
1156
1459
  )
1157
1460
  break
1158
1461
  }
1462
+ default:
1463
+ throw new Error("Unsupported query statement kind")
1159
1464
  }
1160
1465
 
1161
- if (state.ctes.length === 0) {
1466
+ if (state.ctes.length === 0 || options.emitCtes === false) {
1162
1467
  return {
1163
1468
  sql,
1164
1469
  projections
@@ -1206,9 +1511,19 @@ const renderSourceReference = (
1206
1511
  readonly plan: Query.Plan.Any
1207
1512
  readonly recursive?: boolean
1208
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
+ }
1209
1518
  if (!state.cteNames.has(cte.name)) {
1210
1519
  state.cteNames.add(cte.name)
1211
- 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
+ )
1212
1527
  state.ctes.push({
1213
1528
  name: cte.name,
1214
1529
  sql: rendered.sql,
@@ -1225,14 +1540,14 @@ const renderSourceReference = (
1225
1540
  if (!state.cteNames.has(derived.name)) {
1226
1541
  // derived tables are inlined, so no CTE registration is needed
1227
1542
  }
1228
- 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)}`
1229
1544
  }
1230
1545
  if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "lateral") {
1231
1546
  const lateral = source as unknown as {
1232
1547
  readonly name: string
1233
1548
  readonly plan: Query.Plan.Any
1234
1549
  }
1235
- 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)}`
1236
1551
  }
1237
1552
  if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "values") {
1238
1553
  const values = source as unknown as {
@@ -1267,6 +1582,22 @@ const renderSourceReference = (
1267
1582
  return dialect.renderTableReference(tableName, baseTableName, schemaName)
1268
1583
  }
1269
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
+
1270
1601
  /**
1271
1602
  * Renders a scalar expression AST into SQL text.
1272
1603
  *
@@ -1301,10 +1632,13 @@ export const renderExpression = (
1301
1632
  : ">="
1302
1633
  switch (ast.kind) {
1303
1634
  case "column":
1304
- return ast.tableName.length === 0
1635
+ return state.rowLocalColumns || ast.tableName.length === 0
1305
1636
  ? dialect.quoteIdentifier(ast.columnName)
1306
1637
  : `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
1307
1638
  case "literal":
1639
+ if (typeof ast.value === "number" && !Number.isFinite(ast.value)) {
1640
+ throw new Error("Expected a finite numeric value")
1641
+ }
1308
1642
  return dialect.renderLiteral(ast.value, state, expression[Expression.TypeId])
1309
1643
  case "excluded":
1310
1644
  return dialect.name === "mysql"
@@ -1360,6 +1694,7 @@ export const renderExpression = (
1360
1694
  : `(${renderExpression(ast.left, state, dialect)} is not distinct from ${renderExpression(ast.right, state, dialect)})`
1361
1695
  case "contains":
1362
1696
  if (dialect.name === "postgres") {
1697
+ assertCompatiblePostgresRangeOperands(ast.left, ast.right)
1363
1698
  const left = isJsonExpression(ast.left)
1364
1699
  ? renderPostgresJsonValue(ast.left, state, dialect)
1365
1700
  : renderExpression(ast.left, state, dialect)
@@ -1374,6 +1709,7 @@ export const renderExpression = (
1374
1709
  throw new Error("Unsupported container operator for SQL rendering")
1375
1710
  case "containedBy":
1376
1711
  if (dialect.name === "postgres") {
1712
+ assertCompatiblePostgresRangeOperands(ast.left, ast.right)
1377
1713
  const left = isJsonExpression(ast.left)
1378
1714
  ? renderPostgresJsonValue(ast.left, state, dialect)
1379
1715
  : renderExpression(ast.left, state, dialect)
@@ -1388,6 +1724,7 @@ export const renderExpression = (
1388
1724
  throw new Error("Unsupported container operator for SQL rendering")
1389
1725
  case "overlaps":
1390
1726
  if (dialect.name === "postgres") {
1727
+ assertCompatiblePostgresRangeOperands(ast.left, ast.right)
1391
1728
  const left = isJsonExpression(ast.left)
1392
1729
  ? renderPostgresJsonValue(ast.left, state, dialect)
1393
1730
  : renderExpression(ast.left, state, dialect)
@@ -1417,14 +1754,26 @@ export const renderExpression = (
1417
1754
  case "min":
1418
1755
  return `min(${renderExpression(ast.value, state, dialect)})`
1419
1756
  case "and":
1757
+ if (ast.values.length === 0) {
1758
+ throw new Error("and(...) requires at least one predicate")
1759
+ }
1420
1760
  return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" and ")})`
1421
1761
  case "or":
1762
+ if (ast.values.length === 0) {
1763
+ throw new Error("or(...) requires at least one predicate")
1764
+ }
1422
1765
  return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" or ")})`
1423
1766
  case "coalesce":
1424
1767
  return `coalesce(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")})`
1425
1768
  case "in":
1769
+ if (ast.values.length < 2) {
1770
+ throw new Error("in(...) requires at least one candidate value")
1771
+ }
1426
1772
  return `(${renderExpression(ast.values[0]!, state, dialect)} in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1427
1773
  case "notIn":
1774
+ if (ast.values.length < 2) {
1775
+ throw new Error("notIn(...) requires at least one candidate value")
1776
+ }
1428
1777
  return `(${renderExpression(ast.values[0]!, state, dialect)} not in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1429
1778
  case "between":
1430
1779
  return `(${renderExpression(ast.values[0]!, state, dialect)} between ${renderExpression(ast.values[1]!, state, dialect)} and ${renderExpression(ast.values[2]!, state, dialect)})`
@@ -1435,35 +1784,15 @@ export const renderExpression = (
1435
1784
  `when ${renderExpression(branch.when, state, dialect)} then ${renderExpression(branch.then, state, dialect)}`
1436
1785
  ).join(" ")} else ${renderExpression(ast.else, state, dialect)} end`
1437
1786
  case "exists":
1438
- return `exists (${renderQueryAst(
1439
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1440
- state,
1441
- dialect
1442
- ).sql})`
1787
+ return `exists (${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
1443
1788
  case "scalarSubquery":
1444
- return `(${renderQueryAst(
1445
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1446
- state,
1447
- dialect
1448
- ).sql})`
1789
+ return `(${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
1449
1790
  case "inSubquery":
1450
- return `(${renderExpression(ast.left, state, dialect)} in (${renderQueryAst(
1451
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1452
- state,
1453
- dialect
1454
- ).sql}))`
1791
+ return `(${renderExpression(ast.left, state, dialect)} in (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1455
1792
  case "comparisonAny":
1456
- return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderQueryAst(
1457
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1458
- state,
1459
- dialect
1460
- ).sql}))`
1793
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1461
1794
  case "comparisonAll":
1462
- return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderQueryAst(
1463
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1464
- state,
1465
- dialect
1466
- ).sql}))`
1795
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1467
1796
  case "window": {
1468
1797
  if (!Array.isArray(ast.partitionBy) || !Array.isArray(ast.orderBy) || typeof ast.function !== "string") {
1469
1798
  break