Files
MetonaSqlark/tests/sql/parser.test.ts
T
thzxx 4ab04df882 fix(A15): 三值逻辑求值器统一 —— 消除 WHERE 的第二套语义(静默错值根治)
背景(PLAN-v0.7.5.md 根因 1/7、缺陷 A15):
项目里 `matchWhere`(布尔版,自带 matchField)与三值求值器并存。同一条 SQL 的
语义取决于走哪个函数,实测三类静默错值:
  - `WHERE n = NULL` 命中 NULL 行、`WHERE n != NULL` 返回所有非 NULL 行;
  - `WHERE s NOT LIKE 'x'` 会把 NULL 行判真(布尔取反);
  - `WHERE n NOT BETWEEN 1 AND 2` 恒空集 —— parser 生成的字段级
    `{ n: { $or: [ {$lt:1}, {$gt:2} ] } }` 递归进了 where 子句级求值器,
    子项 `{ $lt: 1 }` 被当成"查询列 `$lt`" → 每行 UNKNOWN。

根治方式(不是打补丁,而是取消第二套实现):
1. where-matcher.ts 重写为**唯一一个递归求值器**,同时理解 where 子句级
   (键是列名/逻辑连接词)与操作符级(键是 `$gt` …),位置由上下文承载而非
   由另一个函数承载;`matchWhere` 退化为"三值结果是否恰为 TRUE"。
   行上下文随求值上下文下传,`$col` 在任意嵌套深度都能解析。
2. parser:`IS NULL` / `IS NOT NULL` 生成 `$isNull` / `$isNotNull` **谓词**
   (此前与 `= NULL` / `!= NULL` 共用 `$eq: null` / `$ne: null`,两者语义无法区分);
   `BETWEEN` 生成真正的范围条件(此前把同一对象同时当操作符对象与操作数);
   `NOT BETWEEN` 展开为 `$or: [{$lt}, {$gt}]`。
3. executor 新增 enginePreFilter:逐行求值谓词(`$col` / `$exists` / CASE 键)
   必须整体移出引擎层 —— 引擎无外层行上下文,会把它们判 UNKNOWN 并把**所有行**
   过滤掉,逐行求值再正确也无行可算。粒度按连接词决定:`$and` 成员可单独移除,
   `$or`/`$not` 成员一移除就改变结果集(漏行/多行),故整条下推放弃。

契约变更(旧测试编码了错误语义,已按 SQL 标准改正并注明理由):
  - a) `{ $eq: null }` 不再命中 NULL 行(`= NULL` 恒 UNKNOWN)→ 用 `$isNull`;
  - b) `IN` 列表含 NULL:`x IN (NULL, 'a')` 只命中 'a'(`null = NULL` 为 UNKNOWN),
       未命中的行仍因 UNKNOWN 不保留。
  - c) 引擎层 `$in: [null, ...]` 与 `$eq: null` 的断言同步修正。

验证:
  - 新增 tests/v080-sql-three-valued.test.ts:26 条 SQL 语义矩阵 × 4 引擎
    (memory/disk/hybrid/aria)+ UPDATE/DELETE 写路径,共 104 断言;
  - 全量 83 套件 / 1458 测试通过(含 Aria 生产负载 10 万行);
  - typecheck(src+tests) 与 lint 零错误。
2026-09-14 23:22:26 +08:00

255 lines
9.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'));
// v0.8.0A15):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 优先级高于 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'));
// v0.8.0A15):见上方 "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 } });
});
});
});