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:
thzxx
2026-09-14 23:22:26 +08:00
parent 674da6b7b7
commit 4ab04df882
9 changed files with 929 additions and 162 deletions
+15 -2
View File
@@ -74,12 +74,17 @@ describe('Parser 边缘场景', () => {
it('WHERE IS NULL', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NULL'));
expect(ast.where).toEqual({ bio: { $eq: null } });
// v0.8.0A15):IS NULL 是**谓词**,不是 `$eq: null`。
// `WHERE bio = NULL` 在 SQL 里恒为 UNKNOWN(空集),只有 IS NULL 能命中 NULL 行;
// 此前两者编译成同一个 `{ $eq: null }`,于是任何调用方都无法表达"等于 NULL"。
expect(ast.where).toEqual({ bio: { $isNull: true } });
});
it('WHERE IS NOT NULL', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NOT NULL'));
expect(ast.where).toEqual({ bio: { $ne: null } });
// 同理:`$ne: null` 在老语义下会返回所有非 NULL 行,看起来"对"
// 但它与 `!= NULL`(应为空集)共用同一个 AST,语义无法区分。
expect(ast.where).toEqual({ bio: { $isNotNull: true } });
});
it('WHERE NOT', () => {
@@ -235,6 +240,14 @@ describe('Parser 边缘场景', () => {
it('NULL 值', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NULL'));
// v0.8.0A15):见上方 "WHERE IS NULL" —— 解析为谓词而非与 NULL 的比较
expect(ast.where).toEqual({ bio: { $isNull: true } });
});
it('与 NULL 的比较保留为比较运算(结果为 UNKNOWN)', () => {
// 回归护栏:`= NULL` 必须编译成 `$eq: null`(而不是被"优化"成 $isNull)。
// 两者结果集完全不同:前者空集,后者命中 NULL 行。
const ast = asSelect(parse('SELECT * FROM users WHERE bio = NULL'));
expect(ast.where).toEqual({ bio: { $eq: null } });
});
});