# MetonaSqlark

version license coverage tests

> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API, > 5 种存储引擎可选,内置自研 LSM-Tree 存储引擎(AriaEngine)。 > 零运行时依赖,浏览器 / Node.js 开箱即用。 --- ## 目录 - [核心特性](#核心特性) - [安装](#安装) - [快速开始](#快速开始) - [API 速览](#api-速览) - [存储引擎](#存储引擎) - [AriaEngine 自研存储引擎](#ariaengine-自研存储引擎) - [框架集成](#框架集成) - [开发](#开发) - [项目状态](#项目状态) - [License](#license) --- ## 核心特性 **数据库能力** - **完整 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+ --- ## 安装 ```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 ``` 或从 [`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' 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 模式用户可通过一次性迁移工具导入: ```typescript 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 事务 + 页面化物理存储 + 全库加密**。 ```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 页面) │ │ +空洞检测 │ ├────────────────┼───────────────┼────────────┤ │ 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 页面化存储 | --- ## 框架集成 ```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 # 运行测试(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](https://git.metona.cn/MetonaTeam/MetonaSqlark)