You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(driver-memory): the analytics face refuses a filter it cannot compile instead of dropping it (#5345)
`MemoryAnalyticsService` lowers `AnalyticsQuery.where` into a flat cube-style
`{member, operator, values}` list, and answered everything that did not fit with
`continue` — `$or` and `$not` wholesale, plus the five declared operators with no
row in the mongo→cube table (`$between`, `$startsWith`, `$endsWith`, `$null`,
`$regex`). A comment presented this as a feature: "ignore so a partial query
still runs rather than failing entirely" — the identical reasoning ADR-0078 /
#4286 judged wrong on `objectql`'s `having`. Deleted along with the behaviour it
justified.
The direction is what makes it a defect: a dropped predicate is FEWER
constraints, therefore MORE rows. Measured on `FILTER_LOGIC_CASES`, 15 of its 17
cases returned a wider row set than the standard specifies, usually every row.
`$not` makes it more than a wrong number — `cel-to-filter.ts` compiles a CEL
`!expr` RLS read scope into `{$not: {…}}`, so the dropped branch was the read
scope itself.
Route 1 of the issue's two: refuse, do not backfill the cube pipeline — where
#5366 and #5368 put the two neighbouring faces.
Implementation reuses #5349's primitives rather than adding a second validator.
`assertFilterConditionShape` now takes the calling face's `FilterFaceCapabilities`
— what that face can COMPILE, distinct from what the protocol declares — and
refuses the difference through `unsupportedFilterError` (INVALID_FILTER / 400).
The query path and the matcher pass nothing and are byte-for-byte unaffected.
The analytics face derives its capabilities from its own `MONGO_TO_CUBE_OPERATOR`
table, so widening what it accepts and teaching it to compile the operator are
now one edit. Two refusals distinct on purpose: an operator the protocol never
declared is still `unknownFieldOperatorError` ("you have a typo"), one it
declares and this face cannot lower is the new `uncompilableFieldOperatorError`.
The gate runs in `normalizeFilters`, before any lowering, for the reason that
module already documents — a refusal raised mid-lowering fires or not depending
on key order. Both public entry points (`query()`, `generateSql()`) go through
it. The two former `continue` sites now throw; the operator one is reachable via
the nested-relation branch, which re-enters with a synthesised node the gate
never walked.
Tests: `FILTER_LOGIC_CASES` now covers this third face — the package had three
filter surfaces and the shared table watched two. It cannot pass row-for-row (a
cube pipeline has no `$or`), so it is held to the predicate that actually
matters: agree with `find()`, or refuse with INVALID_FILTER, never a third
quieter answer. Reverting only `memory-analytics.ts` fails 16 of the new
assertions. Plus a dedicated suite asserting the envelope (code, status) and
that each refusal names the offending operator or combinator.
Out of scope, filed not fixed: #5373 (the cube comparand round-trip loses
booleans and `null`), #5374 (`$notContains` lowers to a bare mingo `{$not: 'x'}`
that constrains nothing).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
0 commit comments