Files
MetonaSqlark/tests/maintenance-sql.test.ts
T
thzxx 24b3ca1079
CI / test (20.x) (push) Successful in 10m53s
CI / test (22.x) (push) Successful in 10m47s
CI / test (24.x) (push) Successful in 10m42s
CI / e2e (push) Successful in 10m27s
CI / test (18.x) (push) Successful in 11m1s
release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
2026-08-10 13:56:04 +08:00

176 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 { installOPFSMock } from './helpers/opfs-mock';
beforeEach(() => { installOPFSMock(new Map()); });
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: 'opfs',
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();
});
});