230 lines
8.5 KiB
TypeScript
230 lines
8.5 KiB
TypeScript
/**
|
||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||
* @module query/where-matcher
|
||
*
|
||
* MemoryEngine / IndexedDBEngine / QueryExecutor 共享此模块,
|
||
* 消除 220+ 行重复代码,统一 $and/$or/$not/$col 行为。
|
||
*/
|
||
|
||
import type { WhereCondition, OrderBy } from '../constants';
|
||
import { DatabaseError } from '../constants';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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;
|
||
}
|
||
|
||
/**
|
||
* 匹配完整 WHERE 条件
|
||
* @param row 当前数据行
|
||
* @param where WHERE 条件对象
|
||
* @param options.$col 是否启用 $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;
|
||
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;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 字段匹配
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function matchField(
|
||
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));
|
||
}
|
||
// 嵌套 $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];
|
||
}
|
||
|
||
// 遍历操作符
|
||
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;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 操作符匹配
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function matchOperator(value: unknown, op: string, operand: unknown): boolean {
|
||
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));
|
||
// v0.7.2: 未知操作符显式报错 —— 此前静默返回 true(所有行匹配),
|
||
// 拼错操作符(如 $betwen)时过滤形同虚设且无任何提示
|
||
default:
|
||
throw new DatabaseError(`Unknown where operator "${op}"`, 'QUERY_ERROR');
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 排序
|
||
// ---------------------------------------------------------------------------
|
||
|
||
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;
|
||
}
|