工作流 C-1 / C-3 前半 + 测试代码类型检查。 【故障注入基座】新增 tests/helpers/storage-harness.ts + faulty-backend.ts - TransactionalFileStore:忠实 OPFS 提交语义(close 才可见)+ 字节级故障注入 (failNextWrite/Append/Delete、truncateAppendTo 撕裂写、crashPending 真崩溃) - 删除旧 opfs-mock:读返回内部引用、keepExistingData:false 不截断、close 空实现 导致"提交前可见"等真实缺陷无法被测出(31 个测试文件迁移至新 harness) - 删除 aria-opfs-backend 内的第三份重复 mock(含从未被断言使用的 writeCalls 死代码 与 entry.content.subarray 恒等分支) - FaultyBackend:包装任意 IStorageBackend 注入故障;crash() 明确区别于 close() (后者是优雅停机,会刷完写队列 —— 这正是此前所有"崩溃恢复"测试的真相) - 16 条基座自测证明注入真的生效(含 close 不能当崩溃的对照组) 【覆盖率口径】jest.config.cjs - 移除 '!src/**/index.ts'(该 glob 把 AriaEngine 主实现等 15 个实现文件整体 排除出统计,与 v0.2.6 曾承认过的问题同源),改为只排除纯类型声明文件并附理由 - 新增 coverageThreshold 门禁(此前完全不存在) - 真实基线:语句 90.66% / 分支 82.94% / 函数 94.36% / 行 93.43% - 修正 testMatch 使 tests/helpers 下的测试可被发现 【测试代码类型检查】tsconfig.test.json + npm run typecheck:tests - 修复 103 个测试代码类型错误(此前 babel 剥离类型 + tsconfig 排除 tests,全部隐藏) - 新增 tests/helpers/assertions.ts:nonNull/decode/rows/object/engineMethod/expectCode 以断言收窄替代 as any - 消除 21 个 lint warning(含 v043-hardening 中定义后从未调用的 mockOPFS 死代码) - parser.test.ts 12 处 toBeDefined() 空断言升级为结构断言(并新增 AND/OR 优先级用例, 当前红灯,对应总账第 11 项,将在工作流 A 修复) 【版本契约】新增 tests/version-contract.test.ts - 校验 src VERSION / package.json / dist 三者一致,替代两处硬编码版本字面量 【CI 门禁】.gitea/workflows/ci.yml - lint 去掉 continue-on-error(此前永远不让 CI 变红) - 新增 tests 类型检查、--coverage 覆盖率门禁、dist 与源码同步校验 - 版本 0.7.4 升至 0.8.0
175 lines
7.1 KiB
TypeScript
175 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 { resetOPFSMock } from './helpers/storage-harness';
|
||
|
||
beforeEach(() => { resetOPFSMock(); });
|
||
|
||
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();
|
||
});
|
||
});
|