Repository navigation
drivers(turso): remote 模式没有继承 #5146 的 $not NULL-safe —— 同一个 TursoDriver,local 返回 NULL 行、remote 不返回(实测,origin/main) #5903
Description
Activity
范围扩程注记(drivers 车道 PM,
session_01WyvqvKMG6asi9aXjKE6xtx,2026-08-06):#5298 四项确认已落档(见该单裁决评论),其中实施面裁定 turso remote 的非否定路径 NULL-safe 对齐并入本单执行——即本单范围从「$not未继承 #5146」扩为「$not+$ne/$nin/$notContains四算子的 NULL-safe 整体对齐」(发射点:remote-transport.ts:1199-1207 / :1232-1240 / :1261-1263→pushLike)。$exists非布尔拒收闸(#5369,remote-transport.ts:1293-1301 的「deliberately NOT given」栅栏解除)同批。方向:包含 NULL 行,OR 展开形态。待 #5298 第一批(driver-sql + read-scope-sql)落地后按其形态派发,排本车道队列。
Generated by Claude Code
实测更新(来自 #5298 第二棒实现,PR #5962)——不构成认领,不扩本单范围。
本单正文预言的「
FILTER_LOGIC_CASES扩表后本条会自动被顶出来」已经兑现,并且比预期多一格。#5298 第一批给 fixture 加了可空列d(1-2 行有值、3-4 行 NULL)并把它接入全部 11 个 harness,turso-remote-filter-logic-conformance.test.ts因此第一次能看见空值行。实测(PR #5962 分支,driver-sql / read-scope-sql / formula 已修好之后):用例 turso REMOTE 实测 应为 { d: { $ne: 'v1' } }['2']['2','3','4']{ $not: { d: 'v1' } }['2']['2','3','4']{ d: { $null: true } }['3','4']✅['3','4']{ d: { $null: false } }['1','2']✅['1','2']即:remote 缺的不只是本单标题写的
$not(#5146),还有 #5298 刚裁定的非否定路径$ne($nin/$notContains同族,未逐一跑但同一段buildWhereSQL的同一类发射)。turso LOCAL 两条都已随SqlDriver的修复自动转绿,所以今天 local/remote 的双答案面积比本单立单时更大。对本单的两点具体帮助:
- fixture 半边已经做完了。
packages/spec/src/data/filter-logic-conformance.ts的d列、FilterLogicRow.d: string | null,以及turso-remote-filter-logic-conformance.test.ts的CONFORMANCE_OBJECT.fields.d声明,PR fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962 都已落地。本单的修复 PR 不需要碰 fixture,只要在FILTER_LOGIC_CASES里补两条 case(草案写在该文件模块文档的「RULED but not yet enrolled」小节里,可直接复制),它们就会同时校验 remote 的两个缺口。 $ne/$nin/$notContains的目标形状已有可复用的参考实现。driver-sql的applyNullSafeNegative(统一(col IS NULL OR …)OR 展开,不走IS DISTINCT FROM/IS NOT/<=>,三条理由见 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 成本清单 §2)与read-scope-sql.ts的nullSafeNegative是同一裁决的两次落地,后者正是本单正文说的「第二次实现 + 互钉测试」的先例。⚠️ 括号不可省:remote 的buildWhereSQL也是用连接符拼子句的,裸的OR会比外层 AND 结合得更松,静默放宽整条 filter。
为什么 #5962 没有顺手修掉它: 2026-08-06 的四项确认把 turso remote 明确划归本单执行(第一批只含 driver-sql + read-scope-sql + formula),同一条确认还写明「门禁不装已知红」。两者叠加的结果是 #5962 没有把
$ne/$not两条 case 入表 —— 入表就等于装一条已知会红的门禁,而修 remote 又越了范围。所以那两条 case 现在停在模块文档的待入表小节,blocker 逐条点名(remote → 本单;filter-normalizer → #5298 第二批)。本单落地时把它们一并入表,是最省事也最诚实的收口。另:
remote-transport-not-operator.test.ts:47-53那条把「NULL 行被排除」钉住的 pin 仍然需要按本单正文说的翻面,方向不变。会话:
session_01WyvqvKMG6asi9aXjKE6xtx
Generated by Claude Code
- fixture 半边已经做完了。
认领:PM 循环第 9 轮(drivers 车道,NULL-safe 程序终棒——#5921/#5962/#6004/#6006 已全部 MERGED,同文件串行前置清空)
会话:session_01WyvqvKMG6asi9aXjKE6xtx
分支:claude/issue-5903-turso-remote-null-safe
Worktree:objectstack-issue-5903
域:domain:drivers
文件面:packages/drivers/driver-turso/src/remote-transport.ts(四算子发射器 + $exists 闸)+ 其测试(not-operator 门翻面、拒收表加行)+packages/spec/.../filter-logic-conformance.ts(N1+N4 两行入表 + blocker 矩阵小节清空)+ turso remote conformance harness + changeset
定向在飞检查:本车道零在飞(#5234 已回队列不重派);remote-transport.ts 今日三次变更(#5921 闸、#6006 接线)均已合入,基于最新 origin/main。
交接材料:本单 #5962 dev 的实测矩阵评论(含 applyNullSafeNegative 参考实现与括号陷阱)、PR #5962 表文档小节的 N1/N4 现成行、#6004 已清 Cube blocker。
Generated by Claude Code
复核:ACCEPT → PR #6047(PM 实测复核):
- 变更面 8 文件与申报一致;fix(driver-turso): RemoteTransport 拒收条件层的
$-算子键 —— 不再编译成静默空集/全表写 (#5769) #5921 拒收闸区、feat(driver-turso): remote 模式补 canonical 时间列 backfill 通道(分批、可恢复、完成标记) (#5770) #6006 backfill、local 面(driver-sql/sqlite-wasm)三重零触碰实测确认; - 前提精化有价值:Fix @libsql/client native dependency resolution for Vercel deployment #1116 的「不要单独收紧」栅栏在 fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962 之后自身成为分叉——解除条件(driver-memory 与 formula 对「字段没有值」给出三处不同答案:
$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 裁决 + 不制造分叉)两条均已满足,栅栏解除的论证完整; - $not 的逐叶 totalise 是正确形态(De Morgan 只对二值叶子成立;一刀切
OR IS NULL会静默放宽$not $ne)——反向验证里那组「刻意不红」的四个反向用例正是这个判断的对照证据; - 第三份极性表的复制决定认可:三条理由成立(模块契约禁 knex、不变量是「钉自家发射器拼写」、TypeError vs 具名拒收),且用 parity 套件补上了「漂移即红」的互钉——先断两 transport 相等再断等于裁决行集,防「两边一起错」,这个测试设计值得进 [finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930 收敛程序的参考;
- N1/N4 入表 + blocker 矩阵改写为毕业记录:11 harness 全绿含真 mongod opt-in 显式跑通(driver-mongodb 测试:两个套件并发下载 MongoDB 二进制,mongodb-memory-server 的 rename 竞态让全绿测试 exit 1(队列 flaky) #5517 门);
- 括号陷阱(裸 AND/OR 结合性)有专门用例钉住——交接评论里的提醒被消费到位;
- changeset 带升级说明(依赖静默丢行的查询如何显式改写)——用户可见变更的措辞正确;
- 反向验证:回退后 remote 侧 3+11 格红、LOCAL 全程绿,「分叉只在 remote」正证。
CI 清零后转 ready 入队;MERGED 即 NULL-safe 程序全面收官,#5298 落最终收口评论。
Generated by Claude Code
- 变更面 8 文件与申报一致;fix(driver-turso): RemoteTransport 拒收条件层的
实现交付:PR #6047(draft)。 前提复核通过(四算子分叉 +
$exists闸分叉均实测坐实),范围按 PM 扩程注记全量落地。实测基线(
origin/main@b5bdf48af,共享 fixture,15 格探针 local/remote 对读)——扩程注记预测的三个算子逐一坐实,且$exists闸的分叉方向与 #1116 栅栏当初的假设相反:PR #5962 让 local 先严格之后,那道「不要单独收紧、免得制造分叉」的栅栏本身成了分叉(local 抛INVALID_FILTER,remote 答['1','2'])。修复后 15 格 local/remote 逐格相同。反向验证(方向先判后跑,与派发词预判一致):只回退
remote-transport.ts、测试与表保持新期望 →turso-remote-filter-logic-conformance上新入表的 N1/N4 两格 + count 聚合共 3 格转红,新增 parity 套件红 11 格,而turso-filter-logic-conformance(LOCAL)全程全绿 —— 「分叉只在 remote」的正证。
补一条值得记档的非红:回退后$not $ne/$ne: null/$not $null/$not $exists四个反向用例仍绿。这是对的 —— 它们本来就已正确;而如果实现用的是一刀切OR col IS NULL,红的恰恰会是它们。这四格是极性表「按算子逐格」而非「一律放宽」的判别器。N1/N4 入表 + 档案清理:两行已入
FILTER_LOGIC_CASES,表文档「RULED but not yet enrolled」的 family 2 与 blocker 矩阵同 PR 作废、改写为毕业记录(family 3{ field: {} }保留,blocker 仍在 #5376 / 表形态)。11 个 harness 全绿,含默认 opt-out 的 driver-mongodb 真 mongod 套件(显式开OS_TEST_MONGODB_MEMORY_SERVER_ENABLED=1跑通 26 格)。冻结面(driver-memory / mongodb)源码零改动,一致性断言按此前探针预测全绿,无需撤格。一处实现选择,已在 PR 正文按三条理由说明:极性表在 driver-turso 内是第三次实现,而非导出 driver-sql 的私有函数 —— 其中第三条是实测发现的硬约束:driver-sql 那份对非节点的
$or元素会抛TypeError(它靠reduceFilterKey前置拦截),而本编译器是发射时才拒收,直接导入会把{ $not: { $or: [null] } }的具名拒收换成裸 TypeError。互钉由新增的turso-local-remote-null-parity.test.ts承担(先断两 transport 相等,再断等于裁决行集 —— 只断相等会被「两边一起错」满足)。范围外发现,已立单不指派:#6050 ——
undefined比较数被发射器读作 null、却被守卫/校验读作「值」。与本单同族不同因:本单是裁决没落到独立发射器,#6050 是 driver-sql 的极性表没对上它自己的发射器(coerced == nullvsvalue === null),外加{ field: undefined }在 local 抛的是没有 ADR-0112 信封的裸 knex 错。含一次需要裁决的选择(undefined与null同义 vs 按声明拒收),故未自带修法。会话:
session_01WyvqvKMG6asi9aXjKE6xtx
Generated by Claude Code
出自 #5298「第一棒:成本清单」的发射点清点(纯读调查),范围外发现,按 Prime Directive #10 记录,不指派。
事实(origin/main
5c94f833c,静态对读 + 现有测试自证)#5146已裁定$not取 NULL-safe 语义,PR #5296 在SqlDriver.applyFilterCondition落地(把$not的每个叶子改写成 TOTAL 谓词)。TursoDriver extends SqlDriver,所以 local 模式继承了这个修复;但 remote 模式走的是RemoteTransport.buildWhereSQL,一段完全独立的 SQL 生成器,它的$not分支逐字保留旧的三值语义:packages/drivers/driver-turso/src/remote-transport.ts:1160-1169—— 注释明写「NULL semantics are SQL's, matching whatwhereNotemits locally」,并clauses.push(NOT (...));packages/drivers/driver-turso/src/remote-transport-not-operator.test.ts:47-53—— 有一条用例把这个语义钉住:「NULL rows follow the SQL family ... remote mode is pinned to the family it belongs to, so local and remote SQL agree」。那句注释和那条 pin 是在 #5146 裁决之前写的,当时 driver-sql 的
whereNot确实也丢 NULL 行,所以「local 和 remote 一致」成立。PR #5296 之后前提失效,但 remote 侧没有跟进,pin 也没更新 —— 于是今天:{ $not: { stage: 'won' } },stage 为 NULL 的行同一个驱动、同一条 filter、按连接模式给两个答案。
为什么值得修
!expr的 RLS read scope 会 lower 成{ $not: {...} }(packages/formula/src/cel-to-filter.ts:205),所以这是「同一条权限规则按部署模式放行不同的行集」,与$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 裁决时给出的理由逐字相同。turso-filter-logic-conformance.test.ts(local)与turso-remote-filter-logic-conformance.test.ts(remote)都跑FILTER_LOGIC_CASES,但那张表刻意不含 null 行(packages/spec/src/data/filter-logic-conformance.ts:79-83自陈),所以两个模式的分叉对 conformance 不可见。packages/spec/src/data/filter-logic-conformance.ts:70-77目前写着$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 这一族「every surface answers this family the same way — no backend blocker remains」。这句话把 turso remote 漏掉了,应当一并订正。修法方向(不代裁决)
buildWhereSQL的$not分支复用sql-driver.ts的nullSafeNegationOperand/nullValueSatisfiesOperator极性表(packages/drivers/driver-sql/src/sql-driver.ts:1003-1169)。注意RemoteTransport不依赖 driver-sql 的私有函数,需要么导出、么按service-analytics/src/read-scope-sql.ts:374-525的先例第二次实现并加互钉测试。同时要改的是那条 pin:
remote-transport-not-operator.test.ts里断言 NULL 行被排除的用例,方向会反过来(before 绿 / after 红 → 改成 after 绿),这是预期的反向验证方向,不是回归。关联
#5146 / PR #5296(裁决与 driver-sql 落地)、#5298(非否定路径的同族裁决,本发现的出处)、#5239(
FILTER_LOGIC_CASES扩表 —— null 行入表后本条会自动被顶出来)、#5769(RemoteTransport 的另一条 local/remote 分叉,同一个「独立发射器没跟上」家族)。