effect-qb 0.12.3
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 +1294 -0
- package/dist/mysql.js +57575 -0
- package/dist/postgres.js +6303 -0
- package/package.json +42 -0
- package/src/internal/aggregation-validation.ts +57 -0
- package/src/internal/case-analysis.ts +50 -0
- package/src/internal/coercion-analysis.ts +30 -0
- package/src/internal/coercion-errors.ts +29 -0
- package/src/internal/coercion-kind.ts +32 -0
- package/src/internal/coercion-normalize.ts +7 -0
- package/src/internal/coercion-rules.ts +25 -0
- package/src/internal/column-state.ts +453 -0
- package/src/internal/column.ts +417 -0
- package/src/internal/datatypes/define.ts +44 -0
- package/src/internal/datatypes/lookup.ts +280 -0
- package/src/internal/datatypes/shape.ts +72 -0
- package/src/internal/derived-table.ts +149 -0
- package/src/internal/dialect.ts +30 -0
- package/src/internal/executor.ts +390 -0
- package/src/internal/expression-ast.ts +349 -0
- package/src/internal/expression.ts +325 -0
- package/src/internal/grouping-key.ts +82 -0
- package/src/internal/json/ast.ts +63 -0
- package/src/internal/json/errors.ts +13 -0
- package/src/internal/json/path.ts +227 -0
- package/src/internal/json/shape.ts +1 -0
- package/src/internal/json/types.ts +386 -0
- package/src/internal/mysql-dialect.ts +39 -0
- package/src/internal/mysql-renderer.ts +37 -0
- package/src/internal/plan.ts +64 -0
- package/src/internal/postgres-dialect.ts +34 -0
- package/src/internal/postgres-renderer.ts +40 -0
- package/src/internal/predicate-analysis.ts +71 -0
- package/src/internal/predicate-atom.ts +43 -0
- package/src/internal/predicate-branches.ts +40 -0
- package/src/internal/predicate-context.ts +279 -0
- package/src/internal/predicate-formula.ts +100 -0
- package/src/internal/predicate-key.ts +28 -0
- package/src/internal/predicate-nnf.ts +12 -0
- package/src/internal/predicate-normalize.ts +202 -0
- package/src/internal/projection-alias.ts +15 -0
- package/src/internal/projections.ts +101 -0
- package/src/internal/query-ast.ts +297 -0
- package/src/internal/query-factory.ts +6757 -0
- package/src/internal/query-requirements.ts +40 -0
- package/src/internal/query.ts +1590 -0
- package/src/internal/renderer.ts +102 -0
- package/src/internal/runtime-normalize.ts +344 -0
- package/src/internal/runtime-schema.ts +428 -0
- package/src/internal/runtime-value.ts +85 -0
- package/src/internal/schema-derivation.ts +131 -0
- package/src/internal/sql-expression-renderer.ts +1353 -0
- package/src/internal/table-options.ts +225 -0
- package/src/internal/table.ts +674 -0
- package/src/mysql/column.ts +30 -0
- package/src/mysql/datatypes/index.ts +6 -0
- package/src/mysql/datatypes/spec.ts +180 -0
- package/src/mysql/errors/catalog.ts +51662 -0
- package/src/mysql/errors/fields.ts +21 -0
- package/src/mysql/errors/index.ts +18 -0
- package/src/mysql/errors/normalize.ts +232 -0
- package/src/mysql/errors/requirements.ts +73 -0
- package/src/mysql/executor.ts +134 -0
- package/src/mysql/query.ts +189 -0
- package/src/mysql/renderer.ts +19 -0
- package/src/mysql/table.ts +157 -0
- package/src/mysql.ts +18 -0
- package/src/postgres/column.ts +20 -0
- package/src/postgres/datatypes/index.ts +8 -0
- package/src/postgres/datatypes/spec.ts +264 -0
- package/src/postgres/errors/catalog.ts +452 -0
- package/src/postgres/errors/fields.ts +48 -0
- package/src/postgres/errors/index.ts +4 -0
- package/src/postgres/errors/normalize.ts +209 -0
- package/src/postgres/errors/requirements.ts +65 -0
- package/src/postgres/errors/types.ts +38 -0
- package/src/postgres/executor.ts +131 -0
- package/src/postgres/query.ts +189 -0
- package/src/postgres/renderer.ts +29 -0
- package/src/postgres/table.ts +157 -0
- package/src/postgres.ts +18 -0
|
@@ -0,0 +1,1353 @@
|
|
|
1
|
+
import * as Query from "./query.js"
|
|
2
|
+
import * as Expression from "./expression.js"
|
|
3
|
+
import * as Table from "./table.js"
|
|
4
|
+
import * as QueryAst from "./query-ast.js"
|
|
5
|
+
import type { RenderState, SqlDialect } from "./dialect.js"
|
|
6
|
+
import * as ExpressionAst from "./expression-ast.js"
|
|
7
|
+
import * as JsonPath from "./json/path.js"
|
|
8
|
+
import { flattenSelection, type Projection } from "./projections.js"
|
|
9
|
+
import { type SelectionValue, validateAggregationSelection } from "./aggregation-validation.js"
|
|
10
|
+
|
|
11
|
+
const renderDbType = (
|
|
12
|
+
dialect: SqlDialect,
|
|
13
|
+
dbType: Expression.DbType.Any
|
|
14
|
+
): string => {
|
|
15
|
+
if (dialect.name === "mysql" && dbType.dialect === "mysql" && dbType.kind === "uuid") {
|
|
16
|
+
return "char(36)"
|
|
17
|
+
}
|
|
18
|
+
return dbType.kind
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const renderCastType = (
|
|
22
|
+
dialect: SqlDialect,
|
|
23
|
+
dbType: Expression.DbType.Any
|
|
24
|
+
): string => {
|
|
25
|
+
if (dialect.name !== "mysql") {
|
|
26
|
+
return dbType.kind
|
|
27
|
+
}
|
|
28
|
+
switch (dbType.kind) {
|
|
29
|
+
case "text":
|
|
30
|
+
return "char"
|
|
31
|
+
case "uuid":
|
|
32
|
+
return "char(36)"
|
|
33
|
+
case "numeric":
|
|
34
|
+
return "decimal"
|
|
35
|
+
case "timestamp":
|
|
36
|
+
return "datetime"
|
|
37
|
+
case "bool":
|
|
38
|
+
case "boolean":
|
|
39
|
+
return "boolean"
|
|
40
|
+
case "json":
|
|
41
|
+
return "json"
|
|
42
|
+
default:
|
|
43
|
+
return dbType.kind
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const renderColumnDefinition = (
|
|
48
|
+
dialect: SqlDialect,
|
|
49
|
+
columnName: string,
|
|
50
|
+
column: Table.AnyTable[typeof Table.TypeId]["fields"][string]
|
|
51
|
+
): string => {
|
|
52
|
+
const clauses = [
|
|
53
|
+
dialect.quoteIdentifier(columnName),
|
|
54
|
+
renderDbType(dialect, column.metadata.dbType)
|
|
55
|
+
]
|
|
56
|
+
if (!column.metadata.nullable) {
|
|
57
|
+
clauses.push("not null")
|
|
58
|
+
}
|
|
59
|
+
return clauses.join(" ")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const renderCheckPredicate = (
|
|
63
|
+
predicate: unknown,
|
|
64
|
+
state: RenderState,
|
|
65
|
+
dialect: SqlDialect
|
|
66
|
+
): string => {
|
|
67
|
+
if (typeof predicate === "string") {
|
|
68
|
+
return predicate
|
|
69
|
+
}
|
|
70
|
+
if (predicate !== null && typeof predicate === "object" && Expression.TypeId in predicate) {
|
|
71
|
+
return renderExpression(predicate as Expression.Any, state, dialect)
|
|
72
|
+
}
|
|
73
|
+
throw new Error("Unsupported check constraint predicate for DDL rendering")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const renderCreateTableSql = (
|
|
77
|
+
targetSource: QueryAst.FromClause,
|
|
78
|
+
state: RenderState,
|
|
79
|
+
dialect: SqlDialect,
|
|
80
|
+
ifNotExists: boolean
|
|
81
|
+
): string => {
|
|
82
|
+
const table = targetSource.source as Table.AnyTable
|
|
83
|
+
const fields = table[Table.TypeId].fields
|
|
84
|
+
const definitions = Object.entries(fields).map(([columnName, column]) =>
|
|
85
|
+
renderColumnDefinition(dialect, columnName, column)
|
|
86
|
+
)
|
|
87
|
+
for (const option of table[Table.OptionsSymbol]) {
|
|
88
|
+
switch (option.kind) {
|
|
89
|
+
case "primaryKey":
|
|
90
|
+
definitions.push(`primary key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`)
|
|
91
|
+
break
|
|
92
|
+
case "unique":
|
|
93
|
+
definitions.push(`unique (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`)
|
|
94
|
+
break
|
|
95
|
+
case "foreignKey": {
|
|
96
|
+
const reference = option.references()
|
|
97
|
+
definitions.push(
|
|
98
|
+
`foreign key (${option.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")}) references ${dialect.renderTableReference(reference.tableName, reference.tableName, reference.schemaName)} (${reference.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`
|
|
99
|
+
)
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
case "check":
|
|
103
|
+
definitions.push(
|
|
104
|
+
`constraint ${dialect.quoteIdentifier(option.name)} check (${renderCheckPredicate(option.predicate, state, dialect)})`
|
|
105
|
+
)
|
|
106
|
+
break
|
|
107
|
+
case "index":
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return `create table${ifNotExists ? " if not exists" : ""} ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${definitions.join(", ")})`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const renderCreateIndexSql = (
|
|
115
|
+
targetSource: QueryAst.FromClause,
|
|
116
|
+
ddl: Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
|
|
117
|
+
state: RenderState,
|
|
118
|
+
dialect: SqlDialect
|
|
119
|
+
): string => {
|
|
120
|
+
const maybeIfNotExists = dialect.name === "postgres" && ddl.ifNotExists ? " if not exists" : ""
|
|
121
|
+
return `create${ddl.unique ? " unique" : ""} index${maybeIfNotExists} ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)} (${ddl.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const renderDropIndexSql = (
|
|
125
|
+
targetSource: QueryAst.FromClause,
|
|
126
|
+
ddl: Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
|
|
127
|
+
state: RenderState,
|
|
128
|
+
dialect: SqlDialect
|
|
129
|
+
): string =>
|
|
130
|
+
dialect.name === "postgres"
|
|
131
|
+
? `drop index${ddl.ifExists ? " if exists" : ""} ${dialect.quoteIdentifier(ddl.name)}`
|
|
132
|
+
: `drop index ${dialect.quoteIdentifier(ddl.name)} on ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
|
|
133
|
+
|
|
134
|
+
const isExpression = (value: unknown): value is Expression.Any =>
|
|
135
|
+
value !== null && typeof value === "object" && Expression.TypeId in value
|
|
136
|
+
|
|
137
|
+
const isJsonDbType = (dbType: Expression.DbType.Any): boolean =>
|
|
138
|
+
dbType.kind === "jsonb" || dbType.kind === "json" || ("variant" in dbType && dbType.variant === "json")
|
|
139
|
+
|
|
140
|
+
const isJsonExpression = (value: unknown): value is Expression.Any =>
|
|
141
|
+
isExpression(value) && isJsonDbType(value[Expression.TypeId].dbType)
|
|
142
|
+
|
|
143
|
+
const unsupportedJsonFeature = (
|
|
144
|
+
dialect: SqlDialect,
|
|
145
|
+
feature: string
|
|
146
|
+
): never => {
|
|
147
|
+
const error = new Error(`Unsupported JSON feature for ${dialect.name}: ${feature}`) as Error & {
|
|
148
|
+
readonly tag: string
|
|
149
|
+
readonly dialect: string
|
|
150
|
+
readonly feature: string
|
|
151
|
+
}
|
|
152
|
+
Object.assign(error, {
|
|
153
|
+
tag: `@${dialect.name}/unsupported/json-feature`,
|
|
154
|
+
dialect: dialect.name,
|
|
155
|
+
feature
|
|
156
|
+
})
|
|
157
|
+
throw error
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const extractJsonBase = (node: Record<string, unknown>): unknown =>
|
|
161
|
+
node.value ?? node.base ?? node.input ?? node.left ?? node.target
|
|
162
|
+
|
|
163
|
+
const isJsonPathValue = (value: unknown): value is JsonPath.Path<any> =>
|
|
164
|
+
value !== null && typeof value === "object" && JsonPath.TypeId in value
|
|
165
|
+
|
|
166
|
+
const extractJsonPathSegments = (node: Record<string, unknown>): ReadonlyArray<JsonPath.AnySegment> => {
|
|
167
|
+
const path = node.path ?? node.segments ?? node.keys
|
|
168
|
+
if (isJsonPathValue(path)) {
|
|
169
|
+
return path.segments
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(path)) {
|
|
172
|
+
return path as readonly JsonPath.AnySegment[]
|
|
173
|
+
}
|
|
174
|
+
if ("key" in node) {
|
|
175
|
+
return [JsonPath.key(String(node.key))]
|
|
176
|
+
}
|
|
177
|
+
if ("segment" in node) {
|
|
178
|
+
const segment = node.segment
|
|
179
|
+
if (typeof segment === "string") {
|
|
180
|
+
return [JsonPath.key(segment)]
|
|
181
|
+
}
|
|
182
|
+
if (typeof segment === "number") {
|
|
183
|
+
return [JsonPath.index(segment)]
|
|
184
|
+
}
|
|
185
|
+
if (segment !== null && typeof segment === "object" && JsonPath.SegmentTypeId in segment) {
|
|
186
|
+
return [segment as JsonPath.AnySegment]
|
|
187
|
+
}
|
|
188
|
+
return []
|
|
189
|
+
}
|
|
190
|
+
if ("right" in node && isJsonPathValue(node.right)) {
|
|
191
|
+
return node.right.segments
|
|
192
|
+
}
|
|
193
|
+
return []
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const extractJsonValue = (node: Record<string, unknown>): unknown =>
|
|
197
|
+
node.newValue ?? node.insert ?? node.right
|
|
198
|
+
|
|
199
|
+
const renderJsonPathSegment = (segment: JsonPath.AnySegment | string | number): string => {
|
|
200
|
+
if (typeof segment === "string") {
|
|
201
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)
|
|
202
|
+
? `.${segment}`
|
|
203
|
+
: `."${segment.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
204
|
+
}
|
|
205
|
+
if (typeof segment === "number") {
|
|
206
|
+
return `[${segment}]`
|
|
207
|
+
}
|
|
208
|
+
switch (segment.kind) {
|
|
209
|
+
case "key":
|
|
210
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment.key)
|
|
211
|
+
? `.${segment.key}`
|
|
212
|
+
: `."${segment.key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
213
|
+
case "index":
|
|
214
|
+
return `[${segment.index}]`
|
|
215
|
+
case "wildcard":
|
|
216
|
+
return "[*]"
|
|
217
|
+
case "slice":
|
|
218
|
+
return `[${segment.start ?? 0} to ${segment.end ?? "last"}]`
|
|
219
|
+
case "descend":
|
|
220
|
+
return ".**"
|
|
221
|
+
default:
|
|
222
|
+
throw new Error("Unsupported JSON path segment")
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const renderJsonPathStringLiteral = (segments: ReadonlyArray<JsonPath.AnySegment | string | number>): string => {
|
|
227
|
+
let path = "$"
|
|
228
|
+
for (const segment of segments) {
|
|
229
|
+
path += renderJsonPathSegment(segment)
|
|
230
|
+
}
|
|
231
|
+
return path
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const renderMySqlJsonPath = (
|
|
235
|
+
segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
|
|
236
|
+
state: RenderState,
|
|
237
|
+
dialect: SqlDialect
|
|
238
|
+
): string => dialect.renderLiteral(renderJsonPathStringLiteral(segments), state)
|
|
239
|
+
|
|
240
|
+
const renderPostgresJsonPathArray = (
|
|
241
|
+
segments: ReadonlyArray<JsonPath.AnySegment | string | number>,
|
|
242
|
+
state: RenderState,
|
|
243
|
+
dialect: SqlDialect
|
|
244
|
+
): string => `array[${segments.map((segment) => {
|
|
245
|
+
if (typeof segment === "string") {
|
|
246
|
+
return dialect.renderLiteral(segment, state)
|
|
247
|
+
}
|
|
248
|
+
if (typeof segment === "number") {
|
|
249
|
+
return dialect.renderLiteral(String(segment), state)
|
|
250
|
+
}
|
|
251
|
+
switch (segment.kind) {
|
|
252
|
+
case "key":
|
|
253
|
+
return dialect.renderLiteral(segment.key, state)
|
|
254
|
+
case "index":
|
|
255
|
+
return dialect.renderLiteral(String(segment.index), state)
|
|
256
|
+
default:
|
|
257
|
+
throw new Error("Postgres JSON traversal requires exact key/index segments")
|
|
258
|
+
}
|
|
259
|
+
}).join(", ")}]`
|
|
260
|
+
|
|
261
|
+
const renderPostgresJsonAccessStep = (
|
|
262
|
+
segment: JsonPath.AnySegment,
|
|
263
|
+
textMode: boolean,
|
|
264
|
+
state: RenderState,
|
|
265
|
+
dialect: SqlDialect
|
|
266
|
+
): string => {
|
|
267
|
+
switch (segment.kind) {
|
|
268
|
+
case "key":
|
|
269
|
+
return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(segment.key, state)}`
|
|
270
|
+
case "index":
|
|
271
|
+
return `${textMode ? "->>" : "->"} ${dialect.renderLiteral(String(segment.index), state)}`
|
|
272
|
+
default:
|
|
273
|
+
throw new Error("Postgres exact JSON access requires key/index segments")
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const renderPostgresJsonValue = (
|
|
278
|
+
value: unknown,
|
|
279
|
+
state: RenderState,
|
|
280
|
+
dialect: SqlDialect
|
|
281
|
+
): string => {
|
|
282
|
+
if (!isExpression(value)) {
|
|
283
|
+
throw new Error("Expected a JSON expression")
|
|
284
|
+
}
|
|
285
|
+
const rendered = renderExpression(value, state, dialect)
|
|
286
|
+
return value[Expression.TypeId].dbType.kind === "jsonb"
|
|
287
|
+
? rendered
|
|
288
|
+
: `cast(${rendered} as jsonb)`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const renderJsonOpaquePath = (
|
|
292
|
+
value: unknown,
|
|
293
|
+
state: RenderState,
|
|
294
|
+
dialect: SqlDialect
|
|
295
|
+
): string => {
|
|
296
|
+
if (isJsonPathValue(value)) {
|
|
297
|
+
return dialect.renderLiteral(renderJsonPathStringLiteral(value.segments), state)
|
|
298
|
+
}
|
|
299
|
+
if (typeof value === "string") {
|
|
300
|
+
return dialect.renderLiteral(value, state)
|
|
301
|
+
}
|
|
302
|
+
if (isExpression(value)) {
|
|
303
|
+
return renderExpression(value, state, dialect)
|
|
304
|
+
}
|
|
305
|
+
throw new Error("Unsupported SQL/JSON path input")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const renderJsonExpression = (
|
|
309
|
+
ast: Record<string, unknown>,
|
|
310
|
+
state: RenderState,
|
|
311
|
+
dialect: SqlDialect
|
|
312
|
+
): string | undefined => {
|
|
313
|
+
const kind = typeof ast.kind === "string" ? ast.kind : undefined
|
|
314
|
+
if (!kind) {
|
|
315
|
+
return undefined
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const base = extractJsonBase(ast)
|
|
319
|
+
const segments = extractJsonPathSegments(ast)
|
|
320
|
+
const exact = segments.every((segment) => segment.kind === "key" || segment.kind === "index")
|
|
321
|
+
|
|
322
|
+
switch (kind) {
|
|
323
|
+
case "jsonGet":
|
|
324
|
+
case "jsonPath":
|
|
325
|
+
case "jsonAccess":
|
|
326
|
+
case "jsonTraverse":
|
|
327
|
+
case "jsonGetText":
|
|
328
|
+
case "jsonPathText":
|
|
329
|
+
case "jsonAccessText":
|
|
330
|
+
case "jsonTraverseText": {
|
|
331
|
+
if (!isExpression(base) || segments.length === 0) {
|
|
332
|
+
return undefined
|
|
333
|
+
}
|
|
334
|
+
const baseSql = renderExpression(base, state, dialect)
|
|
335
|
+
const textMode = kind.endsWith("Text") || ast.text === true || ast.asText === true
|
|
336
|
+
if (dialect.name === "postgres") {
|
|
337
|
+
if (exact) {
|
|
338
|
+
return segments.length === 1
|
|
339
|
+
? `(${baseSql} ${renderPostgresJsonAccessStep(segments[0]!, textMode, state, dialect)})`
|
|
340
|
+
: `(${baseSql} ${textMode ? "#>>" : "#>"} ${renderPostgresJsonPathArray(segments, state, dialect)})`
|
|
341
|
+
}
|
|
342
|
+
const jsonPathLiteral = dialect.renderLiteral(renderJsonPathStringLiteral(segments), state)
|
|
343
|
+
const queried = `jsonb_path_query_first(${renderPostgresJsonValue(base, state, dialect)}, ${jsonPathLiteral})`
|
|
344
|
+
return textMode ? `cast(${queried} as text)` : queried
|
|
345
|
+
}
|
|
346
|
+
if (dialect.name === "mysql") {
|
|
347
|
+
const extracted = `json_extract(${baseSql}, ${renderMySqlJsonPath(segments, state, dialect)})`
|
|
348
|
+
return textMode ? `json_unquote(${extracted})` : extracted
|
|
349
|
+
}
|
|
350
|
+
return undefined
|
|
351
|
+
}
|
|
352
|
+
case "jsonHasKey":
|
|
353
|
+
case "jsonKeyExists":
|
|
354
|
+
case "jsonHasAnyKeys":
|
|
355
|
+
case "jsonHasAllKeys": {
|
|
356
|
+
if (!isExpression(base)) {
|
|
357
|
+
return undefined
|
|
358
|
+
}
|
|
359
|
+
const baseSql = dialect.name === "postgres"
|
|
360
|
+
? renderPostgresJsonValue(base, state, dialect)
|
|
361
|
+
: renderExpression(base, state, dialect)
|
|
362
|
+
const keys = segments
|
|
363
|
+
if (keys.length === 0) {
|
|
364
|
+
return undefined
|
|
365
|
+
}
|
|
366
|
+
if (dialect.name === "postgres") {
|
|
367
|
+
if (kind === "jsonHasAnyKeys") {
|
|
368
|
+
return `(${baseSql} ?| ${renderPostgresJsonPathArray(keys, state, dialect)})`
|
|
369
|
+
}
|
|
370
|
+
if (kind === "jsonHasAllKeys") {
|
|
371
|
+
return `(${baseSql} ?& ${renderPostgresJsonPathArray(keys, state, dialect)})`
|
|
372
|
+
}
|
|
373
|
+
return `(${baseSql} ? ${dialect.renderLiteral(keys[0]!, state)})`
|
|
374
|
+
}
|
|
375
|
+
if (dialect.name === "mysql") {
|
|
376
|
+
const mode = kind === "jsonHasAllKeys" ? "all" : "one"
|
|
377
|
+
const paths = keys.map((segment) => renderMySqlJsonPath([segment], state, dialect)).join(", ")
|
|
378
|
+
return `json_contains_path(${baseSql}, ${dialect.renderLiteral(mode, state)}, ${paths})`
|
|
379
|
+
}
|
|
380
|
+
return undefined
|
|
381
|
+
}
|
|
382
|
+
case "jsonConcat":
|
|
383
|
+
case "jsonMerge": {
|
|
384
|
+
if (!isExpression(ast.left) || !isExpression(ast.right)) {
|
|
385
|
+
return undefined
|
|
386
|
+
}
|
|
387
|
+
if (dialect.name === "postgres") {
|
|
388
|
+
return `(${renderPostgresJsonValue(ast.left, state, dialect)} || ${renderPostgresJsonValue(ast.right, state, dialect)})`
|
|
389
|
+
}
|
|
390
|
+
if (dialect.name === "mysql") {
|
|
391
|
+
return `json_merge_preserve(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
|
|
392
|
+
}
|
|
393
|
+
return undefined
|
|
394
|
+
}
|
|
395
|
+
case "jsonBuildObject": {
|
|
396
|
+
const entries = Array.isArray((ast as { readonly entries?: readonly { readonly key: string; readonly value: Expression.Any }[] }).entries)
|
|
397
|
+
? (ast as { readonly entries: readonly { readonly key: string; readonly value: Expression.Any }[] }).entries
|
|
398
|
+
: []
|
|
399
|
+
const renderedEntries = entries.flatMap((entry) => [
|
|
400
|
+
dialect.renderLiteral(entry.key, state),
|
|
401
|
+
renderExpression(entry.value, state, dialect)
|
|
402
|
+
])
|
|
403
|
+
if (dialect.name === "postgres") {
|
|
404
|
+
return `jsonb_build_object(${renderedEntries.join(", ")})`
|
|
405
|
+
}
|
|
406
|
+
if (dialect.name === "mysql") {
|
|
407
|
+
return `json_object(${renderedEntries.join(", ")})`
|
|
408
|
+
}
|
|
409
|
+
return undefined
|
|
410
|
+
}
|
|
411
|
+
case "jsonBuildArray": {
|
|
412
|
+
const values = Array.isArray((ast as { readonly values?: readonly Expression.Any[] }).values)
|
|
413
|
+
? (ast as { readonly values: readonly Expression.Any[] }).values
|
|
414
|
+
: []
|
|
415
|
+
const renderedValues = values.map((value) => renderExpression(value, state, dialect)).join(", ")
|
|
416
|
+
if (dialect.name === "postgres") {
|
|
417
|
+
return `jsonb_build_array(${renderedValues})`
|
|
418
|
+
}
|
|
419
|
+
if (dialect.name === "mysql") {
|
|
420
|
+
return `json_array(${renderedValues})`
|
|
421
|
+
}
|
|
422
|
+
return undefined
|
|
423
|
+
}
|
|
424
|
+
case "jsonToJson":
|
|
425
|
+
if (!isExpression(base)) {
|
|
426
|
+
return undefined
|
|
427
|
+
}
|
|
428
|
+
if (dialect.name === "postgres") {
|
|
429
|
+
return `to_json(${renderExpression(base, state, dialect)})`
|
|
430
|
+
}
|
|
431
|
+
if (dialect.name === "mysql") {
|
|
432
|
+
return `cast(${renderExpression(base, state, dialect)} as json)`
|
|
433
|
+
}
|
|
434
|
+
return undefined
|
|
435
|
+
case "jsonToJsonb":
|
|
436
|
+
if (!isExpression(base)) {
|
|
437
|
+
return undefined
|
|
438
|
+
}
|
|
439
|
+
if (dialect.name === "postgres") {
|
|
440
|
+
return `to_jsonb(${renderExpression(base, state, dialect)})`
|
|
441
|
+
}
|
|
442
|
+
if (dialect.name === "mysql") {
|
|
443
|
+
return `cast(${renderExpression(base, state, dialect)} as json)`
|
|
444
|
+
}
|
|
445
|
+
return undefined
|
|
446
|
+
case "jsonTypeOf":
|
|
447
|
+
if (!isExpression(base)) {
|
|
448
|
+
return undefined
|
|
449
|
+
}
|
|
450
|
+
if (dialect.name === "postgres") {
|
|
451
|
+
return `jsonb_typeof(${renderPostgresJsonValue(base, state, dialect)})`
|
|
452
|
+
}
|
|
453
|
+
if (dialect.name === "mysql") {
|
|
454
|
+
return `json_type(${renderExpression(base, state, dialect)})`
|
|
455
|
+
}
|
|
456
|
+
return undefined
|
|
457
|
+
case "jsonLength":
|
|
458
|
+
if (!isExpression(base)) {
|
|
459
|
+
return undefined
|
|
460
|
+
}
|
|
461
|
+
if (dialect.name === "postgres") {
|
|
462
|
+
const jsonb = renderPostgresJsonValue(base, state, dialect)
|
|
463
|
+
return `(case when jsonb_typeof(${jsonb}) = 'array' then jsonb_array_length(${jsonb}) when jsonb_typeof(${jsonb}) = 'object' then jsonb_object_length(${jsonb}) else null end)`
|
|
464
|
+
}
|
|
465
|
+
if (dialect.name === "mysql") {
|
|
466
|
+
return `json_length(${renderExpression(base, state, dialect)})`
|
|
467
|
+
}
|
|
468
|
+
return undefined
|
|
469
|
+
case "jsonKeys":
|
|
470
|
+
if (!isExpression(base)) {
|
|
471
|
+
return undefined
|
|
472
|
+
}
|
|
473
|
+
if (dialect.name === "postgres") {
|
|
474
|
+
const jsonb = renderPostgresJsonValue(base, state, dialect)
|
|
475
|
+
return `(case when jsonb_typeof(${jsonb}) = 'object' then array(select jsonb_object_keys(${jsonb})) else null end)`
|
|
476
|
+
}
|
|
477
|
+
if (dialect.name === "mysql") {
|
|
478
|
+
return `json_keys(${renderExpression(base, state, dialect)})`
|
|
479
|
+
}
|
|
480
|
+
return undefined
|
|
481
|
+
case "jsonStripNulls":
|
|
482
|
+
if (!isExpression(base)) {
|
|
483
|
+
return undefined
|
|
484
|
+
}
|
|
485
|
+
if (dialect.name === "postgres") {
|
|
486
|
+
return `jsonb_strip_nulls(${renderPostgresJsonValue(base, state, dialect)})`
|
|
487
|
+
}
|
|
488
|
+
unsupportedJsonFeature(dialect, "jsonStripNulls")
|
|
489
|
+
return undefined
|
|
490
|
+
case "jsonDelete":
|
|
491
|
+
case "jsonDeletePath":
|
|
492
|
+
case "jsonRemove": {
|
|
493
|
+
if (!isExpression(base) || segments.length === 0) {
|
|
494
|
+
return undefined
|
|
495
|
+
}
|
|
496
|
+
if (dialect.name === "postgres") {
|
|
497
|
+
const baseSql = renderPostgresJsonValue(base, state, dialect)
|
|
498
|
+
if (segments.length === 1 && (segments[0]!.kind === "key" || segments[0]!.kind === "index")) {
|
|
499
|
+
const segment = segments[0]!
|
|
500
|
+
return `(${baseSql} - ${segment.kind === "key"
|
|
501
|
+
? dialect.renderLiteral(segment.key, state)
|
|
502
|
+
: dialect.renderLiteral(String(segment.index), state)})`
|
|
503
|
+
}
|
|
504
|
+
return `(${baseSql} #- ${renderPostgresJsonPathArray(segments, state, dialect)})`
|
|
505
|
+
}
|
|
506
|
+
if (dialect.name === "mysql") {
|
|
507
|
+
return `json_remove(${renderExpression(base, state, dialect)}, ${segments.map((segment) =>
|
|
508
|
+
renderMySqlJsonPath([segment], state, dialect)
|
|
509
|
+
).join(", ")})`
|
|
510
|
+
}
|
|
511
|
+
return undefined
|
|
512
|
+
}
|
|
513
|
+
case "jsonSet":
|
|
514
|
+
case "jsonInsert": {
|
|
515
|
+
if (!isExpression(base) || segments.length === 0) {
|
|
516
|
+
return undefined
|
|
517
|
+
}
|
|
518
|
+
const nextValue = extractJsonValue(ast)
|
|
519
|
+
if (!isExpression(nextValue)) {
|
|
520
|
+
return undefined
|
|
521
|
+
}
|
|
522
|
+
const createMissing = ast.createMissing === true
|
|
523
|
+
const insertAfter = ast.insertAfter === true
|
|
524
|
+
if (dialect.name === "postgres") {
|
|
525
|
+
const functionName = kind === "jsonInsert" ? "jsonb_insert" : "jsonb_set"
|
|
526
|
+
const extra =
|
|
527
|
+
kind === "jsonInsert"
|
|
528
|
+
? `, ${insertAfter ? "true" : "false"}`
|
|
529
|
+
: `, ${createMissing ? "true" : "false"}`
|
|
530
|
+
return `${functionName}(${renderPostgresJsonValue(base, state, dialect)}, ${renderPostgresJsonPathArray(segments, state, dialect)}, ${renderPostgresJsonValue(nextValue, state, dialect)}${extra})`
|
|
531
|
+
}
|
|
532
|
+
if (dialect.name === "mysql") {
|
|
533
|
+
const functionName = kind === "jsonInsert" ? "json_insert" : "json_set"
|
|
534
|
+
return `${functionName}(${renderExpression(base, state, dialect)}, ${renderMySqlJsonPath(segments, state, dialect)}, ${renderExpression(nextValue, state, dialect)})`
|
|
535
|
+
}
|
|
536
|
+
return undefined
|
|
537
|
+
}
|
|
538
|
+
case "jsonPathExists": {
|
|
539
|
+
if (!isExpression(base)) {
|
|
540
|
+
return undefined
|
|
541
|
+
}
|
|
542
|
+
const path = ast.path ?? ast.query ?? ast.right
|
|
543
|
+
if (path === undefined) {
|
|
544
|
+
return undefined
|
|
545
|
+
}
|
|
546
|
+
if (dialect.name === "postgres") {
|
|
547
|
+
return `(${renderPostgresJsonValue(base, state, dialect)} @? ${renderJsonOpaquePath(path, state, dialect)})`
|
|
548
|
+
}
|
|
549
|
+
if (dialect.name === "mysql") {
|
|
550
|
+
return `json_contains_path(${renderExpression(base, state, dialect)}, ${dialect.renderLiteral("one", state)}, ${renderJsonOpaquePath(path, state, dialect)})`
|
|
551
|
+
}
|
|
552
|
+
return undefined
|
|
553
|
+
}
|
|
554
|
+
case "jsonPathMatch": {
|
|
555
|
+
if (!isExpression(base)) {
|
|
556
|
+
return undefined
|
|
557
|
+
}
|
|
558
|
+
const path = ast.path ?? ast.query ?? ast.right
|
|
559
|
+
if (path === undefined) {
|
|
560
|
+
return undefined
|
|
561
|
+
}
|
|
562
|
+
if (dialect.name === "postgres") {
|
|
563
|
+
return `(${renderPostgresJsonValue(base, state, dialect)} @@ ${renderJsonOpaquePath(path, state, dialect)})`
|
|
564
|
+
}
|
|
565
|
+
unsupportedJsonFeature(dialect, "jsonPathMatch")
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return undefined
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export interface RenderedQueryAst {
|
|
573
|
+
readonly sql: string
|
|
574
|
+
readonly projections: readonly Projection[]
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const selectionProjections = (selection: Record<string, unknown>): readonly Projection[] =>
|
|
578
|
+
flattenSelection(selection).map(({ path, alias }) => ({
|
|
579
|
+
path,
|
|
580
|
+
alias
|
|
581
|
+
}))
|
|
582
|
+
|
|
583
|
+
const renderMutationAssignment = (
|
|
584
|
+
entry: QueryAst.AssignmentClause,
|
|
585
|
+
state: RenderState,
|
|
586
|
+
dialect: SqlDialect
|
|
587
|
+
): string => {
|
|
588
|
+
const column = entry.tableName && dialect.name === "mysql"
|
|
589
|
+
? `${dialect.quoteIdentifier(entry.tableName)}.${dialect.quoteIdentifier(entry.columnName)}`
|
|
590
|
+
: dialect.quoteIdentifier(entry.columnName)
|
|
591
|
+
return `${column} = ${renderExpression(entry.value, state, dialect)}`
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const renderJoinSourcesForMutation = (
|
|
595
|
+
joins: readonly QueryAst.JoinClause[],
|
|
596
|
+
state: RenderState,
|
|
597
|
+
dialect: SqlDialect
|
|
598
|
+
): string => joins.map((join) =>
|
|
599
|
+
renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
|
|
600
|
+
).join(", ")
|
|
601
|
+
|
|
602
|
+
const renderFromSources = (
|
|
603
|
+
sources: readonly QueryAst.FromClause[],
|
|
604
|
+
state: RenderState,
|
|
605
|
+
dialect: SqlDialect
|
|
606
|
+
): string => sources.map((source) =>
|
|
607
|
+
renderSourceReference(source.source, source.tableName, source.baseTableName, state, dialect)
|
|
608
|
+
).join(", ")
|
|
609
|
+
|
|
610
|
+
const renderJoinPredicatesForMutation = (
|
|
611
|
+
joins: readonly QueryAst.JoinClause[],
|
|
612
|
+
state: RenderState,
|
|
613
|
+
dialect: SqlDialect
|
|
614
|
+
): readonly string[] =>
|
|
615
|
+
joins.flatMap((join) =>
|
|
616
|
+
join.kind === "cross" || !join.on
|
|
617
|
+
? []
|
|
618
|
+
: [renderExpression(join.on, state, dialect)]
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
const renderDeleteTargets = (
|
|
622
|
+
targets: readonly QueryAst.FromClause[],
|
|
623
|
+
dialect: SqlDialect
|
|
624
|
+
): string => targets.map((target) => dialect.quoteIdentifier(target.tableName)).join(", ")
|
|
625
|
+
|
|
626
|
+
const renderMysqlMutationLock = (
|
|
627
|
+
lock: QueryAst.LockClause | undefined,
|
|
628
|
+
statement: "update" | "delete"
|
|
629
|
+
): string => {
|
|
630
|
+
if (!lock) {
|
|
631
|
+
return ""
|
|
632
|
+
}
|
|
633
|
+
switch (lock.mode) {
|
|
634
|
+
case "lowPriority":
|
|
635
|
+
return " low_priority"
|
|
636
|
+
case "ignore":
|
|
637
|
+
return " ignore"
|
|
638
|
+
case "quick":
|
|
639
|
+
return statement === "delete" ? " quick" : ""
|
|
640
|
+
default:
|
|
641
|
+
return ""
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const renderTransactionClause = (
|
|
646
|
+
clause: QueryAst.TransactionClause,
|
|
647
|
+
dialect: SqlDialect
|
|
648
|
+
): string => {
|
|
649
|
+
switch (clause.kind) {
|
|
650
|
+
case "transaction": {
|
|
651
|
+
const modes: string[] = []
|
|
652
|
+
if (clause.isolationLevel) {
|
|
653
|
+
modes.push(`isolation level ${clause.isolationLevel}`)
|
|
654
|
+
}
|
|
655
|
+
if (clause.readOnly === true) {
|
|
656
|
+
modes.push("read only")
|
|
657
|
+
}
|
|
658
|
+
return modes.length > 0
|
|
659
|
+
? `start transaction ${modes.join(", ")}`
|
|
660
|
+
: "start transaction"
|
|
661
|
+
}
|
|
662
|
+
case "commit":
|
|
663
|
+
return "commit"
|
|
664
|
+
case "rollback":
|
|
665
|
+
return "rollback"
|
|
666
|
+
case "savepoint":
|
|
667
|
+
return `savepoint ${dialect.quoteIdentifier(clause.name)}`
|
|
668
|
+
case "rollbackTo":
|
|
669
|
+
return `rollback to savepoint ${dialect.quoteIdentifier(clause.name)}`
|
|
670
|
+
case "releaseSavepoint":
|
|
671
|
+
return `release savepoint ${dialect.quoteIdentifier(clause.name)}`
|
|
672
|
+
}
|
|
673
|
+
return ""
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const renderSelectionList = (
|
|
677
|
+
selection: Record<string, unknown>,
|
|
678
|
+
state: RenderState,
|
|
679
|
+
dialect: SqlDialect,
|
|
680
|
+
validateAggregation: boolean
|
|
681
|
+
): RenderedQueryAst => {
|
|
682
|
+
if (validateAggregation) {
|
|
683
|
+
validateAggregationSelection(selection as SelectionValue, [])
|
|
684
|
+
}
|
|
685
|
+
const flattened = flattenSelection(selection)
|
|
686
|
+
const projections = selectionProjections(selection)
|
|
687
|
+
const sql = flattened.map(({ expression, alias }) =>
|
|
688
|
+
`${renderExpression(expression, state, dialect)} as ${dialect.quoteIdentifier(alias)}`).join(", ")
|
|
689
|
+
return {
|
|
690
|
+
sql,
|
|
691
|
+
projections
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export const renderQueryAst = (
|
|
696
|
+
ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
697
|
+
state: RenderState,
|
|
698
|
+
dialect: SqlDialect
|
|
699
|
+
): RenderedQueryAst => {
|
|
700
|
+
let sql = ""
|
|
701
|
+
let projections: readonly Projection[] = []
|
|
702
|
+
|
|
703
|
+
switch (ast.kind) {
|
|
704
|
+
case "select": {
|
|
705
|
+
validateAggregationSelection(ast.select as SelectionValue, ast.groupBy)
|
|
706
|
+
const rendered = renderSelectionList(ast.select as Record<string, unknown>, state, dialect, false)
|
|
707
|
+
projections = rendered.projections
|
|
708
|
+
const clauses = [
|
|
709
|
+
ast.distinctOn && ast.distinctOn.length > 0
|
|
710
|
+
? `select distinct on (${ast.distinctOn.map((value) => renderExpression(value, state, dialect)).join(", ")}) ${rendered.sql}`
|
|
711
|
+
: `select${ast.distinct ? " distinct" : ""} ${rendered.sql}`
|
|
712
|
+
]
|
|
713
|
+
if (ast.from) {
|
|
714
|
+
clauses.push(`from ${renderSourceReference(ast.from.source, ast.from.tableName, ast.from.baseTableName, state, dialect)}`)
|
|
715
|
+
}
|
|
716
|
+
for (const join of ast.joins) {
|
|
717
|
+
const source = renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)
|
|
718
|
+
clauses.push(
|
|
719
|
+
join.kind === "cross"
|
|
720
|
+
? `cross join ${source}`
|
|
721
|
+
: `${join.kind} join ${source} on ${renderExpression(join.on!, state, dialect)}`
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
if (ast.where.length > 0) {
|
|
725
|
+
clauses.push(`where ${ast.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect)).join(" and ")}`)
|
|
726
|
+
}
|
|
727
|
+
if (ast.groupBy.length > 0) {
|
|
728
|
+
clauses.push(`group by ${ast.groupBy.map((value: QueryAst.Ast["groupBy"][number]) => renderExpression(value, state, dialect)).join(", ")}`)
|
|
729
|
+
}
|
|
730
|
+
if (ast.having.length > 0) {
|
|
731
|
+
clauses.push(`having ${ast.having.map((entry: QueryAst.HavingClause) => renderExpression(entry.predicate, state, dialect)).join(" and ")}`)
|
|
732
|
+
}
|
|
733
|
+
if (ast.orderBy.length > 0) {
|
|
734
|
+
clauses.push(`order by ${ast.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`)
|
|
735
|
+
}
|
|
736
|
+
if (ast.limit) {
|
|
737
|
+
clauses.push(`limit ${renderExpression(ast.limit, state, dialect)}`)
|
|
738
|
+
}
|
|
739
|
+
if (ast.offset) {
|
|
740
|
+
clauses.push(`offset ${renderExpression(ast.offset, state, dialect)}`)
|
|
741
|
+
}
|
|
742
|
+
if (ast.lock) {
|
|
743
|
+
clauses.push(
|
|
744
|
+
`${ast.lock.mode === "update" ? "for update" : "for share"}${ast.lock.nowait ? " nowait" : ""}${ast.lock.skipLocked ? " skip locked" : ""}`
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
sql = clauses.join(" ")
|
|
748
|
+
break
|
|
749
|
+
}
|
|
750
|
+
case "set": {
|
|
751
|
+
const setAst = ast as QueryAst.Ast<Record<string, unknown>, any, "set">
|
|
752
|
+
const base = renderQueryAst(
|
|
753
|
+
Query.getAst(setAst.setBase as Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>) as QueryAst.Ast<
|
|
754
|
+
Record<string, unknown>,
|
|
755
|
+
any,
|
|
756
|
+
QueryAst.QueryStatement
|
|
757
|
+
>,
|
|
758
|
+
state,
|
|
759
|
+
dialect
|
|
760
|
+
)
|
|
761
|
+
projections = selectionProjections(setAst.select as Record<string, unknown>)
|
|
762
|
+
sql = [
|
|
763
|
+
`(${base.sql})`,
|
|
764
|
+
...(setAst.setOperations ?? []).map((entry) => {
|
|
765
|
+
const rendered = renderQueryAst(
|
|
766
|
+
Query.getAst(entry.query as Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>) as QueryAst.Ast<
|
|
767
|
+
Record<string, unknown>,
|
|
768
|
+
any,
|
|
769
|
+
QueryAst.QueryStatement
|
|
770
|
+
>,
|
|
771
|
+
state,
|
|
772
|
+
dialect
|
|
773
|
+
)
|
|
774
|
+
return `${entry.kind}${entry.all ? " all" : ""} (${rendered.sql})`
|
|
775
|
+
})
|
|
776
|
+
].join(" ")
|
|
777
|
+
break
|
|
778
|
+
}
|
|
779
|
+
case "insert": {
|
|
780
|
+
const insertAst = ast as QueryAst.Ast<Record<string, unknown>, any, "insert">
|
|
781
|
+
const targetSource = insertAst.into!
|
|
782
|
+
const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
|
|
783
|
+
sql = `insert into ${target}`
|
|
784
|
+
if (insertAst.insertSource?.kind === "values") {
|
|
785
|
+
const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
|
|
786
|
+
const rows = insertAst.insertSource.rows.map((row) =>
|
|
787
|
+
`(${row.values.map((entry) => renderExpression(entry.value, state, dialect)).join(", ")})`
|
|
788
|
+
).join(", ")
|
|
789
|
+
sql += ` (${columns}) values ${rows}`
|
|
790
|
+
} else if (insertAst.insertSource?.kind === "query") {
|
|
791
|
+
const columns = insertAst.insertSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
|
|
792
|
+
const renderedQuery = renderQueryAst(
|
|
793
|
+
Query.getAst(insertAst.insertSource.query as Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>) as QueryAst.Ast<
|
|
794
|
+
Record<string, unknown>,
|
|
795
|
+
any,
|
|
796
|
+
QueryAst.QueryStatement
|
|
797
|
+
>,
|
|
798
|
+
state,
|
|
799
|
+
dialect
|
|
800
|
+
)
|
|
801
|
+
sql += ` (${columns}) ${renderedQuery.sql}`
|
|
802
|
+
} else if (insertAst.insertSource?.kind === "unnest") {
|
|
803
|
+
const unnestSource = insertAst.insertSource
|
|
804
|
+
const columns = unnestSource.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")
|
|
805
|
+
if (dialect.name === "postgres") {
|
|
806
|
+
const table = targetSource.source as Table.AnyTable
|
|
807
|
+
const fields = table[Table.TypeId].fields
|
|
808
|
+
const rendered = unnestSource.values.map((entry) =>
|
|
809
|
+
`cast(${dialect.renderLiteral(entry.values, state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
|
|
810
|
+
).join(", ")
|
|
811
|
+
sql += ` (${columns}) select * from unnest(${rendered})`
|
|
812
|
+
} else {
|
|
813
|
+
const rowCount = unnestSource.values[0]?.values.length ?? 0
|
|
814
|
+
const rows = Array.from({ length: rowCount }, (_, index) =>
|
|
815
|
+
`(${unnestSource.values.map((entry) =>
|
|
816
|
+
dialect.renderLiteral(entry.values[index], state)
|
|
817
|
+
).join(", ")})`
|
|
818
|
+
).join(", ")
|
|
819
|
+
sql += ` (${columns}) values ${rows}`
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
const columns = (insertAst.values ?? []).map((entry) => dialect.quoteIdentifier(entry.columnName)).join(", ")
|
|
823
|
+
const values = (insertAst.values ?? []).map((entry) => renderExpression(entry.value, state, dialect)).join(", ")
|
|
824
|
+
if ((insertAst.values ?? []).length > 0) {
|
|
825
|
+
sql += ` (${columns}) values (${values})`
|
|
826
|
+
} else {
|
|
827
|
+
sql += " default values"
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (insertAst.conflict) {
|
|
831
|
+
const updateValues = (insertAst.conflict.values ?? []).map((entry) =>
|
|
832
|
+
`${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
|
|
833
|
+
).join(", ")
|
|
834
|
+
if (dialect.name === "postgres") {
|
|
835
|
+
const targetSql = insertAst.conflict.target?.kind === "constraint"
|
|
836
|
+
? ` on conflict on constraint ${dialect.quoteIdentifier(insertAst.conflict.target.name)}`
|
|
837
|
+
: insertAst.conflict.target?.kind === "columns"
|
|
838
|
+
? ` on conflict (${insertAst.conflict.target.columns.map((column) => dialect.quoteIdentifier(column)).join(", ")})${insertAst.conflict.target.where ? ` where ${renderExpression(insertAst.conflict.target.where, state, dialect)}` : ""}`
|
|
839
|
+
: " on conflict"
|
|
840
|
+
sql += targetSql
|
|
841
|
+
sql += insertAst.conflict.action === "doNothing"
|
|
842
|
+
? " do nothing"
|
|
843
|
+
: ` do update set ${updateValues}${insertAst.conflict.where ? ` where ${renderExpression(insertAst.conflict.where, state, dialect)}` : ""}`
|
|
844
|
+
} else if (insertAst.conflict.action === "doNothing") {
|
|
845
|
+
sql = sql.replace(/^insert/, "insert ignore")
|
|
846
|
+
} else {
|
|
847
|
+
sql += ` on duplicate key update ${updateValues}`
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const returning = renderSelectionList(insertAst.select as Record<string, unknown>, state, dialect, false)
|
|
851
|
+
projections = returning.projections
|
|
852
|
+
if (returning.sql.length > 0) {
|
|
853
|
+
sql += ` returning ${returning.sql}`
|
|
854
|
+
}
|
|
855
|
+
break
|
|
856
|
+
}
|
|
857
|
+
case "update": {
|
|
858
|
+
const updateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "update">
|
|
859
|
+
const targetSource = updateAst.target!
|
|
860
|
+
const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
|
|
861
|
+
const targets = updateAst.targets ?? [targetSource]
|
|
862
|
+
const fromSources = updateAst.fromSources ?? []
|
|
863
|
+
const assignments = updateAst.set!.map((entry) =>
|
|
864
|
+
renderMutationAssignment(entry, state, dialect)).join(", ")
|
|
865
|
+
if (dialect.name === "mysql") {
|
|
866
|
+
const modifiers = renderMysqlMutationLock(updateAst.lock, "update")
|
|
867
|
+
const extraSources = renderFromSources(fromSources, state, dialect)
|
|
868
|
+
const joinSources = updateAst.joins.map((join) =>
|
|
869
|
+
join.kind === "cross"
|
|
870
|
+
? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
|
|
871
|
+
: `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
|
|
872
|
+
).join(" ")
|
|
873
|
+
const targetList = [
|
|
874
|
+
...targets.map((entry) =>
|
|
875
|
+
renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
|
|
876
|
+
),
|
|
877
|
+
...(extraSources.length > 0 ? [extraSources] : [])
|
|
878
|
+
].join(", ")
|
|
879
|
+
sql = `update${modifiers} ${targetList}${joinSources.length > 0 ? ` ${joinSources}` : ""} set ${assignments}`
|
|
880
|
+
} else {
|
|
881
|
+
sql = `update ${target} set ${assignments}`
|
|
882
|
+
const mutationSources = [
|
|
883
|
+
...(fromSources.length > 0 ? [renderFromSources(fromSources, state, dialect)] : []),
|
|
884
|
+
...(updateAst.joins.length > 0 ? [renderJoinSourcesForMutation(updateAst.joins, state, dialect)] : [])
|
|
885
|
+
].filter((part) => part.length > 0)
|
|
886
|
+
if (mutationSources.length > 0) {
|
|
887
|
+
sql += ` from ${mutationSources.join(", ")}`
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const whereParts = [
|
|
891
|
+
...(dialect.name === "postgres" ? renderJoinPredicatesForMutation(updateAst.joins, state, dialect) : []),
|
|
892
|
+
...updateAst.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect))
|
|
893
|
+
]
|
|
894
|
+
if (whereParts.length > 0) {
|
|
895
|
+
sql += ` where ${whereParts.join(" and ")}`
|
|
896
|
+
}
|
|
897
|
+
if (dialect.name === "mysql" && updateAst.orderBy.length > 0) {
|
|
898
|
+
sql += ` order by ${updateAst.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`
|
|
899
|
+
}
|
|
900
|
+
if (dialect.name === "mysql" && updateAst.limit) {
|
|
901
|
+
sql += ` limit ${renderExpression(updateAst.limit, state, dialect)}`
|
|
902
|
+
}
|
|
903
|
+
const returning = renderSelectionList(updateAst.select as Record<string, unknown>, state, dialect, false)
|
|
904
|
+
projections = returning.projections
|
|
905
|
+
if (returning.sql.length > 0) {
|
|
906
|
+
sql += ` returning ${returning.sql}`
|
|
907
|
+
}
|
|
908
|
+
break
|
|
909
|
+
}
|
|
910
|
+
case "delete": {
|
|
911
|
+
const deleteAst = ast as QueryAst.Ast<Record<string, unknown>, any, "delete">
|
|
912
|
+
const targetSource = deleteAst.target!
|
|
913
|
+
const target = renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)
|
|
914
|
+
const targets = deleteAst.targets ?? [targetSource]
|
|
915
|
+
if (dialect.name === "mysql") {
|
|
916
|
+
const modifiers = renderMysqlMutationLock(deleteAst.lock, "delete")
|
|
917
|
+
const hasJoinedSources = deleteAst.joins.length > 0 || targets.length > 1
|
|
918
|
+
const targetList = renderDeleteTargets(targets, dialect)
|
|
919
|
+
const fromSources = targets.map((entry) =>
|
|
920
|
+
renderSourceReference(entry.source, entry.tableName, entry.baseTableName, state, dialect)
|
|
921
|
+
).join(", ")
|
|
922
|
+
const joinSources = deleteAst.joins.map((join) =>
|
|
923
|
+
join.kind === "cross"
|
|
924
|
+
? `cross join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)}`
|
|
925
|
+
: `${join.kind} join ${renderSourceReference(join.source, join.tableName, join.baseTableName, state, dialect)} on ${renderExpression(join.on!, state, dialect)}`
|
|
926
|
+
).join(" ")
|
|
927
|
+
sql = hasJoinedSources
|
|
928
|
+
? `delete${modifiers} ${targetList} from ${fromSources}${joinSources.length > 0 ? ` ${joinSources}` : ""}`
|
|
929
|
+
: `delete${modifiers} from ${fromSources}`
|
|
930
|
+
} else {
|
|
931
|
+
sql = `delete from ${target}`
|
|
932
|
+
if (deleteAst.joins.length > 0) {
|
|
933
|
+
sql += ` using ${renderJoinSourcesForMutation(deleteAst.joins, state, dialect)}`
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const whereParts = [
|
|
937
|
+
...(dialect.name === "postgres" ? renderJoinPredicatesForMutation(deleteAst.joins, state, dialect) : []),
|
|
938
|
+
...deleteAst.where.map((entry: QueryAst.WhereClause) => renderExpression(entry.predicate, state, dialect))
|
|
939
|
+
]
|
|
940
|
+
if (whereParts.length > 0) {
|
|
941
|
+
sql += ` where ${whereParts.join(" and ")}`
|
|
942
|
+
}
|
|
943
|
+
if (dialect.name === "mysql" && deleteAst.orderBy.length > 0) {
|
|
944
|
+
sql += ` order by ${deleteAst.orderBy.map((entry: QueryAst.OrderByClause) => `${renderExpression(entry.value, state, dialect)} ${entry.direction}`).join(", ")}`
|
|
945
|
+
}
|
|
946
|
+
if (dialect.name === "mysql" && deleteAst.limit) {
|
|
947
|
+
sql += ` limit ${renderExpression(deleteAst.limit, state, dialect)}`
|
|
948
|
+
}
|
|
949
|
+
const returning = renderSelectionList(deleteAst.select as Record<string, unknown>, state, dialect, false)
|
|
950
|
+
projections = returning.projections
|
|
951
|
+
if (returning.sql.length > 0) {
|
|
952
|
+
sql += ` returning ${returning.sql}`
|
|
953
|
+
}
|
|
954
|
+
break
|
|
955
|
+
}
|
|
956
|
+
case "truncate": {
|
|
957
|
+
const truncateAst = ast as QueryAst.Ast<Record<string, unknown>, any, "truncate">
|
|
958
|
+
const targetSource = truncateAst.target!
|
|
959
|
+
sql = `truncate table ${renderSourceReference(targetSource.source, targetSource.tableName, targetSource.baseTableName, state, dialect)}`
|
|
960
|
+
if (truncateAst.truncate?.restartIdentity) {
|
|
961
|
+
sql += " restart identity"
|
|
962
|
+
}
|
|
963
|
+
if (truncateAst.truncate?.cascade) {
|
|
964
|
+
sql += " cascade"
|
|
965
|
+
}
|
|
966
|
+
break
|
|
967
|
+
}
|
|
968
|
+
case "merge": {
|
|
969
|
+
if (dialect.name !== "postgres") {
|
|
970
|
+
throw new Error(`Unsupported merge statement for ${dialect.name}`)
|
|
971
|
+
}
|
|
972
|
+
const mergeAst = ast as QueryAst.Ast<Record<string, unknown>, any, "merge">
|
|
973
|
+
const targetSource = mergeAst.target!
|
|
974
|
+
const usingSource = mergeAst.using!
|
|
975
|
+
const merge = mergeAst.merge!
|
|
976
|
+
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)}`
|
|
977
|
+
if (merge.whenMatched) {
|
|
978
|
+
sql += " when matched"
|
|
979
|
+
if (merge.whenMatched.predicate) {
|
|
980
|
+
sql += ` and ${renderExpression(merge.whenMatched.predicate, state, dialect)}`
|
|
981
|
+
}
|
|
982
|
+
if (merge.whenMatched.kind === "delete") {
|
|
983
|
+
sql += " then delete"
|
|
984
|
+
} else {
|
|
985
|
+
sql += ` then update set ${merge.whenMatched.values.map((entry) =>
|
|
986
|
+
`${dialect.quoteIdentifier(entry.columnName)} = ${renderExpression(entry.value, state, dialect)}`
|
|
987
|
+
).join(", ")}`
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (merge.whenNotMatched) {
|
|
991
|
+
sql += " when not matched"
|
|
992
|
+
if (merge.whenNotMatched.predicate) {
|
|
993
|
+
sql += ` and ${renderExpression(merge.whenNotMatched.predicate, state, dialect)}`
|
|
994
|
+
}
|
|
995
|
+
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(", ")})`
|
|
996
|
+
}
|
|
997
|
+
break
|
|
998
|
+
}
|
|
999
|
+
case "transaction":
|
|
1000
|
+
case "commit":
|
|
1001
|
+
case "rollback":
|
|
1002
|
+
case "savepoint":
|
|
1003
|
+
case "rollbackTo":
|
|
1004
|
+
case "releaseSavepoint": {
|
|
1005
|
+
sql = renderTransactionClause(ast.transaction!, dialect)
|
|
1006
|
+
break
|
|
1007
|
+
}
|
|
1008
|
+
case "createTable": {
|
|
1009
|
+
const createTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createTable">
|
|
1010
|
+
sql = renderCreateTableSql(createTableAst.target!, state, dialect, createTableAst.ddl?.kind === "createTable" && createTableAst.ddl.ifNotExists)
|
|
1011
|
+
break
|
|
1012
|
+
}
|
|
1013
|
+
case "dropTable": {
|
|
1014
|
+
const dropTableAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropTable">
|
|
1015
|
+
const ifExists = dropTableAst.ddl?.kind === "dropTable" && dropTableAst.ddl.ifExists
|
|
1016
|
+
sql = `drop table${ifExists ? " if exists" : ""} ${renderSourceReference(dropTableAst.target!.source, dropTableAst.target!.tableName, dropTableAst.target!.baseTableName, state, dialect)}`
|
|
1017
|
+
break
|
|
1018
|
+
}
|
|
1019
|
+
case "createIndex": {
|
|
1020
|
+
const createIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "createIndex">
|
|
1021
|
+
sql = renderCreateIndexSql(
|
|
1022
|
+
createIndexAst.target!,
|
|
1023
|
+
createIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "createIndex" }>,
|
|
1024
|
+
state,
|
|
1025
|
+
dialect
|
|
1026
|
+
)
|
|
1027
|
+
break
|
|
1028
|
+
}
|
|
1029
|
+
case "dropIndex": {
|
|
1030
|
+
const dropIndexAst = ast as QueryAst.Ast<Record<string, unknown>, any, "dropIndex">
|
|
1031
|
+
sql = renderDropIndexSql(
|
|
1032
|
+
dropIndexAst.target!,
|
|
1033
|
+
dropIndexAst.ddl as Extract<QueryAst.DdlClause, { readonly kind: "dropIndex" }>,
|
|
1034
|
+
state,
|
|
1035
|
+
dialect
|
|
1036
|
+
)
|
|
1037
|
+
break
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (state.ctes.length === 0) {
|
|
1042
|
+
return {
|
|
1043
|
+
sql,
|
|
1044
|
+
projections
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
sql: `with${state.ctes.some((entry) => entry.recursive) ? " recursive" : ""} ${state.ctes.map((entry) => `${dialect.quoteIdentifier(entry.name)} as (${entry.sql})`).join(", ")} ${sql}`,
|
|
1049
|
+
projections
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const renderSourceReference = (
|
|
1054
|
+
source: unknown,
|
|
1055
|
+
tableName: string,
|
|
1056
|
+
baseTableName: string,
|
|
1057
|
+
state: RenderState,
|
|
1058
|
+
dialect: SqlDialect
|
|
1059
|
+
): string => {
|
|
1060
|
+
const renderSelectRows = (
|
|
1061
|
+
rows: readonly Record<string, Expression.Any>[],
|
|
1062
|
+
columnNames: readonly string[]
|
|
1063
|
+
): string => {
|
|
1064
|
+
const renderedRows = rows.map((row) =>
|
|
1065
|
+
`select ${columnNames.map((columnName) =>
|
|
1066
|
+
`${renderExpression(row[columnName]!, state, dialect)} as ${dialect.quoteIdentifier(columnName)}`
|
|
1067
|
+
).join(", ")}`
|
|
1068
|
+
)
|
|
1069
|
+
return `(${renderedRows.join(" union all ")}) as ${dialect.quoteIdentifier(tableName)}(${columnNames.map((columnName) => dialect.quoteIdentifier(columnName)).join(", ")})`
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const renderUnnestRows = (
|
|
1073
|
+
arrays: Readonly<Record<string, readonly Expression.Any[]>>,
|
|
1074
|
+
columnNames: readonly string[]
|
|
1075
|
+
): string => {
|
|
1076
|
+
const rowCount = arrays[columnNames[0]!]!.length
|
|
1077
|
+
const rows = Array.from({ length: rowCount }, (_, index) =>
|
|
1078
|
+
Object.fromEntries(columnNames.map((columnName) => [columnName, arrays[columnName]![index]!] as const)) as Record<string, Expression.Any>
|
|
1079
|
+
)
|
|
1080
|
+
return renderSelectRows(rows, columnNames)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "cte") {
|
|
1084
|
+
const cte = source as unknown as {
|
|
1085
|
+
readonly name: string
|
|
1086
|
+
readonly plan: Query.QueryPlan<any, any, any, any, any, any, any, any, any>
|
|
1087
|
+
readonly recursive?: boolean
|
|
1088
|
+
}
|
|
1089
|
+
if (!state.cteNames.has(cte.name)) {
|
|
1090
|
+
state.cteNames.add(cte.name)
|
|
1091
|
+
const rendered = renderQueryAst(Query.getAst(cte.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect)
|
|
1092
|
+
state.ctes.push({
|
|
1093
|
+
name: cte.name,
|
|
1094
|
+
sql: rendered.sql,
|
|
1095
|
+
recursive: cte.recursive
|
|
1096
|
+
})
|
|
1097
|
+
}
|
|
1098
|
+
return dialect.quoteIdentifier(cte.name)
|
|
1099
|
+
}
|
|
1100
|
+
if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "derived") {
|
|
1101
|
+
const derived = source as unknown as {
|
|
1102
|
+
readonly name: string
|
|
1103
|
+
readonly plan: Query.QueryPlan<any, any, any, any, any, any, any, any, any>
|
|
1104
|
+
}
|
|
1105
|
+
if (!state.cteNames.has(derived.name)) {
|
|
1106
|
+
// derived tables are inlined, so no CTE registration is needed
|
|
1107
|
+
}
|
|
1108
|
+
return `(${renderQueryAst(Query.getAst(derived.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect).sql}) as ${dialect.quoteIdentifier(derived.name)}`
|
|
1109
|
+
}
|
|
1110
|
+
if (typeof source === "object" && source !== null && "kind" in source && (source as { readonly kind?: string }).kind === "lateral") {
|
|
1111
|
+
const lateral = source as unknown as {
|
|
1112
|
+
readonly name: string
|
|
1113
|
+
readonly plan: Query.QueryPlan<any, any, any, any, any, any, any, any, any>
|
|
1114
|
+
}
|
|
1115
|
+
return `lateral (${renderQueryAst(Query.getAst(lateral.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>, state, dialect).sql}) as ${dialect.quoteIdentifier(lateral.name)}`
|
|
1116
|
+
}
|
|
1117
|
+
if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "values") {
|
|
1118
|
+
const values = source as unknown as {
|
|
1119
|
+
readonly columns: Record<string, Expression.Any>
|
|
1120
|
+
readonly rows: readonly Record<string, Expression.Any>[]
|
|
1121
|
+
}
|
|
1122
|
+
return renderSelectRows(values.rows, Object.keys(values.columns))
|
|
1123
|
+
}
|
|
1124
|
+
if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "unnest") {
|
|
1125
|
+
const unnest = source as unknown as {
|
|
1126
|
+
readonly columns: Record<string, Expression.Any>
|
|
1127
|
+
readonly arrays: Readonly<Record<string, readonly Expression.Any[]>>
|
|
1128
|
+
}
|
|
1129
|
+
return renderUnnestRows(unnest.arrays, Object.keys(unnest.columns))
|
|
1130
|
+
}
|
|
1131
|
+
if (typeof source === "object" && source !== null && (source as { readonly kind?: string }).kind === "tableFunction") {
|
|
1132
|
+
const tableFunction = source as unknown as {
|
|
1133
|
+
readonly name: string
|
|
1134
|
+
readonly columns: Record<string, Expression.Any>
|
|
1135
|
+
readonly functionName: string
|
|
1136
|
+
readonly args: readonly Expression.Any[]
|
|
1137
|
+
}
|
|
1138
|
+
if (dialect.name !== "postgres") {
|
|
1139
|
+
throw new Error("Unsupported table function source for SQL rendering")
|
|
1140
|
+
}
|
|
1141
|
+
const columnNames = Object.keys(tableFunction.columns)
|
|
1142
|
+
return `${tableFunction.functionName}(${tableFunction.args.map((arg) => renderExpression(arg, state, dialect)).join(", ")}) as ${dialect.quoteIdentifier(tableFunction.name)}(${columnNames.map((columnName) => dialect.quoteIdentifier(columnName)).join(", ")})`
|
|
1143
|
+
}
|
|
1144
|
+
const schemaName = typeof source === "object" && source !== null && Table.TypeId in source
|
|
1145
|
+
? (source as Table.AnyTable)[Table.TypeId].schemaName
|
|
1146
|
+
: undefined
|
|
1147
|
+
return dialect.renderTableReference(tableName, baseTableName, schemaName)
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Renders a scalar expression AST into SQL text.
|
|
1152
|
+
*
|
|
1153
|
+
* This is parameterized by a runtime dialect so the same expression walker can
|
|
1154
|
+
* be reused across dialect-specific renderers while still delegating quoting
|
|
1155
|
+
* and literal serialization to the concrete dialect implementation.
|
|
1156
|
+
*/
|
|
1157
|
+
export const renderExpression = (
|
|
1158
|
+
expression: Expression.Any,
|
|
1159
|
+
state: RenderState,
|
|
1160
|
+
dialect: SqlDialect
|
|
1161
|
+
): string => {
|
|
1162
|
+
const rawAst = (expression as Expression.Any & {
|
|
1163
|
+
readonly [ExpressionAst.TypeId]: ExpressionAst.Any
|
|
1164
|
+
})[ExpressionAst.TypeId] as ExpressionAst.Any | Record<string, unknown>
|
|
1165
|
+
const jsonSql = renderJsonExpression(rawAst as Record<string, unknown>, state, dialect)
|
|
1166
|
+
if (jsonSql !== undefined) {
|
|
1167
|
+
return jsonSql
|
|
1168
|
+
}
|
|
1169
|
+
const ast = rawAst as ExpressionAst.Any
|
|
1170
|
+
const renderComparisonOperator = (operator: "eq" | "neq" | "lt" | "lte" | "gt" | "gte"): "=" | "<>" | "<" | "<=" | ">" | ">=" =>
|
|
1171
|
+
operator === "eq"
|
|
1172
|
+
? "="
|
|
1173
|
+
: operator === "neq"
|
|
1174
|
+
? "<>"
|
|
1175
|
+
: operator === "lt"
|
|
1176
|
+
? "<"
|
|
1177
|
+
: operator === "lte"
|
|
1178
|
+
? "<="
|
|
1179
|
+
: operator === "gt"
|
|
1180
|
+
? ">"
|
|
1181
|
+
: ">="
|
|
1182
|
+
switch (ast.kind) {
|
|
1183
|
+
case "column":
|
|
1184
|
+
return `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
|
|
1185
|
+
case "literal":
|
|
1186
|
+
return dialect.renderLiteral(ast.value, state)
|
|
1187
|
+
case "excluded":
|
|
1188
|
+
return dialect.name === "mysql"
|
|
1189
|
+
? `values(${dialect.quoteIdentifier(ast.columnName)})`
|
|
1190
|
+
: `excluded.${dialect.quoteIdentifier(ast.columnName)}`
|
|
1191
|
+
case "cast":
|
|
1192
|
+
return `cast(${renderExpression(ast.value, state, dialect)} as ${renderCastType(dialect, ast.target)})`
|
|
1193
|
+
case "eq":
|
|
1194
|
+
return `(${renderExpression(ast.left, state, dialect)} = ${renderExpression(ast.right, state, dialect)})`
|
|
1195
|
+
case "neq":
|
|
1196
|
+
return `(${renderExpression(ast.left, state, dialect)} <> ${renderExpression(ast.right, state, dialect)})`
|
|
1197
|
+
case "lt":
|
|
1198
|
+
return `(${renderExpression(ast.left, state, dialect)} < ${renderExpression(ast.right, state, dialect)})`
|
|
1199
|
+
case "lte":
|
|
1200
|
+
return `(${renderExpression(ast.left, state, dialect)} <= ${renderExpression(ast.right, state, dialect)})`
|
|
1201
|
+
case "gt":
|
|
1202
|
+
return `(${renderExpression(ast.left, state, dialect)} > ${renderExpression(ast.right, state, dialect)})`
|
|
1203
|
+
case "gte":
|
|
1204
|
+
return `(${renderExpression(ast.left, state, dialect)} >= ${renderExpression(ast.right, state, dialect)})`
|
|
1205
|
+
case "like":
|
|
1206
|
+
return `(${renderExpression(ast.left, state, dialect)} like ${renderExpression(ast.right, state, dialect)})`
|
|
1207
|
+
case "ilike":
|
|
1208
|
+
return dialect.name === "postgres"
|
|
1209
|
+
? `(${renderExpression(ast.left, state, dialect)} ilike ${renderExpression(ast.right, state, dialect)})`
|
|
1210
|
+
: `(lower(${renderExpression(ast.left, state, dialect)}) like lower(${renderExpression(ast.right, state, dialect)}))`
|
|
1211
|
+
case "isDistinctFrom":
|
|
1212
|
+
return dialect.name === "mysql"
|
|
1213
|
+
? `(not (${renderExpression(ast.left, state, dialect)} <=> ${renderExpression(ast.right, state, dialect)}))`
|
|
1214
|
+
: `(${renderExpression(ast.left, state, dialect)} is distinct from ${renderExpression(ast.right, state, dialect)})`
|
|
1215
|
+
case "isNotDistinctFrom":
|
|
1216
|
+
return dialect.name === "mysql"
|
|
1217
|
+
? `(${renderExpression(ast.left, state, dialect)} <=> ${renderExpression(ast.right, state, dialect)})`
|
|
1218
|
+
: `(${renderExpression(ast.left, state, dialect)} is not distinct from ${renderExpression(ast.right, state, dialect)})`
|
|
1219
|
+
case "contains":
|
|
1220
|
+
if (dialect.name === "postgres") {
|
|
1221
|
+
const left = isJsonExpression(ast.left)
|
|
1222
|
+
? renderPostgresJsonValue(ast.left, state, dialect)
|
|
1223
|
+
: renderExpression(ast.left, state, dialect)
|
|
1224
|
+
const right = isJsonExpression(ast.right)
|
|
1225
|
+
? renderPostgresJsonValue(ast.right, state, dialect)
|
|
1226
|
+
: renderExpression(ast.right, state, dialect)
|
|
1227
|
+
return `(${left} @> ${right})`
|
|
1228
|
+
}
|
|
1229
|
+
if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
|
|
1230
|
+
return `json_contains(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
|
|
1231
|
+
}
|
|
1232
|
+
throw new Error("Unsupported container operator for SQL rendering")
|
|
1233
|
+
case "containedBy":
|
|
1234
|
+
if (dialect.name === "postgres") {
|
|
1235
|
+
const left = isJsonExpression(ast.left)
|
|
1236
|
+
? renderPostgresJsonValue(ast.left, state, dialect)
|
|
1237
|
+
: renderExpression(ast.left, state, dialect)
|
|
1238
|
+
const right = isJsonExpression(ast.right)
|
|
1239
|
+
? renderPostgresJsonValue(ast.right, state, dialect)
|
|
1240
|
+
: renderExpression(ast.right, state, dialect)
|
|
1241
|
+
return `(${left} <@ ${right})`
|
|
1242
|
+
}
|
|
1243
|
+
if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
|
|
1244
|
+
return `json_contains(${renderExpression(ast.right, state, dialect)}, ${renderExpression(ast.left, state, dialect)})`
|
|
1245
|
+
}
|
|
1246
|
+
throw new Error("Unsupported container operator for SQL rendering")
|
|
1247
|
+
case "overlaps":
|
|
1248
|
+
if (dialect.name === "postgres") {
|
|
1249
|
+
const left = isJsonExpression(ast.left)
|
|
1250
|
+
? renderPostgresJsonValue(ast.left, state, dialect)
|
|
1251
|
+
: renderExpression(ast.left, state, dialect)
|
|
1252
|
+
const right = isJsonExpression(ast.right)
|
|
1253
|
+
? renderPostgresJsonValue(ast.right, state, dialect)
|
|
1254
|
+
: renderExpression(ast.right, state, dialect)
|
|
1255
|
+
return `(${left} && ${right})`
|
|
1256
|
+
}
|
|
1257
|
+
if (dialect.name === "mysql" && isJsonExpression(ast.left) && isJsonExpression(ast.right)) {
|
|
1258
|
+
return `json_overlaps(${renderExpression(ast.left, state, dialect)}, ${renderExpression(ast.right, state, dialect)})`
|
|
1259
|
+
}
|
|
1260
|
+
throw new Error("Unsupported container operator for SQL rendering")
|
|
1261
|
+
case "isNull":
|
|
1262
|
+
return `(${renderExpression(ast.value, state, dialect)} is null)`
|
|
1263
|
+
case "isNotNull":
|
|
1264
|
+
return `(${renderExpression(ast.value, state, dialect)} is not null)`
|
|
1265
|
+
case "not":
|
|
1266
|
+
return `(not ${renderExpression(ast.value, state, dialect)})`
|
|
1267
|
+
case "upper":
|
|
1268
|
+
return `upper(${renderExpression(ast.value, state, dialect)})`
|
|
1269
|
+
case "lower":
|
|
1270
|
+
return `lower(${renderExpression(ast.value, state, dialect)})`
|
|
1271
|
+
case "count":
|
|
1272
|
+
return `count(${renderExpression(ast.value, state, dialect)})`
|
|
1273
|
+
case "max":
|
|
1274
|
+
return `max(${renderExpression(ast.value, state, dialect)})`
|
|
1275
|
+
case "min":
|
|
1276
|
+
return `min(${renderExpression(ast.value, state, dialect)})`
|
|
1277
|
+
case "and":
|
|
1278
|
+
return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" and ")})`
|
|
1279
|
+
case "or":
|
|
1280
|
+
return `(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(" or ")})`
|
|
1281
|
+
case "coalesce":
|
|
1282
|
+
return `coalesce(${ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")})`
|
|
1283
|
+
case "in":
|
|
1284
|
+
return `(${renderExpression(ast.values[0]!, state, dialect)} in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
|
|
1285
|
+
case "notIn":
|
|
1286
|
+
return `(${renderExpression(ast.values[0]!, state, dialect)} not in (${ast.values.slice(1).map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}))`
|
|
1287
|
+
case "between":
|
|
1288
|
+
return `(${renderExpression(ast.values[0]!, state, dialect)} between ${renderExpression(ast.values[1]!, state, dialect)} and ${renderExpression(ast.values[2]!, state, dialect)})`
|
|
1289
|
+
case "concat":
|
|
1290
|
+
return dialect.renderConcat(ast.values.map((value: Expression.Any) => renderExpression(value, state, dialect)))
|
|
1291
|
+
case "case":
|
|
1292
|
+
return `case ${ast.branches.map((branch) =>
|
|
1293
|
+
`when ${renderExpression(branch.when, state, dialect)} then ${renderExpression(branch.then, state, dialect)}`
|
|
1294
|
+
).join(" ")} else ${renderExpression(ast.else, state, dialect)} end`
|
|
1295
|
+
case "exists":
|
|
1296
|
+
return `exists (${renderQueryAst(
|
|
1297
|
+
Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
1298
|
+
state,
|
|
1299
|
+
dialect
|
|
1300
|
+
).sql})`
|
|
1301
|
+
case "scalarSubquery":
|
|
1302
|
+
return `(${renderQueryAst(
|
|
1303
|
+
Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
1304
|
+
state,
|
|
1305
|
+
dialect
|
|
1306
|
+
).sql})`
|
|
1307
|
+
case "inSubquery":
|
|
1308
|
+
return `(${renderExpression(ast.left, state, dialect)} in (${renderQueryAst(
|
|
1309
|
+
Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
1310
|
+
state,
|
|
1311
|
+
dialect
|
|
1312
|
+
).sql}))`
|
|
1313
|
+
case "comparisonAny":
|
|
1314
|
+
return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} any (${renderQueryAst(
|
|
1315
|
+
Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
1316
|
+
state,
|
|
1317
|
+
dialect
|
|
1318
|
+
).sql}))`
|
|
1319
|
+
case "comparisonAll":
|
|
1320
|
+
return `(${renderExpression(ast.left, state, dialect)} ${renderComparisonOperator(ast.operator)} all (${renderQueryAst(
|
|
1321
|
+
Query.getAst(ast.plan) as QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
|
|
1322
|
+
state,
|
|
1323
|
+
dialect
|
|
1324
|
+
).sql}))`
|
|
1325
|
+
case "window": {
|
|
1326
|
+
if (!Array.isArray(ast.partitionBy) || !Array.isArray(ast.orderBy) || typeof ast.function !== "string") {
|
|
1327
|
+
break
|
|
1328
|
+
}
|
|
1329
|
+
const clauses: string[] = []
|
|
1330
|
+
if (ast.partitionBy.length > 0) {
|
|
1331
|
+
clauses.push(`partition by ${ast.partitionBy.map((value: Expression.Any) => renderExpression(value, state, dialect)).join(", ")}`)
|
|
1332
|
+
}
|
|
1333
|
+
if (ast.orderBy.length > 0) {
|
|
1334
|
+
clauses.push(`order by ${ast.orderBy.map((entry) =>
|
|
1335
|
+
`${renderExpression(entry.value, state, dialect)} ${entry.direction}`
|
|
1336
|
+
).join(", ")}`)
|
|
1337
|
+
}
|
|
1338
|
+
const specification = clauses.join(" ")
|
|
1339
|
+
switch (ast.function) {
|
|
1340
|
+
case "rowNumber":
|
|
1341
|
+
return `row_number() over (${specification})`
|
|
1342
|
+
case "rank":
|
|
1343
|
+
return `rank() over (${specification})`
|
|
1344
|
+
case "denseRank":
|
|
1345
|
+
return `dense_rank() over (${specification})`
|
|
1346
|
+
case "over":
|
|
1347
|
+
return `${renderExpression(ast.value!, state, dialect)} over (${specification})`
|
|
1348
|
+
}
|
|
1349
|
+
break
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
throw new Error("Unsupported expression for SQL rendering")
|
|
1353
|
+
}
|