Files
MetonaSqlark/README.md
T
thzxx 81c46eb6f2 docs(B-6): 附录 H(B-6 完整实施记录)+ README/CHANGELOG/site/CONTRIBUTING 同步
- PLAN 附录 H:交付物清单、提交顺序不变量、**实施中新发现的 10 个缺陷**(含分片号复用、
  读快照与并发 flush 的窗口、checkpoint 仍等 compaction、takeover 世代竞争、提交中冻结表
  误报 WRITE_LOST、底部层只剩墓碑的 TypeError、介质读故障被当缺失、元数据损坏静默空库等),
  以及可复现的验收命令与实测数字。
- PLAN 待办表:B-6 行改为"完整实施(非降级选项)";原"B-1 遗留"给出结论
  (compaction/merge 输入只来自已校验数据,补校验反而有害;触发条件写明)。
- README:架构图/核心机制加入单一提交点;新增"存储布局在 v0.8.0 变更"的已知限制与迁移说明;
  测试 1935 / 覆盖率 90.34 · 82.16 · 94.06 · 93.23;新增变异验证命令。
- CHANGELOG:0.8.0 条目补齐 B-6 完整实现(含 9 个新错误码与恢复报告)。
- site:错误码表补 7 个新码;AriaEngine 与崩溃恢复卡片按实现改写(不再宣称"空洞截断");
  首页徽章数字同步。
- CONTRIBUTING:新增"变异验证"一节(修复类提交必须能回答"回退后用例会不会失败")。
2026-09-15 10:29:09 +08:00

529 lines
27 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.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.34%25%20stmts-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1935%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 存储引擎(AriaEngineWAL + 页面化 + 可选压缩/加密,
> 事务用未提交快照回滚)。
> 零运行时依赖,浏览器 / Node.js 开箱即用。
---
## 目录
- [核心特性](#核心特性)
- [安装](#安装)
- [快速开始](#快速开始)
- [API 速览](#api-速览)
- [存储引擎](#存储引擎)
- [AriaEngine 自研存储引擎](#ariaengine-自研存储引擎)
- [框架集成](#框架集成)
- [开发](#开发)
- [项目状态](#项目状态)
- [License](#license)
---
## 核心特性
**数据库能力**
- **完整 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 用引擎级事务 + 未提交快照(`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 记录)、空洞检测截断、打开时损坏自愈、`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`(压缩版,gzip ~27KB/ `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 记录校验;full/batch/none 三模式;**LSN 全库单调**(manifest 记高水位);分片号只增不减;空洞(含前缀缺失)显式上报 `ARIA_WAL_GAP`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, 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 位数 |
| `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 # 运行测试(1935 用例 · 91 套件;+4 个重型套件)
python3 scripts/mutation-b6.py # 变异验证:把 B-6 的修复逐项回退,对应用例必须失败
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build
npm run lint # 代码检查
npm run typecheck # 类型检查
```
---
## 项目状态
| 指标 | 数值 |
|------|------|
| 测试用例 | 193591 套件)+ 14 Playwright e2e,另 4 个重型套件在独立 CI job 串行运行 |
| 语句覆盖率 | 90.34%8393/9290 |
| 分支覆盖率 | 82.16%4367/5315 |
| 函数覆盖率 | 94.06%1205/1281 |
| 行覆盖率 | 93.23%7607/8159 |
| SQL 关键字 | 72 |
| 存储模式 | 4`memory` / `disk` / `hybrid` / `aria` |
| 存储后端 | 3OPFS / KVStore / Memory),Aria 引擎另有 LSM-Tree + WAL + 页面化 |
| 运行时依赖 | 0 |
> **覆盖率口径**`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
- **存储布局在 v0.8.0 变更** — 元数据从"每个命名空间一份裸 JSON"(`__aria_lsm_meta*` /
`__aria_schemas`)收敛为 `__aria_manifest_<generation>`(带世代号与双 CRC)。
旧库**首次用 v0.8.0 打开时自动迁移**(旧键保留不删,可回退旧版本),迁移遇到损坏
的旧元数据会明确报 `ARIA_LEGACY_META_CORRUPT` 而不是当成空库。直接读取这些内部
key 的外部脚本需要跟着改(引擎侧无公开 API 依赖它们)。
- **单列主键** — 复合主键暂不支持(建表时显式 `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`
### 浏览器兼容性
| 浏览器 | 最低版本 | 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 分片单文件原子写 + 空洞检测截断保证崩溃一致性。
>
> **多标签页保护**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 # KVStoreEnginedisk 模式,v0.6.0 替代 IndexedDB
│ ├── kvstore/ # 自研 KVStore(日志 + 快照 + 原子写)
│ └── aria/ # AriaEngineLSM-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)