/** * v0.3.3 修复验证测试 * 验证所有 P0/P1 修复点: * - P0-1: Aria WAL DROP_TABLE 崩溃恢复(表/数据不复活)+ WAL 恢复后截断 * - P0-2: MemoryEngine update/delete 索引维护(unique 约束 + 索引查询 + 级联清理) * - P1-3: Aria 事务内读到自己写入的变更(insert 后 update/delete) * - P1-4: Aria clear() 写 WAL + 事务化 * - P1-5: WAL 字节数跟踪(full 模式 walSizeThreshold 生效) * - P1-6: Aria 主键列不建冗余二级索引(PK $in / 范围查询走主 LSM) * - P1-7: ORDER BY 支持 SELECT 别名 * - P1-8: SQL 字符串 '' 标准转义 * - P1-9: Savepoint 回滚后 MVCC 版本链一致 + rollback 索引重建 */ import { VERSION } from '../src/constants'; import { MetonaSqlark } from '../src/core'; import { AriaEngine } from '../src/engine/aria/index'; import { MemoryEngine } from '../src/engine/memory'; import { WAL } from '../src/engine/aria/wal/log'; import { tokenize } from '../src/sql/lexer'; import { installOPFSMock } from './helpers/opfs-mock'; beforeEach(() => { installOPFSMock(new Map()); }); // --------------------------------------------------------------------------- // P0-1: Aria WAL DROP_TABLE 崩溃恢复 // --------------------------------------------------------------------------- describe('[v0.3.3] P0-1: WAL DROP_TABLE 崩溃恢复', () => { const mkEngine = async (name: string): Promise => { const e = new AriaEngine({ storageBackend: 'opfs', walSyncMode: 'full' }); await e.open(name, 1); return e; }; test('删表后崩溃(不 close),重启后表与数据不复活', async () => { const dbName = `crash-drop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; let e = await mkEngine(dbName); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await e.insert('t', [{ id: '1' }, { id: '2' }]); await e.dropTable('t'); // 模拟崩溃:不 close,直接重建引擎(WAL 未 checkpoint) e = await mkEngine(dbName); const names = await e.getTableNames(); expect(names).not.toContain('t'); await e.close(); }); test('删表崩溃后重建同名表,旧数据不复活', async () => { const dbName = `crash-drop2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; let e = await mkEngine(dbName); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await e.insert('t', [{ id: '1' }, { id: '2' }]); await e.dropTable('t'); e = await mkEngine(dbName); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); const rows = await e.find('t', { table: 't' }); expect(rows).toHaveLength(0); await e.close(); }); test('WAL 恢复后截断:重启不再重复回放(checkpoint 后 WAL 空)', async () => { const dbName = `crash-wal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; let e = await mkEngine(dbName); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } }); await e.insert('t', [{ id: '1', v: 1 }, { id: '2', v: 2 }]); await e.dropTable('t'); // 第一次恢复:WAL 回放 + 截断 e = await mkEngine(dbName); expect(await e.getTableNames()).not.toContain('t'); // 第二次恢复:WAL 已空,不再有任何回放副作用 await e.close(); e = await mkEngine(dbName); expect(await e.getTableNames()).not.toContain('t'); await e.close(); }); }); // --------------------------------------------------------------------------- // P0-2: MemoryEngine update/delete 索引维护 // --------------------------------------------------------------------------- describe('[v0.3.3] P0-2: Memory 引擎索引维护', () => { test('update 修改唯一列后,重复值插入被拦截(unique 约束不被绕过)', async () => { const e = new MemoryEngine(); await e.open('x', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } }); await e.insert('t', [{ id: '1', email: 'a@x.com' }]); await e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' }); await expect(e.insert('t', [{ id: '2', email: 'b@x.com' }])).rejects.toThrow('Unique constraint'); }); test('update 后按新值索引查询命中', async () => { const e = new MemoryEngine(); await e.open('x', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true } } }); await e.insert('t', [{ id: '1', email: 'a@x.com' }]); await e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' }); const rows = await e.find('t', { table: 't', where: { email: 'b@x.com' } }); expect(rows).toHaveLength(1); expect(rows[0].id).toBe('1'); }); test('update 到已存在的唯一值时抛 UNIQUE_VIOLATION', async () => { const e = new MemoryEngine(); await e.open('x', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } }); await e.insert('t', [{ id: '1', email: 'a@x.com' }, { id: '2', email: 'b@x.com' }]); await expect(e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' })).rejects.toThrow('Unique constraint'); }); test('delete 后索引清理:同值可重新插入且索引查询正确', async () => { const e = new MemoryEngine(); await e.open('x', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } }); await e.insert('t', [{ id: '1', email: 'a@x.com' }]); await e.delete('t', { table: 't', where: { id: '1' } }); await e.insert('t', [{ id: '2', email: 'a@x.com' }]); const rows = await e.find('t', { table: 't', where: { email: 'a@x.com' } }); expect(rows).toHaveLength(1); expect(rows[0].id).toBe('2'); }); test('级联删除清理子表索引条目', async () => { const e = new MemoryEngine(); await e.open('x', 1); await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true } } }); await e.createTable({ name: 'orders', columns: { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', index: true, references: 'users.id', onDelete: 'CASCADE' } } }); await e.insert('users', [{ id: 'u1' }]); await e.insert('orders', [{ id: 'o1', user_id: 'u1' }]); await e.delete('users', { table: 'users', where: { id: 'u1' } }); // u1 已删,o1 级联删除 → 按 user_id 索引查询应为空 const rows = await e.find('orders', { table: 'orders', where: { user_id: 'u1' } }); expect(rows).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // P1-3: Aria 事务内读到自己写入的变更 // --------------------------------------------------------------------------- describe('[v0.3.3] P1-3: Aria 事务内读写一致', () => { test('事务内 insert 后 update 同一行生效', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('txn1', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } }); await e.beginTransaction(); await e.insert('t', [{ id: '1', name: 'Alice' }]); const n = await e.update('t', { table: 't', where: { id: '1' } }, { name: 'Bob' }); await e.commitTransaction(); const rows = await e.find('t', { table: 't' }); expect(n).toBe(1); expect(rows[0].name).toBe('Bob'); await e.close(); }); test('事务内 insert 后 delete 该行生效', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('txn2', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await e.beginTransaction(); await e.insert('t', [{ id: '1' }, { id: '2' }]); const d = await e.delete('t', { table: 't', where: { id: '2' } }); await e.commitTransaction(); const rows = await e.find('t', { table: 't' }); expect(d).toBe(1); expect(rows).toHaveLength(1); await e.close(); }); test('rollback 后索引无残留(事务内写入的索引被重建清理)', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('txn3', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } }); await e.beginTransaction(); await e.insert('t', [{ id: '1', name: 'Dave' }]); await e.rollbackTransaction(); const rows = await e.find('t', { table: 't', where: { name: 'Dave' } }); expect(rows).toHaveLength(0); await e.close(); }); test('rollback 更新后索引恢复旧值', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('txn4', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } }); await e.insert('t', [{ id: '1', name: 'Bob' }]); await e.beginTransaction(); await e.update('t', { table: 't', where: { id: '1' } }, { name: 'Eve' }); await e.rollbackTransaction(); const rows = await e.find('t', { table: 't', where: { name: 'Bob' } }); expect(rows).toHaveLength(1); const stale = await e.find('t', { table: 't', where: { name: 'Eve' } }); expect(stale).toHaveLength(0); await e.close(); }); test('事务内 clear 生效且可提交', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('txn5', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await e.insert('t', [{ id: '1' }, { id: '2' }]); await e.beginTransaction(); await e.clear('t'); await e.commitTransaction(); expect(await e.count('t')).toBe(0); await e.close(); }); }); // --------------------------------------------------------------------------- // P1-5: WAL 字节数跟踪 // --------------------------------------------------------------------------- describe('[v0.3.3] P1-5: WAL 字节数跟踪', () => { test('full 模式 append 后 getBufferedBytes 反映实际字节数', async () => { const chunks: Uint8Array[] = []; const wal = new WAL({ append: async (d) => { chunks.push(d); }, readAll: async () => { const t = chunks.reduce((s, c) => s + c.byteLength, 0); const out = new Uint8Array(t); let o = 0; for (const c of chunks) { out.set(c, o); o += c.byteLength; } return out; }, truncate: async () => { chunks.length = 0; }, exists: async () => chunks.length > 0, }, true, 'full'); await wal.append({ type: 1, txnId: 0, tableName: 't', key: '1', data: { a: 1 } } as never); const bytesAfterAppend = wal.getBufferedBytes(); expect(bytesAfterAppend).toBeGreaterThan(0); await wal.checkpoint(); expect(wal.getBufferedBytes()).toBe(0); }); test('batch 模式 flush 后字节数保留(未 checkpoint 前)', async () => { const chunks: Uint8Array[] = []; const wal = new WAL({ append: async (d) => { chunks.push(d); }, readAll: async () => new Uint8Array(0), truncate: async () => { chunks.length = 0; }, exists: async () => chunks.length > 0, }, true, 'batch'); await wal.append({ type: 1, txnId: 0, tableName: 't', key: '1', data: { a: 1 } } as never); await wal.flush(); expect(wal.getBufferedBytes()).toBeGreaterThan(0); // 已写盘但未 checkpoint await wal.checkpoint(); expect(wal.getBufferedBytes()).toBe(0); }); }); // --------------------------------------------------------------------------- // P1-6: Aria 主键列走主 LSM(无冗余二级索引) // --------------------------------------------------------------------------- describe('[v0.3.3] P1-6: Aria 主键查询优化', () => { test('PK $in 查询走主 LSM 返回正确行', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('pk1', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } }); await e.insert('t', [{ id: '1', v: 10 }, { id: '2', v: 20 }, { id: '3', v: 30 }]); const rows = await e.find('t', { table: 't', where: { id: { $in: ['1', '3'] } } }); expect(rows.map((r) => r.id).sort()).toEqual(['1', '3']); await e.close(); }); test('PK 范围查询(字符串字典序)', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('pk2', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } }); await e.insert('t', [{ id: 'a', v: 1 }, { id: 'b', v: 2 }, { id: 'c', v: 3 }]); const rows = await e.find('t', { table: 't', where: { id: { $gte: 'b' } } }); expect(rows.map((r) => r.id)).toEqual(['b', 'c']); const lt = await e.find('t', { table: 't', where: { id: { $lt: 'b' } } }); expect(lt.map((r) => r.id)).toEqual(['a']); await e.close(); }); test('主键列不再创建独立二级索引(dropIndex 仍保护主键)', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('pk3', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } }); // 主键索引保护仍在 await expect(e.dropIndex('t', 'id')).rejects.toThrow('Cannot drop primary key'); // 二级索引(name)仍可正常使用 await e.insert('t', [{ id: '1', name: 'x' }]); const rows = await e.find('t', { table: 't', where: { name: 'x' } }); expect(rows).toHaveLength(1); await e.close(); }); }); // --------------------------------------------------------------------------- // P1-7: ORDER BY 别名 // --------------------------------------------------------------------------- describe('[v0.3.3] P1-7: ORDER BY 别名', () => { test('SELECT 别名可被 ORDER BY 引用', async () => { const db = new MetonaSqlark({ name: `alias-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, age: { type: 'number' }, }); await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)"); await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)"); await db.query("INSERT INTO users VALUES ('3', 'Carol', 35)"); const rows = await db.query('SELECT name AS n FROM users ORDER BY n DESC') as Record[]; expect(rows.map((r) => r.n)).toEqual(['Carol', 'Bob', 'Alice']); const withLimit = await db.query('SELECT name AS n FROM users ORDER BY n ASC LIMIT 2') as Record[]; expect(withLimit.map((r) => r.n)).toEqual(['Alice', 'Bob']); await db.close(); }); test('GROUP BY 场景 ORDER BY 聚合别名', async () => { const db = new MetonaSqlark({ name: `alias2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' }); await db.init(); await db.defineTable('emp', { id: { type: 'string', primaryKey: true }, dept: { type: 'string' }, salary: { type: 'number' }, }); await db.query("INSERT INTO emp VALUES ('1', 'eng', 100)"); await db.query("INSERT INTO emp VALUES ('2', 'eng', 200)"); await db.query("INSERT INTO emp VALUES ('3', 'ops', 300)"); const rows = await db.query('SELECT dept, COUNT(*) AS cnt FROM emp GROUP BY dept ORDER BY cnt DESC') as Record[]; expect(rows[0]).toEqual({ dept: 'eng', cnt: 2 }); await db.close(); }); }); // --------------------------------------------------------------------------- // P1-8: SQL 字符串 '' 标准转义 // --------------------------------------------------------------------------- describe('[v0.3.3] P1-8: SQL 字符串转义', () => { test("'' 双引号转义为单个引号", async () => { const db = new MetonaSqlark({ name: `esc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' }); await db.init(); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, note: { type: 'string' }, }); await db.query("INSERT INTO t VALUES ('1', 'it''s a test')"); const rows = await db.query('SELECT * FROM t') as Record[]; expect(rows[0].note).toBe("it's a test"); await db.close(); }); test('反斜杠转义仍兼容', () => { const tokens = tokenize("SELECT 'a\\'b'"); expect(tokens[1].value).toBe("a'b"); }); }); // --------------------------------------------------------------------------- // P1-9: Savepoint 回滚后 MVCC 一致 // --------------------------------------------------------------------------- describe('[v0.3.3] P1-9: Savepoint + MVCC 一致性', () => { test('rollbackToSavepoint 后提交,数据为 savepoint 时状态', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('sp1', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } }); await e.insert('t', [{ id: '1', v: 1 }]); await e.beginTransaction(); await e.insert('t', [{ id: '2', v: 2 }]); await e.savepoint('sp'); await e.insert('t', [{ id: '3', v: 3 }]); await e.rollbackToSavepoint('sp'); await e.commitTransaction(); const rows = await e.find('t', { table: 't' }); expect(rows.map((r) => r.id).sort()).toEqual(['1', '2']); await e.close(); }); test('savepoint 回滚后事务仍可继续写入并提交', async () => { const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' }); await e.open('sp2', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await e.beginTransaction(); await e.savepoint('sp'); await e.insert('t', [{ id: '1' }]); await e.rollbackToSavepoint('sp'); await e.insert('t', [{ id: '2' }]); await e.commitTransaction(); const rows = await e.find('t', { table: 't' }); expect(rows.map((r) => r.id)).toEqual(['2']); await e.close(); }); }); // --------------------------------------------------------------------------- // 端到端冒烟 // --------------------------------------------------------------------------- describe('[v0.3.3] 端到端', () => { test('全部修复点可共存于 MetonaSqlark API', async () => { expect(VERSION).toBe('0.6.2'); const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true }, age: { type: 'number' }, }); await db.query("INSERT INTO users VALUES ('1', 'a@x.com', 20)"); await db.query("INSERT INTO users VALUES ('2', 'b@x.com', 30)"); // 别名排序 + 唯一约束组合 const rows = await db.query('SELECT email AS e FROM users ORDER BY e DESC') as Record[]; expect(rows.map((r) => r.e)).toEqual(['b@x.com', 'a@x.com']); await db.close(); }); });