Skip to content

$not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146

Description

@os-zhuang

修 cloud#1076(给 Turso remote 传输补 $not)时扫到,不在该 PR 范围内。cloud 那条按指示把远程模式钉到 SQL 家族(driver-sql)的行为上,但两族本身对同一条 $not 给出不同答案,这条记录该分叉。

事实

packages/specFilterConditionSchema$and / $or 并列声明 $not: FilterConditionSchema.optional(),三处实现:

三者对良构、且 schema 明确允许的两种形状答案不一致。

分叉一:被比较列为 NULL 的行

{ $not: { stage: 'won' } }、某行 stage IS NULL 为例:

  • driver-sql:Knex 实测 select * from \deal` where (not (`stage` = 'won'))。SQL 三值逻辑下 NULL = 'won' 是 UNKNOWN,NOT UNKNOWN` 仍是 UNKNOWN → 该行不返回
  • driver-memory / formula:match(record, { stage: 'won' })undefined !== 'won' → false → 取反 true → 该行返回

也就是同一条 RLS read scope(CEL !exprcel-to-filter.ts 降解成 { $not: {...} }),落在 SQL 驱动上会把「该列为空」的行整批排除,落在内存驱动 / 公式求值上则会把它们全部包含进来。对权限规则来说,这是「同一条规则在不同后端给出不同可见集合」。

分叉二:$not: {}

FilterConditionSchemaz.record(z.string(), z.unknown()).and(z.object({…optional})){} 合法,所以 $not: {} 是被声明允许的形状。

  • driver-sql:Knex 对空分组直接丢弃整条子句。实测 knex('deal').where(qb => qb.whereNot(sub => {}))select * from \deal``,即 TRUE / 全表
  • driver-memory:match(record, {}) 返回 true → 取反 → FALSE / 零行
  • formula matches-filterevalNode(record, {}) 空对象循环不执行返回 true → 取反 → FALSE / 零行

按布尔代数 NOT TRUE ≡ FALSE,driver-memory / formula 是对的;driver-sql 的 TRUE 是 Knex「空分组不产出 SQL」的副作用,不像是有意的决定——而且它的方向是放宽(本该零行,实际全表),正是 #2704 / cloud#1073 那一族「静默 filter-bypass」。

已经据此落地的东西(供交叉参考)

cloud#1076 的 RemoteTransport.buildWhereSQL

  • NULL 语义跟随 driver-sql(发 NOT (…),不做 NULL-safe 改写)——远程模式与本地 SqlDriver 是同一族,先保证这两者一致;
  • $not: {}FALSE(1 = 0——与 driver-memory / formula 一致,也与 cloud#1073 定下的「子过滤器编译为空 = 真值单位元」纪律自洽。

所以现在的状态是:远程 Turso 在 NULL 上跟 SQL 家族,在 $not: {} 上跟内存家族——因为这两族在这两点上各自只有一边是站得住的。这不是一个能长期维持的形状,需要在 framework 侧定一次。

需要决定的

  1. NULL$not 是否应当 NULL-safe(NOT (…) OR col IS NULL,即把「该列没有值」视为「不满足被否定的条件」,与 JS 家族一致),还是保持 SQL 三值逻辑、并把这条差异写进 spec 文档要求内存驱动改成三值?倾向前者——权限规则的作者写 !(stage == 'won') 时几乎不会预期「stage 为空的行被排除」,而且 SQL 家族是这里的少数派(2 : 1)。
  2. $not: {}:driver-sql 应改为显式 FALSE(在子分组编译为空时发方言的 FALSE 常量),与另外两家对齐。这个方向没有争议,只是要有人动 Knex 那段。

两条都属于「declared = enforced」:$not 是 spec 声明的算子,声明了就该在所有驱动上给同一个答案。定完后建议在 packages/formula 或驱动共享的一致性测试里加一组跨驱动 pin,别只改实现。

关联

cloud#1076(远程传输补 $not)、cloud#1073 / cloud#1074(布尔单位元)、#2704(driver-sql 加 $not 分支)、packages/formula/src/cel-to-filter.ts(CEL !expr 的降解点)。

Activity

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