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 零错误。
This commit is contained in:
@@ -208,7 +208,7 @@ describe('v0.6.2 — Aria 二级索引范围查询边界', () => {
|
||||
});
|
||||
|
||||
describe('v0.6.2 — Aria 索引列 IS NULL', () => {
|
||||
it('引擎层 $eq: null 返回 null 行', async () => {
|
||||
it('引擎层 $isNull 命中 null 行', async () => {
|
||||
const eng = new AriaEngine({ storageBackend: 'memory' });
|
||||
await eng.open(uniqueDB(), 1);
|
||||
await eng.createTable(createSchema('t', {
|
||||
@@ -216,12 +216,28 @@ describe('v0.6.2 — Aria 索引列 IS NULL', () => {
|
||||
email: { type: 'string', index: true },
|
||||
}));
|
||||
await eng.insert('t', [{ id: '1', email: 'a@b.c' }, { id: '2', email: null }, { id: '3', email: 'd@e.f' }]);
|
||||
const rows = await eng.find('t', { table: 't', where: { email: { $eq: null } } });
|
||||
// v0.8.0(A15):此断言此前写作 `{ $eq: null }` 并期望命中 NULL 行 —— 那是
|
||||
// 错误的 SQL 语义(`= NULL` 恒为 UNKNOWN)。索引列 IS NULL 的**目的**
|
||||
//("NULL 行不能被索引漏掉")不变,改为用正确的谓词表达。
|
||||
const rows = await eng.find('t', { table: 't', where: { email: { $isNull: true } } });
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].id).toBe('2');
|
||||
await eng.close();
|
||||
});
|
||||
|
||||
it('引擎层 $eq: null 不再命中 null 行(SQL 标准)', async () => {
|
||||
const eng = new AriaEngine({ storageBackend: 'memory' });
|
||||
await eng.open(uniqueDB(), 1);
|
||||
await eng.createTable(createSchema('t', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
email: { type: 'string', index: true },
|
||||
}));
|
||||
await eng.insert('t', [{ id: '1', email: 'a@b.c' }, { id: '2', email: null }]);
|
||||
const rows = await eng.find('t', { table: 't', where: { email: { $eq: null } } });
|
||||
expect(rows).toHaveLength(0);
|
||||
await eng.close();
|
||||
});
|
||||
|
||||
it('SQL 层 IS NULL / IS NOT NULL 正确', async () => {
|
||||
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' });
|
||||
await db.init();
|
||||
@@ -239,7 +255,7 @@ describe('v0.6.2 — Aria 索引列 IS NULL', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('IN 列表含 null 不走索引(不漏 null 行)', async () => {
|
||||
it('IN 列表含 null 不走索引(不漏可匹配行)', async () => {
|
||||
const eng = new AriaEngine({ storageBackend: 'memory' });
|
||||
await eng.open(uniqueDB(), 1);
|
||||
await eng.createTable(createSchema('t', {
|
||||
@@ -247,8 +263,25 @@ describe('v0.6.2 — Aria 索引列 IS NULL', () => {
|
||||
email: { type: 'string', index: true },
|
||||
}));
|
||||
await eng.insert('t', [{ id: '1', email: 'a@b.c' }, { id: '2', email: null }]);
|
||||
// v0.8.0(A15):`x IN (NULL, 'a@b.c')` 等价于 `x = NULL OR x = 'a@b.c'`:
|
||||
// `null = NULL` 是 UNKNOWN(不成立),所以只有 id=1 命中。
|
||||
// 此前的期望值 2 正是"NULL 与 NULL 相等"这一错误语义的产物。
|
||||
const rows = await eng.find('t', { table: 't', where: { email: { $in: [null, 'a@b.c'] } } });
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows.map((r) => r.id)).toEqual(['1']);
|
||||
await eng.close();
|
||||
});
|
||||
|
||||
it('IN 列表含 null 时不可匹配的行仍不返回(UNKNOWN 不保留)', async () => {
|
||||
const eng = new AriaEngine({ storageBackend: 'memory' });
|
||||
await eng.open(uniqueDB(), 1);
|
||||
await eng.createTable(createSchema('t', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
email: { type: 'string', index: true },
|
||||
}));
|
||||
await eng.insert('t', [{ id: '1', email: 'a@b.c' }, { id: '2', email: null }, { id: '3', email: 'z@z.z' }]);
|
||||
// id=3 既不在列表里,列表又含 NULL → 结果 UNKNOWN → 不保留
|
||||
const rows = await eng.find('t', { table: 't', where: { email: { $in: [null, 'a@b.c'] } } });
|
||||
expect(rows.map((r) => r.id)).toEqual(['1']);
|
||||
await eng.close();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user