Files
MetonaSqlark/tests/v080-sql-three-valued.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

141 lines
5.5 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.8.0 回归套件:SQL 三值逻辑(NULL 语义)—— PB-2
* ============================================================================
* 本套件锁定 PLAN-v0.7.5.md 根因 7"未解析/UNKNOWN 静默变 false")与
* 附录 §5 缺陷 A15 的修复契约,并对**四种存储引擎**逐一验证同一语义。
*
* 历史缺陷(本套件即为其反例):
* 1. `WHERE n = NULL` 命中 NULL 行(`===` 比较 null === null);
* 2. `WHERE n != NULL` 返回所有非 NULL 行;
* 3. `WHERE n NOT BETWEEN 1 AND 2` 恒空集 —— parser 生成的字段级
* `{ n: { $or: [ { $lt: 1 }, { $gt: 2 } ] } }` 被求值成"查询字段 `$lt`"
* 每行都是 UNKNOWN;根因是字段级 `$or` 递归进了 where 子句级求值器。
*
* 参考数据:
* id=1 n=1 s='x'
* id=2 n=2 s=NULL
* id=3 n=NULL s='x'
* id=4 n=3 s='y'
*/
import { MetonaSqlark } from '../src/core';
import type { DatabaseConfig } from '../src/constants';
import { rows as rowsOf } from './helpers/assertions';
interface EngineCase {
label: string;
mode: DatabaseConfig['mode'];
extra?: Partial<DatabaseConfig>;
}
const ENGINES: EngineCase[] = [
{ label: 'memory', mode: 'memory' },
{ label: 'disk', mode: 'disk' },
{ label: 'hybrid', mode: 'hybrid' },
{ label: 'aria', mode: 'aria', extra: { diskEngine: 'memory' } },
];
/** [SQL, 期望 id 集合] */
const MATRIX: Array<[sql: string, expected: string[]]> = [
// --- 与 NULL 的比较:结果 UNKNOWNWHERE 不保留 ---
['SELECT id FROM t WHERE n = NULL', []],
['SELECT id FROM t WHERE n != NULL', []],
['SELECT id FROM t WHERE n <> NULL', []],
// --- IS NULL / IS NOT NULL 是谓词,不走比较 ---
['SELECT id FROM t WHERE n IS NULL', ['3']],
['SELECT id FROM t WHERE n IS NOT NULL', ['1', '2', '4']],
["SELECT id FROM t WHERE s IS NULL", ['2']],
// --- IN / NOT IN:列表含 NULL 且未命中 → UNKNOWN ---
['SELECT id FROM t WHERE n IN (1, NULL)', ['1']],
['SELECT id FROM t WHERE n NOT IN (1, 2)', ['4']],
["SELECT id FROM t WHERE s NOT IN ('x')", ['4']],
['SELECT id FROM t WHERE n IN (1, 2)', ['1', '2']],
// --- LIKE / NOT LIKENULL 操作数 → UNKNOWN ---
["SELECT id FROM t WHERE s LIKE 'x'", ['1', '3']],
["SELECT id FROM t WHERE s NOT LIKE 'x'", ['4']],
// --- NOTUNKNOWN 取反仍是 UNKNOWN ---
['SELECT id FROM t WHERE NOT (n = 1)', ['2', '4']],
['SELECT id FROM t WHERE NOT (n = 4)', ['1', '2', '4']],
// --- BETWEEN / NOT BETWEEN(本套件的核心回归点)---
['SELECT id FROM t WHERE n BETWEEN 1 AND 2', ['1', '2']],
['SELECT id FROM t WHERE n NOT BETWEEN 1 AND 2', ['4']],
['SELECT id FROM t WHERE s BETWEEN \'a\' AND \'z\'', ['1', '3', '4']],
["SELECT id FROM t WHERE s NOT BETWEEN 'a' AND 'z'", []],
// --- 字段级 $or / $not 与 where 级逻辑组合的一致性 ---
['SELECT id FROM t WHERE n > 1 OR n < 0', ['2', '4']],
['SELECT id FROM t WHERE n > 0 AND n < 3', ['1', '2']],
["SELECT id FROM t WHERE s = 'x' OR n IS NULL", ['1', '3']],
["SELECT id FROM t WHERE s = 'x' AND n IS NULL", ['3']],
["SELECT id FROM t WHERE s != 'x'", ['4']],
// --- 裸列作为布尔条件(字段存在性)不应因 NULL 崩溃 ---
['SELECT id FROM t WHERE n >= 3', ['4']],
['SELECT id FROM t WHERE n <= 1', ['1']],
];
describe('v0.8.0 SQL 三值逻辑(PB-2 / A15', () => {
for (const engine of ENGINES) {
describe(`${engine.label} 引擎`, () => {
let db: MetonaSqlark;
beforeAll(async () => {
db = await MetonaSqlark.create({
name: `sql-three-valued-${engine.label}`,
mode: engine.mode,
...(engine.extra ?? {}),
});
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
n: { type: 'number' },
s: { type: 'string' },
});
await db.query(
"INSERT INTO t (id, n, s) VALUES ('1', 1, 'x'), ('2', 2, NULL), ('3', NULL, 'x'), ('4', 3, 'y')",
);
});
afterAll(async () => {
await db.close();
});
it.each(MATRIX)('%s → %j', async (sql, expected) => {
const rows = rowsOf<Record<string, unknown>>(await db.query(sql));
const got = rows.map((row) => row.id as string).sort();
expect(got).toEqual([...expected].sort());
});
it('UPDATE / DELETE 使用同一套三值语义', async () => {
// 独立表,避免污染上面的只读矩阵
await db.defineTable('u', {
id: { type: 'string', primaryKey: true },
n: { type: 'number' },
});
await db.query("INSERT INTO u (id, n) VALUES ('a', 1), ('b', NULL), ('c', 3)");
// `n > 1` 只命中 cNULL → UNKNOWN
await db.query("UPDATE u SET n = 9 WHERE n > 1");
const afterUpdate = rowsOf<Record<string, unknown>>(await db.query('SELECT id, n FROM u ORDER BY id'));
expect(afterUpdate.map((r) => [r.id, r.n])).toEqual([
['a', 1],
['b', null],
['c', 9],
]);
// `n = NULL` 匹配 0 行(不能把 NULL 行删掉)
await db.query('DELETE FROM u WHERE n = NULL');
expect(rowsOf(await db.query('SELECT id FROM u ORDER BY id'))).toHaveLength(3);
// `n IS NULL` 命中 b
await db.query('DELETE FROM u WHERE n IS NULL');
const survivors = rowsOf<Record<string, unknown>>(await db.query('SELECT id FROM u ORDER BY id'));
expect(survivors.map((r) => r.id)).toEqual(['a', 'c']);
});
});
}
});