effect-qb 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +663 -261
- package/dist/mysql.js +2840 -492
- package/dist/postgres/metadata.js +113 -14
- package/dist/postgres.js +2596 -285
- package/dist/sqlite.js +2783 -450
- package/dist/standard.js +663 -261
- package/package.json +1 -1
- package/src/internal/analytics.ts +264 -0
- package/src/internal/coercion/errors.d.ts +1 -1
- package/src/internal/coercion/errors.ts +1 -1
- package/src/internal/custom-sql-renderer.ts +20 -0
- package/src/internal/dialect-numeric.ts +332 -0
- package/src/internal/dialect-renderers/mysql.ts +68 -13
- package/src/internal/dialect-renderers/postgres.ts +83 -13
- package/src/internal/dialect-renderers/sqlite.ts +57 -13
- package/src/internal/dynamic.ts +131 -0
- package/src/internal/executor.ts +198 -7
- package/src/internal/expression-ast.ts +62 -1
- package/src/internal/fragment.ts +120 -0
- package/src/internal/function-constraints.ts +105 -0
- package/src/internal/grouping-key.ts +26 -0
- package/src/internal/numeric.ts +204 -0
- package/src/internal/query.ts +13 -1
- package/src/internal/runtime/normalize.ts +5 -2
- package/src/internal/runtime/schema.ts +2 -1
- package/src/internal/standard-dsl.ts +43 -20
- package/src/internal/window-frame.ts +30 -0
- package/src/internal/window-renderer.ts +25 -0
- package/src/mysql/executor.ts +126 -45
- package/src/mysql/function/aggregate.ts +87 -1
- package/src/mysql/function/index.ts +5 -2
- package/src/mysql/function/numeric.ts +185 -0
- package/src/mysql/function/temporal.ts +6 -6
- package/src/mysql/function/window.ts +12 -0
- package/src/mysql/internal/dsl.ts +38 -20
- package/src/mysql.ts +2 -0
- package/src/postgres/executor.ts +111 -45
- package/src/postgres/function/aggregate.ts +124 -1
- package/src/postgres/function/core.ts +2 -1
- package/src/postgres/function/index.ts +19 -1
- package/src/postgres/function/numeric.ts +246 -0
- package/src/postgres/function/window.ts +12 -0
- package/src/postgres/internal/dsl.ts +38 -20
- package/src/sqlite/executor.ts +96 -45
- package/src/sqlite/function/aggregate.ts +90 -1
- package/src/sqlite/function/index.ts +5 -2
- package/src/sqlite/function/numeric.ts +139 -0
- package/src/sqlite/function/window.ts +12 -0
- package/src/sqlite/internal/dsl.ts +38 -20
- package/src/sqlite.ts +2 -0
- package/src/standard/function/core.ts +8 -1
- package/src/standard/function/index.ts +18 -11
- package/src/standard/function/string.ts +1 -1
- package/src/standard/function/window.ts +10 -1
- package/src/standard/query.ts +19 -7
- package/src/standard/type.ts +16 -0
- package/src/standard.ts +4 -0
- package/src/standard/function/temporal.ts +0 -78
package/src/internal/executor.ts
CHANGED
|
@@ -24,6 +24,39 @@ import { isJsonValue } from "./runtime/normalize.js"
|
|
|
24
24
|
export type FlatRow = Readonly<Record<string, unknown>>
|
|
25
25
|
export type DriverMode = "raw" | "normalized"
|
|
26
26
|
|
|
27
|
+
/** Driver-level result metadata retained alongside returned rows. */
|
|
28
|
+
export interface DriverResult {
|
|
29
|
+
readonly rows: ReadonlyArray<FlatRow>
|
|
30
|
+
readonly affectedRows?: number
|
|
31
|
+
readonly insertId?: string | number | bigint
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Decoded execution result plus portable mutation metadata when available. */
|
|
35
|
+
export interface ExecutionResult<Row> {
|
|
36
|
+
readonly rows: ReadonlyArray<Row>
|
|
37
|
+
readonly affectedRows?: number
|
|
38
|
+
readonly insertId?: string | number | bigint
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Failure raised when an execution result violates a requested cardinality. */
|
|
42
|
+
export interface ResultCardinalityError {
|
|
43
|
+
readonly _tag: "ResultCardinalityError"
|
|
44
|
+
readonly expected: "zeroOrOne" | "exactlyOne" | "nonEmpty"
|
|
45
|
+
readonly actual: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Reusable execution handle for one immutable query plan. */
|
|
49
|
+
export interface PreparedQuery<Row, Error = never, Context = never> {
|
|
50
|
+
readonly execute: Effect.Effect<ReadonlyArray<Row>, Error, Context>
|
|
51
|
+
readonly executeResult: Effect.Effect<ExecutionResult<Row>, Error, Context>
|
|
52
|
+
readonly stream: Stream.Stream<Row, Error, Context>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ExplainOptions {
|
|
56
|
+
readonly analyze?: boolean
|
|
57
|
+
readonly format?: "text" | "json"
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
type AstBackedExpression = Expression.Any & {
|
|
28
61
|
readonly [ExpressionAst.TypeId]: ExpressionAst.Any
|
|
29
62
|
}
|
|
@@ -68,6 +101,9 @@ export interface Driver<
|
|
|
68
101
|
execute<Row>(
|
|
69
102
|
query: Renderer.RenderedQuery<Row, Dialect>
|
|
70
103
|
): Effect.Effect<ReadonlyArray<FlatRow>, Error, Context>
|
|
104
|
+
executeResult?<Row>(
|
|
105
|
+
query: Renderer.RenderedQuery<Row, Dialect>
|
|
106
|
+
): Effect.Effect<DriverResult, Error, Context>
|
|
71
107
|
stream<Row>(
|
|
72
108
|
query: Renderer.RenderedQuery<Row, Dialect>
|
|
73
109
|
): Stream.Stream<FlatRow, Error, Context>
|
|
@@ -89,11 +125,91 @@ export interface Executor<
|
|
|
89
125
|
execute<PlanValue extends Query.Plan.Any>(
|
|
90
126
|
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
91
127
|
): Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
|
|
128
|
+
executeResult<PlanValue extends Query.Plan.Any>(
|
|
129
|
+
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
130
|
+
): Effect.Effect<ExecutionResult<Query.ResultRow<PlanValue>>, Error, Context>
|
|
131
|
+
prepare<PlanValue extends Query.Plan.Any>(
|
|
132
|
+
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
133
|
+
): PreparedQuery<Query.ResultRow<PlanValue>, Error, Context>
|
|
92
134
|
stream<PlanValue extends Query.Plan.Any>(
|
|
93
135
|
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
94
136
|
): Stream.Stream<Query.ResultRow<PlanValue>, Error, Context>
|
|
95
137
|
}
|
|
96
138
|
|
|
139
|
+
type ExecutorBase<
|
|
140
|
+
Dialect extends string,
|
|
141
|
+
Error,
|
|
142
|
+
Context
|
|
143
|
+
> = Pick<Executor<Dialect, Error, Context>, "dialect" | "execute" | "stream"> & {
|
|
144
|
+
readonly executeResult?: Executor<Dialect, Error, Context>["executeResult"]
|
|
145
|
+
readonly explain?: (
|
|
146
|
+
plan: Query.Plan.Any,
|
|
147
|
+
options?: ExplainOptions
|
|
148
|
+
) => Effect.Effect<ReadonlyArray<FlatRow>, Error, Context>
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Requires an execution effect to contain at most one row. */
|
|
152
|
+
export const atMostOne = <Row, Error, Context>(
|
|
153
|
+
self: Effect.Effect<ReadonlyArray<Row>, Error, Context>
|
|
154
|
+
): Effect.Effect<Option.Option<Row>, Error | ResultCardinalityError, Context> =>
|
|
155
|
+
Effect.flatMap(self, (rows) =>
|
|
156
|
+
rows.length <= 1
|
|
157
|
+
? Effect.succeed(rows.length === 0 ? Option.none() : Option.some(rows[0]!))
|
|
158
|
+
: Effect.fail({
|
|
159
|
+
_tag: "ResultCardinalityError",
|
|
160
|
+
expected: "zeroOrOne",
|
|
161
|
+
actual: rows.length
|
|
162
|
+
} satisfies ResultCardinalityError))
|
|
163
|
+
|
|
164
|
+
/** Requires an execution effect to contain exactly one row. */
|
|
165
|
+
export const exactlyOne = <Row, Error, Context>(
|
|
166
|
+
self: Effect.Effect<ReadonlyArray<Row>, Error, Context>
|
|
167
|
+
): Effect.Effect<Row, Error | ResultCardinalityError, Context> =>
|
|
168
|
+
Effect.flatMap(self, (rows) =>
|
|
169
|
+
rows.length === 1
|
|
170
|
+
? Effect.succeed(rows[0]!)
|
|
171
|
+
: Effect.fail({
|
|
172
|
+
_tag: "ResultCardinalityError",
|
|
173
|
+
expected: "exactlyOne",
|
|
174
|
+
actual: rows.length
|
|
175
|
+
} satisfies ResultCardinalityError))
|
|
176
|
+
|
|
177
|
+
/** Requires an execution effect to contain at least one row. */
|
|
178
|
+
export const nonEmpty = <Row, Error, Context>(
|
|
179
|
+
self: Effect.Effect<ReadonlyArray<Row>, Error, Context>
|
|
180
|
+
): Effect.Effect<readonly [Row, ...Row[]], Error | ResultCardinalityError, Context> =>
|
|
181
|
+
Effect.flatMap(self, (rows) =>
|
|
182
|
+
rows.length > 0
|
|
183
|
+
? Effect.succeed(rows as readonly [Row, ...Row[]])
|
|
184
|
+
: Effect.fail({
|
|
185
|
+
_tag: "ResultCardinalityError",
|
|
186
|
+
expected: "nonEmpty",
|
|
187
|
+
actual: 0
|
|
188
|
+
} satisfies ResultCardinalityError))
|
|
189
|
+
|
|
190
|
+
/** Adds the standard result/cardinality contract to an executor implementation. */
|
|
191
|
+
export const withResultContracts = <
|
|
192
|
+
Dialect extends string,
|
|
193
|
+
Error,
|
|
194
|
+
Context
|
|
195
|
+
>(
|
|
196
|
+
base: ExecutorBase<Dialect, Error, Context>
|
|
197
|
+
): Executor<Dialect, Error, Context> => {
|
|
198
|
+
const executeResult = base.executeResult ?? ((plan) =>
|
|
199
|
+
Effect.map(base.execute(plan), (rows) => ({ rows })))
|
|
200
|
+
return {
|
|
201
|
+
...base,
|
|
202
|
+
executeResult,
|
|
203
|
+
prepare(plan) {
|
|
204
|
+
return {
|
|
205
|
+
execute: base.execute(plan),
|
|
206
|
+
executeResult: executeResult(plan),
|
|
207
|
+
stream: base.stream(plan)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} as Executor<Dialect, Error, Context>
|
|
211
|
+
}
|
|
212
|
+
|
|
97
213
|
const setPath = (
|
|
98
214
|
target: Record<string, unknown>,
|
|
99
215
|
path: readonly string[],
|
|
@@ -437,7 +553,7 @@ export const make = <
|
|
|
437
553
|
execute: <PlanValue extends Query.Plan.Any>(
|
|
438
554
|
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
439
555
|
) => Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
|
|
440
|
-
): Executor<Dialect, Error, Context> => ({
|
|
556
|
+
): Executor<Dialect, Error, Context> => withResultContracts({
|
|
441
557
|
dialect,
|
|
442
558
|
execute(plan) {
|
|
443
559
|
return (execute as any)(plan)
|
|
@@ -445,7 +561,7 @@ export const make = <
|
|
|
445
561
|
stream(plan) {
|
|
446
562
|
return Stream.unwrap(Effect.map((execute as any)(plan), (rows: ReadonlyArray<any>) => Stream.fromIterable(rows)))
|
|
447
563
|
}
|
|
448
|
-
})
|
|
564
|
+
})
|
|
449
565
|
|
|
450
566
|
/**
|
|
451
567
|
* Constructs a driver from a dialect and execution callback.
|
|
@@ -473,6 +589,9 @@ export function driver<
|
|
|
473
589
|
readonly stream: <Row>(
|
|
474
590
|
query: Renderer.RenderedQuery<Row, Dialect>
|
|
475
591
|
) => Stream.Stream<FlatRow, Error, Context>
|
|
592
|
+
readonly executeResult?: <Row>(
|
|
593
|
+
query: Renderer.RenderedQuery<Row, Dialect>
|
|
594
|
+
) => Effect.Effect<DriverResult, Error, Context>
|
|
476
595
|
}
|
|
477
596
|
): Driver<Dialect, Error, Context>
|
|
478
597
|
export function driver<
|
|
@@ -492,6 +611,9 @@ export function driver<
|
|
|
492
611
|
readonly stream: <Row>(
|
|
493
612
|
query: Renderer.RenderedQuery<Row, Dialect>
|
|
494
613
|
) => Stream.Stream<FlatRow, Error, Context>
|
|
614
|
+
readonly executeResult?: <Row>(
|
|
615
|
+
query: Renderer.RenderedQuery<Row, Dialect>
|
|
616
|
+
) => Effect.Effect<DriverResult, Error, Context>
|
|
495
617
|
}
|
|
496
618
|
): Driver<Dialect, Error, Context> {
|
|
497
619
|
return {
|
|
@@ -508,7 +630,10 @@ export function driver<
|
|
|
508
630
|
)
|
|
509
631
|
}
|
|
510
632
|
return executeOrHandlers.stream(query)
|
|
511
|
-
}
|
|
633
|
+
},
|
|
634
|
+
...(typeof executeOrHandlers === "function" || executeOrHandlers.executeResult === undefined
|
|
635
|
+
? {}
|
|
636
|
+
: { executeResult: executeOrHandlers.executeResult })
|
|
512
637
|
}
|
|
513
638
|
}
|
|
514
639
|
|
|
@@ -528,30 +653,96 @@ export const fromDriver = <
|
|
|
528
653
|
renderer: Renderer.Renderer<Dialect>,
|
|
529
654
|
sqlDriver: Driver<Dialect, Error, Context>
|
|
530
655
|
): Executor<Dialect, Error, Context> => {
|
|
531
|
-
const
|
|
656
|
+
const renderedCache = new WeakMap<object, Renderer.RenderedQuery<any, Dialect>>()
|
|
657
|
+
const render = (plan: Query.Plan.Any): Renderer.RenderedQuery<any, Dialect> => {
|
|
658
|
+
const cached = renderedCache.get(plan)
|
|
659
|
+
if (cached !== undefined) {
|
|
660
|
+
return cached
|
|
661
|
+
}
|
|
662
|
+
const rendered = renderer.render(plan as any) as Renderer.RenderedQuery<any, Dialect>
|
|
663
|
+
renderedCache.set(plan, rendered)
|
|
664
|
+
return rendered
|
|
665
|
+
}
|
|
666
|
+
const executor = withResultContracts<Dialect, Error, Context>({
|
|
532
667
|
dialect: renderer.dialect,
|
|
533
668
|
execute<PlanValue extends Query.Plan.Any>(
|
|
534
669
|
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
535
670
|
) {
|
|
536
|
-
const rendered =
|
|
671
|
+
const rendered = render(plan as Query.Plan.Any)
|
|
537
672
|
return Effect.map(
|
|
538
673
|
sqlDriver.execute(rendered),
|
|
539
674
|
(rows) => remapRows<any>(rendered, rows)
|
|
540
675
|
) as Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
|
|
541
676
|
},
|
|
677
|
+
executeResult<PlanValue extends Query.Plan.Any>(
|
|
678
|
+
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
679
|
+
) {
|
|
680
|
+
const rendered = render(plan as Query.Plan.Any)
|
|
681
|
+
const result = sqlDriver.executeResult
|
|
682
|
+
? sqlDriver.executeResult(rendered)
|
|
683
|
+
: Effect.map(sqlDriver.execute(rendered), (rows) => ({ rows }))
|
|
684
|
+
return Effect.map(result, ({ rows, ...metadata }) => ({
|
|
685
|
+
...metadata,
|
|
686
|
+
rows: remapRows<any>(rendered, rows)
|
|
687
|
+
})) as Effect.Effect<ExecutionResult<Query.ResultRow<PlanValue>>, Error, Context>
|
|
688
|
+
},
|
|
542
689
|
stream<PlanValue extends Query.Plan.Any>(
|
|
543
690
|
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
544
691
|
) {
|
|
545
|
-
const rendered =
|
|
692
|
+
const rendered = render(plan as Query.Plan.Any)
|
|
546
693
|
return Stream.mapArray(
|
|
547
694
|
sqlDriver.stream(rendered),
|
|
548
695
|
(rows) => remapRows<any>(rendered, rows) as never
|
|
549
696
|
) as Stream.Stream<Query.ResultRow<PlanValue>, Error, Context>
|
|
550
697
|
}
|
|
551
|
-
}
|
|
698
|
+
})
|
|
552
699
|
return executor
|
|
553
700
|
}
|
|
554
701
|
|
|
702
|
+
/** Builds a dialect-specific EXPLAIN statement around an already rendered query. */
|
|
703
|
+
export const explainQuery = <Dialect extends string>(
|
|
704
|
+
query: Renderer.RenderedQuery<any, Dialect>,
|
|
705
|
+
options: ExplainOptions = {}
|
|
706
|
+
): Renderer.RenderedQuery<FlatRow, Dialect> => {
|
|
707
|
+
const analyze = options.analyze ?? false
|
|
708
|
+
const format = options.format ?? "text"
|
|
709
|
+
let prefix: string
|
|
710
|
+
switch (query.dialect) {
|
|
711
|
+
case "postgres":
|
|
712
|
+
prefix = `explain (${[
|
|
713
|
+
...(analyze ? ["analyze true"] : []),
|
|
714
|
+
`format ${format}`
|
|
715
|
+
].join(", ")}) `
|
|
716
|
+
break
|
|
717
|
+
case "mysql":
|
|
718
|
+
if (analyze && format === "json") {
|
|
719
|
+
throw new Error("MySQL EXPLAIN ANALYZE cannot be combined with JSON format")
|
|
720
|
+
}
|
|
721
|
+
prefix = analyze ? "explain analyze " : format === "json" ? "explain format=json " : "explain "
|
|
722
|
+
break
|
|
723
|
+
case "sqlite":
|
|
724
|
+
if (analyze || format === "json") {
|
|
725
|
+
throw new Error("SQLite EXPLAIN QUERY PLAN does not support analyze or JSON format")
|
|
726
|
+
}
|
|
727
|
+
prefix = "explain query plan "
|
|
728
|
+
break
|
|
729
|
+
default:
|
|
730
|
+
if (analyze || format === "json") {
|
|
731
|
+
throw new Error("Portable EXPLAIN only supports text plans without analyze")
|
|
732
|
+
}
|
|
733
|
+
prefix = "explain "
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
...query,
|
|
737
|
+
sql: prefix + query.sql,
|
|
738
|
+
projections: [],
|
|
739
|
+
[Renderer.TypeId]: {
|
|
740
|
+
row: undefined as unknown as FlatRow,
|
|
741
|
+
dialect: query.dialect
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
555
746
|
export const streamFromSqlClient = <Dialect extends string>(
|
|
556
747
|
query: Renderer.RenderedQuery<any, Dialect>
|
|
557
748
|
): Stream.Stream<FlatRow, SqlError.SqlError, SqlClient.SqlClient> =>
|
|
@@ -54,6 +54,24 @@ export interface FunctionCallNode<
|
|
|
54
54
|
readonly args: Args
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Safely quoted identifier interpolation inside a custom SQL expression. */
|
|
58
|
+
export interface SqlIdentifierNode<
|
|
59
|
+
Parts extends readonly [string, ...string[]] = readonly [string, ...string[]]
|
|
60
|
+
> {
|
|
61
|
+
readonly kind: "sqlIdentifier"
|
|
62
|
+
readonly parts: Parts
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Typed custom SQL expression assembled from static text and safe values. */
|
|
66
|
+
export interface CustomSqlNode<
|
|
67
|
+
Values extends readonly (Expression.Any | SqlIdentifierNode)[] =
|
|
68
|
+
readonly (Expression.Any | SqlIdentifierNode)[]
|
|
69
|
+
> {
|
|
70
|
+
readonly kind: "customSql"
|
|
71
|
+
readonly strings: readonly string[]
|
|
72
|
+
readonly values: Values
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
/** `excluded.column` reference used inside insert conflict handlers. */
|
|
58
76
|
export interface ExcludedNode<
|
|
59
77
|
ColumnName extends string = string
|
|
@@ -70,6 +88,11 @@ export type UnaryKind =
|
|
|
70
88
|
| "upper"
|
|
71
89
|
| "lower"
|
|
72
90
|
| "count"
|
|
91
|
+
| "sum"
|
|
92
|
+
| "avg"
|
|
93
|
+
| "abs"
|
|
94
|
+
| "round"
|
|
95
|
+
| "negate"
|
|
73
96
|
| "max"
|
|
74
97
|
| "min"
|
|
75
98
|
|
|
@@ -101,6 +124,11 @@ export type BinaryKind =
|
|
|
101
124
|
| "contains"
|
|
102
125
|
| "containedBy"
|
|
103
126
|
| "overlaps"
|
|
127
|
+
| "add"
|
|
128
|
+
| "subtract"
|
|
129
|
+
| "multiply"
|
|
130
|
+
| "divide"
|
|
131
|
+
| "modulo"
|
|
104
132
|
|
|
105
133
|
/** Binary expression node. */
|
|
106
134
|
export interface BinaryNode<
|
|
@@ -192,7 +220,36 @@ export interface WindowOrderByNode<
|
|
|
192
220
|
}
|
|
193
221
|
|
|
194
222
|
/** Window function kinds supported by the query layer. */
|
|
195
|
-
export type WindowKind =
|
|
223
|
+
export type WindowKind =
|
|
224
|
+
| "rowNumber"
|
|
225
|
+
| "rank"
|
|
226
|
+
| "denseRank"
|
|
227
|
+
| "over"
|
|
228
|
+
| "lag"
|
|
229
|
+
| "lead"
|
|
230
|
+
| "firstValue"
|
|
231
|
+
| "lastValue"
|
|
232
|
+
|
|
233
|
+
export type WindowFrameBoundary =
|
|
234
|
+
| "unboundedPreceding"
|
|
235
|
+
| "currentRow"
|
|
236
|
+
| "unboundedFollowing"
|
|
237
|
+
| {
|
|
238
|
+
readonly preceding: number
|
|
239
|
+
}
|
|
240
|
+
| {
|
|
241
|
+
readonly following: number
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export type WindowFrameUnit = "rows" | "range" | "groups"
|
|
245
|
+
|
|
246
|
+
export interface WindowFrameNode<
|
|
247
|
+
Unit extends WindowFrameUnit = WindowFrameUnit
|
|
248
|
+
> {
|
|
249
|
+
readonly unit: Unit
|
|
250
|
+
readonly start: WindowFrameBoundary
|
|
251
|
+
readonly end?: WindowFrameBoundary
|
|
252
|
+
}
|
|
196
253
|
|
|
197
254
|
/** Window function expression node. */
|
|
198
255
|
export interface WindowNode<
|
|
@@ -204,8 +261,11 @@ export interface WindowNode<
|
|
|
204
261
|
readonly kind: "window"
|
|
205
262
|
readonly function: Kind
|
|
206
263
|
readonly value?: Value
|
|
264
|
+
readonly offset?: Expression.Any
|
|
265
|
+
readonly defaultValue?: Expression.Any
|
|
207
266
|
readonly partitionBy: PartitionBy
|
|
208
267
|
readonly orderBy: OrderBy
|
|
268
|
+
readonly frame?: WindowFrameNode
|
|
209
269
|
}
|
|
210
270
|
|
|
211
271
|
export type JsonSegmentTuple = readonly any[]
|
|
@@ -351,6 +411,7 @@ export type Any =
|
|
|
351
411
|
| CastNode
|
|
352
412
|
| CollateNode
|
|
353
413
|
| FunctionCallNode
|
|
414
|
+
| CustomSqlNode
|
|
354
415
|
| ExcludedNode
|
|
355
416
|
| UnaryNode
|
|
356
417
|
| BinaryNode
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type * as Schema from "effect/Schema"
|
|
2
|
+
|
|
3
|
+
import * as ExpressionAst from "./expression-ast.js"
|
|
4
|
+
import * as Expression from "./scalar.js"
|
|
5
|
+
import {
|
|
6
|
+
makeExpression,
|
|
7
|
+
mergeAggregationManyRuntime,
|
|
8
|
+
mergeManyDependencies,
|
|
9
|
+
type MergeAggregation,
|
|
10
|
+
type NormalizeDialect,
|
|
11
|
+
type TupleDependencies,
|
|
12
|
+
type TupleDialect
|
|
13
|
+
} from "./query.js"
|
|
14
|
+
|
|
15
|
+
/** A structured SQL identifier. Every path segment is quoted by the renderer. */
|
|
16
|
+
export type Identifier<
|
|
17
|
+
Parts extends readonly [string, ...string[]] = readonly [string, ...string[]]
|
|
18
|
+
> = ExpressionAst.SqlIdentifierNode<Parts>
|
|
19
|
+
|
|
20
|
+
/** Creates a safely quoted identifier interpolation for {@link expression}. */
|
|
21
|
+
export const identifier = <
|
|
22
|
+
const Parts extends readonly [string, ...string[]]
|
|
23
|
+
>(...parts: Parts): Identifier<Parts> => ({
|
|
24
|
+
kind: "sqlIdentifier",
|
|
25
|
+
parts
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
type Interpolation = Expression.Any | Identifier
|
|
29
|
+
|
|
30
|
+
type ExpressionsOf<
|
|
31
|
+
Values extends readonly Interpolation[]
|
|
32
|
+
> = Extract<Values[number], Expression.Any>
|
|
33
|
+
|
|
34
|
+
type DialectOfValues<
|
|
35
|
+
Values extends readonly Interpolation[],
|
|
36
|
+
Db extends Expression.DbType.Any
|
|
37
|
+
> = [ExpressionsOf<Values>] extends [never]
|
|
38
|
+
? Db["dialect"]
|
|
39
|
+
: TupleDialect<readonly ExpressionsOf<Values>[]>
|
|
40
|
+
|
|
41
|
+
type DependenciesOfValues<
|
|
42
|
+
Values extends readonly Interpolation[]
|
|
43
|
+
> = TupleDependencies<readonly ExpressionsOf<Values>[]>
|
|
44
|
+
|
|
45
|
+
type KindOfValues<
|
|
46
|
+
Values extends readonly Interpolation[],
|
|
47
|
+
Current extends Expression.ScalarKind = "scalar"
|
|
48
|
+
> = Values extends readonly [infer Head, ...infer Tail extends readonly Interpolation[]]
|
|
49
|
+
? KindOfValues<
|
|
50
|
+
Tail,
|
|
51
|
+
Head extends Expression.Any
|
|
52
|
+
? MergeAggregation<Current, Expression.KindOf<Head>>
|
|
53
|
+
: Current
|
|
54
|
+
>
|
|
55
|
+
: Current
|
|
56
|
+
|
|
57
|
+
export interface Options<
|
|
58
|
+
Runtime,
|
|
59
|
+
Db extends Expression.DbType.Any,
|
|
60
|
+
Nullable extends Expression.Nullability,
|
|
61
|
+
Kind extends Expression.ScalarKind = "scalar"
|
|
62
|
+
> {
|
|
63
|
+
readonly dbType: Db
|
|
64
|
+
readonly schema: Schema.Schema<Runtime>
|
|
65
|
+
readonly nullability: Nullable
|
|
66
|
+
readonly kind?: Kind
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a typed SQL expression from a static template.
|
|
71
|
+
*
|
|
72
|
+
* Interpolations must be existing typed expressions or values returned by
|
|
73
|
+
* {@link identifier}; runtime values should first be lifted with
|
|
74
|
+
* `Query.literal(...)` so they remain bound parameters.
|
|
75
|
+
*/
|
|
76
|
+
export const expression = <
|
|
77
|
+
Runtime,
|
|
78
|
+
Db extends Expression.DbType.Any,
|
|
79
|
+
Nullable extends Expression.Nullability,
|
|
80
|
+
Kind extends Expression.ScalarKind = "scalar"
|
|
81
|
+
>(
|
|
82
|
+
options: Options<Runtime, Db, Nullable, Kind>
|
|
83
|
+
) => <
|
|
84
|
+
const Values extends readonly Interpolation[]
|
|
85
|
+
>(
|
|
86
|
+
strings: TemplateStringsArray,
|
|
87
|
+
...values: Values
|
|
88
|
+
): Expression.Scalar<
|
|
89
|
+
Runtime,
|
|
90
|
+
Db,
|
|
91
|
+
Nullable,
|
|
92
|
+
NormalizeDialect<DialectOfValues<Values, Db>>,
|
|
93
|
+
Kind extends "scalar" ? KindOfValues<Values> : Kind,
|
|
94
|
+
DependenciesOfValues<Values>
|
|
95
|
+
> & {
|
|
96
|
+
readonly [ExpressionAst.TypeId]: ExpressionAst.CustomSqlNode<Values>
|
|
97
|
+
} => {
|
|
98
|
+
const expressions = values.filter((value): value is Expression.Any =>
|
|
99
|
+
Expression.TypeId in value)
|
|
100
|
+
return makeExpression({
|
|
101
|
+
runtime: undefined as unknown as Runtime,
|
|
102
|
+
dbType: options.dbType,
|
|
103
|
+
runtimeSchema: options.schema,
|
|
104
|
+
driverValueMapping: options.dbType.driverValueMapping,
|
|
105
|
+
nullability: options.nullability,
|
|
106
|
+
dialect: (
|
|
107
|
+
expressions.find((value) => value[Expression.TypeId].dialect !== "standard")?.[Expression.TypeId].dialect ??
|
|
108
|
+
expressions[0]?.[Expression.TypeId].dialect ??
|
|
109
|
+
options.dbType.dialect
|
|
110
|
+
) as NormalizeDialect<DialectOfValues<Values, Db>>,
|
|
111
|
+
kind: (options.kind ?? mergeAggregationManyRuntime(expressions)) as Kind extends "scalar"
|
|
112
|
+
? KindOfValues<Values>
|
|
113
|
+
: Kind,
|
|
114
|
+
dependencies: mergeManyDependencies(expressions)
|
|
115
|
+
}, {
|
|
116
|
+
kind: "customSql",
|
|
117
|
+
strings: [...strings],
|
|
118
|
+
values
|
|
119
|
+
}) as never
|
|
120
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { OperandCompatibilityError } from "./coercion/errors.js"
|
|
2
|
+
import type * as Expression from "./scalar.js"
|
|
3
|
+
|
|
4
|
+
type BaseDbType<Db extends Expression.DbType.Any> =
|
|
5
|
+
Db extends Expression.DbType.Domain<any, infer Base extends Expression.DbType.Any, any>
|
|
6
|
+
? BaseDbType<Base>
|
|
7
|
+
: Db
|
|
8
|
+
|
|
9
|
+
type ShallowBaseDbType<Db extends Expression.DbType.Any> =
|
|
10
|
+
Db extends Expression.DbType.Domain<any, infer Base extends Expression.DbType.Any, any>
|
|
11
|
+
? Base
|
|
12
|
+
: Db
|
|
13
|
+
|
|
14
|
+
export type OrderedInput<
|
|
15
|
+
Value extends Expression.Any,
|
|
16
|
+
Dialect extends string,
|
|
17
|
+
Operator extends string
|
|
18
|
+
> = BaseDbType<Expression.DbTypeOf<Value>> extends infer Db extends Expression.DbType.Any
|
|
19
|
+
? Db extends { readonly traits: { readonly ordered: true } }
|
|
20
|
+
? Value
|
|
21
|
+
: OperandCompatibilityError<Operator, Db, Db, Dialect, "an ordered db type">
|
|
22
|
+
: never
|
|
23
|
+
|
|
24
|
+
export type CaseConversionInput<
|
|
25
|
+
Value,
|
|
26
|
+
Db extends Expression.DbType.Any,
|
|
27
|
+
Dialect extends string,
|
|
28
|
+
Operator extends string
|
|
29
|
+
> = BaseDbType<Db> extends infer Base extends Expression.DbType.Any
|
|
30
|
+
? Base extends { readonly family: "text" }
|
|
31
|
+
? Value
|
|
32
|
+
: Dialect extends "mysql" | "sqlite"
|
|
33
|
+
? Base extends { readonly family: "uuid" }
|
|
34
|
+
? Value
|
|
35
|
+
: OperandCompatibilityError<Operator, Base, Base, Dialect, "a character string db type">
|
|
36
|
+
: OperandCompatibilityError<Operator, Base, Base, Dialect, "a character string db type">
|
|
37
|
+
: never
|
|
38
|
+
|
|
39
|
+
type IsNullExpression<Value extends Expression.Any> =
|
|
40
|
+
ShallowBaseDbType<Expression.DbTypeOf<Value>> extends { readonly family: "null" }
|
|
41
|
+
? true
|
|
42
|
+
: false
|
|
43
|
+
|
|
44
|
+
type CompareGroup<Value extends Expression.Any> =
|
|
45
|
+
ShallowBaseDbType<Expression.DbTypeOf<Value>> extends {
|
|
46
|
+
readonly compareGroup: infer Group extends string
|
|
47
|
+
}
|
|
48
|
+
? Group
|
|
49
|
+
: never
|
|
50
|
+
|
|
51
|
+
type CoalescePairGuard<
|
|
52
|
+
Left extends Expression.Any,
|
|
53
|
+
Right extends Expression.Any,
|
|
54
|
+
Dialect extends string
|
|
55
|
+
> = [CompareGroup<Left>] extends [CompareGroup<Right>]
|
|
56
|
+
? [CompareGroup<Right>] extends [CompareGroup<Left>]
|
|
57
|
+
? true
|
|
58
|
+
: OperandCompatibilityError<
|
|
59
|
+
"coalesce",
|
|
60
|
+
Expression.DbTypeOf<Left>,
|
|
61
|
+
Expression.DbTypeOf<Right>,
|
|
62
|
+
Dialect,
|
|
63
|
+
"the same db type family"
|
|
64
|
+
>
|
|
65
|
+
: OperandCompatibilityError<
|
|
66
|
+
"coalesce",
|
|
67
|
+
Expression.DbTypeOf<Left>,
|
|
68
|
+
Expression.DbTypeOf<Right>,
|
|
69
|
+
Dialect,
|
|
70
|
+
"the same db type family"
|
|
71
|
+
>
|
|
72
|
+
|
|
73
|
+
type CoalesceTailGuard<
|
|
74
|
+
Head extends Expression.Any,
|
|
75
|
+
Tail extends readonly Expression.Any[],
|
|
76
|
+
Dialect extends string
|
|
77
|
+
> = Tail extends readonly [
|
|
78
|
+
infer Next extends Expression.Any,
|
|
79
|
+
...infer Rest extends readonly Expression.Any[]
|
|
80
|
+
]
|
|
81
|
+
? IsNullExpression<Next> extends true
|
|
82
|
+
? CoalesceTailGuard<Head, Rest, Dialect>
|
|
83
|
+
: CoalescePairGuard<Head, Next, Dialect> extends true
|
|
84
|
+
? CoalesceTailGuard<Head, Rest, Dialect>
|
|
85
|
+
: CoalescePairGuard<Head, Next, Dialect>
|
|
86
|
+
: true
|
|
87
|
+
|
|
88
|
+
type CoalesceTupleGuard<
|
|
89
|
+
Values extends readonly Expression.Any[],
|
|
90
|
+
Dialect extends string
|
|
91
|
+
> = Values extends readonly [
|
|
92
|
+
infer Head extends Expression.Any,
|
|
93
|
+
...infer Tail extends readonly Expression.Any[]
|
|
94
|
+
]
|
|
95
|
+
? IsNullExpression<Head> extends true
|
|
96
|
+
? CoalesceTupleGuard<Tail, Dialect>
|
|
97
|
+
: CoalesceTailGuard<Head, Tail, Dialect>
|
|
98
|
+
: true
|
|
99
|
+
|
|
100
|
+
export type CoalesceConstraint<
|
|
101
|
+
Values extends readonly Expression.Any[],
|
|
102
|
+
Dialect extends string
|
|
103
|
+
> = CoalesceTupleGuard<Values, Dialect> extends true
|
|
104
|
+
? unknown
|
|
105
|
+
: readonly [CoalesceTupleGuard<Values, Dialect>]
|
|
@@ -81,6 +81,20 @@ const escapeGroupingText = (value: string): string =>
|
|
|
81
81
|
const functionCallNameGroupingKey = (name: unknown): string =>
|
|
82
82
|
escapeGroupingText(name as string)
|
|
83
83
|
|
|
84
|
+
const customSqlGroupingKey = (
|
|
85
|
+
strings: readonly string[],
|
|
86
|
+
values: readonly (Expression.Any | ExpressionAst.SqlIdentifierNode)[]
|
|
87
|
+
): string => strings.map((part, index) => {
|
|
88
|
+
const value = values[index]
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
return escapeGroupingText(part)
|
|
91
|
+
}
|
|
92
|
+
const rendered = isExpression(value)
|
|
93
|
+
? groupingKeyOfExpression(value)
|
|
94
|
+
: `identifier:${value.parts.map(escapeGroupingText).join(".")}`
|
|
95
|
+
return `${escapeGroupingText(part)}${rendered}`
|
|
96
|
+
}).join("|")
|
|
97
|
+
|
|
84
98
|
const quantifiedComparisonGroupingName = (
|
|
85
99
|
kind: "comparisonAny" | "comparisonAll"
|
|
86
100
|
): "compareAny" | "compareAll" =>
|
|
@@ -184,12 +198,19 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
|
|
|
184
198
|
return `collate(${requiredExpressionGroupingKey("collate", ast.value)},${collationGroupingKey(ast.collation)})`
|
|
185
199
|
case "function":
|
|
186
200
|
return `function(${functionCallNameGroupingKey(ast.name)},${functionCallArgsGroupingKey(ast.args)})`
|
|
201
|
+
case "customSql":
|
|
202
|
+
return `customSql(${customSqlGroupingKey(ast.strings, ast.values)})`
|
|
187
203
|
case "isNull":
|
|
188
204
|
case "isNotNull":
|
|
189
205
|
case "not":
|
|
190
206
|
case "upper":
|
|
191
207
|
case "lower":
|
|
192
208
|
case "count":
|
|
209
|
+
case "sum":
|
|
210
|
+
case "avg":
|
|
211
|
+
case "abs":
|
|
212
|
+
case "round":
|
|
213
|
+
case "negate":
|
|
193
214
|
case "max":
|
|
194
215
|
case "min":
|
|
195
216
|
return `${ast.kind}(${requiredExpressionGroupingKey(ast.kind, ast.value)})`
|
|
@@ -210,6 +231,11 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
|
|
|
210
231
|
case "contains":
|
|
211
232
|
case "containedBy":
|
|
212
233
|
case "overlaps":
|
|
234
|
+
case "add":
|
|
235
|
+
case "subtract":
|
|
236
|
+
case "multiply":
|
|
237
|
+
case "divide":
|
|
238
|
+
case "modulo":
|
|
213
239
|
return `${ast.kind}(${requiredBinaryExpressionGroupingKey(ast.kind, ast.left, ast.right)})`
|
|
214
240
|
case "and":
|
|
215
241
|
case "or":
|