Skip to content

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

Description

@zhuangjianguo

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":

{id:'1', v:'2026-07-01'}  {id:'2', v:'2026-07-15'}  {id:'3', v:'2026-07-28'}
{id:'4', v:null}          {id:'5'}                       (key ABSENT)

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:

if (Array.isArray(target) && (value < target[0] || value > target[1])) return false;

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).

Not a duplicate of the two queued matcher cards

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: 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)

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions