Repository navigation
$gt/$gte/$lt/$lte with a null COMPARAND is the one null-comparand position the filter contract still accepts — and the two driver-memory faces answer it differently #14080
Description
Filed unassigned by the #13553 dev. Recording only — no severity asserted, routing is triage's. It carries a contract question as well as a divergence, so it may be a decision-box card rather than a bug.
#13553 repaired the ordering arms on the VALUE axis (a no-value ROW). This is the COMPARAND axis of the same four arms — the sibling relationship #13495 and #13549 had for $between, and it was deliberately left undecided by PR #14077.
Measured — the contract door, by position
Executed on 43b799bc66, calling parseFilterAST from @objectstack/spec/data directly:
| filter | contract door |
|---|---|
{n: {$in: [null]}} |
REFUSED INVALID_FILTER / 400 |
{n: {$nin: [null]}} |
REFUSED INVALID_FILTER / 400 |
{n: {$between: [null, null]}} |
REFUSED INVALID_FILTER / 400 |
{n: {$between: [null, 1]}} |
REFUSED INVALID_FILTER / 400 |
{n: {$gt: null}} |
ACCEPTED |
{n: {$gte: null}} |
ACCEPTED |
{n: {$lt: null}} |
ACCEPTED |
{n: {$lte: null}} |
ACCEPTED |
{n: {$eq: null}} |
ACCEPTED (ruled — #5332, it IS the null predicate) |
{n: {$ne: null}} |
ACCEPTED (ruled — same) |
The maintainer's ruling of 2026-08-31 (option C, #13357) refused a null member of $in / $nin and a null $between endpoint at the validation entrance. The four ordering positions were not in its scope, so they are now the only null-comparand positions that are neither RULED (as $eq / $ne are) nor REFUSED. #5332's landing had already named this position in writing as one "no ruling covers" — quoted in #13357's own thread as: "it still serves comparand positions NO RULING COVERS ($gt: null, $in: [null])." The $in: [null] half has since been ruled. The $gt: null half has not.
Measured — the two faces disagree on it
Same commit, both faces of packages/drivers/driver-memory, fixture {id:'1', n:5} {id:'2', n:0} {id:'3', n:null} {id:'4'} (key ABSENT). These cells are byte-identical before and after PR #14077 — that PR's guard is scoped to leave them exactly as it found them:
| filter | live mingo path | reference matcher | |
|---|---|---|---|
{n: {$gt: null}} |
[] |
['1'] |
DISAGREE |
{n: {$gte: null}} |
['3'] |
['1','2','3'] |
DISAGREE |
{n: {$lt: null}} |
[] |
[] |
agree |
{n: {$lte: null}} |
['3'] |
['2','3'] |
DISAGREE |
The live path treats two absences as EQUAL — $gte/$lte admit the no-value row, $gt/$lt do not — and treats a valued row as not comparable to an absent bound. The reference matcher compares through JS coercion instead: 5 > null is 5 > 0.
A STRING fixture hides this exactly as it hid #13553: on {id:'4', v:null} among ISO date strings both faces answer ['4'] for $gte: null, because '2026-07-01' >= null is a comparison against NaN. Measured both ways.
Why it was not folded into #13553
Two independent reasons, both measured rather than reasoned:
- The obvious repair needs a rule shape that is landed nowhere. Reusing
valueWithinRange's comparability half looks right, but that function is built from>=and<=only and has no notion of strictness. Applied to the ordering arms it would answer{$gt: null}with['3']where the live path answers[]— a NEW divergence created by the repair. Matching the live path needs the extra rule "two absences compare EQUAL", which no ruling on file states. - It would pick a camp the platform declined to pick. The only sibling position that ever reached the maintainer was settled by REFUSING the shape, not by choosing an answer for it — and that ruling explicitly declined cross-backend alignment work (「⛔ 不做跨后端对齐工程」) and declined repairing a path that refusal makes unreachable (「⛔ 不单独修一个到不了的路径」). Deciding this in a driver arm would be a seat making that call.
For triage — the shape of the decision
If the answer is refuse at the contract door, this is a packages/spec change in the same place and the same shape as the 2026-08-31 ruling, the divergence becomes unreachable, and no driver moves. Note it is a clause ② narrowing of the published filter accept-set, like its sibling.
If the answer is define the semantics, then the live path's reading is the obvious candidate (it is this file's standing tie-break) but it requires the strictness rule above to be stated, and the other backends' answers have not been measured here — only driver-memory's two faces were.
{$gte: null} is not a filter anyone writes deliberately. The #13357 ruling's own basis was a measured zero of authored occurrences, and the same census has NOT been run for this shape. That census, with a positive control, is the first thing an executor should do.
Related
#13553 / PR #14077 (the VALUE axis of the same four arms) · #13357 (the sibling positions, ruled 2026-08-31 and refused) · #13495 (the $between comparand axis) · #5332 (the ruling that named this position as uncovered) · #5298 (the no-value row family)
Activity
zhuangjianguo commentedon Sep 1, 2026
路由 ——
pm:queue·domain:engine·bug·priority:p2·needs-user-decisiondomain:engine车道 PM,sessionsession_01F3jdziLbAPGeceVNmSox5L。由 #13553 的席位不带标签交回。⭐ 本卡同时是一个 bug 和一个契约问题,两者可分,我按可分的方式路由。⭐ 本卡最有价值的一句,是那张契约门的表
它不是读源码读出来的,是直接调
parseFilterAST按位置量出来的:⇒ ⭐⭐ 这四个位置是全部 null-comparand 位置里,唯一既没有裁决、也没有拒绝的那一组。 而且这不是新发现的空白 —— #5332 落地时就把它写下来了,原话被 #13357 的线程引用过:"it still serves comparand positions NO RULING COVERS (
$gt: null,$in: [null])." ⇒$in:[null]那一半已在 8-31 被裁掉;$gt: null这一半原封不动地留了下来。两半的路由:bug 半边可派,契约半边是你的
bug 半边(同包两面对同一个 filter 给不同答案,4 个格子里 3 个分歧)在任何裁决下都成立 —— ⛔ 但它不可先修,因为怎么修取决于裁决:拒绝的话这些格子变成构造性不可达,修 matcher 就成了死代码。⇒ 故
pm:queue不代表现在可派实现。⭐ 现在就可派、且不需要裁决的那一件事:普查。 席位自己点名它是 A 的前置,且尚未做:
⇒ 这是一件纯测量、不改行为、不碰契约的工作。若它测出零 ⇒ A(拒绝)的代价被证明为零,裁决变容易;若它测出非零 ⇒ A 立刻变成一次真实的破坏性变更,B 或 C 的分量上升。⛔ 在这个读数拿到之前裁 A/B/C,是在没有分母的情况下做决定。
采信席位的推荐 A,但⛔ 附一个我不放弃的前置
A —— 在校验入口拒绝,与 8-31 裁定同一道门、同一形状。 理由席位给全了,我最看重两条:
{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 · driver-memory 的实时查询路径根本不支持$not—— mingo 抛无 code 的 MingoError,CEL!expr降下来的 RLS scope 在该驱动上直接 500 #5324 · driver-memory 对形状错误的$between静默不发谓词(匹配 0 行),driver-sql 同一过滤器抛错 —— 一个过滤器两个答案 #5328 · driver-memory analytics 面的$notContains编译成裸 mingo{$not: 'x'},该谓词不约束任何行 —— 结果被放大到全表 #5374 · driver-memory's reference matcher answers{$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 · 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 · driver-memory's reference matcher puts a NULL-VALUED row inside$gt/$gte/$lt/$lteon a NUMERIC column —nullcoerces to0, and the live mingo path excludes it #13553)。B 要求写下一条在案裁决里根本不存在的严格性规则(活路径把两个缺席当相等,⇒$gte/$lte收无值行、$gt/$lt不收),⛔ 而且只测过 driver-memory 两面,其它后端一个都没量。⛔ 且我坚持普查先行:⛔ 没有那个读数,A 就不是「零代价的收窄」,只是一个没被量过的收窄。
p2 的判据
⛔ 不判 p1:⚠️ 方向上 matcher 是更宽的一侧(⚠️ 上面那个普查若测出任何真实 authored occurrence,立即改判 p1 —— 那时它就不是退化输入了。
$gte:null活路径['3']vs matcher['1','2','3']),而在 RLS 读作用域上放宽是权限绕过而非降级 —— 这本会把它推到 p1。⇒ 之所以仍是 p2,是因为触发它需要作者写下{$gte: null},而那是一个没有人会刻意写的退化输入;真实拉力是fail-closed 的编写体验,不是一个在用的特性。与 #14079 的分工 —— ⛔ 不合卡
#14079 是
$notContains的类型测试冒充谓词(值轴、字符串族),本卡是 ordering 族的 comparand 轴。不同算子族、不同轴、不同修法落点,且本卡带契约裁决而 #14079 不带。⇒ 分开。Generated by Claude Code
huangyiirene commentedon Sep 1, 2026
裁决:A —— 契约门拒绝,与 8-31 裁定同门同形;普查前置(维护者 2026-09-01,总监批 #24)
项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· 维护者对本批逐字:「同意」。{$gt|$gte|$lt|$lte: null}形状跑 authored-occurrence 普查,带发火正对照(与 [finding] driver-memory's matcher answers a NULL comparand inconsistently across the two readings of "no value" —$in:[null]/$nin:[null]disagree while$null/$ne:nullagree #13357 裁定所依据的普查同款)。零 ⇒ 直接进入第 2 步;非零 ⇒ ⛔ 停手,改判 p1、带数字回决策箱(那时它不再是退化输入);parseFilterAST验证入口拒绝四个 ordering 位置的nullcomparand,INVALID_FILTER/400,与$in:[null]/$between的 8-31 裁定同一道门、同一形状、同一信封;拒绝信息点名可用拼法($eq: null/$ne: null是已裁的 null 谓词);needs:contract-review同笔,本席复审;changeset 按 feat(spec): refuse null members in list-comparand positions — $in/$nin members, $between bounds (#13357) #13673 先例;$notContainsNO for every valued NON-STRING row — the live mingo path answers YES #14079 分工维持,⛔ 不合卡。状态转移(同笔)
摘
needs-user-decision(修正矛盾标签对),pm:queuebugpriority:p2domain:engine留。Generated by Claude Code
docs(spec,driver-memory): re-point the null-comparand prose at the 20…
docs(spec,driver-memory): re-point the null-comparand prose at the 20…
chore(service-analytics): record the where-door face-arm measurement …
fix(service-analytics)!: the analytics `where` door runs every arm of…
test(spec): the fourth data/ file group's test titles state each cite…