Skip to content

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

Closed

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

Labels

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions