effect-qb 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mysql.js +1661 -591
- package/dist/postgres/metadata.js +1930 -135
- package/dist/postgres.js +7808 -6718
- package/dist/sqlite.js +8360 -0
- package/package.json +6 -1
- package/src/internal/derived-table.ts +29 -3
- package/src/internal/dialect.ts +2 -0
- 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 +47 -9
- 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/context.ts +14 -1
- package/src/internal/predicate/key.ts +19 -2
- package/src/internal/predicate/runtime.ts +27 -3
- package/src/internal/query.ts +252 -30
- package/src/internal/renderer.ts +35 -2
- package/src/internal/runtime/driver-value-mapping.ts +58 -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/table-options.ts +108 -1
- package/src/internal/table.ts +87 -29
- package/src/mysql/column.ts +18 -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/internal/dsl.ts +736 -218
- package/src/mysql/internal/renderer.ts +2 -1
- package/src/mysql/internal/sql-expression-renderer.ts +486 -130
- package/src/mysql/query.ts +9 -2
- package/src/mysql/table.ts +38 -12
- package/src/postgres/column.ts +4 -2
- package/src/postgres/errors/normalize.ts +2 -2
- package/src/postgres/executor.ts +48 -5
- package/src/postgres/function/core.ts +19 -1
- package/src/postgres/internal/dsl.ts +683 -240
- package/src/postgres/internal/renderer.ts +2 -1
- 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 +420 -91
- package/src/postgres/json.ts +57 -17
- package/src/postgres/query.ts +9 -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/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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Expression from "./scalar.js"
|
|
2
2
|
import * as Plan from "./row-set.js"
|
|
3
|
+
import { flattenSelection } from "./projections.js"
|
|
3
4
|
|
|
4
5
|
type DslQueryRuntimeContext = {
|
|
5
6
|
readonly profile: {
|
|
@@ -19,6 +20,36 @@ type DslQueryRuntimeContext = {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
|
|
23
|
+
const assertSelectionObject = (apiName: string, selection: any): void => {
|
|
24
|
+
if (
|
|
25
|
+
selection === null ||
|
|
26
|
+
typeof selection !== "object" ||
|
|
27
|
+
Array.isArray(selection) ||
|
|
28
|
+
Expression.TypeId in selection
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`${apiName}(...) expects a projection object`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const assertSelectionTree = (apiName: string, selection: any): void => {
|
|
35
|
+
const visit = (value: any, isRoot: boolean): void => {
|
|
36
|
+
if (value !== null && typeof value === "object" && Expression.TypeId in value) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
40
|
+
throw new Error(`${apiName}(...) selection leaves must be expressions`)
|
|
41
|
+
}
|
|
42
|
+
const nested = Object.values(value)
|
|
43
|
+
if (!isRoot && nested.length === 0) {
|
|
44
|
+
throw new Error(`${apiName}(...) projection objects cannot contain empty nested selections`)
|
|
45
|
+
}
|
|
46
|
+
for (const item of nested) {
|
|
47
|
+
visit(item, false)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
visit(selection, true)
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
const values = (rows: readonly [Record<string, any>, ...Record<string, any>[]]) => {
|
|
23
54
|
if (rows.length === 0) {
|
|
24
55
|
throw new Error("values(...) requires at least one row")
|
|
@@ -28,10 +59,14 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
|
|
|
28
59
|
...Record<string, Expression.Any>[]
|
|
29
60
|
]
|
|
30
61
|
const columnNames = Object.keys(normalizedRows[0]!)
|
|
62
|
+
if (columnNames.length === 0) {
|
|
63
|
+
throw new Error("values(...) rows must specify at least one column")
|
|
64
|
+
}
|
|
65
|
+
const columnNameSet = new Set(columnNames)
|
|
31
66
|
for (const row of normalizedRows) {
|
|
32
67
|
const rowKeys = Object.keys(row)
|
|
33
|
-
if (rowKeys.length !== columnNames.length || !rowKeys.every((key
|
|
34
|
-
throw new Error("values(...) rows must project the same columns
|
|
68
|
+
if (rowKeys.length !== columnNames.length || !rowKeys.every((key) => columnNameSet.has(key))) {
|
|
69
|
+
throw new Error("values(...) rows must project the same columns")
|
|
35
70
|
}
|
|
36
71
|
}
|
|
37
72
|
return Object.assign(Object.create(ctx.ValuesInputProto), {
|
|
@@ -97,8 +132,10 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
|
|
|
97
132
|
return Object.assign(source, columns)
|
|
98
133
|
}
|
|
99
134
|
|
|
100
|
-
const select = (selection: any) =>
|
|
101
|
-
|
|
135
|
+
const select = (selection: any = {}) => {
|
|
136
|
+
assertSelectionObject("select", selection)
|
|
137
|
+
assertSelectionTree("select", selection)
|
|
138
|
+
return ctx.makePlan({
|
|
102
139
|
selection,
|
|
103
140
|
required: ctx.extractRequiredRuntime(selection),
|
|
104
141
|
available: {},
|
|
@@ -112,9 +149,13 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
|
|
|
112
149
|
groupBy: [],
|
|
113
150
|
orderBy: []
|
|
114
151
|
}, undefined, "read", "select")
|
|
152
|
+
}
|
|
115
153
|
|
|
116
154
|
const groupBy = (...values: readonly Expression.Any[]) =>
|
|
117
155
|
(plan: any) => {
|
|
156
|
+
if (values.length === 0) {
|
|
157
|
+
throw new Error("groupBy(...) requires at least one expression")
|
|
158
|
+
}
|
|
118
159
|
const current = plan[Plan.TypeId]
|
|
119
160
|
const currentAst = ctx.getAst(plan)
|
|
120
161
|
const currentQuery = ctx.getQueryState(plan)
|
|
@@ -132,11 +173,23 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
|
|
|
132
173
|
}, currentQuery.assumptions, currentQuery.capabilities, currentQuery.statement)
|
|
133
174
|
}
|
|
134
175
|
|
|
135
|
-
const returning = (selection: any) =>
|
|
136
|
-
(
|
|
176
|
+
const returning = (selection: any) => {
|
|
177
|
+
assertSelectionObject("returning", selection)
|
|
178
|
+
assertSelectionTree("returning", selection)
|
|
179
|
+
if (flattenSelection(selection as Record<string, unknown>).length === 0) {
|
|
180
|
+
throw new Error("returning(...) requires at least one selected expression")
|
|
181
|
+
}
|
|
182
|
+
return (plan: any) => {
|
|
137
183
|
const current = plan[Plan.TypeId]
|
|
138
184
|
const currentAst = ctx.getAst(plan)
|
|
139
185
|
const currentQuery = ctx.getQueryState(plan)
|
|
186
|
+
if (
|
|
187
|
+
currentQuery.statement !== "insert" &&
|
|
188
|
+
currentQuery.statement !== "update" &&
|
|
189
|
+
currentQuery.statement !== "delete"
|
|
190
|
+
) {
|
|
191
|
+
throw new Error(`returning(...) is not supported for ${currentQuery.statement} statements`)
|
|
192
|
+
}
|
|
140
193
|
return ctx.makePlan({
|
|
141
194
|
selection,
|
|
142
195
|
required: [...ctx.currentRequiredList(current.required), ...ctx.extractRequiredRuntime(selection)].filter((name, index, list) =>
|
|
@@ -148,6 +201,7 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
|
|
|
148
201
|
select: selection
|
|
149
202
|
}, currentQuery.assumptions, currentQuery.capabilities, currentQuery.statement, currentQuery.target, currentQuery.insertSource)
|
|
150
203
|
}
|
|
204
|
+
}
|
|
151
205
|
|
|
152
206
|
return {
|
|
153
207
|
values,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Plan from "./row-set.js"
|
|
2
|
+
import * as Table from "./table.js"
|
|
2
3
|
|
|
3
4
|
type DslTransactionDdlRuntimeContext = {
|
|
4
5
|
readonly profile: {
|
|
@@ -10,9 +11,71 @@ type DslTransactionDdlRuntimeContext = {
|
|
|
10
11
|
readonly defaultIndexName: (tableName: string, columns: readonly string[], unique: boolean) => string
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
const allowedIsolationLevels = new Set(["read committed", "repeatable read", "serializable"])
|
|
15
|
+
|
|
16
|
+
export const renderTransactionIsolationLevel = (isolationLevel: unknown): string => {
|
|
17
|
+
if (isolationLevel === undefined) {
|
|
18
|
+
return ""
|
|
19
|
+
}
|
|
20
|
+
if (typeof isolationLevel !== "string" || !allowedIsolationLevels.has(isolationLevel)) {
|
|
21
|
+
throw new Error("Unsupported transaction isolation level")
|
|
22
|
+
}
|
|
23
|
+
return `isolation level ${isolationLevel}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const expectDdlClauseKind = <
|
|
27
|
+
Ddl extends { readonly kind: string },
|
|
28
|
+
Kind extends Ddl["kind"]
|
|
29
|
+
>(
|
|
30
|
+
ddl: Ddl | undefined,
|
|
31
|
+
kind: Kind
|
|
32
|
+
): Extract<Ddl, { readonly kind: Kind }> => {
|
|
33
|
+
if (ddl === undefined || ddl.kind !== kind) {
|
|
34
|
+
throw new Error("Unsupported DDL statement kind")
|
|
35
|
+
}
|
|
36
|
+
return ddl as Extract<Ddl, { readonly kind: Kind }>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const expectTruncateClause = <
|
|
40
|
+
Truncate extends { readonly kind: string }
|
|
41
|
+
>(
|
|
42
|
+
truncate: Truncate | undefined
|
|
43
|
+
): Extract<Truncate, { readonly kind: "truncate" }> => {
|
|
44
|
+
if (truncate === undefined || truncate.kind !== "truncate") {
|
|
45
|
+
throw new Error("Unsupported truncate statement kind")
|
|
46
|
+
}
|
|
47
|
+
return truncate as Extract<Truncate, { readonly kind: "truncate" }>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const validateIsolationLevel = (isolationLevel: unknown): void => {
|
|
51
|
+
renderTransactionIsolationLevel(isolationLevel)
|
|
52
|
+
}
|
|
53
|
+
|
|
13
54
|
export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContext) => {
|
|
14
|
-
const
|
|
15
|
-
|
|
55
|
+
const isRecord = (value: unknown): value is Record<PropertyKey, unknown> =>
|
|
56
|
+
typeof value === "object" && value !== null
|
|
57
|
+
|
|
58
|
+
const assertTableTarget = (target: unknown, apiName: string): void => {
|
|
59
|
+
if (!isRecord(target) || !(Table.TypeId in target) || !(Plan.TypeId in target)) {
|
|
60
|
+
throw new Error(`${apiName}(...) requires a table target`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const validateIndexColumns = (target: any, columns: readonly string[]): void => {
|
|
65
|
+
const fields = target[Table.TypeId]?.fields as Record<string, unknown> | undefined
|
|
66
|
+
if (fields === undefined) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
for (const columnName of columns) {
|
|
70
|
+
if (!(columnName in fields)) {
|
|
71
|
+
throw new Error(`effect-qb: unknown index column '${columnName}'`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const transaction = (options: { readonly isolationLevel?: any; readonly readOnly?: boolean } = {}) => {
|
|
77
|
+
validateIsolationLevel(options.isolationLevel)
|
|
78
|
+
return ctx.makePlan({
|
|
16
79
|
selection: {},
|
|
17
80
|
required: [],
|
|
18
81
|
available: {},
|
|
@@ -31,6 +94,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
|
|
|
31
94
|
groupBy: [],
|
|
32
95
|
orderBy: []
|
|
33
96
|
}, undefined, "transaction", "transaction")
|
|
97
|
+
}
|
|
34
98
|
|
|
35
99
|
const commit = () =>
|
|
36
100
|
ctx.makePlan({
|
|
@@ -131,6 +195,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
|
|
|
131
195
|
}, undefined, "transaction", "releaseSavepoint")
|
|
132
196
|
|
|
133
197
|
const createTable = (target: any, options: { readonly ifNotExists?: boolean } = {}) => {
|
|
198
|
+
assertTableTarget(target, "createTable")
|
|
134
199
|
const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
|
|
135
200
|
return ctx.makePlan({
|
|
136
201
|
selection: {},
|
|
@@ -159,6 +224,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
|
|
|
159
224
|
}
|
|
160
225
|
|
|
161
226
|
const dropTable = (target: any, options: { readonly ifExists?: boolean } = {}) => {
|
|
227
|
+
assertTableTarget(target, "dropTable")
|
|
162
228
|
const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
|
|
163
229
|
return ctx.makePlan({
|
|
164
230
|
selection: {},
|
|
@@ -187,7 +253,9 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
|
|
|
187
253
|
}
|
|
188
254
|
|
|
189
255
|
const createIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly unique?: boolean; readonly ifNotExists?: boolean } = {}) => {
|
|
256
|
+
assertTableTarget(target, "createIndex")
|
|
190
257
|
const normalizedColumns = ctx.normalizeColumnList(columns)
|
|
258
|
+
validateIndexColumns(target, normalizedColumns)
|
|
191
259
|
const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
|
|
192
260
|
return ctx.makePlan({
|
|
193
261
|
selection: {},
|
|
@@ -219,7 +287,9 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
|
|
|
219
287
|
}
|
|
220
288
|
|
|
221
289
|
const dropIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly ifExists?: boolean } = {}) => {
|
|
290
|
+
assertTableTarget(target, "dropIndex")
|
|
222
291
|
const normalizedColumns = ctx.normalizeColumnList(columns)
|
|
292
|
+
validateIndexColumns(target, normalizedColumns)
|
|
223
293
|
const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
|
|
224
294
|
return ctx.makePlan({
|
|
225
295
|
selection: {},
|
package/src/internal/executor.ts
CHANGED
|
@@ -16,6 +16,8 @@ import * as Query from "./query.js"
|
|
|
16
16
|
import * as QueryAst from "./query-ast.js"
|
|
17
17
|
import * as Renderer from "./renderer.js"
|
|
18
18
|
import * as Plan from "./row-set.js"
|
|
19
|
+
import { columnPredicateKey } from "./predicate/runtime.js"
|
|
20
|
+
import { isJsonValue } from "./runtime/normalize.js"
|
|
19
21
|
|
|
20
22
|
/** Flat database row keyed by rendered projection aliases. */
|
|
21
23
|
export type FlatRow = Readonly<Record<string, unknown>>
|
|
@@ -221,7 +223,7 @@ const effectiveRuntimeNullability = (
|
|
|
221
223
|
return "always"
|
|
222
224
|
}
|
|
223
225
|
if (ast.kind === "column") {
|
|
224
|
-
const key =
|
|
226
|
+
const key = columnPredicateKey(ast.tableName, ast.columnName)
|
|
225
227
|
if (scope.absentSourceNames.has(ast.tableName) || scope.nullKeys.has(key)) {
|
|
226
228
|
return "always"
|
|
227
229
|
}
|
|
@@ -237,6 +239,20 @@ const effectiveRuntimeNullability = (
|
|
|
237
239
|
: nullability
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
const dbTypeAllowsTopLevelJsonNull = (
|
|
243
|
+
dbType: Expression.DbType.Any
|
|
244
|
+
): boolean => {
|
|
245
|
+
if ("base" in dbType) {
|
|
246
|
+
return dbTypeAllowsTopLevelJsonNull(dbType.base)
|
|
247
|
+
}
|
|
248
|
+
return ("variant" in dbType && dbType.variant === "json") || dbType.runtime === "json"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const schemaAcceptsNull = (
|
|
252
|
+
schema: Schema.Schema.Any | undefined
|
|
253
|
+
): boolean =>
|
|
254
|
+
schema !== undefined && (Schema.is(schema) as (value: unknown) => boolean)(null)
|
|
255
|
+
|
|
240
256
|
const decodeProjectionValue = (
|
|
241
257
|
rendered: Renderer.RenderedQuery<any, any>,
|
|
242
258
|
projection: Renderer.RenderedQuery<any, any>["projections"][number],
|
|
@@ -262,8 +278,12 @@ const decodeProjectionValue = (
|
|
|
262
278
|
}
|
|
263
279
|
|
|
264
280
|
const nullability = effectiveRuntimeNullability(expression, scope)
|
|
281
|
+
const schema = expressionRuntimeSchema(expression, { assumptions: scope.assumptions })
|
|
265
282
|
if (normalized === null) {
|
|
266
283
|
if (nullability === "never") {
|
|
284
|
+
if (dbTypeAllowsTopLevelJsonNull(expression[Expression.TypeId].dbType) && schemaAcceptsNull(schema)) {
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
267
287
|
throw makeRowDecodeError(
|
|
268
288
|
rendered,
|
|
269
289
|
projection,
|
|
@@ -289,7 +309,18 @@ const decodeProjectionValue = (
|
|
|
289
309
|
)
|
|
290
310
|
}
|
|
291
311
|
|
|
292
|
-
|
|
312
|
+
if (dbTypeAllowsTopLevelJsonNull(expression[Expression.TypeId].dbType) && !isJsonValue(normalized)) {
|
|
313
|
+
throw makeRowDecodeError(
|
|
314
|
+
rendered,
|
|
315
|
+
projection,
|
|
316
|
+
expression,
|
|
317
|
+
raw,
|
|
318
|
+
"schema",
|
|
319
|
+
new Error("Expected a JSON value"),
|
|
320
|
+
normalized
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
293
324
|
if (schema === undefined) {
|
|
294
325
|
return normalized
|
|
295
326
|
}
|
|
@@ -316,8 +347,8 @@ export const makeRowDecoder = (
|
|
|
316
347
|
const projections = flattenSelection(
|
|
317
348
|
Query.getAst(plan).select as Record<string, unknown>
|
|
318
349
|
)
|
|
319
|
-
const
|
|
320
|
-
projections.map((projection) => [projection.
|
|
350
|
+
const byPath = new Map(
|
|
351
|
+
projections.map((projection) => [JSON.stringify(projection.path), projection.expression] as const)
|
|
321
352
|
)
|
|
322
353
|
const driverMode = options.driverMode ?? "raw"
|
|
323
354
|
const valueMappings = options.valueMappings ?? rendered.valueMappings
|
|
@@ -325,12 +356,19 @@ export const makeRowDecoder = (
|
|
|
325
356
|
return (row) => {
|
|
326
357
|
const decoded: Record<string, unknown> = {}
|
|
327
358
|
for (const projection of rendered.projections) {
|
|
328
|
-
|
|
329
|
-
continue
|
|
330
|
-
}
|
|
331
|
-
const expression = byAlias.get(projection.alias)
|
|
359
|
+
const expression = byPath.get(JSON.stringify(projection.path))
|
|
332
360
|
if (expression === undefined) {
|
|
333
|
-
|
|
361
|
+
throw new Error(`Rendered projection path '${projection.path.join(".")}' does not exist in the query selection`)
|
|
362
|
+
}
|
|
363
|
+
if (!(projection.alias in row)) {
|
|
364
|
+
throw makeRowDecodeError(
|
|
365
|
+
rendered,
|
|
366
|
+
projection,
|
|
367
|
+
expression,
|
|
368
|
+
undefined,
|
|
369
|
+
"schema",
|
|
370
|
+
new Error(`Missing required projection alias '${projection.alias}'`)
|
|
371
|
+
)
|
|
334
372
|
}
|
|
335
373
|
setPath(
|
|
336
374
|
decoded,
|
|
@@ -36,11 +36,12 @@ export interface CastNode<
|
|
|
36
36
|
|
|
37
37
|
/** Explicit collation captured by the internal expression AST. */
|
|
38
38
|
export interface CollateNode<
|
|
39
|
-
Value extends Expression.Any = Expression.Any
|
|
39
|
+
Value extends Expression.Any = Expression.Any,
|
|
40
|
+
Collation extends readonly [string, ...string[]] = readonly [string, ...string[]]
|
|
40
41
|
> {
|
|
41
42
|
readonly kind: "collate"
|
|
42
43
|
readonly value: Value
|
|
43
|
-
readonly collation:
|
|
44
|
+
readonly collation: Collation
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/** General SQL function call captured by the internal expression AST. */
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import * as Expression from "./scalar.js"
|
|
2
2
|
import * as ExpressionAst from "./expression-ast.js"
|
|
3
|
+
import * as JsonPath from "./json/path.js"
|
|
4
|
+
import { columnPredicateKey } from "./predicate/runtime.js"
|
|
5
|
+
|
|
6
|
+
const subqueryPlanIds = new WeakMap<object, string>()
|
|
7
|
+
let nextSubqueryPlanId = 0
|
|
8
|
+
|
|
9
|
+
const subqueryPlanGroupingKey = (plan: unknown): string => {
|
|
10
|
+
if (plan === null || typeof plan !== "object") {
|
|
11
|
+
return "unknown"
|
|
12
|
+
}
|
|
13
|
+
const existing = subqueryPlanIds.get(plan)
|
|
14
|
+
if (existing !== undefined) {
|
|
15
|
+
return existing
|
|
16
|
+
}
|
|
17
|
+
const next = `${nextSubqueryPlanId++}`
|
|
18
|
+
subqueryPlanIds.set(plan, next)
|
|
19
|
+
return next
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
const literalGroupingKey = (value: unknown): string => {
|
|
5
23
|
if (value instanceof Date) {
|
|
@@ -20,17 +38,84 @@ const literalGroupingKey = (value: unknown): string => {
|
|
|
20
38
|
}
|
|
21
39
|
}
|
|
22
40
|
|
|
41
|
+
const isExpression = (value: unknown): value is Expression.Any =>
|
|
42
|
+
value !== null && typeof value === "object" && Expression.TypeId in value
|
|
43
|
+
|
|
44
|
+
const expressionGroupingKey = (value: unknown): string =>
|
|
45
|
+
isExpression(value) ? groupingKeyOfExpression(value) : "missing"
|
|
46
|
+
|
|
47
|
+
const escapeGroupingText = (value: string): string =>
|
|
48
|
+
value
|
|
49
|
+
.replace(/\\/g, "\\\\")
|
|
50
|
+
.replace(/,/g, "\\,")
|
|
51
|
+
.replace(/\|/g, "\\|")
|
|
52
|
+
.replace(/=/g, "\\=")
|
|
53
|
+
.replace(/>/g, "\\>")
|
|
54
|
+
|
|
55
|
+
const jsonSegmentGroupingKey = (segment: unknown): string => {
|
|
56
|
+
if (segment !== null && typeof segment === "object" && "kind" in segment) {
|
|
57
|
+
switch ((segment as { readonly kind: string }).kind) {
|
|
58
|
+
case "key":
|
|
59
|
+
return `key:${escapeGroupingText((segment as JsonPath.KeySegment).key)}`
|
|
60
|
+
case "index":
|
|
61
|
+
return `index:${(segment as JsonPath.IndexSegment).index}`
|
|
62
|
+
case "wildcard":
|
|
63
|
+
return "wildcard"
|
|
64
|
+
case "slice": {
|
|
65
|
+
const slice = segment as JsonPath.SliceSegment
|
|
66
|
+
return `slice:${slice.start ?? ""}:${slice.end ?? ""}`
|
|
67
|
+
}
|
|
68
|
+
case "descend":
|
|
69
|
+
return "descend"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (typeof segment === "string") {
|
|
73
|
+
return `key:${escapeGroupingText(segment)}`
|
|
74
|
+
}
|
|
75
|
+
if (typeof segment === "number") {
|
|
76
|
+
return `index:${segment}`
|
|
77
|
+
}
|
|
78
|
+
return "unknown"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const jsonPathGroupingKey = (segments: readonly unknown[] | undefined): string =>
|
|
82
|
+
(segments ?? []).map(jsonSegmentGroupingKey).join(",")
|
|
83
|
+
|
|
84
|
+
const isJsonPath = (value: unknown): value is JsonPath.Path =>
|
|
85
|
+
value !== null && typeof value === "object" && JsonPath.TypeId in value
|
|
86
|
+
|
|
87
|
+
const jsonOpaquePathGroupingKey = (value: unknown): string => {
|
|
88
|
+
if (isJsonPath(value)) {
|
|
89
|
+
return `jsonpath:${jsonPathGroupingKey(value.segments)}`
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === "string") {
|
|
92
|
+
return `jsonpath:${escapeGroupingText(value)}`
|
|
93
|
+
}
|
|
94
|
+
if (isExpression(value)) {
|
|
95
|
+
return `jsonpath:${groupingKeyOfExpression(value)}`
|
|
96
|
+
}
|
|
97
|
+
return "jsonpath:unknown"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const jsonEntryGroupingKey = (
|
|
101
|
+
entry: { readonly key: string; readonly value: Expression.Any }
|
|
102
|
+
): string => `${escapeGroupingText(entry.key)}=>${groupingKeyOfExpression(entry.value)}`
|
|
103
|
+
|
|
23
104
|
export const groupingKeyOfExpression = (expression: Expression.Any): string => {
|
|
24
105
|
const ast = (expression as Expression.Any & {
|
|
25
106
|
readonly [ExpressionAst.TypeId]: ExpressionAst.Any
|
|
26
107
|
})[ExpressionAst.TypeId]
|
|
27
108
|
switch (ast.kind) {
|
|
28
109
|
case "column":
|
|
29
|
-
return `column:${ast.tableName
|
|
110
|
+
return `column:${columnPredicateKey(ast.tableName, ast.columnName)}`
|
|
30
111
|
case "literal":
|
|
31
112
|
return `literal:${literalGroupingKey(ast.value)}`
|
|
32
113
|
case "cast":
|
|
33
114
|
return `cast(${groupingKeyOfExpression(ast.value)} as ${ast.target.dialect}:${ast.target.kind})`
|
|
115
|
+
case "collate":
|
|
116
|
+
return `collate(${groupingKeyOfExpression(ast.value)},${ast.collation.map(escapeGroupingText).join(",")})`
|
|
117
|
+
case "function":
|
|
118
|
+
return `function(${escapeGroupingText(ast.name)},${ast.args.map(groupingKeyOfExpression).join(",")})`
|
|
34
119
|
case "isNull":
|
|
35
120
|
case "isNotNull":
|
|
36
121
|
case "not":
|
|
@@ -48,8 +133,15 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
|
|
|
48
133
|
case "gte":
|
|
49
134
|
case "like":
|
|
50
135
|
case "ilike":
|
|
136
|
+
case "regexMatch":
|
|
137
|
+
case "regexIMatch":
|
|
138
|
+
case "regexNotMatch":
|
|
139
|
+
case "regexNotIMatch":
|
|
51
140
|
case "isDistinctFrom":
|
|
52
141
|
case "isNotDistinctFrom":
|
|
142
|
+
case "contains":
|
|
143
|
+
case "containedBy":
|
|
144
|
+
case "overlaps":
|
|
53
145
|
return `${ast.kind}(${groupingKeyOfExpression(ast.left)},${groupingKeyOfExpression(ast.right)})`
|
|
54
146
|
case "and":
|
|
55
147
|
case "or":
|
|
@@ -62,6 +154,54 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
|
|
|
62
154
|
case "case":
|
|
63
155
|
return `case(${ast.branches.map((branch: ExpressionAst.CaseBranchNode) =>
|
|
64
156
|
`when:${groupingKeyOfExpression(branch.when)}=>${groupingKeyOfExpression(branch.then)}`).join("|")};else:${groupingKeyOfExpression(ast.else)})`
|
|
157
|
+
case "exists":
|
|
158
|
+
return `exists(${subqueryPlanGroupingKey(ast.plan)})`
|
|
159
|
+
case "scalarSubquery":
|
|
160
|
+
return `scalarSubquery(${subqueryPlanGroupingKey(ast.plan)})`
|
|
161
|
+
case "inSubquery":
|
|
162
|
+
return `inSubquery(${groupingKeyOfExpression(ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
|
|
163
|
+
case "comparisonAny":
|
|
164
|
+
case "comparisonAll":
|
|
165
|
+
return `${ast.kind}(${ast.operator},${groupingKeyOfExpression(ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
|
|
166
|
+
case "jsonGet":
|
|
167
|
+
case "jsonPath":
|
|
168
|
+
case "jsonAccess":
|
|
169
|
+
case "jsonTraverse":
|
|
170
|
+
case "jsonGetText":
|
|
171
|
+
case "jsonPathText":
|
|
172
|
+
case "jsonAccessText":
|
|
173
|
+
case "jsonTraverseText":
|
|
174
|
+
return `json(${ast.kind},${expressionGroupingKey(ast.base)},${jsonPathGroupingKey(ast.segments)})`
|
|
175
|
+
case "jsonHasKey":
|
|
176
|
+
case "jsonKeyExists":
|
|
177
|
+
case "jsonHasAnyKeys":
|
|
178
|
+
case "jsonHasAllKeys":
|
|
179
|
+
return `json(${ast.kind},${expressionGroupingKey(ast.base)},${(ast.keys ?? []).map(escapeGroupingText).join(",")})`
|
|
180
|
+
case "jsonConcat":
|
|
181
|
+
case "jsonMerge":
|
|
182
|
+
return `json(${ast.kind},${expressionGroupingKey(ast.left)},${expressionGroupingKey(ast.right)},)`
|
|
183
|
+
case "jsonDelete":
|
|
184
|
+
case "jsonDeletePath":
|
|
185
|
+
case "jsonRemove":
|
|
186
|
+
return `json(${ast.kind},${expressionGroupingKey(ast.base)},${expressionGroupingKey(undefined)},${jsonPathGroupingKey(ast.segments)})`
|
|
187
|
+
case "jsonSet":
|
|
188
|
+
return `json(${ast.kind},${expressionGroupingKey(ast.base)},${expressionGroupingKey(ast.newValue)},${jsonPathGroupingKey(ast.segments)})`
|
|
189
|
+
case "jsonInsert":
|
|
190
|
+
return `json(${ast.kind},${expressionGroupingKey(ast.base)},${expressionGroupingKey(ast.insert)},${jsonPathGroupingKey(ast.segments)})`
|
|
191
|
+
case "jsonPathExists":
|
|
192
|
+
case "jsonPathMatch":
|
|
193
|
+
return `json(${ast.kind},${expressionGroupingKey(ast.base)},${jsonOpaquePathGroupingKey(ast.query)})`
|
|
194
|
+
case "jsonBuildObject":
|
|
195
|
+
return `json(${ast.kind},${(ast.entries ?? []).map(jsonEntryGroupingKey).join("|")})`
|
|
196
|
+
case "jsonBuildArray":
|
|
197
|
+
return `json(${ast.kind},${(ast.values ?? []).map(groupingKeyOfExpression).join(",")})`
|
|
198
|
+
case "jsonToJson":
|
|
199
|
+
case "jsonToJsonb":
|
|
200
|
+
case "jsonTypeOf":
|
|
201
|
+
case "jsonLength":
|
|
202
|
+
case "jsonKeys":
|
|
203
|
+
case "jsonStripNulls":
|
|
204
|
+
return `json(${ast.kind},${expressionGroupingKey(ast.value)})`
|
|
65
205
|
default:
|
|
66
206
|
throw new Error("Unsupported expression for grouping key generation")
|
|
67
207
|
}
|
|
@@ -5,6 +5,7 @@ import * as Table from "./table.js"
|
|
|
5
5
|
import type { PredicateFormula } from "./predicate/formula.js"
|
|
6
6
|
import {
|
|
7
7
|
assumeFormulaTrue,
|
|
8
|
+
columnPredicateKey,
|
|
8
9
|
contradictsFormula,
|
|
9
10
|
guaranteedNonNullKeys,
|
|
10
11
|
guaranteedNullKeys,
|
|
@@ -43,7 +44,7 @@ const collectPresenceWitnesses = (
|
|
|
43
44
|
const expression = selection as unknown as AstBackedExpression
|
|
44
45
|
const ast = expression[ExpressionAst.TypeId]
|
|
45
46
|
if (ast.kind === "column" && expression[Expression.TypeId].nullability === "never") {
|
|
46
|
-
output.add(
|
|
47
|
+
output.add(columnPredicateKey(ast.tableName, ast.columnName))
|
|
47
48
|
}
|
|
48
49
|
return
|
|
49
50
|
}
|