fix(P2): v0.6.3 原子性/一致性/资源治理 — 8 项修复 + 12 回归
CI / test (18.x) (push) Successful in 19m13s
CI / test (20.x) (push) Successful in 18m8s
CI / test (22.x) (push) Successful in 17m18s
CI / test (24.x) (push) Successful in 22m57s
CI / e2e (push) Successful in 9m53s

- KVStore 混合写单记录原子:新增 writeBatch(put+delete 同一条日志记录),
  KVStoreEngine 全部混合写路径统一(兑现真原子宣称,崩溃无新旧行并存)
- WAL full 模式写入失败抛错(此前 console.warn 吞错 → 崩溃即丢且无感知)
- MemoryEngine SET NULL 级联索引残留:复用 removeIndexEntries(消除虚假 UNIQUE_VIOLATION)
- delete 级联两阶段:先全量 RESTRICT 预检(沿 CASCADE 链递归)再执行,无部分级联
  (Memory/Aria 对齐)
- BufferPool 驱逐同步清理 pages Map(EvictionManager onRemove 回调,内存预算真实生效)
- MVCC commit 清理已提交版本(版本链仅作事务内 undo,消除行数据双份常驻)
- LSM.flush 重复入链修复(入链即置空 immutable)+ frozenMemtables 可见性时序
- rollbackToSavepoint 重建受影响表二级索引(消除过期索引条目)

测试 1114 → 1126(71 套件);行覆盖率 89.7%;版本 0.6.3
This commit is contained in:
thzxx
2026-08-13 10:30:39 +08:00
parent ef1934a38c
commit f97c5a6001
26 changed files with 1075 additions and 250 deletions
+294
View File
@@ -0,0 +1,294 @@
/**
* v0.6.3 修复回归测试 — 原子性/一致性/资源治理
*
* 覆盖:
* - B7: KVStore writeBatch 混合 put+delete 单条日志记录(真原子)
* - B8: WAL full 模式写入失败抛错(不再吞错)
* - B6: MemoryEngine SET NULL 级联索引残留(虚假 UNIQUE_VIOLATION
* - B10: delete 级联两阶段(RESTRICT 预检,无部分级联)
* - B13: BufferPool 驱逐同步清理 pages Map(内存预算真实生效)
* - B11: LSM.flush 不再重复入链(无重复 SSTable)
* - B12: rollbackToSavepoint 重建受影响表二级索引
*/
import { MemoryEngine } from '../src/engine/memory';
import { KVStoreEngine } from '../src/engine/kvstore_engine';
import { AriaEngine } from '../src/engine/aria/index';
import { createSchema } from '../src/table/schema';
import { KVStore } from '../src/engine/kvstore/index';
import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium';
import { parseLogRecords, KVLogOp } from '../src/engine/kvstore/log';
import { WAL } from '../src/engine/aria/wal/log';
import { BufferPool } from '../src/engine/aria/buffer/pool';
function uniqueDB(): string {
return `v063-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
/** 断言 Promise 以指定错误码拒绝 */
async function expectCode(promise: Promise<unknown>, code: string): Promise<void> {
try {
await promise;
} catch (e) {
expect((e as { code?: string }).code).toBe(code);
return;
}
throw new Error(`Expected rejection with code "${code}", but promise resolved`);
}
describe('v0.6.3 — KVStore writeBatch 混合原子(B7', () => {
it('put+delete 编码进同一条日志记录', async () => {
const kv = new KVStore(new SharedMemoryBackend(), 0);
await kv.open(uniqueDB());
await kv.put('keep', new TextEncoder().encode('k').buffer);
await kv.writeBatch(
{ a: new TextEncoder().encode('1').buffer, b: new TextEncoder().encode('2').buffer },
['old1', 'old2'],
);
// 直接读取日志文件字节:解析应为 2 条记录(put keep + 混合记录),
// 混合记录包含 2 PUT + 2 DELETE 条目
const medium = (kv as any).medium as SharedMemoryBackend;
const raw = await medium.read('__kv_log');
expect(raw).not.toBeNull();
const records: { entries: { op: KVLogOp }[] }[] = [];
parseLogRecords(new Uint8Array(raw!), (r) => records.push(r as never));
expect(records).toHaveLength(2);
const mixed = records[1].entries.map((e) => e.op);
expect(mixed.filter((op) => op === KVLogOp.PUT)).toHaveLength(2);
expect(mixed.filter((op) => op === KVLogOp.DELETE)).toHaveLength(2);
await kv.close();
});
it('KVStoreEngine deleteSET NULL 级联)混合写为单记录', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
}));
await eng.createTable(createSchema('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'SET NULL' },
}));
await eng.insert('users', [{ id: 'u1' }]);
await eng.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
await eng.delete('users', { table: 'users', where: { id: 'u1' } });
// 日志未 checkpoint:最后一条记录应同时含 orders 行重写(PUT)与 users 行删除(DELETE
const kv = (eng as any).kv as KVStore;
const medium = (kv as any).medium as SharedMemoryBackend;
const raw = await medium.read('__kv_log');
const records: { entries: { op: KVLogOp }[] }[] = [];
parseLogRecords(new Uint8Array(raw!), (r) => records.push(r as never));
const last = records[records.length - 1].entries.map((e) => e.op);
expect(last).toContain(KVLogOp.PUT);
expect(last).toContain(KVLogOp.DELETE);
await eng.close();
// 重开后效果完整(SET NULL + 父行删除均生效)
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const orders = await eng2.find('orders', { table: 'orders' });
expect(orders).toHaveLength(1);
expect(orders[0].user_id).toBeNull();
expect(await eng2.count('users')).toBe(0);
await eng2.close();
});
});
describe('v0.6.3 — WAL 写入失败传播(B8', () => {
class FailingStore {
async append(_d: Uint8Array): Promise<void> { throw new Error('disk full'); }
async readAll(): Promise<Uint8Array> { return new Uint8Array(0); }
async truncate(): Promise<void> {}
async exists(): Promise<boolean> { return false; }
}
it("full 模式 append 失败 reject(不再 console.warn 吞错)", async () => {
const wal = new WAL(new FailingStore() as never, true, 'full');
await expect(wal.append({
type: 1, txnId: 0, tableName: 't', key: '1', data: { v: 1 },
})).rejects.toThrow('disk full');
await expect(wal.appendBatch([{
type: 1, txnId: 0, tableName: 't', key: '2', data: { v: 2 },
}])).rejects.toThrow('disk full');
});
});
describe('v0.6.3 — MemoryEngine SET NULL 索引清理(B6', () => {
it('级联 SET NULL 后旧值可被复用(不再虚假 UNIQUE_VIOLATION', async () => {
const eng = new MemoryEngine();
await eng.open(uniqueDB(), 1);
await eng.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
}));
await eng.createTable(createSchema('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'SET NULL', unique: true },
}));
await eng.insert('users', [{ id: 'u1' }]);
await eng.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
await eng.delete('users', { table: 'users', where: { id: 'u1' } });
// 旧值 u1 的索引条目应被完整清理(含空 Set 移除)
await eng.insert('orders', [{ id: 'o2', user_id: 'u1' }]);
const rows = await eng.find('orders', { table: 'orders' });
expect(rows).toHaveLength(2);
// 索引查询旧值返回正确
const byIdx = await eng.find('orders', { table: 'orders', where: { user_id: 'u1' } });
expect(byIdx).toHaveLength(1);
expect(byIdx[0].id).toBe('o2');
});
});
describe('v0.6.3 — delete 级联两阶段(B10', () => {
async function buildMemory(): Promise<MemoryEngine> {
const eng = new MemoryEngine();
await eng.open(uniqueDB(), 1);
await eng.createTable(createSchema('parents', {
id: { type: 'string', primaryKey: true },
}));
await eng.createTable(createSchema('cascade_child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', references: 'parents.id', onDelete: 'CASCADE' },
}));
await eng.createTable(createSchema('restrict_child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', references: 'parents.id', onDelete: 'RESTRICT' },
}));
return eng;
}
it('Memory: 多行删除含 RESTRICT 违规则整体拒绝(无部分级联)', async () => {
const eng = await buildMemory();
await eng.insert('parents', [{ id: 'p1' }, { id: 'p2' }]);
await eng.insert('cascade_child', [{ id: 'c1', pid: 'p1' }]);
await eng.insert('restrict_child', [{ id: 'r2', pid: 'p2' }]);
await expectCode(eng.delete('parents', { table: 'parents' }), 'FOREIGN_KEY_VIOLATION');
// p1 的级联子行必须仍在(修复前:p1 子行已删、父行未删)
expect(await eng.count('parents')).toBe(2);
expect(await eng.count('cascade_child')).toBe(1);
expect(await eng.count('restrict_child')).toBe(1);
});
it('Aria: 多行删除含 RESTRICT 违规则整体拒绝(无部分级联)', async () => {
const eng = new AriaEngine({ storageBackend: 'memory' });
await eng.open(uniqueDB(), 1);
await eng.createTable(createSchema('parents', {
id: { type: 'string', primaryKey: true },
}));
await eng.createTable(createSchema('cascade_child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', references: 'parents.id', onDelete: 'CASCADE' },
}));
await eng.createTable(createSchema('restrict_child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', references: 'parents.id', onDelete: 'RESTRICT' },
}));
await eng.insert('parents', [{ id: 'p1' }, { id: 'p2' }]);
await eng.insert('cascade_child', [{ id: 'c1', pid: 'p1' }]);
await eng.insert('restrict_child', [{ id: 'r2', pid: 'p2' }]);
await expectCode(eng.delete('parents', { table: 'parents' }), 'FOREIGN_KEY_VIOLATION');
expect(await eng.count('parents')).toBe(2);
expect(await eng.count('cascade_child')).toBe(1);
expect(await eng.count('restrict_child')).toBe(1);
await eng.close();
});
it('深层 CASCADE 链末端的 RESTRICT 也被预检拦截', async () => {
const eng = await buildMemory();
await eng.createTable(createSchema('deep_child', {
id: { type: 'string', primaryKey: true },
cid: { type: 'string', references: 'cascade_child.id', onDelete: 'RESTRICT' },
}));
await eng.insert('parents', [{ id: 'p1' }]);
await eng.insert('cascade_child', [{ id: 'c1', pid: 'p1' }]);
await eng.insert('deep_child', [{ id: 'd1', cid: 'c1' }]);
await expectCode(eng.delete('parents', { table: 'parents', where: { id: 'p1' } }), 'FOREIGN_KEY_VIOLATION');
expect(await eng.count('parents')).toBe(1);
expect(await eng.count('cascade_child')).toBe(1);
expect(await eng.count('deep_child')).toBe(1);
});
it('无 RESTRICT 违规时级联删除照常完整执行', async () => {
const eng = await buildMemory();
await eng.insert('parents', [{ id: 'p1' }, { id: 'p2' }]);
await eng.insert('cascade_child', [{ id: 'c1', pid: 'p1' }, { id: 'c2', pid: 'p2' }]);
const count = await eng.delete('parents', { table: 'parents' });
expect(count).toBe(4); // 2 父 + 2 级联子
expect(await eng.count('parents')).toBe(0);
expect(await eng.count('cascade_child')).toBe(0);
});
});
describe('v0.6.3 — BufferPool 内存预算(B13', () => {
it('驱逐同步清理 pages Map(容量约束真实生效)', async () => {
const pageIO = {
readPage: async (id: number) => {
const b = new ArrayBuffer(4096);
new DataView(b).setUint32(0, id);
new DataView(b).setUint8(4, 1);
return b;
},
writePage: async () => {},
allocatePageId: async () => 0,
freePageId: async () => {},
};
const pool = new BufferPool(pageIO, 2);
for (let i = 1; i <= 20; i++) {
const p = await pool.getPage(i);
pool.unpin(p!);
}
expect((pool as any).pages.size).toBeLessThanOrEqual(2);
// 驱逐后再次访问能重新加载(正确性不受影响)
const p = await pool.getPage(20);
expect(p).not.toBeNull();
pool.unpin(p!);
});
});
describe('v0.6.3 — LSM.flush 不重复入链(B11', () => {
it('2 行 + 中间冻结 + flush → 恰好 2 个 SSTable(无重复产物)', async () => {
const eng = new AriaEngine({ storageBackend: 'memory' });
await eng.open(uniqueDB(), 1);
await eng.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'string' },
}));
const lsm = (eng as any).lsm;
await eng.insert('t', [{ id: '1', v: 'a' }]);
lsm.freezeMemtable();
await eng.insert('t', [{ id: '2', v: 'b' }]);
await lsm.flush();
const metas = lsm.levels[0] as { id: number; minKey: string; maxKey: string }[];
expect(metas).toHaveLength(2);
const keys = metas.flatMap((m) => [m.minKey, m.maxKey]).sort();
expect(keys).toEqual(['t:1', 't:1', 't:2', 't:2']);
const rows = await eng.find('t', { table: 't' });
expect(rows).toHaveLength(2);
await eng.close();
});
});
describe('v0.6.3 — rollbackToSavepoint 索引重建(B12', () => {
it('savepoint 回滚后索引不含过期条目', async () => {
const eng = new AriaEngine({ storageBackend: 'memory' });
await eng.open(uniqueDB(), 1);
await eng.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
}));
await eng.insert('t', [{ id: '1', tag: 'x' }]);
await eng.beginTransaction();
await (eng as any).savepoint('sp1');
await eng.insert('t', [{ id: '2', tag: 'x' }]);
await (eng as any).rollbackToSavepoint('sp1');
await eng.commitTransaction();
// 数据只有 1 行
expect(await eng.count('t')).toBe(1);
// 索引查询 x 只返回 id=1savepoint 后插入的 id=2 索引条目已清理)
const rows = await eng.find('t', { table: 't', where: { tag: 'x' } });
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
await eng.close();
});
});