(key: string, computed: ComputedProjectionIrExpression)
| 338 | } |
| 339 | |
| 340 | const computedSelectExpr = (key: string, computed: ComputedProjectionIrExpression) => { |
| 341 | const relationPath = computed.path |
| 342 | const relationAlias = relationPath |
| 343 | const relationSource = dottedToAccess(`f.${relationPath}`) |
| 344 | const compileExpr = (expression: ComputedProjectionMathIrExpression): string => { |
| 345 | switch (expression._tag) { |
| 346 | case "field": |
| 347 | return dottedToAccess(`${relationAlias}.${expression.field}`) |
| 348 | case "mul": |
| 349 | return `(${compileExpr(expression.left)} * ${compileExpr(expression.right)})` |
| 350 | default: |
| 351 | return assertUnreachable(expression) |
| 352 | } |
| 353 | } |
| 354 | const factorExpr = (unitExpr: string, toBase: string, factors: Readonly<Record<string, number>>) => { |
| 355 | const entries = Object.entries(factors).filter(([, factor]) => Number.isFinite(factor)) |
| 356 | return entries.reduceRight<string>( |
| 357 | (acc, [unit, factor]) => `IIF(${unitExpr} = ${JSON.stringify(unit)}, ${factor}, ${acc})`, |
| 358 | `IIF(${unitExpr} = ${JSON.stringify(toBase)}, 1, 0)` |
| 359 | ) |
| 360 | } |
| 361 | const filter = "filter" in computed ? computed.filter : [] |
| 362 | // Print filter once — `print` mutates the outer `i` parameter counter, so |
| 363 | // re-walking the same filter would double-bump it and desync @v indices. |
| 364 | const filterSql = filter.length > 0 ? print(filter, relationPath, false) : "" |
| 365 | const where = filterSql ? ` WHERE ${filterSql}` : "" |
| 366 | switch (computed._tag) { |
| 367 | case "relation-count": |
| 368 | return `(SELECT VALUE COUNT(1) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}` |
| 369 | case "relation-any": |
| 370 | return `EXISTS(SELECT VALUE ${relationAlias} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}` |
| 371 | case "relation-every": { |
| 372 | // ∀x.P(x) ≡ ¬∃x.¬P(x). Cosmos has no NOT(...) on EXISTS subqueries directly, |
| 373 | // but we can flip via NOT EXISTS(... WHERE NOT (filter)). |
| 374 | if (filter.length === 0) return `true AS ${key}` |
| 375 | return `NOT EXISTS(SELECT VALUE ${relationAlias} FROM ${relationAlias} IN ${relationSource} WHERE NOT (${filterSql})) AS ${key}` |
| 376 | } |
| 377 | case "relation-distinct-count": { |
| 378 | const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`) |
| 379 | return `(SELECT VALUE COUNT(1) FROM (SELECT DISTINCT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where})) AS ${key}` |
| 380 | } |
| 381 | case "relation-sum": { |
| 382 | const fieldRef = dottedToAccess(`${relationAlias}.${computed.field}`) |
| 383 | return `(SELECT VALUE SUM(${fieldRef}) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}` |
| 384 | } |
| 385 | case "relation-sum-expr": { |
| 386 | const expression = compileExpr(computed.expression) |
| 387 | return `(SELECT VALUE SUM(${expression}) FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}` |
| 388 | } |
| 389 | case "relation-sum-expr-by": { |
| 390 | const unitRef = dottedToAccess(`${relationAlias}.${computed.unit}`) |
| 391 | const expression = compileExpr(computed.expression) |
| 392 | return `ARRAY(SELECT VALUE { "unit": ${unitRef}, "total": SUM(${expression}) } FROM ${relationAlias} IN ${relationSource}${where} GROUP BY ${unitRef}) AS ${key}` |
| 393 | } |
| 394 | case "relation-sum-expr-normalized": { |
| 395 | const unitRef = dottedToAccess(`${relationAlias}.${computed.unit}`) |
| 396 | const expression = compileExpr(computed.expression) |
| 397 | const factor = factorExpr(unitRef, computed.toBase, computed.factors) |
no test coverage detected
searching dependent graphs…