# MetonaSqlark

version license coverage tests

> 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研页面式存储引擎**。 --- ## ✨ 特性 - 🚀 **AriaEngine 自研存储引擎** — LSM-Tree 页面式存储,4KB Slotted Page、WAL 崩溃恢复(full 模式真正同步)、LZ4 压缩 - 💾 **OPFS 自研存储后端** — 纯浏览器文件系统,零 IndexedDB 依赖,二进制页面文件,schema 持久化(空表/索引跨重启完整保留) - 🔒 **生产级数据安全** — WAL 原子写入 + CRC 完整性校验、`RESTRICT` 外键约束、崩溃恢复自愈(`repair()` 无需删库重建)、SQL 注入防护 - 🛡 **输入校验全覆盖** — `maxLength`/`min`/`max` 约束、类型检查、必填验证 - 💾 **多引擎架构** — Memory / IndexedDB / OPFS / Hybrid(write-through) / Aria 五种模式 - 📝 **完整 SQL 支持** — SELECT/JOIN/子查询/GROUP BY/HAVING/ORDER BY/LIMIT/BETWEEN/IF NOT EXISTS/ALTER TABLE/TRUNCATE TABLE/UNION/INSERT...SELECT/事务语句/CREATE INDEX/EXISTS(v0.3.0)+ CASE WHEN/哈希连接/组提交(v0.3.1)+ 多标签页同步(v0.3.2) - 🚰 **流式查询** — `queryStream`/`stream()` 逐行回调,Aria LSM 惰性扫描不物化结果集(v0.4.0) - 🧩 **派生表** — `FROM (SELECT ...)` 子查询作为行源,多列 ON 哈希连接,COUNT(DISTINCT),NULLS FIRST/LAST(v0.4.0) - 🔗 **Query Builder API** — 链式 `.select().where().orderBy().limit().execute()` - 🔄 **事务回滚** — Memory/IndexedDB/Hybrid/Aria 四引擎事务原子性,自动回滚,MVCC 版本链接入读写路径 - 🔗 **外键级联** — ON DELETE + ON UPDATE(CASCADE / SET NULL / RESTRICT)全引擎支持,支持更新主键(v0.4.2) - 🛡 **崩溃恢复自愈** — 残缺 SSTable 打开自动跳过、`db.repair()` 自愈、`db.clearAll()` 重置、迁移版本持久化(v0.4.2) - 🧵 **关闭时序与后台任务加固** — 后台 flush/compaction 串行入队(close 排空后才关闭存储,失败显式报告 `ARIA_BACKGROUND_ERROR`)、预加载等待链稳定(compaction 竞态修复)、事务提交先落 WAL 再合并快照(v0.4.3) - 📏 **SSTable 编码修复** — 块大小按 UTF-8 字节精确计算(大段中文内容不再因缓冲区低估崩溃)、长度字段 u32(>64KB value 不截断)、v1/v2 双格式兼容(旧库数据不丢)(v0.4.4) - 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证 - ⚡ **性能优化** — SSTableReader 二分查找统一、IndexedDB 索引利用、crypto 实例化避免全局状态 - 🌐 **浏览器兼容** — Chrome 80+ / Firefox 80+ / Safari 14+ / Edge 80+ / Node.js 16+ - 🧪 **958 测试 · 84.2% 覆盖率** — 52 套件,生产级质量保证 --- ## 📦 安装 ```bash npm install @metona-team/metona-sqlark ``` > 如果提示找不到包,先配置 scope registry(一次性): > ```bash > npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/ > ``` ### CDN / 直接下载 ```html ``` 或从 [`dist/`](./dist/) 目录下载: - `metona-sqlark.js` — UMD 开发版(含 sourcemap) - `metona-sqlark.min.js` — UMD 压缩版(~105KB,gzip ~27KB) - `metona-sqlark.esm.js` — ES Module - `metona-sqlark.cjs` — CommonJS - `metona-sqlark.d.ts` — TypeScript 类型声明 --- ## 🚀 引入方式 ### ESM / TypeScript ```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' }); ``` ### CommonJS ```javascript const { MetonaSqlark } = require('@metona-team/metona-sqlark'); (async () => { const db = await MetonaSqlark.create({ name: 'my-app', mode: 'hybrid' }); })(); ``` ### Browser UMD ```html ``` --- ## 🚀 快速开始 ```typescript import { MetonaSqlark } from '@metona-team/metona-sqlark'; const db = await MetonaSqlark.create({ name: 'my-app', mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' | 'aria' diskEngine: 'indexeddb', // 'indexeddb' | 'opfs' }); // 定义表 — 支持外键级联 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'); // 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.product FROM users u INNER JOIN orders o ON u.id = o.user_id`); // 子查询 v0.1.13 await db.query(`SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)`); // GROUP BY await db.query(`SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1`); // ALTER TABLE — 动态修改表结构 v0.2.5 await db.query('ALTER TABLE users ADD COLUMN phone STRING'); await db.query('ALTER TABLE users DROP COLUMN phone'); // TRUNCATE TABLE — 快速清空表 v0.2.5 await db.query('TRUNCATE TABLE old_logs'); // v0.3.0 — SQL 功能扩展 // 多语句(分号分隔) await db.query("CREATE TABLE t (id STRING PRIMARY KEY); INSERT INTO t VALUES ('1'); INSERT INTO t VALUES ('2')"); // 事务语句 await db.query('BEGIN'); await db.query("INSERT INTO t VALUES ('3')"); await db.query('ROLLBACK'); // 回滚 // INSERT INTO ... SELECT await db.query('INSERT INTO t SELECT id FROM t2 WHERE x > 1'); // UNION / UNION ALL const rows = await db.query('SELECT name FROM users WHERE city = \'Beijing\' UNION SELECT name FROM users WHERE age < 30'); // 动态索引 await db.query('CREATE INDEX idx_users_city ON users (city)'); await db.query('DROP INDEX idx_users_city ON users (city)'); // EXISTS 关联子查询 const hasOrders = await db.query('SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)'); // v0.3.1 — CASE WHEN / JOIN 关联子查询 / 组提交 const labeled = await db.query("SELECT name, CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users"); const joinExists = await db.query('SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id AND o2.amount > 150)'); // v0.4.0 — 流式查询(大表逐行回调,不物化全部结果) let count = 0; await db.queryStream('SELECT * FROM logs WHERE level = \'error\'', (row) => { count++; processRow(row); }); // v0.4.0 — 派生表 / 多列哈希连接 / COUNT(DISTINCT) / NULLS 排序 const top = await db.query('SELECT dept, total FROM (SELECT dept, SUM(salary) AS total FROM emp GROUP BY dept) AS t WHERE total > 100 ORDER BY total DESC'); await db.query('SELECT COUNT(DISTINCT city) AS n FROM users'); await db.query('SELECT name FROM users ORDER BY age ASC NULLS FIRST'); // v0.4.2 — 崩溃恢复自愈(无需删库重建) await db.repair(); // 校验清理损坏数据,恢复一致性 await db.clearAll(); // 清空全部数据与表结构(保留库本身) // v0.4.2 — 迁移版本持久化(重启后从持久化版本继续,不重跑不跳跑) db.addMigration(1, async (d) => { /* ... */ }); await db.migrateTo(1); // 事务 — 自动回滚 v0.1.13 await db.transaction(async (trx) => { await trx.table('users').insert({ id: '3', name: 'Charlie' }); await trx.table('orders').insert({ id: 'o1', userId: '3', amount: 99 }); // 任何一步失败 → 全部回滚 }); // 连接池 v0.1.13 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'` | 存储模式 🆕 aria | | `diskEngine` | `'indexeddb' \| 'opfs'` | `'indexeddb'` | 磁盘引擎(aria 模式下为存储后端) | | `version` | `number` | `1` | 版本号 | | `maxRowsPerQuery` | `number` | `0` | 查询结果行数上限(0=不限制)✅ v0.2.5 生效 | | `debug` | `boolean` | `false` | 调试模式,输出详细日志 🆕 | | `onError` | `(error) => void` | — | 全局错误回调 🆕 | ### 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'` | 删除级联 ✅ v0.4.1 | | `onUpdate` | `'CASCADE'\|'SET NULL'\|'RESTRICT'` | 更新级联(更新主键时触发)✅ v0.4.2 | ### WHERE 操作符 | 操作符 | 含义 | 操作符 | 含义 | |--------|------|--------|------| | `$eq` / 直接值 | 等于 | `$gt` / `$gte` | 大于 / 大于等于 | | `$ne` | 不等于 | `$lt` / `$lte` | 小于 / 小于等于 | | `$in` / `$nin` | 在列表中 | `$like` | 模糊匹配 | | `$and` / `$or` / `$not` | 逻辑组合 | | | ### 核心方法 | 方法 | 说明 | |------|------| | `db.query(sql)` | 执行 SQL 字符串 | | `db.queryStream(sql, onRow)` | 流式查询(逐行回调,不物化)🆕 | | `db.table(name)` | 获取表操作对象 | | `db.defineTable(name, cols)` | 定义表结构 | | `db.transaction(fn)` | 执行事务(自动回滚)🆕 | | `db.repair()` | 崩溃恢复自愈:校验清理损坏数据、恢复索引一致性(无需删库重建)🆕 v0.4.2 | | `db.clearAll()` | 清空全部数据与表结构(保留库本身,实例可继续使用)🆕 v0.4.2 | | `db.exportTable(name)` / `db.exportAll()` | 导出数据 JSON | | `db.importTable(name, data)` | 导入数据 | | `db.addMigration(v, fn)` / `db.migrateTo(v)` | 数据迁移(版本持久化到库内,重启不重跑)🆕 v0.4.2 | | `db.subscribe(table, fn)` | 订阅表变更 | | `db.on(hook, fn)` | 注册钩子 (14 种) | ### 连接池(v0.1.13) | 静态方法 | 说明 | |------|------| | `MetonaSqlark.connect(config)` | 获取或创建数据库实例(单例复用)🆕 | | `MetonaSqlark.disconnect(name)` | 释放连接(引用计数 -1)🆕 | | `MetonaSqlark.disconnectAll()` | 强制关闭所有连接 🆕 | | `MetonaSqlark.getActiveConnections()` | 获取活跃连接列表 🆕 | ### React / Vue 集成 ```tsx // React import { useQuery } from '@metona-team/metona-sqlark/react'; const { data, loading, refresh } = useQuery(db, 'SELECT * FROM users'); // Vue import { useSqlarkQuery } from '@metona-team/metona-sqlark/vue'; const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users'); ``` --- ## 📊 存储模式对比 | 特性 | Memory | Disk (IndexedDB) | Disk (OPFS) | Hybrid | Aria | |------|--------|------------------|-------------|--------|------| | **持久化** | ❌ 重启丢失 | ✅ IndexedDB | ✅ OPFS(schema 持久化) | ✅ 内存+磁盘 | ✅ 后端决定 | | **事务回滚** | ✅ 快照 | ✅ 原子flush | ✅ 快照 | ✅ 双引擎 | ✅ MVCC | | **二级索引** | ✅ Hash | ✅ Hash | ✅ Hash(重启恢复) | ✅ Hash | ✅ LSM(重启恢复) | | **查询性能** | ⚡ O(1) PK | 🟡 O(1) PK | 🟡 O(1) PK | ⚡ O(1) PK | ⚡ O(log n) | | **数据上限** | 内存限制 | ~2GB(IDB限制) | ~磁盘可用(整表重写,≤1000行/表为宜) | ~2GB(IDB) | 内存限制 | | **浏览器** | 全部 | 全部 | Chrome/Edge 102+ | 全部 | 全部 | | **适用场景** | 缓存/测试 | 标准持久化 | Chromium专有 | 速度+持久化 | 大规模/分析 | | **测试覆盖** | 30+ | 30+ | 15 | 15+ | 200+ | ### Memory 模式 - **环境**: 所有浏览器、Node.js - **限制**: 数据不持久化,页面刷新/进程重启后数据丢失 - **能力**: 完整 CRUD、事务回滚、外键级联、二级索引、SQL 全支持 - **适用**: 临时数据、单元测试、缓存层 ### Disk (IndexedDB) 模式 - **环境**: 所有现代浏览器(Chrome/Firefox/Safari/Edge)、Node.js(fake-indexeddb) - **限制**: 受浏览器 IndexedDB 配额限制(通常 ~2GB),多标签页需处理版本冲突 - **能力**: 完整 CRUD、事务原子性(单 IDB 事务包裹)、外键级联、onversionchange 感知 - **适用**: 标准前端数据库持久化场景 ### Disk (OPFS) 模式 - **环境**: **仅限** Chrome 102+ / Edge 102+(Origin Private File System) - **限制**: Firefox/Safari 不支持 OPFS API;每次写入重写整表 JSON 文件(大表性能差,不建议 >1000 行) - **能力**: 完整 CRUD、重启自动加载数据(空表/索引/schema 完整保留,v0.4.2)、事务回滚、并发写安全(内存快照一致) - **适用**: Chromium 独占场景、小数据集持久化 ### Hybrid 模式 - **环境**: 所有浏览器 - **限制**: 磁盘引擎决定底层限制(IndexedDB ~2GB / OPFS Chrome only) - **能力**: write-through 双写(内存+磁盘)、提交顺序保证(磁盘优先)、读从内存 - **适用**: 需要内存速度 + 磁盘持久化的混合场景 ### Aria 模式 - **环境**: 所有浏览器(后端可选 IndexedDB / OPFS / Memory) - **限制**: Memory 后端重启丢失;IndexedDB 后端受配额限制;OPFS 后端仅 Chromium - **能力**: LSM-Tree 存储引擎、二级索引(跨重启恢复)、MVCC 事务、WAL 原子写入崩溃恢复(残缺 SSTable 打开自动跳过)、ON UPDATE/DELETE 外键级联、Bloom Filter、AES-GCM 加密、Savepoint、EXPLAIN、ANALYZE、REINDEX、VACUUM、`repair()` 自愈 - **适用**: 大规模数据分析、需要自研引擎可控性的高级场景 --- ## 🌲 AriaEngine — 自研存储引擎 AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念: ```typescript // 激活 AriaEngine const db = await MetonaSqlark.create({ name: 'my-app', mode: 'aria', // 🆕 自研引擎模式 diskEngine: 'indexeddb', // 'indexeddb' | 'opfs' | 'memory' }); // 与现有 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 架构 ``` ┌──────────────────────────────────────────┐ │ AriaEngine v0.2.5 │ │ (implements IStorageEngine) │ ├──────────────────────────────────────────┤ │ LSM-Tree │ Buffer Pool │ WAL │ │ MemTable │ LRU (256pp) │ Recovery │ │ +SSTable │ +FileManager │ +CRC │ ├──────────────────────────────────────────┤ │ MVCC │ Bloom Filter │ LZ4 │ │ Snapshot │ FNV-1a+Murmur│ Compress │ │ +Savepoint│ +Serialize │ │ ├──────────────────────────────────────────┤ │ AES-GCM │ 二级索引 │ ANALYZE │ │ Encrypt │ Per-Column │ EXPLAIN │ ├──────────────────────────────────────────┤ │ Storage Backend (IDB / OPFS / Memory) │ └──────────────────────────────────────────┘ ``` | 特性 | 说明 | |------|------| | **LSM-Tree** | MemTable (红黑树) → SSTable 多级索引,异步 Compaction(从存储兜底加载,不依赖缓存),写背压 | | **WAL** | Write-Ahead Log 二进制格式,CRC 校验,记录与计数单事务原子写入,full/batch/none 三种模式(full 模式真正同步 ✅ v0.2.5),16MB 阈值自动 checkpoint(活跃事务期间不截断 ✅ v0.4.2) | | **崩溃恢复** | 打开时完整性校验(残缺 SSTable 自动跳过并清理)、WAL 按 key 扫描恢复(不丢记录)、恢复后自动重建二级索引 ✅ v0.4.2 | | **MVCC** | 版本链 + 快照隔离,事务读写不互斥,提交/回滚按事务写入 key 精准清理,自动 GC | | **Buffer Pool** | SSTable 缓存 LRU 上限(`bufferPoolPages` × `pageSize`,默认 256 页 ≈ 1MB 可控内存)✅ v0.2.6 生效,查询前异步预加载兜底,缓存驱逐不丢数据 | | **Bloom Filter** | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时 probe | | **二级索引** | 每列独立 LSM Tree,支持 $eq/$in/$gt/$lt 范围扫描,跨重启自动恢复,WAL 恢复后自动重建 ✅ v0.4.2 | | **AES-GCM** | PBKDF2 密钥派生 + AES-256-GCM 页面级加密,CryptoManager 实例化 ✅ v0.2.5 | | **Compaction** | 异步 Leveled Compaction,Level 0 > 8 触发同步背压,compactLevel public 接口 ✅ v0.2.5 | | **OPFS Backend** | 纯浏览器文件系统,Promise 队列串行写,零外部依赖 | --- ## 🛠 开发 ```bash npm install # 安装依赖 npm run dev # 开发模式(localhost:3001) npm run build # 生产构建(生成 dist/) npm test # 运行测试 npm run lint # 代码检查 npm run typecheck # 类型检查 ``` --- ## 📊 项目状态 | 指标 | 数值 | |------|------| | 测试用例 | 958 | | 测试套件 | 51 | | 行覆盖率 | 84.2% | | SQL 关键字 | 36 | | 存储引擎 | 5(Memory / IndexedDB / OPFS / Hybrid / **Aria**) | ### 🌐 浏览器兼容性 | 浏览器 | 最低版本 | Memory | IndexedDB | OPFS | Aria | |--------|----------|--------|-----------|------|------| | Chrome | 80+ | ✅ | ✅ | ✅ (102+) | ✅ | | Firefox | 80+ | ✅ | ✅ | ❌ | ✅ | | Safari | 14+ | ✅ | ✅ | ❌ | ✅ | | Edge | 80+ | ✅ | ✅ | ✅ (102+) | ✅ | | Node.js | 16+ | ✅ | ✅ (fake-idb) | ❌ | ✅ | > **注意**: OPFS 模式仅限 Chromium 内核浏览器 (Chrome/Edge 102+),Firefox/Safari 请使用 `diskEngine: 'indexeddb'`。 --- ## 📂 项目结构 ``` src/ ├── index.ts # 入口(MetonaSqlark + MeSqlark) ├── core.ts # 主类 ├── constants.ts # 类型定义 + 配置 + DatabaseError ├── connection-manager.ts # 连接池管理 ├── utils.ts # 工具函数 ├── engine/ # 存储引擎(Memory/IndexedDB/OPFS/Aria) ├── hybrid/ # 混合引擎(write-through) ├── table/ # 表管理 + Schema 校验 ├── query/ # AST + Builder + Compiler + Executor ├── sql/ # Lexer + Parser(递归下降) ├── transaction/ # 事务管理(支持回滚) ├── plugin/ # 插件系统(14 hooks) └── integrations/ # React / Vue hooks ``` --- ## 📄 License MIT © [MetonaTeam](https://git.metona.cn/MetonaTeam/MetonaSqlark)