删除: - AUDIT-aria-lsm-v0.7.4.md(50KB) - AUDIT-query-layer-v0.7.4.md(39KB) - AUDIT-storage-engines-v0.7.4.md(39KB) - PLAN-v0.7.5.md(98KB,含附录 G/H/I) 删除前先清除引用面,避免留下断链(共 18 处): - 源码注释 7 处(change-notifier / kvstore index / column-value / expression / sql-compare / where-matcher / validation):保留设计意图,引用改为"v0.8.0 审计根因 N" - 测试注释 9 处(opfs.spec / aria-opfs-backend / faulty-backend / storage-harness / v080-b6 / v080-kvstore / v080-query-layer / v080-sql-three-valued / v080-unified-validation / parser):同上 - CHANGELOG 3 处:改为不依赖已删除文档的自洽表述(B-6 交付物见各条;门禁订正三处 按内容重写),并把变异数量同步为 42 - 校验:三个 md 之间无断链;仓库内已无 PLAN-v0.7.5/AUDIT-* 的任何引用 (git 历史仍可追溯,需要时可 `git show <commit>:PLAN-v0.7.5.md` 找回) 验证:93 套件 / 1985 用例全绿;覆盖率 90.59 / 82.61 / 94.14 / 93.50(阈值 90/82/94/93); e2e 14/14;lint + 两份 tsc 干净;dist 已重建(注释只影响非压缩产物,min 产物 251,731 B / gzip 63,431 B 不变)。 说明:审查记录的核心内容仍在 CHANGELOG.md("全量回归审查"与"现场失败修复"两节), 随 PLAN 一起删除的是附录 G/H/I 的详细表格(门禁逐条验收、交付物清单、未修复项表)。
255 lines
9.9 KiB
TypeScript
255 lines
9.9 KiB
TypeScript
/**
|
||
* Parser 边缘场景测试
|
||
*
|
||
* v0.8.0 强化说明:
|
||
* 1. 此前 12 处断言写着 `expect(ast.where).toBeDefined()` —— 既没有验证结构也没有验证
|
||
* 值,属于"空断言"。审计(v0.8.0 · query 层)指出:`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'));
|
||
// v0.8.0(A15):IS NULL 是**谓词**,不是 `$eq: null`。
|
||
// `WHERE bio = NULL` 在 SQL 里恒为 UNKNOWN(空集),只有 IS NULL 能命中 NULL 行;
|
||
// 此前两者编译成同一个 `{ $eq: null }`,于是任何调用方都无法表达"等于 NULL"。
|
||
expect(ast.where).toEqual({ bio: { $isNull: true } });
|
||
});
|
||
|
||
it('WHERE IS NOT NULL', () => {
|
||
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NOT NULL'));
|
||
// 同理:`$ne: null` 在老语义下会返回所有非 NULL 行,看起来"对",
|
||
// 但它与 `!= NULL`(应为空集)共用同一个 AST,语义无法区分。
|
||
expect(ast.where).toEqual({ bio: { $isNotNull: true } });
|
||
});
|
||
|
||
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 优先级高于 OR(SQL 标准)', () => {
|
||
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'));
|
||
// v0.8.0(A15):见上方 "WHERE IS NULL" —— 解析为谓词而非与 NULL 的比较
|
||
expect(ast.where).toEqual({ bio: { $isNull: true } });
|
||
});
|
||
|
||
it('与 NULL 的比较保留为比较运算(结果为 UNKNOWN)', () => {
|
||
// 回归护栏:`= NULL` 必须编译成 `$eq: null`(而不是被"优化"成 $isNull)。
|
||
// 两者结果集完全不同:前者空集,后者命中 NULL 行。
|
||
const ast = asSelect(parse('SELECT * FROM users WHERE bio = NULL'));
|
||
expect(ast.where).toEqual({ bio: { $eq: null } });
|
||
});
|
||
});
|
||
});
|