Noticed while implementing #10298 (PR #10411). Recorded, not acted on — deciding it is ADR-0049 enforce-or-remove work, not this card's.
The declaration
packages/spec/src/data/analytics.zod.ts, MetricSchema:
/** Filtering for this specific metric (e.g. "Revenue from Premium Users") */
filters: z.array(strictObject(
{ surface: 'this metric filter',
history: 'Until #4001 batch D an undeclared key beside `sql` was silently dropped.' },
{ sql: z.string() },
)).optional(),
It is a real authoring surface: defineCube() parses an author literal and defineStack({ analyticsCubes }) carries every cube through StackSchema.parse, so an author writing filters on a metric gets a clean parse. packages/spec/src/data/analytics.test.ts pins that the key survives the parse (expect(metric.filters).toHaveLength(1)).
Nothing reads it
Measured on origin/main at 801296050:
$ git grep -n "\.filters" -- 'packages/services/service-analytics/src/**' 'packages/drivers/**' \
| grep -v '\.test\.ts'
(no output)
Zero is a finding here only because the same search shape finds a neighbouring metric key that IS consumed — the positive control:
$ git grep -c "\.format" -- 'packages/services/service-analytics/src/**'
packages/services/service-analytics/src/__tests__/dataset-compiler.test.ts:1
packages/services/service-analytics/src/__tests__/dataset-dimension-field-descriptors.test.ts:2
packages/services/service-analytics/src/__tests__/query-dataset.test.ts:1
packages/services/service-analytics/src/analytics-service.ts:1
packages/services/service-analytics/src/dataset-compiler.ts:1
format is read; filters is read nowhere outside the spec's own parse test. NativeSQLStrategy.resolveMeasureSql wraps measure.sql in the aggregate for measure.type and never looks at measure.filters; ObjectQLStrategy.resolveMeasureAggregation does the same.
Why this is the #10298 shape, one level up
#10298 was a dataset measure whose filter was declared and dropped, producing a 200 with different arithmetic than the author wrote. This is the identical failure for a hand-authored cube: filters: [{ sql: "stage = 'closed_won'" }] parses, registers, and returns the unfiltered aggregate under the author's metric name. The dataset path is now repaired (#10411) through a separate channel — the compiled dataset's own registry entry — so MetricSchema.filters remains inert with the dataset half fixed around it.
The decision this needs
filters[].sql is a raw SQL string, which is why #10411 did not adopt it as the carrier and reached for a structured FilterCondition sidecar instead. Enforcing it would mean answering:
- Does the platform want a raw-SQL authoring surface on a metric at all? Every other filter surface in the tree is a structured
FilterCondition that the strategies lower and parameterize; a raw fragment cannot be bound, cannot be re-targeted per driver dialect, and cannot be inspected by the lint rules that walk filters (packages/lint/src/filter-walk.ts enumerates "widget filters, list-view filters, dataset and measure filters" — not this one).
- If not, ADR-0049's remove leg applies, and the retirement route is the
spec-property-retirement playbook.
- If yes, the consumer has to be written, and the injection surface argued.
Either way it is a packages/spec decision, which #10298's lane holds as a zero-ownership boundary.
Related: #10298 · PR #10411 · #10413.
Noticed while implementing #10298 (PR #10411). Recorded, not acted on — deciding it is ADR-0049 enforce-or-remove work, not this card's.
The declaration
packages/spec/src/data/analytics.zod.ts,MetricSchema:It is a real authoring surface:
defineCube()parses an author literal anddefineStack({ analyticsCubes })carries every cube throughStackSchema.parse, so an author writingfilterson a metric gets a clean parse.packages/spec/src/data/analytics.test.tspins that the key survives the parse (expect(metric.filters).toHaveLength(1)).Nothing reads it
Measured on
origin/mainat801296050:Zero is a finding here only because the same search shape finds a neighbouring metric key that IS consumed — the positive control:
formatis read;filtersis read nowhere outside the spec's own parse test.NativeSQLStrategy.resolveMeasureSqlwrapsmeasure.sqlin the aggregate formeasure.typeand never looks atmeasure.filters;ObjectQLStrategy.resolveMeasureAggregationdoes the same.Why this is the #10298 shape, one level up
#10298 was a dataset measure whose
filterwas declared and dropped, producing a 200 with different arithmetic than the author wrote. This is the identical failure for a hand-authored cube:filters: [{ sql: "stage = 'closed_won'" }]parses, registers, and returns the unfiltered aggregate under the author's metric name. The dataset path is now repaired (#10411) through a separate channel — the compiled dataset's own registry entry — soMetricSchema.filtersremains inert with the dataset half fixed around it.The decision this needs
filters[].sqlis a raw SQL string, which is why #10411 did not adopt it as the carrier and reached for a structuredFilterConditionsidecar instead. Enforcing it would mean answering:FilterConditionthat the strategies lower and parameterize; a raw fragment cannot be bound, cannot be re-targeted per driver dialect, and cannot be inspected by the lint rules that walk filters (packages/lint/src/filter-walk.tsenumerates "widget filters, list-view filters, dataset and measure filters" — not this one).spec-property-retirementplaybook.Either way it is a
packages/specdecision, which #10298's lane holds as a zero-ownership boundary.Related: #10298 · PR #10411 · #10413.