fix: v0.6.1 生产可用性深度审查 — MemoryEngine 级联环无限递归(P0)/ KVStore 多表事务与级联原子化 / 未open防护统一 / 27 个异常场景测试 + aria级联回归
CI / test (18.x) (push) Successful in 10m59s
CI / test (20.x) (push) Successful in 10m59s
CI / test (22.x) (push) Successful in 10m49s
CI / test (24.x) (push) Successful in 10m47s
CI / e2e (push) Successful in 10m4s

This commit is contained in:
thzxx
2026-08-10 14:29:40 +08:00
parent a9e1a281d3
commit 8a78f27f7d
15 changed files with 954 additions and 227 deletions
+81 -45
View File
@@ -44,6 +44,8 @@ export class KVStoreEngine implements IStorageEngine {
private txActive = false;
/** 事务中写过的表(commit 时只 flush 这些表) */
private txDirtyTables: Set<string> = new Set();
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
private txSchemaChanged = false;
constructor(medium?: IStorageBackend, checkpointThreshold?: number) {
this.kv = new KVStore(medium, checkpointThreshold);
@@ -175,6 +177,7 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.createTable(schema);
if (this.txActive) {
this.txDirtyTables.add(schema.name);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -185,22 +188,28 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.dropTable(tableName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
// 删除该表全部行(KV 中残留清理)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0) await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0) await this.kv.deleteMany(diff.deletes);
}
async hasTable(tableName: string): Promise<boolean> {
this.ensureOpen();
return this.memory.hasTable(tableName);
}
async getTableNames(): Promise<string[]> {
this.ensureOpen();
return this.memory.getTableNames();
}
async getTableSchema(tableName: string): Promise<TableSchema | null> {
this.ensureOpen();
return this.memory.getTableSchema(tableName);
}
@@ -213,12 +222,15 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.alterTable(tableName, action, column);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
if (action === 'DROP') {
// 重写存储行(移除该列)
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0) await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0) await this.kv.deleteMany(diff.deletes);
}
}
@@ -272,15 +284,17 @@ export class KVStoreEngine implements IStorageEngine {
return count;
}
const puts: Record<string, ArrayBuffer> = {};
const deletes: string[] = [];
if (pkChanged) {
// 主键变更:相关表整表 diff(罕见操作,可靠性优先)
for (const t of await this.affectedTables(tableName)) {
await this.flushTable(t);
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
} else {
// 增量重写受影响行
const puts: Record<string, ArrayBuffer> = {};
const deletes: string[] = [];
for (const pk of affected) {
const row = await this.memory.find(tableName, { table: tableName, where: { [pkCol]: pk } });
if (row.length > 0) {
@@ -289,13 +303,17 @@ export class KVStoreEngine implements IStorageEngine {
deletes.push(this.rowKey(tableName, pk));
}
}
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
// 级联影响表(SET NULL/CASCADE 外键)整表 diff
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName) await this.flushTable(t);
if (t === tableName) continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
}
// 单次原子写(一条日志记录 = 真原子,v0.6.1)
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
return count;
}
@@ -308,12 +326,17 @@ export class KVStoreEngine implements IStorageEngine {
this.txDirtyTables.add(tableName);
return count;
}
const puts: Record<string, ArrayBuffer> = {};
const deletes = pks.map((pk) => this.rowKey(tableName, pk));
if (deletes.length > 0) await this.kv.deleteMany(deletes);
// 级联影响表整表 diff
// 级联影响表整表 diff(合并到单次原子写,v0.6.1)
for (const t of await this.affectedTables(tableName)) {
if (t !== tableName) await this.flushTable(t);
if (t === tableName) continue;
const diff = await this.collectTableDiff(t);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
}
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
return count;
}
@@ -329,7 +352,9 @@ export class KVStoreEngine implements IStorageEngine {
this.txDirtyTables.add(tableName);
return;
}
await this.flushTable(tableName);
const diff = await this.collectTableDiff(tableName);
if (Object.keys(diff.puts).length > 0) await this.kv.putMany(diff.puts);
if (diff.deletes.length > 0) await this.kv.deleteMany(diff.deletes);
}
// ---- 动态索引 ----
@@ -339,6 +364,7 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.createIndex(tableName, column, unique);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -349,6 +375,7 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.dropIndex(tableName, column, indexName);
if (this.txActive) {
this.txDirtyTables.add(tableName);
this.txSchemaChanged = true;
return;
}
await this.persistSchema();
@@ -361,25 +388,36 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.beginTransaction();
this.txActive = true;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
async commitTransaction(): Promise<void> {
this.ensureOpen();
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
// 先持久化(原子),再提交内存快照(失败可回滚)
// v0.6.1: 全部 dirty 表合并为单次原子 flush(一条日志记录 = 真原子,
// 多表事务中途崩溃/失败不会出现"部分表已提交")
const puts: Record<string, ArrayBuffer> = {};
const deletes: string[] = [];
for (const table of this.txDirtyTables) {
if (await this.memory.hasTable(table)) {
await this.flushTable(table);
const diff = await this.collectTableDiff(table);
Object.assign(puts, diff.puts);
deletes.push(...diff.deletes);
} else {
// 事务内 drop 的表:清理 KV 残留行
const all = await this.kv.getAll();
const prefix = this.rowPrefix(table);
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
for (const [key] of all) {
if (key.startsWith(prefix)) deletes.push(key);
}
}
}
// v0.6.0-fix: 事务内 DDLcreate/drop/alter)的 schema 一并持久化
await this.persistSchema();
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
// 事务内 DDL 的 schema 一并持久化
if (this.txSchemaChanged) {
await this.persistSchema();
}
await this.kv.checkpoint();
await this.memory.commitTransaction();
this.txActive = false;
@@ -392,6 +430,7 @@ export class KVStoreEngine implements IStorageEngine {
await this.memory.rollbackTransaction();
this.txActive = false;
this.txDirtyTables = new Set();
this.txSchemaChanged = false;
}
// ---- 内部 ----
@@ -455,38 +494,35 @@ export class KVStoreEngine implements IStorageEngine {
}
/**
* 整表 diff 持久化:内存行全部 put + KV 残留行删除(原子 putMany + deleteMany
* 用于主键变更 / 级联 / dropTable / clear / alterTable DROP / 事务 commit
* v0.6.1: 整表 diff 收集(不落盘):内存行全部 put + KV 残留行删除。
* 调用方合并到单次原子 putMany/deleteMany(多表操作真原子)
*/
private async flushTable(tableName: string): Promise<void> {
private async collectTableDiff(tableName: string): Promise<{ puts: Record<string, ArrayBuffer>; deletes: string[] }> {
const prefix = this.rowPrefix(tableName);
// 表已删除:仅清理 KV 残留行
const schema = await this.memory.getTableSchema(tableName);
if (!schema) {
const all = await this.kv.getAll();
const deletes = all.filter(([key]) => key.startsWith(prefix)).map(([key]) => key);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
return;
}
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const puts: Record<string, ArrayBuffer> = {};
const current = new Set<string>();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)删除
const all = await this.kv.getAll();
const deletes: string[] = [];
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key)) {
deletes.push(key);
// 表已删除:仅收集 KV 残留行删除
const schema = await this.memory.getTableSchema(tableName);
if (schema) {
const pkCol = this.getPK(schema);
const rows = await this.memory.find(tableName, { table: tableName });
const current = new Set<string>();
for (const row of rows) {
const key = this.rowKey(tableName, String(row[pkCol]));
current.add(key);
puts[key] = enc(JSON.stringify(row));
}
// KV 残留行(内存中已不存在)
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix) && !current.has(key)) deletes.push(key);
}
} else {
const all = await this.kv.getAll();
for (const [key] of all) {
if (key.startsWith(prefix)) deletes.push(key);
}
}
if (Object.keys(puts).length > 0) await this.kv.putMany(puts);
if (deletes.length > 0) await this.kv.deleteMany(deletes);
return { puts, deletes };
}
}