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