背景(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 零错误。
483 lines
19 KiB
TypeScript
483 lines
19 KiB
TypeScript
/**
|
||
* metona-sqlark Shared WHERE Matcher —— 统一的条件求值器
|
||
* @module query/where-matcher
|
||
*
|
||
* ============================================================================
|
||
* 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 正则缓存
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const likeCache = new Map<string, RegExp>();
|
||
|
||
function compileLikeRegex(pattern: string): RegExp {
|
||
const cached = likeCache.get(pattern);
|
||
if (cached) return cached;
|
||
const escaped = pattern
|
||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||
.replace(/%/g, '.*')
|
||
.replace(/_/g, '.');
|
||
const regex = new RegExp(`^${escaped}$`, 'i');
|
||
likeCache.set(pattern, regex);
|
||
return regex;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 求值上下文
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** 求值选项(对外 API 保持既有形状) */
|
||
export interface MatchOptions {
|
||
/**
|
||
* 是否解析 `$col` 列引用(关联子查询 / JOIN ON 的列对列比较)。
|
||
*
|
||
* 引擎层(`matchWhere` 直通调用)没有外层行上下文,必须**不**解析:
|
||
* 此时 `$col` 求值为 UNRESOLVED(比较 → UNKNOWN,行被排除),
|
||
* 而不是抛"未知操作符",也不是静默判真。
|
||
*/
|
||
$col?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 内部求值上下文。
|
||
*
|
||
* `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: 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;
|
||
}
|
||
if (k === '$exists') return true;
|
||
if (isPlainObject(v) && operatorObjectHasUnresolved(v)) 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;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 统一递归求值器
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** 逻辑连接词(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,
|
||
): SqlTruth {
|
||
if (!isPlainObject(condition)) {
|
||
// 裸值 = `$eq`;数组 = `$in` 列表(与既有 Mongo 风格 WHERE 兼容)
|
||
if (Array.isArray(condition)) return sqlIn(value, condition);
|
||
return compareEquality(value, condition);
|
||
}
|
||
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)));
|
||
}
|
||
|
||
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 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:
|
||
throw new DatabaseError(`Unknown where operator "${op}"`, 'QUERY_ERROR');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析操作数槽位中的引用标记。
|
||
*
|
||
* - `{ $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;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 排序
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export function applyOrderBy(rows: Record<string, unknown>[], orderBy: OrderBy[]): Record<string, unknown>[] {
|
||
return [...rows].sort((a, b) => {
|
||
for (const { column, direction, nulls } of orderBy) {
|
||
const aNull = a[column] === null || a[column] === undefined;
|
||
const bNull = b[column] === null || b[column] === undefined;
|
||
// v0.4.0: NULLS FIRST/LAST 时 NULL 位置固定,不受升降序反转
|
||
if (nulls && (aNull || bNull)) {
|
||
if (aNull && bNull) continue;
|
||
const cmp = nulls === 'first' ? (aNull ? -1 : 1) : (aNull ? 1 : -1);
|
||
return cmp;
|
||
}
|
||
const cmp = compare(a[column], b[column]);
|
||
if (cmp !== 0) return direction === 'desc' ? -cmp : cmp;
|
||
}
|
||
return 0;
|
||
});
|
||
}
|
||
|
||
function compare(a: unknown, b: unknown): number {
|
||
if (a === b) return 0;
|
||
if (a === null || a === undefined) return 1;
|
||
if (b === null || b === undefined) return -1;
|
||
if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b);
|
||
if (typeof a === 'number' && typeof b === 'number') return a - b;
|
||
return String(a).localeCompare(String(b));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 列投影
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export function projectColumns(row: Record<string, unknown>, columns: string[]): Record<string, unknown> {
|
||
const projected: Record<string, unknown> = {};
|
||
for (const col of columns) {
|
||
if (col in row) {
|
||
projected[col] = row[col];
|
||
} else {
|
||
for (const key of Object.keys(row)) {
|
||
if (key.endsWith(`.${col}`) || key === col) {
|
||
projected[col] = row[key];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return projected;
|
||
}
|