工作流 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
202 lines
6.4 KiB
TypeScript
202 lines
6.4 KiB
TypeScript
/**
|
|
* QueryBuilder 和 Compiler/Executor 测试
|
|
*/
|
|
|
|
import { MemoryEngine } from '../../src/engine/memory';
|
|
import { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from '../../src/query/builder';
|
|
import { compileStatement } from '../../src/query/compiler';
|
|
import { QueryExecutor } from '../../src/query/executor';
|
|
import { parse } from '../../src/sql/parser';
|
|
|
|
describe('QueryBuilder', () => {
|
|
let engine: MemoryEngine;
|
|
|
|
beforeEach(async () => {
|
|
engine = new MemoryEngine();
|
|
await engine.open('test-qb', 1);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.close();
|
|
});
|
|
|
|
// ---- SelectQueryBuilder ----
|
|
|
|
describe('SelectQueryBuilder', () => {
|
|
it('基础查询', () => {
|
|
const qb = new SelectQueryBuilder(engine, 'users');
|
|
expect(qb.toAST()).toMatchObject({
|
|
type: 'SELECT',
|
|
columns: ['*'],
|
|
from: 'users',
|
|
where: {},
|
|
});
|
|
});
|
|
|
|
it('链式调用', () => {
|
|
const qb = new SelectQueryBuilder(engine, 'users', ['id', 'name']);
|
|
qb.where({ age: { $gt: 18 } }).orderBy('name', 'asc').limit(10).offset(5);
|
|
|
|
const ast = qb.toAST();
|
|
expect(ast.columns).toEqual(['id', 'name']);
|
|
expect(ast.where).toEqual({ age: { $gt: 18 } });
|
|
expect(ast.orderBy).toEqual([{ column: 'name', direction: 'asc' }]);
|
|
expect(ast.limit).toBe(10);
|
|
expect(ast.offset).toBe(5);
|
|
});
|
|
|
|
it('多次 where 合并条件', () => {
|
|
const qb = new SelectQueryBuilder(engine, 'users');
|
|
qb.where({ age: { $gt: 18 } }).where({ active: true });
|
|
expect(qb.toAST().where).toEqual({ age: { $gt: 18 }, active: true });
|
|
});
|
|
|
|
it('多次 orderBy 追加排序', () => {
|
|
const qb = new SelectQueryBuilder(engine, 'users');
|
|
qb.orderBy('age', 'desc').orderBy('name', 'asc');
|
|
expect(qb.toAST().orderBy).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
// ---- UpdateQueryBuilder ----
|
|
|
|
describe('UpdateQueryBuilder', () => {
|
|
it('基础更新', () => {
|
|
const qb = new UpdateQueryBuilder(engine, 'users', { age: 31 });
|
|
qb.where({ id: '1' });
|
|
const ast = qb.toAST();
|
|
expect(ast).toMatchObject({
|
|
type: 'UPDATE',
|
|
table: 'users',
|
|
sets: { age: 31 },
|
|
where: { id: '1' },
|
|
});
|
|
});
|
|
|
|
it('不带 where', () => {
|
|
const qb = new UpdateQueryBuilder(engine, 'users', { active: false });
|
|
expect(qb.toAST().where).toEqual({});
|
|
});
|
|
});
|
|
|
|
// ---- DeleteQueryBuilder ----
|
|
|
|
describe('DeleteQueryBuilder', () => {
|
|
it('基础删除', () => {
|
|
const qb = new DeleteQueryBuilder(engine, 'users');
|
|
qb.where({ id: '1' });
|
|
const ast = qb.toAST();
|
|
expect(ast).toMatchObject({
|
|
type: 'DELETE',
|
|
from: 'users',
|
|
where: { id: '1' },
|
|
});
|
|
});
|
|
|
|
it('不带 where', () => {
|
|
const qb = new DeleteQueryBuilder(engine, 'users');
|
|
expect(qb.toAST().where).toEqual({});
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===================================================================
|
|
|
|
describe('Compiler', () => {
|
|
it('编译 SELECT', () => {
|
|
const ast = parse('SELECT id, name FROM users WHERE age > 18 ORDER BY age DESC LIMIT 10');
|
|
const plan = compileStatement(ast);
|
|
expect(plan.table).toBe('users');
|
|
expect(plan.columns).toEqual(['id', 'name']);
|
|
expect(plan.where).toBeDefined();
|
|
expect(plan.orderBy).toEqual([{ column: 'age', direction: 'desc' }]);
|
|
expect(plan.limit).toBe(10);
|
|
});
|
|
|
|
it('编译 DELETE', () => {
|
|
const ast = parse("DELETE FROM users WHERE id = '1'");
|
|
const plan = compileStatement(ast);
|
|
expect(plan.table).toBe('users');
|
|
expect(plan.where).toBeDefined();
|
|
});
|
|
|
|
it('编译 UPDATE', () => {
|
|
const ast = parse("UPDATE users SET age = 31 WHERE id = '1'");
|
|
const plan = compileStatement(ast);
|
|
expect(plan.table).toBe('users');
|
|
expect(plan.where).toBeDefined();
|
|
});
|
|
|
|
it('非查询语句抛出错误', () => {
|
|
const ast = parse('CREATE TABLE users (id STRING PRIMARY KEY)');
|
|
expect(() => compileStatement(ast)).toThrow('Cannot compile');
|
|
});
|
|
});
|
|
|
|
// ===================================================================
|
|
|
|
describe('QueryExecutor', () => {
|
|
let engine: MemoryEngine;
|
|
let executor: QueryExecutor;
|
|
|
|
beforeEach(async () => {
|
|
engine = new MemoryEngine();
|
|
await engine.open('test-exec', 1);
|
|
executor = new QueryExecutor(engine);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.close();
|
|
});
|
|
|
|
it('执行 CREATE TABLE', async () => {
|
|
const ast = parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING)');
|
|
await executor.execute(ast);
|
|
expect(await engine.hasTable('users')).toBe(true);
|
|
});
|
|
|
|
it('执行 INSERT + SELECT', async () => {
|
|
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)'));
|
|
await executor.execute(parse("INSERT INTO users (id, name, age) VALUES ('1', 'Alice', 30)"));
|
|
await executor.execute(parse("INSERT INTO users VALUES ('2', 'Bob', 25)"));
|
|
|
|
const result = await executor.execute(parse('SELECT * FROM users')) as Record<string, unknown>[];
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
it('执行 UPDATE', async () => {
|
|
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)'));
|
|
await executor.execute(parse("INSERT INTO users VALUES ('1', 'Alice', 30)"));
|
|
await executor.execute(parse("UPDATE users SET age = 31 WHERE id = '1'"));
|
|
|
|
const result = await executor.execute(parse("SELECT * FROM users WHERE id = '1'")) as Record<string, unknown>[];
|
|
expect(result[0].age).toBe(31);
|
|
});
|
|
|
|
it('执行 DELETE', async () => {
|
|
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING)'));
|
|
await executor.execute(parse("INSERT INTO users VALUES ('1', 'Alice')"));
|
|
await executor.execute(parse("INSERT INTO users VALUES ('2', 'Bob')"));
|
|
await executor.execute(parse("DELETE FROM users WHERE id = '1'"));
|
|
|
|
const result = await executor.execute(parse('SELECT * FROM users')) as Record<string, unknown>[];
|
|
expect(result).toHaveLength(1);
|
|
});
|
|
|
|
it('执行 DROP TABLE', async () => {
|
|
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY)'));
|
|
await executor.execute(parse('DROP TABLE users'));
|
|
expect(await engine.hasTable('users')).toBe(false);
|
|
});
|
|
|
|
it('操作不存在的表抛出错误', async () => {
|
|
await expect(
|
|
executor.execute(parse('SELECT * FROM nonexistent')),
|
|
).rejects.toThrow('does not exist');
|
|
});
|
|
|
|
it('getEngine', () => {
|
|
expect(executor.getEngine()).toBe(engine);
|
|
});
|
|
});
|