173 lines
7.1 KiB
TypeScript
173 lines
7.1 KiB
TypeScript
/**
|
||
* 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<MetonaSqlark> {
|
||
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<string, unknown>;
|
||
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<string, unknown>;
|
||
expect(planU.type).toBe('UPDATE');
|
||
const planD = await db.query('EXPLAIN DELETE FROM users WHERE id = \'1\'') as Record<string, unknown>;
|
||
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<string, unknown>;
|
||
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<string, unknown>[];
|
||
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<string, unknown>[];
|
||
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<string, unknown>;
|
||
expect(plan.type).toBe('SELECT');
|
||
expect(plan.table).toBe('t');
|
||
await db.close();
|
||
});
|
||
});
|