/** * 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(); 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)) { if (typeof operand === 'object' && operand !== null) { const ops = operand as Record; 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, 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, options: { $col?: boolean }, ): boolean { // 嵌套 $and if (typeof condition === 'object' && condition !== null && '$and' in (condition as Record)) { const subs = (condition as Record).$and as WhereCondition[]; return subs.every((sub) => matchWhere(row, sub, options)); } // 嵌套 $or if (typeof condition === 'object' && condition !== null && '$or' in (condition as Record)) { const subs = (condition as Record).$or as WhereCondition[]; return subs.some((sub) => matchWhere(row, sub, options)); } // $not if (typeof condition === 'object' && condition !== null && '$not' in (condition as Record)) { return !matchField(value, (condition as Record).$not, row, options); } // 简单值 => $eq if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) { return value === condition; } const ops = condition as Record; // $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)) { actualOperand = row[(operand as Record).$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[], orderBy: OrderBy[]): Record[] { 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, columns: string[]): Record { const projected: Record = {}; 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; }