Files
MetonaSqlark/tests/v033-fixes.test.ts
T
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

423 lines
19 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.3.3 修复验证测试
* 验证所有 P0/P1 修复点:
* - P0-1: Aria WAL DROP_TABLE 崩溃恢复(表/数据不复活)+ WAL 恢复后截断
* - P0-2: MemoryEngine update/delete 索引维护(unique 约束 + 索引查询 + 级联清理)
* - P1-3: Aria 事务内读到自己写入的变更(insert 后 update/delete
* - P1-4: Aria clear() 写 WAL + 事务化
* - P1-5: WAL 字节数跟踪(full 模式 walSizeThreshold 生效)
* - P1-6: Aria 主键列不建冗余二级索引(PK $in / 范围查询走主 LSM
* - P1-7: ORDER BY 支持 SELECT 别名
* - P1-8: SQL 字符串 '' 标准转义
* - P1-9: Savepoint 回滚后 MVCC 版本链一致 + rollback 索引重建
*/
import { VERSION } from '../src/constants';
import { MetonaSqlark } from '../src/core';
import { AriaEngine } from '../src/engine/aria/index';
import { MemoryEngine } from '../src/engine/memory';
import { WAL } from '../src/engine/aria/wal/log';
import { tokenize } from '../src/sql/lexer';
import { resetOPFSMock } from './helpers/storage-harness';
beforeEach(() => { resetOPFSMock(); });
// ---------------------------------------------------------------------------
// P0-1: Aria WAL DROP_TABLE 崩溃恢复
// ---------------------------------------------------------------------------
describe('[v0.3.3] P0-1: WAL DROP_TABLE 崩溃恢复', () => {
const mkEngine = async (name: string): Promise<AriaEngine> => {
const e = new AriaEngine({ storageBackend: 'opfs', walSyncMode: 'full' });
await e.open(name, 1);
return e;
};
test('删表后崩溃(不 close),重启后表与数据不复活', async () => {
const dbName = `crash-drop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
let e = await mkEngine(dbName);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
await e.insert('t', [{ id: '1' }, { id: '2' }]);
await e.dropTable('t');
// 模拟崩溃:不 close,直接重建引擎(WAL 未 checkpoint
e = await mkEngine(dbName);
const names = await e.getTableNames();
expect(names).not.toContain('t');
await e.close();
});
test('删表崩溃后重建同名表,旧数据不复活', async () => {
const dbName = `crash-drop2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
let e = await mkEngine(dbName);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
await e.insert('t', [{ id: '1' }, { id: '2' }]);
await e.dropTable('t');
e = await mkEngine(dbName);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
const rows = await e.find('t', { table: 't' });
expect(rows).toHaveLength(0);
await e.close();
});
test('WAL 恢复后截断:重启不再重复回放(checkpoint 后 WAL 空)', async () => {
const dbName = `crash-wal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
let e = await mkEngine(dbName);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } });
await e.insert('t', [{ id: '1', v: 1 }, { id: '2', v: 2 }]);
await e.dropTable('t');
// 第一次恢复:WAL 回放 + 截断
e = await mkEngine(dbName);
expect(await e.getTableNames()).not.toContain('t');
// 第二次恢复:WAL 已空,不再有任何回放副作用
await e.close();
e = await mkEngine(dbName);
expect(await e.getTableNames()).not.toContain('t');
await e.close();
});
});
// ---------------------------------------------------------------------------
// P0-2: MemoryEngine update/delete 索引维护
// ---------------------------------------------------------------------------
describe('[v0.3.3] P0-2: Memory 引擎索引维护', () => {
test('update 修改唯一列后,重复值插入被拦截(unique 约束不被绕过)', async () => {
const e = new MemoryEngine();
await e.open('x', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } });
await e.insert('t', [{ id: '1', email: 'a@x.com' }]);
await e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' });
await expect(e.insert('t', [{ id: '2', email: 'b@x.com' }])).rejects.toThrow('Unique constraint');
});
test('update 后按新值索引查询命中', async () => {
const e = new MemoryEngine();
await e.open('x', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true } } });
await e.insert('t', [{ id: '1', email: 'a@x.com' }]);
await e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' });
const rows = await e.find('t', { table: 't', where: { email: 'b@x.com' } });
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
});
test('update 到已存在的唯一值时抛 UNIQUE_VIOLATION', async () => {
const e = new MemoryEngine();
await e.open('x', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } });
await e.insert('t', [{ id: '1', email: 'a@x.com' }, { id: '2', email: 'b@x.com' }]);
await expect(e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' })).rejects.toThrow('Unique constraint');
});
test('delete 后索引清理:同值可重新插入且索引查询正确', async () => {
const e = new MemoryEngine();
await e.open('x', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } });
await e.insert('t', [{ id: '1', email: 'a@x.com' }]);
await e.delete('t', { table: 't', where: { id: '1' } });
await e.insert('t', [{ id: '2', email: 'a@x.com' }]);
const rows = await e.find('t', { table: 't', where: { email: 'a@x.com' } });
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('2');
});
test('级联删除清理子表索引条目', async () => {
const e = new MemoryEngine();
await e.open('x', 1);
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true } } });
await e.createTable({ name: 'orders', columns: { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', index: true, references: 'users.id', onDelete: 'CASCADE' } } });
await e.insert('users', [{ id: 'u1' }]);
await e.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
await e.delete('users', { table: 'users', where: { id: 'u1' } });
// u1 已删,o1 级联删除 → 按 user_id 索引查询应为空
const rows = await e.find('orders', { table: 'orders', where: { user_id: 'u1' } });
expect(rows).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// P1-3: Aria 事务内读到自己写入的变更
// ---------------------------------------------------------------------------
describe('[v0.3.3] P1-3: Aria 事务内读写一致', () => {
test('事务内 insert 后 update 同一行生效', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('txn1', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } });
await e.beginTransaction();
await e.insert('t', [{ id: '1', name: 'Alice' }]);
const n = await e.update('t', { table: 't', where: { id: '1' } }, { name: 'Bob' });
await e.commitTransaction();
const rows = await e.find('t', { table: 't' });
expect(n).toBe(1);
expect(rows[0].name).toBe('Bob');
await e.close();
});
test('事务内 insert 后 delete 该行生效', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('txn2', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
await e.beginTransaction();
await e.insert('t', [{ id: '1' }, { id: '2' }]);
const d = await e.delete('t', { table: 't', where: { id: '2' } });
await e.commitTransaction();
const rows = await e.find('t', { table: 't' });
expect(d).toBe(1);
expect(rows).toHaveLength(1);
await e.close();
});
test('rollback 后索引无残留(事务内写入的索引被重建清理)', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('txn3', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } });
await e.beginTransaction();
await e.insert('t', [{ id: '1', name: 'Dave' }]);
await e.rollbackTransaction();
const rows = await e.find('t', { table: 't', where: { name: 'Dave' } });
expect(rows).toHaveLength(0);
await e.close();
});
test('rollback 更新后索引恢复旧值', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('txn4', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } });
await e.insert('t', [{ id: '1', name: 'Bob' }]);
await e.beginTransaction();
await e.update('t', { table: 't', where: { id: '1' } }, { name: 'Eve' });
await e.rollbackTransaction();
const rows = await e.find('t', { table: 't', where: { name: 'Bob' } });
expect(rows).toHaveLength(1);
const stale = await e.find('t', { table: 't', where: { name: 'Eve' } });
expect(stale).toHaveLength(0);
await e.close();
});
test('事务内 clear 生效且可提交', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('txn5', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
await e.insert('t', [{ id: '1' }, { id: '2' }]);
await e.beginTransaction();
await e.clear('t');
await e.commitTransaction();
expect(await e.count('t')).toBe(0);
await e.close();
});
});
// ---------------------------------------------------------------------------
// P1-5: WAL 字节数跟踪
// ---------------------------------------------------------------------------
describe('[v0.3.3] P1-5: WAL 字节数跟踪', () => {
test('full 模式 append 后 getBufferedBytes 反映实际字节数', async () => {
const chunks: Uint8Array[] = [];
const wal = new WAL({
append: async (d) => { chunks.push(d); },
readAll: async () => { const t = chunks.reduce((s, c) => s + c.byteLength, 0); const out = new Uint8Array(t); let o = 0; for (const c of chunks) { out.set(c, o); o += c.byteLength; } return out; },
truncate: async () => { chunks.length = 0; },
exists: async () => chunks.length > 0,
}, true, 'full');
await wal.append({ type: 1, txnId: 0, tableName: 't', key: '1', data: { a: 1 } } as never);
const bytesAfterAppend = wal.getBufferedBytes();
expect(bytesAfterAppend).toBeGreaterThan(0);
await wal.checkpoint();
expect(wal.getBufferedBytes()).toBe(0);
});
test('batch 模式 flush 后字节数保留(未 checkpoint 前)', async () => {
const chunks: Uint8Array[] = [];
const wal = new WAL({
append: async (d) => { chunks.push(d); },
readAll: async () => new Uint8Array(0),
truncate: async () => { chunks.length = 0; },
exists: async () => chunks.length > 0,
}, true, 'batch');
await wal.append({ type: 1, txnId: 0, tableName: 't', key: '1', data: { a: 1 } } as never);
await wal.flush();
expect(wal.getBufferedBytes()).toBeGreaterThan(0); // 已写盘但未 checkpoint
await wal.checkpoint();
expect(wal.getBufferedBytes()).toBe(0);
});
});
// ---------------------------------------------------------------------------
// P1-6: Aria 主键列走主 LSM(无冗余二级索引)
// ---------------------------------------------------------------------------
describe('[v0.3.3] P1-6: Aria 主键查询优化', () => {
test('PK $in 查询走主 LSM 返回正确行', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('pk1', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } });
await e.insert('t', [{ id: '1', v: 10 }, { id: '2', v: 20 }, { id: '3', v: 30 }]);
const rows = await e.find('t', { table: 't', where: { id: { $in: ['1', '3'] } } });
expect(rows.map((r) => r.id).sort()).toEqual(['1', '3']);
await e.close();
});
test('PK 范围查询(字符串字典序)', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('pk2', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } });
await e.insert('t', [{ id: 'a', v: 1 }, { id: 'b', v: 2 }, { id: 'c', v: 3 }]);
const rows = await e.find('t', { table: 't', where: { id: { $gte: 'b' } } });
expect(rows.map((r) => r.id)).toEqual(['b', 'c']);
const lt = await e.find('t', { table: 't', where: { id: { $lt: 'b' } } });
expect(lt.map((r) => r.id)).toEqual(['a']);
await e.close();
});
test('主键列不再创建独立二级索引(dropIndex 仍保护主键)', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('pk3', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } });
// 主键索引保护仍在
await expect(e.dropIndex('t', 'id')).rejects.toThrow('Cannot drop primary key');
// 二级索引(name)仍可正常使用
await e.insert('t', [{ id: '1', name: 'x' }]);
const rows = await e.find('t', { table: 't', where: { name: 'x' } });
expect(rows).toHaveLength(1);
await e.close();
});
});
// ---------------------------------------------------------------------------
// P1-7: ORDER BY 别名
// ---------------------------------------------------------------------------
describe('[v0.3.3] P1-7: ORDER BY 别名', () => {
test('SELECT 别名可被 ORDER BY 引用', async () => {
const db = new MetonaSqlark({ name: `alias-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
});
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)");
await db.query("INSERT INTO users VALUES ('3', 'Carol', 35)");
const rows = await db.query('SELECT name AS n FROM users ORDER BY n DESC') as Record<string, unknown>[];
expect(rows.map((r) => r.n)).toEqual(['Carol', 'Bob', 'Alice']);
const withLimit = await db.query('SELECT name AS n FROM users ORDER BY n ASC LIMIT 2') as Record<string, unknown>[];
expect(withLimit.map((r) => r.n)).toEqual(['Alice', 'Bob']);
await db.close();
});
test('GROUP BY 场景 ORDER BY 聚合别名', async () => {
const db = new MetonaSqlark({ name: `alias2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
await db.init();
await db.defineTable('emp', {
id: { type: 'string', primaryKey: true },
dept: { type: 'string' },
salary: { type: 'number' },
});
await db.query("INSERT INTO emp VALUES ('1', 'eng', 100)");
await db.query("INSERT INTO emp VALUES ('2', 'eng', 200)");
await db.query("INSERT INTO emp VALUES ('3', 'ops', 300)");
const rows = await db.query('SELECT dept, COUNT(*) AS cnt FROM emp GROUP BY dept ORDER BY cnt DESC') as Record<string, unknown>[];
expect(rows[0]).toEqual({ dept: 'eng', cnt: 2 });
await db.close();
});
});
// ---------------------------------------------------------------------------
// P1-8: SQL 字符串 '' 标准转义
// ---------------------------------------------------------------------------
describe('[v0.3.3] P1-8: SQL 字符串转义', () => {
test("'' 双引号转义为单个引号", async () => {
const db = new MetonaSqlark({ name: `esc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
await db.init();
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
note: { type: 'string' },
});
await db.query("INSERT INTO t VALUES ('1', 'it''s a test')");
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows[0].note).toBe("it's a test");
await db.close();
});
test('反斜杠转义仍兼容', () => {
const tokens = tokenize("SELECT 'a\\'b'");
expect(tokens[1].value).toBe("a'b");
});
});
// ---------------------------------------------------------------------------
// P1-9: Savepoint 回滚后 MVCC 一致
// ---------------------------------------------------------------------------
describe('[v0.3.3] P1-9: Savepoint + MVCC 一致性', () => {
test('rollbackToSavepoint 后提交,数据为 savepoint 时状态', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('sp1', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } });
await e.insert('t', [{ id: '1', v: 1 }]);
await e.beginTransaction();
await e.insert('t', [{ id: '2', v: 2 }]);
await e.savepoint('sp');
await e.insert('t', [{ id: '3', v: 3 }]);
await e.rollbackToSavepoint('sp');
await e.commitTransaction();
const rows = await e.find('t', { table: 't' });
expect(rows.map((r) => r.id).sort()).toEqual(['1', '2']);
await e.close();
});
test('savepoint 回滚后事务仍可继续写入并提交', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('sp2', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
await e.beginTransaction();
await e.savepoint('sp');
await e.insert('t', [{ id: '1' }]);
await e.rollbackToSavepoint('sp');
await e.insert('t', [{ id: '2' }]);
await e.commitTransaction();
const rows = await e.find('t', { table: 't' });
expect(rows.map((r) => r.id)).toEqual(['2']);
await e.close();
});
});
// ---------------------------------------------------------------------------
// 端到端冒烟
// ---------------------------------------------------------------------------
describe('[v0.3.3] 端到端', () => {
test('全部修复点可共存于 MetonaSqlark API', async () => {
// v0.8.0: 版本一致性由 tests/version-contract.test.ts 统一校验
expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/);
const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true, index: true },
age: { type: 'number' },
});
await db.query("INSERT INTO users VALUES ('1', 'a@x.com', 20)");
await db.query("INSERT INTO users VALUES ('2', 'b@x.com', 30)");
// 别名排序 + 唯一约束组合
const rows = await db.query('SELECT email AS e FROM users ORDER BY e DESC') as Record<string, unknown>[];
expect(rows.map((r) => r.e)).toEqual(['b@x.com', 'a@x.com']);
await db.close();
});
});