diff --git a/src/engine/aria/crypto.ts b/src/engine/aria/crypto.ts new file mode 100644 index 0000000..1df0789 --- /dev/null +++ b/src/engine/aria/crypto.ts @@ -0,0 +1,54 @@ +/** + * AriaEngine Crypto — 页面级 AES-GCM 加密 + * @module engine/aria/crypto + * + * 使用 Web Crypto API (SubtleCrypto) 进行 AES-256-GCM 加密。 + * 浏览器原生支持,无需额外依赖。 + */ + +const ALGO = 'AES-GCM'; +const KEY_LENGTH = 256; +const IV_LENGTH = 12; // GCM 推荐 96-bit nonce + +let cryptoKey: CryptoKey | null = null; +let enabled = false; + +/** + * 初始化加密密钥。传入原始密码字符串,通过 PBKDF2 派生 AES 密钥。 + */ +export async function initCrypto(password: string, salt?: Uint8Array): Promise { + const enc = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + 'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'], + ); + const actualSalt = salt || crypto.getRandomValues(new Uint8Array(16)); + cryptoKey = await crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' }, + keyMaterial, { name: ALGO, length: KEY_LENGTH }, false, ['encrypt', 'decrypt'], + ); + enabled = true; + return actualSalt; +} + +/** 是否已启用加密 */ +export function isCryptoEnabled(): boolean { return enabled; } + +/** 加密 ArrayBuffer,返回 { iv, ciphertext } */ +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)); + const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, cryptoKey, data); + return { iv, data: ciphertext }; +} + +/** 解密 ArrayBuffer */ +export async function decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise { + if (!cryptoKey) throw new Error('Crypto not initialized'); + return crypto.subtle.decrypt({ name: ALGO, iv }, cryptoKey, data); +} + +/** 关闭加密,清除密钥 */ +export function closeCrypto(): void { + cryptoKey = null; + enabled = false; +} diff --git a/src/engine/aria/index.ts b/src/engine/aria/index.ts index abfd853..9e974f1 100644 --- a/src/engine/aria/index.ts +++ b/src/engine/aria/index.ts @@ -302,6 +302,7 @@ export class AriaEngine implements IStorageEngine { } this.opCounter += rows.length; + this.checkMemoryBudget(); await this.checkpointManager.tick(); this.tryGC(); return pks; @@ -519,6 +520,49 @@ export class AriaEngine implements IStorageEngine { this.currentTxnId = null; } + // ---- Savepoint 嵌套事务 ---- + + private savepoints: Map> | null }> = new Map(); + + async savepoint(name: string): Promise { + if (!this.currentTxnId) throw new DatabaseError('No active transaction for savepoint', 'TX_NONE'); + if (this.savepoints.has(name)) throw new DatabaseError(`Savepoint "${name}" already exists`, 'SAVEPOINT_EXISTS'); + // 保存当前事务快照 + this.savepoints.set(name, { + txnId: this.currentTxnId, + snapshot: this.txnSnapshot ? new Map(this.txnSnapshot) : null, + }); + } + + async rollbackToSavepoint(name: string): Promise { + const sp = this.savepoints.get(name); + if (!sp) throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND'); + // 恢复到 savepoint 时的快照 + this.txnSnapshot = sp.snapshot ? new Map(sp.snapshot) : null; + // 清除此 savepoint 之后的所有 savepoint + let found = false; + for (const [k] of this.savepoints) { + if (k === name) { found = true; continue; } + if (found) this.savepoints.delete(k); + } + } + + async releaseSavepoint(name: string): Promise { + if (!this.savepoints.has(name)) throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND'); + this.savepoints.delete(name); + } + + // ---- 在线备份 ---- + + async backup(): Promise[]>> { + this.ensureOpen(); + const result: Record[]> = {}; + for (const tableName of this.schemas.keys()) { + result[tableName] = this.getAllRows(tableName); + } + return result; + } + // ======================================================================= // 内部 // ======================================================================= @@ -821,6 +865,50 @@ export class AriaEngine implements IStorageEngine { } } + /** 检查内存预算,超出时强制 flush + GC */ + private checkMemoryBudget(): void { + const maxBytes = this.config.maxMemoryMB * 1024 * 1024; + const used = this.lsm.getEstimatedMemory(); + if (used > maxBytes) { + this.lsm.flush().catch(() => {}); + this.mvcc.gc(50); + } + } + + /** + * ANALYZE: 收集表统计信息 + * 返回行数、平均行大小、索引深度等 + */ + async analyzeTable(tableName: string): Promise> { + this.ensureOpen(); + this.ensureTable(tableName); + const rows = this.getAllRows(tableName); + const stats: Record = { + table: tableName, + rowCount: rows.length, + avgRowSize: rows.length > 0 + ? Math.round(rows.reduce((s, r) => s + JSON.stringify(r).length, 0) / rows.length) + : 0, + indexDepth: this.lsm.getStats().levelCounts.filter((c: number) => c > 0).length, + sstableCount: this.lsm.getStats().sstableCount, + memtableSize: this.lsm.getStats().memtableSize, + estimatedMemory: this.lsm.getEstimatedMemory(), + }; + + // 列基数统计 + const schema = this.schemas.get(tableName); + if (schema && rows.length > 0) { + const columnStats: Record = {}; + for (const colName of Object.keys(schema.columns)) { + const values = new Set(rows.map((r) => String(r[colName]))); + columnStats[colName] = { distinctValues: values.size }; + } + stats.columnStats = columnStats; + } + + return stats; + } + private ensureOpen(): void { if (!this.opened) throw new DatabaseError('AriaEngine not opened', 'DB_NOT_OPEN'); } diff --git a/src/engine/aria/index/lsm.ts b/src/engine/aria/index/lsm.ts index 5f9d4ed..abc26b3 100644 --- a/src/engine/aria/index/lsm.ts +++ b/src/engine/aria/index/lsm.ts @@ -123,6 +123,11 @@ export class LSM { // ======================================================================= put(key: string, value: Record): void { + // 写背压:level 0 SSTable 过多时等待 compaction + if (this.levels[0].length >= 8) { + // 同步执行一次 compaction 缓解压力 + this.compactLevelSync(0); + } this.memtable.put(key, value); this.operationCount++; if (this.memtable.shouldFlush()) { @@ -131,6 +136,9 @@ export class LSM { } delete(key: string): void { + if (this.levels[0].length >= 8) { + this.compactLevelSync(0); + } this.memtable.put(key, { __tombstone: true } as unknown as Record); this.operationCount++; if (this.memtable.shouldFlush()) { @@ -138,6 +146,14 @@ export class LSM { } } + /** 获取估算内存使用(字节) */ + getEstimatedMemory(): number { + let mem = this.memtable.getEstimatedSize(); + if (this.immutableMemtable) mem += this.immutableMemtable.getEstimatedSize(); + for (const [, buf] of this.sstableCache) mem += buf.byteLength; + return mem; + } + freezeMemtable(): void { if (this.immutableMemtable) { this.flushImmutableSync(); @@ -242,9 +258,19 @@ export class LSM { } rangeScan(startKey: string, endKey: string): [string, Record][] { + const result: [string, Record][] = []; + this.rangeScanLazy(startKey, endKey, (k, v) => result.push([k, v])); + return result; + } + + /** 惰性范围扫描:通过回调逐条返回,不一次性物化所有源 */ + rangeScanLazy( + startKey: string, + endKey: string, + callback: (key: string, value: Record) => void, + ): void { const mergeIter = new MergeIterator(); - // MemTable(最新优先) mergeIter.addSource(new ArrayEntrySource( this.memtable.rangeScan(startKey, endKey), )); @@ -255,23 +281,23 @@ export class LSM { )); } - // SSTable for (let level = 0; level < MAX_LSM_LEVELS; level++) { for (const meta of this.levels[level]) { if (endKey < meta.minKey || startKey > meta.maxKey) continue; - const reader = this.loadSSTableReader(meta); if (!reader) continue; - - const entries: [string, Record][] = []; - reader.rangeScan(startKey, endKey, (k, v) => entries.push([k, v])); - mergeIter.addSource(new ArrayEntrySource(entries)); + reader.rangeScan(startKey, endKey, (k, v) => { + mergeIter.addSource(new ArrayEntrySource([[k, v]])); + }); } } const merged = mergeIter.drain(); - return merged - .filter(([, v]) => !(v as unknown as Record).__tombstone); + for (const [k, v] of merged) { + if (!(v as unknown as Record).__tombstone) { + callback(k, v); + } + } } getAllEntries(): [string, Record][] { diff --git a/src/engine/interface.ts b/src/engine/interface.ts index 4cb8135..6a44ee1 100644 --- a/src/engine/interface.ts +++ b/src/engine/interface.ts @@ -65,4 +65,20 @@ export interface IStorageEngine { /** 回滚事务 */ rollbackTransaction(): Promise; + + // ---- Savepoint (可选) ---- + + /** 创建 Savepoint */ + savepoint?(name: string): Promise; + + /** 回滚到 Savepoint */ + rollbackToSavepoint?(name: string): Promise; + + /** 释放 Savepoint */ + releaseSavepoint?(name: string): Promise; + + // ---- 备份 (可选) ---- + + /** 在线备份:导出全库一致性快照 */ + backup?(): Promise[]>>; }