Found while landing #16810 (the array/Date comparand arm of the same function). Filed unassigned; the fix is deliberately NOT in that PR, which pins the behaviour below as unchanged so the refusal it lands could not move it by accident.
What is measured
driver-memory's two filter faces disagree when a scalar comparand meets a stored array value — and they disagree in BOTH directions, one widening and one narrowing. Measured on this repo's own driver, one row per case, with a firing control (a scalar comparand that legitimately matches answered true / 1 row and its negative twin false / 0 rows in the same run).
| filter |
stored value |
reference matcher (memory-matcher.ts) |
live query path (InMemoryDriver.find, mingo) |
{ tags: 'a' } |
['a','b'] |
false |
1 row |
{ tags: 'a,b' } |
['a','b'] |
true |
0 rows |
{ tags: 'a' } |
['a'] |
true |
1 row |
Both rows of the disagreement come from one line, checkCondition's return value == condition: == converts the stored ARRAY to a primitive, so ['a','b'] becomes the string "a,b". The matcher therefore answers a query nobody wrote — a row whose tags joins to "a,b" matches a comparand of "a,b" — while missing the membership reading ('a' against ['a','b']) that mingo gives on the live path.
The second row is the sharper one: it is a false positive, so a filter meant to narrow returns a row it should not. On an RLS read scope that direction is a permission concern rather than a degraded filter.
Why it is a card rather than a note
It is the #5240 / #5328 / #5347 / #13549 shape exactly — one filter, one package, two answers, nothing reported — and the package's whole refusal architecture exists to remove that class. It is not covered by #16810: that card's cell is the COMPARAND side (an array in the comparand position, now refused), while this is the VALUE side, which the spec's comparand door does not judge at all.
What is NOT decided here
Which face is right. The live path's membership reading is MongoDB's array semantics; the matcher's string-join reading is an accident of ==. A third answer (refuse, as the comparand side was ruled) is also available. The spec's comparand door rules comparands, so it does not settle a stored-array VALUE, and no ruling in the tree names this cell — which is why this is filed rather than fixed.
Repro
Both faces, same row, no build needed beyond the driver's own dependency closure:
import { match } from '@objectstack/driver-memory/src/memory-matcher.js';
match({ tags: ['a', 'b'] }, { tags: 'a,b' }); // true — the false positive
match({ tags: ['a', 'b'] }, { tags: 'a' }); // false — while the live path returns the row
Found while landing #16810 (the array/Date comparand arm of the same function). Filed unassigned; the fix is deliberately NOT in that PR, which pins the behaviour below as unchanged so the refusal it lands could not move it by accident.
What is measured
driver-memory's two filter faces disagree when a scalar comparand meets a stored array value — and they disagree in BOTH directions, one widening and one narrowing. Measured on this repo's own driver, one row per case, with a firing control (a scalar comparand that legitimately matches answeredtrue/ 1 row and its negative twinfalse/ 0 rows in the same run).memory-matcher.ts)InMemoryDriver.find, mingo){ tags: 'a' }['a','b']false{ tags: 'a,b' }['a','b']true{ tags: 'a' }['a']trueBoth rows of the disagreement come from one line,
checkCondition'sreturn value == condition:==converts the stored ARRAY to a primitive, so['a','b']becomes the string"a,b". The matcher therefore answers a query nobody wrote — a row whosetagsjoins to"a,b"matches a comparand of"a,b"— while missing the membership reading ('a'against['a','b']) that mingo gives on the live path.The second row is the sharper one: it is a false positive, so a filter meant to narrow returns a row it should not. On an RLS read scope that direction is a permission concern rather than a degraded filter.
Why it is a card rather than a note
It is the #5240 / #5328 / #5347 / #13549 shape exactly — one filter, one package, two answers, nothing reported — and the package's whole refusal architecture exists to remove that class. It is not covered by #16810: that card's cell is the COMPARAND side (an array in the comparand position, now refused), while this is the VALUE side, which the spec's comparand door does not judge at all.
What is NOT decided here
Which face is right. The live path's membership reading is MongoDB's array semantics; the matcher's string-join reading is an accident of
==. A third answer (refuse, as the comparand side was ruled) is also available. The spec's comparand door rules comparands, so it does not settle a stored-array VALUE, and no ruling in the tree names this cell — which is why this is filed rather than fixed.Repro
Both faces, same row, no build needed beyond the driver's own dependency closure: