fix: crypto TS类型修复 + EXPLAIN AST类型补充
This commit is contained in:
Vendored
+173
-9
@@ -2235,6 +2235,7 @@ class LSM {
|
||||
this.nextSSTableId = 1;
|
||||
this.operationCount = 0;
|
||||
this.initialized = false;
|
||||
this.compacting = false; // 防止重复触发 compaction
|
||||
this.memtable = new MemTable(config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE);
|
||||
this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER;
|
||||
this.blockSize = config.blockSize ?? 4096;
|
||||
@@ -2279,6 +2280,11 @@ class LSM {
|
||||
// 写入
|
||||
// =======================================================================
|
||||
put(key, value) {
|
||||
// 写背压: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()) {
|
||||
@@ -2286,12 +2292,24 @@ class LSM {
|
||||
}
|
||||
}
|
||||
delete(key) {
|
||||
if (this.levels[0].length >= 8) {
|
||||
this.compactLevelSync(0);
|
||||
}
|
||||
this.memtable.put(key, { __tombstone: true });
|
||||
this.operationCount++;
|
||||
if (this.memtable.shouldFlush()) {
|
||||
this.freezeMemtable();
|
||||
}
|
||||
}
|
||||
/** 获取估算内存使用(字节) */
|
||||
getEstimatedMemory() {
|
||||
let mem = this.memtable.getEstimatedSize();
|
||||
if (this.immutableMemtable)
|
||||
mem += this.immutableMemtable.getEstimatedSize();
|
||||
for (const [, buf] of this.sstableCache)
|
||||
mem += buf.byteLength;
|
||||
return mem;
|
||||
}
|
||||
freezeMemtable() {
|
||||
if (this.immutableMemtable) {
|
||||
this.flushImmutableSync();
|
||||
@@ -2330,10 +2348,33 @@ class LSM {
|
||||
this.sstableStore.saveMeta(meta).catch(() => { });
|
||||
this.levels[0].push(meta);
|
||||
this.immutableMemtable = null;
|
||||
if (this.levels[0].length >= 4) {
|
||||
this.compactLevelSync(0);
|
||||
// 异步触发 compaction(不阻塞当前写入)
|
||||
if (this.levels[0].length >= 4 && !this.compacting) {
|
||||
this.scheduleCompact(0);
|
||||
}
|
||||
}
|
||||
/** 异步调度 compaction,使用 setTimeout 分片执行 */
|
||||
scheduleCompact(level) {
|
||||
if (level >= MAX_LSM_LEVELS - 1 || this.compacting)
|
||||
return;
|
||||
this.compacting = true;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.compactLevelSync(level);
|
||||
}
|
||||
finally {
|
||||
this.compacting = false;
|
||||
// 连续触发:如果 compaction 后仍然超标,继续调度
|
||||
if (this.levels[level].length >= 4) {
|
||||
this.scheduleCompact(level);
|
||||
}
|
||||
// 检查下一级是否需要 compaction
|
||||
if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
|
||||
this.scheduleCompact(level + 1);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
// =======================================================================
|
||||
// 读取
|
||||
// =======================================================================
|
||||
@@ -2364,13 +2405,17 @@ class LSM {
|
||||
return null;
|
||||
}
|
||||
rangeScan(startKey, endKey) {
|
||||
const result = [];
|
||||
this.rangeScanLazy(startKey, endKey, (k, v) => result.push([k, v]));
|
||||
return result;
|
||||
}
|
||||
/** 惰性范围扫描:通过回调逐条返回,不一次性物化所有源 */
|
||||
rangeScanLazy(startKey, endKey, callback) {
|
||||
const mergeIter = new MergeIterator();
|
||||
// MemTable(最新优先)
|
||||
mergeIter.addSource(new ArrayEntrySource(this.memtable.rangeScan(startKey, endKey)));
|
||||
if (this.immutableMemtable) {
|
||||
mergeIter.addSource(new ArrayEntrySource(this.immutableMemtable.rangeScan(startKey, endKey)));
|
||||
}
|
||||
// SSTable
|
||||
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
|
||||
for (const meta of this.levels[level]) {
|
||||
if (endKey < meta.minKey || startKey > meta.maxKey)
|
||||
@@ -2378,14 +2423,17 @@ class LSM {
|
||||
const reader = this.loadSSTableReader(meta);
|
||||
if (!reader)
|
||||
continue;
|
||||
const entries = [];
|
||||
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.__tombstone);
|
||||
for (const [k, v] of merged) {
|
||||
if (!v.__tombstone) {
|
||||
callback(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
getAllEntries() {
|
||||
const result = new Map();
|
||||
@@ -3242,6 +3290,8 @@ class AriaEngine {
|
||||
this.currentTxnId = null;
|
||||
this.txnSnapshot = null;
|
||||
this.gcCounter = 0;
|
||||
// ---- Savepoint 嵌套事务 ----
|
||||
this.savepoints = new Map();
|
||||
this.config = { ...DEFAULT_ARIA_CONFIG, ...config };
|
||||
}
|
||||
// =======================================================================
|
||||
@@ -3455,6 +3505,7 @@ class AriaEngine {
|
||||
});
|
||||
}
|
||||
this.opCounter += rows.length;
|
||||
this.checkMemoryBudget();
|
||||
await this.checkpointManager.tick();
|
||||
this.tryGC();
|
||||
return pks;
|
||||
@@ -3643,6 +3694,48 @@ class AriaEngine {
|
||||
});
|
||||
this.currentTxnId = null;
|
||||
}
|
||||
async savepoint(name) {
|
||||
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) {
|
||||
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) {
|
||||
if (!this.savepoints.has(name))
|
||||
throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND');
|
||||
this.savepoints.delete(name);
|
||||
}
|
||||
// ---- 在线备份 ----
|
||||
async backup() {
|
||||
this.ensureOpen();
|
||||
const result = {};
|
||||
for (const tableName of this.schemas.keys()) {
|
||||
result[tableName] = this.getAllRows(tableName);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// =======================================================================
|
||||
// 内部
|
||||
// =======================================================================
|
||||
@@ -3941,6 +4034,46 @@ class AriaEngine {
|
||||
this.gcCounter = 0;
|
||||
}
|
||||
}
|
||||
/** 检查内存预算,超出时强制 flush + GC */
|
||||
checkMemoryBudget() {
|
||||
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) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
const rows = this.getAllRows(tableName);
|
||||
const stats = {
|
||||
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) => 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 = {};
|
||||
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;
|
||||
}
|
||||
ensureOpen() {
|
||||
if (!this.opened)
|
||||
throw new DatabaseError('AriaEngine not opened', 'DB_NOT_OPEN');
|
||||
@@ -4421,6 +4554,7 @@ class QueryExecutor {
|
||||
async execute(stmt) {
|
||||
switch (stmt.type) {
|
||||
case 'SELECT': return this.executeSelect(stmt);
|
||||
case 'EXPLAIN': return this.executeExplain(stmt);
|
||||
case 'INSERT': return this.executeInsert(stmt);
|
||||
case 'UPDATE': return this.executeUpdate(stmt);
|
||||
case 'DELETE': return this.executeDelete(stmt);
|
||||
@@ -4429,6 +4563,30 @@ class QueryExecutor {
|
||||
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
|
||||
}
|
||||
}
|
||||
/** EXPLAIN: 输出查询计划 */
|
||||
async executeExplain(stmt) {
|
||||
const plan = compileStatement(stmt.query.type === 'SELECT' ? stmt.query : stmt.query);
|
||||
const startTime = Date.now();
|
||||
let result = null;
|
||||
try {
|
||||
result = await this.execute(stmt.query);
|
||||
}
|
||||
catch { /* explain 即使执行失败也返回计划 */ }
|
||||
const elapsed = Date.now() - startTime;
|
||||
const rows = Array.isArray(result) ? result.length : 0;
|
||||
return {
|
||||
type: stmt.query.type,
|
||||
table: plan.table,
|
||||
columns: plan.columns,
|
||||
where: plan.where || {},
|
||||
orderBy: plan.orderBy || [],
|
||||
limit: plan.limit,
|
||||
offset: plan.offset,
|
||||
usingIndex: plan.table ? 'auto' : 'none',
|
||||
estimatedRows: rows,
|
||||
actualTimeMs: elapsed,
|
||||
};
|
||||
}
|
||||
// ===================================================================
|
||||
// SELECT
|
||||
// ===================================================================
|
||||
@@ -6101,10 +6259,16 @@ class MetonaSqlark {
|
||||
/** 执行 SQL 字符串查询 */
|
||||
async query(sql) {
|
||||
this.ensureReady();
|
||||
const startTime = this.debug ? Date.now() : 0;
|
||||
await this.pluginManager.trigger('beforeQuery', sql);
|
||||
const stmt = parse(sql);
|
||||
const result = await this.executor.execute(stmt);
|
||||
await this.pluginManager.trigger('afterQuery', sql, result);
|
||||
if (this.debug) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const rows = Array.isArray(result) ? result.length : 0;
|
||||
this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// ---- 事务 ----
|
||||
|
||||
Reference in New Issue
Block a user