effect-qb 0.17.0 → 0.19.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 (103) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +8065 -0
  3. package/dist/mysql.js +3053 -2505
  4. package/dist/postgres/metadata.js +1366 -1250
  5. package/dist/postgres.js +2020 -2719
  6. package/dist/sqlite.js +3226 -2732
  7. package/dist/standard.js +8019 -0
  8. package/package.json +10 -3
  9. package/src/casing.ts +71 -0
  10. package/src/index.ts +2 -0
  11. package/src/internal/casing.ts +89 -0
  12. package/src/internal/column-state.ts +11 -6
  13. package/src/internal/column.ts +44 -7
  14. package/src/internal/datatypes/define.ts +2 -1
  15. package/src/internal/datatypes/enrich.ts +23 -0
  16. package/src/internal/datatypes/lookup.ts +14 -7
  17. package/src/internal/derived-table.ts +4 -36
  18. package/src/{mysql/internal/sql-expression-renderer.ts → internal/dialect-renderers/mysql.ts} +548 -359
  19. package/src/{postgres/internal/sql-expression-renderer.ts → internal/dialect-renderers/postgres.ts} +654 -399
  20. package/src/{sqlite/internal/sql-expression-renderer.ts → internal/dialect-renderers/sqlite.ts} +501 -345
  21. package/src/internal/dialect.ts +35 -0
  22. package/src/internal/dsl-mutation-runtime.ts +12 -162
  23. package/src/internal/dsl-plan-runtime.ts +10 -138
  24. package/src/internal/dsl-query-runtime.ts +5 -79
  25. package/src/internal/dsl-transaction-ddl-runtime.ts +41 -65
  26. package/src/internal/executor.ts +10 -6
  27. package/src/internal/grouping-key.ts +87 -20
  28. package/src/internal/implication-runtime.ts +1 -1
  29. package/src/internal/predicate/runtime.ts +3 -0
  30. package/src/internal/query.d.ts +38 -11
  31. package/src/internal/query.ts +64 -25
  32. package/src/internal/renderer.ts +26 -14
  33. package/src/internal/runtime/normalize.ts +12 -5
  34. package/src/internal/scalar.ts +6 -1
  35. package/src/internal/schema-derivation.d.ts +12 -61
  36. package/src/internal/schema-derivation.ts +90 -38
  37. package/src/internal/schema-expression.ts +2 -2
  38. package/src/internal/sql-expression-renderer.ts +19 -0
  39. package/src/internal/standard-dsl.ts +6885 -0
  40. package/src/internal/table-options.ts +126 -66
  41. package/src/internal/table.d.ts +33 -32
  42. package/src/internal/table.ts +406 -155
  43. package/src/mysql/column-extension.ts +3 -0
  44. package/src/mysql/column.ts +10 -11
  45. package/src/mysql/datatypes/index.ts +3 -2
  46. package/src/mysql/executor.ts +7 -5
  47. package/src/mysql/internal/dialect.ts +9 -4
  48. package/src/mysql/internal/dsl.ts +219 -155
  49. package/src/mysql/internal/renderer.ts +6 -2
  50. package/src/mysql/json.ts +37 -0
  51. package/src/mysql/query-extension.ts +16 -0
  52. package/src/mysql/renderer.ts +31 -4
  53. package/src/mysql.ts +4 -12
  54. package/src/postgres/column-extension.ts +28 -0
  55. package/src/postgres/column.ts +5 -11
  56. package/src/postgres/datatypes/index.d.ts +2 -1
  57. package/src/postgres/datatypes/index.ts +3 -2
  58. package/src/postgres/executor.ts +7 -5
  59. package/src/postgres/function/core.ts +1 -3
  60. package/src/postgres/function/index.ts +1 -17
  61. package/src/postgres/internal/dialect.ts +9 -4
  62. package/src/postgres/internal/dsl.ts +208 -160
  63. package/src/postgres/internal/renderer.ts +6 -2
  64. package/src/postgres/internal/schema-ddl.ts +22 -10
  65. package/src/postgres/internal/schema-model.ts +238 -7
  66. package/src/postgres/json.ts +43 -7
  67. package/src/postgres/jsonb.ts +38 -0
  68. package/src/postgres/query-extension.ts +2 -0
  69. package/src/postgres/renderer.ts +31 -4
  70. package/src/postgres/schema-management.ts +17 -12
  71. package/src/postgres/schema.ts +98 -15
  72. package/src/postgres/table.ts +193 -524
  73. package/src/postgres/type.ts +8 -7
  74. package/src/postgres.ts +9 -11
  75. package/src/sqlite/column-extension.ts +3 -0
  76. package/src/sqlite/column.ts +10 -11
  77. package/src/sqlite/datatypes/index.ts +3 -2
  78. package/src/sqlite/executor.ts +7 -5
  79. package/src/sqlite/internal/dialect.ts +9 -4
  80. package/src/sqlite/internal/dsl.ts +208 -155
  81. package/src/sqlite/internal/renderer.ts +6 -2
  82. package/src/sqlite/json.ts +37 -0
  83. package/src/sqlite/query-extension.ts +2 -0
  84. package/src/sqlite/renderer.ts +31 -4
  85. package/src/sqlite.ts +4 -12
  86. package/src/standard/column.ts +163 -0
  87. package/src/standard/datatypes/index.ts +83 -0
  88. package/src/standard/datatypes/spec.ts +98 -0
  89. package/src/standard/dialect.ts +40 -0
  90. package/src/standard/function/aggregate.ts +2 -0
  91. package/src/standard/function/core.ts +2 -0
  92. package/src/standard/function/index.ts +18 -0
  93. package/src/standard/function/string.ts +2 -0
  94. package/src/standard/function/temporal.ts +78 -0
  95. package/src/standard/function/window.ts +2 -0
  96. package/src/standard/internal/renderer.ts +45 -0
  97. package/src/standard/query.ts +152 -0
  98. package/src/standard/renderer.ts +21 -0
  99. package/src/standard/table.ts +147 -0
  100. package/src/standard.ts +18 -0
  101. package/src/internal/aggregation-validation.ts +0 -57
  102. package/src/mysql/table.ts +0 -183
  103. package/src/sqlite/table.ts +0 -183
@@ -1,6 +1,9 @@
1
1
  import type * as Schema from "effect/Schema"
2
2
 
3
+ import type * as QueryAst from "./query-ast.js"
4
+ import type { Projection } from "./projections.js"
3
5
  import type * as Expression from "./scalar.js"
6
+ import type * as Casing from "./casing.js"
4
7
 
5
8
  /**
6
9
  * Mutable rendering state shared while serializing SQL for a concrete dialect.
@@ -11,6 +14,7 @@ import type * as Expression from "./scalar.js"
11
14
  export interface RenderState {
12
15
  readonly params: unknown[]
13
16
  readonly valueMappings?: Expression.DriverValueMappings
17
+ readonly casing?: Casing.Options
14
18
  readonly ctes: {
15
19
  readonly name: string
16
20
  readonly sql: string
@@ -18,7 +22,12 @@ export interface RenderState {
18
22
  }[]
19
23
  readonly cteNames: Set<string>
20
24
  readonly cteSources: Map<string, unknown>
25
+ readonly sourceNames?: Map<string, {
26
+ readonly tableName: string
27
+ readonly columns: ReadonlyMap<string, string>
28
+ }>
21
29
  readonly rowLocalColumns?: boolean
30
+ readonly allowExcluded?: boolean
22
31
  }
23
32
 
24
33
  export interface RenderValueContext {
@@ -27,6 +36,22 @@ export interface RenderValueContext {
27
36
  readonly driverValueMapping?: Expression.DriverValueMapping
28
37
  }
29
38
 
39
+ export const quoteDoubleQuotedIdentifier = (value: string): string => {
40
+ return `"${value.replaceAll("\"", "\"\"")}"`
41
+ }
42
+
43
+ export const quoteBacktickIdentifier = (value: string): string => {
44
+ return `\`${value.replaceAll("`", "``")}\``
45
+ }
46
+
47
+ export const renderDbTypeName = (value: string): string =>
48
+ value
49
+
50
+ export interface RenderedAst {
51
+ readonly sql: string
52
+ readonly projections: readonly Projection[]
53
+ }
54
+
30
55
  /**
31
56
  * Minimal runtime contract for a SQL dialect.
32
57
  *
@@ -40,4 +65,14 @@ export interface SqlDialect<Name extends string = string> {
40
65
  renderLiteral(value: unknown, state: RenderState, context?: RenderValueContext): string
41
66
  renderTableReference(tableName: string, baseTableName: string, schemaName?: string): string
42
67
  renderConcat(values: readonly string[]): string
68
+ renderQueryAst(
69
+ ast: QueryAst.Ast<Record<string, unknown>, any, QueryAst.QueryStatement>,
70
+ state: RenderState,
71
+ dialect: SqlDialect<Name>
72
+ ): RenderedAst
73
+ renderExpression(
74
+ expression: Expression.Any,
75
+ state: RenderState,
76
+ dialect: SqlDialect<Name>
77
+ ): string
43
78
  }
@@ -1,6 +1,6 @@
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
+ import { normalizeStatementFlag } from "./dsl-transaction-ddl-runtime.js"
4
4
 
5
5
  type DslMutationRuntimeContext = {
6
6
  readonly profile: {
@@ -23,150 +23,18 @@ type DslMutationRuntimeContext = {
23
23
  readonly sourceDetails: (source: any) => { readonly sourceName: string; readonly sourceBaseName: string }
24
24
  }
25
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
26
  export const expectConflictClause = <
43
27
  Conflict extends {
44
28
  readonly kind: string
45
29
  readonly action: string
46
- readonly target?: { readonly kind: string }
30
+ readonly target?: { readonly kind: string; readonly name?: string }
47
31
  } | undefined
48
32
  >(
49
33
  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
- }
34
+ ): Conflict => conflict
69
35
 
70
36
  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
-
168
37
  const insert = (target: any, values?: Record<string, unknown>) => {
169
- assertMutationTargets(target, "insert")
170
38
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
171
39
  const assignments = values === undefined
172
40
  ? []
@@ -248,7 +116,6 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
248
116
 
249
117
  const sourcePlan = source
250
118
  const selection = sourcePlan[Plan.TypeId].selection as Record<string, Expression.Any>
251
- assertInsertSelectSource(sourcePlan, selection)
252
119
  const columns = ctx.normalizeInsertSelectColumns(selection)
253
120
  return ctx.makePlan({
254
121
  selection: current.selection,
@@ -271,17 +138,15 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
271
138
  const current = plan[Plan.TypeId]
272
139
  const currentAst = ctx.getAst(plan)
273
140
  const currentQuery = ctx.getQueryState(plan)
274
- if (currentQuery.statement !== "insert") {
275
- throw new Error(`onConflict(...) is not supported for ${currentQuery.statement} statements`)
276
- }
277
141
  const insertTarget = currentAst.into!.source
278
- const conflictTarget = ctx.buildConflictTarget(insertTarget, target)
142
+ const conflictTarget = expectConflictClause({
143
+ kind: "conflict",
144
+ action: "doNothing",
145
+ target: ctx.buildConflictTarget(insertTarget, target)
146
+ }).target
279
147
  const updateAssignments = options.update
280
148
  ? ctx.buildMutationAssignments(insertTarget, options.update)
281
149
  : []
282
- if (options.update !== undefined && updateAssignments.length === 0) {
283
- throw new Error("conflict update assignments require at least one assignment")
284
- }
285
150
  const updateWhere = options.where === undefined
286
151
  ? undefined
287
152
  : ctx.toDialectExpression(options.where)
@@ -311,9 +176,7 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
311
176
  }
312
177
 
313
178
  const update = (target: any, values: Record<string, unknown>) => {
314
- assertMutationTargets(target, "update", { allowMultiple: true })
315
179
  const targets = ctx.mutationTargetClauses(target)
316
- assertUniqueTargetNames(targets)
317
180
  const primaryTarget = targets[0]!
318
181
  const assignments = ctx.buildMutationAssignments(target, values)
319
182
  const targetNames = new Set(targets.map((entry: any) => entry.tableName))
@@ -340,13 +203,9 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
340
203
  }
341
204
 
342
205
  const upsert = (target: any, values: Record<string, unknown>, conflictColumns: string | readonly string[], updateValues?: Record<string, unknown>) => {
343
- assertMutationTargets(target, "upsert")
344
206
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
345
207
  const assignments = ctx.buildMutationAssignments(target, values)
346
208
  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
- }
350
209
  const required = [
351
210
  ...assignments.flatMap((entry) => Object.keys(entry.value[Expression.TypeId].dependencies)),
352
211
  ...updateAssignments.flatMap((entry) => Object.keys(entry.value[Expression.TypeId].dependencies))
@@ -390,9 +249,7 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
390
249
  }
391
250
 
392
251
  const delete_ = (target: any) => {
393
- assertMutationTargets(target, "delete", { allowMultiple: true })
394
252
  const targets = ctx.mutationTargetClauses(target)
395
- assertUniqueTargetNames(targets)
396
253
  const primaryTarget = targets[0]!
397
254
  return ctx.makePlan({
398
255
  selection: {},
@@ -413,7 +270,8 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
413
270
  }
414
271
 
415
272
  const truncate = (target: any, options: { readonly restartIdentity?: boolean; readonly cascade?: boolean } = {}) => {
416
- assertMutationTargets(target, "truncate")
273
+ const restartIdentity = normalizeStatementFlag(options.restartIdentity)
274
+ const cascade = normalizeStatementFlag(options.cascade)
417
275
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
418
276
  return ctx.makePlan({
419
277
  selection: {},
@@ -431,8 +289,8 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
431
289
  },
432
290
  truncate: {
433
291
  kind: "truncate",
434
- restartIdentity: options.restartIdentity ?? false,
435
- cascade: options.cascade ?? false
292
+ restartIdentity,
293
+ cascade
436
294
  },
437
295
  where: [],
438
296
  having: [],
@@ -443,19 +301,11 @@ export const makeDslMutationRuntime = (ctx: DslMutationRuntimeContext) => {
443
301
  }
444
302
 
445
303
  const merge = (target: any, source: any, on: any, options: any = {}) => {
446
- assertMutationTargets(target, "merge")
447
- assertAliasedSource(source, "merge")
448
304
  const { sourceName: targetName, sourceBaseName: targetBaseName } = ctx.targetSourceDetails(target)
449
305
  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
- }
453
306
  const onExpression = ctx.toDialectExpression(on)
454
307
  const matched = options.whenMatched
455
308
  const notMatched = options.whenNotMatched
456
- if (matched && "delete" in matched && "update" in matched) {
457
- throw new Error("merge whenMatched cannot specify both update and delete")
458
- }
459
309
  const matchedPredicate = matched?.predicate ? ctx.toDialectExpression(matched.predicate) : undefined
460
310
  const matchedAssignments = matched && "update" in matched && matched.update
461
311
  ? ctx.buildMutationAssignments(target, matched.update)
@@ -1,6 +1,5 @@
1
1
  import * as Expression from "./scalar.js"
2
2
  import * as Plan from "./row-set.js"
3
- import * as Table from "./table.js"
4
3
 
5
4
  type DslPlanRuntimeContext = {
6
5
  readonly profile: {
@@ -22,122 +21,28 @@ type DslPlanRuntimeContext = {
22
21
  readonly attachInsertSource: (plan: any, source: any) => any
23
22
  }
24
23
 
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
- }
24
+ type LockMode = "update" | "share" | "lowPriority" | "ignore" | "quick"
25
+
26
+ export const renderSelectLockMode = (mode: LockMode): string =>
27
+ mode === "update" ? "for update" : "for share"
34
28
 
35
29
  export const renderMysqlMutationLockMode = (
36
- mode: unknown,
37
- statement: "update" | "delete"
30
+ mode: LockMode,
31
+ _statement: "update" | "delete"
38
32
  ): 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
33
+ if (mode === "lowPriority") {
34
+ return " low_priority"
49
35
  }
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
- )
36
+ return mode === "ignore" ? " ignore" : " quick"
55
37
  }
56
38
 
57
39
  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
40
  const sourceRequiredList = (source: any): readonly string[] =>
66
41
  typeof source === "object" && source !== null && "required" in source
67
42
  ? ctx.currentRequiredList(source.required)
68
43
  : []
69
44
 
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
-
136
45
  const buildSetOperation = (kind: string, all: boolean, left: any, right: any) => {
137
- assertSetOperandStatement(left)
138
- assertSetOperandStatement(right)
139
- assertPlanComplete(left)
140
- assertPlanComplete(right)
141
46
  const leftState = left[Plan.TypeId]
142
47
  const leftAst = ctx.getAst(left)
143
48
  const basePlan = leftAst.kind === "set"
@@ -206,17 +111,10 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
206
111
  return ctx.attachInsertSource(plan, source)
207
112
  }
208
113
 
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")
213
- }
214
-
215
114
  const sourceLike = source
216
115
  const { sourceName, sourceBaseName } = ctx.sourceDetails(sourceLike)
217
116
  const presenceWitnesses = ctx.presenceWitnessesOfSourceLike(sourceLike)
218
117
  const sourceRequired = sourceRequiredList(sourceLike)
219
- assertSourceNameAvailable(current.available, sourceName)
220
118
 
221
119
  if (currentQuery.statement === "select") {
222
120
  const nextAvailable = {
@@ -276,7 +174,7 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
276
174
  }, currentQuery.assumptions, currentQuery.capabilities, currentQuery.statement)
277
175
  }
278
176
 
279
- throw new Error(`from(...) is not supported for ${currentQuery.statement} statements`)
177
+ return plan
280
178
  }
281
179
 
282
180
  const having = (predicate: any) =>
@@ -309,16 +207,9 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
309
207
  const current = plan[Plan.TypeId]
310
208
  const currentAst = ctx.getAst(plan)
311
209
  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
- }
316
210
  const { sourceName, sourceBaseName } = ctx.sourceDetails(table)
317
211
  const presenceWitnesses = ctx.presenceWitnessesOfSourceLike(table)
318
212
  const sourceRequired = sourceRequiredList(table)
319
- if (supportsJoinSources(currentQuery.statement)) {
320
- assertSourceNameAvailable(current.available, sourceName)
321
- }
322
213
  const nextAvailable = {
323
214
  ...current.available,
324
215
  [sourceName]: {
@@ -353,16 +244,9 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
353
244
  const currentQuery = ctx.getQueryState(plan)
354
245
  const onExpression = ctx.toDialectExpression(on)
355
246
  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
- }
360
247
  const { sourceName, sourceBaseName } = ctx.sourceDetails(table)
361
248
  const presenceWitnesses = ctx.presenceWitnessesOfSourceLike(table)
362
249
  const sourceRequired = sourceRequiredList(table)
363
- if (supportsJoinSources(currentQuery.statement)) {
364
- assertSourceNameAvailable(current.available, sourceName)
365
- }
366
250
  const baseAvailable = (kind === "right" || kind === "full"
367
251
  ? Object.fromEntries(
368
252
  Object.entries(current.available as Record<string, any>).map(([name, source]) => [name, {
@@ -408,9 +292,6 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
408
292
 
409
293
  const orderBy = (value: any, direction: "asc" | "desc" = "asc") =>
410
294
  (plan: any) => {
411
- if (direction !== "asc" && direction !== "desc") {
412
- throw new Error("orderBy(...) direction must be asc or desc")
413
- }
414
295
  const current = plan[Plan.TypeId]
415
296
  const currentAst = ctx.getAst(plan)
416
297
  const currentQuery = ctx.getQueryState(plan)
@@ -437,15 +318,6 @@ export const makeDslPlanRuntime = (ctx: DslPlanRuntimeContext) => {
437
318
  const current = plan[Plan.TypeId]
438
319
  const currentAst = ctx.getAst(plan)
439
320
  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
- }
449
321
  return ctx.makePlan({
450
322
  selection: current.selection,
451
323
  required: current.required,
@@ -1,6 +1,5 @@
1
1
  import * as Expression from "./scalar.js"
2
2
  import * as Plan from "./row-set.js"
3
- import { flattenSelection } from "./projections.js"
4
3
 
5
4
  type DslQueryRuntimeContext = {
6
5
  readonly profile: {
@@ -20,55 +19,13 @@ type DslQueryRuntimeContext = {
20
19
  }
21
20
 
22
21
  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
-
53
22
  const values = (rows: readonly [Record<string, any>, ...Record<string, any>[]]) => {
54
- if (rows.length === 0) {
55
- throw new Error("values(...) requires at least one row")
56
- }
57
- const normalizedRows = rows.map((row) => ctx.normalizeValuesRow(row)) as unknown as readonly [
58
- Record<string, Expression.Any>,
59
- ...Record<string, Expression.Any>[]
60
- ]
23
+ const [first, ...rest] = rows
24
+ const normalizedRows = [
25
+ ctx.normalizeValuesRow(first),
26
+ ...rest.map((row) => ctx.normalizeValuesRow(row))
27
+ ] satisfies readonly [Record<string, Expression.Any>, ...Record<string, Expression.Any>[]]
61
28
  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)
66
- for (const row of normalizedRows) {
67
- const rowKeys = Object.keys(row)
68
- if (rowKeys.length !== columnNames.length || !rowKeys.every((key) => columnNameSet.has(key))) {
69
- throw new Error("values(...) rows must project the same columns")
70
- }
71
- }
72
29
  return Object.assign(Object.create(ctx.ValuesInputProto), {
73
30
  kind: "values",
74
31
  dialect: ctx.profile.dialect,
@@ -80,20 +37,6 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
80
37
  const unnest = (columns: Record<string, readonly any[]>, alias: string) => {
81
38
  const normalizedColumns = ctx.normalizeUnnestColumns(columns)
82
39
  const columnNames = Object.keys(normalizedColumns)
83
- if (columnNames.length === 0) {
84
- throw new Error("unnest(...) requires at least one column array")
85
- }
86
- const firstColumn = normalizedColumns[columnNames[0] as keyof typeof normalizedColumns]
87
- const rowCount = firstColumn?.length ?? 0
88
- if (rowCount === 0) {
89
- throw new Error("unnest(...) requires at least one row")
90
- }
91
- for (const columnName of columnNames) {
92
- const values = normalizedColumns[columnName]!
93
- if (values.length !== rowCount) {
94
- throw new Error("unnest(...) column arrays must have the same length")
95
- }
96
- }
97
40
  const firstRow = Object.fromEntries(
98
41
  columnNames.map((columnName) => [columnName, normalizedColumns[columnName]![0]!])
99
42
  ) as Record<string, Expression.Any>
@@ -133,8 +76,6 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
133
76
  }
134
77
 
135
78
  const select = (selection: any = {}) => {
136
- assertSelectionObject("select", selection)
137
- assertSelectionTree("select", selection)
138
79
  return ctx.makePlan({
139
80
  selection,
140
81
  required: ctx.extractRequiredRuntime(selection),
@@ -153,9 +94,6 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
153
94
 
154
95
  const groupBy = (...values: readonly Expression.Any[]) =>
155
96
  (plan: any) => {
156
- if (values.length === 0) {
157
- throw new Error("groupBy(...) requires at least one expression")
158
- }
159
97
  const current = plan[Plan.TypeId]
160
98
  const currentAst = ctx.getAst(plan)
161
99
  const currentQuery = ctx.getQueryState(plan)
@@ -174,22 +112,10 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
174
112
  }
175
113
 
176
114
  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
115
  return (plan: any) => {
183
116
  const current = plan[Plan.TypeId]
184
117
  const currentAst = ctx.getAst(plan)
185
118
  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
- }
193
119
  return ctx.makePlan({
194
120
  selection,
195
121
  required: [...ctx.currentRequiredList(current.required), ...ctx.extractRequiredRuntime(selection)].filter((name, index, list) =>