fix(v0.8.0): 全量回归审查 —— 1 处 P0 数据丢失 + 4 处 P1 + 9 处 P2 根因修复
方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 vs 实现 / 公共 API 契约 / 测试质量),每条结论要求可复现证据;逐条复核 + 探针确认 + 变异验证(40 项全部 被对应用例拦住)。 P0:事务活跃期间 repair()/close()/周期 checkpoint 推进 WAL 水位 → 已 COMMIT 的 事务整批消失且恢复报告"干净"。根因 hasPendingFlushData()/computeDurableLsn() 不看 txnSnapshot;守卫此前只在 CheckpointManager 两个回调里。修复:守卫下沉到 computeDurableLsn() 与 advanceWalCheckpoint() 入口(唯一实现)。 P1: - WAL 前缀缺失丢弃整段活分片(回退上一代 manifest 时 kept 为空)→ 前缀缺失单独 记录,后缀照常重放;仅 fromLsn === 0 时才算真异常 - 孤儿回收门槛只看引擎层 dataLossSuspected,漏掉 LSM 层被丢的 SSTable → 统一 describeRecoveryDamage() 聚合判定(损坏时绝不删"引用不到"的文件) - vacuum() 逐层压缩绕过维护链 → vacuumLevels() 每层作为维护链任务执行 - reclaimRetiredNow() 无视在途读者(读者把"已退休"读成"文件损坏")→ 有读者时 退化为延迟回收 P2:WAL 记录级 CRC 损坏不计数不上报;旧格式表结构记录形状损坏静默当空库; bloomFilterBitsPerKey 配置被接受却完全不生效(构建器写死默认值,实现缺陷); 幽灵 meta;介质读故障等于文件损坏的语义无用例;manifest 回读校验两条守卫无用例; 文件名≠载荷世代判定无用例;pageIdWatermark 单调性无用例;分片号两条真实不变量 无用例。 覆盖率口径(第二处漏洞):interface.ts 混着三个运行时函数(cloneRow 等)却被 描述为"纯类型、不纳入统计" → 实现搬到 src/engine/row_clone.ts;搬完门禁真的 失败(functions 93.84% < 94%),补测退化路径后通过。 测试质量:3 条空壳用例改值级断言;1 条"全损坏"用例实际只走缓存 → 拆成两条真 用例;5 秒墙钟 race 改门控 + 失败上限;setTimeout 改 whenIdle();<= 收紧为 <。 变异脚本加固:正控(干净基线必须全绿)、编译失败/0 用例单独归类、300s 超时、 逐字节 sha256 恢复校验、O_EXCL 进程锁、锚点唯一性;变异 22 → 40 项。 文档两轮订正(16 + 11 条不成立宣称):MVCC 快照隔离、backup 一致性快照、 "空洞检测截断"、体积(251,109 B / gzip 63,145 B)、测试与覆盖率数字、 "5 种存储引擎"、Tree-shakable、错误码表补 16 个码、恢复报告字段、已知限制 (回退单向 / 多实例依赖 Web Locks / manifest 体积 / 尾部 WAL 分片不可识别)。 验证:常规套件 92 套件 / 1980 用例全绿;覆盖率 90.59 / 82.59 / 94.14 / 93.50 (阈值 90/82/94/93);e2e 14/14(真实 Chromium + OPFS + CDP 崩溃); 重型套件 4 套件 / 27 用例;变异 40/40;lint + 两份 tsc 干净;dist 已重建。
This commit is contained in:
+150
-13
@@ -2,7 +2,7 @@
|
||||
|
||||
All notable changes to MetonaSqlark will be documented in this file.
|
||||
|
||||
## [0.8.0] - 2026-08-16
|
||||
## [0.8.0] - 2026-09-15
|
||||
|
||||
### 根治性迭代 —— 统一语义 / 消灭复发结构 / 验证基础设施
|
||||
|
||||
@@ -11,8 +11,9 @@ All notable changes to MetonaSqlark will be documented in this file.
|
||||
> 必然复发的结构**:多处并存的语义实现被收敛为唯一实现,并第一次让崩溃语义、
|
||||
> 错误码一致性、入口等价性变成可机器验证的门禁。
|
||||
>
|
||||
> 测试规模 1304 → **1935(91 套件)+ 14 项 e2e**(另 4 个重型套件在独立 CI
|
||||
> job 串行运行);B-6 的全部修复另有 **22 项变异验证**(`scripts/mutation-b6.py`,全部通过)。
|
||||
> 测试规模 1304 → **1980(92 套件)+ 14 项 e2e**(另 4 个重型套件在独立 CI
|
||||
> job 串行运行);B-6 与审查轮的全部修复另有 **40 项变异验证**(`scripts/mutation-b6.py`,
|
||||
> 全部通过,且脚本自带正控 / 编译失败区分 / 超时 / 逐字节恢复校验 / 进程锁)。
|
||||
|
||||
### 工作流 B · 结构根治(消除整类缺陷)
|
||||
|
||||
@@ -53,11 +54,28 @@ All notable changes to MetonaSqlark will be documented in this file.
|
||||
- **元数据损坏不再静默空库**:此前 `__aria_lsm_meta*` 是裸 JSON,解析失败即
|
||||
`[]` → 看不到任何表,随后 `repair()` 还会把"没人引用"的活页删光(不可逆)。
|
||||
现在全部世代校验失败抛 `ARIA_MANIFEST_CORRUPT`,旧格式迁移遇到坏 JSON 抛
|
||||
`ARIA_LEGACY_META_CORRUPT`。
|
||||
`ARIA_LEGACY_META_CORRUPT`。表结构记录的**形状**坏掉(数组 / null / 表名映射到
|
||||
非对象 / 列定义不是对象)此前被静默忽略(打开后"看不到任何表")—— 现在与坏 JSON
|
||||
同一条路径显式报错(v0.8.0 审查修复)。
|
||||
- **WAL**:LSN 改为一库一条单调水位(manifest 记账);按水位删除旧分片;
|
||||
**分片号只增不减**(修复前全量截断后重置为 0,会与 manifest 记录的
|
||||
`startSegment` 错位,实测造成"删掉的行复活"与"已确认写入丢失"两个方向的损坏);
|
||||
分片空洞(含前缀缺失)显式报 `ARIA_WAL_GAP` 而不是静默丢弃尾部。
|
||||
**分片号绝不回退、也绝不低于 manifest 水位**(修复前全量截断后重置为 0,
|
||||
会与 manifest 记录的 `startSegment` 错位,实测造成"删掉的行复活"与
|
||||
"已确认写入丢失"两个方向的损坏;整体清空后允许复用**最后用过的那个号** ——
|
||||
记录自带 LSN,旧世代记录按水位跳过,这一点在审查轮做了语义澄清);
|
||||
分片内部空洞显式上报,水位从未推进时的前缀缺失同样按空洞上报,
|
||||
水位已推进时的前缀缺失视为"已清理的前缀"(回退上一代 manifest 的正常情况)。
|
||||
另修:前缀缺失此前会把**整段活分片**丢掉(`kept = segments.filter(seq < firstGap)`
|
||||
在只有前缀缺失时结果为空的连带后果)。
|
||||
- **WAL 记录级损坏**:CRC 失败的记录此前只打一条日志 —— 现在计数并进入恢复报告
|
||||
(`droppedWALRecords` + `dataLossSuspected = true`),"少了几条已提交写入"不再
|
||||
不可观测。
|
||||
- **checkpoint 不再把 LSN 归零**:LSN 是 manifest 记录的全库单调水位,归零会让
|
||||
"跳过 `lsn <= startLsn`"的判定与历史分片冲突(同一段 LSN 区间对应两批不同记录)。
|
||||
- **`vacuum()` 报告真实层数**:修复前硬编码返回 6 且**底部层永不压缩**
|
||||
(墓碑与历史版本在最底层永久累积);现在逐层尝试(含底部层原地合并)并只统计
|
||||
真正合并了的层,同时把逐层压缩挂到维护链上串行执行。
|
||||
- **`close()` 在落盘失败时也必须释放资源**:修复前会卡在 flush 上,后端/锁/状态
|
||||
都不复位(现在 `try/finally` 保证清理,错误照常抛出)。
|
||||
- **LSM**:冻结表成为一等状态(失败可重试,`flush()` 先入链再报错,修复前
|
||||
一次后台失败会让之后每次 flush 直接抛错、数据永远等不到落盘);compaction
|
||||
不再"先摘整层再合并"(窗口内该层对读者不可见 → 少行);底部层原地合并
|
||||
@@ -72,9 +90,10 @@ All notable changes to MetonaSqlark will be documented in this file.
|
||||
不再被折叠成 null 从而误删元数据。
|
||||
- **恢复报告**:`engine.getRecoveryReport()` 返回
|
||||
`{droppedSSTables, dataLossSuspected, walGaps, legacyImported, manifestFallback}`。
|
||||
- 新增 `tests/v080-b6-single-commit-point.test.ts`(63 项:含 manifest 严格校验
|
||||
表驱动 25 例)与
|
||||
`scripts/mutation-b6.py`(22 项变异验证:把修复回退到修复前行为,对应用例
|
||||
- 新增 `tests/v080-b6-single-commit-point.test.ts`(审查轮后扩到 **96 项**:
|
||||
manifest 严格校验表驱动 24 例 + 事务水位 P0 + 前缀空洞语义 + 在途读者与退休文件 +
|
||||
孤儿回收门槛 + 稀疏/损坏分支 + 旧格式形状校验 + bloom 配置透传)与
|
||||
`scripts/mutation-b6.py`(**40 项**变异验证:把修复回退到修复前行为,对应用例
|
||||
必须失败 —— 全部被拦住)。
|
||||
- **B-6 存储提交点(KVStore 侧止血)** — 两处 P0:① `open()` 遇损坏日志尾部会**清空整个
|
||||
日志**(写 3 条 → 第 4 条撕裂 → 重开可见 → 再重开全空);② 自动 checkpoint
|
||||
@@ -116,8 +135,8 @@ All notable changes to MetonaSqlark will be documented in this file.
|
||||
(statements 90 / branches 82 / functions 94 / lines 93);CI 常规 job 带
|
||||
`--coverage`;lint 去掉 `continue-on-error`;新增 `tests/` 类型检查
|
||||
(修复 **103 个**被 babel 剥离类型掩盖的测试类型错误);CI 校验 dist 与源码同步。
|
||||
→ 实测 Statements 90.43% / Branches 82.21% / Functions 94.27% / Lines 93.44%
|
||||
(命令与 CI 常规 job 完全一致,可复现)。
|
||||
→ 实测(v0.8.0 收尾复测)Statements 90.59% / Branches 82.59% / Functions 94.14% /
|
||||
Lines 93.50%(命令与 CI 常规 job 完全一致,可复现)。
|
||||
- **测试介质忠实性修正**(两处同源缺陷,此前让所有多实例/多库验证跑在错误语义上)
|
||||
- `SharedMemoryBackend` 的读缓存是每实例私有的 → 介质退化为"每实例一份快照",
|
||||
跨实例写入不可见(这正是陈旧实例覆盖新实例写入那条 bug 起初查不出来的原因);
|
||||
@@ -140,12 +159,130 @@ All notable changes to MetonaSqlark will be documented in this file.
|
||||
→ 现构建 `dist/react.js` / `dist/vue.js` 并配套**手写精确类型声明**;
|
||||
补 `peerDependencies`(react / vue,均可选)。
|
||||
|
||||
### 全量回归审查(发版前最后一轮,独立复现 + 变异验证)
|
||||
|
||||
> 方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 vs 实现 / 公共 API 契约 /
|
||||
> 测试质量),**每一条结论都要求可复现证据**;我再逐条复核并用探针确认。共确认
|
||||
> 14 项实现缺陷(1 项 P0、4 项 P1、9 项 P2)与 16 条不成立的文档宣称。
|
||||
|
||||
**P0 · 事务活跃期间推进 WAL 水位会让已 COMMIT 的事务整批消失**
|
||||
|
||||
- 复现:`BEGIN` → `INSERT` → `repair()` → `COMMIT` → 崩溃 → 重开 →
|
||||
已提交的行不见了,而 `getRecoveryReport()` 报告"干净"(`dataLossSuspected:false`、
|
||||
`walGaps:[]`)。
|
||||
- 根因:`hasPendingFlushData()` / `computeDurableLsn()` 只看 memtable/frozen,
|
||||
而事务内的写入只落在 `txnSnapshot`(内存)+ WAL —— 于是"没有未落盘数据"成立,
|
||||
水位被推到当前 LSN 并按该水位删掉旧分片;随后 `COMMIT` 返回成功,但那些
|
||||
INSERT 记录已因 `lsn <= startLsn` 被跳过。此前只有 `CheckpointManager` 的两个
|
||||
回调带事务守卫,`repair()` / `close()` / 周期 checkpoint 三条路径都能踩到。
|
||||
- 修复:守卫下沉到 `computeDurableLsn()` 与 `advanceWalCheckpoint()` 入口(唯一实现),
|
||||
三条路径同时覆盖;新增回归用例与变异验证(R1)。
|
||||
|
||||
**P1**
|
||||
|
||||
1. **WAL 前缀缺失丢弃整段活分片** — 前缀缺失与内部空洞被混为一谈时,
|
||||
`kept = segments.filter(s => s.seq < firstGap)` 的结果为空 → 回退到上一代
|
||||
manifest(`startSegment` 比现存最小分片更小)时**所有活分片被丢掉**。
|
||||
现在前缀缺失单独记录(`missingPrefix`),后缀照常重放;只有"水位从未推进
|
||||
(`fromLsn === 0`)"时前缀缺失才算真异常(R4)。
|
||||
2. **孤儿回收门槛漏掉 LSM 层损坏** — 门槛只看引擎层 `dataLossSuspected`
|
||||
(只在 WAL 无兜底时才置位),于是"manifest 已推进 + 某 SSTable 因损坏被丢"
|
||||
这类真损坏下,`repair()` 仍会把"引用不到"的页面(含在途读者持有的退休文件)
|
||||
当作孤儿删掉(不可逆)。现在统一用 `describeRecoveryDamage()` 聚合引擎层与
|
||||
各 LSM 的全部损坏迹象(被丢的 SSTable / WAL 空洞 / 记录损坏 / 回退世代)(R3)。
|
||||
3. **`vacuum()` 绕过维护链** — 逐层压缩改为 `vacuumLevels()`:每层作为维护链任务
|
||||
执行(复用 `compacting` 防重入),不再与后台 compaction 并发向同一目标层写产物
|
||||
(产物一律 `unshift` 到队首,层内顺序 = 新旧顺序,交错会读到旧值 / 底部层丢墓碑
|
||||
导致已删除行复活)。**如实说明**:引擎层 `vacuum()` 开头的 `lsm.flush()` 本来就会
|
||||
drain 维护链,所以"引擎层调用"这条路径恰好被顺带串行化 —— 真正被测试锁定的是
|
||||
LSM 层不变量(R 系列未覆盖 `drainMaintenance` 那一行,它在实现上只是优化:
|
||||
抢在链前重算层内文件数,去掉后仍串行,只是会多排几个空任务)。
|
||||
4. **`reclaimRetiredNow()` 无视在途读者** — 读者的快照可能正持有被"退休"的文件,
|
||||
强制回收会让它读不到数据(并被误判为"文件损坏")。现在有活跃读者时退化为
|
||||
延迟回收,读者退出后再物理删除(R2)。
|
||||
|
||||
**P2**
|
||||
|
||||
- WAL 记录级 CRC 损坏只打日志(`droppedWALRecords` + 数据丢失标记,R9)。
|
||||
- 旧格式表结构记录的**形状**损坏(数组 / null / 非对象条目)静默当空库
|
||||
→ 与坏 JSON 走同一条显式报错路径(R16)。
|
||||
- **`bloomFilterBitsPerKey` 配置被接受却完全不生效**:`SSTableBuilder` 始终用默认
|
||||
位数构造 Bloom Filter —— 这是实现缺陷,修的是实现(配置透传到构建器)而不是文档;
|
||||
新增"位数改变落盘 bloom 段大小 + 任意位数都不产生 false negative"的测试(R17/R18)。
|
||||
- `pageIdWatermark` 只做单调 max 的断言此前不存在(白盒水位回退用例,R7)。
|
||||
- manifest 提交的**回读校验**此前无用例覆盖(吞写 → 必须 `ARIA_MANIFEST_WRITE_FAILED`,
|
||||
且世代号不前进;覆盖"文件读不回"与"内容读回来是坏的"两条守卫,R8)。
|
||||
- "文件名世代 ≠ 载荷世代"的世代无效判定无用例(R6)。
|
||||
- `dropInvalidSSTable` 丢弃文件后不同步 `levels` 会留幽灵 meta;"介质读故障 ≠
|
||||
文件损坏"此前无用例(读故障必须让 compaction 失败且一个 meta 都不许丢,R11)。
|
||||
- 旧格式迁移的失败分支(`__aria_schemas` 坏 JSON / 非数组 / 条目形状非法)只有
|
||||
坏 JSON 一条有用例。
|
||||
- WAL 整体清空后"分片号绝不回退到 0"与"新写入绝不低于 manifest 水位"两条不变量
|
||||
此前无用例(R13 / R14)。
|
||||
|
||||
**覆盖率口径(审查发现的第二处口径问题)**
|
||||
|
||||
- README / `jest.config.cjs` / PLAN 都声称 `collectCoverageFrom` 只排除了**两个纯类型文件**
|
||||
(`engine/interface.ts`、`query/ast.ts`,"可执行语句为 0")。审查实测:`interface.ts`
|
||||
里有三个**运行时函数**(`cloneRow` / `cloneRowFallback` / `cloneRows`,约 18 条语句),
|
||||
被 Memory 引擎与 AriaEngine 的读路径调用 —— 也就是说这块真实实现代码一直逃过覆盖率
|
||||
统计,正是 G5 门禁要根治的"口径虚高"。
|
||||
- 根因修复(而不是改文档):实现搬到新模块 `src/engine/row_clone.ts`,
|
||||
`interface.ts` 回到**纯类型**(`query/ast.ts` 本来就是)。搬完覆盖率门禁立刻**失败**
|
||||
(functions 94% 阈值 → 实测 93.84%),暴露出退化路径 `cloneRowFallback` 从未被测试;
|
||||
补 `tests/engine/row-clone.test.ts`(8 项:structuredClone 可用/不可用/抛错三条路径、
|
||||
Date/TypedArray/ArrayBuffer、深拷贝不泄漏、批量拷贝)后门禁恢复通过。
|
||||
- 最终数字(口径修正后,含新增的 8 项测试):92 套件 / 1980 用例,
|
||||
语句 90.59% / 分支 82.59% / 函数 94.14% / 行 93.50%。
|
||||
|
||||
**测试质量修正("绿"不等于"有保护")**
|
||||
|
||||
- 3 条空壳用例改为值级断言(孤儿回收门槛、强制回收、页面映射退休);
|
||||
- 1 条"整层文件全损坏"用例实际上走的是**缓存**(根本没读介质,等于什么都没测)
|
||||
→ 拆成"介质读故障 = compaction 必须失败且不许丢 meta"与"文件真残缺 = 丢弃必须
|
||||
进恢复报告"两条真用例;
|
||||
- 5 秒墙钟 `Promise.race` 改为门控信号 + 失败上限(超时只会让测试失败,绝不会让
|
||||
它在实现错误时"碰巧通过");`setTimeout` 睡眠改为 `whenIdle()` 确定性等待;
|
||||
- `<=` 收紧为严格 `<`(冻结意图的水位必须**严格低于**意图起点);删掉死代码
|
||||
(`const inner`、`void manifest`)与误名的"非 JSON"用例。
|
||||
|
||||
### 文档订正(第二轮 G6:16 条不成立的宣称)
|
||||
|
||||
- **MVCC**:`site/docs.html`(表格、事务行、能力表)与 `site/demo.html` 仍写
|
||||
"MVCC 快照隔离",而实现是"版本链仅作事务内 undo + 事务串行";`mvcc.ts` 的文件头
|
||||
同样写错 → 全部改为如实描述。
|
||||
- **`backup()`**:`interface.ts` 注释仍写"一致性快照" → 改为"逐表读取(非跨表快照)"。
|
||||
- **WAL 空洞**:`docs.html` 写"空洞检测截断" → 改为"内部空洞如实上报并拒绝静默继续"。
|
||||
- **体积**:README 与 docs.html 写"~105KB / gzip ~27KB" → 实测 min 产物
|
||||
251,109 字节 / gzip 63,145 字节(`stat -c%s dist/metona-sqlark.min.js`;
|
||||
`gzip -c dist/metona-sqlark.min.js | wc -c`)。
|
||||
- **测试与覆盖率**:README badge/正文、site 徽标与数字卡、CHANGELOG 全部更新为
|
||||
实测值(1980 / 92 套件 / 90.59% / 82.59% / 94.14% / 93.50%)。
|
||||
- **存储模式**:site 仍写"5 种存储引擎" → 改为"4 种模式 + 3 种后端";
|
||||
"Tree-shakable"改为"UMD/ESM/CJS 多格式输出"(单文件 bundle + 未声明
|
||||
`sideEffects`,不宜宣称可摇树)。
|
||||
- **错误码表**:site 文档缺 **16 个**错误码(`ARIA_MANIFEST_NOT_LOADED`、
|
||||
`ARIA_SSTABLE_SAVE_CONTRACT`、`ARIA_DB_NOT_OPEN`、`ARIA_OPEN_ERROR`、
|
||||
`DB_NOT_OPEN`、`KV_*`、`TX_*`、`SAVEPOINT_*`、`UNKNOWN_STATEMENT`、
|
||||
`COLUMN_EXISTS`、`FOREIGN_KEY_VIOLATION`)→ 全部补齐。
|
||||
- **维护脚本与门禁**:CONTRIBUTING 的变异数量 17 → 40,并补上脚本自身的保证
|
||||
(正控 / 编译失败区分 / 超时 / 逐字节恢复校验 / 进程锁);PLAN 附录 G 的
|
||||
"12 项事故级"改为 11 项(与总账 A1~A11 一致)、G4 的"零丢失"限定为"故障注入
|
||||
矩阵覆盖范围内"(并记录矩阵之外发现的那处 P0)、H.1 的"25 例"改为 24 例。
|
||||
|
||||
### 已知限制(v0.8.0 新增/变更)
|
||||
|
||||
- `backup()` 不是跨表一致性快照(逐表读取)
|
||||
- 复合主键仍不支持(建表时 `SCHEMA_ERROR`)
|
||||
- 简单 CASE 形式(`CASE <表达式> WHEN <值>`)不支持,仅支持搜索式
|
||||
(`CASE WHEN <条件> THEN ...`)
|
||||
- **回退旧版本是单向的**:迁移后新写入只进 manifest,旧的 `__aria_lsm_meta*` /
|
||||
`__aria_schemas` 停留在迁移那一刻;用旧版本打开会看到迁移时刻的旧视图
|
||||
- **多实例写入依赖 Web Locks**:无 Web Locks 时降级为提交点冲突检测
|
||||
(`STALE_INSTANCE`),只保证不静默覆盖别人的提交,不保证多实例写入的数据完整性
|
||||
- **manifest 体积随 SSTable 数量增长**(单文件整体重写、保留两代,无增量/分层机制)
|
||||
- **尾部 WAL 分片丢失无法从介质自身识别**(无"它本该存在"的证据;`ARIA_WRITE_LOST`
|
||||
只兜住"有未落盘冻结表却重放不到任何记录"的情况)
|
||||
|
||||
|
||||
## [0.7.4] - 2026-08-15
|
||||
@@ -1096,7 +1233,7 @@ All notable changes to MetonaSqlark will be documented in this file.
|
||||
- **SSTable 构建器/读取器** (`index/sstable_builder.ts`, `sstable.ts`): 二分查找 + 范围扫描
|
||||
- **Merge Iterator** (`index/merge_iterator.ts`): 最小堆多路归并,去重保留最新值
|
||||
- **WAL** (`wal/`): 二进制日志格式 (LSN/type/txnId/table/key/json/CRC) + Checkpoint 管理
|
||||
- **MVCC** (`transaction/mvcc.ts`): 版本链 + 快照隔离 + GC
|
||||
- **MVCC** (`transaction/mvcc.ts`): 版本链 + GC(**v0.8.0 澄清**:此处当时宣称的「快照隔离」从未实现 —— 版本链仅作事务内 undo,事务串行)
|
||||
- **存储后端** (`store/`): IndexedDB / Memory 双后端抽象
|
||||
- **LZ4 压缩** (`compression/lz4.ts`): 简易页面级压缩
|
||||
- **`mode: 'aria'`** — 新增存储模式,可通过 `MetonaSqlark.create({ mode: 'aria' })` 激活
|
||||
|
||||
Reference in New Issue
Block a user