/** * v0.5.1 — 维护语句 SQL 入口测试(EXPLAIN / ANALYZE / REINDEX / VACUUM / SAVEPOINT) * * 此前这些功能仅有引擎方法/executor 分支而无 SQL 入口(parser 无法解析), * 属"宣称存在但用户无法触发"的半成品。本次补齐 SQL 语法并验证真实执行。 */ import { MetonaSqlark } from '../src/core'; import { parse } from '../src/sql/parser'; import { createSchema } from '../src/table/schema'; import 'fake-indexeddb/auto'; let dbCounter = 0; function uniqueDB(): string { return `maint-${Date.now()}-${++dbCounter}-${Math.random().toString(36).slice(2, 8)}`; } describe('v0.5.1 — 维护语句解析', () => { it('EXPLAIN SELECT 解析为 ExplainStatement', () => { const stmt = parse('EXPLAIN SELECT * FROM users WHERE id = \'1\''); expect(stmt.type).toBe('EXPLAIN'); expect((stmt as { query: { type: string } }).query.type).toBe('SELECT'); }); it('EXPLAIN 嵌套 EXPLAIN 拒绝', () => { expect(() => parse('EXPLAIN EXPLAIN SELECT 1')).toThrow('Nested EXPLAIN'); }); it('ANALYZE / ANALYZE TABLE 解析', () => { expect(parse('ANALYZE users')).toMatchObject({ type: 'ANALYZE', table: 'users' }); expect(parse('ANALYZE TABLE users')).toMatchObject({ type: 'ANALYZE', table: 'users' }); }); it('REINDEX / VACUUM 解析', () => { expect(parse('REINDEX users')).toMatchObject({ type: 'REINDEX', table: 'users' }); expect(parse('REINDEX TABLE users')).toMatchObject({ type: 'REINDEX', table: 'users' }); expect(parse('VACUUM')).toMatchObject({ type: 'VACUUM' }); }); it('SAVEPOINT / ROLLBACK TO / RELEASE 解析', () => { expect(parse('SAVEPOINT sp1')).toMatchObject({ type: 'SAVEPOINT', name: 'sp1', action: 'SAVE' }); expect(parse('ROLLBACK TO sp1')).toMatchObject({ type: 'SAVEPOINT', name: 'sp1', action: 'ROLLBACK' }); expect(parse('ROLLBACK TO SAVEPOINT sp1')).toMatchObject({ type: 'SAVEPOINT', name: 'sp1', action: 'ROLLBACK' }); expect(parse('RELEASE SAVEPOINT sp1')).toMatchObject({ type: 'SAVEPOINT', name: 'sp1', action: 'RELEASE' }); expect(parse('RELEASE sp1')).toMatchObject({ type: 'SAVEPOINT', name: 'sp1', action: 'RELEASE' }); // ROLLBACK [TRANSACTION] 保持原语义 expect(parse('ROLLBACK')).toMatchObject({ type: 'ROLLBACK' }); expect(parse('ROLLBACK TRANSACTION')).toMatchObject({ type: 'ROLLBACK' }); }); }); describe('v0.5.1 — 维护语句执行(Aria 引擎)', () => { async function createAriaDb(): Promise { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'aria', diskEngine: 'indexeddb', aria: { checkpointInterval: 100000, walSyncMode: 'full' }, }); await db.init(); await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true }, age: { type: 'number' }, }); await db.table('users').insertMany([ { id: '1', name: 'Alice', age: 30 }, { id: '2', name: 'Bob', age: 25 }, { id: '3', name: 'Carol', age: 35 }, ]); return db; } it('EXPLAIN SELECT 返回查询计划', async () => { const db = await createAriaDb(); const plan = await db.query("EXPLAIN SELECT * FROM users WHERE id = '1'") as Record; expect(plan.type).toBe('SELECT'); expect(plan.table).toBe('users'); expect(plan.estimatedRows).toBe(1); await db.close(); }); it('EXPLAIN UPDATE / DELETE 返回计划', async () => { const db = await createAriaDb(); const planU = await db.query("EXPLAIN UPDATE users SET age = 31 WHERE id = '1'") as Record; expect(planU.type).toBe('UPDATE'); const planD = await db.query('EXPLAIN DELETE FROM users WHERE id = \'1\'') as Record; expect(planD.type).toBe('DELETE'); await db.close(); }); it('ANALYZE TABLE 收集统计信息', async () => { const db = await createAriaDb(); const stats = await db.query('ANALYZE TABLE users') as Record; expect(stats.rowCount).toBe(3); expect(stats.table).toBe('users'); await db.close(); }); it('REINDEX TABLE 重建索引后索引查询正常', async () => { const db = await createAriaDb(); const count = await db.query('REINDEX TABLE users') as number; expect(count).toBeGreaterThanOrEqual(0); const rows = await db.query("SELECT * FROM users WHERE name = 'Bob'") as Record[]; expect(rows).toHaveLength(1); await db.close(); }); it('VACUUM 压缩成功', async () => { const db = await createAriaDb(); const result = await db.query('VACUUM') as { compactedLevels: number }; expect(typeof result.compactedLevels).toBe('number'); await db.close(); }); it('SAVEPOINT / ROLLBACK TO / RELEASE 真实执行', async () => { const db = await createAriaDb(); await db.query('BEGIN'); await db.query("UPDATE users SET age = 99 WHERE id = '1'"); await db.query('SAVEPOINT sp_a'); await db.query("UPDATE users SET age = 88 WHERE id = '1'"); // 回滚到 savepoint:恢复 age=99 await db.query('ROLLBACK TO SAVEPOINT sp_a'); await db.query('RELEASE SAVEPOINT sp_a'); await db.query('COMMIT'); const rows = await db.query("SELECT age FROM users WHERE id = '1'") as Record[]; expect(rows[0].age).toBe(99); await db.close(); }); it('savepoint 已存在 → 抛 SAVEPOINT_EXISTS', async () => { const db = await createAriaDb(); await db.query('BEGIN'); await db.query('SAVEPOINT sp1'); await expect(db.query('SAVEPOINT sp1')).rejects.toMatchObject({ code: 'SAVEPOINT_EXISTS' }); await db.query('ROLLBACK'); await db.close(); }); it('回滚到不存在的 savepoint → 抛 SAVEPOINT_NOT_FOUND', async () => { const db = await createAriaDb(); await db.query('BEGIN'); await expect(db.query('ROLLBACK TO nope')).rejects.toMatchObject({ code: 'SAVEPOINT_NOT_FOUND' }); await db.query('ROLLBACK'); await db.close(); }); }); describe('v0.5.1 — 维护语句执行(Memory 引擎不支持)', () => { it('ANALYZE / REINDEX / VACUUM / SAVEPOINT → NOT_SUPPORTED', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' }); await db.init(); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await expect(db.query('ANALYZE TABLE t')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await expect(db.query('REINDEX TABLE t')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await expect(db.query('VACUUM')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await expect(db.query('SAVEPOINT sp')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' }); await db.close(); }); it('EXPLAIN 在所有引擎可用(返回基本信息)', async () => { const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' }); await db.init(); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.table('t').insert({ id: '1' }); const plan = await db.query('EXPLAIN SELECT * FROM t') as Record; expect(plan.type).toBe('SELECT'); expect(plan.table).toBe('t'); await db.close(); }); });