fix(P2): v0.6.3 原子性/一致性/资源治理 — 8 项修复 + 12 回归
- 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:
@@ -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 delete(SET 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=1(savepoint 后插入的 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user