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:
thzxx
2026-09-14 23:22:26 +08:00
parent 674da6b7b7
commit 4ab04df882
9 changed files with 929 additions and 162 deletions
+373 -142
View File
@@ -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)) {
// 顶层 $caseResultv0.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;
}
// 顶层 $existsv0.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;
}
// 顶层 $notv0.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;
}
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------