/** * v0.4.2 生产就绪审计(第三轮) * 覆盖: * - P0: Aria 事务进行中 checkpoint 截断 WAL → 崩溃恢复丢事务数据 * - P1: Aria dropTable / ALTER DROP 索引列 的二级索引清理 * - P1: OPFS 并发写乱序丢更新 + 空表/schema/索引持久化 * - P2: Aria memtable 阈值衰减、红黑树随机压力、onUpdate 级联、事务 DDL 显式拒绝 */ import { AriaEngine } from '../src/engine/aria/index'; import { createSchema } from '../src/table/schema'; import { OPFSEngine } from '../src/engine/opfs'; import { MemTable } from '../src/engine/aria/index/memtable'; import { MetonaSqlark } from '../src/core'; import 'fake-indexeddb/auto'; let idbCounter = 0; function uniqueDB(): string { return `prod-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`; } // =================================================================== // OPFS mock(模拟真实 I/O:getFileHandle 延迟 + 写入按内容差异化耗时) // =================================================================== function mockOPFS( files?: Map, opts: { ioDelay?: number; writeDelayFor?: (data: string) => number } = {}, ): Map { const store = files ?? new Map(); const dirMock = { getDirectoryHandle: async (_name: string, _opts?: any) => dirMock as any, getFileHandle: async (name: string, fileOpts?: any) => { if (fileOpts?.create) { if (opts.ioDelay) { await new Promise((r) => setTimeout(r, opts.ioDelay)); } return { createWritable: async () => ({ write: async (d: string) => { const delay = opts.writeDelayFor ? opts.writeDelayFor(d) : 0; if (delay > 0) { await new Promise((r) => setTimeout(r, delay)); } store.set(name, d); }, close: async () => {}, }), }; } if (!store.has(name)) throw new Error('Not found'); return { getFile: async () => ({ text: async () => store.get(name)!, arrayBuffer: async () => new ArrayBuffer(0) }) }; }, removeEntry: async (name: string) => { store.delete(name); }, }; (dirMock as any).entries = () => ({ [Symbol.asyncIterator]: async function* () { for (const [k] of store) yield [k]; }, }); const nav = (globalThis as any).navigator || {}; nav.storage = { getDirectory: async () => dirMock }; (globalThis as any).navigator = nav; return store; } // =================================================================== // P0: 事务与 checkpoint 冲突 // =================================================================== describe('P0 — 事务进行中 checkpoint 不得截断 WAL', () => { it('事务中触发 checkpoint → 崩溃恢复不丢事务数据', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 2, // 每 2 次操作即触发 checkpoint(事务中途) }); await engine.open(dbName, 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' }, })); await engine.beginTransaction(); await engine.insert('t', [{ id: '1', v: 1 }]); await engine.insert('t', [{ id: '2', v: 2 }]); // opCounter=2 → tick → checkpoint(修复前截断 WAL) await engine.commitTransaction(); // 模拟异常退出(不 close) await (engine as any).backend.close(); (engine as any).opened = false; const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 }); await engine2.open(dbName, 1); // 修复前:checkpoint 截断了事务的 WAL 记录 → 恢复后数据丢失 expect(await engine2.count('t')).toBe(2); await engine2.close(); }); it('事务回滚后再 checkpoint 正常截断', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 1, }); await engine.open(dbName, 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })); await engine.insert('t', [{ id: 'keep' }]); await engine.beginTransaction(); await engine.insert('t', [{ id: 'tx1' }]); await engine.rollbackTransaction(); // 事务结束后 checkpoint 可正常截断 await (engine as any).checkpointManager.forceCheckpoint(); await (engine as any).backend.close(); (engine as any).opened = false; const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 }); await engine2.open(dbName, 1); expect(await engine2.count('t')).toBe(1); await engine2.close(); }); }); // =================================================================== // P1: Aria DDL 的二级索引清理 // =================================================================== describe('P1 — Aria DDL 索引清理', () => { it('dropTable 清理二级索引(重建同名表索引不脏)', async () => { const engine = new AriaEngine({ storageBackend: 'memory' }); await engine.open(uniqueDB(), 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await engine.insert('t', [{ id: '1', email: 'a@x.com' }]); await engine.dropTable('t'); // 索引 LSM 必须清理(修复前残留) expect((engine as any).secondaryIndexes.size).toBe(0); // 重建同名表 + 相同 id:旧索引残留会返回错误行 await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await engine.insert('t', [{ id: '1', email: 'b@x.com' }]); // 旧索引残留场景:按旧 email 查询不得命中新行(主 LSM 有 t:1 → 修复前返回错误结果) const byOld = await engine.find('t', { table: 't', where: { email: 'a@x.com' } }); expect(byOld).toHaveLength(0); const byNew = await engine.find('t', { table: 't', where: { email: 'b@x.com' } }); expect(byNew).toHaveLength(1); await engine.close(); }); it('ALTER TABLE DROP 索引列清理索引 LSM', async () => { const engine = new AriaEngine({ storageBackend: 'memory' }); await engine.open(uniqueDB(), 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await engine.insert('t', [{ id: '1', email: 'a@x.com' }]); await engine.alterTable('t', 'DROP', { name: 'email', type: 'string' }); expect((engine as any).secondaryIndexes.size).toBe(0); await engine.close(); }); it('DROP_TABLE 崩溃恢复同样清理索引', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 }); await engine.open(dbName, 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await engine.insert('t', [{ id: '1', email: 'a@x.com' }]); await engine.dropTable('t'); // 模拟崩溃(不 close,WAL 含 DROP_TABLE) await (engine as any).backend.close(); (engine as any).opened = false; const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 }); await engine2.open(dbName, 1); // 表不存在,且无索引残留 expect(await engine2.hasTable('t')).toBe(false); expect((engine2 as any).secondaryIndexes.size).toBe(0); await engine2.close(); }); }); // =================================================================== // P1: OPFS 持久化与并发 // =================================================================== describe('P1 — OPFS 生产加固', () => { it('并发写不丢数据(内存快照总是最新,多写内容一致)', async () => { // 模拟真实 I/O 延迟:内存写同步、持久化异步 → 并发写内容均基于最新内存快照 const files = mockOPFS(undefined, { ioDelay: 10, writeDelayFor: (data) => (data.length < 40 ? 30 : 5), }); const engine = new OPFSEngine(); await engine.open('opfs-race', 1); await engine.createTable(createSchema('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, })); // 两个写并发:内存同步写保证两个快照一致 → 无论完成顺序文件内容完整 const p1 = engine.insert('users', [{ id: '1', name: 'A' }]); const p2 = engine.insert('users', [{ id: '2', name: 'B' }]); await Promise.all([p1, p2]); const rows = await engine.find('users', { table: 'users' }); expect(rows).toHaveLength(2); // 文件内容完整(重启不丢) expect(files.get('users.json')).toContain('"B"'); await engine.close(); }); it('空表重启后保留(schema 持久化)', async () => { mockOPFS(); const e1 = new OPFSEngine(); await e1.open('opfs-schema', 1); await e1.createTable(createSchema('users', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await e1.close(); // 重启:空表也应恢复(修复前空表消失)+ 索引标记恢复 const e2 = new OPFSEngine(); await e2.open('opfs-schema', 1); expect(await e2.hasTable('users')).toBe(true); const schema = await e2.getTableSchema('users'); expect(schema!.columns.email.index).toBe(true); await e2.close(); }); it('有数据重启后索引查询可用', async () => { mockOPFS(); const e1 = new OPFSEngine(); await e1.open('opfs-idx', 1); await e1.createTable(createSchema('users', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await e1.insert('users', [{ id: '1', email: 'a@x.com' }]); await e1.close(); const e2 = new OPFSEngine(); await e2.open('opfs-idx', 1); const byEmail = await e2.find('users', { table: 'users', where: { email: 'a@x.com' } }); expect(byEmail).toHaveLength(1); await e2.close(); }); it('dropTable 后重启无幽灵表', async () => { mockOPFS(); const e1 = new OPFSEngine(); await e1.open('opfs-drop', 1); await e1.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })); await e1.insert('t', [{ id: '1' }]); await e1.dropTable('t'); await e1.close(); const e2 = new OPFSEngine(); await e2.open('opfs-drop', 1); expect(await e2.hasTable('t')).toBe(false); expect(await e2.getTableNames()).toEqual([]); await e2.close(); }); }); // =================================================================== // P2: Aria 内部状态 // =================================================================== describe('P2 — Aria 内部状态加固', () => { it('freezeMemtable 后阈值不衰减', async () => { const engine = new AriaEngine({ storageBackend: 'memory', memtableSizeThreshold: 1024 * 1024, }); await engine.open(uniqueDB(), 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })); // 少量写入 + 手动 flush(freezeMemtable 用旧表已用大小当新阈值 → 衰减) for (let i = 0; i < 10; i++) await engine.insert('t', [{ id: `${i}` }]); await (engine as any).lsm.flush(); // 修复前:新 memtable maxSize ≈ 已用字节(远小于配置) const memtable = (engine as any).lsm.memtable; expect((memtable as any).maxSize).toBeGreaterThanOrEqual(1024 * 1024); await engine.close(); }); it('红黑树随机 insert/delete 5000 次保持有序且无丢失', () => { const mem = new MemTable(1 << 30); const reference = new Set(); let seed = 42; const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; }; for (let i = 0; i < 5000; i++) { const k = `k${Math.floor(rnd() * 800)}`; if (rnd() < 0.3) { reference.delete(k); mem.delete(k); } else { reference.add(k); mem.put(k, { v: i }); } } const entries = mem.getAllEntries(); // 有序 for (let i = 1; i < entries.length; i++) { expect(entries[i][0] > entries[i - 1][0]).toBe(true); } // 与引用集合完全一致(无丢失/无残留) expect(entries.map(([k]) => k)).toEqual([...reference].sort()); expect(mem.getEntryCount()).toBe(reference.size); }); it('onUpdate CASCADE:更新父表主键级联更新子表外键', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true } }); await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', references: 'users.id', onUpdate: 'CASCADE' }, }); await db.table('users').insert({ id: '1' }); await db.table('orders').insert({ id: 'o1', user_id: '1' }); // 更新父表主键 1 → 2 await db.table('users').update({ id: '2' }).where({ id: '1' }).execute(); const orders = await db.table('orders').select().execute(); expect(orders[0].user_id).toBe('2'); // 旧 id 不可再被引用 expect(await db.table('orders').count({ user_id: '1' })).toBe(0); await db.close(); }); it('onUpdate RESTRICT:存在引用行时禁止更新主键', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true } }); await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', references: 'users.id', onUpdate: 'RESTRICT' }, }); await db.table('users').insert({ id: '1' }); await db.table('orders').insert({ id: 'o1', user_id: '1' }); await expect( db.table('users').update({ id: '2' }).where({ id: '1' }).execute(), ).rejects.toThrow(); // 主键未被修改 const users = await db.table('users').select().execute(); expect(users[0].id).toBe('1'); await db.close(); }); it('onUpdate CASCADE:更新父表主键级联更新子表外键(Aria)', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true } }); await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', references: 'users.id', onUpdate: 'CASCADE' }, }); await db.table('users').insert({ id: '1' }); await db.table('orders').insert({ id: 'o1', user_id: '1' }); await db.table('users').update({ id: '2' }).where({ id: '1' }).execute(); const orders = await db.table('orders').select().execute(); expect(orders[0].user_id).toBe('2'); expect(await db.table('orders').count({ user_id: '1' })).toBe(0); await db.close(); }); it('Aria 事务中 DDL 显式拒绝(NOT_SUPPORTED)而非静默不一致', async () => { const engine = new AriaEngine({ storageBackend: 'memory' }); await engine.open(uniqueDB(), 1); await engine.beginTransaction(); await expect( engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })), ).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await engine.rollbackTransaction(); // 表未创建 expect(await engine.hasTable('t')).toBe(false); await engine.close(); }); });