feat: metona-sqlark v0.1.12 — 前端TypeScript关系型数据库
- 4种存储引擎:Memory / IndexedDB / OPFS / Hybrid - 完整SQL支持:SELECT/INSERT/UPDATE/DELETE/JOIN/GROUP BY/HAVING/DISTINCT - Query Builder链式API + TypeScript泛型支持 - 聚合函数:COUNT/SUM/AVG/MIN/MAX - 事务、插件系统(14 hooks)、发布订阅、数据迁移、导入导出 - React/Vue框架集成 - 264个测试用例,93.46%覆盖率 - 零运行时依赖
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
* @module query/where-matcher
|
||||
*
|
||||
* MemoryEngine / IndexedDBEngine / QueryExecutor 共享此模块,
|
||||
* 消除 220+ 行重复代码,统一 $and/$or/$not/$col 行为。
|
||||
*/
|
||||
|
||||
import type { WhereCondition, OrderBy } 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 匹配(顶层入口)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 匹配完整 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)) {
|
||||
// 顶层 $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;
|
||||
}
|
||||
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));
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 排序
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function applyOrderBy(rows: Record<string, unknown>[], orderBy: OrderBy[]): Record<string, unknown>[] {
|
||||
return [...rows].sort((a, b) => {
|
||||
for (const { column, direction } of orderBy) {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user