Skip to content

SqlDriver.applyFilterCondition 丢弃编译成空的 $and/$or 子过滤器,而不是套用布尔单位元 —— 与同仓 matchesFilterCondition / driver-memory 相反 #5134

Description

@os-zhuang

修 cloud#1073(Turso RemoteTransport.buildWhereSQL 的同一个 bug)时,按要求只读核对了框架侧 SqlDriver 的行为,发现同样的问题。按纪律不在那个 PR 里修,单独记在这里,unassigned。

现象

packages/plugins/driver-sql/src/sql-driver.ts 的 applyFilterCondition 这样编译 $and/$or:

} else if (key === '$or' && Array.isArray(value)) {
  const method = logicalOp === 'or' ? 'orWhere' : 'where';
  (builder as any)[method]((qb: any) => {
    for (const sub of value) {
      qb.orWhere((subQb: any) => {
        this.applyFilterCondition(subQb, sub, 'and', table);
      });
    }
  });
}

空数组 / 空子过滤器都会让分组回调什么都不加,而 knex 对「没加任何子句的分组」不产出 SQL。用本仓自己的 knex(3.3.0,packages/plugins/driver-sql/node_modules/knex,sqlite3 dialect)复现同一控制流:

$or: []            "select * from `deal`"                                []
$and: []           "select * from `deal`"                                []
$or: [{a:1},{}]    "select * from `deal` where ((`a` = ?))"              [1]
$or: [{a:1},{b:2}] "select * from `deal` where ((`a` = ?) or (`b` = ?))" [1,2]

为什么是 bug

丢弃子句 ≠ 套用单位元。布尔代数里空 AND 是 TRUE、空 OR 是 FALSE,方向相反,而这段代码对两者都「当没看见」:

写法 语义应为 实际编译 错的方向
{ $and: [] } TRUE → 全部行 全表 碰巧正确
{ $or: [] } FALSE → 零行 全表 静默放松
{ $or: [{a:1}, {}] } {} 是 TRUE 析取项 → 全部行 (("a" = ?)) 静默收紧

$and: [] 对的理由不是代码理解了单位元,而是「丢掉」在 AND 侧恰好等价于 TRUE,同一段代码在 OR 侧就必然错。

关键点:这与同仓另外两个后端相反

filter-logic-conformance 的 changeset 说得很明白,FilterCondition 有四个独立实现,「一条 case 只有在每个后端都必须一致时才进表」。这三种形状没有进 FILTER_LOGIC_CASES,于是四个后端在它们上面各行其是——而它们其实已经分成了两派:

  • packages/formula/src/matches-filter.ts:35 — if (!Array.isArray(val) || val.length === 0 || !val.some(...)) return false;,且被 matches-filter.test.ts:80 钉住:
    expect(m(rec, { $or: [] })).toBe(false); // empty OR matches nothing
    {} 走 evalNode 空循环 → true(TRUE 析取项),$and: [] 走 .every([]) → true。三条全对。
  • packages/plugins/driver-memory/src/memory-matcher.ts:31-35 — .every() / .some() over [],match(rec, {}) 直接 return true。三条全对。
  • driver-sql — 三条里错两条(见上表)。
  • driver-mongodb(mongodb-filter.ts:77)把空数组原样透传给 MongoDB,而 MongoDB 对空 $and/$or 是报错($and/$or must be a nonempty array)——第三种行为。

也就是说:同一个 { $or: [] },RLS check(formula)判为拒绝、memory driver 判为零行、SQL driver 返回全表、mongodb 抛错。$or: [] 最常见的来源正是「本该有条件、但循环没填进去」,SQL 侧静默当成全表是其中最坏的一种兜底——在 RLS read scope 下推上,它意味着本该看不到任何行的人看到了整表。

Spec 侧:三种形状都是声明合法的

packages/spec/src/data/filter.zod.ts:

export const FilterConditionSchema: z.ZodType< FilterCondition, FilterCondition > = z.lazy(() =>
  z.record(z.string(), z.unknown()).and(
    z.object({
      $and: z.array(FilterConditionSchema).optional(),
      $or: z.array(FilterConditionSchema).optional(),
      $not: FilterConditionSchema.optional(),
    })
  )
);

裸 z.array(...),没有 .nonempty() / .min(1);元素类型的非递归半边是 z.record(z.string(), z.unknown()),{} 满足。所以正确动作是按单位元编译对,不是拒收。

建议

  1. applyFilterCondition 的 $or 分支:全部析取项为空 → 产出该方言的 FALSE(如 knex whereRaw('1 = 0'));任一析取项可证为 TRUE → 整个 $or 不产出子句。$and 维持「编译掉」,但作为有意的单位元并加测试钉住。
  2. 关键约束(cloud#1073 里踩过):必须把「因为它本来就是空({})才编译成空」与「因为有东西没编译出来才编译成空」分开。$or: [null] / $or: ['x'] / $or: [[...]] / $or: [new Date()] 目前也都静默无痕消失;若不先把它们拦掉就上单位元,它们会升级成「匹配所有行」——比现在更糟。cloud 那边的做法是按形状在编译前拒收非 filter 节点的元素,使「编译成空」只剩一个成因。
  3. 补进 FILTER_LOGIC_CASES 三条($and: []、$or: []、$or: [{a},{}]),让四个后端一次对齐——这正是那张表存在的意义。但注意 driver-mongodb 会因 MongoDB 拒收空数组而无法直接通过,需要它在 translate 时先做同样的单位元归约。

关联

cloud#1073(Turso remote transport 的同一 bug 与其修法)、#3774($or 分支内多键被 OR 而非 AND)、filter-logic-conformance。


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    范围并入注记(PM 循环第 1 轮,会话 session_01Pbu27iNUfQCHeuS551Rqo7):#5146 分叉二与本单同函数同家族 —— $not: {}(及任何编译成空的 $not 子分组)在 Knex 下整条子句被丢弃,实测退化为 TRUE/全表,而 driver-memory / formula 给 FALSE/零行;按布尔代数与 cloud#1073 的单位元纪律,NOT TRUE ≡ FALSE,driver-sql 应在 $not 子分组编译为空时发方言 FALSE 常量。派发本单时把该情形并入验收(空 $and→TRUE 单位元、空 $or→FALSE 单位元、空 $not→FALSE,三者同一批 pin 测试)。#5146 剩余的 NULL 语义分叉另行等维护者拍板,不在本单范围。


    Generated by Claude Code

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

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮
    会话:session_01Pbu27iNUfQCHeuS551Rqo7
    分支:claude/issue-5134-sql-driver-boolean-identity
    Worktree:objectstack-issue-5134
    域:domain:engine
    文件面:packages/plugins/driver-sql/src/sql-driver.ts(applyFilterCondition)及本包测试、changeset(越界即停,报告说明)

    范围含 07:29Z 注记并入的 #5146 分叉二($not 空子分组 → 方言 FALSE)。⛔ packages/spec/** 零改动 —— FILTER_LOGIC_CASES 定义在 packages/spec/src/data/filter-logic-conformance.ts,一致性表扩条(建议 3)归 spec 车道,由 dev 立后续单移交;⛔ 不碰 driver-memory / formula(它们是对的参照)与 driver-mongodb(其对齐随一致性表后续单走)。在飞不相交已核:driver-sql 无其它单在飞。


    Generated by Claude Code

  4. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    验收:ACCEPT —— PR #5243(转 ready + 入队)

    会话:session_01Pbu27iNUfQCHeuS551Rqo7(PM 循环第 3 轮)

    落地内容:applyFilterCondition 改为结构性三值归约(reduceFilterNode → true/false/clause)后再产 SQL —— 这是比派发词更好的形状:原缺陷正是「编译完再观察分组是否为空」的观察式判定,结构判定天然分得清「本来就空」与「有东西没编译出来」。空 $and=TRUE(刻意的无子句)、空 $or 与空 $not 子组=方言 FALSE 1=0(与 read-scope-sql.ts 同拼写)、{} 析取项使整个 $or 为 TRUE;形状拒收(ADR-0112 INVALID_FILTER,点名位置)含原型判定(Date/RegExp/class 实例枚举为空,不拦会被单位元升格为全表)。#5146 分叉二扩围落实;非空组合子逐字节不变;{field:{}} 不越权裁决(→ #5240)。

    实况核验:draft ✓ / Fixes 行 ✓ / 3 文件在面内、spec 零改动 ✓ / 修前红证据($or:[null] 实测全表)+ 修后全绿 / 同日 churn 处置完整:并入含 #5212 的 main(merge commit bb4d4994d,git merge-base --is-ancestor 实证),并后全量重验 729+232 绿 / changeset 响亮标注查询行为变更与 RLS 影响。

    顺带发现处置:#5239 已分诊转 spec 车道队列(mongodb 同 PR 约束在案);#5240 已挂 needs-user-decision(PM 推荐序:拒收 > FALSE)。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions