fix: v0.6.1 生产可用性深度审查 — MemoryEngine 级联环无限递归(P0)/ KVStore 多表事务与级联原子化 / 未open防护统一 / 27 个异常场景测试 + aria级联回归
This commit is contained in:
Vendored
+106
-55
@@ -725,9 +725,14 @@ class MemoryEngine {
|
||||
// ---- 外键级联 ----
|
||||
/**
|
||||
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
|
||||
* v0.6.1-fix: 环路保护(A→B→A 级联环不再无限递归栈溢出,AriaEngine 同语义)。
|
||||
* @returns 级联删除的行数
|
||||
*/
|
||||
async cascadeDelete(tableName, pkValue, _row) {
|
||||
async cascadeDelete(tableName, pkValue, _row, visited = new Set()) {
|
||||
const visitKey = `${tableName}:${pkValue}`;
|
||||
if (visited.has(visitKey))
|
||||
return 0;
|
||||
visited.add(visitKey);
|
||||
let totalCascade = 0;
|
||||
for (const [refTableName, refSchema] of this.schemas) {
|
||||
if (refTableName === tableName)
|
||||
@@ -759,7 +764,7 @@ class MemoryEngine {
|
||||
if (refRow) {
|
||||
// v0.3.3: 级联删除前清理索引条目
|
||||
this.removeIndexEntries(refTableName, refRow, refPk);
|
||||
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
|
||||
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow, visited);
|
||||
}
|
||||
refTableData.delete(refPk);
|
||||
totalCascade++;
|
||||
@@ -1722,6 +1727,8 @@ class KVStoreEngine {
|
||||
this.txActive = false;
|
||||
/** 事务中写过的表(commit 时只 flush 这些表) */
|
||||
this.txDirtyTables = new Set();
|
||||
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
|
||||
this.txSchemaChanged = false;
|
||||
this.kv = new KVStore(medium, checkpointThreshold);
|
||||
}
|
||||
// ---- 行 key 编解码 ----
|
||||
@@ -1846,6 +1853,7 @@ class KVStoreEngine {
|
||||
await this.memory.createTable(schema);
|
||||
if (this.txActive) {
|
||||
this.txDirtyTables.add(schema.name);
|
||||
this.txSchemaChanged = true;
|
||||
return;
|
||||
}
|
||||
await this.persistSchema();
|
||||
@@ -1855,19 +1863,27 @@ class KVStoreEngine {
|
||||
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) {
|
||||
this.ensureOpen();
|
||||
return this.memory.hasTable(tableName);
|
||||
}
|
||||
async getTableNames() {
|
||||
this.ensureOpen();
|
||||
return this.memory.getTableNames();
|
||||
}
|
||||
async getTableSchema(tableName) {
|
||||
this.ensureOpen();
|
||||
return this.memory.getTableSchema(tableName);
|
||||
}
|
||||
async alterTable(tableName, action, column) {
|
||||
@@ -1875,12 +1891,17 @@ class KVStoreEngine {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// ---- CRUD ----
|
||||
@@ -1925,16 +1946,18 @@ class KVStoreEngine {
|
||||
this.txDirtyTables.add(tableName);
|
||||
return count;
|
||||
}
|
||||
const puts = {};
|
||||
const deletes = [];
|
||||
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 = {};
|
||||
const deletes = [];
|
||||
for (const pk of affected) {
|
||||
const row = await this.memory.find(tableName, { table: tableName, where: { [pkCol]: pk } });
|
||||
if (row.length > 0) {
|
||||
@@ -1944,16 +1967,20 @@ class KVStoreEngine {
|
||||
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;
|
||||
}
|
||||
async delete(tableName, query) {
|
||||
@@ -1965,14 +1992,20 @@ class KVStoreEngine {
|
||||
this.txDirtyTables.add(tableName);
|
||||
return count;
|
||||
}
|
||||
const puts = {};
|
||||
const deletes = pks.map((pk) => this.rowKey(tableName, pk));
|
||||
// 级联影响表整表 diff(合并到单次原子写,v0.6.1)
|
||||
for (const t of await this.affectedTables(tableName)) {
|
||||
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);
|
||||
// 级联影响表整表 diff
|
||||
for (const t of await this.affectedTables(tableName)) {
|
||||
if (t !== tableName)
|
||||
await this.flushTable(t);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
async count(tableName, query) {
|
||||
@@ -1986,7 +2019,11 @@ class KVStoreEngine {
|
||||
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);
|
||||
}
|
||||
// ---- 动态索引 ----
|
||||
async createIndex(tableName, column, unique) {
|
||||
@@ -1994,6 +2031,7 @@ class KVStoreEngine {
|
||||
await this.memory.createIndex(tableName, column, unique);
|
||||
if (this.txActive) {
|
||||
this.txDirtyTables.add(tableName);
|
||||
this.txSchemaChanged = true;
|
||||
return;
|
||||
}
|
||||
await this.persistSchema();
|
||||
@@ -2003,6 +2041,7 @@ class KVStoreEngine {
|
||||
await this.memory.dropIndex(tableName, column, indexName);
|
||||
if (this.txActive) {
|
||||
this.txDirtyTables.add(tableName);
|
||||
this.txSchemaChanged = true;
|
||||
return;
|
||||
}
|
||||
await this.persistSchema();
|
||||
@@ -2013,27 +2052,40 @@ class KVStoreEngine {
|
||||
await this.memory.beginTransaction();
|
||||
this.txActive = true;
|
||||
this.txDirtyTables = new Set();
|
||||
this.txSchemaChanged = false;
|
||||
}
|
||||
async commitTransaction() {
|
||||
this.ensureOpen();
|
||||
if (!this.txActive)
|
||||
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
// 先持久化(原子),再提交内存快照(失败可回滚)
|
||||
// v0.6.1: 全部 dirty 表合并为单次原子 flush(一条日志记录 = 真原子,
|
||||
// 多表事务中途崩溃/失败不会出现"部分表已提交")
|
||||
const puts = {};
|
||||
const deletes = [];
|
||||
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;
|
||||
@@ -2046,6 +2098,7 @@ class KVStoreEngine {
|
||||
await this.memory.rollbackTransaction();
|
||||
this.txActive = false;
|
||||
this.txDirtyTables = new Set();
|
||||
this.txSchemaChanged = false;
|
||||
}
|
||||
// ---- 内部 ----
|
||||
ensureOpen() {
|
||||
@@ -2108,41 +2161,39 @@ class KVStoreEngine {
|
||||
await this.kv.put(SCHEMA_KEY, enc(JSON.stringify(schemas)));
|
||||
}
|
||||
/**
|
||||
* 整表 diff 持久化:内存行全部 put + KV 残留行删除(原子 putMany + deleteMany)。
|
||||
* 用于主键变更 / 级联 / dropTable / clear / alterTable DROP / 事务 commit。
|
||||
* v0.6.1: 整表 diff 收集(不落盘):内存行全部 put + KV 残留行删除。
|
||||
* 调用方合并到单次原子 putMany/deleteMany(多表操作真原子)。
|
||||
*/
|
||||
async flushTable(tableName) {
|
||||
async collectTableDiff(tableName) {
|
||||
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 = {};
|
||||
const current = new Set();
|
||||
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 = [];
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (Object.keys(puts).length > 0)
|
||||
await this.kv.putMany(puts);
|
||||
if (deletes.length > 0)
|
||||
await this.kv.deleteMany(deletes);
|
||||
else {
|
||||
const all = await this.kv.getAll();
|
||||
for (const [key] of all) {
|
||||
if (key.startsWith(prefix))
|
||||
deletes.push(key);
|
||||
}
|
||||
}
|
||||
return { puts, deletes };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user