Files
MetonaSqlark/tests/maintenance-sql.test.ts
thzxx 0dba1abf2a test(P0): v0.8.0 验证基座与工程门禁根治
工作流 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
2026-09-14 21:03:06 +08:00

175 lines
7.1 KiB
TypeScript
Raw Permalink 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 { 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();
});
});