From 67e72d897f2659041be17662501e22a4aef2cc7d Mon Sep 17 00:00:00 2001 From: thzxx Date: Mon, 14 Sep 2026 23:22:42 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20PLAN=20=E9=99=84=E5=BD=95=20F=20?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20A9/A10/A15=20=E5=AE=8C=E6=88=90=E6=83=85?= =?UTF-8?q?=E5=86=B5=E4=B8=8E=E4=B8=89=E5=80=BC=E9=80=BB=E8=BE=91=E6=A0=B9?= =?UTF-8?q?=E6=B2=BB=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLAN-v0.7.5.md | 62 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/PLAN-v0.7.5.md b/PLAN-v0.7.5.md index 2f0d46d..7fac766 100644 --- a/PLAN-v0.7.5.md +++ b/PLAN-v0.7.5.md @@ -710,10 +710,56 @@ connection-manager/integrations/migration)两份完整报告,以及测试质 | `074afd3` | **A8 + LSM 读自洽** | 行所有权写进 `IStorageEngine` 契约(读返副本/写收副本)+ `cloneRow`;Memory/KVStore/Hybrid/Aria 全部生效(改返回值不再改库、索引不再失配);**LSM 读取自洽**:未命中即回源 + CRC 校验,消除"缓存未命中 = 静默丢数据"(实测小缓存下 300 行只回 59 行);缓存上限语义修正(超大 SSTable 常驻) | | `89243ef` | **A3/A4 SAVEPOINT** | 新增 `SAVEPOINT_ROLLBACK` WAL 记录(复用 `data` 字段,格式不变);恢复按**事务内**下标窗口重放 → 已回滚的行不再复活;事务结束清空 savepoint 并校验归属 → 陈旧 savepoint 不再静默吞写入 | | `765df80` | **A1/A2 约束** | UPDATE 阶段 1 增加批内新主键互查(四引擎)→ 不再静默丢行;ALTER ADD UNIQUE 真正建索引并做存量校验(复用 `createIndex`),Memory 侧"重启静默丢行"随之消失 | +| `674da6b` | **A9/A10** | `ChangeNotifierEngine` 引擎装饰器:INSERT/UPDATE/DELETE/CLEAR/DDL 统一派发变更事件,`db.subscribe` 对本地写入生效(此前只在跨标签页广播时触发);关联子查询的 `$col` 绑定补上外层别名剥离与**递归进入子查询**(`WHERE id IN (SELECT ... WHERE o.user_id = u.id)` 此前静默空结果) | +| `4ab04df` | **A15 三值逻辑(PB-2)** | 见下节"三值逻辑根治" | -**测试规模**:1304 → **1334 通过 / 79 套件**;新增 `tests/v080-savepoint.test.ts`、`tests/v080-atomicity.test.ts`、`tests/version-contract.test.ts`、`tests/helpers/storage-harness.test.ts`。 +### 三值逻辑根治(A15 / PB-2,提交 `4ab04df`) -**验证状态**:`typecheck`(src + tests)、`lint` 全部为零错误。 +**根因**:`matchWhere`(布尔版,自带 `matchField`)与三值求值器**并存**,同一条 SQL +的语义取决于走哪个函数;更严重的是字段级 `$or` 递归进了"where 子句级"求值器。 + +**三类实测静默错值**(修复前的实际返回): + +| SQL | 修复前 | 修复后 | 机制 | +|---|---|---|---| +| `WHERE n = NULL`(n 列含 NULL 行) | 命中 NULL 行 | 空集 | `===` 比较 `null === null` 为真 | +| `WHERE n != NULL` | 所有非 NULL 行 | 空集 | 同上取反 | +| `WHERE s NOT LIKE 'x'`(含 NULL 行) | NULL 行被判真 | 仅真正不匹配的行 | 布尔取反未传播 UNKNOWN | +| `WHERE n NOT BETWEEN 1 AND 2` | **恒空集** | 真正的越界行 | 字段级 `$or` 子项 `{ $lt: 1 }` 被当成"查询列 `$lt`" | + +**根治方式(取消第二套实现,而非打补丁)**: + +1. `query/where-matcher.ts` 重写为**唯一一个递归求值器**,同时理解 where 子句级 + (键是列名 / 逻辑连接词)与操作符级(键是 `$gt` 之类)。区别由**位置**承载, + 不再由另一个函数承载;行上下文随求值上下文下传,`$col` 在任意嵌套深度都能解析。 + `matchWhere` 退化为"三值结果是否恰为 TRUE",并新增 `matchWhereThreeValued`。 +2. `sql/parser.ts`:`IS NULL` / `IS NOT NULL` 生成 `$isNull` / `$isNotNull` **谓词** + (此前与 `= NULL` / `!= NULL` 共用 `$eq: null` / `$ne: null`,调用方无法表达区别); + `BETWEEN` 生成真正的范围条件(此前把同一对象同时当操作符对象与操作数); + `NOT BETWEEN` 展开为 `$or: [{ $lt }, { $gt }]`。 +3. `query/executor.ts` 新增 `enginePreFilter`:逐行求值谓词(`$col` / `$exists` / + CASE 键)必须整体移出引擎层 —— 引擎无外层行上下文,会把它们判 UNKNOWN 并把 + **所有行**过滤掉,逐行求值再正确也无行可算(这正是 `WHERE t.x = t.y` 静默空结果的 + 第二层原因)。粒度按连接词决定:`$and` 成员可单独移除;`$or` / `$not` 成员一旦 + 移除就改变结果集(漏行 / 多行),故整条放弃下推。 + +**契约变更**(旧测试编码了错误语义,已按 SQL 标准改正并在测试内注明理由): + +- `{ $eq: null }` 不再命中 NULL 行(`= NULL` 恒 UNKNOWN)→ 需 NULL 匹配时用 `$isNull`; +- `IN` 列表含 NULL:`x IN (NULL, 'a')` 只命中 `'a'`(`null = NULL` 为 UNKNOWN), + 未命中的非 NULL 行因 UNKNOWN 同样不保留; +- 引擎层(`MemoryEngine.find` / `AriaEngine.find`)与 Query Builder 路径同步适用。 + +**验证**:新增 `tests/v080-sql-three-valued.test.ts` —— 26 条 SQL 语义矩阵 × +4 引擎(memory/disk/hybrid/aria)+ UPDATE/DELETE 写路径,共 **104 断言**。 + +**测试规模**:1304 → **1458 通过 / 83 套件**;新增 `tests/v080-savepoint.test.ts`、 +`tests/v080-atomicity.test.ts`、`tests/v080-subscribe.test.ts`、 +`tests/v080-correlated.test.ts`、`tests/v080-sql-three-valued.test.ts`、 +`tests/version-contract.test.ts`、`tests/helpers/storage-harness.test.ts`。 + +**验证状态**:`typecheck`(src + tests)、`lint` 全部为零错误;Aria 生产负载 +(10 万行 / OPFS / KV 后端崩溃恢复)全绿。 ### 实施过程中的额外发现(方案未列出,已一并修复) @@ -728,11 +774,11 @@ connection-manager/integrations/migration)两份完整报告,以及测试质 | 优先级 | 项目 | 状态 | |---|---|---| -| P0 | A9 `subscribe()` 对本地写入不触发 | 待做 | -| P0 | A10 `t.x = t.y` 与关联 `IN (SELECT ...)` 静默空结果 | 待做 | +| ~~P0~~ | ~~A9 `subscribe()` 对本地写入不触发~~ | ✅ `674da6b` | +| ~~P0~~ | ~~A10 `t.x = t.y` 与关联 `IN (SELECT ...)` 静默空结果~~ | ✅ `674da6b` + `4ab04df`(enginePreFilter) | +| ~~P1~~ | ~~A15 三值逻辑(PB-2 统一 `sqlCompare`)~~ | ✅ `4ab04df` | | P1 | A12 `maxLength`/`min`/`max` 在 memory/disk/hybrid 失效(PB-1 统一校验) | 待做 | | P1 | A13/A14 自引用外键失效、ON UPDATE CASCADE 只一层 | 待做 | -| P1 | A15 三值逻辑(PB-2 统一 `sqlEquals/sqlCompare`) | 待做 | | P1 | A17 INSERT 未知列静默丢弃 | 待做 | | P1 | A22/A23/A25/A26/A27/A29 查询层剩余项 | 待做 | | P1 | A35/A36/A37 JOIN NULL 键、派生表别名、未限定列名 | 待做 | @@ -740,7 +786,9 @@ connection-manager/integrations/migration)两份完整报告,以及测试质 | P1 | A40/A41 Aria `close()` 活跃事务守卫、`dropTable` DDL 原子性 | 待做 | | P0 | **PB-6 存储单一提交点(manifest)** | 待做 | | — | PC-2 e2e 真崩溃注入(CDP `Page.crash`) | 待做 | -| — | PC-4/PC-5 契约测试 + 属性测试 | 部分(跨引擎参数化已用于 A1/A2) | +| — | PC-4/PC-5 契约测试 + 属性测试 | 部分(跨引擎参数化已用于 A1/A2/A15) | | — | PG 文档与站点全量同步 | 待做 | -**下一步建议**:A9/A10(静默错误结果,P0)→ PB-1/PB-2(统一校验与三值逻辑,一次消除 A12/A15/A35 整类)→ PB-4(统一表达式求值,消除 A10 与"未解析即 false"整类)→ 存储层 A38~A41 → PB-6 → PC-2 → PG。 +**下一步建议**:PB-1 统一校验 choke point(一次消除 A12/A17 与 A1 剩余分支)→ +A35/A36/A37(JOIN 与派生表,同属"未限定/未解析即静默"一整类)→ 查询层剩余 +A22~A29 → 存储层 A38~A41 → PB-6 → PC-2 → PG。