@effect-app/infra 4.0.0-beta.213 → 4.0.0-beta.215

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.
@@ -19,6 +19,30 @@ export type ComputedProjectionIrExpression =
19
19
  readonly path: string
20
20
  readonly filter: readonly FilterResult[]
21
21
  }
22
+ | {
23
+ readonly _tag: "relation-every"
24
+ readonly path: string
25
+ readonly filter: readonly FilterResult[]
26
+ }
27
+ | {
28
+ readonly _tag: "relation-distinct-count"
29
+ readonly path: string
30
+ readonly field: string
31
+ readonly filter: readonly FilterResult[]
32
+ }
33
+ | {
34
+ readonly _tag: "relation-sum"
35
+ readonly path: string
36
+ readonly field: string
37
+ readonly filter: readonly FilterResult[]
38
+ }
39
+ | {
40
+ readonly _tag: "relation-collect"
41
+ readonly path: string
42
+ readonly field: string
43
+ readonly distinct: boolean
44
+ readonly filter: readonly FilterResult[]
45
+ }
22
46
 
23
47
  type Result<TFieldValues extends FieldValues, A = TFieldValues, R = never> = {
24
48
  filter: FilterResult[]
@@ -162,7 +186,29 @@ const interpret = <
162
186
  Object.entries(v.computed).map(([key, expression]) => {
163
187
  const e = expression
164
188
  const filter = e.operation ? interpret(e.operation(make())).filter.map(applyPath(e.path)) : []
165
- return [key, { _tag: e._tag, path: e.path, filter } as const]
189
+ switch (e._tag) {
190
+ case "relation-count":
191
+ case "relation-any":
192
+ case "relation-every":
193
+ return [key, { _tag: e._tag, path: e.path, filter } as ComputedProjectionIrExpression]
194
+ case "relation-distinct-count":
195
+ case "relation-sum":
196
+ return [
197
+ key,
198
+ { _tag: e._tag, path: e.path, field: e.field, filter } as ComputedProjectionIrExpression
199
+ ]
200
+ case "relation-collect":
201
+ return [
202
+ key,
203
+ {
204
+ _tag: e._tag,
205
+ path: e.path,
206
+ field: e.field,
207
+ distinct: e.distinct,
208
+ filter
209
+ } as ComputedProjectionIrExpression
210
+ ]
211
+ }
166
212
  })
167
213
  )
168
214
  : undefined
@@ -303,6 +303,29 @@ export function buildWhereCosmosQuery3(
303
303
  return `(SELECT VALUE COUNT(1) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
304
304
  case "relation-any":
305
305
  return `EXISTS(SELECT VALUE ${relationAlias} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
306
+ case "relation-every": {
307
+ // ∀x.P(x) ≡ ¬∃x.¬P(x). Cosmos has no NOT(...) on EXISTS subqueries directly,
308
+ // but we can flip via NOT EXISTS(... WHERE NOT (filter)).
309
+ if (computed.filter.length === 0) return `true AS ${key}`
310
+ return `NOT EXISTS(SELECT VALUE ${relationAlias} FROM ${relationAlias} IN ${relationSource} WHERE NOT (${
311
+ print(computed.filter, relationPath, false)
312
+ })) AS ${key}`
313
+ }
314
+ case "relation-distinct-count": {
315
+ const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`)
316
+ return `(SELECT VALUE COUNT(1) FROM (SELECT DISTINCT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where})) AS ${key}`
317
+ }
318
+ case "relation-sum": {
319
+ const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`)
320
+ return `(SELECT VALUE SUM(${fieldRef}) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
321
+ }
322
+ case "relation-collect": {
323
+ const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`)
324
+ if (computed.distinct) {
325
+ return `ARRAY(SELECT DISTINCT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
326
+ }
327
+ return `ARRAY(SELECT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
328
+ }
306
329
  }
307
330
  }
308
331
  // with joins, you should use DISTINCT
@@ -2,9 +2,11 @@
2
2
 
3
3
  import { Array, Context, Effect, flow, type NonEmptyReadonlyArray, Option, Order, pipe, Ref, Result, Semaphore, Struct } from "effect-app"
4
4
  import { NonEmptyString255 } from "effect-app/Schema"
5
+ import { assertUnreachable } from "effect-app/utils"
5
6
  import { InfraLogger } from "../logger.js"
6
7
  import type { FilterResult } from "../Model/filter/filterApi.js"
7
8
  import type { FieldValues } from "../Model/filter/types.js"
9
+ import type { ComputedProjectionIrExpression } from "../Model/query.js"
8
10
  import { annotateDb } from "../otel.js"
9
11
  import { codeFilter, codeFilter3_ } from "./codeFilter.js"
10
12
  import { type FilterArgs, type PersistenceModelType, type Store, type StoreConfig, StoreMaker } from "./service.js"
@@ -30,24 +32,73 @@ const stripRelationFilterPaths = (state: readonly FilterResult[], relationPath:
30
32
  )
31
33
  }
32
34
 
35
+ const emptyValueFor = (tag: ComputedProjectionIrExpression["_tag"]) => {
36
+ switch (tag) {
37
+ case "relation-count":
38
+ case "relation-distinct-count":
39
+ case "relation-sum":
40
+ return 0
41
+ case "relation-any":
42
+ return false
43
+ case "relation-every":
44
+ return true
45
+ case "relation-collect":
46
+ return [] as unknown[]
47
+ default:
48
+ return assertUnreachable(tag)
49
+ }
50
+ }
51
+
33
52
  const computeProjectionValue = (
34
53
  row: FieldValues,
35
- computed: {
36
- readonly _tag: "relation-count" | "relation-any"
37
- readonly path: string
38
- readonly filter: readonly FilterResult[]
39
- }
54
+ computed: ComputedProjectionIrExpression
40
55
  ) => {
41
56
  const relation = get(row, computed.path)
42
57
  if (!Array.isArray(relation)) {
43
- return computed._tag === "relation-count" ? 0 : false
58
+ return emptyValueFor(computed._tag)
44
59
  }
45
60
  const filter = stripRelationFilterPaths(computed.filter, computed.path)
61
+ // empty filter = unconditional match (codeFilter3_ uses eval on a built
62
+ // string and chokes on `( )`, so short-circuit before invoking it).
63
+ const matches = filter.length === 0
64
+ ? (_value: unknown) => true
65
+ : (value: unknown) => codeFilter3_(filter, value)
46
66
  switch (computed._tag) {
47
67
  case "relation-count":
48
- return relation.reduce<number>((acc, value) => codeFilter3_(filter, value) ? acc + 1 : acc, 0)
68
+ return relation.reduce<number>((acc, value) => matches(value) ? acc + 1 : acc, 0)
49
69
  case "relation-any":
50
- return relation.some((value) => codeFilter3_(filter, value))
70
+ return relation.some(matches)
71
+ case "relation-every":
72
+ return relation.every(matches)
73
+ case "relation-distinct-count": {
74
+ const seen = new Set<unknown>()
75
+ for (const value of relation) {
76
+ if (matches(value)) seen.add(get(value, computed.field))
77
+ }
78
+ return seen.size
79
+ }
80
+ case "relation-sum":
81
+ return relation.reduce<number>((acc, value) => {
82
+ if (!matches(value)) return acc
83
+ const v = get(value, computed.field)
84
+ return acc + (typeof v === "number" ? v : Number(v) || 0)
85
+ }, 0)
86
+ case "relation-collect": {
87
+ const out: unknown[] = []
88
+ const seen = computed.distinct ? new Set<unknown>() : undefined
89
+ for (const value of relation) {
90
+ if (!matches(value)) continue
91
+ const v = get(value, computed.field)
92
+ if (seen) {
93
+ if (seen.has(v)) continue
94
+ seen.add(v)
95
+ }
96
+ out.push(v)
97
+ }
98
+ return out
99
+ }
100
+ default:
101
+ return assertUnreachable(computed)
51
102
  }
52
103
  }
53
104
 
@@ -67,11 +118,7 @@ export function memFilter<T extends FieldValues, U extends keyof T = never>(f: F
67
118
  )
68
119
  const computedKeys = entries.filter((entry): entry is {
69
120
  key: string
70
- computed: {
71
- readonly _tag: "relation-count" | "relation-any"
72
- readonly path: string
73
- readonly filter: readonly FilterResult[]
74
- }
121
+ computed: ComputedProjectionIrExpression
75
122
  } => typeof entry === "object" && entry !== null && "computed" in entry)
76
123
  const n = Struct.pick(i, keys)
77
124
  subKeys.forEach((subKey) => {
@@ -391,18 +391,48 @@ export function buildWhereSQLQuery(
391
391
  const relationPath = dottedToJsonPath(computed.path)
392
392
  const relationAlias = `_${computed.path}`
393
393
  const relationFrom = dialect.jsonEachFrom(relationPath, relationAlias)
394
- const where = computed.filter.length > 0
395
- ? ` WHERE ${print(computed.filter, computed.path, false)}`
396
- : ""
394
+ const whereClause = () =>
395
+ computed.filter.length > 0
396
+ ? ` WHERE ${print(computed.filter, computed.path, false)}`
397
+ : ""
398
+ const boolExpr = (sqlExpr: string) =>
399
+ dialect.jsonColumnType === "JSON"
400
+ ? `CASE WHEN ${sqlExpr} THEN 'true' ELSE 'false' END AS "${key}"`
401
+ : `${sqlExpr} AS "${key}"`
397
402
  switch (computed._tag) {
398
403
  case "relation-count":
399
- return `(SELECT COUNT(1) FROM ${relationFrom}${where}) AS "${key}"`
400
- case "relation-any": {
401
- const existsExpr = `EXISTS(SELECT 1 FROM ${relationFrom}${where})`
404
+ return `(SELECT COUNT(1) FROM ${relationFrom}${whereClause()}) AS "${key}"`
405
+ case "relation-any":
406
+ return boolExpr(`EXISTS(SELECT 1 FROM ${relationFrom}${whereClause()})`)
407
+ case "relation-every":
408
+ // ∀x.P(x) ≡ ¬∃x.¬P(x). When no filter, no element exists that violates ⊤ → true.
409
+ return boolExpr(
410
+ computed.filter.length === 0
411
+ ? `1=1`
412
+ : `NOT EXISTS(SELECT 1 FROM ${relationFrom} WHERE NOT (${print(computed.filter, computed.path, false)}))`
413
+ )
414
+ case "relation-distinct-count": {
415
+ const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
416
+ return `(SELECT COUNT(DISTINCT ${fieldExtract}) FROM ${relationFrom}${whereClause()}) AS "${key}"`
417
+ }
418
+ case "relation-sum": {
419
+ 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}"`
424
+ }
425
+ case "relation-collect": {
426
+ const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
402
427
  if (dialect.jsonColumnType === "JSON") {
403
- return `CASE WHEN ${existsExpr} THEN 'true' ELSE 'false' END AS "${key}"`
428
+ // sqlite: json_group_array does not accept DISTINCT; emulate via inner DISTINCT subquery
429
+ if (computed.distinct) {
430
+ return `(SELECT COALESCE(json_group_array(__v), json_array()) FROM (SELECT DISTINCT ${fieldExtract} AS __v FROM ${relationFrom}${whereClause()})) AS "${key}"`
431
+ }
432
+ return `(SELECT COALESCE(json_group_array(${fieldExtract}), json_array()) FROM ${relationFrom}${whereClause()}) AS "${key}"`
404
433
  }
405
- return `${existsExpr} AS "${key}"`
434
+ const aggArg = computed.distinct ? `DISTINCT ${fieldExtract}` : fieldExtract
435
+ return `(SELECT COALESCE(jsonb_agg(${aggArg}), '[]'::jsonb) FROM ${relationFrom}${whereClause()}) AS "${key}"`
406
436
  }
407
437
  default:
408
438
  return assertUnreachable(computed)
@@ -1 +1 @@
1
- {"version":3,"file":"rawQuery.test.d.ts","sourceRoot":"","sources":["../rawQuery.test.ts"],"names":[],"mappings":"AACA,OAAO,EAA+C,cAAc,EAAmC,MAAM,YAAY,CAAA;AASzH,eAAO,MAAM,EAAE,6CAWb,CAAA"}
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"}