方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 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 已重建。
555 lines
30 KiB
Markdown
555 lines
30 KiB
Markdown
# MetonaSqlark
|
||
|
||
<p align="center">
|
||
<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.59%25%20stmts-brightgreen?style=flat-square" alt="coverage">
|
||
<img src="https://img.shields.io/badge/tests-1980%20passed-success?style=flat-square" alt="tests">
|
||
</p>
|
||
|
||
> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API,
|
||
> 4 种存储模式(memory / disk / hybrid / aria)+ 3 种后端(OPFS / KVStore / Memory)可选,
|
||
> `aria` 模式内置自研 LSM-Tree 存储引擎(AriaEngine:WAL + 页面化 + 可选压缩/加密,
|
||
> 事务用未提交快照回滚)。
|
||
> 零运行时依赖,浏览器 / Node.js 开箱即用。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [核心特性](#核心特性)
|
||
- [安装](#安装)
|
||
- [快速开始](#快速开始)
|
||
- [API 速览](#api-速览)
|
||
- [存储引擎](#存储引擎)
|
||
- [AriaEngine 自研存储引擎](#ariaengine-自研存储引擎)
|
||
- [框架集成](#框架集成)
|
||
- [开发](#开发)
|
||
- [项目状态](#项目状态)
|
||
- [License](#license)
|
||
|
||
---
|
||
|
||
## 核心特性
|
||
|
||
**数据库能力**
|
||
|
||
- **完整 SQL** — SELECT(JOIN / 子查询 / 派生表 / 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 用引擎级事务 + 未提交快照(`txnSnapshot`)回滚。**同一实例同时只允许一个事务**(并发 `beginTransaction` 抛 `TX_ACTIVE`)—— 不是 MVCC 快照隔离
|
||
- **外键级联** — `ON DELETE` / `ON UPDATE` 支持 `CASCADE` / `SET NULL` / `RESTRICT`(含主键变更级联、级联环路保护)
|
||
- **数据迁移** — 版本化迁移(持久化到库内,重启不重跑)、单表/全库导入导出、`backup()` 全库导出(逐表读取,非跨表快照 —— 见「已知限制」)
|
||
- **连接池** — `MetonaSqlark.connect()` 单例复用,引用计数自动关闭
|
||
|
||
**存储模式(4 种)+ 存储后端(3 种)**
|
||
|
||
- `memory` — 纯内存,测试 / 缓存
|
||
- `disk` — 自研 **KVStore 事务引擎**(v0.6.0 起替代 IndexedDB):多 key 原子写、快照 + 日志崩溃恢复
|
||
- `hybrid` — write-through 双写,读走内存
|
||
- `aria` — 自研 **AriaEngine**(LSM-Tree + WAL + 页面化 + 可选压缩/加密),
|
||
后端可选 `diskEngine: 'opfs' | 'kv' | 'memory'`
|
||
|
||
> `disk` 与 `hybrid` 的磁盘侧**恒用自研 KVStore**(`diskEngine` 项只对 `mode: 'aria'`
|
||
> 生效,其余模式忽略该配置 —— 已在类型注释中写明)。
|
||
|
||
**生产级可靠性**
|
||
|
||
- **崩溃恢复** — WAL 原子写入 + CRC-32 完整性校验(SSTable 整文件 + WAL 记录)、分片空洞与记录损坏**如实上报**(`ARIA_WAL_GAP` / `droppedWALRecords`,拒绝静默截断)、打开时损坏自愈、`repair()` 清理重建
|
||
- **全库加密** — AES-256-GCM 透明加密(PBKDF2 密钥派生 + 密码验证 + 篡改检测),`encryption.password` 一键启用
|
||
- **多标签页独占锁** — Web Locks API,第二个标签页打开同一库抛 `ARIA_LOCKED`
|
||
- **数据安全** — 输入校验(`required` / `maxLength` / `min` / `max`)、SQL 注入防护、Bloom Filter 快速否定
|
||
- **大表性能** — 10 万行级验证;批量插入组提交;SSTable 4KB 页面化 + BufferPool LRU 缓存
|
||
|
||
**生态**
|
||
|
||
- **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+
|
||
|
||
---
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
npm install @metona-team/metona-sqlark
|
||
```
|
||
|
||
> 私有 registry(一次性配置):
|
||
> ```bash
|
||
> npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/
|
||
> ```
|
||
|
||
### CDN / 直接下载
|
||
|
||
```html
|
||
<!-- UMD 格式,暴露 window.MetonaSqlark 与 window.MeSqlark -->
|
||
<script src="https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"></script>
|
||
```
|
||
|
||
或从 [`dist/`](./dist/) 下载:`metona-sqlark.js`(UMD 开发版)/ `metona-sqlark.min.js`(压缩版:实测 251,109 字节 / gzip 63,145 字节)/ `metona-sqlark.esm.js` / `metona-sqlark.cjs` / `metona-sqlark.d.ts`
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
```typescript
|
||
import { MetonaSqlark } from '@metona-team/metona-sqlark';
|
||
// 别名:import { MeSqlark } from '@metona-team/metona-sqlark';
|
||
|
||
const db = await MetonaSqlark.create({
|
||
name: 'my-app',
|
||
mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' | 'aria'
|
||
// 仅 mode:'aria' 时生效(Aria 的存储后端);disk/hybrid 恒用自研 KVStore
|
||
diskEngine: 'opfs', // 'opfs' | 'kv' | 'memory'
|
||
});
|
||
|
||
// 定义表 — 支持约束与外键级联
|
||
await db.defineTable('users', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string', required: true },
|
||
age: { type: 'number', default: 0 },
|
||
email: { type: 'string', unique: true, index: true },
|
||
});
|
||
await db.defineTable('orders', {
|
||
id: { type: 'string', primaryKey: true },
|
||
user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' },
|
||
amount: { type: 'number' },
|
||
});
|
||
|
||
// SQL 查询
|
||
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30, 'alice@demo.com')");
|
||
const rows = await db.query('SELECT * FROM users WHERE age > 18 ORDER BY name');
|
||
|
||
// 参数化查询(v0.7.0)— 位置参数 `?`,安全编码杜绝 SQL 注入
|
||
await db.query("INSERT INTO users VALUES (?, ?, ?, ?)", ['2', "O'Brien", 25, 'ob@demo.com']);
|
||
const row2 = await db.query('SELECT * FROM users WHERE name = ?', ["O'Brien"]);
|
||
|
||
// Query Builder
|
||
const result = await db.table('users')
|
||
.select(['name', 'age'])
|
||
.where({ age: { $gt: 18 } })
|
||
.orderBy('age', 'desc')
|
||
.limit(10)
|
||
.execute();
|
||
|
||
// JOIN
|
||
await db.query(`SELECT u.name, o.amount FROM users u
|
||
INNER JOIN orders o ON u.id = o.user_id WHERE o.amount > 100`);
|
||
|
||
// 子查询 / UNION / EXISTS
|
||
await db.query(`SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)`);
|
||
await db.query(`SELECT name FROM users WHERE city = 'Beijing'
|
||
UNION SELECT name FROM users WHERE age < 30`);
|
||
await db.query(`SELECT * FROM users u WHERE EXISTS
|
||
(SELECT 1 FROM orders o WHERE o.user_id = u.id)`);
|
||
|
||
// GROUP BY / CASE WHEN
|
||
await db.query(`SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1`);
|
||
await db.query(`SELECT name, CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users`);
|
||
|
||
// ALTER TABLE / TRUNCATE TABLE
|
||
await db.query('ALTER TABLE users ADD COLUMN phone STRING');
|
||
await db.query('TRUNCATE TABLE old_logs');
|
||
|
||
// 事务 — 失败自动回滚
|
||
await db.transaction(async (trx) => {
|
||
await trx.table('users').insert({ id: '3', name: 'Charlie' });
|
||
await trx.table('orders').insert({ id: 'o1', user_id: '3', amount: 99 });
|
||
});
|
||
|
||
// 流式查询 — 大表逐行回调
|
||
await db.queryStream('SELECT * FROM logs WHERE level = \'error\'', (row) => {
|
||
processRow(row);
|
||
});
|
||
|
||
// 崩溃恢复自愈
|
||
await db.repair();
|
||
await db.clearAll();
|
||
|
||
// 连接池 — 同库复用
|
||
const db2 = await MetonaSqlark.connect({ name: 'my-app', mode: 'hybrid' });
|
||
// db2 === db
|
||
await db2.disconnect(); // 引用计数 -1
|
||
```
|
||
|
||
---
|
||
|
||
## API 速览
|
||
|
||
### 数据库配置
|
||
|
||
| 属性 | 类型 | 默认 | 说明 |
|
||
|------|------|------|------|
|
||
| `name` | `string` | `'metona-sqlark'` | 数据库名称 |
|
||
| `mode` | `'memory' \| 'disk' \| 'hybrid' \| 'aria'` | `'hybrid'` | 存储模式 |
|
||
| `diskEngine` | `'opfs' \| 'memory' \| 'kv'` | `'opfs'` | **仅 `mode: 'aria'` 生效**(Aria 的存储后端;`'kv'` = 自研 KVStore)。`disk`/`hybrid` 恒用 KVStore,忽略此项 |
|
||
| `version` | `number` | `1` | 版本号 |
|
||
| `maxRowsPerQuery` | `number` | `0` | 查询结果行数上限(0 = 不限) |
|
||
| `debug` | `boolean` | `false` | 调试模式 |
|
||
| `multiTabSync` | `boolean` | `false` | 多标签页同步(BroadcastChannel) |
|
||
| `plugins` | `MetonaPlugin[]` | — | 插件列表 |
|
||
| `onReady` | `(db) => void` | — | 就绪回调 |
|
||
| `onError` | `(error) => void` | — | 全局错误回调 |
|
||
| `aria` | `AriaEngineConfig` | — | AriaEngine 配置透传(`walSyncMode` / `encryption` / `pageStorage` / `compression` 等) |
|
||
|
||
### ColumnDef 列定义
|
||
|
||
| 属性 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `type` | `'string' \| 'number' \| 'boolean' \| 'date' \| 'json'` | 数据类型(必填) |
|
||
| `primaryKey` | `boolean` | 主键(建表时至少一个) |
|
||
| `required` | `boolean` | 必填 |
|
||
| `unique` | `boolean` | 唯一约束 |
|
||
| `index` | `boolean` | 创建二级索引 |
|
||
| `default` | `unknown` | 默认值 |
|
||
| `references` | `string` | 外键引用 `'table.column'` |
|
||
| `onDelete` | `'CASCADE' \| 'SET NULL' \| 'RESTRICT'` | 删除级联 |
|
||
| `onUpdate` | `'CASCADE' \| 'SET NULL' \| 'RESTRICT'` | 更新级联(更新主键时触发) |
|
||
| `maxLength` | `number` | 字符串最大长度 |
|
||
| `min` / `max` | `number` | 数字最小值 / 最大值 |
|
||
|
||
### WHERE 操作符
|
||
|
||
| 操作符 | 含义 | 操作符 | 含义 |
|
||
|--------|------|--------|------|
|
||
| `$eq` / 直接值 | 等于 | `$gt` / `$gte` | 大于 / 大于等于 |
|
||
| `$ne` | 不等于 | `$lt` / `$lte` | 小于 / 小于等于 |
|
||
| `$in` / `$nin` | 在列表中 | `$like` | 模糊匹配 |
|
||
| `$and` / `$or` / `$not` | 逻辑组合 | | |
|
||
|
||
### 核心方法
|
||
|
||
| 方法 | 说明 |
|
||
|------|------|
|
||
| `db.query(sql)` / `db.query(sql, params)` | 执行 SQL(支持分号多语句;v0.7.0 位置参数 `?` 绑定) |
|
||
| `db.queryStream(sql, onRow)` | 流式查询(逐行回调,不物化) |
|
||
| `db.table(name)` | 获取表操作对象(`insert` / `select` / `update` / `delete` / `count` / `stream` / `clear` / `drop`) |
|
||
| `db.defineTable(name, cols)` | 定义表结构 |
|
||
| `db.dropTable(name)` / `db.getTableNames()` | 删除表 / 列出表 |
|
||
| `db.transaction(fn)` | 执行事务(自动回滚) |
|
||
| `db.repair()` | 崩溃恢复自愈:校验清理损坏数据、重建索引 |
|
||
| `db.clearAll()` | 清空全部数据与表结构(保留库本身) |
|
||
| `db.exportTable(name)` / `db.exportAll()` | 导出数据 JSON |
|
||
| `db.importTable(name, data)` | 导入数据 |
|
||
| `db.backup()` | 导出全库数据(**逐表读取**,见下方"已知限制") |
|
||
| `db.addMigration(v, fn)` / `db.migrateTo(v)` | 版本化数据迁移(版本持久化,重启不重跑) |
|
||
| `db.subscribe(table, fn)` | 订阅表变更(返回退订函数) |
|
||
| `db.on(hook, fn)` | 注册生命周期钩子(14 种) |
|
||
| `db.close()` | 关闭数据库 |
|
||
|
||
### 维护语句(SQL 入口)
|
||
|
||
| 语句 | 说明 | 支持引擎 |
|
||
|------|------|----------|
|
||
| `EXPLAIN SELECT ...` | 输出查询计划(type/table/where/usingIndex/estimatedRows/actualTimeMs) | 全部 |
|
||
| `ANALYZE [TABLE] name` | 收集表统计信息(行数/行大小/索引深度/列基数) | Aria |
|
||
| `REINDEX [TABLE] name` | 重建表二级索引 | Aria |
|
||
| `VACUUM` | 压缩 LSM + 清理版本碎片 | Aria |
|
||
| `SAVEPOINT name` / `ROLLBACK TO name` / `RELEASE name` | 嵌套事务保存点 | Aria |
|
||
|
||
> 不支持的引擎执行维护语句抛 `NOT_SUPPORTED`。
|
||
|
||
### 旧库迁移(v0.6.0)
|
||
|
||
> 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({
|
||
dbName: 'my-app',
|
||
engine: 'disk', // 仅支持旧 disk 模式(IndexedDBEngine)
|
||
target,
|
||
onProgress: (done, total, table) => console.log(`迁移 ${done}/${total}: ${table}`),
|
||
});
|
||
// result: { migratedTables, rowCount, skippedTables }
|
||
```
|
||
|
||
> 注:旧 aria 模式库为引擎私有格式(SSTable/WAL),无法按行迁移——请从应用层 `exportAll()` 后重新导入。
|
||
|
||
### 连接池
|
||
|
||
| 静态方法 | 说明 |
|
||
|------|------|
|
||
| `MetonaSqlark.connect(config)` | 获取或创建数据库实例(单例复用) |
|
||
| `MetonaSqlark.disconnect(name)` | 释放连接(引用计数 -1) |
|
||
| `MetonaSqlark.disconnectAll()` | 强制关闭所有连接 |
|
||
| `MetonaSqlark.getActiveConnections()` | 获取活跃连接列表 |
|
||
|
||
> **实现说明**:这四个静态方法由 `connection-manager` 模块在**模块加载时注入**
|
||
> (主入口 `src/index.ts` 以 side-effect 方式 `import './connection-manager'`),
|
||
> 因此从包入口引入即可用,无需额外操作。类型上(v0.8.0 起)声明为可选静态成员;
|
||
> 若在**未加载该模块**的自定义构建里调用,其值为 `undefined` 并抛 `TypeError`
|
||
> —— 不会静默无效。
|
||
|
||
---
|
||
|
||
## 存储引擎
|
||
|
||
| 特性 | Memory | Disk (KVStore) | Hybrid | Aria |
|
||
|------|--------|----------------|--------|------|
|
||
| **持久化** | ❌ 重启丢失 | ✅ KVStore(OPFS / 内存介质) | ✅ 内存 + 磁盘 | ✅ 后端决定 |
|
||
| **事务** | ✅ 快照回滚 | ✅ 单日志记录原子写 | ✅ 双引擎(磁盘优先) | ✅ 快照回滚(事务串行,非 MVCC 隔离) |
|
||
| **二级索引** | ✅ Hash | ✅ Hash(重启恢复) | ✅ Hash | ✅ LSM(重启恢复) |
|
||
| **查询性能** | O(1) PK | O(1) PK(内存热路径) | O(1) PK | O(log n) |
|
||
| **数据上限** | 内存 | 磁盘可用 | 磁盘可用 | 内存 |
|
||
| **全库加密** | — | — | — | ✅ AES-GCM |
|
||
| **多标签页锁** | — | — | — | ✅ Web Locks |
|
||
| **适用场景** | 缓存 / 测试 | 标准持久化(替代 IndexedDB) | 速度 + 持久化 | 大规模 / 分析 |
|
||
| **测试覆盖** | 30+ | 50+(含 10 万级压测) | 15+ | 400+ |
|
||
|
||
**选型建议**
|
||
|
||
- **临时数据 / 单元测试** → `memory`
|
||
- **标准前端持久化**(替代 IndexedDB)→ `disk`(KVStore,多 key 原子事务,10 万级验证)
|
||
- **内存速度 + 磁盘持久化** → `hybrid`(write-through,读走内存)
|
||
- **大规模 / 需要自研引擎可控性** → `aria`(LSM-Tree + WAL + 加密 + 页面化存储)
|
||
|
||
---
|
||
|
||
## AriaEngine 自研存储引擎
|
||
|
||
AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念:
|
||
**LSM-Tree 索引 + WAL 崩溃恢复 + 快照回滚事务 + 页面化物理存储 + 全库加密**。
|
||
|
||
```typescript
|
||
const db = await MetonaSqlark.create({
|
||
name: 'my-app',
|
||
mode: 'aria',
|
||
diskEngine: 'opfs', // 存储后端:'opfs' | 'kv' | 'memory'
|
||
aria: {
|
||
walSyncMode: 'full', // 'full' | 'batch' | 'none'
|
||
compression: true, // LZ4 页面压缩
|
||
encryption: { password: 'my-password' }, // 全库 AES-256-GCM 加密
|
||
},
|
||
});
|
||
|
||
// 与其余引擎 API 完全兼容
|
||
await db.defineTable('users', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string', required: true },
|
||
});
|
||
await db.query("INSERT INTO users VALUES ('1', 'Alice')");
|
||
const rows = await db.query('SELECT * FROM users');
|
||
```
|
||
|
||
### 架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────┐
|
||
│ AriaEngine v0.8.0 │
|
||
├──────────────────────────────────────────────┤
|
||
│ LSM-Tree │ Buffer Pool │ WAL │
|
||
│ MemTable │ LRU (256页) │ 分片文件 │
|
||
│ +SSTable │ +FileManager │ +CRC-32 │
|
||
│ (4KB 页面) │ │ +空洞检测 │
|
||
├────────────────┼───────────────┼────────────┤
|
||
│ 事务/版本管理 │ Bloom Filter │ LZ4 压缩 │
|
||
│ 快照回滚 │ 二级索引 LSM │ +大小头 │
|
||
│ +Savepoint │ (每列独立) │ │
|
||
├────────────────┴───────────────┴────────────┤
|
||
│ __aria_manifest 单一提交点(数据→提交→截断)│
|
||
├──────────────────────────────────────────────┤
|
||
│ EncryptedBackend (AES-256-GCM 全库透明加密) │
|
||
├──────────────────────────────────────────────┤
|
||
│ 存储后端: OPFS Backend / KVStore Backend │
|
||
│ Web Locks 多标签页独占锁 │
|
||
└──────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 核心机制
|
||
|
||
| 机制 | 说明 |
|
||
|------|------|
|
||
| **LSM-Tree** | MemTable(红黑树)→ 多级 SSTable,异步 Compaction(从存储兜底加载),写背压 |
|
||
| **页面化存储** | SSTable 存为 4KB 页面(FileManager 分配 pageId + BufferPool LRU 缓存 256 页 ≈ 1MB),`pageStorage` 在 opfs/kv 后端默认启用 |
|
||
| **WAL** | 分片文件 `__wal_%06d.bin` + 真追加;标准 CRC32 记录校验(记录级 CRC 失败会计数并上报 `droppedWALRecords`);full/batch/none 三模式;**LSN 全库单调**(manifest 记高水位);分片号**绝不回退、也绝不低于 manifest 水位**(整体清空后允许复用最后用过的号);内部空洞与记录损坏显式上报(`gaps` / `corruptRecords`),水位从未推进时的前缀缺失同样按空洞上报,水位已推进时前缀缺失视为"已清理的前缀";16MB 阈值自动 checkpoint(活跃事务期间不截断) |
|
||
| **单一提交点** | `__aria_manifest_<gen>`:页面水位 + 各命名空间 SSTable 元数据 + 表结构 + WAL 起始位置 + 待落盘冻结表意图,一次原子提交(头部/载荷双 CRC,先写后验,保留两代)。顺序固定为**数据落盘 → manifest 提交 → 才允许截断 WAL / 删除旧文件**;恢复只认最后一份 CRC 通过的世代,元数据损坏抛 `ARIA_MANIFEST_CORRUPT`(不再静默当空库) |
|
||
| **崩溃恢复** | 打开时完整性校验(整文件 CRC-32;**介质读故障不再被当成"文件不存在"**,抛 `ARIA_SSTABLE_READ_FAILED` 且不误删元数据)、按 LSN 水位重放 WAL、恢复后自动重建二级索引;`getRecoveryReport()` 返回 `{droppedSSTables, dataLossSuspected, walGaps, droppedWALRecords, legacyImported, manifestFallback}`;`repair()` 只在 manifest 健康时回收孤儿页面 |
|
||
| **Compaction** | 整层合并不再"先摘层再合并"(合并期间该层对读者始终可见);底部层原地合并**回收墓碑**(删除密集场景空间不再无界增长);按层 `compacting` 集合(跨层触发不丢失);被取代的 SSTable 进入**退休表**,等更早的读者退出后才物理删除 |
|
||
| **全库加密** | `encryption.password` → EncryptedBackend 透明加解密(WAL/SSTable/Schema/元数据全密文);PBKDF2 派生 + salt 持久化;密码错误/篡改 → `ARIA_DECRYPT_ERROR` |
|
||
| **MVCC** | 版本链仅作事务内 undo(提交即清理,**无快照隔离**;事务串行);自动 GC |
|
||
| **二级索引** | 每列独立 LSM Tree,支持等值/范围扫描,跨重启恢复,WAL 恢复后自动重建 |
|
||
| **Bloom Filter** | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时快速否定 |
|
||
| **多标签页锁** | Web Locks 库级独占锁,第二个标签页抛 `ARIA_LOCKED`;不支持的环境降级无锁并告警 |
|
||
| **维护语句** | ANALYZE(表统计)/ REINDEX(重建索引)/ VACUUM(压缩 + 版本 GC)/ EXPLAIN(查询计划) |
|
||
|
||
### AriaEngine 配置项
|
||
|
||
| 属性 | 类型 | 默认 | 说明 |
|
||
|------|------|------|------|
|
||
| `pageSize` | `number` | `4096` | 页面大小(字节) |
|
||
| `bufferPoolPages` | `number` | `256` | Buffer Pool 页面数(≈1MB) |
|
||
| `memtableSizeThreshold` | `number` | `4MB` | MemTable 刷盘阈值 |
|
||
| `levelSizeMultiplier` | `number` | `10` | LSM 层级容量倍数 |
|
||
| `bloomFilterBitsPerKey` | `number` | `10` | Bloom Filter 每 key 位数 |
|
||
| `maxMemoryMB` | `number` | `64` | 内存预算(MB):主 LSM 估算内存超限时触发 flush + MVCC GC |
|
||
| `walEnabled` | `boolean` | `true` | 是否启用 WAL |
|
||
| `walSyncMode` | `'full' \| 'batch' \| 'none'` | `'full'` | WAL 同步模式 |
|
||
| `checkpointInterval` | `number` | `1000` | Checkpoint 间隔(操作数) |
|
||
| `walSizeThreshold` | `number` | `16MB` | WAL 大小阈值(超则强制 checkpoint) |
|
||
| `compression` | `boolean` | `false` | 是否启用 LZ4 压缩 |
|
||
| `storageBackend` | `'opfs' \| 'kv' \| 'memory'` | `'opfs'` | 存储后端 |
|
||
| `encryption` | `{ password: string }` | — | 全库 AES-256-GCM 加密 |
|
||
| `pageStorage` | `boolean` | 自动(opfs/kv 后端默认启用) | SSTable 页面化存储 |
|
||
|
||
---
|
||
|
||
## 框架集成
|
||
|
||
```tsx
|
||
// React
|
||
import { useQuery, useTable, useDatabase } from '@metona-team/metona-sqlark/react';
|
||
const { data, loading, error, refresh } = useQuery(db, 'SELECT * FROM users');
|
||
|
||
// Vue
|
||
import { useSqlarkQuery, useSqlarkTable, useSqlarkDatabase } from '@metona-team/metona-sqlark/vue';
|
||
const { data, loading, error, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');
|
||
```
|
||
|
||
---
|
||
|
||
## 开发
|
||
|
||
```bash
|
||
npm install # 安装依赖
|
||
npm run dev # 开发模式(localhost:3001)
|
||
npm run build # 生产构建(生成 dist/)
|
||
npm test # 运行测试(1980 用例 · 92 套件;+4 个重型套件)
|
||
python3 scripts/mutation-b6.py # 变异验证:把 B-6 的修复逐项回退,对应用例必须失败
|
||
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build)
|
||
npm run lint # 代码检查
|
||
npm run typecheck # 类型检查
|
||
```
|
||
|
||
---
|
||
|
||
## 项目状态
|
||
|
||
| 指标 | 数值 |
|
||
|------|------|
|
||
| 测试用例 | 1980(92 套件)+ 14 Playwright e2e,另 4 个重型套件在独立 CI job 串行运行 |
|
||
| 语句覆盖率 | 90.59%(8532/9418) |
|
||
| 分支覆盖率 | 82.59%(4452/5390) |
|
||
| 函数覆盖率 | 94.14%(1223/1299) |
|
||
| 行覆盖率 | 93.50%(7727/8264) |
|
||
| SQL 关键字 | 72 |
|
||
| 存储模式 | 4(`memory` / `disk` / `hybrid` / `aria`) |
|
||
| 存储后端 | 3(OPFS / KVStore / Memory),Aria 引擎另有 LSM-Tree + WAL + 页面化 |
|
||
| 运行时依赖 | 0 |
|
||
|
||
> **覆盖率口径**:`collectCoverageFrom = src/**/*.ts`,仅排除两个**纯类型声明**文件
|
||
> (`engine/interface.ts`、`query/ast.ts` —— 它们只有 interface/type,可执行语句为 0,
|
||
> 纳入统计只会稀释分母)。v0.8.0 审查曾发现 `interface.ts` 里混着三个运行时函数
|
||
> (`cloneRow`/`cloneRowFallback`/`cloneRows`)—— 已搬到 `src/engine/row_clone.ts`
|
||
> 并纳入统计(搬完门禁立刻因 functions 93.84% < 94% 失败,补测退化路径后通过)。
|
||
> 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)
|
||
|
||
- **存储布局在 v0.8.0 变更** — 元数据从"每个命名空间一份裸 JSON"(`__aria_lsm_meta*` /
|
||
`__aria_schemas`)收敛为 `__aria_manifest_<generation>`(带世代号与双 CRC)。
|
||
旧库**首次用 v0.8.0 打开时自动迁移**(旧键保留不删),迁移遇到损坏的旧元数据会
|
||
明确报 `ARIA_LEGACY_META_CORRUPT` 而不是当成空库。直接读取这些内部 key 的外部
|
||
脚本需要跟着改(引擎侧无公开 API 依赖它们)。
|
||
* **回退是单向的**:迁移后所有新写入只进 manifest,旧的 `__aria_lsm_meta*` /
|
||
`__aria_schemas` 停留在迁移那一刻。用旧版本打开同一个库会看到**迁移时刻的旧
|
||
视图**(不是"数据都在"),继续写入还会让两套布局分叉 —— 需要回退旧版本时,
|
||
先用 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`
|
||
- **多实例写入保护依赖 Web Locks** — AriaEngine 用 Web Locks 做库级独占:第二个实例
|
||
打开同一个库时抛 `ARIA_LOCKED`,这是**唯一受支持**的多标签页写入方式。运行环境
|
||
没有 Web Locks 时该保护会自动降级为"提交点冲突检测"(manifest 世代号单调 + 提交前
|
||
检查是否存在别的实例提交的更新世代 → 抛 `STALE_INSTANCE`),但降级只保证
|
||
**不静默覆盖别人的提交**,不保证多实例写入的数据完整性:被拒绝的那一方此前
|
||
已写入自己 WAL 分片的记录,可能被胜出实例的 checkpoint 当作可回收前缀清掉。
|
||
结论:**不要在没有 Web Locks 的环境里让两个实例同时写同一个库**;需要并发访问时
|
||
由应用层串行化(如 SharedWorker / 主标签页代理)。
|
||
- **manifest 体积随 SSTable 数量增长** — 单一提交点把全部命名空间的 SSTable 元数据
|
||
(键范围、页面 id 列表、大小)+ 表结构 + WAL 水位写进**同一个文件**,每次提交
|
||
整体重写(保留两代)。因此元数据量与已落盘 SSTable 数成正比:长期高频写入、
|
||
层级很多且迟迟不合并的库,其 manifest 会明显大于数据本身之外的一般预期。当前
|
||
没有"元数据分层/增量"机制,`vacuum()` 合并层级是唯一的收敛手段(列入后续版本)。
|
||
- **尾部 WAL 分片丢失无法从介质自身识别** — 分片内部空洞(中间缺号)与记录级 CRC
|
||
损坏都会被上报;但如果**最后一个**分片整个消失,介质上没有任何"它本该存在"的证据
|
||
(manifest 只记 `startSegment`/`nextLsn`,不记最后分片号),此时只能靠
|
||
`ARIA_WRITE_LOST`(有未落盘冻结表却重放不到任何记录)兜住"确定丢数据"的情况。
|
||
|
||
### 浏览器兼容性
|
||
|
||
| 浏览器 | 最低版本 | Memory | Disk (KVStore) | Aria (OPFS) |
|
||
|--------|----------|--------|----------------|-------------|
|
||
| Chrome / Edge | 102+ | ✅ | ✅ | ✅ |
|
||
| Firefox | 111+ | ✅ | ✅ | ✅ |
|
||
| Safari | 15.2+ | ✅ | ✅ | ✅ |
|
||
| Node.js | 16+ | ✅ | ✅(内存介质) | ✅(内存介质) |
|
||
|
||
> **OPFS**:基础 API(`createWritable` 原子写)在 Chromium 102+ / Firefox 111+ / Safari 15.2+ 均支持。
|
||
> 无跨文件事务,AriaEngine 以 WAL 分片单文件原子写 + manifest 单一提交点保证崩溃一致性;
|
||
> 分片空洞与记录级损坏都会被**显式上报**(恢复报告 + 告警),不做静默截断。
|
||
>
|
||
> **多标签页保护**:AriaEngine 通过 Web Locks 获取库级独占锁,第二个标签页打开同一库抛 `ARIA_LOCKED`。
|
||
|
||
---
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
src/
|
||
├── index.ts # 入口(MetonaSqlark + MeSqlark)
|
||
├── core.ts # 主类(create/query/table/transaction/migration...)
|
||
├── constants.ts # 配置类型 + DatabaseError + VERSION
|
||
├── connection-manager.ts # 连接池管理
|
||
├── engine/ # 存储引擎
|
||
│ ├── memory.ts # MemoryEngine(内存 + 快照事务)
|
||
│ ├── kvstore_engine.ts # KVStoreEngine(disk 模式,v0.6.0 替代 IndexedDB)
|
||
│ ├── kvstore/ # 自研 KVStore(日志 + 快照 + 原子写)
|
||
│ └── aria/ # AriaEngine(LSM-Tree + WAL + MVCC + 页面化 + 加密)
|
||
│ ├── index/ # MemTable / SSTable / LSM / Bloom / MergeIterator
|
||
│ ├── wal/ # WAL 分片 + checkpoint
|
||
│ ├── store/ # OPFS / KVStore / 加密 backend + FileManager
|
||
│ ├── buffer/ # BufferPool + LRU 驱逐
|
||
│ └── transaction/ # MVCC
|
||
├── hybrid/ # 混合引擎(write-through)
|
||
├── migration/ # 旧 IndexedDB 数据迁移工具(一次性)
|
||
├── query/ # AST + Builder + Compiler + Executor
|
||
├── sql/ # Lexer + Parser(递归下降,72 关键字)
|
||
├── table/ # 表管理 + Schema 校验
|
||
├── transaction/ # 事务管理(自动回滚)
|
||
├── plugin/ # 插件系统(14 hooks)
|
||
└── integrations/ # React / Vue hooks
|
||
```
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
MIT © [MetonaTeam](https://git.metona.cn/MetonaTeam/MetonaSqlark)
|