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.
Files changed (75) hide show
  1. package/dist/mysql.js +1661 -591
  2. package/dist/postgres/metadata.js +1930 -135
  3. package/dist/postgres.js +7808 -6718
  4. package/dist/sqlite.js +8360 -0
  5. package/package.json +6 -1
  6. package/src/internal/derived-table.ts +29 -3
  7. package/src/internal/dialect.ts +2 -0
  8. package/src/internal/dsl-mutation-runtime.ts +173 -4
  9. package/src/internal/dsl-plan-runtime.ts +165 -20
  10. package/src/internal/dsl-query-runtime.ts +60 -6
  11. package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
  12. package/src/internal/executor.ts +47 -9
  13. package/src/internal/expression-ast.ts +3 -2
  14. package/src/internal/grouping-key.ts +141 -1
  15. package/src/internal/implication-runtime.ts +2 -1
  16. package/src/internal/json/types.ts +155 -40
  17. package/src/internal/predicate/context.ts +14 -1
  18. package/src/internal/predicate/key.ts +19 -2
  19. package/src/internal/predicate/runtime.ts +27 -3
  20. package/src/internal/query.ts +252 -30
  21. package/src/internal/renderer.ts +35 -2
  22. package/src/internal/runtime/driver-value-mapping.ts +58 -0
  23. package/src/internal/runtime/normalize.ts +62 -38
  24. package/src/internal/runtime/schema.ts +5 -3
  25. package/src/internal/runtime/value.ts +153 -30
  26. package/src/internal/table-options.ts +108 -1
  27. package/src/internal/table.ts +87 -29
  28. package/src/mysql/column.ts +18 -2
  29. package/src/mysql/datatypes/index.ts +21 -0
  30. package/src/mysql/errors/catalog.ts +5 -5
  31. package/src/mysql/errors/normalize.ts +2 -2
  32. package/src/mysql/internal/dsl.ts +736 -218
  33. package/src/mysql/internal/renderer.ts +2 -1
  34. package/src/mysql/internal/sql-expression-renderer.ts +486 -130
  35. package/src/mysql/query.ts +9 -2
  36. package/src/mysql/table.ts +38 -12
  37. package/src/postgres/column.ts +4 -2
  38. package/src/postgres/errors/normalize.ts +2 -2
  39. package/src/postgres/executor.ts +48 -5
  40. package/src/postgres/function/core.ts +19 -1
  41. package/src/postgres/internal/dsl.ts +683 -240
  42. package/src/postgres/internal/renderer.ts +2 -1
  43. package/src/postgres/internal/schema-ddl.ts +2 -1
  44. package/src/postgres/internal/schema-model.ts +6 -3
  45. package/src/postgres/internal/sql-expression-renderer.ts +420 -91
  46. package/src/postgres/json.ts +57 -17
  47. package/src/postgres/query.ts +9 -2
  48. package/src/postgres/schema-management.ts +91 -4
  49. package/src/postgres/schema.ts +1 -1
  50. package/src/postgres/table.ts +189 -53
  51. package/src/sqlite/column.ts +128 -0
  52. package/src/sqlite/datatypes/index.ts +79 -0
  53. package/src/sqlite/datatypes/spec.ts +98 -0
  54. package/src/sqlite/errors/catalog.ts +103 -0
  55. package/src/sqlite/errors/fields.ts +19 -0
  56. package/src/sqlite/errors/index.ts +19 -0
  57. package/src/sqlite/errors/normalize.ts +229 -0
  58. package/src/sqlite/errors/requirements.ts +71 -0
  59. package/src/sqlite/errors/types.ts +29 -0
  60. package/src/sqlite/executor.ts +227 -0
  61. package/src/sqlite/function/aggregate.ts +2 -0
  62. package/src/sqlite/function/core.ts +2 -0
  63. package/src/sqlite/function/index.ts +19 -0
  64. package/src/sqlite/function/string.ts +2 -0
  65. package/src/sqlite/function/temporal.ts +100 -0
  66. package/src/sqlite/function/window.ts +2 -0
  67. package/src/sqlite/internal/dialect.ts +37 -0
  68. package/src/sqlite/internal/dsl.ts +6926 -0
  69. package/src/sqlite/internal/renderer.ts +47 -0
  70. package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
  71. package/src/sqlite/json.ts +2 -0
  72. package/src/sqlite/query.ts +196 -0
  73. package/src/sqlite/renderer.ts +24 -0
  74. package/src/sqlite/table.ts +183 -0
  75. package/src/sqlite.ts +22 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-qb",
3
- "version": "0.16.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
- for (const projection of flattenSelection(ast.select as Record<string, unknown>)) {
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 = undefined as never
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
  }
@@ -17,6 +17,8 @@ export interface RenderState {
17
17
  readonly recursive?: boolean
18
18
  }[]
19
19
  readonly cteNames: Set<string>
20
+ readonly cteSources: Map<string, unknown>
21
+ readonly rowLocalColumns?: boolean
20
22
  }
21
23
 
22
24
  export interface RenderValueContext {
@@ -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 normalizeColumnList: (columns: string | readonly string[]) => readonly string[]
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).filter((name) => name !== sourceName),
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.normalizeColumnList(conflictColumns) as readonly [string, ...string[]]
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
- if (
96
- typeof source !== "object" ||
97
- source === null ||
98
- ("kind" in source && source.kind === "values" && !("name" in source)) ||
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) => name !== sourceName),
112
- available: {
113
- [sourceName]: {
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) => !(name in nextAvailable)),
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) => !(name in nextAvailable)),
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,