/** * v0.8.0 回归套件 —— 外键级联:自引用与传递链(A13/A14) * ============================================================================ * 修复前的两个不同结论(都是实测得出,不是推断): * * A13 **自引用外键完全不做级联**(真实缺陷) * MemoryEngine 与 AriaEngine 的级联实现里都有 `if (refTableName === tableName) continue;` * —— 于是 `parent_id REFERENCES node(id)` 这种树形自引用被整体跳过: * INSERT: root ← a ← b ← c * DELETE root → 只删掉 root,a/b/c 全部残留且 parent_id 指向已删除的行 * 因为父行已经不在了,这些行**之后再也无法通过级联清理**(永久悬挂)。 * `ON DELETE SET NULL` 同样完全失效(子行的 parent_id 保持旧值)。 * * A14 ON UPDATE CASCADE 传递链(**审计结论有误,实测正常**) * 审计记录为"A→B→C 链改 A 主键后 C 悬空",但本套件的用例证明: * `UPDATE a SET id='A2'` 之后 b.a_id 正确变为 A2 —— C 引用的是 **b.id**(未变), * 因此 C 并不需要更新,"悬空"的推断不成立。这里把它固化为回归护栏, * 避免将来有人按那份错误结论去"修"一个不存在的问题。 * * 核心不变量(本套件的重点):**四个引擎对同一份 schema + 同一组操作必须给出 * 相同结果**。此前 Memory 与 Aria 的跳过条件逐字相同,因此缺陷是同步的; * 现在两边的修复也必须同步。 */ import { MetonaSqlark } from '../src/core'; import { rows as rowsOf, expectCode } from './helpers/assertions'; import type { DatabaseConfig } from '../src/constants'; const ENGINES: Array<[string, DatabaseConfig['mode'], Partial]> = [ ['memory', 'memory', {}], ['disk', 'disk', {}], ['hybrid', 'hybrid', {}], ['aria', 'aria', { diskEngine: 'memory' }], ]; describe('[v0.8.0] A13 自引用外键', () => { describe.each(ENGINES)('%s 引擎', (label, mode, extra) => { let db: MetonaSqlark; let seq = 0; beforeEach(async () => { seq += 1; db = await MetonaSqlark.create({ name: `a13-${label}-${seq}-${Math.random().toString(36).slice(2)}`, mode, ...extra, }); }); afterEach(async () => { await db.close(); }); it('ON DELETE CASCADE:删根节点清掉整棵子树', async () => { await db.query( 'CREATE TABLE node (id STRING PRIMARY KEY, parent_id STRING REFERENCES node(id) ON DELETE CASCADE)', ); await db.query("INSERT INTO node VALUES ('root', NULL), ('a', 'root'), ('b', 'a'), ('c', 'b')"); // 修复前:只删掉 root,a/b/c 残留(且永久悬挂) const affected = await db.query("DELETE FROM node WHERE id = 'root'"); expect(affected).toBe(4); expect(rowsOf(await db.query('SELECT id FROM node'))).toEqual([]); }); it('ON DELETE CASCADE:只删被删节点的子树,兄弟分支保留', async () => { await db.query( 'CREATE TABLE node (id STRING PRIMARY KEY, parent_id STRING REFERENCES node(id) ON DELETE CASCADE)', ); await db.query( "INSERT INTO node VALUES ('root', NULL), ('a', 'root'), ('a1', 'a'), ('a2', 'a1'), ('x', 'root'), ('x1', 'x')", ); // 删 a 及其子树 a1/a2,root 与 x/x1 保留 const affected = await db.query("DELETE FROM node WHERE id = 'a'"); expect(affected).toBe(3); const ids = rowsOf<{ id: string }>(await db.query('SELECT id FROM node ORDER BY id')).map((r) => r.id); expect(ids).toEqual(['root', 'x', 'x1']); }); it('ON DELETE SET NULL:直接子节点的外键置空(不递归)', async () => { await db.query( 'CREATE TABLE node (id STRING PRIMARY KEY, parent_id STRING REFERENCES node(id) ON DELETE SET NULL)', ); await db.query("INSERT INTO node VALUES ('root', NULL), ('a', 'root'), ('b', 'a')"); await db.query("DELETE FROM node WHERE id = 'root'"); const rows = rowsOf>( await db.query('SELECT id, parent_id FROM node ORDER BY id'), ); // a 的直接父是 root → 置空;b 的父是 a(还在)→ 不变 expect(rows).toEqual([{ id: 'a', parent_id: null }, { id: 'b', parent_id: 'a' }]); }); it('ON DELETE RESTRICT:有子节点时拒绝删除', async () => { await db.query( 'CREATE TABLE node (id STRING PRIMARY KEY, parent_id STRING REFERENCES node(id) ON DELETE RESTRICT)', ); await db.query("INSERT INTO node VALUES ('root', NULL), ('a', 'root')"); await expectCode(db.query("DELETE FROM node WHERE id = 'root'"), 'FOREIGN_KEY_VIOLATION'); // 拒绝后数据不得有任何变化 expect(rowsOf(await db.query('SELECT id FROM node'))).toHaveLength(2); // 叶子节点可以删除 await db.query("DELETE FROM node WHERE id = 'a'"); expect(rowsOf<{ id: string }>(await db.query('SELECT id FROM node')).map((r) => r.id)).toEqual(['root']); }); it('ON UPDATE CASCADE 对自引用同样生效', async () => { await db.query( 'CREATE TABLE node (id STRING PRIMARY KEY, parent_id STRING REFERENCES node(id) ON UPDATE CASCADE)', ); await db.query("INSERT INTO node VALUES ('root', NULL), ('a', 'root'), ('b', 'a')"); await db.query("UPDATE node SET id = 'root2' WHERE id = 'root'"); const rows = rowsOf>( await db.query('SELECT id, parent_id FROM node ORDER BY id'), ); // a 的 parent_id 应随 root → root2 更新;b 不受影响 expect(rows).toEqual([ { id: 'a', parent_id: 'root2' }, { id: 'b', parent_id: 'a' }, { id: 'root2', parent_id: null }, ]); }); }); }); describe('[v0.8.0] A14 外键传递链(跨表多层)', () => { describe.each(ENGINES)('%s 引擎', (label, mode, extra) => { let db: MetonaSqlark; let seq = 0; beforeEach(async () => { seq += 1; db = await MetonaSqlark.create({ name: `a14-${label}-${seq}-${Math.random().toString(36).slice(2)}`, mode, ...extra, }); }); afterEach(async () => { await db.close(); }); it('ON DELETE CASCADE 沿 A→B→C 链传递', async () => { await db.query('CREATE TABLE a (id STRING PRIMARY KEY)'); await db.query('CREATE TABLE b (id STRING PRIMARY KEY, a_id STRING REFERENCES a(id) ON DELETE CASCADE)'); await db.query('CREATE TABLE c (id STRING PRIMARY KEY, b_id STRING REFERENCES b(id) ON DELETE CASCADE)'); await db.query("INSERT INTO a VALUES ('A1')"); await db.query("INSERT INTO b VALUES ('B1', 'A1')"); await db.query("INSERT INTO c VALUES ('C1', 'B1')"); const affected = await db.query("DELETE FROM a WHERE id = 'A1'"); // a 1 行 + b 1 行 + c 1 行 expect(affected).toBe(3); expect(rowsOf(await db.query('SELECT id FROM b'))).toEqual([]); expect(rowsOf(await db.query('SELECT id FROM c'))).toEqual([]); }); it('ON UPDATE CASCADE 沿 A→B 链传递,C 引用未变故不需更新', async () => { // 审计曾记录"A→B→C 链改 A 主键后 C 悬空",实测不成立: // C 引用的是 **b.id**(未被更新),因此 C 不需要任何变更。 // 本用例固化这一结论,避免将来按错误结论去"修"一个不存在的问题。 await db.query('CREATE TABLE a (id STRING PRIMARY KEY)'); await db.query('CREATE TABLE b (id STRING PRIMARY KEY, a_id STRING REFERENCES a(id) ON UPDATE CASCADE)'); await db.query('CREATE TABLE c (id STRING PRIMARY KEY, b_id STRING REFERENCES b(id) ON UPDATE CASCADE)'); await db.query("INSERT INTO a VALUES ('A1')"); await db.query("INSERT INTO b VALUES ('B1', 'A1')"); await db.query("INSERT INTO c VALUES ('C1', 'B1')"); await db.query("UPDATE a SET id = 'A2' WHERE id = 'A1'"); expect(rowsOf>(await db.query('SELECT id, a_id FROM b'))).toEqual([ { id: 'B1', a_id: 'A2' }, ]); // C 仍指向 B1(未变)—— 没有悬空:B1 依然存在 expect(rowsOf>(await db.query('SELECT id, b_id FROM c'))).toEqual([ { id: 'C1', b_id: 'B1' }, ]); // 真正会悬空的是"把 b.id 也改掉"的场景:此时 C 必须跟着更新 await db.query("UPDATE b SET id = 'B2' WHERE id = 'B1'"); expect(rowsOf>(await db.query('SELECT id, b_id FROM c'))).toEqual([ { id: 'C1', b_id: 'B2' }, ]); }); it('链上 RESTRICT 阻断整条级联(整体拒绝,无部分删除)', async () => { await db.query('CREATE TABLE a (id STRING PRIMARY KEY)'); await db.query('CREATE TABLE b (id STRING PRIMARY KEY, a_id STRING REFERENCES a(id) ON DELETE CASCADE)'); await db.query('CREATE TABLE c (id STRING PRIMARY KEY, b_id STRING REFERENCES b(id) ON DELETE RESTRICT)'); await db.query("INSERT INTO a VALUES ('A1')"); await db.query("INSERT INTO b VALUES ('B1', 'A1')"); await db.query("INSERT INTO c VALUES ('C1', 'B1')"); await expectCode(db.query("DELETE FROM a WHERE id = 'A1'"), 'FOREIGN_KEY_VIOLATION'); // 两阶段预检:拒绝后三张表都必须完好(不能出现"b 被删了、a 还在") expect(rowsOf(await db.query('SELECT id FROM a'))).toHaveLength(1); expect(rowsOf(await db.query('SELECT id FROM b'))).toHaveLength(1); expect(rowsOf(await db.query('SELECT id FROM c'))).toHaveLength(1); }); }); });