Skip to content

drivers(turso): remote 模式没有继承 #5146 的 $not NULL-safe —— 同一个 TursoDriver,local 返回 NULL 行、remote 不返回(实测,origin/main) #5903

Closed

Description

@os-zhuang

出自 #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 what whereNot emits 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 也没更新 —— 于是今天:

filter turso LOCAL(继承 SqlDriver) turso REMOTE(RemoteTransport)
{ $not: { stage: 'won' } },stage 为 NULL 的行 返回 不返回

同一个驱动、同一条 filter、按连接模式给两个答案。

为什么值得修

  1. CEL !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 裁决时给出的理由逐字相同。
  2. 今天没有任何门禁能发现它。 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 不可见。
  3. 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 分叉,同一个「独立发射器没跟上」家族)。

Activity

  1. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    范围扩程注记(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

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    实测更新(来自 #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 的双答案面积比本单立单时更大。

    对本单的两点具体帮助:

    1. 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 的两个缺口。
    2. $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

  3. self-assigned this
    on Aug 6, 2026
  4. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    认领: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

  5. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    复核:ACCEPT → PR #6047(PM 实测复核):

    CI 清零后转 ready 入队;MERGED 即 NULL-safe 程序全面收官,#5298 落最终收口评论。


    Generated by Claude Code

  6. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    实现交付: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 == null vs value === null),外加 { field: undefined } 在 local 抛的是没有 ADR-0112 信封的裸 knex 错。含一次需要裁决的选择(undefined 与 null 同义 vs 按声明拒收),故未自带修法。

    会话:session_01WyvqvKMG6asi9aXjKE6xtx


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions