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.tsapplyFilterCondition 这样编译 $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:35if (!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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions