Commit 399ecad
fix(service-analytics): refuse a cross-object per-measure filter on both ObjectQL doors (#12022)
`ObjectQLStrategy` judges every query by one member view (`filterMemberView`),
documented as "two producers, one inventory" (#10861): the caller's `where` and
the compiled dataset's definition-level `filter`. #10413 phase 2 added a third
producer with the same reach and none of the coverage — a compiled measure's own
`filter`, lowered onto that measure's `aggregations[].filter` entry (#10576) —
and `planCrossObject`'s `query.measures` arm reads only each measure's resolved
FIELD, never its filter. So a cross-object leaf there reached `engine.aggregate`
unrefused on both doors.
Reproduced first (the card was code-read, not executed), one fixture, an honest
in-memory engine that applies `aggregations[].filter` as a property match — all
`engine.aggregate` can do, since it cannot join:
BEFORE execute() ACCEPTED, engine reached once with
{field:"*",method:"count",alias:"west_count",
filter:{"account.region":"West"}} and answered
west_count 0 where the truth is 2 — beside a correct
total_count 3, so the wrong number came back in the
same response shape as the right one
generateSql() ACCEPTED, rendering COUNT(CASE WHEN account.region = $1
THEN 1 END) over a FROM with no join in it at all
AFTER both doors REFUSED, INVALID_FIELD/400, engine never reached
Mirrors #10861 exactly, on the same maintainer ruling (2026-08-22, Option A —
refuse at query time, folding the leaves into the one member view): the origin
becomes a record so provenance can carry the MEASURE the field cannot, only the
REQUESTED measures are folded (both aggregation loops read `measureFilters[m]`
for `m of query.measures` and nothing else), and insertion order keeps every
shape refused before this card on the exact message it already had.
Beyond the internal inconsistency this squares the door with a published
promise: `content/docs/api/data-api.mdx` documents that a bad field in an
`aggregations` entry answers 400 INVALID_FIELD. The same `aggregations` object
kept that promise in the `field` position and broke it in the `filter` position
— 200 with a silent 0, the exact failure class that page's preamble names as
its reason for existing. The page needs no edit: no sentence in it describes
the exception, so it is simply true again.
Fixes #11461
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
Co-authored-by: Claude <noreply@anthropic.com>1 parent 911da5f commit 399ecad
3 files changed
Lines changed: 411 additions & 32 deletions
File tree
- .changeset
- packages/services/service-analytics/src
- __tests__
- strategies
Lines changed: 9 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
0 commit comments