方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 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 已重建。
30 KiB
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 记录)、分片空洞与记录损坏如实上报(
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+
安装
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(压缩版:实测 251,109 字节 / gzip 63,145 字节)/ 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 │ (每列独立) │ │
├────────────────┴───────────────┴────────────┤
│ __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 页面化存储 |
框架集成
// 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 # 运行测试(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 完全相同的命令产出(可复现):
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 导出数据,不要指望旧键是新数据的镜像。
- 回退是单向的:迁移后所有新写入只进 manifest,旧的
- 单列主键 — 复合主键暂不支持(建表时显式
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