/** * 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, code: string): Promise { 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 { throw new Error('disk full'); } async readAll(): Promise { return new Uint8Array(0); } async truncate(): Promise {} async exists(): Promise { 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 { 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(); }); });