release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
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:
+215
-212
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+123
-123
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+579
-466
File diff suppressed because it is too large
Load Diff
+467
-467
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
// 收集有效 slot(offset>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;
|
||||
|
||||
// 收集有效 slot(offset>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
@@ -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
@@ -1,179 +1,179 @@
|
||||
/**
|
||||
* AriaEngine Storage Backend — 存储后端抽象层
|
||||
* @module engine/aria/store/backend
|
||||
*
|
||||
* 封装底层浏览器存储 API(IndexedDB / 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
|
||||
*
|
||||
* 封装底层浏览器存储 API(IndexedDB / 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
/** 截断 WAL(checkpoint 后清理) */
|
||||
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>;
|
||||
/** 截断 WAL(checkpoint 后清理) */
|
||||
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
@@ -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
@@ -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 重建 schema(schema 此前只存内存缓存,重开连接即丢失)
|
||||
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' store,v0.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';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* }
|
||||
*/
|
||||
|
||||
import type { MetonaSqlark } from '../core';
|
||||
import { MetonaSqlark } from '../core';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
/** useQuery: 执行 SQL 查询 */
|
||||
|
||||
@@ -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
@@ -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 INDEX(v0.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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,16 @@ export function matchWhere(
|
||||
options: { $col?: boolean } = {},
|
||||
): boolean {
|
||||
for (const [field, condition] of Object.entries(where)) {
|
||||
// 顶层 $caseResult(v0.3.2):由 Executor 对 CASE WHEN 表达式逐行求值后产生
|
||||
if (field === '$caseResult') {
|
||||
if (condition !== true) return false;
|
||||
continue;
|
||||
}
|
||||
// 顶层 $exists(v0.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;
|
||||
}
|
||||
// 顶层 $not(v0.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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user