修 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()),{} 满足。所以正确动作是按单位元编译对,不是拒收。
建议
applyFilterCondition 的 $or 分支:全部析取项为空 → 产出该方言的 FALSE(如 knex whereRaw('1 = 0'));任一析取项可证为 TRUE → 整个 $or 不产出子句。$and 维持「编译掉」,但作为有意的单位元并加测试钉住。
- 关键约束(cloud#1073 里踩过):必须把「因为它本来就是空(
{})才编译成空」与「因为有东西没编译出来才编译成空」分开。$or: [null] / $or: ['x'] / $or: [[...]] / $or: [new Date()] 目前也都静默无痕消失;若不先把它们拦掉就上单位元,它们会升级成「匹配所有行」——比现在更糟。cloud 那边的做法是按形状在编译前拒收非 filter 节点的元素,使「编译成空」只剩一个成因。
- 补进
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
修 cloud#1073(Turso
RemoteTransport.buildWhereSQL的同一个 bug)时,按要求只读核对了框架侧SqlDriver的行为,发现同样的问题。按纪律不在那个 PR 里修,单独记在这里,unassigned。现象
packages/plugins/driver-sql/src/sql-driver.ts的applyFilterCondition这样编译$and/$or:空数组 / 空子过滤器都会让分组回调什么都不加,而 knex 对「没加任何子句的分组」不产出 SQL。用本仓自己的 knex(3.3.0,
packages/plugins/driver-sql/node_modules/knex,sqlite3 dialect)复现同一控制流:为什么是 bug
丢弃子句 ≠ 套用单位元。布尔代数里空 AND 是 TRUE、空 OR 是 FALSE,方向相反,而这段代码对两者都「当没看见」:
{ $and: [] }{ $or: [] }{ $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: [] },RLScheck(formula)判为拒绝、memory driver 判为零行、SQL driver 返回全表、mongodb 抛错。$or: []最常见的来源正是「本该有条件、但循环没填进去」,SQL 侧静默当成全表是其中最坏的一种兜底——在 RLS read scope 下推上,它意味着本该看不到任何行的人看到了整表。Spec 侧:三种形状都是声明合法的
packages/spec/src/data/filter.zod.ts:裸
z.array(...),没有.nonempty()/.min(1);元素类型的非递归半边是z.record(z.string(), z.unknown()),{}满足。所以正确动作是按单位元编译对,不是拒收。建议
applyFilterCondition的$or分支:全部析取项为空 → 产出该方言的 FALSE(如 knexwhereRaw('1 = 0'));任一析取项可证为 TRUE → 整个$or不产出子句。$and维持「编译掉」,但作为有意的单位元并加测试钉住。{})才编译成空」与「因为有东西没编译出来才编译成空」分开。$or: [null]/$or: ['x']/$or: [[...]]/$or: [new Date()]目前也都静默无痕消失;若不先把它们拦掉就上单位元,它们会升级成「匹配所有行」——比现在更糟。cloud 那边的做法是按形状在编译前拒收非 filter 节点的元素,使「编译成空」只剩一个成因。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