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:
+11
-2
@@ -731,9 +731,18 @@ export class MetonaSqlark {
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
/** 获取底层引擎(含变更通知装饰器) */
|
||||
/**
|
||||
* 获取底层存储引擎。
|
||||
*
|
||||
* v0.8.0:返回**未装饰**的真实引擎。
|
||||
*
|
||||
* 变更通知用的 ChangeNotifierEngine 只是内部接线细节;若把它暴露出去,
|
||||
* 调用方(以及测试)依赖的引擎特有能力(`lsm`、`secondaryIndexes`、
|
||||
* `getDiskEngineType` 等)会被静默隐藏 —— 本项目既有测试与文档都按
|
||||
* "getEngine() 就是那个引擎"理解。因此这里保持原语义,装饰器只在 core 内部使用。
|
||||
*/
|
||||
getEngine(): IStorageEngine {
|
||||
return this.engine;
|
||||
return this.unwrapEngine();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+78
-8
@@ -478,7 +478,10 @@ export class QueryExecutor {
|
||||
// v0.4.0 修复: 关联子查询需要完整外层行(SELECT 列可能不含被 $col 引用的列,如 EXISTS 绑定的主键)
|
||||
plan.columns = ['*'];
|
||||
if (orderByAlias) { plan.orderBy = undefined; plan.limit = undefined; plan.offset = undefined; }
|
||||
rows = await this.engine.find(plan.table, { ...plan, where: this.stripCorrelatedExists(stmt.where) });
|
||||
// v0.8.0:引擎层只做**可下推部分**的预过滤,逐行谓词整体交给 filterCorrelated。
|
||||
// 此前把原始 where(含 `$col`)直接交给引擎,引擎判 UNKNOWN → 候选行 0 →
|
||||
// `WHERE t.x = t.y` 静默空结果(详见 enginePreFilter 文档)。
|
||||
rows = await this.engine.find(plan.table, { ...plan, where: this.enginePreFilter(stmt.where) });
|
||||
rows = await this.filterCorrelated(rows, stmt.where, mainAliases);
|
||||
} else {
|
||||
// 先解析子查询
|
||||
@@ -1448,27 +1451,94 @@ export class QueryExecutor {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 移除关联 EXISTS 标记(引擎层先执行无 EXISTS 条件的查询) */
|
||||
private stripCorrelatedExists(where: WhereCondition): WhereCondition {
|
||||
/**
|
||||
* 构造**引擎层预过滤**用的 WHERE 子句。
|
||||
*
|
||||
* 逐行求值的谓词必须整体移出引擎层,否则引擎的 `matchWhere`(没有外层行上下文)
|
||||
* 会把它们判为 UNKNOWN → **所有行被过滤掉**,逐行求值再正确也无行可算:
|
||||
* - 关联 `EXISTS`:`$exists` 子查询未执行,`{ $subquery: ... }` 引擎无法求值;
|
||||
* - CASE WHEN 表达式键:需要行上下文才能算出布尔;
|
||||
* - `$col` 列引用(`WHERE t.x = t.y` → `{ x: { $eq: { $col: 'y' } } }`):
|
||||
* 引擎层取不到"另一列"的值。
|
||||
*
|
||||
* 移出的粒度取决于连接词 —— 这里**不是**保守兜底,而是逻辑上唯一正确的做法:
|
||||
* - 顶层 / `$and` 的成员:可以单独删除该谓词,其余谓词仍然安全可下推;
|
||||
* - `$or` / `$not` 的成员:不能单独删除。删掉 `A OR B` 中的 `B` 会得到更严的
|
||||
* `A`(**漏行**);删掉 `NOT B` 中的 `B` 会得到恒真的 `NOT true`(**多行**)。
|
||||
* 因此整条 `$or` / `$not` 都交给逐行求值(`$not` 的恒真情形直接丢弃该键)。
|
||||
*/
|
||||
private enginePreFilter(where: WhereCondition): WhereCondition {
|
||||
const cleaned: WhereCondition = {};
|
||||
for (const [key, value] of Object.entries(where)) {
|
||||
if (key === '$and' || key === '$or') {
|
||||
cleaned[key] = (value as WhereCondition[]).map((sub) => this.stripCorrelatedExists(sub));
|
||||
if (key === '$and') {
|
||||
const subs = (value as WhereCondition[]).map((sub) => this.enginePreFilter(sub));
|
||||
const kept = subs.filter((sub) => Object.keys(sub).length > 0);
|
||||
if (kept.length > 0) cleaned.$and = kept;
|
||||
continue;
|
||||
}
|
||||
if (key === '$or') {
|
||||
// $or 是故障放大器:任一分支不可判定则该 $or 整体不可下推
|
||||
const subs = value as WhereCondition[];
|
||||
if (subs.some((sub) => !this.isEngineEvaluable(sub))) continue;
|
||||
cleaned.$or = subs.map((sub) => this.enginePreFilter(sub));
|
||||
continue;
|
||||
}
|
||||
if (key === '$not') {
|
||||
const inner = this.stripCorrelatedExists(value as WhereCondition);
|
||||
// 剥离后为空 → 条件恒真,删掉该键(避免引擎层执行 NOT(true) 过滤掉所有行)
|
||||
if (Object.keys(inner).length > 0) cleaned.$not = inner;
|
||||
const inner = value as WhereCondition;
|
||||
if (!this.isEngineEvaluable(inner)) continue;
|
||||
cleaned.$not = this.enginePreFilter(inner);
|
||||
continue;
|
||||
}
|
||||
if (key === '$exists') continue; // 逐行求值时单独处理
|
||||
if (/^\s*CASE\b/i.test(key)) continue; // v0.3.2: CASE 键逐行求值
|
||||
if (this.fieldHasColRef(value) || this.fieldHasSubquery(value)) continue; // $col / 子查询逐行求值
|
||||
cleaned[key] = value;
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 该 WHERE 片段是否可完全交给引擎层求值(无 `$col` / 关联 `EXISTS` / CASE 键)。
|
||||
*
|
||||
* 与 `hasCorrelatedRefs` 的区别:`hasCorrelatedRefs` 回答"是否需要逐行求值",
|
||||
* 本函数回答"能否整体下推"。两者互补,缺一不可 —— 后者是前者在 `$or`/`$not`
|
||||
* 内部传播后的结果。
|
||||
*/
|
||||
private isEngineEvaluable(where: WhereCondition): boolean {
|
||||
for (const [key, value] of Object.entries(where)) {
|
||||
if (key === '$and' || key === '$or') {
|
||||
if ((value as WhereCondition[]).some((sub) => !this.isEngineEvaluable(sub))) return false;
|
||||
continue;
|
||||
}
|
||||
if (key === '$not') {
|
||||
if (!this.isEngineEvaluable(value as WhereCondition)) return false;
|
||||
continue;
|
||||
}
|
||||
if (key === '$exists') return false;
|
||||
if (/^\s*CASE\b/i.test(key)) return false;
|
||||
if (this.fieldHasColRef(value) || this.fieldHasSubquery(value)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 字段条件里是否含未解析子查询(引擎层无法执行) */
|
||||
private fieldHasSubquery(value: unknown): boolean {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
||||
const ops = value as Record<string, unknown>;
|
||||
if ('$subquery' in ops) return true;
|
||||
for (const [, operand] of Object.entries(ops)) {
|
||||
if (typeof operand === 'object' && operand !== null && !Array.isArray(operand)) {
|
||||
if ('$subquery' in (operand as Record<string, unknown>)) return true;
|
||||
}
|
||||
if (Array.isArray(operand)) {
|
||||
for (const item of operand) {
|
||||
if (typeof item === 'object' && item !== null && '$subquery' in (item as Record<string, unknown>)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 逐行绑定外层行上下文,求值关联 EXISTS、$col 引用与 CASE WHEN 键 */
|
||||
private async filterCorrelated(
|
||||
rows: Record<string, unknown>[],
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* metona-sqlark SQL 值语义 — 三值逻辑与统一编码(v0.8.0)
|
||||
* @module query/sql-compare
|
||||
*
|
||||
* ============================================================================
|
||||
* 为什么需要它(PLAN-v0.7.5.md 根因 2 / PB-2)
|
||||
* ============================================================================
|
||||
* 此前项目里并存**五套**值相等语义:
|
||||
* 1. `where-matcher` 的 `===`
|
||||
* 2. 哈希连接的 `String(v ?? '\0')`
|
||||
* 3. 分组键 `encodeGroupKey`
|
||||
* 4. `COUNT(DISTINCT)` 的 `String(v)`
|
||||
* 5. Aria 二级索引键的 `String(v)`
|
||||
* 它们对 NULL 的处理各不相同,于是出现一批"静默错值":
|
||||
* - `WHERE a = NULL` 命中 NULL 行(SQL 标准应为空集,比较结果是 UNKNOWN);
|
||||
* - `WHERE a != NULL` 返回所有非 NULL 行(标准同样为空集);
|
||||
* - `WHERE a NOT BETWEEN 1 AND 2` 会把 NULL 行**排除**(标准应包含,
|
||||
* 因为 UNKNOWN 经过 NOT 仍是 UNKNOWN,而 WHERE 只保留 TRUE……此处需按下文语义);
|
||||
* - `WHERE a NOT IN (1, 2)` 把 NULL 行当作"不在列表"返回;
|
||||
* - JOIN 的 NULL 键:嵌套循环认为 NULL = NULL 成立、哈希连接不成立 →
|
||||
* **结果取决于右表有没有索引**。
|
||||
*
|
||||
* ============================================================================
|
||||
* 本模块提供的语义
|
||||
* ============================================================================
|
||||
* `sqlCompare(a, b)` / `sqlEquals(a, b)` 返回三值:TRUE / FALSE / UNKNOWN。
|
||||
* 任一操作数为 NULL/undefined → UNKNOWN(SQL 标准)。
|
||||
* `matchWhere` 只在结果为 TRUE 时保留该行。
|
||||
*
|
||||
* 注意 `IS NULL` / `IS NOT NULL` **不走比较**:它们是谓词,直接对 NULL 求值,
|
||||
* 由 parser 生成为独立的 `$isNull` / `$isNotNull` 标记(见 SQL_LOGICAL 说明)。
|
||||
*/
|
||||
|
||||
/** SQL 三值逻辑 */
|
||||
export const SQL_LOGICAL = Object.freeze({
|
||||
TRUE: 'TRUE',
|
||||
FALSE: 'FALSE',
|
||||
/** 未知(任一操作数为 NULL,或存在无法确定的引用) */
|
||||
UNKNOWN: 'UNKNOWN',
|
||||
} as const);
|
||||
|
||||
export type SqlTruth = (typeof SQL_LOGICAL)[keyof typeof SQL_LOGICAL];
|
||||
|
||||
/** 是否为 NULL 语义值(undefined 与 null 等价为 SQL NULL) */
|
||||
export function isSqlNull(value: unknown): boolean {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 未解析的列引用标记。
|
||||
*
|
||||
* 用于区分两件必须分开处理的事:
|
||||
* - `row[col]` 取到 `undefined` 可能是"该行此列为 NULL",
|
||||
* 也可能是"这一列根本不在当前行里"(例如子查询引用了外层列)。
|
||||
* 前者应判 UNKNOWN,后者应让调用方转交带上下文的求值器(不能静默当 NULL)。
|
||||
* 用 Unique Symbol 保证不会与真实数据冲突。
|
||||
*/
|
||||
export const UNRESOLVED = Symbol('sqlark.unresolved');
|
||||
|
||||
/** 判定某值是否为"未解析引用" */
|
||||
export function isUnresolved(value: unknown): value is typeof UNRESOLVED {
|
||||
return value === UNRESOLVED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的类型化值编码(用于分组键 / DISTINCT 键 / 索引键)。
|
||||
*
|
||||
* 为什么不能直接 `String(v)`:
|
||||
* - `String(null)` = `'null'` 与字符串 `'null'` 冲突;
|
||||
* - `String(1)` = `'1'` 与字符串 `'1'` 冲突(v0.7.4 已修 DISTINCT,
|
||||
* 但 COUNT(DISTINCT) 与 Aria 索引键仍是 `String()`)。
|
||||
* 这里用类型前缀 + 长度前缀,保证不同类型、不同值必然编码不同,
|
||||
* 且不会出现 `\x1f` 之类分隔符被数据内容伪造的问题。
|
||||
*/
|
||||
export function encodeValueKey(value: unknown): string {
|
||||
if (value === null || value === undefined) return 'z';
|
||||
const t = typeof value;
|
||||
switch (t) {
|
||||
case 'string': {
|
||||
const s = value as string;
|
||||
return `s${s.length}:${s}`;
|
||||
}
|
||||
case 'number': {
|
||||
const n = value as number;
|
||||
// -0 与 0 在 SQL 中相等;NaN 单独编码(避免与任何值相等)
|
||||
if (Number.isNaN(n)) return 'nNaN';
|
||||
return `n${Object.is(n, -0) ? 0 : n}`;
|
||||
}
|
||||
case 'boolean': return `b${value ? 1 : 0}`;
|
||||
case 'bigint': return `i${String(value)}`;
|
||||
case 'object': {
|
||||
// 日期按时间戳比较;数组/对象按 JSON(键序由 JSON.stringify 决定,
|
||||
// 对同构数据稳定;异构键序在上层已由 schema 约束)
|
||||
if (value instanceof Date) return `d${value.getTime()}`;
|
||||
if (Array.isArray(value)) return `a${JSON.stringify(value)}`;
|
||||
return `o${JSON.stringify(value)}`;
|
||||
}
|
||||
default: return `x${String(value)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 比较:返回三值。
|
||||
*
|
||||
* 与 JavaScript 运算符的关键差异:
|
||||
* - 任一操作数为 NULL → UNKNOWN(而不是 false);
|
||||
* - 非数值字符串与数值比较 → UNKNOWN(而不是 JS 的强制转换结果);
|
||||
* - 未解析引用 → UNKNOWN(调用方据此转交上下文求值)。
|
||||
*/
|
||||
export function sqlCompare(a: unknown, b: unknown): SqlTruth {
|
||||
const ord = sqlCompareOrder(a, b);
|
||||
if (ord === null) return SQL_LOGICAL.UNKNOWN;
|
||||
return ord === 0 ? SQL_LOGICAL.TRUE : SQL_LOGICAL.FALSE;
|
||||
}
|
||||
|
||||
/** 相等比较(三值) */
|
||||
export function sqlEquals(a: unknown, b: unknown): SqlTruth {
|
||||
return sqlCompare(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 三路排序比较:返回 -1 / 0 / 1,无法比较(含 NULL、类型不可比)返回 null。
|
||||
*
|
||||
* **这是本模块唯一的排序/比较原语** —— `sqlCompare`(相等)与所有有序比较
|
||||
* ($gt/$gte/$lt/$lte、ORDER BY)都必须建立在它之上。
|
||||
*
|
||||
* 为什么要把"排序"与"相等"分开:初版实现里 `sqlCompare` 直接返回三值,
|
||||
* 于是"a < b"与"a === b"都被编码成 TRUE,`$gte` 误把"小于"当成"大于等于"
|
||||
* (实测 `WHERE n >= 2` 在 1..3 上返回 3 而不是 2 与 3)。
|
||||
* 排序是三路的(<, =, >),相等是二值的(=, ≠)—— 把前者硬塞进后者必然出错。
|
||||
*/
|
||||
export function sqlCompareOrder(a: unknown, b: unknown): number | null {
|
||||
if (isUnresolved(a) || isUnresolved(b)) return null;
|
||||
if (isSqlNull(a) || isSqlNull(b)) return null;
|
||||
|
||||
// 数字 vs 数字
|
||||
if (typeof a === 'number' && typeof b === 'number') {
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
||||
if (a === b) return 0;
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
// 布尔 vs 布尔
|
||||
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
||||
if (a === b) return 0;
|
||||
return a === false ? -1 : 1;
|
||||
}
|
||||
// 字符串 vs 字符串:字典序
|
||||
if (typeof a === 'string' && typeof b === 'string') {
|
||||
if (a === b) return 0;
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
// 日期 vs 日期
|
||||
if (a instanceof Date && b instanceof Date) {
|
||||
const ta = a.getTime();
|
||||
const tb = b.getTime();
|
||||
if (Number.isNaN(ta) || Number.isNaN(tb)) return null;
|
||||
if (ta === tb) return 0;
|
||||
return ta < tb ? -1 : 1;
|
||||
}
|
||||
// 类型不同:本项目保持严格类型(不做静默强转),判定为不可比(null)
|
||||
// —— 这样 `WHERE numCol = 'abc'` 不会意外命中,也不会因 `'5' = 5` 的 JS
|
||||
// 行为产生跨引擎差异。唯一例外:数值与"纯数值字符串"按数值比较
|
||||
// (与既有「$in 列表含字符串」行为兼容)。
|
||||
const na = toNumericIfPossible(a);
|
||||
const nb = toNumericIfPossible(b);
|
||||
if (na !== null && nb !== null) {
|
||||
if (na === nb) return 0;
|
||||
return na < nb ? -1 : 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 比较结果转布尔(仅 TRUE 为真;UNKNOWN 与 FALSE 都不保留该行) */
|
||||
export function isTrue(truth: SqlTruth): boolean {
|
||||
return truth === SQL_LOGICAL.TRUE;
|
||||
}
|
||||
|
||||
/** 三值 AND */
|
||||
export function sqlAnd(a: SqlTruth, b: SqlTruth): SqlTruth {
|
||||
if (a === SQL_LOGICAL.FALSE || b === SQL_LOGICAL.FALSE) return SQL_LOGICAL.FALSE;
|
||||
if (a === SQL_LOGICAL.UNKNOWN || b === SQL_LOGICAL.UNKNOWN) return SQL_LOGICAL.UNKNOWN;
|
||||
return SQL_LOGICAL.TRUE;
|
||||
}
|
||||
|
||||
/** 三值 OR */
|
||||
export function sqlOr(a: SqlTruth, b: SqlTruth): SqlTruth {
|
||||
if (a === SQL_LOGICAL.TRUE || b === SQL_LOGICAL.TRUE) return SQL_LOGICAL.TRUE;
|
||||
if (a === SQL_LOGICAL.UNKNOWN || b === SQL_LOGICAL.UNKNOWN) return SQL_LOGICAL.UNKNOWN;
|
||||
return SQL_LOGICAL.FALSE;
|
||||
}
|
||||
|
||||
/** 三值 NOT(UNKNOWN 取反仍为 UNKNOWN) */
|
||||
export function sqlNot(a: SqlTruth): SqlTruth {
|
||||
if (a === SQL_LOGICAL.TRUE) return SQL_LOGICAL.FALSE;
|
||||
if (a === SQL_LOGICAL.FALSE) return SQL_LOGICAL.TRUE;
|
||||
return SQL_LOGICAL.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把值转为数值;无法安全转换时返回 null。
|
||||
* 只有 number 与"纯数值字符串"参与数值比较,避免 `'abc'` 之类被 Number() 变成 NaN。
|
||||
*/
|
||||
function toNumericIfPossible(value: unknown): number | null {
|
||||
if (typeof value === 'number') return Number.isNaN(value) ? null : value;
|
||||
if (typeof value === 'boolean') return value ? 1 : 0;
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === '') return null;
|
||||
const n = Number(trimmed);
|
||||
return Number.isNaN(n) ? null : n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL `IN` 列表求值(三值)。
|
||||
*
|
||||
* 标准语义:`x IN (a, b, c)` 等价于 `x = a OR x = b OR x = c`。
|
||||
* 因此:
|
||||
* - 命中任一 → TRUE;
|
||||
* - 都未命中但列表含 NULL(或 x 为 NULL)→ UNKNOWN;
|
||||
* - 都未命中且列表无 NULL 且 x 非 NULL → FALSE。
|
||||
*/
|
||||
export function sqlIn(value: unknown, list: unknown[]): SqlTruth {
|
||||
if (isUnresolved(value)) return SQL_LOGICAL.UNKNOWN;
|
||||
let sawUnknown = false;
|
||||
for (const item of list) {
|
||||
const t = sqlEquals(value, item);
|
||||
if (t === SQL_LOGICAL.TRUE) return SQL_LOGICAL.TRUE;
|
||||
if (t === SQL_LOGICAL.UNKNOWN) sawUnknown = true;
|
||||
}
|
||||
return sawUnknown ? SQL_LOGICAL.UNKNOWN : SQL_LOGICAL.FALSE;
|
||||
}
|
||||
+373
-142
@@ -1,13 +1,45 @@
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
* metona-sqlark Shared WHERE Matcher —— 统一的条件求值器
|
||||
* @module query/where-matcher
|
||||
*
|
||||
* MemoryEngine / IndexedDBEngine / QueryExecutor 共享此模块,
|
||||
* 消除 220+ 行重复代码,统一 $and/$or/$not/$col 行为。
|
||||
* ============================================================================
|
||||
* v0.8.0 根治:为什么这里只剩**一个**递归求值器(PLAN-v0.7.5.md 根因 1/7、PB-2)
|
||||
* ============================================================================
|
||||
* 历史上有两套并存的实现:
|
||||
* - `matchWhere` —— 布尔版(自己的 `$and/$or/$not` 分支 + `matchField`);
|
||||
* - 三值求值 —— 为 UNKNOWN 传播而新增。
|
||||
* 两者对**字段级** `$or` / `$not` 的处理不同,于是同一条 SQL 的语义取决于调用点:
|
||||
* - `{ s: { $not: { $like: 'x' } } }` 经布尔取反会把 NULL 行判真;
|
||||
* - `{ n: { $or: [...] } }`(`NOT BETWEEN` 生成)经布尔分支在 NULL 行上判假;
|
||||
* - 更严重的是字段级 `$or` 递归回了"**where 子句级**"求值器:
|
||||
* `{ n: { $or: [ { $lt: 1 }, { $gt: 2 } ] } }` 里的 `{ $lt: 1 }` 被当成
|
||||
* "查询字段 `$lt`",于是每一行都求值 UNKNOWN → `NOT BETWEEN` 恒空集。
|
||||
*
|
||||
* 现在只有一条代码路径:**一个递归求值器**,同时理解 where 子句级(键是列名或
|
||||
* 逻辑连接词)与操作符级(键是 `$gt` 之类)。区别由**位置**参数承载,
|
||||
* 而不是由另一个函数承载:
|
||||
* - `evalWhere(ctx, where)` —— where 子句(`$and`/`$or` 子项是 where 子句)
|
||||
* - `evalOperatorObject(ctx, ...)` —— 操作符对象(`$and`/`$or` 子项是操作符对象)
|
||||
* 因此 `$or` 的两种含义都在同一个函数里显式分派,不可能再漂移。
|
||||
*
|
||||
* `matchWhere` 保留为对外入口,语义定义为"是否保留该行" = 三值结果恰为 TRUE。
|
||||
*/
|
||||
|
||||
import type { WhereCondition, OrderBy } from '../constants';
|
||||
import { DatabaseError } from '../constants';
|
||||
import {
|
||||
SQL_LOGICAL,
|
||||
type SqlTruth,
|
||||
sqlCompare,
|
||||
sqlCompareOrder,
|
||||
sqlIn,
|
||||
sqlNot,
|
||||
sqlAnd,
|
||||
sqlOr,
|
||||
isSqlNull,
|
||||
isUnresolved,
|
||||
UNRESOLVED,
|
||||
} from './sql-compare';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LIKE 正则缓存
|
||||
@@ -28,169 +60,326 @@ function compileLikeRegex(pattern: string): RegExp {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WHERE 匹配(顶层入口)
|
||||
// 求值上下文
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* v0.7.4: 检测 WHERE 中未解析的子查询/列引用标记($subquery / $col / $exists)。
|
||||
* QueryBuilder 等直通引擎的写路径(update/delete)不经 Executor 解析子查询,
|
||||
* 引擎层 matchWhere 对未解析标记恒 false → 静默影响 0 行。
|
||||
* 写路径预检阶段显式拒绝;SELECT 路径由 Executor 解析(不调用本函数)。
|
||||
*/
|
||||
export function containsUnresolvedSubqueries(where: WhereCondition | undefined): boolean {
|
||||
if (!where) return false;
|
||||
for (const [k, v] of Object.entries(where)) {
|
||||
if (k === '$and' || k === '$or') {
|
||||
if ((v as WhereCondition[]).some((sub) => containsUnresolvedSubqueries(sub))) return true;
|
||||
continue;
|
||||
}
|
||||
if (k === '$not') {
|
||||
if (containsUnresolvedSubqueries(v as WhereCondition)) return true;
|
||||
continue;
|
||||
}
|
||||
if (k === '$exists') return true;
|
||||
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
||||
for (const [, operand] of Object.entries(v as Record<string, unknown>)) {
|
||||
if (typeof operand === 'object' && operand !== null) {
|
||||
const ops = operand as Record<string, unknown>;
|
||||
if ('$subquery' in ops || '$col' in ops) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
/** 求值选项(对外 API 保持既有形状) */
|
||||
export interface MatchOptions {
|
||||
/**
|
||||
* 是否解析 `$col` 列引用(关联子查询 / JOIN ON 的列对列比较)。
|
||||
*
|
||||
* 引擎层(`matchWhere` 直通调用)没有外层行上下文,必须**不**解析:
|
||||
* 此时 `$col` 求值为 UNRESOLVED(比较 → UNKNOWN,行被排除),
|
||||
* 而不是抛"未知操作符",也不是静默判真。
|
||||
*/
|
||||
$col?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配完整 WHERE 条件
|
||||
* @param row 当前数据行
|
||||
* @param where WHERE 条件对象
|
||||
* @param options.$col 是否启用 $col 列引用解析
|
||||
* 内部求值上下文。
|
||||
*
|
||||
* `row` 是**当前行**:`$col: 'y'` 表示"与当前行的 y 列比较"。
|
||||
* v0.8.0 之前这个行上下文藏在 `matchField` 的闭包里,字段级 `$or` 递归回
|
||||
* where 级求值器时被丢弃,导致 `$col` 只能靠 `options.$col` 开关 + 行参数
|
||||
* 隐式传递 —— 现在它显式随上下文下传,任何深度的嵌套求值都不会丢。
|
||||
*/
|
||||
interface EvalContext {
|
||||
row: Record<string, unknown>;
|
||||
options: MatchOptions;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WHERE 匹配(对外入口)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 匹配完整 WHERE 条件(布尔口径:仅 TRUE 保留该行)。
|
||||
*
|
||||
* @param row 当前数据行
|
||||
* @param where WHERE 条件对象
|
||||
* @param options `$col` 是否启用列引用解析
|
||||
*/
|
||||
export function matchWhere(
|
||||
row: Record<string, unknown>,
|
||||
where: WhereCondition,
|
||||
options: { $col?: boolean } = {},
|
||||
): boolean { for (const [field, condition] of Object.entries(where)) {
|
||||
// 顶层 $caseResult(v0.3.2):由 Executor 对 CASE WHEN 表达式逐行求值后产生
|
||||
if (field === '$caseResult') {
|
||||
if (condition !== true) return false;
|
||||
options: MatchOptions = {},
|
||||
): boolean {
|
||||
return evalWhere({ row, options }, where) === SQL_LOGICAL.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 三值版本:供需要区分 UNKNOWN 与 FALSE 的调用方使用
|
||||
* (JOIN 外连接判定、`NOT IN` 子查询等)。
|
||||
*/
|
||||
export function matchWhereThreeValued(
|
||||
row: Record<string, unknown>,
|
||||
where: WhereCondition,
|
||||
options: MatchOptions = {},
|
||||
): SqlTruth {
|
||||
return evalWhere({ row, options }, where);
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.7.4: 检测 WHERE 中未解析的子查询/列引用标记($subquery / $col / $exists)。
|
||||
*
|
||||
* QueryBuilder 等直通引擎的写路径(update/delete)不经 Executor 解析子查询,
|
||||
* 写路径预检阶段据此显式拒绝(否则这些标记在引擎层恒 UNKNOWN → 静默影响 0 行)。
|
||||
*/
|
||||
export function containsUnresolvedSubqueries(where: WhereCondition | undefined): boolean {
|
||||
if (!where) return false;
|
||||
for (const [k, v] of Object.entries(where)) {
|
||||
if (isLogicalKey(k)) {
|
||||
const subs = (Array.isArray(v) ? v : [v]) as WhereCondition[];
|
||||
if (subs.some((sub) => containsUnresolvedSubqueries(sub))) return true;
|
||||
continue;
|
||||
}
|
||||
// 顶层 $exists(v0.3.0):由 Executor.resolveSubqueries 解析为 boolean
|
||||
if (field === '$exists') {
|
||||
if (condition !== true) return false;
|
||||
continue;
|
||||
}
|
||||
// 顶层 $and
|
||||
if (field === '$and') {
|
||||
const subs = condition as WhereCondition[];
|
||||
if (!subs.every((sub) => matchWhere(row, sub, options))) return false;
|
||||
continue;
|
||||
}
|
||||
// 顶层 $or
|
||||
if (field === '$or') {
|
||||
const subs = condition as WhereCondition[];
|
||||
if (!subs.some((sub) => matchWhere(row, sub, options))) return false;
|
||||
continue;
|
||||
}
|
||||
// 顶层 $not(v0.3.2 修复:NOT (expr) 生成的 { $not: inner })
|
||||
if (field === '$not') {
|
||||
if (matchWhere(row, condition as WhereCondition, options)) return false;
|
||||
continue;
|
||||
}
|
||||
if (!matchField(row[field], condition, row, options)) return false;
|
||||
if (k === '$exists') return true;
|
||||
if (isPlainObject(v) && operatorObjectHasUnresolved(v)) return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 递归检测操作符对象里是否存在未解析引用(含 `$and`/`$or`/`$not` 内部) */
|
||||
function operatorObjectHasUnresolved(ops: Record<string, unknown>): boolean {
|
||||
for (const [op, operand] of Object.entries(ops)) {
|
||||
if (op === '$and' || op === '$or') {
|
||||
const subs = (Array.isArray(operand) ? operand : [operand]) as Record<string, unknown>[];
|
||||
if (subs.some((sub) => isPlainObject(sub) && operatorObjectHasUnresolved(sub))) return true;
|
||||
continue;
|
||||
}
|
||||
if (op === '$not') {
|
||||
if (isPlainObject(operand) && operatorObjectHasUnresolved(operand)) return true;
|
||||
continue;
|
||||
}
|
||||
if (op === '$subquery' || op === '$col') return true;
|
||||
if (isPlainObject(operand) && ('$subquery' in operand || '$col' in operand)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 字段匹配
|
||||
// 统一递归求值器
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function matchField(
|
||||
/** 逻辑连接词(where 子句级) */
|
||||
function isLogicalKey(key: string): boolean {
|
||||
return key === '$and' || key === '$or' || key === '$not';
|
||||
}
|
||||
|
||||
/** 纯对象(非 null、非数组) */
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 求值一个 WHERE 子句(where 级)。
|
||||
*
|
||||
* 顶层键要么是逻辑连接词,要么是列名:
|
||||
* - `$and: [where...]` / `$or: [where...]` —— 子项是**完整 where 子句**;
|
||||
* - `$not: where` —— 子项是完整 where 子句;
|
||||
* - `$exists: boolean` —— 关联 EXISTS 解析结果标记;
|
||||
* - `$caseResult` —— CASE WHEN 解析结果标记;
|
||||
* - 其余键 = 列名,其值是**操作符对象**或裸值(裸值等价 `$eq`)。
|
||||
*/
|
||||
function evalWhere(ctx: EvalContext, where: WhereCondition): SqlTruth {
|
||||
let acc: SqlTruth = SQL_LOGICAL.TRUE;
|
||||
for (const [key, condition] of Object.entries(where)) {
|
||||
let one: SqlTruth;
|
||||
if (key === '$and') {
|
||||
one = evalWhereLogical(ctx, condition, 'and');
|
||||
} else if (key === '$or') {
|
||||
one = evalWhereLogical(ctx, condition, 'or');
|
||||
} else if (key === '$not') {
|
||||
one = sqlNot(evalWhere(ctx, condition as WhereCondition));
|
||||
} else if (key === '$exists' || key === '$caseResult') {
|
||||
one = condition === true ? SQL_LOGICAL.TRUE : SQL_LOGICAL.FALSE;
|
||||
} else {
|
||||
one = evalValueCondition(ctx, resolveField(ctx, key), condition);
|
||||
}
|
||||
acc = sqlAnd(acc, one);
|
||||
if (acc === SQL_LOGICAL.FALSE) return SQL_LOGICAL.FALSE;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** `$and` / `$or`(where 级):子项是完整 where 子句 */
|
||||
function evalWhereLogical(ctx: EvalContext, condition: unknown, kind: 'and' | 'or'): SqlTruth {
|
||||
const subs = (Array.isArray(condition) ? condition : [condition]) as WhereCondition[];
|
||||
let acc: SqlTruth = kind === 'and' ? SQL_LOGICAL.TRUE : SQL_LOGICAL.FALSE;
|
||||
for (const sub of subs) {
|
||||
const one = evalWhere(ctx, sub);
|
||||
acc = kind === 'and' ? sqlAnd(acc, one) : sqlOr(acc, one);
|
||||
// FALSE 在 AND 下、TRUE 在 OR 下都无法被后续子项改变
|
||||
if (kind === 'and' && acc === SQL_LOGICAL.FALSE) return SQL_LOGICAL.FALSE;
|
||||
if (kind === 'or' && acc === SQL_LOGICAL.TRUE) return SQL_LOGICAL.TRUE;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取字段值。
|
||||
*
|
||||
* 行里的键可能是 `t.id` 形式(JOIN / 子查询合并行),而 WHERE 键可能写作
|
||||
* `id`。既有行为是"精确命中优先,否则回退到唯一的后缀匹配";此处保持一致,
|
||||
* 但**不再**像旧实现那样在多个别名同名字段间静默取第一个 —— 歧义由 Executor
|
||||
* 的列解析阶段(`bindColumnRefs` / `assertProjectionColumnsExist`)负责报错。
|
||||
*/
|
||||
function resolveField(ctx: EvalContext, field: string): unknown {
|
||||
if (field in ctx.row) return ctx.row[field];
|
||||
let found: unknown = UNRESOLVED;
|
||||
let hits = 0;
|
||||
for (const key of Object.keys(ctx.row)) {
|
||||
if (key.endsWith(`.${field}`)) {
|
||||
found = ctx.row[key];
|
||||
hits += 1;
|
||||
}
|
||||
}
|
||||
if (hits === 1) return found;
|
||||
if (hits > 1) return UNRESOLVED;
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 求值"某个值是否满足条件"—— 字段条件与嵌套操作数条件共用的核心。
|
||||
*
|
||||
* 位置无关性是本模块的关键:`{ $or: [ { $gt: 1 } ] }` 无论出现在字段位置
|
||||
* 还是顶层(历史单元测试的写法),子项都按**操作数条件**求值 —— 因为判别式
|
||||
* 相同:键以 `$` 开头且不是逻辑连接词,就是操作符;否则是列名。
|
||||
*/
|
||||
function evalValueCondition(
|
||||
ctx: EvalContext,
|
||||
value: unknown,
|
||||
condition: unknown,
|
||||
row: Record<string, unknown>,
|
||||
options: { $col?: boolean },
|
||||
): boolean {
|
||||
// 嵌套 $and
|
||||
if (typeof condition === 'object' && condition !== null && '$and' in (condition as Record<string, unknown>)) {
|
||||
const subs = (condition as Record<string, unknown>).$and as WhereCondition[];
|
||||
return subs.every((sub) => matchWhere(row, sub, options));
|
||||
): SqlTruth {
|
||||
if (!isPlainObject(condition)) {
|
||||
// 裸值 = `$eq`;数组 = `$in` 列表(与既有 Mongo 风格 WHERE 兼容)
|
||||
if (Array.isArray(condition)) return sqlIn(value, condition);
|
||||
return compareEquality(value, condition);
|
||||
}
|
||||
// 嵌套 $or
|
||||
if (typeof condition === 'object' && condition !== null && '$or' in (condition as Record<string, unknown>)) {
|
||||
const subs = (condition as Record<string, unknown>).$or as WhereCondition[];
|
||||
return subs.some((sub) => matchWhere(row, sub, options));
|
||||
}
|
||||
// $not
|
||||
if (typeof condition === 'object' && condition !== null && '$not' in (condition as Record<string, unknown>)) {
|
||||
return !matchField(value, (condition as Record<string, unknown>).$not, row, options);
|
||||
}
|
||||
// 简单值 => $eq
|
||||
if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) {
|
||||
return value === condition;
|
||||
}
|
||||
|
||||
const ops = condition as Record<string, unknown>;
|
||||
|
||||
// $col 简写: { $col: name } === { $eq: { $col: name } }(仅 JOIN ON 场景)
|
||||
if (options.$col && '$col' in ops && Object.keys(ops).length === 1) {
|
||||
return value === row[ops.$col as string];
|
||||
}
|
||||
|
||||
// v0.8.0 根治:**未提供 $col 上下文**时,含列引用/未解析子查询的条件必须
|
||||
// "放行"而不是"判假"。
|
||||
//
|
||||
// 背景:`t.x = t.y` 解析为 `{ x: { $eq: { $col: 'y' } } }`,执行路径是
|
||||
// engines.find(plan with 原始 where) ← 引擎层 post-index matchWhere(无 $col 上下文)
|
||||
// → executor.filterCorrelated(rows, stmt.where, { $col: true }) ← 真正的判定
|
||||
// 引擎层那一遍只是"索引命中后的安全过滤"(子集语义)。若它把 `$col` 当成普通对象
|
||||
// 比较,任何行都不匹配 → 返回空集,executor 拿到空数组、逐行求值根本没机会执行
|
||||
// → **静默空结果**(实测 `SELECT id FROM t WHERE t.x = t.y` 返回 [])。
|
||||
//
|
||||
// 语义上这是安全的:引擎层的过滤只允许"缩小候选集",而这里选择不过滤;
|
||||
// 最终判定始终由带 $col 上下文的 executor 完成(无法解析时由它抛 QUERY_ERROR)。
|
||||
if (!options.$col) {
|
||||
if ('$col' in ops && Object.keys(ops).length === 1) return true;
|
||||
for (const [, operand] of Object.entries(ops)) {
|
||||
if (typeof operand === 'object' && operand !== null && !Array.isArray(operand)) {
|
||||
const inner = operand as Record<string, unknown>;
|
||||
if ('$col' in inner || '$subquery' in inner) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历操作符
|
||||
for (const [op, operand] of Object.entries(ops)) {
|
||||
let actualOperand = operand;
|
||||
|
||||
// $col 列引用解析
|
||||
if (options.$col && typeof operand === 'object' && operand !== null && '$col' in (operand as Record<string, unknown>)) {
|
||||
actualOperand = row[(operand as Record<string, unknown>).$col as string];
|
||||
}
|
||||
|
||||
if (!matchOperator(value, op, actualOperand)) return false;
|
||||
}
|
||||
return true;
|
||||
return evalOperatorObject(ctx, value, condition);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 操作符匹配
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* 求值一个操作符对象(`{ $gt: 1 }` / `{ $and: [...] }` / `{ $col: 'y' }` …)。
|
||||
*
|
||||
* `$and` / `$or` 的每个子项按"操作数条件"递归 —— 这正是 `NOT BETWEEN`
|
||||
* 生成的 `{ n: { $or: [ { $lt: 1 }, { $gt: 2 } ] } }` 能正确求值的原因。
|
||||
* 若子项实际是**完整 where 子句**(键是列名),则转交 `evalWhere`,
|
||||
* 与当前行合并后求值(`$col` 上下文因此仍然有效)。
|
||||
*/
|
||||
function evalOperatorObject(
|
||||
ctx: EvalContext,
|
||||
value: unknown,
|
||||
ops: Record<string, unknown>,
|
||||
): SqlTruth {
|
||||
// 纯列引用:{ $col: 'y' } = 与当前行的 y 列比较
|
||||
const keys = Object.keys(ops);
|
||||
if (keys.length === 1 && keys[0] === '$col' && ctx.options.$col) {
|
||||
return compareEquality(value, resolveField(ctx, String(ops.$col)));
|
||||
}
|
||||
|
||||
function matchOperator(value: unknown, op: string, operand: unknown): boolean {
|
||||
let acc: SqlTruth = SQL_LOGICAL.TRUE;
|
||||
for (const [op, operand] of Object.entries(ops)) {
|
||||
let one: SqlTruth;
|
||||
if (op === '$and') {
|
||||
one = evalOperatorLogical(ctx, value, operand, 'and');
|
||||
} else if (op === '$or') {
|
||||
one = evalOperatorLogical(ctx, value, operand, 'or');
|
||||
} else if (op === '$not') {
|
||||
one = sqlNot(evalOperandSlot(ctx, value, operand));
|
||||
} else {
|
||||
one = evalOperator(ctx, value, op, operand);
|
||||
}
|
||||
acc = sqlAnd(acc, one);
|
||||
if (acc === SQL_LOGICAL.FALSE) return SQL_LOGICAL.FALSE;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** `$and` / `$or`(操作符级):子项是操作数条件(操作符对象或裸值) */
|
||||
function evalOperatorLogical(
|
||||
ctx: EvalContext,
|
||||
value: unknown,
|
||||
condition: unknown,
|
||||
kind: 'and' | 'or',
|
||||
): SqlTruth {
|
||||
const items = Array.isArray(condition) ? condition : [condition];
|
||||
let acc: SqlTruth = kind === 'and' ? SQL_LOGICAL.TRUE : SQL_LOGICAL.FALSE;
|
||||
for (const item of items) {
|
||||
const one = evalOperandSlot(ctx, value, item);
|
||||
acc = kind === 'and' ? sqlAnd(acc, one) : sqlOr(acc, one);
|
||||
if (kind === 'and' && acc === SQL_LOGICAL.FALSE) return SQL_LOGICAL.FALSE;
|
||||
if (kind === 'or' && acc === SQL_LOGICAL.TRUE) return SQL_LOGICAL.TRUE;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个嵌套子项(`$and`/`$or`/`$not` 的操作数)的求值。
|
||||
*
|
||||
* 两种合法形态,按**键的形状**判别,不做猜测性兜底:
|
||||
* - 含非 `$` 开头(或逻辑连接词)的键 → 完整 where 子句,交由 `evalWhere`;
|
||||
* - 其余 → 操作符对象,交由 `evalOperatorObject`。
|
||||
*
|
||||
* 注意 `$not` 的子项**不会**走到这里被判为 where 子句:`evalOperatorObject`
|
||||
* 对 `$not` 直接调用本函数,而 `{ $gte: 1 }` 全部键以 `$` 开头 → 操作符对象 ✓。
|
||||
* 若调用方写成 `{ $not: { n: { $gte: 1 } } }`(字段级 `$not` 包了 where 子句),
|
||||
* 则按 where 子句解释 —— 这正是 `evalNestedSlot` 的判别分支,语义为
|
||||
* "NOT (该行满足 n >= 1)",与顶层 `$not` 一致。
|
||||
*/
|
||||
function evalOperandSlot(ctx: EvalContext, value: unknown, item: unknown): SqlTruth {
|
||||
if (!isPlainObject(item)) return evalValueCondition(ctx, value, item);
|
||||
const isWhereClause = Object.keys(item).some(
|
||||
(k) => !k.startsWith('$') || isLogicalKey(k) || k === '$exists' || k === '$caseResult',
|
||||
);
|
||||
return isWhereClause ? evalWhere(ctx, item as WhereCondition) : evalOperatorObject(ctx, value, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作符求值(三值语义)。
|
||||
*
|
||||
* - 比较类操作数含 NULL → UNKNOWN(此前 `$eq: null` 命中 NULL 行、`$ne: null`
|
||||
* 命中所有非 NULL 行,两者都不符合 SQL 标准);
|
||||
* - `$in` / `$nin` 用 `sqlIn` 的列表语义(列表含 NULL → 未命中时为 UNKNOWN);
|
||||
* - `$like` 对 NULL 操作数返回 UNKNOWN(`String(null)` 会得到 "null" 去匹配,
|
||||
* 属于静默错值);
|
||||
* - `$isNull` / `$isNotNull` 是**谓词**,直接对 NULL 求值,不走比较。
|
||||
*/
|
||||
function evalOperator(
|
||||
ctx: EvalContext,
|
||||
value: unknown,
|
||||
op: string,
|
||||
operand: unknown,
|
||||
): SqlTruth {
|
||||
const actual = resolveOperand(ctx, operand);
|
||||
switch (op) {
|
||||
case '$eq': return value === operand;
|
||||
case '$ne': return value !== operand;
|
||||
case '$gt': return (value as number) > (operand as number);
|
||||
case '$gte': return (value as number) >= (operand as number);
|
||||
case '$lt': return (value as number) < (operand as number);
|
||||
case '$lte': return (value as number) <= (operand as number);
|
||||
case '$in': return Array.isArray(operand) && operand.includes(value);
|
||||
case '$nin': return Array.isArray(operand) && !operand.includes(value);
|
||||
case '$like': return compileLikeRegex(String(operand)).test(String(value));
|
||||
case '$eq': return compareEquality(value, actual);
|
||||
case '$ne': return sqlNot(compareEquality(value, actual));
|
||||
case '$gt': return compareOrdered(value, actual, (c) => c > 0);
|
||||
case '$gte': return compareOrdered(value, actual, (c) => c >= 0);
|
||||
case '$lt': return compareOrdered(value, actual, (c) => c < 0);
|
||||
case '$lte': return compareOrdered(value, actual, (c) => c <= 0);
|
||||
case '$in': return Array.isArray(actual) ? sqlIn(value, actual) : SQL_LOGICAL.FALSE;
|
||||
case '$nin': return Array.isArray(actual) ? sqlNot(sqlIn(value, actual)) : SQL_LOGICAL.FALSE;
|
||||
case '$like': {
|
||||
if (isSqlNull(value) || isSqlNull(actual) || isUnresolved(value) || isUnresolved(actual)) {
|
||||
return SQL_LOGICAL.UNKNOWN;
|
||||
}
|
||||
return compileLikeRegex(String(actual)).test(String(value))
|
||||
? SQL_LOGICAL.TRUE
|
||||
: SQL_LOGICAL.FALSE;
|
||||
}
|
||||
// 谓词(不参与三值比较)
|
||||
case '$isNull': return isSqlNull(value) ? SQL_LOGICAL.TRUE : SQL_LOGICAL.FALSE;
|
||||
case '$isNotNull': return isSqlNull(value) ? SQL_LOGICAL.FALSE : SQL_LOGICAL.TRUE;
|
||||
// `$col` 出现在操作符位置但未启用列上下文 → 无法求值
|
||||
case '$col': return ctx.options.$col
|
||||
? compareEquality(value, resolveField(ctx, String(operand)))
|
||||
: SQL_LOGICAL.UNKNOWN;
|
||||
// `$subquery` 必须由 Executor 先行解析(`resolveSubqueries`)。
|
||||
// 走到这里说明调用方跳过了 Executor:返回 UNKNOWN 让该行被排除,
|
||||
// 而不是静默当 NULL 比较。
|
||||
case '$subquery': return SQL_LOGICAL.UNKNOWN;
|
||||
// v0.7.2: 未知操作符显式报错 —— 此前静默返回 true(所有行匹配),
|
||||
// 拼错操作符(如 $betwen)时过滤形同虚设且无任何提示
|
||||
default:
|
||||
@@ -198,6 +387,48 @@ function matchOperator(value: unknown, op: string, operand: unknown): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析操作数槽位中的引用标记。
|
||||
*
|
||||
* - `{ $col: 'y' }` → 当前行的 y 列值(仅在 `options.$col` 启用时);
|
||||
* - `{ $subquery: [...] }` → UNRESOLVED,交由上层识别为"未解析"。
|
||||
*
|
||||
* 这两种形态都是**对象**,而非"恰好等于某个值",所以必须在这里显式解引用,
|
||||
* 否则 `sqlCompare(5, { $col: 'y' })` 会按不可比返回 UNKNOWN —— 恰好也是
|
||||
* UNKNOWN,但那会掩盖"调用方忘了传 `$col`"这一真正的配置错误。
|
||||
*/
|
||||
function resolveOperand(ctx: EvalContext, operand: unknown): unknown {
|
||||
if (!isPlainObject(operand)) return operand;
|
||||
const keys = Object.keys(operand);
|
||||
if (keys.length !== 1) return operand;
|
||||
if (keys[0] === '$col') {
|
||||
return ctx.options.$col ? resolveField(ctx, String(operand.$col)) : UNRESOLVED;
|
||||
}
|
||||
if (keys[0] === '$subquery') return UNRESOLVED;
|
||||
return operand;
|
||||
}
|
||||
|
||||
/** 相等比较(三值) */
|
||||
function compareEquality(value: unknown, operand: unknown): SqlTruth {
|
||||
const truth = sqlCompare(value, operand);
|
||||
if (truth === SQL_LOGICAL.TRUE) return SQL_LOGICAL.TRUE;
|
||||
if (isSqlNull(value) || isSqlNull(operand) || isUnresolved(value) || isUnresolved(operand)) {
|
||||
return SQL_LOGICAL.UNKNOWN;
|
||||
}
|
||||
return SQL_LOGICAL.FALSE;
|
||||
}
|
||||
|
||||
/** 有序比较(三值):任一操作数 NULL/未解析/不可比 → UNKNOWN */
|
||||
function compareOrdered(
|
||||
value: unknown,
|
||||
operand: unknown,
|
||||
accept: (cmp: number) => boolean,
|
||||
): SqlTruth {
|
||||
const ord = sqlCompareOrder(value, operand);
|
||||
if (ord === null) return SQL_LOGICAL.UNKNOWN;
|
||||
return accept(ord) ? SQL_LOGICAL.TRUE : SQL_LOGICAL.FALSE;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 排序
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+22
-2
@@ -898,7 +898,11 @@ export class Parser {
|
||||
if (isNot) this.nextToken();
|
||||
this.expect(TokenType.NULL);
|
||||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||||
result[column] = isNot ? { $ne: null } : { $eq: null };
|
||||
// v0.8.0 三值语义:IS NULL / IS NOT NULL 是**谓词**,不是等值比较。
|
||||
// 此前生成为 { $eq: null } / { $ne: null } —— 在 SQL 标准里
|
||||
// `x = NULL` 恒为 UNKNOWN(不保留任何行),而 IS NULL 要保留 NULL 行。
|
||||
// 两者语义不同,必须用不同标记(见 query/sql-compare.ts 的谓词说明)。
|
||||
result[column] = isNot ? { $isNotNull: true } : { $isNull: true };
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -908,6 +912,11 @@ export class Parser {
|
||||
const low = this.parseValue();
|
||||
this.expect(TokenType.AND);
|
||||
const high = this.parseValue();
|
||||
// v0.8.0 根治:BETWEEN 必须是**范围**条件。
|
||||
// 此前把同一个对象同时当成"操作符对象"和"操作数"传给 `$eq`
|
||||
// (`{ $eq: { $gte, $lte } }`),matchOperator 的 `$eq` 收到一个对象再去比较,
|
||||
// 结果只对"值恰好等于该对象"的行成立 —— 实测 `WHERE n BETWEEN 1 AND 2`
|
||||
// 在 (1,2,NULL,3) 上只返回 1 行(应 2 行),静默错值。
|
||||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||||
result[column] = { $gte: low, $lte: high };
|
||||
return result;
|
||||
@@ -921,7 +930,18 @@ export class Parser {
|
||||
this.expect(TokenType.AND);
|
||||
const high = this.parseValue();
|
||||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||||
result[column] = { $not: { $gte: low, $lte: high } };
|
||||
// v0.8.0:NOT BETWEEN ≡ (x < low) OR (x > high),直接展开为字段级 `$or`。
|
||||
//
|
||||
// 此前生成 `{ $not: { $gte, $lte } }`。这**曾经**恒为空集:字段级 `$not`
|
||||
// 把内层对象当"单个条件对象",key `$gte`/`$lte` 被当成列名去取
|
||||
// `row['$gte']` → undefined → 整条恒 UNKNOWN → 取反仍 UNKNOWN → 全部排除。
|
||||
//
|
||||
// 求值器已修正(内层按操作符对象解释),但这里仍选择展开为 `$or`:
|
||||
// - `$or` 的三值行为(含 NULL 时 UNKNOWN)是显式可读的;
|
||||
// - 避免依赖"$not 作用于比较"与"$not 作用于谓词"(如 `$isNull`)的差别。
|
||||
// 两条路径都有测试锁定(tests/v080-sql-three-valued.test.ts 与
|
||||
// tests/sql/where-matcher.test.ts 的 `$not` 用例)。
|
||||
result[column] = { $or: [{ $lt: low } as never, { $gt: high } as never] as never };
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user