effect-qb 0.12.3 → 0.14.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 (65) hide show
  1. package/README.md +6 -1283
  2. package/dist/mysql.js +6376 -4978
  3. package/dist/postgres/metadata.js +2724 -0
  4. package/dist/postgres.js +5475 -3636
  5. package/package.json +13 -8
  6. package/src/internal/column-state.ts +88 -6
  7. package/src/internal/column.ts +569 -34
  8. package/src/internal/datatypes/define.ts +0 -30
  9. package/src/internal/executor.ts +45 -11
  10. package/src/internal/expression-ast.ts +15 -0
  11. package/src/internal/expression.ts +3 -1
  12. package/src/internal/implication-runtime.ts +171 -0
  13. package/src/internal/mysql-query.ts +7173 -0
  14. package/src/internal/mysql-renderer.ts +2 -2
  15. package/src/internal/plan.ts +14 -4
  16. package/src/internal/{query-factory.ts → postgres-query.ts} +669 -230
  17. package/src/internal/postgres-renderer.ts +2 -2
  18. package/src/internal/postgres-schema-model.ts +144 -0
  19. package/src/internal/predicate-analysis.ts +10 -0
  20. package/src/internal/predicate-context.ts +112 -36
  21. package/src/internal/predicate-formula.ts +31 -19
  22. package/src/internal/predicate-normalize.ts +177 -106
  23. package/src/internal/predicate-runtime.ts +676 -0
  24. package/src/internal/query.ts +471 -41
  25. package/src/internal/renderer.ts +2 -2
  26. package/src/internal/runtime-schema.ts +74 -20
  27. package/src/internal/schema-ddl.ts +55 -0
  28. package/src/internal/schema-derivation.ts +93 -21
  29. package/src/internal/schema-expression.ts +44 -0
  30. package/src/internal/sql-expression-renderer.ts +123 -35
  31. package/src/internal/table-options.ts +88 -7
  32. package/src/internal/table.ts +106 -42
  33. package/src/mysql/column.ts +3 -1
  34. package/src/mysql/datatypes/index.ts +17 -2
  35. package/src/mysql/executor.ts +20 -17
  36. package/src/mysql/function/aggregate.ts +6 -0
  37. package/src/mysql/function/core.ts +5 -0
  38. package/src/mysql/function/index.ts +20 -0
  39. package/src/mysql/function/json.ts +4 -0
  40. package/src/mysql/function/string.ts +6 -0
  41. package/src/mysql/function/temporal.ts +103 -0
  42. package/src/mysql/function/window.ts +7 -0
  43. package/src/mysql/private/query.ts +1 -0
  44. package/src/mysql/query.ts +6 -26
  45. package/src/mysql.ts +2 -0
  46. package/src/postgres/cast.ts +31 -0
  47. package/src/postgres/column.ts +27 -1
  48. package/src/postgres/datatypes/index.ts +40 -5
  49. package/src/postgres/executor.ts +19 -17
  50. package/src/postgres/function/aggregate.ts +6 -0
  51. package/src/postgres/function/core.ts +16 -0
  52. package/src/postgres/function/index.ts +20 -0
  53. package/src/postgres/function/json.ts +501 -0
  54. package/src/postgres/function/string.ts +6 -0
  55. package/src/postgres/function/temporal.ts +107 -0
  56. package/src/postgres/function/window.ts +7 -0
  57. package/src/postgres/metadata.ts +31 -0
  58. package/src/postgres/private/query.ts +1 -0
  59. package/src/postgres/query.ts +6 -28
  60. package/src/postgres/schema-expression.ts +16 -0
  61. package/src/postgres/schema-management.ts +204 -0
  62. package/src/postgres/schema.ts +35 -0
  63. package/src/postgres/table.ts +307 -41
  64. package/src/postgres/type.ts +4 -0
  65. package/src/postgres.ts +16 -0
@@ -12,33 +12,3 @@ export type DatatypeModule<
12
12
  } & {
13
13
  readonly [Alias in keyof Aliases]: () => Expression.DbType.Base<Dialect, Aliases[Alias] & string>
14
14
  }
15
-
16
- export const makeDatatypeModule = <
17
- Dialect extends string,
18
- Kinds extends Record<string, DatatypeKindSpec>,
19
- Aliases extends Record<string, string> = Record<never, never>
20
- >(
21
- dialect: Dialect,
22
- kinds: Kinds,
23
- aliases?: Aliases
24
- ): DatatypeModule<Dialect, Kinds, Aliases> => {
25
- const module: Record<string, (...args: readonly any[]) => Expression.DbType.Base<Dialect, string>> = {
26
- custom: (kind: string) => ({
27
- dialect,
28
- kind
29
- })
30
- }
31
- for (const kind of Object.keys(kinds)) {
32
- module[kind] = () => ({
33
- dialect,
34
- kind
35
- })
36
- }
37
- for (const [alias, kind] of Object.entries(aliases ?? {})) {
38
- module[alias] = () => ({
39
- dialect,
40
- kind
41
- })
42
- }
43
- return module as DatatypeModule<Dialect, Kinds, Aliases>
44
- }
@@ -4,6 +4,8 @@ import * as SqlClient from "@effect/sql/SqlClient"
4
4
  import * as SqlError from "@effect/sql/SqlError"
5
5
 
6
6
  import * as Expression from "./expression.js"
7
+ import * as ExpressionAst from "./expression-ast.js"
8
+ import { resolveImplicationScope, type ImplicationScope } from "./implication-runtime.js"
7
9
  import { normalizeDbValue } from "./runtime-normalize.js"
8
10
  import { expressionRuntimeSchema } from "./runtime-schema.js"
9
11
  import { flattenSelection } from "./projections.js"
@@ -16,6 +18,10 @@ import * as Plan from "./plan.js"
16
18
  export type FlatRow = Readonly<Record<string, unknown>>
17
19
  export type DriverMode = "raw" | "normalized"
18
20
 
21
+ type AstBackedExpression = Expression.Any & {
22
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
23
+ }
24
+
19
25
  export interface RowDecodeError {
20
26
  readonly _tag: "RowDecodeError"
21
27
  readonly message: string
@@ -67,7 +73,7 @@ export interface Executor<
67
73
  Context = never
68
74
  > {
69
75
  readonly dialect: Dialect
70
- execute<PlanValue extends Query.QueryPlan<any, any, any, any, any, any, any, any, any>>(
76
+ execute<PlanValue extends Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>>(
71
77
  plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
72
78
  ): Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
73
79
  }
@@ -189,24 +195,39 @@ const makeRowDecodeError = (
189
195
 
190
196
  const hasOptionalSourceDependency = (
191
197
  expression: Expression.Any,
192
- available: Readonly<Record<string, Plan.Source>>
198
+ scope: ImplicationScope
193
199
  ): boolean => {
194
200
  const state = expression[Expression.TypeId]
195
201
  if (state.sourceNullability === "resolved") {
196
202
  return false
197
203
  }
198
- return Object.keys(state.dependencies).some((sourceName) => available[sourceName]?.mode === "optional")
204
+ return Object.keys(state.dependencies).some((sourceName) =>
205
+ !scope.absentSourceNames.has(sourceName) && scope.sourceModes.get(sourceName) === "optional")
199
206
  }
200
207
 
201
208
  const effectiveRuntimeNullability = (
202
209
  expression: Expression.Any,
203
- available: Readonly<Record<string, Plan.Source>>
210
+ scope: ImplicationScope
204
211
  ): Expression.Nullability => {
205
212
  const nullability = expression[Expression.TypeId].nullability
213
+ const ast = (expression as AstBackedExpression)[ExpressionAst.TypeId]
206
214
  if (nullability === "always") {
207
215
  return "always"
208
216
  }
209
- return hasOptionalSourceDependency(expression, available)
217
+ if (ast.kind === "column") {
218
+ const key = `${ast.tableName}.${ast.columnName}`
219
+ if (scope.absentSourceNames.has(ast.tableName) || scope.nullKeys.has(key)) {
220
+ return "always"
221
+ }
222
+ if (scope.nonNullKeys.has(key)) {
223
+ return "never"
224
+ }
225
+ }
226
+ if (expression[Expression.TypeId].sourceNullability !== "resolved" &&
227
+ Object.keys(expression[Expression.TypeId].dependencies).some((sourceName) => scope.absentSourceNames.has(sourceName))) {
228
+ return "always"
229
+ }
230
+ return hasOptionalSourceDependency(expression, scope)
210
231
  ? "maybe"
211
232
  : nullability
212
233
  }
@@ -216,7 +237,7 @@ const decodeProjectionValue = (
216
237
  projection: Renderer.RenderedQuery<any, any>["projections"][number],
217
238
  expression: Expression.Any,
218
239
  raw: unknown,
219
- available: Readonly<Record<string, Plan.Source>>,
240
+ scope: ImplicationScope,
220
241
  driverMode: DriverMode
221
242
  ): unknown => {
222
243
  let normalized = raw
@@ -228,8 +249,9 @@ const decodeProjectionValue = (
228
249
  }
229
250
  }
230
251
 
252
+ const nullability = effectiveRuntimeNullability(expression, scope)
231
253
  if (normalized === null) {
232
- if (effectiveRuntimeNullability(expression, available) === "never") {
254
+ if (nullability === "never") {
233
255
  throw makeRowDecodeError(
234
256
  rendered,
235
257
  projection,
@@ -243,7 +265,19 @@ const decodeProjectionValue = (
243
265
  return null
244
266
  }
245
267
 
246
- const schema = expressionRuntimeSchema(expression)
268
+ if (nullability === "always") {
269
+ throw makeRowDecodeError(
270
+ rendered,
271
+ projection,
272
+ expression,
273
+ raw,
274
+ "schema",
275
+ new Error("Received non-null for an always-null projection"),
276
+ normalized
277
+ )
278
+ }
279
+
280
+ const schema = expressionRuntimeSchema(expression, { assumptions: scope.assumptions })
247
281
  if (schema === undefined) {
248
282
  return normalized
249
283
  }
@@ -274,7 +308,7 @@ export const decodeRows = (
274
308
  projections.map((projection) => [projection.alias, projection.expression] as const)
275
309
  )
276
310
  const driverMode = options.driverMode ?? "raw"
277
- const available = plan[Plan.TypeId].available
311
+ const scope = resolveImplicationScope(plan[Plan.TypeId].available, Query.getQueryState(plan).assumptions)
278
312
  return rows.map((row) => {
279
313
  const decoded: Record<string, unknown> = {}
280
314
  for (const projection of rendered.projections) {
@@ -288,7 +322,7 @@ export const decodeRows = (
288
322
  setPath(
289
323
  decoded,
290
324
  projection.path,
291
- decodeProjectionValue(rendered, projection, expression, row[projection.alias], available, driverMode)
325
+ decodeProjectionValue(rendered, projection, expression, row[projection.alias], scope, driverMode)
292
326
  )
293
327
  }
294
328
  return decoded
@@ -304,7 +338,7 @@ export const make = <
304
338
  Context = never
305
339
  >(
306
340
  dialect: Dialect,
307
- execute: <PlanValue extends Query.QueryPlan<any, any, any, any, any, any, any, any, any>>(
341
+ execute: <PlanValue extends Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>>(
308
342
  plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
309
343
  ) => Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
310
344
  ): Executor<Dialect, Error, Context> => ({
@@ -34,6 +34,16 @@ export interface CastNode<
34
34
  readonly target: Target
35
35
  }
36
36
 
37
+ /** General SQL function call captured by the internal expression AST. */
38
+ export interface FunctionCallNode<
39
+ Name extends string = string,
40
+ Args extends readonly Expression.Any[] = readonly Expression.Any[]
41
+ > {
42
+ readonly kind: "function"
43
+ readonly name: Name
44
+ readonly args: Args
45
+ }
46
+
37
47
  /** `excluded.column` reference used inside insert conflict handlers. */
38
48
  export interface ExcludedNode<
39
49
  ColumnName extends string = string
@@ -72,6 +82,10 @@ export type BinaryKind =
72
82
  | "gte"
73
83
  | "like"
74
84
  | "ilike"
85
+ | "regexMatch"
86
+ | "regexIMatch"
87
+ | "regexNotMatch"
88
+ | "regexNotIMatch"
75
89
  | "isDistinctFrom"
76
90
  | "isNotDistinctFrom"
77
91
  | "contains"
@@ -325,6 +339,7 @@ export type Any =
325
339
  | ColumnNode
326
340
  | LiteralNode
327
341
  | CastNode
342
+ | FunctionCallNode
328
343
  | ExcludedNode
329
344
  | UnaryNode
330
345
  | BinaryNode
@@ -89,7 +89,7 @@ export declare namespace DbType {
89
89
  SchemaName extends string = "json"
90
90
  > extends Base<Dialect, SchemaName>
91
91
  {
92
- readonly variant: "json"
92
+ readonly variant: SchemaName extends "jsonb" ? "jsonb" : "json"
93
93
  }
94
94
 
95
95
  /** Array database type. */
@@ -168,6 +168,8 @@ export declare namespace DbType {
168
168
  export type PgDate = Base<"postgres", "date">
169
169
  export type PgTime = Base<"postgres", "time">
170
170
  export type PgTimestamp = Base<"postgres", "timestamp">
171
+ export type PgTimetz = Base<"postgres", "timetz">
172
+ export type PgTimestamptz = Base<"postgres", "timestamptz">
171
173
  export type PgInterval = Base<"postgres", "interval">
172
174
  export type PgBytea = Base<"postgres", "bytea">
173
175
  export type PgJsonb = Base<"postgres", "jsonb">
@@ -0,0 +1,171 @@
1
+ import * as Expression from "./expression.js"
2
+ import * as ExpressionAst from "./expression-ast.js"
3
+ import * as Plan from "./plan.js"
4
+ import * as Table from "./table.js"
5
+ import type { PredicateFormula } from "./predicate-formula.js"
6
+ import {
7
+ assumeFormulaTrue,
8
+ contradictsFormula,
9
+ guaranteedNonNullKeys,
10
+ guaranteedNullKeys,
11
+ guaranteedSourceNames,
12
+ trueFormula
13
+ } from "./predicate-runtime.js"
14
+ import type { SourceLike } from "./query.js"
15
+
16
+ export interface ImplicationScope {
17
+ readonly assumptions: PredicateFormula
18
+ readonly nonNullKeys: ReadonlySet<string>
19
+ readonly nullKeys: ReadonlySet<string>
20
+ readonly requiredSourceNames: ReadonlySet<string>
21
+ readonly absentSourceNames: ReadonlySet<string>
22
+ readonly sourceModes: ReadonlyMap<string, Plan.SourceMode>
23
+ }
24
+
25
+ type AstBackedExpression = Expression.Any & {
26
+ readonly [ExpressionAst.TypeId]: ExpressionAst.Any
27
+ }
28
+
29
+ export const presentFormulaOfSource = (source: Plan.Source): PredicateFormula =>
30
+ source._presentFormula ?? trueFormula()
31
+
32
+ export const presenceWitnessesOfSource = (source: Plan.Source): ReadonlySet<string> =>
33
+ new Set(source._presenceWitnesses ?? [])
34
+
35
+ const collectPresenceWitnesses = (
36
+ selection: unknown,
37
+ output: Set<string>
38
+ ): void => {
39
+ if (typeof selection !== "object" || selection === null) {
40
+ return
41
+ }
42
+ if (Expression.TypeId in selection && ExpressionAst.TypeId in selection) {
43
+ const expression = selection as unknown as AstBackedExpression
44
+ const ast = expression[ExpressionAst.TypeId]
45
+ if (ast.kind === "column" && expression[Expression.TypeId].nullability === "never") {
46
+ output.add(`${ast.tableName}.${ast.columnName}`)
47
+ }
48
+ return
49
+ }
50
+ for (const value of Object.values(selection as Record<string, unknown>)) {
51
+ collectPresenceWitnesses(value, output)
52
+ }
53
+ }
54
+
55
+ export const presenceWitnessesOfSourceLike = (source: SourceLike): readonly string[] => {
56
+ const output = new Set<string>()
57
+ if (typeof source !== "object" || source === null) {
58
+ return []
59
+ }
60
+ if (Table.TypeId in source) {
61
+ collectPresenceWitnesses((source as Plan.Any)[Plan.TypeId].selection, output)
62
+ return [...output]
63
+ }
64
+ if ("columns" in source) {
65
+ collectPresenceWitnesses((source as { readonly columns: unknown }).columns, output)
66
+ }
67
+ return [...output]
68
+ }
69
+
70
+ const directAbsentSourceNames = (
71
+ available: Readonly<Record<string, Plan.Source>>,
72
+ assumptions: PredicateFormula
73
+ ): Set<string> => {
74
+ const nullKeys = guaranteedNullKeys(assumptions)
75
+ const absent = new Set<string>()
76
+ for (const [name, source] of Object.entries(available)) {
77
+ if (source._presenceWitnesses?.some((key) => nullKeys.has(key))) {
78
+ absent.add(name)
79
+ continue
80
+ }
81
+ if (contradictsFormula(assumptions, presentFormulaOfSource(source))) {
82
+ absent.add(name)
83
+ }
84
+ }
85
+ return absent
86
+ }
87
+
88
+ const propagateAbsentSourceNames = (
89
+ available: Readonly<Record<string, Plan.Source>>,
90
+ seed: ReadonlySet<string>
91
+ ): Set<string> => {
92
+ const absent = new Set(seed)
93
+ let changed = true
94
+ while (changed) {
95
+ changed = false
96
+ for (const [name, source] of Object.entries(available)) {
97
+ if (absent.has(name)) {
98
+ continue
99
+ }
100
+ const required = guaranteedSourceNames(presentFormulaOfSource(source))
101
+ if (Array.from(required).some((dependency) => absent.has(dependency))) {
102
+ absent.add(name)
103
+ changed = true
104
+ }
105
+ }
106
+ }
107
+ return absent
108
+ }
109
+
110
+ export const resolveImplicationScope = (
111
+ available: Readonly<Record<string, Plan.Source>>,
112
+ initialAssumptions: PredicateFormula
113
+ ): ImplicationScope => {
114
+ let assumptions = initialAssumptions
115
+ const required = new Set<string>(
116
+ Object.entries(available)
117
+ .filter(([, source]) => source.mode === "required")
118
+ .map(([name]) => name)
119
+ )
120
+ const appliedRequired = new Set<string>()
121
+ let absent = new Set<string>()
122
+
123
+ let changed = true
124
+ while (changed) {
125
+ changed = false
126
+
127
+ for (const name of guaranteedSourceNames(assumptions)) {
128
+ if (!required.has(name)) {
129
+ required.add(name)
130
+ changed = true
131
+ }
132
+ }
133
+
134
+ for (const name of required) {
135
+ if (absent.has(name) || appliedRequired.has(name)) {
136
+ continue
137
+ }
138
+ const source = available[name]
139
+ if (source === undefined) {
140
+ continue
141
+ }
142
+ assumptions = assumeFormulaTrue(assumptions, presentFormulaOfSource(source))
143
+ appliedRequired.add(name)
144
+ changed = true
145
+ }
146
+
147
+ const nextAbsent = propagateAbsentSourceNames(available, directAbsentSourceNames(available, assumptions))
148
+ if (nextAbsent.size !== absent.size || Array.from(nextAbsent).some((name) => !absent.has(name))) {
149
+ absent = nextAbsent
150
+ changed = true
151
+ }
152
+ }
153
+
154
+ for (const name of absent) {
155
+ required.delete(name)
156
+ }
157
+
158
+ const sourceModes = new Map<string, Plan.SourceMode>()
159
+ for (const [name, source] of Object.entries(available)) {
160
+ sourceModes.set(name, required.has(name) ? "required" : source.mode)
161
+ }
162
+
163
+ return {
164
+ assumptions,
165
+ nonNullKeys: guaranteedNonNullKeys(assumptions),
166
+ nullKeys: guaranteedNullKeys(assumptions),
167
+ requiredSourceNames: required,
168
+ absentSourceNames: absent,
169
+ sourceModes
170
+ }
171
+ }