effect-qb 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/mysql.js +1957 -595
  2. package/dist/postgres/metadata.js +2507 -182
  3. package/dist/postgres.js +9587 -8201
  4. package/dist/sqlite.js +8360 -0
  5. package/package.json +7 -2
  6. package/src/internal/column-state.ts +7 -0
  7. package/src/internal/column.ts +22 -0
  8. package/src/internal/derived-table.ts +29 -3
  9. package/src/internal/dialect.ts +14 -1
  10. package/src/internal/dsl-mutation-runtime.ts +173 -4
  11. package/src/internal/dsl-plan-runtime.ts +165 -20
  12. package/src/internal/dsl-query-runtime.ts +60 -6
  13. package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
  14. package/src/internal/executor.ts +62 -13
  15. package/src/internal/expression-ast.ts +3 -2
  16. package/src/internal/grouping-key.ts +141 -1
  17. package/src/internal/implication-runtime.ts +2 -1
  18. package/src/internal/json/types.ts +155 -40
  19. package/src/internal/predicate/analysis.ts +103 -1
  20. package/src/internal/predicate/atom.ts +7 -0
  21. package/src/internal/predicate/context.ts +170 -17
  22. package/src/internal/predicate/key.ts +64 -2
  23. package/src/internal/predicate/normalize.ts +115 -34
  24. package/src/internal/predicate/runtime.ts +144 -13
  25. package/src/internal/query.ts +563 -103
  26. package/src/internal/renderer.ts +39 -2
  27. package/src/internal/runtime/driver-value-mapping.ts +244 -0
  28. package/src/internal/runtime/normalize.ts +62 -38
  29. package/src/internal/runtime/schema.ts +5 -3
  30. package/src/internal/runtime/value.ts +153 -30
  31. package/src/internal/scalar.ts +11 -0
  32. package/src/internal/table-options.ts +108 -1
  33. package/src/internal/table.ts +87 -29
  34. package/src/mysql/column.ts +19 -2
  35. package/src/mysql/datatypes/index.ts +21 -0
  36. package/src/mysql/errors/catalog.ts +5 -5
  37. package/src/mysql/errors/normalize.ts +2 -2
  38. package/src/mysql/executor.ts +20 -5
  39. package/src/mysql/internal/dialect.ts +12 -6
  40. package/src/mysql/internal/dsl.ts +995 -263
  41. package/src/mysql/internal/renderer.ts +13 -3
  42. package/src/mysql/internal/sql-expression-renderer.ts +530 -128
  43. package/src/mysql/query.ts +9 -2
  44. package/src/mysql/renderer.ts +7 -2
  45. package/src/mysql/table.ts +38 -12
  46. package/src/postgres/cast.ts +22 -7
  47. package/src/postgres/column.ts +5 -2
  48. package/src/postgres/errors/normalize.ts +2 -2
  49. package/src/postgres/executor.ts +68 -10
  50. package/src/postgres/function/core.ts +19 -1
  51. package/src/postgres/internal/dialect.ts +12 -6
  52. package/src/postgres/internal/dsl.ts +958 -288
  53. package/src/postgres/internal/renderer.ts +13 -3
  54. package/src/postgres/internal/schema-ddl.ts +2 -1
  55. package/src/postgres/internal/schema-model.ts +6 -3
  56. package/src/postgres/internal/sql-expression-renderer.ts +477 -96
  57. package/src/postgres/json.ts +57 -17
  58. package/src/postgres/query.ts +9 -2
  59. package/src/postgres/renderer.ts +7 -2
  60. package/src/postgres/schema-management.ts +91 -4
  61. package/src/postgres/schema.ts +1 -1
  62. package/src/postgres/table.ts +189 -53
  63. package/src/postgres/type.ts +4 -0
  64. package/src/sqlite/column.ts +128 -0
  65. package/src/sqlite/datatypes/index.ts +79 -0
  66. package/src/sqlite/datatypes/spec.ts +98 -0
  67. package/src/sqlite/errors/catalog.ts +103 -0
  68. package/src/sqlite/errors/fields.ts +19 -0
  69. package/src/sqlite/errors/index.ts +19 -0
  70. package/src/sqlite/errors/normalize.ts +229 -0
  71. package/src/sqlite/errors/requirements.ts +71 -0
  72. package/src/sqlite/errors/types.ts +29 -0
  73. package/src/sqlite/executor.ts +227 -0
  74. package/src/sqlite/function/aggregate.ts +2 -0
  75. package/src/sqlite/function/core.ts +2 -0
  76. package/src/sqlite/function/index.ts +19 -0
  77. package/src/sqlite/function/string.ts +2 -0
  78. package/src/sqlite/function/temporal.ts +100 -0
  79. package/src/sqlite/function/window.ts +2 -0
  80. package/src/sqlite/internal/dialect.ts +37 -0
  81. package/src/sqlite/internal/dsl.ts +6926 -0
  82. package/src/sqlite/internal/renderer.ts +47 -0
  83. package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
  84. package/src/sqlite/json.ts +2 -0
  85. package/src/sqlite/query.ts +196 -0
  86. package/src/sqlite/renderer.ts +24 -0
  87. package/src/sqlite/table.ts +183 -0
  88. package/src/sqlite.ts +22 -0
@@ -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,
@@ -1,5 +1,6 @@
1
1
  import * as Expression from "./scalar.js"
2
2
  import * as Plan from "./row-set.js"
3
+ import { flattenSelection } from "./projections.js"
3
4
 
4
5
  type DslQueryRuntimeContext = {
5
6
  readonly profile: {
@@ -19,6 +20,36 @@ type DslQueryRuntimeContext = {
19
20
  }
20
21
 
21
22
  export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
23
+ const assertSelectionObject = (apiName: string, selection: any): void => {
24
+ if (
25
+ selection === null ||
26
+ typeof selection !== "object" ||
27
+ Array.isArray(selection) ||
28
+ Expression.TypeId in selection
29
+ ) {
30
+ throw new Error(`${apiName}(...) expects a projection object`)
31
+ }
32
+ }
33
+
34
+ const assertSelectionTree = (apiName: string, selection: any): void => {
35
+ const visit = (value: any, isRoot: boolean): void => {
36
+ if (value !== null && typeof value === "object" && Expression.TypeId in value) {
37
+ return
38
+ }
39
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
40
+ throw new Error(`${apiName}(...) selection leaves must be expressions`)
41
+ }
42
+ const nested = Object.values(value)
43
+ if (!isRoot && nested.length === 0) {
44
+ throw new Error(`${apiName}(...) projection objects cannot contain empty nested selections`)
45
+ }
46
+ for (const item of nested) {
47
+ visit(item, false)
48
+ }
49
+ }
50
+ visit(selection, true)
51
+ }
52
+
22
53
  const values = (rows: readonly [Record<string, any>, ...Record<string, any>[]]) => {
23
54
  if (rows.length === 0) {
24
55
  throw new Error("values(...) requires at least one row")
@@ -28,10 +59,14 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
28
59
  ...Record<string, Expression.Any>[]
29
60
  ]
30
61
  const columnNames = Object.keys(normalizedRows[0]!)
62
+ if (columnNames.length === 0) {
63
+ throw new Error("values(...) rows must specify at least one column")
64
+ }
65
+ const columnNameSet = new Set(columnNames)
31
66
  for (const row of normalizedRows) {
32
67
  const rowKeys = Object.keys(row)
33
- if (rowKeys.length !== columnNames.length || !rowKeys.every((key, index) => key === columnNames[index])) {
34
- throw new Error("values(...) rows must project the same columns in the same order")
68
+ if (rowKeys.length !== columnNames.length || !rowKeys.every((key) => columnNameSet.has(key))) {
69
+ throw new Error("values(...) rows must project the same columns")
35
70
  }
36
71
  }
37
72
  return Object.assign(Object.create(ctx.ValuesInputProto), {
@@ -97,8 +132,10 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
97
132
  return Object.assign(source, columns)
98
133
  }
99
134
 
100
- const select = (selection: any) =>
101
- ctx.makePlan({
135
+ const select = (selection: any = {}) => {
136
+ assertSelectionObject("select", selection)
137
+ assertSelectionTree("select", selection)
138
+ return ctx.makePlan({
102
139
  selection,
103
140
  required: ctx.extractRequiredRuntime(selection),
104
141
  available: {},
@@ -112,9 +149,13 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
112
149
  groupBy: [],
113
150
  orderBy: []
114
151
  }, undefined, "read", "select")
152
+ }
115
153
 
116
154
  const groupBy = (...values: readonly Expression.Any[]) =>
117
155
  (plan: any) => {
156
+ if (values.length === 0) {
157
+ throw new Error("groupBy(...) requires at least one expression")
158
+ }
118
159
  const current = plan[Plan.TypeId]
119
160
  const currentAst = ctx.getAst(plan)
120
161
  const currentQuery = ctx.getQueryState(plan)
@@ -132,11 +173,23 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
132
173
  }, currentQuery.assumptions, currentQuery.capabilities, currentQuery.statement)
133
174
  }
134
175
 
135
- const returning = (selection: any) =>
136
- (plan: any) => {
176
+ const returning = (selection: any) => {
177
+ assertSelectionObject("returning", selection)
178
+ assertSelectionTree("returning", selection)
179
+ if (flattenSelection(selection as Record<string, unknown>).length === 0) {
180
+ throw new Error("returning(...) requires at least one selected expression")
181
+ }
182
+ return (plan: any) => {
137
183
  const current = plan[Plan.TypeId]
138
184
  const currentAst = ctx.getAst(plan)
139
185
  const currentQuery = ctx.getQueryState(plan)
186
+ if (
187
+ currentQuery.statement !== "insert" &&
188
+ currentQuery.statement !== "update" &&
189
+ currentQuery.statement !== "delete"
190
+ ) {
191
+ throw new Error(`returning(...) is not supported for ${currentQuery.statement} statements`)
192
+ }
140
193
  return ctx.makePlan({
141
194
  selection,
142
195
  required: [...ctx.currentRequiredList(current.required), ...ctx.extractRequiredRuntime(selection)].filter((name, index, list) =>
@@ -148,6 +201,7 @@ export const makeDslQueryRuntime = (ctx: DslQueryRuntimeContext) => {
148
201
  select: selection
149
202
  }, currentQuery.assumptions, currentQuery.capabilities, currentQuery.statement, currentQuery.target, currentQuery.insertSource)
150
203
  }
204
+ }
151
205
 
152
206
  return {
153
207
  values,
@@ -1,4 +1,5 @@
1
1
  import * as Plan from "./row-set.js"
2
+ import * as Table from "./table.js"
2
3
 
3
4
  type DslTransactionDdlRuntimeContext = {
4
5
  readonly profile: {
@@ -10,9 +11,71 @@ type DslTransactionDdlRuntimeContext = {
10
11
  readonly defaultIndexName: (tableName: string, columns: readonly string[], unique: boolean) => string
11
12
  }
12
13
 
14
+ const allowedIsolationLevels = new Set(["read committed", "repeatable read", "serializable"])
15
+
16
+ export const renderTransactionIsolationLevel = (isolationLevel: unknown): string => {
17
+ if (isolationLevel === undefined) {
18
+ return ""
19
+ }
20
+ if (typeof isolationLevel !== "string" || !allowedIsolationLevels.has(isolationLevel)) {
21
+ throw new Error("Unsupported transaction isolation level")
22
+ }
23
+ return `isolation level ${isolationLevel}`
24
+ }
25
+
26
+ export const expectDdlClauseKind = <
27
+ Ddl extends { readonly kind: string },
28
+ Kind extends Ddl["kind"]
29
+ >(
30
+ ddl: Ddl | undefined,
31
+ kind: Kind
32
+ ): Extract<Ddl, { readonly kind: Kind }> => {
33
+ if (ddl === undefined || ddl.kind !== kind) {
34
+ throw new Error("Unsupported DDL statement kind")
35
+ }
36
+ return ddl as Extract<Ddl, { readonly kind: Kind }>
37
+ }
38
+
39
+ export const expectTruncateClause = <
40
+ Truncate extends { readonly kind: string }
41
+ >(
42
+ truncate: Truncate | undefined
43
+ ): Extract<Truncate, { readonly kind: "truncate" }> => {
44
+ if (truncate === undefined || truncate.kind !== "truncate") {
45
+ throw new Error("Unsupported truncate statement kind")
46
+ }
47
+ return truncate as Extract<Truncate, { readonly kind: "truncate" }>
48
+ }
49
+
50
+ const validateIsolationLevel = (isolationLevel: unknown): void => {
51
+ renderTransactionIsolationLevel(isolationLevel)
52
+ }
53
+
13
54
  export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContext) => {
14
- const transaction = (options: { readonly isolationLevel?: any; readonly readOnly?: boolean } = {}) =>
15
- ctx.makePlan({
55
+ const isRecord = (value: unknown): value is Record<PropertyKey, unknown> =>
56
+ typeof value === "object" && value !== null
57
+
58
+ const assertTableTarget = (target: unknown, apiName: string): void => {
59
+ if (!isRecord(target) || !(Table.TypeId in target) || !(Plan.TypeId in target)) {
60
+ throw new Error(`${apiName}(...) requires a table target`)
61
+ }
62
+ }
63
+
64
+ const validateIndexColumns = (target: any, columns: readonly string[]): void => {
65
+ const fields = target[Table.TypeId]?.fields as Record<string, unknown> | undefined
66
+ if (fields === undefined) {
67
+ return
68
+ }
69
+ for (const columnName of columns) {
70
+ if (!(columnName in fields)) {
71
+ throw new Error(`effect-qb: unknown index column '${columnName}'`)
72
+ }
73
+ }
74
+ }
75
+
76
+ const transaction = (options: { readonly isolationLevel?: any; readonly readOnly?: boolean } = {}) => {
77
+ validateIsolationLevel(options.isolationLevel)
78
+ return ctx.makePlan({
16
79
  selection: {},
17
80
  required: [],
18
81
  available: {},
@@ -31,6 +94,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
31
94
  groupBy: [],
32
95
  orderBy: []
33
96
  }, undefined, "transaction", "transaction")
97
+ }
34
98
 
35
99
  const commit = () =>
36
100
  ctx.makePlan({
@@ -131,6 +195,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
131
195
  }, undefined, "transaction", "releaseSavepoint")
132
196
 
133
197
  const createTable = (target: any, options: { readonly ifNotExists?: boolean } = {}) => {
198
+ assertTableTarget(target, "createTable")
134
199
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
135
200
  return ctx.makePlan({
136
201
  selection: {},
@@ -159,6 +224,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
159
224
  }
160
225
 
161
226
  const dropTable = (target: any, options: { readonly ifExists?: boolean } = {}) => {
227
+ assertTableTarget(target, "dropTable")
162
228
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
163
229
  return ctx.makePlan({
164
230
  selection: {},
@@ -187,7 +253,9 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
187
253
  }
188
254
 
189
255
  const createIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly unique?: boolean; readonly ifNotExists?: boolean } = {}) => {
256
+ assertTableTarget(target, "createIndex")
190
257
  const normalizedColumns = ctx.normalizeColumnList(columns)
258
+ validateIndexColumns(target, normalizedColumns)
191
259
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
192
260
  return ctx.makePlan({
193
261
  selection: {},
@@ -219,7 +287,9 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
219
287
  }
220
288
 
221
289
  const dropIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly ifExists?: boolean } = {}) => {
290
+ assertTableTarget(target, "dropIndex")
222
291
  const normalizedColumns = ctx.normalizeColumnList(columns)
292
+ validateIndexColumns(target, normalizedColumns)
223
293
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
224
294
  return ctx.makePlan({
225
295
  selection: {},
@@ -9,13 +9,15 @@ import * as Stream from "effect/Stream"
9
9
  import * as Expression from "./scalar.js"
10
10
  import * as ExpressionAst from "./expression-ast.js"
11
11
  import { resolveImplicationScope, type ImplicationScope } from "./implication-runtime.js"
12
- import { normalizeDbValue } from "./runtime/normalize.js"
12
+ import { fromDriverValue } from "./runtime/driver-value-mapping.js"
13
13
  import { expressionRuntimeSchema } from "./runtime/schema.js"
14
14
  import { flattenSelection } from "./projections.js"
15
15
  import * as Query from "./query.js"
16
16
  import * as QueryAst from "./query-ast.js"
17
17
  import * as Renderer from "./renderer.js"
18
18
  import * as Plan from "./row-set.js"
19
+ import { columnPredicateKey } from "./predicate/runtime.js"
20
+ import { isJsonValue } from "./runtime/normalize.js"
19
21
 
20
22
  /** Flat database row keyed by rendered projection aliases. */
21
23
  export type FlatRow = Readonly<Record<string, unknown>>
@@ -221,7 +223,7 @@ const effectiveRuntimeNullability = (
221
223
  return "always"
222
224
  }
223
225
  if (ast.kind === "column") {
224
- const key = `${ast.tableName}.${ast.columnName}`
226
+ const key = columnPredicateKey(ast.tableName, ast.columnName)
225
227
  if (scope.absentSourceNames.has(ast.tableName) || scope.nullKeys.has(key)) {
226
228
  return "always"
227
229
  }
@@ -237,26 +239,51 @@ const effectiveRuntimeNullability = (
237
239
  : nullability
238
240
  }
239
241
 
242
+ const dbTypeAllowsTopLevelJsonNull = (
243
+ dbType: Expression.DbType.Any
244
+ ): boolean => {
245
+ if ("base" in dbType) {
246
+ return dbTypeAllowsTopLevelJsonNull(dbType.base)
247
+ }
248
+ return ("variant" in dbType && dbType.variant === "json") || dbType.runtime === "json"
249
+ }
250
+
251
+ const schemaAcceptsNull = (
252
+ schema: Schema.Schema.Any | undefined
253
+ ): boolean =>
254
+ schema !== undefined && (Schema.is(schema) as (value: unknown) => boolean)(null)
255
+
240
256
  const decodeProjectionValue = (
241
257
  rendered: Renderer.RenderedQuery<any, any>,
242
258
  projection: Renderer.RenderedQuery<any, any>["projections"][number],
243
259
  expression: Expression.Any,
244
260
  raw: unknown,
245
261
  scope: ImplicationScope,
246
- driverMode: DriverMode
262
+ driverMode: DriverMode,
263
+ valueMappings?: Expression.DriverValueMappings
247
264
  ): unknown => {
248
265
  let normalized = raw
249
266
  if (driverMode === "raw") {
250
267
  try {
251
- normalized = normalizeDbValue(expression[Expression.TypeId].dbType, raw)
268
+ normalized = fromDriverValue(raw, {
269
+ dialect: rendered.dialect,
270
+ dbType: expression[Expression.TypeId].dbType,
271
+ runtimeSchema: expression[Expression.TypeId].runtimeSchema,
272
+ driverValueMapping: expression[Expression.TypeId].driverValueMapping,
273
+ valueMappings
274
+ })
252
275
  } catch (cause) {
253
276
  throw makeRowDecodeError(rendered, projection, expression, raw, "normalize", cause)
254
277
  }
255
278
  }
256
279
 
257
280
  const nullability = effectiveRuntimeNullability(expression, scope)
281
+ const schema = expressionRuntimeSchema(expression, { assumptions: scope.assumptions })
258
282
  if (normalized === null) {
259
283
  if (nullability === "never") {
284
+ if (dbTypeAllowsTopLevelJsonNull(expression[Expression.TypeId].dbType) && schemaAcceptsNull(schema)) {
285
+ return null
286
+ }
260
287
  throw makeRowDecodeError(
261
288
  rendered,
262
289
  projection,
@@ -282,7 +309,18 @@ const decodeProjectionValue = (
282
309
  )
283
310
  }
284
311
 
285
- const schema = expressionRuntimeSchema(expression, { assumptions: scope.assumptions })
312
+ if (dbTypeAllowsTopLevelJsonNull(expression[Expression.TypeId].dbType) && !isJsonValue(normalized)) {
313
+ throw makeRowDecodeError(
314
+ rendered,
315
+ projection,
316
+ expression,
317
+ raw,
318
+ "schema",
319
+ new Error("Expected a JSON value"),
320
+ normalized
321
+ )
322
+ }
323
+
286
324
  if (schema === undefined) {
287
325
  return normalized
288
326
  }
@@ -303,30 +341,39 @@ export const makeRowDecoder = (
303
341
  plan: Query.Plan.Any,
304
342
  options: {
305
343
  readonly driverMode?: DriverMode
344
+ readonly valueMappings?: Expression.DriverValueMappings
306
345
  } = {}
307
346
  ): ((row: FlatRow) => any) => {
308
347
  const projections = flattenSelection(
309
348
  Query.getAst(plan).select as Record<string, unknown>
310
349
  )
311
- const byAlias = new Map(
312
- projections.map((projection) => [projection.alias, projection.expression] as const)
350
+ const byPath = new Map(
351
+ projections.map((projection) => [JSON.stringify(projection.path), projection.expression] as const)
313
352
  )
314
353
  const driverMode = options.driverMode ?? "raw"
354
+ const valueMappings = options.valueMappings ?? rendered.valueMappings
315
355
  const scope = resolveImplicationScope(plan[Plan.TypeId].available, Query.getQueryState(plan).assumptions)
316
356
  return (row) => {
317
357
  const decoded: Record<string, unknown> = {}
318
358
  for (const projection of rendered.projections) {
319
- if (!(projection.alias in row)) {
320
- continue
321
- }
322
- const expression = byAlias.get(projection.alias)
359
+ const expression = byPath.get(JSON.stringify(projection.path))
323
360
  if (expression === undefined) {
324
- continue
361
+ throw new Error(`Rendered projection path '${projection.path.join(".")}' does not exist in the query selection`)
362
+ }
363
+ if (!(projection.alias in row)) {
364
+ throw makeRowDecodeError(
365
+ rendered,
366
+ projection,
367
+ expression,
368
+ undefined,
369
+ "schema",
370
+ new Error(`Missing required projection alias '${projection.alias}'`)
371
+ )
325
372
  }
326
373
  setPath(
327
374
  decoded,
328
375
  projection.path,
329
- decodeProjectionValue(rendered, projection, expression, row[projection.alias], scope, driverMode)
376
+ decodeProjectionValue(rendered, projection, expression, row[projection.alias], scope, driverMode, valueMappings)
330
377
  )
331
378
  }
332
379
  return decoded
@@ -339,6 +386,7 @@ export const decodeChunk = (
339
386
  rows: Chunk.Chunk<FlatRow>,
340
387
  options: {
341
388
  readonly driverMode?: DriverMode
389
+ readonly valueMappings?: Expression.DriverValueMappings
342
390
  } = {}
343
391
  ): Chunk.Chunk<any> => {
344
392
  const decodeRow = makeRowDecoder(rendered, plan, options)
@@ -351,6 +399,7 @@ export const decodeRows = (
351
399
  rows: ReadonlyArray<FlatRow>,
352
400
  options: {
353
401
  readonly driverMode?: DriverMode
402
+ readonly valueMappings?: Expression.DriverValueMappings
354
403
  } = {}
355
404
  ): ReadonlyArray<any> => {
356
405
  const decodeRow = makeRowDecoder(rendered, plan, options)
@@ -36,11 +36,12 @@ export interface CastNode<
36
36
 
37
37
  /** Explicit collation captured by the internal expression AST. */
38
38
  export interface CollateNode<
39
- Value extends Expression.Any = Expression.Any
39
+ Value extends Expression.Any = Expression.Any,
40
+ Collation extends readonly [string, ...string[]] = readonly [string, ...string[]]
40
41
  > {
41
42
  readonly kind: "collate"
42
43
  readonly value: Value
43
- readonly collation: readonly [string, ...string[]]
44
+ readonly collation: Collation
44
45
  }
45
46
 
46
47
  /** General SQL function call captured by the internal expression AST. */