fix(objectql): put having's operator refusals inside the ADR-0112 envelope (#7047) - #7161
Conversation
…nvelope (#7047) `having-filter.ts`'s `unknownOperator()` returned a bare `new Error(...)` from BOTH branches — the retired spellings (`$regex`, `$options`) and the unknown ones (`$nand`, `$median`) — so `code` and `status` were both `undefined` and a 400-class author mistake reached the client 500-shaped through `rest`'s unclassified-fault branch. Measured by executing each of the five refusal faces (#6993), `having` was the only disagreement: the four driver faces all answer `INVALID_FILTER` / 400. Both returns now go through `invalidFilterError`, exported from this package's other filter-refusal site rather than copied, so objectql's two refusal sites cannot answer one mistake with two envelopes. The refusals and their messages are unchanged — the retired branch already printed `RETIRED_FILTER_OPERATORS[op].why` verbatim. Only the envelope was missing, which is the half of #5324 a refusal does not fix on its own. Tests: the rejection assertions in `having-filter.test.ts` were `toThrow(/message/)` only, green whether or not an envelope is present (#6142/#6050) — that is how this survived the PR that wrote those messages. They now pin `code` + `status` + the verbatim prescription, on both branches and through `applyHaving`. A new `having-filter-text-conformance.test.ts` drives the face against `FILTER_TEXT_CASES`, which this package imported nowhere; `having` had no conformance-table coverage, which is why both of the last two defects on it (#5905, #7047) were found by hand-run censuses. Two of the table's five rejection rows are deliberately NOT enrolled and are pinned as measured exclusions instead: this face has no `$icontains` comparand-shape gate, so an empty comparand matches EVERY row. That is a behaviour change beyond an envelope card — filed as #7158. Refs #7047, #6993, #5324, ADR-0112.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Closes #7047.
The defect
having-filter.ts'sunknownOperator()returned a barenew Error(...)from both of its branches — the RETIRED spellings ($regex,$options) and the unknown ones ($nand,$median, a mistyped$icontain).codeandstatuswere bothundefined, sorestserved the refusal through the unclassified-fault branch and a 400-class author mistake reached the client 500-shaped.havingis the fifth of five filter-refusal faces and the only one that disagreed. Re-measured by EXECUTING each face (#6993's method, not grep):code/statusbeforeINVALID_FILTER/ 400filter-refusal.ts), driver-mongodbINVALID_FILTER/ 400havingundefined/undefinedINVALID_FILTER/ 400Probe output, both branches, before → after:
Both branches deliberately: enveloping only the retired path would leave every operator typo — the more likely of the two — arriving 500-shaped, the same defect one operator name away.
The change
unknownOperator()'s two returns go throughinvalidFilterError. The messages are unchanged — the retired branch already printedRETIRED_FILTER_OPERATORS[op].whyverbatim like the four driver faces. Only the envelope was missing, which is the half of driver-memory 的实时查询路径根本不支持$not—— mingo 抛无 code 的 MingoError,CEL!expr降下来的 RLS scope 在该驱动上直接 500 #5324 a refusal does not fix on its own and the halfFilterTextRejectionCase.codeexists to pin.INVALID_FILTERbecause this joins the contract the other four already speak; no new code is invented. A caller swapping HAVING for a driver-sidewheremust not have to catch two shapes for one mistake.invalidFilterErroris exported fromfilter-comparand-shape.ts(this package's other filter-refusal site) rather than copied into a third private definition — the driftfilter-refusal.ts's own doc blocks are written about.Client-visible change, hence the changeset: code catching a
havingrefusal by message substring, or branching onerr.codebeing absent, now seesINVALID_FILTER/ 400. Over HTTP the status moves 500 → 400, which is the point.Tests
The rejection assertions in
having-filter.test.tsweretoThrow(/message/)only — green whether or not the error carries an envelope (the #6142/#6050 hole). That is precisely how this survived the PR that wrote those messages. They now pincode+status+ the verbatimRETIRED_FILTER_OPERATORS[op].why, on both branches, in both positions, and throughapplyHaving(the entry pointengine.aggregate()calls, so the envelope is asserted on the error that actually leaves the module).New
having-filter-text-conformance.test.tsdrives this face againstFILTER_TEXT_CASES— the standard the driver suites answer and whichpackages/objectqlimported nowhere.havinghad no conformance-table coverage at all (check-driver-conformance.mjsscopes topackages/drivers/*), which is why both of the last two defects on it (#5905 and this one) were found by a hand-run census rather than by CI.Reverse-verified: restored the pre-fix source with the new tests in place, predicted 9 red / 39 green, ran it — exactly the 9 predicted failed (3 retired refusals, 2 unknown-operator, 1
applyHaving, 3 fixture retired-rejection rows), 39 passed. No deviations. Restored → 48/48 green.Two findings outside this card's fence — filed, not folded in
objectql
havinghas no$icontainscomparand-shape gate — an empty comparand matches EVERY row (2 of 5FILTER_TEXT_CASESrejection rows unenrollable) #7158 —havinghas no$icontainscomparand-shape gate. Driving the fixture surfaced that 2 of its 5 rejection rows are not refused by this face at all: an empty$icontainscomparand is evaluated and matches ALL NINE rows (a predicate that constrains nothing — the widening of A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948), and a non-string one is answered "no rows". Closing that means refusing filters this face evaluates today — a behaviour change beyond an envelope card. The two rows are therefore not enrolled, and are pinned as measured exclusions so the gap goes red the day someone adds the gate, instead of reading as coverage.check-driver-conformance.mjsFILTER_TEXT requirement-3 calls driver-mongodb's arm "the last place a barenew Errorescaped the ADR-0112 envelope" — it was not, and after #7047 the sentence names the wrong site #7159 —scripts/check-driver-conformance.mjsFILTER_TEXT requirement-3 is stale. It calls driver-mongodb's arm "the last place a barenew Errorescaped the ADR-0112 envelope". That was already false when written (havingwas a sixth place), and after this PR the claim becomes true while attributing it to the wrong site. Not edited here — a conformance-script edit riding on a behaviour fix is exactly what this card's fence excludes.Refs #7047, #6993, #5324, #5905, ADR-0112.
Generated by Claude Code