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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-qb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,11 +33,16 @@
|
|
|
33
33
|
"types": "./src/mysql.ts",
|
|
34
34
|
"import": "./dist/mysql.js"
|
|
35
35
|
},
|
|
36
|
+
"./sqlite": {
|
|
37
|
+
"types": "./src/sqlite.ts",
|
|
38
|
+
"import": "./dist/sqlite.js"
|
|
39
|
+
},
|
|
36
40
|
"./package.json": "./package.json"
|
|
37
41
|
},
|
|
38
42
|
"imports": {
|
|
39
43
|
"#postgres": "./src/postgres.ts",
|
|
40
44
|
"#mysql": "./src/mysql.ts",
|
|
45
|
+
"#sqlite": "./src/sqlite.ts",
|
|
41
46
|
"#internal/*": "./src/internal/*"
|
|
42
47
|
},
|
|
43
48
|
"publishConfig": {
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
type LateralSource,
|
|
11
11
|
type QueryPlan,
|
|
12
12
|
getAst,
|
|
13
|
+
getQueryState,
|
|
13
14
|
makeExpression,
|
|
15
|
+
currentRequiredList,
|
|
14
16
|
type SelectionOfPlan
|
|
15
17
|
} from "./query.js"
|
|
16
18
|
import * as ExpressionAst from "./expression-ast.js"
|
|
17
|
-
import { flattenSelection } from "./projections.js"
|
|
19
|
+
import { flattenSelection, validateProjections } from "./projections.js"
|
|
18
20
|
|
|
19
21
|
const DerivedProto = {
|
|
20
22
|
pipe(this: unknown) {
|
|
@@ -55,6 +57,24 @@ const setPath = (
|
|
|
55
57
|
|
|
56
58
|
const pathAlias = (path: readonly string[]): string => path.join("__")
|
|
57
59
|
|
|
60
|
+
const assertSourceComplete = (
|
|
61
|
+
plan: QueryPlan<any, any, any, any, any, any, any, any, any, any>
|
|
62
|
+
): void => {
|
|
63
|
+
const required = currentRequiredList(plan[Plan.TypeId].required)
|
|
64
|
+
if (required.length > 0) {
|
|
65
|
+
throw new Error(`query references sources that are not yet in scope: ${required.join(", ")}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const assertInlineSourceStatement = (
|
|
70
|
+
plan: QueryPlan<any, any, any, any, any, any, any, any, any, any>
|
|
71
|
+
): void => {
|
|
72
|
+
const statement = getQueryState(plan).statement
|
|
73
|
+
if (statement !== "select" && statement !== "set") {
|
|
74
|
+
throw new Error("inline derived sources only accept select-like query plans")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
const reboundedColumns = <
|
|
59
79
|
PlanValue extends QueryPlan<any, any, any, any, any, any, any, any, any, any>,
|
|
60
80
|
Alias extends string
|
|
@@ -64,7 +84,9 @@ const reboundedColumns = <
|
|
|
64
84
|
): DerivedSelectionOf<SelectionOfPlan<PlanValue>, Alias> => {
|
|
65
85
|
const ast = getAst(plan)
|
|
66
86
|
const selection = {} as Record<string, unknown>
|
|
67
|
-
|
|
87
|
+
const projections = flattenSelection(ast.select as Record<string, unknown>)
|
|
88
|
+
validateProjections(projections)
|
|
89
|
+
for (const projection of projections) {
|
|
68
90
|
const expectedAlias = pathAlias(projection.path)
|
|
69
91
|
if (projection.alias !== expectedAlias) {
|
|
70
92
|
throw new Error(
|
|
@@ -98,6 +120,8 @@ export const makeDerivedSource = <
|
|
|
98
120
|
plan: CompletePlan<PlanValue>,
|
|
99
121
|
alias: Alias
|
|
100
122
|
): DerivedSource<PlanValue, Alias> => {
|
|
123
|
+
assertInlineSourceStatement(plan)
|
|
124
|
+
assertSourceComplete(plan)
|
|
101
125
|
const columns = reboundedColumns(plan, alias)
|
|
102
126
|
const derived = attachPipe(Object.create(DerivedProto)) as Record<string, unknown>
|
|
103
127
|
Object.assign(derived, columns)
|
|
@@ -119,6 +143,7 @@ export const makeCteSource = <
|
|
|
119
143
|
alias: Alias,
|
|
120
144
|
recursive = false
|
|
121
145
|
): CteSource<PlanValue, Alias> => {
|
|
146
|
+
assertSourceComplete(plan)
|
|
122
147
|
const columns = reboundedColumns(plan, alias)
|
|
123
148
|
const cte = attachPipe(Object.create(DerivedProto)) as Record<string, unknown>
|
|
124
149
|
Object.assign(cte, columns)
|
|
@@ -140,6 +165,7 @@ export const makeLateralSource = <
|
|
|
140
165
|
plan: PlanValue,
|
|
141
166
|
alias: Alias
|
|
142
167
|
): LateralSource<PlanValue, Alias> => {
|
|
168
|
+
assertInlineSourceStatement(plan)
|
|
143
169
|
const columns = reboundedColumns(plan, alias)
|
|
144
170
|
const lateral = attachPipe(Object.create(DerivedProto)) as Record<string, unknown>
|
|
145
171
|
Object.assign(lateral, columns)
|
|
@@ -148,7 +174,7 @@ export const makeLateralSource = <
|
|
|
148
174
|
lateral.baseName = alias
|
|
149
175
|
lateral.dialect = plan[Plan.TypeId].dialect
|
|
150
176
|
lateral.plan = plan
|
|
151
|
-
lateral.required =
|
|
177
|
+
lateral.required = currentRequiredList(plan[Plan.TypeId].required) as never
|
|
152
178
|
lateral.columns = columns
|
|
153
179
|
return lateral as unknown as LateralSource<PlanValue, Alias>
|
|
154
180
|
}
|
package/src/internal/dialect.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as Expression from "./scalar.js"
|
|
2
2
|
import * as Plan from "./row-set.js"
|
|
3
|
+
import * as Table from "./table.js"
|
|
3
4
|
|
|
4
5
|
type DslMutationRuntimeContext = {
|
|
6
|
+
readonly profile: {
|
|
7
|
+
readonly dialect: string
|
|
8
|
+
}
|
|
5
9
|
readonly makePlan: (...args: readonly any[]) => any
|
|
6
10
|
readonly getAst: (plan: any) => any
|
|
7
11
|
readonly getQueryState: (plan: any) => any
|
|
@@ -14,13 +18,155 @@ type DslMutationRuntimeContext = {
|
|
|
14
18
|
readonly buildConflictTarget: (target: any, input: any) => any
|
|
15
19
|
readonly mutationTargetClauses: (target: any) => readonly any[]
|
|
16
20
|
readonly mutationAvailableSources: (target: any) => Record<string, any>
|
|
17
|
-
readonly
|
|
21
|
+
readonly normalizeConflictColumns: (target: any, columns: string | readonly string[]) => readonly string[]
|
|
18
22
|
readonly targetSourceDetails: (target: any) => { readonly sourceName: string; readonly sourceBaseName: string }
|
|
19
23
|
readonly sourceDetails: (source: any) => { readonly sourceName: string; readonly sourceBaseName: string }
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
export const expectInsertSourceKind = <
|
|
27
|
+
Source extends { readonly kind: string } | undefined
|
|
28
|
+
>(
|
|
29
|
+
source: Source
|
|
30
|
+
): Source => {
|
|
31
|
+
if (
|
|
32
|
+
source !== undefined &&
|
|
33
|
+
source.kind !== "values" &&
|
|
34
|
+
source.kind !== "query" &&
|
|
35
|
+
source.kind !== "unnest"
|
|
36
|
+
) {
|
|
37
|
+
throw new Error("Unsupported insert source kind")
|
|
38
|
+
}
|
|
39
|
+
return source
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const expectConflictClause = <
|
|
43
|
+
Conflict extends {
|
|
44
|
+
readonly kind: string
|
|
45
|
+
readonly action: string
|
|
46
|
+
readonly target?: { readonly kind: string }
|
|
47
|
+
} | undefined
|
|
48
|
+
>(
|
|
49
|
+
conflict: Conflict
|
|
50
|
+
): Conflict => {
|
|
51
|
+
if (conflict === undefined) {
|
|
52
|
+
return conflict
|
|
53
|
+
}
|
|
54
|
+
if (conflict.kind !== "conflict") {
|
|
55
|
+
throw new Error("Unsupported conflict clause kind")
|
|
56
|
+
}
|
|
57
|
+
if (conflict.action !== "doNothing" && conflict.action !== "doUpdate") {
|
|
58
|
+
throw new Error("Unsupported conflict action")
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
conflict.target !== undefined &&
|
|
62
|
+
conflict.target.kind !== "columns" &&
|
|
63
|
+
conflict.target.kind !== "constraint"
|
|
64
|
+
) {
|
|
65
|
+
throw new Error("Unsupported conflict target kind")
|
|
66
|
+
}
|
|
67
|
+
return conflict
|
|
68
|
+
}
|
|
69
|
+
|
|
22
70
|
export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
71
|
+
const aliasedSourceKinds = new Set(["derived", "cte", "lateral", "values", "unnest", "tableFunction"])
|
|
72
|
+
const isRecord = (value: unknown): value is Record<PropertyKey, unknown> =>
|
|
73
|
+
typeof value === "object" && value !== null
|
|
74
|
+
|
|
75
|
+
const isTableTarget = (target: unknown): boolean =>
|
|
76
|
+
typeof target === "object" && target !== null && Table.TypeId in target && Plan.TypeId in target
|
|
77
|
+
|
|
78
|
+
const hasColumnRecord = (value: Record<PropertyKey, unknown>): boolean => isRecord(value.columns)
|
|
79
|
+
|
|
80
|
+
const isAliasedSource = (source: unknown): boolean => {
|
|
81
|
+
if (!isRecord(source)) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
if (isTableTarget(source)) {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
if (!("kind" in source) || !("name" in source) || !("baseName" in source)) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
if (typeof source.kind !== "string" || !aliasedSourceKinds.has(source.kind)) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
if (typeof source.name !== "string" || typeof source.baseName !== "string") {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
switch (source.kind) {
|
|
97
|
+
case "derived":
|
|
98
|
+
case "cte":
|
|
99
|
+
case "lateral":
|
|
100
|
+
return isRecord(source.plan) && Plan.TypeId in source.plan && hasColumnRecord(source)
|
|
101
|
+
case "values":
|
|
102
|
+
return Array.isArray(source.rows) && hasColumnRecord(source)
|
|
103
|
+
case "unnest":
|
|
104
|
+
return isRecord(source.arrays) && hasColumnRecord(source)
|
|
105
|
+
case "tableFunction":
|
|
106
|
+
return typeof source.functionName === "string" && Array.isArray(source.args) && hasColumnRecord(source)
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const assertMutationTarget = (target: unknown, apiName: string): void => {
|
|
112
|
+
if (!isTableTarget(target)) {
|
|
113
|
+
throw new Error(`${apiName}(...) requires table targets`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const assertAliasedSource = (source: unknown, apiName: string): void => {
|
|
118
|
+
if (!isAliasedSource(source)) {
|
|
119
|
+
throw new Error(`${apiName}(...) requires an aliased source`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const assertMutationTargets = (
|
|
124
|
+
target: unknown,
|
|
125
|
+
apiName: string,
|
|
126
|
+
options: { readonly allowMultiple?: boolean } = {}
|
|
127
|
+
): void => {
|
|
128
|
+
const targets = Array.isArray(target) ? target : [target]
|
|
129
|
+
if (targets.length === 0) {
|
|
130
|
+
throw new Error(`${apiName}(...) requires at least one table target`)
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(target) && targets.length === 1) {
|
|
133
|
+
throw new Error(`${apiName}(...) requires a table target, not a single-element target tuple`)
|
|
134
|
+
}
|
|
135
|
+
for (const entry of targets) {
|
|
136
|
+
assertMutationTarget(entry, apiName)
|
|
137
|
+
}
|
|
138
|
+
if (targets.length > 1 && options.allowMultiple !== true) {
|
|
139
|
+
throw new Error(`${apiName}(...) requires a single table target`)
|
|
140
|
+
}
|
|
141
|
+
if (targets.length > 1 && ctx.profile.dialect !== "mysql" && ctx.profile.dialect !== "sqlite") {
|
|
142
|
+
throw new Error(`${apiName}(...) only supports multiple mutation targets for mysql`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const assertUniqueTargetNames = (targets: readonly { readonly tableName: string }[]): void => {
|
|
147
|
+
const seen = new Set<string>()
|
|
148
|
+
for (const target of targets) {
|
|
149
|
+
if (seen.has(target.tableName)) {
|
|
150
|
+
throw new Error(`mutation target source names must be unique: ${target.tableName}`)
|
|
151
|
+
}
|
|
152
|
+
seen.add(target.tableName)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const assertInsertSelectSource = (sourcePlan: any, selection: Record<string, unknown>): void => {
|
|
157
|
+
const statement = ctx.getQueryState(sourcePlan).statement
|
|
158
|
+
if (statement !== "select" && statement !== "set") {
|
|
159
|
+
throw new Error("insert sources only accept select-like query plans")
|
|
160
|
+
}
|
|
161
|
+
for (const value of Object.values(selection)) {
|
|
162
|
+
if (value === null || typeof value !== "object" || !(Expression.TypeId in value)) {
|
|
163
|
+
throw new Error("insert sources require a flat selection object")
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
23
168
|
const insert = (target: any, values?: Record<string, unknown>) => {
|
|
169
|
+
assertMutationTargets(target, "insert")
|
|
24
170
|
const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
|
|
25
171
|
const assignments = values === undefined
|
|
26
172
|
? []
|
|
@@ -102,10 +248,11 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
|
102
248
|
|
|
103
249
|
const sourcePlan = source
|
|
104
250
|
const selection = sourcePlan[Plan.TypeId].selection as Record<string, Expression.Any>
|
|
251
|
+
assertInsertSelectSource(sourcePlan, selection)
|
|
105
252
|
const columns = ctx.normalizeInsertSelectColumns(selection)
|
|
106
253
|
return ctx.makePlan({
|
|
107
254
|
selection: current.selection,
|
|
108
|
-
required: ctx.currentRequiredList(sourcePlan[Plan.TypeId].required)
|
|
255
|
+
required: ctx.currentRequiredList(sourcePlan[Plan.TypeId].required),
|
|
109
256
|
available: current.available,
|
|
110
257
|
dialect: current.dialect
|
|
111
258
|
}, {
|
|
@@ -124,18 +271,26 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
|
124
271
|
const current = plan[Plan.TypeId]
|
|
125
272
|
const currentAst = ctx.getAst(plan)
|
|
126
273
|
const currentQuery = ctx.getQueryState(plan)
|
|
274
|
+
if (currentQuery.statement !== "insert") {
|
|
275
|
+
throw new Error(`onConflict(...) is not supported for ${currentQuery.statement} statements`)
|
|
276
|
+
}
|
|
127
277
|
const insertTarget = currentAst.into!.source
|
|
128
278
|
const conflictTarget = ctx.buildConflictTarget(insertTarget, target)
|
|
129
279
|
const updateAssignments = options.update
|
|
130
280
|
? ctx.buildMutationAssignments(insertTarget, options.update)
|
|
131
281
|
: []
|
|
282
|
+
if (options.update !== undefined && updateAssignments.length === 0) {
|
|
283
|
+
throw new Error("conflict update assignments require at least one assignment")
|
|
284
|
+
}
|
|
132
285
|
const updateWhere = options.where === undefined
|
|
133
286
|
? undefined
|
|
134
287
|
: ctx.toDialectExpression(options.where)
|
|
288
|
+
const targetWhere = conflictTarget.kind === "columns" ? conflictTarget.where : undefined
|
|
135
289
|
const required = [
|
|
136
290
|
...ctx.currentRequiredList(current.required),
|
|
137
291
|
...updateAssignments.flatMap((entry) => Object.keys(entry.value[Expression.TypeId].dependencies)),
|
|
138
|
-
...(updateWhere ? Object.keys(updateWhere[Expression.TypeId].dependencies) : [])
|
|
292
|
+
...(updateWhere ? Object.keys(updateWhere[Expression.TypeId].dependencies) : []),
|
|
293
|
+
...(targetWhere ? Object.keys(targetWhere[Expression.TypeId].dependencies) : [])
|
|
139
294
|
].filter((name, index, list) =>
|
|
140
295
|
!(name in current.available) && list.indexOf(name) === index)
|
|
141
296
|
return ctx.makePlan({
|
|
@@ -156,7 +311,9 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
|
156
311
|
}
|
|
157
312
|
|
|
158
313
|
const update = (target: any, values: Record<string, unknown>) => {
|
|
314
|
+
assertMutationTargets(target, "update", { allowMultiple: true })
|
|
159
315
|
const targets = ctx.mutationTargetClauses(target)
|
|
316
|
+
assertUniqueTargetNames(targets)
|
|
160
317
|
const primaryTarget = targets[0]!
|
|
161
318
|
const assignments = ctx.buildMutationAssignments(target, values)
|
|
162
319
|
const targetNames = new Set(targets.map((entry: any) => entry.tableName))
|
|
@@ -183,9 +340,13 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
|
183
340
|
}
|
|
184
341
|
|
|
185
342
|
const upsert = (target: any, values: Record<string, unknown>, conflictColumns: string | readonly string[], updateValues?: Record<string, unknown>) => {
|
|
343
|
+
assertMutationTargets(target, "upsert")
|
|
186
344
|
const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
|
|
187
345
|
const assignments = ctx.buildMutationAssignments(target, values)
|
|
188
346
|
const updateAssignments = updateValues ? ctx.buildMutationAssignments(target, updateValues) : []
|
|
347
|
+
if (updateValues !== undefined && updateAssignments.length === 0) {
|
|
348
|
+
throw new Error("upsert update assignments require at least one assignment")
|
|
349
|
+
}
|
|
189
350
|
const required = [
|
|
190
351
|
...assignments.flatMap((entry) => Object.keys(entry.value[Expression.TypeId].dependencies)),
|
|
191
352
|
...updateAssignments.flatMap((entry) => Object.keys(entry.value[Expression.TypeId].dependencies))
|
|
@@ -215,7 +376,7 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
|
215
376
|
kind: "conflict",
|
|
216
377
|
target: {
|
|
217
378
|
kind: "columns",
|
|
218
|
-
columns: ctx.
|
|
379
|
+
columns: ctx.normalizeConflictColumns(target, conflictColumns) as readonly [string, ...string[]]
|
|
219
380
|
},
|
|
220
381
|
action: updateAssignments.length > 0 ? "doUpdate" : "doNothing",
|
|
221
382
|
values: updateAssignments.length > 0 ? updateAssignments : undefined
|
|
@@ -229,7 +390,9 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
|
229
390
|
}
|
|
230
391
|
|
|
231
392
|
const delete_ = (target: any) => {
|
|
393
|
+
assertMutationTargets(target, "delete", { allowMultiple: true })
|
|
232
394
|
const targets = ctx.mutationTargetClauses(target)
|
|
395
|
+
assertUniqueTargetNames(targets)
|
|
233
396
|
const primaryTarget = targets[0]!
|
|
234
397
|
return ctx.makePlan({
|
|
235
398
|
selection: {},
|
|
@@ -250,6 +413,7 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
|
250
413
|
}
|
|
251
414
|
|
|
252
415
|
const truncate = (target: any, options: { readonly restartIdentity?: boolean; readonly cascade?: boolean } = {}) => {
|
|
416
|
+
assertMutationTargets(target, "truncate")
|
|
253
417
|
const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
|
|
254
418
|
return ctx.makePlan({
|
|
255
419
|
selection: {},
|
|
@@ -279,8 +443,13 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
|
|
|
279
443
|
}
|
|
280
444
|
|
|
281
445
|
const merge = (target: any, source: any, on: any, options: any = {}) => {
|
|
446
|
+
assertMutationTargets(target, "merge")
|
|
447
|
+
assertAliasedSource(source, "merge")
|
|
282
448
|
const { sourceName: targetName, sourceBaseName: targetBaseName } = ctx.targetSourceDetails(target)
|
|
283
449
|
const { sourceName: usingName, sourceBaseName: usingBaseName } = ctx.sourceDetails(source)
|
|
450
|
+
if (targetName === usingName) {
|
|
451
|
+
throw new Error(`merge(...) source name must differ from target source name: ${targetName}`)
|
|
452
|
+
}
|
|
284
453
|
const onExpression = ctx.toDialectExpression(on)
|
|
285
454
|
const matched = options.whenMatched
|
|
286
455
|
const notMatched = options.whenNotMatched
|
|
@@ -22,8 +22,122 @@ type DslPlanRuntimeContext = {
|
|
|
22
22
|
readonly attachInsertSource: (plan: any, source: any) => any
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export const renderSelectLockMode = (mode: unknown): string => {
|
|
26
|
+
switch (mode) {
|
|
27
|
+
case "update":
|
|
28
|
+
return "for update"
|
|
29
|
+
case "share":
|
|
30
|
+
return "for share"
|
|
31
|
+
}
|
|
32
|
+
throw new Error("lock(...) mode must be update or share for select statements")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const renderMysqlMutationLockMode = (
|
|
36
|
+
mode: unknown,
|
|
37
|
+
statement: "update" | "delete"
|
|
38
|
+
): string => {
|
|
39
|
+
switch (mode) {
|
|
40
|
+
case "lowPriority":
|
|
41
|
+
return " low_priority"
|
|
42
|
+
case "ignore":
|
|
43
|
+
return " ignore"
|
|
44
|
+
case "quick":
|
|
45
|
+
if (statement === "delete") {
|
|
46
|
+
return " quick"
|
|
47
|
+
}
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
throw new Error(
|
|
51
|
+
statement === "update"
|
|
52
|
+
? "lock(...) mode must be lowPriority or ignore for update statements"
|
|
53
|
+
: "lock(...) mode must be lowPriority, quick, or ignore for delete statements"
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
58
|
+
const aliasedSourceKinds = new Set(["derived", "cte", "lateral", "values", "unnest", "tableFunction"])
|
|
59
|
+
const isRecord = (value: unknown): value is Record<PropertyKey, unknown> =>
|
|
60
|
+
typeof value === "object" && value !== null
|
|
61
|
+
|
|
62
|
+
const isPlan = (value: unknown): boolean => isRecord(value) && Plan.TypeId in value
|
|
63
|
+
const hasColumnRecord = (value: Record<PropertyKey, unknown>): boolean => isRecord(value.columns)
|
|
64
|
+
|
|
65
|
+
const sourceRequiredList = (source: any): readonly string[] =>
|
|
66
|
+
typeof source === "object" && source !== null && "required" in source
|
|
67
|
+
? ctx.currentRequiredList(source.required)
|
|
68
|
+
: []
|
|
69
|
+
|
|
70
|
+
const isAliasedSource = (source: unknown): boolean => {
|
|
71
|
+
if (!isRecord(source)) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
if (Table.TypeId in source) {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
if (!("kind" in source) || !("name" in source) || !("baseName" in source)) {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
if (typeof source.kind !== "string" || !aliasedSourceKinds.has(source.kind)) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
if (typeof source.name !== "string" || typeof source.baseName !== "string") {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
switch (source.kind) {
|
|
87
|
+
case "derived":
|
|
88
|
+
case "cte":
|
|
89
|
+
case "lateral":
|
|
90
|
+
return isPlan(source.plan) && hasColumnRecord(source)
|
|
91
|
+
case "values":
|
|
92
|
+
return Array.isArray(source.rows) && hasColumnRecord(source)
|
|
93
|
+
case "unnest":
|
|
94
|
+
return isRecord(source.arrays) && hasColumnRecord(source)
|
|
95
|
+
case "tableFunction":
|
|
96
|
+
return typeof source.functionName === "string" && Array.isArray(source.args) && hasColumnRecord(source)
|
|
97
|
+
}
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const assertAliasedSource = (source: unknown, message: string): void => {
|
|
102
|
+
if (!isAliasedSource(source)) {
|
|
103
|
+
throw new Error(message)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const assertPlanComplete = (plan: any): void => {
|
|
108
|
+
const required = ctx.currentRequiredList(plan[Plan.TypeId].required)
|
|
109
|
+
if (required.length > 0) {
|
|
110
|
+
throw new Error(`query references sources that are not yet in scope: ${required.join(", ")}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const assertSourceNameAvailable = (available: Record<string, unknown>, sourceName: string): void => {
|
|
115
|
+
if (sourceName in available) {
|
|
116
|
+
throw new Error(`query source name is already in scope: ${sourceName}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const assertSelectHasBaseSourceForJoin = (statement: string, available: Record<string, unknown>): void => {
|
|
121
|
+
if (statement === "select" && Object.keys(available).length === 0) {
|
|
122
|
+
throw new Error("select joins require a from(...) source before joining")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const supportsJoinSources = (statement: string): boolean =>
|
|
127
|
+
statement === "select" || statement === "update" || statement === "delete"
|
|
128
|
+
|
|
129
|
+
const assertSetOperandStatement = (plan: any): void => {
|
|
130
|
+
const statement = ctx.getQueryState(plan).statement
|
|
131
|
+
if (statement !== "select" && statement !== "set") {
|
|
132
|
+
throw new Error("set operator operands only accept select-like query plans")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
26
136
|
const buildSetOperation = (kind: string, all: boolean, left: any, right: any) => {
|
|
137
|
+
assertSetOperandStatement(left)
|
|
138
|
+
assertSetOperandStatement(right)
|
|
139
|
+
assertPlanComplete(left)
|
|
140
|
+
assertPlanComplete(right)
|
|
27
141
|
const leftState = left[Plan.TypeId]
|
|
28
142
|
const leftAst = ctx.getAst(left)
|
|
29
143
|
const basePlan = leftAst.kind === "set"
|
|
@@ -92,32 +206,33 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
|
92
206
|
return ctx.attachInsertSource(plan, source)
|
|
93
207
|
}
|
|
94
208
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
("
|
|
99
|
-
(!(Table.TypeId in source) && !("name" in source && "baseName" in source))
|
|
100
|
-
) {
|
|
101
|
-
throw new Error("from(...) requires an aliased source in select/update statements")
|
|
209
|
+
assertAliasedSource(source, "from(...) requires an aliased source in select/update statements")
|
|
210
|
+
|
|
211
|
+
if (currentQuery.statement === "select" && currentAst.from !== undefined) {
|
|
212
|
+
throw new Error("select statements accept only one from(...) source; use joins for additional sources")
|
|
102
213
|
}
|
|
103
214
|
|
|
104
215
|
const sourceLike = source
|
|
105
216
|
const { sourceName, sourceBaseName } = ctx.sourceDetails(sourceLike)
|
|
106
217
|
const presenceWitnesses = ctx.presenceWitnessesOfSourceLike(sourceLike)
|
|
218
|
+
const sourceRequired = sourceRequiredList(sourceLike)
|
|
219
|
+
assertSourceNameAvailable(current.available, sourceName)
|
|
107
220
|
|
|
108
221
|
if (currentQuery.statement === "select") {
|
|
222
|
+
const nextAvailable = {
|
|
223
|
+
[sourceName]: {
|
|
224
|
+
name: sourceName,
|
|
225
|
+
mode: "required",
|
|
226
|
+
baseName: sourceBaseName,
|
|
227
|
+
_presentFormula: ctx.trueFormula(),
|
|
228
|
+
_presenceWitnesses: presenceWitnesses
|
|
229
|
+
}
|
|
230
|
+
}
|
|
109
231
|
return ctx.makePlan({
|
|
110
232
|
selection: current.selection,
|
|
111
|
-
required: ctx.currentRequiredList(current.required).filter((name) =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
name: sourceName,
|
|
115
|
-
mode: "required",
|
|
116
|
-
baseName: sourceBaseName,
|
|
117
|
-
_presentFormula: ctx.trueFormula(),
|
|
118
|
-
_presenceWitnesses: presenceWitnesses
|
|
119
|
-
}
|
|
120
|
-
},
|
|
233
|
+
required: [...ctx.currentRequiredList(current.required), ...sourceRequired].filter((name, index, values) =>
|
|
234
|
+
!(name in nextAvailable) && values.indexOf(name) === index),
|
|
235
|
+
available: nextAvailable,
|
|
121
236
|
dialect: current.dialect
|
|
122
237
|
}, {
|
|
123
238
|
...currentAst,
|
|
@@ -143,7 +258,8 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
|
143
258
|
}
|
|
144
259
|
return ctx.makePlan({
|
|
145
260
|
selection: current.selection,
|
|
146
|
-
required: ctx.currentRequiredList(current.required).filter((name) =>
|
|
261
|
+
required: [...ctx.currentRequiredList(current.required), ...sourceRequired].filter((name, index, values) =>
|
|
262
|
+
!(name in nextAvailable) && values.indexOf(name) === index),
|
|
147
263
|
available: nextAvailable,
|
|
148
264
|
dialect: current.dialect
|
|
149
265
|
}, {
|
|
@@ -193,8 +309,16 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
|
193
309
|
const current = plan[Plan.TypeId]
|
|
194
310
|
const currentAst = ctx.getAst(plan)
|
|
195
311
|
const currentQuery = ctx.getQueryState(plan)
|
|
312
|
+
if (supportsJoinSources(currentQuery.statement)) {
|
|
313
|
+
assertAliasedSource(table, "join(...) requires an aliased source in select/update/delete statements")
|
|
314
|
+
assertSelectHasBaseSourceForJoin(currentQuery.statement, current.available)
|
|
315
|
+
}
|
|
196
316
|
const { sourceName, sourceBaseName } = ctx.sourceDetails(table)
|
|
197
317
|
const presenceWitnesses = ctx.presenceWitnessesOfSourceLike(table)
|
|
318
|
+
const sourceRequired = sourceRequiredList(table)
|
|
319
|
+
if (supportsJoinSources(currentQuery.statement)) {
|
|
320
|
+
assertSourceNameAvailable(current.available, sourceName)
|
|
321
|
+
}
|
|
198
322
|
const nextAvailable = {
|
|
199
323
|
...current.available,
|
|
200
324
|
[sourceName]: {
|
|
@@ -207,7 +331,8 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
|
207
331
|
}
|
|
208
332
|
return ctx.makePlan({
|
|
209
333
|
selection: current.selection,
|
|
210
|
-
required: ctx.currentRequiredList(current.required).filter((name) =>
|
|
334
|
+
required: [...ctx.currentRequiredList(current.required), ...sourceRequired].filter((name, index, values) =>
|
|
335
|
+
!(name in nextAvailable) && values.indexOf(name) === index),
|
|
211
336
|
available: nextAvailable,
|
|
212
337
|
dialect: current.dialect ?? table[Plan.TypeId]?.dialect ?? table.dialect
|
|
213
338
|
}, {
|
|
@@ -228,8 +353,16 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
|
228
353
|
const currentQuery = ctx.getQueryState(plan)
|
|
229
354
|
const onExpression = ctx.toDialectExpression(on)
|
|
230
355
|
const onFormula = ctx.formulaOfExpressionRuntime(onExpression)
|
|
356
|
+
if (supportsJoinSources(currentQuery.statement)) {
|
|
357
|
+
assertAliasedSource(table, "join(...) requires an aliased source in select/update/delete statements")
|
|
358
|
+
assertSelectHasBaseSourceForJoin(currentQuery.statement, current.available)
|
|
359
|
+
}
|
|
231
360
|
const { sourceName, sourceBaseName } = ctx.sourceDetails(table)
|
|
232
361
|
const presenceWitnesses = ctx.presenceWitnessesOfSourceLike(table)
|
|
362
|
+
const sourceRequired = sourceRequiredList(table)
|
|
363
|
+
if (supportsJoinSources(currentQuery.statement)) {
|
|
364
|
+
assertSourceNameAvailable(current.available, sourceName)
|
|
365
|
+
}
|
|
233
366
|
const baseAvailable = (kind === "right" || kind === "full"
|
|
234
367
|
? Object.fromEntries(
|
|
235
368
|
Object.entries(current.available as Record<string, any>).map(([name, source]) => [name, {
|
|
@@ -253,7 +386,7 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
|
253
386
|
}
|
|
254
387
|
return ctx.makePlan({
|
|
255
388
|
selection: current.selection,
|
|
256
|
-
required: [...ctx.currentRequiredList(current.required), ...ctx.extractRequiredFromDialectInputRuntime(on)].filter((name, index, values) =>
|
|
389
|
+
required: [...ctx.currentRequiredList(current.required), ...sourceRequired, ...ctx.extractRequiredFromDialectInputRuntime(on)].filter((name, index, values) =>
|
|
257
390
|
!(name in nextAvailable) && values.indexOf(name) === index),
|
|
258
391
|
available: nextAvailable,
|
|
259
392
|
dialect: current.dialect ?? table.dialect ?? onExpression[Expression.TypeId].dialect
|
|
@@ -275,6 +408,9 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
|
275
408
|
|
|
276
409
|
const orderBy = (value: any, direction: "asc" | "desc" = "asc") =>
|
|
277
410
|
(plan: any) => {
|
|
411
|
+
if (direction !== "asc" && direction !== "desc") {
|
|
412
|
+
throw new Error("orderBy(...) direction must be asc or desc")
|
|
413
|
+
}
|
|
278
414
|
const current = plan[Plan.TypeId]
|
|
279
415
|
const currentAst = ctx.getAst(plan)
|
|
280
416
|
const currentQuery = ctx.getQueryState(plan)
|
|
@@ -301,6 +437,15 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
|
|
|
301
437
|
const current = plan[Plan.TypeId]
|
|
302
438
|
const currentAst = ctx.getAst(plan)
|
|
303
439
|
const currentQuery = ctx.getQueryState(plan)
|
|
440
|
+
if (currentQuery.statement === "select") {
|
|
441
|
+
renderSelectLockMode(mode)
|
|
442
|
+
}
|
|
443
|
+
if (ctx.profile.dialect === "mysql" && currentQuery.statement === "update") {
|
|
444
|
+
renderMysqlMutationLockMode(mode, "update")
|
|
445
|
+
}
|
|
446
|
+
if (ctx.profile.dialect === "mysql" && currentQuery.statement === "delete") {
|
|
447
|
+
renderMysqlMutationLockMode(mode, "delete")
|
|
448
|
+
}
|
|
304
449
|
return ctx.makePlan({
|
|
305
450
|
selection: current.selection,
|
|
306
451
|
required: current.required,
|