按 PLAN-v0.7.5.md §B-6 的**完整规格**实施(此前只落地了"降级选项"里的五处止血):
B-6 要求的是 `__aria_manifest` 单一提交点 + LSM 单项改造。完整记录见方案附录 H。
一、单一提交点
- 新增 `src/engine/aria/store/manifest.ts`:`__aria_manifest_<generation>`
(magic + formatVersion + generation + 头部 CRC + 载荷 CRC;先写后验;保留两代)。
载荷 = 页面水位 + 各命名空间 SSTable 元数据 + 表结构 + WAL 起始位置 + 待落盘冻结表意图。
- 顺序固定:**数据落盘 → manifest 提交 → 才允许截断 WAL / 删除旧文件 / 删除旧 SSTable**。
- 恢复只认最后一份 CRC 通过的世代;全部世代无效 → `ARIA_MANIFEST_CORRUPT`
(修复前:裸 JSON meta 解析失败 → `[]` → 静默空库,随后 repair 还会删光活页)。
- 旧格式(__aria_lsm_meta/__aria_schemas/__aria_meta)首次打开自动迁移,旧键保留;
迁移遇到损坏 → `ARIA_LEGACY_META_CORRUPT`。
- 陈旧实例保护(STALE_INSTANCE):认领时一次跨过 MANIFEST_TAKEOVER_STRIDE 个世代,
杜绝"旧实例在途提交落在同一世代号上"(实测第二个实例 open 直接失败)。
二、LSM
- 44 冻结表成为一等状态:失败保留 + 可重试(修复前失败即永久失去落盘机会)。
- 45 `flush()` 先入链再报告后台错误(修复前一次后台失败会让之后每次 flush 直接抛错、
数据永远等不到落盘);被重试修复的失败进 `getBackgroundWarnings()`(可见但不误报失败)。
- 47 `MergeIterator` 胜出来源的补充推迟到下一次 `next()`:提前终止不再多算一条。
- 49 `compacting` 由单 boolean 改为按层集合(跨层触发不再被静默丢弃)。
- 50 compaction 不再"先 splice 整层再合并"(窗口内该层对读者可见);
被取代的 SSTable 进"退休表" + 读者 epoch,等更早读者退出才物理删除。
- 51 底部层原地合并回收墓碑(删除密集场景空间不再无界增长);"整层只剩墓碑" 有专门分支
(修复前会读 `merged[0][0]` 抛 TypeError,compaction 永久失败)。
- 55 flush 与 compaction 拆成两条链,checkpoint 只落 memtable;删除引擎层全部
`prefetch*`/`drainChain` 依赖,改为"快照 + 结构版本乐观重试"
(版本号同时覆盖 levels 与前台 memtable/frozen 的变化)。
- 读路径自洽:介质读故障抛 `ARIA_SSTABLE_READ_FAILED`,不再折叠成"文件不存在"误删元数据。
三、WAL
- LSN 全库单调(manifest 记高水位);按水位删除旧分片(`planKeepFrom` → 提交 → 再删除)。
- **分片号只增不减**:修复前全量截断后重置为 0,会与 manifest 记录的 startSegment 错位,
实测造成两个方向的损坏(删掉的行复活 / 已确认写入丢失,见随机压力套件)。
- 分片空洞(含前缀缺失)显式报 `ARIA_WAL_GAP`,不再静默丢弃尾部。
四、其它
- `sstable.ts` 三份解析循环合并为 `iterEntries()`,越界策略统一。
- `vacuum()` 返回真实压缩层数(修复前硬编码 6 且底部层永不压缩)。
- `close()` 加 try/finally(落盘失败也必须释放后端/锁并复位状态)。
- `getRecoveryReport()`:{droppedSSTables, dataLossSuspected, walGaps, legacyImported,
manifestFallback} —— "自愈了什么、有没有真丢数据"成为可读返回值。
五、验证
- 新增 `tests/v080-b6-single-commit-point.test.ts`(63 项,含 manifest 严格校验表驱动 25 例)。
- 新增 `scripts/mutation-b6.py`:22 项变异验证(把每个修复回退到修复前行为,对应用例必须失败),
全部被拦住 —— 这批用例不是陪跑。
- 常规套件 1935 通过 / 91 套件;覆盖率 90.34 / 82.16 / 94.06 / 93.23(阈值 90/82/94/93);
e2e 14/14;重型套件 4 套件 27 项全绿。
MetonaSqlark
基于 TypeScript 的前端关系型数据库:完整 SQL + Query Builder 双 API, 4 种存储模式(memory / disk / hybrid / aria)+ 3 种后端(OPFS / KVStore / Memory)可选,
aria模式内置自研 LSM-Tree 存储引擎(AriaEngine:WAL + 页面化 + 可选压缩/加密, 事务用未提交快照回滚)。 零运行时依赖,浏览器 / Node.js 开箱即用。
目录
核心特性
数据库能力
- 完整 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 记录)、空洞检测截断、打开时损坏自愈、
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+
安装
npm install @metona-team/metona-sqlark
私有 registry(一次性配置):
npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/
CDN / 直接下载
<!-- UMD 格式,暴露 window.MetonaSqlark 与 window.MeSqlark -->
<script src="https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"></script>
或从 dist/ 下载:metona-sqlark.js(UMD 开发版)/ metona-sqlark.min.js(压缩版,gzip ~27KB)/ metona-sqlark.esm.js / metona-sqlark.cjs / metona-sqlark.d.ts
快速开始
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 模式用户可通过一次性迁移工具导入:
// 两种等价写法(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 崩溃恢复 + 快照回滚事务 + 页面化物理存储 + 全库加密。
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 │ (每列独立) │ │
├────────────────┴───────────────┴────────────┤
│ 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 页面化存储 |
框架集成
// 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');
开发
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 # 类型检查
项目状态
| 指标 | 数值 |
|---|---|
| 测试用例 | 1872(90 套件)+ 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) |
| 存储后端 | 3(OPFS / 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 完全相同的命令产出(可复现):
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 # 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