Files
MetonaSqlark/tests/v080-sql-three-valued.test.ts
thzxx 50b1864145 chore(docs): 删除 v0.7.4 审计与 v0.7.5 计划共 4 个 md 文档
删除:
- 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 的详细表格(门禁逐条验收、交付物清单、未修复项表)。
2026-09-15 17:17:24 +08:00

141 lines
5.4 KiB
TypeScript
Raw Permalink 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
* ============================================================================
* 本套件锁定 v0.8.0 审计根因 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']);
});
});
}
});