Files
MetonaSqlark/README.md
T

480 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MetonaSqlark
<p align="center">
<img src="https://img.shields.io/badge/version-0.7.4-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
<img src="https://img.shields.io/badge/coverage-90.1%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1304%20passed-success?style=flat-square" alt="tests">
</p>
> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API
> 5 种存储引擎可选,内置自研 LSM-Tree 存储引擎(AriaEngine)。
> 零运行时依赖,浏览器 / Node.js 开箱即用。
---
## 目录
- [核心特性](#核心特性)
- [安装](#安装)
- [快速开始](#快速开始)
- [API 速览](#api-速览)
- [存储引擎](#存储引擎)
- [AriaEngine 自研存储引擎](#ariaengine-自研存储引擎)
- [框架集成](#框架集成)
- [开发](#开发)
- [项目状态](#项目状态)
- [License](#license)
---
## 核心特性
**数据库能力**
- **完整 SQL** — SELECTJOIN / 子查询 / 派生表 / 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 用 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
<!-- UMD 格式,暴露 window.MetonaSqlark 与 window.MeSqlark -->
<script src="https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"></script>
```
或从 [`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 # 运行测试(1304 用例 · 76 套件)
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build
npm run lint # 代码检查
npm run typecheck # 类型检查
```
---
## 项目状态
| 指标 | 数值 |
|------|------|
| 测试用例 | 1304+12 Playwright e2e |
| 测试套件 | 76 |
| 行覆盖率 | 90.1% |
| SQL 关键字 | 72 |
| 存储引擎 | 5Memory / KVStore / OPFS / Hybrid / Aria |
| 运行时依赖 | 0 |
### 已知限制(v0.7.4
- **单列主键** — 复合主键暂不支持(建表时显式 `SCHEMA_ERROR`),列入 v0.8 路线图
- **写语句关联引用** — UPDATE/DELETE 的 WHERE 支持非关联子查询(`IN (SELECT)` / 标量子查询),关联引用(`$col` / 关联 EXISTS)显式抛 `NOT_SUPPORTED`(不静默)
- **建表 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 # KVStoreEnginedisk 模式,v0.6.0 替代 IndexedDB
│ ├── kvstore/ # 自研 KVStore(日志 + 快照 + 原子写)
│ └── aria/ # AriaEngineLSM-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)