effect-qb 0.16.0 → 0.19.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.
- package/README.md +4 -0
- package/dist/index.js +8065 -0
- package/dist/mysql.js +4036 -2418
- package/dist/postgres/metadata.js +2536 -625
- package/dist/postgres.js +8248 -7857
- package/dist/sqlite.js +8854 -0
- package/dist/standard.js +8019 -0
- package/package.json +15 -3
- package/src/casing.ts +71 -0
- package/src/index.ts +2 -0
- package/src/internal/casing.ts +89 -0
- package/src/internal/column-state.ts +11 -6
- package/src/internal/column.ts +44 -7
- package/src/internal/datatypes/define.ts +2 -1
- package/src/internal/datatypes/enrich.ts +23 -0
- package/src/internal/datatypes/lookup.ts +14 -7
- package/src/internal/derived-table.ts +7 -13
- package/src/internal/dialect-renderers/mysql.ts +2046 -0
- package/src/{postgres/internal/sql-expression-renderer.ts → internal/dialect-renderers/postgres.ts} +867 -283
- package/src/{mysql/internal/sql-expression-renderer.ts → internal/dialect-renderers/sqlite.ts} +834 -358
- package/src/internal/dialect.ts +37 -0
- package/src/internal/dsl-mutation-runtime.ts +29 -10
- package/src/internal/dsl-plan-runtime.ts +41 -24
- package/src/internal/dsl-query-runtime.ts +11 -31
- package/src/internal/dsl-transaction-ddl-runtime.ts +61 -15
- package/src/internal/executor.ts +57 -15
- package/src/internal/expression-ast.ts +3 -2
- package/src/internal/grouping-key.ts +216 -9
- package/src/internal/implication-runtime.ts +3 -2
- package/src/internal/json/types.ts +155 -40
- package/src/internal/predicate/context.ts +14 -1
- package/src/internal/predicate/key.ts +19 -2
- package/src/internal/predicate/runtime.ts +30 -3
- package/src/internal/query.d.ts +38 -11
- package/src/internal/query.ts +315 -54
- package/src/internal/renderer.ts +51 -6
- package/src/internal/runtime/driver-value-mapping.ts +58 -0
- package/src/internal/runtime/normalize.ts +74 -43
- package/src/internal/runtime/schema.ts +5 -3
- package/src/internal/runtime/value.ts +153 -30
- package/src/internal/scalar.ts +6 -1
- package/src/internal/schema-derivation.d.ts +12 -61
- package/src/internal/schema-derivation.ts +90 -38
- package/src/internal/schema-expression.ts +2 -2
- package/src/internal/sql-expression-renderer.ts +19 -0
- package/src/internal/standard-dsl.ts +6885 -0
- package/src/internal/table-options.ts +229 -62
- package/src/internal/table.d.ts +33 -32
- package/src/internal/table.ts +469 -160
- package/src/mysql/column-extension.ts +3 -0
- package/src/mysql/column.ts +27 -12
- package/src/mysql/datatypes/index.ts +24 -2
- package/src/mysql/errors/catalog.ts +5 -5
- package/src/mysql/errors/normalize.ts +2 -2
- package/src/mysql/executor.ts +7 -5
- package/src/mysql/internal/dialect.ts +9 -4
- package/src/mysql/internal/dsl.ts +906 -324
- package/src/mysql/internal/renderer.ts +7 -2
- package/src/mysql/json.ts +37 -0
- package/src/mysql/query-extension.ts +16 -0
- package/src/mysql/query.ts +9 -2
- package/src/mysql/renderer.ts +31 -4
- package/src/mysql.ts +4 -12
- package/src/postgres/column-extension.ts +28 -0
- package/src/postgres/column.ts +9 -13
- package/src/postgres/datatypes/index.d.ts +2 -1
- package/src/postgres/datatypes/index.ts +3 -2
- package/src/postgres/errors/normalize.ts +2 -2
- package/src/postgres/executor.ts +55 -10
- package/src/postgres/function/core.ts +20 -4
- package/src/postgres/function/index.ts +1 -17
- package/src/postgres/internal/dialect.ts +9 -4
- package/src/postgres/internal/dsl.ts +850 -359
- package/src/postgres/internal/renderer.ts +7 -2
- package/src/postgres/internal/schema-ddl.ts +22 -9
- package/src/postgres/internal/schema-model.ts +244 -10
- package/src/postgres/json.ts +100 -24
- package/src/postgres/jsonb.ts +38 -0
- package/src/postgres/query-extension.ts +2 -0
- package/src/postgres/query.ts +9 -2
- package/src/postgres/renderer.ts +31 -4
- package/src/postgres/schema-management.ts +108 -16
- package/src/postgres/schema.ts +98 -15
- package/src/postgres/table.ts +203 -398
- package/src/postgres/type.ts +8 -7
- package/src/postgres.ts +9 -11
- package/src/sqlite/column-extension.ts +3 -0
- package/src/sqlite/column.ts +127 -0
- package/src/sqlite/datatypes/index.ts +80 -0
- package/src/sqlite/datatypes/spec.ts +98 -0
- package/src/sqlite/errors/catalog.ts +103 -0
- package/src/sqlite/errors/fields.ts +19 -0
- package/src/sqlite/errors/index.ts +19 -0
- package/src/sqlite/errors/normalize.ts +229 -0
- package/src/sqlite/errors/requirements.ts +71 -0
- package/src/sqlite/errors/types.ts +29 -0
- package/src/sqlite/executor.ts +229 -0
- package/src/sqlite/function/aggregate.ts +2 -0
- package/src/sqlite/function/core.ts +2 -0
- package/src/sqlite/function/index.ts +19 -0
- package/src/sqlite/function/string.ts +2 -0
- package/src/sqlite/function/temporal.ts +100 -0
- package/src/sqlite/function/window.ts +2 -0
- package/src/sqlite/internal/dialect.ts +42 -0
- package/src/sqlite/internal/dsl.ts +6979 -0
- package/src/sqlite/internal/renderer.ts +51 -0
- package/src/sqlite/json.ts +39 -0
- package/src/sqlite/query-extension.ts +2 -0
- package/src/sqlite/query.ts +196 -0
- package/src/sqlite/renderer.ts +51 -0
- package/src/sqlite.ts +14 -0
- package/src/standard/column.ts +163 -0
- package/src/standard/datatypes/index.ts +83 -0
- package/src/standard/datatypes/spec.ts +98 -0
- package/src/standard/dialect.ts +40 -0
- package/src/standard/function/aggregate.ts +2 -0
- package/src/standard/function/core.ts +2 -0
- package/src/standard/function/index.ts +18 -0
- package/src/standard/function/string.ts +2 -0
- package/src/standard/function/temporal.ts +78 -0
- package/src/standard/function/window.ts +2 -0
- package/src/standard/internal/renderer.ts +45 -0
- package/src/standard/query.ts +152 -0
- package/src/standard/renderer.ts +21 -0
- package/src/standard/table.ts +147 -0
- package/src/standard.ts +18 -0
- package/src/internal/aggregation-validation.ts +0 -57
- package/src/mysql/table.ts +0 -157
|
@@ -0,0 +1,2046 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema"
|
|
2
|
+
|
|
3
|
+
import * as Query from "../query.js"
|
|
4
|
+
import * as Expression from "../scalar.js"
|
|
5
|
+
import * as Table from "../table.js"
|
|
6
|
+
import * as QueryAst from "../query-ast.js"
|
|
7
|
+
import { renderDbTypeName, type RenderState, type RenderValueContext, type SqlDialect } from "../dialect.js"
|
|
8
|
+
import * as ExpressionAst from "../expression-ast.js"
|
|
9
|
+
import * as JsonPath from "../json/path.js"
|
|
10
|
+
import { renderMysqlMutationLockMode, renderSelectLockMode } from "../dsl-plan-runtime.js"
|
|
11
|
+
import { expectConflictClause } from "../dsl-mutation-runtime.js"
|
|
12
|
+
import { expectDdlClauseKind, expectTruncateClause, normalizeStatementFlag, normalizeStatementIdentifier, renderTransactionIsolationLevel } from "../dsl-transaction-ddl-runtime.js"
|
|
13
|
+
import {
|
|
14
|
+
renderJsonSelectSql,
|
|
15
|
+
renderSelectSql,
|
|
16
|
+
toDriverValue
|
|
17
|
+
} from "../runtime/driver-value-mapping.js"
|
|
18
|
+
import { normalizeDbValue } from "../runtime/normalize.js"
|
|
19
|
+
import { flattenSelection, type Projection } from "../projections.js"
|
|
20
|
+
import * as SchemaExpression from "../schema-expression.js"
|
|
21
|
+
import { renderReferentialAction, validateOptions, type DdlExpressionLike, type TableOptionSpec } from "../table-options.js"
|
|
22
|
+
import * as Casing from "../casing.js"
|
|
23
|
+
|
|
24
|
+
const renderDbType = (
|
|
25
|
+
dialect: SqlDialect,
|
|
26
|
+
dbType: Expression.DbType.Any
|
|
27
|
+
): string => {
|
|
28
|
+
if (dialect.name === "mysql" && dbType.kind === "uuid") {
|
|
29
|
+
return "char(36)"
|
|
30
|
+
}
|
|
31
|
+
return renderDbTypeName(dbType.kind)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isArrayDbType = (dbType: Expression.DbType.Any): boolean =>
|
|
35
|
+
"element" in dbType
|
|
36
|
+
|
|
37
|
+
const renderCastType = (
|
|
38
|
+
dialect: SqlDialect,
|
|
39
|
+
dbType: unknown
|
|
40
|
+
): string => {
|
|
41
|
+
const kind = (dbType as { readonly kind?: string } | undefined)?.kind as string
|
|
42
|
+
if (dialect.name !== "mysql") {
|
|
43
|
+
return renderDbTypeName(kind)
|
|
44
|
+
}
|
|
45
|
+
switch (kind) {
|
|
46
|
+
case "text":
|
|
47
|
+
return "char"
|
|
48
|
+
case "uuid":
|
|
49
|
+
return "char(36)"
|
|
50
|
+
case "numeric":
|
|
51
|
+
return "decimal"
|
|
52
|
+
case "timestamp":
|
|
53
|
+
return "datetime"
|
|
54
|
+
case "bool":
|
|
55
|
+
case "boolean":
|
|
56
|
+
return "boolean"
|
|
57
|
+
case "json":
|
|
58
|
+
return "json"
|
|
59
|
+
default:
|
|
60
|
+
return renderDbTypeName(kind)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const renderMysqlDdlString = (value: string): string =>
|
|
65
|
+
`'${value.replaceAll("'", "''")}'`
|
|
66
|
+
|
|
67
|
+
const renderMysqlDdlBytes = (value: Uint8Array): string =>
|
|
68
|
+
`x'${Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("")}'`
|
|
69
|
+
|
|
70
|
+
const renderMysqlDdlLiteral = (
|
|
71
|
+
value: unknown,
|
|
72
|
+
state: RenderState,
|
|
73
|
+
context: RenderValueContext = {}
|
|
74
|
+
): string => {
|
|
75
|
+
const driverValue = toDriverValue(value, {
|
|
76
|
+
dialect: "mysql",
|
|
77
|
+
valueMappings: state.valueMappings,
|
|
78
|
+
...context
|
|
79
|
+
})
|
|
80
|
+
if (driverValue === null) {
|
|
81
|
+
return "null"
|
|
82
|
+
}
|
|
83
|
+
switch (typeof driverValue) {
|
|
84
|
+
case "boolean":
|
|
85
|
+
return driverValue ? "true" : "false"
|
|
86
|
+
case "number":
|
|
87
|
+
if (!Number.isFinite(driverValue)) {
|
|
88
|
+
throw new Error("Expected a finite numeric value")
|
|
89
|
+
}
|
|
90
|
+
return String(driverValue)
|
|
91
|
+
case "bigint":
|
|
92
|
+
return driverValue.toString()
|
|
93
|
+
case "string":
|
|
94
|
+
return renderMysqlDdlString(driverValue)
|
|
95
|
+
case "object":
|
|
96
|
+
if (driverValue instanceof Uint8Array) {
|
|
97
|
+
return renderMysqlDdlBytes(driverValue)
|
|
98
|
+
}
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
throw new Error("Unsupported mysql DDL literal value")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const renderDdlExpression = (
|
|
105
|
+
expression: DdlExpressionLike,
|
|
106
|
+
state: RenderState,
|
|
107
|
+
dialect: SqlDialect
|
|
108
|
+
): string => {
|
|
109
|
+
if (SchemaExpression.isSchemaExpression(expression)) {
|
|
110
|
+
return SchemaExpression.render(expression)
|
|
111
|
+
}
|
|
112
|
+
return renderExpression(expression, state, {
|
|
113
|
+
...dialect,
|
|
114
|
+
renderLiteral: renderMysqlDdlLiteral
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const renderMysqlMutationLimit = (
|
|
119
|
+
expression: Expression.Any,
|
|
120
|
+
state: RenderState,
|
|
121
|
+
dialect: SqlDialect
|
|
122
|
+
): string => {
|
|
123
|
+
const ast = (expression as Expression.Any & {
|
|
124
|
+
readonly [ExpressionAst.TypeId]: ExpressionAst.Any
|
|
125
|
+
})[ExpressionAst.TypeId]
|
|
126
|
+
if (ast.kind === "literal" && typeof ast.value === "number" && Number.isInteger(ast.value) && ast.value >= 0) {
|
|
127
|
+
return String(ast.value)
|
|
128
|
+
}
|
|
129
|
+
return renderExpression(expression, state, dialect)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const casingForTable = (
|
|
133
|
+
table: Table.AnyTable,
|
|
134
|
+
state: RenderState
|
|
135
|
+
): Casing.Options | undefined =>
|
|
136
|
+
Casing.merge(state.casing, table[Table.TypeId].casing)
|
|
137
|
+
|
|
138
|
+
const casedColumnName = (
|
|
139
|
+
columnName: string,
|
|
140
|
+
state: RenderState,
|
|
141
|
+
tableName?: string
|
|
142
|
+
): string => {
|
|
143
|
+
if (tableName !== undefined) {
|
|
144
|
+
const mapped = state.sourceNames?.get(tableName)?.columns.get(columnName)
|
|
145
|
+
if (mapped !== undefined) {
|
|
146
|
+
return mapped
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return Casing.applyCategory(state.casing, "columns", columnName)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const casedTableReferenceName = (
|
|
153
|
+
tableName: string,
|
|
154
|
+
state: RenderState
|
|
155
|
+
): string =>
|
|
156
|
+
state.sourceNames?.get(tableName)?.tableName ?? Casing.applyCategory(state.casing, "tables", tableName)
|
|
157
|
+
|
|
158
|
+
const quoteColumn = (
|
|
159
|
+
columnName: string,
|
|
160
|
+
state: RenderState,
|
|
161
|
+
dialect: SqlDialect,
|
|
162
|
+
tableName?: string
|
|
163
|
+
): string => dialect.quoteIdentifier(casedColumnName(columnName, state, tableName))
|
|
164
|
+
|
|
165
|
+
const stateWithTableCasing = (
|
|
166
|
+
state: RenderState,
|
|
167
|
+
source: unknown
|
|
168
|
+
): RenderState =>
|
|
169
|
+
typeof source === "object" && source !== null && Table.TypeId in source
|
|
170
|
+
? { ...state, casing: casingForTable(source as Table.AnyTable, state) }
|
|
171
|
+
: state
|
|
172
|
+
|
|
173
|
+
const referenceCasing = (
|
|
174
|
+
reference: { readonly casing?: Casing.Options },
|
|
175
|
+
state: RenderState
|
|
176
|
+
): Casing.Options | undefined =>
|
|
177
|
+
Casing.merge(state.casing, reference.casing)
|
|
178
|
+
|
|
179
|
+
const renderReferenceTable = (
|
|
180
|
+
reference: {
|
|
181
|
+
readonly tableName: string
|
|
182
|
+
readonly schemaName?: string
|
|
183
|
+
readonly casing?: Casing.Options
|
|
184
|
+
},
|
|
185
|
+
state: RenderState,
|
|
186
|
+
dialect: SqlDialect
|
|
187
|
+
): string => {
|
|
188
|
+
const casing = referenceCasing(reference, state)
|
|
189
|
+
const tableName = Casing.applyCategory(casing, "tables", reference.tableName)
|
|
190
|
+
const schemaName = reference.schemaName === undefined
|
|
191
|
+
? undefined
|
|
192
|
+
: Casing.applyCategory(casing, "schemas", reference.schemaName)
|
|
193
|
+
return dialect.renderTableReference(tableName, tableName, schemaName)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const quoteReferenceColumn = (
|
|
197
|
+
columnName: string,
|
|
198
|
+
reference: { readonly casing?: Casing.Options },
|
|
199
|
+
state: RenderState,
|
|
200
|
+
dialect: SqlDialect
|
|
201
|
+
): string =>
|
|
202
|
+
dialect.quoteIdentifier(Casing.applyCategory(referenceCasing(reference, state), "columns", columnName))
|
|
203
|
+
|
|
204
|
+
const registerSourceReference = (
|
|
205
|
+
source: unknown,
|
|
206
|
+
tableName: string,
|
|
207
|
+
state: RenderState
|
|
208
|
+
): void => {
|
|
209
|
+
if (typeof source !== "object" || source === null) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
if (Table.TypeId in source) {
|
|
213
|
+
const table = source as Table.AnyTable
|
|
214
|
+
const tableState = table[Table.TypeId]
|
|
215
|
+
const casing = casingForTable(table, state)
|
|
216
|
+
const renderedTableName = tableState.kind === "alias"
|
|
217
|
+
? tableName
|
|
218
|
+
: Casing.applyCategory(casing, "tables", tableState.baseName)
|
|
219
|
+
const columns = new Map(
|
|
220
|
+
Object.keys(tableState.fields).map((columnName) => [
|
|
221
|
+
columnName,
|
|
222
|
+
Casing.applyCategory(casing, "columns", columnName)
|
|
223
|
+
] as const)
|
|
224
|
+
)
|
|
225
|
+
state.sourceNames?.set(tableName, {
|
|
226
|
+
tableName: renderedTableName,
|
|
227
|
+
columns
|
|
228
|
+
})
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
if ("columns" in source && typeof source.columns === "object" && source.columns !== null) {
|
|
232
|
+
state.sourceNames?.set(tableName, {
|
|
233
|
+
tableName,
|
|
234
|
+
columns: new Map(Object.keys(source.columns).map((columnName) => [columnName, columnName] as const))
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const registerQuerySources = (
|
|
240
|
+
ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
241
|
+
state: RenderState
|
|
242
|
+
): void => {
|
|
243
|
+
if (ast.from !== undefined) {
|
|
244
|
+
registerSourceReference(ast.from.source, ast.from.tableName, state)
|
|
245
|
+
}
|
|
246
|
+
for (const source of ast.fromSources ?? []) {
|
|
247
|
+
registerSourceReference(source.source, source.tableName, state)
|
|
248
|
+
}
|
|
249
|
+
for (const join of ast.joins) {
|
|
250
|
+
registerSourceReference(join.source, join.tableName, state)
|
|
251
|
+
}
|
|
252
|
+
if (ast.into !== undefined) {
|
|
253
|
+
registerSourceReference(ast.into.source, ast.into.tableName, state)
|
|
254
|
+
}
|
|
255
|
+
if (ast.target !== undefined) {
|
|
256
|
+
registerSourceReference(ast.target.source, ast.target.tableName, state)
|
|
257
|
+
}
|
|
258
|
+
for (const target of ast.targets ?? []) {
|
|
259
|
+
registerSourceReference(target.source, target.tableName, state)
|
|
260
|
+
}
|
|
261
|
+
if (ast.using !== undefined) {
|
|
262
|
+
registerSourceReference(ast.using.source, ast.using.tableName, state)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const renderColumnDefinition = (
|
|
267
|
+
dialect: SqlDialect,
|
|
268
|
+
state: RenderState,
|
|
269
|
+
columnName: string,
|
|
270
|
+
column: Table.AnyTable[typeof Table.TypeId]["fields"][string],
|
|
271
|
+
tableName?: string,
|
|
272
|
+
casing?: Casing.Options
|
|
273
|
+
): string => {
|
|
274
|
+
const expressionState = { ...state, casing, rowLocalColumns: true }
|
|
275
|
+
if (isArrayDbType(column.metadata.dbType)) {
|
|
276
|
+
throw new Error("Unsupported mysql array column options")
|
|
277
|
+
}
|
|
278
|
+
const clauses = [
|
|
279
|
+
quoteColumn(columnName, state, dialect, tableName),
|
|
280
|
+
column.metadata.ddlType === undefined
|
|
281
|
+
? renderDbType(dialect, column.metadata.dbType)
|
|
282
|
+
: renderDbTypeName(column.metadata.ddlType)
|
|
283
|
+
]
|
|
284
|
+
if (column.metadata.identity) {
|
|
285
|
+
throw new Error("Unsupported mysql identity column options")
|
|
286
|
+
} else if (column.metadata.generatedValue) {
|
|
287
|
+
clauses.push(`generated always as (${renderDdlExpression(column.metadata.generatedValue, expressionState, dialect)}) stored`)
|
|
288
|
+
} else if (column.metadata.defaultValue) {
|
|
289
|
+
clauses.push(`default ${renderDdlExpression(column.metadata.defaultValue, expressionState, dialect)}`)
|
|
290
|
+
}
|
|
291
|
+
if (!column.metadata.nullable) {
|
|
292
|
+
clauses.push("not null")
|
|
293
|
+
}
|
|
294
|
+
return clauses.join(" ")
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const renderCreateTableSql = (
|
|
298
|
+
targetSource: QueryAst.FromClause,
|
|
299
|
+
state: RenderState,
|
|
300
|
+
dialect: SqlDialect,
|
|
301
|
+
ifNotExists: unknown
|
|
302
|
+
): string => {
|
|
303
|
+
const normalizedIfNotExists = normalizeStatementFlag(ifNotExists)
|
|
304
|
+
const table = targetSource.source as Table.AnyTable
|
|
305
|
+
const tableCasing = casingForTable(table, state)
|
|
306
|
+
const fields = table[Table.TypeId].fields
|
|
307
|
+
const definitions = Object.entries(fields).map(([columnName, column]) =>
|
|
308
|
+
renderColumnDefinition(dialect, state, columnName, column, targetSource.tableName, tableCasing)
|
|
309
|
+
)
|
|
310
|
+
const options = table[Table.OptionsSymbol] as unknown
|
|
311
|
+
const tableOptions = (Array.isArray(options) ? options : [options]) as readonly TableOptionSpec[]
|
|
312
|
+
validateOptions(table[Table.TypeId].name, fields, tableOptions)
|
|
313
|
+
for (const option of tableOptions) {
|
|
314
|
+
if (typeof option !== "object" || option === null || !("kind" in option)) {
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
switch (option.kind) {
|
|
318
|
+
case "primaryKey":
|
|
319
|
+
if (option.deferrable || option.initiallyDeferred) {
|
|
320
|
+
throw new Error("Unsupported mysql primary key constraint options")
|
|
321
|
+
}
|
|
322
|
+
definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "constraints", option.name))} ` : ""}primary key (${option.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
|
|
323
|
+
break
|
|
324
|
+
case "unique":
|
|
325
|
+
if (option.nullsNotDistinct || option.deferrable || option.initiallyDeferred) {
|
|
326
|
+
throw new Error("Unsupported mysql unique constraint options")
|
|
327
|
+
}
|
|
328
|
+
definitions.push(`${option.name ? `constraint ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "constraints", option.name))} ` : ""}unique${option.nullsNotDistinct ? " nulls not distinct" : ""} (${option.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")})${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`)
|
|
329
|
+
break
|
|
330
|
+
case "foreignKey": {
|
|
331
|
+
if (option.deferrable || option.initiallyDeferred) {
|
|
332
|
+
throw new Error("Unsupported mysql foreign key constraint options")
|
|
333
|
+
}
|
|
334
|
+
const reference = typeof option.references === "function"
|
|
335
|
+
? option.references()
|
|
336
|
+
: option.references
|
|
337
|
+
definitions.push(
|
|
338
|
+
`${option.name ? `constraint ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "constraints", option.name))} ` : ""}foreign key (${option.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")}) references ${renderReferenceTable(reference, state, dialect)} (${reference.columns.map((column) => quoteReferenceColumn(column, reference, state, dialect)).join(", ")})${option.onDelete !== undefined ? ` on delete ${renderReferentialAction(option.onDelete)}` : ""}${option.onUpdate !== undefined ? ` on update ${renderReferentialAction(option.onUpdate)}` : ""}${option.deferrable ? ` deferrable${option.initiallyDeferred ? " initially deferred" : ""}` : ""}`
|
|
339
|
+
)
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
case "check":
|
|
343
|
+
if (option.noInherit) {
|
|
344
|
+
throw new Error("Unsupported mysql check constraint options")
|
|
345
|
+
}
|
|
346
|
+
definitions.push(
|
|
347
|
+
`constraint ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "constraints", option.name))} check (${renderDdlExpression(option.predicate, { ...state, casing: tableCasing, rowLocalColumns: true }, dialect)})${option.noInherit ? " no inherit" : ""}`
|
|
348
|
+
)
|
|
349
|
+
break
|
|
350
|
+
case "index":
|
|
351
|
+
break
|
|
352
|
+
default:
|
|
353
|
+
throw new Error("Unsupported table option kind")
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return `create table${normalizedIfNotExists ? " if not exists" : ""} ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${definitions.join(", ")})`
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const renderCreateIndexSql = (
|
|
360
|
+
targetSource: QueryAst.FromClause,
|
|
361
|
+
ddl: Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
|
|
362
|
+
state: RenderState,
|
|
363
|
+
dialect: SqlDialect
|
|
364
|
+
): string => {
|
|
365
|
+
const unique = normalizeStatementFlag(ddl.unique)
|
|
366
|
+
const ifNotExists = normalizeStatementFlag(ddl.ifNotExists)
|
|
367
|
+
const name = normalizeStatementIdentifier("createIndex", "option 'name'", ddl.name)
|
|
368
|
+
if (ifNotExists) {
|
|
369
|
+
throw new Error("Unsupported mysql create index options")
|
|
370
|
+
}
|
|
371
|
+
const maybeIfNotExists = dialect.name === "postgres" && ifNotExists ? " if not exists" : ""
|
|
372
|
+
const table = targetSource.source as Table.AnyTable
|
|
373
|
+
const tableCasing = casingForTable(table, state)
|
|
374
|
+
return `create${unique ? " unique" : ""} index${maybeIfNotExists} ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "indexes", name))} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${ddl.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")})`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const renderDropIndexSql = (
|
|
378
|
+
targetSource: QueryAst.FromClause,
|
|
379
|
+
ddl: Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
|
|
380
|
+
state: RenderState,
|
|
381
|
+
dialect: SqlDialect
|
|
382
|
+
): string => {
|
|
383
|
+
const ifExists = normalizeStatementFlag(ddl.ifExists)
|
|
384
|
+
const name = normalizeStatementIdentifier("dropIndex", "option 'name'", ddl.name)
|
|
385
|
+
if (ifExists) {
|
|
386
|
+
throw new Error("Unsupported mysql drop index options")
|
|
387
|
+
}
|
|
388
|
+
const table = targetSource.source as Table.AnyTable
|
|
389
|
+
const tableCasing = casingForTable(table, state)
|
|
390
|
+
return dialect.name === "postgres"
|
|
391
|
+
? `drop index${ifExists ? " if exists" : ""} ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "indexes", name))}`
|
|
392
|
+
: `drop index ${dialect.quoteIdentifier(Casing.applyCategory(tableCasing, "indexes", name))} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const isExpression = (value: unknown): value is Expression.Any =>
|
|
396
|
+
value !== null && typeof value === "object" && Expression.TypeId in value
|
|
397
|
+
|
|
398
|
+
const isJsonDbType = (dbType: Expression.DbType.Any): boolean => {
|
|
399
|
+
if (dbType.kind === "jsonb" || dbType.kind === "json") {
|
|
400
|
+
return true
|
|
401
|
+
}
|
|
402
|
+
if (!("variant" in dbType)) {
|
|
403
|
+
return false
|
|
404
|
+
}
|
|
405
|
+
const variant = dbType.variant as string
|
|
406
|
+
return variant === "json" || variant === "jsonb"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const isJsonExpression = (value: unknown): value is Expression.Any =>
|
|
410
|
+
isExpression(value) && isJsonDbType(value[Expression.TypeId].dbType)
|
|
411
|
+
|
|
412
|
+
const expectValueExpression = (
|
|
413
|
+
_functionName: string,
|
|
414
|
+
value: unknown
|
|
415
|
+
): Expression.Any => value as Expression.Any
|
|
416
|
+
|
|
417
|
+
const expectBinaryExpressions = (
|
|
418
|
+
_functionName: string,
|
|
419
|
+
left: unknown,
|
|
420
|
+
right: unknown
|
|
421
|
+
): readonly [Expression.Any, Expression.Any] => [left as Expression.Any, right as Expression.Any]
|
|
422
|
+
|
|
423
|
+
const renderBinaryExpression = (
|
|
424
|
+
functionName: string,
|
|
425
|
+
operator: string,
|
|
426
|
+
left: unknown,
|
|
427
|
+
right: unknown,
|
|
428
|
+
state: RenderState,
|
|
429
|
+
dialect: SqlDialect
|
|
430
|
+
): string => {
|
|
431
|
+
const [leftExpression, rightExpression] = expectBinaryExpressions(functionName, left, right)
|
|
432
|
+
return `(${renderExpression(leftExpression, state, dialect)} ${operator} ${renderExpression(rightExpression, state, dialect)})`
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const unsupportedJsonFeature = (
|
|
436
|
+
dialect: SqlDialect,
|
|
437
|
+
feature: string
|
|
438
|
+
): never => {
|
|
439
|
+
const error = new Error(`Unsupported JSON feature for ${dialect.name}: ${feature}`) as Error & {
|
|
440
|
+
readonly tag: string
|
|
441
|
+
readonly dialect: string
|
|
442
|
+
readonly feature: string
|
|
443
|
+
}
|
|
444
|
+
Object.assign(error, {
|
|
445
|
+
tag: `@${dialect.name}/unsupported/json-feature`,
|
|
446
|
+
dialect: dialect.name,
|
|
447
|
+
feature
|
|
448
|
+
})
|
|
449
|
+
throw error
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const extractJsonBase = (node: Record<string, unknown>): unknown =>
|
|
453
|
+
node.value ?? node.base ?? node.input ?? node.left ?? node.target
|
|
454
|
+
|
|
455
|
+
const isJsonPathValue = (value: unknown): value is JsonPath.Path<any> =>
|
|
456
|
+
value !== null && typeof value === "object" && JsonPath.TypeId in value
|
|
457
|
+
|
|
458
|
+
const isOptionalJsonPathNumber = (value: unknown): boolean =>
|
|
459
|
+
value === undefined || (typeof value === "number" && Number.isFinite(value))
|
|
460
|
+
|
|
461
|
+
const isJsonPathSegment = (segment: unknown): boolean => {
|
|
462
|
+
if (typeof segment === "string") {
|
|
463
|
+
return true
|
|
464
|
+
}
|
|
465
|
+
if (typeof segment === "number") {
|
|
466
|
+
return Number.isFinite(segment)
|
|
467
|
+
}
|
|
468
|
+
if (segment === null || typeof segment !== "object" || !("kind" in segment)) {
|
|
469
|
+
return false
|
|
470
|
+
}
|
|
471
|
+
switch ((segment as { readonly kind?: unknown }).kind) {
|
|
472
|
+
case "key":
|
|
473
|
+
return typeof (segment as { readonly key?: unknown }).key === "string"
|
|
474
|
+
case "index": {
|
|
475
|
+
const index = (segment as { readonly index?: unknown }).index
|
|
476
|
+
return typeof index === "number" && Number.isFinite(index)
|
|
477
|
+
}
|
|
478
|
+
case "wildcard":
|
|
479
|
+
case "descend":
|
|
480
|
+
return true
|
|
481
|
+
case "slice":
|
|
482
|
+
return isOptionalJsonPathNumber((segment as { readonly start?: unknown }).start) &&
|
|
483
|
+
isOptionalJsonPathNumber((segment as { readonly end?: unknown }).end)
|
|
484
|
+
default:
|
|
485
|
+
return false
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const validateJsonPathSegments = (segments: unknown): ReadonlyArray<JsonPath.AnySegment> => {
|
|
490
|
+
if (!Array.isArray(segments)) {
|
|
491
|
+
throw new Error("JSON path expressions require a segment array")
|
|
492
|
+
}
|
|
493
|
+
if (segments.some((segment) => !isJsonPathSegment(segment))) {
|
|
494
|
+
throw new Error("JSON path segments require string, number, or path segment objects")
|
|
495
|
+
}
|
|
496
|
+
return segments as ReadonlyArray<JsonPath.AnySegment>
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const extractJsonPathSegments = (node: Record<string, unknown>): ReadonlyArray<JsonPath.AnySegment> => {
|
|
500
|
+
const path = node.path ?? node.segments ?? node.keys
|
|
501
|
+
if (isJsonPathValue(path)) {
|
|
502
|
+
return validateJsonPathSegments(path.segments)
|
|
503
|
+
}
|
|
504
|
+
if (Array.isArray(path)) {
|
|
505
|
+
return validateJsonPathSegments(path)
|
|
506
|
+
}
|
|
507
|
+
if (node.segments !== undefined) {
|
|
508
|
+
return validateJsonPathSegments(node.segments)
|
|
509
|
+
}
|
|
510
|
+
if ("key" in node) {
|
|
511
|
+
return [JsonPath.key(String(node.key))]
|
|
512
|
+
}
|
|
513
|
+
if ("segment" in node) {
|
|
514
|
+
const segment = node.segment
|
|
515
|
+
if (typeof segment === "string") {
|
|
516
|
+
return [JsonPath.key(segment)]
|
|
517
|
+
}
|
|
518
|
+
if (typeof segment === "number") {
|
|
519
|
+
return [JsonPath.index(segment)]
|
|
520
|
+
}
|
|
521
|
+
if (segment !== null && typeof segment === "object" && JsonPath.SegmentTypeId in segment) {
|
|
522
|
+
return [segment as JsonPath.AnySegment]
|
|
523
|
+
}
|
|
524
|
+
return []
|
|
525
|
+
}
|
|
526
|
+
if ("right" in node && isJsonPathValue(node.right)) {
|
|
527
|
+
return validateJsonPathSegments(node.right.segments)
|
|
528
|
+
}
|
|
529
|
+
return []
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const extractJsonKeys = (
|
|
533
|
+
node: Record<string, unknown>,
|
|
534
|
+
segments: ReadonlyArray<JsonPath.AnySegment>
|
|
535
|
+
): readonly unknown[] =>
|
|
536
|
+
Array.isArray(node.keys)
|
|
537
|
+
? node.keys
|
|
538
|
+
: segments.map((segment) =>
|
|
539
|
+
typeof segment === "object" && segment !== null && segment.kind === "key"
|
|
540
|
+
? segment.key
|
|
541
|
+
: segment
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
const extractJsonValue = (node: Record<string, unknown>): unknown =>
|
|
545
|
+
node.newValue ?? node.insert ?? node.right
|
|
546
|
+
|
|
547
|
+
const renderJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
|
|
548
|
+
const renderKey = (value: string): string =>
|
|
549
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
|
|
550
|
+
? `.${value}`
|
|
551
|
+
: `.${JSON.stringify(value)}`
|
|
552
|
+
if (typeof segment === "string") {
|
|
553
|
+
return renderKey(segment)
|
|
554
|
+
}
|
|
555
|
+
if (typeof segment === "number") {
|
|
556
|
+
return `[${segment}]`
|
|
557
|
+
}
|
|
558
|
+
switch (segment.kind) {
|
|
559
|
+
case "key":
|
|
560
|
+
return renderKey(segment.key)
|
|
561
|
+
case "index":
|
|
562
|
+
return `[${segment.index}]`
|
|
563
|
+
case "wildcard":
|
|
564
|
+
return "[*]"
|
|
565
|
+
case "slice":
|
|
566
|
+
return `[${segment.start ?? 0} to ${segment.end ?? "last"}]`
|
|
567
|
+
case "descend":
|
|
568
|
+
return ".**"
|
|
569
|
+
default:
|
|
570
|
+
throw new Error("Unsupported JSON path segment")
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const renderMySqlJsonIndex = (index: number): string =>
|
|
575
|
+
index >= 0 ? String(index) : index === -1 ? "last" : `last-${Math.abs(index) - 1}`
|
|
576
|
+
|
|
577
|
+
const renderMySqlJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
|
|
578
|
+
if (typeof segment === "number") {
|
|
579
|
+
return `[${renderMySqlJsonIndex(segment)}]`
|
|
580
|
+
}
|
|
581
|
+
if (typeof segment === "object" && segment !== null && segment.kind === "index") {
|
|
582
|
+
return `[${renderMySqlJsonIndex(segment.index)}]`
|
|
583
|
+
}
|
|
584
|
+
if (typeof segment === "object" && segment !== null && segment.kind === "slice") {
|
|
585
|
+
return `[${renderMySqlJsonIndex(segment.start ?? 0)} to ${segment.end === undefined ? "last" : renderMySqlJsonIndex(segment.end)}]`
|
|
586
|
+
}
|
|
587
|
+
if (typeof segment === "object" && segment !== null && segment.kind === "descend") {
|
|
588
|
+
return "**"
|
|
589
|
+
}
|
|
590
|
+
return renderJsonPathSegment(segment)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const renderJsonPathStringLiteral = (
|
|
594
|
+
segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
|
|
595
|
+
renderSegment: (segment: JsonPath.AnySegment | string | number) => string = renderJsonPathSegment
|
|
596
|
+
): string => {
|
|
597
|
+
let path = "$"
|
|
598
|
+
for (const segment of segments) {
|
|
599
|
+
path += renderSegment(segment)
|
|
600
|
+
}
|
|
601
|
+
return path
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const renderMySqlJsonPath = (
|
|
605
|
+
segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
|
|
606
|
+
state: RenderState,
|
|
607
|
+
dialect: SqlDialect
|
|
608
|
+
): string => dialect.renderLiteral(renderJsonPathStringLiteral(segments, renderMySqlJsonPathSegment), state)
|
|
609
|
+
|
|
610
|
+
const isJsonArrayIndexSegment = (segment: JsonPath.AnySegment | string | number | undefined): boolean =>
|
|
611
|
+
typeof segment === "number" || (typeof segment === "object" && segment !== null && segment.kind === "index")
|
|
612
|
+
|
|
613
|
+
const isExactJsonPathSegment = (segment: JsonPath.AnySegment | string | number): boolean =>
|
|
614
|
+
typeof segment === "string" ||
|
|
615
|
+
typeof segment === "number" ||
|
|
616
|
+
(typeof segment === "object" && segment !== null && (segment.kind === "key" || segment.kind === "index"))
|
|
617
|
+
|
|
618
|
+
const assertMySqlJsonMutationPath = (
|
|
619
|
+
segments: ReadonlyArray<JsonPath.AnySegment | string | number>
|
|
620
|
+
): void => {
|
|
621
|
+
if (!segments.every(isExactJsonPathSegment)) {
|
|
622
|
+
throw new Error("MySQL JSON mutation paths require key/index segments")
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const renderMySqlJsonInsertPath = (
|
|
627
|
+
segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
|
|
628
|
+
insertAfter: boolean,
|
|
629
|
+
state: RenderState,
|
|
630
|
+
dialect: SqlDialect
|
|
631
|
+
): string => {
|
|
632
|
+
if (!insertAfter || segments.length === 0) {
|
|
633
|
+
return renderMySqlJsonPath(segments, state, dialect)
|
|
634
|
+
}
|
|
635
|
+
const last = segments[segments.length - 1]
|
|
636
|
+
const nextSegments = segments.slice(0, -1)
|
|
637
|
+
if (typeof last === "number") {
|
|
638
|
+
return renderMySqlJsonPath([...nextSegments, last + 1], state, dialect)
|
|
639
|
+
}
|
|
640
|
+
if (typeof last === "object" && last !== null && last.kind === "index") {
|
|
641
|
+
return renderMySqlJsonPath([...nextSegments, { ...last, index: last.index + 1 }], state, dialect)
|
|
642
|
+
}
|
|
643
|
+
return renderMySqlJsonPath(segments, state, dialect)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const renderPostgresJsonPathArray = (
|
|
647
|
+
segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
|
|
648
|
+
state: RenderState,
|
|
649
|
+
dialect: SqlDialect
|
|
650
|
+
): string => `array[${segments.map((segment) => {
|
|
651
|
+
if (typeof segment === "string") {
|
|
652
|
+
return dialect.renderLiteral(segment, state)
|
|
653
|
+
}
|
|
654
|
+
if (typeof segment === "number") {
|
|
655
|
+
return dialect.renderLiteral(String(segment), state)
|
|
656
|
+
}
|
|
657
|
+
switch (segment.kind) {
|
|
658
|
+
case "key":
|
|
659
|
+
return dialect.renderLiteral(segment.key, state)
|
|
660
|
+
case "index":
|
|
661
|
+
return dialect.renderLiteral(String(segment.index), state)
|
|
662
|
+
default:
|
|
663
|
+
throw new Error("Postgres JSON traversal requires exact key/index segments")
|
|
664
|
+
}
|
|
665
|
+
}).join(", ")}]`
|
|
666
|
+
|
|
667
|
+
const renderPostgresTextLiteral = (
|
|
668
|
+
value: string,
|
|
669
|
+
state: RenderState,
|
|
670
|
+
dialect: SqlDialect
|
|
671
|
+
): string => `cast(${dialect.renderLiteral(value, state)} as text)`
|
|
672
|
+
|
|
673
|
+
const renderPostgresJsonAccessStep = (
|
|
674
|
+
segment: JsonPath.AnySegment,
|
|
675
|
+
textMode: boolean,
|
|
676
|
+
state: RenderState,
|
|
677
|
+
dialect: SqlDialect
|
|
678
|
+
): string => {
|
|
679
|
+
switch (segment.kind) {
|
|
680
|
+
case "key":
|
|
681
|
+
return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(segment.key, state)}`
|
|
682
|
+
case "index":
|
|
683
|
+
return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(String(segment.index), state)}`
|
|
684
|
+
default:
|
|
685
|
+
throw new Error("Postgres exact JSON access requires key/index segments")
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const renderPostgresJsonValue = (
|
|
690
|
+
value: unknown,
|
|
691
|
+
state: RenderState,
|
|
692
|
+
dialect: SqlDialect
|
|
693
|
+
): string => {
|
|
694
|
+
if (!isExpression(value)) {
|
|
695
|
+
throw new Error("Expected a JSON expression")
|
|
696
|
+
}
|
|
697
|
+
const rendered = renderExpression(value, state, dialect)
|
|
698
|
+
return value[Expression.TypeId].dbType.kind === "jsonb"
|
|
699
|
+
? rendered
|
|
700
|
+
: `cast(${rendered} as jsonb)`
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const expressionDriverContext = (
|
|
704
|
+
expression: Expression.Any,
|
|
705
|
+
state: RenderState,
|
|
706
|
+
dialect: SqlDialect
|
|
707
|
+
) => ({
|
|
708
|
+
dialect: dialect.name,
|
|
709
|
+
valueMappings: state.valueMappings,
|
|
710
|
+
dbType: expression[Expression.TypeId].dbType,
|
|
711
|
+
runtimeSchema: expression[Expression.TypeId].runtimeSchema,
|
|
712
|
+
driverValueMapping: expression[Expression.TypeId].driverValueMapping
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
const renderMySqlStructuredJsonLiteral = (
|
|
716
|
+
expression: Expression.Any,
|
|
717
|
+
state: RenderState
|
|
718
|
+
): string | undefined => {
|
|
719
|
+
const ast = (expression as Expression.Any & {
|
|
720
|
+
readonly [ExpressionAst.TypeId]: ExpressionAst.Any
|
|
721
|
+
})[ExpressionAst.TypeId]
|
|
722
|
+
if (ast.kind !== "literal" || ast.value === null || typeof ast.value !== "object") {
|
|
723
|
+
return undefined
|
|
724
|
+
}
|
|
725
|
+
state.params.push(JSON.stringify(ast.value))
|
|
726
|
+
return "cast(? as json)"
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const renderJsonInputExpression = (
|
|
730
|
+
expression: Expression.Any,
|
|
731
|
+
state: RenderState,
|
|
732
|
+
dialect: SqlDialect
|
|
733
|
+
): string => {
|
|
734
|
+
if (dialect.name === "mysql") {
|
|
735
|
+
const jsonLiteral = renderMySqlStructuredJsonLiteral(expression, state)
|
|
736
|
+
if (jsonLiteral !== undefined) {
|
|
737
|
+
return jsonLiteral
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return renderJsonSelectSql(
|
|
741
|
+
renderExpression(expression, state, dialect),
|
|
742
|
+
expressionDriverContext(expression, state, dialect)
|
|
743
|
+
)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const encodeArrayValues = (
|
|
747
|
+
values: readonly unknown[],
|
|
748
|
+
column: Table.AnyTable[typeof Table.TypeId]["fields"][string],
|
|
749
|
+
state: RenderState,
|
|
750
|
+
dialect: SqlDialect
|
|
751
|
+
): readonly unknown[] =>
|
|
752
|
+
values.map((value) => {
|
|
753
|
+
if (value === null && column.metadata.nullable) {
|
|
754
|
+
return null
|
|
755
|
+
}
|
|
756
|
+
const runtimeSchemaAccepts = column.schema !== undefined &&
|
|
757
|
+
(Schema.is(column.schema) as (candidate: unknown) => boolean)(value)
|
|
758
|
+
const normalizedValue = runtimeSchemaAccepts
|
|
759
|
+
? value
|
|
760
|
+
: normalizeDbValue(column.metadata.dbType, value)
|
|
761
|
+
const encodedValue = column.schema === undefined || runtimeSchemaAccepts
|
|
762
|
+
? normalizedValue
|
|
763
|
+
: (Schema.decodeUnknownSync as any)(column.schema)(normalizedValue)
|
|
764
|
+
return toDriverValue(encodedValue, {
|
|
765
|
+
dialect: dialect.name,
|
|
766
|
+
valueMappings: state.valueMappings,
|
|
767
|
+
dbType: column.metadata.dbType,
|
|
768
|
+
runtimeSchema: column.schema,
|
|
769
|
+
driverValueMapping: column.metadata.driverValueMapping
|
|
770
|
+
})
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
const renderPostgresJsonKind = (
|
|
774
|
+
value: Expression.Any
|
|
775
|
+
): "json" | "jsonb" => value[Expression.TypeId].dbType.kind === "jsonb" ? "jsonb" : "json"
|
|
776
|
+
|
|
777
|
+
const renderJsonOpaquePath = (
|
|
778
|
+
value: unknown,
|
|
779
|
+
state: RenderState,
|
|
780
|
+
dialect: SqlDialect
|
|
781
|
+
): string => {
|
|
782
|
+
if (isJsonPathValue(value)) {
|
|
783
|
+
const renderSegment = dialect.name === "mysql"
|
|
784
|
+
? renderMySqlJsonPathSegment
|
|
785
|
+
: renderJsonPathSegment
|
|
786
|
+
return dialect.renderLiteral(renderJsonPathStringLiteral(value.segments, renderSegment), state)
|
|
787
|
+
}
|
|
788
|
+
if (typeof value === "string") {
|
|
789
|
+
if (value.trim().length === 0) {
|
|
790
|
+
throw new Error("SQL/JSON path input must be a non-empty string")
|
|
791
|
+
}
|
|
792
|
+
return dialect.renderLiteral(value, state)
|
|
793
|
+
}
|
|
794
|
+
if (isExpression(value)) {
|
|
795
|
+
const ast = (value as Expression.Any & {
|
|
796
|
+
readonly [ExpressionAst.TypeId]: ExpressionAst.Any
|
|
797
|
+
})[ExpressionAst.TypeId]
|
|
798
|
+
if (ast.kind === "literal" && typeof ast.value === "string" && ast.value.trim().length === 0) {
|
|
799
|
+
throw new Error("SQL/JSON path input must be a non-empty string")
|
|
800
|
+
}
|
|
801
|
+
return renderExpression(value, state, dialect)
|
|
802
|
+
}
|
|
803
|
+
throw new Error("Unsupported SQL/JSON path input")
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const renderFunctionName = (name: unknown): string => {
|
|
807
|
+
return name as string
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const renderExtractField = (field: Expression.Any): string => {
|
|
811
|
+
const ast = (field as Expression.Any & {
|
|
812
|
+
readonly [ExpressionAst.TypeId]: ExpressionAst.Any
|
|
813
|
+
})[ExpressionAst.TypeId] as ExpressionAst.LiteralNode<string>
|
|
814
|
+
return ast.value
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const renderFunctionCall = (
|
|
818
|
+
name: unknown,
|
|
819
|
+
args: unknown,
|
|
820
|
+
state: RenderState,
|
|
821
|
+
dialect: SqlDialect
|
|
822
|
+
): string => {
|
|
823
|
+
const functionName = renderFunctionName(name)
|
|
824
|
+
const functionArgs = args as readonly Expression.Any[]
|
|
825
|
+
if (functionName === "array") {
|
|
826
|
+
return `ARRAY[${functionArgs.map((arg) => renderExpression(arg, state, dialect)).join(", ")}]`
|
|
827
|
+
}
|
|
828
|
+
if (functionName === "extract") {
|
|
829
|
+
const field = functionArgs[0]!
|
|
830
|
+
const source = functionArgs[1]!
|
|
831
|
+
return `extract(${renderExtractField(field)} from ${renderExpression(source, state, dialect)})`
|
|
832
|
+
}
|
|
833
|
+
const renderedArgs = functionArgs.map((arg) => renderExpression(arg, state, dialect)).join(", ")
|
|
834
|
+
if (functionArgs.length === 0) {
|
|
835
|
+
switch (functionName) {
|
|
836
|
+
case "current_date":
|
|
837
|
+
case "current_time":
|
|
838
|
+
case "current_timestamp":
|
|
839
|
+
case "localtime":
|
|
840
|
+
case "localtimestamp":
|
|
841
|
+
return functionName
|
|
842
|
+
default:
|
|
843
|
+
return `${functionName}()`
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return `${functionName}(${renderedArgs})`
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const renderJsonExpression = (
|
|
850
|
+
expression: Expression.Any,
|
|
851
|
+
ast: Record<string, unknown>,
|
|
852
|
+
state: RenderState,
|
|
853
|
+
dialect: SqlDialect
|
|
854
|
+
): string | undefined => {
|
|
855
|
+
const kind = typeof ast.kind === "string" ? ast.kind : undefined
|
|
856
|
+
if (!kind) {
|
|
857
|
+
return undefined
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const base = extractJsonBase(ast)
|
|
861
|
+
const segments = extractJsonPathSegments(ast)
|
|
862
|
+
const exact = segments.every((segment) => segment.kind === "key" || segment.kind === "index")
|
|
863
|
+
const postgresExpressionKind = dialect.name === "postgres" && isJsonExpression(expression)
|
|
864
|
+
? renderPostgresJsonKind(expression)
|
|
865
|
+
: undefined
|
|
866
|
+
const postgresBaseKind = dialect.name === "postgres" && isJsonExpression(base)
|
|
867
|
+
? renderPostgresJsonKind(base)
|
|
868
|
+
: undefined
|
|
869
|
+
|
|
870
|
+
switch (kind) {
|
|
871
|
+
case "jsonGet":
|
|
872
|
+
case "jsonPath":
|
|
873
|
+
case "jsonAccess":
|
|
874
|
+
case "jsonTraverse":
|
|
875
|
+
case "jsonGetText":
|
|
876
|
+
case "jsonPathText":
|
|
877
|
+
case "jsonAccessText":
|
|
878
|
+
case "jsonTraverseText": {
|
|
879
|
+
if (!isExpression(base) || segments.length === 0) {
|
|
880
|
+
return undefined
|
|
881
|
+
}
|
|
882
|
+
const baseSql = renderExpression(base, state, dialect)
|
|
883
|
+
const textMode = kind.endsWith("Text") || ast.text === true || ast.asText === true
|
|
884
|
+
if (dialect.name === "postgres") {
|
|
885
|
+
if (exact) {
|
|
886
|
+
return segments.length === 1
|
|
887
|
+
? `(${baseSql} ${renderPostgresJsonAccessStep(segments[0]!, textMode, state, dialect)})`
|
|
888
|
+
: `(${baseSql} ${textMode ? "#>>" : "#>"} ${renderPostgresJsonPathArray(segments, state, dialect)})`
|
|
889
|
+
}
|
|
890
|
+
const jsonPathLiteral = dialect.renderLiteral(renderJsonPathStringLiteral(segments), state)
|
|
891
|
+
const queried = `jsonb_path_query_first(${renderPostgresJsonValue(base, state, dialect)}, ${jsonPathLiteral})`
|
|
892
|
+
return textMode ? `(${queried} #>> '{}')` : queried
|
|
893
|
+
}
|
|
894
|
+
if (dialect.name === "mysql") {
|
|
895
|
+
const extracted = `json_extract(${baseSql}, ${renderMySqlJsonPath(segments, state, dialect)})`
|
|
896
|
+
return textMode ? `json_unquote(${extracted})` : extracted
|
|
897
|
+
}
|
|
898
|
+
return undefined
|
|
899
|
+
}
|
|
900
|
+
case "jsonHasKey":
|
|
901
|
+
case "jsonKeyExists":
|
|
902
|
+
case "jsonHasAnyKeys":
|
|
903
|
+
case "jsonHasAllKeys": {
|
|
904
|
+
if (!isExpression(base)) {
|
|
905
|
+
return undefined
|
|
906
|
+
}
|
|
907
|
+
const baseSql = dialect.name === "postgres"
|
|
908
|
+
? renderPostgresJsonValue(base, state, dialect)
|
|
909
|
+
: renderExpression(base, state, dialect)
|
|
910
|
+
const keys = extractJsonKeys(ast, segments)
|
|
911
|
+
if (keys.length === 0) {
|
|
912
|
+
return undefined
|
|
913
|
+
}
|
|
914
|
+
if (keys.some((key) => typeof key !== "string" || key.length === 0)) {
|
|
915
|
+
throw new Error("json key predicates require string keys")
|
|
916
|
+
}
|
|
917
|
+
const keyNames = keys as readonly string[]
|
|
918
|
+
if (dialect.name === "postgres") {
|
|
919
|
+
if (kind === "jsonHasAnyKeys") {
|
|
920
|
+
return `(${baseSql} ?| array[${keyNames.map((key) => renderPostgresTextLiteral(key, state, dialect)).join(", ")}])`
|
|
921
|
+
}
|
|
922
|
+
if (kind === "jsonHasAllKeys") {
|
|
923
|
+
return `(${baseSql} ?& array[${keyNames.map((key) => renderPostgresTextLiteral(key, state, dialect)).join(", ")}])`
|
|
924
|
+
}
|
|
925
|
+
return `(${baseSql} ? ${renderPostgresTextLiteral(keyNames[0]!, state, dialect)})`
|
|
926
|
+
}
|
|
927
|
+
if (dialect.name === "mysql") {
|
|
928
|
+
const mode = kind === "jsonHasAllKeys" ? "all" : "one"
|
|
929
|
+
const modeSql = dialect.renderLiteral(mode, state)
|
|
930
|
+
const paths = keyNames.map((segment) => renderMySqlJsonPath([segment], state, dialect)).join(", ")
|
|
931
|
+
return `json_contains_path(${baseSql}, ${modeSql}, ${paths})`
|
|
932
|
+
}
|
|
933
|
+
return undefined
|
|
934
|
+
}
|
|
935
|
+
case "jsonConcat":
|
|
936
|
+
case "jsonMerge": {
|
|
937
|
+
if (!isExpression(ast.left) || !isExpression(ast.right)) {
|
|
938
|
+
return undefined
|
|
939
|
+
}
|
|
940
|
+
if (dialect.name === "postgres") {
|
|
941
|
+
return `(${renderPostgresJsonValue(ast.left, state, dialect)} || ${renderPostgresJsonValue(ast.right, state, dialect)})`
|
|
942
|
+
}
|
|
943
|
+
if (dialect.name === "mysql") {
|
|
944
|
+
return `json_merge_preserve(${renderJsonInputExpression(ast.left, state, dialect)}, ${renderJsonInputExpression(ast.right, state, dialect)})`
|
|
945
|
+
}
|
|
946
|
+
return undefined
|
|
947
|
+
}
|
|
948
|
+
case "jsonBuildObject": {
|
|
949
|
+
const entries = (ast as { readonly entries: readonly { readonly key: string; readonly value: Expression.Any }[] }).entries
|
|
950
|
+
const renderedEntries = entries.flatMap((entry) => [
|
|
951
|
+
dialect.renderLiteral(entry.key, state),
|
|
952
|
+
renderJsonInputExpression(entry.value, state, dialect)
|
|
953
|
+
])
|
|
954
|
+
if (dialect.name === "postgres") {
|
|
955
|
+
return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_object(${renderedEntries.join(", ")})`
|
|
956
|
+
}
|
|
957
|
+
if (dialect.name === "mysql") {
|
|
958
|
+
return `json_object(${renderedEntries.join(", ")})`
|
|
959
|
+
}
|
|
960
|
+
return undefined
|
|
961
|
+
}
|
|
962
|
+
case "jsonBuildArray": {
|
|
963
|
+
const values = (ast as { readonly values: readonly Expression.Any[] }).values
|
|
964
|
+
const renderedValues = values.map((value) => renderJsonInputExpression(value, state, dialect)).join(", ")
|
|
965
|
+
if (dialect.name === "postgres") {
|
|
966
|
+
return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_array(${renderedValues})`
|
|
967
|
+
}
|
|
968
|
+
if (dialect.name === "mysql") {
|
|
969
|
+
return `json_array(${renderedValues})`
|
|
970
|
+
}
|
|
971
|
+
return undefined
|
|
972
|
+
}
|
|
973
|
+
case "jsonToJson":
|
|
974
|
+
if (!isExpression(base)) {
|
|
975
|
+
return undefined
|
|
976
|
+
}
|
|
977
|
+
if (dialect.name === "postgres") {
|
|
978
|
+
return `to_json(${renderJsonInputExpression(base, state, dialect)})`
|
|
979
|
+
}
|
|
980
|
+
if (dialect.name === "mysql") {
|
|
981
|
+
return renderMySqlStructuredJsonLiteral(base, state) ?? `cast(${renderExpression(base, state, dialect)} as json)`
|
|
982
|
+
}
|
|
983
|
+
return undefined
|
|
984
|
+
case "jsonToJsonb":
|
|
985
|
+
if (!isExpression(base)) {
|
|
986
|
+
return undefined
|
|
987
|
+
}
|
|
988
|
+
if (dialect.name === "postgres") {
|
|
989
|
+
return `to_jsonb(${renderJsonInputExpression(base, state, dialect)})`
|
|
990
|
+
}
|
|
991
|
+
if (dialect.name === "mysql") {
|
|
992
|
+
return renderMySqlStructuredJsonLiteral(base, state) ?? `cast(${renderExpression(base, state, dialect)} as json)`
|
|
993
|
+
}
|
|
994
|
+
return undefined
|
|
995
|
+
case "jsonTypeOf":
|
|
996
|
+
if (!isExpression(base)) {
|
|
997
|
+
return undefined
|
|
998
|
+
}
|
|
999
|
+
if (dialect.name === "postgres") {
|
|
1000
|
+
const baseSql = renderExpression(base, state, dialect)
|
|
1001
|
+
return `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof(${baseSql})`
|
|
1002
|
+
}
|
|
1003
|
+
if (dialect.name === "mysql") {
|
|
1004
|
+
return `json_type(${renderExpression(base, state, dialect)})`
|
|
1005
|
+
}
|
|
1006
|
+
return undefined
|
|
1007
|
+
case "jsonLength":
|
|
1008
|
+
if (!isExpression(base)) {
|
|
1009
|
+
return undefined
|
|
1010
|
+
}
|
|
1011
|
+
if (dialect.name === "postgres") {
|
|
1012
|
+
const baseSql = renderExpression(base, state, dialect)
|
|
1013
|
+
const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
|
|
1014
|
+
const arrayLength = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_array_length`
|
|
1015
|
+
const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
|
|
1016
|
+
return `(case when ${typeOf}(${baseSql}) = 'array' then ${arrayLength}(${baseSql}) when ${typeOf}(${baseSql}) = 'object' then (select count(*)::int from ${objectKeys}(${baseSql})) else null end)`
|
|
1017
|
+
}
|
|
1018
|
+
if (dialect.name === "mysql") {
|
|
1019
|
+
return `json_length(${renderExpression(base, state, dialect)})`
|
|
1020
|
+
}
|
|
1021
|
+
return undefined
|
|
1022
|
+
case "jsonKeys":
|
|
1023
|
+
if (!isExpression(base)) {
|
|
1024
|
+
return undefined
|
|
1025
|
+
}
|
|
1026
|
+
if (dialect.name === "postgres") {
|
|
1027
|
+
const baseSql = renderExpression(base, state, dialect)
|
|
1028
|
+
const typeOf = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_typeof`
|
|
1029
|
+
const objectKeys = `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_object_keys`
|
|
1030
|
+
return `(case when ${typeOf}(${baseSql}) = 'object' then array(select ${objectKeys}(${baseSql})) else null end)`
|
|
1031
|
+
}
|
|
1032
|
+
if (dialect.name === "mysql") {
|
|
1033
|
+
return `json_keys(${renderExpression(base, state, dialect)})`
|
|
1034
|
+
}
|
|
1035
|
+
return undefined
|
|
1036
|
+
case "jsonStripNulls":
|
|
1037
|
+
if (!isExpression(base)) {
|
|
1038
|
+
return undefined
|
|
1039
|
+
}
|
|
1040
|
+
if (dialect.name === "postgres") {
|
|
1041
|
+
return `${postgresBaseKind === "jsonb" ? "jsonb" : "json"}_strip_nulls(${renderExpression(base, state, dialect)})`
|
|
1042
|
+
}
|
|
1043
|
+
unsupportedJsonFeature(dialect, "jsonStripNulls")
|
|
1044
|
+
return undefined
|
|
1045
|
+
case "jsonDelete":
|
|
1046
|
+
case "jsonDeletePath":
|
|
1047
|
+
case "jsonRemove": {
|
|
1048
|
+
if (!isExpression(base) || segments.length === 0) {
|
|
1049
|
+
return undefined
|
|
1050
|
+
}
|
|
1051
|
+
if (dialect.name === "postgres") {
|
|
1052
|
+
const baseSql = renderPostgresJsonValue(base, state, dialect)
|
|
1053
|
+
if (segments.length === 1 && (segments[0]!.kind === "key" || segments[0]!.kind === "index")) {
|
|
1054
|
+
const segment = segments[0]!
|
|
1055
|
+
return `(${baseSql} - ${segment.kind === "key"
|
|
1056
|
+
? dialect.renderLiteral(segment.key, state)
|
|
1057
|
+
: dialect.renderLiteral(String(segment.index), state)})`
|
|
1058
|
+
}
|
|
1059
|
+
return `(${baseSql} #- ${renderPostgresJsonPathArray(segments, state, dialect)})`
|
|
1060
|
+
}
|
|
1061
|
+
if (dialect.name === "mysql") {
|
|
1062
|
+
assertMySqlJsonMutationPath(segments)
|
|
1063
|
+
return `json_remove(${renderExpression(base, state, dialect)}, ${renderMySqlJsonPath(segments, state, dialect)})`
|
|
1064
|
+
}
|
|
1065
|
+
return undefined
|
|
1066
|
+
}
|
|
1067
|
+
case "jsonSet":
|
|
1068
|
+
case "jsonInsert": {
|
|
1069
|
+
if (!isExpression(base) || segments.length === 0) {
|
|
1070
|
+
return undefined
|
|
1071
|
+
}
|
|
1072
|
+
const nextValue = extractJsonValue(ast)
|
|
1073
|
+
if (!isExpression(nextValue)) {
|
|
1074
|
+
return undefined
|
|
1075
|
+
}
|
|
1076
|
+
const createMissing = ast.createMissing === true
|
|
1077
|
+
const insertAfter = ast.insertAfter === true
|
|
1078
|
+
if (dialect.name === "postgres") {
|
|
1079
|
+
const functionName = kind === "jsonInsert" ? "jsonb_insert" : "jsonb_set"
|
|
1080
|
+
const extra =
|
|
1081
|
+
kind === "jsonInsert"
|
|
1082
|
+
? `, ${insertAfter ? "true" : "false"}`
|
|
1083
|
+
: `, ${createMissing ? "true" : "false"}`
|
|
1084
|
+
return `${functionName}(${renderPostgresJsonValue(base, state, dialect)}, ${renderPostgresJsonPathArray(segments, state, dialect)}, ${renderPostgresJsonValue(nextValue, state, dialect)}${extra})`
|
|
1085
|
+
}
|
|
1086
|
+
if (dialect.name === "mysql") {
|
|
1087
|
+
assertMySqlJsonMutationPath(segments)
|
|
1088
|
+
const renderedBase = renderExpression(base, state, dialect)
|
|
1089
|
+
if (kind === "jsonInsert" && isJsonArrayIndexSegment(segments[segments.length - 1])) {
|
|
1090
|
+
const renderedPath = renderMySqlJsonInsertPath(segments, insertAfter, state, dialect)
|
|
1091
|
+
const renderedValue = renderJsonInputExpression(nextValue, state, dialect)
|
|
1092
|
+
return `json_array_insert(${renderedBase}, ${renderedPath}, ${renderedValue})`
|
|
1093
|
+
}
|
|
1094
|
+
const functionName = kind === "jsonInsert" ? "json_insert" : createMissing ? "json_set" : "json_replace"
|
|
1095
|
+
const renderedPath = renderMySqlJsonPath(segments, state, dialect)
|
|
1096
|
+
const renderedValue = renderJsonInputExpression(nextValue, state, dialect)
|
|
1097
|
+
return `${functionName}(${renderedBase}, ${renderedPath}, ${renderedValue})`
|
|
1098
|
+
}
|
|
1099
|
+
return undefined
|
|
1100
|
+
}
|
|
1101
|
+
case "jsonPathExists": {
|
|
1102
|
+
if (!isExpression(base)) {
|
|
1103
|
+
return undefined
|
|
1104
|
+
}
|
|
1105
|
+
const path = ast.path ?? ast.query ?? ast.right
|
|
1106
|
+
if (path === undefined) {
|
|
1107
|
+
return undefined
|
|
1108
|
+
}
|
|
1109
|
+
if (dialect.name === "postgres") {
|
|
1110
|
+
return `(${renderPostgresJsonValue(base, state, dialect)} @? ${renderJsonOpaquePath(path, state, dialect)})`
|
|
1111
|
+
}
|
|
1112
|
+
if (dialect.name === "mysql") {
|
|
1113
|
+
return `json_contains_path(${renderExpression(base, state, dialect)}, ${dialect.renderLiteral("one", state)}, ${renderJsonOpaquePath(path, state, dialect)})`
|
|
1114
|
+
}
|
|
1115
|
+
return undefined
|
|
1116
|
+
}
|
|
1117
|
+
case "jsonPathMatch": {
|
|
1118
|
+
if (!isExpression(base)) {
|
|
1119
|
+
return undefined
|
|
1120
|
+
}
|
|
1121
|
+
const path = ast.path ?? ast.query ?? ast.right
|
|
1122
|
+
if (path === undefined) {
|
|
1123
|
+
return undefined
|
|
1124
|
+
}
|
|
1125
|
+
if (dialect.name === "postgres") {
|
|
1126
|
+
return `(${renderPostgresJsonValue(base, state, dialect)} @@ ${renderJsonOpaquePath(path, state, dialect)})`
|
|
1127
|
+
}
|
|
1128
|
+
unsupportedJsonFeature(dialect, "jsonPathMatch")
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return undefined
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export interface RenderedQueryAst {
|
|
1136
|
+
readonly sql: string
|
|
1137
|
+
readonly projections: readonly Projection[]
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const selectionProjections = (selection: Record<string, unknown>): readonly Projection[] =>
|
|
1141
|
+
flattenSelection(selection).map(({ path, alias }) => ({
|
|
1142
|
+
path,
|
|
1143
|
+
alias
|
|
1144
|
+
}))
|
|
1145
|
+
|
|
1146
|
+
const renderMutationAssignment = (
|
|
1147
|
+
entry: QueryAst.AssignmentClause,
|
|
1148
|
+
state: RenderState,
|
|
1149
|
+
dialect: SqlDialect,
|
|
1150
|
+
targetTableName?: string
|
|
1151
|
+
): string => {
|
|
1152
|
+
const column = entry.tableName && dialect.name === "mysql"
|
|
1153
|
+
? `${dialect.quoteIdentifier(casedTableReferenceName(entry.tableName, state))}.${quoteColumn(entry.columnName, state, dialect, entry.tableName)}`
|
|
1154
|
+
: quoteColumn(entry.columnName, state, dialect, targetTableName)
|
|
1155
|
+
return `${column} = ${renderExpression(entry.value, state, dialect)}`
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const renderJoinSourcesForMutation = (
|
|
1159
|
+
joins: readonly QueryAst.JoinClause[],
|
|
1160
|
+
state: RenderState,
|
|
1161
|
+
dialect: SqlDialect
|
|
1162
|
+
): string => joins.map((join) =>
|
|
1163
|
+
renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
|
|
1164
|
+
).join(", ")
|
|
1165
|
+
|
|
1166
|
+
const renderFromSources = (
|
|
1167
|
+
sources: readonly QueryAst.FromClause[],
|
|
1168
|
+
state: RenderState,
|
|
1169
|
+
dialect: SqlDialect
|
|
1170
|
+
): string => sources.map((source) =>
|
|
1171
|
+
renderSourceReference(source.source, source.tableName, source.baseTableName, state, dialect)
|
|
1172
|
+
).join(", ")
|
|
1173
|
+
|
|
1174
|
+
const renderJoinPredicatesForMutation = (
|
|
1175
|
+
joins: readonly QueryAst.JoinClause[],
|
|
1176
|
+
state: RenderState,
|
|
1177
|
+
dialect: SqlDialect
|
|
1178
|
+
): readonly string[] =>
|
|
1179
|
+
joins.flatMap((join) =>
|
|
1180
|
+
join.kind === "cross" || !join.on
|
|
1181
|
+
? []
|
|
1182
|
+
: [renderExpression(join.on, state, dialect)]
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
const renderDeleteTargets = (
|
|
1186
|
+
targets: readonly QueryAst.FromClause[],
|
|
1187
|
+
state: RenderState,
|
|
1188
|
+
dialect: SqlDialect
|
|
1189
|
+
): string => targets.map((target) => dialect.quoteIdentifier(casedTableReferenceName(target.tableName, state))).join(", ")
|
|
1190
|
+
|
|
1191
|
+
const renderMysqlMutationLock = (
|
|
1192
|
+
lock: QueryAst.LockClause | undefined,
|
|
1193
|
+
statement: "update" | "delete"
|
|
1194
|
+
): string => {
|
|
1195
|
+
if (!lock) {
|
|
1196
|
+
return ""
|
|
1197
|
+
}
|
|
1198
|
+
return renderMysqlMutationLockMode(lock.mode, statement)
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const renderTransactionClause = (
|
|
1202
|
+
clause: QueryAst.TransactionClause,
|
|
1203
|
+
dialect: SqlDialect
|
|
1204
|
+
): string => {
|
|
1205
|
+
switch (clause.kind) {
|
|
1206
|
+
case "transaction": {
|
|
1207
|
+
const modes: string[] = []
|
|
1208
|
+
const isolationLevel = renderTransactionIsolationLevel(clause.isolationLevel)
|
|
1209
|
+
if (isolationLevel) {
|
|
1210
|
+
modes.push(isolationLevel)
|
|
1211
|
+
}
|
|
1212
|
+
if (normalizeStatementFlag(clause.readOnly)) {
|
|
1213
|
+
modes.push("read only")
|
|
1214
|
+
}
|
|
1215
|
+
return modes.length > 0
|
|
1216
|
+
? `start transaction ${modes.join(", ")}`
|
|
1217
|
+
: "start transaction"
|
|
1218
|
+
}
|
|
1219
|
+
case "commit":
|
|
1220
|
+
return "commit"
|
|
1221
|
+
case "rollback":
|
|
1222
|
+
return "rollback"
|
|
1223
|
+
case "savepoint":
|
|
1224
|
+
return `savepoint ${dialect.quoteIdentifier(normalizeStatementIdentifier("savepoint", "name", clause.name))}`
|
|
1225
|
+
case "rollbackTo":
|
|
1226
|
+
return `rollback to savepoint ${dialect.quoteIdentifier(normalizeStatementIdentifier("rollbackTo", "name", clause.name))}`
|
|
1227
|
+
case "releaseSavepoint":
|
|
1228
|
+
return `release savepoint ${dialect.quoteIdentifier(normalizeStatementIdentifier("releaseSavepoint", "name", clause.name))}`
|
|
1229
|
+
}
|
|
1230
|
+
return "start transaction"
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const renderSelectionList = (
|
|
1234
|
+
selection: Record<string, unknown>,
|
|
1235
|
+
state: RenderState,
|
|
1236
|
+
dialect: SqlDialect
|
|
1237
|
+
): RenderedQueryAst => {
|
|
1238
|
+
const flattened = flattenSelection(selection)
|
|
1239
|
+
const projections = selectionProjections(selection)
|
|
1240
|
+
const sql = flattened.map(({ expression, alias }) =>
|
|
1241
|
+
`${renderSelectSql(renderExpression(expression, state, dialect), expressionDriverContext(expression, state, dialect))} as ${dialect.quoteIdentifier(alias)}`).join(", ")
|
|
1242
|
+
return {
|
|
1243
|
+
sql,
|
|
1244
|
+
projections
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const nestedRenderState = (state: RenderState): RenderState => ({
|
|
1249
|
+
params: state.params,
|
|
1250
|
+
valueMappings: state.valueMappings,
|
|
1251
|
+
casing: state.casing,
|
|
1252
|
+
ctes: [],
|
|
1253
|
+
cteNames: new Set(state.cteNames),
|
|
1254
|
+
cteSources: new Map(state.cteSources),
|
|
1255
|
+
sourceNames: new Map(state.sourceNames)
|
|
1256
|
+
})
|
|
1257
|
+
|
|
1258
|
+
export const renderQueryAst = (
|
|
1259
|
+
ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
1260
|
+
state: RenderState,
|
|
1261
|
+
dialect: SqlDialect,
|
|
1262
|
+
options: { readonly emitCtes?: boolean } = {}
|
|
1263
|
+
): RenderedQueryAst => {
|
|
1264
|
+
registerQuerySources(ast, state)
|
|
1265
|
+
let sql = ""
|
|
1266
|
+
let projections: readonly Projection[] = []
|
|
1267
|
+
|
|
1268
|
+
switch (ast.kind) {
|
|
1269
|
+
case "select": {
|
|
1270
|
+
const rendered = renderSelectionList(ast.select as Record<string, unknown>, state, dialect)
|
|
1271
|
+
projections = rendered.projections
|
|
1272
|
+
const selectList = rendered.sql.length > 0 ? ` ${rendered.sql}` : ""
|
|
1273
|
+
const clauses = [
|
|
1274
|
+
ast.distinctOn && ast.distinctOn.length > 0
|
|
1275
|
+
? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")})${selectList}`
|
|
1276
|
+
: `select${ast.distinct ? " distinct" : ""}${selectList}`
|
|
1277
|
+
]
|
|
1278
|
+
if (ast.from) {
|
|
1279
|
+
clauses.push(`from ${renderSourceReference(ast.from.source, ast.from.tableName, ast.from.baseTableName, state, dialect)}`)
|
|
1280
|
+
}
|
|
1281
|
+
for (const join of ast.joins) {
|
|
1282
|
+
if (dialect.name === "mysql" && join.kind === "full") {
|
|
1283
|
+
throw new Error("Unsupported mysql full join")
|
|
1284
|
+
}
|
|
1285
|
+
const source = renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
|
|
1286
|
+
clauses.push(
|
|
1287
|
+
join.kind === "cross"
|
|
1288
|
+
? `cross join ${source}`
|
|
1289
|
+
: `${join.kind} join ${source} on ${renderExpression(join.on!, state, dialect)}`
|
|
1290
|
+
)
|
|
1291
|
+
}
|
|
1292
|
+
if (ast.where.length > 0) {
|
|
1293
|
+
clauses.push(`where ${ast.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect)).join(" and ")}`)
|
|
1294
|
+
}
|
|
1295
|
+
if (ast.groupBy.length > 0) {
|
|
1296
|
+
clauses.push(`group by ${ast.groupBy.map((value: QueryAst.Ast["groupBy"][number]) => renderExpression(value, state, dialect)).join(", ")}`)
|
|
1297
|
+
}
|
|
1298
|
+
if (ast.having.length > 0) {
|
|
1299
|
+
clauses.push(`having ${ast.having.map((entry: QueryAst.HavingClause) => renderExpression(entry.predicate, state, dialect)).join(" and ")}`)
|
|
1300
|
+
}
|
|
1301
|
+
if (ast.orderBy.length > 0) {
|
|
1302
|
+
clauses.push(`order by ${ast.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`)
|
|
1303
|
+
}
|
|
1304
|
+
if (ast.limit) {
|
|
1305
|
+
clauses.push(`limit ${renderExpression(ast.limit, state, dialect)}`)
|
|
1306
|
+
}
|
|
1307
|
+
if (ast.offset) {
|
|
1308
|
+
clauses.push(`offset ${renderExpression(ast.offset, state, dialect)}`)
|
|
1309
|
+
}
|
|
1310
|
+
if (ast.lock) {
|
|
1311
|
+
clauses.push(
|
|
1312
|
+
`${renderSelectLockMode(ast.lock.mode)}${ast.lock.nowait ? " nowait" : ""}${ast.lock.skipLocked ? " skip locked" : ""}`
|
|
1313
|
+
)
|
|
1314
|
+
}
|
|
1315
|
+
sql = clauses.join(" ")
|
|
1316
|
+
break
|
|
1317
|
+
}
|
|
1318
|
+
case "set": {
|
|
1319
|
+
const setAst = ast as QueryAst.Ast<Record<string, unknown>, any, "set">
|
|
1320
|
+
const base = renderQueryAst(
|
|
1321
|
+
Query.getAst(setAst.setBase as Query.Plan.Any) as QueryAst.Ast<
|
|
1322
|
+
Record<string, unknown>,
|
|
1323
|
+
any,
|
|
1324
|
+
QueryAst.QueryStatement
|
|
1325
|
+
>,
|
|
1326
|
+
state,
|
|
1327
|
+
dialect
|
|
1328
|
+
)
|
|
1329
|
+
projections = selectionProjections(setAst.select as Record<string, unknown>)
|
|
1330
|
+
sql = [
|
|
1331
|
+
`(${base.sql})`,
|
|
1332
|
+
...(setAst.setOperations ?? []).map((entry) => {
|
|
1333
|
+
const rendered = renderQueryAst(
|
|
1334
|
+
Query.getAst(entry.query as Query.Plan.Any) as QueryAst.Ast<
|
|
1335
|
+
Record<string, unknown>,
|
|
1336
|
+
any,
|
|
1337
|
+
QueryAst.QueryStatement
|
|
1338
|
+
>,
|
|
1339
|
+
state,
|
|
1340
|
+
dialect
|
|
1341
|
+
)
|
|
1342
|
+
return `${entry.kind}${entry.all ? " all" : ""} (${rendered.sql})`
|
|
1343
|
+
})
|
|
1344
|
+
].join(" ")
|
|
1345
|
+
break
|
|
1346
|
+
}
|
|
1347
|
+
case "insert": {
|
|
1348
|
+
const insertAst = ast as QueryAst.Ast<Record<string, unknown>, any, "insert">
|
|
1349
|
+
const targetSource = insertAst.into!
|
|
1350
|
+
const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
|
|
1351
|
+
const targetCasingState = stateWithTableCasing(state, targetSource.source)
|
|
1352
|
+
const insertSource = insertAst.insertSource
|
|
1353
|
+
const conflict = expectConflictClause(insertAst.conflict)
|
|
1354
|
+
sql = `insert into ${target}`
|
|
1355
|
+
if (insertSource?.kind === "values") {
|
|
1356
|
+
const columns = insertSource.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")
|
|
1357
|
+
const rows = insertSource.rows.map((row) =>
|
|
1358
|
+
`(${row.values.map((entry) => renderExpression(entry.value, targetCasingState, dialect)).join(", ")})`
|
|
1359
|
+
).join(", ")
|
|
1360
|
+
sql += ` (${columns}) values ${rows}`
|
|
1361
|
+
} else if (insertSource?.kind === "query") {
|
|
1362
|
+
const columns = insertSource.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")
|
|
1363
|
+
const renderedQuery = renderQueryAst(
|
|
1364
|
+
Query.getAst(insertSource.query as Query.Plan.Any) as QueryAst.Ast<
|
|
1365
|
+
Record<string, unknown>,
|
|
1366
|
+
any,
|
|
1367
|
+
QueryAst.QueryStatement
|
|
1368
|
+
>,
|
|
1369
|
+
state,
|
|
1370
|
+
dialect
|
|
1371
|
+
)
|
|
1372
|
+
sql += ` (${columns}) ${renderedQuery.sql}`
|
|
1373
|
+
} else if (insertSource?.kind === "unnest") {
|
|
1374
|
+
const columns = insertSource.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")
|
|
1375
|
+
if (dialect.name === "postgres") {
|
|
1376
|
+
const table = targetSource.source as Table.AnyTable
|
|
1377
|
+
const fields = table[Table.TypeId].fields
|
|
1378
|
+
const rendered = insertSource.values.map((entry) =>
|
|
1379
|
+
`cast(${dialect.renderLiteral(encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect), state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
|
|
1380
|
+
).join(", ")
|
|
1381
|
+
sql += ` (${columns}) select * from unnest(${rendered})`
|
|
1382
|
+
} else {
|
|
1383
|
+
const table = targetSource.source as Table.AnyTable
|
|
1384
|
+
const fields = table[Table.TypeId].fields
|
|
1385
|
+
const encodedValues = insertSource.values.map((entry) => ({
|
|
1386
|
+
columnName: entry.columnName,
|
|
1387
|
+
values: encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect)
|
|
1388
|
+
}))
|
|
1389
|
+
const rowCount = encodedValues[0]?.values.length ?? 0
|
|
1390
|
+
const rows = Array.from({ length: rowCount }, (_, index) =>
|
|
1391
|
+
`(${encodedValues.map((entry) => dialect.renderLiteral(entry.values[index], state)).join(", ")})`
|
|
1392
|
+
).join(", ")
|
|
1393
|
+
sql += ` (${columns}) values ${rows}`
|
|
1394
|
+
}
|
|
1395
|
+
} else {
|
|
1396
|
+
const insertValues = insertAst.values ?? []
|
|
1397
|
+
const columns = insertValues.map((entry) => quoteColumn(entry.columnName, state, dialect, targetSource.tableName)).join(", ")
|
|
1398
|
+
const values = insertValues.map((entry) => renderExpression(entry.value, targetCasingState, dialect)).join(", ")
|
|
1399
|
+
if (insertValues.length > 0) {
|
|
1400
|
+
sql += ` (${columns}) values (${values})`
|
|
1401
|
+
} else {
|
|
1402
|
+
sql += " () values ()"
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (conflict) {
|
|
1406
|
+
const conflictValueState = { ...targetCasingState, allowExcluded: true }
|
|
1407
|
+
const updateValues = (conflict.values ?? []).map((entry) =>
|
|
1408
|
+
`${quoteColumn(entry.columnName, state, dialect, targetSource.tableName)} = ${renderExpression(entry.value, conflictValueState, dialect)}`
|
|
1409
|
+
).join(", ")
|
|
1410
|
+
if (dialect.name === "postgres") {
|
|
1411
|
+
const targetSql = conflict.target?.kind === "constraint"
|
|
1412
|
+
? ` on conflict on constraint ${dialect.quoteIdentifier(Casing.applyCategory(targetCasingState.casing, "constraints", conflict.target.name))}`
|
|
1413
|
+
: conflict.target?.kind === "columns"
|
|
1414
|
+
? ` on conflict (${conflict.target.columns.map((column) => quoteColumn(column, state, dialect, targetSource.tableName)).join(", ")})${conflict.target.where ? ` where ${renderExpression(conflict.target.where, targetCasingState, dialect)}` : ""}`
|
|
1415
|
+
: " on conflict"
|
|
1416
|
+
sql += targetSql
|
|
1417
|
+
sql += conflict.action === "doNothing"
|
|
1418
|
+
? " do nothing"
|
|
1419
|
+
: ` do update set ${updateValues}${conflict.where ? ` where ${renderExpression(conflict.where, conflictValueState, dialect)}` : ""}`
|
|
1420
|
+
} else if (conflict.action === "doNothing") {
|
|
1421
|
+
sql = sql.replace(/^insert/, "insert ignore")
|
|
1422
|
+
} else {
|
|
1423
|
+
sql += ` on duplicate key update ${updateValues}`
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const hasReturning = Object.keys(insertAst.select as Record<string, unknown>).length > 0
|
|
1427
|
+
const returning = hasReturning
|
|
1428
|
+
? renderSelectionList(insertAst.select as Record<string, unknown>, state, dialect)
|
|
1429
|
+
: { sql: "", projections: [] }
|
|
1430
|
+
if (dialect.name === "mysql" && returning.sql.length > 0) {
|
|
1431
|
+
throw new Error("Unsupported mysql returning")
|
|
1432
|
+
}
|
|
1433
|
+
projections = returning.projections
|
|
1434
|
+
if (returning.sql.length > 0) {
|
|
1435
|
+
sql += ` returning ${returning.sql}`
|
|
1436
|
+
}
|
|
1437
|
+
break
|
|
1438
|
+
}
|
|
1439
|
+
case "update": {
|
|
1440
|
+
const updateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "update">
|
|
1441
|
+
const targetSource = updateAst.target!
|
|
1442
|
+
const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
|
|
1443
|
+
const targets = updateAst.targets ?? [targetSource]
|
|
1444
|
+
const fromSources = updateAst.fromSources ?? []
|
|
1445
|
+
const assignments = updateAst.set!.map((entry) =>
|
|
1446
|
+
renderMutationAssignment(entry, state, dialect, targetSource.tableName)).join(", ")
|
|
1447
|
+
if (dialect.name === "mysql") {
|
|
1448
|
+
const modifiers = renderMysqlMutationLock(updateAst.lock, "update")
|
|
1449
|
+
const extraSources = renderFromSources(fromSources, state, dialect)
|
|
1450
|
+
const joinSources = updateAst.joins.map((join) =>
|
|
1451
|
+
join.kind === "full"
|
|
1452
|
+
? (() => {
|
|
1453
|
+
throw new Error("Unsupported mysql full join")
|
|
1454
|
+
})()
|
|
1455
|
+
: join.kind === "cross"
|
|
1456
|
+
? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
|
|
1457
|
+
: `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
|
|
1458
|
+
).join(" ")
|
|
1459
|
+
const targetList = [
|
|
1460
|
+
...targets.map((entry) =>
|
|
1461
|
+
renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
|
|
1462
|
+
),
|
|
1463
|
+
...(extraSources.length > 0 ? [extraSources] : [])
|
|
1464
|
+
].join(", ")
|
|
1465
|
+
sql = `update${modifiers} ${targetList}${joinSources.length > 0 ? ` ${joinSources}` : ""} set ${assignments}`
|
|
1466
|
+
} else {
|
|
1467
|
+
sql = `update ${target} set ${assignments}`
|
|
1468
|
+
const mutationSources = [
|
|
1469
|
+
...(fromSources.length > 0 ? [renderFromSources(fromSources, state, dialect)] : []),
|
|
1470
|
+
...(updateAst.joins.length > 0 ? [renderJoinSourcesForMutation(updateAst.joins, state, dialect)] : [])
|
|
1471
|
+
].filter((part) => part.length > 0)
|
|
1472
|
+
if (mutationSources.length > 0) {
|
|
1473
|
+
sql += ` from ${mutationSources.join(", ")}`
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const whereParts = [
|
|
1477
|
+
...(dialect.name === "postgres" ? renderJoinPredicatesForMutation(updateAst.joins, state, dialect) : []),
|
|
1478
|
+
...updateAst.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect))
|
|
1479
|
+
]
|
|
1480
|
+
if (whereParts.length > 0) {
|
|
1481
|
+
sql += ` where ${whereParts.join(" and ")}`
|
|
1482
|
+
}
|
|
1483
|
+
if (dialect.name === "mysql" && updateAst.orderBy.length > 0) {
|
|
1484
|
+
sql += ` order by ${updateAst.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`
|
|
1485
|
+
}
|
|
1486
|
+
if (dialect.name === "mysql" && updateAst.limit) {
|
|
1487
|
+
sql += ` limit ${renderMysqlMutationLimit(updateAst.limit, state, dialect)}`
|
|
1488
|
+
}
|
|
1489
|
+
const hasReturning = Object.keys(updateAst.select as Record<string, unknown>).length > 0
|
|
1490
|
+
const returning = hasReturning
|
|
1491
|
+
? renderSelectionList(updateAst.select as Record<string, unknown>, state, dialect)
|
|
1492
|
+
: { sql: "", projections: [] }
|
|
1493
|
+
if (dialect.name === "mysql" && returning.sql.length > 0) {
|
|
1494
|
+
throw new Error("Unsupported mysql returning")
|
|
1495
|
+
}
|
|
1496
|
+
projections = returning.projections
|
|
1497
|
+
if (returning.sql.length > 0) {
|
|
1498
|
+
sql += ` returning ${returning.sql}`
|
|
1499
|
+
}
|
|
1500
|
+
break
|
|
1501
|
+
}
|
|
1502
|
+
case "delete": {
|
|
1503
|
+
const deleteAst = ast as QueryAst.Ast<Record<string, unknown>, any, "delete">
|
|
1504
|
+
const targetSource = deleteAst.target!
|
|
1505
|
+
const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
|
|
1506
|
+
const targets = deleteAst.targets ?? [targetSource]
|
|
1507
|
+
if (dialect.name === "mysql") {
|
|
1508
|
+
const modifiers = renderMysqlMutationLock(deleteAst.lock, "delete")
|
|
1509
|
+
const hasJoinedSources = deleteAst.joins.length > 0 || targets.length > 1
|
|
1510
|
+
const targetList = renderDeleteTargets(targets, state, dialect)
|
|
1511
|
+
const fromSources = targets.map((entry) =>
|
|
1512
|
+
renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
|
|
1513
|
+
).join(", ")
|
|
1514
|
+
const joinSources = deleteAst.joins.map((join) =>
|
|
1515
|
+
join.kind === "full"
|
|
1516
|
+
? (() => {
|
|
1517
|
+
throw new Error("Unsupported mysql full join")
|
|
1518
|
+
})()
|
|
1519
|
+
: join.kind === "cross"
|
|
1520
|
+
? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
|
|
1521
|
+
: `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
|
|
1522
|
+
).join(" ")
|
|
1523
|
+
sql = hasJoinedSources
|
|
1524
|
+
? `delete${modifiers} ${targetList} from ${fromSources}${joinSources.length > 0 ? ` ${joinSources}` : ""}`
|
|
1525
|
+
: `delete${modifiers} from ${fromSources}`
|
|
1526
|
+
} else {
|
|
1527
|
+
sql = `delete from ${target}`
|
|
1528
|
+
if (deleteAst.joins.length > 0) {
|
|
1529
|
+
sql += ` using ${renderJoinSourcesForMutation(deleteAst.joins, state, dialect)}`
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const whereParts = [
|
|
1533
|
+
...(dialect.name === "postgres" ? renderJoinPredicatesForMutation(deleteAst.joins, state, dialect) : []),
|
|
1534
|
+
...deleteAst.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect))
|
|
1535
|
+
]
|
|
1536
|
+
if (whereParts.length > 0) {
|
|
1537
|
+
sql += ` where ${whereParts.join(" and ")}`
|
|
1538
|
+
}
|
|
1539
|
+
if (dialect.name === "mysql" && deleteAst.orderBy.length > 0) {
|
|
1540
|
+
sql += ` order by ${deleteAst.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`
|
|
1541
|
+
}
|
|
1542
|
+
if (dialect.name === "mysql" && deleteAst.limit) {
|
|
1543
|
+
sql += ` limit ${renderMysqlMutationLimit(deleteAst.limit, state, dialect)}`
|
|
1544
|
+
}
|
|
1545
|
+
const hasReturning = Object.keys(deleteAst.select as Record<string, unknown>).length > 0
|
|
1546
|
+
const returning = hasReturning
|
|
1547
|
+
? renderSelectionList(deleteAst.select as Record<string, unknown>, state, dialect)
|
|
1548
|
+
: { sql: "", projections: [] }
|
|
1549
|
+
if (dialect.name === "mysql" && returning.sql.length > 0) {
|
|
1550
|
+
throw new Error("Unsupported mysql returning")
|
|
1551
|
+
}
|
|
1552
|
+
projections = returning.projections
|
|
1553
|
+
if (returning.sql.length > 0) {
|
|
1554
|
+
sql += ` returning ${returning.sql}`
|
|
1555
|
+
}
|
|
1556
|
+
break
|
|
1557
|
+
}
|
|
1558
|
+
case "truncate": {
|
|
1559
|
+
const truncateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "truncate">
|
|
1560
|
+
const truncate = expectTruncateClause(truncateAst.truncate)
|
|
1561
|
+
const targetSource = truncateAst.target!
|
|
1562
|
+
const restartIdentity = truncate.restartIdentity
|
|
1563
|
+
const cascade = truncate.cascade
|
|
1564
|
+
if (dialect.name === "mysql" && (restartIdentity || cascade)) {
|
|
1565
|
+
throw new Error("Unsupported mysql truncate options")
|
|
1566
|
+
}
|
|
1567
|
+
sql = `truncate table ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
|
|
1568
|
+
if (restartIdentity) {
|
|
1569
|
+
sql += " restart identity"
|
|
1570
|
+
}
|
|
1571
|
+
if (cascade) {
|
|
1572
|
+
sql += " cascade"
|
|
1573
|
+
}
|
|
1574
|
+
break
|
|
1575
|
+
}
|
|
1576
|
+
case "merge": {
|
|
1577
|
+
if (dialect.name !== "postgres") {
|
|
1578
|
+
throw new Error(`Unsupported merge statement for ${dialect.name}`)
|
|
1579
|
+
}
|
|
1580
|
+
const mergeAst = ast as QueryAst.Ast<Record<string, unknown>, any, "merge">
|
|
1581
|
+
const targetSource = mergeAst.target!
|
|
1582
|
+
const usingSource = mergeAst.using!
|
|
1583
|
+
const merge = mergeAst.merge!
|
|
1584
|
+
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)}`
|
|
1585
|
+
if (merge.whenMatched) {
|
|
1586
|
+
sql += " when matched"
|
|
1587
|
+
if (merge.whenMatched.predicate) {
|
|
1588
|
+
sql += ` and ${renderExpression(merge.whenMatched.predicate, state, dialect)}`
|
|
1589
|
+
}
|
|
1590
|
+
if (merge.whenMatched.kind === "delete") {
|
|
1591
|
+
sql += " then delete"
|
|
1592
|
+
} else {
|
|
1593
|
+
sql += ` then update set ${merge.whenMatched.values.map((entry) =>
|
|
1594
|
+
`${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
|
|
1595
|
+
).join(", ")}`
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (merge.whenNotMatched) {
|
|
1599
|
+
sql += " when not matched"
|
|
1600
|
+
if (merge.whenNotMatched.predicate) {
|
|
1601
|
+
sql += ` and ${renderExpression(merge.whenNotMatched.predicate, state, dialect)}`
|
|
1602
|
+
}
|
|
1603
|
+
sql += ` then insert (${merge.whenNotMatched.values.map((entry) => dialect.quoteIdentifier(entry.columnName)).join(", ")}) values (${merge.whenNotMatched.values.map((entry) => renderExpression(entry.value, state, dialect)).join(", ")})`
|
|
1604
|
+
}
|
|
1605
|
+
break
|
|
1606
|
+
}
|
|
1607
|
+
case "transaction":
|
|
1608
|
+
case "commit":
|
|
1609
|
+
case "rollback":
|
|
1610
|
+
case "savepoint":
|
|
1611
|
+
case "rollbackTo":
|
|
1612
|
+
case "releaseSavepoint": {
|
|
1613
|
+
sql = renderTransactionClause(ast.transaction!, dialect)
|
|
1614
|
+
break
|
|
1615
|
+
}
|
|
1616
|
+
case "createTable": {
|
|
1617
|
+
const createTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createTable">
|
|
1618
|
+
const ddl = expectDdlClauseKind(createTableAst.ddl, "createTable")
|
|
1619
|
+
sql = renderCreateTableSql(createTableAst.target!, state, dialect, ddl.ifNotExists)
|
|
1620
|
+
break
|
|
1621
|
+
}
|
|
1622
|
+
case "dropTable": {
|
|
1623
|
+
const dropTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropTable">
|
|
1624
|
+
const ddl = expectDdlClauseKind(dropTableAst.ddl, "dropTable")
|
|
1625
|
+
const ifExists = normalizeStatementFlag(ddl.ifExists)
|
|
1626
|
+
sql = `drop table${ifExists ? " if exists" : ""} ${renderSourceReference(dropTableAst.target!.source, dropTableAst.target!.tableName, dropTableAst.target!.baseTableName, state, dialect)}`
|
|
1627
|
+
break
|
|
1628
|
+
}
|
|
1629
|
+
case "createIndex": {
|
|
1630
|
+
const createIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createIndex">
|
|
1631
|
+
sql = renderCreateIndexSql(
|
|
1632
|
+
createIndexAst.target!,
|
|
1633
|
+
expectDdlClauseKind(createIndexAst.ddl, "createIndex"),
|
|
1634
|
+
state,
|
|
1635
|
+
dialect
|
|
1636
|
+
)
|
|
1637
|
+
break
|
|
1638
|
+
}
|
|
1639
|
+
case "dropIndex": {
|
|
1640
|
+
const dropIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropIndex">
|
|
1641
|
+
sql = renderDropIndexSql(
|
|
1642
|
+
dropIndexAst.target!,
|
|
1643
|
+
expectDdlClauseKind(dropIndexAst.ddl, "dropIndex"),
|
|
1644
|
+
state,
|
|
1645
|
+
dialect
|
|
1646
|
+
)
|
|
1647
|
+
break
|
|
1648
|
+
}
|
|
1649
|
+
default: {
|
|
1650
|
+
if (ast.transaction !== undefined) {
|
|
1651
|
+
sql = renderTransactionClause(ast.transaction, dialect)
|
|
1652
|
+
}
|
|
1653
|
+
break
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
if (state.ctes.length === 0 || options.emitCtes === false) {
|
|
1658
|
+
return {
|
|
1659
|
+
sql,
|
|
1660
|
+
projections
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
sql: `with${state.ctes.some((entry) => entry.recursive) ? " recursive" : ""} ${state.ctes.map((entry) => `${dialect.quoteIdentifier(entry.name)} as (${entry.sql})`).join(", ")} ${sql}`,
|
|
1665
|
+
projections
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const renderSourceReference = (
|
|
1670
|
+
source: unknown,
|
|
1671
|
+
tableName: string,
|
|
1672
|
+
baseTableName: string,
|
|
1673
|
+
state: RenderState,
|
|
1674
|
+
dialect: SqlDialect
|
|
1675
|
+
): string => {
|
|
1676
|
+
const renderSelectRows = (
|
|
1677
|
+
rows: readonly Record<string, Expression.Any>[],
|
|
1678
|
+
columnNames: readonly string[]
|
|
1679
|
+
): string => {
|
|
1680
|
+
const renderedRows = rows.map((row) =>
|
|
1681
|
+
`select ${columnNames.map((columnName) =>
|
|
1682
|
+
`${renderExpression(row[columnName]!, state, dialect)} as ${dialect.quoteIdentifier(columnName)}`
|
|
1683
|
+
).join(", ")}`
|
|
1684
|
+
)
|
|
1685
|
+
return `(${renderedRows.join(" union all ")}) as ${dialect.quoteIdentifier(tableName)}(${columnNames.map((columnName) => dialect.quoteIdentifier(columnName)).join(", ")})`
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
const renderUnnestRows = (
|
|
1689
|
+
arrays: Readonly<Record<string, readonly Expression.Any[]>>,
|
|
1690
|
+
columnNames: readonly string[]
|
|
1691
|
+
): string => {
|
|
1692
|
+
const rowCount = arrays[columnNames[0]!]!.length
|
|
1693
|
+
const rows = Array.from({ length: rowCount }, (_, index) =>
|
|
1694
|
+
Object.fromEntries(columnNames.map((columnName) => [columnName, arrays[columnName]![index]!] as const)) as Record<string, Expression.Any>
|
|
1695
|
+
)
|
|
1696
|
+
return renderSelectRows(rows, columnNames)
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "cte") {
|
|
1700
|
+
const cte = source as unknown as {
|
|
1701
|
+
readonly name: string
|
|
1702
|
+
readonly plan: Query.Plan.Any
|
|
1703
|
+
readonly recursive?: boolean
|
|
1704
|
+
}
|
|
1705
|
+
const registeredCteSource = state.cteSources.get(cte.name)
|
|
1706
|
+
if (registeredCteSource !== undefined && registeredCteSource !== cte.plan) {
|
|
1707
|
+
throw new Error(`common table expression name is already registered with a different plan: ${cte.name}`)
|
|
1708
|
+
}
|
|
1709
|
+
if (!state.cteNames.has(cte.name)) {
|
|
1710
|
+
state.cteNames.add(cte.name)
|
|
1711
|
+
state.cteSources.set(cte.name, cte.plan)
|
|
1712
|
+
const statement = Query.getQueryState(cte.plan).statement
|
|
1713
|
+
if (statement !== "select" && statement !== "set") {
|
|
1714
|
+
const cteAst = Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>
|
|
1715
|
+
if (Object.keys((cteAst.select ?? {}) as Record<string, unknown>).length > 0) {
|
|
1716
|
+
throw new Error("Unsupported mysql returning")
|
|
1717
|
+
}
|
|
1718
|
+
throw new Error("Unsupported mysql data-modifying cte")
|
|
1719
|
+
}
|
|
1720
|
+
const rendered = renderQueryAst(
|
|
1721
|
+
Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
1722
|
+
state,
|
|
1723
|
+
dialect,
|
|
1724
|
+
{ emitCtes: false }
|
|
1725
|
+
)
|
|
1726
|
+
state.ctes.push({
|
|
1727
|
+
name: cte.name,
|
|
1728
|
+
sql: rendered.sql,
|
|
1729
|
+
recursive: cte.recursive
|
|
1730
|
+
})
|
|
1731
|
+
}
|
|
1732
|
+
return dialect.quoteIdentifier(cte.name)
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "derived") {
|
|
1735
|
+
const derived = source as unknown as {
|
|
1736
|
+
readonly name: string
|
|
1737
|
+
readonly plan: Query.Plan.Any
|
|
1738
|
+
}
|
|
1739
|
+
if (!state.cteNames.has(derived.name)) {
|
|
1740
|
+
// derived tables are inlined, so no CTE registration is needed
|
|
1741
|
+
}
|
|
1742
|
+
return `(${renderQueryAst(Query.getAst(derived.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, nestedRenderState(state), dialect).sql}) as ${dialect.quoteIdentifier(derived.name)}`
|
|
1743
|
+
}
|
|
1744
|
+
if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "lateral") {
|
|
1745
|
+
const lateral = source as unknown as {
|
|
1746
|
+
readonly name: string
|
|
1747
|
+
readonly plan: Query.Plan.Any
|
|
1748
|
+
}
|
|
1749
|
+
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)}`
|
|
1750
|
+
}
|
|
1751
|
+
if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "values") {
|
|
1752
|
+
const values = source as unknown as {
|
|
1753
|
+
readonly columns: Record<string, Expression.Any>
|
|
1754
|
+
readonly rows: readonly Record<string, Expression.Any>[]
|
|
1755
|
+
}
|
|
1756
|
+
return renderSelectRows(values.rows, Object.keys(values.columns))
|
|
1757
|
+
}
|
|
1758
|
+
if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "unnest") {
|
|
1759
|
+
const unnest = source as unknown as {
|
|
1760
|
+
readonly columns: Record<string, Expression.Any>
|
|
1761
|
+
readonly arrays: Readonly<Record<string, readonly Expression.Any[]>>
|
|
1762
|
+
}
|
|
1763
|
+
return renderUnnestRows(unnest.arrays, Object.keys(unnest.columns))
|
|
1764
|
+
}
|
|
1765
|
+
if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "tableFunction") {
|
|
1766
|
+
const tableFunction = source as unknown as {
|
|
1767
|
+
readonly name: string
|
|
1768
|
+
readonly columns: Record<string, Expression.Any>
|
|
1769
|
+
readonly functionName: string
|
|
1770
|
+
readonly args: readonly Expression.Any[]
|
|
1771
|
+
}
|
|
1772
|
+
if (dialect.name !== "postgres") {
|
|
1773
|
+
throw new Error("Unsupported table function source for SQL rendering")
|
|
1774
|
+
}
|
|
1775
|
+
const functionName = renderFunctionName(tableFunction.functionName)
|
|
1776
|
+
const columnNames = Object.keys(tableFunction.columns)
|
|
1777
|
+
return `${functionName}(${tableFunction.args.map((arg) => renderExpression(arg, state, dialect)).join(", ")}) as ${dialect.quoteIdentifier(tableFunction.name)}(${columnNames.map((columnName) => dialect.quoteIdentifier(columnName)).join(", ")})`
|
|
1778
|
+
}
|
|
1779
|
+
const schemaName = typeof source === "object" && source !== null && Table.TypeId in source
|
|
1780
|
+
? (source as Table.AnyTable)[Table.TypeId].schemaName
|
|
1781
|
+
: undefined
|
|
1782
|
+
if (typeof source === "object" && source !== null && Table.TypeId in source) {
|
|
1783
|
+
const table = source as Table.AnyTable
|
|
1784
|
+
const tableState = table[Table.TypeId]
|
|
1785
|
+
const casing = casingForTable(table, state)
|
|
1786
|
+
const renderedTableName = tableState.kind === "alias"
|
|
1787
|
+
? tableName
|
|
1788
|
+
: Casing.applyCategory(casing, "tables", baseTableName)
|
|
1789
|
+
const renderedBaseName = Casing.applyCategory(casing, "tables", baseTableName)
|
|
1790
|
+
const renderedSchemaName = schemaName === undefined
|
|
1791
|
+
? undefined
|
|
1792
|
+
: Casing.applyCategory(casing, "schemas", schemaName)
|
|
1793
|
+
return dialect.renderTableReference(renderedTableName, renderedBaseName, renderedSchemaName)
|
|
1794
|
+
}
|
|
1795
|
+
return dialect.renderTableReference(
|
|
1796
|
+
Casing.applyCategory(state.casing, "tables", tableName),
|
|
1797
|
+
Casing.applyCategory(state.casing, "tables", baseTableName),
|
|
1798
|
+
schemaName === undefined ? undefined : Casing.applyCategory(state.casing, "schemas", schemaName)
|
|
1799
|
+
)
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const renderSubqueryExpressionPlan = (
|
|
1803
|
+
plan: Query.Plan.Any,
|
|
1804
|
+
state: RenderState,
|
|
1805
|
+
dialect: SqlDialect
|
|
1806
|
+
): string => {
|
|
1807
|
+
const statement = Query.getQueryState(plan).statement
|
|
1808
|
+
if (statement !== "select" && statement !== "set") {
|
|
1809
|
+
throw new Error("subquery expressions only accept select-like query plans")
|
|
1810
|
+
}
|
|
1811
|
+
return renderQueryAst(
|
|
1812
|
+
Query.getAst(plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
1813
|
+
state,
|
|
1814
|
+
dialect
|
|
1815
|
+
).sql
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Renders a scalar expression AST into SQL text.
|
|
1820
|
+
*
|
|
1821
|
+
* This is parameterized by a runtime dialect so the same expression walker can
|
|
1822
|
+
* be reused across dialect-specific renderers while still delegating quoting
|
|
1823
|
+
* and literal serialization to the concrete dialect implementation.
|
|
1824
|
+
*/
|
|
1825
|
+
export const renderExpression = (
|
|
1826
|
+
expression: Expression.Any,
|
|
1827
|
+
state: RenderState,
|
|
1828
|
+
dialect: SqlDialect
|
|
1829
|
+
): string => {
|
|
1830
|
+
const rawAst = (expression as Expression.Any & {
|
|
1831
|
+
readonly [ExpressionAst.TypeId]: ExpressionAst.Any
|
|
1832
|
+
})[ExpressionAst.TypeId] as ExpressionAst.Any | Record<string, unknown>
|
|
1833
|
+
const jsonSql = renderJsonExpression(expression, rawAst as Record<string, unknown>, state, dialect)
|
|
1834
|
+
if (jsonSql !== undefined) {
|
|
1835
|
+
return jsonSql
|
|
1836
|
+
}
|
|
1837
|
+
const ast = rawAst as ExpressionAst.Any
|
|
1838
|
+
const renderComparisonOperator = (operator: unknown): "=" | "<>" | "<" | "<=" | ">" | ">=" =>
|
|
1839
|
+
({
|
|
1840
|
+
eq: "=",
|
|
1841
|
+
neq: "<>",
|
|
1842
|
+
lt: "<",
|
|
1843
|
+
lte: "<=",
|
|
1844
|
+
gt: ">",
|
|
1845
|
+
gte: ">="
|
|
1846
|
+
} as const)[operator as "eq" | "neq" | "lt" | "lte" | "gt" | "gte"]!
|
|
1847
|
+
switch (ast.kind) {
|
|
1848
|
+
case "column":
|
|
1849
|
+
return state.rowLocalColumns || ast.tableName.length === 0
|
|
1850
|
+
? quoteColumn(ast.columnName, state, dialect, ast.tableName)
|
|
1851
|
+
: `${dialect.quoteIdentifier(casedTableReferenceName(ast.tableName, state))}.${quoteColumn(ast.columnName, state, dialect, ast.tableName)}`
|
|
1852
|
+
case "literal":
|
|
1853
|
+
if (typeof ast.value === "number" && !Number.isFinite(ast.value)) {
|
|
1854
|
+
throw new Error("Expected a finite numeric value")
|
|
1855
|
+
}
|
|
1856
|
+
return dialect.renderLiteral(ast.value, state, expression[Expression.TypeId])
|
|
1857
|
+
case "excluded":
|
|
1858
|
+
if (state.allowExcluded !== true) {
|
|
1859
|
+
throw new Error("excluded(...) is only supported inside insert conflict handlers")
|
|
1860
|
+
}
|
|
1861
|
+
return dialect.name === "mysql"
|
|
1862
|
+
? `values(${quoteColumn(ast.columnName, state, dialect)})`
|
|
1863
|
+
: `excluded.${quoteColumn(ast.columnName, state, dialect)}`
|
|
1864
|
+
case "cast":
|
|
1865
|
+
return `cast(${renderExpression(expectValueExpression("cast", ast.value), state, dialect)} as ${renderCastType(dialect, ast.target)})`
|
|
1866
|
+
case "function":
|
|
1867
|
+
return renderFunctionCall(ast.name, ast.args, state, dialect)
|
|
1868
|
+
case "eq":
|
|
1869
|
+
return renderBinaryExpression("eq", "=", ast.left, ast.right, state, dialect)
|
|
1870
|
+
case "neq":
|
|
1871
|
+
return renderBinaryExpression("neq", "<>", ast.left, ast.right, state, dialect)
|
|
1872
|
+
case "lt":
|
|
1873
|
+
return renderBinaryExpression("lt", "<", ast.left, ast.right, state, dialect)
|
|
1874
|
+
case "lte":
|
|
1875
|
+
return renderBinaryExpression("lte", "<=", ast.left, ast.right, state, dialect)
|
|
1876
|
+
case "gt":
|
|
1877
|
+
return renderBinaryExpression("gt", ">", ast.left, ast.right, state, dialect)
|
|
1878
|
+
case "gte":
|
|
1879
|
+
return renderBinaryExpression("gte", ">=", ast.left, ast.right, state, dialect)
|
|
1880
|
+
case "like":
|
|
1881
|
+
return renderBinaryExpression("like", "like", ast.left, ast.right, state, dialect)
|
|
1882
|
+
case "ilike": {
|
|
1883
|
+
const [left, right] = expectBinaryExpressions("ilike", ast.left, ast.right)
|
|
1884
|
+
return dialect.name === "postgres"
|
|
1885
|
+
? `(${renderExpression(left, state, dialect)} ilike ${renderExpression(right, state, dialect)})`
|
|
1886
|
+
: `(lower(${renderExpression(left, state, dialect)}) like lower(${renderExpression(right, state, dialect)}))`
|
|
1887
|
+
}
|
|
1888
|
+
case "regexMatch": {
|
|
1889
|
+
const [left, right] = expectBinaryExpressions("regexMatch", ast.left, ast.right)
|
|
1890
|
+
return dialect.name === "postgres"
|
|
1891
|
+
? `(${renderExpression(left, state, dialect)} ~ ${renderExpression(right, state, dialect)})`
|
|
1892
|
+
: `(${renderExpression(left, state, dialect)} regexp ${renderExpression(right, state, dialect)})`
|
|
1893
|
+
}
|
|
1894
|
+
case "regexIMatch": {
|
|
1895
|
+
const [left, right] = expectBinaryExpressions("regexIMatch", ast.left, ast.right)
|
|
1896
|
+
return dialect.name === "postgres"
|
|
1897
|
+
? `(${renderExpression(left, state, dialect)} ~* ${renderExpression(right, state, dialect)})`
|
|
1898
|
+
: `(${renderExpression(left, state, dialect)} regexp ${renderExpression(right, state, dialect)})`
|
|
1899
|
+
}
|
|
1900
|
+
case "regexNotMatch": {
|
|
1901
|
+
const [left, right] = expectBinaryExpressions("regexNotMatch", ast.left, ast.right)
|
|
1902
|
+
return dialect.name === "postgres"
|
|
1903
|
+
? `(${renderExpression(left, state, dialect)} !~ ${renderExpression(right, state, dialect)})`
|
|
1904
|
+
: `(${renderExpression(left, state, dialect)} not regexp ${renderExpression(right, state, dialect)})`
|
|
1905
|
+
}
|
|
1906
|
+
case "regexNotIMatch": {
|
|
1907
|
+
const [left, right] = expectBinaryExpressions("regexNotIMatch", ast.left, ast.right)
|
|
1908
|
+
return dialect.name === "postgres"
|
|
1909
|
+
? `(${renderExpression(left, state, dialect)} !~* ${renderExpression(right, state, dialect)})`
|
|
1910
|
+
: `(${renderExpression(left, state, dialect)} not regexp ${renderExpression(right, state, dialect)})`
|
|
1911
|
+
}
|
|
1912
|
+
case "isDistinctFrom": {
|
|
1913
|
+
const [left, right] = expectBinaryExpressions("isDistinctFrom", ast.left, ast.right)
|
|
1914
|
+
return dialect.name === "mysql"
|
|
1915
|
+
? `(not (${renderExpression(left, state, dialect)} <=> ${renderExpression(right, state, dialect)}))`
|
|
1916
|
+
: `(${renderExpression(left, state, dialect)} is distinct from ${renderExpression(right, state, dialect)})`
|
|
1917
|
+
}
|
|
1918
|
+
case "isNotDistinctFrom": {
|
|
1919
|
+
const [left, right] = expectBinaryExpressions("isNotDistinctFrom", ast.left, ast.right)
|
|
1920
|
+
return dialect.name === "mysql"
|
|
1921
|
+
? `(${renderExpression(left, state, dialect)} <=> ${renderExpression(right, state, dialect)})`
|
|
1922
|
+
: `(${renderExpression(left, state, dialect)} is not distinct from ${renderExpression(right, state, dialect)})`
|
|
1923
|
+
}
|
|
1924
|
+
case "contains": {
|
|
1925
|
+
const [leftExpression, rightExpression] = expectBinaryExpressions("contains", ast.left, ast.right)
|
|
1926
|
+
if (dialect.name === "postgres") {
|
|
1927
|
+
const left = isJsonExpression(leftExpression)
|
|
1928
|
+
? renderPostgresJsonValue(leftExpression, state, dialect)
|
|
1929
|
+
: renderExpression(leftExpression, state, dialect)
|
|
1930
|
+
const right = isJsonExpression(rightExpression)
|
|
1931
|
+
? renderPostgresJsonValue(rightExpression, state, dialect)
|
|
1932
|
+
: renderExpression(rightExpression, state, dialect)
|
|
1933
|
+
return `(${left} @> ${right})`
|
|
1934
|
+
}
|
|
1935
|
+
if (dialect.name === "mysql" && isJsonExpression(leftExpression) && isJsonExpression(rightExpression)) {
|
|
1936
|
+
return `json_contains(${renderJsonInputExpression(leftExpression, state, dialect)}, ${renderJsonInputExpression(rightExpression, state, dialect)})`
|
|
1937
|
+
}
|
|
1938
|
+
throw new Error("Unsupported container operator for SQL rendering")
|
|
1939
|
+
}
|
|
1940
|
+
case "containedBy": {
|
|
1941
|
+
const [leftExpression, rightExpression] = expectBinaryExpressions("containedBy", ast.left, ast.right)
|
|
1942
|
+
if (dialect.name === "postgres") {
|
|
1943
|
+
const left = isJsonExpression(leftExpression)
|
|
1944
|
+
? renderPostgresJsonValue(leftExpression, state, dialect)
|
|
1945
|
+
: renderExpression(leftExpression, state, dialect)
|
|
1946
|
+
const right = isJsonExpression(rightExpression)
|
|
1947
|
+
? renderPostgresJsonValue(rightExpression, state, dialect)
|
|
1948
|
+
: renderExpression(rightExpression, state, dialect)
|
|
1949
|
+
return `(${left} <@ ${right})`
|
|
1950
|
+
}
|
|
1951
|
+
if (dialect.name === "mysql" && isJsonExpression(leftExpression) && isJsonExpression(rightExpression)) {
|
|
1952
|
+
return `json_contains(${renderJsonInputExpression(rightExpression, state, dialect)}, ${renderJsonInputExpression(leftExpression, state, dialect)})`
|
|
1953
|
+
}
|
|
1954
|
+
throw new Error("Unsupported container operator for SQL rendering")
|
|
1955
|
+
}
|
|
1956
|
+
case "overlaps": {
|
|
1957
|
+
const [leftExpression, rightExpression] = expectBinaryExpressions("overlaps", ast.left, ast.right)
|
|
1958
|
+
if (dialect.name === "postgres") {
|
|
1959
|
+
const left = isJsonExpression(leftExpression)
|
|
1960
|
+
? renderPostgresJsonValue(leftExpression, state, dialect)
|
|
1961
|
+
: renderExpression(leftExpression, state, dialect)
|
|
1962
|
+
const right = isJsonExpression(rightExpression)
|
|
1963
|
+
? renderPostgresJsonValue(rightExpression, state, dialect)
|
|
1964
|
+
: renderExpression(rightExpression, state, dialect)
|
|
1965
|
+
return `(${left} && ${right})`
|
|
1966
|
+
}
|
|
1967
|
+
if (dialect.name === "mysql" && isJsonExpression(leftExpression) && isJsonExpression(rightExpression)) {
|
|
1968
|
+
return `json_overlaps(${renderJsonInputExpression(leftExpression, state, dialect)}, ${renderJsonInputExpression(rightExpression, state, dialect)})`
|
|
1969
|
+
}
|
|
1970
|
+
throw new Error("Unsupported container operator for SQL rendering")
|
|
1971
|
+
}
|
|
1972
|
+
case "isNull":
|
|
1973
|
+
return `(${renderExpression(expectValueExpression("isNull", ast.value), state, dialect)} is null)`
|
|
1974
|
+
case "isNotNull":
|
|
1975
|
+
return `(${renderExpression(expectValueExpression("isNotNull", ast.value), state, dialect)} is not null)`
|
|
1976
|
+
case "not":
|
|
1977
|
+
return `(not ${renderExpression(expectValueExpression("not", ast.value), state, dialect)})`
|
|
1978
|
+
case "upper":
|
|
1979
|
+
return `upper(${renderExpression(expectValueExpression("upper", ast.value), state, dialect)})`
|
|
1980
|
+
case "lower":
|
|
1981
|
+
return `lower(${renderExpression(expectValueExpression("lower", ast.value), state, dialect)})`
|
|
1982
|
+
case "count":
|
|
1983
|
+
return `count(${renderExpression(expectValueExpression("count", ast.value), state, dialect)})`
|
|
1984
|
+
case "max":
|
|
1985
|
+
return `max(${renderExpression(expectValueExpression("max", ast.value), state, dialect)})`
|
|
1986
|
+
case "min":
|
|
1987
|
+
return `min(${renderExpression(expectValueExpression("min", ast.value), state, dialect)})`
|
|
1988
|
+
case "and":
|
|
1989
|
+
return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" and ")})`
|
|
1990
|
+
case "or":
|
|
1991
|
+
return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" or ")})`
|
|
1992
|
+
case "coalesce":
|
|
1993
|
+
return `coalesce(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")})`
|
|
1994
|
+
case "in":
|
|
1995
|
+
return `(${renderExpression(ast.values[0]!, state, dialect)} in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
|
|
1996
|
+
case "notIn":
|
|
1997
|
+
return `(${renderExpression(ast.values[0]!, state, dialect)} not in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
|
|
1998
|
+
case "between":
|
|
1999
|
+
return `(${renderExpression(ast.values[0]!, state, dialect)} between ${renderExpression(ast.values[1]!, state, dialect)} and ${renderExpression(ast.values[2]!, state, dialect)})`
|
|
2000
|
+
case "concat":
|
|
2001
|
+
return dialect.renderConcat(ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)))
|
|
2002
|
+
case "case":
|
|
2003
|
+
return `case ${ast.branches.map((branch) =>
|
|
2004
|
+
`when ${renderExpression(branch.when, state, dialect)} then ${renderExpression(branch.then, state, dialect)}`
|
|
2005
|
+
).join(" ")} else ${renderExpression(ast.else, state, dialect)} end`
|
|
2006
|
+
case "exists":
|
|
2007
|
+
return `exists (${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
|
|
2008
|
+
case "scalarSubquery":
|
|
2009
|
+
return `(${renderSubqueryExpressionPlan(ast.plan, state, dialect)})`
|
|
2010
|
+
case "inSubquery":
|
|
2011
|
+
return `(${renderExpression(expectValueExpression("inSubquery", ast.left), state, dialect)} in (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
|
|
2012
|
+
case "comparisonAny":
|
|
2013
|
+
return `(${renderExpression(expectValueExpression("compareAny", ast.left), state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
|
|
2014
|
+
case "comparisonAll":
|
|
2015
|
+
return `(${renderExpression(expectValueExpression("compareAll", ast.left), state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderSubqueryExpressionPlan(ast.plan, state, dialect)}))`
|
|
2016
|
+
case "window": {
|
|
2017
|
+
const partitionBy = ast.partitionBy as readonly Expression.Any[]
|
|
2018
|
+
const orderBy = ast.orderBy as readonly {
|
|
2019
|
+
readonly value: Expression.Any
|
|
2020
|
+
readonly direction: string
|
|
2021
|
+
}[]
|
|
2022
|
+
const clauses: string[] = []
|
|
2023
|
+
if (partitionBy.length > 0) {
|
|
2024
|
+
clauses.push(`partition by ${partitionBy.map((value) => renderExpression(value, state, dialect)).join(", ")}`)
|
|
2025
|
+
}
|
|
2026
|
+
if (orderBy.length > 0) {
|
|
2027
|
+
clauses.push(`order by ${orderBy.map((entry) =>
|
|
2028
|
+
`${renderExpression(entry.value, state, dialect)} ${entry.direction}`
|
|
2029
|
+
).join(", ")}`)
|
|
2030
|
+
}
|
|
2031
|
+
const specification = clauses.join(" ")
|
|
2032
|
+
switch (ast.function) {
|
|
2033
|
+
case "rowNumber":
|
|
2034
|
+
return `row_number() over (${specification})`
|
|
2035
|
+
case "rank":
|
|
2036
|
+
return `rank() over (${specification})`
|
|
2037
|
+
case "denseRank":
|
|
2038
|
+
return `dense_rank() over (${specification})`
|
|
2039
|
+
case "over":
|
|
2040
|
+
return `${renderExpression(ast.value as Expression.Any, state, dialect)} over (${specification})`
|
|
2041
|
+
}
|
|
2042
|
+
break
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
throw new Error("Unsupported expression for SQL rendering")
|
|
2046
|
+
}
|