@effect-app/infra 4.0.0-beta.215 → 4.0.0-beta.217
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/CHANGELOG.md +15 -0
- package/dist/Model/Repository/internal/internal.d.ts +1 -1
- package/dist/Model/query/dsl.d.ts +41 -1
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +49 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +28 -1
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +30 -1
- package/dist/QueueMaker/SQLQueue.d.ts +1 -1
- package/dist/QueueMaker/memQueue.d.ts +1 -1
- package/dist/QueueMaker/sbqueue.d.ts +1 -1
- package/dist/Store/Cosmos/query.d.ts +1 -1
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +30 -1
- package/dist/Store/Cosmos.d.ts +1 -1
- package/dist/Store/Memory.d.ts +1 -1
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +44 -1
- package/dist/Store/SQL/Pg.d.ts +1 -1
- package/dist/Store/SQL/query.d.ts +2 -2
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +54 -9
- package/dist/Store/SQL.d.ts +1 -1
- package/dist/Store/SQL.d.ts.map +1 -1
- package/dist/Store/SQL.js +3 -21
- package/dist/adapters/ServiceBus.d.ts +1 -1
- package/dist/adapters/logger.d.ts +1 -1
- package/dist/api/routing.d.ts +1 -1
- package/dist/otel.d.ts +1 -1
- package/package.json +3 -2
- package/src/Model/query/dsl.ts +99 -0
- package/src/Model/query/new-kid-interpreter.ts +62 -0
- package/src/Store/Cosmos/query.ts +33 -1
- package/src/Store/Memory.ts +40 -1
- package/src/Store/SQL/query.ts +58 -9
- package/src/Store/SQL.ts +3 -23
- package/test/dist/rawQuery.test.d.ts.map +1 -1
- package/test/query.test.ts +108 -3
- package/test/rawQuery.test.ts +24 -4
- package/test/sql-store.test.ts +166 -0
package/src/Model/query/dsl.ts
CHANGED
|
@@ -100,6 +100,17 @@ export type QueryProjection<
|
|
|
100
100
|
|
|
101
101
|
export type ComputedProjectionOperation = (q: Query<any>) => QueryWhere<any, any, any>
|
|
102
102
|
|
|
103
|
+
export type ComputedProjectionMathExpression =
|
|
104
|
+
| {
|
|
105
|
+
readonly _tag: "field"
|
|
106
|
+
readonly field: string
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
readonly _tag: "mul"
|
|
110
|
+
readonly left: ComputedProjectionMathExpression
|
|
111
|
+
readonly right: ComputedProjectionMathExpression
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
export type ComputedProjectionExpression =
|
|
104
115
|
| {
|
|
105
116
|
readonly _tag: "relation-count"
|
|
@@ -128,6 +139,28 @@ export type ComputedProjectionExpression =
|
|
|
128
139
|
readonly field: string
|
|
129
140
|
readonly operation?: ComputedProjectionOperation
|
|
130
141
|
}
|
|
142
|
+
| {
|
|
143
|
+
readonly _tag: "relation-sum-expr"
|
|
144
|
+
readonly path: string
|
|
145
|
+
readonly expression: ComputedProjectionMathExpression
|
|
146
|
+
readonly operation?: ComputedProjectionOperation
|
|
147
|
+
}
|
|
148
|
+
| {
|
|
149
|
+
readonly _tag: "relation-sum-expr-by"
|
|
150
|
+
readonly path: string
|
|
151
|
+
readonly expression: ComputedProjectionMathExpression
|
|
152
|
+
readonly unit: string
|
|
153
|
+
readonly operation?: ComputedProjectionOperation
|
|
154
|
+
}
|
|
155
|
+
| {
|
|
156
|
+
readonly _tag: "relation-sum-expr-normalized"
|
|
157
|
+
readonly path: string
|
|
158
|
+
readonly expression: ComputedProjectionMathExpression
|
|
159
|
+
readonly unit: string
|
|
160
|
+
readonly toBase: string
|
|
161
|
+
readonly factors: Readonly<Record<string, number>>
|
|
162
|
+
readonly operation?: ComputedProjectionOperation
|
|
163
|
+
}
|
|
131
164
|
| {
|
|
132
165
|
readonly _tag: "relation-collect"
|
|
133
166
|
readonly path: string
|
|
@@ -482,6 +515,64 @@ export const relation = <TFieldValues extends FieldValues>(
|
|
|
482
515
|
path: path as string,
|
|
483
516
|
field
|
|
484
517
|
},
|
|
518
|
+
sumExpr: (
|
|
519
|
+
expression: ComputedProjectionMathExpression,
|
|
520
|
+
operation?: ComputedProjectionOperation
|
|
521
|
+
): ComputedProjectionExpression =>
|
|
522
|
+
operation
|
|
523
|
+
? {
|
|
524
|
+
_tag: "relation-sum-expr",
|
|
525
|
+
path: path as string,
|
|
526
|
+
expression,
|
|
527
|
+
operation
|
|
528
|
+
}
|
|
529
|
+
: {
|
|
530
|
+
_tag: "relation-sum-expr",
|
|
531
|
+
path: path as string,
|
|
532
|
+
expression
|
|
533
|
+
},
|
|
534
|
+
sumExprBy: (
|
|
535
|
+
expression: ComputedProjectionMathExpression,
|
|
536
|
+
options: { unit: string },
|
|
537
|
+
operation?: ComputedProjectionOperation
|
|
538
|
+
): ComputedProjectionExpression =>
|
|
539
|
+
operation
|
|
540
|
+
? {
|
|
541
|
+
_tag: "relation-sum-expr-by",
|
|
542
|
+
path: path as string,
|
|
543
|
+
expression,
|
|
544
|
+
unit: options.unit,
|
|
545
|
+
operation
|
|
546
|
+
}
|
|
547
|
+
: {
|
|
548
|
+
_tag: "relation-sum-expr-by",
|
|
549
|
+
path: path as string,
|
|
550
|
+
expression,
|
|
551
|
+
unit: options.unit
|
|
552
|
+
},
|
|
553
|
+
sumExprNormalized: (
|
|
554
|
+
expression: ComputedProjectionMathExpression,
|
|
555
|
+
options: { unit: string; toBase: string; factors: Readonly<Record<string, number>> },
|
|
556
|
+
operation?: ComputedProjectionOperation
|
|
557
|
+
): ComputedProjectionExpression =>
|
|
558
|
+
operation
|
|
559
|
+
? {
|
|
560
|
+
_tag: "relation-sum-expr-normalized",
|
|
561
|
+
path: path as string,
|
|
562
|
+
expression,
|
|
563
|
+
unit: options.unit,
|
|
564
|
+
toBase: options.toBase,
|
|
565
|
+
factors: options.factors,
|
|
566
|
+
operation
|
|
567
|
+
}
|
|
568
|
+
: {
|
|
569
|
+
_tag: "relation-sum-expr-normalized",
|
|
570
|
+
path: path as string,
|
|
571
|
+
expression,
|
|
572
|
+
unit: options.unit,
|
|
573
|
+
toBase: options.toBase,
|
|
574
|
+
factors: options.factors
|
|
575
|
+
},
|
|
485
576
|
collect: (field: string, operation?: ComputedProjectionOperation): ComputedProjectionExpression =>
|
|
486
577
|
operation
|
|
487
578
|
? {
|
|
@@ -514,6 +605,14 @@ export const relation = <TFieldValues extends FieldValues>(
|
|
|
514
605
|
}
|
|
515
606
|
})
|
|
516
607
|
|
|
608
|
+
export const expr = {
|
|
609
|
+
field: (field: string): ComputedProjectionMathExpression => ({ _tag: "field", field }),
|
|
610
|
+
mul: (
|
|
611
|
+
left: ComputedProjectionMathExpression,
|
|
612
|
+
right: ComputedProjectionMathExpression
|
|
613
|
+
): ComputedProjectionMathExpression => ({ _tag: "mul", left, right })
|
|
614
|
+
} as const
|
|
615
|
+
|
|
517
616
|
export const computed = <T extends ComputedProjectionMap>(value: T): T => value
|
|
518
617
|
|
|
519
618
|
export const projectComputed: {
|
|
@@ -8,6 +8,17 @@ import type { FieldValues } from "../filter/types.js"
|
|
|
8
8
|
import type { FieldPath } from "../filter/types/path/eager.js"
|
|
9
9
|
import { make, type Q, type QAll } from "../query/dsl.js"
|
|
10
10
|
|
|
11
|
+
export type ComputedProjectionMathIrExpression =
|
|
12
|
+
| {
|
|
13
|
+
readonly _tag: "field"
|
|
14
|
+
readonly field: string
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
readonly _tag: "mul"
|
|
18
|
+
readonly left: ComputedProjectionMathIrExpression
|
|
19
|
+
readonly right: ComputedProjectionMathIrExpression
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export type ComputedProjectionIrExpression =
|
|
12
23
|
| {
|
|
13
24
|
readonly _tag: "relation-count"
|
|
@@ -36,6 +47,28 @@ export type ComputedProjectionIrExpression =
|
|
|
36
47
|
readonly field: string
|
|
37
48
|
readonly filter: readonly FilterResult[]
|
|
38
49
|
}
|
|
50
|
+
| {
|
|
51
|
+
readonly _tag: "relation-sum-expr"
|
|
52
|
+
readonly path: string
|
|
53
|
+
readonly expression: ComputedProjectionMathIrExpression
|
|
54
|
+
readonly filter: readonly FilterResult[]
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
readonly _tag: "relation-sum-expr-by"
|
|
58
|
+
readonly path: string
|
|
59
|
+
readonly expression: ComputedProjectionMathIrExpression
|
|
60
|
+
readonly unit: string
|
|
61
|
+
readonly filter: readonly FilterResult[]
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
readonly _tag: "relation-sum-expr-normalized"
|
|
65
|
+
readonly path: string
|
|
66
|
+
readonly expression: ComputedProjectionMathIrExpression
|
|
67
|
+
readonly unit: string
|
|
68
|
+
readonly toBase: string
|
|
69
|
+
readonly factors: Readonly<Record<string, number>>
|
|
70
|
+
readonly filter: readonly FilterResult[]
|
|
71
|
+
}
|
|
39
72
|
| {
|
|
40
73
|
readonly _tag: "relation-collect"
|
|
41
74
|
readonly path: string
|
|
@@ -197,6 +230,35 @@ const interpret = <
|
|
|
197
230
|
key,
|
|
198
231
|
{ _tag: e._tag, path: e.path, field: e.field, filter } as ComputedProjectionIrExpression
|
|
199
232
|
]
|
|
233
|
+
case "relation-sum-expr":
|
|
234
|
+
return [
|
|
235
|
+
key,
|
|
236
|
+
{ _tag: e._tag, path: e.path, expression: e.expression, filter } as ComputedProjectionIrExpression
|
|
237
|
+
]
|
|
238
|
+
case "relation-sum-expr-by":
|
|
239
|
+
return [
|
|
240
|
+
key,
|
|
241
|
+
{
|
|
242
|
+
_tag: e._tag,
|
|
243
|
+
path: e.path,
|
|
244
|
+
expression: e.expression,
|
|
245
|
+
unit: e.unit,
|
|
246
|
+
filter
|
|
247
|
+
} as ComputedProjectionIrExpression
|
|
248
|
+
]
|
|
249
|
+
case "relation-sum-expr-normalized":
|
|
250
|
+
return [
|
|
251
|
+
key,
|
|
252
|
+
{
|
|
253
|
+
_tag: e._tag,
|
|
254
|
+
path: e.path,
|
|
255
|
+
expression: e.expression,
|
|
256
|
+
unit: e.unit,
|
|
257
|
+
toBase: e.toBase,
|
|
258
|
+
factors: e.factors,
|
|
259
|
+
filter
|
|
260
|
+
} as ComputedProjectionIrExpression
|
|
261
|
+
]
|
|
200
262
|
case "relation-collect":
|
|
201
263
|
return [
|
|
202
264
|
key,
|
|
@@ -4,7 +4,7 @@ import { Array, Effect, type NonEmptyReadonlyArray } from "effect-app"
|
|
|
4
4
|
import { assertUnreachable } from "effect-app/utils"
|
|
5
5
|
import { InfraLogger } from "../../logger.js"
|
|
6
6
|
import type { FilterR, FilterResult, Ops } from "../../Model/filter/filterApi.js"
|
|
7
|
-
import type { ComputedProjectionIrExpression } from "../../Model/query.js"
|
|
7
|
+
import type { ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "../../Model/query.js"
|
|
8
8
|
import { isRelationCheck } from "../codeFilter.js"
|
|
9
9
|
import type { SupportedValues } from "../service.js"
|
|
10
10
|
|
|
@@ -295,6 +295,23 @@ export function buildWhereCosmosQuery3(
|
|
|
295
295
|
const relationPath = computed.path
|
|
296
296
|
const relationAlias = relationPath
|
|
297
297
|
const relationSource = dottedToAccess(`f.${relationPath}`)
|
|
298
|
+
const compileExpr = (expression: ComputedProjectionMathIrExpression): string => {
|
|
299
|
+
switch (expression._tag) {
|
|
300
|
+
case "field":
|
|
301
|
+
return dottedToAccess(`${relationAlias}.${expression.field}`)
|
|
302
|
+
case "mul":
|
|
303
|
+
return `(${compileExpr(expression.left)} * ${compileExpr(expression.right)})`
|
|
304
|
+
default:
|
|
305
|
+
return assertUnreachable(expression)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const factorExpr = (unitExpr: string, toBase: string, factors: Readonly<Record<string, number>>) => {
|
|
309
|
+
const entries = Object.entries(factors).filter(([, factor]) => Number.isFinite(factor))
|
|
310
|
+
return entries.reduceRight<string>(
|
|
311
|
+
(acc, [unit, factor]) => `IIF(${unitExpr} = ${JSON.stringify(unit)}, ${factor}, ${acc})`,
|
|
312
|
+
`IIF(${unitExpr} = ${JSON.stringify(toBase)}, 1, 0)`
|
|
313
|
+
)
|
|
314
|
+
}
|
|
298
315
|
const where = computed.filter.length > 0
|
|
299
316
|
? ` WHERE ${print(computed.filter, relationPath, false)}`
|
|
300
317
|
: ""
|
|
@@ -319,6 +336,21 @@ export function buildWhereCosmosQuery3(
|
|
|
319
336
|
const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`)
|
|
320
337
|
return `(SELECT VALUE SUM(${fieldRef}) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
321
338
|
}
|
|
339
|
+
case "relation-sum-expr": {
|
|
340
|
+
const expression = compileExpr(computed.expression)
|
|
341
|
+
return `(SELECT VALUE SUM(${expression}) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
342
|
+
}
|
|
343
|
+
case "relation-sum-expr-by": {
|
|
344
|
+
const unitRef = dottedToAccess(`${relationAlias}.${computed.unit}`)
|
|
345
|
+
const expression = compileExpr(computed.expression)
|
|
346
|
+
return `ARRAY(SELECT VALUE { "unit": ${unitRef}, "total": SUM(${expression}) } FROM ${relationAlias} IN ${relationSource}${where} GROUP BY ${unitRef}) AS ${key}`
|
|
347
|
+
}
|
|
348
|
+
case "relation-sum-expr-normalized": {
|
|
349
|
+
const unitRef = dottedToAccess(`${relationAlias}.${computed.unit}`)
|
|
350
|
+
const expression = compileExpr(computed.expression)
|
|
351
|
+
const factor = factorExpr(unitRef, computed.toBase, computed.factors)
|
|
352
|
+
return `(SELECT VALUE SUM((${expression}) * (${factor})) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
353
|
+
}
|
|
322
354
|
case "relation-collect": {
|
|
323
355
|
const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`)
|
|
324
356
|
if (computed.distinct) {
|
package/src/Store/Memory.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { assertUnreachable } from "effect-app/utils"
|
|
|
6
6
|
import { InfraLogger } from "../logger.js"
|
|
7
7
|
import type { FilterResult } from "../Model/filter/filterApi.js"
|
|
8
8
|
import type { FieldValues } from "../Model/filter/types.js"
|
|
9
|
-
import type { ComputedProjectionIrExpression } from "../Model/query.js"
|
|
9
|
+
import type { ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "../Model/query.js"
|
|
10
10
|
import { annotateDb } from "../otel.js"
|
|
11
11
|
import { codeFilter, codeFilter3_ } from "./codeFilter.js"
|
|
12
12
|
import { type FilterArgs, type PersistenceModelType, type Store, type StoreConfig, StoreMaker } from "./service.js"
|
|
@@ -37,7 +37,11 @@ const emptyValueFor = (tag: ComputedProjectionIrExpression["_tag"]) => {
|
|
|
37
37
|
case "relation-count":
|
|
38
38
|
case "relation-distinct-count":
|
|
39
39
|
case "relation-sum":
|
|
40
|
+
case "relation-sum-expr":
|
|
41
|
+
case "relation-sum-expr-normalized":
|
|
40
42
|
return 0
|
|
43
|
+
case "relation-sum-expr-by":
|
|
44
|
+
return [] as unknown[]
|
|
41
45
|
case "relation-any":
|
|
42
46
|
return false
|
|
43
47
|
case "relation-every":
|
|
@@ -63,6 +67,18 @@ const computeProjectionValue = (
|
|
|
63
67
|
const matches = filter.length === 0
|
|
64
68
|
? (_value: unknown) => true
|
|
65
69
|
: (value: unknown) => codeFilter3_(filter, value)
|
|
70
|
+
const evalExpr = (value: unknown, expression: ComputedProjectionMathIrExpression): number => {
|
|
71
|
+
switch (expression._tag) {
|
|
72
|
+
case "field": {
|
|
73
|
+
const v = get(value, expression.field)
|
|
74
|
+
return typeof v === "number" ? v : Number(v) || 0
|
|
75
|
+
}
|
|
76
|
+
case "mul":
|
|
77
|
+
return evalExpr(value, expression.left) * evalExpr(value, expression.right)
|
|
78
|
+
default:
|
|
79
|
+
return assertUnreachable(expression)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
66
82
|
switch (computed._tag) {
|
|
67
83
|
case "relation-count":
|
|
68
84
|
return relation.reduce<number>((acc, value) => matches(value) ? acc + 1 : acc, 0)
|
|
@@ -83,6 +99,29 @@ const computeProjectionValue = (
|
|
|
83
99
|
const v = get(value, computed.field)
|
|
84
100
|
return acc + (typeof v === "number" ? v : Number(v) || 0)
|
|
85
101
|
}, 0)
|
|
102
|
+
case "relation-sum-expr":
|
|
103
|
+
return relation.reduce<number>((acc, value) => {
|
|
104
|
+
if (!matches(value)) return acc
|
|
105
|
+
return acc + evalExpr(value, computed.expression)
|
|
106
|
+
}, 0)
|
|
107
|
+
case "relation-sum-expr-by": {
|
|
108
|
+
const totals = new Map<unknown, number>()
|
|
109
|
+
for (const value of relation) {
|
|
110
|
+
if (!matches(value)) continue
|
|
111
|
+
const unit = get(value, computed.unit)
|
|
112
|
+
const current = totals.get(unit) ?? 0
|
|
113
|
+
totals.set(unit, current + evalExpr(value, computed.expression))
|
|
114
|
+
}
|
|
115
|
+
return [...totals.entries()].map(([unit, total]) => ({ unit, total }))
|
|
116
|
+
}
|
|
117
|
+
case "relation-sum-expr-normalized":
|
|
118
|
+
return relation.reduce<number>((acc, value) => {
|
|
119
|
+
if (!matches(value)) return acc
|
|
120
|
+
const unit = get(value, computed.unit)
|
|
121
|
+
const factor = unit === computed.toBase ? 1 : computed.factors[String(unit)]
|
|
122
|
+
if (factor === undefined || !Number.isFinite(factor)) return acc
|
|
123
|
+
return acc + evalExpr(value, computed.expression) * factor
|
|
124
|
+
}, 0)
|
|
86
125
|
case "relation-collect": {
|
|
87
126
|
const out: unknown[] = []
|
|
88
127
|
const seen = computed.distinct ? new Set<unknown>() : undefined
|
package/src/Store/SQL/query.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Effect, type NonEmptyReadonlyArray } from "effect-app"
|
|
|
3
3
|
import { assertUnreachable } from "effect-app/utils"
|
|
4
4
|
import { InfraLogger } from "../../logger.js"
|
|
5
5
|
import type { FilterR, FilterResult } from "../../Model/filter/filterApi.js"
|
|
6
|
-
import type { ComputedProjectionIrExpression } from "../../Model/query.js"
|
|
6
|
+
import type { ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "../../Model/query.js"
|
|
7
7
|
import { isRelationCheck } from "../codeFilter.js"
|
|
8
8
|
|
|
9
9
|
export interface SQLDialect {
|
|
@@ -148,6 +148,8 @@ const dottedToJsonPath = (path: string) =>
|
|
|
148
148
|
.filter((p) => p !== "-1")
|
|
149
149
|
.join(".")
|
|
150
150
|
|
|
151
|
+
const sqlStringLiteral = (value: string) => `'${value.replaceAll("'", "''")}'`
|
|
152
|
+
|
|
151
153
|
export function buildWhereSQLQuery(
|
|
152
154
|
dialect: SQLDialect,
|
|
153
155
|
idKey: PropertyKey,
|
|
@@ -165,7 +167,8 @@ export function buildWhereSQLQuery(
|
|
|
165
167
|
>,
|
|
166
168
|
order?: NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }>,
|
|
167
169
|
skip?: number,
|
|
168
|
-
limit?: number
|
|
170
|
+
limit?: number,
|
|
171
|
+
namespace?: string
|
|
169
172
|
) {
|
|
170
173
|
const params: unknown[] = []
|
|
171
174
|
let paramIndex = 1
|
|
@@ -391,6 +394,23 @@ export function buildWhereSQLQuery(
|
|
|
391
394
|
const relationPath = dottedToJsonPath(computed.path)
|
|
392
395
|
const relationAlias = `_${computed.path}`
|
|
393
396
|
const relationFrom = dialect.jsonEachFrom(relationPath, relationAlias)
|
|
397
|
+
const toNumber = (expr: string) =>
|
|
398
|
+
dialect.jsonColumnType === "JSON" ? `CAST(${expr} AS REAL)` : `(${expr})::numeric`
|
|
399
|
+
const compileExpr = (expression: ComputedProjectionMathIrExpression): string => {
|
|
400
|
+
switch (expression._tag) {
|
|
401
|
+
case "field":
|
|
402
|
+
return toNumber(dialect.jsonExtractElement(relationAlias, expression.field))
|
|
403
|
+
case "mul":
|
|
404
|
+
return `(${compileExpr(expression.left)} * ${compileExpr(expression.right)})`
|
|
405
|
+
default:
|
|
406
|
+
return assertUnreachable(expression)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const factorCaseExpr = (unitExpr: string, toBase: string, factors: Readonly<Record<string, number>>) => {
|
|
410
|
+
const entries = Object.entries(factors).filter(([, factor]) => Number.isFinite(factor))
|
|
411
|
+
const cases = entries.map(([unit, factor]) => ` WHEN ${sqlStringLiteral(unit)} THEN ${factor}`).join("")
|
|
412
|
+
return `CASE ${unitExpr} WHEN ${sqlStringLiteral(toBase)} THEN 1${cases} ELSE NULL END`
|
|
413
|
+
}
|
|
394
414
|
const whereClause = () =>
|
|
395
415
|
computed.filter.length > 0
|
|
396
416
|
? ` WHERE ${print(computed.filter, computed.path, false)}`
|
|
@@ -417,10 +437,25 @@ export function buildWhereSQLQuery(
|
|
|
417
437
|
}
|
|
418
438
|
case "relation-sum": {
|
|
419
439
|
const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
440
|
+
return `(SELECT COALESCE(SUM(${toNumber(fieldExtract)}), 0) FROM ${relationFrom}${whereClause()}) AS "${key}"`
|
|
441
|
+
}
|
|
442
|
+
case "relation-sum-expr": {
|
|
443
|
+
const expression = compileExpr(computed.expression)
|
|
444
|
+
return `(SELECT COALESCE(SUM(${expression}), 0) FROM ${relationFrom}${whereClause()}) AS "${key}"`
|
|
445
|
+
}
|
|
446
|
+
case "relation-sum-expr-by": {
|
|
447
|
+
const expression = compileExpr(computed.expression)
|
|
448
|
+
const unitExpr = dialect.jsonExtractElement(relationAlias, computed.unit)
|
|
449
|
+
if (dialect.jsonColumnType === "JSON") {
|
|
450
|
+
return `(SELECT COALESCE(json_group_array(json_object('unit', __unit, 'total', __total)), json_array()) FROM (SELECT ${unitExpr} AS __unit, COALESCE(SUM(${expression}), 0) AS __total FROM ${relationFrom}${whereClause()} GROUP BY ${unitExpr})) AS "${key}"`
|
|
451
|
+
}
|
|
452
|
+
return `(SELECT COALESCE(jsonb_agg(jsonb_build_object('unit', __unit, 'total', __total)), '[]'::jsonb) FROM (SELECT ${unitExpr} AS __unit, COALESCE(SUM(${expression}), 0) AS __total FROM ${relationFrom}${whereClause()} GROUP BY ${unitExpr}) __grouped) AS "${key}"`
|
|
453
|
+
}
|
|
454
|
+
case "relation-sum-expr-normalized": {
|
|
455
|
+
const expression = compileExpr(computed.expression)
|
|
456
|
+
const unitExpr = dialect.jsonExtractElement(relationAlias, computed.unit)
|
|
457
|
+
const factorExpr = factorCaseExpr(unitExpr, computed.toBase, computed.factors)
|
|
458
|
+
return `(SELECT COALESCE(SUM((${expression}) * (${factorExpr})), 0) FROM ${relationFrom}${whereClause()}) AS "${key}"`
|
|
424
459
|
}
|
|
425
460
|
case "relation-collect": {
|
|
426
461
|
const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
|
|
@@ -455,8 +490,22 @@ export function buildWhereSQLQuery(
|
|
|
455
490
|
return fields.join(", ")
|
|
456
491
|
}
|
|
457
492
|
|
|
458
|
-
|
|
459
|
-
|
|
493
|
+
// Order matters: projection params must be emitted BEFORE user-filter
|
|
494
|
+
// params so positional `?` placeholders in SQLite match `params[]` order.
|
|
495
|
+
const selectExpr = getSelectExpr()
|
|
496
|
+
|
|
497
|
+
const namespaceClause = namespace !== undefined
|
|
498
|
+
? `_namespace = ${addParam(namespace)}`
|
|
499
|
+
: ""
|
|
500
|
+
const userWhere = filter.length
|
|
501
|
+
? print([{ t: "where-scope", result: filter, relation: "some" }], null, false)
|
|
502
|
+
: ""
|
|
503
|
+
const whereClause = namespaceClause && userWhere
|
|
504
|
+
? `WHERE ${namespaceClause} AND ${userWhere}`
|
|
505
|
+
: namespaceClause
|
|
506
|
+
? `WHERE ${namespaceClause}`
|
|
507
|
+
: userWhere
|
|
508
|
+
? `WHERE ${userWhere}`
|
|
460
509
|
: ""
|
|
461
510
|
|
|
462
511
|
const orderClause = order
|
|
@@ -467,7 +516,7 @@ export function buildWhereSQLQuery(
|
|
|
467
516
|
? `LIMIT ${addParam(limit ?? 999999)} OFFSET ${addParam(skip ?? 0)}`
|
|
468
517
|
: ""
|
|
469
518
|
|
|
470
|
-
const sql = `SELECT ${
|
|
519
|
+
const sql = `SELECT ${selectExpr} FROM "${tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim()
|
|
471
520
|
|
|
472
521
|
return { sql, params }
|
|
473
522
|
}
|
package/src/Store/SQL.ts
CHANGED
|
@@ -229,7 +229,7 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
|
|
|
229
229
|
.flatMap((ns) =>
|
|
230
230
|
Effect
|
|
231
231
|
.sync(() => {
|
|
232
|
-
|
|
232
|
+
return buildWhereSQLQuery(
|
|
233
233
|
dialect,
|
|
234
234
|
idKey,
|
|
235
235
|
filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
|
|
@@ -252,29 +252,9 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
|
|
|
252
252
|
f
|
|
253
253
|
.skip,
|
|
254
254
|
f
|
|
255
|
-
.limit
|
|
255
|
+
.limit,
|
|
256
|
+
ns
|
|
256
257
|
)
|
|
257
|
-
const hasWhere = q
|
|
258
|
-
.sql
|
|
259
|
-
.includes("WHERE")
|
|
260
|
-
const nsSql = hasWhere
|
|
261
|
-
? q
|
|
262
|
-
.sql
|
|
263
|
-
.replace("WHERE", `WHERE _namespace = ? AND`)
|
|
264
|
-
: q
|
|
265
|
-
.sql
|
|
266
|
-
.replace(
|
|
267
|
-
`FROM "${tableName}"`,
|
|
268
|
-
`FROM "${tableName}" WHERE _namespace = ?`
|
|
269
|
-
)
|
|
270
|
-
return {
|
|
271
|
-
sql: nsSql,
|
|
272
|
-
params: [
|
|
273
|
-
ns,
|
|
274
|
-
...q
|
|
275
|
-
.params
|
|
276
|
-
]
|
|
277
|
-
}
|
|
278
258
|
})
|
|
279
259
|
.pipe(
|
|
280
260
|
Effect
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rawQuery.test.d.ts","sourceRoot":"","sources":["../rawQuery.test.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rawQuery.test.d.ts","sourceRoot":"","sources":["../rawQuery.test.ts"],"names":[],"mappings":"AAEA,OAAO,EAA+C,cAAc,EAA2C,MAAM,YAAY,CAAA;AAUjI,eAAO,MAAM,EAAE,6CAWb,CAAA"}
|
package/test/query.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Context, Effect, flow, Layer, Option, pipe, S, Struct } from "effect-ap
|
|
|
6
6
|
import { inspect } from "util"
|
|
7
7
|
import { expect, expectTypeOf, it } from "vitest"
|
|
8
8
|
import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js"
|
|
9
|
-
import { and, computed, count, make, one, or, order, page, project, projectComputed, type QueryEnd, type QueryProjection, type QueryWhere, relation, toFilter, where } from "../src/Model/query.js"
|
|
9
|
+
import { and, computed, count, expr, make, one, or, order, page, project, projectComputed, type QueryEnd, type QueryProjection, type QueryWhere, relation, toFilter, where } from "../src/Model/query.js"
|
|
10
10
|
import { makeRepo } from "../src/Model/Repository.js"
|
|
11
11
|
import { RepositoryRegistryLive } from "../src/Model/Repository/Registry.js"
|
|
12
12
|
import { memFilter, MemoryStoreLive } from "../src/Store/Memory.js"
|
|
@@ -730,6 +730,61 @@ it("projectComputed.collect / collectDistinct emit relation-collect IR", () => {
|
|
|
730
730
|
expect((distinct as { distinct: boolean } | undefined)?.distinct).toBe(true)
|
|
731
731
|
})
|
|
732
732
|
|
|
733
|
+
it("projectComputed.sumExpr emits relation-sum-expr IR", () => {
|
|
734
|
+
const baseSchema = S.Struct({
|
|
735
|
+
id: S.String,
|
|
736
|
+
items: S.Array(S.Struct({
|
|
737
|
+
weight: S.Number,
|
|
738
|
+
tradeUnit: S.Struct({ amount: S.Number, unit: S.String })
|
|
739
|
+
}))
|
|
740
|
+
})
|
|
741
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
742
|
+
projectComputed(
|
|
743
|
+
S.Struct({ total: S.Number }),
|
|
744
|
+
computed({
|
|
745
|
+
total: relation<S.Codec.Encoded<typeof baseSchema>>("items").sumExpr(
|
|
746
|
+
expr.mul(expr.field("weight"), expr.field("tradeUnit.amount"))
|
|
747
|
+
)
|
|
748
|
+
})
|
|
749
|
+
)
|
|
750
|
+
)
|
|
751
|
+
const interpreted = toFilter(query, baseSchema)
|
|
752
|
+
const ir = interpreted.computed?.["total"]
|
|
753
|
+
expect(ir?._tag).toBe("relation-sum-expr")
|
|
754
|
+
expect((ir as { expression: unknown } | undefined)?.expression).toEqual({
|
|
755
|
+
_tag: "mul",
|
|
756
|
+
left: { _tag: "field", field: "weight" },
|
|
757
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
758
|
+
})
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
it("projectComputed.sumExprBy emits relation-sum-expr-by IR", () => {
|
|
762
|
+
const baseSchema = S.Struct({
|
|
763
|
+
id: S.String,
|
|
764
|
+
items: S.Array(S.Struct({
|
|
765
|
+
weight: S.Number,
|
|
766
|
+
tradeUnit: S.Struct({ amount: S.Number, unit: S.String })
|
|
767
|
+
}))
|
|
768
|
+
})
|
|
769
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
770
|
+
projectComputed(
|
|
771
|
+
S.Struct({
|
|
772
|
+
totals: S.Array(S.Struct({ unit: S.String, total: S.Number }))
|
|
773
|
+
}),
|
|
774
|
+
computed({
|
|
775
|
+
totals: relation<S.Codec.Encoded<typeof baseSchema>>("items").sumExprBy(
|
|
776
|
+
expr.mul(expr.field("weight"), expr.field("tradeUnit.amount")),
|
|
777
|
+
{ unit: "tradeUnit.unit" }
|
|
778
|
+
)
|
|
779
|
+
})
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
const interpreted = toFilter(query, baseSchema)
|
|
783
|
+
const ir = interpreted.computed?.["totals"]
|
|
784
|
+
expect(ir?._tag).toBe("relation-sum-expr-by")
|
|
785
|
+
expect((ir as { unit: string } | undefined)?.unit).toBe("tradeUnit.unit")
|
|
786
|
+
})
|
|
787
|
+
|
|
733
788
|
it(
|
|
734
789
|
"doesn't mess when refining fields",
|
|
735
790
|
() =>
|
|
@@ -1584,6 +1639,57 @@ it("memFilter: computed projection with multi-statement relation filter", () =>
|
|
|
1584
1639
|
])
|
|
1585
1640
|
})
|
|
1586
1641
|
|
|
1642
|
+
it("memFilter: relation-sum-expr / sum-expr-by / sum-expr-normalized", () => {
|
|
1643
|
+
const schema = S.Struct({
|
|
1644
|
+
id: S.String,
|
|
1645
|
+
items: S.Array(S.Struct({
|
|
1646
|
+
weight: S.Finite,
|
|
1647
|
+
tradeUnit: S.Struct({ amount: S.Finite, unit: S.String })
|
|
1648
|
+
}))
|
|
1649
|
+
})
|
|
1650
|
+
type Row = S.Codec.Encoded<typeof schema>
|
|
1651
|
+
const rows: Row[] = [
|
|
1652
|
+
{
|
|
1653
|
+
id: "r1",
|
|
1654
|
+
items: [
|
|
1655
|
+
{ weight: 2, tradeUnit: { amount: 5, unit: "kg" } },
|
|
1656
|
+
{ weight: 4, tradeUnit: { amount: 1000, unit: "g" } },
|
|
1657
|
+
{ weight: 3, tradeUnit: { amount: 1, unit: "kg" } }
|
|
1658
|
+
]
|
|
1659
|
+
},
|
|
1660
|
+
{ id: "r2", items: [] }
|
|
1661
|
+
]
|
|
1662
|
+
const weighted = expr.mul(expr.field("weight"), expr.field("tradeUnit.amount"))
|
|
1663
|
+
const q = make<Row>().pipe(
|
|
1664
|
+
projectComputed(
|
|
1665
|
+
S.Struct({
|
|
1666
|
+
id: S.String,
|
|
1667
|
+
totalRaw: S.Finite,
|
|
1668
|
+
totalsByUnit: S.Array(S.Struct({ unit: S.String, total: S.Finite })),
|
|
1669
|
+
totalKg: S.Finite
|
|
1670
|
+
}),
|
|
1671
|
+
computed({
|
|
1672
|
+
totalRaw: relation<Row>("items").sumExpr(weighted, where("weight", "gte", 0)),
|
|
1673
|
+
totalsByUnit: relation<Row>("items").sumExprBy(weighted, { unit: "tradeUnit.unit" }, where("weight", "gte", 0)),
|
|
1674
|
+
totalKg: relation<Row>("items").sumExprNormalized(weighted, {
|
|
1675
|
+
unit: "tradeUnit.unit",
|
|
1676
|
+
toBase: "kg",
|
|
1677
|
+
factors: { g: 0.001 }
|
|
1678
|
+
}, where("weight", "gte", 0))
|
|
1679
|
+
})
|
|
1680
|
+
)
|
|
1681
|
+
)
|
|
1682
|
+
expect(memFilter(toFilter(q, schema))(rows)).toEqual([
|
|
1683
|
+
{
|
|
1684
|
+
id: "r1",
|
|
1685
|
+
totalRaw: 4013,
|
|
1686
|
+
totalsByUnit: [{ unit: "kg", total: 13 }, { unit: "g", total: 4000 }],
|
|
1687
|
+
totalKg: 17
|
|
1688
|
+
},
|
|
1689
|
+
{ id: "r2", totalRaw: 0, totalsByUnit: [], totalKg: 0 }
|
|
1690
|
+
])
|
|
1691
|
+
})
|
|
1692
|
+
|
|
1587
1693
|
it("memFilter: computed projection combined with root where filter", () => {
|
|
1588
1694
|
const q = make<ComputedBase>().pipe(
|
|
1589
1695
|
where("id", "neq", "r3"),
|
|
@@ -1680,8 +1786,7 @@ const cfRows: CFRow[] = [
|
|
|
1680
1786
|
{ id: "4", tag: "y", qty: 30, desc: "World cup", tags: [], nested: { kind: "k2", v: 0 } }
|
|
1681
1787
|
]
|
|
1682
1788
|
|
|
1683
|
-
const runCF = (q: any) =>
|
|
1684
|
-
(memFilter(toFilter(q))(cfRows) as unknown as readonly CFRow[]).map((_) => _.id)
|
|
1789
|
+
const runCF = (q: any) => (memFilter(toFilter(q))(cfRows) as unknown as readonly CFRow[]).map((_) => _.id)
|
|
1685
1790
|
|
|
1686
1791
|
it("codeFilter: where + and chain", () => {
|
|
1687
1792
|
const q = make<CFRow>().pipe(
|