Files
MetonaSqlark/README.md
T
thzxx 799560ea05 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 已重建。
2026-09-15 08:06:23 +08:00

519 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.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.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
> 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.6.1 │
├──────────────────────────────────────────────┤
│ LSM-Tree │ Buffer Pool │ WAL │
│ MemTable │ LRU (256页) │ 分片文件 │
│ +SSTable │ +FileManager │ +CRC-32 │
│ (4KB 页面) │ │ +空洞检测 │
├────────────────┼───────────────┼────────────┤
│ 事务/版本管理 │ Bloom Filter │ LZ4 压缩 │
│ 快照隔离 │ 二级索引 LSM │ +大小头 │
│ +Savepoint │ (每列独立) │ │
├────────────────┴───────────────┴────────────┤
│ 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 三模式;空洞检测截断;16MB 阈值自动 checkpoint(活跃事务期间不截断) |
| **崩溃恢复** | 打开时完整性校验(损坏 SSTable 自愈清理 + 整文件 CRC-32)、WAL 恢复、恢复后自动重建二级索引;`repair()` 清理孤儿页面与残留 |
| **全库加密** | `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 # 运行测试(1872 用例 · 90 套件;+4 个重型套件)
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build
npm run lint # 代码检查
npm run typecheck # 类型检查
```
---
## 项目状态
| 指标 | 数值 |
|------|------|
| 测试用例 | 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 |
| 存储模式 | 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
- **单列主键** — 复合主键暂不支持(建表时显式 `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)