Files
MetonaSqlark/README.md
T
thzxx a56a496148
CI / test (18.x) (push) Successful in 11m2s
CI / test (20.x) (push) Successful in 10m55s
CI / test (22.x) (push) Successful in 10m49s
CI / test (24.x) (push) Successful in 10m53s
CI / e2e (push) Successful in 9m51s
docs: README/site 全量同步 v0.6.1(版本徽章/套件数/残留 IndexedDB 描述清理/OPFSEngine 列移除/v0.6.1 里程碑/级联环与原子事务特性)
2026-08-10 14:35:47 +08:00

506 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MetonaSqlark
<p align="center">
<img src="https://img.shields.io/badge/version-0.6.1-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-89.3%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1049%20passed-success?style=flat-square" alt="tests">
</p>
> 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研存储引擎**。
---
## ✨ 特性
- 🚀 **AriaEngine 自研存储引擎** — LSM-Tree + **SSTable 4KB 页面化物理存储**BufferPool LRU 缓存 + FileManager 页面管理,v0.5.0 真实接入)、WAL 分片文件(真追加 + 空洞检测)、LZ4 压缩
- 💾 **OPFS 自研存储后端** — 纯浏览器文件系统,零 IndexedDB 依赖,二进制页面文件,schema 持久化,单文件 COW 原子写,崩溃残留自动清理
- 🔒 **生产级数据安全** — WAL 原子写入 + 标准 CRC32 完整性校验(SSTable 整文件校验 + WAL 记录校验)、**全库 AES-256-GCM 透明加密**PBKDF2 密钥派生 + salt 持久化 + 密码验证,v0.5.0 真实接线)、`RESTRICT` 外键约束、崩溃恢复自愈
- 🔐 **多标签页独占锁** — Web Locks API,第二个标签页打开同一库抛 `ARIA_LOCKED`v0.5.0
- 🛠 **维护语句 SQL 入口**`EXPLAIN`/`ANALYZE`/`REINDEX`/`VACUUM`/`SAVEPOINT` 原生 SQL 支持(v0.5.1 补齐)
- 🛡 **输入校验全覆盖**`maxLength`/`min`/`max` 约束、类型检查、必填验证
- 💾 **多引擎架构** — Memory / **KVStore**(自研 KV 引擎)/ OPFS / Hybrid(write-through) / Aria 五种模式(v0.6.0: IndexedDB 完全移除)
- 💾 **KVStore 自研 KV 引擎** — 日志结构化事务存储:多 key 原子写(putMany/deleteMany 单记录原子追加)、快照 checkpoint、崩溃两阶段恢复、CRC-32 自愈;替代 IndexedDBv0.6.0
- 🔄 **旧库一键迁移**`migrateFromIndexedDB()` 把旧 IndexedDB 数据(schema/索引/行)导入新引擎(v0.6.0)
- 📝 **完整 SQL 支持** — SELECT/JOIN/子查询/GROUP BY/HAVING/ORDER BY/LIMIT/BETWEEN/IF NOT EXISTS/ALTER TABLE/TRUNCATE TABLE/UNION/INSERT...SELECT/事务语句/CREATE INDEX/EXISTSv0.3.0+ CASE WHEN/哈希连接/组提交(v0.3.1+ 多标签页同步(v0.3.2
- 🚰 **流式查询**`queryStream`/`stream()` 逐行回调,Aria LSM 惰性扫描不物化结果集(v0.4.0)
- 🧩 **派生表**`FROM (SELECT ...)` 子查询作为行源,多列 ON 哈希连接,COUNT(DISTINCT)NULLS FIRST/LASTv0.4.0
- 🔗 **Query Builder API** — 链式 `.select().where().orderBy().limit().execute()`
- 🔄 **事务回滚** — Memory/KVStore/Hybrid/Aria 四引擎事务原子性,自动回滚,MVCC 版本链接入读写路径
- 🔗 **外键级联** — ON DELETE + ON UPDATECASCADE / SET NULL / RESTRICT)全引擎支持,支持更新主键(v0.4.2)
- 🛡 **崩溃恢复自愈** — 残缺 SSTable 打开自动跳过、`db.repair()` 自愈(含孤儿页面清理与残留清理)、`db.clearAll()` 重置、迁移版本持久化(v0.4.2/v0.5.0
- 🧵 **关闭时序与后台任务加固** — 后台 flush/compaction 串行入队(close 排空后才关闭存储,失败显式报告 `ARIA_BACKGROUND_ERROR`
- 📏 **SSTable 编码修复** — 块大小按 UTF-8 字节精确计算、长度字段 u32、v1/v2 双格式兼容(v0.4.4
- 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证
-**性能优化** — SSTableReader 二分查找统一、KVStore 内存热路径 O(1) 读、crypto 实例化避免全局状态
- 🛡 **级联环路保护** — A→B→A 循环引用 + CASCADE 不无限递归(visited 保护,全引擎)(v0.6.1
- ⚛️ **多表事务真原子** — 事务 commit / 级联更新合并单条日志记录(崩溃无部分提交)(v0.6.1)
- 🌐 **浏览器兼容** — Chrome 102+ / Firefox 111+ / Safari 15.2+ / Edge 102+ / Node.js 16+
- 🧪 **1049 测试 · 89.3% 覆盖率** — 65 套件 + 12 个 Playwright 真实 Chromium e2e(含崩溃注入),生产级质量保证
---
## 📦 安装
```bash
npm install @metona-team/metona-sqlark
```
> 如果提示找不到包,先配置 scope 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 开发版(含 sourcemap
- `metona-sqlark.min.js` — UMD 压缩版(~105KBgzip ~27KB
- `metona-sqlark.esm.js` — ES Module
- `metona-sqlark.cjs` — CommonJS
- `metona-sqlark.d.ts` — TypeScript 类型声明
---
## 🚀 引入方式
### ESM / TypeScript
```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' });
```
### CommonJS
```javascript
const { MetonaSqlark } = require('@metona-team/metona-sqlark');
(async () => {
const db = await MetonaSqlark.create({ name: 'my-app', mode: 'hybrid' });
})();
```
### Browser UMD
```html
<script src="metona-sqlark.min.js"></script>
<script>
(async () => {
const db = await window.MetonaSqlark.create({ name: 'my-app', mode: 'hybrid' });
// 或 window.MeSqlark(完全等价)
})();
</script>
```
---
## 🚀 快速开始
```typescript
import { MetonaSqlark } from '@metona-team/metona-sqlark';
const db = await MetonaSqlark.create({
name: 'my-app',
mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' | 'aria'
diskEngine: 'opfs', // 'opfs' | 'memory'v0.6.0: IndexedDB 已移除)
});
// 定义表 — 支持外键级联
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');
// 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.product FROM users u INNER JOIN orders o ON u.id = o.user_id`);
// 子查询 v0.1.13
await db.query(`SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)`);
// GROUP BY
await db.query(`SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1`);
// ALTER TABLE — 动态修改表结构 v0.2.5
await db.query('ALTER TABLE users ADD COLUMN phone STRING');
await db.query('ALTER TABLE users DROP COLUMN phone');
// TRUNCATE TABLE — 快速清空表 v0.2.5
await db.query('TRUNCATE TABLE old_logs');
// v0.3.0 — SQL 功能扩展
// 多语句(分号分隔)
await db.query("CREATE TABLE t (id STRING PRIMARY KEY); INSERT INTO t VALUES ('1'); INSERT INTO t VALUES ('2')");
// 事务语句
await db.query('BEGIN');
await db.query("INSERT INTO t VALUES ('3')");
await db.query('ROLLBACK'); // 回滚
// INSERT INTO ... SELECT
await db.query('INSERT INTO t SELECT id FROM t2 WHERE x > 1');
// UNION / UNION ALL
const rows = await db.query('SELECT name FROM users WHERE city = \'Beijing\' UNION SELECT name FROM users WHERE age < 30');
// 动态索引
await db.query('CREATE INDEX idx_users_city ON users (city)');
await db.query('DROP INDEX idx_users_city ON users (city)');
// EXISTS 关联子查询
const hasOrders = await db.query('SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)');
// v0.3.1 — CASE WHEN / JOIN 关联子查询 / 组提交
const labeled = await db.query("SELECT name, CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users");
const joinExists = await db.query('SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id AND o2.amount > 150)');
// v0.4.0 — 流式查询(大表逐行回调,不物化全部结果)
let count = 0;
await db.queryStream('SELECT * FROM logs WHERE level = \'error\'', (row) => {
count++;
processRow(row);
});
// v0.4.0 — 派生表 / 多列哈希连接 / COUNT(DISTINCT) / NULLS 排序
const top = await db.query('SELECT dept, total FROM (SELECT dept, SUM(salary) AS total FROM emp GROUP BY dept) AS t WHERE total > 100 ORDER BY total DESC');
await db.query('SELECT COUNT(DISTINCT city) AS n FROM users');
await db.query('SELECT name FROM users ORDER BY age ASC NULLS FIRST');
// v0.4.2 — 崩溃恢复自愈(无需删库重建)
await db.repair(); // 校验清理损坏数据,恢复一致性
await db.clearAll(); // 清空全部数据与表结构(保留库本身)
// v0.4.2 — 迁移版本持久化(重启后从持久化版本继续,不重跑不跳跑)
db.addMigration(1, async (d) => { /* ... */ });
await db.migrateTo(1);
// 事务 — 自动回滚 v0.1.13
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '3', name: 'Charlie' });
await trx.table('orders').insert({ id: 'o1', userId: '3', amount: 99 });
// 任何一步失败 → 全部回滚
});
// 连接池 v0.1.13
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'` | 存储模式 🆕 aria |
| `diskEngine` | `'opfs' \| 'memory'` | `'opfs'` | 磁盘引擎(aria 模式下为存储后端;v0.6.0: IndexedDB 已移除) |
| `version` | `number` | `1` | 版本号 |
| `maxRowsPerQuery` | `number` | `0` | 查询结果行数上限(0=不限制)✅ v0.2.5 生效 |
| `debug` | `boolean` | `false` | 调试模式,输出详细日志 🆕 |
| `onError` | `(error) => void` | — | 全局错误回调 🆕 |
| `aria` | `AriaEngineConfig` | — | AriaEngine 专属配置透传:`walSyncMode`/`checkpointInterval`/`encryption`/`pageStorage`/`compression` 等 🆕 v0.5.0 |
### 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'` | 删除级联 ✅ v0.4.1 |
| `onUpdate` | `'CASCADE'\|'SET NULL'\|'RESTRICT'` | 更新级联(更新主键时触发)✅ v0.4.2 |
### WHERE 操作符
| 操作符 | 含义 | 操作符 | 含义 |
|--------|------|--------|------|
| `$eq` / 直接值 | 等于 | `$gt` / `$gte` | 大于 / 大于等于 |
| `$ne` | 不等于 | `$lt` / `$lte` | 小于 / 小于等于 |
| `$in` / `$nin` | 在列表中 | `$like` | 模糊匹配 |
| `$and` / `$or` / `$not` | 逻辑组合 | | |
### 核心方法
| 方法 | 说明 |
|------|------|
| `db.query(sql)` | 执行 SQL 字符串 |
| `db.queryStream(sql, onRow)` | 流式查询(逐行回调,不物化)🆕 |
| `db.table(name)` | 获取表操作对象 |
| `db.defineTable(name, cols)` | 定义表结构 |
| `db.transaction(fn)` | 执行事务(自动回滚)🆕 |
| `db.repair()` | 崩溃恢复自愈:校验清理损坏数据、恢复索引一致性(无需删库重建)🆕 v0.4.2 |
| `db.clearAll()` | 清空全部数据与表结构(保留库本身,实例可继续使用)🆕 v0.4.2 |
| `db.exportTable(name)` / `db.exportAll()` | 导出数据 JSON |
| `db.importTable(name, data)` | 导入数据 |
| `db.backup()` | 在线备份:全库一致性快照(Aria 引擎级备份 / 其余引擎回退 exportAll)🆕 v0.5.1 |
| `db.addMigration(v, fn)` / `db.migrateTo(v)` | 数据迁移(版本持久化到库内,重启不重跑)🆕 v0.4.2 |
| `db.subscribe(table, fn)` | 订阅表变更 |
| `db.on(hook, fn)` | 注册钩子 (14 种,全部真实触发)🆕 v0.5.1 |
### 维护语句(v0.5.1 SQL 入口)
| 语句 | 说明 | 引擎支持 |
|------|------|----------|
| `EXPLAIN SELECT ...` | 输出查询计划(type/table/where/estimatedRows/actualTimeMs | 全部 |
| `ANALYZE [TABLE] name` | 收集表统计信息(行数/平均行大小/索引深度/列基数) | Aria |
| `REINDEX [TABLE] name` | 重建表二级索引 | Aria |
| `VACUUM` | 压缩 LSM + 清理 MVCC 碎片 | Aria |
| `SAVEPOINT name` | 创建嵌套事务保存点 | Aria |
| `ROLLBACK TO [SAVEPOINT] name` | 回滚到保存点 | Aria |
| `RELEASE [SAVEPOINT] name` | 释放保存点 | Aria |
> 不支持的引擎执行维护语句抛 `NOT_SUPPORTED`。
### 旧库迁移(v0.6.0
> IndexedDB 已从引擎中完全移除。使用旧版本(v0.5.x 及更早)的用户,可通过
> 一次性迁移工具把磁盘模式(IndexedDBEngine)旧库导入新引擎(KVStore)。
```typescript
import { migrateFromIndexedDB } from '@metona-team/metona-sqlark/migration';
// 目标库(新引擎,disk 模式)
const target = await MetonaSqlark.create({ name: 'my-app-new', mode: 'disk' });
// 从旧 IndexedDB 库导入(旧库名 'my-app',旧引擎 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 模式旧库(IndexedDBBackend)数据为引擎私有格式(SSTable/WAL),
> 无法按行迁移——此类用户请从应用层导出(exportAll)后重新导入。
### 连接池(v0.1.13
| 静态方法 | 说明 |
|------|------|
| `MetonaSqlark.connect(config)` | 获取或创建数据库实例(单例复用)🆕 |
| `MetonaSqlark.disconnect(name)` | 释放连接(引用计数 -1)🆕 |
| `MetonaSqlark.disconnectAll()` | 强制关闭所有连接 🆕 |
| `MetonaSqlark.getActiveConnections()` | 获取活跃连接列表 🆕 |
### React / Vue 集成
```tsx
// React
import { useQuery } from '@metona-team/metona-sqlark/react';
const { data, loading, refresh } = useQuery(db, 'SELECT * FROM users');
// Vue
import { useSqlarkQuery } from '@metona-team/metona-sqlark/vue';
const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');
```
---
## 📊 存储模式对比
| 特性 | Memory | Disk (KVStore) | Hybrid | Aria |
|------|--------|----------------|--------|------|
| **持久化** | ❌ 重启丢失 | ✅ KVStoreOPFS) | ✅ 内存+磁盘 | ✅ 后端决定 |
| **事务回滚** | ✅ 快照 | ✅ 原子日志 flush | ✅ 双引擎 | ✅ MVCC |
| **多 key 原子写** | — | ✅ 单日志记录原子(v0.6.0/0.6.1 | ✅ 委托磁盘 | ✅ WAL 单文件原子 |
| **二级索引** | ✅ Hash | ✅ Hash(重启恢复) | ✅ Hash | ✅ LSM(重启恢复) |
| **查询性能** | ⚡ O(1) PK | ⚡ O(1) PK(内存热路径) | ⚡ O(1) PK | ⚡ O(log n) |
| **数据上限** | 内存限制 | 磁盘可用(行级存储,大表可行) | 磁盘可用 | 内存限制 |
| **浏览器** | 全部 | Chrome/Edge 102+ / Firefox 111+ / Safari 15.2+ | 同上 | 同上 |
| **多标签页** | — | —(无事务锁,Hybrid 场景) | ❌ 无保护 | ✅ Web Locks 独占锁(v0.5.0 |
| **全库加密** | — | — | — | ✅ AES-GCMv0.5.0 |
| **适用场景** | 缓存/测试 | 标准持久化(替代 IndexedDB) | 速度+持久化 | 大规模/分析 |
| **测试覆盖** | 30+ | 50+(含 10 万级压力 + 异常场景) | 15+ | 400+ |
### Memory 模式
- **环境**: 所有浏览器、Node.js
- **限制**: 数据不持久化,页面刷新/进程重启后数据丢失
- **能力**: 完整 CRUD、事务回滚、外键级联、二级索引、SQL 全支持
- **适用**: 临时数据、单元测试、缓存层
### Disk (KVStore) 模式
- **环境**: Chrome/Edge 102+ / Firefox 111+ / Safari 15.2+OPFS)、Node.js(内存介质)
- **限制**: 依赖 OPFS;多标签页并发写无锁保护(单标签页内可靠)
- **能力**: 完整 CRUD、**多 key 原子事务**(单日志记录原子追加)、外键级联、事务内 DDL、
二级索引(重启恢复)、schema/数据/索引完整持久化、10 万级数据量压力验证
- **适用**: 标准前端数据库持久化(v0.6.0 替代 IndexedDB
### Hybrid 模式
- **环境**: 所有浏览器
- **限制**: 磁盘引擎决定底层限制(KVStore 需 OPFS 支持浏览器)
- **能力**: write-through 双写(内存+磁盘)、提交顺序保证(磁盘优先)、读从内存
- **适用**: 需要内存速度 + 磁盘持久化的混合场景
### Aria 模式
- **环境**: 所有浏览器(后端可选 OPFS / Memory
- **限制**: Memory 后端重启丢失;OPFS 后端需 OPFS 支持(Chrome 102+ / Firefox 111+ / Safari 15.2+);同一库仅允许一个标签页打开(Web Locks 独占锁,第二个标签页抛 `ARIA_LOCKED`
- **能力**: LSM-Tree 存储引擎、SSTable 4KB 页面化物理存储(OPFS 后端默认启用,BufferPool LRU 缓存)、二级索引(跨重启恢复)、MVCC 事务、WAL 分片文件原子写入崩溃恢复(真追加 + 空洞检测)、ON UPDATE/DELETE 外键级联、Bloom Filter、**全库 AES-GCM 透明加密**v0.5.0)、LZ4 压缩、Savepoint、EXPLAIN、ANALYZE、REINDEX、VACUUM、`repair()` 自愈(含孤儿页面清理)
- **适用**: 大规模数据分析、需要自研引擎可控性的高级场景
---
## 🌲 AriaEngine — 自研存储引擎
AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念:
```typescript
// 激活 AriaEngine(全库加密 + OPFS 页面化存储,v0.5.0
const db = await MetonaSqlark.create({
name: 'my-app',
mode: 'aria', // 🆕 自研引擎模式
diskEngine: 'opfs', // 'opfs' | 'memory'
aria: { // 🆕 AriaEngine 专属配置透传(v0.5.0
walSyncMode: 'full', // WAL 同步模式
encryption: { password: 'my-password' }, // 全库 AES-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 架构
```
┌──────────────────────────────────────────┐
│ AriaEngine v0.6.0 │
│ (implements IStorageEngine) │
├──────────────────────────────────────────┤
│ LSM-Tree │ Buffer Pool │ WAL │
│ MemTable │ LRU (256pp) │ Segmented │
│ +SSTable │ +FileManager │ +CRC-32 │
│ (4KB Page) │ │ +空洞检测 │
├──────────────────────────────────────────┤
│ MVCC │ Bloom Filter │ LZ4 v2 │
│ Snapshot │ FNV-1a+Murmur│ +大小头 │
│ +Savepoint│ +Serialize │ │
├──────────────────────────────────────────┤
│ EncryptedBackend │ 二级索引 │ ANALYZE │
│ AES-GCM 全库 │ Per-Column│ EXPLAIN │
├──────────────────────────────────────────┤
│ Web Locks 独占锁 │ OPFS Backend (v2) │
│ (多标签页) │ append/COW原子/清理 │
└──────────────────────────────────────────┘
```
| 特性 | 说明 |
|------|------|
| **LSM-Tree** | MemTable (红黑树) → SSTable 多级索引,异步 Compaction(从存储兜底加载,不依赖缓存),写背压 |
| **页面化物理存储** | OPFS 后端下 SSTable 存为 4KB 页面(`pageStorage` 自动启用):FileManager 分配 pageId、BufferPool LRU 缓存(256 页 ≈ 1MB 可控内存)、save 即落盘(WAL checkpoint 截断安全)、`SSTableMeta.pageIds` 持久化(旧整 value 数据兼容)v0.5.0 |
| **WAL** | 分片文件 `__wal_%06d.bin`(4MB 阈值切换)+ 真追加(createWritable keepExistingData);标准 CRC32 记录校验;full/batch/none 三种模式(full 真正同步);空洞检测截断;16MB 阈值自动 checkpoint(活跃事务期间不截断)v0.5.0 |
| **崩溃恢复** | 打开时完整性校验(残缺 SSTable 自动跳过并清理 + 整文件 CRC-32 校验)、WAL 按 key 扫描恢复(不丢记录)、恢复后自动重建二级索引 |
| **全库加密** | `encryption.password` 配置 → EncryptedBackend 透明加解密(WAL/SSTable/Schema/元数据全密文);PBKDF2-SHA256 密钥派生 + salt/verifier 持久化;密码错误/数据篡改 → `ARIA_DECRYPT_ERROR`;明文库/加密库开关一致性检测 v0.5.0 |
| **MVCC** | 版本链 + 快照隔离,事务读写不互斥,提交/回滚按事务写入 key 精准清理,自动 GC |
| **Bloom Filter** | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时 probe |
| **二级索引** | 每列独立 LSM Tree,支持 $eq/$in/$gt/$lt 范围扫描,跨重启自动恢复,WAL 恢复后自动重建 |
| **多标签页锁** | Web Locks API 库级独占锁(`ifAvailable` 不排队):第二个标签页抛 `ARIA_LOCKED`;不支持的环境降级无锁并告警 v0.5.0 |
| **Compaction** | 异步 Leveled CompactionLevel 0 > 8 触发同步背压,compactLevel public 接口 |
| **OPFS Backend** | 零外部依赖纯浏览器文件系统:单文件 COW 原子写、`append` 真追加、写队列失败不中断、close 排空、崩溃残留自动清理 v0.5.0 |
---
## 🛠 开发
```bash
npm install # 安装依赖
npm run dev # 开发模式(localhost:3001
npm run build # 生产构建(生成 dist/
npm test # 运行测试
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build
npm run lint # 代码检查
npm run typecheck # 类型检查
```
---
## 📊 项目状态
| 指标 | 数值 |
|------|------|
| 测试用例 | 1049 |
| 测试套件 | 65+12 Playwright e2e |
| 行覆盖率 | 89.3% |
| SQL 关键字 | 72 |
| 存储引擎 | 5Memory / **KVStore** / OPFS / Hybrid / **Aria** |
### 🌐 浏览器兼容性
| 浏览器 | 最低版本 | Memory | KVStore(OPFS) | OPFS | Web Locks | Aria |
|--------|----------|--------|---------------|------|-----------|------|
| Chrome | 102+ | ✅ | ✅ | ✅ | ✅ (69+) | ✅ |
| Firefox | 111+ | ✅ | ✅ | ✅ | ✅ (96+) | ✅ |
| Safari | 15.2+ | ✅ | ✅ | ✅ | ✅ (15.4+) | ✅ |
| Edge | 102+ | ✅ | ✅ | ✅ | ✅ (79+) | ✅ |
| Node.js | 16+ | ✅ | ✅ (内存介质) | ❌ (测试用 mock) | ❌ | ✅ |
> **OPFS 支持说明(v0.4.5 更新)**: ChromiumChrome/Edge 102+)、Firefox 111+、Safari 15.2+
> 均已支持基础 OPFS API`createWritable` 原子写)。OPFS 无跨文件事务,
> AriaEngine 以 WAL 分片单文件原子写 + 空洞检测截断保证崩溃一致性。
>
> **多标签页保护(v0.4.5**: AriaEngine 打开库时通过 Web Locks API 获取库级独占锁,
> 第二个标签页打开同一库会抛 `ARIA_LOCKED`。Web Locks 不可用的环境降级为无锁并告警
> (仅理论上,现代浏览器均支持)。
---
## 📂 项目结构
```
src/
├── index.ts # 入口(MetonaSqlark + MeSqlark
├── core.ts # 主类
├── constants.ts # 类型定义 + 配置 + DatabaseError
├── connection-manager.ts # 连接池管理
├── engine/ # 存储引擎(Memory/KVStore/OPFS/Aria + kvstore 自研 KV 引擎)
├── migration/ # 旧 IndexedDB 数据迁移工具(一次性)
├── hybrid/ # 混合引擎(write-through
├── table/ # 表管理 + Schema 校验
├── query/ # AST + Builder + Compiler + Executor
├── sql/ # Lexer + Parser(递归下降)
├── transaction/ # 事务管理(支持回滚)
├── plugin/ # 插件系统(14 hooks
└── integrations/ # React / Vue hooks
```
---
## 📄 License
MIT © [MetonaTeam](https://git.metona.cn/MetonaTeam/MetonaSqlark)