Filed while implementing #10413 phase 2 (per-measure filter lowering, PR TBD). Not fixed there: fixing it means widening planCrossObject's refusal set, which #10413's dispatch explicitly ruled out of scope for that card (a defect-fix card, not a contract-behaviour change).
What is measured (read from code, not yet executed against a live driver)
ObjectQLStrategy's cross-object envelope check (planCrossObject, #3654 / #10759 / #10861) judges every query by exactly one member view, built by filterMemberView (packages/services/service-analytics/src/strategies/objectql-strategy.ts). That view is documented as "Two producers, one inventory (#10861)": the caller's own where (origin: 'where'), and the compiled dataset's definition-level filter (origin: 'dataset-filter'). Both are folded in and checked for a cross-object leaf before the query reaches engine.aggregate.
#10413 phase 2 adds a third producer of a predicate that reaches engine.aggregate: a measure's own filter (DatasetScope.measureFilters, e.g. { won_count: { filter: { stage: 'closed_won' } } }), now lowered into that measure's own aggregations[].filter entry (the #10576 contract field). filterMemberView was not extended to include these members, and planCrossObject's nonDim check (the arm that inspects query.measures) only looks at each measure's resolved field (this.resolveMeasureAggregation(cube, m).field), never at that measure's own filter.
So a dataset declaring a measure like:
{name: 'west_won_count',aggregate: 'count',filter: {'account.region': 'West'}}on a cube with include: ['account'] would have its filter lowered straight onto aggregations[].filter — {"account.region":"West"} — with no refusal, on either execute() or the /analytics/sql echo. engine.aggregate cannot join, so on a raw/in-memory driver the literal key "account.region" will not match any row property and the aggregation silently answers 0 (or, on a driver that treats an unrecognised filter key some other way, an equally wrong number) — never a loud INVALID_FIELD/400, and never the "run this on a native-SQL driver" guidance the four sibling refusals give.
This is the identical hazard #10759 and #10861 were opened and ruled on (maintainer, 2026-08-22, Option A — refuse at query time, folding leaves into the one member view) for the other two producers. Whether this third producer gets the same treatment, and where the line belongs (fold measureFilters leaves into filterMemberView with a new origin: 'measure-filter', matching #10861's shape exactly), is the same class of decision those two cards made — not a mechanical fix bundled into #10413 phase 2, which was scoped and ruled as a pure defect-fix (no refusal-set change).
Suggested shape (not evaluated for cost/tradeoffs — that is triage's job)
Mirror #10861 exactly: fold each measure's filter leaves into filterMemberView under a new origin (e.g. 'measure-filter'), extend planCrossObject's refusal to cover it with a message analogous to the existing dataset-scope one (naming the measure and the cube), and pin it in crossobject-conjunct-refusal.test.ts alongside the existing ①–⑥ directions.
Reproduction status
Not executed against a live fixture — this is a code-reading finding surfaced while extending the aggregations array in ObjectQLStrategy.execute/generateSql for #10413 phase 2. A reproduction would need a dataset with include, a cross-object dimension, and a measure whose own filter names a cross-object field, run on the ObjectQL door (nativeSql: false).
Related: #10413 (phase 2, where this was found), #10759, #10861 (the two prior cards on the same hazard class, different producers), #10576 (the contract this measure-filter lowering consumes).
Filed while implementing #10413 phase 2 (per-measure filter lowering, PR TBD). Not fixed there: fixing it means widening
planCrossObject's refusal set, which #10413's dispatch explicitly ruled out of scope for that card (a defect-fix card, not a contract-behaviour change).What is measured (read from code, not yet executed against a live driver)
ObjectQLStrategy's cross-object envelope check (planCrossObject,#3654/#10759/#10861) judges every query by exactly one member view, built byfilterMemberView(packages/services/service-analytics/src/strategies/objectql-strategy.ts). That view is documented as "Two producers, one inventory (#10861)": the caller's ownwhere(origin: 'where'), and the compiled dataset's definition-levelfilter(origin: 'dataset-filter'). Both are folded in and checked for a cross-object leaf before the query reachesengine.aggregate.#10413 phase 2 adds a third producer of a predicate that reaches
engine.aggregate: a measure's ownfilter(DatasetScope.measureFilters, e.g.{ won_count: { filter: { stage: 'closed_won' } } }), now lowered into that measure's ownaggregations[].filterentry (the #10576 contract field).filterMemberViewwas not extended to include these members, andplanCrossObject'snonDimcheck (the arm that inspectsquery.measures) only looks at each measure's resolved field (this.resolveMeasureAggregation(cube, m).field), never at that measure's ownfilter.So a dataset declaring a measure like:
on a cube with
include: ['account']would have itsfilterlowered straight ontoaggregations[].filter—{"account.region":"West"}— with no refusal, on eitherexecute()or the/analytics/sqlecho.engine.aggregatecannot join, so on a raw/in-memory driver the literal key"account.region"will not match any row property and the aggregation silently answers 0 (or, on a driver that treats an unrecognised filter key some other way, an equally wrong number) — never a loudINVALID_FIELD/400, and never the "run this on a native-SQL driver" guidance the four sibling refusals give.This is the identical hazard #10759 and #10861 were opened and ruled on (maintainer, 2026-08-22, Option A — refuse at query time, folding leaves into the one member view) for the other two producers. Whether this third producer gets the same treatment, and where the line belongs (fold
measureFiltersleaves intofilterMemberViewwith a neworigin: 'measure-filter', matching #10861's shape exactly), is the same class of decision those two cards made — not a mechanical fix bundled into #10413 phase 2, which was scoped and ruled as a pure defect-fix (no refusal-set change).Suggested shape (not evaluated for cost/tradeoffs — that is triage's job)
Mirror #10861 exactly: fold each measure's
filterleaves intofilterMemberViewunder a new origin (e.g.'measure-filter'), extendplanCrossObject's refusal to cover it with a message analogous to the existing dataset-scope one (naming the measure and the cube), and pin it incrossobject-conjunct-refusal.test.tsalongside the existing ①–⑥ directions.Reproduction status
Not executed against a live fixture — this is a code-reading finding surfaced while extending the aggregations array in
ObjectQLStrategy.execute/generateSqlfor #10413 phase 2. A reproduction would need a dataset withinclude, a cross-object dimension, and a measure whose ownfilternames a cross-object field, run on the ObjectQL door (nativeSql: false).Related: #10413 (phase 2, where this was found), #10759, #10861 (the two prior cards on the same hazard class, different producers), #10576 (the contract this measure-filter lowering consumes).