Filed unassigned by the #13494 + #13495 + #13549 dev while measuring that fold's arms across a NUMERIC fixture. Recording only — no severity asserted, routing is triage's.
This is the ordering-operator sibling of #13549, in four arms that no queued card names. It is reported rather than repaired because it is a different arm family from the one those three cards cover, and the PR that closes them deliberately leaves it alone.
Measured
Executed on df18120502 (i.e. after PR #13550 landed), on both faces of packages/drivers/driver-memory, fixture:
{id:'1', n:5} {id:'2', n:0} {id:'3', n:null} {id:'4'} (key ABSENT)
| filter |
live mingo path |
reference matcher |
|
{n: {$gte: -1}} |
['1','2'] |
['1','2','3'] |
DISAGREE |
{n: {$gt: -1}} |
['1','2'] |
['1','2','3'] |
DISAGREE |
{n: {$lte: 1}} |
['2'] |
['2','3'] |
DISAGREE |
{n: {$lt: 1}} |
['2'] |
['2','3'] |
DISAGREE |
Row 3 has no value. The matcher says it is greater than -1 and less than 1 at the same time.
Cause
packages/drivers/driver-memory/src/memory-matcher.ts, the four ordering arms:
case '$gte':
if (!(value >= target)) return false;
break;
With value === null and a NUMERIC comparand, JS coerces null to 0, so null >= -1 is a true comparison between two numbers rather than a meaningless one. The arm has no reason to exclude the row, and the no-value row lands inside the bound.
The MISSING reading (row 4) escapes only because the pre-switch guard in checkCondition short-circuits on value === undefined before these arms run — so the matcher also disagrees with ITSELF across the two readings of "no value", the same shape #13549 recorded for $between.
Why a STRING fixture hides it
Every fixture in #13494, #13495 and #13549 uses ISO date strings, and there null >= '2026-07-01' compares 0 against NaN and is false, so the arms look correct. #13549's body records that "the other seventeen declared operators each returned the identical id set on both faces on this fixture" — that observation is true and is fixture-specific: the four ordering arms agree on strings and disagree on numbers. The $eq/$in/$contains families are unaffected either way, because none of them use a relational comparison.
Not covered by the queued cards
After the fold's PR lands, $between and the four ordering arms answer a null-valued numeric row differently from each other — $between excludes it, $gte/$lte include it — even though the live path compiles $between INTO $gte plus $lte. That inconsistency is pre-existing and in the opposite direction on string columns today; closing it is what this card is for.
Deliberately not asserting the target
The live path excludes the no-value row, and the platform's settled reading is that only the negation-carrying operators ($ne / $nin / $notContains) admit a no-value row (#5298 option A, re-affirmed 2026-08-10). Both point the same way, but null in an ordering position was recorded by the #5332 lane as "positions no ruling covers", so the target is left to triage rather than asserted here.
Related
#13549 (the $between value axis) · #13495 (the $between comparand axis) · #13494 (the $eq: null guard) · #13357 (the list spellings, awaiting a ruling) · #13166 (the same guard, negation allowlist) · #3948 (widening on a read scope)
Filed unassigned by the #13494 + #13495 + #13549 dev while measuring that fold's arms across a NUMERIC fixture. Recording only — no severity asserted, routing is triage's.
This is the ordering-operator sibling of #13549, in four arms that no queued card names. It is reported rather than repaired because it is a different arm family from the one those three cards cover, and the PR that closes them deliberately leaves it alone.
Measured
Executed on
df18120502(i.e. after PR #13550 landed), on both faces ofpackages/drivers/driver-memory, fixture:{n: {$gte: -1}}['1','2']['1','2','3']{n: {$gt: -1}}['1','2']['1','2','3']{n: {$lte: 1}}['2']['2','3']{n: {$lt: 1}}['2']['2','3']Row 3 has no value. The matcher says it is greater than
-1and less than1at the same time.Cause
packages/drivers/driver-memory/src/memory-matcher.ts, the four ordering arms:With
value === nulland a NUMERIC comparand, JS coercesnullto0, sonull >= -1is a true comparison between two numbers rather than a meaningless one. The arm has no reason to exclude the row, and the no-value row lands inside the bound.The MISSING reading (row 4) escapes only because the pre-switch guard in
checkConditionshort-circuits onvalue === undefinedbefore these arms run — so the matcher also disagrees with ITSELF across the two readings of "no value", the same shape #13549 recorded for$between.Why a STRING fixture hides it
Every fixture in #13494, #13495 and #13549 uses ISO date strings, and there
null >= '2026-07-01'compares0againstNaNand is false, so the arms look correct. #13549's body records that "the other seventeen declared operators each returned the identical id set on both faces on this fixture" — that observation is true and is fixture-specific: the four ordering arms agree on strings and disagree on numbers. The$eq/$in/$containsfamilies are unaffected either way, because none of them use a relational comparison.Not covered by the queued cards
$between— the live mingo path excludes it, so one package answers one filter two ways #13549 is the$betweenarm on the value axis. Its repair decides comparability before comparing, which fixes$betweenon numeric columns too — but it is confined to that arm, so these four keep today's 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 is the$betweenCOMPARAND axis.$in:[null]/$nin:[null]disagree while$null/$ne:nullagree #13357 is$in: [null]/$nin: [null],needs-user-decision, and is a comparand question rather than a stored-value one.After the fold's PR lands,
$betweenand the four ordering arms answer a null-valued numeric row differently from each other —$betweenexcludes it,$gte/$lteinclude it — even though the live path compiles$betweenINTO$gteplus$lte. That inconsistency is pre-existing and in the opposite direction on string columns today; closing it is what this card is for.Deliberately not asserting the target
The live path excludes the no-value row, and the platform's settled reading is that only the negation-carrying operators (
$ne/$nin/$notContains) admit a no-value row (#5298 option A, re-affirmed 2026-08-10). Both point the same way, but null in an ordering position was recorded by the #5332 lane as "positions no ruling covers", so the target is left to triage rather than asserted here.Related
#13549 (the
$betweenvalue axis) · #13495 (the$betweencomparand axis) · #13494 (the$eq: nullguard) · #13357 (the list spellings, awaiting a ruling) · #13166 (the same guard, negation allowlist) · #3948 (widening on a read scope)