Files
MetonaSqlark/CHANGELOG.md
T
thzxx 0b44620721 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 已重建。
2026-09-15 16:33:40 +08:00

1374 lines
106 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to MetonaSqlark will be documented in this file.
## [0.8.0] - 2026-09-15
### 根治性迭代 —— 统一语义 / 消灭复发结构 / 验证基础设施
> 依据 `PLAN-v0.7.5.md` 的三条并行工作流(A 缺陷修复 / B 结构根治 / C 验证基础设施)
> 完成的一次系统性迭代。**这一版的重点不是"再修一批 bug",而是砍掉让同类 bug
> 必然复发的结构**:多处并存的语义实现被收敛为唯一实现,并第一次让崩溃语义、
> 错误码一致性、入口等价性变成可机器验证的门禁。
>
> 测试规模 1304 → **198092 套件)+ 14 项 e2e**(另 4 个重型套件在独立 CI
> job 串行运行);B-6 与审查轮的全部修复另有 **40 项变异验证**`scripts/mutation-b6.py`
> 全部通过,且脚本自带正控 / 编译失败区分 / 超时 / 逐字节恢复校验 / 进程锁)。
### 工作流 B · 结构根治(消除整类缺陷)
- **B-1 唯一校验 choke point** — 此前有**三份**行校验实现,覆盖面各不相同
memory 一份缺 `maxLength`/`min`/`max`Aria 一份有,schema.ts 第三份):
同一份 schema、同一条 INSERT 是否报错取决于选了哪个引擎(A12);四份实现
对未知列一律静默丢弃(A17:INSERT 报成功、`SELECT nope` 报 COLUMN_NOT_FOUND)。
现收敛为 `src/table/validation.ts#compileValidator` 唯一实现,四个引擎新增
`validatePayload` 契约,校验先于任何副作用;未知列、`NaN`/±Infinity
(JSON 无法表示,落盘会变 null)显式拒绝。
- **B-2 唯一值比较与编码** — `sqlCompare`/`sqlCompareOrder`/`encodeValueKey`
成为唯一原语;GROUP BY 键、DISTINCT 键、UNION 去重、聚合去重全部改用它
(此前四份编码并存,对 null/undefined 处理各不相同)。
- **B-3 单管线** — QueryBuilder 此前**自己执行**:无 JOIN 时直通 `engine.find`
写操作直通 `engine.update/delete`,于是"同一条语义"在 TABLE API 与 SQL API
两条路径上规则各写一份(投影、列校验、LIMIT 下推、`maxRowsPerQuery` 全缺失;
`$subquery` 无人解析 → 静默影响 0 行)。现在 builder 只产出 AST,执行一律经
Executor;生命周期钩子由 `Table` 注入、顺序与传参不变。
→ 新增 `tests/v080-single-pipeline.test.ts`(两入口逐值等价,四引擎)。
- **B-4 统一表达式求值** — CASE 此前用**正则**切分 WHEN/THEN/ELSE,不认字符串
字面量与嵌套:嵌套 CASE 返回字符串残片 `"big' END ELSE 'small"`;条件引用
不存在的列时静默把整列变成 ELSE 值;`GROUP BY CASE ... END` 完全不可用
(报"未知列 CASE WHEN ...")。现复用 `sql/lexer` 的 token 流做递归下降,
条件交给与 WHERE 相同的解析器,无法识别的表达式显式报错。
→ 顺带修正 `Token.position` 语义按类型不一致的缺陷(字符串 token 指向引号之内,
导致按位置切片少一个字符)。
- **B-5 输出列序号 + 分隔标识符** — `ORDER BY 1` / `GROUP BY 2` 此前直接
`PARSE_ERROR``SELECT "1"`(列名就叫 1)被当成**常量 1**(与 `SELECT *` 结论相反)。
现支持输出列序号(越界、`ORDER BY 0``GROUP BY <聚合列>` 各自显式报错),
并统一分隔标识符语义:引号只在"解析→执行"边界脱去。
顺带补上 **ORDER BY 的列存在性/歧义校验**(此前 JOIN 里裸写两表同名列既不报错
也不确定按哪列排)。
- **B-6 存储提交点(完整实施,非降级方案)** — 见 `PLAN-v0.7.5.md` 附录 H。
- **`__aria_manifest_<generation>` 单一提交点**:页面水位 + 各命名空间 SSTable
元数据 + 表结构 + WAL 起始位置 + 待落盘冻结表意图,一次原子提交
(头部/载荷双 CRC、先写后验、保留两代)。顺序固定为
**数据落盘 → manifest 提交 → 才允许截断 WAL / 删除旧文件**
- **元数据损坏不再静默空库**:此前 `__aria_lsm_meta*` 是裸 JSON,解析失败即
`[]` → 看不到任何表,随后 `repair()` 还会把"没人引用"的活页删光(不可逆)。
现在全部世代校验失败抛 `ARIA_MANIFEST_CORRUPT`,旧格式迁移遇到坏 JSON 抛
`ARIA_LEGACY_META_CORRUPT`。表结构记录的**形状**坏掉(数组 / null / 表名映射到
非对象 / 列定义不是对象)此前被静默忽略(打开后"看不到任何表")—— 现在与坏 JSON
同一条路径显式报错(v0.8.0 审查修复)。
- **WAL**LSN 改为一库一条单调水位(manifest 记账);按水位删除旧分片;
**分片号绝不回退、也绝不低于 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
不再"先摘整层再合并"(窗口内该层对读者不可见 → 少行);底部层原地合并
**回收墓碑**`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`(审查轮后扩到 **96 项**
manifest 严格校验表驱动 24 例 + 事务水位 P0 + 前缀空洞语义 + 在途读者与退休文件 +
孤儿回收门槛 + 稀疏/损坏分支 + 旧格式形状校验 + bloom 配置透传)与
`scripts/mutation-b6.py`(**40 项**变异验证:把修复回退到修复前行为,对应用例
必须失败 —— 全部被拦住)。
- **B-6 存储提交点(KVStore 侧止血)** — 两处 P0:① `open()` 遇损坏日志尾部会**清空整个
日志**(写 3 条 → 第 4 条撕裂 → 重开可见 → 再重开全空);② 自动 checkpoint
失败会让**已确认写入**报错(而该写入已在 WAL 中,报错与事实相反)。另修陈旧实例
的 checkpoint 会**静默抹掉**新实例写入(现抛 `STALE_INSTANCE` 拒绝提交)。
### 工作流 A · 缺陷修复(24 项,含 6 项事故级)
- **A15 三值逻辑** — `= NULL` 命中 NULL 行、`!= NULL` 返回所有非 NULL 行、
`NOT LIKE` 把 NULL 判真、**`NOT BETWEEN 1 AND 2` 恒空集**(字段级 `$or` 递归进了
where 子句级求值器)。现 WHERE 只有**一个**递归求值器;`IS NULL`/`IS NOT NULL`
是与比较不同的**谓词**(此前与 `= NULL` 共用同一 AST,语义无法区分)。
- **A9/A10 发布订阅与关联子查询** — `subscribe()` 对本地写入永不触发(仅跨标签页
广播);`WHERE t.x = t.y``WHERE id IN (SELECT ... WHERE o.user_id = u.id)`
静默空结果(引擎层预过滤把逐行谓词判 UNKNOWN → 候选行 0)。
- **A13 自引用外键** — `parent_id REFERENCES node(id)` 的级联被整体跳过:
`DELETE root` 只删根,子树**永久悬挂**(父行已不在,再也无法级联清理)。
`ON DELETE SET NULL` / `ON UPDATE CASCADE` / RESTRICT 预检同样失效。
- **A22/A23/A25/A26/A27/A29/A30/A36 查询层** — GROUP BY 别名、HAVING 未选中聚合、
带前缀聚合参数恒 0、UNION 尾部子句归属、DISTINCT 作用于输出列、
`maxRowsPerQuery` 静默截断写入、INSERT 值多于列、派生表别名引用。
- **A37 列引用** — 未限定列不能作比较操作数(`WHERE x = y` 报 PARSE_ERROR);
`$col` 引用不存在的列**静默返回空集**。
- **A38/A39 Aria 存储** — `compression` 在页面化路径(默认)被静默忽略;
`compressLZ4` 匹配搜索 O(n²)60KB 伪随机 2345ms → 6ms**390×**)。
- **A41 DDL 原子性** — DDL 的 WAL 意图记录写在生效**之后**:`dropTable` 后崩溃 →
重开表又回来了(DROP 被静默撤销);`alterTable` 完全不写 WAL,崩溃丢失结构变更。
现统一为"先写 WAL 意图并刷盘 → 再改内存 → 最后落盘 schema"。
### 工作流 C · 验证基础设施(让门禁真的能拦)
- **真崩溃注入(PC-2** — e2e 的 `crashPage()` 此前只是 `page.close()`
(**优雅关闭**),所有"崩溃恢复"用例测的其实是"正常关闭后重开"。现改用 CDP
`Page.crash` 终止渲染进程,并新增两个真实窗口:`createWritable().write()` 中途、
`close()` 原子替换前(copy-on-write 的核心不变量)。
- **覆盖率门禁真正生效** — `collectCoverageFrom` 不再排除实现文件
(此前 `!src/**/index.ts` 把 2282 行的 AriaEngine 整文件排除在统计外,
于是"90.1% 行覆盖率"是虚高的口径);新增 `coverageThreshold`
statements 90 / branches 82 / functions 94 / lines 93);CI 常规 job 带
`--coverage`lint 去掉 `continue-on-error`;新增 `tests/` 类型检查
(修复 **103 个**被 babel 剥离类型掩盖的测试类型错误);CI 校验 dist 与源码同步。
→ 实测(v0.8.0 收尾复测)Statements 90.59% / Branches 82.59% / Functions 94.14% /
Lines 93.50%(命令与 CI 常规 job 完全一致,可复现)。
- **测试介质忠实性修正**(两处同源缺陷,此前让所有多实例/多库验证跑在错误语义上)
- `SharedMemoryBackend` 的读缓存是每实例私有的 → 介质退化为"每实例一份快照",
跨实例写入不可见(这正是陈旧实例覆盖新实例写入那条 bug 起初查不出来的原因);
- OPFS mock 把 `getDirectoryHandle(name)` 的库名**丢弃** → 所有库共用一棵文件树
`open('db-beta')` 能看到 `db-alpha` 的表)。
- **变异验证成为回归套件的标准做法** — 把修复回退到修复前的行为,对应用例必须
失败。本版 A15 / B-6(①②③) / A13 / A37 / A38 / A39 / A41 / B-4 / B-5 全部通过该检查
——这是"测试真能拦住回归"与"测试只是陪跑"的分界线。
### 文档与宣称同步(G6
- 修正 README 全部**不成立的能力宣称**:覆盖率数字改为**四个准确数字 + 明确口径**;
测试规模与套件数更新为实测值;"5 种存储引擎"改为"4 种模式 + 3 种后端"
`backup()` 由"在线一致性快照"改为"全库导出(逐表读取)"并写入已知限制
(引擎层没有跨表快照原语);`db.disconnect()` 真正进入类型系统
(此前仅运行时注入,TypeScript 使用者编译失败)。
- **打包缺陷修复**`package.json``./migration` 子路径此前指向的产物里
**没有** `migrateFromIndexedDB`(主入口未导出该函数)→ 现主入口导出 + 子路径可用;
`./react` / `./vue` 此前指向**裸 TS 源码**且声明类型为 `any`
→ 现构建 `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
### 写语句子查询 / 约束硬化 / 真惰性流式
> 深度审计第七阶段:修复 UPDATE/DELETE WHERE 子查询静默 0 行(P1,四引擎)、
> 主键 NULL 静默入库(P1)、DROP INDEX 解除建表 UNIQUE 约束(P2)等 8 项正确性问题;
> Aria findStream 迭代器化落地"真惰性"宣称;REINDEX 单次扫描性能优化。
### Fixed
- **UPDATE/DELETE WHERE 子查询静默 0 行(P1,四引擎)** — `executeUpdate/executeDelete`
直接 compileStatement 调引擎,`$subquery` 未解析:引擎层 matchWhere 的 `$in/$nin`
遇对象恒 false → 所有行不匹配,写语句静默影响 0 行(与 v0.7.3 修的 queryStream
同类)。executor 写路径先 `resolveWriteWhere` 解析非关联子查询(IN 列表/标量),
EXPLAIN 的 estimatedRows 同步修复;关联引用($col / 关联 EXISTS)显式
`NOT_SUPPORTED`(写语句无法逐行绑定外层上下文);QueryBuilder 直通引擎路径
由引擎层 `containsUnresolvedSubqueries` 防御(Memory/Aria 双引擎)
- **主键 NULL/undefined 静默入库(P1,四引擎)** — 主键列缺失/置 null 时
`String(undefined)``"undefined"``String(null)``"null"` 作为主键落库。
validateRow 层强制主键非空(SQL 语义 PK 隐含 NOT NULL)抛 `VALIDATION_ERROR`
default 值生效时仍可省略主键(语义不变)
- **DROP INDEX 解除建表 UNIQUE 约束(P2** — `dropIndex` 此前 `colDef.unique = false`
静默解除建表约束(aria 侧 persistSchemas 后重启约束永久消失)。现在建表 UNIQUE
列 DROP INDEX 抛 `NOT_SUPPORTED`(对齐 SQLite:需重建表解除);仅
`CREATE UNIQUE INDEX` 添加的约束可随索引删除(引擎内 `uniqueIndexCols` 跟踪来源,
重启后 schema 中的 unique 一律按建表约束保护)
- **GROUP BY 把 null 与字符串 'null' 合并(P2** — 分组键 `String(row[col] ?? 'null')`
使两类值合并为一组;DISTINCT/UNION 去重键 `String(v ?? '\0')` 同类吞并。
统一类型安全编码(类型前缀),仅同类型同值合并(与 where-matcher 的 === 语义一致)
- **UPDATE 未知列静默写入存储行(P2)** — `SET nonexistent = ...` 的脏列残留在行内
并随持久化落盘。预检阶段显式 `COLUMN_NOT_FOUND`SQL + Table API 双路径,
Memory/Aria 双引擎)
- **queryStream 多语句语义不一致(P2** — `parseAll(sql)[0]` 静默忽略后续语句:
可流式时不执行(如 DELETE),不可流式时回退 query() 却执行全部语句。
多语句显式 `PARSE_ERROR`(流式 API 要求单条 SELECT
- **KVStore lastBackgroundError 跨生命周期残留(P3** — close/open 未清理,
重开后首次 checkpoint 抛出上一次生命周期的旧错误。open/close 统一清零
- **Hybrid beginTransaction 部分成功泄漏(P3** — 内存 begin 成功 + 磁盘 begin
抛错 → 内存快照泄漏(后续事务永久 TX_ACTIVE)。磁盘失败补偿回滚内存
- **MemTable RB-Tree 删除双黑修复边界(P3)** — 两子节点删除时 successor 右子
为 null 的占位 parent 传 null → fixDelete 直接跳过修复(树失衡)。
修复 parent 传递(直接右子传 successor,间接右子传 successor 原父)
- **死代码清理** — 移除 LSM.getAllEntries(无调用者;其 frozen 遍历顺序与 `get()`
相反属死代码路径)
### Changed
- **Aria findStream 真惰性(宣称落地)** — 此前 `rangeScanLazy` 内部
`mergeIter.drain()` 全量物化,与"流式不物化"宣称不符。重写为真惰性:
MemTable 红黑树显式栈生成器 + SSTableReader 块级生成器 + GeneratorEntrySource
接入 MergeIterator 逐条拉取;callback 返回 false 提前终止(limit 达成时
未消费块/子树不再解析),大表流式内存 O(1)
- **REINDEX 单次全表扫描(性能)** — `reindexTableInternal` 此前每个索引列一次
getAllRowsN 列 × M 行 + 每列一次 drainChain),改为单次扫描重建全部索引列
- VERSION 0.7.3 → **0.7.4**
### Changed(测试)
- 测试 1256 → **1304**76 套件,+48 个 v0.7.4 回归);新增 `tests/v074-fixes.test.ts`
(写语句子查询 ×10 / 关联引用 ×3 / EXPLAIN ×1 / 主键非空 ×8 / DROP INDEX unique ×5 /
分组键编码 ×4 / 未知列 ×4 / queryStream ×2 / KVStore 生命周期 ×1 / Hybrid 补偿 ×1 /
真惰性流式 ×2 / REINDEX ×2);VERSION 断言同步
- 行覆盖率 90.0% → **90.1%**
---
## [0.7.3] - 2026-08-14
### INSERT 语句级原子性补全 / 索引一致性 / 流式查询回退安全
> 深度审计第六阶段:修复 INSERT 语句级部分提交(三引擎 + Aria PK 批内重复)、
> 索引列 IS NULL 恒空、delete RESTRICT 预检破坏索引、queryStream 子查询静默空结果
> 等 10 项问题,四引擎语义对齐。
### Fixed
- **INSERT 语句级部分提交(P1Memory/KVStore/Hybrid** — 第 N 行主键重复/唯一冲突
抛错时前 N-1 行已提交(v0.7.2 只修了 UPDATEINSERT 漏修)。改为两阶段:
先全量预检(主键批内 Set 互查 + 索引查 + 唯一批内互查),任何一行失败整句不执行
- **Aria insert 批内主键重复部分提交(P1)** — PK 重复检查在写入循环内:第 N 行
重复抛错时前 N-1 行已 put LSM 且其 WAL 记录随 appendBatch 一起丢失 → 部分提交 +
内存/WAL 不一致。PK 检查移入批预检阶段(与 v0.6.2 的 unique 预检同一阶段)
- **索引列 IS NULL 恒空(P1Memory/KVStore/Hybrid** — `tryIndexLookup`
`colIndex.get(null)` 恒 undefined → `return []` 短路全表扫描(AriaEngine v0.6.2
已修,Memory 漏修)。null/undefined 条件跳过索引路径回退全表扫描
- **delete RESTRICT 预检破坏索引(P1Memory/KVStore/Hybrid** — `removeIndexEntries`
在收集阶段(RESTRICT 预检前)执行:预检抛错时行未删但索引条目已删 → 唯一约束
永久失效 + 索引查询丢行。索引清理移到预检通过之后(与 AriaEngine 对齐)
- **queryStream 子查询静默空结果(P1** — WHERE 含 `$subquery`/`$exists`/`$col`
未回退物化:引擎层 matchWhere 的 `$in` 遇未解析的 `$subquery` 对象返回 false →
所有行被静默过滤(`$col` 会抛 QUERY_ERROR)。streamable 判定增加递归检测,
回退物化路径(resolveSubqueries 正确解析)
- **ALTER DROP 索引列残留(P2Memory/KVStore** — 删列只删 schema.columns
indexes Map 中该列条目残留 → 查询已删列走旧索引(不含新行)→ 结果不完整。
DROP 时同步清理索引 Map(对齐 AriaEngine cleanupTableIndexes
- **CREATE UNIQUE INDEX 存量重复数据静默成功(P2)** — 回填不校验存量唯一性
SQLite 语义应报错)。Memory/Aria 回填时检查重复 → 抛 `UNIQUE_VIOLATION`
失败路径清理半初始化索引(标志未落、索引 Map/LSM 移除),保持原子语义
- **`SELECT *, col AS alias` 解析与投影(P3** — 此前 parser 的 '*' 独占分支使
`SELECT *, name AS nick` 直接 PARSE_ERRORexecutor 侧 columns[0]==='*' 不投影。
parser 支持 '*' 后接列列表;projectRow 以原行全部列为基、其余表达式覆盖/追加
- **INSERT hooks 行键错位(P3** — SQL 省略列名时 beforeInsert/afterInsert 收到
数字键行(与 executor 写入的 schema 列名行不一致)。hooks 行映射对齐 executor
(省略列名时按 schema 列顺序)
- **KVStoreEngine insert 持久化原始行(P3)** — 非事务路径 `JSON.stringify(row)`
写入参原始对象:default 值不落盘、schema 外列被持久化。改为持久化内存中
validated 行(MemoryEngine 新增 `getRow()`O(rows) 无性能回退)
### Changed
- 测试 1198 → **1256**75 套件,+58 个 v0.7.3 回归);新增 `tests/v073-fixes.test.ts`
IS NULL ×6 / RESTRICT 索引 ×3 / INSERT 原子 ×11 / queryStream ×4 / ALTER DROP ×2 /
UNIQUE INDEX ×4 / SELECT * ×3 / hooks ×2 / KVStore validated ×3 / getRow ×1 /
WAL 失败窗口 ×2 / aria $in ×2 / $and 下推 ×4 / 常量转义 ×3 / ANALYZE ×2
+ React config 重建 ×1 + Vue 卸载 ×2 + 迁移无主键 ×2
- 行覆盖率 89.8% → **90.0%**
### Fixed(审计第二阶段:边界窗口 / 性能 / 生态收尾)
- **WAL BEGIN 写失败事务泄漏(P3** — full 模式 BEGIN 记录写失败时 currentTxnId 已设置
→ TX_ACTIVE 永久泄漏(后续无法开始新事务)。失败回滚 mvcc 登记与快照后重抛,
调用方可重试
- **WAL ROLLBACK 顺序(P3** — 内存先回滚、ROLLBACK 记录后写:full 模式写失败时
崩溃重放无 ROLLBACK 记录 → 已回滚事务的数据复活。改为先持久化 ROLLBACK 再回滚内存
(与 commitTransaction 的"WAL 领先内存"对齐):写失败 → 事务仍活跃可重试,
崩溃重放看到 ROLLBACK 记录不复活数据
- **aria $in 逐值 drainChain 性能悬崖(P2** — 索引 `$in` 逐值 indexScanToRows
每个值一次 prefetchRange + prefetchKeys(各一次 drainChain 排空后台链),
compaction 长耗时时 N 倍放大(与 v0.6.1 修的 insert 批量预加载同类)。
批级预加载全部值索引范围 + 主表行各一次,循环内同步 rangeScan/get
- **update 主键变更级联重复全表扫描(P2 性能)** — applyUpdateCascade /
applyForeignKeyUpdateRules 的阶段 1 RESTRICT 扫描与两阶段预检
checkUpdateRestrict / checkForeignKeyUpdateRestrict)完全重复,删除冗余扫描
- **多条件 AND 永不走索引(P2)** — `WHERE a AND b` 解析为顶层 $andMemory/Aria
的 tryIndexLookup 只查顶层键 → 永远全表扫描,索引形同虚设。递归展开 $and 等值
条件下推($or/$not 保守跳过,命中后全条件 matchWhere 过滤,子集语义安全);
EXPLAIN usingIndex 同步递归识别 $and 嵌套(与引擎行为对齐)
- **ANALYZE 统计不含二级索引(P3** — indexDepth/sstableCount/memtableSize 只统计
主 LSM,多索引表严重低估。汇总该表全部二级索引 LSM
- **Vue useSqlarkDatabase 卸载不 closeP3)** — 组件卸载后实例永不关闭(连接/锁/
后端句柄泄漏)。onUnmounted 时 close(失败不阻塞卸载)
- **React useDatabase config 变更不生效(P3** — initRef 只建一次,配置更新永不
重建且旧实例残留。以 config 序列化指纹为依赖:变更时 cleanup 关闭旧实例再重建
close 幂等,未完成 init 亦可安全关闭)
- **migrateFromIndexedDB 无 id 列旧库迁移中断(P3)** — 推断 schema 无主键 →
createSchema 抛 SCHEMA_ERROR 中断整个迁移。第一个非 json 列兜底为主键;
全 json 列无可用主键 → 跳过该表(skippedTables)不中断
- **SELECT 常量列 SQL 标准 '' 转义未还原(P3** — `SELECT 'O''Brien'` 输出
`O''Brien`projectRow 只处理反斜杠转义)。`''` 还原为 `'`(常量列与别名常量两处)
- **死代码清理** — 移除 AriaEngine.getWALEstimatedSizeCheckpointManager 自带
真实缓冲字节估算,此方法无调用者)
---
## [0.7.2] - 2026-08-13
### 语句级原子性 / 事务 DDL 语义统一 / 约束与绑定硬化
> 深度审计第五阶段:修复 UPDATE 语句级部分提交、事务内 DDL 回滚残留、
> SET NULL 级联绕过 required 约束、参数绑定注释误判等 6 项问题,
> Hybrid 写穿透失败补偿,四引擎语义对齐。
### Fixed
- **UPDATE 语句级部分提交(P1Memory/Aria/KVStore/Hybrid** — 单条 UPDATE 匹配
多行时,第 N 行唯一约束/校验失败抛错,前 N-1 行已写入(aria 场景其 WAL 记录
随 appendBatch 一起丢失,内存与 WAL 进一步不一致)。改为两阶段:先全量预检
validateRow + 批内唯一互查 + 索引唯一查 + 主键冲突 + 外键 RESTRICT/SET NULL
预检),任何一行失败整句不执行,后统一执行
- **批内唯一互查缺失** — 两行在同一语句中更新到同一新唯一值:索引尚未反映本语句
变更,逐行检查相互看不见 → 绕过唯一约束。两阶段预检增加批内 Set 互查
- **事务内 DDL 回滚残留(P1)** — `BEGIN; ALTER ADD COLUMN; ROLLBACK` 后新列残留
Memory/KVStore 事务快照对 schema 是浅拷贝,alterTable 直改共享 columns 对象)。
Memory/KVStore 的 ALTER TABLE / CREATE INDEX / DROP INDEX 在事务内显式拒绝
`NOT_SUPPORTED`,与 AriaEngine 对齐);createTable/dropTable 保持可回滚
- **SET NULL 级联绕过 required 约束(P1)** — 外键列 `required: true` 时 delete 级联
与 update 主键变更级联静默写入 null(不经过 validateRow)。预检阶段整体拒绝
`FOREIGN_KEY_VIOLATION`),Memory/Aria 双引擎对齐
- **bindParameters 注释误判(P2** — `-- comment ?` 中的 `?` 计入占位符(参数错位
PARAM_ERROR);注释中的单引号触发 "Unterminated string literal"。词法扫描感知
行注释与块注释(其中 `?`/引号不参与绑定与字符串状态机)
- **未闭合字符串静默接受** — `SELECT 'abc` 此前解析成功(错误结果),lexer 现抛
`PARSE_ERROR`
- **未知 where 操作符静默全匹配(P3)** — `$betwen` 等未实现/拼错操作符此前
`default: return true`(所有行匹配、过滤形同虚设)。现抛 `QUERY_ERROR`
- **UPDATE undefined 覆盖列值** — `update({ col: undefined })` 此前把 undefined 写入
行(列键丢失)。undefined 现语义化为"不更新该列"(保留旧值),null 仍显式置空
(四引擎统一;KVStore 主键 undefined 不再误触主键变更路径)
- **Hybrid write-through 非原子(P1)** — 磁盘写失败时内存已写入(重启丢数据且错误
已抛)。insert/update/delete/createTable/dropTable/alterTable/clear/createIndex/
dropIndex 磁盘失败后自动从磁盘重载内存(内存=磁盘对齐),再抛原始错误;
事务路径由双引擎快照回滚保证不受影响
### Changed
- 测试 1155 → **1198**74 套件,+43 个 v0.7.2 回归);新增 `tests/v072-fixes.test.ts`
(语句原子性 ×6 / 批内互查 ×3 / 事务 DDL ×6 / SET NULL 约束 ×5 / 注释感知 ×6 /
未闭合字符串 ×2 / 未知操作符 ×2 / undefined 语义 ×9 / Hybrid 补偿 ×4
- `CONTRIBUTING.md` 项目结构同步(移除已删除的 indexeddb/opfs 引擎与 utils.ts
---
## [0.7.1] - 2026-08-13
### API 修复 / 防御统一 / 工程质量
> 深度审计第四阶段:修复 README 示例 API 缺失、未 open 防护不一致、
> 事务错误掩盖、原型污染防护,lint 清零。
### Fixed
- **`MetonaSqlark.create` 静态工厂缺失(API/文档不一致)** — create 此前仅存在于
api 对象与 window 挂载,README/CONTRIBUTING/site 全部示例的
`MetonaSqlark.create({...})` 在 ESM/Node 下是 undefinedTypeError)。
类增加静态 create(与 connect/disconnect 同入口风格),独立 create 函数委托
- **close 未初始化崩溃** — `new MetonaSqlark(cfg).close()`init 失败/未调用)时
`this.engine` undefined → TypeError。close 增加防御
- **AriaEngine 未 open 防护不一致** — hasTable/getTableNames/getTableSchema 未检查
DB_NOT_OPEN(返回空而非报错,与 KVStoreEngine 及自身其余方法不一致)。
统一 ensureOpen
- **事务回滚失败掩盖原始错误** — rollbackTransaction 抛错会替换掉真正导致事务
失败的异常(定位困难)。回滚失败吞掉,保留原始错误
- **`__proto__` 列名原型污染防护** — SQL CREATE TABLE 的列名 `__proto__` 在普通
对象上触发原型 setter 静默丢列。Executor 列映射改用 Object.create(null)
(键可见),schema 校验层显式拒绝该列名(SCHEMA_ERROR
- **lint 清零** — 移除 5 处未使用导入(backend/file_manager/kvstore_engine/parser
### Changed
- 测试 1147 → **1155**73 套件,+8 个 v0.7.1 回归);新增 `tests/v071-hardening.test.ts`
行覆盖率 89.8%lint 0 error 0 warning
---
## [0.7.0] - 2026-08-13
### 参数化查询 / 事务性能 / 语义硬化
> 深度审计第三阶段:能力补全(参数化查询)、大库事务性能、
> 复合主键语义陷阱显式化、EXPLAIN 真实索引信息。
### Added
- **参数化查询** — `db.query(sql, params)` 位置参数(`?`):
- 绑定在词法层完成(仅替换字符串字面量之外的 `?``''` 转义与原样保留)
- 值按 SQL 字面量安全编码(字符串 `''` 转义 / 数字 / 布尔 / NULL),
注入防护从根上成立(`'x'; DROP TABLE users; --` 作为字面量原样入库)
- 参数数量不匹配 → `PARAM_ERROR`;对象/数组参数显式拒绝(SQL 方言无 json
字面量,防静默类型错配);NaN/Infinity → NULL
- **EXPLAIN 真实索引信息** — `usingIndex` 从 'auto' 占位升级为真实命中判定:
主键条件 → `pk`、索引/唯一列条件 → `index:col`、无索引 → `none`
(引擎无关启发式:按 schema 标记判定,与各引擎 tryIndexLookup 路径一致)
### Changed
- **KVStoreEngine 事务增量 flush(性能)** — commit 时此前对每个 dirty 表整表
diff(大表事务改 1 行也 O(表大小))。现在行级变更追踪:普通 insert/update/delete
仅写事务内改动行(单次全表扫描 + 变更集合过滤);主键变更/级联影响表整表 diff
兜底;事务内 clear/drop 只删 KV 行;clear 后写入自动切换兜底。实测:1000 行表
事务改 1 行 commit 日志仅 1 条记录 1 个条目(此前 1000 条目)
- **移除每次 commit 的强制全量 checkpoint** — KVStore 按日志阈值自动 checkpoint
(日志重放保证崩溃恢复),close() 时统一 checkpoint 截断(重开更快);
大库高频事务不再 O(库大小)
- **复合主键显式拒绝(语义陷阱)** — 此前 schema 允许多个 primaryKey 但引擎
静默取第一个(其余标记失效)。createSchema 校验期抛 `SCHEMA_ERROR`
(含明确指引);ALTER TABLE ADD 主键列同样防护(防绕过建表校验)。
复合主键列入 v0.8 路线图
### Fixed
- 事务级联影响表漏写 — delete/update 的级联影响表加入 txFullTables 但未加入
txDirtyTables → commit 循环不处理 → 级联改动(SET NULL 等)不落盘
### Changed(测试)
- 测试 1126 → **1147**72 套件,+21 个 v0.7.0 回归);新增 `tests/v070-features.test.ts`
(复合主键拒绝 ×3 / 参数化 ×7 / 事务增量 flush ×8 / EXPLAIN ×2
---
## [0.6.3] - 2026-08-13
### 原子性 / 一致性 / 资源治理
> 深度审计第二阶段:兑现"单记录真原子"宣称、补齐 WAL 失败可见性、
> 修复级联/索引残留/内存泄漏类问题。
### Fixed
- **KVStore 混合写非单记录原子(P2,宣称失实)** — delete/update 主键变更等路径
putMany 与 deleteMany 分两次调用 = 两条日志记录:崩溃在两条记录之间 → 新旧行并存
(主键变更产生重复行 / SET NULL 重写与父行删除不一致)。新增 `KVStore.writeBatch`
put+delete 编码进同一条日志记录,真全有或全无),KVStoreEngine 全部混合写路径
update/delete/dropTable/alterTable/clear/commitTransaction)统一走 writeBatch
- **WAL full 模式写入失败吞错(P2** — append/appendBatch 失败仅 console.warn
内存已提交而 WAL 缺失,崩溃即丢且调用方无感知。现在失败直接抛出(batch 模式
flush 早已抛错,行为对齐)
- **MemoryEngine SET NULL 级联索引残留(P2** — 手动 `pks.get(v)?.delete(pk)` 遗留
空 Set → checkUniqueness 对旧值永久误报 UNIQUE_VIOLATION(外键列带 unique 约束时)。
统一复用 removeIndexEntries(空 Set 正确清理)+ updateIndexes
- **delete 级联部分执行(P2)** — 多行删除时第 N 行 RESTRICT 抛错 → 前 N-1 行的级联
子行已删除、父行未删(无事务下数据不一致)。改为两阶段:先对全部待删行做 RESTRICT
预检(沿 CASCADE 链递归),任何违规则整体拒绝;Memory/Aria 双引擎对齐
- **BufferPool 驱逐不清理 pages MapP2** — 驱逐仅清 LRU 链表,pages Map 保留全部
历史页面 → 内存无限增长(256 页 ≈ 1MB 预算形同虚设)。EvictionManager 增加
onRemove 回调,驱逐时同步从 pages Map 移除(容量约束真实生效,重载仍正确)
- **MVCC 已提交版本不清理(P2** — commit 仅标记 committedversionStore 随写入量
无限增长(行数据双份常驻)。快照读取已移除(v0.5.1),版本链仅作事务内 undo:
commit 时直接清理本事务版本(rollback/discardVersions 语义不变,gc 保留未提交链裁剪)
- **LSM.flush 重复入链(P2** — freezeMemtable 会把同一张 immutable 再次入链 →
重复 SSTable2 行 flush 出 3 个文件);且 frozenMemtables 在链执行前被清空 →
并发读短暂看不到冻结数据。修复:入链后立即置空 immutable(防重复),
frozenMemtables 保持到链排空后再清理
- **rollbackToSavepoint 不重建二级索引(P2)** — 事务内直写索引 LSMsavepoint 回滚
只还原快照 → savepoint 之后的过期索引条目残留。回滚后重建受影响表索引
### Changed
- 测试 1114 → **1126**71 套件,+12 个 v0.6.3 回归);新增 `tests/v063-fixes.test.ts`
MVCC 语义测试同步改写(commit 清理版本、gc 裁剪未提交链)
---
## [0.6.2] - 2026-08-13
### 深度审计修复(数据正确性专项)
> 全源码通读 + 针对性实验验证,修复 6 个测试盲区中的数据丢失/查询错误/约束缺失问题。
### Fixed
- **KVStoreEngine 数值主键 update 丢行(P0)** — 非主键更新路径把受影响主键 `String()` 化后按
`where { [pkCol]: pk }` 回查内存行,数值型主键(123 !== "123")不命中 → 行被误判删除 →
重启后该行永久丢失。改为单次全表扫描 + 受影响集合过滤(同时消除 O(N×M) 逐主键回查开销),
字符串/数值主键语义统一
- **update 主键变更撞已有主键静默覆盖(P0** — MemoryEngine / AriaEngine 更新主键为目标已
存在值时静默覆盖另一行(数据丢失)。现在抛 `DUPLICATE_KEY`(与 insert 语义对齐),
事务内路径(txnSnapshot + 主 LSM 双查)同样拦截
- **Aria 二级索引范围查询边界算法错误(P1)** — `$gt/$gte/$lt/$lte``Number(v)±1` 构造边界
key:小数数值($gt:2 → "3:",漏 2.5)与字符串("NaN:" 前缀错位,数字/大写开头值被漏)
静默丢数据。改为全索引扫描 + 行级 matchWhere 过滤(与主键范围路径同方案),边界语义与
where-matcher 完全一致;新增小数/字符串/大写/数字开头/整数 flush 前后一致性回归
- **Aria 索引列 IS NULL 返回空(P1** — `$eq: null` 走索引路径时 `String(null)="null"` 查找
返回空并短路全表扫描 → 索引列 IS NULL 恒空。null 等值/含 null 的 IN 列表不再走索引
(回退全表扫描),与全表扫描语义一致
- **AriaEngine unique 约束未强制(P1** — 此前 insert/update 仅检查主键重复,唯一列重复值
被静默接受(MemoryEngine 已强制)。补齐:insert 整批预检(批内互查 + 索引 LSM 前缀扫描,
失败整批不落库);update 排除自身旧索引条目后检查;null 不受唯一约束(对齐 Memory);
新增 LSM.prefetchPrefixRanges(批量前缀范围预加载,一次 drainChain 避免逐行性能悬崖)
- **非主键 update 索引旧值残留** — update 时旧行未传给 `updateSecondaryIndexes`(仅主键变更
时传),索引 LSM 旧条目累积 → 唯一性检查误报 / 索引存储膨胀。统一传旧行清理旧值
- **EXPLAIN 写语句产生真实副作用(P2)** — `EXPLAIN DELETE/UPDATE/INSERT` 此前真实执行语句
(删/改数据)。现在仅 SELECT 类执行(只读),UPDATE/DELETE 用 count 估算影响行数,
INSERT/DDL 仅输出计划
### Changed
- 测试 1092 → **1114**70 套件,+22 个 v0.6.2 回归);新增 `tests/v062-fixes.test.ts`
(数值主键持久化 ×3 / 主键碰撞 ×3 / 索引范围 ×4 / IS NULL ×3 / unique ×7 / EXPLAIN ×2
---
## [0.6.1] - 2026-08-10
### 生产可用性深度审查(异常场景)
> 以"生产可用"标准全面审查:修复级联环无限递归、多表事务原子性缺口、
> 统一未 open 防护,并新增 27 个异常场景测试。
### Fixed
- **页面 id 崩溃回退导致静默丢数据(P0** — FileManager `allocatePageIds` 在分配页面后、
`saveMeta` 持久化 nextPageId 前崩溃 → `__aria_meta` 停留在旧水位 → 恢复时页面 id 复用
(覆盖崩溃前旧页面)→ 恢复期 compaction 按 meta.pageIds 删除"旧"SSTable 时误删被复用的
新数据页面 → 崩溃恢复后静默丢 75%(kv 后端 2 万行 + 加密 + 双索引场景复现 5228/20000)。
修复:`init` 以"现存最大页面 id + 1"为准(单调不回退,绝不复用已存在页面)
- **LSM.flush 剩余数据与 compaction 并发写 metaP0** — `flush()` 对 immutable/memtable
的剩余落盘直接 await(不在链上),与链上 compaction 并发写 SSTable metacompaction 产物
`saveMeta` 后,memtable flush 的 `saveMeta` 读到中间态列表(含 compaction 产物)→ 覆盖产物
引用 → compaction 产物变孤儿 → 主表/索引数据静默丢失(kv 后端优雅关闭后重开丢 75%)。
修复:剩余落盘挂链串行(memtable flush 在 compaction 之后),meta 无竞态
- **生产矩阵审计**(13 个组合全量验收):后端(opfs/kv/memory)× 特性(页面化/加密/压缩/
WAL 开关/同步模式)× 功能(CRUD/双索引/事务/主键变更/级联删除/崩溃恢复)——
新增 `tests/engine/aria-matrix-audit.test.ts`,暴露并修复上述 2 个 P0
- **二级索引范围扫描漏读尾部数据(P0)** — SSTable 索引块记录的是"块内最后一个 key"
builder 约定),读端 `locateBlockLE` 却按块首 key 语义二分:范围查询 endKey 落在
某块尾部时,下一块(尾 key 越界但首 key 仍在范围内)被排除 → 二级索引条件查询
静默丢数据(5 万行写入查 `tag='t5'` 丢 106~771 条,主表全扫描完整)。
修复:rangeScan 的 endBlockIdx 多扫一个块(条目级过滤兜底,不丢不错);
新增 4 个 SSTable 边界回归(跨前缀尾部/精确块尾/超出全量/多前缀一致性)
+ 重建 3 个二级索引大数据量回归(5 万行/高频小批量/多索引列,含崩溃恢复)
- **checkpoint 未落盘二级索引(P1** — checkpoint 只 flush 主 LSM,截断 WAL 后崩溃时
索引 memtable 未落盘、WAL 为空跳过重建 → 二级索引静默丢失最后一批条目。
checkpoint 的 flushAll 同步 flush 全部二级索引 LSM
- **kv 后端页面化缺失** — `pageStorage` 默认只对 opfs 生效;kv 后端整 value SSTable
(4MB)超过 1MB 页面缓存时每次读取全量重载。kv 后端同样启用页面化,
读放大消除;SharedMemoryBackend 改为 chunk 列表(append O(1)+ 惰性拼接缓存
- **MemoryEngine 级联环无限递归(P0** — A→B→A 循环引用 + CASCADE 删除导致栈溢出
RangeError: Maximum call stack size exceeded)—— MemoryEngine.cascadeDelete 无环路保护,
AriaEngine 已有 visited 保护(v0.4.1),Memory 侧缺失。新增 visited 集合(表:主键),
与 Aria 语义对齐;A→B→A 环 + 深链 A→B→C→D 均有回归测试
- **KVStore 快照损坏水位 bugP0** — baseSeq = max(metaSeq, snapshotSeq) 在快照损坏
回退全量日志重放时,meta.seq(最后一次 checkpoint 水位)错误跳过日志中 checkpoint 后
的有效记录 → 静默丢数据。水位只信任快照内嵌 seq
- **多表事务 commit 非原子(P1** — 事务 commit 逐表 flush:中途失败/崩溃 → 部分表已提交。
重构为全部 dirty 表合并单次 putMany/deleteMany(一条日志记录 = 真原子)
- **级联更新/删除非原子(P1)** — update 主键变更/delete 的级联影响表逐表 diff 分多次写。
重构 collectTableDiff 合并到单次原子写(主表 + 全部级联表同一条日志记录)
- **未 open 防护不一致** — getTableNames/hasTable/getTableSchema 未检查 DB_NOT_OPEN
(返回空而非报错,与其余方法不一致)。统一 ensureOpen
- **测试盲区** — 快照损坏测试此前篡改未生效(close 后介质不可读),从未真正覆盖损坏路径;
修复并新增 metaSeq 超前回归测试
- **批量插入性能悬崖(P1)** — insert 循环内逐行 `lsm.prefetchKeys([key])`
每行 `await drainChain()` 排空后台链:后台 compaction 在链上数秒时每行阻塞数秒 →
10 万级插入从 ~5ms/批暴跌到 8~11s/批(kv 后端 10 万行共 353s)。
修复:批级预加载本批全部 PK(一次 drainChain),循环内 `lsm.get`
memtable/flush 产物(自动入缓存)兜底,完整性与批量语义不变。
实测:kv 后端 10 万行 **353s → 12.5s**28 倍),opfs 后端 25s
新增 kv/opfs 双后端 10 万行回归(性能护栏 + 索引完整 + 崩溃恢复)
### Added
- **AriaEngine 可选自研 KVStore 后端**`storageBackend: 'kv'` / `diskEngine: 'kv'`):
- aria 完全跑在自研存储栈上(KVStore 日志+快照),不再依赖浏览器 OPFS APINode/测试用 SharedMemory 介质)
- KVStore 新增 APPEND 日志操作类型:aria WAL 分片追加 O(chunk) 高效,恢复按序拼接,checkpoint 快照含最终值
- writeMany/deleteMany 经 KVStore 单日志记录真原子(此前 OPFS 逐文件写)
- aria 崩溃恢复 = KVStore 双层恢复 + WAL 重放;二级索引/页面化兼容(kv 后端 SSTable 整 value 存储)
- 10 个 aria+kv 集成测试(后端单元/CRUD 持久化/WAL 追加恢复/checkpoint 混合/索引/大数据崩溃/高层 API 事务级联)
- 27 个生产异常场景测试(`tests/production-abnormal.test.ts`):
- KVStore 边界:空 value/空 key、1MB 大 value、中文/emoji/冒号/超长 key、未 open、
重复 open/close 幂等、clear 后写、1 万小 key、写失败后继续可用
- KVStoreEngine:未 open 全操作、重复 close、不存在表、事务嵌套 TX_ACTIVE、
无事务 TX_NONE、commit 失败回滚(磁盘无部分提交)、多表事务原子性、级联环、
深链级联、主键变更+索引一致性、空表操作、5000 行批量删除持久化
- SharedMemoryBackend 全分支:append 拼接、writeMany/deleteMany、close 后安全、跨实例共享
- 日志解析损坏分支:onCorrupt false 停止、keyLen 越界记录丢弃
- AriaEngine 级联环/深链回归测试(与 Memory 对齐)
### Changed
- 测试 1022 → **1049**(65 套件);行覆盖率 89.15% → **89.33%**
- 事务/级联写入路径:逐表 flush → 单条日志记录原子(性能与一致性双提升)
---
## [0.6.0] - 2026-08-10
### 里程碑:完全移除 IndexedDB,自研 KV 事务存储引擎
> 达成"彻底删除 IndexedDB + 自研类似 IndexedDB 的存储后端"目标:
> KVStore 引擎在 OPFS 之上实现多 key 原子事务(IndexedDB 的核心能力),
> disk 模式全面切换,旧库一键迁移。
### Added
- **KVStore 自研 KV 事务引擎**`src/engine/kvstore/`):
- 多 key 原子写:putMany/deleteMany = 单条日志记录(单文件 COW 原子追加)→ 崩溃全有或全无
- 持久化与崩溃恢复:快照(checkpoint)+ 追加日志,两阶段恢复(快照水位跳过)
- 自愈:快照损坏回退全量日志重放;日志损坏截断至损坏处;`repair()` 清理
- 容错时序:checkpoint = 写快照 → 写 meta → 清空日志(meta 先于截断,任何崩溃窗口不丢数据)
- 写操作与 checkpoint 串行队列(无交错窗口);标准 CRC-32 全程校验
- 介质层:OPFS(浏览器)/ SharedMemoryBackendNode/测试,跨实例共享模拟持久化)
- **KVStoreEngine**disk 模式,替代 IndexedDBEngine + OPFSEngine):
- 内存热路径 + KVStore 原子持久化;读 O(1),写增量/整表 diff 分级
- 事务:内存快照 + commit 原子 flush(受影响表),事务内 DDL 支持(schema 一并持久化)
- 外键级联(CASCADE/SET NULL/RESTRICT)持久化、主键变更、二级索引跨重启恢复
- `reload()` 支持多标签页同步重载(Hybrid 场景)
- **迁移工具** `migrateFromIndexedDB()`:一次性把旧 IndexedDB 库(schema/索引/行)导入新引擎;
旧库不存在/aria 私有格式(SSTable/WAL)明确报错;目标库已有表跳过不覆盖
- **可靠性强化**
- 10 万 key 写入+checkpoint+重开全量验证(928ms
- 5 万混合操作+崩溃模拟零丢失
- e2e 新增写入中途崩溃 / checkpoint 前后崩溃注入 / KVStoreEngine 真实环境持久化-索引-事务(12 用例)
- **`MetonaSqlark` 配置**`diskEngine` 类型收敛为 `'opfs' | 'memory'`(默认 'opfs'
### Removed
- `src/engine/indexeddb.ts`840 行 IndexedDBEngine)—— 版本冲突/blocked 重试/onversionchange/flushToIDB 全部消失
- `src/engine/opfs.ts`375 行 OPFSEngine)—— 能力被 KVStoreEngine 覆盖
- `IndexedDBBackend`aria 后端)—— aria storageBackend 收敛为 'opfs' | 'memory'
- 26 个测试文件的 fake-indexeddb 依赖改造为 KVStore SharedMemory / OPFS mock
### Fixed
- **Hybrid 多标签页重载失效**KVStoreEngine 读内存 vs 磁盘引擎读穿透)—— 新增 `reload()` 从介质重载
- **事务内 DDL schema 不持久化**—— commit 时统一 persistSchema(重启后表结构完整)
- **repair 缓存掩盖损坏**—— 先清 BufferPool 再校验(缓存中"完好页面"不再掩盖磁盘损坏)
- **dropInvalidSSTable 孤儿页面**—— 先删数据文件再删 meta(页面化删除依赖 pageIds 定位)
### Changed
- 测试 1009 → **1022**64 套件;+58 新增 kvstore/kvstore-engine/压力/迁移测试,-46 删除/改造 IDB 测试)
- 行覆盖率 87.25% → **89.13%**
- 浏览器支持收敛到 OPFS 矩阵(Chrome/Edge 102+、Firefox 111+、Safari 15.2+);Node 用内存介质
- fake-indexeddb 保留为 devDependency(仅迁移工具测试用)
---
## [0.5.1] - 2026-08-10
### 深度审查:消除假实现/半成品/死代码
> 对全部源码做系统性审查,修复"宣称存在但从未接线"的假实现、补齐无入口的半成品、
> 删除无引用的死代码。
### Added(假实现接线)
- **CRUD 生命周期钩子真实接线** — 此前 `beforeInsert`/`afterInsert`/`beforeUpdate`/`afterUpdate`/`beforeDelete`/`afterDelete`
六个钩子仅存在于类型与文档宣称("14 种钩子"),从未被任何路径触发。现已在两条路径真实触发:
- Table API`table().insert/insertMany/update().execute()/delete().execute()`
- SQL`db.query("INSERT/UPDATE/DELETE ...")`
参数与文档一致(beforeInsert: rows[]、afterUpdate: query, updates, count 等);事务路径不触发(Table 无 hooks 回调,与现状一致)
- **EXPLAIN SQL 入口** — 此前 executor 有 EXPLAIN 分支但 parser 无法解析(无 token),只能手工构造 AST 测试。
补齐 `EXPLAIN <statement>` 语法(tokens/lexer/parser),任意引擎可用
- **ANALYZE / REINDEX / VACUUM SQL 入口** — 此前仅有 AriaEngine 方法(`(engine as any)` 才能调用),README 宣称能力但用户无法触发。
补齐 `ANALYZE [TABLE] name` / `REINDEX [TABLE] name` / `VACUUM` 语法;不支持的引擎抛 `NOT_SUPPORTED`
- **SAVEPOINT SQL 入口** — 补齐 `SAVEPOINT name` / `ROLLBACK TO [SAVEPOINT] name` / `RELEASE [SAVEPOINT] name`
`ROLLBACK [TRANSACTION]` 保持原语义
- **db.backup() 公共方法** — 引擎级 backup(MVCC 一致性视图)此前无公共入口;Aria 走引擎级,其余引擎回退 exportAll
### Removed(死代码)
- **`src/utils.ts` 整文件** — generateId/escapeHTML/deepMerge/debounce/throttle/isBrowser 在 src 内零引用
(v0.0.1 遗留,仅测试锁定);连同 tests/utils.test.ts 删除
- **`estimateQueryCost`(未接线的"查询优化器")** — 实现与测试存在但从未接入查询路径(find 用 tryIndexLookup 硬编码),删除消除"优化器存在但没用"假象
- **MVCC 读侧全套** — readVersion/getLatestCommittedVersions/clearTable/getActiveTxnCount/isActive/getActiveWriteKeys
引擎事务读取走 txnSnapshotMVCC 仅作 undo log),读侧实现从未执行。删除并改写相关测试为验证版本链清理/GC 行为
- **统计/辅助死方法** — EvictionManager.getAllPages/getSize/popLRU/getCapacity、BufferPool.getCachedPageCount/getDirtyPageCount/getCapacity/newPage、
WAL.isEnabled/getLSN、CheckpointManager.forceCheckpoint/setInterval/getOpCount、LSM.isInitialized/getCacheSize/getCacheLimit、
SSTableReader.getMeta/getIndexBlockCount/getStoredChecksum、BloomFilter.getBitSize/getInsertedCount、MemTable.contains、
FileManager.allocateTableRootPage/createEmptyPage、OPFSEngine.loadTableIntoMemory、QueryExecutor.setMaxRowsPerQuery、
ConnectionManager.getRefCount
- **类型/常量死代码** — STORAGE_MODES/DISK_ENGINES、ColumnEncoding/DEFAULT_BLOCK_SIZE/DataBlock、CallbackEntrySource、StatementType、AggregateExpression、SubqueryExpression
- **Crypto 全局兼容层** — initCrypto/encryptPage/decryptPage/closeCrypto/isCryptoEnabledv0.5.0 后引擎走 EncryptedBackend 实例,全局单例无消费者);删除过时的"全局加密落盘"测试(aria-encryption.test.ts 已覆盖等价路径)
### Changed
- 测试 1022 → **1009**(62 套件;删除 13 个死代码测试 + 新增 22 个 hooks/维护语句测试);行覆盖率 86.68% → **87.25%**
- README 补维护语句表(EXPLAIN/ANALYZE/REINDEX/VACUUM/SAVEPOINT SQL 入口)与 `db.backup()` API
- 14 个生命周期钩子现在全部真实触发(site/docs.html 与 CONTRIBUTING 宣称变为事实)
---
## [0.5.0] - 2026-08-10
### 里程碑:存储后端生产级硬化(自研 OPFS 后端全面落地)
> 本次版本对 AriaEngine 存储层做全链路生产级改造:修复"宣称有校验和/加密/页面式存储但实际未实现"的失实实现,
> WAL 重构为分片文件(消除多文件原子性与文件爆炸问题)、SSTable 页面化物理存储(BufferPool/FileManager 真正接入)、
> 多标签页独占锁、repair 自愈增强,并引入真实 Chromium e2e 验证。
### Added
- **全库 AES-256-GCM 加密(真实接线)** — `AriaEngineConfig.encryption.password`
- `EncryptedBackend` 装饰器在 backend 层透明加解密(WAL/SSTable/Schema/元数据全覆盖,每个 value 独立随机 IV + GCM 认证)
- PBKDF2-SHA256100000 迭代)派生密钥,salt + verifier 持久化于 `__aria_keymeta`(密码错误 → `ARIA_DECRYPT_ERROR`
- 加密开关一致性检测:明文旧库 + 加密配置 → `ARIA_ENCRYPT_CONFIG_ERROR`;加密库 + 无密码 → `ARIA_ENCRYPT_REQUIRED`
- clearAll 保留密钥元数据(库身份保留,密码不变)
- **WAL 分片存储重构** — `SegmentedWALStore``__wal_%06d.bin`):
- 取代"每条记录一个 key"旧结构:分片阈值 4MB 自动切换,文件数量可控
- WAL 序号(LSN)内嵌记录字节流,移除 `__wal_count` 键 → append 单文件原子写(createWritable COW
- 空洞检测:分片序号不连续 → 空洞之后保守丢弃(仅可能来自 truncate 部分完成,数据已落盘,无害)
- 旧格式 `__wal_N` + `__wal_count` 兼容:读取迁移重放,checkpoint 统一清空
- **OPFS 后端 v2** —
- `append()` 真追加:`createWritable({ keepExistingData })` + seekO(chunk) 无需读旧内容
- 写队列健壮性:单次任务失败不中断队列链(错误返回给调用方),close 等待队列排空
- open 自动清理崩溃残留临时文件(`.crswap`/`.tmp`
- 原子性文档化:单文件 createWritable 为 copy-on-write 原子替换
- **SSTable 页面化物理存储(BufferPool/FileManager 真正接入)** —
- `PageSSTableStore`SSTable 切分为 4KB 页面,经 FileManager 分配 pageId、BufferPool LRU 缓存
- `save` 语义 = 已持久化(逐页 flushPage 落盘后才返回,WAL checkpoint 截断安全)
- 页面 ID 列表经 `SSTableMeta.pageIds` 持久化;旧 meta(无 pageIds)自动走整 value 读取(兼容)
- OPFS 后端默认启用(`pageStorage` 配置可覆盖);FileManager 批量分配 `allocatePageIds`(一次 meta 持久化)
- 读取缺失页面返回 null(此前返回空页会静默掩盖页面丢失,自愈路径依赖 null 触发清理)
- **多标签页独占锁** — Web Locks APIChrome 69+ / Firefox 96+ / Safari 15.4+):
- `ifAvailable` 模式:第二个标签页打开同一库 → 抛 `ARIA_LOCKED`(不排队挂起)
- 打开失败自动释放锁(不泄漏);close 等待锁真正归还
- 不支持 Web Locks 的环境降级为无锁 + 告警(如实文档化)
- **repair() 自愈增强** — 孤儿页面清理(meta 未引用的 `pg_` 文件)、OPFS 残留临时文件清理、WAL 空洞截断落地
- **LZ4 格式 v2** — 压缩流前置 4 字节原始大小头:解压不再依赖外部估算(高压缩率数据下 `buf.length*2` 估算不足会截断),损坏头显式抛错
- **SSTable 真实 CRC-32 校验和** — footer checksum 字段从写死 0 升级为整文件标准 CRC32(checksum=0 识别旧版文件跳过校验,兼容旧库);打开校验/compaction 加载/运行期预加载三处均校验,损坏文件自动清理自愈
- **WAL 标准 CRC32** — 记录校验从弱滚动校验升级为标准 CRC32;双算法探测(新算法失败再试旧算法)兼容旧 WAL 记录
- **配置透传** — `DatabaseConfig.aria``MetonaSqlark.create()` 可配置 `walSyncMode`/`checkpointInterval`/`encryption`/`pageStorage`/`compression` 等 AriaEngine 专属参数
- **Playwright e2e(真实 Chromium + OPFS)** — 7 用例:持久化(刷新保留)、关闭重开、崩溃恢复(WAL 重放)、多标签页锁、加密密码往返、页面化端到端 + 索引查询、repair 自愈;`npm run test:e2e`
### Fixed
- **加密此前从未接线(严重失实)** — crypto 模块全局单例从未被引擎调用、salt 不持久化重启无法解密;README 宣称的 AES-GCM 实际不可用。改为 EncryptedBackend 装饰器 + keymeta 持久化 + 密码验证
- **SSTable checksum 写死 0(严重失实)** — 数据腐坏(bit flip)完全无法检测。升级为真实 CRC-32,损坏文件打开自愈
- **OPFS writeMany 无原子性(P0** — WAL 记录 + count 多键写入非原子,记录空洞导致后续记录字节错位解析失败。分片重构后 WAL append 单文件原子写 + 空洞检测截断根治
- **OPFS 高频写入文件爆炸(P1)** — 每条 WAL 记录一个文件,8 万条记录 = 8 万次 createWritable。分片存储 + 真追加根治
- **LZ4 高压缩率解压截断(P1)** — `buf.length * 2` 估算原始大小,压缩率 >50% 时截断(SSTable 魔数在文件末尾被丢 → 整库数据丢失)。格式 v2 头部自描述根治
- **OPFS 写队列失败永久中断(P1)** — 队列任务抛错后链 rejected,后续所有写挂起。单次失败不中断链
- **OPFS close 不等挂起写(P1** — close 立即置空目录句柄,挂起写静默丢失。close 等待队列排空
- **Web Locks 方法解构 this 绑定丢失(e2e 发现)** — `navigator.locks.request` 解构后调用抛 Illegal invocation。改用 `.bind(lockManager)` 保持 this
- **页面化 + 崩溃窗口数据丢失(压力测试发现)** — 修复前页面化路径在模拟崩溃测试中丢失已确认写入;根治后 500 随机操作 + 崩溃重放零丢失
### Changed
- `AriaEngineConfig` 新增 `encryption` / `pageStorage``DatabaseConfig` 新增 `aria` 透传
- 页面层精简:删除从未接入主路径的 slot/tuple 模块(假"页面式存储"实现),仅保留生产代码实际使用的 `initPageHeader`/`createPage`;架构宣称如实更新(4KB 页面承载 SSTable 字节切片,非行级 Slotted Page
- LZ4 压缩格式不向后兼容(v0.2.6 已声明 compression 数据需重新导入,本次格式 v2 同理)
- 测试 958 → **1022**(62 套件);行覆盖率 84.2% → **86.68%**;新增 6 套件:crc32 / aria-checksum / aria-wal-crc / aria-wal-segment / aria-opfs-backend / aria-page-store / aria-encryption / aria-locks / aria-repair-hardening / aria-page-core
- 新增 Playwright e2e 基建(`npm run test:e2e`7 用例真实 Chromium
---
## [0.4.4] - 2026-08-09
### Fixed
- **SSTable 编码缺陷导致大内容写入崩溃(P0)** — AriaEngine 保存大段中文内容(如 300KB 笔记)→ close 构建 SSTable 崩溃 → 半写文件 → 下次打开 ARIA_OPEN_ERROR
- **块大小估算用 UTF-16 码元而非 UTF-8 字节** — `computeBlockSize``json.length`(中文 1 字 = 1 码元)估算,实际写入 UTF-8 字节(中文 1 字 = 3 字节)→ 300KB 中文实际 900KB → 缓冲区低估 → `Uint8Array.set` 越界崩溃。改为 TextEncoder 预编码,全部按字节长度计算(切块、块大小、索引块、总大小)
- **valueLen/keyLen 字段 u16 截断** — 长度字段 u16 上限 64KB,大 value 被截断 → 线性格式整体错乱。格式 v2magic "SSTC")升级为 u32 长度字段
- **大 value 块切分** — 单条 value 超过块大小(4096)时独立成块(基于字节估算正确切分)
- **格式兼容** — reader 支持 v1"SSTB"u16)与 v2"SSTC"u32)双格式:旧库 v1 文件仍可读(小 value 场景无缺陷),新写入用 v2,旧库数据不丢失
- 连带受益:中文主键 key、大内容二级索引列(索引 key 含列值)同样支持 u32 长度字段
### Changed
- 测试 950 → **958**52 套件),新增 `tests/v045-hardening.test.ts`8 条:300KB 中文 builder/端到端跨重启、>64KB value、大 value 独立成块、中文 key、v1 兼容、v2 魔数)
- 版本号升至 v0.4.4
---
## [0.4.3] - 2026-08-09
### Fixed
- **close 后后台 compaction 吞错并在 backend 关闭后执行(P0)** — `scheduleCompact``setTimeout(0)` 调度:close 时未触发的定时器在 `backend.close()` 后执行,用已关闭的存储写(错误被吞);close 后立即 reopen 时旧闭包引用新 backend 交叉污染。改为 compaction 直接挂 flushChain 串行执行,close 的 `drainChain()` 排空全部级联任务后再关闭存储
- **后台 flush/compaction 失败完全静默(P0)** — 吞错防死锁的同时记录 `lastBackgroundError`,显式 `flush()`/`close()` 时抛 `ARIA_BACKGROUND_ERROR`(消费一次),不再永久不可见
- **prefetch 与 compaction 竞态丢数据(P0)** — 后台任务在链上动态增长,`prefetchRange/prefetchKeys` 单次 `await flushChain` 后 compaction 仍可能合并 levels,扫描时新 meta 缓存未命中 → 跳块丢数据(缓存驱逐场景 UPDATE/DELETE 少删行)。改为等待链稳定(`drainChain`)再预加载
- **commitTransaction 提交顺序(P1** — 先合并快照后写 WAL COMMIT:WAL 写失败时内存已提交但持久化缺失,崩溃重启丢失。改为先持久化 WAL COMMIT 再合并快照(WAL 始终领先,崩溃恢复一致)
- **OPFS close 不等挂起写(P1)** — 写操作整体串行入队(内存写 + 快照 + 文件持久化),close/repair/clearAll 先等队列排空再释放目录句柄,避免 close 后挂起写读旧数据
- **IndexedDBEngine close 活跃事务防御** — close 时自动回滚活跃事务(避免对已关闭连接操作报错)
### Changed
- 测试 944 → **950**51 套件),新增 `tests/v044-hardening.test.ts`(6 条:close 后台任务排空、reopen 无污染、后台失败可见、链不卡死、WAL 提交顺序、OPFS close 等待)
- 版本号升至 v0.4.3
---
## [0.4.2] - 2026-08-09
### Fixed
- **AriaEngine 残缺 SSTable 导致打开崩溃(P0)** — 异常退出后下次打开抛原生 `RangeError: offset is out of bounds`,库打不开只能删库重建。三处根治:
- `SSTableReader` 读路径防御:块偏移/大小越界时跳过该块(get/rangeScan/scanAll 返回空或继续),索引块/块内条目解析全程边界检查,不再抛 RangeError
- 打开时完整性校验:`LSM.init()` 校验 meta 引用的文件存在、长度 ≥ 头、可解析,残缺的忽略并清理 meta/文件(自愈,不阻塞打开)
- WAL 原子写入:`IndexedDBBackend` 新增 `writeMany`/`deleteMany`(单事务批量写删),WAL 记录与 count 计数同事务提交,中断整体回滚不留半写
- **IndexedDB 版本管理缺陷导致重启必报 VersionErrorP0** — 建表每张版本号 +1 而 `open()``config.version`,重启后版本过低直接失败。`open()` 遇 VersionError 自动用无版本参数探测库当前实际版本并以该版本重开
- **`create({ version: 0 })` 抛原生 TypeErrorP0** — 版本 < 1 统一归一化为 1,不再直接炸 `indexedDB.open(name, 0)`
- **WAL 写丢失(P1)** — 连续快速写入 200 条 close+重开实测丢 4 条。WAL 恢复不再依赖 count 计数,改为扫描全部 `__wal_*` 键(数值排序拼接),count 键只作序号分配器;配合原子写入彻底消除 count/记录竞态
- **AriaEngine.close() 不 checkpointWAL 无限重放(P1** — close 前补 `wal.checkpoint()` 截断,下次打开不再重放已落盘的历史记录
- **引擎内部错误不包装 DatabaseErrorP1** — AriaEngine `open()` 整体 try/catch 包装为 `ARIA_OPEN_ERROR`IndexedDBEngine 打开路径统一 `IDB_OPEN_ERROR`/`IDB_VERSION_ERROR`/`IDB_BLOCKED`,应用层可拿 code 分类处理
- **`migrateTo` 版本门槛与引擎版本脱节(P2)** — 迁移版本持久化到库内(`IStorageEngine.getMeta/setMeta` 可选接口,Aria 存 backend、IndexedDB 复用 `__metona_schema` store、OPFS 存元数据文件、Hybrid 委托磁盘引擎),重启后从持久化版本继续执行,不重跑不跳跑
- **IndexedDB `onblocked` 直接 reject(P2)** — 改为等待后重试(阻塞解除后 success 仍触发,仅超时判失败,并防止连接泄漏),多次重试仍阻塞才抛 `IDB_BLOCKED`
- **缺少官方崩溃恢复/自愈接口(P2)** — 新增 `db.repair()`(校验清理损坏数据并恢复一致性)与 `db.clearAll()`(清空全部数据与表结构,保留库),全部引擎(Aria/IndexedDB/OPFS/Memory/Hybrid)实现,无需删库重建
- **IndexedDB `__metona_schema` store 缺失时元数据无法落盘** — `open()` 时确保 schema/meta store 存在(缺失则一次版本升级创建)
### Fixed(深度审计第二轮)
- **AriaEngine 二级索引跨重启丢失(严重)** — 重开只恢复 schema 不恢复索引 LSM:索引查询静默回退全表、`createIndex` 因 schema 标记已存在而直接 return → 索引永久缺失;且 WAL 崩溃恢复只回放主 LSM,最后一批写入的索引缺失 → 索引查询丢行。三处根治:open 时按 schema 标记重建索引 LSM(数据已持久化直接加载)、close 时同步落盘全部索引 LSM、WAL 恢复完成后全量重建索引
- **AriaEngine Compaction 依赖缓存丢数据(严重)** — `compactLevelAsync` 仅从 LRU 缓存读 SSTable,缓存未命中(单文件超缓存上限被驱逐、频繁 flush)时跳过全部文件并从 levels 移除 → 运行中数据全部不可见(重启才恢复)。改为从存储兜底加载;无有效数据时将文件放回 levels 不删除
- **AriaEngine flush/compaction 失败卡死写路径(严重)** — flushChain 链上任务抛错后永久 rejected,后续所有 flush/compaction 挂起。链上任务统一吞错恢复(记录告警,链继续)
- **ALTER TABLE 在 IndexedDB/Hybrid/OPFS 引擎不持久化** — v0.4.1 只给 Aria 加了引擎级 `alterTable`,其余引擎走通用路径只改内存 schema 引用:重启后 ADD/DROP 全部回退,DROP 的行数据也残留。为 IndexedDB(持久化 schema + 重写存储行)/ Hybrid(双引擎)/ OPFS / Memory 统一实现引擎级 `alterTable`
- **IndexedDB 事务内 DDL 崩溃(严重)** — 事务内建表 commit 时 IDB 无对应 store 报错;事务内删表 IDB store 残留 → 重启幽灵表。`flushToIDB` 提交时 diff 内存表与 IDB store:缺失的创建(含 schema 持久化)、多余的删除(含 schema 记录清理);commit 顺序调整为"先刷盘后提交内存快照",失败可回滚
- **Hybrid 内存/磁盘引擎共享 schema 引用污染** — `reloadMemoryFromDisk` 把磁盘引擎的 schema 对象直接存入内存引擎,任一引擎 ALTER 都改到对方。`MemoryEngine.createTable` 深拷贝 schema(全引擎受益)
- **Hybrid commit 失败路径错误掩盖** — 磁盘已提交后内存提交失败时调 `diskEngine.rollbackTransaction()` 抛 TX_NONE 掩盖原错误;如实上报磁盘已提交状态
- **AriaEngine.close 运行期状态残留** — close 后 MVCC/事务快照/savepoint 残留,重开后 `beginTransaction` 报 TX_ACTIVE。close 统一清理
- **VACUUM 对 2-3 个文件层级不压缩** — `compactLevel` public 门槛与内部自动调度门槛混用(内部 4 / VACUUM 期望 2)。分离参数化
### Fixed(生产就绪审计第三轮)
- **Aria 事务进行中 checkpoint 截断 WAL(严重,P0** — checkpointtick/forceCheckpoint)在活跃事务中途执行时 truncate WALBEGIN/INSERT 记录被截断,事务 COMMIT 后崩溃恢复丢失整个事务数据。CheckpointManager 包装 WAL:事务活跃时跳过截断(close/repair/clearAll 仍正常截断)
- **Aria dropTable / DROP_TABLE 恢复不清理二级索引(P1)** — 索引 LSM 孤儿残留,重建同名表后旧索引数据污染新表(按旧值索引查询返回不匹配行)。新增 `cleanupTableIndexes`dropTable、崩溃恢复、ALTER DROP 索引列统一清理
- **Aria ALTER TABLE DROP 索引列残留(P1)** — 删列但索引 LSM 保留,后续同名列索引数据脏
- **Aria freezeMemtable 阈值逐次衰减(P2** — 新 memtable 用旧表已用大小当阈值,手动 flush 后阈值塌缩 → 频繁小文件。改回配置阈值
- **Aria 事务中 DDL 静默不一致(P2)** — 事务快照只覆盖行数据,createTable/dropTable/alterTable 无法回滚;与 Memory/IndexedDB 可回滚行为不一致 → 显式抛 `NOT_SUPPORTED`(不再静默)
- **ON UPDATE 外键级联未实现(P2,声称支持但静默忽略)** — 实现 `ON UPDATE CASCADE/SET NULL/RESTRICT`Memory + Aria 双引擎):更新主键时级联更新/拒绝,含二级索引与 WAL 记录,先全量 RESTRICT 检查再执行防部分修改;update 同时支持主键变更(旧键删除 + 新键落表)
- **MVCC commit/rollback 全库版本遍历(P2 性能)** — 大表事务 commit O(全库版本数)。按事务记录写入的 key 精准清理(O(写入数))
- **OPFS 空表重启消失 + schema/索引丢失(P1** — 表 schema 持久化到元数据文件:createTable/dropTable/alterTable/createIndex 同步;重启从 schema 恢复(空表保留、主键/索引标记/约束完整),无 schema 的旧库回退数据推断(兼容)
- **OPFS dropTable 不清理 schema 记录(P1)** — 重启恢复幽灵表;删表同步清理
- **OPFS 并发写验证** — 审计确认内存写同步、持久化快照总是最新(单标签页无丢更新竞态),新增并发写回归测试锁定行为
- **VACUUM 返回值语义** — `gcVersions` 返回活跃事务数改为全局提交序列号
### Changed
- 测试 894 → **944**50 套件),新增 `tests/v042-fixes.test.ts`23 条问题清单回归)、`tests/v042-hardening.test.ts`12 条深度审计回归)与 `tests/v043-hardening.test.ts`(15 条生产就绪回归:事务×checkpoint、DDL 索引清理、OPFS 持久化、memtable 阈值、红黑树 5000 次随机压力、onUpdate 级联)
- 版本号升至 v0.4.2
---
## [0.4.1] - 2026-08-08
### Added
- **AriaEngine 外键级联删除** — 对齐 Memory 引擎:`CASCADE`(递归多层)、`SET NULL``RESTRICT`(抛 `FOREIGN_KEY_VIOLATION`),含二级索引清理、WAL 记录、事务内可提交/回滚、环路保护
- **`AriaEngine.clearAll()`** — 重置数据库(清空后端存储/LSM/WAL/MVCC/二级索引),实例可继续使用;供演示页刷新场景与 API 用户使用
- **演示页引擎切换器** — 右上角 `⚡ Memory` / `🌲 Aria` 一键切换,Aria 模式每次加载 `clearAll()` 保证演示确定性;aria 预设文案更新(数据库即 Aria)
### Changed
- 版本号升至 v0.4.1
- 测试 880 → **894**46 → 47 套件)
- `AriaEngine` 级联行为与 MemoryEngine 对齐:`SET NULL` 不影响删除返回行数
### Fixed
- **JOIN 主表 WHERE 不下推** — `SELECT ... FROM orders o JOIN users u ... WHERE o.user_id = '1'` 的 WHERE 只在 JOIN 后过滤,二级索引形同虚设。v0.4.1 将主表前缀等值条件下推到引擎(`extractPushableWhere`),索引真正生效
- **DROP INDEX 不存在的索引静默成功** — Memory/Aria 引擎 `dropIndex` 对无索引列静默返回,改为抛 `INDEX_NOT_FOUND`
- **Memory 索引查找不支持 `$eq` 对象形式** — SQL 解析器生成的 `{ $eq: value }` 等值条件不走哈希索引(仅简单值走),补 `$eq` 分支
- **演示页预设数据被前序预设污染** — 连续点击预设时,cascade(删除 Alice/重建 orders)、truncate、alter 等会修改种子数据,后续预设(如 index)查询返回空。`loadPreset` 点击预设前自动重置数据库到初始状态,每个预设可预期演示
- **Aria ALTER TABLE 不生效(严重)** — `DROP COLUMN` 仅改 schema 引用,`find()` 返回行副本无法就地删除,存储行残留已删列值(SELECT * 仍显示);且 ALTER 不持久化 schema,重启后列定义回退。新增引擎级 `alterTable``IStorageEngine` 可选接口):Aria 重写主 LSM 移除列值 + `persistSchemas` + WAL UPDATE 记录,崩溃恢复后 schema 与行一致
- **裸布尔列条件不支持(严重)** — `WHERE done` / `CASE WHEN done THEN` 解析失败(无比较运算符即抛错),条件被忽略导致 CASE 恒走 ELSE。parser 支持裸列真值判断(`{ col: { $eq: true } }`
- **Aria $in 返回重复行** — IN 子查询含重复值(如 Alice 两条订单)时 PK/二级索引 `$in` 逐值查找重复 push,按主键去重
---
## [0.4.0] - 2026-08-08
### Added
- **流式查询 API** — `db.queryStream(sql, onRow)` / `db.table(name).stream(onRow)` / `IStorageEngine.findStream()`
- Aria 引擎走 LSM `rangeScanLazy` 惰性扫描,逐行回调不物化结果数组(大表友好)
- Memory/Hybrid/OPFS 逐行迭代;IndexedDB 批量读入后逐行回调(保持接口一致)
- 支持 WHERE/LIMIT/OFFSET/列投影;JOIN/GROUP BY/UNION/聚合/ORDER BY 自动回退物化
- 用户 async 回调自动回退物化(同步扫描无法 await,避免吞 Promise
- **多列 ON 哈希连接** — 复合等值条件(`ON a.tenant = b.tenant AND a.key = b.key`)走哈希连接(含顶层 `$and` 展开),任一右列为索引/主键即可;不适用自动回退嵌套循环
- **FROM 子查询(派生表)** — `SELECT ... FROM (SELECT ...) AS alias WHERE ...`,子查询结果作为行源,支持 WHERE/ORDER BY/LIMIT 与 JOIN 组合
- **COUNT(DISTINCT col)** — 聚合去重(COUNT/SUM/AVG/MIN/MAX 均支持),任意类型去重
- **NULLS FIRST / NULLS LAST** — `ORDER BY col ASC NULLS FIRST` 等,NULL 位置固定不受升降序反转
- **普通列别名** — `SELECT name AS n` / `SELECT name n`(此前仅聚合/CASE 支持别名),别名可用于 ORDER BY
- **无表查询与字符串常量列** — `SELECT 1` / `SELECT 'lit' AS x`(无需 FROM),`''` 转义与常量列投影
### Fixed
- **AriaEngine 崩溃恢复后已删除表/数据复活(严重,P0)** — WAL 回放忽略 `DROP_TABLE``CREATE_TABLE` 无条件恢复 schema,删表后崩溃重启表+数据全部复活。v0.4.0 新增 `applyDropTableRecovery()` 回放删除 schema + 清除 LSM 残留
- **WAL 恢复后不截断** — 每次重启重复回放 + WAL 无限膨胀。恢复完成将回放数据落盘后 `wal.checkpoint()` 截断
- **MemoryEngine update 不维护索引(P0** — `update()` 后唯一约束可被绕过、按新值索引查询丢行。`removeIndexEntries()` 在 update/delete/级联删除/SET NULL 前清理旧值索引
- **Aria 事务内读不到自己写入的行** — `update/delete/count/clear``getAllRows()` 不合并 `txnSnapshot`,事务内先 insert 再 update/delete 失效;统一 `mergeTxnSnapshot()`
- **Aria TRUNCATE 不写 WAL** — `clear()` 崩溃恢复后丢失;事务内清空走快照删除标记,提交生效
- **WAL 大小阈值在 full 模式永不触发** — `getBufferedBytes()` 跟踪真实字节数替代缓冲计数估算
- **rollback 后二级索引残留** — 事务内直接写入索引 LSM,回滚后对受影响表全量重建索引
- **Savepoint 回滚 MVCC 不一致** — `discardVersions()` 清理版本链保留事务登记
- **PK 范围查询字符串算术 bug** — `$gt/$gte/$lt/$lte``Number(v)+1` 构造 key 对字符串 PK 失效;改为主 LSM 前缀扫描 + 条件过滤;主键列不再建冗余二级索引(PK `$in` 走多次精确查找)
- **SQL 字符串 `''` 标准转义** — 双引号转义此前从未生效(lexer 条件先于转义分支退出);重写 `readString`
- **INSERT INTO ... SELECT 位置错位(严重)** — 源行 `validateRow` 跳过 undefined 值导致行键缺失/乱序(如 ALTER 加列后省略列插入的行),`Object.values` 位置映射把 age 数字填进 email 列报类型错。改为按源表 schema 列顺序/SELECT 列列表对齐,缺列不填
- **ORDER BY / GROUP BY 表前缀列** — `ORDER BY o.amount` / `GROUP BY u.name` 解析失败(`Expected ';' ... got "."`),parser 支持 `table.column` 引用,executor 非 JOIN 路径剥离前缀
- **关联 EXISTS 绑定失效(严重)** — `SELECT u.name ... WHERE EXISTS (SELECT 1 FROM o WHERE o.user_id = u.id)` 返回空:引擎按 SELECT 列投影后外层行缺 `id` 键,`$col` 绑定为 null。关联子查询路径强制 `plan.columns=['*']` 取完整行;非 JOIN 路径 SELECT 列带表前缀(`u.name`)统一剥离
- **HAVING 标量子查询失效(静默返回空)** — `HAVING SUM(o.amount) > (SELECT AVG(amount) FROM orders)``$subquery` 未解析 + 聚合表达式键(`SUM(o.amount)`)与别名键(`spent`)不匹配。HAVING 先 `resolveSubqueries`,GROUP BY 结果维护「表达式键→别名键」映射做归一化
### Changed
- 版本号升至 v0.4.0
- 测试 837 → **876**(44 → 46 套件),覆盖率 81.1% → **81.5%**Lines 84.7%
---
## [0.3.2] - 2026-08-08
### Added
- **CASE WHEN 用于 WHERE** — `WHERE CASE WHEN cond THEN a ELSE b END = 'x'` 逐行求值(含 NOT/AND/OR 组合)
- **CASE WHEN 用于聚合** — `SUM(CASE WHEN age > 18 THEN 1 ELSE 0 END)``COUNT/AVG/MIN/MAX(CASE...)``GROUP BY + CASE` 非聚合列
- **JOIN 哈希连接** — 等值 ON + 右表主键/索引列时,收集左表连接值 → 一次 `$in` 查询 → 哈希映射匹配,替代嵌套循环(INNER/LEFT JOINO(N+M));不适用自动回退
- **多标签页同步** — `multiTabSync: true` 配置 + BroadcastChannel
- SQL 写语句(INSERT/UPDATE/DELETE/DDL)自动广播表变更
- Table API 写入(insert/insertMany/update/delete/clear/drop)自动广播
- 其他标签页收到 `external` 变更事件(订阅者可见)+ Hybrid 内存自动重载
- `db.broadcastChange(table)` 手动广播
- **IndexedDB schema 持久化** — `__metona_schema` store 保存完整列定义,reopen(页面刷新/重连)后恢复;旧数据回退为"主键 + 索引 + 样例推断"
### Fixed
- **IndexedDB reopen 后 schema 丢失(严重)** — schema 仅存内存缓存,close 后重开连接表结构即丢失(INSERT 列映射截断、UPDATE 全列失效)。v0.3.2 持久化 + 重建
- **where-matcher 顶层 $not 不生效** — `NOT (expr)` 解析为顶层 `{ $not }` 但被当作字段匹配,始终返回 true
- **CASE 剥离后空 `$not` 恒假** — 引擎层执行 `NOT(true)` 过滤掉所有行
### Changed
- 版本号升至 v0.3.2
---
## [0.3.1] - 2026-08-08
### Added
- **CASE WHEN 表达式(SELECT 列)** — `CASE WHEN cond THEN value [WHEN ...] [ELSE value] END [AS alias]`
- 多 WHEN 按顺序匹配、ELSE 缺省返回 null
- THEN/ELSE 值支持字面量(字符串/数字/布尔/null)与列引用
- 条件支持 AND/OR/比较运算符/IS NULL 等完整 WHERE 语法
- 引擎层返回原始行供求值(避免投影丢失条件列),与普通列混合投影
- **JOIN + 关联子查询** — JOIN 结果的 WHERE 支持 `EXISTS (SELECT ... WHERE o2.x = u.y)``$col` 引用外层行,逐行绑定求值
- **WAL 批量组提交** — `WAL.appendBatch()` 合并多条记录为一次底层写入;引擎 insert/update/delete 批量记录后一次落盘(写放大从 O(N) 降为 O(1))
- **`parseWhereCondition()` 导出** — 独立条件表达式解析(CASE WHEN 求值内部使用)
### Fixed
- **LSM pending frozen 可见性** — 异步 flush 链期间,中间冻结的 MemTable 仅存在于闭包中,读取路径不可见导致数据缺失。新增 `frozenMemtables` 列表,get/rangeScan/getAllEntries 遍历所有 pending frozen(新→旧)
- **flush 缓存裁剪与预加载竞争** — flush 完成时的 LRU 裁剪会驱逐 prefetch 刚加载的 SSTable,导致扫描静默丢数据。prefetchRange/prefetchKeys 先等待 flush 链完成再加载
- **CASE 值解析顺序** — `true`/`false`/`null` 字面量被误判为列引用
### Changed
- 版本号升至 v0.3.1
---
## [0.3.0] - 2026-08-08
### Added
- **SQL 多语句支持** — `parseAll()` 解析分号分隔的多条语句,`db.query()` 顺序执行并返回最后一条结果(`parse()` 保持单语句兼容)
- **事务语句(TCL** — `BEGIN` / `COMMIT` / `ROLLBACK`(支持可选 TRANSACTION 关键字),直接驱动引擎层事务,五引擎可用
- **INSERT INTO ... SELECT** — 支持 `INSERT INTO t (cols) SELECT ...`(含 WHERE 过滤、列映射)
- **UNION / UNION ALL** — 支持去重合并与链式组合(`A UNION B UNION C`),右侧结果按位置投影到左侧列结构
- **CREATE INDEX / DROP INDEX** — 动态二级索引:
- `CREATE [UNIQUE] INDEX idx ON table (col)` / `DROP INDEX idx ON table (col)`
- 五引擎实现:Memory 哈希索引 / IndexedDB 版本升级建 `idx_col` / OPFS 委托 / Hybrid 双写 / Aria 动态 LSM 索引
- Aria 主键索引受保护(不可 DROP);CREATE INDEX 对已有数据立即建索引(回填),后续写入自动同步
- **EXISTS / NOT EXISTS** — 支持关联子查询(`WHERE EXISTS (SELECT 1 FROM o WHERE o.user_id = u.id)`),逐行绑定外层上下文求值
- **别名 WHERE 修复** — `WHERE u.age > 20`(主表别名前缀)此前不工作,v0.3.0 字段名规范化(JOIN 路径不受影响)
- **`SELECT 1` 常量列** — 数字字面量可作为 SELECT 列(EXISTS 子查询常见用法)
- **benchmark 页面** — `site/benchmark.html` 浏览器内实测 Memory / Aria 引擎 × 1K/10K/50K 行的 INSERT/PK 查询/索引查询/UPDATE/DELETE 吞吐(ops/sec
- **`parseAll` / 新 AST 类型导出** — `SelectUnionStatement` / `CreateIndexStatement` / `DropIndexStatement` / `BeginTransactionStatement`
### Changed
- 版本号升至 v0.3.0
### Fixed
- `NOT EXISTS` 解析丢 EXISTS token 导致 parse 失败
---
## [0.2.6] - 2026-08-08
### Fixed
- **AriaEngine 二级索引 SSTable 互相覆盖(严重)** — 主 LSM 与二级索引 LSM 此前共享同一 `sstableStore`id 空间 + meta 列表),`allocateId()` 返回 `Date.now()` 且各 LSM 从同一 meta 恢复 `nextSSTableId`,高频写入时 id 冲突导致索引数据文件覆盖主数据、meta 与数据错配。v0.2.6 按命名空间隔离(文件前缀 + meta key + 独立 id 序列),索引 LSM 各持独立 store
- **LSM 多版本读取顺序错误(严重)** — `levels[]` 数组按 `push` 追加(新文件在尾部),但 `get()`/`rangeScan` 从头遍历(旧文件优先),同一 key 跨多次 flush 更新后读到旧值;`MergeIterator` 去重只取"先出堆者",不保证最新来源。v0.2.6 改为"数组头部即最新"(`unshift` + 按 id 降序加载),MergeIterator 显式取 sourceIndex 最小者
- **LZ4 往返不一致** — ① 匹配长度超过 19 字节被截断但 `si` 跳过全部匹配,解压丢数据;② matchField=0 的组合 token 与纯字面量 token 格式歧义;③ 字面量累积超过 15 字节后组合 token 永远无法输出;④ 输出缓冲区 `maxOut` 不足导致越界写入;⑤ 压缩失败时返回原样 input,解压端无法区分。v0.2.6 重写压缩器(长匹配分段、纯字面量 token 语义明确、动态上限)
- **React/Vue 集成运行时 ReferenceError** — `useDatabase`/`useSqlarkDatabase``import type` 导入 `MetonaSqlark` 但运行时 `new MetonaSqlark()``@ts-nocheck` 掩盖此 bug,实际使用必炸。改为 value import
- **sstableCache 无限增长** — SSTable 缓存无内存上限且 `init()` 预加载全部数据,大库 OOM。v0.2.6 引入 LRU 容量上限(`bufferPoolPages` × `pageSize`),查询前异步预加载兜底(修复缓存未命中静默返回 null 丢数据的隐患)
- **integrations 子路径缺失** — README 宣称 `metona-sqlark/react``metona-sqlark/vue` 可用但 `exports` 未声明,现已补齐(react/vue 为 peer dependencyrollup external
### Changed
- **Buffer Pool 配置真正生效** — `bufferPoolPages` 从无效配置变为 SSTable 缓存容量上限(默认 256 页 ≈ 1MB 可控内存)
- **LZ4 格式变更** — 旧版本压缩数据与新解压器不兼容,使用 `compression: true` 的用户需重新导入数据
- **SSTable 持久化串行化** — flush/compaction 改为串行链(`flushChain`),id 分配与持久化顺序一致,消除 fire-and-forget 竞态
- 文档数据修正 — 测试 761(41 套件)、覆盖率 81.1%(此前 91.0% 为排除 Aria 模块的陈旧数据)、体积 ~105KB / gzip ~27KB
### Added
- **AriaEngine 缓存内存上限测试** — 9 个测试:LRU 上限约束、驱逐后全表/PK/索引查询完整性、UPDATE/DELETE 全量作用、写路径上限、命名空间隔离回归、多版本回归、tombstone 回归
- **LZ4 往返一致性测试** — 6 个往返测试(重复/文本/随机/边界长度/15 字节边界/长匹配)
- **CryptoManager 加解密测试** — 7 个测试(往返/错误密码/不同 salt/实例隔离/全局兼容层/4KB 页面)
- **React/Vue hooks 集成测试** — 16 个测试(jest.mock 零依赖:mount 执行、错误路径、refresh、注入防护、数据库初始化)
- **jest.setup.js WebCrypto polyfill** — jsdom 环境补齐 `crypto.subtle` 支持 AES-GCM
---
## [0.2.5] - 2026-07-29
### Fixed
- **版本号统一** — `constants.ts` VERSION 从 `'0.2.0'` 更新为 `'0.2.5'`,修正 `index.ts`/`utils.ts`/`CONTRIBUTING.md` 中过时的版本注释和数据
- **AriaEngine OPFS 后端映射** — `core.ts``mode: 'aria'` + `diskEngine: 'opfs'` 时实际使用 Memory 后端的 bug 已修复
- **`_onError` 接入执行路径** — `query()`/`defineTable()`/`dropTable()`/`transaction()`/`importTable()` 的 catch 路径现在调用 `_onError` 全局错误回调
- **`maxRowsPerQuery` 生效** — `QueryExecutor` 构造时接收 `maxRowsPerQuery` 参数,SELECT 结果在返回前截断
- **WAL full 模式真正同步** — `WAL.append()` 改为 `async``full` 模式下 `await this.store.append()` 真正等待写入完成,不再 fire-and-forget
- **PluginManager.install 传 db 实例** — `register()` 增加可选 `db` 参数,`core.ts` 初始化时传入 `this`,插件可获取 db 引用
### Changed
- **SSTableReader 二分查找统一** — `locateBlockGE`/`locateBlockLE` 从线性扫描改为二分查找,rangeScan 性能在大型 SSTable 下不再退化
- **crypto 实例化** — 全局状态改为 `CryptoManager` 类,每个 AriaEngine 实例可拥有独立加密配置,保留全局函数向后兼容
- **compactLevelSync 接口公开化** — LSM 新增 public `compactLevel()` 方法,`vacuum()` 不再使用 `as any` 绕过 private 访问
- **WAL 大小阈值接入 checkpoint** — `CheckpointManager` 接收 `walSizeThreshold` 参数,WAL 缓冲超阈值时自动触发 checkpoint
### Added
- **ALTER TABLE 语法** — 支持 `ALTER TABLE ... ADD COLUMN` / `DROP COLUMN`(含可选 COLUMN 关键字)
- **TRUNCATE TABLE 语法** — 支持 `TRUNCATE TABLE name` 快速清空表数据
- **MVCC 接入读写路径** — 事务内 insert/update/delete 调用 `mvcc.writeVersion`/`mvcc.deleteVersion`,版本链作为 undo log
- **IndexedDB 索引利用** — `IndexedDBEngine.find` 等值查询时优先使用 IDB 索引(`idx_col` 命名约定),避免全量 getAll
- **SQL 注入防护** — React `useTable` / Vue `useSqlarkTable` 增加表名合法性校验(`/^[a-zA-Z_][a-zA-Z0-9_]*$/`
---
## [0.2.4] - 2026-07-27
### Added
- **二级索引** — 每列可独立维护 LSM Tree 索引,`$eq`/`$in`/`$gt`/`$lt` 走索引 O(log n)
- **MVCC 接入引擎** — MVCCManager 正式投产,替换 ad-hoc txnSnapshot
- **MVCC 自动 GC** — 每 10 次 checkpoint 自动回收过旧版本(保留最新 100 个)
- **Bloom Filter 序列化** — 写入 SSTable footer + 读取时加载 + 查询时 probe 快速否定
- **WAL 大小阈值** — `walSizeThreshold` 配置(默认 16MB),超阈值强制 checkpoint
- **内存预算** — `maxMemoryMB` 配置(默认 64MB
### Changed
- **WAL 默认同步模式** — `walSyncMode``'batch'` 改为 `'full'`,消除 crash 丢数据风险
- **查询优化** — `tryIndexLookup` 扩展支持非 PK 列索引查找
---
## [0.2.3] - 2026-07-27
### Fixed
- **RB-Tree fixDelete 完整实现** — 补全标准红黑树删除修复,保证 O(log n)
- **LSM SSTable 缓存预热** — `init()` 预加载所有 SSTable,消除 cache miss
- **OPFS 数据恢复** — `open()` 自动从 OPFS 文件加载已有表数据到内存
- **Aria WAL 事务恢复** — 两阶段恢复:仅回放已提交事务,未提交数据不回放
- **LZ4 格式修复** — 重写 token 格式,消除中间字面量 token 歧义
---
## [0.2.2] - 2026-07-27
### Added
- **OPFS 自研存储后端** — AriaEngine 新增 `OPFSBackend`,纯浏览器文件系统 API,零 IndexedDB 依赖
- 每个 key 对应一个二进制文件,存储在 `navigator.storage.getDirectory()`
- 支持 read/write/delete/list/exists/clear 完整接口
- 页面文件(4KB)、WAL 日志、Schema、SSTable 全部存储为独立文件
- `{ dbName }/pg_1`, `{ dbName }/__wal_0`, `{ dbName }/__aria_schemas` ...
### Changed
- `AriaEngineConfig.storageBackend` 已支持 `'opfs'``AriaEngine.open()` 自动选择 OPFSBackend
---
## [0.2.1] - 2026-07-27
### Fixed
- **WAL CRC 校验验证** — 恢复时计算并验证 CRC32,损坏记录自动跳过并告警
- **Hybrid 引擎提交顺序** — commit 先写磁盘再写内存,磁盘失败回滚内存,消除数据不一致风险
- **RESTRICT 外键行为修正** — 检测到引用行时抛出 `FOREIGN_KEY_VIOLATION`,不再静默保留孤儿
- **SSTable rangeScan 边界保护** — 空索引或 startBlockIdx > endBlockIdx 时安全返回
### Added
- **ColumnDef 约束激活** — `maxLength`/`min`/`max` 约束在 `checkFieldType` 中正式生效
- **查询结果上限** — `DatabaseConfig.maxRowsPerQuery`(默认 0 不限制),防止超大结果集 OOM
- **onError 全局回调** — 配置中的 `onError` 回调实际接入 CRUD 异常路径
- **debug 调试模式** — `DatabaseConfig.debug: true` 输出 `[MetonaSqlark:name]` 前缀的结构化日志
- **浏览器兼容性声明** — README 增加 Chrome 80+/Firefox 80+/Safari 14+/Edge 80+ 支持矩阵
### Changed
- MemoryEngine RESTRICT 外键行为:从"不级联保留孤儿"改为"禁止删除抛异常"
- Hybrid 引擎 commitTransaction 磁盘优先于内存
- DB_DEFAULTS 新增 `maxRowsPerQuery: 0``debug: false`
---
## [0.2.0] - 2026-07-27
### Added
- **AriaEngine 自研存储引擎** — 基于 LSM-Tree 的页面式存储引擎,19 个新模块,~3500 行 TypeScript
- **页面格式层** (`page/`): 4KB Slotted Page 编解码、Tuple 二进制序列化、CRC32 校验
- **Buffer Pool** (`buffer/`): LRU 页面缓存 + 驱逐策略,可控内存占用
- **LSM-Tree 索引** (`index/`): MemTable (红黑树) + 多级 SSTable + Leveled Compaction
- **Bloom Filter** (`index/bloom.ts`): FNV-1a + Murmur 双哈希,快速键否定判定
- **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**v0.8.0 澄清**:此处当时宣称的「快照隔离」从未实现 —— 版本链仅作事务内 undo,事务串行)
- **存储后端** (`store/`): IndexedDB / Memory 双后端抽象
- **LZ4 压缩** (`compression/lz4.ts`): 简易页面级压缩
- **`mode: 'aria'`** — 新增存储模式,可通过 `MetonaSqlark.create({ mode: 'aria' })` 激活
- **Schema 持久化** — 表结构自动保存到 `__aria_schemas`,重启自动恢复
- **SSTable 元数据管理** — SSTable 索引信息持久化,启动时自动扫描加载
- **事务感知 CRUD** — insert/update/delete 在事务中缓冲到 snapshotcommit 批量写入 LSM
- **244 个 AriaEngine 专项测试** — 覆盖生命周期/表管理/CRUD/事务/持久化/SQL 集成/页面格式/LSM/压缩
### Changed
- `StorageMode` 类型新增 `'aria'`
- `STORAGE_MODES` 数组新增 `'aria'`
- `createEngine()` 支持 `mode: 'aria'` 分支
- 测试从 318 → **526**,套件从 20 → **27**
- 新增 7 个模块级测试文件:`aria-page``aria-index``aria-sstable``aria-buffer``aria-wal-mvcc``aria-compress``aria`
- LRUList 修复 size 追踪 bug
- WAL 存储改为按记录独立 key(避免拼接缓冲区越界)
- CheckpointManager 解耦 BufferPool 依赖
### Fixed
- **LZ4 压缩无限循环** — 字面量分支在发现匹配后回退导致 litLen=0 死循环,CI 卡死根因
- **SSTableReader.get() 自比较 bug** — 参数 key 被循环变量遮蔽导致永远返回第一条
- **CheckpointManager 测试 null 引用** — 改为 Mock 对象避免 TypeError 导致进程无法退出
- **IndexedDB 持久化测试** — 替换为 Memory Backend 验证,消除 fake-indexeddb timer 堆积
- **LSM.flush() 不必要 setTimeout** — 替换为 Promise.resolve(),消除额外 timer 延迟
---
## [0.1.14] - 2026-07-26
### Fixed
- **IndexedDB 事务原子性**: `flushToIDB` 改为单 IDB 事务包裹 clear+insert,消除崩溃丢数据风险
- **多标签页冲突**: `open()` 添加 `onversionchange` 监听,其他标签页升级版本时自动关闭过期连接
- **Memory 引擎幂等**: `open()` 重复调用安全无副作用
- 关闭时清理 `onversionchange` 监听器,防止内存泄漏
---
## [0.1.13] - 2026-07-26
### Added
- **事务回滚机制**: `IStorageEngine` 新增 `beginTransaction/commitTransaction/rollbackTransaction` 接口
- MemoryEngine: 快照式回滚(深拷贝 tables/schemas/indexes
- IndexedDBEngine: 延迟写入策略(事务中仅写内存,commit 批量刷 IDB)
- OPFSEngine: 快照式回滚(commit 批量写文件)
- HybridEngine: 同时代理内存+磁盘引擎事务
- **子查询支持**: SQL Parser + Executor 支持 `IN (SELECT ...)``op (SELECT ...)` 子查询
- AST 新增 `SubqueryExpression` 类型
- Parser 在 IN 和比较运算符后检测子查询
- Executor 新增 `resolveSubqueries()` 递归解析,自动执行子查询并替换为具体值
- **外键级联操作**: ColumnDef 新增 `onDelete/onUpdate` 选项
- 支持 `CASCADE`(递归级联删除)、`SET NULL``RESTRICT`
- SQL Parser 解析 `REFERENCES table(col) ON DELETE CASCADE ON UPDATE CASCADE`
- MemoryEngine 内置 `cascadeDelete()` 递归级联逻辑
- **连接池管理**: `MetonaSqlark.connect()` 静态方法
- 同名数据库复用已打开实例,引用计数管理
- `db.disconnect()` 释放连接,归零自动 close
- `MetonaSqlark.disconnectAll()` 强制关闭所有连接
### Changed
- `TransactionManager.execute()` 调用引擎层 begin/commit/rollback 实现真正原子性
- IndexedDBEngine CRUD 事务感知:活跃事务中延迟 IDB 写入
- ASTColumnDef / astColumnToColumnDef 支持 references/onDelete/onUpdate 透传
### Fixed
- IndexedDBEngine 事务中 find/count 从内存缓存读取(保证读到未提交变更)
- SQL Parser parseColumnDef 循环条件扩展,避免 REFERENCES 语法解析中断
---
## [0.1.12] - 2026-07-26
### Added
- `MeSqlark` 别名导出,与 `MetonaSqlark` 完全等价
- `metona-sqlark.cjs.js` CommonJS 构建产物
- 完善的 `where-matcher.ts` 消除 220+ 行重复代码
- `$col` 列引用支持 JOIN ON 条件
- `$and/$or` 嵌套支持(字段级 + 顶层)
- `$not` 逻辑非操作符
- `$like` 正则缓存加速
- `projectColumns` 列投影支持 `table.column` 格式
- `DISTINCT` 去重支持(O(n) 时间,列值拼接优化)
- 测试覆盖率提升至 93.46%(264 测试/15 套件)
### Changed
- `where-matcher` 统一 MemoryEngine / IndexedDBEngine / Executor 的 WHERE 逻辑
- JOIN ON 匹配支持 `$col` 语法
- LIKE 正则改为缓存式编译
- Executor DISTINCT 用列值拼接代替 JSON.stringify
- JOIN 嵌套循环连接优化:避免 ON 时对象扩散
### Fixed
- IndexedDBEngine `update/delete` 方法正确同步内存缓存
- OPFSEngine `getTableNames` 兼容 `entries()` 返回值
- SQL Parser 正确处理字符串转义字符
- SQL Parser 正确处理表别名(无 AS 关键字)
- SQL Parser 解析 `IS NULL` / `IS NOT NULL` / `NOT LIKE`
- `peekTokenIs` 方法正确暴露给语法分析
- 多处边界条件空值/假值处理
---
## [0.1.11] - 2026-07-25
### Added
- React 集成 hooks: `useQuery`, `useTable`, `useDatabase`
- Vue 集成 composables: `useSqlarkQuery`, `useSqlarkTable`, `useSqlarkDatabase`
- 发布订阅系统: `subscribe()`, `emit()`
- 数据迁移系统: `addMigration()`, `migrateTo()`
- 导入导出: `exportTable()`, `exportAll()`, `importTable()`
- 完整 SQL 解析器: `Lexer` + `Parser`(递归下降)
- SQL 支持: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
- JOIN 支持: INNER, LEFT, RIGHT, CROSS
- GROUP BY + HAVING + 聚合函数 (COUNT, SUM, AVG, MIN, MAX)
- DISTINCT, ORDER BY, LIMIT, OFFSET
- WHERE 条件: AND, OR, NOT, IN, LIKE, IS NULL
- Query Builder 链式 API: `select().where().orderBy().limit().execute()`
- 插件系统: 14 种生命周期钩子 + PluginManager
- Metadata 系统: ColumnDef 完整约束 (type/primaryKey/required/unique/index/default/references/maxLength/min/max)
### Changed
- 架构重构为分层设计:Engine → QueryExecutor → Table/QueryBuilder → SQL Parser → Public API
- AST 作为统一中间表示,SQL 和 QueryBuilder 行为完全一致
- 测试框架完善:core / edge / groupby / join / query-system / sql / table / engine / transaction / plugin / hybrid 全覆盖
---
## [0.0.1] - 2026-07-24
### Added
- 初始项目骨架
- TypeScript 配置(严格模式)
- Rollup 构建(UMD / ESM / CJS / min / .d.ts
- Jest + jsdom 测试环境
- ESLint + @typescript-eslint
- Gitea Actions CI 工作流
- 示例站点(index / demo / docs
- 构建脚本 build.sh