@effect-app/infra 4.0.0-beta.216 → 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 +8 -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 +1 -1
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +37 -5
- package/dist/Store/SQL.d.ts +1 -1
- 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 +2 -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 +39 -5
- package/test/dist/rawQuery.test.d.ts.map +1 -1
- package/test/query.test.ts +108 -3
- package/test/rawQuery.test.ts +5 -9
- package/test/sql-store.test.ts +166 -0
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,
|
|
@@ -392,6 +394,23 @@ export function buildWhereSQLQuery(
|
|
|
392
394
|
const relationPath = dottedToJsonPath(computed.path)
|
|
393
395
|
const relationAlias = `_${computed.path}`
|
|
394
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
|
+
}
|
|
395
414
|
const whereClause = () =>
|
|
396
415
|
computed.filter.length > 0
|
|
397
416
|
? ` WHERE ${print(computed.filter, computed.path, false)}`
|
|
@@ -418,10 +437,25 @@ export function buildWhereSQLQuery(
|
|
|
418
437
|
}
|
|
419
438
|
case "relation-sum": {
|
|
420
439
|
const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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}"`
|
|
425
459
|
}
|
|
426
460
|
case "relation-collect": {
|
|
427
461
|
const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
|
|
@@ -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(
|
package/test/rawQuery.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SqliteClient } from "@effect/sql-sqlite-node"
|
|
1
2
|
import { describe, expect, it } from "@effect/vitest"
|
|
2
3
|
import { Array, Config, Context, Effect, flow, Layer, ManagedRuntime, Redacted, References, Result, S, Struct } from "effect-app"
|
|
3
4
|
import { LogLevels } from "effect-app/utils"
|
|
@@ -5,7 +6,6 @@ import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js"
|
|
|
5
6
|
import { and, computed, or, project, projectComputed, relation, where, whereEvery, whereSome } from "../src/Model/query.js"
|
|
6
7
|
import { makeRepo } from "../src/Model/Repository/makeRepo.js"
|
|
7
8
|
import { RepositoryRegistryLive } from "../src/Model/Repository/Registry.js"
|
|
8
|
-
import { SqliteClient } from "@effect/sql-sqlite-node"
|
|
9
9
|
import { CosmosStoreLayer } from "../src/Store/Cosmos.js"
|
|
10
10
|
import { MemoryStoreLive } from "../src/Store/Memory.js"
|
|
11
11
|
import { SQLiteStoreLayer } from "../src/Store/SQL.js"
|
|
@@ -596,11 +596,9 @@ describe("scanner-style AllPickList computed projections", () => {
|
|
|
596
596
|
})
|
|
597
597
|
.pipe(setupRequestContextFromCurrent())
|
|
598
598
|
|
|
599
|
-
it("works well in Memory", () =>
|
|
600
|
-
test.pipe(Effect.provide(OrderRepo.Test), rt.runPromise))
|
|
599
|
+
it("works well in Memory", () => test.pipe(Effect.provide(OrderRepo.Test), rt.runPromise))
|
|
601
600
|
|
|
602
|
-
it("works well in SQLite", () =>
|
|
603
|
-
test.pipe(Effect.provide(OrderRepo.TestSqlite), rt.runPromise))
|
|
601
|
+
it("works well in SQLite", () => test.pipe(Effect.provide(OrderRepo.TestSqlite), rt.runPromise))
|
|
604
602
|
})
|
|
605
603
|
|
|
606
604
|
// Same but mimics the FULL controller projection: includes `items` array
|
|
@@ -647,11 +645,9 @@ describe("scanner-style AllPickList — items + computed combined", () => {
|
|
|
647
645
|
})
|
|
648
646
|
.pipe(setupRequestContextFromCurrent())
|
|
649
647
|
|
|
650
|
-
it("works well in Memory", () =>
|
|
651
|
-
test.pipe(Effect.provide(OrderRepo.Test), rt.runPromise))
|
|
648
|
+
it("works well in Memory", () => test.pipe(Effect.provide(OrderRepo.Test), rt.runPromise))
|
|
652
649
|
|
|
653
|
-
it("works well in SQLite", () =>
|
|
654
|
-
test.pipe(Effect.provide(OrderRepo.TestSqlite), rt.runPromise))
|
|
650
|
+
it("works well in SQLite", () => test.pipe(Effect.provide(OrderRepo.TestSqlite), rt.runPromise))
|
|
655
651
|
})
|
|
656
652
|
|
|
657
653
|
describe("removeByIds", () => {
|
package/test/sql-store.test.ts
CHANGED
|
@@ -345,6 +345,90 @@ describe("SQL query builder (SQLite dialect)", () => {
|
|
|
345
345
|
expect(result.sql).toContain(`AS "totalWeight"`)
|
|
346
346
|
})
|
|
347
347
|
|
|
348
|
+
it("computed relation-sum-expr projection (sqlite)", () => {
|
|
349
|
+
const result = buildWhereSQLQuery(
|
|
350
|
+
sqliteDialect,
|
|
351
|
+
"id",
|
|
352
|
+
[],
|
|
353
|
+
"users",
|
|
354
|
+
{},
|
|
355
|
+
[{
|
|
356
|
+
key: "totalWeighted",
|
|
357
|
+
computed: {
|
|
358
|
+
_tag: "relation-sum-expr",
|
|
359
|
+
path: "items",
|
|
360
|
+
expression: {
|
|
361
|
+
_tag: "mul",
|
|
362
|
+
left: { _tag: "field", field: "weight" },
|
|
363
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
364
|
+
},
|
|
365
|
+
filter: []
|
|
366
|
+
}
|
|
367
|
+
}]
|
|
368
|
+
)
|
|
369
|
+
expect(result.sql).toContain(
|
|
370
|
+
`COALESCE(SUM((CAST(json_extract(_items.value, '$.weight') AS REAL) * CAST(json_extract(_items.value, '$.tradeUnit.amount') AS REAL))), 0)`
|
|
371
|
+
)
|
|
372
|
+
expect(result.sql).toContain(`AS "totalWeighted"`)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it("computed relation-sum-expr-by projection (sqlite)", () => {
|
|
376
|
+
const result = buildWhereSQLQuery(
|
|
377
|
+
sqliteDialect,
|
|
378
|
+
"id",
|
|
379
|
+
[],
|
|
380
|
+
"users",
|
|
381
|
+
{},
|
|
382
|
+
[{
|
|
383
|
+
key: "totalsByUnit",
|
|
384
|
+
computed: {
|
|
385
|
+
_tag: "relation-sum-expr-by",
|
|
386
|
+
path: "items",
|
|
387
|
+
expression: {
|
|
388
|
+
_tag: "mul",
|
|
389
|
+
left: { _tag: "field", field: "weight" },
|
|
390
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
391
|
+
},
|
|
392
|
+
unit: "tradeUnit.unit",
|
|
393
|
+
filter: []
|
|
394
|
+
}
|
|
395
|
+
}]
|
|
396
|
+
)
|
|
397
|
+
expect(result.sql).toContain(`json_group_array(json_object('unit', __unit, 'total', __total))`)
|
|
398
|
+
expect(result.sql).toContain(`GROUP BY json_extract(_items.value, '$.tradeUnit.unit')`)
|
|
399
|
+
expect(result.sql).toContain(`AS "totalsByUnit"`)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it("computed relation-sum-expr-normalized projection (sqlite)", () => {
|
|
403
|
+
const result = buildWhereSQLQuery(
|
|
404
|
+
sqliteDialect,
|
|
405
|
+
"id",
|
|
406
|
+
[],
|
|
407
|
+
"users",
|
|
408
|
+
{},
|
|
409
|
+
[{
|
|
410
|
+
key: "totalKg",
|
|
411
|
+
computed: {
|
|
412
|
+
_tag: "relation-sum-expr-normalized",
|
|
413
|
+
path: "items",
|
|
414
|
+
expression: {
|
|
415
|
+
_tag: "mul",
|
|
416
|
+
left: { _tag: "field", field: "weight" },
|
|
417
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
418
|
+
},
|
|
419
|
+
unit: "tradeUnit.unit",
|
|
420
|
+
toBase: "kg",
|
|
421
|
+
factors: { g: 0.001 },
|
|
422
|
+
filter: []
|
|
423
|
+
}
|
|
424
|
+
}]
|
|
425
|
+
)
|
|
426
|
+
expect(result.sql).toContain(
|
|
427
|
+
`CASE json_extract(_items.value, '$.tradeUnit.unit') WHEN 'kg' THEN 1 WHEN 'g' THEN 0.001 ELSE NULL END`
|
|
428
|
+
)
|
|
429
|
+
expect(result.sql).toContain(`AS "totalKg"`)
|
|
430
|
+
})
|
|
431
|
+
|
|
348
432
|
it("computed relation-collect (non-distinct) projection (sqlite)", () => {
|
|
349
433
|
const result = buildWhereSQLQuery(
|
|
350
434
|
sqliteDialect,
|
|
@@ -515,6 +599,88 @@ describe("SQL query builder (PostgreSQL dialect)", () => {
|
|
|
515
599
|
expect(result.sql).toContain(`AS "totalWeight"`)
|
|
516
600
|
})
|
|
517
601
|
|
|
602
|
+
it("computed relation-sum-expr (pg)", () => {
|
|
603
|
+
const result = buildWhereSQLQuery(
|
|
604
|
+
pgDialect,
|
|
605
|
+
"id",
|
|
606
|
+
[],
|
|
607
|
+
"users",
|
|
608
|
+
{},
|
|
609
|
+
[{
|
|
610
|
+
key: "totalWeighted",
|
|
611
|
+
computed: {
|
|
612
|
+
_tag: "relation-sum-expr",
|
|
613
|
+
path: "items",
|
|
614
|
+
expression: {
|
|
615
|
+
_tag: "mul",
|
|
616
|
+
left: { _tag: "field", field: "weight" },
|
|
617
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
618
|
+
},
|
|
619
|
+
filter: []
|
|
620
|
+
}
|
|
621
|
+
}]
|
|
622
|
+
)
|
|
623
|
+
expect(result.sql).toContain(
|
|
624
|
+
`COALESCE(SUM(((_items->>'weight')::numeric * (_items->'tradeUnit'->>'amount')::numeric)), 0)`
|
|
625
|
+
)
|
|
626
|
+
expect(result.sql).toContain(`AS "totalWeighted"`)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it("computed relation-sum-expr-by (pg)", () => {
|
|
630
|
+
const result = buildWhereSQLQuery(
|
|
631
|
+
pgDialect,
|
|
632
|
+
"id",
|
|
633
|
+
[],
|
|
634
|
+
"users",
|
|
635
|
+
{},
|
|
636
|
+
[{
|
|
637
|
+
key: "totalsByUnit",
|
|
638
|
+
computed: {
|
|
639
|
+
_tag: "relation-sum-expr-by",
|
|
640
|
+
path: "items",
|
|
641
|
+
expression: {
|
|
642
|
+
_tag: "mul",
|
|
643
|
+
left: { _tag: "field", field: "weight" },
|
|
644
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
645
|
+
},
|
|
646
|
+
unit: "tradeUnit.unit",
|
|
647
|
+
filter: []
|
|
648
|
+
}
|
|
649
|
+
}]
|
|
650
|
+
)
|
|
651
|
+
expect(result.sql).toContain(`jsonb_agg(jsonb_build_object('unit', __unit, 'total', __total))`)
|
|
652
|
+
expect(result.sql).toContain(`GROUP BY _items->'tradeUnit'->>'unit'`)
|
|
653
|
+
expect(result.sql).toContain(`AS "totalsByUnit"`)
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
it("computed relation-sum-expr-normalized (pg)", () => {
|
|
657
|
+
const result = buildWhereSQLQuery(
|
|
658
|
+
pgDialect,
|
|
659
|
+
"id",
|
|
660
|
+
[],
|
|
661
|
+
"users",
|
|
662
|
+
{},
|
|
663
|
+
[{
|
|
664
|
+
key: "totalKg",
|
|
665
|
+
computed: {
|
|
666
|
+
_tag: "relation-sum-expr-normalized",
|
|
667
|
+
path: "items",
|
|
668
|
+
expression: {
|
|
669
|
+
_tag: "mul",
|
|
670
|
+
left: { _tag: "field", field: "weight" },
|
|
671
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
672
|
+
},
|
|
673
|
+
unit: "tradeUnit.unit",
|
|
674
|
+
toBase: "kg",
|
|
675
|
+
factors: { g: 0.001 },
|
|
676
|
+
filter: []
|
|
677
|
+
}
|
|
678
|
+
}]
|
|
679
|
+
)
|
|
680
|
+
expect(result.sql).toContain(`CASE _items->'tradeUnit'->>'unit' WHEN 'kg' THEN 1 WHEN 'g' THEN 0.001 ELSE NULL END`)
|
|
681
|
+
expect(result.sql).toContain(`AS "totalKg"`)
|
|
682
|
+
})
|
|
683
|
+
|
|
518
684
|
it("computed relation-collect (pg jsonb_agg)", () => {
|
|
519
685
|
const result = buildWhereSQLQuery(
|
|
520
686
|
pgDialect,
|