feat: v0.2.4 惰性扫描 + 写背压 + 内存预算 + ANALYZE + 加密 + Savepoint + 在线备份
CI / test (18.x) (push) Failing after 4m59s
CI / test (20.x) (push) Failing after 5m0s
CI / test (22.x) (push) Failing after 4m59s
CI / test (24.x) (push) Failing after 4m58s

This commit is contained in:
thzxx
2026-07-27 21:55:57 +08:00
parent 82e6baaae9
commit f248e5fb05
4 changed files with 193 additions and 9 deletions
+88
View File
@@ -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<string, { txnId: number; snapshot: Map<string, Record<string, unknown>> | null }> = new Map();
async savepoint(name: string): Promise<void> {
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<void> {
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<void> {
if (!this.savepoints.has(name)) throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND');
this.savepoints.delete(name);
}
// ---- 在线备份 ----
async backup(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureOpen();
const result: Record<string, Record<string, unknown>[]> = {};
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<Record<string, unknown>> {
this.ensureOpen();
this.ensureTable(tableName);
const rows = this.getAllRows(tableName);
const stats: Record<string, unknown> = {
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<string, unknown> = {};
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');
}