release: v0.2.2 AriaEngine OPFS 自研存储后端 — 零依赖纯文件系统
This commit is contained in:
@@ -1,54 +1,11 @@
|
||||
# v0.2.1 生产加固方案
|
||||
# v0.2.2 AriaEngine OPFS 自研存储后端
|
||||
|
||||
## 核心原则
|
||||
- **所有新增测试方法必须确保不会导致 CI 卡死**(无 setTimeout/setInterval 泄漏,无 fake-indexeddb 滥用,无无限循环)
|
||||
- 优先级 P0 → P1 → P2 顺序执行
|
||||
## 实现内容
|
||||
|
||||
## P0 — Critical(4项)
|
||||
|
||||
1. **Memory 引擎事务回滚修复** — `src/engine/memory.ts` 的事务快照机制已存在但 TransactionManager 未正确调用。修复 `src/transaction/index.ts` 在 rollback 时调用引擎层 rollback。
|
||||
|
||||
2. **WAL CRC 校验验证** — `src/engine/aria/wal/log.ts` decodeAllRecords 跳过 CRC,恢复时验证 CRC 不匹配则丢弃 + 告警。
|
||||
|
||||
3. **Hybrid 引擎提交顺序** — `src/hybrid/index.ts` commit 先写磁盘再写内存,磁盘失败回滚内存。
|
||||
|
||||
4. **SSTableReader rangeScan 边界条件** — 修复多 block 场景下 startBlockIdx/endBlockIdx 可能越界。
|
||||
|
||||
## P1 — High(6项)
|
||||
|
||||
5. **ColumnDef 约束激活** — `src/table/schema.ts` checkFieldType 中激活 min/max/maxLength 校验。配套新增 schema 测试(纯同步,不会卡死)。
|
||||
|
||||
6. **查询结果上限** — `DatabaseConfig` 增加 `maxRowsPerQuery` 默认 10000,所有引擎 find() 中截断。测试用 memory 引擎(纯同步)。
|
||||
|
||||
7. **`onError` 回调接入** — `src/core.ts` CRUD 操作 catch 块调用 `this.config.onError`。测试用 jest.fn() 验证(同步断言)。
|
||||
|
||||
8. **OPFS 引擎基础测试** — 补 5 个测试:open/close、createTable、insert、find、count。由于 OPFS 浏览器专属,测试用内存 fallback mock 方式,避免 jsdom 不具备 OPFS 导致卡死。
|
||||
|
||||
9. **MVCC gc() 定期调用** — AriaEngine `checkpointManager.tick()` 后每 N 次 op 调用一次 gc()。测试用 MVCC 单元测试(纯同步)。
|
||||
|
||||
10. **SSTable `get()` 返回 null 兜底** — 已修复(P0-4包含)
|
||||
|
||||
## P2 — Medium(6项)
|
||||
|
||||
11. **LZ4 压缩修复 + 正确性测试** — 修复 compress/decompress 往返 bug,补 3 个往返正确性测试。**注意**:测试数据量控制在 500 字节以内,避免大循环。纯同步测试。
|
||||
|
||||
12. **debug 模式** — `DatabaseConfig` 加 `debug: boolean`,console.debug 输出关键操作日志。
|
||||
|
||||
13. **RESTRICT 外键行为修正** — cascadeDelete 检测到引用行时抛 `FOREIGN_KEY_VIOLATION` 而非静默保留孤儿。
|
||||
|
||||
14. **浏览器兼容表** — README 补充 Chrome 80+/Firefox 80+/Safari 14+/Edge 80+ 支持声明。
|
||||
|
||||
15. **React/Vue 集成去 @ts-nocheck** — 移除注释,补充完整类型声明。(测试仅验证编译不验证运行时,避免 jsdom 缺失 hooks 导致卡死)
|
||||
|
||||
16. **SQL 多语句防护** — Parser 检测到多余分号后还有有效 token 时报 `PARSE_ERROR`。
|
||||
|
||||
## 严禁事项(新增测试必须遵守)
|
||||
|
||||
- ❌ 不使用 `setTimeout`/`setInterval` 除非配合 `jest.useFakeTimers()`
|
||||
- ❌ 不使用 `fake-indexeddb` 进行多轮 open/close/reopen
|
||||
- ❌ 不创建 IndexedDB 连接后不关闭
|
||||
- ❌ 不使用 `done()` 回调模式
|
||||
- ❌ 不对大数据集(>1000条)做 O(n²) 操作
|
||||
- ✅ 优先使用 async/await + Memory Backend
|
||||
- ✅ Mock 代替真实浏览器 API
|
||||
- ✅ 每个 afterEach 确保资源清理
|
||||
1. **新增 `src/engine/aria/store/opfs_backend.ts`** — OPFSBackend 实现 IStorageBackend 接口,纯文件系统,零依赖
|
||||
2. **`src/engine/aria/types.ts`** — storageBackend 类型增加 `'opfs'`
|
||||
3. **`src/engine/aria/index.ts`** — open() 增加 opfs 分支
|
||||
4. **`src/index.ts`** — 导出 OPFSBackend
|
||||
5. **`package.json`** — 版本 0.2.2
|
||||
6. **README.md / CHANGELOG.md / site** — 更新版本号和特性说明
|
||||
7. **测试** — OPFS 浏览器专属,用 mock 验证不卡死
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
All notable changes to MetonaSqlark will be documented in this file.
|
||||
|
||||
## [0.2.2] - 2026-07-27
|
||||
|
||||
### Added
|
||||
- **OPFS 自研存储后端** — AriaEngine 新增 `OPFSBackend`,纯浏览器文件系统 API,零 IndexedDB 依赖
|
||||
- 每个 key 对应一个二进制文件,存储在 `navigator.storage.getDirectory()` 下
|
||||
- 支持 read/write/delete/list/exists/clear 完整接口
|
||||
- 页面文件(4KB)、WAL 日志、Schema、SSTable 全部存储为独立文件
|
||||
- `{ dbName }/pg_1`, `{ dbName }/__wal_0`, `{ dbName }/__aria_schemas` ...
|
||||
|
||||
### Changed
|
||||
- `AriaEngineConfig.storageBackend` 已支持 `'opfs'`,`AriaEngine.open()` 自动选择 OPFSBackend
|
||||
|
||||
---
|
||||
|
||||
## [0.2.1] - 2026-07-27
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# MetonaSqlark
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-0.2.1-blue?style=flat-square" alt="version">
|
||||
<img src="https://img.shields.io/badge/version-0.2.2-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-91.0%25-brightgreen?style=flat-square" alt="coverage">
|
||||
<img src="https://img.shields.io/badge/tests-526%20passed-success?style=flat-square" alt="tests">
|
||||
@@ -271,6 +271,7 @@ const rows = await db.query('SELECT * FROM users');
|
||||
| **Bloom Filter** | FNV-1a + Murmur 双哈希,快速否定 key |
|
||||
| **Slotted Page** | 4KB 固定页面,Slot Directory + Tuple 二进制序列化 |
|
||||
| **Compaction** | Leveled Compaction,自动合并回收空间 |
|
||||
| **OPFS Backend** 🆕 | 自研文件存储后端,零外部依赖,纯二进制页面文件 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Vendored
+88
-1
@@ -2780,6 +2780,89 @@ class MemoryBackend {
|
||||
}
|
||||
}
|
||||
|
||||
class OPFSBackend {
|
||||
constructor() {
|
||||
this.root = null;
|
||||
this.dbDir = null;
|
||||
this.dbName = '';
|
||||
}
|
||||
async open(name) {
|
||||
this.dbName = name;
|
||||
this.root = await navigator.storage.getDirectory();
|
||||
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
|
||||
}
|
||||
async close() {
|
||||
this.dbDir = null;
|
||||
this.root = null;
|
||||
}
|
||||
isOpen() {
|
||||
return this.dbDir !== null;
|
||||
}
|
||||
async read(key) {
|
||||
if (!this.dbDir)
|
||||
return null;
|
||||
try {
|
||||
const fh = await this.dbDir.getFileHandle(key);
|
||||
const file = await fh.getFile();
|
||||
return await file.arrayBuffer();
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async write(key, data) {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
const fh = await this.dbDir.getFileHandle(key, { create: true });
|
||||
const writable = await fh.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
async delete(key) {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
try {
|
||||
await this.dbDir.removeEntry(key);
|
||||
}
|
||||
catch {
|
||||
// 文件不存在则忽略
|
||||
}
|
||||
}
|
||||
async listKeys() {
|
||||
if (!this.dbDir)
|
||||
return [];
|
||||
const keys = [];
|
||||
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
|
||||
const dir = this.dbDir;
|
||||
for await (const [name] of dir.entries()) {
|
||||
keys.push(name);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
async exists(key) {
|
||||
if (!this.dbDir)
|
||||
return false;
|
||||
try {
|
||||
await this.dbDir.getFileHandle(key);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async clear() {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
const dir = this.dbDir;
|
||||
for await (const [name] of dir.entries()) {
|
||||
try {
|
||||
await this.dbDir.removeEntry(name);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
@@ -2817,7 +2900,10 @@ class AriaEngine {
|
||||
return;
|
||||
this.dbName = dbName;
|
||||
// 1. 存储后端
|
||||
if (this.config.storageBackend === 'indexeddb') {
|
||||
if (this.config.storageBackend === 'opfs') {
|
||||
this.backend = new OPFSBackend();
|
||||
}
|
||||
else if (this.config.storageBackend === 'indexeddb') {
|
||||
this.backend = new IndexedDBBackend();
|
||||
}
|
||||
else {
|
||||
@@ -5825,6 +5911,7 @@ exports.IndexedDBEngine = IndexedDBEngine;
|
||||
exports.MeSqlark = MeSqlark;
|
||||
exports.MemoryEngine = MemoryEngine;
|
||||
exports.MetonaSqlark = MetonaSqlark;
|
||||
exports.OPFSBackend = OPFSBackend;
|
||||
exports.OPFSEngine = OPFSEngine;
|
||||
exports.Table = Table;
|
||||
exports.VERSION = VERSION;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+54
-1
@@ -851,6 +851,59 @@ interface Token {
|
||||
/** 将 SQL 字符串解析为 Token 列表 */
|
||||
declare function tokenize(sql: string): Token[];
|
||||
|
||||
/**
|
||||
* AriaEngine Storage Backend — 存储后端抽象层
|
||||
* @module engine/aria/store/backend
|
||||
*
|
||||
* 封装底层浏览器存储 API(IndexedDB / OPFS / Memory 回退),
|
||||
* 供 Buffer Pool 的 PageIO 和 WAL 的 WALStore 使用。
|
||||
*/
|
||||
interface IStorageBackend {
|
||||
/** 打开存储 */
|
||||
open(name: string): Promise<void>;
|
||||
/** 关闭存储 */
|
||||
close(): Promise<void>;
|
||||
/** 是否已打开 */
|
||||
isOpen(): boolean;
|
||||
/** 读取数据块 */
|
||||
read(key: string): Promise<ArrayBuffer | null>;
|
||||
/** 写入数据块 */
|
||||
write(key: string, data: ArrayBuffer): Promise<void>;
|
||||
/** 删除数据块 */
|
||||
delete(key: string): Promise<void>;
|
||||
/** 列出所有 key */
|
||||
listKeys(): Promise<string[]>;
|
||||
/** 检查 key 是否存在 */
|
||||
exists(key: string): Promise<boolean>;
|
||||
/** 清空所有数据 */
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine OPFS Backend — 基于 Origin Private File System 的自研存储后端
|
||||
* @module engine/aria/store/opfs_backend
|
||||
*
|
||||
* 零外部依赖,纯浏览器文件系统 API。
|
||||
* 每个 key 对应 OPFS 目录下的一个二进制文件。
|
||||
*
|
||||
* 浏览器要求:Chrome 102+ / Edge 102+
|
||||
*/
|
||||
|
||||
declare class OPFSBackend implements IStorageBackend {
|
||||
private root;
|
||||
private dbDir;
|
||||
private dbName;
|
||||
open(name: string): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
isOpen(): boolean;
|
||||
read(key: string): Promise<ArrayBuffer | null>;
|
||||
write(key: string, data: ArrayBuffer): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
listKeys(): Promise<string[]>;
|
||||
exists(key: string): Promise<boolean>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark — 入口文件
|
||||
* @module metona-sqlark
|
||||
@@ -896,4 +949,4 @@ declare global {
|
||||
|
||||
declare const MeSqlark: typeof MetonaSqlark;
|
||||
|
||||
export { AriaEngine, AriaEngineConfig, ColumnDef, DatabaseConfig, DeleteStatement, DiskEngine, FieldType, HybridEngine, IStorageEngine, IndexedDBEngine, InsertStatement, MeSqlark, MemoryEngine, MetonaSqlark, OPFSEngine, SelectStatement, Statement, StorageMode, Table, TableSchema, UpdateStatement, VERSION, api, create, api as default, parse, tokenize };
|
||||
export { AriaEngine, AriaEngineConfig, ColumnDef, DatabaseConfig, DeleteStatement, DiskEngine, FieldType, HybridEngine, IStorageEngine, IndexedDBEngine, InsertStatement, MeSqlark, MemoryEngine, MetonaSqlark, OPFSBackend, OPFSEngine, SelectStatement, Statement, StorageMode, Table, TableSchema, UpdateStatement, VERSION, api, create, api as default, parse, tokenize };
|
||||
|
||||
Vendored
+88
-2
@@ -2776,6 +2776,89 @@ class MemoryBackend {
|
||||
}
|
||||
}
|
||||
|
||||
class OPFSBackend {
|
||||
constructor() {
|
||||
this.root = null;
|
||||
this.dbDir = null;
|
||||
this.dbName = '';
|
||||
}
|
||||
async open(name) {
|
||||
this.dbName = name;
|
||||
this.root = await navigator.storage.getDirectory();
|
||||
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
|
||||
}
|
||||
async close() {
|
||||
this.dbDir = null;
|
||||
this.root = null;
|
||||
}
|
||||
isOpen() {
|
||||
return this.dbDir !== null;
|
||||
}
|
||||
async read(key) {
|
||||
if (!this.dbDir)
|
||||
return null;
|
||||
try {
|
||||
const fh = await this.dbDir.getFileHandle(key);
|
||||
const file = await fh.getFile();
|
||||
return await file.arrayBuffer();
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async write(key, data) {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
const fh = await this.dbDir.getFileHandle(key, { create: true });
|
||||
const writable = await fh.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
async delete(key) {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
try {
|
||||
await this.dbDir.removeEntry(key);
|
||||
}
|
||||
catch {
|
||||
// 文件不存在则忽略
|
||||
}
|
||||
}
|
||||
async listKeys() {
|
||||
if (!this.dbDir)
|
||||
return [];
|
||||
const keys = [];
|
||||
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
|
||||
const dir = this.dbDir;
|
||||
for await (const [name] of dir.entries()) {
|
||||
keys.push(name);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
async exists(key) {
|
||||
if (!this.dbDir)
|
||||
return false;
|
||||
try {
|
||||
await this.dbDir.getFileHandle(key);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async clear() {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
const dir = this.dbDir;
|
||||
for await (const [name] of dir.entries()) {
|
||||
try {
|
||||
await this.dbDir.removeEntry(name);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
@@ -2813,7 +2896,10 @@ class AriaEngine {
|
||||
return;
|
||||
this.dbName = dbName;
|
||||
// 1. 存储后端
|
||||
if (this.config.storageBackend === 'indexeddb') {
|
||||
if (this.config.storageBackend === 'opfs') {
|
||||
this.backend = new OPFSBackend();
|
||||
}
|
||||
else if (this.config.storageBackend === 'indexeddb') {
|
||||
this.backend = new IndexedDBBackend();
|
||||
}
|
||||
else {
|
||||
@@ -5815,5 +5901,5 @@ if (typeof window !== 'undefined') {
|
||||
// 别名
|
||||
const MeSqlark = MetonaSqlark;
|
||||
|
||||
export { AriaEngine, HybridEngine, IndexedDBEngine, MeSqlark, MemoryEngine, MetonaSqlark, OPFSEngine, Table, VERSION, api, create, api as default, parse, tokenize };
|
||||
export { AriaEngine, HybridEngine, IndexedDBEngine, MeSqlark, MemoryEngine, MetonaSqlark, OPFSBackend, OPFSEngine, Table, VERSION, api, create, api as default, parse, tokenize };
|
||||
//# sourceMappingURL=metona-sqlark.esm.js.map
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+88
-1
@@ -2782,6 +2782,89 @@
|
||||
}
|
||||
}
|
||||
|
||||
class OPFSBackend {
|
||||
constructor() {
|
||||
this.root = null;
|
||||
this.dbDir = null;
|
||||
this.dbName = '';
|
||||
}
|
||||
async open(name) {
|
||||
this.dbName = name;
|
||||
this.root = await navigator.storage.getDirectory();
|
||||
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
|
||||
}
|
||||
async close() {
|
||||
this.dbDir = null;
|
||||
this.root = null;
|
||||
}
|
||||
isOpen() {
|
||||
return this.dbDir !== null;
|
||||
}
|
||||
async read(key) {
|
||||
if (!this.dbDir)
|
||||
return null;
|
||||
try {
|
||||
const fh = await this.dbDir.getFileHandle(key);
|
||||
const file = await fh.getFile();
|
||||
return await file.arrayBuffer();
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async write(key, data) {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
const fh = await this.dbDir.getFileHandle(key, { create: true });
|
||||
const writable = await fh.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
async delete(key) {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
try {
|
||||
await this.dbDir.removeEntry(key);
|
||||
}
|
||||
catch {
|
||||
// 文件不存在则忽略
|
||||
}
|
||||
}
|
||||
async listKeys() {
|
||||
if (!this.dbDir)
|
||||
return [];
|
||||
const keys = [];
|
||||
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
|
||||
const dir = this.dbDir;
|
||||
for await (const [name] of dir.entries()) {
|
||||
keys.push(name);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
async exists(key) {
|
||||
if (!this.dbDir)
|
||||
return false;
|
||||
try {
|
||||
await this.dbDir.getFileHandle(key);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async clear() {
|
||||
if (!this.dbDir)
|
||||
return;
|
||||
const dir = this.dbDir;
|
||||
for await (const [name] of dir.entries()) {
|
||||
try {
|
||||
await this.dbDir.removeEntry(name);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
@@ -2819,7 +2902,10 @@
|
||||
return;
|
||||
this.dbName = dbName;
|
||||
// 1. 存储后端
|
||||
if (this.config.storageBackend === 'indexeddb') {
|
||||
if (this.config.storageBackend === 'opfs') {
|
||||
this.backend = new OPFSBackend();
|
||||
}
|
||||
else if (this.config.storageBackend === 'indexeddb') {
|
||||
this.backend = new IndexedDBBackend();
|
||||
}
|
||||
else {
|
||||
@@ -5827,6 +5913,7 @@
|
||||
exports.MeSqlark = MeSqlark;
|
||||
exports.MemoryEngine = MemoryEngine;
|
||||
exports.MetonaSqlark = MetonaSqlark;
|
||||
exports.OPFSBackend = OPFSBackend;
|
||||
exports.OPFSEngine = OPFSEngine;
|
||||
exports.Table = Table;
|
||||
exports.VERSION = VERSION;
|
||||
|
||||
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
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@metona-team/metona-sqlark",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"description": "Frontend SQL database with in-memory and disk dual-mode storage",
|
||||
"type": "module",
|
||||
"main": "dist/metona-sqlark.js",
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🧪 在线演示 — MetonaSqlark v0.2.1</title>
|
||||
<title>🧪 在线演示 — MetonaSqlark v0.2.2</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
|
||||
<style>
|
||||
:root {
|
||||
@@ -83,13 +83,13 @@
|
||||
<a href="docs.html">文档</a>
|
||||
<a href="demo.html" class="nav-active">演示</a>
|
||||
</nav>
|
||||
<div class="status"><span class="dot"></span> Memory 模式 — v0.2.1</div>
|
||||
<div class="status"><span class="dot"></span> Memory 模式 — v0.2.2</div>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
<div class="editor-panel">
|
||||
<div class="editor-area">
|
||||
<textarea id="sql-input" placeholder="输入 SQL 语句... SELECT * FROM users; INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28); SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.2.1 在线演示
|
||||
<textarea id="sql-input" placeholder="输入 SQL 语句... SELECT * FROM users; INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28); SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.2.2 在线演示
|
||||
-- 已预置 users / orders / products 表数据
|
||||
-- 新特性: AriaEngine · LSM-Tree · WAL · MVCC
|
||||
|
||||
@@ -469,7 +469,7 @@ LIMIT 5 OFFSET 0;
|
||||
-- NOT LIKE 模糊排除
|
||||
SELECT * FROM users
|
||||
WHERE name NOT LIKE 'A%' AND age > 20;`,
|
||||
aria: `-- 🌲 AriaEngine 演示 (v0.2.1)
|
||||
aria: `-- 🌲 AriaEngine 演示 (v0.2.2)
|
||||
-- AriaEngine: LSM-Tree 自研存储引擎
|
||||
-- 支持 WAL 崩溃恢复 + MVCC 快照隔离
|
||||
|
||||
@@ -522,7 +522,7 @@ document.addEventListener('keydown', e => {
|
||||
|
||||
// Boot
|
||||
initDB().then(() => {
|
||||
console.log('✅ MetonaSqlark v0.2.1 demo ready');
|
||||
console.log('✅ MetonaSqlark v0.2.2 demo ready');
|
||||
setTimeout(runQuery, 300);
|
||||
}).catch(err => {
|
||||
renderError('初始化失败: ' + err.message);
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>📖 API 文档 — MetonaSqlark v0.2.1</title>
|
||||
<title>📖 API 文档 — MetonaSqlark v0.2.2</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
|
||||
<style>
|
||||
:root {
|
||||
@@ -629,7 +629,7 @@ db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s"
|
||||
|
||||
<h2 id="aria-engine">🌲 AriaEngine 自研存储引擎</h2>
|
||||
<p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br>
|
||||
<strong>v0.2.1 生产加固</strong> — WAL CRC 完整性校验、约束激活、RESTRICT 外键修正、Hybrid 提交顺序修复。</p>
|
||||
<strong>v0.2.2 OPFS 自研后端</strong> — 新增 OPFSBackend,纯浏览器文件系统,零 IndexedDB 依赖,完全自研存储。</p>
|
||||
|
||||
<h3>核心特性</h3>
|
||||
<table>
|
||||
|
||||
+1
-1
@@ -152,7 +152,7 @@
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.2.1 生产加固 — WAL CRC校验 · 约束激活 · RESTRICT外键 · Hybrid提交修复 · 浏览器兼容声明</div>
|
||||
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.2.2 发布 — OPFS 自研存储后端 · 零依赖纯文件系统 · AriaEngine 完全自研</div>
|
||||
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
|
||||
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL + MVCC。</p>
|
||||
<div class="actions">
|
||||
|
||||
@@ -25,6 +25,7 @@ import { WAL } from './wal/log';
|
||||
import { WALRecordType, type WALRecord } from './types';
|
||||
import { CheckpointManager } from './wal/checkpoint';
|
||||
import { IndexedDBBackend, MemoryBackend, type IStorageBackend } from './store/backend';
|
||||
import { OPFSBackend } from './store/opfs_backend';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AriaEngine
|
||||
@@ -63,7 +64,9 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.dbName = dbName;
|
||||
|
||||
// 1. 存储后端
|
||||
if (this.config.storageBackend === 'indexeddb') {
|
||||
if (this.config.storageBackend === 'opfs') {
|
||||
this.backend = new OPFSBackend();
|
||||
} else if (this.config.storageBackend === 'indexeddb') {
|
||||
this.backend = new IndexedDBBackend();
|
||||
} else {
|
||||
this.backend = new MemoryBackend();
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* AriaEngine OPFS Backend — 基于 Origin Private File System 的自研存储后端
|
||||
* @module engine/aria/store/opfs_backend
|
||||
*
|
||||
* 零外部依赖,纯浏览器文件系统 API。
|
||||
* 每个 key 对应 OPFS 目录下的一个二进制文件。
|
||||
*
|
||||
* 浏览器要求:Chrome 102+ / Edge 102+
|
||||
*/
|
||||
import type { IStorageBackend } from './backend';
|
||||
|
||||
export class OPFSBackend implements IStorageBackend {
|
||||
private root: FileSystemDirectoryHandle | null = null;
|
||||
private dbDir: FileSystemDirectoryHandle | null = null;
|
||||
private dbName = '';
|
||||
|
||||
async open(name: string): Promise<void> {
|
||||
this.dbName = name;
|
||||
this.root = await navigator.storage.getDirectory();
|
||||
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.dbDir = null;
|
||||
this.root = null;
|
||||
}
|
||||
|
||||
isOpen(): boolean {
|
||||
return this.dbDir !== null;
|
||||
}
|
||||
|
||||
async read(key: string): Promise<ArrayBuffer | null> {
|
||||
if (!this.dbDir) return null;
|
||||
try {
|
||||
const fh = await this.dbDir.getFileHandle(key);
|
||||
const file = await fh.getFile();
|
||||
return await file.arrayBuffer();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async write(key: string, data: ArrayBuffer): Promise<void> {
|
||||
if (!this.dbDir) return;
|
||||
const fh = await this.dbDir.getFileHandle(key, { create: true });
|
||||
const writable = await fh.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
if (!this.dbDir) return;
|
||||
try {
|
||||
await this.dbDir.removeEntry(key);
|
||||
} catch {
|
||||
// 文件不存在则忽略
|
||||
}
|
||||
}
|
||||
|
||||
async listKeys(): Promise<string[]> {
|
||||
if (!this.dbDir) return [];
|
||||
const keys: string[] = [];
|
||||
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
|
||||
const dir = this.dbDir as any;
|
||||
for await (const [name] of dir.entries()) {
|
||||
keys.push(name);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
if (!this.dbDir) return false;
|
||||
try {
|
||||
await this.dbDir.getFileHandle(key);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
if (!this.dbDir) return;
|
||||
const dir = this.dbDir as any;
|
||||
for await (const [name] of dir.entries()) {
|
||||
try { await this.dbDir!.removeEntry(name); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -86,5 +86,6 @@ export { Table } from './table/table';
|
||||
export { parse } from './sql/parser';
|
||||
export { tokenize } from './sql/lexer';
|
||||
|
||||
// AriaEngine 类型
|
||||
// AriaEngine 类型 & 后端
|
||||
export type { AriaEngineConfig } from './engine/aria/types';
|
||||
export { OPFSBackend } from './engine/aria/store/opfs_backend';
|
||||
|
||||
Reference in New Issue
Block a user