docs(G6): 宣称与实现一致性收口 —— priority 真正生效、MVCC/backup/引擎数/打包 全部对齐

独立核验(12 条宣称逐条对源码验证)发现 5 处**硬伤**与 2 处**数字过期**,
本提交按"能改代码就让宣称成立、改不动就如实描述"的原则全部收口。

让实现符合文档(2 处):
1. **插件 priority 此前不生效** — `register()` 虽按 priority 插入数组,但
   `install()` 在 register 内**立即**执行,因此 install 与钩子顺序 = config 数组
   顺序(实测 priority low=1/high=100/mid=50 时钩子按 low→high→mid 触发,
   只有 `getPlugins()` 是 high,mid,low)。而 README/CONTRIBUTING/constants
   一直宣称"越大越先执行"。
   现在 Core 注册前按 priority **稳定降序**排序(同优先级保持数组顺序),
   install 与钩子都按优先级执行 → 宣称成立。新增
   `tests/v080-plugin-priority.test.ts` 锁定 install 顺序、钩子顺序、稳定性、缺省值。
2. **连接池静态方法不在类型系统里** — `MetonaSqlark.connect/disconnect/
   disconnectAll/getActiveConnections` 由 connection-manager 用
   `as unknown as Record<string, unknown>` 注入,README 的连接池表格在
   TypeScript 下全部 TS2339。现在在类上声明为可选静态成员,注入处去掉断言。

如实描述(3 处):
3. **MVCC 快照隔离**(README 三处 + 实现对照)— `snapshotLsn` / `prevVersion`
   只写不读,事务读走 `txnSnapshot`+LSM,commit 即清理版本链,并发
   `beginTransaction` 抛 `TX_ACTIVE`。改为"快照回滚(事务串行,非 MVCC 隔离)",
   并在 README 架构图与维护语句表里同步措辞。
4. **"存储引擎(5 种)"** — 实际是 4 种模式 + 3 种后端,引擎类只有 4 个
   (Memory / KVStore / Hybrid / Aria),OPFS 是后端而非引擎。标题与条目已改写,
   并写明"`disk`/`hybrid` 恒用 KVStore"。
5. **`diskEngine` 生效范围** — 仅 `mode:'aria'` 生效;`constants.ts` 的注释
   此前写成"仅 mode='disk'|'hybrid' 时生效"(正好写反),已改正;README 配置表、
   快速开始示例与 Aria 示例同步标注。

数字口径统一(可复现):
- 测试 1872(90 套件)+ 14 e2e,另 4 个重型套件在独立 CI job 串行运行;
- 覆盖率 语句 90.43% / 分支 82.21% / 函数 94.27% / 行 93.44%;
- README 明确写出**产出这些数字的完整命令**(与 CI 常规 job 一致),
  并要求改动覆盖范围/阈值时同步更新表格(G5)。
- CHANGELOG 0.8.0 条目与 site 首页/文档页同步。

另修 **CONTRIBUTING 的钩子契约**:明确写出"返回值被忽略(不能取消/改写)、
就地改参数在 Table API 生效、抛异常可取消、SQL 路径的 beforeInsert 收到副本"
—— 此前只写 "allow intercepting",容易被理解为返回值可改变行为。

验证:全量 90 套件 / 1872 测试通过(+4 重型套件);覆盖率四项均高于阈值;
typecheck(src+tests)、lint、build 零错误零告警;e2e 14 项通过;dist 已重建。
This commit is contained in:
thzxx
2026-09-15 08:06:23 +08:00
parent d14663ef80
commit 799560ea05
26 changed files with 35817 additions and 106 deletions
+66 -27
View File
@@ -1,14 +1,16 @@
# MetonaSqlark
<p align="center">
<img src="https://img.shields.io/badge/version-0.7.4-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/version-0.8.0-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
<img src="https://img.shields.io/badge/coverage-90.1%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1304%20passed-success?style=flat-square" alt="tests">
<img src="https://img.shields.io/badge/coverage-90.43%25%20stmts-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1872%20passed-success?style=flat-square" alt="tests">
</p>
> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API
> 5 种存储引擎可选,内置自研 LSM-Tree 存储引擎(AriaEngine)。
> 4 种存储模式(memory / disk / hybrid / aria+ 3 种后端(OPFS / KVStore / Memory)可选,
> `aria` 模式内置自研 LSM-Tree 存储引擎(AriaEngineWAL + 页面化 + 可选压缩/加密,
> 事务用未提交快照回滚)。
> 零运行时依赖,浏览器 / Node.js 开箱即用。
---
@@ -35,18 +37,21 @@
- **完整 SQL** — SELECTJOIN / 子查询 / 派生表 / UNION / GROUP BY / HAVING / DISTINCT / CASE WHEN / EXISTS / BETWEEN / NULLS 排序)、INSERT...SELECT、UPDATE/DELETE 子查询、ALTER TABLE、TRUNCATE TABLE、CREATE INDEX、事务语句(BEGIN / COMMIT / ROLLBACK / SAVEPOINT)、维护语句(EXPLAIN / ANALYZE / REINDEX / VACUUM
- **Query Builder** — 链式 `.select().where().innerJoin().orderBy().limit().execute()`,类型安全
- **流式查询** — `queryStream` / `table().stream()` 逐行回调,Aria 引擎真惰性扫描(limit 提前终止,不物化结果集)
- **事务** — 四引擎事务原子性 + 自动回滚;Aria 用 MVCC 快照隔离,事务读写不互斥
- **事务** — 四引擎事务原子性 + 自动回滚;Aria 用引擎级事务 + 未提交快照(`txnSnapshot`)回滚。**同一实例同时只允许一个事务**(并发 `beginTransaction``TX_ACTIVE`)—— 不是 MVCC 快照隔离
- **外键级联** — `ON DELETE` / `ON UPDATE` 支持 `CASCADE` / `SET NULL` / `RESTRICT`(含主键变更级联、级联环路保护)
- **数据迁移** — 版本化迁移(持久化到库内,重启不重跑)、导入导出、在线一致性备份
- **数据迁移** — 版本化迁移(持久化到库内,重启不重跑)、单表/全库导入导出、`backup()` 全库导出(逐表读取,非跨表快照 —— 见「已知限制」)
- **连接池** — `MetonaSqlark.connect()` 单例复用,引用计数自动关闭
**存储引擎(5 种)**
**存储模式(4 种)+ 存储后端(3 种)**
- `memory` — 纯内存,测试 / 缓存
- `disk` — 自研 **KVStore 事务引擎**v0.6.0 起替代 IndexedDB):多 key 原子写、快照 + 日志崩溃恢复
- `hybrid` — write-through 双写,读走内存
- `aria` — 自研 **AriaEngine**LSM-Tree + WAL + MVCC),后端可选 OPFS / KVStore / Memory
- **KVStore** — 日志结构化事务 KV 引擎,可独立用作 aria 后端
- `aria` — 自研 **AriaEngine**LSM-Tree + WAL + 页面化 + 可选压缩/加密),
后端可选 `diskEngine: 'opfs' | 'kv' | 'memory'`
> `disk` 与 `hybrid` 的磁盘侧**恒用自研 KVStore**`diskEngine` 项只对 `mode: 'aria'`
> 生效,其余模式忽略该配置 —— 已在类型注释中写明)。
**生产级可靠性**
@@ -58,8 +63,11 @@
**生态**
- **React / Vue 集成** — `useQuery` / `useSqlarkQuery`开箱即用 hooks
- **插件系统** — 14 种生命周期钩子(beforeInsert / afterQuery / ...),按优先级注册
- **React / Vue 集成** — `useQuery` / `useSqlarkQuery` 等 hooks(构建为 `dist/react.js` /
`dist/vue.js`,配套手写精确类型声明;`react` / `vue` 为**可选 peer dependency**
不装也不影响核心库使用)
- **插件系统** — 14 种生命周期钩子(beforeInsert / afterQuery / ...),
`priority` 越大越先执行(同优先级保持注册顺序)
- **旧库迁移** — `migrateFromIndexedDB()` 一键把旧 IndexedDB 库导入新引擎
- **浏览器兼容** — Chrome 102+ / Firefox 111+ / Safari 15.2+ / Edge 102+ / Node.js 16+
@@ -96,7 +104,8 @@ import { MetonaSqlark } from '@metona-team/metona-sqlark';
const db = await MetonaSqlark.create({
name: 'my-app',
mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' | 'aria'
diskEngine: 'opfs', // 'opfs' | 'kv'(自研 KVStore 后端)| 'memory'
// 仅 mode:'aria' 时生效(Aria 的存储后端);disk/hybrid 恒用自研 KVStore
diskEngine: 'opfs', // 'opfs' | 'kv' | 'memory'
});
// 定义表 — 支持约束与外键级联
@@ -178,7 +187,7 @@ await db2.disconnect(); // 引用计数 -1
|------|------|------|------|
| `name` | `string` | `'metona-sqlark'` | 数据库名称 |
| `mode` | `'memory' \| 'disk' \| 'hybrid' \| 'aria'` | `'hybrid'` | 存储模式 |
| `diskEngine` | `'opfs' \| 'memory' \| 'kv'` | `'opfs'` | 磁盘引擎(aria 模式下为存储后端;`'kv'` = 自研 KVStore |
| `diskEngine` | `'opfs' \| 'memory' \| 'kv'` | `'opfs'` | **仅 `mode: 'aria'` 生效**Aria 的存储后端;`'kv'` = 自研 KVStore)。`disk`/`hybrid` 恒用 KVStore,忽略此项 |
| `version` | `number` | `1` | 版本号 |
| `maxRowsPerQuery` | `number` | `0` | 查询结果行数上限(0 = 不限) |
| `debug` | `boolean` | `false` | 调试模式 |
@@ -227,7 +236,7 @@ await db2.disconnect(); // 引用计数 -1
| `db.clearAll()` | 清空全部数据与表结构(保留库本身) |
| `db.exportTable(name)` / `db.exportAll()` | 导出数据 JSON |
| `db.importTable(name, data)` | 导入数据 |
| `db.backup()` | 在线备份:全库一致性快照 |
| `db.backup()` | 导出全库数据(**逐表读取**,见下方"已知限制" |
| `db.addMigration(v, fn)` / `db.migrateTo(v)` | 版本化数据迁移(版本持久化,重启不重跑) |
| `db.subscribe(table, fn)` | 订阅表变更(返回退订函数) |
| `db.on(hook, fn)` | 注册生命周期钩子(14 种) |
@@ -240,7 +249,7 @@ await db2.disconnect(); // 引用计数 -1
| `EXPLAIN SELECT ...` | 输出查询计划(type/table/where/usingIndex/estimatedRows/actualTimeMs | 全部 |
| `ANALYZE [TABLE] name` | 收集表统计信息(行数/行大小/索引深度/列基数) | Aria |
| `REINDEX [TABLE] name` | 重建表二级索引 | Aria |
| `VACUUM` | 压缩 LSM + 清理 MVCC 碎片 | Aria |
| `VACUUM` | 压缩 LSM + 清理版本碎片 | Aria |
| `SAVEPOINT name` / `ROLLBACK TO name` / `RELEASE name` | 嵌套事务保存点 | Aria |
> 不支持的引擎执行维护语句抛 `NOT_SUPPORTED`。
@@ -250,7 +259,9 @@ await db2.disconnect(); // 引用计数 -1
> IndexedDB 已从引擎中完全移除。旧版本(v0.5.x 及更早)的 disk 模式用户可通过一次性迁移工具导入:
```typescript
// 两种等价写法(v0.8.0 起主入口也导出,避免深路径依赖)
import { migrateFromIndexedDB } from '@metona-team/metona-sqlark/migration';
// import { migrateFromIndexedDB } from '@metona-team/metona-sqlark';
const target = await MetonaSqlark.create({ name: 'my-app-new', mode: 'disk' });
const result = await migrateFromIndexedDB({
@@ -273,6 +284,12 @@ const result = await migrateFromIndexedDB({
| `MetonaSqlark.disconnectAll()` | 强制关闭所有连接 |
| `MetonaSqlark.getActiveConnections()` | 获取活跃连接列表 |
> **实现说明**:这四个静态方法由 `connection-manager` 模块在**模块加载时注入**
> (主入口 `src/index.ts` 以 side-effect 方式 `import './connection-manager'`),
> 因此从包入口引入即可用,无需额外操作。类型上(v0.8.0 起)声明为可选静态成员;
> 若在**未加载该模块**的自定义构建里调用,其值为 `undefined` 并抛 `TypeError`
> —— 不会静默无效。
---
## 存储引擎
@@ -280,7 +297,7 @@ const result = await migrateFromIndexedDB({
| 特性 | Memory | Disk (KVStore) | Hybrid | Aria |
|------|--------|----------------|--------|------|
| **持久化** | ❌ 重启丢失 | ✅ KVStore(OPFS / 内存介质) | ✅ 内存 + 磁盘 | ✅ 后端决定 |
| **事务** | ✅ 快照回滚 | ✅ 单日志记录原子写 | ✅ 双引擎(磁盘优先) | ✅ MVCC 快照隔离 |
| **事务** | ✅ 快照回滚 | ✅ 单日志记录原子写 | ✅ 双引擎(磁盘优先) | ✅ 快照回滚(事务串行,非 MVCC 隔离 |
| **二级索引** | ✅ Hash | ✅ Hash(重启恢复) | ✅ Hash | ✅ LSM(重启恢复) |
| **查询性能** | O(1) PK | O(1) PK(内存热路径) | O(1) PK | O(log n) |
| **数据上限** | 内存 | 磁盘可用 | 磁盘可用 | 内存 |
@@ -294,14 +311,14 @@ const result = await migrateFromIndexedDB({
- **临时数据 / 单元测试** → `memory`
- **标准前端持久化**(替代 IndexedDB)→ `disk`KVStore,多 key 原子事务,10 万级验证)
- **内存速度 + 磁盘持久化** → `hybrid`write-through,读走内存)
- **大规模 / 需要自研引擎可控性** → `aria`LSM-Tree + WAL + MVCC + 加密 + 页面化存储)
- **大规模 / 需要自研引擎可控性** → `aria`LSM-Tree + WAL + 加密 + 页面化存储)
---
## AriaEngine 自研存储引擎
AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念:
**LSM-Tree 索引 + WAL 崩溃恢复 + MVCC 事务 + 页面化物理存储 + 全库加密**
**LSM-Tree 索引 + WAL 崩溃恢复 + 快照回滚事务 + 页面化物理存储 + 全库加密**
```typescript
const db = await MetonaSqlark.create({
@@ -335,7 +352,7 @@ const rows = await db.query('SELECT * FROM users');
│ +SSTable │ +FileManager │ +CRC-32 │
│ (4KB 页面) │ │ +空洞检测 │
├────────────────┼───────────────┼────────────┤
MVCC 事务 │ Bloom Filter │ LZ4 压缩 │
事务/版本管理 │ Bloom Filter │ LZ4 压缩 │
│ 快照隔离 │ 二级索引 LSM │ +大小头 │
│ +Savepoint │ (每列独立) │ │
├────────────────┴───────────────┴────────────┤
@@ -355,11 +372,11 @@ const rows = await db.query('SELECT * FROM users');
| **WAL** | 分片文件 `__wal_%06d.bin` + 真追加;标准 CRC32 记录校验;full/batch/none 三模式;空洞检测截断;16MB 阈值自动 checkpoint(活跃事务期间不截断) |
| **崩溃恢复** | 打开时完整性校验(损坏 SSTable 自愈清理 + 整文件 CRC-32)、WAL 恢复、恢复后自动重建二级索引;`repair()` 清理孤儿页面与残留 |
| **全库加密** | `encryption.password` → EncryptedBackend 透明加解密(WAL/SSTable/Schema/元数据全密文);PBKDF2 派生 + salt 持久化;密码错误/篡改 → `ARIA_DECRYPT_ERROR` |
| **MVCC** | 版本链 + 快照隔离,事务读写不互斥,自动 GC |
| **MVCC** | 版本链仅作事务内 undo(提交即清理,**无快照隔离**;事务串行);自动 GC |
| **二级索引** | 每列独立 LSM Tree,支持等值/范围扫描,跨重启恢复,WAL 恢复后自动重建 |
| **Bloom Filter** | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时快速否定 |
| **多标签页锁** | Web Locks 库级独占锁,第二个标签页抛 `ARIA_LOCKED`;不支持的环境降级无锁并告警 |
| **维护语句** | ANALYZE(表统计)/ REINDEX(重建索引)/ VACUUM(压缩 + MVCC GC/ EXPLAIN(查询计划) |
| **维护语句** | ANALYZE(表统计)/ REINDEX(重建索引)/ VACUUM(压缩 + 版本 GC/ EXPLAIN(查询计划) |
### AriaEngine 配置项
@@ -401,7 +418,7 @@ const { data, loading, error, refresh } = useSqlarkQuery(db, 'SELECT * FROM user
npm install # 安装依赖
npm run dev # 开发模式(localhost:3001
npm run build # 生产构建(生成 dist/
npm test # 运行测试(1304 用例 · 76 套件)
npm test # 运行测试(1872 用例 · 90 套件;+4 个重型套件)
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build
npm run lint # 代码检查
npm run typecheck # 类型检查
@@ -413,17 +430,39 @@ npm run typecheck # 类型检查
| 指标 | 数值 |
|------|------|
| 测试用例 | 1304+12 Playwright e2e |
| 测试套件 | 76 |
| 覆盖率 | 90.1% |
| 测试用例 | 187290 套件)+ 14 Playwright e2e,另 4 个重型套件在独立 CI job 串行运行 |
| 语句覆盖率 | 90.43%7835/8664 |
| 分支覆盖率 | 82.21%4092/4977 |
| 函数覆盖率 | 94.27%1103/1170 |
| 行覆盖率 | 93.44%7103/7601 |
| SQL 关键字 | 72 |
| 存储引擎 | 5Memory / KVStore / OPFS / Hybrid / Aria |
| 存储模式 | 4`memory` / `disk` / `hybrid` / `aria` |
| 存储后端 | 3OPFS / KVStore / Memory),Aria 引擎另有 LSM-Tree + WAL + 页面化 |
| 运行时依赖 | 0 |
### 已知限制(v0.7.4
> **覆盖率口径**`collectCoverageFrom = src/**/*.ts`,仅排除两个**纯类型声明**文件
> `engine/interface.ts`、`query/ast.ts` —— 它们只有 interface/type,可执行语句为 0
> 纳入统计只会稀释分母)。CI 常规 job 带 `--coverage` 运行,
> `jest.config.cjs` 的 `coverageThreshold` 为 statements 90 / branches 82 /
> functions 94 / lines 93,任一项不达标即失败 —— **门槛不达标不允许发版**。
>
> 上述四个数字由**与 CI 常规 job 完全相同的命令**产出(可复现):
> ```bash
> npx jest --coverage --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race'
> ```
> 修改覆盖范围、阈值或测试选择时**必须同步更新本表**(G5 门禁要求
> README 数字与 CI 产出一致)。
### 已知限制(v0.8.0
- **单列主键** — 复合主键暂不支持(建表时显式 `SCHEMA_ERROR`),列入 v0.8 路线图
- **写语句关联引用** — UPDATE/DELETE 的 WHERE 支持非关联子查询(`IN (SELECT)` / 标量子查询),关联引用(`$col` / 关联 EXISTS)显式抛 `NOT_SUPPORTED`(不静默)
- **`backup()` 不是跨表一致性快照** — 实现为**逐表读取**(Aria 走引擎级 `backup()`
其余引擎回退 `exportAll()`)。若在备份过程中有并发写入,不同表之间可能来自
不同时间点(单表内部是一致的)。需要强一致备份时请先 `close()` 或用
`db.transaction()` 包住调用(事务期间并发写会被 `TX_ACTIVE` 拒绝)。
* 说明:此前文档宣称"在线一致性快照",但引擎层没有实现跨表快照原语 ——
与其保留一个不成立的宣称,这里按实际行为描述(真快照列入后续版本)。
- **建表 UNIQUE 约束** — 不可经 `DROP INDEX` 解除(对齐 SQLite,需重建表);仅 `CREATE UNIQUE INDEX` 添加的约束可随索引删除
- **唯一值交换更新** — 同一语句内两行互换唯一列值(A:x→y, B:y→x)保守拒绝(最终状态合法但报 `UNIQUE_VIOLATION`
- **主键非空** — 主键列强制非空(SQL 语义 PK 隐含 NOT NULL),`INSERT`/`UPDATE` 置 null/undefined 抛 `VALIDATION_ERROR`