/** * v0.2.5 修复验证测试 * 验证所有 P0/P1/P2 修复点 */ import { VERSION } from '../src/constants'; import { MetonaSqlark } from '../src/core'; import { AriaEngine } from '../src/engine/aria/index'; import { MemoryEngine } from '../src/engine/memory'; import { parse } from '../src/sql/parser'; import { QueryExecutor } from '../src/query/executor'; import { WAL } from '../src/engine/aria/wal/log'; import { SSTableReader } from '../src/engine/aria/index/sstable'; import { BloomFilter } from '../src/engine/aria/index/bloom'; import { CryptoManager } from '../src/engine/aria/crypto'; import { PluginManager } from '../src/plugin/index'; import type { SSTableMeta } from '../src/engine/aria/types'; import type { MetonaPlugin } from '../src/constants'; import { createSchema } from '../src/table/schema'; import { installOPFSMock } from './helpers/opfs-mock'; beforeEach(() => { installOPFSMock(new Map()); }); // --------------------------------------------------------------------------- // P0-1: 版本号统一 // --------------------------------------------------------------------------- describe('[v0.2.5] P0-1: 版本号统一', () => { test('VERSION 常量为当前版本(0.6.0)', () => { expect(VERSION).toBe('0.7.3'); }); }); // --------------------------------------------------------------------------- // P0-2: AriaEngine OPFS 后端映射修复 // --------------------------------------------------------------------------- describe('[v0.2.5] P0-2: AriaEngine OPFS 后端映射', () => { test('mode=aria + diskEngine=opfs 时应使用 opfs 后端', () => { const db = new MetonaSqlark({ name: `test-opfs-map-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'aria', diskEngine: 'opfs' }); // 不实际 open(需要浏览器环境),只验证 createEngine 逻辑 // 通过 getEngine 在 init 后检查 expect(db).toBeDefined(); }); test('mode=aria + diskEngine=indexeddb 时应使用 indexeddb 后端', () => { const db = new MetonaSqlark({ name: `test-idb-map-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'aria', diskEngine: 'opfs' }); expect(db).toBeDefined(); }); }); // --------------------------------------------------------------------------- // P0-3: _onError 接入执行路径 // --------------------------------------------------------------------------- describe('[v0.2.5] P0-3: _onError 接入执行路径', () => { test('query 失败时调用 onError 回调', async () => { const errors: Error[] = []; const db = new MetonaSqlark({ name: 'test-onerror', mode: 'memory', onError: (e) => errors.push(e), }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, }); // 故意执行不存在的表查询 await expect(db.query('SELECT * FROM nonexistent')).rejects.toThrow(); expect(errors.length).toBeGreaterThan(0); await db.close(); }); test('defineTable 失败时调用 onError', async () => { const errors: Error[] = []; const db = new MetonaSqlark({ name: 'test-onerror2', mode: 'memory', onError: (e) => errors.push(e), }); await db.init(); await db.defineTable('dup', { id: { type: 'string', primaryKey: true }, }); // 重复创建 await expect(db.defineTable('dup', { id: { type: 'string', primaryKey: true }, })).rejects.toThrow(); expect(errors.length).toBeGreaterThan(0); await db.close(); }); }); // --------------------------------------------------------------------------- // P0-4: maxRowsPerQuery 生效 // --------------------------------------------------------------------------- describe('[v0.2.5] P0-4: maxRowsPerQuery 生效', () => { test('结果集被截断为 maxRowsPerQuery', async () => { const engine = new MemoryEngine(); await engine.open('test-maxrows', 1); await engine.createTable(createSchema('items', { id: { type: 'string', primaryKey: true }, val: { type: 'number' }, })); // 插入 100 行 for (let i = 0; i < 100; i++) { await engine.insert('items', [{ id: `item${i}`, val: i }]); } const executor = new QueryExecutor(engine, 10); // maxRowsPerQuery=10 const stmt = parse('SELECT * FROM items'); const result = await executor.execute(stmt) as Record[]; expect(result.length).toBe(10); // 截断为 10 行 await engine.close(); }); test('maxRowsPerQuery=0 表示不限制', async () => { const engine = new MemoryEngine(); await engine.open('test-nolimit', 1); await engine.createTable(createSchema('items', { id: { type: 'string', primaryKey: true }, })); for (let i = 0; i < 50; i++) { await engine.insert('items', [{ id: `i${i}` }]); } const executor = new QueryExecutor(engine, 0); const stmt = parse('SELECT * FROM items'); const result = await executor.execute(stmt) as Record[]; expect(result.length).toBe(50); await engine.close(); }); }); // --------------------------------------------------------------------------- // P0-5: WAL full 模式真正同步 // --------------------------------------------------------------------------- describe('[v0.2.5] P0-5: WAL full 模式同步', () => { test('append 在 full 模式下是 async 且可 await', async () => { let appendCount = 0; const wal = new WAL({ append: async (_data: Uint8Array) => { appendCount++; }, readAll: async () => new Uint8Array(0), truncate: async () => {}, exists: async () => false, }, true, 'full'); // append 现在返回 Promise await wal.append({ type: 1, // INSERT txnId: 0, tableName: 'test', key: 'k1', data: { v: 1 }, } as any); expect(appendCount).toBe(1); }); }); // --------------------------------------------------------------------------- // P0-6: PluginManager.install 传 db 实例 // --------------------------------------------------------------------------- describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => { test('register 传 db 给 install', () => { let receivedDb: unknown = null; const plugin: MetonaPlugin = { name: 'test-plugin', install: (db) => { receivedDb = db; }, destroy: () => {}, }; const pm = new PluginManager(); const fakeDb = { name: 'fake' }; pm.register(plugin, fakeDb); expect(receivedDb).toBe(fakeDb); }); }); // --------------------------------------------------------------------------- // P1-7: SSTableReader 二分查找统一 // --------------------------------------------------------------------------- describe('[v0.2.5] P1-7: SSTableReader 二分查找', () => { test('rangeScan 使用二分查找正确定位', () => { // 构建一个 SSTable 手工 const { SSTableBuilder } = require('../src/engine/aria/index/sstable_builder'); const builder = new SSTableBuilder(4096); // 添加足够多的条目以形成多个 block for (let i = 0; i < 100; i++) { const key = `key${String(i).padStart(5, '0')}`; builder.add(key, { data: `value${i}` }); } const { sstableData } = builder.build(); const meta: SSTableMeta = { id: 1, level: 0, minKey: 'key00000', maxKey: 'key00099', blockCount: 1, totalSize: sstableData.byteLength, bloomData: null, }; const reader = new SSTableReader(sstableData, meta); // 精确查找 const result = reader.get('key00050'); expect(result).not.toBeNull(); expect((result as any).data).toBe('value50'); // 范围扫描 const collected: string[] = []; reader.rangeScan('key00010', 'key00020', (k) => collected.push(k)); expect(collected.length).toBeGreaterThan(0); expect(collected[0]).toBe('key00010'); }); }); // --------------------------------------------------------------------------- // P1-8: SQL 注入防护 — 表名校验 // 真实覆盖位置:tests/integrations/react.test.ts(useTable) // tests/integrations/vue.test.ts(useSqlarkTable) // 通过真实 hooks 调用触发 validateTableName,验证非法表名抛错。 // 注:v0.2.6 移除此处"复制正则自测"的伪测试(未触达真实代码)。 // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // P1-9: crypto 实例化 // --------------------------------------------------------------------------- describe('[v0.2.5] P1-9: CryptoManager 实例化', () => { test('CryptoManager 可以独立实例化', () => { const cm1 = new CryptoManager(); const cm2 = new CryptoManager(); expect(cm1.enabled).toBe(false); expect(cm2.enabled).toBe(false); // 两个实例互不影响 expect(cm1).not.toBe(cm2); }); }); // --------------------------------------------------------------------------- // P2-14: ALTER TABLE 语法 // --------------------------------------------------------------------------- describe('[v0.2.5] P2-14: ALTER TABLE', () => { test('解析 ALTER TABLE ADD COLUMN', () => { const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE'); expect(stmt.type).toBe('ALTER_TABLE'); expect((stmt as any).name).toBe('users'); expect((stmt as any).action).toBe('ADD'); expect((stmt as any).column.name).toBe('email'); }); test('解析 ALTER TABLE DROP COLUMN', () => { const stmt = parse('ALTER TABLE users DROP COLUMN email'); expect(stmt.type).toBe('ALTER_TABLE'); expect((stmt as any).action).toBe('DROP'); expect((stmt as any).column.name).toBe('email'); }); test('执行 ALTER TABLE ADD COLUMN', async () => { const engine = new MemoryEngine(); await engine.open('test-alter', 1); await engine.createTable(createSchema('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, })); const executor = new QueryExecutor(engine); const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255)'); await executor.execute(stmt); const schema = await engine.getTableSchema('users'); expect(schema!.columns.email).toBeDefined(); await engine.close(); }); test('执行 ALTER TABLE DROP COLUMN', async () => { const engine = new MemoryEngine(); await engine.open('test-alter-drop', 1); await engine.createTable(createSchema('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, email: { type: 'string' }, })); const executor = new QueryExecutor(engine); const stmt = parse('ALTER TABLE users DROP COLUMN email'); await executor.execute(stmt); const schema = await engine.getTableSchema('users'); expect(schema!.columns.email).toBeUndefined(); await engine.close(); }); }); // --------------------------------------------------------------------------- // P2-15: TRUNCATE TABLE 语法 // --------------------------------------------------------------------------- describe('[v0.2.5] P2-15: TRUNCATE TABLE', () => { test('解析 TRUNCATE TABLE', () => { const stmt = parse('TRUNCATE TABLE users'); expect(stmt.type).toBe('TRUNCATE_TABLE'); expect((stmt as any).name).toBe('users'); }); test('执行 TRUNCATE TABLE 清空数据', async () => { const engine = new MemoryEngine(); await engine.open('test-truncate', 1); await engine.createTable(createSchema('items', { id: { type: 'string', primaryKey: true }, })); await engine.insert('items', [ { id: 'a' }, { id: 'b' }, { id: 'c' }, ]); const executor = new QueryExecutor(engine); const stmt = parse('TRUNCATE TABLE items'); await executor.execute(stmt); const rows = await engine.find('items', { table: 'items' }); expect(rows.length).toBe(0); await engine.close(); }); }); // --------------------------------------------------------------------------- // P2-12: WAL 大小阈值接入 checkpoint // --------------------------------------------------------------------------- describe('[v0.2.5] P2-12: WAL 大小阈值', () => { test('CheckpointManager 接收 walSizeThreshold 参数', () => { const { CheckpointManager } = require('../src/engine/aria/wal/checkpoint'); const fakeLsm = { flush: async () => {} }; const fakeWal = { flush: async () => {}, checkpoint: async () => {}, getBufferedCount: () => 0 }; const cm = new CheckpointManager(fakeLsm, fakeWal, null, 1000, 1024); expect(cm).toBeDefined(); }); }); // --------------------------------------------------------------------------- // P2-13: compactLevelSync 接口公开化 // --------------------------------------------------------------------------- describe('[v0.2.5] P2-13: compactLevel public', () => { test('LSM.compactLevel 是 public 方法', () => { const { LSM } = require('../src/engine/aria/index/lsm'); const lsm = new LSM({ sstableStore: { save: async () => {}, load: async () => null, delete: async () => {}, allocateId: async () => 1, listMeta: async () => [], saveMeta: async () => {}, deleteMeta: async () => {}, }, }); expect(typeof lsm.compactLevel).toBe('function'); }); });