@effect-app/infra 4.0.0-beta.216 → 4.0.0-beta.218
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 +24 -0
- package/dist/Model/Repository/internal/internal.d.ts +1 -1
- package/dist/Model/query/dsl.d.ts +49 -1
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +77 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +34 -1
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +41 -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 +42 -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 +64 -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 +54 -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 +139 -0
- package/src/Model/query/new-kid-interpreter.ts +80 -0
- package/src/Store/Cosmos/query.ts +45 -1
- package/src/Store/Memory.ts +58 -1
- package/src/Store/SQL/query.ts +56 -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
|
@@ -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
|
|
@@ -43,6 +76,13 @@ export type ComputedProjectionIrExpression =
|
|
|
43
76
|
readonly distinct: boolean
|
|
44
77
|
readonly filter: readonly FilterResult[]
|
|
45
78
|
}
|
|
79
|
+
| {
|
|
80
|
+
readonly _tag: "relation-collect-fields"
|
|
81
|
+
readonly path: string
|
|
82
|
+
readonly fields: readonly string[]
|
|
83
|
+
readonly distinct: boolean
|
|
84
|
+
readonly filter: readonly FilterResult[]
|
|
85
|
+
}
|
|
46
86
|
|
|
47
87
|
type Result<TFieldValues extends FieldValues, A = TFieldValues, R = never> = {
|
|
48
88
|
filter: FilterResult[]
|
|
@@ -197,6 +237,35 @@ const interpret = <
|
|
|
197
237
|
key,
|
|
198
238
|
{ _tag: e._tag, path: e.path, field: e.field, filter } as ComputedProjectionIrExpression
|
|
199
239
|
]
|
|
240
|
+
case "relation-sum-expr":
|
|
241
|
+
return [
|
|
242
|
+
key,
|
|
243
|
+
{ _tag: e._tag, path: e.path, expression: e.expression, filter } as ComputedProjectionIrExpression
|
|
244
|
+
]
|
|
245
|
+
case "relation-sum-expr-by":
|
|
246
|
+
return [
|
|
247
|
+
key,
|
|
248
|
+
{
|
|
249
|
+
_tag: e._tag,
|
|
250
|
+
path: e.path,
|
|
251
|
+
expression: e.expression,
|
|
252
|
+
unit: e.unit,
|
|
253
|
+
filter
|
|
254
|
+
} as ComputedProjectionIrExpression
|
|
255
|
+
]
|
|
256
|
+
case "relation-sum-expr-normalized":
|
|
257
|
+
return [
|
|
258
|
+
key,
|
|
259
|
+
{
|
|
260
|
+
_tag: e._tag,
|
|
261
|
+
path: e.path,
|
|
262
|
+
expression: e.expression,
|
|
263
|
+
unit: e.unit,
|
|
264
|
+
toBase: e.toBase,
|
|
265
|
+
factors: e.factors,
|
|
266
|
+
filter
|
|
267
|
+
} as ComputedProjectionIrExpression
|
|
268
|
+
]
|
|
200
269
|
case "relation-collect":
|
|
201
270
|
return [
|
|
202
271
|
key,
|
|
@@ -208,6 +277,17 @@ const interpret = <
|
|
|
208
277
|
filter
|
|
209
278
|
} as ComputedProjectionIrExpression
|
|
210
279
|
]
|
|
280
|
+
case "relation-collect-fields":
|
|
281
|
+
return [
|
|
282
|
+
key,
|
|
283
|
+
{
|
|
284
|
+
_tag: e._tag,
|
|
285
|
+
path: e.path,
|
|
286
|
+
fields: e.fields,
|
|
287
|
+
distinct: e.distinct,
|
|
288
|
+
filter
|
|
289
|
+
} as ComputedProjectionIrExpression
|
|
290
|
+
]
|
|
211
291
|
}
|
|
212
292
|
})
|
|
213
293
|
)
|
|
@@ -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) {
|
|
@@ -326,6 +358,18 @@ export function buildWhereCosmosQuery3(
|
|
|
326
358
|
}
|
|
327
359
|
return `ARRAY(SELECT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
328
360
|
}
|
|
361
|
+
case "relation-collect-fields": {
|
|
362
|
+
const subqueries = computed.fields.map((field) => {
|
|
363
|
+
const fieldRef = dottedToAccess(`${relationAlias}.${field}`)
|
|
364
|
+
return computed.distinct
|
|
365
|
+
? `ARRAY(SELECT DISTINCT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where})`
|
|
366
|
+
: `ARRAY(SELECT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where})`
|
|
367
|
+
})
|
|
368
|
+
const combined = computed.distinct
|
|
369
|
+
? subqueries.reduce((acc, sq) => `SetUnion(${acc}, ${sq})`)
|
|
370
|
+
: subqueries.reduce((acc, sq) => `ARRAY_CONCAT(${acc}, ${sq})`)
|
|
371
|
+
return `${combined} AS ${key}`
|
|
372
|
+
}
|
|
329
373
|
}
|
|
330
374
|
}
|
|
331
375
|
// with joins, you should use 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,13 +37,19 @@ 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":
|
|
44
48
|
return true
|
|
45
49
|
case "relation-collect":
|
|
46
50
|
return [] as unknown[]
|
|
51
|
+
case "relation-collect-fields":
|
|
52
|
+
return [] as unknown[]
|
|
47
53
|
default:
|
|
48
54
|
return assertUnreachable(tag)
|
|
49
55
|
}
|
|
@@ -63,6 +69,18 @@ const computeProjectionValue = (
|
|
|
63
69
|
const matches = filter.length === 0
|
|
64
70
|
? (_value: unknown) => true
|
|
65
71
|
: (value: unknown) => codeFilter3_(filter, value)
|
|
72
|
+
const evalExpr = (value: unknown, expression: ComputedProjectionMathIrExpression): number => {
|
|
73
|
+
switch (expression._tag) {
|
|
74
|
+
case "field": {
|
|
75
|
+
const v = get(value, expression.field)
|
|
76
|
+
return typeof v === "number" ? v : Number(v) || 0
|
|
77
|
+
}
|
|
78
|
+
case "mul":
|
|
79
|
+
return evalExpr(value, expression.left) * evalExpr(value, expression.right)
|
|
80
|
+
default:
|
|
81
|
+
return assertUnreachable(expression)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
66
84
|
switch (computed._tag) {
|
|
67
85
|
case "relation-count":
|
|
68
86
|
return relation.reduce<number>((acc, value) => matches(value) ? acc + 1 : acc, 0)
|
|
@@ -83,6 +101,29 @@ const computeProjectionValue = (
|
|
|
83
101
|
const v = get(value, computed.field)
|
|
84
102
|
return acc + (typeof v === "number" ? v : Number(v) || 0)
|
|
85
103
|
}, 0)
|
|
104
|
+
case "relation-sum-expr":
|
|
105
|
+
return relation.reduce<number>((acc, value) => {
|
|
106
|
+
if (!matches(value)) return acc
|
|
107
|
+
return acc + evalExpr(value, computed.expression)
|
|
108
|
+
}, 0)
|
|
109
|
+
case "relation-sum-expr-by": {
|
|
110
|
+
const totals = new Map<unknown, number>()
|
|
111
|
+
for (const value of relation) {
|
|
112
|
+
if (!matches(value)) continue
|
|
113
|
+
const unit = get(value, computed.unit)
|
|
114
|
+
const current = totals.get(unit) ?? 0
|
|
115
|
+
totals.set(unit, current + evalExpr(value, computed.expression))
|
|
116
|
+
}
|
|
117
|
+
return [...totals.entries()].map(([unit, total]) => ({ unit, total }))
|
|
118
|
+
}
|
|
119
|
+
case "relation-sum-expr-normalized":
|
|
120
|
+
return relation.reduce<number>((acc, value) => {
|
|
121
|
+
if (!matches(value)) return acc
|
|
122
|
+
const unit = get(value, computed.unit)
|
|
123
|
+
const factor = unit === computed.toBase ? 1 : computed.factors[String(unit)]
|
|
124
|
+
if (factor === undefined || !Number.isFinite(factor)) return acc
|
|
125
|
+
return acc + evalExpr(value, computed.expression) * factor
|
|
126
|
+
}, 0)
|
|
86
127
|
case "relation-collect": {
|
|
87
128
|
const out: unknown[] = []
|
|
88
129
|
const seen = computed.distinct ? new Set<unknown>() : undefined
|
|
@@ -97,6 +138,22 @@ const computeProjectionValue = (
|
|
|
97
138
|
}
|
|
98
139
|
return out
|
|
99
140
|
}
|
|
141
|
+
case "relation-collect-fields": {
|
|
142
|
+
const out: unknown[] = []
|
|
143
|
+
const seen = computed.distinct ? new Set<unknown>() : undefined
|
|
144
|
+
for (const value of relation) {
|
|
145
|
+
if (!matches(value)) continue
|
|
146
|
+
for (const field of computed.fields) {
|
|
147
|
+
const v = get(value, field)
|
|
148
|
+
if (seen) {
|
|
149
|
+
if (seen.has(v)) continue
|
|
150
|
+
seen.add(v)
|
|
151
|
+
}
|
|
152
|
+
out.push(v)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out
|
|
156
|
+
}
|
|
100
157
|
default:
|
|
101
158
|
return assertUnreachable(computed)
|
|
102
159
|
}
|
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)
|
|
@@ -435,6 +469,23 @@ export function buildWhereSQLQuery(
|
|
|
435
469
|
const aggArg = computed.distinct ? `DISTINCT ${fieldExtract}` : fieldExtract
|
|
436
470
|
return `(SELECT COALESCE(jsonb_agg(${aggArg}), '[]'::jsonb) FROM ${relationFrom}${whereClause()}) AS "${key}"`
|
|
437
471
|
}
|
|
472
|
+
case "relation-collect-fields": {
|
|
473
|
+
const branches = computed.fields.map((field) => {
|
|
474
|
+
const fieldExtract = dialect.jsonExtractElement(relationAlias, field)
|
|
475
|
+
return `SELECT ${fieldExtract} AS __v FROM ${relationFrom}${whereClause()}`
|
|
476
|
+
})
|
|
477
|
+
const unionQuery = branches.join(" UNION ALL ")
|
|
478
|
+
if (dialect.jsonColumnType === "JSON") {
|
|
479
|
+
if (computed.distinct) {
|
|
480
|
+
return `(SELECT COALESCE(json_group_array(__v), json_array()) FROM (SELECT DISTINCT __v FROM (${unionQuery}))) AS "${key}"`
|
|
481
|
+
}
|
|
482
|
+
return `(SELECT COALESCE(json_group_array(__v), json_array()) FROM (${unionQuery})) AS "${key}"`
|
|
483
|
+
}
|
|
484
|
+
if (computed.distinct) {
|
|
485
|
+
return `(SELECT COALESCE(jsonb_agg(__v), '[]'::jsonb) FROM (SELECT DISTINCT __v FROM (${unionQuery}) inner_q) outer_q) AS "${key}"`
|
|
486
|
+
}
|
|
487
|
+
return `(SELECT COALESCE(jsonb_agg(__v), '[]'::jsonb) FROM (${unionQuery}) t) AS "${key}"`
|
|
488
|
+
}
|
|
438
489
|
default:
|
|
439
490
|
return assertUnreachable(computed)
|
|
440
491
|
}
|
|
@@ -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", () => {
|