You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
driver-memory's reference matcher matches a NULL-VALUED row against a well-formed bounded $between — the live mingo path excludes it, so one package answers one filter two ways #13549
Filed unassigned by the #13524 dev while sweeping the declared operator vocabulary through both faces of this package. Recording only — no severity asserted, routing is triage's.
The clobber card needed a single-operator agreement baseline before it could score composed cells, so it measured every declared operator alone on both faces. Seventeen of eighteen agreed. $between did not.
Measured
Executed on 50cf2940b9. Fixture, five rows, both readings of "no value":
Filter {v: {$between: ['2026-07-01', '2026-07-15']}} — bounds well formed, both non-null:
face
ids
driver-memory live mingo path (find())
['1','2']
driver-memory reference matcher (match())
['1','2','4']
Row 4 has no value. The reference matcher says it lies inside a bounded range. Row 5 — the same absence spelled as a missing key — is excluded, so the matcher also disagrees with itself across the two readings of "no value".
For contrast, measured in the same run: the other seventeen declared operators ($eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, $notContains, $startsWith, $endsWith, $icontains, $like, $ilike, $null, $exists) each returned the identical id set on both faces on this fixture.
Cause
packages/drivers/driver-memory/src/memory-matcher.ts, the $between arm:
With value === null and string bounds, relational comparison coerces null to 0 and compares it against a string, so BOTH null < '2026-07-01' and null > '2026-07-15' are false. Neither disjunct fires, nothing returns false, and the range stops constraining — the no-value row matches. The MISSING reading escapes only because the pre-switch guard in checkCondition short-circuits on value === undefined before this arm runs, which is why the two readings come apart.
This is the WIDENING direction: on an RLS read scope a range that stops bounding is a permission bypass rather than a degraded filter (#3948).
#13524 is the composed-constraint clobber in the two LIVE translators. Its repair leaves the reference matcher untouched by design — the matcher is that card's ORACLE, and it loops the operators so it cannot express the clobber at all. This single-operator divergence predates and survives that repair; #13524's vocabulary sweep is written to score the live path against itself for exactly this reason, so it is green with this cell still open.
Related
#13495 (the null-BOUND axis of the same arm) · #13494 (the $eq: null guard) · #13357 (null comparands, awaiting a ruling) · #13166 (the same guard, negation allowlist) · #5328 (the $between SHAPE refusal, closed) · #3948 (widening on a read scope) · #13524 (the card that measured this)
Filed unassigned by the #13524 dev while sweeping the declared operator vocabulary through both faces of this package. Recording only — no severity asserted, routing is triage's.
The clobber card needed a single-operator agreement baseline before it could score composed cells, so it measured every declared operator alone on both faces. Seventeen of eighteen agreed.
$betweendid not.Measured
Executed on
50cf2940b9. Fixture, five rows, both readings of "no value":Filter
{v: {$between: ['2026-07-01', '2026-07-15']}}— bounds well formed, both non-null:find())['1','2']match())['1','2','4']Row 4 has no value. The reference matcher says it lies inside a bounded range. Row 5 — the same absence spelled as a missing key — is excluded, so the matcher also disagrees with itself across the two readings of "no value".
For contrast, measured in the same run: the other seventeen declared operators (
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin,$contains,$notContains,$startsWith,$endsWith,$icontains,$like,$ilike,$null,$exists) each returned the identical id set on both faces on this fixture.Cause
packages/drivers/driver-memory/src/memory-matcher.ts, the$betweenarm:With
value === nulland string bounds, relational comparison coercesnullto0and compares it against a string, so BOTHnull < '2026-07-01'andnull > '2026-07-15'are false. Neither disjunct fires, nothing returns false, and the range stops constraining — the no-value row matches. The MISSING reading escapes only because the pre-switch guard incheckConditionshort-circuits onvalue === undefinedbefore this arm runs, which is why the two readings come apart.This is the WIDENING direction: on an RLS read scope a range that stops bounding is a permission bypass rather than a degraded filter (#3948).
Not a duplicate of the two queued matcher cards
{$between: [null, null]}with EVERY valued row — the range arm's two comparisons are both false against a null bound, so a bounded range stops bounding #13495 is the same arm and the same coercion, but on the comparand axis:{$between: [null, null]}, a null BOUND, matching every VALUED row. This card is the value axis: valid non-null bounds, a null STORED VALUE. Fixing null bounds does not decide what a null value does, and driver-memory's reference matcher answers{$between: [null, null]}with EVERY valued row — the range arm's two comparisons are both false against a null bound, so a bounded range stops bounding #13495's own body asserts no target for either.{$eq: null}with NO MATCH on a MISSING key — it is the one surface of five that does not read$eq: nullas the null predicate (#5332) #13494 is the$eq: nullscalar spelling against a MISSING key, in the pre-switch guard rather than the range arm.Both were read before filing; neither carries this cell.
Not in scope for #13524
#13524 is the composed-constraint clobber in the two LIVE translators. Its repair leaves the reference matcher untouched by design — the matcher is that card's ORACLE, and it loops the operators so it cannot express the clobber at all. This single-operator divergence predates and survives that repair; #13524's vocabulary sweep is written to score the live path against itself for exactly this reason, so it is green with this cell still open.
Related
#13495 (the null-BOUND axis of the same arm) · #13494 (the
$eq: nullguard) · #13357 (null comparands, awaiting a ruling) · #13166 (the same guard, negation allowlist) · #5328 (the$betweenSHAPE refusal, closed) · #3948 (widening on a read scope) · #13524 (the card that measured this)