docs: 重写 README 与 site 站点 — 剥离历史版本注释、统一信息架构(核心特性/快速开始/API速览/引擎对比/AriaEngine 专章/项目状态);更新 docs.html 存储模式对比表(移除 IndexedDB 遗留列)+ v0.6.1 修复记录;同步测试数 1093/70 套件/89.7%;修正 VERSION 常量 0.6.0→0.6.1 并重建 dist
This commit is contained in:
@@ -3,50 +3,75 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-0.6.1-blue?style=flat-square" alt="version">
|
<img src="https://img.shields.io/badge/version-0.6.1-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/license-MIT-green?style=flat-square" alt="license">
|
||||||
<img src="https://img.shields.io/badge/coverage-89.4%25-brightgreen?style=flat-square" alt="coverage">
|
<img src="https://img.shields.io/badge/coverage-89.7%25-brightgreen?style=flat-square" alt="coverage">
|
||||||
<img src="https://img.shields.io/badge/tests-1064%20passed-success?style=flat-square" alt="tests">
|
<img src="https://img.shields.io/badge/tests-1093%20passed-success?style=flat-square" alt="tests">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研存储引擎**。
|
> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API,
|
||||||
|
> 5 种存储引擎可选,内置自研 LSM-Tree 存储引擎(AriaEngine)。
|
||||||
|
> 零运行时依赖,浏览器 / Node.js 开箱即用。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ 特性
|
## 目录
|
||||||
|
|
||||||
- 🚀 **AriaEngine 自研存储引擎** — LSM-Tree + **SSTable 4KB 页面化物理存储**(BufferPool LRU 缓存 + FileManager 页面管理,v0.5.0 真实接入)、WAL 分片文件(真追加 + 空洞检测)、LZ4 压缩
|
- [核心特性](#核心特性)
|
||||||
- 💾 **OPFS 自研存储后端** — 纯浏览器文件系统,零 IndexedDB 依赖,二进制页面文件,schema 持久化,单文件 COW 原子写,崩溃残留自动清理
|
- [安装](#安装)
|
||||||
- 🔒 **生产级数据安全** — WAL 原子写入 + 标准 CRC32 完整性校验(SSTable 整文件校验 + WAL 记录校验)、**全库 AES-256-GCM 透明加密**(PBKDF2 密钥派生 + salt 持久化 + 密码验证,v0.5.0 真实接线)、`RESTRICT` 外键约束、崩溃恢复自愈
|
- [快速开始](#快速开始)
|
||||||
- 🔐 **多标签页独占锁** — Web Locks API,第二个标签页打开同一库抛 `ARIA_LOCKED`(v0.5.0)
|
- [API 速览](#api-速览)
|
||||||
- 🛠 **维护语句 SQL 入口** — `EXPLAIN`/`ANALYZE`/`REINDEX`/`VACUUM`/`SAVEPOINT` 原生 SQL 支持(v0.5.1 补齐)
|
- [存储引擎](#存储引擎)
|
||||||
- 🛡 **输入校验全覆盖** — `maxLength`/`min`/`max` 约束、类型检查、必填验证
|
- [AriaEngine 自研存储引擎](#ariaengine-自研存储引擎)
|
||||||
- 💾 **多引擎架构** — Memory / **KVStore**(自研 KV 引擎)/ OPFS / Hybrid(write-through) / Aria 五种模式(v0.6.0: IndexedDB 完全移除)
|
- [框架集成](#框架集成)
|
||||||
- 💾 **KVStore 自研 KV 引擎** — 日志结构化事务存储:多 key 原子写(putMany/deleteMany 单记录原子追加)、快照 checkpoint、崩溃两阶段恢复、CRC-32 自愈、APPEND 追加写入(aria WAL 分片用);替代 IndexedDB(v0.6.0)且可作 AriaEngine 后端(v0.6.1)
|
- [开发](#开发)
|
||||||
- 🔄 **旧库一键迁移** — `migrateFromIndexedDB()` 把旧 IndexedDB 数据(schema/索引/行)导入新引擎(v0.6.0)
|
- [项目状态](#项目状态)
|
||||||
- 📝 **完整 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)
|
- [License](#license)
|
||||||
- 🚰 **流式查询** — `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/KVStore/Hybrid/Aria 四引擎事务原子性,自动回滚,MVCC 版本链接入读写路径
|
|
||||||
- 🔗 **外键级联** — ON DELETE + ON UPDATE(CASCADE / SET NULL / RESTRICT)全引擎支持,支持更新主键(v0.4.2)
|
|
||||||
- 🛡 **崩溃恢复自愈** — 残缺 SSTable 打开自动跳过、`db.repair()` 自愈(含孤儿页面清理与残留清理)、`db.clearAll()` 重置、迁移版本持久化(v0.4.2/v0.5.0)
|
|
||||||
- 🧵 **关闭时序与后台任务加固** — 后台 flush/compaction 串行入队(close 排空后才关闭存储,失败显式报告 `ARIA_BACKGROUND_ERROR`)
|
|
||||||
- 📏 **SSTable 编码修复** — 块大小按 UTF-8 字节精确计算、长度字段 u32、v1/v2 双格式兼容(v0.4.4)
|
|
||||||
- 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证
|
|
||||||
- ⚡ **性能优化** — SSTableReader 二分查找统一、KVStore 内存热路径 O(1) 读、crypto 实例化避免全局状态
|
|
||||||
- 🛡 **级联环路保护** — A→B→A 循环引用 + CASCADE 不无限递归(visited 保护,全引擎)(v0.6.1)
|
|
||||||
- ⚛️ **多表事务真原子** — 事务 commit / 级联更新合并单条日志记录(崩溃无部分提交)(v0.6.1)
|
|
||||||
- 🌐 **浏览器兼容** — Chrome 102+ / Firefox 111+ / Safari 15.2+ / Edge 102+ / Node.js 16+
|
|
||||||
- 🧪 **1064 测试 · 89.4% 覆盖率** — 66 套件 + 12 个 Playwright 真实 Chromium e2e(含崩溃注入),生产级质量保证
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 安装
|
## 核心特性
|
||||||
|
|
||||||
|
**数据库能力**
|
||||||
|
|
||||||
|
- **完整 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
|
```bash
|
||||||
npm install @metona-team/metona-sqlark
|
npm install @metona-team/metona-sqlark
|
||||||
```
|
```
|
||||||
|
|
||||||
> 如果提示找不到包,先配置 scope registry(一次性):
|
> 私有 registry(一次性配置):
|
||||||
> ```bash
|
> ```bash
|
||||||
> npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/
|
> npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/
|
||||||
> ```
|
> ```
|
||||||
@@ -54,74 +79,33 @@ npm install @metona-team/metona-sqlark
|
|||||||
### CDN / 直接下载
|
### CDN / 直接下载
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- UMD 格式,暴露 window.MetonaSqlark 和 window.MeSqlark -->
|
<!-- UMD 格式,暴露 window.MetonaSqlark 与 window.MeSqlark -->
|
||||||
<script src="https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"></script>
|
<script src="https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
或从 [`dist/`](./dist/) 目录下载:
|
或从 [`dist/`](./dist/) 下载:`metona-sqlark.js`(UMD 开发版)/ `metona-sqlark.min.js`(压缩版,gzip ~27KB)/ `metona-sqlark.esm.js` / `metona-sqlark.cjs` / `metona-sqlark.d.ts`
|
||||||
- `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
|
|
||||||
<script src="metona-sqlark.min.js"></script>
|
|
||||||
<script>
|
|
||||||
(async () => {
|
|
||||||
const db = await window.MetonaSqlark.create({ name: 'my-app', mode: 'hybrid' });
|
|
||||||
// 或 window.MeSqlark(完全等价)
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { MetonaSqlark } from '@metona-team/metona-sqlark';
|
import { MetonaSqlark } from '@metona-team/metona-sqlark';
|
||||||
|
// 别名:import { MeSqlark } from '@metona-team/metona-sqlark';
|
||||||
|
|
||||||
const db = await MetonaSqlark.create({
|
const db = await MetonaSqlark.create({
|
||||||
name: 'my-app',
|
name: 'my-app',
|
||||||
mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' | 'aria'
|
mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' | 'aria'
|
||||||
diskEngine: 'opfs', // 'opfs' | 'kv'(自研 KVStore 后端)| 'memory'(v0.6.0: IndexedDB 已移除)
|
diskEngine: 'opfs', // 'opfs' | 'kv'(自研 KVStore 后端)| 'memory'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 定义表 — 支持外键级联
|
// 定义表 — 支持约束与外键级联
|
||||||
await db.defineTable('users', {
|
await db.defineTable('users', {
|
||||||
id: { type: 'string', primaryKey: true },
|
id: { type: 'string', primaryKey: true },
|
||||||
name: { type: 'string', required: true },
|
name: { type: 'string', required: true },
|
||||||
age: { type: 'number', default: 0 },
|
age: { type: 'number', default: 0 },
|
||||||
email: { type: 'string', unique: true, index: true },
|
email: { type: 'string', unique: true, index: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.defineTable('orders', {
|
await db.defineTable('orders', {
|
||||||
id: { type: 'string', primaryKey: true },
|
id: { type: 'string', primaryKey: true },
|
||||||
user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' },
|
user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' },
|
||||||
@@ -141,103 +125,80 @@ const result = await db.table('users')
|
|||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// JOIN
|
// JOIN
|
||||||
await db.query(`SELECT u.name, o.product FROM users u INNER JOIN orders o ON u.id = o.user_id`);
|
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`);
|
||||||
|
|
||||||
// 子查询 v0.1.13
|
// 子查询 / UNION / EXISTS
|
||||||
await db.query(`SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)`);
|
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
|
// GROUP BY / CASE WHEN
|
||||||
await db.query(`SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1`);
|
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 — 动态修改表结构 v0.2.5
|
// ALTER TABLE / TRUNCATE TABLE
|
||||||
await db.query('ALTER TABLE users ADD COLUMN phone STRING');
|
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');
|
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 db.transaction(async (trx) => {
|
||||||
await trx.table('users').insert({ id: '3', name: 'Charlie' });
|
await trx.table('users').insert({ id: '3', name: 'Charlie' });
|
||||||
await trx.table('orders').insert({ id: 'o1', userId: '3', amount: 99 });
|
await trx.table('orders').insert({ id: 'o1', user_id: '3', amount: 99 });
|
||||||
// 任何一步失败 → 全部回滚
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 连接池 v0.1.13
|
// 流式查询 — 大表逐行回调
|
||||||
|
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' });
|
const db2 = await MetonaSqlark.connect({ name: 'my-app', mode: 'hybrid' });
|
||||||
// db2 === db(复用已有实例)
|
// db2 === db
|
||||||
await db2.disconnect(); // 引用计数 -1
|
await db2.disconnect(); // 引用计数 -1
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 API 速览
|
## API 速览
|
||||||
|
|
||||||
### 数据库配置
|
### 数据库配置
|
||||||
|
|
||||||
| 属性 | 类型 | 默认 | 说明 |
|
| 属性 | 类型 | 默认 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| `name` | `string` | `'metona-sqlark'` | 数据库名称 |
|
| `name` | `string` | `'metona-sqlark'` | 数据库名称 |
|
||||||
| `mode` | `'memory' \| 'disk' \| 'hybrid' \| 'aria'` | `'hybrid'` | 存储模式 🆕 aria |
|
| `mode` | `'memory' \| 'disk' \| 'hybrid' \| 'aria'` | `'hybrid'` | 存储模式 |
|
||||||
| `diskEngine` | `'opfs' \| 'memory' \| 'kv'` | `'opfs'` | 磁盘引擎(aria 模式下为存储后端;'kv' = 自研 KVStore 后端 v0.6.1) |
|
| `diskEngine` | `'opfs' \| 'memory' \| 'kv'` | `'opfs'` | 磁盘引擎(aria 模式下为存储后端;`'kv'` = 自研 KVStore) |
|
||||||
| `version` | `number` | `1` | 版本号 |
|
| `version` | `number` | `1` | 版本号 |
|
||||||
| `maxRowsPerQuery` | `number` | `0` | 查询结果行数上限(0=不限制)✅ v0.2.5 生效 |
|
| `maxRowsPerQuery` | `number` | `0` | 查询结果行数上限(0 = 不限) |
|
||||||
| `debug` | `boolean` | `false` | 调试模式,输出详细日志 🆕 |
|
| `debug` | `boolean` | `false` | 调试模式 |
|
||||||
| `onError` | `(error) => void` | — | 全局错误回调 🆕 |
|
| `multiTabSync` | `boolean` | `false` | 多标签页同步(BroadcastChannel) |
|
||||||
| `aria` | `AriaEngineConfig` | — | AriaEngine 专属配置透传:`walSyncMode`/`checkpointInterval`/`encryption`/`pageStorage`/`compression` 等 🆕 v0.5.0 |
|
| `plugins` | `MetonaPlugin[]` | — | 插件列表 |
|
||||||
|
| `onReady` | `(db) => void` | — | 就绪回调 |
|
||||||
|
| `onError` | `(error) => void` | — | 全局错误回调 |
|
||||||
|
| `aria` | `AriaEngineConfig` | — | AriaEngine 配置透传(`walSyncMode` / `encryption` / `pageStorage` / `compression` 等) |
|
||||||
|
|
||||||
### ColumnDef 列定义
|
### ColumnDef 列定义
|
||||||
|
|
||||||
| 属性 | 类型 | 说明 |
|
| 属性 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `type` | `'string'\|'number'\|'boolean'\|'date'\|'json'` | 数据类型 |
|
| `type` | `'string' \| 'number' \| 'boolean' \| 'date' \| 'json'` | 数据类型(必填) |
|
||||||
| `primaryKey` | `boolean` | 主键 |
|
| `primaryKey` | `boolean` | 主键(建表时至少一个) |
|
||||||
| `required` | `boolean` | 必填 |
|
| `required` | `boolean` | 必填 |
|
||||||
| `unique` | `boolean` | 唯一约束 |
|
| `unique` | `boolean` | 唯一约束 |
|
||||||
| `index` | `boolean` | 创建哈希索引 |
|
| `index` | `boolean` | 创建二级索引 |
|
||||||
| `default` | `unknown` | 默认值 |
|
| `default` | `unknown` | 默认值 |
|
||||||
| `references` | `string` | 外键引用 `'table.column'` |
|
| `references` | `string` | 外键引用 `'table.column'` |
|
||||||
| `onDelete` | `'CASCADE'\|'SET NULL'\|'RESTRICT'` | 删除级联 ✅ v0.4.1 |
|
| `onDelete` | `'CASCADE' \| 'SET NULL' \| 'RESTRICT'` | 删除级联 |
|
||||||
| `onUpdate` | `'CASCADE'\|'SET NULL'\|'RESTRICT'` | 更新级联(更新主键时触发)✅ v0.4.2 |
|
| `onUpdate` | `'CASCADE' \| 'SET NULL' \| 'RESTRICT'` | 更新级联(更新主键时触发) |
|
||||||
|
| `maxLength` | `number` | 字符串最大长度 |
|
||||||
|
| `min` / `max` | `number` | 数字最小值 / 最大值 |
|
||||||
|
|
||||||
### WHERE 操作符
|
### WHERE 操作符
|
||||||
|
|
||||||
@@ -252,141 +213,105 @@ await db2.disconnect(); // 引用计数 -1
|
|||||||
|
|
||||||
| 方法 | 说明 |
|
| 方法 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `db.query(sql)` | 执行 SQL 字符串 |
|
| `db.query(sql)` | 执行 SQL(支持分号多语句) |
|
||||||
| `db.queryStream(sql, onRow)` | 流式查询(逐行回调,不物化)🆕 |
|
| `db.queryStream(sql, onRow)` | 流式查询(逐行回调,不物化) |
|
||||||
| `db.table(name)` | 获取表操作对象 |
|
| `db.table(name)` | 获取表操作对象(`insert` / `select` / `update` / `delete` / `count` / `stream` / `clear` / `drop`) |
|
||||||
| `db.defineTable(name, cols)` | 定义表结构 |
|
| `db.defineTable(name, cols)` | 定义表结构 |
|
||||||
| `db.transaction(fn)` | 执行事务(自动回滚)🆕 |
|
| `db.dropTable(name)` / `db.getTableNames()` | 删除表 / 列出表 |
|
||||||
| `db.repair()` | 崩溃恢复自愈:校验清理损坏数据、恢复索引一致性(无需删库重建)🆕 v0.4.2 |
|
| `db.transaction(fn)` | 执行事务(自动回滚) |
|
||||||
| `db.clearAll()` | 清空全部数据与表结构(保留库本身,实例可继续使用)🆕 v0.4.2 |
|
| `db.repair()` | 崩溃恢复自愈:校验清理损坏数据、重建索引 |
|
||||||
|
| `db.clearAll()` | 清空全部数据与表结构(保留库本身) |
|
||||||
| `db.exportTable(name)` / `db.exportAll()` | 导出数据 JSON |
|
| `db.exportTable(name)` / `db.exportAll()` | 导出数据 JSON |
|
||||||
| `db.importTable(name, data)` | 导入数据 |
|
| `db.importTable(name, data)` | 导入数据 |
|
||||||
| `db.backup()` | 在线备份:全库一致性快照(Aria 引擎级备份 / 其余引擎回退 exportAll)🆕 v0.5.1 |
|
| `db.backup()` | 在线备份:全库一致性快照 |
|
||||||
| `db.addMigration(v, fn)` / `db.migrateTo(v)` | 数据迁移(版本持久化到库内,重启不重跑)🆕 v0.4.2 |
|
| `db.addMigration(v, fn)` / `db.migrateTo(v)` | 版本化数据迁移(版本持久化,重启不重跑) |
|
||||||
| `db.subscribe(table, fn)` | 订阅表变更 |
|
| `db.subscribe(table, fn)` | 订阅表变更(返回退订函数) |
|
||||||
| `db.on(hook, fn)` | 注册钩子 (14 种,全部真实触发)🆕 v0.5.1 |
|
| `db.on(hook, fn)` | 注册生命周期钩子(14 种) |
|
||||||
|
| `db.close()` | 关闭数据库 |
|
||||||
|
|
||||||
### 维护语句(v0.5.1 SQL 入口)
|
### 维护语句(SQL 入口)
|
||||||
|
|
||||||
| 语句 | 说明 | 引擎支持 |
|
| 语句 | 说明 | 支持引擎 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| `EXPLAIN SELECT ...` | 输出查询计划(type/table/where/estimatedRows/actualTimeMs) | 全部 |
|
| `EXPLAIN SELECT ...` | 输出查询计划(type/table/where/estimatedRows/actualTimeMs) | 全部 |
|
||||||
| `ANALYZE [TABLE] name` | 收集表统计信息(行数/平均行大小/索引深度/列基数) | Aria |
|
| `ANALYZE [TABLE] name` | 收集表统计信息(行数/行大小/索引深度/列基数) | Aria |
|
||||||
| `REINDEX [TABLE] name` | 重建表二级索引 | Aria |
|
| `REINDEX [TABLE] name` | 重建表二级索引 | Aria |
|
||||||
| `VACUUM` | 压缩 LSM + 清理 MVCC 碎片 | Aria |
|
| `VACUUM` | 压缩 LSM + 清理 MVCC 碎片 | Aria |
|
||||||
| `SAVEPOINT name` | 创建嵌套事务保存点 | Aria |
|
| `SAVEPOINT name` / `ROLLBACK TO name` / `RELEASE name` | 嵌套事务保存点 | Aria |
|
||||||
| `ROLLBACK TO [SAVEPOINT] name` | 回滚到保存点 | Aria |
|
|
||||||
| `RELEASE [SAVEPOINT] name` | 释放保存点 | Aria |
|
|
||||||
|
|
||||||
> 不支持的引擎执行维护语句抛 `NOT_SUPPORTED`。
|
> 不支持的引擎执行维护语句抛 `NOT_SUPPORTED`。
|
||||||
|
|
||||||
### 旧库迁移(v0.6.0)
|
### 旧库迁移(v0.6.0)
|
||||||
|
|
||||||
> IndexedDB 已从引擎中完全移除。使用旧版本(v0.5.x 及更早)的用户,可通过
|
> IndexedDB 已从引擎中完全移除。旧版本(v0.5.x 及更早)的 disk 模式用户可通过一次性迁移工具导入:
|
||||||
> 一次性迁移工具把磁盘模式(IndexedDBEngine)旧库导入新引擎(KVStore)。
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { migrateFromIndexedDB } from '@metona-team/metona-sqlark/migration';
|
import { migrateFromIndexedDB } from '@metona-team/metona-sqlark/migration';
|
||||||
|
|
||||||
// 目标库(新引擎,disk 模式)
|
|
||||||
const target = await MetonaSqlark.create({ name: 'my-app-new', mode: 'disk' });
|
const target = await MetonaSqlark.create({ name: 'my-app-new', mode: 'disk' });
|
||||||
|
|
||||||
// 从旧 IndexedDB 库导入(旧库名 'my-app',旧引擎 disk 模式)
|
|
||||||
const result = await migrateFromIndexedDB({
|
const result = await migrateFromIndexedDB({
|
||||||
dbName: 'my-app',
|
dbName: 'my-app',
|
||||||
engine: 'disk', // 仅支持 disk 模式(IndexedDBEngine)
|
engine: 'disk', // 仅支持旧 disk 模式(IndexedDBEngine)
|
||||||
target,
|
target,
|
||||||
onProgress: (done, total, table) => console.log(`迁移 ${done}/${total}: ${table}`),
|
onProgress: (done, total, table) => console.log(`迁移 ${done}/${total}: ${table}`),
|
||||||
});
|
});
|
||||||
// result: { migratedTables, rowCount, skippedTables }
|
// result: { migratedTables, rowCount, skippedTables }
|
||||||
```
|
```
|
||||||
|
|
||||||
> 注:aria 模式旧库(IndexedDBBackend)数据为引擎私有格式(SSTable/WAL),
|
> 注:旧 aria 模式库为引擎私有格式(SSTable/WAL),无法按行迁移——请从应用层 `exportAll()` 后重新导入。
|
||||||
> 无法按行迁移——此类用户请从应用层导出(exportAll)后重新导入。
|
|
||||||
|
|
||||||
### 连接池(v0.1.13)
|
### 连接池
|
||||||
|
|
||||||
| 静态方法 | 说明 |
|
| 静态方法 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `MetonaSqlark.connect(config)` | 获取或创建数据库实例(单例复用)🆕 |
|
| `MetonaSqlark.connect(config)` | 获取或创建数据库实例(单例复用) |
|
||||||
| `MetonaSqlark.disconnect(name)` | 释放连接(引用计数 -1)🆕 |
|
| `MetonaSqlark.disconnect(name)` | 释放连接(引用计数 -1) |
|
||||||
| `MetonaSqlark.disconnectAll()` | 强制关闭所有连接 🆕 |
|
| `MetonaSqlark.disconnectAll()` | 强制关闭所有连接 |
|
||||||
| `MetonaSqlark.getActiveConnections()` | 获取活跃连接列表 🆕 |
|
| `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 (KVStore) | Hybrid | Aria |
|
| 特性 | Memory | Disk (KVStore) | Hybrid | Aria |
|
||||||
|------|--------|----------------|--------|------|
|
|------|--------|----------------|--------|------|
|
||||||
| **持久化** | ❌ 重启丢失 | ✅ KVStore(OPFS) | ✅ 内存+磁盘 | ✅ 后端决定 |
|
| **持久化** | ❌ 重启丢失 | ✅ KVStore(OPFS / 内存介质) | ✅ 内存 + 磁盘 | ✅ 后端决定 |
|
||||||
| **事务回滚** | ✅ 快照 | ✅ 原子日志 flush | ✅ 双引擎 | ✅ MVCC |
|
| **事务** | ✅ 快照回滚 | ✅ 单日志记录原子写 | ✅ 双引擎(磁盘优先) | ✅ MVCC 快照隔离 |
|
||||||
| **多 key 原子写** | — | ✅ 单日志记录原子(v0.6.0/0.6.1) | ✅ 委托磁盘 | ✅ WAL 单文件原子 |
|
|
||||||
| **二级索引** | ✅ Hash | ✅ Hash(重启恢复) | ✅ Hash | ✅ LSM(重启恢复) |
|
| **二级索引** | ✅ Hash | ✅ Hash(重启恢复) | ✅ Hash | ✅ LSM(重启恢复) |
|
||||||
| **查询性能** | ⚡ O(1) PK | ⚡ O(1) PK(内存热路径) | ⚡ O(1) PK | ⚡ O(log n) |
|
| **查询性能** | O(1) PK | O(1) PK(内存热路径) | O(1) PK | O(log n) |
|
||||||
| **数据上限** | 内存限制 | 磁盘可用(行级存储,大表可行) | 磁盘可用 | 内存限制 |
|
| **数据上限** | 内存 | 磁盘可用 | 磁盘可用 | 内存 |
|
||||||
| **浏览器** | 全部 | Chrome/Edge 102+ / Firefox 111+ / Safari 15.2+ | 同上 | 同上 |
|
| **全库加密** | — | — | — | ✅ AES-GCM |
|
||||||
| **多标签页** | — | —(无事务锁,Hybrid 场景) | ❌ 无保护 | ✅ Web Locks 独占锁(v0.5.0) |
|
| **多标签页锁** | — | — | — | ✅ Web Locks |
|
||||||
| **全库加密** | — | — | — | ✅ AES-GCM(v0.5.0) |
|
| **适用场景** | 缓存 / 测试 | 标准持久化(替代 IndexedDB) | 速度 + 持久化 | 大规模 / 分析 |
|
||||||
| **适用场景** | 缓存/测试 | 标准持久化(替代 IndexedDB) | 速度+持久化 | 大规模/分析 |
|
| **测试覆盖** | 30+ | 50+(含 10 万级压测) | 15+ | 400+ |
|
||||||
| **测试覆盖** | 30+ | 50+(含 10 万级压力 + 异常场景) | 15+ | 400+ |
|
|
||||||
|
|
||||||
### Memory 模式
|
**选型建议**
|
||||||
- **环境**: 所有浏览器、Node.js
|
|
||||||
- **限制**: 数据不持久化,页面刷新/进程重启后数据丢失
|
|
||||||
- **能力**: 完整 CRUD、事务回滚、外键级联、二级索引、SQL 全支持
|
|
||||||
- **适用**: 临时数据、单元测试、缓存层
|
|
||||||
|
|
||||||
### Disk (KVStore) 模式
|
- **临时数据 / 单元测试** → `memory`
|
||||||
- **环境**: Chrome/Edge 102+ / Firefox 111+ / Safari 15.2+(OPFS)、Node.js(内存介质)
|
- **标准前端持久化**(替代 IndexedDB)→ `disk`(KVStore,多 key 原子事务,10 万级验证)
|
||||||
- **限制**: 依赖 OPFS;多标签页并发写无锁保护(单标签页内可靠)
|
- **内存速度 + 磁盘持久化** → `hybrid`(write-through,读走内存)
|
||||||
- **能力**: 完整 CRUD、**多 key 原子事务**(单日志记录原子追加)、外键级联、事务内 DDL、
|
- **大规模 / 需要自研引擎可控性** → `aria`(LSM-Tree + WAL + MVCC + 加密 + 页面化存储)
|
||||||
二级索引(重启恢复)、schema/数据/索引完整持久化、10 万级数据量压力验证
|
|
||||||
- **适用**: 标准前端数据库持久化(v0.6.0 替代 IndexedDB)
|
|
||||||
|
|
||||||
### Hybrid 模式
|
|
||||||
- **环境**: 所有浏览器
|
|
||||||
- **限制**: 磁盘引擎决定底层限制(KVStore 需 OPFS 支持浏览器)
|
|
||||||
- **能力**: write-through 双写(内存+磁盘)、提交顺序保证(磁盘优先)、读从内存
|
|
||||||
- **适用**: 需要内存速度 + 磁盘持久化的混合场景
|
|
||||||
|
|
||||||
### Aria 模式
|
|
||||||
- **环境**: 所有浏览器(后端可选 OPFS / **KVStore**(自研,v0.6.1)/ Memory)
|
|
||||||
- **限制**: Memory 后端重启丢失;OPFS 后端需 OPFS 支持(Chrome 102+ / Firefox 111+ / Safari 15.2+);同一库仅允许一个标签页打开(Web Locks 独占锁,第二个标签页抛 `ARIA_LOCKED`)
|
|
||||||
- **能力**: LSM-Tree 存储引擎、SSTable 4KB 页面化物理存储(OPFS 后端默认启用,BufferPool LRU 缓存)、二级索引(跨重启恢复)、MVCC 事务、WAL 分片文件原子写入崩溃恢复(真追加 + 空洞检测)、ON UPDATE/DELETE 外键级联、Bloom Filter、**全库 AES-GCM 透明加密**(v0.5.0)、LZ4 压缩、Savepoint、EXPLAIN、ANALYZE、REINDEX、VACUUM、`repair()` 自愈(含孤儿页面清理)
|
|
||||||
- **适用**: 大规模数据分析、需要自研引擎可控性的高级场景
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌲 AriaEngine — 自研存储引擎
|
## AriaEngine 自研存储引擎
|
||||||
|
|
||||||
AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念:
|
AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念:
|
||||||
|
**LSM-Tree 索引 + WAL 崩溃恢复 + MVCC 事务 + 页面化物理存储 + 全库加密**。
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 激活 AriaEngine(全库加密 + OPFS 页面化存储,v0.5.0)
|
|
||||||
const db = await MetonaSqlark.create({
|
const db = await MetonaSqlark.create({
|
||||||
name: 'my-app',
|
name: 'my-app',
|
||||||
mode: 'aria', // 🆕 自研引擎模式
|
mode: 'aria',
|
||||||
diskEngine: 'opfs', // 'opfs' | 'kv'(自研 KVStore 后端)| 'memory'
|
diskEngine: 'opfs', // 存储后端:'opfs' | 'kv' | 'memory'
|
||||||
aria: { // 🆕 AriaEngine 专属配置透传(v0.5.0)
|
aria: {
|
||||||
walSyncMode: 'full', // WAL 同步模式
|
walSyncMode: 'full', // 'full' | 'batch' | 'none'
|
||||||
encryption: { password: 'my-password' }, // 全库 AES-GCM 加密
|
compression: true, // LZ4 页面压缩
|
||||||
|
encryption: { password: 'my-password' }, // 全库 AES-256-GCM 加密
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 与现有 API 完全兼容
|
// 与其余引擎 API 完全兼容
|
||||||
await db.defineTable('users', {
|
await db.defineTable('users', {
|
||||||
id: { type: 'string', primaryKey: true },
|
id: { type: 'string', primaryKey: true },
|
||||||
name: { type: 'string', required: true },
|
name: { type: 'string', required: true },
|
||||||
@@ -395,53 +320,84 @@ await db.query("INSERT INTO users VALUES ('1', 'Alice')");
|
|||||||
const rows = await db.query('SELECT * FROM users');
|
const rows = await db.query('SELECT * FROM users');
|
||||||
```
|
```
|
||||||
|
|
||||||
### AriaEngine 架构
|
### 架构
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────────────┐
|
┌──────────────────────────────────────────────┐
|
||||||
│ AriaEngine v0.6.0 │
|
│ AriaEngine v0.6.1 │
|
||||||
│ (implements IStorageEngine) │
|
├──────────────────────────────────────────────┤
|
||||||
├──────────────────────────────────────────┤
|
│ LSM-Tree │ Buffer Pool │ WAL │
|
||||||
│ LSM-Tree │ Buffer Pool │ WAL │
|
│ MemTable │ LRU (256页) │ 分片文件 │
|
||||||
│ MemTable │ LRU (256pp) │ Segmented │
|
│ +SSTable │ +FileManager │ +CRC-32 │
|
||||||
│ +SSTable │ +FileManager │ +CRC-32 │
|
│ (4KB 页面) │ │ +空洞检测 │
|
||||||
│ (4KB Page) │ │ +空洞检测 │
|
├────────────────┼───────────────┼────────────┤
|
||||||
├──────────────────────────────────────────┤
|
│ MVCC 事务 │ Bloom Filter │ LZ4 压缩 │
|
||||||
│ MVCC │ Bloom Filter │ LZ4 v2 │
|
│ 快照隔离 │ 二级索引 LSM │ +大小头 │
|
||||||
│ Snapshot │ FNV-1a+Murmur│ +大小头 │
|
│ +Savepoint │ (每列独立) │ │
|
||||||
│ +Savepoint│ +Serialize │ │
|
├────────────────┴───────────────┴────────────┤
|
||||||
├──────────────────────────────────────────┤
|
│ EncryptedBackend (AES-256-GCM 全库透明加密) │
|
||||||
│ EncryptedBackend │ 二级索引 │ ANALYZE │
|
├──────────────────────────────────────────────┤
|
||||||
│ AES-GCM 全库 │ Per-Column│ EXPLAIN │
|
│ 存储后端: OPFS Backend / KVStore Backend │
|
||||||
├──────────────────────────────────────────┤
|
│ Web Locks 多标签页独占锁 │
|
||||||
│ Web Locks 独占锁 │ OPFS Backend (v2) │
|
└──────────────────────────────────────────────┘
|
||||||
│ (多标签页) │ append/COW原子/清理 │
|
|
||||||
└──────────────────────────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| 特性 | 说明 |
|
### 核心机制
|
||||||
|
|
||||||
|
| 机制 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **LSM-Tree** | MemTable (红黑树) → SSTable 多级索引,异步 Compaction(从存储兜底加载,不依赖缓存),写背压 |
|
| **LSM-Tree** | MemTable(红黑树)→ 多级 SSTable,异步 Compaction(从存储兜底加载),写背压 |
|
||||||
| **页面化物理存储** | OPFS 后端下 SSTable 存为 4KB 页面(`pageStorage` 自动启用):FileManager 分配 pageId、BufferPool LRU 缓存(256 页 ≈ 1MB 可控内存)、save 即落盘(WAL checkpoint 截断安全)、`SSTableMeta.pageIds` 持久化(旧整 value 数据兼容)v0.5.0 |
|
| **页面化存储** | SSTable 存为 4KB 页面(FileManager 分配 pageId + BufferPool LRU 缓存 256 页 ≈ 1MB),`pageStorage` 在 opfs/kv 后端默认启用 |
|
||||||
| **WAL** | 分片文件 `__wal_%06d.bin`(4MB 阈值切换)+ 真追加(createWritable keepExistingData);标准 CRC32 记录校验;full/batch/none 三种模式(full 真正同步);空洞检测截断;16MB 阈值自动 checkpoint(活跃事务期间不截断)v0.5.0 |
|
| **WAL** | 分片文件 `__wal_%06d.bin` + 真追加;标准 CRC32 记录校验;full/batch/none 三模式;空洞检测截断;16MB 阈值自动 checkpoint(活跃事务期间不截断) |
|
||||||
| **崩溃恢复** | 打开时完整性校验(残缺 SSTable 自动跳过并清理 + 整文件 CRC-32 校验)、WAL 按 key 扫描恢复(不丢记录)、恢复后自动重建二级索引 |
|
| **崩溃恢复** | 打开时完整性校验(损坏 SSTable 自愈清理 + 整文件 CRC-32)、WAL 恢复、恢复后自动重建二级索引;`repair()` 清理孤儿页面与残留 |
|
||||||
| **全库加密** | `encryption.password` 配置 → EncryptedBackend 透明加解密(WAL/SSTable/Schema/元数据全密文);PBKDF2-SHA256 密钥派生 + salt/verifier 持久化;密码错误/数据篡改 → `ARIA_DECRYPT_ERROR`;明文库/加密库开关一致性检测 v0.5.0 |
|
| **全库加密** | `encryption.password` → EncryptedBackend 透明加解密(WAL/SSTable/Schema/元数据全密文);PBKDF2 派生 + salt 持久化;密码错误/篡改 → `ARIA_DECRYPT_ERROR` |
|
||||||
| **MVCC** | 版本链 + 快照隔离,事务读写不互斥,提交/回滚按事务写入 key 精准清理,自动 GC |
|
| **MVCC** | 版本链 + 快照隔离,事务读写不互斥,自动 GC |
|
||||||
| **Bloom Filter** | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时 probe |
|
| **二级索引** | 每列独立 LSM Tree,支持等值/范围扫描,跨重启恢复,WAL 恢复后自动重建 |
|
||||||
| **二级索引** | 每列独立 LSM Tree,支持 $eq/$in/$gt/$lt 范围扫描,跨重启自动恢复,WAL 恢复后自动重建 |
|
| **Bloom Filter** | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时快速否定 |
|
||||||
| **多标签页锁** | Web Locks API 库级独占锁(`ifAvailable` 不排队):第二个标签页抛 `ARIA_LOCKED`;不支持的环境降级无锁并告警 v0.5.0 |
|
| **多标签页锁** | Web Locks 库级独占锁,第二个标签页抛 `ARIA_LOCKED`;不支持的环境降级无锁并告警 |
|
||||||
| **Compaction** | 异步 Leveled Compaction,Level 0 > 8 触发同步背压,compactLevel public 接口 |
|
| **维护语句** | ANALYZE(表统计)/ REINDEX(重建索引)/ VACUUM(压缩 + MVCC GC)/ EXPLAIN(查询计划) |
|
||||||
| **OPFS Backend** | 零外部依赖纯浏览器文件系统:单文件 COW 原子写、`append` 真追加、写队列失败不中断、close 排空、崩溃残留自动清理 v0.5.0 |
|
|
||||||
|
### 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
|
```bash
|
||||||
npm install # 安装依赖
|
npm install # 安装依赖
|
||||||
npm run dev # 开发模式(localhost:3001)
|
npm run dev # 开发模式(localhost:3001)
|
||||||
npm run build # 生产构建(生成 dist/)
|
npm run build # 生产构建(生成 dist/)
|
||||||
npm test # 运行测试
|
npm test # 运行测试(1093 用例 · 70 套件)
|
||||||
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build)
|
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build)
|
||||||
npm run lint # 代码检查
|
npm run lint # 代码检查
|
||||||
npm run typecheck # 类型检查
|
npm run typecheck # 类型检查
|
||||||
@@ -449,57 +405,63 @@ npm run typecheck # 类型检查
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 项目状态
|
## 项目状态
|
||||||
|
|
||||||
| 指标 | 数值 |
|
| 指标 | 数值 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 测试用例 | 1064 |
|
| 测试用例 | 1093(+12 Playwright e2e) |
|
||||||
| 测试套件 | 66(+12 Playwright e2e) |
|
| 测试套件 | 70 |
|
||||||
| 行覆盖率 | 89.4% |
|
| 行覆盖率 | 89.7% |
|
||||||
| SQL 关键字 | 72 |
|
| SQL 关键字 | 72 |
|
||||||
| 存储引擎 | 5(Memory / **KVStore** / OPFS / Hybrid / **Aria**) |
|
| 存储引擎 | 5(Memory / KVStore / OPFS / Hybrid / Aria) |
|
||||||
|
| 运行时依赖 | 0 |
|
||||||
|
|
||||||
### 🌐 浏览器兼容性
|
### 浏览器兼容性
|
||||||
|
|
||||||
| 浏览器 | 最低版本 | Memory | KVStore(OPFS) | OPFS | Web Locks | Aria |
|
| 浏览器 | 最低版本 | Memory | Disk (KVStore) | Aria (OPFS) |
|
||||||
|--------|----------|--------|---------------|------|-----------|------|
|
|--------|----------|--------|----------------|-------------|
|
||||||
| Chrome | 102+ | ✅ | ✅ | ✅ | ✅ (69+) | ✅ |
|
| Chrome / Edge | 102+ | ✅ | ✅ | ✅ |
|
||||||
| Firefox | 111+ | ✅ | ✅ | ✅ | ✅ (96+) | ✅ |
|
| Firefox | 111+ | ✅ | ✅ | ✅ |
|
||||||
| Safari | 15.2+ | ✅ | ✅ | ✅ | ✅ (15.4+) | ✅ |
|
| Safari | 15.2+ | ✅ | ✅ | ✅ |
|
||||||
| Edge | 102+ | ✅ | ✅ | ✅ | ✅ (79+) | ✅ |
|
| Node.js | 16+ | ✅ | ✅(内存介质) | ✅(内存介质) |
|
||||||
| Node.js | 16+ | ✅ | ✅ (内存介质) | ❌ (测试用 mock) | ❌ | ✅ |
|
|
||||||
|
|
||||||
> **OPFS 支持说明(v0.4.5 更新)**: Chromium(Chrome/Edge 102+)、Firefox 111+、Safari 15.2+
|
> **OPFS**:基础 API(`createWritable` 原子写)在 Chromium 102+ / Firefox 111+ / Safari 15.2+ 均支持。
|
||||||
> 均已支持基础 OPFS API(`createWritable` 原子写)。OPFS 无跨文件事务,
|
> 无跨文件事务,AriaEngine 以 WAL 分片单文件原子写 + 空洞检测截断保证崩溃一致性。
|
||||||
> AriaEngine 以 WAL 分片单文件原子写 + 空洞检测截断保证崩溃一致性。
|
|
||||||
>
|
>
|
||||||
> **多标签页保护(v0.4.5)**: AriaEngine 打开库时通过 Web Locks API 获取库级独占锁,
|
> **多标签页保护**:AriaEngine 通过 Web Locks 获取库级独占锁,第二个标签页打开同一库抛 `ARIA_LOCKED`。
|
||||||
> 第二个标签页打开同一库会抛 `ARIA_LOCKED`。Web Locks 不可用的环境降级为无锁并告警
|
|
||||||
> (仅理论上,现代浏览器均支持)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📂 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── index.ts # 入口(MetonaSqlark + MeSqlark)
|
├── index.ts # 入口(MetonaSqlark + MeSqlark)
|
||||||
├── core.ts # 主类
|
├── core.ts # 主类(create/query/table/transaction/migration...)
|
||||||
├── constants.ts # 类型定义 + 配置 + DatabaseError
|
├── constants.ts # 配置类型 + DatabaseError + VERSION
|
||||||
├── connection-manager.ts # 连接池管理
|
├── connection-manager.ts # 连接池管理
|
||||||
├── engine/ # 存储引擎(Memory/KVStore/OPFS/Aria + kvstore 自研 KV 引擎)
|
├── engine/ # 存储引擎
|
||||||
├── migration/ # 旧 IndexedDB 数据迁移工具(一次性)
|
│ ├── 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)
|
├── hybrid/ # 混合引擎(write-through)
|
||||||
├── table/ # 表管理 + Schema 校验
|
├── migration/ # 旧 IndexedDB 数据迁移工具(一次性)
|
||||||
├── query/ # AST + Builder + Compiler + Executor
|
├── query/ # AST + Builder + Compiler + Executor
|
||||||
├── sql/ # Lexer + Parser(递归下降)
|
├── sql/ # Lexer + Parser(递归下降,72 关键字)
|
||||||
├── transaction/ # 事务管理(支持回滚)
|
├── table/ # 表管理 + Schema 校验
|
||||||
|
├── transaction/ # 事务管理(自动回滚)
|
||||||
├── plugin/ # 插件系统(14 hooks)
|
├── plugin/ # 插件系统(14 hooks)
|
||||||
└── integrations/ # React / Vue hooks
|
└── integrations/ # React / Vue hooks
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📄 License
|
## License
|
||||||
|
|
||||||
MIT © [MetonaTeam](https://git.metona.cn/MetonaTeam/MetonaSqlark)
|
MIT © [MetonaTeam](https://git.metona.cn/MetonaTeam/MetonaSqlark)
|
||||||
|
|||||||
Vendored
+101
-40
@@ -34,7 +34,7 @@ class DatabaseError extends Error {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 版本
|
// 版本
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const VERSION = '0.6.0';
|
const VERSION = '0.6.1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||||
@@ -978,12 +978,15 @@ class OPFSBackend {
|
|||||||
*
|
*
|
||||||
* 仅用于测试与 Node 环境;浏览器使用 OPFS 介质(KVStore 默认自动选择)。
|
* 仅用于测试与 Node 环境;浏览器使用 OPFS 介质(KVStore 默认自动选择)。
|
||||||
*/
|
*/
|
||||||
/** 全局注册表:dbName → key → ArrayBuffer(跨实例共享,模拟持久化) */
|
/** 全局注册表:dbName → key → chunk 列表(跨实例共享,模拟持久化) */
|
||||||
const registry = new Map();
|
const registry = new Map();
|
||||||
class SharedMemoryBackend {
|
class SharedMemoryBackend {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dbName = '';
|
this.dbName = '';
|
||||||
this.store = null;
|
/** 存储:key → chunk 列表(append O(1),read 时一次性拼接缓存) */
|
||||||
|
this.chunks = new Map();
|
||||||
|
/** 惰性拼接缓存(read 后缓存,write/append 失效) */
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
/** 清空全局注册表(测试隔离用) */
|
/** 清空全局注册表(测试隔离用) */
|
||||||
static clearRegistry() {
|
static clearRegistry() {
|
||||||
@@ -994,61 +997,80 @@ class SharedMemoryBackend {
|
|||||||
if (!registry.has(name)) {
|
if (!registry.has(name)) {
|
||||||
registry.set(name, new Map());
|
registry.set(name, new Map());
|
||||||
}
|
}
|
||||||
this.store = registry.get(name);
|
// 从注册表恢复 chunks(持久化语义)
|
||||||
|
this.chunks = registry.get(name);
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
/** close 不清除数据(持久化语义:重开同名库数据仍在) */
|
/** close 不清除数据(持久化语义:重开同名库数据仍在) */
|
||||||
async close() {
|
async close() {
|
||||||
this.store = null;
|
this.chunks = new Map();
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
isOpen() {
|
isOpen() {
|
||||||
return this.store !== null;
|
return this.dbName !== '';
|
||||||
}
|
}
|
||||||
async read(key) {
|
async read(key) {
|
||||||
return this.store?.get(key) ?? null;
|
if (this.materialized.has(key))
|
||||||
|
return this.materialized.get(key);
|
||||||
|
const list = this.chunks.get(key);
|
||||||
|
if (!list || list.length === 0)
|
||||||
|
return null;
|
||||||
|
if (list.length === 1) {
|
||||||
|
this.materialized.set(key, list[0]);
|
||||||
|
return list[0];
|
||||||
|
}
|
||||||
|
const total = list.reduce((s2, c) => s2 + c.byteLength, 0);
|
||||||
|
const combined = new Uint8Array(total);
|
||||||
|
let off = 0;
|
||||||
|
for (const c of list) {
|
||||||
|
combined.set(new Uint8Array(c), off);
|
||||||
|
off += c.byteLength;
|
||||||
|
}
|
||||||
|
const buf = combined.buffer;
|
||||||
|
this.materialized.set(key, buf);
|
||||||
|
return buf;
|
||||||
}
|
}
|
||||||
async write(key, data) {
|
async write(key, data) {
|
||||||
this.store?.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
|
this.materialized.set(key, data);
|
||||||
}
|
}
|
||||||
async append(key, data) {
|
async append(key, data) {
|
||||||
if (!this.store)
|
// O(1) 追加:只记录 chunk,read 时惰性拼接
|
||||||
return;
|
const list = this.chunks.get(key);
|
||||||
const existing = this.store.get(key);
|
if (list) {
|
||||||
if (existing) {
|
list.push(data);
|
||||||
const combined = new Uint8Array(existing.byteLength + data.byteLength);
|
|
||||||
combined.set(new Uint8Array(existing), 0);
|
|
||||||
combined.set(new Uint8Array(data), existing.byteLength);
|
|
||||||
this.store.set(key, combined.buffer);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.store.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
}
|
}
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
async writeMany(entries) {
|
async writeMany(entries) {
|
||||||
if (!this.store)
|
|
||||||
return;
|
|
||||||
// 同步批量写入 = 原子(JS 单线程,无中间 await 点)
|
// 同步批量写入 = 原子(JS 单线程,无中间 await 点)
|
||||||
for (const [key, data] of Object.entries(entries)) {
|
for (const [key, data] of Object.entries(entries)) {
|
||||||
this.store.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
|
this.materialized.set(key, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async delete(key) {
|
async delete(key) {
|
||||||
this.store?.delete(key);
|
this.chunks.delete(key);
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
async deleteMany(keys) {
|
async deleteMany(keys) {
|
||||||
if (!this.store)
|
|
||||||
return;
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
this.store.delete(key);
|
this.chunks.delete(key);
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async listKeys() {
|
async listKeys() {
|
||||||
return this.store ? Array.from(this.store.keys()) : [];
|
return Array.from(this.chunks.keys());
|
||||||
}
|
}
|
||||||
async exists(key) {
|
async exists(key) {
|
||||||
return this.store?.has(key) ?? false;
|
return this.chunks.has(key);
|
||||||
}
|
}
|
||||||
async clear() {
|
async clear() {
|
||||||
this.store?.clear();
|
this.chunks.clear();
|
||||||
|
this.materialized.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3282,7 +3304,11 @@ class SSTableReader {
|
|||||||
if (this.indexEntries.length === 0)
|
if (this.indexEntries.length === 0)
|
||||||
return;
|
return;
|
||||||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||||||
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
|
// v0.6.1-fix(P0): 索引键是"块内最后一个 key"(builder 约定),
|
||||||
|
// locateBlockLE 返回最后一个 tail <= endKey 的块,但下一个块(tail > endKey)
|
||||||
|
// 可能仍包含 < endKey 的条目(如末块 t5:k49425..k49995 尾 key 是 t6 前缀)→
|
||||||
|
// 被排除导致范围扫描漏读尾部数据。多扫一个块,条目级过滤保证不丢。
|
||||||
|
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey) + 1);
|
||||||
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
|
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
|
||||||
return;
|
return;
|
||||||
const lenSize = this.lenFieldSize();
|
const lenSize = this.lenFieldSize();
|
||||||
@@ -4084,16 +4110,21 @@ class LSM {
|
|||||||
}
|
}
|
||||||
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction)
|
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction)
|
||||||
await this.drainChain();
|
await this.drainChain();
|
||||||
// 若仍有 frozen 数据未刷盘,在链尾追加
|
// v0.6.1-fix(P0): 剩余数据 flush 必须挂链串行 —— 此前直接 await 执行,
|
||||||
|
// 与链上 compaction 并发写 SSTable meta:compaction 产物 saveMeta 后,
|
||||||
|
// memtable flush 的 saveMeta 读到中间态列表(含 compaction 产物)→ 覆盖产物
|
||||||
|
// 引用 → compaction 产物变孤儿 → 索引/主表数据静默丢失(优雅关闭后重开丢 75%)。
|
||||||
|
// 挂链后按序执行(memtable flush 在 compaction 之后),meta 无竞态。
|
||||||
if (this.immutableMemtable) {
|
if (this.immutableMemtable) {
|
||||||
const frozen = this.immutableMemtable;
|
const frozen = this.immutableMemtable;
|
||||||
await this.flushImmutableAsync(frozen);
|
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
|
||||||
}
|
}
|
||||||
if (this.memtable.getEntryCount() > 0) {
|
if (this.memtable.getEntryCount() > 0) {
|
||||||
this.freezeMemtable();
|
this.freezeMemtable();
|
||||||
const frozen = this.immutableMemtable;
|
const frozen = this.immutableMemtable;
|
||||||
if (frozen)
|
if (frozen) {
|
||||||
await this.flushImmutableAsync(frozen);
|
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.frozenMemtables = [];
|
this.frozenMemtables = [];
|
||||||
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
|
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
|
||||||
@@ -5290,12 +5321,25 @@ class FileManager {
|
|||||||
async init(dbName) {
|
async init(dbName) {
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
const meta = await this.backend.read('__aria_meta');
|
const meta = await this.backend.read('__aria_meta');
|
||||||
|
let nextPageId = 1;
|
||||||
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
|
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
|
||||||
const view = new DataView(meta);
|
nextPageId = new DataView(meta).getUint32(0, false);
|
||||||
this.nextPageId = view.getUint32(0, false);
|
|
||||||
}
|
}
|
||||||
else {
|
// v0.6.1-fix(P0): 崩溃一致性 —— allocatePageIds 的 saveMeta 可能在分配
|
||||||
this.nextPageId = 1;
|
// 页面后被崩溃中断(nextPageId 回退)。恢复时若按回退值继续分配,
|
||||||
|
// 页面 id 复用会覆盖旧页面;随后 compaction 按 meta.pageIds 删除"旧"SSTable
|
||||||
|
// 时误删被复用的新数据 → 崩溃恢复后静默丢数据(kv 后端 2 万行丢 75%)。
|
||||||
|
// 修复:以"现存最大页面 id + 1"为准(单调不回退,绝不复用已存在页面)。
|
||||||
|
const keys = await this.backend.listKeys();
|
||||||
|
for (const k of keys) {
|
||||||
|
if (k.startsWith('pg_')) {
|
||||||
|
const id = Number.parseInt(k.slice(3), 10);
|
||||||
|
if (!Number.isNaN(id) && id + 1 > nextPageId)
|
||||||
|
nextPageId = id + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nextPageId = nextPageId;
|
||||||
|
if (!meta || !(meta instanceof ArrayBuffer) || meta.byteLength < 4 || nextPageId !== new DataView(meta).getUint32(0, false)) {
|
||||||
await this.saveMeta();
|
await this.saveMeta();
|
||||||
}
|
}
|
||||||
this.metaLoaded = true;
|
this.metaLoaded = true;
|
||||||
@@ -6181,7 +6225,17 @@ class AriaEngine {
|
|||||||
},
|
},
|
||||||
getBufferedBytes: () => this.wal.getBufferedBytes(),
|
getBufferedBytes: () => this.wal.getBufferedBytes(),
|
||||||
getBufferedCount: () => this.wal.getBufferedCount(),
|
getBufferedCount: () => this.wal.getBufferedCount(),
|
||||||
}, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
|
}, {
|
||||||
|
flushAll: async () => {
|
||||||
|
// v0.6.1-fix: checkpoint 必须同时落盘二级索引 LSM —
|
||||||
|
// 此前只 flush 主 LSM,checkpoint 截断 WAL 后崩溃时索引 memtable 未落盘、
|
||||||
|
// WAL 为空跳过重建 → 二级索引静默丢失最后一批条目(生产数据一致性问题)
|
||||||
|
await this.lsm.flush();
|
||||||
|
for (const idxLsm of this.secondaryIndexes.values()) {
|
||||||
|
await idxLsm.flush();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, this.config.checkpointInterval, this.config.walSizeThreshold);
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
}
|
}
|
||||||
async close() {
|
async close() {
|
||||||
@@ -6432,12 +6486,17 @@ class AriaEngine {
|
|||||||
const pks = [];
|
const pks = [];
|
||||||
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
|
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
|
||||||
const walRecords = [];
|
const walRecords = [];
|
||||||
|
// v0.6.1-perf: 批量预加载本批 PK 涉及的 SSTable(一次 drainChain)。
|
||||||
|
// 此前循环内逐行 prefetchKeys —— 每行 await drainChain 排空后台链,
|
||||||
|
// 后台 compaction 在链上数秒时每行阻塞数秒 → 大数据量插入性能悬崖
|
||||||
|
// (10 万行 kv 后端从 5ms/批暴跌到 8~11s/批)。批内新数据在 memtable
|
||||||
|
// 或 flush 产物(自动入缓存),循环内 lsm.get 始终完整。
|
||||||
|
await this.lsm.prefetchKeys(rows.map((r) => `${tableName}:${String(r[pkCol])}`));
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const validated = this.validateRow(schema, row);
|
const validated = this.validateRow(schema, row);
|
||||||
const pkValue = String(validated[pkCol]);
|
const pkValue = String(validated[pkCol]);
|
||||||
const key = `${tableName}:${pkValue}`;
|
const key = `${tableName}:${pkValue}`;
|
||||||
// Check duplicate in LSM + transaction snapshot
|
// Check duplicate in LSM + transaction snapshot
|
||||||
await this.lsm.prefetchKeys([key]);
|
|
||||||
const existing = this.currentTxnId
|
const existing = this.currentTxnId
|
||||||
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
|
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
|
||||||
: this.lsm.get(key);
|
: this.lsm.get(key);
|
||||||
@@ -7306,13 +7365,15 @@ class AriaEngine {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS 后端启用,显式配置可覆盖) */
|
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS / KVStore 后端启用,显式配置可覆盖) */
|
||||||
isPageStorage() {
|
isPageStorage() {
|
||||||
if (this.config.pageStorage === true)
|
if (this.config.pageStorage === true)
|
||||||
return true;
|
return true;
|
||||||
if (this.config.pageStorage === false)
|
if (this.config.pageStorage === false)
|
||||||
return false;
|
return false;
|
||||||
return this.config.storageBackend === 'opfs';
|
// v0.6.1: kv 后端同样页面化 — 整 value SSTable(4MB)超过 1MB 页面缓存时
|
||||||
|
// 每次读取全量重载;拆 4KB 页面后缓存按页命中,大数据量读放大消除
|
||||||
|
return this.config.storageBackend === 'opfs' || this.config.storageBackend === 'kv';
|
||||||
}
|
}
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// WAL 恢复
|
// WAL 恢复
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -164,7 +164,7 @@ interface MetonaPlugin {
|
|||||||
/** 销毁 */
|
/** 销毁 */
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
}
|
}
|
||||||
declare const VERSION = "0.6.0";
|
declare const VERSION = "0.6.1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark Plugin — 插件系统
|
* metona-sqlark Plugin — 插件系统
|
||||||
@@ -1065,7 +1065,7 @@ declare class AriaEngine implements IStorageEngine {
|
|||||||
* - id 序列独立(避免 v0.2.4 共享 id 空间导致的文件互相覆盖)
|
* - id 序列独立(避免 v0.2.4 共享 id 空间导致的文件互相覆盖)
|
||||||
*/
|
*/
|
||||||
private createSSTableStore;
|
private createSSTableStore;
|
||||||
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS 后端启用,显式配置可覆盖) */
|
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS / KVStore 后端启用,显式配置可覆盖) */
|
||||||
private isPageStorage;
|
private isPageStorage;
|
||||||
private applyWALRecord;
|
private applyWALRecord;
|
||||||
/**
|
/**
|
||||||
|
|||||||
Vendored
+101
-40
@@ -30,7 +30,7 @@ class DatabaseError extends Error {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 版本
|
// 版本
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const VERSION = '0.6.0';
|
const VERSION = '0.6.1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||||
@@ -974,12 +974,15 @@ class OPFSBackend {
|
|||||||
*
|
*
|
||||||
* 仅用于测试与 Node 环境;浏览器使用 OPFS 介质(KVStore 默认自动选择)。
|
* 仅用于测试与 Node 环境;浏览器使用 OPFS 介质(KVStore 默认自动选择)。
|
||||||
*/
|
*/
|
||||||
/** 全局注册表:dbName → key → ArrayBuffer(跨实例共享,模拟持久化) */
|
/** 全局注册表:dbName → key → chunk 列表(跨实例共享,模拟持久化) */
|
||||||
const registry = new Map();
|
const registry = new Map();
|
||||||
class SharedMemoryBackend {
|
class SharedMemoryBackend {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dbName = '';
|
this.dbName = '';
|
||||||
this.store = null;
|
/** 存储:key → chunk 列表(append O(1),read 时一次性拼接缓存) */
|
||||||
|
this.chunks = new Map();
|
||||||
|
/** 惰性拼接缓存(read 后缓存,write/append 失效) */
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
/** 清空全局注册表(测试隔离用) */
|
/** 清空全局注册表(测试隔离用) */
|
||||||
static clearRegistry() {
|
static clearRegistry() {
|
||||||
@@ -990,61 +993,80 @@ class SharedMemoryBackend {
|
|||||||
if (!registry.has(name)) {
|
if (!registry.has(name)) {
|
||||||
registry.set(name, new Map());
|
registry.set(name, new Map());
|
||||||
}
|
}
|
||||||
this.store = registry.get(name);
|
// 从注册表恢复 chunks(持久化语义)
|
||||||
|
this.chunks = registry.get(name);
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
/** close 不清除数据(持久化语义:重开同名库数据仍在) */
|
/** close 不清除数据(持久化语义:重开同名库数据仍在) */
|
||||||
async close() {
|
async close() {
|
||||||
this.store = null;
|
this.chunks = new Map();
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
isOpen() {
|
isOpen() {
|
||||||
return this.store !== null;
|
return this.dbName !== '';
|
||||||
}
|
}
|
||||||
async read(key) {
|
async read(key) {
|
||||||
return this.store?.get(key) ?? null;
|
if (this.materialized.has(key))
|
||||||
|
return this.materialized.get(key);
|
||||||
|
const list = this.chunks.get(key);
|
||||||
|
if (!list || list.length === 0)
|
||||||
|
return null;
|
||||||
|
if (list.length === 1) {
|
||||||
|
this.materialized.set(key, list[0]);
|
||||||
|
return list[0];
|
||||||
|
}
|
||||||
|
const total = list.reduce((s2, c) => s2 + c.byteLength, 0);
|
||||||
|
const combined = new Uint8Array(total);
|
||||||
|
let off = 0;
|
||||||
|
for (const c of list) {
|
||||||
|
combined.set(new Uint8Array(c), off);
|
||||||
|
off += c.byteLength;
|
||||||
|
}
|
||||||
|
const buf = combined.buffer;
|
||||||
|
this.materialized.set(key, buf);
|
||||||
|
return buf;
|
||||||
}
|
}
|
||||||
async write(key, data) {
|
async write(key, data) {
|
||||||
this.store?.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
|
this.materialized.set(key, data);
|
||||||
}
|
}
|
||||||
async append(key, data) {
|
async append(key, data) {
|
||||||
if (!this.store)
|
// O(1) 追加:只记录 chunk,read 时惰性拼接
|
||||||
return;
|
const list = this.chunks.get(key);
|
||||||
const existing = this.store.get(key);
|
if (list) {
|
||||||
if (existing) {
|
list.push(data);
|
||||||
const combined = new Uint8Array(existing.byteLength + data.byteLength);
|
|
||||||
combined.set(new Uint8Array(existing), 0);
|
|
||||||
combined.set(new Uint8Array(data), existing.byteLength);
|
|
||||||
this.store.set(key, combined.buffer);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.store.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
}
|
}
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
async writeMany(entries) {
|
async writeMany(entries) {
|
||||||
if (!this.store)
|
|
||||||
return;
|
|
||||||
// 同步批量写入 = 原子(JS 单线程,无中间 await 点)
|
// 同步批量写入 = 原子(JS 单线程,无中间 await 点)
|
||||||
for (const [key, data] of Object.entries(entries)) {
|
for (const [key, data] of Object.entries(entries)) {
|
||||||
this.store.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
|
this.materialized.set(key, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async delete(key) {
|
async delete(key) {
|
||||||
this.store?.delete(key);
|
this.chunks.delete(key);
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
async deleteMany(keys) {
|
async deleteMany(keys) {
|
||||||
if (!this.store)
|
|
||||||
return;
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
this.store.delete(key);
|
this.chunks.delete(key);
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async listKeys() {
|
async listKeys() {
|
||||||
return this.store ? Array.from(this.store.keys()) : [];
|
return Array.from(this.chunks.keys());
|
||||||
}
|
}
|
||||||
async exists(key) {
|
async exists(key) {
|
||||||
return this.store?.has(key) ?? false;
|
return this.chunks.has(key);
|
||||||
}
|
}
|
||||||
async clear() {
|
async clear() {
|
||||||
this.store?.clear();
|
this.chunks.clear();
|
||||||
|
this.materialized.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3278,7 +3300,11 @@ class SSTableReader {
|
|||||||
if (this.indexEntries.length === 0)
|
if (this.indexEntries.length === 0)
|
||||||
return;
|
return;
|
||||||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||||||
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
|
// v0.6.1-fix(P0): 索引键是"块内最后一个 key"(builder 约定),
|
||||||
|
// locateBlockLE 返回最后一个 tail <= endKey 的块,但下一个块(tail > endKey)
|
||||||
|
// 可能仍包含 < endKey 的条目(如末块 t5:k49425..k49995 尾 key 是 t6 前缀)→
|
||||||
|
// 被排除导致范围扫描漏读尾部数据。多扫一个块,条目级过滤保证不丢。
|
||||||
|
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey) + 1);
|
||||||
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
|
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
|
||||||
return;
|
return;
|
||||||
const lenSize = this.lenFieldSize();
|
const lenSize = this.lenFieldSize();
|
||||||
@@ -4080,16 +4106,21 @@ class LSM {
|
|||||||
}
|
}
|
||||||
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction)
|
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction)
|
||||||
await this.drainChain();
|
await this.drainChain();
|
||||||
// 若仍有 frozen 数据未刷盘,在链尾追加
|
// v0.6.1-fix(P0): 剩余数据 flush 必须挂链串行 —— 此前直接 await 执行,
|
||||||
|
// 与链上 compaction 并发写 SSTable meta:compaction 产物 saveMeta 后,
|
||||||
|
// memtable flush 的 saveMeta 读到中间态列表(含 compaction 产物)→ 覆盖产物
|
||||||
|
// 引用 → compaction 产物变孤儿 → 索引/主表数据静默丢失(优雅关闭后重开丢 75%)。
|
||||||
|
// 挂链后按序执行(memtable flush 在 compaction 之后),meta 无竞态。
|
||||||
if (this.immutableMemtable) {
|
if (this.immutableMemtable) {
|
||||||
const frozen = this.immutableMemtable;
|
const frozen = this.immutableMemtable;
|
||||||
await this.flushImmutableAsync(frozen);
|
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
|
||||||
}
|
}
|
||||||
if (this.memtable.getEntryCount() > 0) {
|
if (this.memtable.getEntryCount() > 0) {
|
||||||
this.freezeMemtable();
|
this.freezeMemtable();
|
||||||
const frozen = this.immutableMemtable;
|
const frozen = this.immutableMemtable;
|
||||||
if (frozen)
|
if (frozen) {
|
||||||
await this.flushImmutableAsync(frozen);
|
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.frozenMemtables = [];
|
this.frozenMemtables = [];
|
||||||
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
|
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
|
||||||
@@ -5286,12 +5317,25 @@ class FileManager {
|
|||||||
async init(dbName) {
|
async init(dbName) {
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
const meta = await this.backend.read('__aria_meta');
|
const meta = await this.backend.read('__aria_meta');
|
||||||
|
let nextPageId = 1;
|
||||||
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
|
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
|
||||||
const view = new DataView(meta);
|
nextPageId = new DataView(meta).getUint32(0, false);
|
||||||
this.nextPageId = view.getUint32(0, false);
|
|
||||||
}
|
}
|
||||||
else {
|
// v0.6.1-fix(P0): 崩溃一致性 —— allocatePageIds 的 saveMeta 可能在分配
|
||||||
this.nextPageId = 1;
|
// 页面后被崩溃中断(nextPageId 回退)。恢复时若按回退值继续分配,
|
||||||
|
// 页面 id 复用会覆盖旧页面;随后 compaction 按 meta.pageIds 删除"旧"SSTable
|
||||||
|
// 时误删被复用的新数据 → 崩溃恢复后静默丢数据(kv 后端 2 万行丢 75%)。
|
||||||
|
// 修复:以"现存最大页面 id + 1"为准(单调不回退,绝不复用已存在页面)。
|
||||||
|
const keys = await this.backend.listKeys();
|
||||||
|
for (const k of keys) {
|
||||||
|
if (k.startsWith('pg_')) {
|
||||||
|
const id = Number.parseInt(k.slice(3), 10);
|
||||||
|
if (!Number.isNaN(id) && id + 1 > nextPageId)
|
||||||
|
nextPageId = id + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nextPageId = nextPageId;
|
||||||
|
if (!meta || !(meta instanceof ArrayBuffer) || meta.byteLength < 4 || nextPageId !== new DataView(meta).getUint32(0, false)) {
|
||||||
await this.saveMeta();
|
await this.saveMeta();
|
||||||
}
|
}
|
||||||
this.metaLoaded = true;
|
this.metaLoaded = true;
|
||||||
@@ -6177,7 +6221,17 @@ class AriaEngine {
|
|||||||
},
|
},
|
||||||
getBufferedBytes: () => this.wal.getBufferedBytes(),
|
getBufferedBytes: () => this.wal.getBufferedBytes(),
|
||||||
getBufferedCount: () => this.wal.getBufferedCount(),
|
getBufferedCount: () => this.wal.getBufferedCount(),
|
||||||
}, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
|
}, {
|
||||||
|
flushAll: async () => {
|
||||||
|
// v0.6.1-fix: checkpoint 必须同时落盘二级索引 LSM —
|
||||||
|
// 此前只 flush 主 LSM,checkpoint 截断 WAL 后崩溃时索引 memtable 未落盘、
|
||||||
|
// WAL 为空跳过重建 → 二级索引静默丢失最后一批条目(生产数据一致性问题)
|
||||||
|
await this.lsm.flush();
|
||||||
|
for (const idxLsm of this.secondaryIndexes.values()) {
|
||||||
|
await idxLsm.flush();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, this.config.checkpointInterval, this.config.walSizeThreshold);
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
}
|
}
|
||||||
async close() {
|
async close() {
|
||||||
@@ -6428,12 +6482,17 @@ class AriaEngine {
|
|||||||
const pks = [];
|
const pks = [];
|
||||||
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
|
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
|
||||||
const walRecords = [];
|
const walRecords = [];
|
||||||
|
// v0.6.1-perf: 批量预加载本批 PK 涉及的 SSTable(一次 drainChain)。
|
||||||
|
// 此前循环内逐行 prefetchKeys —— 每行 await drainChain 排空后台链,
|
||||||
|
// 后台 compaction 在链上数秒时每行阻塞数秒 → 大数据量插入性能悬崖
|
||||||
|
// (10 万行 kv 后端从 5ms/批暴跌到 8~11s/批)。批内新数据在 memtable
|
||||||
|
// 或 flush 产物(自动入缓存),循环内 lsm.get 始终完整。
|
||||||
|
await this.lsm.prefetchKeys(rows.map((r) => `${tableName}:${String(r[pkCol])}`));
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const validated = this.validateRow(schema, row);
|
const validated = this.validateRow(schema, row);
|
||||||
const pkValue = String(validated[pkCol]);
|
const pkValue = String(validated[pkCol]);
|
||||||
const key = `${tableName}:${pkValue}`;
|
const key = `${tableName}:${pkValue}`;
|
||||||
// Check duplicate in LSM + transaction snapshot
|
// Check duplicate in LSM + transaction snapshot
|
||||||
await this.lsm.prefetchKeys([key]);
|
|
||||||
const existing = this.currentTxnId
|
const existing = this.currentTxnId
|
||||||
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
|
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
|
||||||
: this.lsm.get(key);
|
: this.lsm.get(key);
|
||||||
@@ -7302,13 +7361,15 @@ class AriaEngine {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS 后端启用,显式配置可覆盖) */
|
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS / KVStore 后端启用,显式配置可覆盖) */
|
||||||
isPageStorage() {
|
isPageStorage() {
|
||||||
if (this.config.pageStorage === true)
|
if (this.config.pageStorage === true)
|
||||||
return true;
|
return true;
|
||||||
if (this.config.pageStorage === false)
|
if (this.config.pageStorage === false)
|
||||||
return false;
|
return false;
|
||||||
return this.config.storageBackend === 'opfs';
|
// v0.6.1: kv 后端同样页面化 — 整 value SSTable(4MB)超过 1MB 页面缓存时
|
||||||
|
// 每次读取全量重载;拆 4KB 页面后缓存按页命中,大数据量读放大消除
|
||||||
|
return this.config.storageBackend === 'opfs' || this.config.storageBackend === 'kv';
|
||||||
}
|
}
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// WAL 恢复
|
// WAL 恢复
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+101
-40
@@ -36,7 +36,7 @@
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 版本
|
// 版本
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const VERSION = '0.6.0';
|
const VERSION = '0.6.1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||||
@@ -980,12 +980,15 @@
|
|||||||
*
|
*
|
||||||
* 仅用于测试与 Node 环境;浏览器使用 OPFS 介质(KVStore 默认自动选择)。
|
* 仅用于测试与 Node 环境;浏览器使用 OPFS 介质(KVStore 默认自动选择)。
|
||||||
*/
|
*/
|
||||||
/** 全局注册表:dbName → key → ArrayBuffer(跨实例共享,模拟持久化) */
|
/** 全局注册表:dbName → key → chunk 列表(跨实例共享,模拟持久化) */
|
||||||
const registry = new Map();
|
const registry = new Map();
|
||||||
class SharedMemoryBackend {
|
class SharedMemoryBackend {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dbName = '';
|
this.dbName = '';
|
||||||
this.store = null;
|
/** 存储:key → chunk 列表(append O(1),read 时一次性拼接缓存) */
|
||||||
|
this.chunks = new Map();
|
||||||
|
/** 惰性拼接缓存(read 后缓存,write/append 失效) */
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
/** 清空全局注册表(测试隔离用) */
|
/** 清空全局注册表(测试隔离用) */
|
||||||
static clearRegistry() {
|
static clearRegistry() {
|
||||||
@@ -996,61 +999,80 @@
|
|||||||
if (!registry.has(name)) {
|
if (!registry.has(name)) {
|
||||||
registry.set(name, new Map());
|
registry.set(name, new Map());
|
||||||
}
|
}
|
||||||
this.store = registry.get(name);
|
// 从注册表恢复 chunks(持久化语义)
|
||||||
|
this.chunks = registry.get(name);
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
/** close 不清除数据(持久化语义:重开同名库数据仍在) */
|
/** close 不清除数据(持久化语义:重开同名库数据仍在) */
|
||||||
async close() {
|
async close() {
|
||||||
this.store = null;
|
this.chunks = new Map();
|
||||||
|
this.materialized = new Map();
|
||||||
}
|
}
|
||||||
isOpen() {
|
isOpen() {
|
||||||
return this.store !== null;
|
return this.dbName !== '';
|
||||||
}
|
}
|
||||||
async read(key) {
|
async read(key) {
|
||||||
return this.store?.get(key) ?? null;
|
if (this.materialized.has(key))
|
||||||
|
return this.materialized.get(key);
|
||||||
|
const list = this.chunks.get(key);
|
||||||
|
if (!list || list.length === 0)
|
||||||
|
return null;
|
||||||
|
if (list.length === 1) {
|
||||||
|
this.materialized.set(key, list[0]);
|
||||||
|
return list[0];
|
||||||
|
}
|
||||||
|
const total = list.reduce((s2, c) => s2 + c.byteLength, 0);
|
||||||
|
const combined = new Uint8Array(total);
|
||||||
|
let off = 0;
|
||||||
|
for (const c of list) {
|
||||||
|
combined.set(new Uint8Array(c), off);
|
||||||
|
off += c.byteLength;
|
||||||
|
}
|
||||||
|
const buf = combined.buffer;
|
||||||
|
this.materialized.set(key, buf);
|
||||||
|
return buf;
|
||||||
}
|
}
|
||||||
async write(key, data) {
|
async write(key, data) {
|
||||||
this.store?.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
|
this.materialized.set(key, data);
|
||||||
}
|
}
|
||||||
async append(key, data) {
|
async append(key, data) {
|
||||||
if (!this.store)
|
// O(1) 追加:只记录 chunk,read 时惰性拼接
|
||||||
return;
|
const list = this.chunks.get(key);
|
||||||
const existing = this.store.get(key);
|
if (list) {
|
||||||
if (existing) {
|
list.push(data);
|
||||||
const combined = new Uint8Array(existing.byteLength + data.byteLength);
|
|
||||||
combined.set(new Uint8Array(existing), 0);
|
|
||||||
combined.set(new Uint8Array(data), existing.byteLength);
|
|
||||||
this.store.set(key, combined.buffer);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.store.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
}
|
}
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
async writeMany(entries) {
|
async writeMany(entries) {
|
||||||
if (!this.store)
|
|
||||||
return;
|
|
||||||
// 同步批量写入 = 原子(JS 单线程,无中间 await 点)
|
// 同步批量写入 = 原子(JS 单线程,无中间 await 点)
|
||||||
for (const [key, data] of Object.entries(entries)) {
|
for (const [key, data] of Object.entries(entries)) {
|
||||||
this.store.set(key, data);
|
this.chunks.set(key, [data]);
|
||||||
|
this.materialized.set(key, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async delete(key) {
|
async delete(key) {
|
||||||
this.store?.delete(key);
|
this.chunks.delete(key);
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
async deleteMany(keys) {
|
async deleteMany(keys) {
|
||||||
if (!this.store)
|
|
||||||
return;
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
this.store.delete(key);
|
this.chunks.delete(key);
|
||||||
|
this.materialized.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async listKeys() {
|
async listKeys() {
|
||||||
return this.store ? Array.from(this.store.keys()) : [];
|
return Array.from(this.chunks.keys());
|
||||||
}
|
}
|
||||||
async exists(key) {
|
async exists(key) {
|
||||||
return this.store?.has(key) ?? false;
|
return this.chunks.has(key);
|
||||||
}
|
}
|
||||||
async clear() {
|
async clear() {
|
||||||
this.store?.clear();
|
this.chunks.clear();
|
||||||
|
this.materialized.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3284,7 +3306,11 @@
|
|||||||
if (this.indexEntries.length === 0)
|
if (this.indexEntries.length === 0)
|
||||||
return;
|
return;
|
||||||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||||||
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
|
// v0.6.1-fix(P0): 索引键是"块内最后一个 key"(builder 约定),
|
||||||
|
// locateBlockLE 返回最后一个 tail <= endKey 的块,但下一个块(tail > endKey)
|
||||||
|
// 可能仍包含 < endKey 的条目(如末块 t5:k49425..k49995 尾 key 是 t6 前缀)→
|
||||||
|
// 被排除导致范围扫描漏读尾部数据。多扫一个块,条目级过滤保证不丢。
|
||||||
|
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey) + 1);
|
||||||
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
|
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
|
||||||
return;
|
return;
|
||||||
const lenSize = this.lenFieldSize();
|
const lenSize = this.lenFieldSize();
|
||||||
@@ -4086,16 +4112,21 @@
|
|||||||
}
|
}
|
||||||
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction)
|
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction)
|
||||||
await this.drainChain();
|
await this.drainChain();
|
||||||
// 若仍有 frozen 数据未刷盘,在链尾追加
|
// v0.6.1-fix(P0): 剩余数据 flush 必须挂链串行 —— 此前直接 await 执行,
|
||||||
|
// 与链上 compaction 并发写 SSTable meta:compaction 产物 saveMeta 后,
|
||||||
|
// memtable flush 的 saveMeta 读到中间态列表(含 compaction 产物)→ 覆盖产物
|
||||||
|
// 引用 → compaction 产物变孤儿 → 索引/主表数据静默丢失(优雅关闭后重开丢 75%)。
|
||||||
|
// 挂链后按序执行(memtable flush 在 compaction 之后),meta 无竞态。
|
||||||
if (this.immutableMemtable) {
|
if (this.immutableMemtable) {
|
||||||
const frozen = this.immutableMemtable;
|
const frozen = this.immutableMemtable;
|
||||||
await this.flushImmutableAsync(frozen);
|
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
|
||||||
}
|
}
|
||||||
if (this.memtable.getEntryCount() > 0) {
|
if (this.memtable.getEntryCount() > 0) {
|
||||||
this.freezeMemtable();
|
this.freezeMemtable();
|
||||||
const frozen = this.immutableMemtable;
|
const frozen = this.immutableMemtable;
|
||||||
if (frozen)
|
if (frozen) {
|
||||||
await this.flushImmutableAsync(frozen);
|
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.frozenMemtables = [];
|
this.frozenMemtables = [];
|
||||||
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
|
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
|
||||||
@@ -5292,12 +5323,25 @@
|
|||||||
async init(dbName) {
|
async init(dbName) {
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
const meta = await this.backend.read('__aria_meta');
|
const meta = await this.backend.read('__aria_meta');
|
||||||
|
let nextPageId = 1;
|
||||||
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
|
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
|
||||||
const view = new DataView(meta);
|
nextPageId = new DataView(meta).getUint32(0, false);
|
||||||
this.nextPageId = view.getUint32(0, false);
|
|
||||||
}
|
}
|
||||||
else {
|
// v0.6.1-fix(P0): 崩溃一致性 —— allocatePageIds 的 saveMeta 可能在分配
|
||||||
this.nextPageId = 1;
|
// 页面后被崩溃中断(nextPageId 回退)。恢复时若按回退值继续分配,
|
||||||
|
// 页面 id 复用会覆盖旧页面;随后 compaction 按 meta.pageIds 删除"旧"SSTable
|
||||||
|
// 时误删被复用的新数据 → 崩溃恢复后静默丢数据(kv 后端 2 万行丢 75%)。
|
||||||
|
// 修复:以"现存最大页面 id + 1"为准(单调不回退,绝不复用已存在页面)。
|
||||||
|
const keys = await this.backend.listKeys();
|
||||||
|
for (const k of keys) {
|
||||||
|
if (k.startsWith('pg_')) {
|
||||||
|
const id = Number.parseInt(k.slice(3), 10);
|
||||||
|
if (!Number.isNaN(id) && id + 1 > nextPageId)
|
||||||
|
nextPageId = id + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nextPageId = nextPageId;
|
||||||
|
if (!meta || !(meta instanceof ArrayBuffer) || meta.byteLength < 4 || nextPageId !== new DataView(meta).getUint32(0, false)) {
|
||||||
await this.saveMeta();
|
await this.saveMeta();
|
||||||
}
|
}
|
||||||
this.metaLoaded = true;
|
this.metaLoaded = true;
|
||||||
@@ -6183,7 +6227,17 @@
|
|||||||
},
|
},
|
||||||
getBufferedBytes: () => this.wal.getBufferedBytes(),
|
getBufferedBytes: () => this.wal.getBufferedBytes(),
|
||||||
getBufferedCount: () => this.wal.getBufferedCount(),
|
getBufferedCount: () => this.wal.getBufferedCount(),
|
||||||
}, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
|
}, {
|
||||||
|
flushAll: async () => {
|
||||||
|
// v0.6.1-fix: checkpoint 必须同时落盘二级索引 LSM —
|
||||||
|
// 此前只 flush 主 LSM,checkpoint 截断 WAL 后崩溃时索引 memtable 未落盘、
|
||||||
|
// WAL 为空跳过重建 → 二级索引静默丢失最后一批条目(生产数据一致性问题)
|
||||||
|
await this.lsm.flush();
|
||||||
|
for (const idxLsm of this.secondaryIndexes.values()) {
|
||||||
|
await idxLsm.flush();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, this.config.checkpointInterval, this.config.walSizeThreshold);
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
}
|
}
|
||||||
async close() {
|
async close() {
|
||||||
@@ -6434,12 +6488,17 @@
|
|||||||
const pks = [];
|
const pks = [];
|
||||||
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
|
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
|
||||||
const walRecords = [];
|
const walRecords = [];
|
||||||
|
// v0.6.1-perf: 批量预加载本批 PK 涉及的 SSTable(一次 drainChain)。
|
||||||
|
// 此前循环内逐行 prefetchKeys —— 每行 await drainChain 排空后台链,
|
||||||
|
// 后台 compaction 在链上数秒时每行阻塞数秒 → 大数据量插入性能悬崖
|
||||||
|
// (10 万行 kv 后端从 5ms/批暴跌到 8~11s/批)。批内新数据在 memtable
|
||||||
|
// 或 flush 产物(自动入缓存),循环内 lsm.get 始终完整。
|
||||||
|
await this.lsm.prefetchKeys(rows.map((r) => `${tableName}:${String(r[pkCol])}`));
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const validated = this.validateRow(schema, row);
|
const validated = this.validateRow(schema, row);
|
||||||
const pkValue = String(validated[pkCol]);
|
const pkValue = String(validated[pkCol]);
|
||||||
const key = `${tableName}:${pkValue}`;
|
const key = `${tableName}:${pkValue}`;
|
||||||
// Check duplicate in LSM + transaction snapshot
|
// Check duplicate in LSM + transaction snapshot
|
||||||
await this.lsm.prefetchKeys([key]);
|
|
||||||
const existing = this.currentTxnId
|
const existing = this.currentTxnId
|
||||||
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
|
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
|
||||||
: this.lsm.get(key);
|
: this.lsm.get(key);
|
||||||
@@ -7308,13 +7367,15 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS 后端启用,显式配置可覆盖) */
|
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS / KVStore 后端启用,显式配置可覆盖) */
|
||||||
isPageStorage() {
|
isPageStorage() {
|
||||||
if (this.config.pageStorage === true)
|
if (this.config.pageStorage === true)
|
||||||
return true;
|
return true;
|
||||||
if (this.config.pageStorage === false)
|
if (this.config.pageStorage === false)
|
||||||
return false;
|
return false;
|
||||||
return this.config.storageBackend === 'opfs';
|
// v0.6.1: kv 后端同样页面化 — 整 value SSTable(4MB)超过 1MB 页面缓存时
|
||||||
|
// 每次读取全量重载;拆 4KB 页面后缓存按页命中,大数据量读放大消除
|
||||||
|
return this.config.storageBackend === 'opfs' || this.config.storageBackend === 'kv';
|
||||||
}
|
}
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// WAL 恢复
|
// WAL 恢复
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+11
-9
@@ -768,16 +768,18 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
|
|||||||
<strong>v0.5.0 存储后端生产级硬化</strong> — 真实 CRC-32 完整性校验(SSTable 整文件 + WAL 记录,旧文件兼容)· 全库 AES-256-GCM 透明加密(EncryptedBackend + PBKDF2 密钥派生 + 密码验证)· WAL 分片文件重构(真追加 + 空洞检测 + 旧格式迁移)· SSTable 4KB 页面化物理存储(BufferPool/FileManager 真实接入,meta 存 pageIds 兼容旧数据)· OPFS 后端 v2(append 真追加 / 写队列健壮性 / 残留清理)· Web Locks 多标签页独占锁(ARIA_LOCKED)· LZ4 v2 原始大小头 · Playwright 真实 Chromium e2e(7 用例)· DatabaseConfig.aria 配置透传。<br>
|
<strong>v0.5.0 存储后端生产级硬化</strong> — 真实 CRC-32 完整性校验(SSTable 整文件 + WAL 记录,旧文件兼容)· 全库 AES-256-GCM 透明加密(EncryptedBackend + PBKDF2 密钥派生 + 密码验证)· WAL 分片文件重构(真追加 + 空洞检测 + 旧格式迁移)· SSTable 4KB 页面化物理存储(BufferPool/FileManager 真实接入,meta 存 pageIds 兼容旧数据)· OPFS 后端 v2(append 真追加 / 写队列健壮性 / 残留清理)· Web Locks 多标签页独占锁(ARIA_LOCKED)· LZ4 v2 原始大小头 · Playwright 真实 Chromium e2e(7 用例)· DatabaseConfig.aria 配置透传。<br>
|
||||||
<strong>v0.5.1 深度审查修复</strong> — 14 个生命周期钩子全部真实接线(此前 6 个 CRUD 钩子从未触发)· EXPLAIN / ANALYZE / REINDEX / VACUUM / SAVEPOINT SQL 入口补齐(此前仅有引擎方法无法触发)· db.backup() 公共方法 · 删除全部死代码(utils.ts 整文件 / MVCC 读侧 / estimateQueryCost 未接线优化器 / 40+ 统计辅助方法)。<br>
|
<strong>v0.5.1 深度审查修复</strong> — 14 个生命周期钩子全部真实接线(此前 6 个 CRUD 钩子从未触发)· EXPLAIN / ANALYZE / REINDEX / VACUUM / SAVEPOINT SQL 入口补齐(此前仅有引擎方法无法触发)· db.backup() 公共方法 · 删除全部死代码(utils.ts 整文件 / MVCC 读侧 / estimateQueryCost 未接线优化器 / 40+ 统计辅助方法)。<br>
|
||||||
<strong>v0.6.0 完全移除 IndexedDB</strong> — 自研 KVStore 事务存储引擎(多 key 原子写 = 单日志记录原子追加 · 快照 checkpoint + 两阶段崩溃恢复 · CRC-32 自愈)· disk 模式切换 KVStoreEngine(替代 IndexedDBEngine + OPFSEngine)· 事务内 DDL / 外键级联 / 二级索引完整持久化 · migrateFromIndexedDB() 旧库一键迁移 · 10 万 key 压力验证 · e2e 崩溃注入 + KVStoreEngine 真实环境(12 用例)。<br>
|
<strong>v0.6.0 完全移除 IndexedDB</strong> — 自研 KVStore 事务存储引擎(多 key 原子写 = 单日志记录原子追加 · 快照 checkpoint + 两阶段崩溃恢复 · CRC-32 自愈)· disk 模式切换 KVStoreEngine(替代 IndexedDBEngine + OPFSEngine)· 事务内 DDL / 外键级联 / 二级索引完整持久化 · migrateFromIndexedDB() 旧库一键迁移 · 10 万 key 压力验证 · e2e 崩溃注入 + KVStoreEngine 真实环境(12 用例)。<br>
|
||||||
<strong>v0.6.1 生产可用性深度审查</strong> — MemoryEngine 级联环(A→B→A)无限递归修复(visited 保护,与 Aria 对齐)· KVStore 快照损坏水位 bug 修复(metaSeq 误跳日志)· 多表事务 / 级联写入合并单条日志记录真原子(崩溃无部分提交)· 未 open 防护统一 · <strong>AriaEngine 可选自研 KVStore 后端</strong>(storageBackend: 'kv':KVStore APPEND 日志支持 WAL 追加、writeMany 真原子、不再依赖浏览器 OPFS)· 27 个异常场景测试 + 10 个 aria+kv 集成测试 · 1064 测试 66 套件 · 89.4% 行覆盖率。</p>
|
<strong>v0.6.1 生产可用性深度审查</strong> — MemoryEngine 级联环(A→B→A)无限递归修复(visited 保护,与 Aria 对齐)· KVStore 快照损坏水位 bug 修复(metaSeq 误跳日志)· 多表事务 / 级联写入合并单条日志记录真原子(崩溃无部分提交)· 未 open 防护统一 · <strong>AriaEngine 可选自研 KVStore 后端</strong>(storageBackend: 'kv':KVStore APPEND 日志支持 WAL 追加、writeMany 真原子、不再依赖浏览器 OPFS)· 二级索引范围扫描尾块漏读 P0 修复 · 批量插入性能悬崖修复(10 万行 kv 后端 353s → 12.5s)· 页面 id 崩溃回退 / flush 并发写 meta 两个崩溃恢复 P0 修复 · 10 万级 kv/opfs 双后端回归 · 1093 测试 70 套件 · 89.7% 行覆盖率。</p>
|
||||||
|
|
||||||
<h3>存储模式对比</h3>
|
<h3>存储模式对比</h3>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>特性</th><th>Memory</th><th>Disk(IDB)</th><th>Disk(OPFS)</th><th>Hybrid</th><th>Aria</th></tr>
|
<tr><th>特性</th><th>Memory</th><th>Disk (KVStore)</th><th>Hybrid</th><th>Aria</th></tr>
|
||||||
<tr><td>持久化</td><td>❌</td><td>✅ IDB</td><td>✅ OPFS</td><td>✅ 双写</td><td>✅ 后端决定</td></tr>
|
<tr><td>持久化</td><td>❌</td><td>✅ KVStore(OPFS / 内存介质)</td><td>✅ 双写</td><td>✅ 后端决定</td></tr>
|
||||||
<tr><td>事务</td><td>✅ 快照</td><td>✅ 原子</td><td>✅ 快照</td><td>✅ 双引擎</td><td>✅ MVCC</td></tr>
|
<tr><td>事务</td><td>✅ 快照回滚</td><td>✅ 单日志记录原子写</td><td>✅ 双引擎(磁盘优先)</td><td>✅ MVCC 快照隔离</td></tr>
|
||||||
<tr><td>索引</td><td>Hash</td><td>Hash</td><td>Hash</td><td>Hash</td><td>LSM二级</td></tr>
|
<tr><td>索引</td><td>Hash</td><td>Hash(重启恢复)</td><td>Hash</td><td>LSM 二级(重启恢复)</td></tr>
|
||||||
<tr><td>上限</td><td>内存</td><td>~2GB</td><td>磁盘</td><td>~2GB</td><td>内存</td></tr>
|
<tr><td>上限</td><td>内存</td><td>磁盘可用</td><td>磁盘可用</td><td>内存</td></tr>
|
||||||
<tr><td>浏览器</td><td>全部</td><td>全部</td><td>Chrome/Edge 102+ · Firefox 111+ · Safari 15.2+</td><td>全部</td><td>全部</td></tr>
|
<tr><td>多标签页锁</td><td>—</td><td>—</td><td>—</td><td>✅ Web Locks</td></tr>
|
||||||
|
<tr><td>全库加密</td><td>—</td><td>—</td><td>—</td><td>✅ AES-GCM</td></tr>
|
||||||
|
<tr><td>浏览器</td><td>全部</td><td>Chrome/Edge 102+ · Firefox 111+ · Safari 15.2+</td><td>同上</td><td>同上(后端决定)</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3>核心特性</h3>
|
<h3>核心特性</h3>
|
||||||
@@ -799,7 +801,7 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
|
|||||||
<span class="k">const</span> db = <span class="k">await</span> <span class="f">MetonaSqlark.create</span>({
|
<span class="k">const</span> db = <span class="k">await</span> <span class="f">MetonaSqlark.create</span>({
|
||||||
<span class="s">name</span>: <span class="s">'my-app'</span>,
|
<span class="s">name</span>: <span class="s">'my-app'</span>,
|
||||||
<span class="s">mode</span>: <span class="s">'aria'</span>, <span class="c">// 🆕 AriaEngine 模式</span>
|
<span class="s">mode</span>: <span class="s">'aria'</span>, <span class="c">// 🆕 AriaEngine 模式</span>
|
||||||
<span class="s">diskEngine</span>: <span class="s">'opfs'</span>, <span class="c">// 底层存储后端(opfs | memory)</span>
|
<span class="s">diskEngine</span>: <span class="s">'opfs'</span>, <span class="c">// 底层存储后端(opfs | kv | memory)</span>
|
||||||
<span class="s">aria</span>: { <span class="c">// AriaEngine 专属配置透传 🆕 v0.5.0</span>
|
<span class="s">aria</span>: { <span class="c">// AriaEngine 专属配置透传 🆕 v0.5.0</span>
|
||||||
<span class="s">walSyncMode</span>: <span class="s">'full'</span>, <span class="c">// WAL 同步模式</span>
|
<span class="s">walSyncMode</span>: <span class="s">'full'</span>, <span class="c">// WAL 同步模式</span>
|
||||||
<span class="s">encryption</span>: { <span class="s">password</span>: <span class="s">'my-password'</span> }, <span class="c">// 全库 AES-GCM 加密 🆕 v0.5.0</span>
|
<span class="s">encryption</span>: { <span class="s">password</span>: <span class="s">'my-password'</span> }, <span class="c">// 全库 AES-GCM 加密 🆕 v0.5.0</span>
|
||||||
@@ -812,7 +814,7 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
|
|||||||
<span class="s">bufferPoolPages</span>: <span class="n">256</span>, <span class="c">// Buffer Pool 页面数量</span>
|
<span class="s">bufferPoolPages</span>: <span class="n">256</span>, <span class="c">// Buffer Pool 页面数量</span>
|
||||||
<span class="s">memtableSizeThreshold</span>: <span class="n">4194304</span>, <span class="c">// MemTable 刷盘阈值 4MB</span>
|
<span class="s">memtableSizeThreshold</span>: <span class="n">4194304</span>, <span class="c">// MemTable 刷盘阈值 4MB</span>
|
||||||
<span class="s">walSyncMode</span>: <span class="s">'full'</span>, <span class="c">// 'full' | 'batch' | 'none'(默认 full)</span>
|
<span class="s">walSyncMode</span>: <span class="s">'full'</span>, <span class="c">// 'full' | 'batch' | 'none'(默认 full)</span>
|
||||||
<span class="s">storageBackend</span>: <span class="s">'opfs'</span>, <span class="c">// 存储后端(opfs | memory)</span>
|
<span class="s">storageBackend</span>: <span class="s">'opfs'</span>, <span class="c">// 存储后端(opfs | kv | memory)</span>
|
||||||
<span class="s">encryption</span>: { <span class="s">password</span>: <span class="s">'my-password'</span> }, <span class="c">// 全库加密</span>
|
<span class="s">encryption</span>: { <span class="s">password</span>: <span class="s">'my-password'</span> }, <span class="c">// 全库加密</span>
|
||||||
});</pre>
|
});</pre>
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -153,7 +153,7 @@
|
|||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.6.1 生产可用性硬化 — 1064测试 66套件 · 自研 KVStore 事务引擎 · 多 key 原子写 · 级联环保护 · 崩溃恢复 · 旧库一键迁移</div>
|
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.6.1 生产可用性硬化 — 1093测试 70套件 · 自研 KVStore 事务引擎 · 多 key 原子写 · 级联环保护 · 崩溃恢复 · 旧库一键迁移</div>
|
||||||
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
|
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
|
||||||
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
|
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -342,9 +342,9 @@ npm install @metona-team/metona-sqlark
|
|||||||
├──────────────────────────────────────────────────────────┤
|
├──────────────────────────────────────────────────────────┤
|
||||||
│ <span style="color:#34d399;">IStorageEngine</span> Interface │
|
│ <span style="color:#34d399;">IStorageEngine</span> Interface │
|
||||||
├──────────┬──────────┬──────────┬──────────┬──────────────┤
|
├──────────┬──────────┬──────────┬──────────┬──────────────┤
|
||||||
│ <span style="color:#fbbf24;">Memory </span>│ <span style="color:#fbbf24;">KVStore </span>│ <span style="color:#fbbf24;">OPFS </span>│ <span style="color:#fbbf24;">Hybrid </span>│ <span style="color:#ec4899;font-weight:bold;">AriaEngine 🆕</span> │
|
│ <span style="color:#fbbf24;">Memory </span>│ <span style="color:#fbbf24;">KVStore </span>│ <span style="color:#fbbf24;">OPFS </span>│ <span style="color:#fbbf24;">Hybrid </span>│ <span style="color:#ec4899;font-weight:bold;">AriaEngine</span> │
|
||||||
│ Engine │ Engine │ Engine │ Engine │ LSM-Tree │
|
│ Engine │ Engine │ Engine │ Engine │ LSM-Tree │
|
||||||
│ (Map) │ (IDB) │ (File) │ (W-Thru) │ WAL + MVCC │
|
│ (Map) │ (自研KV) │ (OPFS) │ (W-Thru) │ WAL + MVCC │
|
||||||
└──────────┴──────────┴──────────┴──────────┴──────────────┘
|
└──────────┴──────────┴──────────┴──────────┴──────────────┘
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -414,12 +414,12 @@ npm install @metona-team/metona-sqlark
|
|||||||
<p>MetonaSqlark 的核心指标</p>
|
<p>MetonaSqlark 的核心指标</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat-card"><div class="num">1064</div><div class="label">测试用例</div></div>
|
<div class="stat-card"><div class="num">1093</div><div class="label">测试用例</div></div>
|
||||||
<div class="stat-card"><div class="num">89.4%</div><div class="label">行覆盖率</div></div>
|
<div class="stat-card"><div class="num">89.7%</div><div class="label">行覆盖率</div></div>
|
||||||
<div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div>
|
<div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div>
|
||||||
<div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div>
|
<div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div>
|
||||||
<div class="stat-card"><div class="num">72</div><div class="label">SQL 关键字</div></div>
|
<div class="stat-card"><div class="num">72</div><div class="label">SQL 关键字</div></div>
|
||||||
<div class="stat-card"><div class="num">66</div><div class="label">测试套件</div></div>
|
<div class="stat-card"><div class="num">70</div><div class="label">测试套件</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+1
-1
@@ -214,4 +214,4 @@ export class DatabaseError extends Error {
|
|||||||
// 版本
|
// 版本
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const VERSION = '0.6.0';
|
export const VERSION = '0.6.1';
|
||||||
|
|||||||
Reference in New Issue
Block a user