Uh oh!
There was an error while loading. Please reload this page.
fix(driver-mongodb): the $contains family is case-SENSITIVE — drop the hardcoded $options: 'i' (#6682) - #7625
Conversation
…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
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
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/$endsWithto a$regexwith a hardcoded$options: 'i'beside it — MongoDB's full-Unicode case fold. That flag is off all four arms.{ name: { $contains: 'acme' } }no longer returnsACME Corp.escapeRegexis untouched — dropping the flag changes which cases match, never which characters are metacharacters, soa.bstill matchesa.band notaxb.$notContainsneeded 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.$icontainsis 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$optionsis spelled nowhere in the emitter — the right shape, since$optionsis a RETIRED operator this same driver refuses on input (#5702).Why — and why both directions of the defect mattered
#4706Q2 = A: the
$containsfamily 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$icontainsto.Multi-face declaration — the whole scoreboard, with evidence
{ name: { $contains: 'acme' } }vsACME Corpdriver-sql/-sqlite-wasm/-turso(local + remote)GLOBon the SQLite dialects,LIKEover a binary cast on MySQL,LIKEunchanged on Postgres;turso-local-remote-text-parity.test.tsholds both transports to the same rows@objectstack/formulamatchesFilterConditionString.includes), re-measured #6993havinghaving-filter-text-conformance.test.tsLIKEdriver-mongodbmongodb-filter-text-conformance.test.ts, this PRdriver-memory— query + analytics facesdriver-memory— reference matcherOne 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 onetranslateFilter, so unlike driver-memory there was no analytics face to align separately.Conformance cell CLEARED — measured, not argued
The
FILTER_TEXT_CASEScell is enrolled and its DEBT row deleted in this PR, becausescripts/check-driver-conformance.mjsjudges 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 againstorigin/main@744b8f5before the fix:Exactly the five requirement-2 rows — every
$icontainsrow, 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 suite
mongodb-filter-text-conformance.test.ts: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 asmongodb-filter-logic-translation.test.tsandmongodb-aggregation-translation.test.ts. The real-mongod half remains absent and is recorded as such rather than implied.FILTER_TEXT_ROWSrather than pinning their spelling, and the matcher models$optionsinstead 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'].code+statusper ADR-0112, not a baretoThrow()— this driver is the reason that bar exists (itsdefault:arm threw a barenew Erroruntil drivers:$regex响亮拒收 +$icontains各后端实现(#4706 裁决 B 案 · 驱动半边) #5702).Gate now reads:
driver-mongodb … FILTER_TEXT ok AGGREGATION ok—38 covered cell(s), 2 in the DEBT ledger(bothdriver-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 theparseFilterASTcontainsrow, plus a new family-wide assertion that no arm emits$optionsat all.mongodb-icontains.test.ts—it('does not disturb the $contains family — still \$options: i` (the open drivers(memory, mongodb): the$containsfamily 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$containssection + one casenote),packages/spec/src/data/filter.zod.ts, and the user-facingcontent/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 infilter.zod.tsis deliberately untouched — it is a record of what was true then.⛔ No
content/docs/releases/edit. The changeset is.changeset/mongodb-contains-case-sensitive.md— patch for@objectstack/driver-mongodb, and it tells a user relying on the fold to write$icontains.Gates run
node scripts/check-driver-conformance.mjsnode scripts/check-driver-conformance.mjs --self-testfilter-text-conformance+filter-operator-vocabularytsc --noEmit— driver-mongodb, specmongodb-memory-serveris not downloadable in this environment (proxy 403), so the real-mongod suites were not executed; they are skipIf-gated and skipped onmaintoo. All verification is at thetranslateFiltertranslation layer.Generated by Claude Code