@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/Model/Repository/internal/internal.d.ts +1 -1
  3. package/dist/Model/query/dsl.d.ts +41 -1
  4. package/dist/Model/query/dsl.d.ts.map +1 -1
  5. package/dist/Model/query/dsl.js +49 -1
  6. package/dist/Model/query/new-kid-interpreter.d.ts +28 -1
  7. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  8. package/dist/Model/query/new-kid-interpreter.js +30 -1
  9. package/dist/QueueMaker/SQLQueue.d.ts +1 -1
  10. package/dist/QueueMaker/memQueue.d.ts +1 -1
  11. package/dist/QueueMaker/sbqueue.d.ts +1 -1
  12. package/dist/Store/Cosmos/query.d.ts +1 -1
  13. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  14. package/dist/Store/Cosmos/query.js +30 -1
  15. package/dist/Store/Cosmos.d.ts +1 -1
  16. package/dist/Store/Memory.d.ts +1 -1
  17. package/dist/Store/Memory.d.ts.map +1 -1
  18. package/dist/Store/Memory.js +44 -1
  19. package/dist/Store/SQL/Pg.d.ts +1 -1
  20. package/dist/Store/SQL/query.d.ts +2 -2
  21. package/dist/Store/SQL/query.d.ts.map +1 -1
  22. package/dist/Store/SQL/query.js +54 -9
  23. package/dist/Store/SQL.d.ts +1 -1
  24. package/dist/Store/SQL.d.ts.map +1 -1
  25. package/dist/Store/SQL.js +3 -21
  26. package/dist/adapters/ServiceBus.d.ts +1 -1
  27. package/dist/adapters/logger.d.ts +1 -1
  28. package/dist/api/routing.d.ts +1 -1
  29. package/dist/otel.d.ts +1 -1
  30. package/package.json +3 -2
  31. package/src/Model/query/dsl.ts +99 -0
  32. package/src/Model/query/new-kid-interpreter.ts +62 -0
  33. package/src/Store/Cosmos/query.ts +33 -1
  34. package/src/Store/Memory.ts +40 -1
  35. package/src/Store/SQL/query.ts +58 -9
  36. package/src/Store/SQL.ts +3 -23
  37. package/test/dist/rawQuery.test.d.ts.map +1 -1
  38. package/test/query.test.ts +108 -3
  39. package/test/rawQuery.test.ts +24 -4
  40. package/test/sql-store.test.ts +166 -0
@@ -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) {
@@ -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
@@ -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
- const cast = dialect.jsonColumnType === "JSON"
421
- ? `CAST(${fieldExtract} AS REAL)`
422
- : `(${fieldExtract})::numeric`
423
- return `(SELECT COALESCE(SUM(${cast}), 0) FROM ${relationFrom}${whereClause()}) AS "${key}"`
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
- const whereClause = filter.length
459
- ? `WHERE ${print([{ t: "where-scope", result: filter, relation: "some" }], null, false)}`
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 ${getSelectExpr()} FROM "${tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim()
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
- const q = buildWhereSQLQuery(
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":"AACA,OAAO,EAA+C,cAAc,EAA2C,MAAM,YAAY,CAAA;AASjI,eAAO,MAAM,EAAE,6CAWb,CAAA"}
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"}
@@ -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(