/** * v0.8.0 回归 —— Aria SAVEPOINT 语义根治 * * 覆盖两个此前会导致**静默数据错误**的缺陷: * * A3 已回滚的行在崩溃重启后复活 * `ROLLBACK TO ` 此前只改内存快照、不写 WAL,而 COMMIT 会把整个 * txnId 标记为已提交,恢复时按"该事务的全部记录"重放 —— 于是被回滚掉的写入 * 被重新应用。实测:实时只剩 a,崩溃重开变成 a+b。 * 修复:savepoint 回滚写入 SAVEPOINT_ROLLBACK 记录(携带 replayFromIndex * 边界),恢复时只应用"保存点之后、最后一个标记之前"的写入。 * * A4 跨事务复用的陈旧 savepoint 静默丢弃当前事务的写入 * savepoints 在 commit/rollback 时从不清空,且 rollbackToSavepoint 不校验归属。 * 实测:新事务 update 后 ROLLBACK TO 上一事务遗留的名字 + COMMIT,写入凭空消失。 * 修复:事务结束清空 savepoint;回滚前校验 txnId 归属。 */ import { MetonaSqlark } from '../src/core'; import { AriaEngine } from '../src/engine/aria/index'; describe('[v0.8.0] A4 跨事务 savepoint 泄漏', () => { test('陈旧 savepoint 名不可重用,当前事务写入不被替换', async () => { const db = await MetonaSqlark.create({ name: 'v080-sp-stale', mode: 'aria', diskEngine: 'memory' } as never); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }); await db.query("INSERT INTO t VALUES ('x', 0)"); await db.query('BEGIN'); await db.query("UPDATE t SET v = 9 WHERE id='x'"); await db.query('SAVEPOINT sp'); await db.query('COMMIT'); expect((await db.query("SELECT v FROM t WHERE id='x'") as Array<{ v: number }>)[0].v).toBe(9); await db.query('BEGIN'); await db.query("UPDATE t SET v = 5 WHERE id='x'"); // 上一事务的 savepoint 随事务结束失效 await expect(db.query('ROLLBACK TO sp')).rejects.toMatchObject({ code: 'SAVEPOINT_NOT_FOUND' }); await db.query('COMMIT'); // 关键:本事务的写入必须保留(此前会被旧快照静默替换回 9) expect((await db.query("SELECT v FROM t WHERE id='x'") as Array<{ v: number }>)[0].v).toBe(5); await db.close(); }); }); describe('[v0.8.0] A3 savepoint 回滚的崩溃一致性', () => { /** kv 后端介质按库名存于全局注册表且 close 不清数据 → 新引擎 open 同名库 = 崩溃重启 */ const CFG = { storageBackend: 'kv' as const, walSyncMode: 'full' as const, checkpointInterval: 999999 }; test('回滚到保存点后提交:崩溃重放不得复活已回滚的行', async () => { const eng = new AriaEngine(CFG) as unknown as { open(n: string, v: number): Promise; createTable(s: unknown): Promise; beginTransaction(): Promise; insert(t: string, r: unknown[]): Promise; savepoint(n: string): Promise; rollbackToSavepoint(n: string): Promise; commitTransaction(): Promise; find(t: string, q: unknown): Promise>>; close(): Promise; }; await eng.open('v080-sp-crash', 1); await eng.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await eng.beginTransaction(); await eng.insert('t', [{ id: 'a' }]); await eng.savepoint('s'); await eng.insert('t', [{ id: 'b' }]); await eng.rollbackToSavepoint('s'); await eng.commitTransaction(); const live = (await eng.find('t', { table: 't' })).map((r) => r.id).sort(); // 崩溃:不调用 close()(close 会 checkpoint 截断 WAL,掩盖重放路径) const eng2 = new AriaEngine(CFG) as unknown as typeof eng; await eng2.open('v080-sp-crash', 1); const after = (await eng2.find('t', { table: 't' })).map((r) => r.id).sort(); expect(live).toEqual(['a']); expect(after).toEqual(live); // 不得复活 b await eng2.close(); }); test('无 savepoint 的普通事务崩溃重放仍然完整(不得误伤)', async () => { const eng = new AriaEngine(CFG) as unknown as { open(n: string, v: number): Promise; createTable(s: unknown): Promise; beginTransaction(): Promise; insert(t: string, r: unknown[]): Promise; commitTransaction(): Promise; find(t: string, q: unknown): Promise>>; close(): Promise; }; await eng.open('v080-sp-crash2', 1); await eng.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await eng.beginTransaction(); await eng.insert('t', [{ id: 'a' }]); await eng.insert('t', [{ id: 'b' }]); await eng.commitTransaction(); const live = (await eng.find('t', { table: 't' })).map((r) => r.id).sort(); const eng2 = new AriaEngine(CFG) as unknown as typeof eng; await eng2.open('v080-sp-crash2', 1); const after = (await eng2.find('t', { table: 't' })).map((r) => r.id).sort(); expect(live).toEqual(['a', 'b']); expect(after).toEqual(live); await eng2.close(); }); test('多个保存点:回到最早保存点,其后写入全部丢弃', async () => { const eng = new AriaEngine(CFG) as unknown as { open(n: string, v: number): Promise; createTable(s: unknown): Promise; beginTransaction(): Promise; insert(t: string, r: unknown[]): Promise; savepoint(n: string): Promise; rollbackToSavepoint(n: string): Promise; commitTransaction(): Promise; find(t: string, q: unknown): Promise>>; close(): Promise; }; await eng.open('v080-sp-multi', 1); await eng.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await eng.beginTransaction(); await eng.insert('t', [{ id: 'a' }]); await eng.savepoint('s1'); await eng.insert('t', [{ id: 'b' }]); await eng.savepoint('s2'); await eng.insert('t', [{ id: 'c' }]); await eng.rollbackToSavepoint('s1'); await eng.commitTransaction(); const live = (await eng.find('t', { table: 't' })).map((r) => r.id).sort(); const eng2 = new AriaEngine(CFG) as unknown as typeof eng; await eng2.open('v080-sp-multi', 1); const after = (await eng2.find('t', { table: 't' })).map((r) => r.id).sort(); expect(live).toEqual(['a']); expect(after).toEqual(['a']); await eng2.close(); }); });