effect-qb 0.20.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.
Files changed (58) hide show
  1. package/dist/index.js +663 -261
  2. package/dist/mysql.js +2840 -492
  3. package/dist/postgres/metadata.js +113 -14
  4. package/dist/postgres.js +2596 -285
  5. package/dist/sqlite.js +2783 -450
  6. package/dist/standard.js +663 -261
  7. package/package.json +2 -3
  8. package/src/internal/analytics.ts +264 -0
  9. package/src/internal/coercion/errors.d.ts +1 -1
  10. package/src/internal/coercion/errors.ts +1 -1
  11. package/src/internal/custom-sql-renderer.ts +20 -0
  12. package/src/internal/dialect-numeric.ts +332 -0
  13. package/src/internal/dialect-renderers/mysql.ts +68 -13
  14. package/src/internal/dialect-renderers/postgres.ts +83 -13
  15. package/src/internal/dialect-renderers/sqlite.ts +57 -13
  16. package/src/internal/dynamic.ts +131 -0
  17. package/src/internal/executor.ts +198 -7
  18. package/src/internal/expression-ast.ts +62 -1
  19. package/src/internal/fragment.ts +120 -0
  20. package/src/internal/function-constraints.ts +105 -0
  21. package/src/internal/grouping-key.ts +26 -0
  22. package/src/internal/numeric.ts +204 -0
  23. package/src/internal/query.ts +13 -1
  24. package/src/internal/runtime/normalize.ts +5 -2
  25. package/src/internal/runtime/schema.ts +2 -1
  26. package/src/internal/standard-dsl.ts +43 -20
  27. package/src/internal/window-frame.ts +30 -0
  28. package/src/internal/window-renderer.ts +25 -0
  29. package/src/mysql/executor.ts +126 -45
  30. package/src/mysql/function/aggregate.ts +87 -1
  31. package/src/mysql/function/index.ts +5 -2
  32. package/src/mysql/function/numeric.ts +185 -0
  33. package/src/mysql/function/temporal.ts +6 -6
  34. package/src/mysql/function/window.ts +12 -0
  35. package/src/mysql/internal/dsl.ts +38 -20
  36. package/src/mysql.ts +2 -0
  37. package/src/postgres/executor.ts +111 -45
  38. package/src/postgres/function/aggregate.ts +124 -1
  39. package/src/postgres/function/core.ts +2 -1
  40. package/src/postgres/function/index.ts +19 -1
  41. package/src/postgres/function/numeric.ts +246 -0
  42. package/src/postgres/function/window.ts +12 -0
  43. package/src/postgres/internal/dsl.ts +38 -20
  44. package/src/sqlite/executor.ts +96 -45
  45. package/src/sqlite/function/aggregate.ts +90 -1
  46. package/src/sqlite/function/index.ts +5 -2
  47. package/src/sqlite/function/numeric.ts +139 -0
  48. package/src/sqlite/function/window.ts +12 -0
  49. package/src/sqlite/internal/dsl.ts +38 -20
  50. package/src/sqlite.ts +2 -0
  51. package/src/standard/function/core.ts +8 -1
  52. package/src/standard/function/index.ts +18 -11
  53. package/src/standard/function/string.ts +1 -1
  54. package/src/standard/function/window.ts +10 -1
  55. package/src/standard/query.ts +19 -7
  56. package/src/standard/type.ts +16 -0
  57. package/src/standard.ts +4 -0
  58. package/src/standard/function/temporal.ts +0 -78
@@ -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
- }) as Executor<Dialect, Error, Context>
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 executor: Executor<Dialect, Error, Context> = {
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 = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
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 = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
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 = "rowNumber" | "rank" | "denseRank" | "over"
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":