背景(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 零错误。
- KVStoreEngine 数值主键 update 丢行(P0):String 化主键回查不命中 → 误删 KV 行 (重启丢数据);改为单次全表扫描 + 受影响集合过滤(兼消 O(N×M) 回查开销) - update 主键撞已有主键静默覆盖(P0,Memory/Aria):抛 DUPLICATE_KEY,事务路径同拦截 - Aria 二级索引范围查询边界算法错误(P1):Number(v)±1 构造 key 漏小数/字符串数据; 改全索引扫描 + matchWhere 过滤,边界语义统一 - Aria 索引列 IS NULL 返回空(P1):null 等值/含 null 的 IN 不走索引(回退全表) - AriaEngine unique 约束未强制(P1):insert 整批预检(批内互查+索引扫描,失败整批 不落库)+ update 排除自身旧条目检查;新增 LSM.prefetchPrefixRanges 批量预加载 - 非主键 update 索引旧值残留:统一传旧行清理(消除唯一性误报与索引膨胀) - EXPLAIN 写语句产生真实副作用(P2):仅 SELECT 执行,UPDATE/DELETE 用 count 估算 测试 1092 → 1114(70 套件);行覆盖率 89.6%;版本 0.6.2