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. **新增 `src/engine/aria/store/opfs_backend.ts`** — OPFSBackend 实现 IStorageBackend 接口,纯文件系统,零依赖
|
||||||
|
2. **`src/engine/aria/types.ts`** — storageBackend 类型增加 `'opfs'`
|
||||||
1. **Memory 引擎事务回滚修复** — `src/engine/memory.ts` 的事务快照机制已存在但 TransactionManager 未正确调用。修复 `src/transaction/index.ts` 在 rollback 时调用引擎层 rollback。
|
3. **`src/engine/aria/index.ts`** — open() 增加 opfs 分支
|
||||||
|
4. **`src/index.ts`** — 导出 OPFSBackend
|
||||||
2. **WAL CRC 校验验证** — `src/engine/aria/wal/log.ts` decodeAllRecords 跳过 CRC,恢复时验证 CRC 不匹配则丢弃 + 告警。
|
5. **`package.json`** — 版本 0.2.2
|
||||||
|
6. **README.md / CHANGELOG.md / site** — 更新版本号和特性说明
|
||||||
3. **Hybrid 引擎提交顺序** — `src/hybrid/index.ts` commit 先写磁盘再写内存,磁盘失败回滚内存。
|
7. **测试** — OPFS 浏览器专属,用 mock 验证不卡死
|
||||||
|
|
||||||
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 确保资源清理
|
|
||||||
@@ -2,6 +2,20 @@
|
|||||||
|
|
||||||
All notable changes to MetonaSqlark will be documented in this file.
|
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
|
## [0.2.1] - 2026-07-27
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# MetonaSqlark
|
# MetonaSqlark
|
||||||
|
|
||||||
<p align="center">
|
<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/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/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">
|
<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 |
|
| **Bloom Filter** | FNV-1a + Murmur 双哈希,快速否定 key |
|
||||||
| **Slotted Page** | 4KB 固定页面,Slot Directory + Tuple 二进制序列化 |
|
| **Slotted Page** | 4KB 固定页面,Slot Directory + Tuple 二进制序列化 |
|
||||||
| **Compaction** | Leveled Compaction,自动合并回收空间 |
|
| **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 — 自研页面式存储引擎主类
|
* AriaEngine — 自研页面式存储引擎主类
|
||||||
* @module engine/aria/index
|
* @module engine/aria/index
|
||||||
@@ -2817,7 +2900,10 @@ class AriaEngine {
|
|||||||
return;
|
return;
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
// 1. 存储后端
|
// 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();
|
this.backend = new IndexedDBBackend();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -5825,6 +5911,7 @@ exports.IndexedDBEngine = IndexedDBEngine;
|
|||||||
exports.MeSqlark = MeSqlark;
|
exports.MeSqlark = MeSqlark;
|
||||||
exports.MemoryEngine = MemoryEngine;
|
exports.MemoryEngine = MemoryEngine;
|
||||||
exports.MetonaSqlark = MetonaSqlark;
|
exports.MetonaSqlark = MetonaSqlark;
|
||||||
|
exports.OPFSBackend = OPFSBackend;
|
||||||
exports.OPFSEngine = OPFSEngine;
|
exports.OPFSEngine = OPFSEngine;
|
||||||
exports.Table = Table;
|
exports.Table = Table;
|
||||||
exports.VERSION = VERSION;
|
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 列表 */
|
/** 将 SQL 字符串解析为 Token 列表 */
|
||||||
declare function tokenize(sql: string): 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 — 入口文件
|
* metona-sqlark — 入口文件
|
||||||
* @module metona-sqlark
|
* @module metona-sqlark
|
||||||
@@ -896,4 +949,4 @@ declare global {
|
|||||||
|
|
||||||
declare const MeSqlark: typeof MetonaSqlark;
|
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 — 自研页面式存储引擎主类
|
* AriaEngine — 自研页面式存储引擎主类
|
||||||
* @module engine/aria/index
|
* @module engine/aria/index
|
||||||
@@ -2813,7 +2896,10 @@ class AriaEngine {
|
|||||||
return;
|
return;
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
// 1. 存储后端
|
// 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();
|
this.backend = new IndexedDBBackend();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -5815,5 +5901,5 @@ if (typeof window !== 'undefined') {
|
|||||||
// 别名
|
// 别名
|
||||||
const MeSqlark = MetonaSqlark;
|
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
|
//# 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 — 自研页面式存储引擎主类
|
* AriaEngine — 自研页面式存储引擎主类
|
||||||
* @module engine/aria/index
|
* @module engine/aria/index
|
||||||
@@ -2819,7 +2902,10 @@
|
|||||||
return;
|
return;
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
// 1. 存储后端
|
// 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();
|
this.backend = new IndexedDBBackend();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -5827,6 +5913,7 @@
|
|||||||
exports.MeSqlark = MeSqlark;
|
exports.MeSqlark = MeSqlark;
|
||||||
exports.MemoryEngine = MemoryEngine;
|
exports.MemoryEngine = MemoryEngine;
|
||||||
exports.MetonaSqlark = MetonaSqlark;
|
exports.MetonaSqlark = MetonaSqlark;
|
||||||
|
exports.OPFSBackend = OPFSBackend;
|
||||||
exports.OPFSEngine = OPFSEngine;
|
exports.OPFSEngine = OPFSEngine;
|
||||||
exports.Table = Table;
|
exports.Table = Table;
|
||||||
exports.VERSION = VERSION;
|
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",
|
"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",
|
"description": "Frontend SQL database with in-memory and disk dual-mode storage",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/metona-sqlark.js",
|
"main": "dist/metona-sqlark.js",
|
||||||
|
|||||||
+5
-5
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>">
|
<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>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@@ -83,13 +83,13 @@
|
|||||||
<a href="docs.html">文档</a>
|
<a href="docs.html">文档</a>
|
||||||
<a href="demo.html" class="nav-active">演示</a>
|
<a href="demo.html" class="nav-active">演示</a>
|
||||||
</nav>
|
</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>
|
</header>
|
||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="editor-panel">
|
<div class="editor-panel">
|
||||||
<div class="editor-area">
|
<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 表数据
|
-- 已预置 users / orders / products 表数据
|
||||||
-- 新特性: AriaEngine · LSM-Tree · WAL · MVCC
|
-- 新特性: AriaEngine · LSM-Tree · WAL · MVCC
|
||||||
|
|
||||||
@@ -469,7 +469,7 @@ LIMIT 5 OFFSET 0;
|
|||||||
-- NOT LIKE 模糊排除
|
-- NOT LIKE 模糊排除
|
||||||
SELECT * FROM users
|
SELECT * FROM users
|
||||||
WHERE name NOT LIKE 'A%' AND age > 20;`,
|
WHERE name NOT LIKE 'A%' AND age > 20;`,
|
||||||
aria: `-- 🌲 AriaEngine 演示 (v0.2.1)
|
aria: `-- 🌲 AriaEngine 演示 (v0.2.2)
|
||||||
-- AriaEngine: LSM-Tree 自研存储引擎
|
-- AriaEngine: LSM-Tree 自研存储引擎
|
||||||
-- 支持 WAL 崩溃恢复 + MVCC 快照隔离
|
-- 支持 WAL 崩溃恢复 + MVCC 快照隔离
|
||||||
|
|
||||||
@@ -522,7 +522,7 @@ document.addEventListener('keydown', e => {
|
|||||||
|
|
||||||
// Boot
|
// Boot
|
||||||
initDB().then(() => {
|
initDB().then(() => {
|
||||||
console.log('✅ MetonaSqlark v0.2.1 demo ready');
|
console.log('✅ MetonaSqlark v0.2.2 demo ready');
|
||||||
setTimeout(runQuery, 300);
|
setTimeout(runQuery, 300);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
renderError('初始化失败: ' + err.message);
|
renderError('初始化失败: ' + err.message);
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>">
|
<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>
|
<style>
|
||||||
:root {
|
: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>
|
<h2 id="aria-engine">🌲 AriaEngine 自研存储引擎</h2>
|
||||||
<p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br>
|
<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>
|
<h3>核心特性</h3>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
+1
-1
@@ -152,7 +152,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.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>
|
<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">
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { WAL } from './wal/log';
|
|||||||
import { WALRecordType, type WALRecord } from './types';
|
import { WALRecordType, type WALRecord } from './types';
|
||||||
import { CheckpointManager } from './wal/checkpoint';
|
import { CheckpointManager } from './wal/checkpoint';
|
||||||
import { IndexedDBBackend, MemoryBackend, type IStorageBackend } from './store/backend';
|
import { IndexedDBBackend, MemoryBackend, type IStorageBackend } from './store/backend';
|
||||||
|
import { OPFSBackend } from './store/opfs_backend';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// AriaEngine
|
// AriaEngine
|
||||||
@@ -63,7 +64,9 @@ export class AriaEngine implements IStorageEngine {
|
|||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
|
|
||||||
// 1. 存储后端
|
// 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();
|
this.backend = new IndexedDBBackend();
|
||||||
} else {
|
} else {
|
||||||
this.backend = new MemoryBackend();
|
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 { parse } from './sql/parser';
|
||||||
export { tokenize } from './sql/lexer';
|
export { tokenize } from './sql/lexer';
|
||||||
|
|
||||||
// AriaEngine 类型
|
// AriaEngine 类型 & 后端
|
||||||
export type { AriaEngineConfig } from './engine/aria/types';
|
export type { AriaEngineConfig } from './engine/aria/types';
|
||||||
|
export { OPFSBackend } from './engine/aria/store/opfs_backend';
|
||||||
|
|||||||
Reference in New Issue
Block a user