release: v0.2.5 — 质量加固 + Bug修复 + 性能优化 + SQL扩展
This commit is contained in:
+60
-23
@@ -2,43 +2,80 @@
|
||||
* AriaEngine Crypto — 页面级 AES-GCM 加密
|
||||
* @module engine/aria/crypto
|
||||
*
|
||||
* 使用 Web Crypto API (SubtleCrypto) 进行 AES-256-GCM 加密。
|
||||
* v0.2.5: 改为实例化 CryptoManager,避免多实例共享全局状态。
|
||||
* 保留全局函数兼容旧代码(委托给全局单例)。
|
||||
*/
|
||||
|
||||
const ALGO = 'AES-GCM';
|
||||
const IV_LENGTH = 12;
|
||||
|
||||
let cryptoKey: CryptoKey | null = null;
|
||||
let enabled = false;
|
||||
/**
|
||||
* CryptoManager — 实例级加密管理器
|
||||
* 每个 AriaEngine 实例可拥有独立的加密配置。
|
||||
*/
|
||||
export class CryptoManager {
|
||||
private cryptoKey: CryptoKey | null = null;
|
||||
private _enabled = false;
|
||||
|
||||
get enabled(): boolean { return this._enabled; }
|
||||
|
||||
async init(password: string, salt?: Uint8Array): Promise<Uint8Array> {
|
||||
const enc = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'],
|
||||
);
|
||||
const actualSalt: any = salt || crypto.getRandomValues(new Uint8Array(16));
|
||||
this.cryptoKey = await crypto.subtle.deriveKey(
|
||||
{ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' } as any,
|
||||
keyMaterial, { name: ALGO, length: 256 } as any, false, ['encrypt', 'decrypt'],
|
||||
);
|
||||
this._enabled = true;
|
||||
return actualSalt as Uint8Array;
|
||||
}
|
||||
|
||||
async encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
|
||||
if (!this.cryptoKey) throw new Error('Crypto not initialized');
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)) as any;
|
||||
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv } as any, this.cryptoKey, data);
|
||||
return { iv: iv as Uint8Array, data: ciphertext };
|
||||
}
|
||||
|
||||
async decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
if (!this.cryptoKey) throw new Error('Crypto not initialized');
|
||||
return crypto.subtle.decrypt({ name: ALGO, iv } as any, this.cryptoKey, data);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.cryptoKey = null;
|
||||
this._enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 全局兼容层(旧代码仍可使用全局函数)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const globalCrypto = new CryptoManager();
|
||||
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
export async function initCrypto(password: string, salt?: Uint8Array): Promise<Uint8Array> {
|
||||
const enc = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'],
|
||||
);
|
||||
const actualSalt: any = salt || crypto.getRandomValues(new Uint8Array(16));
|
||||
cryptoKey = await crypto.subtle.deriveKey(
|
||||
{ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' } as any,
|
||||
keyMaterial, { name: ALGO, length: 256 } as any, false, ['encrypt', 'decrypt'],
|
||||
);
|
||||
enabled = true;
|
||||
return actualSalt as Uint8Array;
|
||||
return globalCrypto.init(password, salt);
|
||||
}
|
||||
|
||||
export function isCryptoEnabled(): boolean { return enabled; }
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
export function isCryptoEnabled(): boolean { return globalCrypto.enabled; }
|
||||
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
export async function encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
|
||||
if (!cryptoKey) throw new Error('Crypto not initialized');
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)) as any;
|
||||
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv } as any, cryptoKey, data);
|
||||
return { iv: iv as Uint8Array, data: ciphertext };
|
||||
return globalCrypto.encryptPage(data);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
export async function decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
if (!cryptoKey) throw new Error('Crypto not initialized');
|
||||
return crypto.subtle.decrypt({ name: ALGO, iv } as any, cryptoKey, data);
|
||||
return globalCrypto.decryptPage(iv, data);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
export function closeCrypto(): void {
|
||||
cryptoKey = null;
|
||||
enabled = false;
|
||||
globalCrypto.close();
|
||||
}
|
||||
|
||||
+23
-13
@@ -2,7 +2,7 @@
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
*
|
||||
* v0.2.4: 二级索引 + MVCC 集成 + 生产加固
|
||||
* v0.2.5: WAL 同步修复 + MVCC 接入 + 版本统一 + 生产加固
|
||||
*/
|
||||
|
||||
import type { IStorageEngine } from '../interface';
|
||||
@@ -27,6 +27,7 @@ import { BloomFilter } from './index/bloom';
|
||||
import { BufferPool } from './buffer/pool';
|
||||
import { compressLZ4, decompressLZ4 } from './compression/lz4';
|
||||
import { isCryptoEnabled, encryptPage, decryptPage } from './crypto';
|
||||
import type { WALRecord as _WALRecord } from './types';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AriaEngine
|
||||
@@ -163,12 +164,13 @@ export class AriaEngine implements IStorageEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Checkpoint Manager(BufferPool 暂简化,使用 flush 替代)
|
||||
// 8. Checkpoint Manager(接入 WAL 大小阈值)
|
||||
this.checkpointManager = new CheckpointManager(
|
||||
this.lsm,
|
||||
this.wal,
|
||||
{ flushAll: async () => { await this.lsm.flush(); } } as any,
|
||||
this.config.checkpointInterval,
|
||||
this.config.walSizeThreshold,
|
||||
);
|
||||
|
||||
this.opened = true;
|
||||
@@ -220,7 +222,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
|
||||
await this.persistSchemas();
|
||||
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.CREATE_TABLE,
|
||||
txnId: 0,
|
||||
tableName: schema.name,
|
||||
@@ -244,7 +246,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.tablePKs.delete(tableName);
|
||||
await this.persistSchemas();
|
||||
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.DROP_TABLE,
|
||||
txnId: 0,
|
||||
tableName,
|
||||
@@ -293,8 +295,9 @@ export class AriaEngine implements IStorageEngine {
|
||||
}
|
||||
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
// Within transaction: buffer to snapshot
|
||||
// Within transaction: buffer to snapshot + MVCC version chain
|
||||
this.txnSnapshot.set(key, validated);
|
||||
this.mvcc.writeVersion(tableName, pkValue, validated, this.currentTxnId);
|
||||
} else {
|
||||
// Direct write to LSM (PK index)
|
||||
this.lsm.put(key, validated);
|
||||
@@ -305,7 +308,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
|
||||
pks.push(pkValue);
|
||||
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.INSERT,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -399,12 +402,13 @@ export class AriaEngine implements IStorageEngine {
|
||||
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
this.txnSnapshot.set(key, updated);
|
||||
this.mvcc.writeVersion(tableName, String(row[pkCol]), updated, this.currentTxnId);
|
||||
} else {
|
||||
this.lsm.put(key, updated);
|
||||
}
|
||||
count++;
|
||||
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.UPDATE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -435,14 +439,15 @@ export class AriaEngine implements IStorageEngine {
|
||||
|
||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
// Buffer delete in snapshot
|
||||
// Buffer delete in snapshot + MVCC tombstone
|
||||
this.txnSnapshot.set(key, { __txn_deleted: true } as unknown as Record<string, unknown>);
|
||||
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
|
||||
} else {
|
||||
this.lsm.delete(key);
|
||||
}
|
||||
count++;
|
||||
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.DELETE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -486,7 +491,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.currentTxnId = this.mvcc.beginTransaction();
|
||||
this.txnSnapshot = new Map();
|
||||
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.BEGIN,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -509,7 +514,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
|
||||
this.mvcc.commitTransaction(this.currentTxnId);
|
||||
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.COMMIT,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -527,7 +532,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
this.mvcc.rollbackTransaction(this.currentTxnId);
|
||||
this.txnSnapshot = null;
|
||||
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.ROLLBACK,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -922,6 +927,11 @@ export class AriaEngine implements IStorageEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/** 估算 WAL 大小(字节) */
|
||||
getWALEstimatedSize(): number {
|
||||
return this.wal.getBufferedCount() * 200; // 粗略估算每条 ~200B
|
||||
}
|
||||
|
||||
/**
|
||||
* ANALYZE: 收集表统计信息
|
||||
* 返回行数、平均行大小、索引深度等
|
||||
@@ -997,7 +1007,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
// 压缩各层级
|
||||
for (let level = 0; level < 6; level++) {
|
||||
if (this.lsm.getStats().levelCounts[level] >= 2) {
|
||||
(this.lsm as any).compactLevelSync(level);
|
||||
this.lsm.compactLevel(level);
|
||||
}
|
||||
}
|
||||
// GC MVCC 版本(保留最新 10 个)
|
||||
|
||||
@@ -331,7 +331,12 @@ export class LSM {
|
||||
// Compaction
|
||||
// =======================================================================
|
||||
|
||||
/** 同步执行 Compaction(简化版,仅供内部调用) */
|
||||
/** 同步执行 Compaction(public,供 VACUUM 等外部调用) */
|
||||
compactLevel(level: number): void {
|
||||
this.compactLevelSync(level);
|
||||
}
|
||||
|
||||
/** 同步执行 Compaction(简化版,内部实现) */
|
||||
private compactLevelSync(level: number): void {
|
||||
if (level >= MAX_LSM_LEVELS - 1) return;
|
||||
if (this.levels[level].length < 4) return;
|
||||
|
||||
@@ -240,16 +240,22 @@ export class SSTableReader {
|
||||
}
|
||||
|
||||
private locateBlockGE(key: string): number {
|
||||
for (let i = 0; i < this.indexEntries.length; i++) {
|
||||
if (this.indexEntries[i].key >= key) return i;
|
||||
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 this.indexEntries.length - 1;
|
||||
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
|
||||
}
|
||||
|
||||
private locateBlockLE(key: string): number {
|
||||
for (let i = this.indexEntries.length - 1; i >= 0; i--) {
|
||||
if (this.indexEntries[i].key <= key) return i;
|
||||
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 0;
|
||||
return lo > 0 ? lo - 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,26 +24,36 @@ export class CheckpointManager {
|
||||
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++;
|
||||
if (this.opCount >= this.interval) {
|
||||
// 检查操作计数或 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) {
|
||||
|
||||
@@ -59,8 +59,8 @@ export class WAL {
|
||||
// 写入
|
||||
// =======================================================================
|
||||
|
||||
/** 追加一条 WAL 记录 */
|
||||
append(record: Omit<WALRecord, 'lsn' | 'checksum'>): void {
|
||||
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
|
||||
async append(record: Omit<WALRecord, 'lsn' | 'checksum'>): Promise<void> {
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.lsn++;
|
||||
@@ -73,10 +73,12 @@ export class WAL {
|
||||
const bytes = this.encodeRecord(fullRecord);
|
||||
|
||||
if (this.syncMode === 'full') {
|
||||
this.store.append(bytes).catch(() => {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -187,6 +187,14 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
|
||||
private async idbFind(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
|
||||
const db = this.ensureDB();
|
||||
|
||||
// 尝试使用 IDB 索引进行等值查询
|
||||
if (query.where) {
|
||||
const indexResult = await this.tryIDBIndexLookup(db, tableName, query);
|
||||
if (indexResult !== null) return indexResult;
|
||||
}
|
||||
|
||||
// 回退到全量 getAll + 内存过滤
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
const req = tx.objectStore(tableName).getAll();
|
||||
@@ -210,6 +218,69 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
});
|
||||
}
|
||||
|
||||
/** 尝试使用 IDB 索引进行等值查询,成功返回结果,不适用返回 null */
|
||||
private async tryIDBIndexLookup(
|
||||
db: IDBDatabase,
|
||||
tableName: string,
|
||||
query: QueryPlan,
|
||||
): Promise<Record<string, unknown>[] | null> {
|
||||
if (!query.where) return null;
|
||||
|
||||
for (const [col, condition] of Object.entries(query.where)) {
|
||||
// 跳过逻辑组合符
|
||||
if (col === '$and' || col === '$or' || col === '$not') continue;
|
||||
|
||||
// 只处理等值查询
|
||||
let targetValue: unknown;
|
||||
if (typeof condition !== 'object' || condition === null) {
|
||||
targetValue = condition;
|
||||
} else if ('$eq' in (condition as Record<string, unknown>)) {
|
||||
targetValue = (condition as Record<string, unknown>).$eq;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有对应的 IDB 索引
|
||||
const indexName = `idx_${col}`;
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
const store = tx.objectStore(tableName);
|
||||
if (!store.indexNames.contains(indexName)) {
|
||||
resolve(null); // 没有索引,回退
|
||||
return;
|
||||
}
|
||||
const index = store.index(indexName);
|
||||
const req = index.getAll(targetValue as IDBValidKey);
|
||||
req.onsuccess = () => {
|
||||
let results: Record<string, unknown>[] = req.result ?? [];
|
||||
// 如果有其他 WHERE 条件,继续过滤
|
||||
const otherKeys = Object.keys(query.where!).filter(
|
||||
(k) => k !== col && k !== '$and' && k !== '$or' && k !== '$not',
|
||||
);
|
||||
if (otherKeys.length > 0) {
|
||||
results = results.filter((row) => matchWhere(row, query.where!));
|
||||
}
|
||||
if (query.orderBy && query.orderBy.length > 0) {
|
||||
results = applyOrderBy(results, query.orderBy);
|
||||
}
|
||||
const offset = query.offset ?? 0;
|
||||
const limit = query.limit ?? results.length;
|
||||
results = results.slice(offset, offset + limit);
|
||||
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
|
||||
results = results.map((r) => projectColumns(r, query.columns!));
|
||||
}
|
||||
resolve(results);
|
||||
};
|
||||
req.onerror = () => reject(new DatabaseError(`Index lookup failed for "${tableName}.${col}"`, 'IDB_READ_ERROR', req.error));
|
||||
});
|
||||
} catch {
|
||||
return null; // 索引不可用,回退
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async idbUpdate(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Reference in New Issue
Block a user