Skip to content

fix(service-analytics): fence $icontains comparands on the analytics where door (#7693) - #7843

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-7693-icontains-comparand-fence
Aug 11, 2026
Merged

huangyiirene merged 1 commit into
mainfrom
claude/issue-7693-icontains-comparand-fence

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #7693

The measured defect

$icontains was the one text-pattern operator the #5234 comparand fence never covered on the analytics where door. It arrived after the fence: #6520 added it to filter-normalizer.ts's MONGO_TO_CUBE_OP and gave read-scope-sql.ts's arm its assertRenderableText call, but not the entry in comparand-shape.ts's TEXT_PATTERN_OPERATORS — the set the where door's shape gate reads.

Re-measured on origin/main @ b54aaab (the card measured @ 5823d593d, triage re-verified @ a3c0865; the premise holds unchanged on today's tip):

filter analytics where door read-scope-sql
{name: {$contains: {foo: 1}}} REFUSED (INVALID_FILTER / 400) REFUSED (READ_SCOPE_COMPILE_FAILED / 500)
{name: {$icontains: {foo: 1}}} compiled — leaf {operator: 'icontains', values: [{foo: 1}]} REFUSED

and the leaf reached the statement, verbatim from the reproduction:

sql    = … WHERE translate(name, 'ABC…', 'abc…') LIKE translate($1, 'ABC…', 'abc…') ESCAPE $2 …
params = ["%[object Object]%","\\"]

That is #5234's defect at the operator its fence was never extended to — a parameterised, syntactically perfect LIKE pattern nobody wrote, which a row whose text really is [object Object] matches. It is also a split inside one package, the shape #5234 / #5332 / #5567 each spent a round removing. #7158 closed the same gap at the objectql having face; this is the third and last.

The change

One entry in TEXT_PATTERN_OPERATORS. The gate in filter-normalizer.ts is generic over the set, so nothing else in the compiler moves.

The consumer measurement the card required (quoted-exact-name grep across the repo, not just the package): inside service-analytics the set has exactly one reader — strategies/filter-normalizer.ts :358 (import) and :577 (the gate in assertCompilableComparand) — plus its own definition. Every remaining hit is either a comment or driver-sql's module-private set of the same name (sql-driver.ts:1201), a separate declaration in a package this one does not depend on; it has listed $icontains since #6520, which is why the drivers were already right. So the fix really is one entry, measured rather than assumed.

What changes for a caller

A malformed $icontains comparand on the /analytics where door — an object, a {$field} reference, or an array — is now refused with INVALID_FILTER / 400 and the same sentence $contains gets (asserted byte-identical apart from the operator name), instead of compiling into a pattern that matches the wrong rows. Well-formed comparands are untouched: strings, numbers, null, booleans and Dates compile exactly as before, ASCII fold and metacharacter escaping included. The read-scope door is unchanged — it already refused these shapes.

Tests

Reverse verification, direction predicted before running: delete the entry and re-run the whole package → 5 failed / 1553 passed (with it: 1558 / 0). The five are exactly the where-door cells — three new ones, the now-unfiltered corpus case, and the fifth loop member. Every read-scope $icontains assertion and every narrowness control stayed green, which is the other half of the proof: that door's refusal comes from its own assertRenderableText, and a well-formed comparand compiles identically in both states, so the entry closes a hole rather than retiring the operator #6520 added.

Gates run locally

gate result
pnpm --filter @objectstack/service-analytics... build ✅ pass (closure, incl. driver-sql)
service-analytics package suite ✅ 72 files / 1558 tests pass
reverse verification (entry removed) ✅ 5 failed / 1553 passed, exactly the predicted cells
pnpm check:docs-audit-scope ✅ pass
eslint on the three changed files ✅ clean
tsc --noEmit (package) ⚠️ 10 pre-existing errors, all in untouched test files (measure-source-field-gate.test.ts, objectql-timedimension-projection.test.ts); zero in the files this PR touches
changeset ✅ .changeset/icontains-comparand-fence-analytics-where.md (patch — accept/reject behavior change on a served door)

No new test fakes were added, so the engine-double dispatch gate does not apply. No content/docs/releases/ edits.


Generated by Claude Code

…s `where` door (#7693)

`$icontains` was the one text-pattern operator the #5234 comparand fence never
covered on the analytics `where` door. It arrived after the fence: #6520 added
it to `MONGO_TO_CUBE_OP` and gave `read-scope-sql.ts`'s arm its
`assertRenderableText` call, but not the entry in `comparand-shape.ts`'s
`TEXT_PATTERN_OPERATORS` — the set the `where` door's shape gate reads. One
operator, two answers inside one package.

Measured on origin/main @ b54aaab: `{name: {$icontains: {foo: 1}}}` compiled to
the leaf `{operator: 'icontains', values: [{foo: 1}]}` and
`NativeSQLStrategy.generateSql` bound `'%[object Object]%'` into a
parameterised, syntactically perfect `LIKE` nobody wrote — while the sibling
read-scope door refused the same comparand. `$contains` refused it on both.

The fix is the entry in `TEXT_PATTERN_OPERATORS`; the gate in
`filter-normalizer.ts` is generic over the set. Consumer measurement, quoted
exact name across the repo: within this package the set has exactly one reader
(`filter-normalizer.ts` :358 import, :577 gate) plus its definition — every
other hit is a comment or `driver-sql`'s module-private set of the same name,
which has listed `$icontains` since #6520.

Tests: #7598's RECORDED GAP pin in `cross-field-reference-refusal.test.ts` is
FLIPPED to assert the refusal (`INVALID_FILTER` / 400, LIKE-family wording,
byte-identical to what `$contains` says), the shared #5222 corpus is now driven
whole there instead of filtering `$icontains` out, and the LIKE-family loops in
`comparand-shape-refusal.test.ts` gain their fifth member on both doors.

Reverse-verified over the whole package: deleting the entry gives 5 failed /
1553 passed, exactly the `where`-door cells; every read-scope `$icontains`
assertion and every narrowness control stays green, so the entry is shown to
close a hole rather than retire the operator #6520 added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M7h5X9FCDSAkT531cJZ9D2
@vercel

vercel Bot commented Aug 11, 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 11, 2026 8:26pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics.

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

  • content/docs/api/data-api.mdx (via @objectstack/service-analytics)
  • content/docs/api/index.mdx (via @objectstack/service-analytics)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-analytics)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/service-analytics)
  • content/docs/plugins/packages.mdx (via @objectstack/service-analytics)

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

  • content/docs/releases/implementation-status.mdx (via @objectstack/service-analytics)
  • content/docs/releases/v17.mdx (via @objectstack/service-analytics)
  • content/docs/releases/v9.mdx (via @objectstack/service-analytics)

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

2 participants