MetonaSqlark
基于 TypeScript 的前端关系型数据库:完整 SQL + Query Builder 双 API, 5 种存储引擎可选,内置自研 LSM-Tree 存储引擎(AriaEngine)。 零运行时依赖,浏览器 / Node.js 开箱即用。
目录
核心特性
数据库能力
- 完整 SQL — SELECT(JOIN / 子查询 / 派生表 / UNION / GROUP BY / HAVING / DISTINCT / CASE WHEN / EXISTS / BETWEEN / NULLS 排序)、INSERT...SELECT、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 引擎惰性扫描不物化结果集 - 事务 — 四引擎事务原子性 + 自动回滚;Aria 用 MVCC 快照隔离,事务读写不互斥
- 外键级联 —
ON DELETE/ON UPDATE支持CASCADE/SET NULL/RESTRICT(含主键变更级联、级联环路保护) - 数据迁移 — 版本化迁移(持久化到库内,重启不重跑)、导入导出、在线一致性备份
- 连接池 —
MetonaSqlark.connect()单例复用,引用计数自动关闭
存储引擎(5 种)
memory— 纯内存,测试 / 缓存disk— 自研 KVStore 事务引擎(v0.6.0 起替代 IndexedDB):多 key 原子写、快照 + 日志崩溃恢复hybrid— write-through 双写,读走内存aria— 自研 AriaEngine(LSM-Tree + WAL + MVCC),后端可选 OPFS / KVStore / Memory- KVStore — 日志结构化事务 KV 引擎,可独立用作 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 - 插件系统 — 14 种生命周期钩子(beforeInsert / afterQuery / ...),按优先级注册
- 旧库迁移 —
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'
diskEngine: 'opfs', // 'opfs' | 'kv'(自研 KVStore 后端)| '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' |
磁盘引擎(aria 模式下为存储后端;'kv' = 自研 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 + 清理 MVCC 碎片 | Aria |
SAVEPOINT name / ROLLBACK TO name / RELEASE name |
嵌套事务保存点 | Aria |
不支持的引擎执行维护语句抛
NOT_SUPPORTED。
旧库迁移(v0.6.0)
IndexedDB 已从引擎中完全移除。旧版本(v0.5.x 及更早)的 disk 模式用户可通过一次性迁移工具导入:
import { migrateFromIndexedDB } from '@metona-team/metona-sqlark/migration';
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() |
获取活跃连接列表 |
存储引擎
| 特性 | 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 + MVCC + 加密 + 页面化存储)
AriaEngine 自研存储引擎
AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念: LSM-Tree 索引 + WAL 崩溃恢复 + MVCC 事务 + 页面化物理存储 + 全库加密。
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 页面) │ │ +空洞检测 │
├────────────────┼───────────────┼────────────┤
│ MVCC 事务 │ 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 | 版本链 + 快照隔离,事务读写不互斥,自动 GC |
| 二级索引 | 每列独立 LSM Tree,支持等值/范围扫描,跨重启恢复,WAL 恢复后自动重建 |
| Bloom Filter | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时快速否定 |
| 多标签页锁 | Web Locks 库级独占锁,第二个标签页抛 ARIA_LOCKED;不支持的环境降级无锁并告警 |
| 维护语句 | ANALYZE(表统计)/ REINDEX(重建索引)/ VACUUM(压缩 + MVCC 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 # 运行测试(1256 用例 · 75 套件)
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build)
npm run lint # 代码检查
npm run typecheck # 类型检查
项目状态
| 指标 | 数值 |
|---|---|
| 测试用例 | 1256(+12 Playwright e2e) |
| 测试套件 | 75 |
| 行覆盖率 | 89.8% |
| SQL 关键字 | 72 |
| 存储引擎 | 5(Memory / KVStore / OPFS / Hybrid / Aria) |
| 运行时依赖 | 0 |
浏览器兼容性
| 浏览器 | 最低版本 | 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