fix(A15): 三值逻辑求值器统一 —— 消除 WHERE 的第二套语义(静默错值根治)
背景(PLAN-v0.7.5.md 根因 1/7、缺陷 A15):
项目里 `matchWhere`(布尔版,自带 matchField)与三值求值器并存。同一条 SQL 的
语义取决于走哪个函数,实测三类静默错值:
- `WHERE n = NULL` 命中 NULL 行、`WHERE n != NULL` 返回所有非 NULL 行;
- `WHERE s NOT LIKE 'x'` 会把 NULL 行判真(布尔取反);
- `WHERE n NOT BETWEEN 1 AND 2` 恒空集 —— parser 生成的字段级
`{ n: { $or: [ {$lt:1}, {$gt:2} ] } }` 递归进了 where 子句级求值器,
子项 `{ $lt: 1 }` 被当成"查询列 `$lt`" → 每行 UNKNOWN。
根治方式(不是打补丁,而是取消第二套实现):
1. where-matcher.ts 重写为**唯一一个递归求值器**,同时理解 where 子句级
(键是列名/逻辑连接词)与操作符级(键是 `$gt` …),位置由上下文承载而非
由另一个函数承载;`matchWhere` 退化为"三值结果是否恰为 TRUE"。
行上下文随求值上下文下传,`$col` 在任意嵌套深度都能解析。
2. parser:`IS NULL` / `IS NOT NULL` 生成 `$isNull` / `$isNotNull` **谓词**
(此前与 `= NULL` / `!= NULL` 共用 `$eq: null` / `$ne: null`,两者语义无法区分);
`BETWEEN` 生成真正的范围条件(此前把同一对象同时当操作符对象与操作数);
`NOT BETWEEN` 展开为 `$or: [{$lt}, {$gt}]`。
3. executor 新增 enginePreFilter:逐行求值谓词(`$col` / `$exists` / CASE 键)
必须整体移出引擎层 —— 引擎无外层行上下文,会把它们判 UNKNOWN 并把**所有行**
过滤掉,逐行求值再正确也无行可算。粒度按连接词决定:`$and` 成员可单独移除,
`$or`/`$not` 成员一移除就改变结果集(漏行/多行),故整条下推放弃。
契约变更(旧测试编码了错误语义,已按 SQL 标准改正并注明理由):
- a) `{ $eq: null }` 不再命中 NULL 行(`= NULL` 恒 UNKNOWN)→ 用 `$isNull`;
- b) `IN` 列表含 NULL:`x IN (NULL, 'a')` 只命中 'a'(`null = NULL` 为 UNKNOWN),
未命中的行仍因 UNKNOWN 不保留。
- c) 引擎层 `$in: [null, ...]` 与 `$eq: null` 的断言同步修正。
验证:
- 新增 tests/v080-sql-three-valued.test.ts:26 条 SQL 语义矩阵 × 4 引擎
(memory/disk/hybrid/aria)+ UPDATE/DELETE 写路径,共 104 断言;
- 全量 83 套件 / 1458 测试通过(含 Aria 生产负载 10 万行);
- typecheck(src+tests) 与 lint 零错误。
This commit is contained in:
+22
-2
@@ -898,7 +898,11 @@ export class Parser {
|
||||
if (isNot) this.nextToken();
|
||||
this.expect(TokenType.NULL);
|
||||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||||
result[column] = isNot ? { $ne: null } : { $eq: null };
|
||||
// v0.8.0 三值语义:IS NULL / IS NOT NULL 是**谓词**,不是等值比较。
|
||||
// 此前生成为 { $eq: null } / { $ne: null } —— 在 SQL 标准里
|
||||
// `x = NULL` 恒为 UNKNOWN(不保留任何行),而 IS NULL 要保留 NULL 行。
|
||||
// 两者语义不同,必须用不同标记(见 query/sql-compare.ts 的谓词说明)。
|
||||
result[column] = isNot ? { $isNotNull: true } : { $isNull: true };
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -908,6 +912,11 @@ export class Parser {
|
||||
const low = this.parseValue();
|
||||
this.expect(TokenType.AND);
|
||||
const high = this.parseValue();
|
||||
// v0.8.0 根治:BETWEEN 必须是**范围**条件。
|
||||
// 此前把同一个对象同时当成"操作符对象"和"操作数"传给 `$eq`
|
||||
// (`{ $eq: { $gte, $lte } }`),matchOperator 的 `$eq` 收到一个对象再去比较,
|
||||
// 结果只对"值恰好等于该对象"的行成立 —— 实测 `WHERE n BETWEEN 1 AND 2`
|
||||
// 在 (1,2,NULL,3) 上只返回 1 行(应 2 行),静默错值。
|
||||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||||
result[column] = { $gte: low, $lte: high };
|
||||
return result;
|
||||
@@ -921,7 +930,18 @@ export class Parser {
|
||||
this.expect(TokenType.AND);
|
||||
const high = this.parseValue();
|
||||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||||
result[column] = { $not: { $gte: low, $lte: high } };
|
||||
// v0.8.0:NOT BETWEEN ≡ (x < low) OR (x > high),直接展开为字段级 `$or`。
|
||||
//
|
||||
// 此前生成 `{ $not: { $gte, $lte } }`。这**曾经**恒为空集:字段级 `$not`
|
||||
// 把内层对象当"单个条件对象",key `$gte`/`$lte` 被当成列名去取
|
||||
// `row['$gte']` → undefined → 整条恒 UNKNOWN → 取反仍 UNKNOWN → 全部排除。
|
||||
//
|
||||
// 求值器已修正(内层按操作符对象解释),但这里仍选择展开为 `$or`:
|
||||
// - `$or` 的三值行为(含 NULL 时 UNKNOWN)是显式可读的;
|
||||
// - 避免依赖"$not 作用于比较"与"$not 作用于谓词"(如 `$isNull`)的差别。
|
||||
// 两条路径都有测试锁定(tests/v080-sql-three-valued.test.ts 与
|
||||
// tests/sql/where-matcher.test.ts 的 `$not` 用例)。
|
||||
result[column] = { $or: [{ $lt: low } as never, { $gt: high } as never] as never };
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user