# 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 → **1980(92 套件)+ 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_` 单一提交点**:页面水位 + 各命名空间 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` 此前每个索引列一次 getAllRows(N 列 × 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 语句级部分提交(P1,Memory/KVStore/Hybrid)** — 第 N 行主键重复/唯一冲突 抛错时前 N-1 行已提交(v0.7.2 只修了 UPDATE,INSERT 漏修)。改为两阶段: 先全量预检(主键批内 Set 互查 + 索引查 + 唯一批内互查),任何一行失败整句不执行 - **Aria insert 批内主键重复部分提交(P1)** — PK 重复检查在写入循环内:第 N 行 重复抛错时前 N-1 行已 put LSM 且其 WAL 记录随 appendBatch 一起丢失 → 部分提交 + 内存/WAL 不一致。PK 检查移入批预检阶段(与 v0.6.2 的 unique 预检同一阶段) - **索引列 IS NULL 恒空(P1,Memory/KVStore/Hybrid)** — `tryIndexLookup` 中 `colIndex.get(null)` 恒 undefined → `return []` 短路全表扫描(AriaEngine v0.6.2 已修,Memory 漏修)。null/undefined 条件跳过索引路径回退全表扫描 - **delete RESTRICT 预检破坏索引(P1,Memory/KVStore/Hybrid)** — `removeIndexEntries` 在收集阶段(RESTRICT 预检前)执行:预检抛错时行未删但索引条目已删 → 唯一约束 永久失效 + 索引查询丢行。索引清理移到预检通过之后(与 AriaEngine 对齐) - **queryStream 子查询静默空结果(P1)** — WHERE 含 `$subquery`/`$exists`/`$col` 时 未回退物化:引擎层 matchWhere 的 `$in` 遇未解析的 `$subquery` 对象返回 false → 所有行被静默过滤(`$col` 会抛 QUERY_ERROR)。streamable 判定增加递归检测, 回退物化路径(resolveSubqueries 正确解析) - **ALTER DROP 索引列残留(P2,Memory/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_ERROR;executor 侧 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` 解析为顶层 $and,Memory/Aria 的 tryIndexLookup 只查顶层键 → 永远全表扫描,索引形同虚设。递归展开 $and 等值 条件下推($or/$not 保守跳过,命中后全条件 matchWhere 过滤,子集语义安全); EXPLAIN usingIndex 同步递归识别 $and 嵌套(与引擎行为对齐) - **ANALYZE 统计不含二级索引(P3)** — indexDepth/sstableCount/memtableSize 只统计 主 LSM,多索引表严重低估。汇总该表全部二级索引 LSM - **Vue useSqlarkDatabase 卸载不 close(P3)** — 组件卸载后实例永不关闭(连接/锁/ 后端句柄泄漏)。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.getWALEstimatedSize(CheckpointManager 自带 真实缓冲字节估算,此方法无调用者) --- ## [0.7.2] - 2026-08-13 ### 语句级原子性 / 事务 DDL 语义统一 / 约束与绑定硬化 > 深度审计第五阶段:修复 UPDATE 语句级部分提交、事务内 DDL 回滚残留、 > SET NULL 级联绕过 required 约束、参数绑定注释误判等 6 项问题, > Hybrid 写穿透失败补偿,四引擎语义对齐。 ### Fixed - **UPDATE 语句级部分提交(P1,Memory/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 下是 undefined(TypeError)。 类增加静态 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 Map(P2)** — 驱逐仅清 LRU 链表,pages Map 保留全部 历史页面 → 内存无限增长(256 页 ≈ 1MB 预算形同虚设)。EvictionManager 增加 onRemove 回调,驱逐时同步从 pages Map 移除(容量约束真实生效,重载仍正确) - **MVCC 已提交版本不清理(P2)** — commit 仅标记 committed,versionStore 随写入量 无限增长(行数据双份常驻)。快照读取已移除(v0.5.1),版本链仅作事务内 undo: commit 时直接清理本事务版本(rollback/discardVersions 语义不变,gc 保留未提交链裁剪) - **LSM.flush 重复入链(P2)** — freezeMemtable 会把同一张 immutable 再次入链 → 重复 SSTable(2 行 flush 出 3 个文件);且 frozenMemtables 在链执行前被清空 → 并发读短暂看不到冻结数据。修复:入链后立即置空 immutable(防重复), frozenMemtables 保持到链排空后再清理 - **rollbackToSavepoint 不重建二级索引(P2)** — 事务内直写索引 LSM,savepoint 回滚 只还原快照 → 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 并发写 meta(P0)** — `flush()` 对 immutable/memtable 的剩余落盘直接 await(不在链上),与链上 compaction 并发写 SSTable meta:compaction 产物 `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 快照损坏水位 bug(P0)** — 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 API(Node/测试用 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(浏览器)/ SharedMemoryBackend(Node/测试,跨实例共享模拟持久化) - **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 ` 语法(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: 引擎事务读取走 txnSnapshot(MVCC 仅作 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/isCryptoEnabled(v0.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-SHA256(100000 迭代)派生密钥,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 })` + seek,O(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 API(Chrome 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 被截断 → 线性格式整体错乱。格式 v2(magic "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 版本管理缺陷导致重启必报 VersionError(P0)** — 建表每张版本号 +1 而 `open()` 用 `config.version`,重启后版本过低直接失败。`open()` 遇 VersionError 自动用无版本参数探测库当前实际版本并以该版本重开 - **`create({ version: 0 })` 抛原生 TypeError(P0)** — 版本 < 1 统一归一化为 1,不再直接炸 `indexedDB.open(name, 0)` - **WAL 写丢失(P1)** — 连续快速写入 200 条 close+重开实测丢 4 条。WAL 恢复不再依赖 count 计数,改为扫描全部 `__wal_*` 键(数值排序拼接),count 键只作序号分配器;配合原子写入彻底消除 count/记录竞态 - **AriaEngine.close() 不 checkpoint,WAL 无限重放(P1)** — close 前补 `wal.checkpoint()` 截断,下次打开不再重放已落盘的历史记录 - **引擎内部错误不包装 DatabaseError(P1)** — 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)** — checkpoint(tick/forceCheckpoint)在活跃事务中途执行时 truncate WAL,BEGIN/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 JOIN,O(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 dependency,rollup 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 在事务中缓冲到 snapshot,commit 批量写入 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