fix: v0.6.1 生产可用性深度审查 — MemoryEngine 级联环无限递归(P0)/ KVStore 多表事务与级联原子化 / 未open防护统一 / 27 个异常场景测试 + aria级联回归
This commit is contained in:
@@ -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: 事务内 DDL(create/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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user