Skip to content

fix(driver-mongodb): the $contains family is case-SENSITIVE — drop the hardcoded $options: 'i' (#6682) - #7625

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-6682-mongodb-contains-case
Aug 11, 2026
Merged

fix(driver-mongodb): the $contains family is case-SENSITIVE — drop the hardcoded $options: 'i' (#6682)#7625
huangyiirene merged 1 commit into
mainfrom
claude/issue-6682-mongodb-contains-case

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Part of #6682 — the driver-mongodb half only. The driver-memory half stays frozen under #5499, so this card must remain OPEN after merge.

What changed

translateFieldOperators (packages/drivers/driver-mongodb/src/mongodb-filter.ts) lowered $contains / $notContains / $startsWith / $endsWith to a $regex with a hardcoded $options: 'i' beside it — MongoDB's full-Unicode case fold. That flag is off all four arms.

// before // after{ name: {$regex: 'acme',$options: 'i'}}{name: {$regex: 'acme'}}{ name: { $not: { $regex: 'acme',{name: {$not: {$regex: 'acme'}}}
$options: 'i'}}}

{ name: { $contains: 'acme' } } no longer returns ACME Corp. escapeRegex is untouched — dropping the flag changes which cases match, never which characters are metacharacters, so a.b still matches a.b and not axb.

$notContains needed the treatment in one place, not two: its $not-wrapped operator document is the only other site, and a flag left there would have made the negation exclude rows the positive form includes.

$icontains is untouched. Its fold has always lived in the pattern (asciiCaseInsensitiveRegexSource, one [Aa] class per ASCII letter), which is why it was already the one arm without the flag. After this change $options is spelled nowhere in the emitter — the right shape, since $options is a RETIRED operator this same driver refuses on input (#5702).

Why — and why both directions of the defect mattered

#4706Q2 = A: the $contains family is case-SENSITIVE on every backend. The fold over-matched — rows the filter excludes were returned, which on an RLS read scope is over-reach rather than a loose filter (#3948) — and it folded the whole Unicode range, overshooting the ASCII-only boundary #4706Q1 = A holds $icontains to.

Multi-face declaration — the whole scoreboard, with evidence

face{ name: { $contains: 'acme' } } vs ACME Corpevidence
driver-sql / -sqlite-wasm / -turso (local + remote)does NOT match#6518GLOB on the SQLite dialects, LIKE over a binary cast on MySQL, LIKE unchanged on Postgres; turso-local-remote-text-parity.test.ts holds both transports to the same rows
@objectstack/formulamatchesFilterConditiondoes NOT matchalready compliant (String.includes), re-measured #6993
ObjectQL havingdoes NOT matchalready compliant; having-filter-text-conformance.test.ts
service-analytics (both SQL compilers)does NOT matchalready compliant — Postgres-shaped LIKE
driver-mongodbdoes NOT match — FIXED HEREmongodb-filter-text-conformance.test.ts, this PR
driver-memory — query + analytics facesMATCHESout of scope — frozen under #5499; DEBT row retained
driver-memory — reference matcherdoes NOT match⛔ out of scope — that package's two-face divergence is #6682's remaining half

One more face worth naming, because "not mentioned" reads as "not checked": this driver has no second lowering. find / count / update / delete (mongodb-driver.ts) and the aggregation $match (mongodb-aggregation.ts:164) all route through the one translateFilter, so unlike driver-memory there was no analytics face to align separately.

Conformance cell CLEARED — measured, not argued

The FILTER_TEXT_CASES cell is enrolled and its DEBT row deleted in this PR, because scripts/check-driver-conformance.mjs judges a cell by IMPORT: deleting the row without the suite fails CONSUMED, keeping it alongside the suite fails RECONCILED.

The card assumed enrollment was blocked on #6520. It is not — $icontains (requirement 1) landed on this driver in #6520, so this change closes the last open requirement. Verified by running the case-set against origin/main @ 744b8f5before the fix:

Tests 5 failed | 17 passed (22)
× $contains is case-SENSITIVE — a lower-case comparand misses the upper-case row
emitted: {"name":{"$regex":"acme","$options":"i"}}: expected [ '1', '2' ] to equal [ '2' ]
× $contains is case-SENSITIVE — an upper-case comparand misses the lower-case row
× $startsWith is case-SENSITIVE
× $endsWith is case-SENSITIVE
× $notContains is case-SENSITIVE, and negation does not widen it

Exactly the five requirement-2 rows — every $icontains row, every literal-comparand row and all five rejection rows already passed. After the fix: 22 passed. So the flag was the whole remaining gap, and nothing was widened to reach green.

New suitemongodb-filter-text-conformance.test.ts:

  • Server-free. This package's real-mongod suites are opt-in (OS_TEST_MONGODB_MEMORY_SERVER_ENABLED=1, driver-mongodb 测试:两个套件并发下载 MongoDB 二进制,mongodb-memory-server 的 rename 竞态让全绿测试 exit 1(队列 flaky) #5517), so a suite needing a server would not run in CI. Same judgement as mongodb-filter-logic-translation.test.ts and mongodb-aggregation-translation.test.ts. The real-mongod half remains absent and is recorded as such rather than implied.
  • It evaluates the emitted documents over FILTER_TEXT_ROWS rather than pinning their spelling, and the matcher models $options instead of ignoring it — a matcher that dropped the flag would have reported the fixed answer from the broken translator. Its own discrimination is proved: { $regex: 'acme', $options: 'i' } selects ['1','2'], the bare pattern selects ['2'].
  • Refusal rows assert code + status per ADR-0112, not a bare toThrow() — this driver is the reason that bar exists (its default: arm threw a bare new Error until drivers: $regex 响亮拒收 + $icontains 各后端实现(#4706 裁决 B 案 · 驱动半边) #5702).
  • A guard row pins the five declared rejection-case names, so a new refusal joining the table forces a re-read instead of silently skipping.

Gate now reads: driver-mongodb … FILTER_TEXT ok AGGREGATION ok38 covered cell(s), 2 in the DEBT ledger (both driver-memory).

Pins FLIPPED, not deleted

Every pin that encoded the folded answer now asserts the new substance:

  • mongodb-filter.test.ts — the five string-operator pins and the parseFilterASTcontains row, plus a new family-wide assertion that no arm emits $options at all.
  • mongodb-icontains.test.ts⚠️one deviation from the dispatch note, reported rather than worked around. That file carried a pin explicitly asserting the defect: it('does not disturb the $contains family — still \$options: i` (the open drivers(memory, mongodb): the $contains family still folds case — the last two backends left on the wrong side of #4706 Q2 = A #6682 defect)'). It is labelled in-file as "pinned here as the current state", i.e. authored to go red exactly when this card lands. It is flipped to assert the ruled answer and extended to assert the two arms have **not** collapsed into one ($contains→ bare pattern,$icontains[Aa][Cc][Mm][Ee]), which is what that row has always guarded. **Every $icontains` assertion in that file is unchanged and green.**

Refusal pins for genuinely invalid shapes are verbatim.

Docs and docblocks corrected

Sites that recorded this driver as folding: packages/spec/src/data/filter-text-conformance.ts (header + the $contains section + one case note), packages/spec/src/data/filter.zod.ts, and the user-facing content/docs/protocol/objectql/query-syntax.mdx (two callouts — the second also still described the SQL family as unaligned, stale since #6518, corrected in the same breath rather than left half-true). The historical "measured before the ruling" table in filter.zod.ts is deliberately untouched — it is a record of what was true then.

⛔ No content/docs/releases/ edit. The changeset is .changeset/mongodb-contains-case-sensitive.mdpatch for @objectstack/driver-mongodb, and it tells a user relying on the fold to write $icontains.

Gates run

gateresult
node scripts/check-driver-conformance.mjsOK — 38 covered, 2 DEBT, 0 exempt
node scripts/check-driver-conformance.mjs --self-testOK
driver-mongodb suite325 passed, 143 skipped (5 files skipped — real-mongod, opt-in)
spec filter-text-conformance + filter-operator-vocabulary36 passed
tsc --noEmit — driver-mongodb, specclean
build — spec, driver-mongodbclean

mongodb-memory-server is not downloadable in this environment (proxy 403), so the real-mongod suites were not executed; they are skipIf-gated and skipped on main too. All verification is at the translateFilter translation layer.


Generated by Claude Code

…the hardcoded `$options: 'i'` (#6682)
`translateFieldOperators` lowered `$contains` / `$notContains` / `$startsWith` /
`$endsWith` to a `$regex` with `$options: 'i'` beside it — MongoDB's full-Unicode
case fold. #4706 Q2 = A rules that family case-SENSITIVE on every backend, and
this driver was the last face still on the wrong side of it (#6518 flipped the
SQL family; `formula`, ObjectQL's `having` and service-analytics were already
compliant).
Both directions of the defect mattered: the fold OVER-matched — rows the filter
excludes came back, which on an RLS read scope is over-reach rather than a loose
filter (#3948) — and it folded the whole Unicode range, overshooting the
ASCII-only boundary Q1 = A holds `$icontains` to.
`escapeRegex` is untouched, so comparands stay LITERAL; dropping the flag changes
which CASES match, never which characters are metacharacters. `$icontains` is
untouched too — its fold has always lived in the pattern
(`asciiCaseInsensitiveRegexSource`), which is why it was the one arm without the
flag. `$options` is now spelled nowhere in the emitter, which is the right shape:
it is a RETIRED operator this same driver refuses on input (#5702).
The `FILTER_TEXT_CASES` cell is CLEARED in the same PR, because the gate judges a
cell by IMPORT and both halves have to move together. Measured rather than
argued: driving the case-set against `origin/main` @ `744b8f5` failed exactly the
five case-sensitivity rows and passed the other twelve — every `$icontains` row,
every literal-comparand row and all five rejection rows — so the flag was the
whole remaining gap.
- NEW `mongodb-filter-text-conformance.test.ts`: server-free (this package's
real-mongod suites are opt-in per #5517), imports the marker and evaluates the
emitted documents over `FILTER_TEXT_ROWS`. Rejection rows assert `code` +
`status` per ADR-0112, and the matcher models `$options` rather than ignoring
it, so it fails the folded translator.
- The mongodb DEBT row is deleted from `scripts/check-driver-conformance.mjs`.
`driver-memory`'s row stands — that half stays frozen under #5499.
- Pins FLIPPED, not deleted: `mongodb-filter.test.ts` and the `$contains`
contrast row in `mongodb-icontains.test.ts` now assert the ruled answer, plus a
new family-wide assertion that no arm emits `$options` at all. Every
`$icontains` assertion is unchanged.
- Docs and spec docblocks corrected where they recorded this driver as folding.
Part of #6682 — the driver-memory half stays open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqhD3BLddBixYd6y46mM2L
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 11, 2026 9:15am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-mongodb, @objectstack/spec.

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

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx(via @objectstack/driver-mongodb)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

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

  • content/docs/releases/implementation-status.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@huangyiirene@claude