release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
CI / test (18.x) (push) Failing after 5m11s
CI / test (20.x) (push) Failing after 5m8s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m56s

v0.2.6 质量加固:
- 修复 AriaEngine 二级索引 SSTable 互相覆盖(命名空间隔离)
- 修复 LSM 多版本读取顺序错误 + MergeIterator 取最新来源
- 重写 LZ4 压缩器(往返一致性 + 缓冲区溢出)
- sstableCache LRU 上限 + 预加载兜底(BufferPool 配置生效)
- 修复 React/Vue 集成 import type 运行时 bug + exports 子路径
- 新增 38 个测试(LZ4往返/Crypto/集成), 删除伪测试

v0.3.0 SQL 功能扩展:
- 多语句 parseAll + 事务语句 BEGIN/COMMIT/ROLLBACK
- INSERT INTO ... SELECT + UNION/UNION ALL + EXISTS 关联子查询
- CREATE/DROP INDEX 五引擎实现 + 别名 WHERE 修复
- benchmark 页面 + 36 个新测试

v0.3.1 表达式与性能:
- CASE WHEN 表达式(SELECT 列/WHERE/聚合)
- JOIN + 关联子查询逐行绑定
- WAL 批量组提交(写放大 O(N)→O(1))
- 修复 pending frozen 可见性 + flush 缓存竞争

v0.3.2 并发:
- CASE WHEN 用于 WHERE/聚合 + JOIN 哈希连接
- 多标签页同步(multiTabSync + BroadcastChannel)
- IndexedDB schema 持久化(reopen 后表结构恢复)
- 修复 where-matcher 顶层 $not
- 修复 CJS 产物 .js 被 ESM 解析(exports 空) — .cjs 后缀 + exports 修正
- 836 测试 / 44 套件 / 81.0% 覆盖率
This commit is contained in:
thzxx
2026-08-08 10:41:30 +08:00
parent 3ae7d6e8fb
commit d544501e1c
77 changed files with 29007 additions and 20395 deletions
+215 -212
View File
@@ -1,212 +1,215 @@
/**
* 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;
}
/** 数据库默认配置 */
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,
});
// ---------------------------------------------------------------------------
// 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;
}
// ---------------------------------------------------------------------------
// 查询计划(引擎层使用)
// ---------------------------------------------------------------------------
/** 查询计划 — 由 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.2.5';
/**
* 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;
}
// ---------------------------------------------------------------------------
// 查询计划(引擎层使用)
// ---------------------------------------------------------------------------
/** 查询计划 — 由 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.3.2';
+391 -329
View File
@@ -1,329 +1,391 @@
/**
* metona-sqlark Core — 数据库主类
* @module core
*
* 管理数据库生命周期、引擎调度、表操作、SQL 查询、事务和插件。
*/
import type { IStorageEngine } from './engine/interface';
import type { DatabaseConfig, ColumnDef } from './constants';
import { DB_DEFAULTS, DatabaseError } from './constants';
import { MemoryEngine } from './engine/memory';
import { IndexedDBEngine } from './engine/indexeddb';
import { OPFSEngine } from './engine/opfs';
import { AriaEngine } from './engine/aria/index';
import { HybridEngine } from './hybrid/index';
import { Table } from './table/table';
import { createSchema } from './table/schema';
import { QueryExecutor } from './query/executor';
import { parse } from './sql/parser';
import { TransactionManager } from './transaction/index';
import { PluginManager } from './plugin/index';
import type { Statement } from './query/ast';
// ---------------------------------------------------------------------------
// MetonaSqlark
// ---------------------------------------------------------------------------
export class MetonaSqlark {
/** 数据库名称 */
readonly name: string;
/** 存储模式 */
readonly mode: string;
/** 版本号 */
private _version: number;
/** 获取版本号 */
get version(): number { return this._version; }
private engine!: IStorageEngine;
private executor!: QueryExecutor;
private transactionManager!: TransactionManager;
private pluginManager: PluginManager;
private config: DatabaseConfig;
private ready = false;
private tableCache: Map<string, Table> = new Map();
/** 查询结果行数上限 */
get maxRowsPerQuery(): number { return this.config.maxRowsPerQuery ?? 0; }
/** 调试模式 */
get debug(): boolean { return this.config.debug ?? false; }
constructor(config: DatabaseConfig) {
this.config = config;
this.name = config.name ?? DB_DEFAULTS.name;
this.mode = config.mode ?? DB_DEFAULTS.mode;
this._version = config.version ?? DB_DEFAULTS.version;
this.pluginManager = new PluginManager();
}
// ---- 初始化 ----
/** 初始化数据库(创建引擎、打开连接) */
async init(): Promise<void> {
// 创建引擎
this.engine = this.createEngine();
// 打开连接
await this.engine.open(this.name, this.version);
// 初始化执行器和事务管理器
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
this.pluginManager.register(plugin, this);
}
}
this.ready = true;
// 回调
if (this.config.onReady) {
this.config.onReady(this);
}
}
/** 检查是否就绪 */
isReady(): boolean {
return this.ready;
}
// ---- 表管理 ----
/** 创建表 */
async defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void> {
this.ensureReady();
const schema = createSchema(name, columns);
try {
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
} catch (error) {
this._onError(error as Error);
throw error;
}
// 清除缓存
this.tableCache.delete(name);
}
/** 获取表操作对象 */
table(name: string): Table {
this.ensureReady();
let t = this.tableCache.get(name);
if (!t) {
t = new Table(this.engine, name, this.executor);
this.tableCache.set(name, t);
}
return t;
}
/** 删除表 */
async dropTable(name: string): Promise<void> {
this.ensureReady();
try {
await this.pluginManager.trigger('beforeDropTable', name);
await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
} catch (error) {
this._onError(error as Error);
throw error;
}
this.tableCache.delete(name);
}
/** 获取所有表名 */
async getTableNames(): Promise<string[]> {
this.ensureReady();
return this.engine.getTableNames();
}
// ---- SQL 查询 ----
/** 执行 SQL 字符串查询 */
async query(sql: string): Promise<unknown> {
this.ensureReady();
const startTime = this.debug ? Date.now() : 0;
await this.pluginManager.trigger('beforeQuery', sql);
let result: unknown;
try {
const stmt: Statement = parse(sql);
result = await this.executor.execute(stmt);
} catch (error) {
this._onError(error as Error);
throw error;
}
await this.pluginManager.trigger('afterQuery', sql, result);
if (this.debug) {
const elapsed = Date.now() - startTime;
const rows = Array.isArray(result) ? (result as any[]).length : 0;
this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`);
}
return result;
}
// ---- 事务 ----
/** 执行事务 */
async transaction<T>(fn: (trx: import('./transaction/index').Transaction) => Promise<T>): Promise<T> {
this.ensureReady();
await this.pluginManager.trigger('beforeTransaction');
try {
const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
return result;
} catch (error) {
this._onError(error as Error);
throw error;
}
}
// ---- 导入导出 ----
/** 导出表数据为 JSON */
async exportTable(tableName: string): Promise<Record<string, unknown>[]> {
this.ensureReady();
return this.engine.find(tableName, { table: tableName });
}
/** 导入 JSON 数据到表 */
async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
this.ensureReady();
try {
return await this.engine.insert(tableName, data);
} catch (error) {
this._onError(error as Error);
throw error;
}
}
/** 导出整个数据库为 JSON */
async exportAll(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
const result: Record<string, Record<string, unknown>[]> = {};
const names = await this.engine.getTableNames();
for (const name of names) {
result[name] = await this.engine.find(name, { table: name });
}
return result;
}
// ---- 发布订阅 ----
private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
/** 订阅表变更 */
subscribe(tableName: string, callback: (event: { type: string; row?: unknown }) => void): () => void {
const key = `change:${tableName}`;
if (!this.listeners.has(key)) this.listeners.set(key, new Set());
this.listeners.get(key)!.add(callback as (data: unknown) => void);
return () => this.listeners.get(key)?.delete(callback as (data: unknown) => void);
}
/** 触发变更事件 */
emit(tableName: string, event: { type: string; row?: unknown }): void {
const key = `change:${tableName}`;
this.listeners.get(key)?.forEach((cb) => cb(event));
}
// ---- 迁移 ----
private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map();
/** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void {
this.migrations.set(version, up);
}
/** 执行迁移到指定版本 */
async migrateTo(targetVersion: number): Promise<void> {
this.ensureReady();
for (const [version, up] of [...this.migrations.entries()].sort((a, b) => a[0] - b[0])) {
if (version <= targetVersion && version > this.version) {
await up(this);
this._version = version;
}
}
}
// ---- 插件 ----
/** 获取插件管理器 */
getPluginManager(): PluginManager {
return this.pluginManager;
}
/** 注册钩子 */
on(hook: import('./constants').HookName, callback: import('./plugin/index').HookCallback): void {
this.pluginManager.on(hook, callback);
}
// ---- 生命周期 ----
/** 关闭数据库 */
async close(): Promise<void> {
this.pluginManager.destroy();
await this.engine.close();
this.tableCache.clear();
this.ready = false;
}
/** 获取底层引擎 */
getEngine(): IStorageEngine {
return this.engine;
}
// ---- 内部 ----
private createEngine(): IStorageEngine {
const mode = this.mode;
const diskEngine = this.config.diskEngine ?? 'indexeddb';
switch (mode) {
case 'memory':
return new MemoryEngine();
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'aria':
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
case 'hybrid':
return new HybridEngine(diskEngine);
default:
throw new DatabaseError(`Unknown storage mode: ${mode}`, 'CONFIG_ERROR');
}
}
private ensureReady(): void {
if (!this.ready) {
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY');
}
}
/** 错误回调分发 */
private _onError(error: Error): void {
if (this.config.onError) {
try { this.config.onError(error); } catch { /* 避免回调自身异常影响主流程 */ }
}
}
/** 调试日志 */
private _debug(msg: string, ...args: unknown[]): void {
if (this.debug) {
console.debug(`[MetonaSqlark:${this.name}] ${msg}`, ...args);
}
}
}
/**
* metona-sqlark Core — 数据库主类
* @module core
*
* 管理数据库生命周期、引擎调度、表操作、SQL 查询、事务和插件。
*/
import type { IStorageEngine } from './engine/interface';
import type { DatabaseConfig, ColumnDef } from './constants';
import { DB_DEFAULTS, DatabaseError } from './constants';
import { MemoryEngine } from './engine/memory';
import { IndexedDBEngine } from './engine/indexeddb';
import { OPFSEngine } from './engine/opfs';
import { AriaEngine } from './engine/aria/index';
import { HybridEngine } from './hybrid/index';
import { Table } from './table/table';
import { createSchema } from './table/schema';
import { QueryExecutor } from './query/executor';
import { parseAll } from './sql/parser';
import { TransactionManager } from './transaction/index';
import { PluginManager } from './plugin/index';
import type { Statement } from './query/ast';
// ---------------------------------------------------------------------------
// MetonaSqlark
// ---------------------------------------------------------------------------
export class MetonaSqlark {
/** 数据库名称 */
readonly name: string;
/** 存储模式 */
readonly mode: string;
/** 版本号 */
private _version: number;
/** 获取版本号 */
get version(): number { return this._version; }
private engine!: IStorageEngine;
private executor!: QueryExecutor;
private transactionManager!: TransactionManager;
private pluginManager: PluginManager;
private config: DatabaseConfig;
private ready = false;
private tableCache: Map<string, Table> = new Map();
/** 查询结果行数上限 */
get maxRowsPerQuery(): number { return this.config.maxRowsPerQuery ?? 0; }
/** 调试模式 */
get debug(): boolean { return this.config.debug ?? false; }
/** 多标签页同步通道(v0.3.2 */
private channel: BroadcastChannel | null = null;
constructor(config: DatabaseConfig) {
this.config = config;
this.name = config.name ?? DB_DEFAULTS.name;
this.mode = config.mode ?? DB_DEFAULTS.mode;
this._version = config.version ?? DB_DEFAULTS.version;
this.pluginManager = new PluginManager();
// v0.3.2: 多标签页同步 — BroadcastChannel 广播表变更
if (config.multiTabSync && typeof BroadcastChannel !== 'undefined') {
this.channel = new BroadcastChannel(`metona-sqlark:${this.name}`);
this.channel.onmessage = (event) => {
const msg = event.data as { type?: string; table?: string } | null;
if (!msg || msg.type !== 'change') return;
this.emit(msg.table ?? '', { type: 'external', table: msg.table ?? '' });
// Hybrid 引擎:从磁盘重载内存,保证读到其他标签页的最新数据
if (this.engine instanceof HybridEngine) {
(this.engine as HybridEngine).reloadMemoryFromDisk().catch(() => {
// 重载失败不影响主流程(下次读可能短暂过期)
});
}
};
}
}
// ---- 初始化 ----
/** 初始化数据库(创建引擎、打开连接) */
async init(): Promise<void> {
// 创建引擎
this.engine = this.createEngine();
// 打开连接
await this.engine.open(this.name, this.version);
// 初始化执行器和事务管理器
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
this.pluginManager.register(plugin, this);
}
}
this.ready = true;
// 回调
if (this.config.onReady) {
this.config.onReady(this);
}
}
/** 检查是否就绪 */
isReady(): boolean {
return this.ready;
}
// ---- 表管理 ----
/** 创建表 */
async defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void> {
this.ensureReady();
const schema = createSchema(name, columns);
try {
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
} catch (error) {
this._onError(error as Error);
throw error;
}
// 清除缓存
this.tableCache.delete(name);
}
/** 获取表操作对象 */
table(name: string): Table {
this.ensureReady();
let t = this.tableCache.get(name);
if (!t) {
// v0.3.2: 表操作写入后广播变更(多标签页同步)
t = new Table(this.engine, name, this.executor, (tableName) => this.broadcastChange(tableName));
this.tableCache.set(name, t);
}
return t;
}
/** 删除表 */
async dropTable(name: string): Promise<void> {
this.ensureReady();
try {
await this.pluginManager.trigger('beforeDropTable', name);
await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
} catch (error) {
this._onError(error as Error);
throw error;
}
this.tableCache.delete(name);
}
/** 获取所有表名 */
async getTableNames(): Promise<string[]> {
this.ensureReady();
return this.engine.getTableNames();
}
// ---- SQL 查询 ----
/** 执行 SQL 字符串查询 */
async query(sql: string): Promise<unknown> {
this.ensureReady();
const startTime = this.debug ? Date.now() : 0;
await this.pluginManager.trigger('beforeQuery', sql);
let result: unknown;
try {
// v0.3.0: 支持分号分隔的多语句,逐条顺序执行,返回最后一条的结果
const statements: Statement[] = parseAll(sql);
for (const stmt of statements) {
result = await this.executor.execute(stmt);
// v0.3.2: 写语句广播表变更(多标签页同步)
const table = this.writeStatementTable(stmt);
if (table) this.broadcastChange(table);
}
} catch (error) {
this._onError(error as Error);
throw error;
}
await this.pluginManager.trigger('afterQuery', sql, result);
if (this.debug) {
const elapsed = Date.now() - startTime;
const rows = Array.isArray(result) ? (result as any[]).length : 0;
this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`);
}
return result;
}
// ---- 事务 ----
/** 执行事务 */
async transaction<T>(fn: (trx: import('./transaction/index').Transaction) => Promise<T>): Promise<T> {
this.ensureReady();
await this.pluginManager.trigger('beforeTransaction');
try {
const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
return result;
} catch (error) {
this._onError(error as Error);
throw error;
}
}
// ---- 导入导出 ----
/** 导出表数据为 JSON */
async exportTable(tableName: string): Promise<Record<string, unknown>[]> {
this.ensureReady();
return this.engine.find(tableName, { table: tableName });
}
/** 导入 JSON 数据到表 */
async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
this.ensureReady();
try {
return await this.engine.insert(tableName, data);
} catch (error) {
this._onError(error as Error);
throw error;
}
}
/** 导出整个数据库为 JSON */
async exportAll(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
const result: Record<string, Record<string, unknown>[]> = {};
const names = await this.engine.getTableNames();
for (const name of names) {
result[name] = await this.engine.find(name, { table: name });
}
return result;
}
// ---- 发布订阅 ----
private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
/** 订阅表变更 */
subscribe(tableName: string, callback: (event: { type: string; row?: unknown; table?: string }) => void): () => void {
const key = `change:${tableName}`;
if (!this.listeners.has(key)) this.listeners.set(key, new Set());
this.listeners.get(key)!.add(callback as (data: unknown) => void);
return () => this.listeners.get(key)?.delete(callback as (data: unknown) => void);
}
/** 触发变更事件 */
emit(tableName: string, event: { type: string; row?: unknown; table?: string }): void {
const key = `change:${tableName}`;
this.listeners.get(key)?.forEach((cb) => cb(event));
}
// ---- 多标签页同步(v0.3.2 ----
/** 广播表变更到其他标签页(多标签页同步) */
broadcastChange(tableName: string): void {
if (!this.channel) return;
try {
this.channel.postMessage({ type: 'change', table: tableName });
} catch {
// 广播失败不影响主流程
}
}
/** 写语句对应的表名(多标签页广播用) */
private writeStatementTable(stmt: Statement): string | null {
switch (stmt.type) {
case 'INSERT': return stmt.into;
case 'UPDATE': return stmt.table;
case 'DELETE': return stmt.from;
case 'CREATE_TABLE':
case 'DROP_TABLE':
case 'TRUNCATE_TABLE':
return stmt.name;
case 'ALTER_TABLE': return stmt.name;
case 'CREATE_INDEX':
case 'DROP_INDEX':
return stmt.table;
default:
return null;
}
}
// ---- 迁移 ----
private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map();
/** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void {
this.migrations.set(version, up);
}
/** 执行迁移到指定版本 */
async migrateTo(targetVersion: number): Promise<void> {
this.ensureReady();
for (const [version, up] of [...this.migrations.entries()].sort((a, b) => a[0] - b[0])) {
if (version <= targetVersion && version > this.version) {
await up(this);
this._version = version;
}
}
}
// ---- 插件 ----
/** 获取插件管理器 */
getPluginManager(): PluginManager {
return this.pluginManager;
}
/** 注册钩子 */
on(hook: import('./constants').HookName, callback: import('./plugin/index').HookCallback): void {
this.pluginManager.on(hook, callback);
}
// ---- 生命周期 ----
/** 关闭数据库 */
async close(): Promise<void> {
if (this.channel) {
this.channel.close();
this.channel = null;
}
this.pluginManager.destroy();
await this.engine.close();
this.tableCache.clear();
this.ready = false;
}
/** 获取底层引擎 */
getEngine(): IStorageEngine {
return this.engine;
}
// ---- 内部 ----
private createEngine(): IStorageEngine {
const mode = this.mode;
const diskEngine = this.config.diskEngine ?? 'indexeddb';
switch (mode) {
case 'memory':
return new MemoryEngine();
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'aria':
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
case 'hybrid':
return new HybridEngine(diskEngine);
default:
throw new DatabaseError(`Unknown storage mode: ${mode}`, 'CONFIG_ERROR');
}
}
private ensureReady(): void {
if (!this.ready) {
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY');
}
}
/** 错误回调分发 */
private _onError(error: Error): void {
if (this.config.onError) {
try { this.config.onError(error); } catch { /* 避免回调自身异常影响主流程 */ }
}
}
/** 调试日志 */
private _debug(msg: string, ...args: unknown[]): void {
if (this.debug) {
// eslint-disable-next-line no-console
console.debug(`[MetonaSqlark:${this.name}] ${msg}`, ...args);
}
}
}
+206 -206
View File
@@ -1,206 +1,206 @@
/**
* 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;
}
/** 获取 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();
}
}
+185 -185
View File
@@ -1,185 +1,185 @@
/**
* 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>;
/** 释放页面 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;
}
}
+39 -19
View File
@@ -2,39 +2,48 @@
* 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 歧义"
*
* Token 格式(1 字节):
* hi 4bit = litLen (0-15)
* lo 4bit = matchField (0-15, 实际匹配 = field+4)
* lo 4bit = matchField (1-15, 实际匹配 = field+4)
*
* 字面量-匹配序列: [token] [litLen bytes] [2B LE offset]
* 末尾纯字面量: [token with lo=0] [litLen bytes] ← 仅在流末尾出现
*/
const MIN_MATCH = 4;
const MAX_MATCH = MIN_MATCH + 15; // 19,匹配长度上限
export function compressLZ4(input: Uint8Array): Uint8Array {
if (input.byteLength < MIN_MATCH) return input;
// 空输入直接返回(无 token 可输出)
if (input.byteLength === 0) return input;
const maxOut = input.byteLength + (input.byteLength >> 8) + 32;
// 最坏情况:纯字面量分块输出 len/15 个 token + 末尾 token
// 上限:len + ceil(len/15) + 8(组合 token 的 offset 开销已包含在内)
const maxOut = input.byteLength + Math.ceil(input.byteLength / 15) + 8;
const out = new Uint8Array(maxOut);
let si = 0, di = 0;
let litStart = 0;
while (si < input.byteLength) {
// 搜索最长 backward match
// 搜索最长 backward match(截断到 MAX_MATCH,避免 token 字段溢出)
let bestLen = 0, bestOff = 0;
const searchStart = Math.max(0, si - 65535);
for (let p = searchStart; p < si; p++) {
let ml = 0;
while (si + ml < input.byteLength && p + ml < si &&
input[p + ml] === input[si + ml] && ml < 255) ml++;
input[p + ml] === input[si + ml] && ml < MAX_MATCH) ml++;
if (ml >= MIN_MATCH && ml > bestLen) { bestLen = ml; bestOff = si - p; }
}
if (bestLen >= MIN_MATCH && (si - litStart) <= 15) {
// 有匹配 → 输出组合 token(字面量+匹配)
// 仅当匹配完整可编码(field 1-15)且字面量不超过 15 时才输出组合 token
if (bestLen > MIN_MATCH && (si - litStart) <= 15) {
const litLen = si - litStart;
const matchField = Math.min(bestLen - MIN_MATCH, 15);
const matchField = bestLen - MIN_MATCH; // 1..15
out[di++] = ((litLen & 0x0F) << 4) | (matchField & 0x0F);
for (let j = 0; j < litLen; j++) out[di++] = input[litStart + j];
out[di++] = bestOff & 0xFF;
@@ -42,8 +51,15 @@ export function compressLZ4(input: Uint8Array): Uint8Array {
si += bestLen;
litStart = si;
} else {
// 无匹配或字面量已满 15 → 继续累积(不单独输出,等下个匹配合并)
// 无匹配 / 匹配长度 4(field=0 有歧义)→ 继续累积字面量
si++;
// 字面量达到 15 字节上限:结清为纯字面量 token(lo=0),
// 否则后续组合 token 的字面量长度会超过 token 字段上限
if (si - litStart >= 15) {
out[di++] = (15 & 0x0F) << 4; // lo=0 无匹配
for (let j = 0; j < 15; j++) out[di++] = input[litStart + j];
litStart = si;
}
}
}
@@ -57,7 +73,10 @@ export function compressLZ4(input: Uint8Array): Uint8Array {
litStart += chunk;
}
return di >= input.byteLength ? input : out.slice(0, di);
// 始终输出压缩流(即使比原数据略大)。
// 注意:不能返回原样 input —— 解压端无法区分"压缩流"与"原始数据"
// 原样返回会导致解压器将原始字节误解析为 token(v0.2.6 修复)
return out.slice(0, di);
}
export function decompressLZ4(input: Uint8Array, originalSize: number): Uint8Array {
@@ -74,16 +93,17 @@ export function decompressLZ4(input: Uint8Array, originalSize: number): Uint8Arr
out[di++] = input[si++];
}
if (di >= originalSize || si >= input.byteLength) break;
// matchField=0:纯字面量 token(无 offset 无匹配)。
// 可能出现在流中任意位置(超长字面量分块输出),不能 break
if (matchField === 0) continue;
// 非末尾 → 必有 offset + 匹配(即使 matchField==0 也复制 MIN_MATCH 字节
if (si + 1 < input.byteLength) {
const offset = input[si++] | (input[si++] << 8);
const matchLen = matchField + MIN_MATCH;
for (let i = 0; i < matchLen && di < originalSize; i++) {
out[di] = out[di - offset];
di++;
}
// 组合 token:读取 offset + 复制匹配(可能自重叠
if (si + 1 >= input.byteLength) break;
const offset = input[si++] | (input[si++] << 8);
const matchLen = matchField + MIN_MATCH;
for (let i = 0; i < matchLen && di < originalSize; i++) {
out[di] = out[di - offset];
di++;
}
}
+1195 -1080
View File
File diff suppressed because it is too large Load Diff
+123 -123
View File
@@ -1,123 +1,123 @@
/**
* 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;
}
/** 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);
}
}
File diff suppressed because it is too large Load Diff
+467 -467
View File
@@ -1,467 +1,467 @@
/**
* 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;
}
/** 检查 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;
}
}
+192 -189
View File
@@ -1,189 +1,192 @@
/**
* 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 node = this.heap.pop()!;
const key = node.key;
const value = node.value;
// 刷新来源的下一个值
this.seedFromSource(node.sourceIndex);
// 跳过重复 key取最新的(堆顶的即是最新的,因为来源下标越小越新)
while (this.heap.peek() && this.heap.peek()!.key === key) {
const dup = this.heap.pop()!;
this.seedFromSource(dup.sourceIndex);
}
return [key, 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;
}
}
/** 回调数据源的迭代器 */
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,
});
}
}
}
+261 -261
View File
@@ -1,261 +1,261 @@
/**
* AriaEngine SSTable Reader — 从 SSTable 二进制数据中读取
* @module engine/aria/index/sstable
*/
import type { IndexEntry, SSTableMeta } from '../types';
import { BloomFilter } from './bloom';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
// ---------------------------------------------------------------------------
// SSTableReader
// ---------------------------------------------------------------------------
export class SSTableReader {
private data: Uint8Array;
private view: DataView;
private indexEntries: IndexEntry[] = [];
private entryCount = 0;
private meta: SSTableMeta;
private bloomFilter: BloomFilter | null = null;
constructor(data: Uint8Array, meta: SSTableMeta) {
this.data = data;
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
this.meta = meta;
this.parseFooter();
}
// -----------------------------------------------------------------------
// 查询
// -----------------------------------------------------------------------
/** 精确查找 key */
get(targetKey: string): Record<string, unknown> | null {
// Bloom Filter 快速否定
if (this.bloomFilter && !this.bloomFilter.mayContain(targetKey)) return null;
const blockIdx = this.locateBlock(targetKey);
if (blockIdx < 0) return null;
const entry = this.indexEntries[blockIdx];
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const entryCount = blockView.getUint32(0, false);
let offset = 4;
// 顺序扫描 block 内的条目(生产中应二分查找)
for (let i = 0; i < entryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key === targetKey) {
try {
return JSON.parse(new TextDecoder().decode(valBytes));
} catch {
return null;
}
}
}
return null;
}
/** 范围扫描 */
rangeScan(
startKey: string,
endKey: string,
callback: (key: string, value: Record<string, unknown>) => void,
): void {
if (this.indexEntries.length === 0) return;
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx) return;
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
const entry = this.indexEntries[bi];
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key >= startKey && key <= endKey) {
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
} catch {
// skip corrupted entry
}
}
}
}
}
/** 扫描所有条目 */
scanAll(callback: (key: string, value: Record<string, unknown>) => void): void {
for (const entry of this.indexEntries) {
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
} catch {
// skip corrupted entry
}
}
}
}
/** 获取元数据 */
getMeta(): SSTableMeta {
return this.meta;
}
/** 获取索引条目数 */
getIndexBlockCount(): number {
return this.indexEntries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private parseFooter(): void {
if (this.data.byteLength < 32) {
throw new Error('SSTable too small: missing footer');
}
const footerOffset = this.data.byteLength - 32;
// 验证魔数
const magic = this.view.getUint32(footerOffset + 24, false);
if (magic !== SSTABLE_MAGIC) {
throw new Error(`Invalid SSTable magic: expected ${SSTABLE_MAGIC}, got ${magic}`);
}
const indexOffset = this.view.getUint32(footerOffset, false);
const indexSize = this.view.getUint32(footerOffset + 4, false);
const bloomOffset = this.view.getUint32(footerOffset + 8, false);
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.parseIndexBlock(indexOffset, indexSize);
// 加载 Bloom Filter
if (bloomOffset > 0 && bloomSize > 0 && bloomOffset + bloomSize <= this.data.byteLength) {
try {
const bloomBytes = this.data.slice(bloomOffset, bloomOffset + bloomSize);
this.bloomFilter = BloomFilter.fromData(bloomBytes, bloomHashCount || 10);
} catch {
// 损坏的 bloom filter 不影响读取(仅跳过快速否定优化)
}
}
}
private parseIndexBlock(offset: number, _size: number): void {
const entryCount = this.view.getUint32(offset, false);
offset += 4;
for (let i = 0; i < entryCount; i++) {
const keyLen = this.view.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(this.data.slice(offset, offset + keyLen));
offset += keyLen;
const blockOffset = this.view.getUint32(offset, false);
offset += 4;
const blockSize = this.view.getUint32(offset, false);
offset += 4;
this.indexEntries.push({ key, blockOffset, blockSize });
}
}
/** 二分查找某 key 所在的 block 索引 */
private locateBlock(key: string): number {
let lo = 0;
let hi = this.indexEntries.length - 1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const entry = this.indexEntries[mid];
if (key <= entry.key) {
// 检查是否在此 block 范围内
const firstKey = mid === 0 ? '' : this.indexEntries[mid - 1].key;
if (key > firstKey) return mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return -1;
}
private locateBlockGE(key: string): number {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key < key) lo = mid + 1;
else hi = mid;
}
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
}
private locateBlockLE(key: string): number {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key <= key) lo = mid + 1;
else hi = mid;
}
return lo > 0 ? lo - 1 : 0;
}
}
/**
* AriaEngine SSTable Reader — 从 SSTable 二进制数据中读取
* @module engine/aria/index/sstable
*/
import type { IndexEntry, SSTableMeta } from '../types';
import { BloomFilter } from './bloom';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
// ---------------------------------------------------------------------------
// SSTableReader
// ---------------------------------------------------------------------------
export class SSTableReader {
private data: Uint8Array;
private view: DataView;
private indexEntries: IndexEntry[] = [];
private entryCount = 0;
private meta: SSTableMeta;
private bloomFilter: BloomFilter | null = null;
constructor(data: Uint8Array, meta: SSTableMeta) {
this.data = data;
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
this.meta = meta;
this.parseFooter();
}
// -----------------------------------------------------------------------
// 查询
// -----------------------------------------------------------------------
/** 精确查找 key */
get(targetKey: string): Record<string, unknown> | null {
// Bloom Filter 快速否定
if (this.bloomFilter && !this.bloomFilter.mayContain(targetKey)) return null;
const blockIdx = this.locateBlock(targetKey);
if (blockIdx < 0) return null;
const entry = this.indexEntries[blockIdx];
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const entryCount = blockView.getUint32(0, false);
let offset = 4;
// 顺序扫描 block 内的条目(生产中应二分查找)
for (let i = 0; i < entryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key === targetKey) {
try {
return JSON.parse(new TextDecoder().decode(valBytes));
} catch {
return null;
}
}
}
return null;
}
/** 范围扫描 */
rangeScan(
startKey: string,
endKey: string,
callback: (key: string, value: Record<string, unknown>) => void,
): void {
if (this.indexEntries.length === 0) return;
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx) return;
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
const entry = this.indexEntries[bi];
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key >= startKey && key <= endKey) {
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
} catch {
// skip corrupted entry
}
}
}
}
}
/** 扫描所有条目 */
scanAll(callback: (key: string, value: Record<string, unknown>) => void): void {
for (const entry of this.indexEntries) {
const blockData = new Uint8Array(
this.data.buffer,
this.data.byteOffset + entry.blockOffset,
entry.blockSize,
);
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = blockView.getUint16(offset, false);
offset += 2;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
} catch {
// skip corrupted entry
}
}
}
}
/** 获取元数据 */
getMeta(): SSTableMeta {
return this.meta;
}
/** 获取索引条目数 */
getIndexBlockCount(): number {
return this.indexEntries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private parseFooter(): void {
if (this.data.byteLength < 32) {
throw new Error('SSTable too small: missing footer');
}
const footerOffset = this.data.byteLength - 32;
// 验证魔数
const magic = this.view.getUint32(footerOffset + 24, false);
if (magic !== SSTABLE_MAGIC) {
throw new Error(`Invalid SSTable magic: expected ${SSTABLE_MAGIC}, got ${magic}`);
}
const indexOffset = this.view.getUint32(footerOffset, false);
const indexSize = this.view.getUint32(footerOffset + 4, false);
const bloomOffset = this.view.getUint32(footerOffset + 8, false);
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.parseIndexBlock(indexOffset, indexSize);
// 加载 Bloom Filter
if (bloomOffset > 0 && bloomSize > 0 && bloomOffset + bloomSize <= this.data.byteLength) {
try {
const bloomBytes = this.data.slice(bloomOffset, bloomOffset + bloomSize);
this.bloomFilter = BloomFilter.fromData(bloomBytes, bloomHashCount || 10);
} catch {
// 损坏的 bloom filter 不影响读取(仅跳过快速否定优化)
}
}
}
private parseIndexBlock(offset: number, _size: number): void {
const entryCount = this.view.getUint32(offset, false);
offset += 4;
for (let i = 0; i < entryCount; i++) {
const keyLen = this.view.getUint16(offset, false);
offset += 2;
const key = new TextDecoder().decode(this.data.slice(offset, offset + keyLen));
offset += keyLen;
const blockOffset = this.view.getUint32(offset, false);
offset += 4;
const blockSize = this.view.getUint32(offset, false);
offset += 4;
this.indexEntries.push({ key, blockOffset, blockSize });
}
}
/** 二分查找某 key 所在的 block 索引 */
private locateBlock(key: string): number {
let lo = 0;
let hi = this.indexEntries.length - 1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const entry = this.indexEntries[mid];
if (key <= entry.key) {
// 检查是否在此 block 范围内
const firstKey = mid === 0 ? '' : this.indexEntries[mid - 1].key;
if (key > firstKey) return mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return -1;
}
private locateBlockGE(key: string): number {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key < key) lo = mid + 1;
else hi = mid;
}
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
}
private locateBlockLE(key: string): number {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key <= key) lo = mid + 1;
else hi = mid;
}
return lo > 0 ? lo - 1 : 0;
}
}
+247 -249
View File
@@ -1,249 +1,247 @@
/**
* AriaEngine SSTable Builder — 构建有序字符串表
* @module engine/aria/index/sstable_builder
*
* 将排序后的 key-value 数据写入 SSTable 格式。
*
* SSTable 文件布局:
* ┌──────────────────────────────────────────────┐
* │ Data Block 0 │
* │ Data Block 1 │
* │ ... │
* │ Index Block (block offset → key range) │
* │ Bloom Filter │
* │ Footer (32 bytes) │
* │ - index_offset (u32) │
* │ - index_size (u32) │
* │ - bloom_offset (u32) │
* │ - bloom_size (u32) │
* │ - bloom_hash_count (u32) │
* │ - entry_count (u32) │
* │ - magic_number (u32, 0x53535442 ="SSTB")│
* │ - checksum (u32) │
* └──────────────────────────────────────────────┘
*/
import { BloomFilter } from './bloom';
import type { IndexEntry, DataBlock } from '../types';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
const SSTABLE_FOOTER_SIZE = 32;
// ---------------------------------------------------------------------------
// SSTableBuilder
// ---------------------------------------------------------------------------
export class SSTableBuilder {
private entries: [string, Record<string, unknown>][] = [];
private currentBlock: [string, Record<string, unknown>][] = [];
private currentBlockStartKey = '';
private blockSizeLimit: number;
constructor(blockSizeLimit: number = 4096) {
this.blockSizeLimit = blockSizeLimit;
}
/** 添加一个 key-value 条目(必须按键排序添加) */
add(key: string, value: Record<string, unknown>): void {
if (this.currentBlock.length === 0) {
this.currentBlockStartKey = key;
}
this.currentBlock.push([key, value]);
this.entries.push([key, value]);
// 如果当前 Block 达到大小限制,切割
const estimated = this.estimateBlockSize();
if (estimated >= this.blockSizeLimit && this.currentBlock.length > 1) {
// 当前 block 结束(不在这里切割,在 build 时统一处理)
}
}
/**
* 构建 SSTable 文件的二进制数据。
* 返回 { data: Uint8Array, indexEntries: IndexEntry[], bloomFilter: BloomFilter }
*/
build(): { sstableData: Uint8Array; indexEntries: IndexEntry[] } {
const blocks = this.splitIntoBlocks();
const bloomFilter = new BloomFilter(this.entries.length);
// 预计算总大小
let totalSize = 0;
const blockOffsets: number[] = [];
for (const block of blocks) {
blockOffsets.push(totalSize);
const blockSize = this.computeBlockSize(block);
totalSize += blockSize;
}
// 索引块
const indexEntries: IndexEntry[] = [];
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
const lastKey = block[block.length - 1][0];
const blockSize = this.computeBlockSize(block);
indexEntries.push({
key: lastKey,
blockOffset: blockOffsets[i],
blockSize,
});
}
const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
// 序列化 bloom filter 以获取其大小
const bloomData = bloomFilter.serialize();
const bloomSize = bloomData.byteLength;
// 写入到 buffer(包含 bloom block
const finalSize = totalSize + indexBlockSize + bloomSize + SSTABLE_FOOTER_SIZE;
const buf = new ArrayBuffer(finalSize);
const view = new DataView(buf);
let offset = 0;
// ---- Data Blocks ----
for (const block of blocks) {
offset = this.writeDataBlock(view, offset, block, bloomFilter);
}
// ---- Index Block ----
const indexOffset = offset;
offset = this.writeIndexBlock(view, offset, indexEntries);
// ---- Bloom Filter Block ----
const bloomOffset = offset;
new Uint8Array(view.buffer).set(bloomData, offset);
offset += bloomSize;
// ---- Footer ----
const footerOffset = offset;
view.setUint32(footerOffset, indexOffset, false); // index_offset
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
view.setUint32(footerOffset + 8, bloomOffset, false); // bloom_offset
view.setUint32(footerOffset + 12, bloomSize, false); // bloom_size
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
view.setUint32(footerOffset + 20, this.entries.length, false);
view.setUint32(footerOffset + 24, SSTABLE_MAGIC, false);
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
return {
sstableData: new Uint8Array(buf),
indexEntries,
};
}
/** 获取条目数 */
getEntryCount(): number {
return this.entries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private splitIntoBlocks(): [string, Record<string, unknown>][][] {
const blocks: [string, Record<string, unknown>][][] = [];
let current: [string, Record<string, unknown>][] = [];
for (const entry of this.entries) {
current.push(entry);
if (this.estimateBlockSizeFromEntries(current) >= this.blockSizeLimit && current.length > 1) {
blocks.push(current.slice(0, -1));
current = [entry];
}
}
if (current.length > 0) blocks.push(current);
return blocks;
}
private estimateBlockSize(): number {
return this.estimateBlockSizeFromEntries(this.currentBlock);
}
private estimateBlockSizeFromEntries(entries: [string, unknown][]): number {
let size = 0;
for (const [key, value] of entries) {
size += 4 + key.length + JSON.stringify(value).length;
}
return size;
}
private computeBlockSize(block: [string, unknown][]): number {
// entryCount (u32) + 每对: keyLen(u16) + key + valueLen(u16) + value json
let size = 4;
for (const [key, value] of block) {
const json = JSON.stringify(value);
size += 2 + key.length + 2 + json.length;
}
return size;
}
private writeDataBlock(
view: DataView,
offset: number,
block: [string, Record<string, unknown>][],
bloomFilter: BloomFilter,
): number {
const start = offset;
// entry count
view.setUint32(offset, block.length, false);
offset += 4;
for (const [key, value] of block) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(key);
const valueBytes = encoder.encode(JSON.stringify(value));
// key length
view.setUint16(offset, keyBytes.length, false);
offset += 2;
// key
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
// value length
view.setUint16(offset, valueBytes.length, false);
offset += 2;
// value
new Uint8Array(view.buffer).set(valueBytes, offset);
offset += valueBytes.length;
// 插入 bloom filter
bloomFilter.insert(key);
}
return offset;
}
private estimateIndexBlockSize(entries: IndexEntry[]): number {
// entryCount(u32) + each: keyLen(u16)+key+blockOffset(u32)+blockSize(u32)
let size = 4;
for (const entry of entries) {
size += 2 + entry.key.length + 8;
}
return size;
}
private writeIndexBlock(view: DataView, offset: number, entries: IndexEntry[]): number {
view.setUint32(offset, entries.length, false);
offset += 4;
for (const entry of entries) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(entry.key);
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, entry.blockOffset, false);
offset += 4;
view.setUint32(offset, entry.blockSize, false);
offset += 4;
}
return offset;
}
}
/**
* AriaEngine SSTable Builder — 构建有序字符串表
* @module engine/aria/index/sstable_builder
*
* 将排序后的 key-value 数据写入 SSTable 格式。
*
* SSTable 文件布局:
* ┌──────────────────────────────────────────────┐
* │ Data Block 0 │
* │ Data Block 1 │
* │ ... │
* │ Index Block (block offset → key range) │
* │ Bloom Filter │
* │ Footer (32 bytes) │
* │ - index_offset (u32) │
* │ - index_size (u32) │
* │ - bloom_offset (u32) │
* │ - bloom_size (u32) │
* │ - bloom_hash_count (u32) │
* │ - entry_count (u32) │
* │ - magic_number (u32, 0x53535442 ="SSTB")│
* │ - checksum (u32) │
* └──────────────────────────────────────────────┘
*/
import { BloomFilter } from './bloom';
import type { IndexEntry } from '../types';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
const SSTABLE_FOOTER_SIZE = 32;
// ---------------------------------------------------------------------------
// SSTableBuilder
// ---------------------------------------------------------------------------
export class SSTableBuilder {
private entries: [string, Record<string, unknown>][] = [];
private currentBlock: [string, Record<string, unknown>][] = [];
private currentBlockStartKey = '';
private blockSizeLimit: number;
constructor(blockSizeLimit: number = 4096) {
this.blockSizeLimit = blockSizeLimit;
}
/** 添加一个 key-value 条目(必须按键排序添加) */
add(key: string, value: Record<string, unknown>): void {
if (this.currentBlock.length === 0) {
this.currentBlockStartKey = key;
}
this.currentBlock.push([key, value]);
this.entries.push([key, value]);
// 如果当前 Block 达到大小限制,切割
const estimated = this.estimateBlockSize();
if (estimated >= this.blockSizeLimit && this.currentBlock.length > 1) {
// 当前 block 结束(不在这里切割,在 build 时统一处理)
}
}
/**
* 构建 SSTable 文件的二进制数据。
* 返回 { data: Uint8Array, indexEntries: IndexEntry[], bloomFilter: BloomFilter }
*/
build(): { sstableData: Uint8Array; indexEntries: IndexEntry[] } {
const blocks = this.splitIntoBlocks();
const bloomFilter = new BloomFilter(this.entries.length);
// 预计算总大小
let totalSize = 0;
const blockOffsets: number[] = [];
for (const block of blocks) {
blockOffsets.push(totalSize);
const blockSize = this.computeBlockSize(block);
totalSize += blockSize;
}
// 索引块
const indexEntries: IndexEntry[] = [];
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
const lastKey = block[block.length - 1][0];
const blockSize = this.computeBlockSize(block);
indexEntries.push({
key: lastKey,
blockOffset: blockOffsets[i],
blockSize,
});
}
const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
// 序列化 bloom filter 以获取其大小
const bloomData = bloomFilter.serialize();
const bloomSize = bloomData.byteLength;
// 写入到 buffer(包含 bloom block
const finalSize = totalSize + indexBlockSize + bloomSize + SSTABLE_FOOTER_SIZE;
const buf = new ArrayBuffer(finalSize);
const view = new DataView(buf);
let offset = 0;
// ---- Data Blocks ----
for (const block of blocks) {
offset = this.writeDataBlock(view, offset, block, bloomFilter);
}
// ---- Index Block ----
const indexOffset = offset;
offset = this.writeIndexBlock(view, offset, indexEntries);
// ---- Bloom Filter Block ----
const bloomOffset = offset;
new Uint8Array(view.buffer).set(bloomData, offset);
offset += bloomSize;
// ---- Footer ----
const footerOffset = offset;
view.setUint32(footerOffset, indexOffset, false); // index_offset
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
view.setUint32(footerOffset + 8, bloomOffset, false); // bloom_offset
view.setUint32(footerOffset + 12, bloomSize, false); // bloom_size
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
view.setUint32(footerOffset + 20, this.entries.length, false);
view.setUint32(footerOffset + 24, SSTABLE_MAGIC, false);
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
return {
sstableData: new Uint8Array(buf),
indexEntries,
};
}
/** 获取条目数 */
getEntryCount(): number {
return this.entries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
private splitIntoBlocks(): [string, Record<string, unknown>][][] {
const blocks: [string, Record<string, unknown>][][] = [];
let current: [string, Record<string, unknown>][] = [];
for (const entry of this.entries) {
current.push(entry);
if (this.estimateBlockSizeFromEntries(current) >= this.blockSizeLimit && current.length > 1) {
blocks.push(current.slice(0, -1));
current = [entry];
}
}
if (current.length > 0) blocks.push(current);
return blocks;
}
private estimateBlockSize(): number {
return this.estimateBlockSizeFromEntries(this.currentBlock);
}
private estimateBlockSizeFromEntries(entries: [string, unknown][]): number {
let size = 0;
for (const [key, value] of entries) {
size += 4 + key.length + JSON.stringify(value).length;
}
return size;
}
private computeBlockSize(block: [string, unknown][]): number {
// entryCount (u32) + 每对: keyLen(u16) + key + valueLen(u16) + value json
let size = 4;
for (const [key, value] of block) {
const json = JSON.stringify(value);
size += 2 + key.length + 2 + json.length;
}
return size;
}
private writeDataBlock(
view: DataView,
offset: number,
block: [string, Record<string, unknown>][],
bloomFilter: BloomFilter,
): number {
// entry count
view.setUint32(offset, block.length, false);
offset += 4;
for (const [key, value] of block) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(key);
const valueBytes = encoder.encode(JSON.stringify(value));
// key length
view.setUint16(offset, keyBytes.length, false);
offset += 2;
// key
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
// value length
view.setUint16(offset, valueBytes.length, false);
offset += 2;
// value
new Uint8Array(view.buffer).set(valueBytes, offset);
offset += valueBytes.length;
// 插入 bloom filter
bloomFilter.insert(key);
}
return offset;
}
private estimateIndexBlockSize(entries: IndexEntry[]): number {
// entryCount(u32) + each: keyLen(u16)+key+blockOffset(u32)+blockSize(u32)
let size = 4;
for (const entry of entries) {
size += 2 + entry.key.length + 8;
}
return size;
}
private writeIndexBlock(view: DataView, offset: number, entries: IndexEntry[]): number {
view.setUint32(offset, entries.length, false);
offset += 4;
for (const entry of entries) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(entry.key);
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, entry.blockOffset, false);
offset += 4;
view.setUint32(offset, entry.blockSize, false);
offset += 4;
}
return offset;
}
}
+164 -167
View File
@@ -1,167 +1,164 @@
/**
* AriaEngine Page Format — 页面格式整合层
* @module engine/aria/page/format
*
* 组合 Header / Slot / Tuple 操作,提供统一的页面管理接口。
*/
import { PAGE_SIZE, PageType, type PageHandle } from '../types';
import {
initPageHeader,
decodePageHeader,
encodePageHeader,
getSlotCount,
getPageId,
} from './header';
import { allocateSlot, freeSlot, readSlotData, getAllSlots } 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,可能会在不同位置
const newSlot = 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
*
* 组合 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;
}
+97 -97
View File
@@ -1,97 +1,97 @@
/**
* 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
*/
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);
}
+184 -184
View File
@@ -1,184 +1,184 @@
/**
* 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);
}
/**
* 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 -252
View File
@@ -1,252 +1,251 @@
/**
* 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;
const nullBitmap = bytes.slice(offset, offset + nullBitmapBytes);
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;
}
/**
* 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;
}
+179 -179
View File
@@ -1,179 +1,179 @@
/**
* AriaEngine Storage Backend — 存储后端抽象层
* @module engine/aria/store/backend
*
* 封装底层浏览器存储 APIIndexedDB / OPFS / Memory 回退),
* 供 Buffer Pool 的 PageIO 和 WAL 的 WALStore 使用。
*/
import { DatabaseError } from '../../../constants';
// ---------------------------------------------------------------------------
// StorageBackend 接口
// ---------------------------------------------------------------------------
export interface IStorageBackend {
/** 打开存储 */
open(name: string): Promise<void>;
/** 关闭存储 */
close(): Promise<void>;
/** 是否已打开 */
isOpen(): boolean;
/** 读取数据块 */
read(key: string): Promise<ArrayBuffer | null>;
/** 写入数据块 */
write(key: string, data: ArrayBuffer): Promise<void>;
/** 删除数据块 */
delete(key: string): Promise<void>;
/** 列出所有 key */
listKeys(): Promise<string[]>;
/** 检查 key 是否存在 */
exists(key: string): Promise<boolean>;
/** 清空所有数据 */
clear(): Promise<void>;
}
// =======================================================================
// IndexedDB Backend
// =======================================================================
export class IndexedDBBackend implements IStorageBackend {
private db: IDBDatabase | null = null;
private dbName = '';
private storeName = 'data';
async open(name: string): Promise<void> {
this.dbName = `aria-${name}`;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onerror = () => reject(new DatabaseError('Failed to open AriaEngine IndexedDB', 'ARIA_IDB_OPEN_ERROR'));
});
}
async close(): Promise<void> {
if (this.db) {
this.db.close();
this.db = null;
}
}
isOpen(): boolean {
return this.db !== null;
}
async read(key: string): Promise<ArrayBuffer | null> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).get(key);
req.onsuccess = () => resolve(req.result ?? null);
req.onerror = () => reject(new DatabaseError('Failed to read from AriaEngine store', 'ARIA_READ_ERROR'));
});
}
async write(key: string, data: ArrayBuffer): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put(data, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to write to AriaEngine store', 'ARIA_WRITE_ERROR'));
});
}
async delete(key: string): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to delete from AriaEngine store', 'ARIA_DELETE_ERROR'));
});
}
async listKeys(): Promise<string[]> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).getAllKeys();
req.onsuccess = () => resolve((req.result ?? []) as string[]);
req.onerror = () => reject(new DatabaseError('Failed to list keys', 'ARIA_LIST_ERROR'));
});
}
async exists(key: string): Promise<boolean> {
const result = await this.read(key);
return result !== null;
}
async clear(): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).clear();
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to clear AriaEngine store', 'ARIA_CLEAR_ERROR'));
});
}
private ensureDB(): IDBDatabase {
if (!this.db) throw new DatabaseError('AriaEngine storage not opened', 'ARIA_DB_NOT_OPEN');
return this.db;
}
}
// =======================================================================
// Memory Backend(回退 / 测试用)
// =======================================================================
export class MemoryBackend implements IStorageBackend {
private store: Map<string, ArrayBuffer> = new Map();
private opened = false;
async open(_name: string): Promise<void> {
this.opened = true;
}
async close(): Promise<void> {
this.store.clear();
this.opened = false;
}
isOpen(): boolean {
return this.opened;
}
async read(key: string): Promise<ArrayBuffer | null> {
return this.store.get(key) ?? null;
}
async write(key: string, data: ArrayBuffer): Promise<void> {
this.store.set(key, data);
}
async delete(key: string): Promise<void> {
this.store.delete(key);
}
async listKeys(): Promise<string[]> {
return Array.from(this.store.keys());
}
async exists(key: string): Promise<boolean> {
return this.store.has(key);
}
async clear(): Promise<void> {
this.store.clear();
}
}
/**
* AriaEngine Storage Backend — 存储后端抽象层
* @module engine/aria/store/backend
*
* 封装底层浏览器存储 APIIndexedDB / OPFS / Memory 回退),
* 供 Buffer Pool 的 PageIO 和 WAL 的 WALStore 使用。
*/
import { DatabaseError } from '../../../constants';
// ---------------------------------------------------------------------------
// StorageBackend 接口
// ---------------------------------------------------------------------------
export interface IStorageBackend {
/** 打开存储 */
open(name: string): Promise<void>;
/** 关闭存储 */
close(): Promise<void>;
/** 是否已打开 */
isOpen(): boolean;
/** 读取数据块 */
read(key: string): Promise<ArrayBuffer | null>;
/** 写入数据块 */
write(key: string, data: ArrayBuffer): Promise<void>;
/** 删除数据块 */
delete(key: string): Promise<void>;
/** 列出所有 key */
listKeys(): Promise<string[]>;
/** 检查 key 是否存在 */
exists(key: string): Promise<boolean>;
/** 清空所有数据 */
clear(): Promise<void>;
}
// =======================================================================
// IndexedDB Backend
// =======================================================================
export class IndexedDBBackend implements IStorageBackend {
private db: IDBDatabase | null = null;
private dbName = '';
private storeName = 'data';
async open(name: string): Promise<void> {
this.dbName = `aria-${name}`;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onerror = () => reject(new DatabaseError('Failed to open AriaEngine IndexedDB', 'ARIA_IDB_OPEN_ERROR'));
});
}
async close(): Promise<void> {
if (this.db) {
this.db.close();
this.db = null;
}
}
isOpen(): boolean {
return this.db !== null;
}
async read(key: string): Promise<ArrayBuffer | null> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).get(key);
req.onsuccess = () => resolve(req.result ?? null);
req.onerror = () => reject(new DatabaseError('Failed to read from AriaEngine store', 'ARIA_READ_ERROR'));
});
}
async write(key: string, data: ArrayBuffer): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put(data, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to write to AriaEngine store', 'ARIA_WRITE_ERROR'));
});
}
async delete(key: string): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to delete from AriaEngine store', 'ARIA_DELETE_ERROR'));
});
}
async listKeys(): Promise<string[]> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).getAllKeys();
req.onsuccess = () => resolve((req.result ?? []) as string[]);
req.onerror = () => reject(new DatabaseError('Failed to list keys', 'ARIA_LIST_ERROR'));
});
}
async exists(key: string): Promise<boolean> {
const result = await this.read(key);
return result !== null;
}
async clear(): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).clear();
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to clear AriaEngine store', 'ARIA_CLEAR_ERROR'));
});
}
private ensureDB(): IDBDatabase {
if (!this.db) throw new DatabaseError('AriaEngine storage not opened', 'ARIA_DB_NOT_OPEN');
return this.db;
}
}
// =======================================================================
// Memory Backend(回退 / 测试用)
// =======================================================================
export class MemoryBackend implements IStorageBackend {
private store: Map<string, ArrayBuffer> = new Map();
private opened = false;
async open(_name: string): Promise<void> {
this.opened = true;
}
async close(): Promise<void> {
this.store.clear();
this.opened = false;
}
isOpen(): boolean {
return this.opened;
}
async read(key: string): Promise<ArrayBuffer | null> {
return this.store.get(key) ?? null;
}
async write(key: string, data: ArrayBuffer): Promise<void> {
this.store.set(key, data);
}
async delete(key: string): Promise<void> {
this.store.delete(key);
}
async listKeys(): Promise<string[]> {
return Array.from(this.store.keys());
}
async exists(key: string): Promise<boolean> {
return this.store.has(key);
}
async clear(): Promise<void> {
this.store.clear();
}
}
+111 -111
View File
@@ -1,111 +1,111 @@
/**
* 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) {
// 第一次访问:创建新页面
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();
}
}
+243 -243
View File
@@ -1,243 +1,243 @@
/**
* 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;
// =======================================================================
// 事务管理
// =======================================================================
/** 开始一个事务,返回事务 ID */
beginTransaction(): number {
const txnId = this.nextTxnId++;
this.activeTxns.set(txnId, {
txnId,
state: TransactionState.ACTIVE,
snapshotLsn: this.globalCommitLsn,
startTime: Date.now(),
});
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++;
// 标记此事务写入的所有版本为已提交
for (const [, versions] of this.versionStore) {
for (const version of versions) {
if (version.txnId === txnId) {
version.committed = true;
}
}
}
// 清理已提交事务的记录
this.activeTxns.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;
// 移除此事务写入的所有版本
for (const [tableKey, versions] of this.versionStore) {
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);
}
/** 检查事务是否活跃 */
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);
}
/**
* 读取一行(对指定事务可见的最新版本)。
*/
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;
}
/**
* 清理过旧版本(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;
// =======================================================================
// 事务管理
// =======================================================================
/** 开始一个事务,返回事务 ID */
beginTransaction(): number {
const txnId = this.nextTxnId++;
this.activeTxns.set(txnId, {
txnId,
state: TransactionState.ACTIVE,
snapshotLsn: this.globalCommitLsn,
startTime: Date.now(),
});
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++;
// 标记此事务写入的所有版本为已提交
for (const [, versions] of this.versionStore) {
for (const version of versions) {
if (version.txnId === txnId) {
version.committed = true;
}
}
}
// 清理已提交事务的记录
this.activeTxns.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;
// 移除此事务写入的所有版本
for (const [tableKey, versions] of this.versionStore) {
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);
}
/** 检查事务是否活跃 */
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);
}
/**
* 读取一行(对指定事务可见的最新版本)。
*/
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;
}
/**
* 清理过旧版本(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;
}
}
+302 -302
View File
@@ -1,302 +1,302 @@
/**
* 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;
/** 每个 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,
};
+77 -77
View File
@@ -1,77 +1,77 @@
/**
* 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 count = typeof this.wal.getBufferedCount === 'function' ? this.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 count = typeof this.wal.getBufferedCount === 'function' ? this.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;
}
}
+313 -283
View File
@@ -1,283 +1,313 @@
/**
* 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 type { BufferPool } from '../buffer/pool';
// ---------------------------------------------------------------------------
// 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';
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);
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(bytes);
}
// 'none' mode: 不写 WAL
}
/** 批量刷新缓冲的 WAL 记录 */
async flush(): Promise<void> {
if (!this.enabled || this.buffer.length === 0) return;
const totalLen = this.buffer.reduce((sum, b) => sum + b.byteLength, 0);
const combined = new Uint8Array(totalLen);
let offset = 0;
for (const buf of this.buffer) {
combined.set(buf, offset);
offset += buf.byteLength;
}
await this.store.append(combined);
this.buffer = [];
}
// =======================================================================
// 恢复
// =======================================================================
/** 从 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;
}
// =======================================================================
// 统计
// =======================================================================
isEnabled(): boolean {
return this.enabled;
}
getLSN(): number {
return this.lsn;
}
getBufferedCount(): number {
return this.buffer.length;
}
// -----------------------------------------------------------------------
// 编解码
// -----------------------------------------------------------------------
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 不匹配,跳过此损坏记录
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';
// ---------------------------------------------------------------------------
// 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';
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);
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append record');
}
} else if (this.syncMode === 'batch') {
this.buffer.push(bytes);
}
// '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);
} 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);
}
// '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;
}
// =======================================================================
// 统计
// =======================================================================
isEnabled(): boolean {
return this.enabled;
}
getLSN(): number {
return this.lsn;
}
getBufferedCount(): number {
return this.buffer.length;
}
// -----------------------------------------------------------------------
// 编解码
// -----------------------------------------------------------------------
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;
}
}
+11 -11
View File
@@ -1,11 +1,11 @@
/**
* metona-sqlark Engine — 存储引擎层
* @module engine
*/
export type { IStorageEngine } from './interface';
export { MemoryEngine } from './memory';
export { IndexedDBEngine } from './indexeddb';
export { OPFSEngine } from './opfs';
export { AriaEngine } from './aria/index';
export type { AriaEngineConfig } from './aria/types';
/**
* metona-sqlark Engine — 存储引擎层
* @module engine
*/
export type { IStorageEngine } from './interface';
export { MemoryEngine } from './memory';
export { IndexedDBEngine } from './indexeddb';
export { OPFSEngine } from './opfs';
export { AriaEngine } from './aria/index';
export type { AriaEngineConfig } from './aria/types';
+153 -4
View File
@@ -27,7 +27,7 @@ export class IndexedDBEngine implements IStorageEngine {
await this.memoryCache.open(dbName, version);
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onsuccess = () => {
request.onsuccess = async () => {
this.db = request.result;
// 多标签页冲突处理:其他标签页升级版本时自动关闭当前连接
this.db.onversionchange = () => {
@@ -38,13 +38,89 @@ export class IndexedDBEngine implements IStorageEngine {
console.warn(`[metona-sqlark] Database "${dbName}" was upgraded in another tab. Connection closed. Please re-open.`);
}
};
resolve();
try {
// v0.3.2: reopen 后从 IDB 重建 schemaschema 此前只存内存缓存,重开连接即丢失)
await this.rebuildSchemaFromIDB();
resolve();
} catch (error) {
reject(new DatabaseError(`Failed to restore schema for "${dbName}"`, 'IDB_SCHEMA_RESTORE_ERROR', error));
}
};
request.onerror = () => reject(new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED'));
});
}
/**
* 从 IDB 恢复内存 schema
* 1. 优先读取持久化的 schema 记录('__metona_schema' storev0.3.2
* 2. 旧数据回退:从 objectStore 主键 / 索引 / 样例数据推断
*/
private async rebuildSchemaFromIDB(): Promise<void> {
const db = this.ensureDB();
// 1. 持久化 schema
if (db.objectStoreNames.contains('__metona_schema')) {
const records: { name: string; schema: string }[] = await new Promise((resolve, reject) => {
const req = db.transaction('__metona_schema', 'readonly').objectStore('__metona_schema').getAll();
req.onsuccess = () => resolve((req.result ?? []) as { name: string; schema: string }[]);
req.onerror = () => reject(req.error);
});
for (const rec of records) {
try {
const schema = JSON.parse(rec.schema) as TableSchema;
if (!(await this.memoryCache.getTableSchema(schema.name))) {
await this.memoryCache.createTable(schema);
}
} catch {
// 损坏的 schema 记录忽略
}
}
}
// 2. 回退:无持久化 schema 的表从 IDB 结构推断
const storeNames = Array.from(db.objectStoreNames).filter((n) => n !== '__metona_schema');
for (const tableName of storeNames) {
// 已有 schema(持久化恢复或连续 open)则跳过
const existing = await this.memoryCache.getTableSchema(tableName);
if (existing) continue;
const columns: Record<string, import('../constants').ColumnDef> = {};
const tx = db.transaction(tableName, 'readonly');
const store = tx.objectStore(tableName);
// 主键列
const pk = store.keyPath as string;
columns[pk] = { type: 'string', primaryKey: true };
// 索引列(idx_ 前缀约定)
for (const idxName of Array.from(store.indexNames)) {
if (idxName.startsWith('idx_')) {
const col = idxName.slice(4);
if (!columns[col]) {
columns[col] = { type: 'string', index: true };
}
}
}
// 从样例数据推断其余列的类型
const rows: Record<string, unknown>[] = await new Promise((resolve, reject) => {
const req = store.getAll();
req.onsuccess = () => resolve((req.result ?? []) as Record<string, unknown>[]);
req.onerror = () => reject(req.error);
});
if (rows.length > 0) {
for (const [key, value] of Object.entries(rows[0])) {
if (!columns[key]) {
columns[key] = { type: inferFieldType(value) };
}
}
}
await this.memoryCache.createTable({ name: tableName, columns });
}
}
async close(): Promise<void> {
if (this.db) {
this.db.onversionchange = null; // 清理监听器
@@ -113,6 +189,48 @@ export class IndexedDBEngine implements IStorageEngine {
await this.idbClear(tableName);
}
// ---- 动态索引(v0.3.0):通过版本升级创建/删除 IDB 索引 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
await this.memoryCache.createIndex(tableName, column, unique);
if (this.txActive) return;
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const idb = (event.target as IDBOpenDBRequest).result;
const tx = idb.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
if (!store.indexNames.contains(`idx_${column}`)) {
store.createIndex(`idx_${column}`, column, { unique: unique ?? false });
}
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to create index "${tableName}.${column}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
async dropIndex(tableName: string, column: string, _indexName?: string): Promise<void> {
await this.memoryCache.dropIndex(tableName, column);
if (this.txActive) return;
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const idb = (event.target as IDBOpenDBRequest).result;
const tx = idb.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
if (store.indexNames.contains(`idx_${column}`)) {
store.deleteIndex(`idx_${column}`);
}
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to drop index "${tableName}.${column}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
@@ -154,8 +272,19 @@ export class IndexedDBEngine implements IStorageEngine {
store.createIndex(`idx_${colName}`, colName, { unique: colDef.unique ?? false });
}
}
// v0.3.2: schema 持久化 store(记录在升级完成后的 onsuccess 写入)
if (!db.objectStoreNames.contains('__metona_schema')) {
db.createObjectStore('__metona_schema', { keyPath: 'name' });
}
};
request.onsuccess = () => {
this.db = request.result;
// v0.3.2: 升级完成后持久化 schema(upgrade 事务内异步写会失败)
const schemaTx = this.db.transaction('__metona_schema', 'readwrite');
schemaTx.objectStore('__metona_schema').put({ name: schema.name, schema: JSON.stringify(schema) });
schemaTx.oncomplete = () => resolve();
schemaTx.onerror = () => reject(new DatabaseError(`Failed to persist schema for "${schema.name}"`, 'IDB_SCHEMA_ERROR', schemaTx.error));
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
@@ -169,7 +298,16 @@ export class IndexedDBEngine implements IStorageEngine {
const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(tableName)) db.deleteObjectStore(tableName);
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onsuccess = () => {
this.db = request.result;
// v0.3.2: 清理持久化 schema 记录(upgrade 后执行,失败不阻塞删除)
if (this.db.objectStoreNames.contains('__metona_schema')) {
const schemaTx = this.db.transaction('__metona_schema', 'readwrite');
schemaTx.objectStore('__metona_schema').delete(tableName);
schemaTx.onerror = () => { /* 忽略:旧库可能无此记录 */ };
}
resolve();
};
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
@@ -354,3 +492,14 @@ export class IndexedDBEngine implements IStorageEngine {
return this.db;
}
}
/** 从存储值推断字段类型(schema 重建用,v0.3.2 */
function inferFieldType(value: unknown): import('../constants').FieldType {
if (typeof value === 'number') return 'number';
if (typeof value === 'boolean') return 'boolean';
if (typeof value === 'object' && value !== null) return 'json';
if (typeof value === 'string') {
return isNaN(Date.parse(value)) ? 'string' : 'string';
}
return 'string';
}
+8
View File
@@ -55,6 +55,14 @@ export interface IStorageEngine {
/** 清空表数据(保留结构) */
clear(tableName: string): Promise<void>;
// ---- 动态索引(可选,v0.3.0 ----
/** 创建二级索引(CREATE INDEX */
createIndex?(tableName: string, column: string, unique?: boolean): Promise<void>;
/** 删除二级索引(DROP INDEX */
dropIndex?(tableName: string, column: string, indexName?: string): Promise<void>;
// ---- 事务 ----
/** 开始事务 */
+36 -2
View File
@@ -148,6 +148,41 @@ export class MemoryEngine implements IStorageEngine {
if (tableIndexes) for (const colIndex of tableIndexes.values()) colIndex.clear();
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const colDef = schema.columns[column];
if (!colDef) throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
if (colDef.index || colDef.unique) return; // 已存在
colDef.index = true;
if (unique) colDef.unique = true;
const tableIndexes = this.indexes.get(tableName)!;
if (!tableIndexes.has(column)) tableIndexes.set(column, new Map());
const colIndex = tableIndexes.get(column)!;
const table = this.tables.get(tableName)!;
for (const [pk, row] of table) {
const value = row[column];
if (value !== undefined && value !== null) {
if (!colIndex.has(value)) colIndex.set(value, new Set());
colIndex.get(value)!.add(pk);
}
}
}
async dropIndex(tableName: string, column: string, _indexName?: string): Promise<void> {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const colDef = schema.columns[column];
if (!colDef) throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
colDef.index = false;
colDef.unique = false;
const tableIndexes = this.indexes.get(tableName);
if (tableIndexes) tableIndexes.delete(column);
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
@@ -297,10 +332,9 @@ export class MemoryEngine implements IStorageEngine {
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onDelete) continue;
const [refTable, refCol] = colDef.references.split('.');
const [refTable] = colDef.references.split('.');
if (refTable !== tableName) continue;
const refPkCol = refCol;
const refTableData = this.tables.get(refTableName);
if (!refTableData) continue;
+10
View File
@@ -132,6 +132,16 @@ export class OPFSEngine implements IStorageEngine {
await this.writeTableData(tableName, []);
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
return this.memoryCache.createIndex(tableName, column, unique);
}
async dropIndex(tableName: string, column: string, indexName?: string): Promise<void> {
return this.memoryCache.dropIndex(tableName, column, indexName);
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
+37 -6
View File
@@ -25,6 +25,8 @@ export class HybridEngine implements IStorageEngine {
private memoryEngine: MemoryEngine;
private diskEngine: IStorageEngine;
private diskEngineType: DiskEngine;
private dbName = '';
private version = 1;
constructor(diskEngine: DiskEngine = 'indexeddb') {
this.memoryEngine = new MemoryEngine();
@@ -35,6 +37,8 @@ export class HybridEngine implements IStorageEngine {
// ---- 生命周期 ----
async open(dbName: string, version: number): Promise<void> {
this.dbName = dbName;
this.version = version;
// 先打开磁盘引擎
await this.diskEngine.open(dbName, version);
@@ -42,6 +46,17 @@ export class HybridEngine implements IStorageEngine {
await this.memoryEngine.open(dbName, version);
// 从磁盘加载现存表
await this.reloadMemoryFromDisk();
}
/**
* 从磁盘重载内存缓存(v0.3.2:多标签页同步)。
* 其他标签页写入磁盘后调用,使本标签页读到最新数据。
*/
async reloadMemoryFromDisk(): Promise<void> {
await this.memoryEngine.close();
await this.memoryEngine.open(this.dbName, this.version);
const tableNames = await this.diskEngine.getTableNames();
for (const tableName of tableNames) {
const schema = await this.diskEngine.getTableSchema(tableName);
@@ -53,12 +68,12 @@ export class HybridEngine implements IStorageEngine {
// 从磁盘加载数据到内存
const rows = await this.diskEngine.find(tableName, { table: tableName });
if (rows.length > 0) {
try {
await this.memoryEngine.insert(tableName, rows);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e);
}
try {
await this.memoryEngine.insert(tableName, rows);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e);
}
}
}
}
@@ -133,6 +148,22 @@ export class HybridEngine implements IStorageEngine {
await this.diskEngine.clear(tableName);
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName: string, column: string, unique?: boolean): Promise<void> {
await this.memoryEngine.createIndex(tableName, column, unique);
if (typeof this.diskEngine.createIndex === 'function') {
await this.diskEngine.createIndex(tableName, column, unique);
}
}
async dropIndex(tableName: string, column: string, indexName?: string): Promise<void> {
await this.memoryEngine.dropIndex(tableName, column, indexName);
if (typeof this.diskEngine.dropIndex === 'function') {
await this.diskEngine.dropIndex(tableName, column, indexName);
}
}
// ---- 事务 ----
async beginTransaction(): Promise<void> {
+91 -91
View File
@@ -1,91 +1,91 @@
/**
* metona-sqlark — 入口文件
* @module metona-sqlark
* @version 0.2.5
*
* 前端关系型数据库,内存与磁盘双模式。
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
*/
import { MetonaSqlark } from './core';
import type { DatabaseConfig } from './constants';
import { VERSION } from './constants';
// 连接池管理器(side-effect: 注入 MetonaSqlark.connect / disconnect 等静态方法)
import './connection-manager';
// ---------------------------------------------------------------------------
// 工厂函数
// ---------------------------------------------------------------------------
/**
* 创建数据库实例并初始化
*
* @example
* ```ts
* const db = await MetonaSqlark.create({
* name: 'my-app',
* mode: 'hybrid',
* });
*
* await db.defineTable('users', {
* id: { type: 'string', primaryKey: true },
* name: { type: 'string', required: true },
* });
*
* await db.table('users').insert({ id: '1', name: 'Alice' });
* const results = await db.query('SELECT * FROM users');
* ```
*/
async function create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
// ---------------------------------------------------------------------------
// 全局 API
// ---------------------------------------------------------------------------
const api = {
VERSION,
version: VERSION,
create,
MetonaSqlark,
MeSqlark: MetonaSqlark,
};
// 浏览器全局挂载
declare global { interface Window { MetonaSqlark: typeof api; MeSqlark: typeof api; } }
if (typeof window !== 'undefined') {
window.MetonaSqlark = api;
window.MeSqlark = api;
}
export default api;
export {
api,
VERSION,
create,
MetonaSqlark,
};
// 别名
export const MeSqlark = MetonaSqlark;
// 类型导出
export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants';
export type { IStorageEngine } from './engine/interface';
export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast';
export { MemoryEngine } from './engine/memory';
export { IndexedDBEngine } from './engine/indexeddb';
export { OPFSEngine } from './engine/opfs';
export { AriaEngine } from './engine/aria/index';
export { HybridEngine } from './hybrid/index';
export { Table } from './table/table';
export { parse } from './sql/parser';
export { tokenize } from './sql/lexer';
// AriaEngine 类型 & 后端
export type { AriaEngineConfig } from './engine/aria/types';
export { OPFSBackend } from './engine/aria/store/opfs_backend';
/**
* metona-sqlark — 入口文件
* @module metona-sqlark
* @version 0.2.5
*
* 前端关系型数据库,内存与磁盘双模式。
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
*/
import { MetonaSqlark } from './core';
import type { DatabaseConfig } from './constants';
import { VERSION } from './constants';
// 连接池管理器(side-effect: 注入 MetonaSqlark.connect / disconnect 等静态方法)
import './connection-manager';
// ---------------------------------------------------------------------------
// 工厂函数
// ---------------------------------------------------------------------------
/**
* 创建数据库实例并初始化
*
* @example
* ```ts
* const db = await MetonaSqlark.create({
* name: 'my-app',
* mode: 'hybrid',
* });
*
* await db.defineTable('users', {
* id: { type: 'string', primaryKey: true },
* name: { type: 'string', required: true },
* });
*
* await db.table('users').insert({ id: '1', name: 'Alice' });
* const results = await db.query('SELECT * FROM users');
* ```
*/
async function create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
// ---------------------------------------------------------------------------
// 全局 API
// ---------------------------------------------------------------------------
const api = {
VERSION,
version: VERSION,
create,
MetonaSqlark,
MeSqlark: MetonaSqlark,
};
// 浏览器全局挂载
declare global { interface Window { MetonaSqlark: typeof api; MeSqlark: typeof api; } }
if (typeof window !== 'undefined') {
window.MetonaSqlark = api;
window.MeSqlark = api;
}
export default api;
export {
api,
VERSION,
create,
MetonaSqlark,
};
// 别名
export const MeSqlark = MetonaSqlark;
// 类型导出
export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants';
export type { IStorageEngine } from './engine/interface';
export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast';
export { MemoryEngine } from './engine/memory';
export { IndexedDBEngine } from './engine/indexeddb';
export { OPFSEngine } from './engine/opfs';
export { AriaEngine } from './engine/aria/index';
export { HybridEngine } from './hybrid/index';
export { Table } from './table/table';
export { parse, parseAll } from './sql/parser';
export { tokenize } from './sql/lexer';
// AriaEngine 类型 & 后端
export type { AriaEngineConfig } from './engine/aria/types';
export { OPFSBackend } from './engine/aria/store/opfs_backend';
+1 -1
View File
@@ -15,7 +15,7 @@
* }
*/
import type { MetonaSqlark } from '../core';
import { MetonaSqlark } from '../core';
import { useState, useEffect, useCallback, useRef } from 'react';
/** useQuery: 执行 SQL 查询 */
+1 -1
View File
@@ -12,7 +12,7 @@
* const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');
*/
import type { MetonaSqlark } from '../core';
import { MetonaSqlark } from '../core';
import { ref, watch, onMounted, type Ref } from 'vue';
/** useSqlarkQuery: 执行 SQL 查询 */
+275 -209
View File
@@ -1,209 +1,275 @@
/**
* metona-sqlark Query AST — 查询抽象语法树类型定义
* @module query/ast
*
* QueryBuilder 和 SQL Parser 统一输出此 AST
* Executor 只认 AST,保证两种查询接口行为一致。
*/
import type { WhereCondition, OrderBy } from '../constants';
// ---------------------------------------------------------------------------
// AST 语句类型枚举
// ---------------------------------------------------------------------------
export type StatementType =
| 'SELECT'
| 'EXPLAIN'
| 'INSERT'
| 'UPDATE'
| 'DELETE'
| 'CREATE_TABLE'
| 'DROP_TABLE'
| 'ALTER_TABLE'
| 'TRUNCATE_TABLE';
// ---------------------------------------------------------------------------
// 列引用
// ---------------------------------------------------------------------------
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */
export type ColumnRef = string;
// ---------------------------------------------------------------------------
// JOIN
// ---------------------------------------------------------------------------
/** JOIN 类型 */
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
/** JOIN 子句 */
export interface JoinClause {
type: JoinType;
table: string;
alias?: string;
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;
}
// ---------------------------------------------------------------------------
// DDL: CREATE TABLE
// ---------------------------------------------------------------------------
export interface ASTColumnDef {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
/** 外键引用 */
references?: string;
/** 级联删除 */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 级联更新 */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
}
export interface CreateTableStatement {
type: 'CREATE_TABLE';
name: string;
columns: ASTColumnDef[];
/** IF NOT EXISTS — 表已存在时不报错 */
ifNotExists?: boolean;
}
// ---------------------------------------------------------------------------
// DDL: DROP TABLE
// ---------------------------------------------------------------------------
export interface DropTableStatement {
type: 'DROP_TABLE';
name: string;
/** IF EXISTS — 表不存在时不报错 */
ifExists?: boolean;
}
/** EXPLAIN 查询计划 */
export interface ExplainStatement {
type: 'EXPLAIN';
query: Statement;
}
// ---------------------------------------------------------------------------
// DML: INSERT
// ---------------------------------------------------------------------------
export interface InsertStatement {
type: 'INSERT';
into: string;
columns?: string[];
values: unknown[][];
}
// ---------------------------------------------------------------------------
// DML: UPDATE
// ---------------------------------------------------------------------------
export interface UpdateStatement {
type: 'UPDATE';
table: string;
sets: Record<string, unknown>;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DML: DELETE
// ---------------------------------------------------------------------------
export interface DeleteStatement {
type: 'DELETE';
from: string;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DQL: SELECT
// ---------------------------------------------------------------------------
export interface SelectStatement {
type: 'SELECT';
columns: ColumnRef[];
distinct?: boolean;
from: string;
/** 主表别名 */
alias?: string;
/** JOIN 子句列表 */
joins?: JoinClause[];
where: WhereCondition;
/** GROUP BY */
groupBy?: string[];
/** HAVING */
having?: WhereCondition;
orderBy?: OrderBy[];
limit?: number;
offset?: number;
}
// ---------------------------------------------------------------------------
// DDL: ALTER TABLE
// ---------------------------------------------------------------------------
export interface AlterTableStatement {
type: 'ALTER_TABLE';
name: string;
action: 'ADD' | 'DROP';
column: ASTColumnDef;
}
// ---------------------------------------------------------------------------
// DDL: TRUNCATE TABLE
// ---------------------------------------------------------------------------
export interface TruncateTableStatement {
type: 'TRUNCATE_TABLE';
name: string;
}
// ---------------------------------------------------------------------------
// AST 联合类型
// ---------------------------------------------------------------------------
export type Statement =
| SelectStatement
| ExplainStatement
| InsertStatement
| UpdateStatement
| DeleteStatement
| CreateTableStatement
| DropTableStatement
| AlterTableStatement
| TruncateTableStatement;
/**
* metona-sqlark Query AST — 查询抽象语法树类型定义
* @module query/ast
*
* QueryBuilder 和 SQL Parser 统一输出此 AST
* Executor 只认 AST,保证两种查询接口行为一致。
*/
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';
// ---------------------------------------------------------------------------
// 列引用
// ---------------------------------------------------------------------------
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */
export type ColumnRef = string;
// ---------------------------------------------------------------------------
// JOIN
// ---------------------------------------------------------------------------
/** JOIN 类型 */
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
/** JOIN 子句 */
export interface JoinClause {
type: JoinType;
table: string;
alias?: string;
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
// ---------------------------------------------------------------------------
export interface ASTColumnDef {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
/** 外键引用 */
references?: string;
/** 级联删除 */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 级联更新 */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
}
export interface CreateTableStatement {
type: 'CREATE_TABLE';
name: string;
columns: ASTColumnDef[];
/** IF NOT EXISTS — 表已存在时不报错 */
ifNotExists?: boolean;
}
// ---------------------------------------------------------------------------
// DDL: DROP TABLE
// ---------------------------------------------------------------------------
export interface DropTableStatement {
type: 'DROP_TABLE';
name: string;
/** IF EXISTS — 表不存在时不报错 */
ifExists?: boolean;
}
/** EXPLAIN 查询计划 */
export interface ExplainStatement {
type: 'EXPLAIN';
query: Statement;
}
// ---------------------------------------------------------------------------
// DML: INSERT
// ---------------------------------------------------------------------------
export interface InsertStatement {
type: 'INSERT';
into: string;
columns?: string[];
/** VALUES 字面量 */
values?: unknown[][];
/** INSERT INTO ... SELECT ...v0.3.0 */
select?: SelectStatement | SelectUnionStatement;
}
// ---------------------------------------------------------------------------
// DML: UPDATE
// ---------------------------------------------------------------------------
export interface UpdateStatement {
type: 'UPDATE';
table: string;
sets: Record<string, unknown>;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DML: DELETE
// ---------------------------------------------------------------------------
export interface DeleteStatement {
type: 'DELETE';
from: string;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DQL: SELECT
// ---------------------------------------------------------------------------
export interface SelectStatement {
type: 'SELECT';
columns: ColumnRef[];
distinct?: boolean;
from: string;
/** 主表别名 */
alias?: string;
/** JOIN 子句列表 */
joins?: JoinClause[];
where: WhereCondition;
/** GROUP BY */
groupBy?: string[];
/** HAVING */
having?: WhereCondition;
orderBy?: OrderBy[];
limit?: number;
offset?: number;
}
// ---------------------------------------------------------------------------
// DQL: UNION
// ---------------------------------------------------------------------------
export interface SelectUnionStatement {
type: 'SELECT_UNION';
/** 左操作数(可以是 SELECT 或嵌套 UNION */
left: SelectStatement | SelectUnionStatement;
/** 右操作数 */
right: SelectStatement | SelectUnionStatement;
/** UNION ALL 不去重 */
all?: boolean;
}
// ---------------------------------------------------------------------------
// DDL: ALTER TABLE
// ---------------------------------------------------------------------------
export interface AlterTableStatement {
type: 'ALTER_TABLE';
name: string;
action: 'ADD' | 'DROP';
column: ASTColumnDef;
}
// ---------------------------------------------------------------------------
// DDL: TRUNCATE TABLE
// ---------------------------------------------------------------------------
export interface TruncateTableStatement {
type: 'TRUNCATE_TABLE';
name: string;
}
// ---------------------------------------------------------------------------
// DDL: CREATE INDEX / DROP INDEXv0.3.0
// ---------------------------------------------------------------------------
export interface CreateIndexStatement {
type: 'CREATE_INDEX';
/** 索引名(语法占位) */
name: string;
table: string;
column: string;
/** UNIQUE 索引 */
unique?: boolean;
}
export interface DropIndexStatement {
type: 'DROP_INDEX';
name: string;
table: string;
column: string;
}
// ---------------------------------------------------------------------------
// TCL: 事务语句(v0.3.0
// ---------------------------------------------------------------------------
export interface BeginTransactionStatement {
type: 'BEGIN';
}
export interface CommitTransactionStatement {
type: 'COMMIT';
}
export interface RollbackTransactionStatement {
type: 'ROLLBACK';
}
// ---------------------------------------------------------------------------
// AST 联合类型
// ---------------------------------------------------------------------------
export type Statement =
| SelectStatement
| SelectUnionStatement
| ExplainStatement
| InsertStatement
| UpdateStatement
| DeleteStatement
| CreateTableStatement
| DropTableStatement
| AlterTableStatement
| TruncateTableStatement
| CreateIndexStatement
| DropIndexStatement
| BeginTransactionStatement
| CommitTransactionStatement
| RollbackTransactionStatement;
+16 -4
View File
@@ -134,12 +134,16 @@ export class SelectQueryBuilder {
export class UpdateQueryBuilder {
private _where: WhereCondition = {};
private onWrite?: (table: string) => void;
constructor(
private engine: IStorageEngine,
private tableName: string,
private _updates: Record<string, unknown>,
) {}
onWrite?: (table: string) => void,
) {
this.onWrite = onWrite;
}
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
@@ -147,7 +151,9 @@ export class UpdateQueryBuilder {
}
async execute(): Promise<number> {
return this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
const count = await this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
this.onWrite?.(this.tableName);
return count;
}
toAST(): UpdateStatement {
@@ -161,11 +167,15 @@ export class UpdateQueryBuilder {
export class DeleteQueryBuilder {
private _where: WhereCondition = {};
private onWrite?: (table: string) => void;
constructor(
private engine: IStorageEngine,
private tableName: string,
) {}
onWrite?: (table: string) => void,
) {
this.onWrite = onWrite;
}
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
@@ -173,7 +183,9 @@ export class DeleteQueryBuilder {
}
async execute(): Promise<number> {
return this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
const count = await this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
this.onWrite?.(this.tableName);
return count;
}
toAST(): DeleteStatement {
+1061 -448
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -42,6 +42,16 @@ export function matchWhere(
options: { $col?: boolean } = {},
): boolean {
for (const [field, condition] of Object.entries(where)) {
// 顶层 $caseResultv0.3.2):由 Executor 对 CASE WHEN 表达式逐行求值后产生
if (field === '$caseResult') {
if (condition !== true) return false;
continue;
}
// 顶层 $existsv0.3.0):由 Executor.resolveSubqueries 解析为 boolean
if (field === '$exists') {
if (condition !== true) return false;
continue;
}
// 顶层 $and
if (field === '$and') {
const subs = condition as WhereCondition[];
@@ -54,6 +64,11 @@ export function matchWhere(
if (!subs.some((sub) => matchWhere(row, sub, options))) return false;
continue;
}
// 顶层 $notv0.3.2 修复:NOT (expr) 生成的 { $not: inner }
if (field === '$not') {
if (matchWhere(row, condition as WhereCondition, options)) return false;
continue;
}
if (!matchField(row[field], condition, row, options)) return false;
}
return true;
+1164 -901
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -68,6 +68,21 @@ export enum TokenType {
MAX = 'MAX',
DISTINCT = 'DISTINCT',
// v0.3.0: 事务 / UNION / EXISTS / 动态索引
BEGIN = 'BEGIN',
COMMIT = 'COMMIT',
ROLLBACK = 'ROLLBACK',
UNION = 'UNION',
ALL = 'ALL',
INDEX = 'INDEX',
// v0.3.1: CASE WHEN 表达式
CASE = 'CASE',
WHEN = 'WHEN',
THEN = 'THEN',
ELSE = 'ELSE',
END = 'END',
// 标识符 & 字面量
IDENTIFIER = 'IDENTIFIER',
STRING = 'STRING',
@@ -165,4 +180,19 @@ export const KEYWORDS: Record<string, TokenType> = {
'MIN': TokenType.MIN,
'MAX': TokenType.MAX,
'DISTINCT': TokenType.DISTINCT,
// v0.3.0
'BEGIN': TokenType.BEGIN,
'COMMIT': TokenType.COMMIT,
'ROLLBACK': TokenType.ROLLBACK,
'UNION': TokenType.UNION,
'ALL': TokenType.ALL,
'INDEX': TokenType.INDEX,
// v0.3.1
'CASE': TokenType.CASE,
'WHEN': TokenType.WHEN,
'THEN': TokenType.THEN,
'ELSE': TokenType.ELSE,
'END': TokenType.END,
};
+14 -6
View File
@@ -18,11 +18,14 @@ export class Table<T = Record<string, unknown>> {
private engine: IStorageEngine;
private schema: TableSchema | null = null;
private executor: QueryExecutor | undefined;
/** 写入回调(多标签页广播,v0.3.2) */
private onWrite?: (table: string) => void;
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor) {
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor, onWrite?: (table: string) => void) {
this.engine = engine;
this.name = tableName;
this.executor = executor;
this.onWrite = onWrite;
}
// ---- Schema ----
@@ -40,11 +43,14 @@ export class Table<T = Record<string, unknown>> {
async insert(row: T & Record<string, unknown>): Promise<string> {
const pks = await this.engine.insert(this.name, [row as Record<string, unknown>]);
this.onWrite?.(this.name);
return pks[0];
}
async insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]> {
return this.engine.insert(this.name, rows as Record<string, unknown>[]);
const pks = await this.engine.insert(this.name, rows as Record<string, unknown>[]);
this.onWrite?.(this.name);
return pks;
}
// ---- 查询 ----
@@ -56,13 +62,13 @@ export class Table<T = Record<string, unknown>> {
// ---- 更新 ----
update(updates: Partial<T> & Record<string, unknown>): UpdateQueryBuilder {
return new UpdateQueryBuilder(this.engine, this.name, updates);
return new UpdateQueryBuilder(this.engine, this.name, updates, this.onWrite);
}
// ---- 删除 ----
delete(): DeleteQueryBuilder {
return new DeleteQueryBuilder(this.engine, this.name);
return new DeleteQueryBuilder(this.engine, this.name, this.onWrite);
}
// ---- 聚合 ----
@@ -74,10 +80,12 @@ export class Table<T = Record<string, unknown>> {
// ---- 管理 ----
async clear(): Promise<void> {
return this.engine.clear(this.name);
await this.engine.clear(this.name);
this.onWrite?.(this.name);
}
async drop(): Promise<void> {
return this.engine.dropTable(this.name);
await this.engine.dropTable(this.name);
this.onWrite?.(this.name);
}
}