/** * v0.4.2 深度审计回归测试(第二轮) * 覆盖: * - P0-A: compaction 不依赖缓存(缓存未命中不得丢数据/数据不可见) * - P0-B: flush/compaction 失败不得卡死 flushChain(写路径死锁) * - P1-A: 重开后二级索引恢复 + createIndex 幂等重建 + WAL 恢复后索引一致 * - P1-B: ALTER TABLE 在 IndexedDB/Hybrid 引擎持久化 * - P1-C: IndexedDB 事务内 DDL(建表/删表)commit 后磁盘一致 */ import { AriaEngine } from '../src/engine/aria/index'; import { createSchema } from '../src/table/schema'; import { MemoryEngine } from '../src/engine/memory'; import { HybridEngine } from '../src/hybrid/index'; import { MetonaSqlark } from '../src/core'; import { resetOPFSMock } from './helpers/storage-harness'; beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { return `audit-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`; } // =================================================================== // P0-A: compaction 不得依赖 SSTable 缓存 // =================================================================== describe('P0-A — Compaction 缓存独立性', () => { it('缓存上限小于单个 SSTable 时 compaction 不丢数据(此前数据不可见)', async () => { const engine = new AriaEngine({ storageBackend: 'memory', // 缓存上限 4 页 = 16KB,memtable 32KB → 每次 flush 的文件都超出缓存上限被驱逐 bufferPoolPages: 4, memtableSizeThreshold: 32 * 1024, checkpointInterval: 100000, }); await engine.open(uniqueDB(), 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' }, data: { type: 'string' }, })); for (let i = 0; i < 400; i++) { await engine.insert('t', [{ id: `k${String(i).padStart(4, '0')}`, v: i, data: 'x'.repeat(200) }]); } // 等待 flush + compaction 链全部完成 await (engine as any).lsm.flush(); await new Promise((r) => setTimeout(r, 100)); // 修复前:compaction 缓存未命中跳过全部文件并从 levels 移除 → 查询为空 expect(await engine.count('t')).toBe(400); await engine.close(); }); it('多层级 compaction 后数据仍完整且可重开', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', bufferPoolPages: 8, // 32KB 缓存 memtableSizeThreshold: 16 * 1024, checkpointInterval: 100000, }); await engine.open(dbName, 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' }, })); for (let i = 0; i < 300; i++) { await engine.insert('t', [{ id: `k${String(i).padStart(4, '0')}`, v: i }]); } await (engine as any).lsm.flush(); await new Promise((r) => setTimeout(r, 150)); const before = await engine.count('t'); expect(before).toBe(300); await engine.close(); // 重开:数据完整(meta/文件未被 compaction 破坏) const engine2 = new AriaEngine({ storageBackend: 'opfs', bufferPoolPages: 8, memtableSizeThreshold: 16 * 1024, checkpointInterval: 100000, }); await engine2.open(dbName, 1); expect(await engine2.count('t')).toBe(300); await engine2.close(); }); }); // =================================================================== // P1-A: 二级索引跨重启恢复 // =================================================================== describe('P1-A — 二级索引恢复', () => { it('Aria 重开后二级索引可用(索引 LSM 持久化恢复)', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 }); await engine.open(dbName, 1); await engine.createTable(createSchema('users', { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true }, })); await engine.insert('users', [ { id: '1', email: 'a@x.com' }, { id: '2', email: 'b@x.com' }, { id: '3', email: 'a@x.com' }, ]); await (engine as any).lsm.flush(); await engine.close(); // 重开:二级索引 LSM 应自动恢复(修复前为空 → 索引查询回退全表,createIndex 也静默跳过) const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 }); await engine2.open(dbName, 1); // 强断言:索引 LSM 真实存在且数据完整(防止"索引缺失静默回退全表扫描"的假通过) const idxLsm = (engine2 as any).secondaryIndexes.get('users:idx:email'); expect(idxLsm).toBeDefined(); expect(idxLsm.getStats().sstableCount).toBeGreaterThan(0); await idxLsm.prefetchRange('', '\uffff'); expect(idxLsm.rangeScan('', '\uffff')).toHaveLength(3); const byEmail = await engine2.find('users', { table: 'users', where: { email: 'a@x.com' } }); expect(byEmail).toHaveLength(2); // createIndex 对已持久化的索引列应幂等可重建(不得静默跳过导致索引永久缺失) await engine2.createIndex('users', 'email'); await engine2.close(); }); it('WAL 恢复后二级索引与主数据一致(崩溃前索引未更新)', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 }); await engine.open(dbName, 1); await engine.createTable(createSchema('users', { id: { type: 'string', primaryKey: true }, city: { type: 'string', index: true }, })); await engine.insert('users', [{ id: '1', city: 'Beijing' }]); await (engine as any).lsm.flush(); // 写入 WAL 但强制不 flush(模拟崩溃:新行只在 WAL,索引 LSM 未更新) await engine.insert('users', [{ id: '2', city: 'Shanghai' }]); // 模拟异常退出(不 close) await (engine as any).backend.close(); (engine as any).opened = false; const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 }); await engine2.open(dbName, 1); // 强断言:索引 LSM 已恢复且包含 WAL 回放的行(崩溃前索引未更新,恢复后必须重建) const idxLsm = (engine2 as any).secondaryIndexes.get('users:idx:city'); expect(idxLsm).toBeDefined(); await idxLsm.prefetchRange('', '\uffff'); expect(idxLsm.rangeScan('', '\uffff')).toHaveLength(2); // 索引查询应看到 WAL 恢复的行(修复前索引与主数据不一致 → 丢行) const byCity = await engine2.find('users', { table: 'users', where: { city: 'Shanghai' } }); expect(byCity).toHaveLength(1); expect(byCity[0].id).toBe('2'); await engine2.close(); }); it('createIndex 重开后仍可新建(schema 标记恢复后不阻塞)', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 }); await engine.open(dbName, 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, })); await engine.insert('t', [{ id: '1', name: 'A' }]); await engine.close(); const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 }); await engine2.open(dbName, 1); await engine2.createIndex('t', 'name'); // 修复前 schema 无标记时正常;此处验证无标记场景 const byName = await engine2.find('t', { table: 't', where: { name: 'A' } }); expect(byName).toHaveLength(1); await engine2.close(); }); }); // =================================================================== // P1-B: ALTER TABLE 跨引擎持久化 // =================================================================== describe('P1-B — ALTER TABLE 持久化', () => { it('KVStoreEngine DROP COLUMN 后重启不复活', async () => { const dbName = uniqueDB(); const db = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'opfs' }); await db.init(); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, old_col: { type: 'string' }, keep: { type: 'string' }, }); await db.table('t').insert({ id: '1', old_col: 'x', keep: 'y' }); await db.query('ALTER TABLE t DROP COLUMN old_col'); // 行数据中该列已移除 const rows = await db.table('t').select().execute(); expect(rows[0].old_col).toBeUndefined(); await db.close(); // 重启:schema 不复活,列定义已持久化 const db2 = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'opfs' }); await db2.init(); const schema2 = await db2.getEngine().getTableSchema('t'); expect(schema2!.columns.old_col).toBeUndefined(); expect(schema2!.columns.keep).toBeDefined(); await db2.close(); }); it('HybridEngine ADD COLUMN 后重启保留', async () => { const dbName = uniqueDB(); const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' }); await db.init(); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query('ALTER TABLE t ADD COLUMN phone STRING'); await db.close(); const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' }); await db2.init(); const schema2 = await db2.getEngine().getTableSchema('t'); expect(schema2!.columns.phone).toBeDefined(); await db2.close(); }); }); // =================================================================== // P1-C: KVStore 事务内 DDL // =================================================================== describe('P1-C — KVStore 事务内 DDL', () => { it('事务内建表 → commit 后磁盘一致(重启表存在且可查)', async () => { const dbName = uniqueDB(); const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' }); await db.init(); await db.defineTable('base', { id: { type: 'string', primaryKey: true } }); await db.table('base').insert({ id: '1' }); await db.transaction(async (trx) => { await trx.table('base').insert({ id: '2' }); // 事务内建新表(此前 commit 时 IDB 无对应 store → 事务失败) await db.defineTable('created_in_tx', { id: { type: 'string', primaryKey: true } }); await db.table('created_in_tx').insert({ id: 'tx1' }); }); expect(await db.table('created_in_tx').count()).toBe(1); await db.close(); const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' }); await db2.init(); expect(await db2.table('created_in_tx').count()).toBe(1); expect(await db2.table('base').count()).toBe(2); await db2.close(); }); it('事务内删表 → commit 后磁盘一致(重启无幽灵表)', async () => { const dbName = uniqueDB(); const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' }); await db.init(); await db.defineTable('ghost', { id: { type: 'string', primaryKey: true } }); await db.table('ghost').insert({ id: '1' }); await db.transaction(async () => { await db.dropTable('ghost'); }); await db.close(); const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' }); await db2.init(); const names = await db2.getTableNames(); expect(names).not.toContain('ghost'); await db2.close(); }); }); // =================================================================== // 各引擎 clearAll / repair / 元数据 一致性抽查 // =================================================================== describe('全模式抽查 — 生命周期与元数据', () => { it('MemoryEngine clearAll/repair/getMeta/setMeta', async () => { const e = new MemoryEngine(); await e.open('m', 1); await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } }); await e.insert('t', [{ id: '1' }]); await e.setMeta('k', 'v'); expect(await e.getMeta('k')).toBe('v'); await e.repair(); await e.clearAll(); expect(await e.getTableNames()).toEqual([]); await e.close(); }); it('HybridEngine getMeta/setMeta 委托磁盘', async () => { const dbName = uniqueDB(); const e = new HybridEngine('opfs'); await e.open(dbName, 1); await e.setMeta('__metona_version', '7'); expect(await e.getMeta('__metona_version')).toBe('7'); await e.close(); const e2 = new HybridEngine('opfs'); await e2.open(dbName, 1); expect(await e2.getMeta('__metona_version')).toBe('7'); await e2.close(); }); it('AriaEngine repair 幂等', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs' }); await engine.open(dbName, 1); await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })); await engine.insert('t', [{ id: '1' }]); await engine.repair(); await engine.repair(); expect(await engine.count('t')).toBe(1); await engine.close(); }); });