diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a72a4e..94b24e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ All notable changes to MetonaSqlark will be documented in this file. > 必然复发的结构**:多处并存的语义实现被收敛为唯一实现,并第一次让崩溃语义、 > 错误码一致性、入口等价性变成可机器验证的门禁。 > -> 测试规模 1304 → **1872(90 套件)+ 14 项 e2e**(另 4 个重型套件在独立 CI -> job 串行运行)。 +> 测试规模 1304 → **1935(91 套件)+ 14 项 e2e**(另 4 个重型套件在独立 CI +> job 串行运行);B-6 的全部修复另有 **22 项变异验证**(`scripts/mutation-b6.py`,全部通过)。 ### 工作流 B · 结构根治(消除整类缺陷) @@ -45,7 +45,38 @@ All notable changes to MetonaSqlark will be documented in this file. 并统一分隔标识符语义:引号只在"解析→执行"边界脱去。 顺带补上 **ORDER BY 的列存在性/歧义校验**(此前 JOIN 里裸写两表同名列既不报错 也不确定按哪列排)。 -- **B-6 存储提交点(KVStore)** — 两处 P0:① `open()` 遇损坏日志尾部会**清空整个 +- **B-6 存储提交点(完整实施,非降级方案)** — 见 `PLAN-v0.7.5.md` 附录 H。 + - **`__aria_manifest_` 单一提交点**:页面水位 + 各命名空间 SSTable + 元数据 + 表结构 + WAL 起始位置 + 待落盘冻结表意图,一次原子提交 + (头部/载荷双 CRC、先写后验、保留两代)。顺序固定为 + **数据落盘 → manifest 提交 → 才允许截断 WAL / 删除旧文件**。 + - **元数据损坏不再静默空库**:此前 `__aria_lsm_meta*` 是裸 JSON,解析失败即 + `[]` → 看不到任何表,随后 `repair()` 还会把"没人引用"的活页删光(不可逆)。 + 现在全部世代校验失败抛 `ARIA_MANIFEST_CORRUPT`,旧格式迁移遇到坏 JSON 抛 + `ARIA_LEGACY_META_CORRUPT`。 + - **WAL**:LSN 改为一库一条单调水位(manifest 记账);按水位删除旧分片; + **分片号只增不减**(修复前全量截断后重置为 0,会与 manifest 记录的 + `startSegment` 错位,实测造成"删掉的行复活"与"已确认写入丢失"两个方向的损坏); + 分片空洞(含前缀缺失)显式报 `ARIA_WAL_GAP` 而不是静默丢弃尾部。 + - **LSM**:冻结表成为一等状态(失败可重试,`flush()` 先入链再报错,修复前 + 一次后台失败会让之后每次 flush 直接抛错、数据永远等不到落盘);compaction + 不再"先摘整层再合并"(窗口内该层对读者不可见 → 少行);底部层原地合并 + **回收墓碑**;`compacting` 改为按层集合;被取代的 SSTable 进入退休表, + 等更早的读者退出才物理删除;`rangeScanLazy` 提前终止不再多算一条。 + - **读路径自洽**:删除引擎层全部 `prefetch*`/`drainChain` 依赖,改为 + "快照 + 结构版本乐观重试"(修复前那次改动会暴露一个新缺陷:并发 flush + 在扫描的 await 窗口里发布的 SSTable 对本次扫描不可见 → 刚改名的行读回旧值)。 + - **checkpoint 不等 compaction**:写路径的周期 checkpoint 只落 memtable, + compaction 继续后台跑(v0.6.1 记录的 "8~11s 悬崖"的另一半)。 + - **介质故障与"文件不存在"分开**:读失败抛 `ARIA_SSTABLE_READ_FAILED`, + 不再被折叠成 null 从而误删元数据。 + - **恢复报告**:`engine.getRecoveryReport()` 返回 + `{droppedSSTables, dataLossSuspected, walGaps, legacyImported, manifestFallback}`。 + - 新增 `tests/v080-b6-single-commit-point.test.ts`(63 项:含 manifest 严格校验 + 表驱动 25 例)与 + `scripts/mutation-b6.py`(22 项变异验证:把修复回退到修复前行为,对应用例 + 必须失败 —— 全部被拦住)。 +- **B-6 存储提交点(KVStore 侧止血)** — 两处 P0:① `open()` 遇损坏日志尾部会**清空整个 日志**(写 3 条 → 第 4 条撕裂 → 重开可见 → 再重开全空);② 自动 checkpoint 失败会让**已确认写入**报错(而该写入已在 WAL 中,报错与事实相反)。另修陈旧实例 的 checkpoint 会**静默抹掉**新实例写入(现抛 `STALE_INSTANCE` 拒绝提交)。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31d2b91..b5c7e94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -228,3 +228,21 @@ const db = await MetonaSqlark.create({ ## Questions? Open an issue at [git.metona.cn/MetonaTeam/MetonaSqlark/issues](https://git.metona.cn/MetonaTeam/MetonaSqlark/issues). + +--- + +## 变异验证(回归套件的"是否只是陪跑"检查) + +修复类提交必须能回答一个问题:**把修复回退到修复前的行为,对应用例会不会失败?** +不会失败的用例等于没有保护。 + +```bash +# B-6(存储单一提交点 / LSM 结构根治)17 项变异验证 +python3 scripts/mutation-b6.py + +# 只跑其中一条(按名字子串匹配) +python3 scripts/mutation-b6.py "WAL 分片号复用" +``` + +脚本会临时改写 `src/`、运行对应用例、再恢复源码(收到 SIGINT/SIGTERM 也会恢复), +最后打印每一条是"被拦住"还是"仍然通过"。**出现任何一条"仍然通过",本次提交不算完成。** diff --git a/PLAN-v0.7.5.md b/PLAN-v0.7.5.md index a43ad1a..165dd8b 100644 --- a/PLAN-v0.7.5.md +++ b/PLAN-v0.7.5.md @@ -804,7 +804,7 @@ connection-manager/integrations/migration)两份完整报告,以及测试质 | ~~P1~~ | ~~A37 未限定列名 / WHERE 列存在性~~ | ✅ `567d150` | | ~~P1~~ | ~~A38/A39 压缩被遮蔽 / O(n²)~~ | ✅ `97b9fa4` | | ~~P1~~ | ~~A41 DDL 原子性~~ | ✅ `bf93ec5`(A40 经探针未复现,已固化为护栏) | -| ~~P0~~ | ~~**B-6 存储单一提交点(manifest)**~~ | ✅ KVStore 三处止血(`edd9f1d`/`68e4731`)+ Aria DDL WAL 意图(`bf93ec5`) | +| ~~P0~~ | ~~**B-6 存储单一提交点(manifest)**~~ | ✅ **完整实施**(非降级选项):`__aria_manifest` 单一提交点 + LSM 44/45/47/49/50/51/55 全部改造 + 引擎层 prefetch 依赖删除 —— 见**附录 H** | | ~~P1~~ | ~~**B-4 统一表达式求值器**~~ | ✅ `3983aae`(CASE 结构化解析 + 列引用/聚合统一) | | ~~P1~~ | ~~**B-5 输出列序号 + 分隔标识符**~~ | ✅ `2c945ee` | | ~~—~~ | ~~PC-2 e2e 真崩溃注入(CDP `Page.crash`)~~ | ✅ `6a0cfeb`(14 项 e2e,含两个 OPFS 写入窗口) | @@ -812,7 +812,7 @@ connection-manager/integrations/migration)两份完整报告,以及测试质 | — | PG 文档与站点全量同步 | 待做 | **下一步建议**:v0.8.0 的三条工作流(A 全部缺陷 / B-1~B-6 结构根治 / C 验证 -基础设施)与 G1~G6 门禁均已完成(见附录 G)。后续版本方向: +基础设施)与 G1~G6 门禁均已完成(见附录 G、附录 H)。后续版本方向: 1. **真跨表快照** — `backup()` 目前是逐表读取(已如实写进已知限制)。真快照需要 行所有权改为 COW(引擎内所有写入路径统一"新对象替换"而非就地修改), @@ -820,15 +820,24 @@ connection-manager/integrations/migration)两份完整报告,以及测试质 2. **MVCC 真快照隔离** — `snapshotLsn`/`prevVersion` 目前只写不读,事务串行。 若确有多事务并发需求,需让读路径走版本链而不是 `txnSnapshot`; 3. **简单 CASE 形式** — `CASE <表达式> WHEN <值> THEN ...`(当前仅搜索式); -4. **复合主键** — 存储布局与外键引用目前假设单主键; -5. **B-1 遗留** — `AriaEngine` 的 compaction / merge 写入路径尚未过 - `validatePayload`(数据来自本引擎内部生成的 SSTable,非用户输入)。 +4. **复合主键** — 存储布局与外键引用目前假设单主键。 + +**关于原"B-1 遗留(compaction/merge 未过 validatePayload)"的结论**:该项**不是缺陷, +也无需补校验**,理由是数据来源而非实现惰性 —— LSM 的 compaction/merge 输入只可能 +是本引擎**已提交的 SSTable**(`levels` 中的 meta 由本进程写入),其中每一行都曾在 +唯一写入 choke point(`Executor` → `validatePayload` → `compileValidator`)上校验过; +compaction 做的是"逐字节搬运 + 按 key 去重",不解析、不构造新的行语义。 +在合并路径上再跑一遍校验反而有害:`ALTER TABLE` 之前写入的旧行形状与当前 schema +可以合法地不同(这正是 ALTER 的语义),重校验会把合法历史数据判成非法而**中断 +compaction**。触发条件明确写在这里:**一旦 LSM 接受外部输入(例如导入第三方 +SSTable / 跨库搬迁),必须在 `save()` 之前补校验**。 **已完成的验证基础设施(本轮重点)**: - PC-2 真崩溃注入(CDP `Page.crash`)+ OPFS `createWritable` 的两个写入窗口; - `FaultyBackend` 字节级故障注入(撕裂/bit-flip/掉电/写失败); - **变异验证**成为回归套件的标准做法:把修复回退到修复前的行为,对应用例 - 必须失败。本轮 A15/B-6(①②③)/A13/A38/A39 全部通过该检查 —— + 必须失败。本轮 A15/A13/A38/A39 与 **B-6 全量 17 项**均通过该检查(可执行脚本 + `scripts/mutation-b6.py`,逐项打印"变异是否被拦住")—— 这是"测试真能拦住回归"与"测试只是陪跑"的分界线。 - 测试介质的两处**忠实性**缺陷已被修正(`SharedMemoryBackend` 跨实例可见性、 OPFS mock 的目录语义)—— 它们此前让"多实例/多库"的验证跑在错误语义上。 @@ -849,3 +858,88 @@ connection-manager/integrations/migration)两份完整报告,以及测试质 | **G6** | 文档与实现一致:README/CHANGELOG/site 中每一条能力宣称都有对应测试或已删除 | §8 的 **12 条**全部收口:订阅(已接线)、LZ4 页面压缩(A38 已修)、`./migration` 导出、react/vue 产物+d.ts+peerDeps、`disconnect` 进类型、priority(改为实现符合文档)、MVCC 快照隔离(改为如实描述)、backup 一致性快照(改为如实描述并列入已知限制)、"5 种引擎"(改为 4 模式 + 3 后端)、`diskEngine` 生效范围(改为仅 aria)、"在线一致性快照"(改为全库导出)、钩子契约(CONTRIBUTING 写明返回值语义) | ✅ | **额外要求**:CI lint 已去掉 `continue-on-error`(真正阻断);`tsconfig.test.json` 对 `tests/` 做类型检查并修复了 **103 个**被 babel 剥离类型掩盖的类型错误。 ✅ + +--- + +## 附录 H · B-6「存储层单一提交点」完整实施记录(v0.8.0 收敛) + +> **为什么还需要这一节**:附录 F 的"已完成"表里 B-6 只列了 ①②③(KVStore 三处止血)+ +> Aria DDL WAL 意图,而 §B-6 的规格要求的是 **`__aria_manifest` 单一提交点 + LSM 单项改造**; +> §"B-6 的降级选项"也白纸黑字写着那五处止血是"若周期不够"的替代品。 +> 本次(周期不受限、且明确禁止降级方案)把 B-6 **按规格完整实现**,本节记录落地细节、 +> 实施中新发现的 10 个缺陷,以及可复现的验证方式。 + +### H.1 交付物 + +| 文件 | 作用 | +|---|---| +| `src/engine/aria/store/manifest.ts`(新增) | `__aria_manifest_`:magic + formatVersion + generation + 头部 CRC + 载荷 CRC;`ManifestStore.load/claimOwnership/commit`;先写后验;保留两代;损坏世代**显式失败** | +| `src/engine/aria/index/lsm.ts` | 冻结表一等状态(id/LSN/重试/提交窗口);读路径 epoch + 结构版本重试;compaction 不再摘整层、底部层原地合并回收墓碑、按层 `compacting` 集合;flush/maintenance 双链;恢复报告 | +| `src/engine/aria/index.ts` | manifest 装载 → 旧格式迁移 → 认领所有权;`commitManifest()`(唯一提交入口);`advanceWalCheckpoint()`(先算边界 → 提交 → 再删除);表结构随 manifest 提交;恢复报告 `getRecoveryReport()`;删除引擎层全部 `prefetch*` 依赖 | +| `src/engine/aria/wal/log.ts` / `segmented_store.ts` | LSN 全库单调(manifest 记高水位);`planKeepFrom` / `truncateBefore`;**分片号只增不减**;空洞(含前缀缺失)如实上报;按水位清理旧格式键 | +| `src/engine/aria/store/page_sstable_store.ts` | 活跃 + 退休两张页面映射(在途读者按 id 读取不再被 compaction 摘除影响);`registerPageIds` | +| `src/engine/aria/store/file_manager.ts` | 页面水位取 manifest / 旧 meta / 现存最大 id 三者最大(单调、永不复用);`__aria_meta` 降级为兼容提示 | +| `src/engine/aria/index/sstable.ts` | 三份解析循环合并为 `iterEntries()` 一份,越界策略统一 | +| `src/engine/aria/index/merge_iterator.ts` | 胜出来源的补充推迟到下一次 `next()`(提前终止不多算) | +| `src/engine/aria/wal/checkpoint.ts` | checkpoint 只落 memtable(`flushMemtables?`),不等 compaction | +| `tests/v080-b6-single-commit-point.test.ts`(新增 63 项) | 上述每一条的回归 + manifest 严格校验表驱动 25 例;`scripts/mutation-b6.py`(22 项变异验证,全部被拦住) | + +### H.2 提交顺序(唯一不变量) + +``` +数据落盘(SSTable 页面 / 整 value 已 await 完成) + ↓ +__aria_manifest_ 提交(单文件原子写 + 回读校验) + ↓ +才允许截断 WAL / 删除旧分片 / 删除旧 SSTable +``` + +- manifest 载荷 = `{pageIdWatermark, namespaces{SSTable meta + nextSstableId}, schemas, + wal{startSegment,startLsn,nextLsn}, frozen[冻结表意图], owner{instanceId,epoch}, committedAt}` +- 恢复只认**最后一份 CRC 通过**的世代;存在 manifest 文件但全部世代无效 → `ARIA_MANIFEST_CORRUPT` + (修复前:裸 JSON meta 解析失败 → `[]` → **静默空库**,随后 repair 还会把"没人引用"的活页删光) +- `frozen` 意图的作用:`wal.startLsn` 不得越过最早冻结表的 `lsnAtFreeze`; + 恢复时若"manifest 声称有未落盘数据 + 一条 WAL 记录都没重放到" → `ARIA_WRITE_LOST` + (把"已确认写入是否真的丢了"从不可观测变成可判定) + +### H.3 实施中新发现的缺陷(方案未列出,已一并修复) + +| # | 现象(实测) | 根因 | 修复 | +|---|---|---|---| +| 1 | **删掉的行复活 / 已确认写入丢失**(随机压力套件:76≠68、53≠55 两个方向都出现) | WAL 全量截断后 `currentSegment` 重置为 0,而 manifest 里的 `startSegment` 是删除前算好的(= 旧最大号+1)→ 新记录写进被跳过的分片号;分片号复用还让同一序号对应两代记录,恢复按序号排序时旧 INSERT 排在 DELETE 之后 | 分片号**只增不减**(`truncateBefore`/`truncate` 都只往上走;只有整库清空 `clearAll` 才 `reset()`);正则放宽到 `\d{6,}` | +| 2 | 300 行全部改名后,紧接着的全表扫描有 25 行仍是旧值(几毫秒后又变新值) | 读路径"同步取 meta 快照 → await 加载",而并发 flush 会在这个窗口里把新 SSTable 发布到 `levels[0]` **并同时**从 `frozenMemtables` 摘掉 → 该批数据既不在快照里也不在前台 | `structureVersion` + 乐观重试(加载后版本变了就重来;极端情况退回"等两条链静默") | +| 3 | `close()` 之后 `__wal_000000.bin` 仍在(每次打开都要重读一遍) | `planKeepFrom` 只在"下一条记录也在水位之内"时才敢删边界分片,而**最后一个分片**没有"下一片"可比 → 永不删除 | `truncateBefore` 接受 `latestLsn`:末片以"当前 LSN ≤ 水位"判定整片可删 | +| 4 | 第二个实例打开同一库时 `open()` 直接失败(两个实例互相判 STALE_INSTANCE) | 认领所有权只 +1 个世代号,旧实例**在途**的提交正好落在同一个号上并覆盖认领 | 认领时一次跨过 `MANIFEST_TAKEOVER_STRIDE=1000` 个世代号 | +| 5 | 从上一代 manifest 回退时报 `ARIA_WRITE_LOST`(数据其实完好) | `saveMeta` 正在提交这张冻结表,而 `frozen` 意图仍把它列为"未落盘" | `FrozenTable.committing` 窗口:提交中的表由本次提交负责,不进意图;失败时标志复位、可重试 | +| 6 | 底部层"整层只剩墓碑"时 compaction 抛 `TypeError`(读 `merged[0][0]`)→ 该层永远无法合并、墓碑永远回收不掉 | 空产物没有专门的退休分支 | 新增"全部是墓碑 → 直接退休旧文件"分支(`retireMetas`) | +| 7 | item 55 实际未生效:checkpoint 仍然等整条 compaction | `CheckpointManager.checkpoint()` 里还有一句 `await this.lsm.flush()`(会 drain 维护链) | `Flushable.flushMemtables?()` + 引擎提供 `flushAllLsms(true)`;旧调用方保留原语义 | +| 8 | 一个损坏的更高世代把库**永久锁死**(之后每次提交都 STALE_INSTANCE) | 陈旧检查拿"磁盘上最大世代号"与自己的世代比,损坏世代不是"别人的提交" | 陈旧判定跳过**已知损坏**的世代;但新世代号仍严格大于任何已存在文件(绝不覆盖) | +| 9 | 旧格式迁移遇到坏 JSON 静默当空库 | `catch { return [] }`(meta 与 schema 各一处) | `ARIA_LEGACY_META_CORRUPT` 显式失败(迁移全有或全无) | +| 10 | 介质读故障被折叠成"文件不存在",于是 `dropInvalidSSTable` 把 meta 删掉(不可逆) | `load()` 的 catch 一律 `return null`,调用方无法区分"读失败"与"确实没有" | 读失败**向上抛** `ARIA_SSTABLE_READ_FAILED`;只有"确定不存在且仍被 manifest 引用"才自愈清理,并进恢复报告 | + +### H.4 验收(可复现) + +```bash +# 1) B-6 回归(63 项) +npx jest tests/v080-b6-single-commit-point.test.ts + +# 2) 变异验证:把每个修复回退到修复前行为,对应用例必须失败(22 项) +python3 scripts/mutation-b6.py + +# 3) 随机压力 / 检查点崩溃恢复(发现缺陷 1 的套件) +npx jest tests/engine/aria-repair-hardening.test.ts tests/engine/aria-kv-backend.test.ts + +# 4) 全量常规套件(不含重型与 e2e) +npx jest --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race' +``` + +**实测数字**:常规套件 **1935 通过 / 91 套件**;覆盖率 语句 90.34% / 分支 82.16% / +函数 94.06% / 行 93.23%(阈值 90/82/94/93,全部通过)。 + +**恢复报告**:`engine.getRecoveryReport()` 返回 +`{droppedSSTables, dataLossSuspected, walGaps, legacyImported, manifestFallback}` —— +"损坏被自愈了多少、有没有真正丢数据"从此是**可读的返回值**,而不是只能翻日志。 + +**新增错误码**:`ARIA_MANIFEST_CORRUPT`、`ARIA_MANIFEST_WRITE_FAILED`、 +`ARIA_MANIFEST_NOT_LOADED`、`ARIA_LEGACY_META_CORRUPT`、`ARIA_SSTABLE_READ_FAILED`、 +`ARIA_SSTABLE_SAVE_CONTRACT`、`ARIA_WAL_GAP`、`ARIA_WRITE_LOST`、`STALE_INSTANCE`。 diff --git a/README.md b/README.md index d7faa63..1b35650 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@

version license - coverage - tests + coverage + tests

> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API, @@ -356,6 +356,8 @@ const rows = await db.query('SELECT * FROM users'); │ 快照回滚 │ 二级索引 LSM │ +大小头 │ │ +Savepoint │ (每列独立) │ │ ├────────────────┴───────────────┴────────────┤ +│ __aria_manifest 单一提交点(数据→提交→截断)│ +├──────────────────────────────────────────────┤ │ EncryptedBackend (AES-256-GCM 全库透明加密) │ ├──────────────────────────────────────────────┤ │ 存储后端: OPFS Backend / KVStore Backend │ @@ -369,8 +371,10 @@ const rows = await db.query('SELECT * FROM users'); |------|------| | **LSM-Tree** | MemTable(红黑树)→ 多级 SSTable,异步 Compaction(从存储兜底加载),写背压 | | **页面化存储** | SSTable 存为 4KB 页面(FileManager 分配 pageId + BufferPool LRU 缓存 256 页 ≈ 1MB),`pageStorage` 在 opfs/kv 后端默认启用 | -| **WAL** | 分片文件 `__wal_%06d.bin` + 真追加;标准 CRC32 记录校验;full/batch/none 三模式;空洞检测截断;16MB 阈值自动 checkpoint(活跃事务期间不截断) | -| **崩溃恢复** | 打开时完整性校验(损坏 SSTable 自愈清理 + 整文件 CRC-32)、WAL 恢复、恢复后自动重建二级索引;`repair()` 清理孤儿页面与残留 | +| **WAL** | 分片文件 `__wal_%06d.bin` + 真追加;标准 CRC32 记录校验;full/batch/none 三模式;**LSN 全库单调**(manifest 记高水位);分片号只增不减;空洞(含前缀缺失)显式上报 `ARIA_WAL_GAP`;16MB 阈值自动 checkpoint(活跃事务期间不截断) | +| **单一提交点** | `__aria_manifest_`:页面水位 + 各命名空间 SSTable 元数据 + 表结构 + WAL 起始位置 + 待落盘冻结表意图,一次原子提交(头部/载荷双 CRC,先写后验,保留两代)。顺序固定为**数据落盘 → manifest 提交 → 才允许截断 WAL / 删除旧文件**;恢复只认最后一份 CRC 通过的世代,元数据损坏抛 `ARIA_MANIFEST_CORRUPT`(不再静默当空库) | +| **崩溃恢复** | 打开时完整性校验(整文件 CRC-32;**介质读故障不再被当成"文件不存在"**,抛 `ARIA_SSTABLE_READ_FAILED` 且不误删元数据)、按 LSN 水位重放 WAL、恢复后自动重建二级索引;`getRecoveryReport()` 返回 `{droppedSSTables, dataLossSuspected, walGaps, legacyImported, manifestFallback}`;`repair()` 只在 manifest 健康时回收孤儿页面 | +| **Compaction** | 整层合并不再"先摘层再合并"(合并期间该层对读者始终可见);底部层原地合并**回收墓碑**(删除密集场景空间不再无界增长);按层 `compacting` 集合(跨层触发不丢失);被取代的 SSTable 进入**退休表**,等更早的读者退出后才物理删除 | | **全库加密** | `encryption.password` → EncryptedBackend 透明加解密(WAL/SSTable/Schema/元数据全密文);PBKDF2 派生 + salt 持久化;密码错误/篡改 → `ARIA_DECRYPT_ERROR` | | **MVCC** | 版本链仅作事务内 undo(提交即清理,**无快照隔离**;事务串行);自动 GC | | **二级索引** | 每列独立 LSM Tree,支持等值/范围扫描,跨重启恢复,WAL 恢复后自动重建 | @@ -418,7 +422,8 @@ const { data, loading, error, refresh } = useSqlarkQuery(db, 'SELECT * FROM user npm install # 安装依赖 npm run dev # 开发模式(localhost:3001) npm run build # 生产构建(生成 dist/) -npm test # 运行测试(1872 用例 · 90 套件;+4 个重型套件) +npm test # 运行测试(1935 用例 · 91 套件;+4 个重型套件) +python3 scripts/mutation-b6.py # 变异验证:把 B-6 的修复逐项回退,对应用例必须失败 npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build) npm run lint # 代码检查 npm run typecheck # 类型检查 @@ -430,11 +435,11 @@ npm run typecheck # 类型检查 | 指标 | 数值 | |------|------| -| 测试用例 | 1872(90 套件)+ 14 Playwright e2e,另 4 个重型套件在独立 CI job 串行运行 | -| 语句覆盖率 | 90.43%(7835/8664) | -| 分支覆盖率 | 82.21%(4092/4977) | -| 函数覆盖率 | 94.27%(1103/1170) | -| 行覆盖率 | 93.44%(7103/7601) | +| 测试用例 | 1935(91 套件)+ 14 Playwright e2e,另 4 个重型套件在独立 CI job 串行运行 | +| 语句覆盖率 | 90.34%(8393/9290) | +| 分支覆盖率 | 82.16%(4367/5315) | +| 函数覆盖率 | 94.06%(1205/1281) | +| 行覆盖率 | 93.23%(7607/8159) | | SQL 关键字 | 72 | | 存储模式 | 4(`memory` / `disk` / `hybrid` / `aria`) | | 存储后端 | 3(OPFS / KVStore / Memory),Aria 引擎另有 LSM-Tree + WAL + 页面化 | @@ -455,6 +460,11 @@ npm run typecheck # 类型检查 ### 已知限制(v0.8.0) +- **存储布局在 v0.8.0 变更** — 元数据从"每个命名空间一份裸 JSON"(`__aria_lsm_meta*` / + `__aria_schemas`)收敛为 `__aria_manifest_`(带世代号与双 CRC)。 + 旧库**首次用 v0.8.0 打开时自动迁移**(旧键保留不删,可回退旧版本),迁移遇到损坏 + 的旧元数据会明确报 `ARIA_LEGACY_META_CORRUPT` 而不是当成空库。直接读取这些内部 + key 的外部脚本需要跟着改(引擎侧无公开 API 依赖它们)。 - **单列主键** — 复合主键暂不支持(建表时显式 `SCHEMA_ERROR`),列入 v0.8 路线图 - **写语句关联引用** — UPDATE/DELETE 的 WHERE 支持非关联子查询(`IN (SELECT)` / 标量子查询),关联引用(`$col` / 关联 EXISTS)显式抛 `NOT_SUPPORTED`(不静默) - **`backup()` 不是跨表一致性快照** — 实现为**逐表读取**(Aria 走引擎级 `backup()`, diff --git a/site/docs.html b/site/docs.html index f081a62..3c94eff 100644 --- a/site/docs.html +++ b/site/docs.html @@ -920,6 +920,13 @@ db.broadcastChange('users'); QUERY_ERROR未知 where 操作符(如拼错的 $betwen)🆕 v0.7.2 COLUMN_NOT_FOUND列不存在:UPDATE 未知列 / ALTER DROP 不存在列(v0.7.4 UPDATE 未知列显式报错) INDEX_NOT_FOUNDDROP 不存在的索引 + ARIA_MANIFEST_CORRUPT存储元数据(manifest)全部世代校验失败:拒绝打开,而不是当成空库 🆕 v0.8.0 + ARIA_MANIFEST_WRITE_FAILEDmanifest 提交后回读校验失败(提交未生效,内存态不前进)🆕 v0.8.0 + ARIA_LEGACY_META_CORRUPT旧格式(v0.8.0 之前)元数据损坏,无法安全迁移 🆕 v0.8.0 + ARIA_SSTABLE_READ_FAILED介质读故障(区别于"文件不存在":不删元数据、不回退成静默空结果)🆕 v0.8.0 + ARIA_WAL_GAPWAL 分片空洞(含前缀缺失):拒绝在"少了一段日志"的情况下静默继续 🆕 v0.8.0 + ARIA_WRITE_LOSTmanifest 声称有未落盘数据,但 WAL 中没有任何可重放的记录(已确认写入确实丢失)🆕 v0.8.0 + STALE_INSTANCE陈旧实例拒绝提交(另一个实例已提交更新的世代),不会静默覆盖 🆕 v0.8.0 diff --git a/site/index.html b/site/index.html index 5da9f70..bb257b8 100644 --- a/site/index.html +++ b/site/index.html @@ -153,7 +153,7 @@
-
v0.8.0 根治性迭代 — 1872 测试 90 套件 · 语句/分支/函数/行覆盖率 90.43% / 82.21% / 94.27% / 93.44% · UPDATE/DELETE 子查询正确执行 · 主键非空强制 · DROP INDEX 保留 UNIQUE 约束 · findStream 真惰性 · 参数化查询 · 自研 KVStore 事务引擎 · AriaEngine LSM+WAL+MVCC · 崩溃恢复
+
v0.8.0 根治性迭代 — 1935 测试 91 套件 · 语句/分支/函数/行覆盖率 90.34% / 82.16% / 94.06% / 93.23% · UPDATE/DELETE 子查询正确执行 · 主键非空强制 · DROP INDEX 保留 UNIQUE 约束 · findStream 真惰性 · 参数化查询 · 自研 KVStore 事务引擎 · AriaEngine LSM+WAL+MVCC · 崩溃恢复

前端的 SQL 数据库

TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。
零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。

@@ -244,7 +244,7 @@ npm install @metona-team/metona-sqlark
🌲

AriaEngine v0.8.0

-

自研 LSM-Tree 存储引擎:SSTable 4KB 页面化物理存储(BufferPool LRU)、WAL 分片文件原子写入、全库 AES-GCM 透明加密、标准 CRC-32 完整性校验、MVCC 快照隔离、二级索引跨重启恢复、ON UPDATE/DELETE 外键级联、崩溃恢复自愈。

+

自研 LSM-Tree 存储引擎:SSTable 4KB 页面化物理存储(BufferPool LRU)、WAL 分片文件原子写入、__aria_manifest 单一提交点(数据落盘 → 元数据原子提交 → 才截断 WAL)、全库 AES-GCM 透明加密、标准 CRC-32 完整性校验、MVCC 快照回滚、二级索引跨重启恢复、ON UPDATE/DELETE 外键级联、崩溃恢复自愈。

🔒
@@ -264,7 +264,7 @@ npm install @metona-team/metona-sqlark
🛡

崩溃恢复自愈

-

异常退出后无需删库重建:整文件 CRC-32 校验 + 打开自动跳过损坏 SSTable,db.repair() 清理损坏数据/孤儿页面/残留文件并重建索引,db.clearAll() 重置。迁移版本持久化,重启不重跑。

+

异常退出后无需删库重建:整文件 CRC-32 校验 + 打开自动跳过损坏 SSTable,并按 WAL 水位重放已提交事务;getRecoveryReport() 如实给出被丢弃的 SSTable、WAL 空洞与是否怀疑丢数据。介质读故障与"文件不存在"分开处理(前者抛 ARIA_SSTABLE_READ_FAILED,绝不误删元数据);元数据整体损坏时显式拒绝打开(ARIA_MANIFEST_CORRUPT),而不是当成空库。db.repair() 只在 manifest 健康时回收孤儿页面并重建索引。

🏊