Found while repairing #7349 (PR #7377) on origin/maind53e472. Deliberately not changed there: #7349's defect class is "an unrecognised operator matches every row", and this is a recognised operator computing the wrong answer. Changing it would have moved rows for filters that work today, under cover of a card about filters that do not work at all. Dedupe: listed all 334 open issues via the repo-scoped REST endpoint and grepped titles for icontains / case.sensit / case.insensit — no hit; control term 7349 fired on the same listing.
The defect
packages/core/src/adapters/ValueDataSource.ts, the contains arm of the in-memory matcher, unchanged in shape since before #7349:
case'contains': {constlv=typeofvalue==='string' ? value.toLowerCase() : '';returntypeofvalue==='string'&&lv.includes(String(target).toLowerCase());}Both sides are lowercased, so contains performs a case-insensitive substring match.
The spec treats these as two different questions, not two spellings of one. AST_OPERATOR_MAP (@objectstack/spec/data) lowers contains to $contains and icontains to $icontains, and the map's own comment records the ruling for the neighbouring pair: an operator "must never be folded onto" its case-variant sibling "in either vocabulary, nor the reverse. That is a semantic boundary, not two spellings of one thing." icontains entered the view vocabulary in #5328 specifically because the case-insensitive capability was authorable from only one of the three dialects — which presupposes contains is the case-SENSITIVE one.
Consequence
A provider: 'value' list filtered with contains returns strictly more rows than the same filter run against a real driver, which executes $contains case-sensitively. The two disagree on exactly the rows whose case differs — and both look plausible on screen. Same failure shape as #7349 (a silently wider in-memory answer), one layer down: the operator is recognised, it just answers a different question.
PR #7377 preserved the existing behaviour deliberately and mapped icontains onto the same body, so today contains and icontains are executed identically. That is the state this card exists to resolve.
Why it needs a decision rather than an obvious patch
Making contains case-sensitive is the spec-correct direction, but it is a behaviour change for filters that work today: an author who wrote contains: 'admin' against rows holding Admin currently gets matches and would stop getting them. The alternatives are (a) make contains case-sensitive and let icontains keep the current body, (b) leave both case-insensitive and record the divergence from the wire as accepted, (c) something staged. This is a small enough surface that (a) is probably right, but it moves shipped results, so it should be someone's call rather than a passing fix.
Suggested acceptance criteria
contains and icontains no longer execute the same predicate in ValueDataSource.- A test pins each against a row set whose case differs, so the two cannot silently re-converge.
- The chosen direction is stated where the operator arm lives.
Found while repairing #7349 (PR #7377) on
origin/maind53e472. Deliberately not changed there: #7349's defect class is "an unrecognised operator matches every row", and this is a recognised operator computing the wrong answer. Changing it would have moved rows for filters that work today, under cover of a card about filters that do not work at all. Dedupe: listed all 334 open issues via the repo-scoped REST endpoint and grepped titles foricontains/case.sensit/case.insensit— no hit; control term7349fired on the same listing.The defect
packages/core/src/adapters/ValueDataSource.ts, thecontainsarm of the in-memory matcher, unchanged in shape since before #7349:Both sides are lowercased, so
containsperforms a case-insensitive substring match.The spec treats these as two different questions, not two spellings of one.
AST_OPERATOR_MAP(@objectstack/spec/data) lowerscontainsto$containsandicontainsto$icontains, and the map's own comment records the ruling for the neighbouring pair: an operator "must never be folded onto" its case-variant sibling "in either vocabulary, nor the reverse. That is a semantic boundary, not two spellings of one thing."icontainsentered the view vocabulary in #5328 specifically because the case-insensitive capability was authorable from only one of the three dialects — which presupposescontainsis the case-SENSITIVE one.Consequence
A
provider: 'value'list filtered withcontainsreturns strictly more rows than the same filter run against a real driver, which executes$containscase-sensitively. The two disagree on exactly the rows whose case differs — and both look plausible on screen. Same failure shape as #7349 (a silently wider in-memory answer), one layer down: the operator is recognised, it just answers a different question.PR #7377 preserved the existing behaviour deliberately and mapped
icontainsonto the same body, so todaycontainsandicontainsare executed identically. That is the state this card exists to resolve.Why it needs a decision rather than an obvious patch
Making
containscase-sensitive is the spec-correct direction, but it is a behaviour change for filters that work today: an author who wrotecontains: 'admin'against rows holdingAdmincurrently gets matches and would stop getting them. The alternatives are (a) makecontainscase-sensitive and leticontainskeep the current body, (b) leave both case-insensitive and record the divergence from the wire as accepted, (c) something staged. This is a small enough surface that (a) is probably right, but it moves shipped results, so it should be someone's call rather than a passing fix.Suggested acceptance criteria
containsandicontainsno longer execute the same predicate inValueDataSource.