Files
MetonaSqlark/tests/sql/parser.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

242 lines
8.9 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.
/**
* Parser 边缘场景测试
*
* v0.8.0 强化说明:
* 1. 此前 12 处断言写着 `expect(ast.where).toBeDefined()` —— 既没有验证结构也没有验证
* 值,属于"空断言"。审计(AUDIT-query-layer / SQL 层)指出:`parser.test.ts` 无一处
* 断言混合 AND/OR 的**结构**,这正是 AND/OR 无优先级缺陷(总账第 11 项)能长期存活的原因。
* 2. 本文件此前 `parse()` 的返回类型是联合类型 `Statement`,直接访问 `.where` / `.columns`
* 在类型上不成立(tests 从不做类型检查,所以没人发现)。现在用类型收窄辅助函数显式
* 区分语句种类 —— 收窄失败即测试失败,而不是静默读到一个 undefined。
*/
import { parse } from '../../src/sql/parser';
import type {
SelectStatement,
InsertStatement,
UpdateStatement,
DeleteStatement,
CreateTableStatement,
} from '../../src/query/ast';
// ---------------------------------------------------------------------------
// 类型收窄辅助:断言语句种类并返回收窄后的类型
// (替代 `as any` —— 收窄失败会立刻让测试失败,而不是让断言落空)
// ---------------------------------------------------------------------------
function asSelect(stmt: ReturnType<typeof parse>): SelectStatement {
expect(stmt.type).toBe('SELECT');
if (stmt.type !== 'SELECT') throw new Error(`expected SELECT, got ${stmt.type}`);
return stmt;
}
function asInsert(stmt: ReturnType<typeof parse>): InsertStatement & { values: unknown[][] } {
expect(stmt.type).toBe('INSERT');
if (stmt.type !== 'INSERT') throw new Error(`expected INSERT, got ${stmt.type}`);
if (!stmt.values) throw new Error('INSERT statement is missing values');
return stmt as InsertStatement & { values: unknown[][] };
}
function asUpdate(stmt: ReturnType<typeof parse>): UpdateStatement {
expect(stmt.type).toBe('UPDATE');
if (stmt.type !== 'UPDATE') throw new Error(`expected UPDATE, got ${stmt.type}`);
return stmt;
}
function asDelete(stmt: ReturnType<typeof parse>): DeleteStatement {
expect(stmt.type).toBe('DELETE');
if (stmt.type !== 'DELETE') throw new Error(`expected DELETE, got ${stmt.type}`);
return stmt;
}
function asCreateTable(stmt: ReturnType<typeof parse>): CreateTableStatement {
expect(stmt.type).toBe('CREATE_TABLE');
if (stmt.type !== 'CREATE_TABLE') throw new Error(`expected CREATE_TABLE, got ${stmt.type}`);
return stmt;
}
describe('Parser 边缘场景', () => {
// ---- SELECT 扩展 ----
describe('SELECT 扩展', () => {
it('WHERE 多条件 AND', () => {
const ast = asSelect(parse("SELECT * FROM users WHERE age > 18 AND name LIKE 'A%'"));
// v0.8.0: 断言真实结构而非 toBeDefined()
expect(ast.where).toEqual({
$and: [{ age: { $gt: 18 } }, { name: { $like: 'A%' } }],
});
});
it('WHERE IN 列表', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE id IN (1, 2, 3)'));
expect(ast.where).toEqual({ id: { $in: [1, 2, 3] } });
});
it('WHERE IS NULL', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NULL'));
expect(ast.where).toEqual({ bio: { $eq: null } });
});
it('WHERE IS NOT NULL', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NOT NULL'));
expect(ast.where).toEqual({ bio: { $ne: null } });
});
it('WHERE NOT', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE NOT age = 18'));
expect(ast.where).toEqual({ $not: { age: { $eq: 18 } } });
});
it('WHERE 括号分组', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE (age > 18 OR age < 10) AND active = TRUE'));
expect(ast.where).toEqual({
$and: [{ $or: [{ age: { $gt: 18 } }, { age: { $lt: 10 } }] }, { active: { $eq: true } }],
});
});
// v0.8.0 新增:AND 优先级必须高于 OR(SQL 标准),此前是纯左折叠
it('AND 优先级高于 ORSQL 标准)', () => {
const ast = asSelect(parse('SELECT * FROM t WHERE a = 1 OR a = 2 AND b = 3'));
expect(ast.where).toEqual({
$or: [{ a: { $eq: 1 } }, { $and: [{ a: { $eq: 2 } }, { b: { $eq: 3 } }] }],
});
});
it('显式括号可覆盖默认优先级', () => {
const ast = asSelect(parse('SELECT * FROM t WHERE (a = 1 OR a = 2) AND b = 3'));
expect(ast.where).toEqual({
$and: [{ $or: [{ a: { $eq: 1 } }, { a: { $eq: 2 } }] }, { b: { $eq: 3 } }],
});
});
it('NOT 优先级高于 AND/OR', () => {
const ast = asSelect(parse('SELECT * FROM t WHERE NOT a = 1 OR b = 2'));
expect(ast.where).toEqual({
$or: [{ $not: { a: { $eq: 1 } } }, { b: { $eq: 2 } }],
});
});
it('多层 AND/OR 交替的结合性', () => {
const ast = asSelect(parse('SELECT * FROM t WHERE a = 1 AND b = 2 OR c = 3 AND d = 4'));
expect(ast.where).toEqual({
$or: [
{ $and: [{ a: { $eq: 1 } }, { b: { $eq: 2 } }] },
{ $and: [{ c: { $eq: 3 } }, { d: { $eq: 4 } }] },
],
});
});
it('ORDER BY 多列', () => {
const ast = asSelect(parse('SELECT * FROM users ORDER BY age DESC, name ASC'));
expect(ast.orderBy).toHaveLength(2);
expect(ast.orderBy![0]).toEqual({ column: 'age', direction: 'desc' });
expect(ast.orderBy![1]).toEqual({ column: 'name', direction: 'asc' });
});
it('只有 LIMIT 没有 OFFSET', () => {
const ast = asSelect(parse('SELECT * FROM users LIMIT 5'));
expect(ast.limit).toBe(5);
expect(ast.offset).toBeUndefined();
});
it('只有 OFFSET 没有 LIMIT', () => {
const ast = asSelect(parse('SELECT * FROM users OFFSET 10'));
expect(ast.offset).toBe(10);
expect(ast.limit).toBeUndefined();
});
it('不带 WHERE 的 SELECT', () => {
const ast = asSelect(parse('SELECT id, name FROM users ORDER BY id LIMIT 5'));
expect(ast.where).toEqual({});
});
});
// ---- INSERT 扩展 ----
describe('INSERT 扩展', () => {
it('不带列名的 INSERT', () => {
const ast = asInsert(parse("INSERT INTO users VALUES ('1', 'Alice', 30)"));
expect(ast.columns).toBeUndefined();
expect(ast.values).toEqual([['1', 'Alice', 30]]);
});
it('INSERT 布尔值和 NULL', () => {
const ast = asInsert(parse('INSERT INTO users VALUES (TRUE, FALSE, NULL)'));
expect(ast.values[0]).toEqual([true, false, null]);
});
it('INSERT 负数和浮点数', () => {
const ast = asInsert(parse('INSERT INTO scores VALUES (-1, 3.14)'));
expect(ast.values[0]).toEqual([-1, 3.14]);
});
});
// ---- UPDATE 扩展 ----
describe('UPDATE 扩展', () => {
it('UPDATE 多列', () => {
const ast = asUpdate(parse("UPDATE users SET name = 'Bob', age = 26 WHERE id = '1'"));
expect(ast.sets).toEqual({ name: 'Bob', age: 26 });
expect(ast.where).toEqual({ id: { $eq: '1' } });
});
it('UPDATE 不带 WHERE', () => {
const ast = asUpdate(parse('UPDATE users SET active = FALSE'));
expect(ast.where).toEqual({});
});
});
// ---- DELETE 扩展 ----
describe('DELETE 扩展', () => {
it('DELETE 不带 WHERE', () => {
const ast = asDelete(parse('DELETE FROM users'));
expect(ast.where).toEqual({});
});
it('DELETE 带复杂 WHERE', () => {
const ast = asDelete(parse("DELETE FROM users WHERE age < 18 OR status = 'inactive'"));
expect(ast.where).toEqual({
$or: [{ age: { $lt: 18 } }, { status: { $eq: 'inactive' } }],
});
});
});
// ---- DDL 扩展 ----
describe('DDL 扩展', () => {
it('CREATE TABLE 完整修饰符', () => {
const ast = asCreateTable(parse("CREATE TABLE products (id STRING PRIMARY KEY, name STRING NOT NULL UNIQUE, price NUMBER DEFAULT 0, active BOOLEAN DEFAULT TRUE)"));
expect(ast.columns).toHaveLength(4);
expect(ast.columns[0]).toMatchObject({ name: 'id', primaryKey: true });
expect(ast.columns[1]).toMatchObject({ name: 'name', required: true, unique: true });
expect(ast.columns[2]).toMatchObject({ name: 'price', default: 0 });
expect(ast.columns[3]).toMatchObject({ name: 'active', default: true });
});
it('支持 DROP TABLE IF EXISTS 语法', () => {
const ast = parse('DROP TABLE IF EXISTS users');
expect(ast).toMatchObject({ type: 'DROP_TABLE', name: 'users', ifExists: true });
});
it('DROP TABLE 不带 IF EXISTS', () => {
const ast = parse('DROP TABLE users');
expect(ast).toMatchObject({ type: 'DROP_TABLE', name: 'users' });
});
});
// ---- 值类型 ----
describe('常量值解析', () => {
it('布尔值 TRUE/FALSE', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE active = TRUE'));
expect(ast.where).toEqual({ active: { $eq: true } });
});
it('NULL 值', () => {
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NULL'));
expect(ast.where).toEqual({ bio: { $eq: null } });
});
});
});