/** * v0.6.1-fix(P0) — 二级索引大数据量完整性回归测试 * * 覆盖两类根因: * 1. LSM.put 的 `!this.flushing` 条件竞态(修复:每次 freeze 一律挂链) * 2. SSTable rangeScan 尾块漏读(索引键为块尾 key,locateBlockLE 排除 * 尾 key 越界的下一块 → 范围扫描丢尾部数据;修复:endBlockIdx 多扫一块) */ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; import { installOPFSMock } from '../helpers/opfs-mock'; let counter = 0; function uniqueDB(): string { return `idxrace-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`; } beforeEach(() => { installOPFSMock(new Map()); }); describe('AriaEngine — 二级索引完整性(P0 回归)', () => { it('5 万行写入:索引查询与主表一致(修复前丢 106~771 条)', async () => { const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 256 * 1024, checkpointInterval: 2000, walSyncMode: 'full', }); await engine.open(uniqueDB(), 1); await engine.createTable(createSchema('big', { id: { type: 'string', primaryKey: true }, val: { type: 'number' }, tag: { type: 'string', index: true }, name: { type: 'string' }, })); const TOTAL = 50000; for (let batch = 0; batch < TOTAL / 1000; batch++) { const rows = [] as Record[]; for (let i = 0; i < 1000; i++) { const idx = batch * 1000 + i; rows.push({ id: `k${idx}`, val: idx, tag: `t${idx % 10}`, name: `User${idx}` }); } await engine.insert('big', rows); } expect(await engine.count('big')).toBe(TOTAL); // 每个 tag 的索引查询都完整(修复前丢 106-771 条) for (let t = 0; t < 10; t++) { const viaIdx = await engine.find('big', { table: 'big', where: { tag: `t${t}` } }); expect(viaIdx.length).toBe(5000); } // 崩溃恢复后索引仍完整 await (engine as any).backend.close(); (engine as any).opened = false; const engine2 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 256 * 1024, checkpointInterval: 2000, walSyncMode: 'full', }); await engine2.open((engine as any).dbName, 1); for (let t = 0; t < 10; t++) { const viaIdx = await engine2.find('big', { table: 'big', where: { tag: `t${t}` } }); expect(viaIdx.length).toBe(5000); } await engine2.close(); }, 180000); it('小批量高频写入(每批 50 行)触发极端 freeze 竞态', async () => { const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 32 * 1024, checkpointInterval: 5000, walSyncMode: 'full', }); await engine.open(uniqueDB(), 1); await engine.createTable(createSchema('items', { id: { type: 'string', primaryKey: true }, tag: { type: 'string', index: true }, })); const TOTAL = 4000; for (let i = 0; i < TOTAL; i++) { await engine.insert('items', [{ id: `k${i}`, tag: i % 3 === 0 ? 'a' : 'b' }]); } expect(await engine.count('items')).toBe(TOTAL); expect(await engine.find('items', { table: 'items', where: { tag: 'a' } })).toHaveLength(Math.ceil(TOTAL / 3)); expect(await engine.find('items', { table: 'items', where: { tag: 'b' } })).toHaveLength(TOTAL - Math.ceil(TOTAL / 3)); await engine.close(); }, 120000); it('多索引列同时写入:每列索引都完整', async () => { const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024, checkpointInterval: 5000, walSyncMode: 'full', }); await engine.open(uniqueDB(), 1); await engine.createTable(createSchema('multi', { id: { type: 'string', primaryKey: true }, cat: { type: 'string', index: true }, grp: { type: 'number', index: true }, })); const TOTAL = 6000; for (let batch = 0; batch < TOTAL / 100; batch++) { const rows = [] as Record[]; for (let i = 0; i < 100; i++) { const idx = batch * 100 + i; rows.push({ id: `k${idx}`, cat: idx % 2 === 0 ? 'even' : 'odd', grp: idx % 5 }); } await engine.insert('multi', rows); } expect(await engine.find('multi', { table: 'multi', where: { cat: 'even' } })).toHaveLength(TOTAL / 2); expect(await engine.find('multi', { table: 'multi', where: { cat: 'odd' } })).toHaveLength(TOTAL / 2); expect(await engine.find('multi', { table: 'multi', where: { grp: 0 } })).toHaveLength(TOTAL / 5); expect(await engine.find('multi', { table: 'multi', where: { grp: 4 } })).toHaveLength(TOTAL / 5); await engine.close(); }, 120000); });