release: v0.5.1 — 存储后端生产级硬化(CRC-32/全库加密/WAL分片/页面化存储/多标签页锁/e2e)+ 深度审查修复(假实现接线/死代码清理)
CI / test (18.x) (push) Successful in 10m10s
CI / test (20.x) (push) Successful in 10m10s
CI / test (22.x) (push) Successful in 10m6s
CI / e2e (push) Successful in 9m51s
CI / test (24.x) (push) Successful in 10m28s

This commit is contained in:
thzxx
2026-08-10 12:07:00 +08:00
parent cff98b0903
commit 334067d89e
88 changed files with 15713 additions and 10626 deletions
-6
View File
@@ -106,12 +106,6 @@ class ConnectionManager {
return Array.from(this.connections.keys());
}
/**
* 获取连接的引用计数
*/
getRefCount(dbName: string): number {
return this.refCount.get(dbName) ?? 0;
}
}
// ---------------------------------------------------------------------------
+217 -217
View File
@@ -1,217 +1,217 @@
/**
* metona-sqlark Constants — 类型定义 / 默认配置 / 枚举
* @module constants
*/
// ---------------------------------------------------------------------------
// 存储模式
// ---------------------------------------------------------------------------
/** 存储模式 */
export type StorageMode = 'memory' | 'disk' | 'hybrid' | 'aria';
/** 磁盘引擎类型 */
export type DiskEngine = 'indexeddb' | 'opfs';
/** 所有存储模式 */
export const STORAGE_MODES: StorageMode[] = ['memory', 'disk', 'hybrid', 'aria'];
/** 所有磁盘引擎 */
export const DISK_ENGINES: DiskEngine[] = ['indexeddb', 'opfs'];
// ---------------------------------------------------------------------------
// 字段类型
// ---------------------------------------------------------------------------
/** 字段数据类型 */
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json';
/** 所有字段类型 */
export const FIELD_TYPES: FieldType[] = ['string', 'number', 'boolean', 'date', 'json'];
// ---------------------------------------------------------------------------
// 列定义 & 表结构
// ---------------------------------------------------------------------------
/** 列定义 */
export interface ColumnDef {
/** 字段类型 */
type: FieldType;
/** 是否主键 */
primaryKey?: boolean;
/** 是否必填 */
required?: boolean;
/** 是否唯一 */
unique?: boolean;
/** 默认值 */
default?: unknown;
/** 是否创建索引 */
index?: boolean;
/** 外键引用: 'table.column' */
references?: string;
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 字符串最大长度 */
maxLength?: number;
/** 数字最小值 */
min?: number;
/** 数字最大值 */
max?: number;
}
/** 表结构定义 */
export interface TableSchema {
/** 表名 */
name: string;
/** 列定义映射 */
columns: Record<string, ColumnDef>;
}
// ---------------------------------------------------------------------------
// 数据库配置
// ---------------------------------------------------------------------------
/** 数据库配置 */
export interface DatabaseConfig {
/** 数据库名称 */
name: string;
/** 存储模式 */
mode?: StorageMode;
/** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */
diskEngine?: DiskEngine;
/** 版本号 */
version?: number;
/** 插件列表 */
plugins?: MetonaPlugin[];
/** 数据库就绪回调 */
onReady?: (db: unknown) => void;
/** 错误回调 */
onError?: (error: Error) => void;
/** 查询结果行数上限(默认 0,0 表示不限制) */
maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */
debug?: boolean;
/** 多标签页同步(v0.3.2):BroadcastChannel 广播表变更,其他标签页自动刷新 */
multiTabSync?: boolean;
}
/** 数据库默认配置 */
export const DB_DEFAULTS: Readonly<Required<Omit<DatabaseConfig, 'plugins' | 'onReady' | 'onError'>>> = Object.freeze({
name: 'metona-sqlark',
mode: 'hybrid' as const,
diskEngine: 'indexeddb' as const,
version: 1,
maxRowsPerQuery: 0, // 0 = 不限制
debug: false,
multiTabSync: false,
});
// ---------------------------------------------------------------------------
// Where 操作符
// ---------------------------------------------------------------------------
/** Where 条件操作符 */
export type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
/** 简单条件值:直接相等 */
export type SimpleCondition = unknown;
/** 操作符条件 */
export type OperatorCondition = Partial<Record<WhereOperator, unknown>>;
/** 字段条件:简单值 | 操作符对象 */
export type FieldCondition = SimpleCondition | OperatorCondition;
/** Where 条件对象 */
export type WhereCondition = Record<string, FieldCondition>;
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
/** 排序方向 */
export type SortDirection = 'asc' | 'desc';
/** 排序定义 */
export interface OrderBy {
/** 列名 */
column: string;
/** 排序方向 */
direction: SortDirection;
/** v0.4.0: NULL 值排序位置(first 排最前 / last 排最后,默认同引擎行为) */
nulls?: 'first' | 'last';
}
// ---------------------------------------------------------------------------
// 查询计划(引擎层使用)
// ---------------------------------------------------------------------------
/** 查询计划 — 由 Executor 编译 AST 后生成 */
export interface QueryPlan {
/** 表名 */
table: string;
/** 要返回的列(undefined = 全部,['*'] = 全部) */
columns?: string[];
/** 过滤条件 */
where?: WhereCondition;
/** 排序 */
orderBy?: OrderBy[];
/** 限制条数 */
limit?: number;
/** 偏移量 */
offset?: number;
}
// ---------------------------------------------------------------------------
// 插件接口
// ---------------------------------------------------------------------------
/** 钩子名称 */
export type HookName =
| 'beforeCreateTable' | 'afterCreateTable'
| 'beforeDropTable' | 'afterDropTable'
| 'beforeInsert' | 'afterInsert'
| 'beforeUpdate' | 'afterUpdate'
| 'beforeDelete' | 'afterDelete'
| 'beforeQuery' | 'afterQuery'
| 'beforeTransaction' | 'afterTransaction';
/** 插件定义 */
export interface MetonaPlugin {
/** 插件名称 */
name: string;
/** 插件版本 */
version: string;
/** 描述 */
description?: string;
/** 优先级,越大越先执行 */
priority?: number;
/** 安装 */
install(db: unknown): void;
/** 销毁 */
destroy(): void;
}
// ---------------------------------------------------------------------------
// 错误类型
// ---------------------------------------------------------------------------
/** 数据库错误 */
export class DatabaseError extends Error {
constructor(
message: string,
public code: string,
public details?: unknown,
) {
super(message);
this.name = 'DatabaseError';
}
}
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.4.4';
/**
* metona-sqlark Constants — 类型定义 / 默认配置 / 枚举
* @module constants
*/
// ---------------------------------------------------------------------------
// 存储模式
// ---------------------------------------------------------------------------
/** 存储模式 */
export type StorageMode = 'memory' | 'disk' | 'hybrid' | 'aria';
/** 磁盘引擎类型 */
export type DiskEngine = 'indexeddb' | 'opfs';
// ---------------------------------------------------------------------------
// 字段类型
// ---------------------------------------------------------------------------
/** 字段数据类型 */
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json';
/** 所有字段类型 */
export const FIELD_TYPES: FieldType[] = ['string', 'number', 'boolean', 'date', 'json'];
// ---------------------------------------------------------------------------
// 列定义 & 表结构
// ---------------------------------------------------------------------------
/** 列定义 */
export interface ColumnDef {
/** 字段类型 */
type: FieldType;
/** 是否主键 */
primaryKey?: boolean;
/** 是否必填 */
required?: boolean;
/** 是否唯一 */
unique?: boolean;
/** 默认值 */
default?: unknown;
/** 是否创建索引 */
index?: boolean;
/** 外键引用: 'table.column' */
references?: string;
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 字符串最大长度 */
maxLength?: number;
/** 数字最小值 */
min?: number;
/** 数字最大值 */
max?: number;
}
/** 表结构定义 */
export interface TableSchema {
/** 表名 */
name: string;
/** 列定义映射 */
columns: Record<string, ColumnDef>;
}
// ---------------------------------------------------------------------------
// 数据库配置
// ---------------------------------------------------------------------------
/** 数据库配置 */
export interface DatabaseConfig {
/** 数据库名称 */
name: string;
/** 存储模式 */
mode?: StorageMode;
/** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */
diskEngine?: DiskEngine;
/** 版本号 */
version?: number;
/** 插件列表 */
plugins?: MetonaPlugin[];
/** 数据库就绪回调 */
onReady?: (db: unknown) => void;
/** 错误回调 */
onError?: (error: Error) => void;
/** 查询结果行数上限(默认 0,0 表示不限制) */
maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */
debug?: boolean;
/** 多标签页同步(v0.3.2):BroadcastChannel 广播表变更,其他标签页自动刷新 */
multiTabSync?: boolean;
/**
* v0.4.5: AriaEngine 专属配置(mode='aria' 时透传):
* walSyncMode / checkpointInterval / encryption / pageStorage / compression 等
*/
aria?: import('./engine/aria/types').AriaEngineConfig;
}
/** 数据库默认配置 */
export const DB_DEFAULTS: Readonly<Required<Omit<DatabaseConfig, 'plugins' | 'onReady' | 'onError' | 'aria'>>> & { aria: undefined } = Object.freeze({
name: 'metona-sqlark',
mode: 'hybrid' as const,
diskEngine: 'indexeddb' as const,
version: 1,
maxRowsPerQuery: 0, // 0 = 不限制
debug: false,
multiTabSync: false,
aria: undefined,
});
// ---------------------------------------------------------------------------
// Where 操作符
// ---------------------------------------------------------------------------
/** Where 条件操作符 */
export type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
/** 简单条件值:直接相等 */
export type SimpleCondition = unknown;
/** 操作符条件 */
export type OperatorCondition = Partial<Record<WhereOperator, unknown>>;
/** 字段条件:简单值 | 操作符对象 */
export type FieldCondition = SimpleCondition | OperatorCondition;
/** Where 条件对象 */
export type WhereCondition = Record<string, FieldCondition>;
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
/** 排序方向 */
export type SortDirection = 'asc' | 'desc';
/** 排序定义 */
export interface OrderBy {
/** 列名 */
column: string;
/** 排序方向 */
direction: SortDirection;
/** v0.4.0: NULL 值排序位置(first 排最前 / last 排最后,默认同引擎行为) */
nulls?: 'first' | 'last';
}
// ---------------------------------------------------------------------------
// 查询计划(引擎层使用)
// ---------------------------------------------------------------------------
/** 查询计划 — 由 Executor 编译 AST 后生成 */
export interface QueryPlan {
/** 表名 */
table: string;
/** 要返回的列(undefined = 全部,['*'] = 全部) */
columns?: string[];
/** 过滤条件 */
where?: WhereCondition;
/** 排序 */
orderBy?: OrderBy[];
/** 限制条数 */
limit?: number;
/** 偏移量 */
offset?: number;
}
// ---------------------------------------------------------------------------
// 插件接口
// ---------------------------------------------------------------------------
/** 钩子名称 */
export type HookName =
| 'beforeCreateTable' | 'afterCreateTable'
| 'beforeDropTable' | 'afterDropTable'
| 'beforeInsert' | 'afterInsert'
| 'beforeUpdate' | 'afterUpdate'
| 'beforeDelete' | 'afterDelete'
| 'beforeQuery' | 'afterQuery'
| 'beforeTransaction' | 'afterTransaction';
/** 插件定义 */
export interface MetonaPlugin {
/** 插件名称 */
name: string;
/** 插件版本 */
version: string;
/** 描述 */
description?: string;
/** 优先级,越大越先执行 */
priority?: number;
/** 安装 */
install(db: unknown): void;
/** 销毁 */
destroy(): void;
}
// ---------------------------------------------------------------------------
// 错误类型
// ---------------------------------------------------------------------------
/** 数据库错误 */
export class DatabaseError extends Error {
constructor(
message: string,
public code: string,
public details?: unknown,
) {
super(message);
this.name = 'DatabaseError';
}
}
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.5.1';
+65 -2
View File
@@ -152,7 +152,14 @@ export class MetonaSqlark {
let t = this.tableCache.get(name);
if (!t) {
// v0.3.2: 表操作写入后广播变更(多标签页同步)
t = new Table(this.engine, name, this.executor, (tableName) => this.broadcastChange(tableName));
// v0.5.1: CRUD 生命周期钩子真实接线(beforeInsert/afterInsert/...
t = new Table(
this.engine,
name,
this.executor,
(tableName) => this.broadcastChange(tableName),
(hook, args) => this.pluginManager.trigger(hook, ...args),
);
this.tableCache.set(name, t);
}
return t;
@@ -192,7 +199,10 @@ export class MetonaSqlark {
// v0.3.0: 支持分号分隔的多语句,逐条顺序执行,返回最后一条的结果
const statements: Statement[] = parseAll(sql);
for (const stmt of statements) {
// v0.5.1: SQL 写语句触发 CRUD 生命周期钩子(与 Table API 路径一致)
await this.triggerStatementHooks(stmt, 'before');
result = await this.executor.execute(stmt);
await this.triggerStatementHooks(stmt, 'after', result);
// v0.3.2: 写语句广播表变更(多标签页同步)
const table = this.writeStatementTable(stmt);
if (table) this.broadcastChange(table);
@@ -344,6 +354,18 @@ export class MetonaSqlark {
return result;
}
/**
* v0.5.1: 在线备份 — 导出全库一致性快照。
* Aria 引擎走引擎级 backup()(MVCC 一致性视图);其余引擎回退 exportAll()。
*/
async backup(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
if (typeof this.engine.backup === 'function') {
return this.engine.backup();
}
return this.exportAll();
}
// ---- 发布订阅 ----
private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
@@ -393,6 +415,43 @@ export class MetonaSqlark {
}
}
/**
* v0.5.1: SQL 写语句触发 CRUD 生命周期钩子。
* INSERT/UPDATE/DELETE 分别触发 beforeInsert/afterInsert、beforeUpdate/afterUpdate、
* beforeDelete/afterDelete(参数与 Table API 路径一致)。
*/
private async triggerStatementHooks(stmt: Statement, phase: 'before' | 'after', result?: unknown): Promise<void> {
switch (stmt.type) {
case 'INSERT': {
const rows: Record<string, unknown>[] = (stmt.values ?? []).map((vals: unknown[]) => {
const row: Record<string, unknown> = {};
const cols = stmt.columns ?? [];
for (let i = 0; i < vals.length; i++) {
row[cols[i] ?? String(i)] = vals[i];
}
return row;
});
if (phase === 'before') await this.pluginManager.trigger('beforeInsert', rows);
else await this.pluginManager.trigger('afterInsert', rows, result as string[]);
break;
}
case 'UPDATE': {
const query = { table: stmt.table, where: stmt.where };
if (phase === 'before') await this.pluginManager.trigger('beforeUpdate', query, stmt.sets);
else await this.pluginManager.trigger('afterUpdate', query, stmt.sets, result as number);
break;
}
case 'DELETE': {
const query = { table: stmt.from, where: stmt.where };
if (phase === 'before') await this.pluginManager.trigger('beforeDelete', query);
else await this.pluginManager.trigger('afterDelete', query, result as number);
break;
}
default:
break;
}
}
// ---- 迁移 ----
private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map();
@@ -497,7 +556,11 @@ export class MetonaSqlark {
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'aria':
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
// v0.4.5: 透传 AriaEngine 专属配置(walSyncMode/checkpointInterval/encryption/pageStorage 等)
return new AriaEngine({
storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb',
...(this.config.aria ?? {}),
});
case 'hybrid':
return new HybridEngine(diskEngine);
default:
+177 -206
View File
@@ -1,206 +1,177 @@
/**
* AriaEngine Buffer Pool Eviction — LRU 驱逐策略
* @module engine/aria/buffer/eviction
*/
import type { PageHandle } from '../types';
// ---------------------------------------------------------------------------
// LRU 双向链表
// ---------------------------------------------------------------------------
/**
* LRU 链表管理器 — 双向链表,头部是 most recently used,尾部是 least recently used。
*/
export class LRUList {
private head: PageHandle | null = null;
private tail: PageHandle | null = null;
private _size = 0;
get size(): number {
return this._size;
}
/** 将页面移到链表头部。如果是新页面则插入,已存在则移动。 */
moveToHead(page: PageHandle): void {
// 如果已经在头部,无需操作
if (this.head === page) return;
// 检测是否在链表中
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (inList) {
// 先从当前位置移除
this.detach(page);
} else {
this._size++;
}
// 插入头部
page.prev = null;
page.next = this.head;
if (this.head) {
this.head.prev = page;
}
this.head = page;
if (!this.tail) {
this.tail = page;
}
}
/** 从链表中移除页面 */
remove(page: PageHandle): void {
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (!inList) return;
this.detach(page);
this._size = Math.max(0, this._size - 1);
}
/** 内部:只调整指针,不修改 _size */
private detach(page: PageHandle): void {
if (page.prev) {
page.prev.next = page.next;
} else if (this.head === page) {
this.head = page.next;
}
if (page.next) {
page.next.prev = page.prev;
} else if (this.tail === page) {
this.tail = page.prev;
}
page.prev = null;
page.next = null;
}
/** 获取 LRU 尾部(最久未使用的页面) */
getLRU(): PageHandle | null {
return this.tail;
}
/** 弹出 LRU 尾部 */
popLRU(): PageHandle | null {
const lru = this.tail;
if (lru) {
this.remove(lru);
}
return lru;
}
/** 清空链表 */
clear(): void {
this.head = null;
this.tail = null;
this._size = 0;
}
/** 获取所有页面(用于迭代) */
getAllPages(): PageHandle[] {
const pages: PageHandle[] = [];
let current = this.head;
while (current) {
pages.push(current);
current = current.next;
}
return pages;
}
}
// ---------------------------------------------------------------------------
// Eviction 策略
// ---------------------------------------------------------------------------
export type EvictionCallback = (page: PageHandle) => Promise<void>;
/**
* 驱逐管理器 — 当 Buffer Pool 满时驱逐页面。
*/
export class EvictionManager {
private lru: LRUList;
private onEvict: EvictionCallback;
private capacity: number;
constructor(capacity: number, onEvict: EvictionCallback) {
this.lru = new LRUList();
this.capacity = capacity;
this.onEvict = onEvict;
}
/** 访问页面,更新 LRU */
access(page: PageHandle): void {
page.lastAccess = Date.now();
this.lru.moveToHead(page);
}
/** 添加新页面到池中 */
add(page: PageHandle): void {
this.access(page);
}
/** 移除指定页面 */
remove(page: PageHandle): void {
this.lru.remove(page);
}
/**
* 驱逐页面直到池中有足够空间。
* 只驱逐未 pin 的干净页面(dirty=false)。
* 如果没有干净页面可驱逐,尝试刷脏页。
*/
async evictIfNeeded(count: number): Promise<number> {
let evicted = 0;
while (this.lru.size + count > this.capacity && this.lru.size > 0) {
// 找到可驱逐的页面
const victim = this.findEvictionCandidate();
if (!victim) break;
// 脏页先刷盘
if (victim.dirty) {
await this.onEvict(victim);
victim.dirty = false;
}
this.lru.remove(victim);
evicted++;
}
return evicted;
}
/** 查找驱逐候选(优先干净页面,然后最久未用的脏页) */
private findEvictionCandidate(): PageHandle | null {
// 先从尾部找未 pin 的干净页面
let current = this.lru.getLRU();
while (current) {
if (current.pins === 0 && !current.dirty) return current;
current = current.prev;
}
// 没有干净页,找未 pin 的脏页
current = this.lru.getLRU();
while (current) {
if (current.pins === 0) return current;
current = current.prev;
}
return null;
}
/** 获取当前大小 */
getSize(): number {
return this.lru.size;
}
/** 获取容量 */
getCapacity(): number {
return this.capacity;
}
/** 清空 */
clear(): void {
this.lru.clear();
}
}
/**
* AriaEngine Buffer Pool Eviction — LRU 驱逐策略
* @module engine/aria/buffer/eviction
*/
import type { PageHandle } from '../types';
// ---------------------------------------------------------------------------
// LRU 双向链表
// ---------------------------------------------------------------------------
/**
* LRU 链表管理器 — 双向链表,头部是 most recently used,尾部是 least recently used。
*/
export class LRUList {
private head: PageHandle | null = null;
private tail: PageHandle | null = null;
private _size = 0;
get size(): number {
return this._size;
}
/** 将页面移到链表头部。如果是新页面则插入,已存在则移动。 */
moveToHead(page: PageHandle): void {
// 如果已经在头部,无需操作
if (this.head === page) return;
// 检测是否在链表中
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (inList) {
// 先从当前位置移除
this.detach(page);
} else {
this._size++;
}
// 插入头部
page.prev = null;
page.next = this.head;
if (this.head) {
this.head.prev = page;
}
this.head = page;
if (!this.tail) {
this.tail = page;
}
}
/** 从链表中移除页面 */
remove(page: PageHandle): void {
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (!inList) return;
this.detach(page);
this._size = Math.max(0, this._size - 1);
}
/** 内部:只调整指针,不修改 _size */
private detach(page: PageHandle): void {
if (page.prev) {
page.prev.next = page.next;
} else if (this.head === page) {
this.head = page.next;
}
if (page.next) {
page.next.prev = page.prev;
} else if (this.tail === page) {
this.tail = page.prev;
}
page.prev = null;
page.next = null;
}
/** 清空链表 */
clear(): void {
this.head = null;
this.tail = null;
this._size = 0;
}
/** 获取 LRU 尾部(最久未使用的页面) */
getLRU(): PageHandle | null {
return this.tail;
}
}
// ---------------------------------------------------------------------------
// Eviction 策略
// ---------------------------------------------------------------------------
export type EvictionCallback = (page: PageHandle) => Promise<void>;
/**
* 驱逐管理器 — 当 Buffer Pool 满时驱逐页面。
*/
export class EvictionManager {
private lru: LRUList;
private onEvict: EvictionCallback;
private capacity: number;
constructor(capacity: number, onEvict: EvictionCallback) {
this.lru = new LRUList();
this.capacity = capacity;
this.onEvict = onEvict;
}
/** 访问页面,更新 LRU */
access(page: PageHandle): void {
page.lastAccess = Date.now();
this.lru.moveToHead(page);
}
/** 添加新页面到池中 */
add(page: PageHandle): void {
this.access(page);
}
/** 移除指定页面 */
remove(page: PageHandle): void {
this.lru.remove(page);
}
/**
* 驱逐页面直到池中有足够空间。
* 只驱逐未 pin 的干净页面(dirty=false)。
* 如果没有干净页面可驱逐,尝试刷脏页。
*/
async evictIfNeeded(count: number): Promise<number> {
let evicted = 0;
while (this.lru.size + count > this.capacity && this.lru.size > 0) {
// 找到可驱逐的页面
const victim = this.findEvictionCandidate();
if (!victim) break;
// 脏页先刷盘
if (victim.dirty) {
await this.onEvict(victim);
victim.dirty = false;
}
this.lru.remove(victim);
evicted++;
}
return evicted;
}
/** 查找驱逐候选(优先干净页面,然后最久未用的脏页) */
private findEvictionCandidate(): PageHandle | null {
// 先从尾部找未 pin 的干净页面
let current = this.lru.getLRU();
while (current) {
if (current.pins === 0 && !current.dirty) return current;
current = current.prev;
}
// 没有干净页,找未 pin 的脏页
current = this.lru.getLRU();
while (current) {
if (current.pins === 0) return current;
current = current.prev;
}
return null;
}
/** 清空 */
clear(): void {
this.lru.clear();
}
}
+177 -185
View File
@@ -1,185 +1,177 @@
/**
* AriaEngine Buffer Pool — 页面缓存池
* @module engine/aria/buffer/pool
*
* 管理固定数量页面的 LRU 缓存,减少对底层储存的访问。
*/
import type { PageHandle } from '../types';
import { PageType, DEFAULT_BUFFER_POOL_PAGES } from '../types';
import { createPage } from '../page/format';
import { EvictionManager } from './eviction';
// ---------------------------------------------------------------------------
// Page Read / Write 回调
// ---------------------------------------------------------------------------
export interface PageIO {
/** 从存储后端读取页面 */
readPage(pageId: number): Promise<ArrayBuffer | null>;
/** 将页面写入存储后端 */
writePage(pageId: number, data: ArrayBuffer): Promise<void>;
/** 分配新页面 ID */
allocatePageId(): Promise<number>;
/** 释放页面 ID */
freePageId(pageId: number): Promise<void>;
}
// ---------------------------------------------------------------------------
// Buffer Pool
// ---------------------------------------------------------------------------
export class BufferPool {
private pages: Map<number, PageHandle> = new Map();
private eviction: EvictionManager;
private pageIO: PageIO;
private nextPageId = 0;
constructor(pageIO: PageIO, capacity: number = DEFAULT_BUFFER_POOL_PAGES) {
this.pageIO = pageIO;
this.eviction = new EvictionManager(capacity, async (page) => {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
});
}
// -----------------------------------------------------------------------
// 页面获取
// -----------------------------------------------------------------------
/**
* 获取页面(必要时从磁盘读取)。
* 返回 pin 的页面,使用完成后必须调用 unpin()。
*/
async getPage(pageId: number): Promise<PageHandle | null> {
// 已在池中
let page = this.pages.get(pageId);
if (page) {
this.eviction.access(page);
page.pins++;
return page;
}
// 需要从磁盘加载
const buffer = await this.pageIO.readPage(pageId);
if (!buffer) return null;
// 确保有空间
await this.eviction.evictIfNeeded(1);
const type = new DataView(buffer).getUint8(4) as PageType;
page = {
pageId,
type,
data: buffer,
dirty: false,
pins: 1,
prev: null,
next: null,
lastAccess: Date.now(),
};
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* 创建新页面。
*/
async newPage(type: PageType = PageType.DATA): Promise<PageHandle> {
const pageId = await this.pageIO.allocatePageId();
await this.eviction.evictIfNeeded(1);
const page = createPage(pageId, type);
page.pins = 1;
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* 释放页面的 pin。
*/
unpin(page: PageHandle): void {
if (page.pins > 0) {
page.pins--;
}
}
/**
* 标记页面为脏(需要写回)。
*/
markDirty(page: PageHandle): void {
page.dirty = true;
}
/**
* 将脏页面刷新到磁盘。
*/
async flushPage(pageId: number): Promise<void> {
const page = this.pages.get(pageId);
if (page && page.dirty) {
await this.pageIO.writePage(pageId, page.data);
page.dirty = false;
}
}
/**
* 刷新所有脏页面。
*/
async flushAll(): Promise<void> {
for (const [, page] of this.pages) {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
}
}
/**
* 从缓存中删除指定页面(不刷盘)。
*/
removePage(pageId: number): void {
const page = this.pages.get(pageId);
if (page) {
this.eviction.remove(page);
this.pages.delete(pageId);
}
}
/**
* 清空缓存池(先刷脏页)。
*/
async clear(): Promise<void> {
await this.flushAll();
this.pages.clear();
this.eviction.clear();
}
// -----------------------------------------------------------------------
// 统计
// -----------------------------------------------------------------------
/** 获取当前缓存页面数 */
getCachedPageCount(): number {
return this.pages.size;
}
/** 获取缓存容量 */
getCapacity(): number {
return this.eviction.getCapacity();
}
/** 获取脏页面数 */
getDirtyPageCount(): number {
let count = 0;
for (const [, page] of this.pages) {
if (page.dirty) count++;
}
return count;
}
}
/**
* AriaEngine Buffer Pool — 页面缓存池
* @module engine/aria/buffer/pool
*
* 管理固定数量页面的 LRU 缓存,减少对底层储存的访问。
*/
import type { PageHandle } from '../types';
import { PageType, DEFAULT_BUFFER_POOL_PAGES } from '../types';
import { createPage } from '../page/format';
import { EvictionManager } from './eviction';
// ---------------------------------------------------------------------------
// Page Read / Write 回调
// ---------------------------------------------------------------------------
export interface PageIO {
/** 从存储后端读取页面 */
readPage(pageId: number): Promise<ArrayBuffer | null>;
/** 将页面写入存储后端 */
writePage(pageId: number, data: ArrayBuffer): Promise<void>;
/** 分配新页面 ID */
allocatePageId(): Promise<number>;
/** v0.4.5: 批量分配页面 ID(一次 meta 持久化) */
allocatePageIds?(count: number): Promise<number[]>;
/** 释放页面 ID */
freePageId(pageId: number): Promise<void>;
}
// ---------------------------------------------------------------------------
// Buffer Pool
// ---------------------------------------------------------------------------
export class BufferPool {
private pages: Map<number, PageHandle> = new Map();
private eviction: EvictionManager;
private pageIO: PageIO;
private nextPageId = 0;
constructor(pageIO: PageIO, capacity: number = DEFAULT_BUFFER_POOL_PAGES) {
this.pageIO = pageIO;
this.eviction = new EvictionManager(capacity, async (page) => {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
});
}
// -----------------------------------------------------------------------
// 页面获取
// -----------------------------------------------------------------------
/**
* 获取页面(必要时从磁盘读取)。
* 返回 pin 的页面,使用完成后必须调用 unpin()。
*/
async getPage(pageId: number): Promise<PageHandle | null> {
// 已在池中
let page = this.pages.get(pageId);
if (page) {
this.eviction.access(page);
page.pins++;
return page;
}
// 需要从磁盘加载
const buffer = await this.pageIO.readPage(pageId);
if (!buffer) return null;
// 确保有空间
await this.eviction.evictIfNeeded(1);
const type = new DataView(buffer).getUint8(4) as PageType;
page = {
pageId,
type,
data: buffer,
dirty: false,
pins: 1,
prev: null,
next: null,
lastAccess: Date.now(),
};
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* v0.4.5: 批量创建新页面(一次页面 ID 分配,页面化 SSTable 保存用)。
* 返回的页面均 pin 且 dirty=false(调用方写入后需 markDirty + flushPage)。
*/
async newPages(count: number, type: PageType = PageType.DATA): Promise<PageHandle[]> {
if (count <= 0) return [];
let pageIds: number[];
if (typeof this.pageIO.allocatePageIds === 'function') {
pageIds = await this.pageIO.allocatePageIds!(count);
} else {
pageIds = [];
for (let i = 0; i < count; i++) pageIds.push(await this.pageIO.allocatePageId());
}
await this.eviction.evictIfNeeded(count);
const handles: PageHandle[] = [];
for (const pageId of pageIds) {
const page = createPage(pageId, type);
page.pins = 1;
this.pages.set(pageId, page);
this.eviction.add(page);
handles.push(page);
}
return handles;
}
/**
* 释放页面的 pin。
*/
unpin(page: PageHandle): void {
if (page.pins > 0) {
page.pins--;
}
}
/**
* 标记页面为脏(需要写回)。
*/
markDirty(page: PageHandle): void {
page.dirty = true;
}
/**
* 将脏页面刷新到磁盘。
*/
async flushPage(pageId: number): Promise<void> {
const page = this.pages.get(pageId);
if (page && page.dirty) {
await this.pageIO.writePage(pageId, page.data);
page.dirty = false;
}
}
/**
* 刷新所有脏页面。
*/
async flushAll(): Promise<void> {
for (const [, page] of this.pages) {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
}
}
/**
* 从缓存中删除指定页面(不刷盘)。
*/
removePage(pageId: number): void {
const page = this.pages.get(pageId);
if (page) {
this.eviction.remove(page);
this.pages.delete(pageId);
}
}
/**
* 清空缓存池(先刷脏页)。
*/
async clear(): Promise<void> {
await this.flushAll();
this.pages.clear();
this.eviction.clear();
}
}
+36 -17
View File
@@ -2,10 +2,9 @@
* AriaEngine LZ4 Compression — 简化 LZ4 压缩/解压
* @module engine/aria/compression/lz4
*
* v0.2.6: 修复往返一致性
* - 匹配长度截断到 19 字节(matchField 上限 15 + MIN_MATCH),长匹配分段输出
* - 组合 token 的 matchField ∈ [1,15]matchField=0 且 lo=0 表示末尾纯字面量(无 offset)
* - 消除"matchField=0 的组合 token 与纯字面量 token 歧义"
* v0.4.5 格式 v2:压缩流前增加 4 字节原始大小头(LE u32),
* 解压不再依赖外部估算(高压缩率数据下 buf.length*2 估算不足会截断)。
* 旧版压缩数据(无头)视为损坏(compression 选项自 v0.2.6 起已声明不向后兼容)。
*
* Token 格式(1 字节):
* hi 4bit = litLen (0-15)
@@ -17,10 +16,16 @@
const MIN_MATCH = 4;
const MAX_MATCH = MIN_MATCH + 15; // 19,匹配长度上限
/** 原始大小头字节数 */
const HEADER_SIZE = 4;
export function compressLZ4(input: Uint8Array): Uint8Array {
// 空输入直接返回(无 token 可输出
if (input.byteLength === 0) return input;
// 空输入:仅头部(原始大小 0
if (input.byteLength === 0) {
const empty = new Uint8Array(HEADER_SIZE);
new DataView(empty.buffer).setUint32(0, 0, true);
return empty;
}
// 最坏情况:纯字面量分块输出 len/15 个 token + 末尾 token
// 上限:len + ceil(len/15) + 8(组合 token 的 offset 开销已包含在内)
@@ -73,24 +78,38 @@ export function compressLZ4(input: Uint8Array): Uint8Array {
litStart += chunk;
}
// 始终输出压缩流(即使比原数据略大)。
// 注意:不能返回原样 input —— 解压端无法区分"压缩流"与"原始数据"
// 原样返回会导致解压器将原始字节误解析为 token(v0.2.6 修复)
return out.slice(0, di);
// v0.4.5: 前置原始大小头,解压端自描述
const stream = out.slice(0, di);
const combined = new Uint8Array(HEADER_SIZE + stream.byteLength);
new DataView(combined.buffer).setUint32(0, input.byteLength, true);
combined.set(stream, HEADER_SIZE);
return combined;
}
export function decompressLZ4(input: Uint8Array, originalSize: number): Uint8Array {
export function decompressLZ4(input: Uint8Array, _originalSize?: number): Uint8Array {
if (input.byteLength < HEADER_SIZE) {
throw new Error('LZ4 stream too short: missing header');
}
const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
const originalSize = view.getUint32(0, true);
if (originalSize === 0 && input.byteLength === HEADER_SIZE) {
return new Uint8Array(0); // 空输入
}
if (originalSize <= 0 || originalSize > 0x3fffffff) {
throw new Error('Invalid LZ4 header: bad original size');
}
const stream = input.subarray(HEADER_SIZE);
const out = new Uint8Array(originalSize);
let si = 0, di = 0;
while (si < input.byteLength && di < originalSize) {
const token = input[si++];
while (si < stream.byteLength && di < originalSize) {
const token = stream[si++];
const litLen = (token >> 4) & 0x0F;
const matchField = token & 0x0F;
// 复制字面量
for (let i = 0; i < litLen && si < input.byteLength && di < originalSize; i++) {
out[di++] = input[si++];
for (let i = 0; i < litLen && si < stream.byteLength && di < originalSize; i++) {
out[di++] = stream[si++];
}
// matchField=0:纯字面量 token(无 offset 无匹配)。
@@ -98,8 +117,8 @@ export function decompressLZ4(input: Uint8Array, originalSize: number): Uint8Arr
if (matchField === 0) continue;
// 组合 token:读取 offset + 复制匹配(可能自重叠)
if (si + 1 >= input.byteLength) break;
const offset = input[si++] | (input[si++] << 8);
if (si + 1 >= stream.byteLength) break;
const offset = stream[si++] | (stream[si++] << 8);
const matchLen = matchField + MIN_MATCH;
for (let i = 0; i < matchLen && di < originalSize; i++) {
out[di] = out[di - offset];
+58
View File
@@ -0,0 +1,58 @@
/**
* AriaEngine CRC32 — 标准 CRC-32IEEE 802.3,多项式 0xEDB88320
* @module engine/aria/crc32
*
* 查表法实现。
* 分段计算约定:
* crc32(head + tail) === crc32Finalize(crc32Update(crc32Update(0xFFFFFFFF, head), tail))
* === crc32(tail, crc32(head))
* 用于 SSTable 文件校验和与 WAL 记录完整性校验。
*/
/** CRC-32 查找表(0xEDB88320 反射多项式) */
const CRC32_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[i] = c >>> 0;
}
return table;
})();
/** CRC-32 初始累加器状态 */
export const CRC32_INIT = 0xffffffff;
/**
* 更新内部 CRC 累加状态(供分段计算使用,接收中间状态返回中间状态)。
*/
export function crc32Update(state: number, data: Uint8Array): number {
let crc = state >>> 0;
for (let i = 0; i < data.byteLength; i++) {
crc = (CRC32_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)) >>> 0;
}
return crc >>> 0;
}
/** 将中间状态转换为最终校验和(终止计算) */
export function crc32Finalize(state: number): number {
return (state ^ CRC32_INIT) >>> 0;
}
/**
* 计算标准 CRC-32 校验和。
* @param data 输入字节
* @param seed 前序片段计算的最终校验和(首段传 0 或不传)
* @returns 32 位无符号校验和
*/
export function crc32(data: Uint8Array, seed = 0): number {
const state = crc32Update(seed ^ CRC32_INIT, data);
return crc32Finalize(state);
}
/** 分段计算便捷函数:crc32Continue(prevFinal, chunk) === crc32(chunk, prevFinal) */
export function crc32Continue(prev: number, data: Uint8Array): number {
return crc32(data, prev);
}
+6 -32
View File
@@ -33,18 +33,20 @@ export class CryptoManager {
return actualSalt as Uint8Array;
}
async encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
async encryptPage(data: ArrayBuffer | Uint8Array): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
if (!this.cryptoKey) throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)) as any;
// 传 TypedArray 视图而非裸 ArrayBufferSubtleCrypto 通过 ArrayBuffer.isView 检查,
// 对跨 realm / 跨 vm 环境的 ArrayBuffer 兼容(Node 18/20 的 webcrypto 对裸 ArrayBuffer 检查严格)
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv } as any, this.cryptoKey, new Uint8Array(data));
const plain = (data instanceof Uint8Array ? data : new Uint8Array(data)) as BufferSource;
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv } as any, this.cryptoKey, plain);
return { iv: iv as Uint8Array, data: ciphertext };
}
async decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> {
async decryptPage(iv: Uint8Array, data: ArrayBuffer | Uint8Array): Promise<ArrayBuffer> {
if (!this.cryptoKey) throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv } as any, this.cryptoKey, new Uint8Array(data));
const ciphertext = (data instanceof Uint8Array ? data : new Uint8Array(data)) as BufferSource;
return crypto.subtle.decrypt({ name: ALGO, iv } as any, this.cryptoKey, ciphertext);
}
close(): void {
@@ -53,31 +55,3 @@ export class CryptoManager {
}
}
// ---------------------------------------------------------------------------
// 全局兼容层(旧代码仍可使用全局函数)
// ---------------------------------------------------------------------------
const globalCrypto = new CryptoManager();
/** @deprecated 使用 CryptoManager 实例代替 */
export async function initCrypto(password: string, salt?: Uint8Array): Promise<Uint8Array> {
return globalCrypto.init(password, salt);
}
/** @deprecated 使用 CryptoManager 实例代替 */
export function isCryptoEnabled(): boolean { return globalCrypto.enabled; }
/** @deprecated 使用 CryptoManager 实例代替 */
export async function encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
return globalCrypto.encryptPage(data);
}
/** @deprecated 使用 CryptoManager 实例代替 */
export async function decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> {
return globalCrypto.decryptPage(iv, data);
}
/** @deprecated 使用 CryptoManager 实例代替 */
export function closeCrypto(): void {
globalCrypto.close();
}
+1848 -1827
View File
File diff suppressed because it is too large Load Diff
+113 -123
View File
@@ -1,123 +1,113 @@
/**
* AriaEngine Bloom Filter — 快速判定 key 是否可能存在
* @module engine/aria/index/bloom
*
* 使用双哈希函数 + Kirsch-Mitzenmacher 优化生成 k 个哈希值。
*/
import { DEFAULT_BLOOM_BITS_PER_KEY } from '../types';
// ---------------------------------------------------------------------------
// BloomFilter
// ---------------------------------------------------------------------------
export class BloomFilter {
private bits: Uint8Array;
private numHashes: number;
private _inserted = 0;
/**
* @param numKeys 预期插入的 key 数量
* @param bitsPerKey 每个 key 的位数(默认 10,误报率约 1%)
*/
constructor(numKeys: number, bitsPerKey: number = DEFAULT_BLOOM_BITS_PER_KEY) {
// ceil(numKeys * bitsPerKey / 8),最少 64 位
const numBits = Math.max(64, numKeys * bitsPerKey);
const numBytes = Math.ceil(numBits / 8);
this.bits = new Uint8Array(numBytes);
// k = bitsPerKey * ln(2) ≈ bitsPerKey * 0.69
this.numHashes = Math.max(1, Math.floor(bitsPerKey * 0.69));
}
/** 从现有数据恢复 */
static fromData(data: Uint8Array, numHashes: number): BloomFilter {
const bf = new BloomFilter(1); // dummy
bf.bits = data;
bf.numHashes = numHashes;
return bf;
}
/** 插入 key */
insert(key: string): void {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
this.bits[byteIdx] |= (1 << bitIdx);
}
this._inserted++;
}
/** 检查 key 可能存在(false positive 可能,false negative 不可能) */
mayContain(key: string): boolean {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
if ((this.bits[byteIdx] & (1 << bitIdx)) === 0) {
return false; // 确定不存在
}
}
return true; // 可能存在
}
/** 获取序列化数据 */
serialize(): Uint8Array {
return this.bits;
}
/** bit 数组大小 */
getBitSize(): number {
return this.bits.byteLength * 8;
}
/** 已插入 key 数量 */
getInsertedCount(): number {
return this._inserted;
}
/** hash 函数数量 */
getHashCount(): number {
return this.numHashes;
}
// -----------------------------------------------------------------------
// 哈希
// -----------------------------------------------------------------------
private getHashes(key: string): number[] {
const bits = this.bits.byteLength * 8;
const h1 = this.fnv1a(key);
const h2 = this.murmurSimple(key);
const hashes: number[] = [];
for (let i = 0; i < this.numHashes; i++) {
// Kirsch-Mitzenmacher: h_i = h1 + i * h2
const h = Math.abs((h1 + i * h2) % bits);
hashes.push(h);
}
return hashes;
}
/** FNV-1a 哈希 */
private fnv1a(str: string): number {
let hash = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash = (hash * 0x01000193) >>> 0;
}
return hash;
}
/** 简化的 Murmur-like 哈希 */
private murmurSimple(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
hash = ((hash << 5) - hash + ch) | 0;
hash = (hash ^ (hash >>> 16)) >>> 0;
}
return Math.abs(hash);
}
}
/**
* AriaEngine Bloom Filter — 快速判定 key 是否可能存在
* @module engine/aria/index/bloom
*
* 使用双哈希函数 + Kirsch-Mitzenmacher 优化生成 k 个哈希值。
*/
import { DEFAULT_BLOOM_BITS_PER_KEY } from '../types';
// ---------------------------------------------------------------------------
// BloomFilter
// ---------------------------------------------------------------------------
export class BloomFilter {
private bits: Uint8Array;
private numHashes: number;
private _inserted = 0;
/**
* @param numKeys 预期插入的 key 数量
* @param bitsPerKey 每个 key 的位数(默认 10,误报率约 1%)
*/
constructor(numKeys: number, bitsPerKey: number = DEFAULT_BLOOM_BITS_PER_KEY) {
// ceil(numKeys * bitsPerKey / 8),最少 64 位
const numBits = Math.max(64, numKeys * bitsPerKey);
const numBytes = Math.ceil(numBits / 8);
this.bits = new Uint8Array(numBytes);
// k = bitsPerKey * ln(2) ≈ bitsPerKey * 0.69
this.numHashes = Math.max(1, Math.floor(bitsPerKey * 0.69));
}
/** 从现有数据恢复 */
static fromData(data: Uint8Array, numHashes: number): BloomFilter {
const bf = new BloomFilter(1); // dummy
bf.bits = data;
bf.numHashes = numHashes;
return bf;
}
/** 插入 key */
insert(key: string): void {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
this.bits[byteIdx] |= (1 << bitIdx);
}
this._inserted++;
}
/** 检查 key 可能存在(false positive 可能,false negative 不可能) */
mayContain(key: string): boolean {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
if ((this.bits[byteIdx] & (1 << bitIdx)) === 0) {
return false; // 确定不存在
}
}
return true; // 可能存在
}
/** 获取序列化数据 */
serialize(): Uint8Array {
return this.bits;
}
/** hash 函数数量 */
getHashCount(): number {
return this.numHashes;
}
// -----------------------------------------------------------------------
// 哈希
// -----------------------------------------------------------------------
private getHashes(key: string): number[] {
const bits = this.bits.byteLength * 8;
const h1 = this.fnv1a(key);
const h2 = this.murmurSimple(key);
const hashes: number[] = [];
for (let i = 0; i < this.numHashes; i++) {
// Kirsch-Mitzenmacher: h_i = h1 + i * h2
const h = Math.abs((h1 + i * h2) % bits);
hashes.push(h);
}
return hashes;
}
/** FNV-1a 哈希 */
private fnv1a(str: string): number {
let hash = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash = (hash * 0x01000193) >>> 0;
}
return hash;
}
/** 简化的 Murmur-like 哈希 */
private murmurSimple(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
hash = ((hash << 5) - hash + ch) | 0;
hash = (hash ^ (hash >>> 16)) >>> 0;
}
return Math.abs(hash);
}
}
File diff suppressed because it is too large Load Diff
+462 -467
View File
@@ -1,467 +1,462 @@
/**
* AriaEngine MemTable — 基于红黑树的内存表
* @module engine/aria/index/memtable
*
* 写操作先进入 MemTable,达到阈值后冻结并 flush 成 SSTable。
*/
// ---------------------------------------------------------------------------
// RB-Tree Node
// ---------------------------------------------------------------------------
enum Color { RED, BLACK }
class RBNode<K, V> {
key: K;
value: V;
color: Color = Color.RED;
left: RBNode<K, V> | null = null;
right: RBNode<K, V> | null = null;
parent: RBNode<K, V> | null = null;
constructor(key: K, value: V) {
this.key = key;
this.value = value;
}
}
// ---------------------------------------------------------------------------
// Red-Black Tree
// ---------------------------------------------------------------------------
class RedBlackTree<K, V> {
private root: RBNode<K, V> | null = null;
private _size = 0;
get size(): number { return this._size; }
// ---- 插入 ----
insert(key: K, value: V): void {
const node = new RBNode(key, value);
if (!this.root) {
this.root = node;
node.color = Color.BLACK;
this._size++;
return;
}
let parent: RBNode<K, V> | null = null;
let current: RBNode<K, V> | null = this.root;
while (current) {
parent = current;
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
// 更新已存在的 key
current.value = value;
return;
}
}
node.parent = parent;
if (key < parent!.key) {
parent!.left = node;
} else {
parent!.right = node;
}
this._size++;
this.fixInsert(node);
}
// ---- 查找 ----
find(key: K): V | null {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
return current.value;
}
}
return null;
}
// ---- 删除 ----
delete(key: K): boolean {
// 简化实现:标记删除(实际改为找到并调整树)
const node = this.findNode(key);
if (!node) return false;
this.deleteNode(node);
this._size--;
return true;
}
// ---- 遍历 ----
/** 中序遍历(有序) */
inorder(callback: (key: K, value: V) => void): void {
this._inorder(this.root, callback);
}
/** 范围遍历 */
rangeScan(
startKey: K,
endKey: K,
callback: (key: K, value: V) => void,
): void {
this._rangeScan(this.root, startKey, endKey, callback);
}
/** 获取所有条目 */
getAllEntries(): [K, V][] {
const entries: [K, V][] = [];
this.inorder((k, v) => entries.push([k, v]));
return entries;
}
/** 清空 */
clear(): void {
this.root = null;
this._size = 0;
}
// ---- 内部方法 ----
private findNode(key: K): RBNode<K, V> | null {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
return current;
}
}
return null;
}
private deleteNode(node: RBNode<K, V>): void {
// 简化:用左子树最大或右子树最小替换
// 完整实现较复杂,这里采用简化策略
if (!node.left && !node.right) {
this.transplant(node, null);
if (node.color === Color.BLACK) this.fixDelete(null, node.parent);
} else if (!node.left) {
this.transplant(node, node.right);
if (node.color === Color.BLACK) this.fixDelete(node.right, node.right!.parent);
} else if (!node.right) {
this.transplant(node, node.left);
if (node.color === Color.BLACK) this.fixDelete(node.left, node.left!.parent);
} else {
const successor = this.minimum(node.right);
if (successor!.parent !== node) {
this.transplant(successor!, successor!.right);
successor!.right = node.right;
successor!.right!.parent = successor;
}
this.transplant(node, successor);
successor!.left = node.left;
successor!.left!.parent = successor;
const origColor = successor!.color;
successor!.color = node.color;
if (origColor === Color.BLACK) this.fixDelete(successor!.right, successor!.right?.parent ?? null);
}
}
private transplant(u: RBNode<K, V> | null, v: RBNode<K, V> | null): void {
if (!u!.parent) {
this.root = v;
} else if (u === u!.parent.left) {
u!.parent.left = v;
} else {
u!.parent.right = v;
}
if (v) v.parent = u!.parent;
}
private minimum(node: RBNode<K, V>): RBNode<K, V> {
while (node.left) node = node.left;
return node;
}
private fixInsert(node: RBNode<K, V>): void {
while (node.parent && node.parent.color === Color.RED) {
const parent = node.parent;
const grandparent = parent.parent;
if (!grandparent) break;
if (parent === grandparent.left) {
const uncle = grandparent.right;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
} else {
if (node === parent.right) {
node = parent;
this.rotateLeft(node);
}
if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateRight(node.parent.parent);
}
} else {
const uncle = grandparent.left;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
} else {
if (node === parent.left) {
node = parent;
this.rotateRight(node);
}
if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateLeft(node.parent.parent);
}
}
}
if (this.root) this.root.color = Color.BLACK;
}
private fixDelete(x: RBNode<K, V> | null, parent: RBNode<K, V> | null): void {
// 标准 RB-Tree 删除修复(修复"双黑"问题)
let node = x;
let nodeParent = parent;
while ((!node || node.color === Color.BLACK) && node !== this.root) {
if (!nodeParent) break;
if (node === nodeParent.left) {
let sibling = nodeParent.right;
if (!sibling) break;
// Case 1: 兄弟是红色
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateLeft(nodeParent);
sibling = nodeParent.right;
if (!sibling) break;
}
// Case 2: 兄弟的两个子节点都是黑色
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
} else {
// Case 3: 兄弟右子黑色(左子红色)
if (!sibRight || sibRight.color === Color.BLACK) {
if (sibLeft) sibLeft.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateRight(sibling);
sibling = nodeParent.right;
if (!sibling) break;
}
// Case 4: 兄弟右子红色
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.right) sibling.right.color = Color.BLACK;
this.rotateLeft(nodeParent);
node = this.root;
}
} else {
// 镜像:node 是父节点的右子
let sibling = nodeParent.left;
if (!sibling) break;
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateRight(nodeParent);
sibling = nodeParent.left;
if (!sibling) break;
}
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
} else {
if (!sibLeft || sibLeft.color === Color.BLACK) {
if (sibRight) sibRight.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateLeft(sibling);
sibling = nodeParent.left;
if (!sibling) break;
}
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.left) sibling.left.color = Color.BLACK;
this.rotateRight(nodeParent);
node = this.root;
}
}
}
if (node) node.color = Color.BLACK;
}
private rotateLeft(x: RBNode<K, V>): void {
const y = x.right;
if (!y) return;
x.right = y.left;
if (y.left) y.left.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
y.left = x;
x.parent = y;
}
private rotateRight(x: RBNode<K, V>): void {
const y = x.left;
if (!y) return;
x.left = y.right;
if (y.right) y.right.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.right) {
x.parent.right = y;
} else {
x.parent.left = y;
}
y.right = x;
x.parent = y;
}
private _inorder(node: RBNode<K, V> | null, cb: (k: K, v: V) => void): void {
if (!node) return;
this._inorder(node.left, cb);
cb(node.key, node.value);
this._inorder(node.right, cb);
}
private _rangeScan(
node: RBNode<K, V> | null,
start: K,
end: K,
cb: (k: K, v: V) => void,
): void {
if (!node) return;
if (node.key > start) this._rangeScan(node.left, start, end, cb);
if (node.key >= start && node.key <= end) cb(node.key, node.value);
if (node.key < end) this._rangeScan(node.right, start, end, cb);
}
}
// ---------------------------------------------------------------------------
// MemTable
// ---------------------------------------------------------------------------
export class MemTable {
private tree: RedBlackTree<string, Record<string, unknown>>;
private _estimatedSize = 0;
private maxSize: number;
constructor(maxSize: number = 4 * 1024 * 1024) {
this.tree = new RedBlackTree();
this.maxSize = maxSize;
}
/** 插入或更新 */
put(key: string, value: Record<string, unknown>): void {
const oldSize = this.estimateEntrySize(key, this.tree.find(key));
const newSize = this.estimateEntrySize(key, value);
this.tree.insert(key, value);
this._estimatedSize += newSize - oldSize;
}
/** 获取 */
get(key: string): Record<string, unknown> | null {
return this.tree.find(key);
}
/** 删除 */
delete(key: string): boolean {
const oldVal = this.tree.find(key);
if (oldVal) {
this._estimatedSize -= this.estimateEntrySize(key, oldVal);
}
return this.tree.delete(key);
}
/** 是否应刷盘 */
shouldFlush(): boolean {
return this._estimatedSize >= this.maxSize;
}
/** 获取所有有序条目 */
getAllEntries(): [string, Record<string, unknown>][] {
return this.tree.getAllEntries();
}
/** 范围扫描 */
rangeScan(
startKey: string,
endKey: string,
): [string, Record<string, unknown>][] {
const entries: [string, Record<string, unknown>][] = [];
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
return entries;
}
/** 条目数 */
getEntryCount(): number {
return this.tree.size;
}
/** 估计大小(字节) */
getEstimatedSize(): number {
return this._estimatedSize;
}
/** 清空 */
clear(): void {
this.tree.clear();
this._estimatedSize = 0;
}
/** 检查 key 是否存在 */
contains(key: string): boolean {
return this.tree.find(key) !== null;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private estimateEntrySize(key: string, value: Record<string, unknown> | null): number {
if (!value) return 0;
let size = key.length * 2; // UTF-16
for (const entry of Object.entries(value)) {
size += entry[0].length * 2;
const v = entry[1];
if (typeof v === 'string') size += v.length * 2;
else if (typeof v === 'number') size += 8;
else if (typeof v === 'boolean') size += 1;
else if (v === null || v === undefined) size += 1;
else size += 16; // rough estimate
}
return size;
}
}
/**
* AriaEngine MemTable — 基于红黑树的内存表
* @module engine/aria/index/memtable
*
* 写操作先进入 MemTable,达到阈值后冻结并 flush 成 SSTable。
*/
// ---------------------------------------------------------------------------
// RB-Tree Node
// ---------------------------------------------------------------------------
enum Color { RED, BLACK }
class RBNode<K, V> {
key: K;
value: V;
color: Color = Color.RED;
left: RBNode<K, V> | null = null;
right: RBNode<K, V> | null = null;
parent: RBNode<K, V> | null = null;
constructor(key: K, value: V) {
this.key = key;
this.value = value;
}
}
// ---------------------------------------------------------------------------
// Red-Black Tree
// ---------------------------------------------------------------------------
class RedBlackTree<K, V> {
private root: RBNode<K, V> | null = null;
private _size = 0;
get size(): number { return this._size; }
// ---- 插入 ----
insert(key: K, value: V): void {
const node = new RBNode(key, value);
if (!this.root) {
this.root = node;
node.color = Color.BLACK;
this._size++;
return;
}
let parent: RBNode<K, V> | null = null;
let current: RBNode<K, V> | null = this.root;
while (current) {
parent = current;
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
// 更新已存在的 key
current.value = value;
return;
}
}
node.parent = parent;
if (key < parent!.key) {
parent!.left = node;
} else {
parent!.right = node;
}
this._size++;
this.fixInsert(node);
}
// ---- 查找 ----
find(key: K): V | null {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
return current.value;
}
}
return null;
}
// ---- 删除 ----
delete(key: K): boolean {
// 简化实现:标记删除(实际改为找到并调整树)
const node = this.findNode(key);
if (!node) return false;
this.deleteNode(node);
this._size--;
return true;
}
// ---- 遍历 ----
/** 中序遍历(有序) */
inorder(callback: (key: K, value: V) => void): void {
this._inorder(this.root, callback);
}
/** 范围遍历 */
rangeScan(
startKey: K,
endKey: K,
callback: (key: K, value: V) => void,
): void {
this._rangeScan(this.root, startKey, endKey, callback);
}
/** 获取所有条目 */
getAllEntries(): [K, V][] {
const entries: [K, V][] = [];
this.inorder((k, v) => entries.push([k, v]));
return entries;
}
/** 清空 */
clear(): void {
this.root = null;
this._size = 0;
}
// ---- 内部方法 ----
private findNode(key: K): RBNode<K, V> | null {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
} else if (key > current.key) {
current = current.right;
} else {
return current;
}
}
return null;
}
private deleteNode(node: RBNode<K, V>): void {
// 简化:用左子树最大或右子树最小替换
// 完整实现较复杂,这里采用简化策略
if (!node.left && !node.right) {
this.transplant(node, null);
if (node.color === Color.BLACK) this.fixDelete(null, node.parent);
} else if (!node.left) {
this.transplant(node, node.right);
if (node.color === Color.BLACK) this.fixDelete(node.right, node.right!.parent);
} else if (!node.right) {
this.transplant(node, node.left);
if (node.color === Color.BLACK) this.fixDelete(node.left, node.left!.parent);
} else {
const successor = this.minimum(node.right);
if (successor!.parent !== node) {
this.transplant(successor!, successor!.right);
successor!.right = node.right;
successor!.right!.parent = successor;
}
this.transplant(node, successor);
successor!.left = node.left;
successor!.left!.parent = successor;
const origColor = successor!.color;
successor!.color = node.color;
if (origColor === Color.BLACK) this.fixDelete(successor!.right, successor!.right?.parent ?? null);
}
}
private transplant(u: RBNode<K, V> | null, v: RBNode<K, V> | null): void {
if (!u!.parent) {
this.root = v;
} else if (u === u!.parent.left) {
u!.parent.left = v;
} else {
u!.parent.right = v;
}
if (v) v.parent = u!.parent;
}
private minimum(node: RBNode<K, V>): RBNode<K, V> {
while (node.left) node = node.left;
return node;
}
private fixInsert(node: RBNode<K, V>): void {
while (node.parent && node.parent.color === Color.RED) {
const parent = node.parent;
const grandparent = parent.parent;
if (!grandparent) break;
if (parent === grandparent.left) {
const uncle = grandparent.right;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
} else {
if (node === parent.right) {
node = parent;
this.rotateLeft(node);
}
if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateRight(node.parent.parent);
}
} else {
const uncle = grandparent.left;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
} else {
if (node === parent.left) {
node = parent;
this.rotateRight(node);
}
if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateLeft(node.parent.parent);
}
}
}
if (this.root) this.root.color = Color.BLACK;
}
private fixDelete(x: RBNode<K, V> | null, parent: RBNode<K, V> | null): void {
// 标准 RB-Tree 删除修复(修复"双黑"问题)
let node = x;
let nodeParent = parent;
while ((!node || node.color === Color.BLACK) && node !== this.root) {
if (!nodeParent) break;
if (node === nodeParent.left) {
let sibling = nodeParent.right;
if (!sibling) break;
// Case 1: 兄弟是红色
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateLeft(nodeParent);
sibling = nodeParent.right;
if (!sibling) break;
}
// Case 2: 兄弟的两个子节点都是黑色
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
} else {
// Case 3: 兄弟右子黑色(左子红色)
if (!sibRight || sibRight.color === Color.BLACK) {
if (sibLeft) sibLeft.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateRight(sibling);
sibling = nodeParent.right;
if (!sibling) break;
}
// Case 4: 兄弟右子红色
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.right) sibling.right.color = Color.BLACK;
this.rotateLeft(nodeParent);
node = this.root;
}
} else {
// 镜像:node 是父节点的右子
let sibling = nodeParent.left;
if (!sibling) break;
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateRight(nodeParent);
sibling = nodeParent.left;
if (!sibling) break;
}
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
} else {
if (!sibLeft || sibLeft.color === Color.BLACK) {
if (sibRight) sibRight.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateLeft(sibling);
sibling = nodeParent.left;
if (!sibling) break;
}
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.left) sibling.left.color = Color.BLACK;
this.rotateRight(nodeParent);
node = this.root;
}
}
}
if (node) node.color = Color.BLACK;
}
private rotateLeft(x: RBNode<K, V>): void {
const y = x.right;
if (!y) return;
x.right = y.left;
if (y.left) y.left.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
y.left = x;
x.parent = y;
}
private rotateRight(x: RBNode<K, V>): void {
const y = x.left;
if (!y) return;
x.left = y.right;
if (y.right) y.right.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.right) {
x.parent.right = y;
} else {
x.parent.left = y;
}
y.right = x;
x.parent = y;
}
private _inorder(node: RBNode<K, V> | null, cb: (k: K, v: V) => void): void {
if (!node) return;
this._inorder(node.left, cb);
cb(node.key, node.value);
this._inorder(node.right, cb);
}
private _rangeScan(
node: RBNode<K, V> | null,
start: K,
end: K,
cb: (k: K, v: V) => void,
): void {
if (!node) return;
if (node.key > start) this._rangeScan(node.left, start, end, cb);
if (node.key >= start && node.key <= end) cb(node.key, node.value);
if (node.key < end) this._rangeScan(node.right, start, end, cb);
}
}
// ---------------------------------------------------------------------------
// MemTable
// ---------------------------------------------------------------------------
export class MemTable {
private tree: RedBlackTree<string, Record<string, unknown>>;
private _estimatedSize = 0;
private maxSize: number;
constructor(maxSize: number = 4 * 1024 * 1024) {
this.tree = new RedBlackTree();
this.maxSize = maxSize;
}
/** 插入或更新 */
put(key: string, value: Record<string, unknown>): void {
const oldSize = this.estimateEntrySize(key, this.tree.find(key));
const newSize = this.estimateEntrySize(key, value);
this.tree.insert(key, value);
this._estimatedSize += newSize - oldSize;
}
/** 获取 */
get(key: string): Record<string, unknown> | null {
return this.tree.find(key);
}
/** 删除 */
delete(key: string): boolean {
const oldVal = this.tree.find(key);
if (oldVal) {
this._estimatedSize -= this.estimateEntrySize(key, oldVal);
}
return this.tree.delete(key);
}
/** 是否应刷盘 */
shouldFlush(): boolean {
return this._estimatedSize >= this.maxSize;
}
/** 获取所有有序条目 */
getAllEntries(): [string, Record<string, unknown>][] {
return this.tree.getAllEntries();
}
/** 范围扫描 */
rangeScan(
startKey: string,
endKey: string,
): [string, Record<string, unknown>][] {
const entries: [string, Record<string, unknown>][] = [];
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
return entries;
}
/** 条目数 */
getEntryCount(): number {
return this.tree.size;
}
/** 估计大小(字节) */
getEstimatedSize(): number {
return this._estimatedSize;
}
/** 清空 */
clear(): void {
this.tree.clear();
this._estimatedSize = 0;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private estimateEntrySize(key: string, value: Record<string, unknown> | null): number {
if (!value) return 0;
let size = key.length * 2; // UTF-16
for (const entry of Object.entries(value)) {
size += entry[0].length * 2;
const v = entry[1];
if (typeof v === 'string') size += v.length * 2;
else if (typeof v === 'number') size += 8;
else if (typeof v === 'boolean') size += 1;
else if (v === null || v === undefined) size += 1;
else size += 16; // rough estimate
}
return size;
}
}
+169 -192
View File
@@ -1,192 +1,169 @@
/**
* AriaEngine Merge Iterator — 多路归并迭代器
* @module engine/aria/index/merge_iterator
*
* 对多个有序 SSTable 或 MemTable 的结果进行归并去重(保留最新值)。
*/
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
export interface EntrySource {
/** 获取下一个条目,无更多时返回 null */
next(): [string, Record<string, unknown>] | null;
/** 重置迭代器 */
reset(): void;
}
/** 数组数据源的迭代器 */
export class ArrayEntrySource implements EntrySource {
private entries: [string, Record<string, unknown>][];
private index = 0;
constructor(entries: [string, Record<string, unknown>][]) {
this.entries = entries;
}
next(): [string, Record<string, unknown>] | null {
if (this.index >= this.entries.length) return null;
return this.entries[this.index++];
}
reset(): void {
this.index = 0;
}
}
/** 回调数据源的迭代器 */
export class CallbackEntrySource implements EntrySource {
private items: [string, Record<string, unknown>][] = [];
private index = 0;
private consumed = false;
/**
* @param producer 产生所有条目的回调
*/
constructor(producer: (cb: (key: string, value: Record<string, unknown>) => void) => void) {
producer((key, value) => this.items.push([key, value]));
}
next(): [string, Record<string, unknown>] | null {
if (this.index >= this.items.length) return null;
return this.items[this.index++];
}
reset(): void {
this.index = 0;
}
}
// ---------------------------------------------------------------------------
// Heap 节点(用于多路归并)
// ---------------------------------------------------------------------------
interface HeapNode {
key: string;
value: Record<string, unknown>;
sourceIndex: number;
}
/** 最小堆 */
class MinHeap {
private heap: HeapNode[] = [];
push(node: HeapNode): void {
this.heap.push(node);
this.bubbleUp(this.heap.length - 1);
}
pop(): HeapNode | null {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop()!;
const result = this.heap[0];
this.heap[0] = this.heap.pop()!;
this.bubbleDown(0);
return result;
}
peek(): HeapNode | null {
return this.heap.length > 0 ? this.heap[0] : null;
}
get size(): number {
return this.heap.length;
}
private bubbleUp(idx: number): void {
while (idx > 0) {
const parent = Math.floor((idx - 1) / 2);
if (this.heap[idx].key >= this.heap[parent].key) break;
[this.heap[idx], this.heap[parent]] = [this.heap[parent], this.heap[idx]];
idx = parent;
}
}
private bubbleDown(idx: number): void {
const n = this.heap.length;
while (true) {
let smallest = idx;
const left = 2 * idx + 1;
const right = 2 * idx + 2;
if (left < n && this.heap[left].key < this.heap[smallest].key) smallest = left;
if (right < n && this.heap[right].key < this.heap[smallest].key) smallest = right;
if (smallest === idx) break;
[this.heap[idx], this.heap[smallest]] = [this.heap[smallest], this.heap[idx]];
idx = smallest;
}
}
}
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
/**
* 对多个有序数据源进行归并,重复 key 保留最新(后出现的)。
* 数据源按新鲜度排序:越新的数据源在下标越小(如 MemTable 在 SSTable 之前)。
*/
export class MergeIterator {
private sources: EntrySource[];
private heap: MinHeap;
constructor() {
this.sources = [];
this.heap = new MinHeap();
}
/** 添加数据源 */
addSource(source: EntrySource): void {
this.sources.push(source);
this.seedFromSource(this.sources.length - 1);
}
/** 获取下一个归并后的条目 */
next(): [string, Record<string, unknown>] | null {
if (this.heap.size === 0) return null;
const first = this.heap.pop()!;
const key = first.key;
let best = first;
// 刷新 first 来源的下一个值
this.seedFromSource(first.sourceIndex);
// 跳过重复 key:在多个来源中保留 sourceIndex 最小(最新)的条目
while (this.heap.peek() && this.heap.peek()!.key === key) {
const dup = this.heap.pop()!;
this.seedFromSource(dup.sourceIndex);
if (dup.sourceIndex < best.sourceIndex) {
best = dup;
}
}
return [best.key, best.value];
}
/** 耗尽管道,返回所有归并结果 */
drain(): [string, Record<string, unknown>][] {
const result: [string, Record<string, unknown>][] = [];
let entry = this.next();
while (entry) {
result.push(entry);
entry = this.next();
}
return result;
}
private seedFromSource(sourceIndex: number): void {
const entry = this.sources[sourceIndex].next();
if (entry) {
this.heap.push({
key: entry[0],
value: entry[1],
sourceIndex,
});
}
}
}
/**
* AriaEngine Merge Iterator — 多路归并迭代器
* @module engine/aria/index/merge_iterator
*
* 对多个有序 SSTable 或 MemTable 的结果进行归并去重(保留最新值)。
*/
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
export interface EntrySource {
/** 获取下一个条目,无更多时返回 null */
next(): [string, Record<string, unknown>] | null;
/** 重置迭代器 */
reset(): void;
}
/** 数组数据源的迭代器 */
export class ArrayEntrySource implements EntrySource {
private entries: [string, Record<string, unknown>][];
private index = 0;
constructor(entries: [string, Record<string, unknown>][]) {
this.entries = entries;
}
next(): [string, Record<string, unknown>] | null {
if (this.index >= this.entries.length) return null;
return this.entries[this.index++];
}
reset(): void {
this.index = 0;
}
}
// ---------------------------------------------------------------------------
// Heap 节点(用于多路归并)
// ---------------------------------------------------------------------------
interface HeapNode {
key: string;
value: Record<string, unknown>;
sourceIndex: number;
}
/** 最小堆 */
class MinHeap {
private heap: HeapNode[] = [];
push(node: HeapNode): void {
this.heap.push(node);
this.bubbleUp(this.heap.length - 1);
}
pop(): HeapNode | null {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop()!;
const result = this.heap[0];
this.heap[0] = this.heap.pop()!;
this.bubbleDown(0);
return result;
}
peek(): HeapNode | null {
return this.heap.length > 0 ? this.heap[0] : null;
}
get size(): number {
return this.heap.length;
}
private bubbleUp(idx: number): void {
while (idx > 0) {
const parent = Math.floor((idx - 1) / 2);
if (this.heap[idx].key >= this.heap[parent].key) break;
[this.heap[idx], this.heap[parent]] = [this.heap[parent], this.heap[idx]];
idx = parent;
}
}
private bubbleDown(idx: number): void {
const n = this.heap.length;
while (true) {
let smallest = idx;
const left = 2 * idx + 1;
const right = 2 * idx + 2;
if (left < n && this.heap[left].key < this.heap[smallest].key) smallest = left;
if (right < n && this.heap[right].key < this.heap[smallest].key) smallest = right;
if (smallest === idx) break;
[this.heap[idx], this.heap[smallest]] = [this.heap[smallest], this.heap[idx]];
idx = smallest;
}
}
}
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
/**
* 对多个有序数据源进行归并,重复 key 保留最新(后出现的)。
* 数据源按新鲜度排序:越新的数据源在下标越小(如 MemTable 在 SSTable 之前)。
*/
export class MergeIterator {
private sources: EntrySource[];
private heap: MinHeap;
constructor() {
this.sources = [];
this.heap = new MinHeap();
}
/** 添加数据源 */
addSource(source: EntrySource): void {
this.sources.push(source);
this.seedFromSource(this.sources.length - 1);
}
/** 获取下一个归并后的条目 */
next(): [string, Record<string, unknown>] | null {
if (this.heap.size === 0) return null;
const first = this.heap.pop()!;
const key = first.key;
let best = first;
// 刷新 first 来源的下一个值
this.seedFromSource(first.sourceIndex);
// 跳过重复 key:在多个来源中保留 sourceIndex 最小(最新)的条目
while (this.heap.peek() && this.heap.peek()!.key === key) {
const dup = this.heap.pop()!;
this.seedFromSource(dup.sourceIndex);
if (dup.sourceIndex < best.sourceIndex) {
best = dup;
}
}
return [best.key, best.value];
}
/** 耗尽管道,返回所有归并结果 */
drain(): [string, Record<string, unknown>][] {
const result: [string, Record<string, unknown>][] = [];
let entry = this.next();
while (entry) {
result.push(entry);
entry = this.next();
}
return result;
}
private seedFromSource(sourceIndex: number): void {
const entry = this.sources[sourceIndex].next();
if (entry) {
this.heap.push({
key: entry[0],
value: entry[1],
sourceIndex,
});
}
}
}
+15 -10
View File
@@ -8,6 +8,7 @@
import type { IndexEntry, SSTableMeta } from '../types';
import { BloomFilter } from './bloom';
import { crc32 } from '../crc32';
import { SSTABLE_MAGIC_V1, SSTABLE_MAGIC_V2 } from './sstable_builder';
// ---------------------------------------------------------------------------
@@ -23,6 +24,8 @@ export class SSTableReader {
private bloomFilter: BloomFilter | null = null;
/** 格式版本:1 = u16 长度字段(旧),2 = u32 长度字段(v0.4.4 */
private format: 1 | 2 = 2;
/** footer 中存储的 checksum0 = 旧版无校验文件) */
private storedChecksum = 0;
constructor(data: Uint8Array, meta: SSTableMeta) {
this.data = data;
@@ -31,6 +34,17 @@ export class SSTableReader {
this.parseFooter();
}
/**
* 校验整文件 CRC-32(覆盖除 checksum 字段外的全部字节)。
* checksum === 0 表示旧版文件(v1 / v0.4.4 及更早的 v2),跳过校验返回 true(兼容)。
*/
verifyChecksum(): boolean {
if (this.storedChecksum === 0) return true;
if (this.data.byteLength < 4) return false;
const computed = crc32(this.data.subarray(0, this.data.byteLength - 4));
return computed === this.storedChecksum;
}
/** 长度字段宽度:v2 = 4 字节 u32v1 = 2 字节 u16 */
private lenFieldSize(): number {
return this.format === 2 ? 4 : 2;
@@ -170,16 +184,6 @@ export class SSTableReader {
}
}
/** 获取元数据 */
getMeta(): SSTableMeta {
return this.meta;
}
/** 获取索引条目数 */
getIndexBlockCount(): number {
return this.indexEntries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
@@ -207,6 +211,7 @@ export class SSTableReader {
const bloomSize = this.view.getUint32(footerOffset + 12, false);
const bloomHashCount = this.view.getUint32(footerOffset + 16, false);
this.entryCount = this.view.getUint32(footerOffset + 20, false);
this.storedChecksum = this.view.getUint32(footerOffset + 28, false);
// v0.4.1-fix: 完整性校验 — 索引块必须完全落在文件内,否则视为残缺文件跳过
if (indexOffset + 4 > this.data.byteLength || indexOffset + indexSize > this.data.byteLength) {
+10 -1
View File
@@ -34,6 +34,7 @@
*/
import { BloomFilter } from './bloom';
import { crc32 } from '../crc32';
import type { IndexEntry } from '../types';
/** v1 格式魔数("SSTB",u16 长度字段,兼容旧文件读取) */
@@ -137,7 +138,15 @@ export class SSTableBuilder {
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
view.setUint32(footerOffset + 20, this.entries.length, false);
view.setUint32(footerOffset + 24, SSTABLE_MAGIC_V2, false);
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
// checksum 字段先写 0,全部字节就绪后计算整文件 CRC32 再回填
view.setUint32(footerOffset + 28, 0, false);
// v0.4.5: 真实 CRC-32 校验和 — 覆盖除自身(最后 4 字节)外的全部内容。
// checksum 永远非 0(计算结果为 0 时用 1 代替),读取端以 0 识别旧版无校验文件
const all = new Uint8Array(buf);
let checksum = crc32(all.subarray(0, all.byteLength - 4));
if (checksum === 0) checksum = 1;
view.setUint32(footerOffset + 28, checksum, false);
return {
sstableData: new Uint8Array(buf),
+92
View File
@@ -0,0 +1,92 @@
/**
* AriaEngine Database Lock — 多标签页独占锁(Web Locks API
* @module engine/aria/locks
*
* v0.4.5: OPFS 等无事务后端缺乏多标签页并发协调,多个标签页同时打开同一库
* 会导致写竞态与数据损坏。用 Web Locks APIChrome 69+ / Firefox 96+ / Safari 15.4+
* 获取库级排他锁:
* - ifAvailable 模式:锁被其他标签页持有 → 立即抛 ARIA_LOCKED(不排队挂起)
* - 持锁期间回调挂起,close() 时释放
* - 浏览器不支持 navigator.locks → 返回 false(如实降级:无并发保护,文档注明)
*/
import { DatabaseError } from '../../constants';
/** Web Locks 锁名(库级排他) */
export function lockName(dbName: string): string {
return `metona-sqlark:${dbName}`;
}
export class DatabaseLock {
private acquired = false;
private supported = false;
private releaseResolve: (() => void) | null = null;
private releasePromise: Promise<void> | null = null;
/**
* 尝试获取独占锁。
* @returns true = 已持锁;false = 环境不支持 Web Locks(无并发保护,调用方可警告)
* @throws ARIA_LOCKED 锁被其他标签页持有
*/
async acquire(dbName: string): Promise<boolean> {
const nav = (globalThis as { navigator?: unknown }).navigator as {
locks?: { request: (name: string, opts: unknown, cb: (lock: { name: string } | null) => Promise<void> | void) => Promise<void> };
} | undefined;
const lockManager = nav?.locks;
if (!lockManager || typeof lockManager.request !== 'function') {
this.supported = false;
return false;
}
this.supported = true;
await new Promise<void>((resolve, reject) => {
// 注意:必须直接调用(不能解构 request —— LockManager 方法依赖 this 绑定)
const request = lockManager.request.bind(lockManager);
request(
lockName(dbName),
{ ifAvailable: true, mode: 'exclusive' },
async (lock) => {
if (!lock) {
reject(new DatabaseError(
`Database "${dbName}" is already open in another tab (locked)`,
'ARIA_LOCKED',
));
return;
}
this.acquired = true;
const releasePromise = new Promise<void>((res) => { this.releaseResolve = res; });
this.releasePromise = releasePromise;
// 锁已获取:acquire 返回(open 流程继续)
resolve();
// 回调挂起:保持锁直到 release() 触发
await releasePromise;
},
);
});
return true;
}
/** 释放锁(等待回调真正结束,保证锁已归还) */
async release(): Promise<void> {
if (!this.acquired) return;
if (this.releaseResolve) {
const res = this.releaseResolve;
const p = this.releasePromise!;
this.releaseResolve = null;
this.releasePromise = null;
res();
try { await p; } catch { /* 释放过程异常不阻塞 */ }
}
this.acquired = false;
}
/** 是否已持锁 */
isAcquired(): boolean {
return this.acquired;
}
/** 环境是否支持 Web Locks */
isSupported(): boolean {
return this.supported;
}
}
+28 -164
View File
@@ -1,164 +1,28 @@
/**
* AriaEngine Page Format — 页面格式整合层
* @module engine/aria/page/format
*
* 组合 Header / Slot / Tuple 操作,提供统一的页面管理接口
*/
import { PAGE_SIZE, PageType, type PageHandle } from '../types';
import {
initPageHeader,
getSlotCount,
} from './header';
import { allocateSlot, freeSlot, readSlotData } from './slot';
import { encodeTuple, decodeTuple } from './tuple';
// ---------------------------------------------------------------------------
// 页面创建
// ---------------------------------------------------------------------------
/** 创建一个新的空页面 */
export function createPage(pageId: number, type: PageType): PageHandle {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, type);
return {
pageId,
type,
data,
dirty: true,
pins: 0,
prev: null,
next: null,
lastAccess: Date.now(),
};
}
/** 从 ArrayBuffer 恢复页面句柄 */
export function pageFromBuffer(
pageId: number,
buffer: ArrayBuffer,
): PageHandle {
return {
pageId,
type: new DataView(buffer).getUint8(4) as PageType,
data: buffer,
dirty: false,
pins: 0,
prev: null,
next: null,
lastAccess: Date.now(),
};
}
// ---------------------------------------------------------------------------
// 行操作(页面级)
// ---------------------------------------------------------------------------
/**
* 插入一行到页面,返回 slot 索引,空间不足返回 -1。
*/
export function pageInsertRow(
page: PageHandle,
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): number {
const encoded = encodeTuple(row, columnOrder, columnTypes);
const slotIdx = allocateSlot(page.data, encoded);
if (slotIdx >= 0) {
page.dirty = true;
page.lastAccess = Date.now();
}
return slotIdx;
}
/**
* 读取指定 slot 的行数据并解码。
*/
export function pageReadRow(
page: PageHandle,
slotIndex: number,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown> | null {
const slotData = readSlotData(page.data, slotIndex);
if (!slotData) return null;
return decodeTuple(slotData, columnOrder, columnTypes);
}
/**
* 读取页面中的所有行。
*/
export function pageReadAllRows(
page: PageHandle,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown>[] {
const rows: Record<string, unknown>[] = [];
const slotCount = getSlotCount(page.data);
for (let i = 0; i < slotCount; i++) {
const row = pageReadRow(page, i, columnOrder, columnTypes);
if (row) rows.push(row);
}
return rows;
}
/**
* 标记 slot 为已删除。
*/
export function pageDeleteRow(page: PageHandle, slotIndex: number): void {
freeSlot(page.data, slotIndex);
page.dirty = true;
page.lastAccess = Date.now();
}
/**
* 重写指定 slot 的行数据。
*/
export function pageUpdateRow(
page: PageHandle,
slotIndex: number,
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): void {
// 先标记旧 slot 为删除
pageDeleteRow(page, slotIndex);
// 分配新 slot,可能会在不同位置
pageInsertRow(page, row, columnOrder, columnTypes);
// 注意:调用者需要自行维护 slot index → pk 的映射
}
// ---------------------------------------------------------------------------
// 校验和
// ---------------------------------------------------------------------------
/** 简单 CRC32(使用预先计算的查找表简化) */
export function computeChecksum(data: ArrayBuffer): number {
const view = new Uint8Array(data);
let hash = 0;
for (let i = 0; i < view.byteLength; i++) {
hash = ((hash << 5) - hash + view[i]) | 0;
}
return hash >>> 0;
}
/** 更新页面的校验和字段 */
export function updateChecksum(page: PageHandle): void {
// 先清零校验和字段
const view = new DataView(page.data);
view.setUint32(11, 0, false);
// 计算校验和
const cksum = computeChecksum(page.data);
view.setUint32(11, cksum, false);
}
/** 验证页面校验和 */
export function verifyChecksum(page: PageHandle): boolean {
const stored = new DataView(page.data).getUint32(11, false);
// 临时清零
new DataView(page.data).setUint32(11, 0, false);
const computed = computeChecksum(page.data);
new DataView(page.data).setUint32(11, stored, false);
return stored === computed;
}
/**
* AriaEngine Page Format — 页面创建
* @module engine/aria/page/format
*
* v0.4.5: 仅保留生产代码实际使用的页面创建逻辑
* Slot Directory / Tuple 编解码曾为行级页面存储设计,但从未接入 LSM 主路径,
* 属"宣称页面式但未实现"的半成品,已删除 —— SSTable 以 4KB 页面承载,
* 页面内容为原始字节切片,由 PageSSTableStore 管理)
*/
import { PAGE_SIZE, PageType, type PageHandle } from '../types';
import { initPageHeader } from './header';
/** 创建一个新的空页面 */
export function createPage(pageId: number, type: PageType): PageHandle {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, type);
return {
pageId,
type,
data,
dirty: true,
pins: 0,
prev: null,
next: null,
lastAccess: Date.now(),
};
}
+37 -97
View File
@@ -1,97 +1,37 @@
/**
* AriaEngine Page Header — 页面头部编解码
* @module engine/aria/page/header
*/
import { PAGE_HEADER_SIZE, PageType, type PageHeader } from '../types';
// ---------------------------------------------------------------------------
// 编码
// ---------------------------------------------------------------------------
/**
* 将 PageHeader 编码写入 ArrayBuffer 的前 16 字节。
* 布局(大端序):
* [0-3] page_id u32
* [4] type u8
* [5-6] free_start u16
* [7-8] free_end u16
* [9-10] slot_count u16
* [11-14] checksum u32
* [15] reserved u8
*/
export function encodePageHeader(header: PageHeader, buf: ArrayBuffer): void {
const view = new DataView(buf);
view.setUint32(0, header.pageId, false);
view.setUint8(4, header.type);
view.setUint16(5, header.freeStart, false);
view.setUint16(7, header.freeEnd, false);
view.setUint16(9, header.slotCount, false);
view.setUint32(11, header.checksum, false);
view.setUint8(15, 0); // reserved
}
/**
* 从 ArrayBuffer 解码 PageHeader。
*/
export function decodePageHeader(buf: ArrayBuffer): PageHeader {
const view = new DataView(buf);
return {
pageId: view.getUint32(0, false),
type: view.getUint8(4) as PageType,
freeStart: view.getUint16(5, false),
freeEnd: view.getUint16(7, false),
slotCount: view.getUint16(9, false),
checksum: view.getUint32(11, false),
};
}
/**
* 初始化新页面的 Header。
*/
export function initPageHeader(
buf: ArrayBuffer,
pageId: number,
type: PageType,
): void {
const view = new DataView(buf);
view.setUint32(0, pageId, false);
view.setUint8(4, type);
view.setUint16(5, PAGE_HEADER_SIZE, false); // freeStart = header 之后
view.setUint16(7, buf.byteLength, false); // freeEnd = 页面末尾
view.setUint16(9, 0, false); // slotCount = 0
view.setUint32(11, 0, false); // checksum = 0
view.setUint8(15, 0);
}
// ---------------------------------------------------------------------------
// 从 Buffer 中提取 Header 字段的辅助函数
// ---------------------------------------------------------------------------
export function getPageType(buf: ArrayBuffer): PageType {
return new DataView(buf).getUint8(4) as PageType;
}
export function getPageId(buf: ArrayBuffer): number {
return new DataView(buf).getUint32(0, false);
}
export function getSlotCount(buf: ArrayBuffer): number {
return new DataView(buf).getUint16(9, false);
}
export function getFreeStart(buf: ArrayBuffer): number {
return new DataView(buf).getUint16(5, false);
}
export function setFreeStart(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(5, val, false);
}
export function setFreeEnd(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(7, val, false);
}
export function setSlotCount(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(9, val, false);
}
/**
* AriaEngine Page Header — 页面头初始化
* @module engine/aria/page/header
*
* v0.4.5: 仅保留生产代码实际使用的 initPageHeader。
* 页面头部布局(大端序):
* [0-3] page_id u32
* [4] type u8
* [5-6] free_start u16
* [7-8] free_end u16
* [9-10] slot_count u16
* [11-14] checksum u32
* [15] reserved u8
*
* 注:free_start/free_end/slot_count/checksum 字段为历史行级页面格式遗留,
* 页面化 SSTable 使用页面原始字节区(跳过头部),字段保留以维持 16 字节头部对齐。
*/
import { PAGE_HEADER_SIZE } from '../types';
/**
* 初始化新页面的 Header。
*/
export function initPageHeader(
buf: ArrayBuffer,
pageId: number,
type: number,
): void {
const view = new DataView(buf);
view.setUint32(0, pageId, false);
view.setUint8(4, type);
view.setUint16(5, PAGE_HEADER_SIZE, false); // freeStart = header 之后
view.setUint16(7, buf.byteLength, false); // freeEnd = 页面末尾
view.setUint16(9, 0, false); // slotCount = 0
view.setUint32(11, 0, false); // checksum = 0
view.setUint8(15, 0);
}
-184
View File
@@ -1,184 +0,0 @@
/**
* AriaEngine Slot Directory — 页面内行槽位管理
* @module engine/aria/page/slot
*
* Slot 目录从页面 Header 之后向下增长,每条记录 4 字节。
*/
import {
PAGE_HEADER_SIZE,
SLOT_ENTRY_SIZE,
PAGE_SIZE,
type SlotEntry,
} from '../types';
// ---------------------------------------------------------------------------
// Slot 读写
// ---------------------------------------------------------------------------
/**
* 读取 slot 号对应的 SlotEntry。
*/
export function getSlotEntry(buf: ArrayBuffer, slotIndex: number): SlotEntry {
const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE;
const view = new DataView(buf);
return {
offset: view.getUint16(offset, false),
length: view.getUint16(offset + 2, false),
};
}
/**
* 写入 slot 号对应的 SlotEntry。
*/
export function setSlotEntry(
buf: ArrayBuffer,
slotIndex: number,
entry: SlotEntry,
): void {
const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE;
const view = new DataView(buf);
view.setUint16(offset, entry.offset, false);
view.setUint16(offset + 2, entry.length, false);
}
/**
* 获取页面中所有 slot 条目。
*/
export function getAllSlots(
buf: ArrayBuffer,
slotCount: number,
): SlotEntry[] {
const entries: SlotEntry[] = [];
for (let i = 0; i < slotCount; i++) {
entries.push(getSlotEntry(buf, i));
}
return entries;
}
// ---------------------------------------------------------------------------
// Slot 空间计算
// ---------------------------------------------------------------------------
/** 获取 slot 目录占用的总字节数 */
export function getSlotDirectorySize(slotCount: number): number {
return slotCount * SLOT_ENTRY_SIZE;
}
/** 获取可用空闲空间(字节) */
export function getFreeSpace(buf: ArrayBuffer): number {
const view = new DataView(buf);
const freeStart = view.getUint16(5, false); // slot 区之后
const freeEnd = view.getUint16(7, false); // 数据区之前
return freeEnd - freeStart;
}
/** 检查是否有足够空间存放长度为 len 的行 */
export function hasEnoughSpace(buf: ArrayBuffer, len: number): boolean {
const slotCount = new DataView(buf).getUint16(9, false);
const neededSlotSize = (slotCount + 1) * SLOT_ENTRY_SIZE;
const freeStart = PAGE_HEADER_SIZE + neededSlotSize;
const freeEnd = new DataView(buf).getUint16(7, false);
return freeEnd - freeStart >= len;
}
// ---------------------------------------------------------------------------
// 插入 / 删除 slot
// ---------------------------------------------------------------------------
/**
* 在页面中分配一个 slot 并写入行数据。
* 返回分配的 slot 索引,失败返回 -1。
*/
export function allocateSlot(
buf: ArrayBuffer,
rowData: Uint8Array,
): number {
const view = new DataView(buf);
const slotCount = view.getUint16(9, false);
const neededSlotSpace = (slotCount + 1) * SLOT_ENTRY_SIZE;
const freeStart = PAGE_HEADER_SIZE + neededSlotSpace;
const freeEnd = view.getUint16(7, false);
if (freeEnd - freeStart < rowData.byteLength) {
return -1; // 空间不足
}
// 将数据放入页面底部
const dataOffset = freeEnd - rowData.byteLength;
const dest = new Uint8Array(buf, dataOffset, rowData.byteLength);
dest.set(rowData);
// 写入 slot 条目
setSlotEntry(buf, slotCount, { offset: dataOffset, length: rowData.byteLength });
// 更新 header
view.setUint16(5, freeStart, false); // freeStart
view.setUint16(7, dataOffset, false); // freeEnd
view.setUint16(9, slotCount + 1, false); // slotCount
return slotCount;
}
/**
* 从页面中删除指定 slot 的数据(标记为无效,offset 置 0)。
* 注:简化实现,不做 slot 压缩。
*/
export function freeSlot(buf: ArrayBuffer, slotIndex: number): void {
setSlotEntry(buf, slotIndex, { offset: 0, length: 0 });
}
/**
* 压缩页面槽位:移除已删除 slot,整理碎片空间。
* 将有效数据紧凑排列,释放空洞。
*/
export function compactSlots(buf: ArrayBuffer): number {
const view = new DataView(buf);
const slotCount = view.getUint16(9, false);
if (slotCount === 0) return 0;
// 收集有效 slotoffset>0 的)
const validSlots: { index: number; offset: number; length: number; data: Uint8Array }[] = [];
for (let i = 0; i < slotCount; i++) {
const entry = getSlotEntry(buf, i);
if (entry.offset > 0 && entry.length > 0) {
const data = new Uint8Array(buf, entry.offset, entry.length);
validSlots.push({ index: i, offset: entry.offset, length: entry.length, data: new Uint8Array(data) });
}
}
if (validSlots.length === slotCount) return 0; // 无碎片
// 从页面底部重新紧凑排列
let dataEnd = PAGE_SIZE;
const newSlots: { offset: number; length: number }[] = [];
for (let i = validSlots.length - 1; i >= 0; i--) {
const s = validSlots[i];
dataEnd -= s.length;
new Uint8Array(buf).set(s.data, dataEnd);
newSlots.unshift({ offset: dataEnd, length: s.length });
}
// 重写 slot directory
view.setUint16(9, validSlots.length, false); // slotCount
view.setUint16(7, dataEnd, false); // freeEnd
for (let i = 0; i < validSlots.length; i++) {
setSlotEntry(buf, i, newSlots[i]);
}
// 清除剩余 slot 条目
for (let i = validSlots.length; i < slotCount; i++) {
setSlotEntry(buf, i, { offset: 0, length: 0 });
}
return slotCount - validSlots.length; // 回收的 slot 数量
}
/**
* 读取指定 slot 的行数据。
*/
export function readSlotData(buf: ArrayBuffer, slotIndex: number): Uint8Array | null {
const entry = getSlotEntry(buf, slotIndex);
if (entry.offset === 0 || entry.length === 0) return null;
return new Uint8Array(buf, entry.offset, entry.length);
}
-251
View File
@@ -1,251 +0,0 @@
/**
* AriaEngine Tuple Codec — 行数据的二进制编解码
* @module engine/aria/page/tuple
*
* 将 Record<string, unknown> 编码为紧凑的二进制格式。
*
* 格式:
* [null bitmap: ceil(colCount/8) bytes]
* [column 1 data]
* [column 2 data]
* ...
*
* 每列:
* type tag (u8) + data
* - STRING: [len: u16][UTF-8 bytes]
* - NUMBER: [f64: 8 bytes]
* - BOOLEAN: [u8: 1 byte]
* - DATE: [f64: 8 bytes] (epoch ms)
* - JSON: [len: u16][UTF-8 bytes]
* - NULL: (no data, just the tag)
*/
import { ColumnEncoding } from '../types';
// ---------------------------------------------------------------------------
// 编码
// ---------------------------------------------------------------------------
/**
* 将行数据编码为二进制字节数组。
* @param row 行数据
* @param columnOrder 列名顺序列表(决定编码顺序)
* @param columnTypes 列名 → FieldType 映射
*/
export function encodeTuple(
row: Record<string, unknown>,
columnOrder: string[],
columnTypes: Record<string, string>,
): Uint8Array {
// 先计算总大小
let size = 0;
const nullBitmapBytes = Math.ceil(columnOrder.length / 8);
size += nullBitmapBytes;
// 预计算每列编码后的字节
const colData: (Uint8Array | null)[] = [];
for (let i = 0; i < columnOrder.length; i++) {
const col = columnOrder[i];
const val = row[col];
const encoded = encodeColumn(val, columnTypes[col] ?? 'string');
colData.push(encoded);
if (encoded) {
size += 1 + encoded.byteLength; // tag + data
} else {
size += 1; // just the NULL tag
}
}
const buf = new Uint8Array(size);
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
let offset = 0;
// Null bitmap
const nullBitmap = new Uint8Array(nullBitmapBytes);
for (let i = 0; i < columnOrder.length; i++) {
if (colData[i] === null) {
nullBitmap[Math.floor(i / 8)] |= (1 << (i % 8));
}
}
buf.set(nullBitmap, offset);
offset += nullBitmapBytes;
// Column data
for (let i = 0; i < columnOrder.length; i++) {
const encoded = colData[i];
if (encoded === null) {
view.setUint8(offset, ColumnEncoding.NULL);
offset += 1;
} else {
const tag = getEncodingTag(columnTypes[columnOrder[i]] ?? 'string');
view.setUint8(offset, tag);
offset += 1;
buf.set(encoded, offset);
offset += encoded.byteLength;
}
}
return buf;
}
/**
* 编码单个列的值。
*/
function encodeColumn(value: unknown, fieldType: string): Uint8Array | null {
if (value === null || value === undefined) return null;
switch (fieldType) {
case 'string': {
const str = String(value);
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const buf = new Uint8Array(2 + bytes.byteLength);
new DataView(buf.buffer).setUint16(0, bytes.byteLength, false);
buf.set(bytes, 2);
return buf;
}
case 'number': {
const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, Number(value), false);
return new Uint8Array(buf);
}
case 'boolean': {
return new Uint8Array([value ? 1 : 0]);
}
case 'date': {
const ts = value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, ts, false);
return new Uint8Array(buf);
}
case 'json': {
const str = JSON.stringify(value);
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const buf = new Uint8Array(2 + bytes.byteLength);
new DataView(buf.buffer).setUint16(0, bytes.byteLength, false);
buf.set(bytes, 2);
return buf;
}
default:
return null;
}
}
// ---------------------------------------------------------------------------
// 解码
// ---------------------------------------------------------------------------
/**
* 从二进制字节数组解码行数据。
* @returns 行数据,如果格式错误返回 null
*/
export function decodeTuple(
bytes: Uint8Array,
columnOrder: string[],
columnTypes: Record<string, string>,
): Record<string, unknown> | null {
try {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let offset = 0;
const nullBitmapBytes = Math.ceil(columnOrder.length / 8);
if (offset + nullBitmapBytes > bytes.byteLength) return null;
offset += nullBitmapBytes;
const row: Record<string, unknown> = {};
for (let i = 0; i < columnOrder.length; i++) {
if (offset >= bytes.byteLength) break;
const tag = view.getUint8(offset);
offset += 1;
if (tag === ColumnEncoding.NULL) {
row[columnOrder[i]] = null;
continue;
}
const col = columnOrder[i];
const fType = columnTypes[col] ?? 'string';
const result = decodeColumnValue(bytes, offset, tag, fType);
if (result === null) return null;
row[col] = result.value;
offset = result.nextOffset;
}
return row;
} catch {
return null;
}
}
function decodeColumnValue(
bytes: Uint8Array,
offset: number,
tag: number,
_fieldType: string,
): { value: unknown; nextOffset: number } | null {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
switch (tag) {
case ColumnEncoding.STRING:
case ColumnEncoding.JSON: {
if (offset + 2 > bytes.byteLength) return null;
const len = view.getUint16(offset, false);
offset += 2;
if (offset + len > bytes.byteLength) return null;
const decoder = new TextDecoder();
const str = decoder.decode(bytes.slice(offset, offset + len));
return {
value: tag === ColumnEncoding.JSON ? JSON.parse(str) : str,
nextOffset: offset + len,
};
}
case ColumnEncoding.NUMBER: {
if (offset + 8 > bytes.byteLength) return null;
const val = view.getFloat64(offset, false);
return { value: val, nextOffset: offset + 8 };
}
case ColumnEncoding.BOOLEAN: {
if (offset >= bytes.byteLength) return null;
return { value: view.getUint8(offset) !== 0, nextOffset: offset + 1 };
}
case ColumnEncoding.DATE: {
if (offset + 8 > bytes.byteLength) return null;
const ts = view.getFloat64(offset, false);
return { value: new Date(ts).toISOString(), nextOffset: offset + 8 };
}
default:
return null;
}
}
// ---------------------------------------------------------------------------
// 辅助
// ---------------------------------------------------------------------------
function getEncodingTag(fieldType: string): ColumnEncoding {
switch (fieldType) {
case 'string': return ColumnEncoding.STRING;
case 'number': return ColumnEncoding.NUMBER;
case 'boolean': return ColumnEncoding.BOOLEAN;
case 'date': return ColumnEncoding.DATE;
case 'json': return ColumnEncoding.JSON;
default: return ColumnEncoding.NULL;
}
}
/** 获取列类型到编码标签的映射 */
export function getColumnEncodingMap(
columnOrder: string[],
columnTypes: Record<string, string>,
): Map<string, ColumnEncoding> {
const map = new Map<string, ColumnEncoding>();
for (const col of columnOrder) {
map.set(col, getEncodingTag(columnTypes[col] ?? 'string'));
}
return map;
}
+7
View File
@@ -23,6 +23,13 @@ export interface IStorageBackend {
read(key: string): Promise<ArrayBuffer | null>;
/** 写入数据块 */
write(key: string, data: ArrayBuffer): Promise<void>;
/**
* 追加写入(v0.4.5 WAL 分片用,可选):
* - OPFS 后端实现真追加(createWritable keepExistingData + seekO(chunk)
* - 未实现的后端由调用方回退 read+writeEncryptedBackend 包装时整体重写保正确性)
* 语义:在 key 现有内容末尾追加 data;key 不存在时等同 write。
*/
append?(key: string, data: ArrayBuffer): Promise<void>;
/**
* 批量原子写入(v0.4.2-fix):多个 key 在单个底层事务中提交,
* 中断时整体回滚,不留半写状态。WAL count 与记录同事务保证一致性。
+233
View File
@@ -0,0 +1,233 @@
/**
* AriaEngine Encrypted Backend — 全库透明 AES-256-GCM 加密后端
* @module engine/aria/store/encrypted_backend
*
* 装饰器模式包装底层 IStorageBackend
* - 写入时加密(每个 value 独立随机 IV:格式 [12B IV][AES-GCM ciphertext]
* - 读取时解密(GCM 认证标签同时保证完整性)
* - WAL / SSTable / Schema / 元数据 全部密文存储(除密钥元数据本身)
*
* 密钥管理:
* - PBKDF2-SHA256100000 迭代)从密码派生 AES-256-GCM 密钥
* - `__aria_keymeta` 明文保存 { salt, verifier }
* - salt:PBKDF2 盐(重启后用同一密码重新派生密钥)
* - verifier:对固定明文加密的密文(打开时解密验证密码正确性)
* - 密码错误 → GCM 认证失败 → 抛 ARIA_DECRYPT_ERROR
*
* 依赖浏览器/Node 的 WebCryptojest.setup.js 已提供 polyfill)。
*/
import { DatabaseError } from '../../../constants';
import type { IStorageBackend } from './backend';
import { CryptoManager } from '../crypto';
const IV_LENGTH = 12;
const KEYMETA_KEY = '__aria_keymeta';
/** verifier 固定明文(无数据泄露风险) */
const VERIFIER_PLAIN = 'metona-sqlark-encryption-verifier';
// ---------------------------------------------------------------------------
// Base64 工具(浏览器 btoa/atob 与 Node Buffer 双环境)
// ---------------------------------------------------------------------------
function bytesToBase64(bytes: Uint8Array): string {
if (typeof Buffer !== 'undefined') {
return Buffer.from(bytes).toString('base64');
}
let bin = '';
for (let i = 0; i < bytes.byteLength; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
function base64ToBytes(b64: string): Uint8Array {
if (typeof Buffer !== 'undefined') {
return new Uint8Array(Buffer.from(b64, 'base64'));
}
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
// ---------------------------------------------------------------------------
// EncryptedBackend
// ---------------------------------------------------------------------------
export class EncryptedBackend implements IStorageBackend {
private crypto: CryptoManager = new CryptoManager();
private opened = false;
constructor(
private inner: IStorageBackend,
private password: string,
) {
if (!password || password.length === 0) {
throw new DatabaseError('Encryption password must not be empty', 'ARIA_ENCRYPT_CONFIG_ERROR');
}
}
/** 底层后端(测试/调试用) */
getInner(): IStorageBackend {
return this.inner;
}
/** 加密是否已初始化(打开并验证/创建密钥后为 true) */
isCryptoReady(): boolean {
return this.crypto.enabled;
}
async open(name: string): Promise<void> {
if (!this.inner.isOpen()) {
await this.inner.open(name);
}
const raw = await this.inner.read(KEYMETA_KEY);
if (raw) {
// 已有密钥元数据:用持久化 salt 重新派生并验证密码
let meta: { salt?: string; verifier?: string };
try {
meta = JSON.parse(new TextDecoder().decode(raw)) as { salt?: string; verifier?: string };
} catch {
throw new DatabaseError('Corrupted encryption key metadata', 'ARIA_DECRYPT_ERROR');
}
if (!meta.salt || !meta.verifier) {
throw new DatabaseError('Corrupted encryption key metadata', 'ARIA_DECRYPT_ERROR');
}
const salt = base64ToBytes(meta.salt);
await this.crypto.init(this.password, salt);
const verifierBytes = base64ToBytes(meta.verifier);
if (verifierBytes.byteLength <= IV_LENGTH) {
throw new DatabaseError('Corrupted encryption key metadata', 'ARIA_DECRYPT_ERROR');
}
const iv = verifierBytes.subarray(0, IV_LENGTH);
const ciphertext = verifierBytes.subarray(IV_LENGTH);
try {
await this.crypto.decryptPage(iv, ciphertext);
} catch {
// GCM 认证失败:密码错误(或密钥元数据被篡改)
throw new DatabaseError(
'Decryption failed: wrong password or corrupted key metadata',
'ARIA_DECRYPT_ERROR',
);
}
} else {
// 无 keymeta:若库中已存在其他数据 → 明文旧库或 keymeta 丢失,拒绝以加密模式打开
const keys = await this.inner.listKeys();
if (keys.some((k) => k !== KEYMETA_KEY)) {
throw new DatabaseError(
'Cannot open with encryption: existing database has no key metadata ' +
'(database was created without encryption, or key metadata was lost)',
'ARIA_ENCRYPT_CONFIG_ERROR',
);
}
// 新库:生成随机 salt + 派生密钥 + 写入 verifier
const salt = await this.crypto.init(this.password);
const enc = await this.crypto.encryptPage(new TextEncoder().encode(VERIFIER_PLAIN).buffer);
const verifier = new Uint8Array(IV_LENGTH + enc.data.byteLength);
verifier.set(enc.iv, 0);
verifier.set(new Uint8Array(enc.data), IV_LENGTH);
const keymeta = JSON.stringify({
salt: bytesToBase64(salt),
verifier: bytesToBase64(verifier),
});
await this.inner.write(KEYMETA_KEY, new TextEncoder().encode(keymeta).buffer);
}
this.opened = true;
}
async close(): Promise<void> {
await this.inner.close();
this.crypto.close();
this.opened = false;
}
isOpen(): boolean {
return this.opened;
}
async read(key: string): Promise<ArrayBuffer | null> {
this.ensureReady();
const raw = await this.inner.read(key);
if (raw === null) return null;
return this.decrypt(raw);
}
async write(key: string, data: ArrayBuffer): Promise<void> {
this.ensureReady();
await this.inner.write(key, await this.encrypt(data));
}
async writeMany(entries: Record<string, ArrayBuffer>): Promise<void> {
this.ensureReady();
const encrypted: Record<string, ArrayBuffer> = {};
for (const [key, data] of Object.entries(entries)) {
encrypted[key] = await this.encrypt(data);
}
await this.inner.writeMany(encrypted);
}
async delete(key: string): Promise<void> {
this.ensureReady();
await this.inner.delete(key);
}
async deleteMany(keys: string[]): Promise<void> {
this.ensureReady();
await this.inner.deleteMany(keys);
}
async listKeys(): Promise<string[]> {
this.ensureReady();
return this.inner.listKeys();
}
async exists(key: string): Promise<boolean> {
this.ensureReady();
return this.inner.exists(key);
}
async clear(): Promise<void> {
// 清空全部数据但保留密钥元数据 —— clearAll 语义:库本身保留,密码不失效
const keys = await this.inner.listKeys();
const toDelete = keys.filter((k) => k !== KEYMETA_KEY);
if (toDelete.length > 0) {
await this.inner.deleteMany(toDelete);
}
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private ensureReady(): void {
if (!this.opened) throw new DatabaseError('EncryptedBackend not opened', 'ARIA_DB_NOT_OPEN');
if (!this.crypto.enabled) throw new DatabaseError('EncryptedBackend key not initialized', 'ARIA_DECRYPT_ERROR');
}
/** 加密单块数据:[IV(12)][ciphertext] */
private async encrypt(data: ArrayBuffer): Promise<ArrayBuffer> {
const enc = await this.crypto.encryptPage(data);
const out = new ArrayBuffer(IV_LENGTH + enc.data.byteLength);
const outBytes = new Uint8Array(out);
outBytes.set(enc.iv, 0);
outBytes.set(new Uint8Array(enc.data), IV_LENGTH);
return out;
}
/** 解密单块数据(GCM 认证失败抛错) */
private async decrypt(data: ArrayBuffer): Promise<ArrayBuffer> {
const bytes = new Uint8Array(data);
if (bytes.byteLength <= IV_LENGTH) {
throw new DatabaseError('Corrupted encrypted data block (too short)', 'ARIA_DECRYPT_ERROR');
}
const iv = bytes.subarray(0, IV_LENGTH);
const ciphertext = bytes.subarray(IV_LENGTH);
try {
return await this.crypto.decryptPage(iv, ciphertext);
} catch (error) {
if (error instanceof DatabaseError) throw error;
throw new DatabaseError('Decryption failed (data corrupted or wrong key)', 'ARIA_DECRYPT_ERROR', error);
}
}
}
+104 -111
View File
@@ -1,111 +1,104 @@
/**
* AriaEngine File Manager — 页面文件管理 + PageIO 实现
* @module engine/aria/store/file_manager
*
* 负责管理页面文件的生命周期:分配/释放页面 ID,读写页面。
*/
import type { IStorageBackend } from './backend';
import type { PageIO } from '../buffer/pool';
import { PAGE_SIZE, PageType } from '../types';
import { initPageHeader } from '../page/header';
// ---------------------------------------------------------------------------
// FileManager (implements PageIO)
// ---------------------------------------------------------------------------
export class FileManager implements PageIO {
private backend: IStorageBackend;
private nextPageId = 0;
private metaLoaded = false;
private dbName = '';
constructor(backend: IStorageBackend) {
this.backend = backend;
}
/** 初始化:从存储中读取元数据 */
async init(dbName: string): Promise<void> {
this.dbName = dbName;
const meta = await this.backend.read('__aria_meta');
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
const view = new DataView(meta);
this.nextPageId = view.getUint32(0, false);
} else {
this.nextPageId = 1;
await this.saveMeta();
}
this.metaLoaded = true;
}
// ---- PageIO ----
async readPage(pageId: number): Promise<ArrayBuffer | null> {
const key = `pg_${pageId}`;
const data = await this.backend.read(key);
if (!data) {
// 第一次访问:创建新页面
return this.createEmptyPage(pageId, PageType.DATA);
}
// 确保大小正确
if (data.byteLength < PAGE_SIZE) {
const padded = new ArrayBuffer(PAGE_SIZE);
new Uint8Array(padded).set(new Uint8Array(data));
return padded;
}
return data;
}
async writePage(pageId: number, data: ArrayBuffer): Promise<void> {
const key = `pg_${pageId}`;
await this.backend.write(key, data);
}
async allocatePageId(): Promise<number> {
const id = this.nextPageId++;
await this.saveMeta();
return id;
}
async freePageId(_pageId: number): Promise<void> {
// 简化实现:不回收 pageId
const key = `pg_${_pageId}`;
await this.backend.delete(key);
}
// ---- 表页面分配 ----
/**
* 分配一个新的表元数据页面。
*/
async allocateTableRootPage(): Promise<number> {
const pageId = await this.allocatePageId();
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, PageType.META);
await this.writePage(pageId, data);
return pageId;
}
// ---- 辅助 ----
private async saveMeta(): Promise<void> {
const buf = new ArrayBuffer(8);
new DataView(buf).setUint32(0, this.nextPageId, false);
await this.backend.write('__aria_meta', buf);
}
private createEmptyPage(pageId: number, type: PageType): ArrayBuffer {
const buf = new ArrayBuffer(PAGE_SIZE);
initPageHeader(buf, pageId, type);
return buf;
}
/** 清空所有数据 */
async clearAll(): Promise<void> {
await this.backend.clear();
this.nextPageId = 1;
await this.saveMeta();
}
}
/**
* AriaEngine File Manager — 页面文件管理 + PageIO 实现
* @module engine/aria/store/file_manager
*
* 负责管理页面文件的生命周期:分配/释放页面 ID,读写页面。
*/
import type { IStorageBackend } from './backend';
import type { PageIO } from '../buffer/pool';
import { PAGE_SIZE, PageType } from '../types';
import { initPageHeader } from '../page/header';
// ---------------------------------------------------------------------------
// FileManager (implements PageIO)
// ---------------------------------------------------------------------------
export class FileManager implements PageIO {
private backend: IStorageBackend;
private nextPageId = 0;
private metaLoaded = false;
private dbName = '';
constructor(backend: IStorageBackend) {
this.backend = backend;
}
/** 初始化:从存储中读取元数据 */
async init(dbName: string): Promise<void> {
this.dbName = dbName;
const meta = await this.backend.read('__aria_meta');
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
const view = new DataView(meta);
this.nextPageId = view.getUint32(0, false);
} else {
this.nextPageId = 1;
await this.saveMeta();
}
this.metaLoaded = true;
}
// ---- PageIO ----
async readPage(pageId: number): Promise<ArrayBuffer | null> {
const key = `pg_${pageId}`;
const data = await this.backend.read(key);
if (!data) {
// v0.4.5: 页面总是先分配(allocatePageId 持久化)后写入 —— 读取缺失页面视为损坏
// (此前返回空页面会静默掩盖页面丢失,页面化 SSTable 依赖 null 触发自愈清理)
return null;
}
// 确保大小正确
if (data.byteLength < PAGE_SIZE) {
const padded = new ArrayBuffer(PAGE_SIZE);
new Uint8Array(padded).set(new Uint8Array(data));
return padded;
}
return data;
}
async writePage(pageId: number, data: ArrayBuffer): Promise<void> {
const key = `pg_${pageId}`;
await this.backend.write(key, data);
}
async allocatePageId(): Promise<number> {
const id = this.nextPageId++;
await this.saveMeta();
return id;
}
/** v0.4.5: 批量分配页面 ID(一次 meta 持久化,避免页面化 SSTable 保存时逐页写 meta */
async allocatePageIds(count: number): Promise<number[]> {
if (count <= 0) return [];
const ids: number[] = [];
const start = this.nextPageId;
this.nextPageId += count;
for (let i = 0; i < count; i++) ids.push(start + i);
await this.saveMeta();
return ids;
}
async freePageId(_pageId: number): Promise<void> {
// 简化实现:不回收 pageId
const key = `pg_${_pageId}`;
await this.backend.delete(key);
}
// ---- 辅助 ----
private async saveMeta(): Promise<void> {
const buf = new ArrayBuffer(8);
new DataView(buf).setUint32(0, this.nextPageId, false);
await this.backend.write('__aria_meta', buf);
}
/** 清空所有数据 */
async clearAll(): Promise<void> {
await this.backend.clear();
this.nextPageId = 1;
await this.saveMeta();
}
}
+72 -9
View File
@@ -5,10 +5,23 @@
* 零外部依赖,纯浏览器文件系统 API。
* 每个 key 对应 OPFS 目录下的一个二进制文件。
*
* 浏览器要求:Chrome 102+ / Edge 102+
* 原子性与一致性保证(v0.4.5 固化):
* - 单文件 write/appendcreateWritable 为 copy-on-write —— close 前崩溃旧文件保持不变,
* close 后原子替换(单文件写入原子)
* - 多文件 writeMany/deleteManyOPFS 无跨文件事务,串行逐个落盘;调用方(WAL 分片)
* 已改为单文件语义,多键操作仅用于一次性的 schema/meta 写入
* - 所有写操作串行队列化(同源同进程顺序一致);单次任务失败不中断队列链,
* 错误如实返回给该次调用的调用方
* - close() 等待写队列排空后再释放目录句柄(杜绝 close 后挂起写丢失/读旧数据)
* - open() 自动清理崩溃残留临时文件(Chromium createWritable 的 .crswap 等)
*
* 浏览器要求:Chrome 102+ / Edge 102+Safari 15.2+ / Firefox 111+ 支持基础 OPFS
*/
import type { IStorageBackend } from './backend';
/** 崩溃残留临时文件后缀(createWritable 底层实现可能留下) */
const STALE_SUFFIXES = ['.crswap', '.tmp'];
export class OPFSBackend implements IStorageBackend {
private root: FileSystemDirectoryHandle | null = null;
private dbDir: FileSystemDirectoryHandle | null = null;
@@ -19,9 +32,15 @@ export class OPFSBackend implements IStorageBackend {
this.dbName = name;
this.root = await navigator.storage.getDirectory();
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
// v0.4.5: 清理崩溃残留临时文件(不阻塞打开)
await this.cleanupStaleFiles();
}
async close(): Promise<void> {
// v0.4.5-fix: 等待所有挂起写完成(否则 close 后挂起写被静默丢弃/读旧数据)
try {
await this.writeQueue;
} catch { /* 写失败已返回给调用方 */ }
this.dbDir = null;
this.root = null;
}
@@ -30,6 +49,23 @@ export class OPFSBackend implements IStorageBackend {
return this.dbDir !== null;
}
/** 清理崩溃残留的临时文件(open 时自动调用,repair 也可调用) */
async cleanupStaleFiles(): Promise<void> {
if (!this.dbDir) return;
try {
const dir = this.dbDir as any;
const stale: string[] = [];
for await (const [name] of dir.entries()) {
if (STALE_SUFFIXES.some((s) => name.endsWith(s))) {
stale.push(name);
}
}
for (const name of stale) {
try { await this.dbDir!.removeEntry(name); } catch { /* ignore */ }
}
} catch { /* 清理失败不阻塞 */ }
}
async read(key: string): Promise<ArrayBuffer | null> {
if (!this.dbDir) return null;
try {
@@ -41,21 +77,45 @@ export class OPFSBackend implements IStorageBackend {
}
}
/**
* 单文件原子写:createWritable 为 copy-on-writeclose 后原子替换;
* 写入期间崩溃 → 旧文件保持(原子性由浏览器 OPFS 实现保证)。
*/
async write(key: string, data: ArrayBuffer): Promise<void> {
if (!this.dbDir) return;
this.writeQueue = this.writeQueue.then(async () => {
const run = this.writeQueue.then(async () => {
const fh = await this.dbDir!.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
});
return this.writeQueue;
// v0.4.5-fix: 单次任务失败不中断队列链(错误仍返回给本次调用方)
this.writeQueue = run.then(() => undefined, () => undefined);
return run;
}
/**
* v0.4.5: 真追加写 — createWritable(keepExistingData) + seek 到文件末尾。
* 单文件 COW 原子(close 前崩溃旧文件保持),无需读旧内容即实现 O(chunk) 追加
* (WAL 分片高频写入用)。
*/
async append(key: string, data: ArrayBuffer): Promise<void> {
if (!this.dbDir) return;
const run = this.writeQueue.then(async () => {
const fh = await this.dbDir!.getFileHandle(key, { create: true });
const existing = await fh.getFile();
const writable = await fh.createWritable({ keepExistingData: true });
await writable.write({ type: 'write', position: existing.size, data });
await writable.close();
});
this.writeQueue = run.then(() => undefined, () => undefined);
return run;
}
/** v0.4.2-fix: 批量写入 — 串行队列内逐个落盘(OPFS 无跨文件事务,顺序保证一致) */
async writeMany(entries: Record<string, ArrayBuffer>): Promise<void> {
if (!this.dbDir) return;
this.writeQueue = this.writeQueue.then(async () => {
const run = this.writeQueue.then(async () => {
for (const [key, data] of Object.entries(entries)) {
const fh = await this.dbDir!.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
@@ -63,26 +123,29 @@ export class OPFSBackend implements IStorageBackend {
await writable.close();
}
});
return this.writeQueue;
this.writeQueue = run.then(() => undefined, () => undefined);
return run;
}
async delete(key: string): Promise<void> {
if (!this.dbDir) return;
this.writeQueue = this.writeQueue.then(async () => {
const run = this.writeQueue.then(async () => {
try { await this.dbDir!.removeEntry(key); } catch { /* ignore */ }
});
return this.writeQueue;
this.writeQueue = run.then(() => undefined, () => undefined);
return run;
}
/** v0.4.2-fix: 批量删除 — 串行队列内逐个删除 */
async deleteMany(keys: string[]): Promise<void> {
if (!this.dbDir) return;
this.writeQueue = this.writeQueue.then(async () => {
const run = this.writeQueue.then(async () => {
for (const key of keys) {
try { await this.dbDir!.removeEntry(key); } catch { /* ignore */ }
}
});
return this.writeQueue;
this.writeQueue = run.then(() => undefined, () => undefined);
return run;
}
async listKeys(): Promise<string[]> {
@@ -0,0 +1,95 @@
/**
* AriaEngine Page SSTable Store — SSTable 页面化物理存储
* @module engine/aria/store/page_sstable_store
*
* v0.4.5: 让 BufferPool/FileManager 真正接入 LSM 读写路径。
* SSTable 不再整体存为一个 backend value,而是切分为 4KB 页面:
* - 页面由 FileManager 分配 pageId,经 BufferPool 缓存(LRU 驱逐,脏页写回)
* - save 语义 = 数据已落盘:页面写入后逐个 flushPage(await 底层写)才返回,
* 保证 WAL checkpoint(截断)前 SSTable 数据真实持久化
* - 页面 ID 列表经 SSTableMeta.pageIds 持久化;旧 meta(无 pageIds)走整 value 读取
*
* 与 LSM 的 sstableCache(整文件 LRU)双层缓存并存:
* - BufferPool 缓存物理页面(跨 SSTable 共享、受 bufferPoolPages 上限约束)
* - LSM 缓存解析后的整文件字节(查询热点复用)
*/
import type { FileManager } from './file_manager';
import type { BufferPool } from '../buffer/pool';
import type { PageHandle } from '../types';
import { PAGE_SIZE, PageType } from '../types';
export class PageSSTableStore {
/** SSTable id → 页面 ID 列表(save 时记录,saveMeta 时注入 meta */
private pageIds = new Map<number, number[]>();
constructor(
private fileManager: FileManager,
private bufferPool: BufferPool,
) {}
/** 保存数据:切页 → 写入 BufferPool → 逐页落盘 → 记录 pageIds */
async save(id: number, data: Uint8Array): Promise<void> {
const pageCount = Math.max(1, Math.ceil(data.byteLength / PAGE_SIZE));
const handles: PageHandle[] = await this.bufferPool.newPages(pageCount, PageType.DATA);
const ids: number[] = [];
for (let i = 0; i < pageCount; i++) {
const page = handles[i];
ids.push(page.pageId);
const dest = new Uint8Array(page.data);
dest.fill(0); // 清空(最后一页可能不满)
const slice = data.subarray(i * PAGE_SIZE, Math.min((i + 1) * PAGE_SIZE, data.byteLength));
dest.set(slice, 0);
page.dirty = true;
// save 语义 = 已持久化:立即落盘(WAL checkpoint 截断依赖此保证)
await this.bufferPool.flushPage(page.pageId);
this.bufferPool.unpin(page);
}
this.pageIds.set(id, ids);
}
/** 获取指定 SSTable 的页面 ID 列表(saveMeta 注入用) */
getPageIds(id: number): number[] | undefined {
return this.pageIds.get(id);
}
/**
* 按页面 ID 列表读取并拼接为完整字节流。
* @param totalSize SSTable 真实大小(meta 持久化)——最后一页可能有 0 填充,按真实大小截断
* @returns 缺失页面/读取失败返回 null(调用方视为损坏并清理)
*/
async load(id: number, pageIds: number[], totalSize: number): Promise<Uint8Array | null> {
if (pageIds.length === 0) return null;
const chunks: Uint8Array[] = [];
for (const pageId of pageIds) {
const page = await this.bufferPool.getPage(pageId);
if (!page) return null;
// 立即复制(后续驱逐安全)
chunks.push(new Uint8Array(page.data));
this.bufferPool.unpin(page);
}
const total = chunks.reduce((s, c) => s + c.byteLength, 0);
const out = new Uint8Array(Math.min(total, totalSize));
let off = 0;
for (const c of chunks) {
const take = Math.min(c.byteLength, out.byteLength - off);
if (take <= 0) break;
out.set(c.subarray(0, take), off);
off += take;
}
this.pageIds.delete(id);
return out;
}
/** 释放页面(删除物理页面文件 + 移出 BufferPool */
async delete(id: number, pageIds: number[]): Promise<void> {
for (const pageId of pageIds) {
this.bufferPool.removePage(pageId);
try {
await this.fileManager.freePageId(pageId);
} catch { /* 清理失败不阻塞 */ }
}
this.pageIds.delete(id);
}
}
+181 -284
View File
@@ -1,284 +1,181 @@
/**
* AriaEngine MVCC — 多版本并发控制
* @module engine/aria/transaction/mvcc
*
* 实现快照隔离 (Snapshot Isolation)。
* 每个事务看到数据库在事务开始时的快照。
*/
import type { RowVersion, TxnEntry } from '../types';
import { TransactionState } from '../types';
// ---------------------------------------------------------------------------
// MVCCManager
// ---------------------------------------------------------------------------
export class MVCCManager {
/** 所有行版本的存储:tableName.key → 版本链 */
private versionStore: Map<string, RowVersion[]> = new Map();
/** 活跃事务表:txnId → TxnEntry */
private activeTxns: Map<number, TxnEntry> = new Map();
/** 事务 ID 计数器 */
private nextTxnId = 1;
/** 全局提交序列号(用于可见性判断) */
private globalCommitLsn = 0;
/**
* v0.4.2-fix: 每个事务写入的 tableKey 集合 —
* commit/rollback 只遍历本事务写过的 key,避免全库版本链扫描(大表事务 O(N) → O(写入数))
*/
private txnWriteKeys: Map<number, Set<string>> = new Map();
// =======================================================================
// 事务管理
// =======================================================================
/** 开始一个事务,返回事务 ID */
beginTransaction(): number {
const txnId = this.nextTxnId++;
this.activeTxns.set(txnId, {
txnId,
state: TransactionState.ACTIVE,
snapshotLsn: this.globalCommitLsn,
startTime: Date.now(),
});
this.txnWriteKeys.set(txnId, new Set());
return txnId;
}
/** 提交事务 */
commitTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.COMMITTED;
this.globalCommitLsn++;
// v0.4.2-fix: 仅标记本事务写入的版本(此前遍历全库 versionStore
const writeKeys = this.txnWriteKeys.get(txnId);
if (writeKeys) {
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions) continue;
for (const version of versions) {
if (version.txnId === txnId) {
version.committed = true;
}
}
}
}
// 清理已提交事务的记录
this.activeTxns.delete(txnId);
this.txnWriteKeys.delete(txnId);
}
/** 回滚事务 */
rollbackTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.ABORTED;
// v0.4.2-fix: 仅移除本事务写入的版本(此前遍历全库 versionStore
const writeKeys = this.txnWriteKeys.get(txnId);
if (writeKeys) {
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions) continue;
const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) {
this.versionStore.delete(tableKey);
} else {
this.versionStore.set(tableKey, filtered);
}
}
}
this.activeTxns.delete(txnId);
this.txnWriteKeys.delete(txnId);
}
/** 检查事务是否活跃 */
isActive(txnId: number): boolean {
const txn = this.activeTxns.get(txnId);
return txn !== undefined && txn.state === TransactionState.ACTIVE;
}
// =======================================================================
// 版本读写
// =======================================================================
/**
* 写入一行(创建新版本)。
*/
writeVersion(
tableName: string,
key: string,
data: Record<string, unknown>,
txnId: number,
): void {
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey) ?? [];
const newVersion: RowVersion = {
txnId,
data,
prevVersion: versions.length > 0 ? versions[versions.length - 1] : null,
committed: false,
};
versions.push(newVersion);
this.versionStore.set(tableKey, versions);
// v0.4.2-fix: 记录本事务写过的 keycommit/rollback 精准清理)
this.txnWriteKeys.get(txnId)?.add(tableKey);
}
/**
* 读取一行(对指定事务可见的最新版本)。
*/
readVersion(
tableName: string,
key: string,
txnId: number,
): Record<string, unknown> | null {
const txn = this.activeTxns.get(txnId);
if (!txn) return null;
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey);
if (!versions || versions.length === 0) return null;
// 从最新版本向前遍历
for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i];
// 1. 如果是当前事务写入的(未提交),可见
if (version.txnId === txnId) {
return version.data;
}
// 2. 如果是已提交的版本,且在快照 LSN 之前提交,可见
if (version.committed) {
// 简化:所有已提交版本都可见
return version.data;
}
// 3. 其他事务的未提交版本,不可见,继续找更早的版本
}
return null;
}
/**
* 删除一行(创建墓碑版本)。
*/
deleteVersion(tableName: string, key: string, txnId: number): void {
this.writeVersion(tableName, key, { __mvcc_tombstone: true } as unknown as Record<string, unknown>, txnId);
}
/**
* 获取所有行的最新已提交版本(用于非事务读取)。
*/
getLatestCommittedVersions(
tableName: string,
): Record<string, Record<string, unknown>> {
const result: Record<string, Record<string, unknown>> = {};
for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(`${tableName}.`)) continue;
const key = tableKey.slice(tableName.length + 1);
for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i];
if (version.committed && !(version.data as unknown as Record<string, unknown>).__mvcc_tombstone) {
result[key] = version.data;
break;
}
}
}
return result;
}
/**
* v0.3.3: 丢弃指定事务的所有版本记录,但保留事务登记(Savepoint 回滚用)。
* 快照数据由调用方(引擎 txnSnapshot)负责恢复。
* v0.4.2-fix: 仅遍历本事务写过的 key(此前全库扫描)。
*/
discardVersions(txnId: number): void {
const writeKeys = this.txnWriteKeys.get(txnId);
if (!writeKeys) return;
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions) continue;
const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) {
this.versionStore.delete(tableKey);
} else {
this.versionStore.set(tableKey, filtered);
}
}
}
/**
* 清理过旧版本(GC)。
* 保留每个 key 的最新 N 个已提交版本。
*/
gc(maxVersionsPerKey: number = 100): void {
for (const [tableKey, versions] of this.versionStore) {
if (versions.length <= maxVersionsPerKey) continue;
// 保留最新的 maxVersionsPerKey 个版本
const pruned = versions.slice(versions.length - maxVersionsPerKey);
this.versionStore.set(tableKey, pruned);
}
}
/**
* 获取所有未提交事务中的 key 列表。
*/
getActiveWriteKeys(tableName: string, txnId: number): Set<string> {
const keys = new Set<string>();
const prefix = `${tableName}.`;
for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(prefix)) continue;
const latestVersion = versions[versions.length - 1];
if (latestVersion.txnId === txnId && !latestVersion.committed) {
keys.add(tableKey.slice(prefix.length));
}
}
return keys;
}
/**
* 清理指定表的所有版本。
*/
clearTable(tableName: string): void {
const prefix = `${tableName}.`;
for (const [tableKey] of this.versionStore) {
if (tableKey.startsWith(prefix)) {
this.versionStore.delete(tableKey);
}
}
}
/**
* 获取活跃事务数。
*/
getActiveTxnCount(): number {
return this.activeTxns.size;
}
/**
* 获取全局 LSN。
*/
getGlobalLSN(): number {
return this.globalCommitLsn;
}
}
/**
* AriaEngine MVCC — 多版本并发控制
* @module engine/aria/transaction/mvcc
*
* 实现快照隔离 (Snapshot Isolation)。
* 每个事务看到数据库在事务开始时的快照。
*/
import type { RowVersion, TxnEntry } from '../types';
import { TransactionState } from '../types';
// ---------------------------------------------------------------------------
// MVCCManager
// ---------------------------------------------------------------------------
export class MVCCManager {
/** 所有行版本的存储:tableName.key → 版本链 */
private versionStore: Map<string, RowVersion[]> = new Map();
/** 活跃事务表:txnId → TxnEntry */
private activeTxns: Map<number, TxnEntry> = new Map();
/** 事务 ID 计数器 */
private nextTxnId = 1;
/** 全局提交序列号(用于可见性判断) */
private globalCommitLsn = 0;
/**
* v0.4.2-fix: 每个事务写入的 tableKey 集合 —
* commit/rollback 只遍历本事务写过的 key,避免全库版本链扫描(大表事务 O(N) → O(写入数))
*/
private txnWriteKeys: Map<number, Set<string>> = new Map();
// =======================================================================
// 事务管理
// =======================================================================
/** 开始一个事务,返回事务 ID */
beginTransaction(): number {
const txnId = this.nextTxnId++;
this.activeTxns.set(txnId, {
txnId,
state: TransactionState.ACTIVE,
snapshotLsn: this.globalCommitLsn,
startTime: Date.now(),
});
this.txnWriteKeys.set(txnId, new Set());
return txnId;
}
/** 提交事务 */
commitTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.COMMITTED;
this.globalCommitLsn++;
// v0.4.2-fix: 仅标记本事务写入的版本(此前遍历全库 versionStore
const writeKeys = this.txnWriteKeys.get(txnId);
if (writeKeys) {
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions) continue;
for (const version of versions) {
if (version.txnId === txnId) {
version.committed = true;
}
}
}
}
// 清理已提交事务的记录
this.activeTxns.delete(txnId);
this.txnWriteKeys.delete(txnId);
}
/** 回滚事务 */
rollbackTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.ABORTED;
// v0.4.2-fix: 仅移除本事务写入的版本(此前遍历全库 versionStore
const writeKeys = this.txnWriteKeys.get(txnId);
if (writeKeys) {
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions) continue;
const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) {
this.versionStore.delete(tableKey);
} else {
this.versionStore.set(tableKey, filtered);
}
}
}
this.activeTxns.delete(txnId);
this.txnWriteKeys.delete(txnId);
}
// =======================================================================
// 版本读写
// =======================================================================
/**
* 写入一行(创建新版本)。
*/
writeVersion(
tableName: string,
key: string,
data: Record<string, unknown>,
txnId: number,
): void {
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey) ?? [];
const newVersion: RowVersion = {
txnId,
data,
prevVersion: versions.length > 0 ? versions[versions.length - 1] : null,
committed: false,
};
versions.push(newVersion);
this.versionStore.set(tableKey, versions);
// v0.4.2-fix: 记录本事务写过的 keycommit/rollback 精准清理)
this.txnWriteKeys.get(txnId)?.add(tableKey);
}
/**
* 删除一行(创建墓碑版本)。
*/
deleteVersion(tableName: string, key: string, txnId: number): void {
this.writeVersion(tableName, key, { __mvcc_tombstone: true } as unknown as Record<string, unknown>, txnId);
}
/**
* v0.3.3: 丢弃指定事务的所有版本记录,但保留事务登记(Savepoint 回滚用)。
* 快照数据由调用方(引擎 txnSnapshot)负责恢复。
* v0.4.2-fix: 仅遍历本事务写过的 key(此前全库扫描)。
*/
discardVersions(txnId: number): void {
const writeKeys = this.txnWriteKeys.get(txnId);
if (!writeKeys) return;
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions) continue;
const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) {
this.versionStore.delete(tableKey);
} else {
this.versionStore.set(tableKey, filtered);
}
}
}
/**
* 清理过旧版本(GC)。
* 保留每个 key 的最新 N 个已提交版本。
*/
gc(maxVersionsPerKey: number = 100): void {
for (const [tableKey, versions] of this.versionStore) {
if (versions.length <= maxVersionsPerKey) continue;
// 保留最新的 maxVersionsPerKey 个版本
const pruned = versions.slice(versions.length - maxVersionsPerKey);
this.versionStore.set(tableKey, pruned);
}
}
/**
* 获取全局 LSN。
*/
getGlobalLSN(): number {
return this.globalCommitLsn;
}
}
+249 -302
View File
@@ -1,302 +1,249 @@
/**
* AriaEngine Types — 内部类型定义
* @module engine/aria/types
*
* 页面式存储引擎的所有内部枚举、接口和常量。
*/
// =============================================================================
// 页面常量
// =============================================================================
/** 页面大小:4KB */
export const PAGE_SIZE = 4096;
/** 页面头大小:16 字节 */
export const PAGE_HEADER_SIZE = 16;
/** 每个 Slot 目录项大小:4 字节 (offset: u16 + len: u16) */
export const SLOT_ENTRY_SIZE = 4;
/** 页面数据区起始偏移(头部之后) */
export const PAGE_DATA_START = PAGE_HEADER_SIZE;
/** 无效页面 ID */
export const INVALID_PAGE_ID = 0xFFFFFFFF;
// =============================================================================
// 页面类型
// =============================================================================
export enum PageType {
/** 数据页面:存储行数据 */
DATA = 1,
/** 索引页面:存储索引节点 */
INDEX = 2,
/** 溢出页面:存储大字段 */
OVERFLOW = 3,
/** 元数据页面:存储表/库元信息 */
META = 4,
}
// =============================================================================
// 页面头部(16 字节)
// =============================================================================
export interface PageHeader {
/** 页面 ID(全局唯一) */
pageId: number;
/** 页面类型 */
type: PageType;
/** 空闲空间起始偏移(slot 区结束位置) */
freeStart: number;
/** 数据区结束偏移(从页面底部向上增长) */
freeEnd: number;
/** 当前 slot 数量 */
slotCount: number;
/** CRC32 校验和 */
checksum: number;
}
// =============================================================================
// Slot 目录项(4 字节)
// =============================================================================
export interface SlotEntry {
/** 行数据在页面内的偏移 */
offset: number;
/** 行数据长度(字节) */
length: number;
}
// =============================================================================
// 页面句柄(Buffer Pool 中的页面)
// =============================================================================
export interface PageHandle {
/** 页面 ID */
pageId: number;
/** 页面类型 */
type: PageType;
/** 页面数据缓冲区(4KB ArrayBuffer */
data: ArrayBuffer;
/** 是否被修改(脏页) */
dirty: boolean;
/** 引用计数(pin count */
pins: number;
/** LRU 链表前驱 */
prev: PageHandle | null;
/** LRU 链表后继 */
next: PageHandle | null;
/** 最后访问时间戳 */
lastAccess: number;
}
// =============================================================================
// 行编解码
// =============================================================================
/** 行/元组的二进制表示 */
export interface SerializedTuple {
/** 序列化后的字节数组 */
bytes: Uint8Array;
/** 该行中 null 列的位图 */
nullBitmap: Uint8Array;
}
/** 列类型(内部二进制编码用) */
export enum ColumnEncoding {
STRING = 1,
NUMBER = 2,
BOOLEAN = 3,
DATE = 4,
JSON = 5,
NULL = 6,
}
// =============================================================================
// LSM-Tree
// =============================================================================
/** MemTable 最大大小(默认 4MB */
export const DEFAULT_MEMTABLE_SIZE = 4 * 1024 * 1024;
/** SSTable 中每个 Data Block 的默认大小 */
export const DEFAULT_BLOCK_SIZE = 4096;
/** Bloom Filter 每 key 的默认位数 */
export const DEFAULT_BLOOM_BITS_PER_KEY = 10;
/** SSTable 最大层级 */
export const MAX_LSM_LEVELS = 7;
/** 每层之间的大小倍数 */
export const DEFAULT_LEVEL_SIZE_MULTIPLIER = 10;
/** SSTable 元数据 */
export interface SSTableMeta {
/** SSTable 文件 ID */
id: number;
/** 所在层级 */
level: number;
/** 最小 key */
minKey: string;
/** 最大 key */
maxKey: string;
/** 数据块数量 */
blockCount: number;
/** 总大小(字节) */
totalSize: number;
/** Bloom Filter 序列化数据 */
bloomData: Uint8Array | null;
}
/** SSTable 内部的 Data Block */
export interface DataBlock {
/** 该块内的 key-value 条目数 */
entryCount: number;
/** 该块数据区 */
data: Uint8Array;
/** 该块起始 key */
startKey: string;
/** 该块结束 key */
endKey: string;
}
/** 索引块条目:key → data block offset */
export interface IndexEntry {
/** 到此 block 的最后一个 key */
key: string;
/** data block 在 SSTable 文件中的偏移 */
blockOffset: number;
/** data block 大小 */
blockSize: number;
}
// =============================================================================
// WAL (Write-Ahead Log)
// =============================================================================
/** WAL 记录类型 */
export enum WALRecordType {
INSERT = 1,
UPDATE = 2,
DELETE = 3,
BEGIN = 4,
COMMIT = 5,
ROLLBACK = 6,
CREATE_TABLE = 7,
DROP_TABLE = 8,
}
/** 单条 WAL 记录 */
export interface WALRecord {
/** 日志序列号 */
lsn: number;
/** 记录类型 */
type: WALRecordType;
/** 事务 ID */
txnId: number;
/** 表名 */
tableName: string;
/** 主键值 */
key: string;
/** 操作数据(INSERT/UPDATE 时有效) */
data?: Record<string, unknown>;
/** 校验和 */
checksum: number;
}
// =============================================================================
// MVCC
// =============================================================================
/** 事务隔离级别 */
export enum IsolationLevel {
READ_COMMITTED = 1,
SNAPSHOT = 2,
}
/** 事务状态 */
export enum TransactionState {
ACTIVE = 1,
COMMITTED = 2,
ABORTED = 3,
}
/** 行版本 */
export interface RowVersion {
/** 事务 ID(创建此版本的事务) */
txnId: number;
/** 版本数据 */
data: Record<string, unknown>;
/** 指向上一版本的指针(undo 链) */
prevVersion: RowVersion | null;
/** 该版本是否已提交 */
committed: boolean;
}
/** 活跃事务表项 */
export interface TxnEntry {
/** 事务 ID */
txnId: number;
/** 事务状态 */
state: TransactionState;
/** 快照序列号(用于可见性判断) */
snapshotLsn: number;
/** 事务开始时间 */
startTime: number;
}
// =============================================================================
// Buffer Pool
// =============================================================================
/** Buffer Pool 默认容量:256 页 ≈ 1MB */
export const DEFAULT_BUFFER_POOL_PAGES = 256;
// =============================================================================
// AriaEngine 配置
// =============================================================================
export interface AriaEngineConfig {
/** 页面大小(默认 4096 */
pageSize?: number;
/** Buffer Pool 页面数量(默认 256 */
bufferPoolPages?: number;
/** MemTable 刷盘阈值(默认 4MB */
memtableSizeThreshold?: number;
/** LSM 层级之间的容量倍数(默认 10) */
levelSizeMultiplier?: number;
/** Bloom Filter 每 key 位数(默认 10 */
bloomFilterBitsPerKey?: number;
/** 是否启用 WAL(默认 true */
walEnabled?: boolean;
/** WAL 同步模式 */
walSyncMode?: 'full' | 'batch' | 'none';
/** Checkpoint 间隔(操作数,默认 1000 */
checkpointInterval?: number;
/** 是否启用页面压缩(默认 false) */
compression?: boolean;
/** 存储后端 */
storageBackend?: 'indexeddb' | 'opfs' | 'memory';
/** WAL 大小阈值(字节,超过则强制 checkpoint,默认 16MB */
walSizeThreshold?: number;
/** 最大内存预算(MB,默认 64) */
maxMemoryMB?: number;
}
export const DEFAULT_ARIA_CONFIG: Required<AriaEngineConfig> = {
pageSize: PAGE_SIZE,
bufferPoolPages: DEFAULT_BUFFER_POOL_PAGES,
memtableSizeThreshold: DEFAULT_MEMTABLE_SIZE,
levelSizeMultiplier: DEFAULT_LEVEL_SIZE_MULTIPLIER,
bloomFilterBitsPerKey: DEFAULT_BLOOM_BITS_PER_KEY,
walEnabled: true,
walSyncMode: 'full',
checkpointInterval: 1000,
compression: false,
storageBackend: 'indexeddb',
walSizeThreshold: 16 * 1024 * 1024, // 16MB
maxMemoryMB: 64,
};
/**
* AriaEngine Types — 内部类型定义
* @module engine/aria/types
*
* 页面式存储引擎的所有内部枚举、接口和常量。
*/
// =============================================================================
// 页面常量
// =============================================================================
/** 页面大小:4KB */
export const PAGE_SIZE = 4096;
/** 页面头大小:16 字节 */
export const PAGE_HEADER_SIZE = 16;
// =============================================================================
// 页面类型
// =============================================================================
export enum PageType {
/** 数据页面:存储 SSTable 字节切片 */
DATA = 1,
/** 索引页面:存储索引节点 */
INDEX = 2,
/** 溢出页面:存储大字段 */
OVERFLOW = 3,
/** 元数据页面:存储表/库元信息 */
META = 4,
}
// =============================================================================
// 页面句柄(Buffer Pool 中的页面)
// =============================================================================
export interface PageHandle {
/** 页面 ID */
pageId: number;
/** 页面类型 */
type: PageType;
/** 页面数据缓冲区(4KB ArrayBuffer */
data: ArrayBuffer;
/** 是否被修改(脏页) */
dirty: boolean;
/** 引用计数(pin count */
pins: number;
/** LRU 链表前驱 */
prev: PageHandle | null;
/** LRU 链表后继 */
next: PageHandle | null;
/** 最后访问时间戳 */
lastAccess: number;
}
// =============================================================================
// 列类型(内部二进制编码用)
// =============================================================================
// =============================================================================
// LSM-Tree
// =============================================================================
/** MemTable 最大大小(默认 4MB */
export const DEFAULT_MEMTABLE_SIZE = 4 * 1024 * 1024;
/** Bloom Filter 每 key 的默认位数 */
export const DEFAULT_BLOOM_BITS_PER_KEY = 10;
/** SSTable 最大层级 */
export const MAX_LSM_LEVELS = 7;
/** 每层之间的大小倍数 */
export const DEFAULT_LEVEL_SIZE_MULTIPLIER = 10;
/** SSTable 元数据 */
export interface SSTableMeta {
/** SSTable 文件 ID */
id: number;
/** 所在层级 */
level: number;
/** 最小 key */
minKey: string;
/** 最大 key */
maxKey: string;
/** 数据块数量 */
blockCount: number;
/** 总大小(字节) */
totalSize: number;
/** Bloom Filter 序列化数据 */
bloomData: Uint8Array | null;
/**
* v0.4.5: 页面化存储(OPFS 后端)— SSTable 的物理页面 ID 列表。
* 存在 → 数据以 4KB 页面存储(BufferPool/FileManager 管理);
* 缺失 → 整 value 存储(旧格式,兼容读取)。
*/
pageIds?: number[];
}
/** 索引块条目:key → data block offset */
export interface IndexEntry {
/** 到此 block 的最后一个 key */
key: string;
/** data block 在 SSTable 文件中的偏移 */
blockOffset: number;
/** data block 大小 */
blockSize: number;
}
// =============================================================================
// WAL (Write-Ahead Log)
// =============================================================================
/** WAL 记录类型 */
export enum WALRecordType {
INSERT = 1,
UPDATE = 2,
DELETE = 3,
BEGIN = 4,
COMMIT = 5,
ROLLBACK = 6,
CREATE_TABLE = 7,
DROP_TABLE = 8,
}
/** 单条 WAL 记录 */
export interface WALRecord {
/** 日志序列号 */
lsn: number;
/** 记录类型 */
type: WALRecordType;
/** 事务 ID */
txnId: number;
/** 表名 */
tableName: string;
/** 主键值 */
key: string;
/** 操作数据(INSERT/UPDATE 时有效) */
data?: Record<string, unknown>;
/** 校验和 */
checksum: number;
}
// =============================================================================
// MVCC
// =============================================================================
/** 事务状态 */
export enum TransactionState {
ACTIVE = 1,
COMMITTED = 2,
ABORTED = 3,
}
/** 行版本 */
export interface RowVersion {
/** 事务 ID(创建此版本的事务) */
txnId: number;
/** 版本数据 */
data: Record<string, unknown>;
/** 指向上一版本的指针(undo 链) */
prevVersion: RowVersion | null;
/** 该版本是否已提交 */
committed: boolean;
}
/** 活跃事务表项 */
export interface TxnEntry {
/** 事务 ID */
txnId: number;
/** 事务状态 */
state: TransactionState;
/** 快照序列号(用于可见性判断) */
snapshotLsn: number;
/** 事务开始时间 */
startTime: number;
}
// =============================================================================
// Buffer Pool
// =============================================================================
/** Buffer Pool 默认容量:256 页 ≈ 1MB */
export const DEFAULT_BUFFER_POOL_PAGES = 256;
// =============================================================================
// AriaEngine 配置
// =============================================================================
export interface AriaEngineConfig {
/** 页面大小(默认 4096 */
pageSize?: number;
/** Buffer Pool 页面数量(默认 256 */
bufferPoolPages?: number;
/** MemTable 刷盘阈值(默认 4MB */
memtableSizeThreshold?: number;
/** LSM 层级之间的容量倍数(默认 10) */
levelSizeMultiplier?: number;
/** Bloom Filter 每 key 位数(默认 10 */
bloomFilterBitsPerKey?: number;
/** 是否启用 WAL(默认 true */
walEnabled?: boolean;
/** WAL 同步模式 */
walSyncMode?: 'full' | 'batch' | 'none';
/** Checkpoint 间隔(操作数,默认 1000 */
checkpointInterval?: number;
/** 是否启用页面压缩(默认 false) */
compression?: boolean;
/** 存储后端 */
storageBackend?: 'indexeddb' | 'opfs' | 'memory';
/** WAL 大小阈值(字节,超过则强制 checkpoint,默认 16MB */
walSizeThreshold?: number;
/** 最大内存预算(MB,默认 64) */
maxMemoryMB?: number;
/**
* v0.4.5: 全库 AES-256-GCM 加密(backend 层透明加解密,WAL/SSTable/Schema/元数据全覆盖)。
* 密钥由 PBKDF2salt 持久化于库内 __aria_keymeta)派生,重启用同一密码即可解密。
*/
encryption?: {
/** 加密密码 */
password: string;
};
/**
* v0.4.5: SSTable 页面化存储(4KB 页面 + BufferPool/FileManager 管理,LRU 缓存)。
* 默认:storageBackend === 'opfs' 时自动启用(大文件随机读/写放大优化);
* 显式 false 强制关闭(整 value 存储,兼容旧行为)。
*/
pageStorage?: boolean;
}
export const DEFAULT_ARIA_CONFIG: Required<Omit<AriaEngineConfig, 'encryption' | 'pageStorage'>> & {
encryption: undefined;
pageStorage: undefined;
} = {
pageSize: PAGE_SIZE,
bufferPoolPages: DEFAULT_BUFFER_POOL_PAGES,
memtableSizeThreshold: DEFAULT_MEMTABLE_SIZE,
levelSizeMultiplier: DEFAULT_LEVEL_SIZE_MULTIPLIER,
bloomFilterBitsPerKey: DEFAULT_BLOOM_BITS_PER_KEY,
walEnabled: true,
walSyncMode: 'full',
checkpointInterval: 1000,
compression: false,
storageBackend: 'indexeddb',
walSizeThreshold: 16 * 1024 * 1024, // 16MB
maxMemoryMB: 64,
encryption: undefined,
pageStorage: undefined,
};
+71 -82
View File
@@ -1,82 +1,71 @@
/**
* AriaEngine Checkpoint — 检查点机制
* @module engine/aria/wal/checkpoint
*/
import type { LSM } from '../index/lsm';
import type { WAL } from './log';
// ---------------------------------------------------------------------------
// 简化的 flush 接口
// ---------------------------------------------------------------------------
export interface Flushable {
flushAll(): Promise<void>;
}
// ---------------------------------------------------------------------------
// CheckpointManager
// ---------------------------------------------------------------------------
export class CheckpointManager {
private lsm: LSM;
private wal: WAL;
private flushable: Flushable | null;
private interval: number;
private opCount = 0;
private walSizeThreshold: number;
constructor(
lsm: LSM,
wal: WAL,
flushable: Flushable | null = null,
interval: number = 1000,
walSizeThreshold: number = 16 * 1024 * 1024,
) {
this.lsm = lsm;
this.wal = wal;
this.flushable = flushable;
this.interval = interval;
this.walSizeThreshold = walSizeThreshold;
}
async tick(): Promise<void> {
this.opCount++;
// 检查操作计数或 WAL 大小是否超阈值
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
await this.checkpoint();
}
}
/** 估算 WAL 大小(优先真实字节数,回退到缓冲计数估算) */
private getWALEstimatedSize(): number {
const wal = this.wal as unknown as { getBufferedBytes?: () => number; getBufferedCount?: () => number };
if (typeof wal.getBufferedBytes === 'function') {
const bytes = wal.getBufferedBytes();
if (bytes > 0) return bytes;
}
const count = typeof wal.getBufferedCount === 'function' ? wal.getBufferedCount() : 0;
return count * 200;
}
async checkpoint(): Promise<void> {
await this.lsm.flush();
if (this.flushable) {
await this.flushable.flushAll();
}
await this.wal.checkpoint();
this.opCount = 0;
}
async forceCheckpoint(): Promise<void> {
await this.checkpoint();
}
setInterval(ops: number): void {
this.interval = ops;
}
getOpCount(): number {
return this.opCount;
}
}
/**
* AriaEngine Checkpoint — 检查点机制
* @module engine/aria/wal/checkpoint
*/
import type { LSM } from '../index/lsm';
import type { WAL } from './log';
// ---------------------------------------------------------------------------
// 简化的 flush 接口
// ---------------------------------------------------------------------------
export interface Flushable {
flushAll(): Promise<void>;
}
// ---------------------------------------------------------------------------
// CheckpointManager
// ---------------------------------------------------------------------------
export class CheckpointManager {
private lsm: LSM;
private wal: WAL;
private flushable: Flushable | null;
private interval: number;
private opCount = 0;
private walSizeThreshold: number;
constructor(
lsm: LSM,
wal: WAL,
flushable: Flushable | null = null,
interval: number = 1000,
walSizeThreshold: number = 16 * 1024 * 1024,
) {
this.lsm = lsm;
this.wal = wal;
this.flushable = flushable;
this.interval = interval;
this.walSizeThreshold = walSizeThreshold;
}
async tick(): Promise<void> {
this.opCount++;
// 检查操作计数或 WAL 大小是否超阈值
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
await this.checkpoint();
}
}
/** 估算 WAL 大小(优先真实字节数,回退到缓冲计数估算) */
private getWALEstimatedSize(): number {
const wal = this.wal as unknown as { getBufferedBytes?: () => number; getBufferedCount?: () => number };
if (typeof wal.getBufferedBytes === 'function') {
const bytes = wal.getBufferedBytes();
if (bytes > 0) return bytes;
}
const count = typeof wal.getBufferedCount === 'function' ? wal.getBufferedCount() : 0;
return count * 200;
}
async checkpoint(): Promise<void> {
await this.lsm.flush();
if (this.flushable) {
await this.flushable.flushAll();
}
await this.wal.checkpoint();
this.opCount = 0;
}
}
+322 -325
View File
@@ -1,325 +1,322 @@
/**
* AriaEngine WAL — Write-Ahead Log
* @module engine/aria/wal/log
*
* 崩溃恢复前的写操作持久化日志。
*
* WAL 文件格式:
* ┌──────────┬──────────────┬──────────┐
* │ Record 1│ Record 2 │ ... │
* │ 4B LSN │ │ │
* │ 1B type │ │ │
* │ 4B txnId│ │ │
* │ 2B tblLen│ │ │
* │ N table│ │ │
* │ 2B keyLen│ │ │
* │ N key │ │ │
* │ 4B jsonLen│ │ │
* │ N json │ │ │
* │ 4B CRC │ │ │
* └──────────┴──────────────┴──────────┘
*/
import { WALRecordType, type WALRecord } from '../types';
// ---------------------------------------------------------------------------
// WAL 存储接口
// ---------------------------------------------------------------------------
export interface WALStore {
/** 追加 WAL 记录 */
append(data: Uint8Array): Promise<void>;
/** 读取所有 WAL 记录 */
readAll(): Promise<Uint8Array>;
/** 截断 WALcheckpoint 后清理) */
truncate(): Promise<void>;
/** 检查 WAL 是否存在 */
exists(): Promise<boolean>;
}
// ---------------------------------------------------------------------------
// WAL
// ---------------------------------------------------------------------------
export class WAL {
private lsn = 0;
private store: WALStore;
private enabled: boolean;
private buffer: Uint8Array[] = [];
private syncMode: 'full' | 'batch' | 'none';
/** v0.3.3: 未 checkpoint 的 WAL 累计字节数(full/batch/none 通用) */
private bufferedBytes = 0;
constructor(store: WALStore, enabled: boolean = true, syncMode: 'full' | 'batch' | 'none' = 'batch') {
this.store = store;
this.enabled = enabled;
this.syncMode = syncMode;
}
// =======================================================================
// 写入
// =======================================================================
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
async append(record: Omit<WALRecord, 'lsn' | 'checksum'>): Promise<void> {
if (!this.enabled) return;
this.lsn++;
const fullRecord: WALRecord = {
...record,
lsn: this.lsn,
checksum: 0, // 稍后计算
};
const bytes = this.encodeRecord(fullRecord);
if (this.syncMode === 'full') {
try {
await this.store.append(bytes);
this.bufferedBytes += bytes.byteLength;
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(bytes);
this.bufferedBytes += bytes.byteLength;
}
// 'none' mode: 不写 WAL
}
/** 批量追加多条 WAL 记录(组提交:合并为一次底层写入,v0.3.1) */
async appendBatch(records: Omit<WALRecord, 'lsn' | 'checksum'>[]): Promise<void> {
if (!this.enabled || records.length === 0) return;
const chunks: Uint8Array[] = [];
for (const record of records) {
this.lsn++;
chunks.push(this.encodeRecord({ ...record, lsn: this.lsn, checksum: 0 }));
}
const combined = this.mergeChunks(chunks);
if (this.syncMode === 'full') {
try {
await this.store.append(combined);
this.bufferedBytes += combined.byteLength;
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append batch record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(combined);
this.bufferedBytes += combined.byteLength;
}
// 'none' mode: 不写 WAL
}
/** 批量刷新缓冲的 WAL 记录 */
async flush(): Promise<void> {
if (!this.enabled || this.buffer.length === 0) return;
const combined = this.mergeChunks(this.buffer);
await this.store.append(combined);
this.buffer = [];
}
/** 合并多个字节块为一个连续缓冲区 */
private mergeChunks(chunks: Uint8Array[]): Uint8Array {
if (chunks.length === 1) return chunks[0];
const totalLen = chunks.reduce((sum, b) => sum + b.byteLength, 0);
const combined = new Uint8Array(totalLen);
let offset = 0;
for (const buf of chunks) {
combined.set(buf, offset);
offset += buf.byteLength;
}
return combined;
}
// =======================================================================
// 恢复
// =======================================================================
/** 从 WAL 恢复未提交的事务数据 */
async recover(
applyRecord: (record: WALRecord) => void,
): Promise<number> {
if (!this.enabled) return 0;
const exists = await this.store.exists();
if (!exists) return 0;
const data = await this.store.readAll();
if (data.byteLength === 0) return 0;
const records = this.decodeAllRecords(data);
for (const record of records) {
applyRecord(record);
}
this.lsn = records.length > 0 ? records[records.length - 1].lsn : 0;
return records.length;
}
// =======================================================================
// Checkpoint
// =======================================================================
/** Checkpoint 后清空 WAL */
async checkpoint(): Promise<void> {
if (!this.enabled) return;
await this.flush();
await this.store.truncate();
this.lsn = 0;
this.bufferedBytes = 0;
}
// =======================================================================
// 统计
// =======================================================================
isEnabled(): boolean {
return this.enabled;
}
getLSN(): number {
return this.lsn;
}
getBufferedCount(): number {
return this.buffer.length;
}
/** v0.3.3: 未 checkpoint 的 WAL 累计字节数(full/batch/none 通用) */
getBufferedBytes(): number {
return this.bufferedBytes;
}
// -----------------------------------------------------------------------
// 编解码
// -----------------------------------------------------------------------
private encodeRecord(record: WALRecord): Uint8Array {
const encoder = new TextEncoder();
const tableBytes = encoder.encode(record.tableName);
const keyBytes = encoder.encode(record.key);
const jsonStr = record.data ? JSON.stringify(record.data) : '';
const jsonBytes = encoder.encode(jsonStr);
const size =
4 + // LSN
1 + // type
4 + // txnId
2 + tableBytes.length + // table
2 + keyBytes.length + // key
4 + jsonBytes.length + // json
4; // CRC
const buf = new ArrayBuffer(size);
const view = new DataView(buf);
let offset = 0;
view.setUint32(offset, record.lsn, false);
offset += 4;
view.setUint8(offset, record.type);
offset += 1;
view.setUint32(offset, record.txnId, false);
offset += 4;
view.setUint16(offset, tableBytes.length, false);
offset += 2;
new Uint8Array(buf).set(tableBytes, offset);
offset += tableBytes.length;
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(buf).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, jsonBytes.length, false);
offset += 4;
new Uint8Array(buf).set(jsonBytes, offset);
offset += jsonBytes.length;
// 简单 CRC
let crc = 0;
const u8 = new Uint8Array(buf, 0, offset);
for (let i = 0; i < u8.length; i++) {
crc = ((crc << 5) - crc + u8[i]) | 0;
}
view.setUint32(offset, crc >>> 0, false);
return new Uint8Array(buf);
}
private decodeAllRecords(data: Uint8Array): WALRecord[] {
const records: WALRecord[] = [];
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let offset = 0;
while (offset + 15 <= data.byteLength) {
try {
const recordStart = offset;
const lsn = view.getUint32(offset, false);
offset += 4;
const type = view.getUint8(offset) as WALRecordType;
offset += 1;
const txnId = view.getUint32(offset, false);
offset += 4;
const tableLen = view.getUint16(offset, false);
offset += 2;
if (offset + tableLen > data.byteLength) break;
const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen));
offset += tableLen;
const keyLen = view.getUint16(offset, false);
offset += 2;
if (offset + keyLen > data.byteLength) break;
const key = new TextDecoder().decode(data.slice(offset, offset + keyLen));
offset += keyLen;
const jsonLen = view.getUint32(offset, false);
offset += 4;
if (offset + jsonLen > data.byteLength) break;
let recordData: Record<string, unknown> | undefined;
if (jsonLen > 0) {
const json = new TextDecoder().decode(data.slice(offset, offset + jsonLen));
try {
recordData = JSON.parse(json);
} catch { /* ok */ }
}
offset += jsonLen;
// 验证 CRC(跨记录数据计算,不含 CRC 自身)
const storedCrc = view.getUint32(offset, false);
offset += 4;
const recordBytes = data.slice(recordStart, offset - 4);
let computedCrc = 0;
for (let i = 0; i < recordBytes.length; i++) {
computedCrc = ((computedCrc << 5) - computedCrc + recordBytes[i]) | 0;
}
if ((computedCrc >>> 0) !== storedCrc) {
// CRC 不匹配,跳过此损坏记录
// eslint-disable-next-line no-console
console.warn(`[AriaEngine WAL] CRC mismatch at record LSN=${lsn}, skipping`);
continue;
}
records.push({
lsn,
type,
txnId,
tableName,
key,
data: recordData,
checksum: storedCrc,
});
} catch {
break;
}
}
return records;
}
}
/**
* AriaEngine WAL — Write-Ahead Log
* @module engine/aria/wal/log
*
* 崩溃恢复前的写操作持久化日志。
*
* WAL 文件格式:
* ┌──────────┬──────────────┬──────────┐
* │ Record 1│ Record 2 │ ... │
* │ 4B LSN │ │ │
* │ 1B type │ │ │
* │ 4B txnId│ │ │
* │ 2B tblLen│ │ │
* │ N table│ │ │
* │ 2B keyLen│ │ │
* │ N key │ │ │
* │ 4B jsonLen│ │ │
* │ N json │ │ │
* │ 4B CRC │ │ │
* └──────────┴──────────────┴──────────┘
*/
import { WALRecordType, type WALRecord } from '../types';
import { crc32 } from '../crc32';
// ---------------------------------------------------------------------------
// WAL 存储接口
// ---------------------------------------------------------------------------
export interface WALStore {
/** 追加 WAL 记录 */
append(data: Uint8Array): Promise<void>;
/** 读取所有 WAL 记录 */
readAll(): Promise<Uint8Array>;
/** 截断 WALcheckpoint 后清理) */
truncate(): Promise<void>;
/** 检查 WAL 是否存在 */
exists(): Promise<boolean>;
}
// ---------------------------------------------------------------------------
// WAL
// ---------------------------------------------------------------------------
export class WAL {
private lsn = 0;
private store: WALStore;
private enabled: boolean;
private buffer: Uint8Array[] = [];
private syncMode: 'full' | 'batch' | 'none';
/** v0.3.3: 未 checkpoint 的 WAL 累计字节数(full/batch/none 通用) */
private bufferedBytes = 0;
constructor(store: WALStore, enabled: boolean = true, syncMode: 'full' | 'batch' | 'none' = 'batch') {
this.store = store;
this.enabled = enabled;
this.syncMode = syncMode;
}
// =======================================================================
// 写入
// =======================================================================
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
async append(record: Omit<WALRecord, 'lsn' | 'checksum'>): Promise<void> {
if (!this.enabled) return;
this.lsn++;
const fullRecord: WALRecord = {
...record,
lsn: this.lsn,
checksum: 0, // 稍后计算
};
const bytes = this.encodeRecord(fullRecord);
if (this.syncMode === 'full') {
try {
await this.store.append(bytes);
this.bufferedBytes += bytes.byteLength;
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(bytes);
this.bufferedBytes += bytes.byteLength;
}
// 'none' mode: 不写 WAL
}
/** 批量追加多条 WAL 记录(组提交:合并为一次底层写入,v0.3.1) */
async appendBatch(records: Omit<WALRecord, 'lsn' | 'checksum'>[]): Promise<void> {
if (!this.enabled || records.length === 0) return;
const chunks: Uint8Array[] = [];
for (const record of records) {
this.lsn++;
chunks.push(this.encodeRecord({ ...record, lsn: this.lsn, checksum: 0 }));
}
const combined = this.mergeChunks(chunks);
if (this.syncMode === 'full') {
try {
await this.store.append(combined);
this.bufferedBytes += combined.byteLength;
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append batch record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(combined);
this.bufferedBytes += combined.byteLength;
}
// 'none' mode: 不写 WAL
}
/** 批量刷新缓冲的 WAL 记录 */
async flush(): Promise<void> {
if (!this.enabled || this.buffer.length === 0) return;
const combined = this.mergeChunks(this.buffer);
await this.store.append(combined);
this.buffer = [];
}
/** 合并多个字节块为一个连续缓冲区 */
private mergeChunks(chunks: Uint8Array[]): Uint8Array {
if (chunks.length === 1) return chunks[0];
const totalLen = chunks.reduce((sum, b) => sum + b.byteLength, 0);
const combined = new Uint8Array(totalLen);
let offset = 0;
for (const buf of chunks) {
combined.set(buf, offset);
offset += buf.byteLength;
}
return combined;
}
// =======================================================================
// 恢复
// =======================================================================
/** 从 WAL 恢复未提交的事务数据 */
async recover(
applyRecord: (record: WALRecord) => void,
): Promise<number> {
if (!this.enabled) return 0;
const exists = await this.store.exists();
if (!exists) return 0;
const data = await this.store.readAll();
if (data.byteLength === 0) return 0;
const records = this.decodeAllRecords(data);
for (const record of records) {
applyRecord(record);
}
this.lsn = records.length > 0 ? records[records.length - 1].lsn : 0;
return records.length;
}
// =======================================================================
// Checkpoint
// =======================================================================
/** Checkpoint 后清空 WAL */
async checkpoint(): Promise<void> {
if (!this.enabled) return;
await this.flush();
await this.store.truncate();
this.lsn = 0;
this.bufferedBytes = 0;
}
// =======================================================================
// 统计
// =======================================================================
getBufferedCount(): number {
return this.buffer.length;
}
/** v0.3.3: 未 checkpoint 的 WAL 累计字节数(full/batch/none 通用) */
getBufferedBytes(): number {
return this.bufferedBytes;
}
// -----------------------------------------------------------------------
// 编解码
// -----------------------------------------------------------------------
/** 旧版弱滚动校验(v0.4.4 及更早写入的 WAL 记录使用,双算法探测兼容) */
private legacyChecksum(data: Uint8Array): number {
let crc = 0;
for (let i = 0; i < data.length; i++) {
crc = ((crc << 5) - crc + data[i]) | 0;
}
return crc >>> 0;
}
private encodeRecord(record: WALRecord): Uint8Array {
const encoder = new TextEncoder();
const tableBytes = encoder.encode(record.tableName);
const keyBytes = encoder.encode(record.key);
const jsonStr = record.data ? JSON.stringify(record.data) : '';
const jsonBytes = encoder.encode(jsonStr);
const size =
4 + // LSN
1 + // type
4 + // txnId
2 + tableBytes.length + // table
2 + keyBytes.length + // key
4 + jsonBytes.length + // json
4; // CRC
const buf = new ArrayBuffer(size);
const view = new DataView(buf);
let offset = 0;
view.setUint32(offset, record.lsn, false);
offset += 4;
view.setUint8(offset, record.type);
offset += 1;
view.setUint32(offset, record.txnId, false);
offset += 4;
view.setUint16(offset, tableBytes.length, false);
offset += 2;
new Uint8Array(buf).set(tableBytes, offset);
offset += tableBytes.length;
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(buf).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, jsonBytes.length, false);
offset += 4;
new Uint8Array(buf).set(jsonBytes, offset);
offset += jsonBytes.length;
// v0.4.5: 标准 CRC-32 校验(此前为弱滚动校验,误检率更高)
const u8 = new Uint8Array(buf, 0, offset);
const crc = crc32(u8);
view.setUint32(offset, crc, false);
return new Uint8Array(buf);
}
private decodeAllRecords(data: Uint8Array): WALRecord[] {
const records: WALRecord[] = [];
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let offset = 0;
while (offset + 15 <= data.byteLength) {
try {
const recordStart = offset;
const lsn = view.getUint32(offset, false);
offset += 4;
const type = view.getUint8(offset) as WALRecordType;
offset += 1;
const txnId = view.getUint32(offset, false);
offset += 4;
const tableLen = view.getUint16(offset, false);
offset += 2;
if (offset + tableLen > data.byteLength) break;
const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen));
offset += tableLen;
const keyLen = view.getUint16(offset, false);
offset += 2;
if (offset + keyLen > data.byteLength) break;
const key = new TextDecoder().decode(data.slice(offset, offset + keyLen));
offset += keyLen;
const jsonLen = view.getUint32(offset, false);
offset += 4;
if (offset + jsonLen > data.byteLength) break;
let recordData: Record<string, unknown> | undefined;
if (jsonLen > 0) {
const json = new TextDecoder().decode(data.slice(offset, offset + jsonLen));
try {
recordData = JSON.parse(json);
} catch { /* ok */ }
}
offset += jsonLen;
// 验证 CRC:先标准 CRC-32,失败再尝试旧版弱滚动校验(兼容旧库 WAL 记录)
const storedCrc = view.getUint32(offset, false);
offset += 4;
const recordBytes = data.slice(recordStart, offset - 4);
const computedNew = crc32(recordBytes);
const computedLegacy = this.legacyChecksum(recordBytes);
if ((computedNew >>> 0) !== storedCrc && (computedLegacy >>> 0) !== storedCrc) {
// CRC 不匹配,跳过此损坏记录
// eslint-disable-next-line no-console
console.warn(`[AriaEngine WAL] CRC mismatch at record LSN=${lsn}, skipping`);
continue;
}
records.push({
lsn,
type,
txnId,
tableName,
key,
data: recordData,
checksum: storedCrc,
});
} catch {
break;
}
}
return records;
}
}
+161
View File
@@ -0,0 +1,161 @@
/**
* AriaEngine Segmented WAL Store — 分片式 WAL 持久化存储
* @module engine/aria/wal/segmented_store
*
* v0.4.5: 取代"每条记录一个 key"的旧结构:
* - 分片文件 `__wal_%06d.bin`,达到阈值(默认 4MB)切新分片 → 文件数量可控
* - append 追加写当前分片(后端支持真追加则 O(chunk),否则回退 read+write
* - WAL 序号(LSN)内嵌于记录字节流,无需独立 count 键 → append 单文件原子写
* - readAll 检测分片序号空洞:序号不连续 → 丢弃空洞之后的分片(保守截断,
* 空洞仅可能来自 truncate 部分完成——此时数据已落盘,丢弃无害)
* - 兼容旧格式 `__wal_N`(每条记录一个键)+ `__wal_count`:读取时迁移重放,
* checkpoint 时一并清空
*/
import type { IStorageBackend } from '../store/backend';
/** 分片文件名:__wal_%06d.bin */
export const WAL_SEGMENT_PREFIX = '__wal_';
const SEGMENT_REGEX = /^__wal_(\d{6})\.bin$/;
const LEGACY_RECORD_REGEX = /^__wal_(\d+)$/;
const LEGACY_COUNT_KEY = '__wal_count';
/** 默认分片阈值 */
export const DEFAULT_WAL_SEGMENT_SIZE = 4 * 1024 * 1024;
// ---------------------------------------------------------------------------
// WAL 存储接口(与 WAL 类解耦)
// ---------------------------------------------------------------------------
export interface WALStore {
append(data: Uint8Array): Promise<void>;
readAll(): Promise<Uint8Array>;
truncate(): Promise<void>;
exists(): Promise<boolean>;
}
// ---------------------------------------------------------------------------
// SegmentedWALStore
// ---------------------------------------------------------------------------
export class SegmentedWALStore implements WALStore {
private segmentSize: number;
/** 当前分片序号(append 定位) */
private currentSegment = 0;
/** 当前分片字节数(内存跟踪,append 切分片判断) */
private currentSize = 0;
constructor(
private backend: IStorageBackend,
segmentSize: number = DEFAULT_WAL_SEGMENT_SIZE,
) {
this.segmentSize = segmentSize;
}
/** 分片 key 生成 */
private segmentKey(seq: number): string {
return `${WAL_SEGMENT_PREFIX}${String(seq).padStart(6, '0')}.bin`;
}
async append(data: Uint8Array): Promise<void> {
if (data.byteLength === 0) return;
if (this.currentSize + data.byteLength > this.segmentSize) {
// 当前分片放不下 → 切新分片
this.currentSegment++;
this.currentSize = 0;
}
const key = this.segmentKey(this.currentSegment);
const copy = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
if (typeof this.backend.append === 'function') {
await this.backend.append!(key, copy);
} else {
// 回退:读旧 + 拼接 + 写(单文件原子写)
const existing = await this.backend.read(key);
if (existing) {
const combined = new ArrayBuffer(existing.byteLength + copy.byteLength);
new Uint8Array(combined).set(new Uint8Array(existing), 0);
new Uint8Array(combined).set(new Uint8Array(copy), existing.byteLength);
await this.backend.write(key, combined);
} else {
await this.backend.write(key, copy);
}
}
this.currentSize += data.byteLength;
}
async readAll(): Promise<Uint8Array> {
const keys = await this.backend.listKeys();
// ---- 新格式分片 ----
const segments = keys
.filter((k) => SEGMENT_REGEX.test(k))
.map((k) => ({ seq: Number(k.match(SEGMENT_REGEX)![1]), key: k }))
.sort((a, b) => a.seq - b.seq);
// 空洞检测:分片序号必须从 0 严格连续,空洞后的分片整体丢弃
let keepCount = 0;
for (let i = 0; i < segments.length; i++) {
if (segments[i].seq !== i) break;
keepCount = i + 1;
}
const validSegments = segments.slice(0, keepCount);
// ---- 旧格式兼容:__wal_N 单记录键(迁移前数据) ----
const legacyKeys = keys
.filter((k) => LEGACY_RECORD_REGEX.test(k))
.map((k) => ({ seq: Number(k.match(LEGACY_RECORD_REGEX)![1]), key: k }))
.sort((a, b) => a.seq - b.seq);
const parts: Uint8Array[] = [];
// 旧格式在前(它们是最早的记录)
for (const { key } of legacyKeys) {
const raw = await this.backend.read(key);
if (raw) parts.push(new Uint8Array(raw));
}
// 新格式分片在后
for (const { key } of validSegments) {
const raw = await this.backend.read(key);
if (raw) parts.push(new Uint8Array(raw));
}
// 同步当前分片状态(追加定位)
if (validSegments.length > 0) {
this.currentSegment = validSegments[validSegments.length - 1].seq;
const lastRaw = await this.backend.read(validSegments[validSegments.length - 1].key);
this.currentSize = lastRaw ? lastRaw.byteLength : 0;
// 旧格式键存在时(迁移中),下一条记录另起分片,避免与旧键序号冲突
if (legacyKeys.length > 0) {
this.currentSegment++;
this.currentSize = 0;
}
} else if (legacyKeys.length > 0) {
// 仅有旧格式:迁移中,新写入从分片 0 开始(checkpoint 会清空旧键)
this.currentSegment = 0;
this.currentSize = 0;
}
const total = parts.reduce((s, c) => s + c.byteLength, 0);
const combined = new Uint8Array(total);
let off = 0;
for (const c of parts) { combined.set(c, off); off += c.byteLength; }
return combined;
}
async truncate(): Promise<void> {
const keys = await this.backend.listKeys();
const walKeys = keys.filter((k) =>
k.startsWith(WAL_SEGMENT_PREFIX) || k === LEGACY_COUNT_KEY);
if (walKeys.length > 0) {
await this.backend.deleteMany(walKeys);
}
this.currentSegment = 0;
this.currentSize = 0;
}
async exists(): Promise<boolean> {
const keys = await this.backend.listKeys();
return keys.some((k) =>
k.startsWith(WAL_SEGMENT_PREFIX) || k === LEGACY_COUNT_KEY);
}
}
-12
View File
@@ -360,16 +360,4 @@ export class OPFSEngine implements IStorageEngine {
}
}
}
/** 从 OPFS 加载表数据到内存缓存 */
async loadTableIntoMemory(tableName: string, schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema);
const rows = await this.readTableData(tableName);
if (rows.length > 0) {
// 直接用 Map 设置绕过 insert 校验
for (const row of rows) {
await this.memoryCache.insert(tableName, [row]);
}
}
}
}
+34 -39
View File
@@ -12,23 +12,6 @@ import type { WhereCondition, OrderBy } from '../constants';
// AST 语句类型枚举
// ---------------------------------------------------------------------------
export type StatementType =
| 'SELECT'
| 'SELECT_UNION'
| 'EXPLAIN'
| 'INSERT'
| 'UPDATE'
| 'DELETE'
| 'CREATE_TABLE'
| 'DROP_TABLE'
| 'ALTER_TABLE'
| 'TRUNCATE_TABLE'
| 'CREATE_INDEX'
| 'DROP_INDEX'
| 'BEGIN'
| 'COMMIT'
| 'ROLLBACK';
// ---------------------------------------------------------------------------
// 列引用
// ---------------------------------------------------------------------------
@@ -51,31 +34,10 @@ export interface JoinClause {
on: WhereCondition;
}
// ---------------------------------------------------------------------------
// 聚合函数
// ---------------------------------------------------------------------------
/** 聚合函数类型 */
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
/** 聚合表达式 */
export interface AggregateExpression {
type: 'AGGREGATE';
func: AggregateFunc;
column: string; // '*' for COUNT(*)
alias?: string;
}
// ---------------------------------------------------------------------------
// 子查询
// ---------------------------------------------------------------------------
/** 子查询表达式 */
export interface SubqueryExpression {
type: 'SUBQUERY';
statement: SelectStatement | SelectUnionStatement;
}
// ---------------------------------------------------------------------------
// DDL: CREATE TABLE
// ---------------------------------------------------------------------------
@@ -255,6 +217,35 @@ export interface RollbackTransactionStatement {
type: 'ROLLBACK';
}
// ---------------------------------------------------------------------------
// TCL: 维护语句(v0.5.1
// ---------------------------------------------------------------------------
/** SAVEPOINT name / ROLLBACK TO SAVEPOINT name / RELEASE SAVEPOINT name */
export interface SavepointStatement {
type: 'SAVEPOINT';
name: string;
/** SAVE = 创建;ROLLBACK = 回滚到;RELEASE = 释放 */
action: 'SAVE' | 'ROLLBACK' | 'RELEASE';
}
/** ANALYZE TABLE name — 收集表统计信息 */
export interface AnalyzeStatement {
type: 'ANALYZE';
table: string;
}
/** REINDEX TABLE name — 重建表二级索引 */
export interface ReindexStatement {
type: 'REINDEX';
table: string;
}
/** VACUUM — 压缩 LSM + 清理碎片 */
export interface VacuumStatement {
type: 'VACUUM';
}
// ---------------------------------------------------------------------------
// AST 联合类型
// ---------------------------------------------------------------------------
@@ -274,4 +265,8 @@ export type Statement =
| DropIndexStatement
| BeginTransactionStatement
| CommitTransactionStatement
| RollbackTransactionStatement;
| RollbackTransactionStatement
| SavepointStatement
| AnalyzeStatement
| ReindexStatement
| VacuumStatement;
+16 -2
View File
@@ -135,14 +135,18 @@ export class SelectQueryBuilder {
export class UpdateQueryBuilder {
private _where: WhereCondition = {};
private onWrite?: (table: string) => void;
/** v0.5.1: CRUD hooks 触发回调 */
private onHooks?: (hook: import('../constants').HookName, args: unknown[]) => Promise<void>;
constructor(
private engine: IStorageEngine,
private tableName: string,
private _updates: Record<string, unknown>,
onWrite?: (table: string) => void,
onHooks?: (hook: import('../constants').HookName, args: unknown[]) => Promise<void>,
) {
this.onWrite = onWrite;
this.onHooks = onHooks;
}
where(condition: WhereCondition): this {
@@ -151,8 +155,11 @@ export class UpdateQueryBuilder {
}
async execute(): Promise<number> {
const count = await this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
const query = { table: this.tableName, where: this._where };
await this.onHooks?.('beforeUpdate', [query, this._updates]);
const count = await this.engine.update(this.tableName, query, this._updates);
this.onWrite?.(this.tableName);
await this.onHooks?.('afterUpdate', [query, this._updates, count]);
return count;
}
@@ -168,13 +175,17 @@ export class UpdateQueryBuilder {
export class DeleteQueryBuilder {
private _where: WhereCondition = {};
private onWrite?: (table: string) => void;
/** v0.5.1: CRUD hooks 触发回调 */
private onHooks?: (hook: import('../constants').HookName, args: unknown[]) => Promise<void>;
constructor(
private engine: IStorageEngine,
private tableName: string,
onWrite?: (table: string) => void,
onHooks?: (hook: import('../constants').HookName, args: unknown[]) => Promise<void>,
) {
this.onWrite = onWrite;
this.onHooks = onHooks;
}
where(condition: WhereCondition): this {
@@ -183,8 +194,11 @@ export class DeleteQueryBuilder {
}
async execute(): Promise<number> {
const count = await this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
const query = { table: this.tableName, where: this._where };
await this.onHooks?.('beforeDelete', [query]);
const count = await this.engine.delete(this.tableName, query);
this.onWrite?.(this.tableName);
await this.onHooks?.('afterDelete', [query, count]);
return count;
}
+1315 -1244
View File
File diff suppressed because it is too large Load Diff
+77 -1
View File
@@ -72,6 +72,19 @@ export class Parser {
return this.parseCommit();
case TokenType.ROLLBACK:
return this.parseRollback();
// v0.5.1: 维护语句入口
case TokenType.EXPLAIN:
return this.parseExplain();
case TokenType.ANALYZE:
return this.parseAnalyze();
case TokenType.REINDEX:
return this.parseReindex();
case TokenType.VACUUM:
return this.parseVacuum();
case TokenType.SAVEPOINT:
return this.parseSavepoint();
case TokenType.RELEASE:
return this.parseSavepoint();
default:
throw this.error(`Unexpected token "${this.curToken.value}"`);
}
@@ -95,6 +108,57 @@ export class Parser {
return statements;
}
// ---- 维护语句(v0.5.1 ----
/** EXPLAIN <statement> — 输出查询计划 */
private parseExplain(): import('../query/ast').ExplainStatement {
this.expect(TokenType.EXPLAIN);
if (this.curTokenIs(TokenType.EXPLAIN)) {
throw this.error('Nested EXPLAIN is not allowed');
}
const query = this.parseStatement();
return { type: 'EXPLAIN', query };
}
/** ANALYZE [TABLE] name — 收集表统计信息 */
private parseAnalyze(): import('../query/ast').AnalyzeStatement {
this.expect(TokenType.ANALYZE);
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TABLE') {
this.nextToken();
}
return { type: 'ANALYZE', table: this.expectIdentifier('table name') };
}
/** REINDEX [TABLE] name — 重建表二级索引 */
private parseReindex(): import('../query/ast').ReindexStatement {
this.expect(TokenType.REINDEX);
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TABLE') {
this.nextToken();
}
return { type: 'REINDEX', table: this.expectIdentifier('table name') };
}
/** VACUUM — 压缩 LSM + 清理碎片 */
private parseVacuum(): import('../query/ast').VacuumStatement {
this.expect(TokenType.VACUUM);
return { type: 'VACUUM' };
}
/** SAVEPOINT name | RELEASE [SAVEPOINT] name */
private parseSavepoint(): import('../query/ast').SavepointStatement {
let action: 'SAVE' | 'ROLLBACK' | 'RELEASE';
if (this.curTokenIs(TokenType.RELEASE)) {
action = 'RELEASE';
this.nextToken();
} else {
action = 'SAVE';
this.expect(TokenType.SAVEPOINT);
}
// 可选 SAVEPOINT 关键字(RELEASE SAVEPOINT name
if (this.curTokenIs(TokenType.SAVEPOINT)) this.nextToken();
return { type: 'SAVEPOINT', name: this.expectIdentifier('savepoint name'), action };
}
// ---- 事务语句 ----
private parseBegin(): BeginTransactionStatement {
@@ -114,10 +178,22 @@ export class Parser {
return { type: 'COMMIT' };
}
private parseRollback(): RollbackTransactionStatement {
/** ROLLBACK [TRANSACTION] | ROLLBACK TO [SAVEPOINT] namev0.5.1 */
private parseRollback(): Statement {
this.expect(TokenType.ROLLBACK);
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TRANSACTION') {
this.nextToken();
return { type: 'ROLLBACK' };
}
// ROLLBACK TO [SAVEPOINT] name
if (this.curTokenIs(TokenType.TO) ||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TO')) {
this.nextToken();
if (this.curTokenIs(TokenType.SAVEPOINT) ||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'SAVEPOINT')) {
this.nextToken();
}
return { type: 'SAVEPOINT', name: this.expectIdentifier('savepoint name'), action: 'ROLLBACK' };
}
return { type: 'ROLLBACK' };
}
+18
View File
@@ -83,6 +83,15 @@ export enum TokenType {
ELSE = 'ELSE',
END = 'END',
// v0.5.1: 维护语句(EXPLAIN / ANALYZE / REINDEX / VACUUM / SAVEPOINT
EXPLAIN = 'EXPLAIN',
ANALYZE = 'ANALYZE',
REINDEX = 'REINDEX',
VACUUM = 'VACUUM',
SAVEPOINT = 'SAVEPOINT',
RELEASE = 'RELEASE',
TO = 'TO',
// 标识符 & 字面量
IDENTIFIER = 'IDENTIFIER',
STRING = 'STRING',
@@ -195,4 +204,13 @@ export const KEYWORDS: Record<string, TokenType> = {
'THEN': TokenType.THEN,
'ELSE': TokenType.ELSE,
'END': TokenType.END,
// v0.5.1
'EXPLAIN': TokenType.EXPLAIN,
'ANALYZE': TokenType.ANALYZE,
'REINDEX': TokenType.REINDEX,
'VACUUM': TokenType.VACUUM,
'SAVEPOINT': TokenType.SAVEPOINT,
'RELEASE': TokenType.RELEASE,
'TO': TokenType.TO,
};
+17 -3
View File
@@ -20,12 +20,21 @@ export class Table<T = Record<string, unknown>> {
private executor: QueryExecutor | undefined;
/** 写入回调(多标签页广播,v0.3.2) */
private onWrite?: (table: string) => void;
/** v0.5.1: CRUD 生命周期钩子触发回调(beforeInsert/afterInsert/... 真实接线) */
private onHooks?: (hook: import('../constants').HookName, args: unknown[]) => Promise<void>;
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor, onWrite?: (table: string) => void) {
constructor(
engine: IStorageEngine,
tableName: string,
executor?: QueryExecutor,
onWrite?: (table: string) => void,
onHooks?: (hook: import('../constants').HookName, args: unknown[]) => Promise<void>,
) {
this.engine = engine;
this.name = tableName;
this.executor = executor;
this.onWrite = onWrite;
this.onHooks = onHooks;
}
// ---- Schema ----
@@ -42,14 +51,19 @@ export class Table<T = Record<string, unknown>> {
// ---- 插入 ----
async insert(row: T & Record<string, unknown>): Promise<string> {
// beforeInsert 参数约定:rows[](单行时也是数组)
await this.onHooks?.('beforeInsert', [[row]]);
const pks = await this.engine.insert(this.name, [row as Record<string, unknown>]);
this.onWrite?.(this.name);
await this.onHooks?.('afterInsert', [[row], pks]);
return pks[0];
}
async insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]> {
await this.onHooks?.('beforeInsert', [rows]);
const pks = await this.engine.insert(this.name, rows as Record<string, unknown>[]);
this.onWrite?.(this.name);
await this.onHooks?.('afterInsert', [rows, pks]);
return pks;
}
@@ -87,13 +101,13 @@ export class Table<T = Record<string, unknown>> {
// ---- 更新 ----
update(updates: Partial<T> & Record<string, unknown>): UpdateQueryBuilder {
return new UpdateQueryBuilder(this.engine, this.name, updates, this.onWrite);
return new UpdateQueryBuilder(this.engine, this.name, updates, this.onWrite, this.onHooks);
}
// ---- 删除 ----
delete(): DeleteQueryBuilder {
return new DeleteQueryBuilder(this.engine, this.name, this.onWrite);
return new DeleteQueryBuilder(this.engine, this.name, this.onWrite, this.onHooks);
}
// ---- 聚合 ----
-80
View File
@@ -1,80 +0,0 @@
/**
* metona-sqlark Utils — 工具函数
* @module utils
*/
/** 生成唯一 ID */
export const generateId = (): string => {
return 'id-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
};
/** HTML 转义 */
export const escapeHTML = (s: unknown): string => {
const str = String(s == null ? '' : s);
if (typeof document === 'undefined') {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
};
/** 防抖 */
export function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number,
immediate = false,
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null;
return function (this: any, ...args: Parameters<T>) {
const context = this;
const later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
/** 节流 */
export function throttle<T extends (...args: any[]) => void>(
func: T,
limit: number,
): (...args: Parameters<T>) => void {
let inThrottle = false;
return function (this: any, ...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => { inThrottle = false; }, limit);
}
};
}
/** 深度合并 */
export const deepMerge = <T extends Record<string, unknown>>(target: T, source: Partial<T>): T => {
const output: Record<string, unknown> = { ...target };
for (const key of Object.keys(source as Record<string, unknown>)) {
const sv = (source as Record<string, unknown>)[key];
const tv = (target as Record<string, unknown>)[key];
if (sv instanceof Object && key in target && tv instanceof Object) {
output[key] = deepMerge(tv as Record<string, unknown>, sv as Record<string, unknown>);
} else {
output[key] = source[key];
}
}
return output as T;
};
/** 浏览器环境检测 */
export const isBrowser = (): boolean => {
return typeof window !== 'undefined' && typeof document !== 'undefined';
};