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 { renderMysqlMutationLockMode, 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 renderMysqlDdlString = (value: string): string =>
61
+ `'${value.replaceAll("'", "''")}'`
62
+
63
+ const renderMysqlDdlBytes = (value: Uint8Array): string =>
64
+ `x'${Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("")}'`
65
+
66
+ const renderMysqlDdlLiteral = (
67
+ value: unknown,
68
+ state: RenderState,
69
+ context: RenderValueContext = {}
70
+ ): string => {
71
+ const driverValue = toDriverValue(value, {
72
+ dialect: "mysql",
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 renderMysqlDdlString(driverValue)
91
+ case "object":
92
+ if (driverValue instanceof Uint8Array) {
93
+ return renderMysqlDdlBytes(driverValue)
94
+ }
95
+ break
96
+ }
97
+ throw new Error("Unsupported mysql 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: renderMysqlDdlLiteral
111
+ })
112
+ }
57
113
 
58
114
  const renderMysqlMutationLimit = (
59
115
  expression: Expression.Any,
@@ -109,22 +165,27 @@ const renderCreateTableSql = (
109
165
  definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}primary key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
110
166
  break
111
167
  case "unique":
168
+ if (option.nullsNotDistinct || option.deferrable || option.initiallyDeferred) {
169
+ throw new Error("Unsupported mysql unique constraint options")
170
+ }
112
171
  definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}unique${option.nullsNotDistinct ? " nulls not distinct" : ""} (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
113
172
  break
114
173
  case "foreignKey": {
115
174
  const reference = option.references()
116
175
  definitions.push(
117
- `${option.name ? `constraint ${dialect.quoteIdentifier(option.name)} ` : ""}foreign key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")}) references ${dialect.renderTableReference(reference.tableName, reference.tableName, reference.schemaName)} (${reference.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${option.onDelete ? ` on delete ${option.onDelete.replace(/[A-Z]/g, (value) => ` ${value.toLowerCase()}`).trim()}` : ""}${option.onUpdate ? ` on update ${option.onUpdate.replace(/[A-Z]/g, (value) => ` ${value.toLowerCase()}`).trim()}` : ""}${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`
176
+ `${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" : ""}` : ""}`
118
177
  )
119
178
  break
120
179
  }
121
180
  case "check":
122
181
  definitions.push(
123
- `constraint ${dialect.quoteIdentifier(option.name)} check (${renderDdlExpression(option.predicate, state, dialect)})${option.noInherit ? " no inherit" : ""}`
182
+ `constraint ${dialect.quoteIdentifier(option.name)} check (${renderDdlExpression(option.predicate, { ...state, rowLocalColumns: true }, dialect)})${option.noInherit ? " no inherit" : ""}`
124
183
  )
125
184
  break
126
185
  case "index":
127
186
  break
187
+ default:
188
+ throw new Error("Unsupported table option kind")
128
189
  }
129
190
  }
130
191
  return `create table${ifNotExists ? " if not exists" : ""} ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${definitions.join(", ")})`
@@ -136,6 +197,9 @@ const renderCreateIndexSql = (
136
197
  state: RenderState,
137
198
  dialect: SqlDialect
138
199
  ): string => {
200
+ if (ddl.ifNotExists) {
201
+ throw new Error("Unsupported mysql create index options")
202
+ }
139
203
  const maybeIfNotExists = dialect.name === "postgres" && ddl.ifNotExists ? " if not exists" : ""
140
204
  return `create${ddl.unique ? " unique" : ""} index${maybeIfNotExists} ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${ddl.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`
141
205
  }
@@ -145,16 +209,28 @@ const renderDropIndexSql = (
145
209
  ddl: Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
146
210
  state: RenderState,
147
211
  dialect: SqlDialect
148
- ): string =>
149
- dialect.name === "postgres"
212
+ ): string => {
213
+ if (ddl.ifExists) {
214
+ throw new Error("Unsupported mysql drop index options")
215
+ }
216
+ return dialect.name === "postgres"
150
217
  ? `drop index${ddl.ifExists ? " if exists" : ""} ${dialect.quoteIdentifier(ddl.name)}`
151
218
  : `drop index ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
219
+ }
152
220
 
153
221
  const isExpression = (value: unknown): value is Expression.Any =>
154
222
  value !== null && typeof value === "object" && Expression.TypeId in value
155
223
 
156
- const isJsonDbType = (dbType: Expression.DbType.Any): boolean =>
157
- dbType.kind === "jsonb" || dbType.kind === "json" || ("variant" in dbType && dbType.variant === "json")
224
+ const isJsonDbType = (dbType: Expression.DbType.Any): boolean => {
225
+ if (dbType.kind === "jsonb" || dbType.kind === "json") {
226
+ return true
227
+ }
228
+ if (!("variant" in dbType)) {
229
+ return false
230
+ }
231
+ const variant = dbType.variant as string
232
+ return variant === "json" || variant === "jsonb"
233
+ }
158
234
 
159
235
  const isJsonExpression = (value: unknown): value is Expression.Any =>
160
236
  isExpression(value) && isJsonDbType(value[Expression.TypeId].dbType)
@@ -216,19 +292,19 @@ const extractJsonValue = (node: Record<string, unknown>): unknown =>
216
292
  node.newValue ?? node.insert ?? node.right
217
293
 
218
294
  const renderJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
295
+ const renderKey = (value: string): string =>
296
+ /^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
297
+ ? `.${value}`
298
+ : `.${JSON.stringify(value)}`
219
299
  if (typeof segment === "string") {
220
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)
221
- ? `.${segment}`
222
- : `."${segment.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
300
+ return renderKey(segment)
223
301
  }
224
302
  if (typeof segment === "number") {
225
303
  return `[${segment}]`
226
304
  }
227
305
  switch (segment.kind) {
228
306
  case "key":
229
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment.key)
230
- ? `.${segment.key}`
231
- : `."${segment.key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
307
+ return renderKey(segment.key)
232
308
  case "index":
233
309
  return `[${segment.index}]`
234
310
  case "wildcard":
@@ -242,10 +318,32 @@ const renderJsonPathSegment = (segment: JsonPath.AnySegment | string | number):
242
318
  }
243
319
  }
244
320
 
245
- const renderJsonPathStringLiteral = (segments: ReadonlyArray<JsonPath.AnySegment | string | number>): string => {
321
+ const renderMySqlJsonIndex = (index: number): string =>
322
+ index >= 0 ? String(index) : index === -1 ? "last" : `last-${Math.abs(index) - 1}`
323
+
324
+ const renderMySqlJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
325
+ if (typeof segment === "number") {
326
+ return `[${renderMySqlJsonIndex(segment)}]`
327
+ }
328
+ if (typeof segment === "object" && segment !== null && segment.kind === "index") {
329
+ return `[${renderMySqlJsonIndex(segment.index)}]`
330
+ }
331
+ if (typeof segment === "object" && segment !== null && segment.kind === "slice") {
332
+ return `[${renderMySqlJsonIndex(segment.start ?? 0)} to ${segment.end === undefined ? "last" : renderMySqlJsonIndex(segment.end)}]`
333
+ }
334
+ if (typeof segment === "object" && segment !== null && segment.kind === "descend") {
335
+ return "**"
336
+ }
337
+ return renderJsonPathSegment(segment)
338
+ }
339
+
340
+ const renderJsonPathStringLiteral = (
341
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
342
+ renderSegment: (segment: JsonPath.AnySegment | string | number) => string = renderJsonPathSegment
343
+ ): string => {
246
344
  let path = "$"
247
345
  for (const segment of segments) {
248
- path += renderJsonPathSegment(segment)
346
+ path += renderSegment(segment)
249
347
  }
250
348
  return path
251
349
  }
@@ -254,7 +352,30 @@ const renderMySqlJsonPath = (
254
352
  segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
255
353
  state: RenderState,
256
354
  dialect: SqlDialect
257
- ): string => dialect.renderLiteral(renderJsonPathStringLiteral(segments), state)
355
+ ): string => dialect.renderLiteral(renderJsonPathStringLiteral(segments, renderMySqlJsonPathSegment), state)
356
+
357
+ const isJsonArrayIndexSegment = (segment: JsonPath.AnySegment | string | number | undefined): boolean =>
358
+ typeof segment === "number" || (typeof segment === "object" && segment !== null && segment.kind === "index")
359
+
360
+ const renderMySqlJsonInsertPath = (
361
+ segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
362
+ insertAfter: boolean,
363
+ state: RenderState,
364
+ dialect: SqlDialect
365
+ ): string => {
366
+ if (!insertAfter || segments.length === 0) {
367
+ return renderMySqlJsonPath(segments, state, dialect)
368
+ }
369
+ const last = segments[segments.length - 1]
370
+ const nextSegments = segments.slice(0, -1)
371
+ if (typeof last === "number") {
372
+ return renderMySqlJsonPath([...nextSegments, last + 1], state, dialect)
373
+ }
374
+ if (typeof last === "object" && last !== null && last.kind === "index") {
375
+ return renderMySqlJsonPath([...nextSegments, { ...last, index: last.index + 1 }], state, dialect)
376
+ }
377
+ return renderMySqlJsonPath(segments, state, dialect)
378
+ }
258
379
 
259
380
  const renderPostgresJsonPathArray = (
260
381
  segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
@@ -313,6 +434,76 @@ const renderPostgresJsonValue = (
313
434
  : `cast(${rendered} as jsonb)`
314
435
  }
315
436
 
437
+ const expressionDriverContext = (
438
+ expression: Expression.Any,
439
+ state: RenderState,
440
+ dialect: SqlDialect
441
+ ) => ({
442
+ dialect: dialect.name,
443
+ valueMappings: state.valueMappings,
444
+ dbType: expression[Expression.TypeId].dbType,
445
+ runtimeSchema: expression[Expression.TypeId].runtimeSchema,
446
+ driverValueMapping: expression[Expression.TypeId].driverValueMapping
447
+ })
448
+
449
+ const renderMySqlStructuredJsonLiteral = (
450
+ expression: Expression.Any,
451
+ state: RenderState
452
+ ): string | undefined => {
453
+ const ast = (expression as Expression.Any & {
454
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
455
+ })[ExpressionAst.TypeId]
456
+ if (ast.kind !== "literal" || ast.value === null || typeof ast.value !== "object") {
457
+ return undefined
458
+ }
459
+ state.params.push(JSON.stringify(ast.value))
460
+ return "cast(? as json)"
461
+ }
462
+
463
+ const renderJsonInputExpression = (
464
+ expression: Expression.Any,
465
+ state: RenderState,
466
+ dialect: SqlDialect
467
+ ): string => {
468
+ if (dialect.name === "mysql") {
469
+ const jsonLiteral = renderMySqlStructuredJsonLiteral(expression, state)
470
+ if (jsonLiteral !== undefined) {
471
+ return jsonLiteral
472
+ }
473
+ }
474
+ return renderJsonSelectSql(
475
+ renderExpression(expression, state, dialect),
476
+ expressionDriverContext(expression, state, dialect)
477
+ )
478
+ }
479
+
480
+ const encodeArrayValues = (
481
+ values: readonly unknown[],
482
+ column: Table.AnyTable[typeof Table.TypeId]["fields"][string],
483
+ state: RenderState,
484
+ dialect: SqlDialect
485
+ ): readonly unknown[] =>
486
+ values.map((value) => {
487
+ if (value === null && column.metadata.nullable) {
488
+ return null
489
+ }
490
+ const runtimeSchemaAccepts = column.schema !== undefined &&
491
+ (Schema.is(column.schema) as (candidate: unknown) => boolean)(value)
492
+ const normalizedValue = runtimeSchemaAccepts
493
+ ? value
494
+ : normalizeDbValue(column.metadata.dbType, value)
495
+ const encodedValue = column.schema === undefined || runtimeSchemaAccepts
496
+ ? normalizedValue
497
+ : (Schema.decodeUnknownSync as any)(column.schema)(normalizedValue)
498
+ return toDriverValue(encodedValue, {
499
+ dialect: dialect.name,
500
+ valueMappings: state.valueMappings,
501
+ dbType: column.metadata.dbType,
502
+ runtimeSchema: column.schema,
503
+ driverValueMapping: column.metadata.driverValueMapping
504
+ })
505
+ })
506
+
316
507
  const renderPostgresJsonKind = (
317
508
  value: Expression.Any
318
509
  ): "json" | "jsonb" => value[Expression.TypeId].dbType.kind === "jsonb" ? "jsonb" : "json"
@@ -323,7 +514,10 @@ const renderJsonOpaquePath = (
323
514
  dialect: SqlDialect
324
515
  ): string => {
325
516
  if (isJsonPathValue(value)) {
326
- return dialect.renderLiteral(renderJsonPathStringLiteral(value.segments), state)
517
+ const renderSegment = dialect.name === "mysql"
518
+ ? renderMySqlJsonPathSegment
519
+ : renderJsonPathSegment
520
+ return dialect.renderLiteral(renderJsonPathStringLiteral(value.segments, renderSegment), state)
327
521
  }
328
522
  if (typeof value === "string") {
329
523
  return dialect.renderLiteral(value, state)
@@ -450,8 +644,9 @@ const renderJsonExpression = (
450
644
  }
451
645
  if (dialect.name === "mysql") {
452
646
  const mode = kind === "jsonHasAllKeys" ? "all" : "one"
647
+ const modeSql = dialect.renderLiteral(mode, state)
453
648
  const paths = keys.map((segment) => renderMySqlJsonPath([segment], state, dialect)).join(", ")
454
- return `json_contains_path(${baseSql}, ${dialect.renderLiteral(mode, state)}, ${paths})`
649
+ return `json_contains_path(${baseSql}, ${modeSql}, ${paths})`
455
650
  }
456
651
  return undefined
457
652
  }
@@ -464,7 +659,7 @@ const renderJsonExpression = (
464
659
  return `(${renderPostgresJsonValue(ast.left, state, dialect)} || ${renderPostgresJsonValue(ast.right, state, dialect)})`
465
660
  }
466
661
  if (dialect.name === "mysql") {
467
- return `json_merge_preserve(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
662
+ return `json_merge_preserve(${renderJsonInputExpression(ast.left, state, dialect)}, ${renderJsonInputExpression(ast.right, state, dialect)})`
468
663
  }
469
664
  return undefined
470
665
  }
@@ -474,7 +669,7 @@ const renderJsonExpression = (
474
669
  : []
475
670
  const renderedEntries = entries.flatMap((entry) => [
476
671
  dialect.renderLiteral(entry.key, state),
477
- renderExpression(entry.value, state, dialect)
672
+ renderJsonInputExpression(entry.value, state, dialect)
478
673
  ])
479
674
  if (dialect.name === "postgres") {
480
675
  return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_object(${renderedEntries.join(", ")})`
@@ -488,7 +683,7 @@ const renderJsonExpression = (
488
683
  const values = Array.isArray((ast as { readonly values?: readonly Expression.Any[] }).values)
489
684
  ? (ast as { readonly values: readonly Expression.Any[] }).values
490
685
  : []
491
- const renderedValues = values.map((value) => renderExpression(value, state, dialect)).join(", ")
686
+ const renderedValues = values.map((value) => renderJsonInputExpression(value, state, dialect)).join(", ")
492
687
  if (dialect.name === "postgres") {
493
688
  return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_array(${renderedValues})`
494
689
  }
@@ -502,10 +697,10 @@ const renderJsonExpression = (
502
697
  return undefined
503
698
  }
504
699
  if (dialect.name === "postgres") {
505
- return `to_json(${renderExpression(base, state, dialect)})`
700
+ return `to_json(${renderJsonInputExpression(base, state, dialect)})`
506
701
  }
507
702
  if (dialect.name === "mysql") {
508
- return `cast(${renderExpression(base, state, dialect)} as json)`
703
+ return renderMySqlStructuredJsonLiteral(base, state) ?? `cast(${renderExpression(base, state, dialect)} as json)`
509
704
  }
510
705
  return undefined
511
706
  case "jsonToJsonb":
@@ -513,10 +708,10 @@ const renderJsonExpression = (
513
708
  return undefined
514
709
  }
515
710
  if (dialect.name === "postgres") {
516
- return `to_jsonb(${renderExpression(base, state, dialect)})`
711
+ return `to_jsonb(${renderJsonInputExpression(base, state, dialect)})`
517
712
  }
518
713
  if (dialect.name === "mysql") {
519
- return `cast(${renderExpression(base, state, dialect)} as json)`
714
+ return renderMySqlStructuredJsonLiteral(base, state) ?? `cast(${renderExpression(base, state, dialect)} as json)`
520
715
  }
521
716
  return undefined
522
717
  case "jsonTypeOf":
@@ -586,9 +781,7 @@ const renderJsonExpression = (
586
781
  return `(${baseSql} #- ${renderPostgresJsonPathArray(segments, state, dialect)})`
587
782
  }
588
783
  if (dialect.name === "mysql") {
589
- return `json_remove(${renderExpression(base, state, dialect)}, ${segments.map((segment) =>
590
- renderMySqlJsonPath([segment], state, dialect)
591
- ).join(", ")})`
784
+ return `json_remove(${renderExpression(base, state, dialect)}, ${renderMySqlJsonPath(segments, state, dialect)})`
592
785
  }
593
786
  return undefined
594
787
  }
@@ -612,8 +805,16 @@ const renderJsonExpression = (
612
805
  return `${functionName}(${renderPostgresJsonValue(base, state, dialect)}, ${renderPostgresJsonPathArray(segments, state, dialect)}, ${renderPostgresJsonValue(nextValue, state, dialect)}${extra})`
613
806
  }
614
807
  if (dialect.name === "mysql") {
615
- const functionName = kind === "jsonInsert" ? "json_insert" : "json_set"
616
- return `${functionName}(${renderExpression(base, state, dialect)}, ${renderMySqlJsonPath(segments, state, dialect)}, ${renderExpression(nextValue, state, dialect)})`
808
+ const renderedBase = renderExpression(base, state, dialect)
809
+ if (kind === "jsonInsert" && isJsonArrayIndexSegment(segments[segments.length - 1])) {
810
+ const renderedPath = renderMySqlJsonInsertPath(segments, insertAfter, state, dialect)
811
+ const renderedValue = renderJsonInputExpression(nextValue, state, dialect)
812
+ return `json_array_insert(${renderedBase}, ${renderedPath}, ${renderedValue})`
813
+ }
814
+ const functionName = kind === "jsonInsert" ? "json_insert" : createMissing ? "json_set" : "json_replace"
815
+ const renderedPath = renderMySqlJsonPath(segments, state, dialect)
816
+ const renderedValue = renderJsonInputExpression(nextValue, state, dialect)
817
+ return `${functionName}(${renderedBase}, ${renderedPath}, ${renderedValue})`
617
818
  }
618
819
  return undefined
619
820
  }
@@ -712,16 +913,7 @@ const renderMysqlMutationLock = (
712
913
  if (!lock) {
713
914
  return ""
714
915
  }
715
- switch (lock.mode) {
716
- case "lowPriority":
717
- return " low_priority"
718
- case "ignore":
719
- return " ignore"
720
- case "quick":
721
- return statement === "delete" ? " quick" : ""
722
- default:
723
- return ""
724
- }
916
+ return renderMysqlMutationLockMode(lock.mode, statement)
725
917
  }
726
918
 
727
919
  const renderTransactionClause = (
@@ -731,8 +923,9 @@ const renderTransactionClause = (
731
923
  switch (clause.kind) {
732
924
  case "transaction": {
733
925
  const modes: string[] = []
734
- if (clause.isolationLevel) {
735
- modes.push(`isolation level ${clause.isolationLevel}`)
926
+ const isolationLevel = renderTransactionIsolationLevel(clause.isolationLevel)
927
+ if (isolationLevel) {
928
+ modes.push(isolationLevel)
736
929
  }
737
930
  if (clause.readOnly === true) {
738
931
  modes.push("read only")
@@ -752,7 +945,7 @@ const renderTransactionClause = (
752
945
  case "releaseSavepoint":
753
946
  return `release savepoint ${dialect.quoteIdentifier(clause.name)}`
754
947
  }
755
- return ""
948
+ throw new Error("Unsupported transaction statement kind")
756
949
  }
757
950
 
758
951
  const renderSelectionList = (
@@ -765,19 +958,117 @@ const renderSelectionList = (
765
958
  validateAggregationSelection(selection as SelectionValue, [])
766
959
  }
767
960
  const flattened = flattenSelection(selection)
961
+ if (dialect.name === "mysql" && flattened.length === 0) {
962
+ throw new Error("mysql select statements require at least one selected expression")
963
+ }
768
964
  const projections = selectionProjections(selection)
769
965
  const sql = flattened.map(({ expression, alias }) =>
770
- `${renderExpression(expression, state, dialect)} as ${dialect.quoteIdentifier(alias)}`).join(", ")
966
+ `${renderSelectSql(renderExpression(expression, state, dialect), expressionDriverContext(expression, state, dialect))} as ${dialect.quoteIdentifier(alias)}`).join(", ")
771
967
  return {
772
968
  sql,
773
969
  projections
774
970
  }
775
971
  }
776
972
 
973
+ const nestedRenderState = (state: RenderState): RenderState => ({
974
+ params: state.params,
975
+ valueMappings: state.valueMappings,
976
+ ctes: [],
977
+ cteNames: new Set(state.cteNames),
978
+ cteSources: new Map(state.cteSources)
979
+ })
980
+
981
+ const assertMatchingSetProjections = (
982
+ left: readonly Projection[],
983
+ right: readonly Projection[]
984
+ ): void => {
985
+ const leftKeys = left.map((projection) => JSON.stringify(projection.path))
986
+ const rightKeys = right.map((projection) => JSON.stringify(projection.path))
987
+ if (leftKeys.length !== rightKeys.length || leftKeys.some((key, index) => key !== rightKeys[index])) {
988
+ throw new Error("set operator operands must have matching result rows")
989
+ }
990
+ }
991
+
992
+ const assertNoGroupedMutationClauses = (
993
+ ast: Pick<QueryAst.Ast, "groupBy" | "having">,
994
+ statement: string
995
+ ): void => {
996
+ if (ast.groupBy.length > 0) {
997
+ throw new Error(`groupBy(...) is not supported for ${statement} statements`)
998
+ }
999
+ if (ast.having.length > 0) {
1000
+ throw new Error(`having(...) is not supported for ${statement} statements`)
1001
+ }
1002
+ }
1003
+
1004
+ const assertNoInsertQueryClauses = (
1005
+ ast: Pick<QueryAst.Ast, "where" | "joins" | "orderBy" | "limit" | "offset" | "lock">
1006
+ ): void => {
1007
+ if (ast.where.length > 0) {
1008
+ throw new Error("where(...) is not supported for insert statements")
1009
+ }
1010
+ if (ast.joins.length > 0) {
1011
+ throw new Error("join(...) is not supported for insert statements")
1012
+ }
1013
+ if (ast.orderBy.length > 0) {
1014
+ throw new Error("orderBy(...) is not supported for insert statements")
1015
+ }
1016
+ if (ast.limit) {
1017
+ throw new Error("limit(...) is not supported for insert statements")
1018
+ }
1019
+ if (ast.offset) {
1020
+ throw new Error("offset(...) is not supported for insert statements")
1021
+ }
1022
+ if (ast.lock) {
1023
+ throw new Error("lock(...) is not supported for insert statements")
1024
+ }
1025
+ }
1026
+
1027
+ const assertNoStatementQueryClauses = (
1028
+ ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1029
+ statement: string,
1030
+ options: { readonly allowSelection?: boolean } = {}
1031
+ ): void => {
1032
+ if (ast.distinct) {
1033
+ throw new Error(`distinct(...) is not supported for ${statement} statements`)
1034
+ }
1035
+ if (ast.where.length > 0) {
1036
+ throw new Error(`where(...) is not supported for ${statement} statements`)
1037
+ }
1038
+ if ((ast.fromSources?.length ?? 0) > 0 || ast.from) {
1039
+ throw new Error(`from(...) is not supported for ${statement} statements`)
1040
+ }
1041
+ if (ast.joins.length > 0) {
1042
+ throw new Error(`join(...) is not supported for ${statement} statements`)
1043
+ }
1044
+ if (ast.groupBy.length > 0) {
1045
+ throw new Error(`groupBy(...) is not supported for ${statement} statements`)
1046
+ }
1047
+ if (ast.having.length > 0) {
1048
+ throw new Error(`having(...) is not supported for ${statement} statements`)
1049
+ }
1050
+ if (ast.orderBy.length > 0) {
1051
+ throw new Error(`orderBy(...) is not supported for ${statement} statements`)
1052
+ }
1053
+ if (ast.limit) {
1054
+ throw new Error(`limit(...) is not supported for ${statement} statements`)
1055
+ }
1056
+ if (ast.offset) {
1057
+ throw new Error(`offset(...) is not supported for ${statement} statements`)
1058
+ }
1059
+ if (ast.lock) {
1060
+ throw new Error(`lock(...) is not supported for ${statement} statements`)
1061
+ }
1062
+ if (options.allowSelection !== true && Object.keys(ast.select).length > 0) {
1063
+ throw new Error(`returning(...) is not supported for ${statement} statements`)
1064
+ }
1065
+ }
1066
+
777
1067
  export const renderQueryAst = (
778
1068
  ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
779
1069
  state: RenderState,
780
- dialect: SqlDialect
1070
+ dialect: SqlDialect,
1071
+ options: { readonly emitCtes?: boolean } = {}
781
1072
  ): RenderedQueryAst => {
782
1073
  let sql = ""
783
1074
  let projections: readonly Projection[] = []
@@ -787,15 +1078,19 @@ export const renderQueryAst = (
787
1078
  validateAggregationSelection(ast.select as SelectionValue, ast.groupBy)
788
1079
  const rendered = renderSelectionList(ast.select as Record<string, unknown>, state, dialect, false)
789
1080
  projections = rendered.projections
1081
+ const selectList = rendered.sql.length > 0 ? ` ${rendered.sql}` : ""
790
1082
  const clauses = [
791
1083
  ast.distinctOn && ast.distinctOn.length > 0
792
- ? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")}) ${rendered.sql}`
793
- : `select${ast.distinct ? " distinct" : ""} ${rendered.sql}`
1084
+ ? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")})${selectList}`
1085
+ : `select${ast.distinct ? " distinct" : ""}${selectList}`
794
1086
  ]
795
1087
  if (ast.from) {
796
1088
  clauses.push(`from ${renderSourceReference(ast.from.source, ast.from.tableName, ast.from.baseTableName, state, dialect)}`)
797
1089
  }
798
1090
  for (const join of ast.joins) {
1091
+ if (dialect.name === "mysql" && join.kind === "full") {
1092
+ throw new Error("Unsupported mysql full join")
1093
+ }
799
1094
  const source = renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
800
1095
  clauses.push(
801
1096
  join.kind === "cross"
@@ -822,8 +1117,11 @@ export const renderQueryAst = (
822
1117
  clauses.push(`offset ${renderExpression(ast.offset, state, dialect)}`)
823
1118
  }
824
1119
  if (ast.lock) {
1120
+ if (ast.lock.nowait && ast.lock.skipLocked) {
1121
+ throw new Error("lock(...) cannot specify both nowait and skipLocked")
1122
+ }
825
1123
  clauses.push(
826
- `${ast.lock.mode === "update" ? "for update" : "for share"}${ast.lock.nowait ? " nowait" : ""}${ast.lock.skipLocked ? " skip locked" : ""}`
1124
+ `${renderSelectLockMode(ast.lock.mode)}${ast.lock.nowait ? " nowait" : ""}${ast.lock.skipLocked ? " skip locked" : ""}`
827
1125
  )
828
1126
  }
829
1127
  sql = clauses.join(" ")
@@ -831,6 +1129,7 @@ export const renderQueryAst = (
831
1129
  }
832
1130
  case "set": {
833
1131
  const setAst = ast as QueryAst.Ast<Record<string, unknown>, any, "set">
1132
+ assertNoStatementQueryClauses(setAst, "set", { allowSelection: true })
834
1133
  const base = renderQueryAst(
835
1134
  Query.getAst(setAst.setBase as Query.Plan.Any) as QueryAst.Ast<
836
1135
  Record<string, unknown>,
@@ -841,6 +1140,7 @@ export const renderQueryAst = (
841
1140
  dialect
842
1141
  )
843
1142
  projections = selectionProjections(setAst.select as Record<string, unknown>)
1143
+ assertMatchingSetProjections(projections, base.projections)
844
1144
  sql = [
845
1145
  `(${base.sql})`,
846
1146
  ...(setAst.setOperations ?? []).map((entry) => {
@@ -853,6 +1153,7 @@ export const renderQueryAst = (
853
1153
  state,
854
1154
  dialect
855
1155
  )
1156
+ assertMatchingSetProjections(projections, rendered.projections)
856
1157
  return `${entry.kind}${entry.all ? " all" : ""} (${rendered.sql})`
857
1158
  })
858
1159
  ].join(" ")
@@ -860,19 +1161,26 @@ export const renderQueryAst = (
860
1161
  }
861
1162
  case "insert": {
862
1163
  const insertAst = ast as QueryAst.Ast<Record<string, unknown>, any, "insert">
1164
+ if (insertAst.distinct) {
1165
+ throw new Error("distinct(...) is not supported for insert statements")
1166
+ }
1167
+ assertNoGroupedMutationClauses(insertAst, "insert")
1168
+ assertNoInsertQueryClauses(insertAst)
863
1169
  const targetSource = insertAst.into!
864
1170
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1171
+ const insertSource = expectInsertSourceKind(insertAst.insertSource)
1172
+ const conflict = expectConflictClause(insertAst.conflict)
865
1173
  sql = `insert into ${target}`
866
- if (insertAst.insertSource?.kind === "values") {
867
- const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
868
- const rows = insertAst.insertSource.rows.map((row) =>
1174
+ if (insertSource?.kind === "values") {
1175
+ const columns = insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
1176
+ const rows = insertSource.rows.map((row) =>
869
1177
  `(${row.values.map((entry) => renderExpression(entry.value, state, dialect)).join(", ")})`
870
1178
  ).join(", ")
871
1179
  sql += ` (${columns}) values ${rows}`
872
- } else if (insertAst.insertSource?.kind === "query") {
873
- const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
1180
+ } else if (insertSource?.kind === "query") {
1181
+ const columns = insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
874
1182
  const renderedQuery = renderQueryAst(
875
- Query.getAst(insertAst.insertSource.query as Query.Plan.Any) as QueryAst.Ast<
1183
+ Query.getAst(insertSource.query as Query.Plan.Any) as QueryAst.Ast<
876
1184
  Record<string, unknown>,
877
1185
  any,
878
1186
  QueryAst.QueryStatement
@@ -881,22 +1189,25 @@ export const renderQueryAst = (
881
1189
  dialect
882
1190
  )
883
1191
  sql += ` (${columns}) ${renderedQuery.sql}`
884
- } else if (insertAst.insertSource?.kind === "unnest") {
885
- const unnestSource = insertAst.insertSource
886
- const columns = unnestSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
1192
+ } else if (insertSource?.kind === "unnest") {
1193
+ const columns = insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
887
1194
  if (dialect.name === "postgres") {
888
1195
  const table = targetSource.source as Table.AnyTable
889
1196
  const fields = table[Table.TypeId].fields
890
- const rendered = unnestSource.values.map((entry) =>
891
- `cast(${dialect.renderLiteral(entry.values, state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
1197
+ const rendered = insertSource.values.map((entry) =>
1198
+ `cast(${dialect.renderLiteral(encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect), state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
892
1199
  ).join(", ")
893
1200
  sql += ` (${columns}) select * from unnest(${rendered})`
894
1201
  } else {
895
- const rowCount = unnestSource.values[0]?.values.length ?? 0
1202
+ const table = targetSource.source as Table.AnyTable
1203
+ const fields = table[Table.TypeId].fields
1204
+ const encodedValues = insertSource.values.map((entry) => ({
1205
+ columnName: entry.columnName,
1206
+ values: encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect)
1207
+ }))
1208
+ const rowCount = encodedValues[0]?.values.length ?? 0
896
1209
  const rows = Array.from({ length: rowCount }, (_, index) =>
897
- `(${unnestSource.values.map((entry) =>
898
- dialect.renderLiteral(entry.values[index], state)
899
- ).join(", ")})`
1210
+ `(${encodedValues.map((entry) => dialect.renderLiteral(entry.values[index], state)).join(", ")})`
900
1211
  ).join(", ")
901
1212
  sql += ` (${columns}) values ${rows}`
902
1213
  }
@@ -906,30 +1217,39 @@ export const renderQueryAst = (
906
1217
  if ((insertAst.values ?? []).length > 0) {
907
1218
  sql += ` (${columns}) values (${values})`
908
1219
  } else {
909
- sql += " default values"
1220
+ sql += " () values ()"
910
1221
  }
911
1222
  }
912
- if (insertAst.conflict) {
913
- const updateValues = (insertAst.conflict.values ?? []).map((entry) =>
1223
+ if (conflict) {
1224
+ if (conflict.where) {
1225
+ throw new Error("Unsupported mysql conflict action predicates")
1226
+ }
1227
+ const updateValues = (conflict.values ?? []).map((entry) =>
914
1228
  `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
915
1229
  ).join(", ")
916
1230
  if (dialect.name === "postgres") {
917
- const targetSql = insertAst.conflict.target?.kind === "constraint"
918
- ? ` on conflict on constraint ${dialect.quoteIdentifier(insertAst.conflict.target.name)}`
919
- : insertAst.conflict.target?.kind === "columns"
920
- ? ` on conflict (${insertAst.conflict.target.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${insertAst.conflict.target.where ? ` where ${renderExpression(insertAst.conflict.target.where, state, dialect)}` : ""}`
1231
+ const targetSql = conflict.target?.kind === "constraint"
1232
+ ? ` on conflict on constraint ${dialect.quoteIdentifier(conflict.target.name)}`
1233
+ : conflict.target?.kind === "columns"
1234
+ ? ` on conflict (${conflict.target.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${conflict.target.where ? ` where ${renderExpression(conflict.target.where, state, dialect)}` : ""}`
921
1235
  : " on conflict"
922
1236
  sql += targetSql
923
- sql += insertAst.conflict.action === "doNothing"
1237
+ sql += conflict.action === "doNothing"
924
1238
  ? " do nothing"
925
- : ` do update set ${updateValues}${insertAst.conflict.where ? ` where ${renderExpression(insertAst.conflict.where, state, dialect)}` : ""}`
926
- } else if (insertAst.conflict.action === "doNothing") {
1239
+ : ` do update set ${updateValues}${conflict.where ? ` where ${renderExpression(conflict.where, state, dialect)}` : ""}`
1240
+ } else if (conflict.action === "doNothing") {
927
1241
  sql = sql.replace(/^insert/, "insert ignore")
928
1242
  } else {
929
1243
  sql += ` on duplicate key update ${updateValues}`
930
1244
  }
931
1245
  }
932
- const returning = renderSelectionList(insertAst.select as Record<string, unknown>, state, dialect, false)
1246
+ const hasReturning = Object.keys(insertAst.select as Record<string, unknown>).length > 0
1247
+ const returning = hasReturning
1248
+ ? renderSelectionList(insertAst.select as Record<string, unknown>, state, dialect, false)
1249
+ : { sql: "", projections: [] }
1250
+ if (dialect.name === "mysql" && returning.sql.length > 0) {
1251
+ throw new Error("Unsupported mysql returning")
1252
+ }
933
1253
  projections = returning.projections
934
1254
  if (returning.sql.length > 0) {
935
1255
  sql += ` returning ${returning.sql}`
@@ -938,19 +1258,33 @@ export const renderQueryAst = (
938
1258
  }
939
1259
  case "update": {
940
1260
  const updateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "update">
1261
+ if (updateAst.distinct) {
1262
+ throw new Error("distinct(...) is not supported for update statements")
1263
+ }
1264
+ assertNoGroupedMutationClauses(updateAst, "update")
1265
+ if (updateAst.offset) {
1266
+ throw new Error("offset(...) is not supported for update statements")
1267
+ }
941
1268
  const targetSource = updateAst.target!
942
1269
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
943
1270
  const targets = updateAst.targets ?? [targetSource]
944
1271
  const fromSources = updateAst.fromSources ?? []
1272
+ if ((updateAst.set ?? []).length === 0) {
1273
+ throw new Error("update statements require at least one assignment")
1274
+ }
945
1275
  const assignments = updateAst.set!.map((entry) =>
946
1276
  renderMutationAssignment(entry, state, dialect)).join(", ")
947
1277
  if (dialect.name === "mysql") {
948
1278
  const modifiers = renderMysqlMutationLock(updateAst.lock, "update")
949
1279
  const extraSources = renderFromSources(fromSources, state, dialect)
950
1280
  const joinSources = updateAst.joins.map((join) =>
951
- join.kind === "cross"
952
- ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
953
- : `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
1281
+ join.kind === "full"
1282
+ ? (() => {
1283
+ throw new Error("Unsupported mysql full join")
1284
+ })()
1285
+ : join.kind === "cross"
1286
+ ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
1287
+ : `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
954
1288
  ).join(" ")
955
1289
  const targetList = [
956
1290
  ...targets.map((entry) =>
@@ -982,7 +1316,13 @@ export const renderQueryAst = (
982
1316
  if (dialect.name === "mysql" && updateAst.limit) {
983
1317
  sql += ` limit ${renderMysqlMutationLimit(updateAst.limit, state, dialect)}`
984
1318
  }
985
- const returning = renderSelectionList(updateAst.select as Record<string, unknown>, state, dialect, false)
1319
+ const hasReturning = Object.keys(updateAst.select as Record<string, unknown>).length > 0
1320
+ const returning = hasReturning
1321
+ ? renderSelectionList(updateAst.select as Record<string, unknown>, state, dialect, false)
1322
+ : { sql: "", projections: [] }
1323
+ if (dialect.name === "mysql" && returning.sql.length > 0) {
1324
+ throw new Error("Unsupported mysql returning")
1325
+ }
986
1326
  projections = returning.projections
987
1327
  if (returning.sql.length > 0) {
988
1328
  sql += ` returning ${returning.sql}`
@@ -991,6 +1331,13 @@ export const renderQueryAst = (
991
1331
  }
992
1332
  case "delete": {
993
1333
  const deleteAst = ast as QueryAst.Ast<Record<string, unknown>, any, "delete">
1334
+ if (deleteAst.distinct) {
1335
+ throw new Error("distinct(...) is not supported for delete statements")
1336
+ }
1337
+ assertNoGroupedMutationClauses(deleteAst, "delete")
1338
+ if (deleteAst.offset) {
1339
+ throw new Error("offset(...) is not supported for delete statements")
1340
+ }
994
1341
  const targetSource = deleteAst.target!
995
1342
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
996
1343
  const targets = deleteAst.targets ?? [targetSource]
@@ -1002,9 +1349,13 @@ export const renderQueryAst = (
1002
1349
  renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
1003
1350
  ).join(", ")
1004
1351
  const joinSources = deleteAst.joins.map((join) =>
1005
- join.kind === "cross"
1006
- ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
1007
- : `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
1352
+ join.kind === "full"
1353
+ ? (() => {
1354
+ throw new Error("Unsupported mysql full join")
1355
+ })()
1356
+ : join.kind === "cross"
1357
+ ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
1358
+ : `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
1008
1359
  ).join(" ")
1009
1360
  sql = hasJoinedSources
1010
1361
  ? `delete${modifiers} ${targetList} from ${fromSources}${joinSources.length > 0 ? ` ${joinSources}` : ""}`
@@ -1028,7 +1379,13 @@ export const renderQueryAst = (
1028
1379
  if (dialect.name === "mysql" && deleteAst.limit) {
1029
1380
  sql += ` limit ${renderMysqlMutationLimit(deleteAst.limit, state, dialect)}`
1030
1381
  }
1031
- const returning = renderSelectionList(deleteAst.select as Record<string, unknown>, state, dialect, false)
1382
+ const hasReturning = Object.keys(deleteAst.select as Record<string, unknown>).length > 0
1383
+ const returning = hasReturning
1384
+ ? renderSelectionList(deleteAst.select as Record<string, unknown>, state, dialect, false)
1385
+ : { sql: "", projections: [] }
1386
+ if (dialect.name === "mysql" && returning.sql.length > 0) {
1387
+ throw new Error("Unsupported mysql returning")
1388
+ }
1032
1389
  projections = returning.projections
1033
1390
  if (returning.sql.length > 0) {
1034
1391
  sql += ` returning ${returning.sql}`
@@ -1037,12 +1394,17 @@ export const renderQueryAst = (
1037
1394
  }
1038
1395
  case "truncate": {
1039
1396
  const truncateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "truncate">
1397
+ assertNoStatementQueryClauses(truncateAst, "truncate")
1398
+ const truncate = expectTruncateClause(truncateAst.truncate)
1040
1399
  const targetSource = truncateAst.target!
1400
+ if (dialect.name === "mysql" && (truncate.restartIdentity || truncate.cascade)) {
1401
+ throw new Error("Unsupported mysql truncate options")
1402
+ }
1041
1403
  sql = `truncate table ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
1042
- if (truncateAst.truncate?.restartIdentity) {
1404
+ if (truncate.restartIdentity) {
1043
1405
  sql += " restart identity"
1044
1406
  }
1045
- if (truncateAst.truncate?.cascade) {
1407
+ if (truncate.cascade) {
1046
1408
  sql += " cascade"
1047
1409
  }
1048
1410
  break
@@ -1055,6 +1417,9 @@ export const renderQueryAst = (
1055
1417
  const targetSource = mergeAst.target!
1056
1418
  const usingSource = mergeAst.using!
1057
1419
  const merge = mergeAst.merge!
1420
+ if (Object.keys(mergeAst.select as Record<string, unknown>).length > 0) {
1421
+ throw new Error("returning(...) is not supported for merge statements")
1422
+ }
1058
1423
  sql = `merge into ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} using ${renderSourceReference(usingSource.source, usingSource.tableName, usingSource.baseTableName, state, dialect)} on ${renderExpression(merge.on, state, dialect)}`
1059
1424
  if (merge.whenMatched) {
1060
1425
  sql += " when matched"
@@ -1084,25 +1449,30 @@ export const renderQueryAst = (
1084
1449
  case "savepoint":
1085
1450
  case "rollbackTo":
1086
1451
  case "releaseSavepoint": {
1452
+ assertNoStatementQueryClauses(ast, ast.kind)
1087
1453
  sql = renderTransactionClause(ast.transaction!, dialect)
1088
1454
  break
1089
1455
  }
1090
1456
  case "createTable": {
1091
1457
  const createTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createTable">
1092
- sql = renderCreateTableSql(createTableAst.target!, state, dialect, createTableAst.ddl?.kind === "createTable" && createTableAst.ddl.ifNotExists)
1458
+ assertNoStatementQueryClauses(createTableAst, "createTable")
1459
+ const ddl = expectDdlClauseKind(createTableAst.ddl, "createTable")
1460
+ sql = renderCreateTableSql(createTableAst.target!, state, dialect, ddl.ifNotExists)
1093
1461
  break
1094
1462
  }
1095
1463
  case "dropTable": {
1096
1464
  const dropTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropTable">
1097
- const ifExists = dropTableAst.ddl?.kind === "dropTable" && dropTableAst.ddl.ifExists
1098
- sql = `drop table${ifExists ? " if exists" : ""} ${renderSourceReference(dropTableAst.target!.source, dropTableAst.target!.tableName, dropTableAst.target!.baseTableName, state, dialect)}`
1465
+ assertNoStatementQueryClauses(dropTableAst, "dropTable")
1466
+ const ddl = expectDdlClauseKind(dropTableAst.ddl, "dropTable")
1467
+ sql = `drop table${ddl.ifExists ? " if exists" : ""} ${renderSourceReference(dropTableAst.target!.source, dropTableAst.target!.tableName, dropTableAst.target!.baseTableName, state, dialect)}`
1099
1468
  break
1100
1469
  }
1101
1470
  case "createIndex": {
1102
1471
  const createIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createIndex">
1472
+ assertNoStatementQueryClauses(createIndexAst, "createIndex")
1103
1473
  sql = renderCreateIndexSql(
1104
1474
  createIndexAst.target!,
1105
- createIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
1475
+ expectDdlClauseKind(createIndexAst.ddl, "createIndex"),
1106
1476
  state,
1107
1477
  dialect
1108
1478
  )
@@ -1110,17 +1480,20 @@ export const renderQueryAst = (
1110
1480
  }
1111
1481
  case "dropIndex": {
1112
1482
  const dropIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropIndex">
1483
+ assertNoStatementQueryClauses(dropIndexAst, "dropIndex")
1113
1484
  sql = renderDropIndexSql(
1114
1485
  dropIndexAst.target!,
1115
- dropIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
1486
+ expectDdlClauseKind(dropIndexAst.ddl, "dropIndex"),
1116
1487
  state,
1117
1488
  dialect
1118
1489
  )
1119
1490
  break
1120
1491
  }
1492
+ default:
1493
+ throw new Error("Unsupported query statement kind")
1121
1494
  }
1122
1495
 
1123
- if (state.ctes.length === 0) {
1496
+ if (state.ctes.length === 0 || options.emitCtes === false) {
1124
1497
  return {
1125
1498
  sql,
1126
1499
  projections
@@ -1168,9 +1541,27 @@ const renderSourceReference = (
1168
1541
  readonly plan: Query.Plan.Any
1169
1542
  readonly recursive?: boolean
1170
1543
  }
1544
+ const registeredCteSource = state.cteSources.get(cte.name)
1545
+ if (registeredCteSource !== undefined && registeredCteSource !== cte.plan) {
1546
+ throw new Error(`common table expression name is already registered with a different plan: ${cte.name}`)
1547
+ }
1171
1548
  if (!state.cteNames.has(cte.name)) {
1172
1549
  state.cteNames.add(cte.name)
1173
- const rendered = renderQueryAst(Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect)
1550
+ state.cteSources.set(cte.name, cte.plan)
1551
+ const statement = Query.getQueryState(cte.plan).statement
1552
+ if (statement !== "select" && statement !== "set") {
1553
+ const cteAst = Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>
1554
+ if (Object.keys((cteAst.select ?? {}) as Record<string, unknown>).length > 0) {
1555
+ throw new Error("Unsupported mysql returning")
1556
+ }
1557
+ throw new Error("Unsupported mysql data-modifying cte")
1558
+ }
1559
+ const rendered = renderQueryAst(
1560
+ Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1561
+ state,
1562
+ dialect,
1563
+ { emitCtes: false }
1564
+ )
1174
1565
  state.ctes.push({
1175
1566
  name: cte.name,
1176
1567
  sql: rendered.sql,
@@ -1187,14 +1578,14 @@ const renderSourceReference = (
1187
1578
  if (!state.cteNames.has(derived.name)) {
1188
1579
  // derived tables are inlined, so no CTE registration is needed
1189
1580
  }
1190
- return `(${renderQueryAst(Query.getAst(derived.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect).sql}) as ${dialect.quoteIdentifier(derived.name)}`
1581
+ return `(${renderQueryAst(Query.getAst(derived.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, nestedRenderState(state), dialect).sql}) as ${dialect.quoteIdentifier(derived.name)}`
1191
1582
  }
1192
1583
  if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "lateral") {
1193
1584
  const lateral = source as unknown as {
1194
1585
  readonly name: string
1195
1586
  readonly plan: Query.Plan.Any
1196
1587
  }
1197
- return `lateral (${renderQueryAst(Query.getAst(lateral.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect).sql}) as ${dialect.quoteIdentifier(lateral.name)}`
1588
+ 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)}`
1198
1589
  }
1199
1590
  if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "values") {
1200
1591
  const values = source as unknown as {
@@ -1229,6 +1620,22 @@ const renderSourceReference = (
1229
1620
  return dialect.renderTableReference(tableName, baseTableName, schemaName)
1230
1621
  }
1231
1622
 
1623
+ const renderSubqueryExpressionPlan = (
1624
+ plan: Query.Plan.Any,
1625
+ state: RenderState,
1626
+ dialect: SqlDialect
1627
+ ): string => {
1628
+ const statement = Query.getQueryState(plan).statement
1629
+ if (statement !== "select" && statement !== "set") {
1630
+ throw new Error("subquery expressions only accept select-like query plans")
1631
+ }
1632
+ return renderQueryAst(
1633
+ Query.getAst(plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1634
+ state,
1635
+ dialect
1636
+ ).sql
1637
+ }
1638
+
1232
1639
  /**
1233
1640
  * Renders a scalar expression AST into SQL text.
1234
1641
  *
@@ -1263,11 +1670,14 @@ export const renderExpression = (
1263
1670
  : ">="
1264
1671
  switch (ast.kind) {
1265
1672
  case "column":
1266
- return ast.tableName.length === 0
1673
+ return state.rowLocalColumns || ast.tableName.length === 0
1267
1674
  ? dialect.quoteIdentifier(ast.columnName)
1268
1675
  : `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
1269
1676
  case "literal":
1270
- return dialect.renderLiteral(ast.value, state)
1677
+ if (typeof ast.value === "number" && !Number.isFinite(ast.value)) {
1678
+ throw new Error("Expected a finite numeric value")
1679
+ }
1680
+ return dialect.renderLiteral(ast.value, state, expression[Expression.TypeId])
1271
1681
  case "excluded":
1272
1682
  return dialect.name === "mysql"
1273
1683
  ? `values(${dialect.quoteIdentifier(ast.columnName)})`
@@ -1329,7 +1739,7 @@ export const renderExpression = (
1329
1739
  return `(${left} @> ${right})`
1330
1740
  }
1331
1741
  if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1332
- return `json_contains(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
1742
+ return `json_contains(${renderJsonInputExpression(ast.left, state, dialect)}, ${renderJsonInputExpression(ast.right, state, dialect)})`
1333
1743
  }
1334
1744
  throw new Error("Unsupported container operator for SQL rendering")
1335
1745
  case "containedBy":
@@ -1343,7 +1753,7 @@ export const renderExpression = (
1343
1753
  return `(${left} <@ ${right})`
1344
1754
  }
1345
1755
  if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1346
- return `json_contains(${renderExpression(ast.right, state, dialect)}, ${renderExpression(ast.left, state, dialect)})`
1756
+ return `json_contains(${renderJsonInputExpression(ast.right, state, dialect)}, ${renderJsonInputExpression(ast.left, state, dialect)})`
1347
1757
  }
1348
1758
  throw new Error("Unsupported container operator for SQL rendering")
1349
1759
  case "overlaps":
@@ -1357,7 +1767,7 @@ export const renderExpression = (
1357
1767
  return `(${left} && ${right})`
1358
1768
  }
1359
1769
  if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1360
- return `json_overlaps(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
1770
+ return `json_overlaps(${renderJsonInputExpression(ast.left, state, dialect)}, ${renderJsonInputExpression(ast.right, state, dialect)})`
1361
1771
  }
1362
1772
  throw new Error("Unsupported container operator for SQL rendering")
1363
1773
  case "isNull":
@@ -1377,14 +1787,26 @@ export const renderExpression = (
1377
1787
  case "min":
1378
1788
  return `min(${renderExpression(ast.value, state, dialect)})`
1379
1789
  case "and":
1790
+ if (ast.values.length === 0) {
1791
+ throw new Error("and(...) requires at least one predicate")
1792
+ }
1380
1793
  return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" and ")})`
1381
1794
  case "or":
1795
+ if (ast.values.length === 0) {
1796
+ throw new Error("or(...) requires at least one predicate")
1797
+ }
1382
1798
  return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" or ")})`
1383
1799
  case "coalesce":
1384
1800
  return `coalesce(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")})`
1385
1801
  case "in":
1802
+ if (ast.values.length < 2) {
1803
+ throw new Error("in(...) requires at least one candidate value")
1804
+ }
1386
1805
  return `(${renderExpression(ast.values[0]!, state, dialect)} in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1387
1806
  case "notIn":
1807
+ if (ast.values.length < 2) {
1808
+ throw new Error("notIn(...) requires at least one candidate value")
1809
+ }
1388
1810
  return `(${renderExpression(ast.values[0]!, state, dialect)} not in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1389
1811
  case "between":
1390
1812
  return `(${renderExpression(ast.values[0]!, state, dialect)} between ${renderExpression(ast.values[1]!, state, dialect)} and ${renderExpression(ast.values[2]!, state, dialect)})`
@@ -1395,35 +1817,15 @@ export const renderExpression = (
1395
1817
  `when ${renderExpression(branch.when, state, dialect)} then ${renderExpression(branch.then, state, dialect)}`
1396
1818
  ).join(" ")} else ${renderExpression(ast.else, state, dialect)} end`
1397
1819
  case "exists":
1398
- return `exists (${renderQueryAst(
1399
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1400
- state,
1401
- dialect
1402
- ).sql})`
1820
+ return `exists (${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
1403
1821
  case "scalarSubquery":
1404
- return `(${renderQueryAst(
1405
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1406
- state,
1407
- dialect
1408
- ).sql})`
1822
+ return `(${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
1409
1823
  case "inSubquery":
1410
- return `(${renderExpression(ast.left, state, dialect)} in (${renderQueryAst(
1411
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1412
- state,
1413
- dialect
1414
- ).sql}))`
1824
+ return `(${renderExpression(ast.left, state, dialect)} in (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1415
1825
  case "comparisonAny":
1416
- return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderQueryAst(
1417
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1418
- state,
1419
- dialect
1420
- ).sql}))`
1826
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1421
1827
  case "comparisonAll":
1422
- return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderQueryAst(
1423
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1424
- state,
1425
- dialect
1426
- ).sql}))`
1828
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1427
1829
  case "window": {
1428
1830
  if (!Array.isArray(ast.partitionBy) || !Array.isArray(ast.orderBy) || typeof ast.function !== "string") {
1429
1831
  break