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 { 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"
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 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
+
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: renderMysqlDdlLiteral
111
+ })
112
+ }
62
113
 
63
114
  const renderMysqlMutationLimit = (
64
115
  expression: Expression.Any,
@@ -114,22 +165,27 @@ const renderCreateTableSql = (
114
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" : ""}` : ""}`)
115
166
  break
116
167
  case "unique":
168
+ if (option.nullsNotDistinct || option.deferrable || option.initiallyDeferred) {
169
+ throw new Error("Unsupported mysql unique constraint options")
170
+ }
117
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" : ""}` : ""}`)
118
172
  break
119
173
  case "foreignKey": {
120
174
  const reference = option.references()
121
175
  definitions.push(
122
- `${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" : ""}` : ""}`
123
177
  )
124
178
  break
125
179
  }
126
180
  case "check":
127
181
  definitions.push(
128
- `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" : ""}`
129
183
  )
130
184
  break
131
185
  case "index":
132
186
  break
187
+ default:
188
+ throw new Error("Unsupported table option kind")
133
189
  }
134
190
  }
135
191
  return `create table${ifNotExists ? " if not exists" : ""} ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${definitions.join(", ")})`
@@ -141,6 +197,9 @@ const renderCreateIndexSql = (
141
197
  state: RenderState,
142
198
  dialect: SqlDialect
143
199
  ): string => {
200
+ if (ddl.ifNotExists) {
201
+ throw new Error("Unsupported mysql create index options")
202
+ }
144
203
  const maybeIfNotExists = dialect.name === "postgres" && ddl.ifNotExists ? " if not exists" : ""
145
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(", ")})`
146
205
  }
@@ -150,16 +209,28 @@ const renderDropIndexSql = (
150
209
  ddl: Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
151
210
  state: RenderState,
152
211
  dialect: SqlDialect
153
- ): string =>
154
- dialect.name === "postgres"
212
+ ): string => {
213
+ if (ddl.ifExists) {
214
+ throw new Error("Unsupported mysql drop index options")
215
+ }
216
+ return dialect.name === "postgres"
155
217
  ? `drop index${ddl.ifExists ? " if exists" : ""} ${dialect.quoteIdentifier(ddl.name)}`
156
218
  : `drop index ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
219
+ }
157
220
 
158
221
  const isExpression = (value: unknown): value is Expression.Any =>
159
222
  value !== null && typeof value === "object" && Expression.TypeId in value
160
223
 
161
- const isJsonDbType = (dbType: Expression.DbType.Any): boolean =>
162
- 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
+ }
163
234
 
164
235
  const isJsonExpression = (value: unknown): value is Expression.Any =>
165
236
  isExpression(value) && isJsonDbType(value[Expression.TypeId].dbType)
@@ -221,19 +292,19 @@ const extractJsonValue = (node: Record<string, unknown>): unknown =>
221
292
  node.newValue ?? node.insert ?? node.right
222
293
 
223
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)}`
224
299
  if (typeof segment === "string") {
225
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)
226
- ? `.${segment}`
227
- : `."${segment.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
300
+ return renderKey(segment)
228
301
  }
229
302
  if (typeof segment === "number") {
230
303
  return `[${segment}]`
231
304
  }
232
305
  switch (segment.kind) {
233
306
  case "key":
234
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment.key)
235
- ? `.${segment.key}`
236
- : `."${segment.key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
307
+ return renderKey(segment.key)
237
308
  case "index":
238
309
  return `[${segment.index}]`
239
310
  case "wildcard":
@@ -247,10 +318,32 @@ const renderJsonPathSegment = (segment: JsonPath.AnySegment | string | number):
247
318
  }
248
319
  }
249
320
 
250
- 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 => {
251
344
  let path = "$"
252
345
  for (const segment of segments) {
253
- path += renderJsonPathSegment(segment)
346
+ path += renderSegment(segment)
254
347
  }
255
348
  return path
256
349
  }
@@ -259,7 +352,30 @@ const renderMySqlJsonPath = (
259
352
  segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
260
353
  state: RenderState,
261
354
  dialect: SqlDialect
262
- ): 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
+ }
263
379
 
264
380
  const renderPostgresJsonPathArray = (
265
381
  segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
@@ -330,15 +446,36 @@ const expressionDriverContext = (
330
446
  driverValueMapping: expression[Expression.TypeId].driverValueMapping
331
447
  })
332
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
+
333
463
  const renderJsonInputExpression = (
334
464
  expression: Expression.Any,
335
465
  state: RenderState,
336
466
  dialect: SqlDialect
337
- ): string =>
338
- renderJsonSelectSql(
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(
339
475
  renderExpression(expression, state, dialect),
340
476
  expressionDriverContext(expression, state, dialect)
341
477
  )
478
+ }
342
479
 
343
480
  const encodeArrayValues = (
344
481
  values: readonly unknown[],
@@ -346,14 +483,26 @@ const encodeArrayValues = (
346
483
  state: RenderState,
347
484
  dialect: SqlDialect
348
485
  ): readonly unknown[] =>
349
- values.map((value) =>
350
- toDriverValue(value, {
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, {
351
499
  dialect: dialect.name,
352
500
  valueMappings: state.valueMappings,
353
501
  dbType: column.metadata.dbType,
354
502
  runtimeSchema: column.schema,
355
503
  driverValueMapping: column.metadata.driverValueMapping
356
- }))
504
+ })
505
+ })
357
506
 
358
507
  const renderPostgresJsonKind = (
359
508
  value: Expression.Any
@@ -365,7 +514,10 @@ const renderJsonOpaquePath = (
365
514
  dialect: SqlDialect
366
515
  ): string => {
367
516
  if (isJsonPathValue(value)) {
368
- 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)
369
521
  }
370
522
  if (typeof value === "string") {
371
523
  return dialect.renderLiteral(value, state)
@@ -492,8 +644,9 @@ const renderJsonExpression = (
492
644
  }
493
645
  if (dialect.name === "mysql") {
494
646
  const mode = kind === "jsonHasAllKeys" ? "all" : "one"
647
+ const modeSql = dialect.renderLiteral(mode, state)
495
648
  const paths = keys.map((segment) => renderMySqlJsonPath([segment], state, dialect)).join(", ")
496
- return `json_contains_path(${baseSql}, ${dialect.renderLiteral(mode, state)}, ${paths})`
649
+ return `json_contains_path(${baseSql}, ${modeSql}, ${paths})`
497
650
  }
498
651
  return undefined
499
652
  }
@@ -506,7 +659,7 @@ const renderJsonExpression = (
506
659
  return `(${renderPostgresJsonValue(ast.left, state, dialect)} || ${renderPostgresJsonValue(ast.right, state, dialect)})`
507
660
  }
508
661
  if (dialect.name === "mysql") {
509
- 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)})`
510
663
  }
511
664
  return undefined
512
665
  }
@@ -547,7 +700,7 @@ const renderJsonExpression = (
547
700
  return `to_json(${renderJsonInputExpression(base, state, dialect)})`
548
701
  }
549
702
  if (dialect.name === "mysql") {
550
- return `cast(${renderExpression(base, state, dialect)} as json)`
703
+ return renderMySqlStructuredJsonLiteral(base, state) ?? `cast(${renderExpression(base, state, dialect)} as json)`
551
704
  }
552
705
  return undefined
553
706
  case "jsonToJsonb":
@@ -558,7 +711,7 @@ const renderJsonExpression = (
558
711
  return `to_jsonb(${renderJsonInputExpression(base, state, dialect)})`
559
712
  }
560
713
  if (dialect.name === "mysql") {
561
- return `cast(${renderExpression(base, state, dialect)} as json)`
714
+ return renderMySqlStructuredJsonLiteral(base, state) ?? `cast(${renderExpression(base, state, dialect)} as json)`
562
715
  }
563
716
  return undefined
564
717
  case "jsonTypeOf":
@@ -628,9 +781,7 @@ const renderJsonExpression = (
628
781
  return `(${baseSql} #- ${renderPostgresJsonPathArray(segments, state, dialect)})`
629
782
  }
630
783
  if (dialect.name === "mysql") {
631
- return `json_remove(${renderExpression(base, state, dialect)}, ${segments.map((segment) =>
632
- renderMySqlJsonPath([segment], state, dialect)
633
- ).join(", ")})`
784
+ return `json_remove(${renderExpression(base, state, dialect)}, ${renderMySqlJsonPath(segments, state, dialect)})`
634
785
  }
635
786
  return undefined
636
787
  }
@@ -654,8 +805,16 @@ const renderJsonExpression = (
654
805
  return `${functionName}(${renderPostgresJsonValue(base, state, dialect)}, ${renderPostgresJsonPathArray(segments, state, dialect)}, ${renderPostgresJsonValue(nextValue, state, dialect)}${extra})`
655
806
  }
656
807
  if (dialect.name === "mysql") {
657
- const functionName = kind === "jsonInsert" ? "json_insert" : "json_set"
658
- 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})`
659
818
  }
660
819
  return undefined
661
820
  }
@@ -754,16 +913,7 @@ const renderMysqlMutationLock = (
754
913
  if (!lock) {
755
914
  return ""
756
915
  }
757
- switch (lock.mode) {
758
- case "lowPriority":
759
- return " low_priority"
760
- case "ignore":
761
- return " ignore"
762
- case "quick":
763
- return statement === "delete" ? " quick" : ""
764
- default:
765
- return ""
766
- }
916
+ return renderMysqlMutationLockMode(lock.mode, statement)
767
917
  }
768
918
 
769
919
  const renderTransactionClause = (
@@ -773,8 +923,9 @@ const renderTransactionClause = (
773
923
  switch (clause.kind) {
774
924
  case "transaction": {
775
925
  const modes: string[] = []
776
- if (clause.isolationLevel) {
777
- modes.push(`isolation level ${clause.isolationLevel}`)
926
+ const isolationLevel = renderTransactionIsolationLevel(clause.isolationLevel)
927
+ if (isolationLevel) {
928
+ modes.push(isolationLevel)
778
929
  }
779
930
  if (clause.readOnly === true) {
780
931
  modes.push("read only")
@@ -794,7 +945,7 @@ const renderTransactionClause = (
794
945
  case "releaseSavepoint":
795
946
  return `release savepoint ${dialect.quoteIdentifier(clause.name)}`
796
947
  }
797
- return ""
948
+ throw new Error("Unsupported transaction statement kind")
798
949
  }
799
950
 
800
951
  const renderSelectionList = (
@@ -807,6 +958,9 @@ const renderSelectionList = (
807
958
  validateAggregationSelection(selection as SelectionValue, [])
808
959
  }
809
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
+ }
810
964
  const projections = selectionProjections(selection)
811
965
  const sql = flattened.map(({ expression, alias }) =>
812
966
  `${renderSelectSql(renderExpression(expression, state, dialect), expressionDriverContext(expression, state, dialect))} as ${dialect.quoteIdentifier(alias)}`).join(", ")
@@ -816,10 +970,105 @@ const renderSelectionList = (
816
970
  }
817
971
  }
818
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
+
819
1067
  export const renderQueryAst = (
820
1068
  ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
821
1069
  state: RenderState,
822
- dialect: SqlDialect
1070
+ dialect: SqlDialect,
1071
+ options: { readonly emitCtes?: boolean } = {}
823
1072
  ): RenderedQueryAst => {
824
1073
  let sql = ""
825
1074
  let projections: readonly Projection[] = []
@@ -829,15 +1078,19 @@ export const renderQueryAst = (
829
1078
  validateAggregationSelection(ast.select as SelectionValue, ast.groupBy)
830
1079
  const rendered = renderSelectionList(ast.select as Record<string, unknown>, state, dialect, false)
831
1080
  projections = rendered.projections
1081
+ const selectList = rendered.sql.length > 0 ? ` ${rendered.sql}` : ""
832
1082
  const clauses = [
833
1083
  ast.distinctOn && ast.distinctOn.length > 0
834
- ? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")}) ${rendered.sql}`
835
- : `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}`
836
1086
  ]
837
1087
  if (ast.from) {
838
1088
  clauses.push(`from ${renderSourceReference(ast.from.source, ast.from.tableName, ast.from.baseTableName, state, dialect)}`)
839
1089
  }
840
1090
  for (const join of ast.joins) {
1091
+ if (dialect.name === "mysql" && join.kind === "full") {
1092
+ throw new Error("Unsupported mysql full join")
1093
+ }
841
1094
  const source = renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
842
1095
  clauses.push(
843
1096
  join.kind === "cross"
@@ -864,8 +1117,11 @@ export const renderQueryAst = (
864
1117
  clauses.push(`offset ${renderExpression(ast.offset, state, dialect)}`)
865
1118
  }
866
1119
  if (ast.lock) {
1120
+ if (ast.lock.nowait && ast.lock.skipLocked) {
1121
+ throw new Error("lock(...) cannot specify both nowait and skipLocked")
1122
+ }
867
1123
  clauses.push(
868
- `${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" : ""}`
869
1125
  )
870
1126
  }
871
1127
  sql = clauses.join(" ")
@@ -873,6 +1129,7 @@ export const renderQueryAst = (
873
1129
  }
874
1130
  case "set": {
875
1131
  const setAst = ast as QueryAst.Ast<Record<string, unknown>, any, "set">
1132
+ assertNoStatementQueryClauses(setAst, "set", { allowSelection: true })
876
1133
  const base = renderQueryAst(
877
1134
  Query.getAst(setAst.setBase as Query.Plan.Any) as QueryAst.Ast<
878
1135
  Record<string, unknown>,
@@ -883,6 +1140,7 @@ export const renderQueryAst = (
883
1140
  dialect
884
1141
  )
885
1142
  projections = selectionProjections(setAst.select as Record<string, unknown>)
1143
+ assertMatchingSetProjections(projections, base.projections)
886
1144
  sql = [
887
1145
  `(${base.sql})`,
888
1146
  ...(setAst.setOperations ?? []).map((entry) => {
@@ -895,6 +1153,7 @@ export const renderQueryAst = (
895
1153
  state,
896
1154
  dialect
897
1155
  )
1156
+ assertMatchingSetProjections(projections, rendered.projections)
898
1157
  return `${entry.kind}${entry.all ? " all" : ""} (${rendered.sql})`
899
1158
  })
900
1159
  ].join(" ")
@@ -902,19 +1161,26 @@ export const renderQueryAst = (
902
1161
  }
903
1162
  case "insert": {
904
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)
905
1169
  const targetSource = insertAst.into!
906
1170
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1171
+ const insertSource = expectInsertSourceKind(insertAst.insertSource)
1172
+ const conflict = expectConflictClause(insertAst.conflict)
907
1173
  sql = `insert into ${target}`
908
- if (insertAst.insertSource?.kind === "values") {
909
- const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
910
- 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) =>
911
1177
  `(${row.values.map((entry) => renderExpression(entry.value, state, dialect)).join(", ")})`
912
1178
  ).join(", ")
913
1179
  sql += ` (${columns}) values ${rows}`
914
- } else if (insertAst.insertSource?.kind === "query") {
915
- 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(", ")
916
1182
  const renderedQuery = renderQueryAst(
917
- Query.getAst(insertAst.insertSource.query as Query.Plan.Any) as QueryAst.Ast<
1183
+ Query.getAst(insertSource.query as Query.Plan.Any) as QueryAst.Ast<
918
1184
  Record<string, unknown>,
919
1185
  any,
920
1186
  QueryAst.QueryStatement
@@ -923,26 +1189,25 @@ export const renderQueryAst = (
923
1189
  dialect
924
1190
  )
925
1191
  sql += ` (${columns}) ${renderedQuery.sql}`
926
- } else if (insertAst.insertSource?.kind === "unnest") {
927
- const unnestSource = insertAst.insertSource
928
- 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(", ")
929
1194
  if (dialect.name === "postgres") {
930
1195
  const table = targetSource.source as Table.AnyTable
931
1196
  const fields = table[Table.TypeId].fields
932
- const rendered = unnestSource.values.map((entry) =>
1197
+ const rendered = insertSource.values.map((entry) =>
933
1198
  `cast(${dialect.renderLiteral(encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect), state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
934
1199
  ).join(", ")
935
1200
  sql += ` (${columns}) select * from unnest(${rendered})`
936
1201
  } else {
937
- 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
938
1209
  const rows = Array.from({ length: rowCount }, (_, index) =>
939
- `(${unnestSource.values.map((entry) =>
940
- dialect.renderLiteral(
941
- entry.values[index],
942
- state,
943
- (targetSource.source as Table.AnyTable)[Table.TypeId].fields[entry.columnName]![Expression.TypeId]
944
- )
945
- ).join(", ")})`
1210
+ `(${encodedValues.map((entry) => dialect.renderLiteral(entry.values[index], state)).join(", ")})`
946
1211
  ).join(", ")
947
1212
  sql += ` (${columns}) values ${rows}`
948
1213
  }
@@ -952,30 +1217,39 @@ export const renderQueryAst = (
952
1217
  if ((insertAst.values ?? []).length > 0) {
953
1218
  sql += ` (${columns}) values (${values})`
954
1219
  } else {
955
- sql += " default values"
1220
+ sql += " () values ()"
956
1221
  }
957
1222
  }
958
- if (insertAst.conflict) {
959
- 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) =>
960
1228
  `${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
961
1229
  ).join(", ")
962
1230
  if (dialect.name === "postgres") {
963
- const targetSql = insertAst.conflict.target?.kind === "constraint"
964
- ? ` on conflict on constraint ${dialect.quoteIdentifier(insertAst.conflict.target.name)}`
965
- : insertAst.conflict.target?.kind === "columns"
966
- ? ` 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)}` : ""}`
967
1235
  : " on conflict"
968
1236
  sql += targetSql
969
- sql += insertAst.conflict.action === "doNothing"
1237
+ sql += conflict.action === "doNothing"
970
1238
  ? " do nothing"
971
- : ` do update set ${updateValues}${insertAst.conflict.where ? ` where ${renderExpression(insertAst.conflict.where, state, dialect)}` : ""}`
972
- } 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") {
973
1241
  sql = sql.replace(/^insert/, "insert ignore")
974
1242
  } else {
975
1243
  sql += ` on duplicate key update ${updateValues}`
976
1244
  }
977
1245
  }
978
- 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
+ }
979
1253
  projections = returning.projections
980
1254
  if (returning.sql.length > 0) {
981
1255
  sql += ` returning ${returning.sql}`
@@ -984,19 +1258,33 @@ export const renderQueryAst = (
984
1258
  }
985
1259
  case "update": {
986
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
+ }
987
1268
  const targetSource = updateAst.target!
988
1269
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
989
1270
  const targets = updateAst.targets ?? [targetSource]
990
1271
  const fromSources = updateAst.fromSources ?? []
1272
+ if ((updateAst.set ?? []).length === 0) {
1273
+ throw new Error("update statements require at least one assignment")
1274
+ }
991
1275
  const assignments = updateAst.set!.map((entry) =>
992
1276
  renderMutationAssignment(entry, state, dialect)).join(", ")
993
1277
  if (dialect.name === "mysql") {
994
1278
  const modifiers = renderMysqlMutationLock(updateAst.lock, "update")
995
1279
  const extraSources = renderFromSources(fromSources, state, dialect)
996
1280
  const joinSources = updateAst.joins.map((join) =>
997
- join.kind === "cross"
998
- ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
999
- : `${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)}`
1000
1288
  ).join(" ")
1001
1289
  const targetList = [
1002
1290
  ...targets.map((entry) =>
@@ -1028,7 +1316,13 @@ export const renderQueryAst = (
1028
1316
  if (dialect.name === "mysql" && updateAst.limit) {
1029
1317
  sql += ` limit ${renderMysqlMutationLimit(updateAst.limit, state, dialect)}`
1030
1318
  }
1031
- 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
+ }
1032
1326
  projections = returning.projections
1033
1327
  if (returning.sql.length > 0) {
1034
1328
  sql += ` returning ${returning.sql}`
@@ -1037,6 +1331,13 @@ export const renderQueryAst = (
1037
1331
  }
1038
1332
  case "delete": {
1039
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
+ }
1040
1341
  const targetSource = deleteAst.target!
1041
1342
  const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
1042
1343
  const targets = deleteAst.targets ?? [targetSource]
@@ -1048,9 +1349,13 @@ export const renderQueryAst = (
1048
1349
  renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
1049
1350
  ).join(", ")
1050
1351
  const joinSources = deleteAst.joins.map((join) =>
1051
- join.kind === "cross"
1052
- ? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
1053
- : `${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)}`
1054
1359
  ).join(" ")
1055
1360
  sql = hasJoinedSources
1056
1361
  ? `delete${modifiers} ${targetList} from ${fromSources}${joinSources.length > 0 ? ` ${joinSources}` : ""}`
@@ -1074,7 +1379,13 @@ export const renderQueryAst = (
1074
1379
  if (dialect.name === "mysql" && deleteAst.limit) {
1075
1380
  sql += ` limit ${renderMysqlMutationLimit(deleteAst.limit, state, dialect)}`
1076
1381
  }
1077
- 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
+ }
1078
1389
  projections = returning.projections
1079
1390
  if (returning.sql.length > 0) {
1080
1391
  sql += ` returning ${returning.sql}`
@@ -1083,12 +1394,17 @@ export const renderQueryAst = (
1083
1394
  }
1084
1395
  case "truncate": {
1085
1396
  const truncateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "truncate">
1397
+ assertNoStatementQueryClauses(truncateAst, "truncate")
1398
+ const truncate = expectTruncateClause(truncateAst.truncate)
1086
1399
  const targetSource = truncateAst.target!
1400
+ if (dialect.name === "mysql" && (truncate.restartIdentity || truncate.cascade)) {
1401
+ throw new Error("Unsupported mysql truncate options")
1402
+ }
1087
1403
  sql = `truncate table ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
1088
- if (truncateAst.truncate?.restartIdentity) {
1404
+ if (truncate.restartIdentity) {
1089
1405
  sql += " restart identity"
1090
1406
  }
1091
- if (truncateAst.truncate?.cascade) {
1407
+ if (truncate.cascade) {
1092
1408
  sql += " cascade"
1093
1409
  }
1094
1410
  break
@@ -1101,6 +1417,9 @@ export const renderQueryAst = (
1101
1417
  const targetSource = mergeAst.target!
1102
1418
  const usingSource = mergeAst.using!
1103
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
+ }
1104
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)}`
1105
1424
  if (merge.whenMatched) {
1106
1425
  sql += " when matched"
@@ -1130,25 +1449,30 @@ export const renderQueryAst = (
1130
1449
  case "savepoint":
1131
1450
  case "rollbackTo":
1132
1451
  case "releaseSavepoint": {
1452
+ assertNoStatementQueryClauses(ast, ast.kind)
1133
1453
  sql = renderTransactionClause(ast.transaction!, dialect)
1134
1454
  break
1135
1455
  }
1136
1456
  case "createTable": {
1137
1457
  const createTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createTable">
1138
- 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)
1139
1461
  break
1140
1462
  }
1141
1463
  case "dropTable": {
1142
1464
  const dropTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropTable">
1143
- const ifExists = dropTableAst.ddl?.kind === "dropTable" && dropTableAst.ddl.ifExists
1144
- 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)}`
1145
1468
  break
1146
1469
  }
1147
1470
  case "createIndex": {
1148
1471
  const createIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createIndex">
1472
+ assertNoStatementQueryClauses(createIndexAst, "createIndex")
1149
1473
  sql = renderCreateIndexSql(
1150
1474
  createIndexAst.target!,
1151
- createIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
1475
+ expectDdlClauseKind(createIndexAst.ddl, "createIndex"),
1152
1476
  state,
1153
1477
  dialect
1154
1478
  )
@@ -1156,17 +1480,20 @@ export const renderQueryAst = (
1156
1480
  }
1157
1481
  case "dropIndex": {
1158
1482
  const dropIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropIndex">
1483
+ assertNoStatementQueryClauses(dropIndexAst, "dropIndex")
1159
1484
  sql = renderDropIndexSql(
1160
1485
  dropIndexAst.target!,
1161
- dropIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
1486
+ expectDdlClauseKind(dropIndexAst.ddl, "dropIndex"),
1162
1487
  state,
1163
1488
  dialect
1164
1489
  )
1165
1490
  break
1166
1491
  }
1492
+ default:
1493
+ throw new Error("Unsupported query statement kind")
1167
1494
  }
1168
1495
 
1169
- if (state.ctes.length === 0) {
1496
+ if (state.ctes.length === 0 || options.emitCtes === false) {
1170
1497
  return {
1171
1498
  sql,
1172
1499
  projections
@@ -1214,9 +1541,27 @@ const renderSourceReference = (
1214
1541
  readonly plan: Query.Plan.Any
1215
1542
  readonly recursive?: boolean
1216
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
+ }
1217
1548
  if (!state.cteNames.has(cte.name)) {
1218
1549
  state.cteNames.add(cte.name)
1219
- 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
+ )
1220
1565
  state.ctes.push({
1221
1566
  name: cte.name,
1222
1567
  sql: rendered.sql,
@@ -1233,14 +1578,14 @@ const renderSourceReference = (
1233
1578
  if (!state.cteNames.has(derived.name)) {
1234
1579
  // derived tables are inlined, so no CTE registration is needed
1235
1580
  }
1236
- 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)}`
1237
1582
  }
1238
1583
  if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "lateral") {
1239
1584
  const lateral = source as unknown as {
1240
1585
  readonly name: string
1241
1586
  readonly plan: Query.Plan.Any
1242
1587
  }
1243
- 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)}`
1244
1589
  }
1245
1590
  if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "values") {
1246
1591
  const values = source as unknown as {
@@ -1275,6 +1620,22 @@ const renderSourceReference = (
1275
1620
  return dialect.renderTableReference(tableName, baseTableName, schemaName)
1276
1621
  }
1277
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
+
1278
1639
  /**
1279
1640
  * Renders a scalar expression AST into SQL text.
1280
1641
  *
@@ -1309,10 +1670,13 @@ export const renderExpression = (
1309
1670
  : ">="
1310
1671
  switch (ast.kind) {
1311
1672
  case "column":
1312
- return ast.tableName.length === 0
1673
+ return state.rowLocalColumns || ast.tableName.length === 0
1313
1674
  ? dialect.quoteIdentifier(ast.columnName)
1314
1675
  : `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
1315
1676
  case "literal":
1677
+ if (typeof ast.value === "number" && !Number.isFinite(ast.value)) {
1678
+ throw new Error("Expected a finite numeric value")
1679
+ }
1316
1680
  return dialect.renderLiteral(ast.value, state, expression[Expression.TypeId])
1317
1681
  case "excluded":
1318
1682
  return dialect.name === "mysql"
@@ -1375,7 +1739,7 @@ export const renderExpression = (
1375
1739
  return `(${left} @> ${right})`
1376
1740
  }
1377
1741
  if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1378
- 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)})`
1379
1743
  }
1380
1744
  throw new Error("Unsupported container operator for SQL rendering")
1381
1745
  case "containedBy":
@@ -1389,7 +1753,7 @@ export const renderExpression = (
1389
1753
  return `(${left} <@ ${right})`
1390
1754
  }
1391
1755
  if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1392
- 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)})`
1393
1757
  }
1394
1758
  throw new Error("Unsupported container operator for SQL rendering")
1395
1759
  case "overlaps":
@@ -1403,7 +1767,7 @@ export const renderExpression = (
1403
1767
  return `(${left} && ${right})`
1404
1768
  }
1405
1769
  if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
1406
- 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)})`
1407
1771
  }
1408
1772
  throw new Error("Unsupported container operator for SQL rendering")
1409
1773
  case "isNull":
@@ -1423,14 +1787,26 @@ export const renderExpression = (
1423
1787
  case "min":
1424
1788
  return `min(${renderExpression(ast.value, state, dialect)})`
1425
1789
  case "and":
1790
+ if (ast.values.length === 0) {
1791
+ throw new Error("and(...) requires at least one predicate")
1792
+ }
1426
1793
  return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" and ")})`
1427
1794
  case "or":
1795
+ if (ast.values.length === 0) {
1796
+ throw new Error("or(...) requires at least one predicate")
1797
+ }
1428
1798
  return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" or ")})`
1429
1799
  case "coalesce":
1430
1800
  return `coalesce(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")})`
1431
1801
  case "in":
1802
+ if (ast.values.length < 2) {
1803
+ throw new Error("in(...) requires at least one candidate value")
1804
+ }
1432
1805
  return `(${renderExpression(ast.values[0]!, state, dialect)} in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1433
1806
  case "notIn":
1807
+ if (ast.values.length < 2) {
1808
+ throw new Error("notIn(...) requires at least one candidate value")
1809
+ }
1434
1810
  return `(${renderExpression(ast.values[0]!, state, dialect)} not in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
1435
1811
  case "between":
1436
1812
  return `(${renderExpression(ast.values[0]!, state, dialect)} between ${renderExpression(ast.values[1]!, state, dialect)} and ${renderExpression(ast.values[2]!, state, dialect)})`
@@ -1441,35 +1817,15 @@ export const renderExpression = (
1441
1817
  `when ${renderExpression(branch.when, state, dialect)} then ${renderExpression(branch.then, state, dialect)}`
1442
1818
  ).join(" ")} else ${renderExpression(ast.else, state, dialect)} end`
1443
1819
  case "exists":
1444
- return `exists (${renderQueryAst(
1445
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1446
- state,
1447
- dialect
1448
- ).sql})`
1820
+ return `exists (${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
1449
1821
  case "scalarSubquery":
1450
- return `(${renderQueryAst(
1451
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1452
- state,
1453
- dialect
1454
- ).sql})`
1822
+ return `(${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
1455
1823
  case "inSubquery":
1456
- return `(${renderExpression(ast.left, state, dialect)} in (${renderQueryAst(
1457
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1458
- state,
1459
- dialect
1460
- ).sql}))`
1824
+ return `(${renderExpression(ast.left, state, dialect)} in (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1461
1825
  case "comparisonAny":
1462
- return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderQueryAst(
1463
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1464
- state,
1465
- dialect
1466
- ).sql}))`
1826
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1467
1827
  case "comparisonAll":
1468
- return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderQueryAst(
1469
- Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
1470
- state,
1471
- dialect
1472
- ).sql}))`
1828
+ return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
1473
1829
  case "window": {
1474
1830
  if (!Array.isArray(ast.partitionBy) || !Array.isArray(ast.orderBy) || typeof ast.function !== "string") {
1475
1831
  break