出自 #5903 的实施(PR #6047)—— 给 remote 侧写 NULL-safe 极性表时,为把每一格对齐到「守卫必须匹配它自己的发射器」这条 #5298 不变量,逐格复核了 undefined 比较数,发现一族与 #5903 同类但成因不同的分叉。范围外,按 Prime Directive #10 记录,不指派。
事实(origin/main @ efedd289f,同一个 TursoDriver,共享 fixture:1-2 行 d 有值、3-4 行 d 为 NULL)
| filter |
turso LOCAL(继承 SqlDriver) |
turso REMOTE(RemoteTransport) |
{ d: undefined } |
抛 Undefined binding(s) detected when compiling SELECT(knex 裸 Error) |
['3','4'] |
{ d: { $eq: undefined } } |
抛 同上 |
['3','4'] |
{ $not: { d: undefined } } |
抛 Undefined binding(s) … compiling WHERE |
['1','2'] |
{ d: { $ne: undefined } } |
['1','2'] ✅ |
['1','2'] ✅ |
{ $not: { d: { $ne: undefined } } } |
[] |
['3','4'] |
两个可分开的毛病:
A. 抛出的那三格没有 ADR-0112 信封
LOCAL 抛的是 knex 的 Undefined binding(s) …,code 与 status 都是 undefined —— 正是 #1116 / #4436 为这条通路装信封时清点过的形态,只是这一格漏在名单外。mapDataError 落默认分支,客户端拿到一个没有 code 的 500,而它其实是一条 caller 写坏的 filter。
B. $ne: undefined 一格,driver-sql 的极性表与它自己的发射器互相矛盾
sql-driver.ts 的发射器读 coerced == null(宽松相等,所以 undefined 走 whereNotNull,编译成 IS NOT NULL —— 一条 TOTAL 谓词):
case '$ne':
if (coerced == null) (builder)[…'whereNotNull'](field);
但同文件的两张守卫表读的是严格相等:
operatorIsNullTotal('$ne', undefined) → undefined === null → false(判它「不 total」,与发射器矛盾)
nullValueSatisfiesOperator('$ne', undefined) → undefined !== null → true(判「NULL 行满足它」,与 IS NOT NULL 相反)
两处一起,nullGuardForFieldSpec 得出 allowNull,于是 $not 的操作数被改写成 d IS NULL OR d IS NOT NULL —— 恒真 —— 取反后恒假,答 []。remote 侧(PR #6047 的表,两格都按 == null 语义写,与它自己的发射器一致)答 ['3','4']。
这正是 #5298 立的那条不变量:「每张极性表钉的是它自己发射器的拼写,不变量是守卫与发射器一致,不是三份源码字面相同」。driver-sql 这一格恰好没有对上自己。⚠️ 因此 ['3','4'] 是按该驱动自身发射器推出的正确答案,[] 是错的 —— 但这句话是本单的分析,不是裁决。
为什么值得修(不是纯理论)
undefined 过不了 JSON 往返,所以只会从进程内代码来 —— 而那正是它最危险的地方:{ owner_id: ctx.user?.id } 里 id 缺失时,remote 会把整条谓词静默变成 owner_id IS NULL,匹配所有环境级(env-wide)行,而不是响亮地失败。同一条规则在 local 上抛错、在 remote 上放行,是 #5903 刚关掉的那个「按连接模式给两个答案」家族的又一格,只是这次触发键是比较数的形态而不是算子。
需要一次裁决(所以本单不自带修法)
undefined 比较数应当:
B 案与本仓 contract-first 的方向更一致,但会改变 remote 今天已经在答的行为,且要先量一遍仓内有没有依赖 A 案的调用方 —— 请裁决方拍。
落点
关联
#5903 / PR #6047(同族,不同成因:那单是裁决没落到独立发射器,本单是守卫没对上自己的发射器)、#5298(NULL/无值语义族的裁决入口与「守卫匹配发射器」不变量的出处)、#5146、#5347 / #5369(比较数按声明拒收的先例)、#1116 / #4436(ADR-0112 信封,毛病 A 的清点来源)。
会话:session_01WyvqvKMG6asi9aXjKE6xtx
出自 #5903 的实施(PR #6047)—— 给 remote 侧写 NULL-safe 极性表时,为把每一格对齐到「守卫必须匹配它自己的发射器」这条 #5298 不变量,逐格复核了
undefined比较数,发现一族与 #5903 同类但成因不同的分叉。范围外,按 Prime Directive #10 记录,不指派。事实(
origin/main@efedd289f,同一个TursoDriver,共享 fixture:1-2 行d有值、3-4 行d为 NULL){ d: undefined }Undefined binding(s) detected when compiling SELECT(knex 裸 Error)['3','4']{ d: { $eq: undefined } }['3','4']{ $not: { d: undefined } }Undefined binding(s) … compiling WHERE['1','2']{ d: { $ne: undefined } }['1','2']✅['1','2']✅{ $not: { d: { $ne: undefined } } }[]['3','4']两个可分开的毛病:
A. 抛出的那三格没有 ADR-0112 信封
LOCAL 抛的是 knex 的
Undefined binding(s) …,code与status都是undefined—— 正是 #1116 / #4436 为这条通路装信封时清点过的形态,只是这一格漏在名单外。mapDataError落默认分支,客户端拿到一个没有code的 500,而它其实是一条 caller 写坏的 filter。B.
$ne: undefined一格,driver-sql 的极性表与它自己的发射器互相矛盾sql-driver.ts的发射器读coerced == null(宽松相等,所以undefined走whereNotNull,编译成IS NOT NULL—— 一条 TOTAL 谓词):但同文件的两张守卫表读的是严格相等:
operatorIsNullTotal('$ne', undefined)→undefined === null→false(判它「不 total」,与发射器矛盾)nullValueSatisfiesOperator('$ne', undefined)→undefined !== null→true(判「NULL 行满足它」,与IS NOT NULL相反)两处一起,
nullGuardForFieldSpec得出allowNull,于是$not的操作数被改写成d IS NULL OR d IS NOT NULL—— 恒真 —— 取反后恒假,答[]。remote 侧(PR #6047 的表,两格都按== null语义写,与它自己的发射器一致)答['3','4']。这正是 #5298 立的那条不变量:「每张极性表钉的是它自己发射器的拼写,不变量是守卫与发射器一致,不是三份源码字面相同」。driver-sql 这一格恰好没有对上自己。⚠️ 因此
['3','4']是按该驱动自身发射器推出的正确答案,[]是错的 —— 但这句话是本单的分析,不是裁决。为什么值得修(不是纯理论)
undefined过不了 JSON 往返,所以只会从进程内代码来 —— 而那正是它最危险的地方:{ owner_id: ctx.user?.id }里 id 缺失时,remote 会把整条谓词静默变成owner_id IS NULL,匹配所有环境级(env-wide)行,而不是响亮地失败。同一条规则在 local 上抛错、在 remote 上放行,是 #5903 刚关掉的那个「按连接模式给两个答案」家族的又一格,只是这次触发键是比较数的形态而不是算子。需要一次裁决(所以本单不自带修法)
undefined比较数应当:null同义,四个发射面统一编译成空值谓词(remote 现行为),守卫表相应读== null;INVALID_FILTER/ 400,同$null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347-A 的信封),理由是 spec 的FieldOperatorsSchema没有声明undefined这个比较数,「声明即强制」;{ field: undefined }与「没有这个键」在 JS 里不可区分,而两者语义相反(前者是谓词,后者是无约束)。B 案与本仓 contract-first 的方向更一致,但会改变 remote 今天已经在答的行为,且要先量一遍仓内有没有依赖 A 案的调用方 —— 请裁决方拍。
落点
packages/drivers/driver-sql/src/sql-driver.ts:operatorIsNullTotal/nullValueSatisfiesOperator的$eq/$ne两臂(严格 vs 宽松相等),以及$eq: undefined交给 knex 前缺一道闸;packages/drivers/driver-turso/src/remote-transport.ts:同名两函数已按== null写,若裁 B 案需一并改;read-scope-sql.ts、formula/matches-filter.ts、driver-memory、driver-mongodb)未逐格实测,裁决后需按 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 的清点方式补测。关联
#5903 / PR #6047(同族,不同成因:那单是裁决没落到独立发射器,本单是守卫没对上自己的发射器)、#5298(NULL/无值语义族的裁决入口与「守卫匹配发射器」不变量的出处)、#5146、#5347 / #5369(比较数按声明拒收的先例)、#1116 / #4436(ADR-0112 信封,毛病 A 的清点来源)。
会话:
session_01WyvqvKMG6asi9aXjKE6xtx