Skip to content

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

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions