Skip to content

fix(objectql): put having's operator refusals inside the ADR-0112 envelope (#7047) - #7161

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7047-having-adr0112-envelope
Aug 10, 2026
Merged

fix(objectql): put having's operator refusals inside the ADR-0112 envelope (#7047)#7161
os-zhuang merged 1 commit into
mainfrom
claude/issue-7047-having-adr0112-envelope

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #7047.

The defect

having-filter.ts's unknownOperator() returned a bare new Error(...) from both of its branches — the RETIRED spellings ($regex, $options) and the unknown ones ($nand, $median, a mistyped $icontain). code and status were both undefined, so rest served the refusal through the unclassified-fault branch and a 400-class author mistake reached the client 500-shaped.

having is the fifth of five filter-refusal faces and the only one that disagreed. Re-measured by EXECUTING each face (#6993's method, not grep):

face code / status before after
driver-sql, driver-sqlite-wasm, driver-turso (local + remote) INVALID_FILTER / 400 unchanged
driver-memory (filter-refusal.ts), driver-mongodb INVALID_FILTER / 400 unchanged
objectql having undefined / undefined INVALID_FILTER / 400

Probe output, both branches, before → after:

retired  $regex   :: THROW code=undefined status=undefined  →  code=INVALID_FILTER status=400
retired  $options :: THROW code=undefined status=undefined  →  code=INVALID_FILTER status=400
unknown  $median  :: THROW code=undefined status=undefined  →  code=INVALID_FILTER status=400
unknown  $nand    :: THROW code=undefined status=undefined  →  code=INVALID_FILTER status=400  (logical position)
unknown  $icontain:: THROW code=undefined status=undefined  →  code=INVALID_FILTER status=400  (operator typo)

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 through invalidFilterError. The messages are unchanged — the retired branch already printed RETIRED_FILTER_OPERATORS[op].why verbatim 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 half FilterTextRejectionCase.code exists to pin.
  • The code is INVALID_FILTER because this joins the contract the other four already speak; no new code is invented. A caller swapping HAVING for a driver-side where must not have to catch two shapes for one mistake.
  • invalidFilterError is exported from filter-comparand-shape.ts (this package's other filter-refusal site) rather than copied into a third private definition — the drift filter-refusal.ts's own doc blocks are written about.

Client-visible change, hence the changeset: code catching a having refusal by message substring, or branching on err.code being absent, now sees INVALID_FILTER / 400. Over HTTP the status moves 500 → 400, which is the point.

Tests

The rejection assertions in having-filter.test.ts were toThrow(/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 pin code + status + the verbatim RETIRED_FILTER_OPERATORS[op].why, on both branches, in both positions, and through applyHaving (the entry point engine.aggregate() calls, so the envelope is asserted on the error that actually leaves the module).

New having-filter-text-conformance.test.ts drives this face against FILTER_TEXT_CASES — the standard the driver suites answer and which packages/objectql imported nowhere. having had no conformance-table coverage at all (check-driver-conformance.mjs scopes to packages/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

  1. objectql having has no $icontains comparand-shape gate — an empty comparand matches EVERY row (2 of 5 FILTER_TEXT_CASES rejection rows unenrollable) #7158having has no $icontains comparand-shape gate. Driving the fixture surfaced that 2 of its 5 rejection rows are not refused by this face at all: an empty $icontains comparand 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.

  2. check-driver-conformance.mjs FILTER_TEXT requirement-3 calls driver-mongodb's arm "the last place a bare new Error escaped the ADR-0112 envelope" — it was not, and after #7047 the sentence names the wrong site #7159scripts/check-driver-conformance.mjs FILTER_TEXT requirement-3 is stale. It calls driver-mongodb's arm "the last place a bare new Error escaped the ADR-0112 envelope". That was already false when written (having was 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

…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.
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 1:27am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx (via packages/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectql having 拒收 retired/unknown 算子时缺 ADR-0112 信封(裸 Error,无 code/status)—— 五个拒收面中唯一一个

2 participants