A6 LIMIT/OFFSET 被应用两次(丢行)
compileSelect 无条件下推 limit/offset,引擎切一次,executeSelect 末尾又切一次。
实测 4 行表:LIMIT 2 OFFSET 1 只返回 1 行;LIMIT 10 OFFSET 3 返回空。
JOIN/派生表路径因不走 plan 反而正确,同一 executor 内自相矛盾。
现引入 analyzeSelect() 统一判定 limitPushdownSafe,两条路径互斥且只应用一次。
实测 10 种查询形态(含 DISTINCT/GROUP BY/别名/深 OFFSET/LIMIT 0)全部正确。
A7 queryStream 与 query 结果不一致(四类静默分歧)
core.ts 自己重写了一套能否走引擎快路径/如何投影的规则,与 executor 各写一份:
SELECT id AS x FROM t query [{x}],stream [{id,v}](全列+原列名)
SELECT t.id FROM t query [{id}],stream [{}](空对象)
LIMIT 2 OFFSET 1 query 1 行,stream 2 行
LIMIT 0 query 0 行,stream 1 行(Aria 又是 0 行)
且流式路径不触发 beforeQuery/afterQuery、不受 maxRowsPerQuery 约束。
现由 executor.analyzeSelect() 做单一事实来源;不可流式一律回退物化路径;
列引用剥离主表别名前缀;LIMIT 0 短路;回调返回 Promise 时显式报错
(此前用 constructor.name === 'AsyncFunction' 判定,普通函数返回 Promise 会静默丢弃)。
实测 16 种查询形态 × 4 引擎 = 64 组,query 与 queryStream 逐值相等。
A5 MIN/MAX 栈溢出(崩溃)
Math.min(...arr) 展开实参:20 万行同组直接 RangeError。改为单次遍历归约 reduceNumeric。
A24 空集聚合语义
SUM/AVG/MIN/MAX 对空集返回 0(使 和为 0 与 无数据 不可区分),改为 SQL 标准的 NULL;
COUNT 仍返回 0。
A18 未知列静默产出空对象行
SELECT bogus FROM t 返回 [{},{},...](行数对、内容空、无报错);SELECT NAME(列名 name)
同样静默空。新增 assertProjectionColumnsExist:投影前校验列存在性,
未知列抛 COLUMN_NOT_FOUND;同一后缀命中多个表别名时抛歧义错误。空结果集不误报。
A16 INSERT 列/值个数不校验(静默丢弃/写半行)
显式列名时在解析期校验每行值个数与列数一致:
INSERT INTO t (id,name) VALUES ('4','z',9) 此前静默丢弃 9,现报错。
A28 UPDATE SET __proto__ 静默吞掉
sets['__proto__'] 触发原型 setter,sets 变空对象,既不写入也不被未知列预检看到,
表现为返回成功但什么都没发生。parser 的列名映射统一改为 Object.create(null)。
附带修复(A18 验证时发现):
SELECT 1 AS one FROM t 返回 [{}] —— parseColumnRef 的数字分支提前 return 吞掉别名;
裸 SELECT 1 同样产出空对象 —— 投影未处理匿名常量列。现按 SQLite 语义以表达式原文为键。
1688 lines
72 KiB
TypeScript
1688 lines
72 KiB
TypeScript
/**
|
||
* metona-sqlark Query Executor — AST 执行器
|
||
* @module query/executor
|
||
*
|
||
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
|
||
*/
|
||
|
||
import type { IStorageEngine } from '../engine/interface';
|
||
import type {
|
||
Statement, SelectStatement, SelectUnionStatement, InsertStatement, UpdateStatement,
|
||
DeleteStatement, CreateTableStatement, DropTableStatement, JoinClause,
|
||
AlterTableStatement, TruncateTableStatement, CreateIndexStatement, DropIndexStatement,
|
||
} from './ast';
|
||
import { DatabaseError } from '../constants';
|
||
import { compileStatement } from './compiler';
|
||
import { createSchema, astColumnToColumnDef } from '../table/schema';
|
||
import { matchWhere, applyOrderBy, projectColumns, containsUnresolvedSubqueries } from './where-matcher';
|
||
import { parseWhereCondition } from '../sql/parser';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// SELECT 执行形态(v0.8.0)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* 一条 SELECT 的执行形态判定结果。
|
||
*
|
||
* 由 {@link QueryExecutor.analyzeSelect} 统一产出,供 executor 自身与
|
||
* `core.queryStream` 共享 —— 避免"两条入口各写一套规则"导致的语义漂移。
|
||
*/
|
||
export interface SelectExecutionShape {
|
||
/** 有 GROUP BY */
|
||
hasGroupBy: boolean;
|
||
/** 无 GROUP BY 但 SELECT 列表含聚合函数 */
|
||
hasAggregate: boolean;
|
||
/** 含 JOIN */
|
||
isJoinQuery: boolean;
|
||
/** 需要原始行(SELECT 列或 WHERE 含 CASE 表达式) */
|
||
needsRawRows: boolean;
|
||
/** ORDER BY 引用了 SELECT 别名(投影后才存在) */
|
||
orderByAlias: boolean;
|
||
/** SELECT 列含 `col AS alias` */
|
||
hasSelectAlias: boolean;
|
||
/** LIMIT/OFFSET 可安全下推给引擎(否则由 executor 末尾应用一次) */
|
||
limitPushdownSafe: boolean;
|
||
/** 引擎层投影与 executor 投影语义等价(列均为裸列引用) */
|
||
engineEquivalentProjection: boolean;
|
||
/** 可直连引擎 findStream 做真流式(无任何改变行集合/行序/行形状的阶段) */
|
||
streamable: boolean;
|
||
}
|
||
import type { WhereCondition } from '../constants';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 分组 / 去重键编码(v0.7.4)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* v0.7.4: 分组/去重键的类型安全编码 —— 此前 `String(v ?? 'null')` 使
|
||
* null 与字符串 'null' 合并为一组(GROUP BY 静默少组),`String(v ?? '\0')`
|
||
* 使 null/undefined/'\0' 在 DISTINCT/UNION 中互相吞并。类型前缀编码后
|
||
* 各类型独立,仅同类型同值合并(与 where-matcher 的 === 语义一致)。
|
||
*/
|
||
function encodeGroupKey(v: unknown): string {
|
||
if (v === null) return 'n';
|
||
if (v === undefined) return 'u';
|
||
if (typeof v === 'string') return `s${v}`;
|
||
if (typeof v === 'number') return `d${v}`;
|
||
if (typeof v === 'boolean') return `b${v}`;
|
||
if (typeof v === 'object') return `o${JSON.stringify(v)}`;
|
||
return `x${String(v)}`;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CASE WHEN 表达式(v0.3.1)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
interface CaseWhenClause {
|
||
/** 条件(已解析为 WhereCondition,解析失败为 null 表示跳过) */
|
||
cond: WhereCondition | null;
|
||
/** THEN 值(字面量或列引用文本) */
|
||
value: string;
|
||
}
|
||
|
||
interface CaseExpression {
|
||
whens: CaseWhenClause[];
|
||
elseValue: string | null;
|
||
alias: string | null;
|
||
}
|
||
|
||
/**
|
||
* v0.8.0: 解析 `expr AS alias` 中的 `expr` 取值来源。
|
||
*
|
||
* 此前只处理"字符串常量"与"列引用"两种情况,数字/布尔/NULL 常量会走
|
||
* `row[source]` → undefined,于是 `SELECT 1 AS one FROM t` 返回 `[{}]`
|
||
* (键在、值为 undefined,JSON 序列化后整个键消失),
|
||
* 而这正是 EXISTS 子查询里最常见的写法(`SELECT 1 FROM ...`)。
|
||
*/
|
||
function resolveAliasSource(source: string, row: Record<string, unknown>): unknown {
|
||
const text = source.trim();
|
||
// 字符串常量(含 SQL 标准 '' 转义还原)
|
||
const strLit = text.match(/^'(.*)'$/s);
|
||
if (strLit) return strLit[1].replace(/''/g, "'");
|
||
// 数字常量(含负号与小数)
|
||
if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
|
||
if (/^TRUE$/i.test(text)) return true;
|
||
if (/^FALSE$/i.test(text)) return false;
|
||
if (/^NULL$/i.test(text)) return null;
|
||
return row[text];
|
||
}
|
||
|
||
/**
|
||
* v0.8.0: 数值型聚合的单次遍历实现。
|
||
*
|
||
* 为什么替换 `Math.min(...arr)` / `Math.max(...arr)`:
|
||
* 展开实参会把整个数组压进调用栈,20 万行同组即 RangeError(栈溢出);
|
||
* 而这是普通查询就能触发的崩溃,不是边界场景。
|
||
*
|
||
* 空集语义(SQL 标准):SUM/AVG/MIN/MAX 对**空集或全 NULL** 返回 NULL。
|
||
* 此前统一返回 0,使 `SUM(x) = 0` 与"没有数据"不可区分。
|
||
* 注意 COUNT 不在此列 —— COUNT 对空集返回 0(由调用方处理)。
|
||
*/
|
||
function reduceNumeric(values: number[], op: 'SUM' | 'AVG' | 'MIN' | 'MAX'): number | null {
|
||
if (values.length === 0) return null;
|
||
let acc = op === 'SUM' || op === 'AVG' ? 0 : values[0];
|
||
for (let i = 0; i < values.length; i++) {
|
||
const v = values[i];
|
||
switch (op) {
|
||
case 'SUM':
|
||
case 'AVG':
|
||
acc += v;
|
||
break;
|
||
case 'MIN':
|
||
if (i > 0 && v < acc) acc = v;
|
||
break;
|
||
case 'MAX':
|
||
if (i > 0 && v > acc) acc = v;
|
||
break;
|
||
}
|
||
}
|
||
return op === 'AVG' ? acc / values.length : acc;
|
||
}
|
||
|
||
/** 解析 "CASE WHEN c1 THEN v1 WHEN c2 THEN v2 ELSE v3 END [AS alias]" */
|
||
function parseCaseExpression(expr: string): CaseExpression | null {
|
||
const m = expr.match(/^\s*CASE\s+([\s\S]*?)\s+END\s*(?:AS\s+(\w+))?\s*$/i);
|
||
if (!m) return null;
|
||
const body = m[1];
|
||
const alias = m[2] ?? null;
|
||
|
||
const whens: CaseWhenClause[] = [];
|
||
const re = /WHEN\s+([\s\S]*?)\s+THEN\s+([\s\S]*?)(?=\s+WHEN\s+|\s+ELSE\s+|\s*$)/gi;
|
||
let match: RegExpExecArray | null;
|
||
while ((match = re.exec(body)) !== null) {
|
||
let cond: WhereCondition | null = null;
|
||
try {
|
||
cond = parseWhereCondition(match[1].trim());
|
||
} catch {
|
||
// 条件解析失败视为不匹配
|
||
}
|
||
whens.push({ cond, value: match[2].trim() });
|
||
}
|
||
|
||
let elseValue: string | null = null;
|
||
const elseMatch = body.match(/\sELSE\s+([\s\S]*)$/i);
|
||
if (elseMatch) elseValue = elseMatch[1].trim();
|
||
|
||
return { whens, elseValue, alias };
|
||
}
|
||
|
||
/** 解析 CASE 值:字面量(null/true/false/数字/字符串)优先,其次列引用 → 行值 */
|
||
function resolveCaseValue(text: string, row: Record<string, unknown>): unknown {
|
||
const v = text.trim();
|
||
if (v === 'null') return null;
|
||
if (v === 'true') return true;
|
||
if (v === 'false') return false;
|
||
const num = Number(v);
|
||
if (v !== '' && !isNaN(num)) return num;
|
||
const str = v.match(/^'(.*)'$/s) || v.match(/^"(.*)"$/s);
|
||
if (str) return str[1];
|
||
if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(v)) {
|
||
return row[v] ?? null; // 列引用(含 table.col)
|
||
}
|
||
return v;
|
||
}
|
||
|
||
/** 对行求值 CASE WHEN 表达式 */
|
||
function evaluateCase(expr: CaseExpression, row: Record<string, unknown>): unknown {
|
||
for (const { cond, value } of expr.whens) {
|
||
if (cond && matchWhere(row, cond)) {
|
||
return resolveCaseValue(value, row);
|
||
}
|
||
}
|
||
return expr.elseValue !== null ? resolveCaseValue(expr.elseValue, row) : null;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Executor
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export class QueryExecutor {
|
||
private maxRowsPerQuery: number;
|
||
|
||
constructor(private engine: IStorageEngine, maxRowsPerQuery: number = 0) {
|
||
this.maxRowsPerQuery = maxRowsPerQuery;
|
||
}
|
||
|
||
async execute(stmt: Statement): Promise<unknown> {
|
||
switch (stmt.type) {
|
||
case 'SELECT': return this.executeSelect(stmt);
|
||
case 'SELECT_UNION': return this.executeSelectUnion(stmt);
|
||
case 'EXPLAIN': return this.executeExplain(stmt as any);
|
||
case 'INSERT': return this.executeInsert(stmt);
|
||
case 'UPDATE': return this.executeUpdate(stmt);
|
||
case 'DELETE': return this.executeDelete(stmt);
|
||
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
|
||
case 'DROP_TABLE': return this.executeDropTable(stmt);
|
||
case 'ALTER_TABLE': return this.executeAlterTable(stmt as any);
|
||
case 'TRUNCATE_TABLE': return this.executeTruncateTable(stmt as any);
|
||
case 'CREATE_INDEX': return this.executeCreateIndex(stmt as any);
|
||
case 'DROP_INDEX': return this.executeDropIndex(stmt as any);
|
||
case 'BEGIN': return this.executeBegin();
|
||
case 'COMMIT': return this.executeCommit();
|
||
case 'ROLLBACK': return this.executeRollback();
|
||
// v0.5.1: 维护语句
|
||
case 'SAVEPOINT': return this.executeSavepoint(stmt);
|
||
case 'ANALYZE': return this.executeAnalyze(stmt);
|
||
case 'REINDEX': return this.executeReindex(stmt);
|
||
case 'VACUUM': return this.executeVacuum();
|
||
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
|
||
}
|
||
}
|
||
|
||
// ===================================================================
|
||
// UNION(v0.3.0)
|
||
// ===================================================================
|
||
|
||
/** 递归执行 UNION / UNION ALL,返回合并结果 */
|
||
private async executeSelectUnion(stmt: SelectUnionStatement): Promise<Record<string, unknown>[]> {
|
||
const leftRows = await this.executeSelectPart(stmt.left);
|
||
const rightRows = await this.executeSelectPart(stmt.right);
|
||
|
||
// 列名以左侧为准,右侧只取值
|
||
const leftCols = leftRows.length > 0 ? Object.keys(leftRows[0]) : [];
|
||
const normalized: Record<string, unknown>[] = leftRows.map((row) => row);
|
||
|
||
if (stmt.all) {
|
||
for (const row of rightRows) normalized.push(this.projectUnionRow(row, leftCols));
|
||
return normalized;
|
||
}
|
||
|
||
// UNION 去重(与 DISTINCT 相同的列值拼接键)
|
||
const seen = new Set<string>();
|
||
const result: Record<string, unknown>[] = [];
|
||
for (const row of normalized) {
|
||
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
|
||
if (!seen.has(key)) {
|
||
seen.add(key);
|
||
result.push(row);
|
||
}
|
||
}
|
||
for (const row of rightRows) {
|
||
const projected = this.projectUnionRow(row, leftCols);
|
||
const key = Object.values(projected).map(encodeGroupKey).join('\x1f');
|
||
if (!seen.has(key)) {
|
||
seen.add(key);
|
||
result.push(projected);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private async executeSelectPart(part: SelectStatement | SelectUnionStatement): Promise<Record<string, unknown>[]> {
|
||
return part.type === 'SELECT_UNION' ? this.executeSelectUnion(part) : this.executeSelect(part);
|
||
}
|
||
|
||
/** 将 UNION 右侧行投影为左侧列结构(按位置取值) */
|
||
private projectUnionRow(row: Record<string, unknown>, leftCols: string[]): Record<string, unknown> {
|
||
if (leftCols.length === 0) return row;
|
||
const values = Object.values(row);
|
||
const projected: Record<string, unknown> = {};
|
||
for (let i = 0; i < leftCols.length; i++) {
|
||
projected[leftCols[i]] = i < values.length ? values[i] : null;
|
||
}
|
||
return projected;
|
||
}
|
||
|
||
/** EXPLAIN: 输出查询计划 */
|
||
private async executeExplain(stmt: { query: Statement }): Promise<Record<string, unknown>> {
|
||
const startTime = Date.now();
|
||
let result: unknown = null;
|
||
let rows = 0;
|
||
|
||
// v0.6.2-fix: EXPLAIN 不得真实执行写语句 —— 此前 EXPLAIN DELETE/UPDATE 会产生
|
||
// 真实副作用(删/改数据)。仅 SELECT 类语句执行(只读);UPDATE/DELETE 用
|
||
// count 估算影响行数(无副作用);INSERT/DDL 仅输出计划不执行。
|
||
if (stmt.query.type === 'SELECT' || stmt.query.type === 'SELECT_UNION') {
|
||
try {
|
||
result = await this.execute(stmt.query);
|
||
} catch { /* explain 即使执行失败也返回计划 */ }
|
||
rows = Array.isArray(result) ? result.length : 0;
|
||
} else if (stmt.query.type === 'UPDATE' || stmt.query.type === 'DELETE') {
|
||
try {
|
||
// v0.7.4: 子查询解析后估算 —— 此前 $subquery 未解析使 count 恒 0
|
||
await this.resolveWriteWhere(stmt.query);
|
||
const plan = compileStatement(stmt.query);
|
||
plan.where = stmt.query.where;
|
||
rows = await this.engine.count(plan.table, plan);
|
||
} catch { rows = 0; }
|
||
}
|
||
const elapsed = Date.now() - startTime;
|
||
|
||
// v0.5.1: 仅 SELECT/DELETE/UPDATE 有引擎查询计划;其他语句输出基本信息
|
||
let plan: import('../constants').QueryPlan | null = null;
|
||
try {
|
||
plan = compileStatement(stmt.query);
|
||
} catch { /* 非查询语句无 QueryPlan */ }
|
||
|
||
// v0.7.0: 真实索引命中信息(此前 usingIndex 恒为 'auto' 占位)。
|
||
// 引擎无关启发式:WHERE 中存在主键/索引/唯一列条件 → 对应引擎索引路径。
|
||
// v0.7.3: 递归识别 $and 嵌套等值条件(与 Memory/Aria 的 $and 下推行为对齐;
|
||
// $or/$not 不下推,保持 none)。
|
||
let usingIndex: string = plan?.table ? 'none' : 'none';
|
||
if (plan && plan.table && plan.where && Object.keys(plan.where).length > 0) {
|
||
try {
|
||
const schema = await this.engine.getTableSchema(plan.table);
|
||
if (schema) {
|
||
const findIndex = (w: import('../constants').WhereCondition): string | null => {
|
||
for (const [k, v] of Object.entries(w)) {
|
||
if (k === '$and') {
|
||
for (const sub of (v as import('../constants').WhereCondition[])) {
|
||
const hit = findIndex(sub);
|
||
if (hit) return hit;
|
||
}
|
||
continue;
|
||
}
|
||
if (k === '$or' || k === '$not') continue;
|
||
const colDef = schema.columns[k];
|
||
if (!colDef) continue;
|
||
if (colDef.primaryKey) return 'pk';
|
||
if (colDef.index || colDef.unique) return `index:${k}`;
|
||
}
|
||
return null;
|
||
};
|
||
usingIndex = findIndex(plan.where) ?? 'none';
|
||
}
|
||
} catch { /* schema 读取失败保持 none */ }
|
||
}
|
||
|
||
return {
|
||
type: stmt.query.type,
|
||
table: plan?.table,
|
||
columns: plan?.columns,
|
||
where: plan?.where || {},
|
||
orderBy: plan?.orderBy || [],
|
||
limit: plan?.limit,
|
||
offset: plan?.offset,
|
||
usingIndex,
|
||
estimatedRows: rows,
|
||
actualTimeMs: elapsed,
|
||
};
|
||
}
|
||
|
||
// ===================================================================
|
||
// SELECT
|
||
// ===================================================================
|
||
|
||
/**
|
||
* v0.8.0: SELECT 语句的**执行形态分析**(单一事实来源)。
|
||
*
|
||
* 为什么把它独立出来:core.queryStream 此前在 core.ts 里**自己重新推导**了一遍
|
||
* "这条 SELECT 能不能走引擎快路径、列投影怎么算",与 executor 的规则各写一份,
|
||
* 于是两者漂移出四类静默不一致(实测):
|
||
* SELECT id AS x FROM t query=[{x}] stream=[{id,v}](全列 + 原列名)
|
||
* SELECT t.id FROM t query=[{id}] stream=[{}](空对象)
|
||
* ... LIMIT 2 OFFSET 1 query=1 行 stream=2 行(引擎与 executor 各切一次)
|
||
* ... LIMIT 0 query=[] stream=1 行
|
||
*
|
||
* 现在由 executor 提供唯一判定,core 只消费结论,不再复制规则。
|
||
*/
|
||
analyzeSelect(stmt: SelectStatement): SelectExecutionShape {
|
||
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
||
const hasAggregate = !hasGroupBy && this._hasAggregateColumn(stmt.columns);
|
||
const isJoinQuery = !!(stmt.joins && stmt.joins.length > 0);
|
||
const needsRawRows = this.hasCaseColumn(stmt.columns) ||
|
||
(!!stmt.where && this.whereHasCase(stmt.where));
|
||
const orderByAlias = this.orderByUsesSelectAlias(stmt);
|
||
const hasSelectAlias = stmt.columns.some((c) => /\s+AS\s+\w+$/i.test(c));
|
||
|
||
// LIMIT/OFFSET 下推安全性:只有"引擎返回的行 == LIMIT 应当作用其上的行"时才可下推。
|
||
// 此前 compileSelect 无条件下推、executor 末尾又切一次 → LIMIT 被应用两遍
|
||
// (4 行表上 LIMIT 2 OFFSET 1 只返回 1 行)。
|
||
const limitPushdownSafe = !hasGroupBy && !hasAggregate && !isJoinQuery
|
||
&& !stmt.fromSubquery && !stmt.distinct
|
||
&& !(stmt.having && Object.keys(stmt.having).length > 0)
|
||
&& !orderByAlias && !needsRawRows && !hasSelectAlias;
|
||
|
||
// 引擎层投影是否与 executor 语义等价:仅当全部列都是裸 * 或简单列引用
|
||
//(可带 table. 前缀)时成立。出现 AS 别名/常量/CASE/聚合就交给 executor 投影。
|
||
let engineEquivalentProjection = stmt.columns.length > 0;
|
||
for (const c of stmt.columns) {
|
||
if (c === '*') continue;
|
||
if (/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(c)) continue;
|
||
engineEquivalentProjection = false;
|
||
break;
|
||
}
|
||
|
||
// 真流式(引擎 findStream 直连)的充分条件:所有会改变行集合/行序/行形状的
|
||
// 阶段都不存在,且 WHERE 已解析(无子查询、无 $col 关联引用)。
|
||
const streamable = engineEquivalentProjection
|
||
&& !hasGroupBy && !hasAggregate && !isJoinQuery && !stmt.fromSubquery
|
||
&& !stmt.distinct && !(stmt.having && Object.keys(stmt.having).length > 0)
|
||
&& !(stmt.orderBy && stmt.orderBy.length > 0)
|
||
&& !needsRawRows && !orderByAlias && !hasSelectAlias
|
||
&& !this.hasCorrelatedRefs(stmt.where)
|
||
&& !containsUnresolvedSubqueries(stmt.where);
|
||
|
||
return {
|
||
hasGroupBy,
|
||
hasAggregate,
|
||
isJoinQuery,
|
||
needsRawRows,
|
||
orderByAlias,
|
||
hasSelectAlias,
|
||
limitPushdownSafe,
|
||
engineEquivalentProjection,
|
||
streamable,
|
||
};
|
||
}
|
||
|
||
private async executeSelect(stmt: SelectStatement): Promise<Record<string, unknown>[]> {
|
||
const shape = this.analyzeSelect(stmt);
|
||
const { hasGroupBy, hasAggregate, isJoinQuery, needsRawRows, orderByAlias, hasSelectAlias, limitPushdownSafe } = shape;
|
||
let rows: Record<string, unknown>[];
|
||
|
||
if (stmt.fromSubquery) {
|
||
// v0.4.0: FROM (SELECT ...) 派生表 — 子查询结果作为行源
|
||
const subRows = await this.executeSelectPart(stmt.fromSubquery);
|
||
rows = isJoinQuery
|
||
? await this.executeJoinSelect(stmt, subRows.map((row) => this.prefixRow(row, stmt.alias ?? '')))
|
||
: subRows;
|
||
if (!isJoinQuery && stmt.where && Object.keys(stmt.where).length > 0) {
|
||
// 非 JOIN:WHERE 在 executor 端过滤(子查询结果不经引擎)
|
||
stmt.where = await this.resolveSubqueries(stmt.where);
|
||
rows = rows.filter((row) => matchWhere(row, stmt.where));
|
||
}
|
||
} else if (!stmt.from && !isJoinQuery) {
|
||
// v0.4.0: 无表查询(SELECT 1 / SELECT 'lit')— 单行空上下文,常量列投影
|
||
rows = [{}];
|
||
} else if (isJoinQuery) {
|
||
// JOIN 路径:行带表别名前缀(如 'd.id'),WHERE 保持原名不剥离
|
||
rows = await this.executeJoinSelect(stmt);
|
||
} else {
|
||
// 非 JOIN 路径:规范化 WHERE 字段名(剥离主表别名前缀,修复 WHERE u.age > 20)
|
||
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||
stmt.where = this.normalizeWhereColumns(stmt.where, [stmt.alias ?? stmt.from]);
|
||
}
|
||
// v0.4.0: ORDER BY / GROUP BY 带表前缀同样剥离(如 ORDER BY u.age)
|
||
const mainAliases = [stmt.alias ?? stmt.from].filter(Boolean);
|
||
if (stmt.orderBy && stmt.orderBy.length > 0) {
|
||
stmt.orderBy = stmt.orderBy.map((o) => ({ ...o, column: this.stripAlias(o.column, mainAliases) }));
|
||
}
|
||
if (stmt.groupBy && stmt.groupBy.length > 0) {
|
||
stmt.groupBy = stmt.groupBy.map((c) => this.stripAlias(c, mainAliases));
|
||
}
|
||
// v0.4.0: SELECT 列带表前缀剥离(SELECT u.name → name,行键无前缀)
|
||
stmt.columns = stmt.columns.map((c) => {
|
||
if (c === '*' || /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(c) || /^\s*CASE\b/i.test(c) || /^'/.test(c)) return c;
|
||
const m = c.match(/^(.+?)\s+AS\s+(\w+)$/i);
|
||
if (m) {
|
||
const stripped = this.stripAlias(m[1].trim(), mainAliases);
|
||
return stripped === m[1].trim() ? c : `${stripped} AS ${m[2]}`;
|
||
}
|
||
return this.stripAlias(c, mainAliases);
|
||
});
|
||
|
||
// WHERE 含关联子查询($col 引用外层行)→ 逐行绑定上下文求值
|
||
if (stmt.where && this.hasCorrelatedRefs(stmt.where)) {
|
||
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
|
||
// v0.4.0 修复: 关联子查询需要完整外层行(SELECT 列可能不含被 $col 引用的列,如 EXISTS 绑定的主键)
|
||
plan.columns = ['*'];
|
||
if (orderByAlias) { plan.orderBy = undefined; plan.limit = undefined; plan.offset = undefined; }
|
||
rows = await this.engine.find(plan.table, { ...plan, where: this.stripCorrelatedExists(stmt.where) });
|
||
rows = await this.filterCorrelated(rows, stmt.where);
|
||
} else {
|
||
// 先解析子查询
|
||
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||
stmt.where = await this.resolveSubqueries(stmt.where);
|
||
}
|
||
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
|
||
if (needsRawRows || hasSelectAlias) plan.columns = ['*'];
|
||
if (!limitPushdownSafe) {
|
||
// 不安全:不把 LIMIT/OFFSET 交给引擎,由末尾统一应用(只应用一次)
|
||
plan.limit = undefined;
|
||
plan.offset = undefined;
|
||
}
|
||
if (orderByAlias) { plan.orderBy = undefined; plan.limit = undefined; plan.offset = undefined; }
|
||
rows = await this.engine.find(plan.table, plan);
|
||
}
|
||
}
|
||
|
||
// 无 GROUP BY 但有聚合 → 计算单行聚合结果
|
||
if (hasAggregate) {
|
||
rows = [this.computeSingleAggregate(rows, stmt)];
|
||
}
|
||
|
||
if (hasGroupBy) rows = this.executeGroupBy(rows, stmt);
|
||
if (stmt.distinct) rows = this.executeDistinct(rows);
|
||
if (stmt.having && Object.keys(stmt.having).length > 0) {
|
||
// v0.4.0 修复: HAVING 中的标量子查询(HAVING SUM(o.amount) > (SELECT AVG(...)))需先解析
|
||
stmt.having = await this.resolveSubqueries(stmt.having);
|
||
// v0.4.0: HAVING 引用聚合表达式键(如 SUM(o.amount))时归一为别名键(如 spent)
|
||
const aliasMap = (stmt as unknown as { _aggAliasMap?: Map<string, string> })._aggAliasMap;
|
||
if (aliasMap && aliasMap.size > 0) {
|
||
const normalized: WhereCondition = {};
|
||
for (const [k, v] of Object.entries(stmt.having)) {
|
||
normalized[aliasMap.get(k) ?? k] = v;
|
||
}
|
||
stmt.having = normalized;
|
||
}
|
||
rows = rows.filter((row) => matchWhere(row, stmt.having!));
|
||
}
|
||
if (stmt.orderBy && stmt.orderBy.length > 0) rows = applyOrderBy(rows, stmt.orderBy);
|
||
// v0.8.0 根治:投影前校验列引用存在性(此前未知列静默产出 {} 行)。
|
||
//
|
||
// 实测缺陷:`SELECT bogus FROM t`(4 行表)返回 `[{},{},{},{}]`,
|
||
// `SELECT NAME FROM t`(列名是 name)同样返回 `[{},{}]` —— 行数正确、内容全空、
|
||
// 无任何报错;`GROUP BY bogus` 会把全表并成一组,`ORDER BY bogus` 顺序随机。
|
||
// SQLite/MySQL 三处都报 "no such column"。
|
||
//
|
||
// 校验时机选在 JOIN/子查询合并完成后(此时是最终行形态),且仅在**未发生聚合**时
|
||
// 进行 —— 聚合/分组会把行替换为计算键,普通列本就不存在(那属于另一类语义问题)。
|
||
if (!hasAggregate) {
|
||
this.assertProjectionColumnsExist(rows, stmt.columns, stmt);
|
||
}
|
||
// v0.7.3: `SELECT *, col AS alias` —— 此前 columns[0]==='*' 直接不投影,
|
||
// 别名列/常量列丢失。仅当 '*' 是唯一列时跳过投影(projectRow 对裸 '*'
|
||
// 合并原行全部列,其余表达式覆盖/追加)
|
||
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0
|
||
&& !(stmt.columns.length === 1 && stmt.columns[0] === '*')) {
|
||
rows = rows.map((row) => this.projectRow(row, stmt.columns));
|
||
}
|
||
// v0.3.3: ORDER BY 别名 → 投影后才存在,需在投影后重新排序
|
||
if (orderByAlias && stmt.orderBy && stmt.orderBy.length > 0) {
|
||
rows = applyOrderBy(rows, stmt.orderBy);
|
||
}
|
||
// v0.8.0: LIMIT/OFFSET 的应用点,两条路径互斥且**只执行一次**:
|
||
// - limitPushdownSafe === true → 引擎已按同一 offset/limit 完成切片,此处不再切片;
|
||
// - limitPushdownSafe === false → 引擎拿不到 limit/offset,此处是唯一应用点。
|
||
// 此前两条路径都切了一次,导致 4 行表上 `LIMIT 2 OFFSET 1` 只返回 1 行(应 2,3)。
|
||
if (!limitPushdownSafe) {
|
||
const offset = stmt.offset ?? 0;
|
||
const limit = stmt.limit ?? rows.length;
|
||
rows = rows.slice(offset, offset + limit);
|
||
}
|
||
|
||
// 全局行数上限保护
|
||
if (this.maxRowsPerQuery > 0 && rows.length > this.maxRowsPerQuery) {
|
||
rows = rows.slice(0, this.maxRowsPerQuery);
|
||
}
|
||
|
||
return rows;
|
||
}
|
||
|
||
// ---- JOIN ----
|
||
|
||
private async executeJoinSelect(stmt: SelectStatement, preloadedMain?: Record<string, unknown>[]): Promise<Record<string, unknown>[]> {
|
||
const mainAlias = stmt.alias ?? stmt.from;
|
||
// v0.4.0: 派生表行源已预加载(行带别名前缀)
|
||
let mainRows: Record<string, unknown>[];
|
||
if (preloadedMain) {
|
||
mainRows = preloadedMain;
|
||
} else {
|
||
// v0.4.1: WHERE 中主表前缀等值条件下推到引擎(走二级索引,如 WHERE o.user_id = '1')
|
||
const { pushable } = this.extractPushableWhere(stmt.where ?? {}, mainAlias);
|
||
mainRows = (await this.engine.find(stmt.from, {
|
||
table: stmt.from,
|
||
where: Object.keys(pushable).length > 0 ? pushable : undefined,
|
||
})).map((row) => this.prefixRow(row, mainAlias));
|
||
}
|
||
let resultRows = mainRows;
|
||
|
||
for (const join of stmt.joins!) {
|
||
const joinAlias = join.alias ?? join.table;
|
||
// v0.3.2: 等值 ON + 右列主键 → 哈希连接(一次 $in 查询替代嵌套循环)
|
||
const hashJoined = await this.tryHashJoin(resultRows, join, joinAlias, mainAlias);
|
||
if (hashJoined) {
|
||
resultRows = hashJoined;
|
||
continue;
|
||
}
|
||
const joinRows = (await this.engine.find(join.table, { table: join.table }))
|
||
.map((row) => this.prefixRow(row, joinAlias));
|
||
resultRows = this.joinRows(resultRows, joinRows, join);
|
||
}
|
||
|
||
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||
// v0.3.1: 关联子查询($col/EXISTS 引用外层行)→ 逐行绑定求值
|
||
if (this.hasCorrelatedRefs(stmt.where)) {
|
||
resultRows = await this.filterCorrelated(resultRows, stmt.where);
|
||
} else {
|
||
// 非关联子查询(IN (SELECT ...) 等),字段名保持别名前缀
|
||
stmt.where = await this.resolveSubqueries(stmt.where);
|
||
resultRows = resultRows.filter((row) => matchWhere(row, stmt.where));
|
||
}
|
||
}
|
||
return resultRows;
|
||
}
|
||
|
||
private prefixRow(row: Record<string, unknown>, alias: string): Record<string, unknown> {
|
||
const prefixed: Record<string, unknown> = {};
|
||
for (const [key, value] of Object.entries(row)) prefixed[`${alias}.${key}`] = value;
|
||
return prefixed;
|
||
}
|
||
|
||
/**
|
||
* v0.4.1: 提取可下推的 WHERE 条件 — 主表别名前缀的普通条件(如 o.user_id = '1')。
|
||
* 下推到引擎可走二级索引;$col/$subquery/$and/$or/$not 等复杂条件保守不下推。
|
||
*/
|
||
private extractPushableWhere(where: WhereCondition, mainAlias: string): { pushable: WhereCondition } {
|
||
const pushable: WhereCondition = {};
|
||
if (!mainAlias) return { pushable };
|
||
const prefix = `${mainAlias}.`;
|
||
for (const [key, value] of Object.entries(where)) {
|
||
if (!key.startsWith(prefix)) continue;
|
||
const v = value as Record<string, unknown> | null;
|
||
if (typeof v === 'object' && v !== null &&
|
||
('$col' in v || '$subquery' in v || '$and' in v || '$or' in v || '$not' in v)) {
|
||
continue;
|
||
}
|
||
pushable[key.slice(prefix.length)] = value;
|
||
}
|
||
return { pushable };
|
||
}
|
||
|
||
/**
|
||
* 哈希连接(v0.3.2 单等值 / v0.4.0 多列等值):
|
||
* ON 为等值条件(单列或多列)且右表任一列为索引/主键时,
|
||
* 收集左表连接值 → 一次 $in 查询右表 → 哈希映射匹配。
|
||
* 替代嵌套循环,大表 INNER/LEFT JOIN 复杂度 O(N + M)。
|
||
* 不适用时返回 null(回退嵌套循环)。
|
||
*/
|
||
private async tryHashJoin(
|
||
leftRows: Record<string, unknown>[],
|
||
join: JoinClause,
|
||
joinAlias: string,
|
||
mainAlias: string,
|
||
): Promise<Record<string, unknown>[] | null> {
|
||
if (join.type === 'CROSS' || join.type === 'RIGHT') return null;
|
||
|
||
// 解析 ON 为 (leftCol, rightCol) 等值对列表(v0.4.0 支持多列,含顶层 $and 展开)
|
||
const pairs: { leftCol: string; rightCol: string }[] = [];
|
||
const collectPairs = (on: WhereCondition): boolean => {
|
||
for (const [keyCol, cond] of Object.entries(on)) {
|
||
if (keyCol === '$and') {
|
||
if (!(cond as WhereCondition[]).every(collectPairs)) return false;
|
||
continue;
|
||
}
|
||
if (keyCol === '$or' || keyCol === '$not') return false; // 非等值逻辑不适用
|
||
let refCol: string | null = null;
|
||
if (typeof cond === 'object' && cond !== null) {
|
||
const c = cond as Record<string, unknown>;
|
||
if ('$eq' in c && typeof c.$eq === 'object' && c.$eq !== null && '$col' in (c.$eq as Record<string, unknown>)) {
|
||
refCol = String((c.$eq as Record<string, unknown>).$col);
|
||
} else if ('$col' in c && Object.keys(c).length === 1) {
|
||
refCol = String(c.$col);
|
||
}
|
||
}
|
||
if (!refCol) return false; // 非等值条件不适用哈希连接
|
||
|
||
const keyIsLeft = mainAlias ? keyCol.startsWith(`${mainAlias}.`) : false;
|
||
pairs.push({
|
||
leftCol: keyIsLeft ? keyCol : refCol,
|
||
rightCol: keyIsLeft ? refCol : keyCol,
|
||
});
|
||
}
|
||
return true;
|
||
};
|
||
if (!collectPairs(join.on)) return null;
|
||
if (pairs.length === 0) return null;
|
||
|
||
// 右表列必须是主键/索引列(确保 $in 走索引)——任一列即可
|
||
const schema = await this.engine.getTableSchema(join.table);
|
||
if (!schema) return null;
|
||
const probePair = pairs.find((p) => {
|
||
const bare = p.rightCol.split('.').pop()!;
|
||
const colDef = schema.columns[bare];
|
||
return colDef && (colDef.primaryKey || colDef.index || colDef.unique);
|
||
});
|
||
if (!probePair) return null;
|
||
|
||
// 收集左表连接值(去重)——用探测列的值缩小候选集
|
||
const probeRightBare = probePair.rightCol.split('.').pop()!;
|
||
const values = Array.from(new Set(leftRows.map((r) => r[probePair.leftCol]).filter((v) => v !== undefined && v !== null)));
|
||
if (values.length === 0) return null;
|
||
|
||
// 一次 $in 查询右表(缩小候选集)
|
||
const rightRows = await this.engine.find(join.table, {
|
||
table: join.table,
|
||
where: { [probeRightBare]: { $in: values } },
|
||
});
|
||
|
||
// 构建复合键哈希映射:右表多列值 → 行列表
|
||
const hash = new Map<string, Record<string, unknown>[]>();
|
||
for (const rr of rightRows) {
|
||
const key = pairs.map((p) => String(rr[p.rightCol.split('.').pop()!] ?? '\0')).join('\x1f');
|
||
if (!hash.has(key)) hash.set(key, []);
|
||
hash.get(key)!.push(rr);
|
||
}
|
||
|
||
const nullRight: Record<string, unknown> = {};
|
||
for (const key of Object.keys(schema.columns)) nullRight[key] = null;
|
||
|
||
const result: Record<string, unknown>[] = [];
|
||
for (const l of leftRows) {
|
||
const key = pairs.map((p) => String(l[p.leftCol] ?? '\0')).join('\x1f');
|
||
const matches = hash.get(key);
|
||
if (matches && matches.length > 0) {
|
||
for (const r of matches) {
|
||
result.push({ ...l, ...this.prefixRow(r, joinAlias) });
|
||
}
|
||
} else if (join.type === 'LEFT') {
|
||
// LEFT JOIN 无匹配 → 右表列置 null
|
||
result.push({ ...l, ...this.prefixRow(nullRight, joinAlias) });
|
||
}
|
||
// INNER JOIN 无匹配 → 跳过
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/** 嵌套循环连接(优化:避免 ON 时对象扩散) */
|
||
private joinRows(
|
||
leftRows: Record<string, unknown>[],
|
||
rightRows: Record<string, unknown>[],
|
||
join: JoinClause,
|
||
): Record<string, unknown>[] {
|
||
if (join.type === 'CROSS') {
|
||
const result: Record<string, unknown>[] = [];
|
||
for (const l of leftRows) for (const r of rightRows) result.push({ ...l, ...r });
|
||
return result;
|
||
}
|
||
|
||
const result: Record<string, unknown>[] = [];
|
||
for (const l of leftRows) {
|
||
let matched = false;
|
||
for (const r of rightRows) {
|
||
// 合并后匹配 ON(避免创建临时对象再丢弃)
|
||
const merged = { ...l, ...r };
|
||
if (matchWhere(merged, join.on, { $col: true })) {
|
||
result.push(merged);
|
||
matched = true;
|
||
}
|
||
}
|
||
if (!matched && join.type === 'LEFT') {
|
||
const nullRight: Record<string, unknown> = {};
|
||
for (const key of Object.keys(rightRows[0] ?? {})) nullRight[key] = null;
|
||
result.push({ ...l, ...nullRight });
|
||
}
|
||
}
|
||
|
||
if (join.type === 'RIGHT') {
|
||
for (const r of rightRows) {
|
||
const isMatched = leftRows.some((l) => {
|
||
const merged = { ...l, ...r };
|
||
return matchWhere(merged, join.on, { $col: true });
|
||
});
|
||
if (!isMatched) {
|
||
const nullLeft: Record<string, unknown> = {};
|
||
for (const key of Object.keys(leftRows[0] ?? {})) nullLeft[key] = null;
|
||
result.push({ ...nullLeft, ...r });
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ---- GROUP BY ----
|
||
|
||
private executeGroupBy(rows: Record<string, unknown>[], stmt: SelectStatement): Record<string, unknown>[] {
|
||
const groups = new Map<string, Record<string, unknown>[]>();
|
||
for (const row of rows) {
|
||
// v0.7.4: 类型安全键编码 —— 此前 String(row[col] ?? 'null') 使
|
||
// null 与字符串 'null' 合并为一组(GROUP BY 静默少组)
|
||
const key = stmt.groupBy!.map((col) => encodeGroupKey(row[col])).join('\x1f');
|
||
if (!groups.has(key)) groups.set(key, []);
|
||
groups.get(key)!.push(row);
|
||
}
|
||
const result: Record<string, unknown>[] = [];
|
||
// v0.4.0: 聚合表达式键 → 输出键 映射(HAVING SUM(...) 引用表达式时归一为别名键)
|
||
const aliasMap = new Map<string, string>();
|
||
for (const groupRows of groups.values()) {
|
||
const aggregated: Record<string, unknown> = {};
|
||
for (const col of stmt.groupBy!) aggregated[col] = groupRows[0][col];
|
||
for (const colExpr of stmt.columns) {
|
||
if (colExpr === '*') continue;
|
||
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
|
||
if (m) {
|
||
const [, func, arg, alias] = m;
|
||
const value = this.computeAggregate(func.toUpperCase(), groupRows, arg.trim());
|
||
const exprKey = `${func.toUpperCase()}(${arg.trim()})`;
|
||
const outputKey = alias || colExpr;
|
||
if (outputKey !== exprKey) aliasMap.set(exprKey, outputKey);
|
||
aggregated[outputKey] = value;
|
||
} else if (/^\s*CASE\b/i.test(colExpr)) {
|
||
// v0.3.2: 非聚合的 CASE WHEN 列取组内第一行求值
|
||
const expr = parseCaseExpression(colExpr);
|
||
aggregated[expr?.alias ?? colExpr] = expr ? evaluateCase(expr, groupRows[0]) : null;
|
||
} else if (!stmt.groupBy!.includes(colExpr)) {
|
||
aggregated[colExpr] = groupRows[0][colExpr];
|
||
}
|
||
}
|
||
result.push(aggregated);
|
||
}
|
||
(stmt as unknown as { _aggAliasMap?: Map<string, string> })._aggAliasMap = aliasMap;
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 计算单个聚合值。
|
||
*
|
||
* v0.8.0: 返回类型放宽为 unknown —— SUM/AVG/MIN/MAX 对空集返回 null(SQL 标准),
|
||
* COUNT 仍返回 number。最小/最大改为单次遍历(不再展开实参,消除栈溢出)。
|
||
*/
|
||
private computeAggregate(func: string, rows: Record<string, unknown>[], col: string): number | null {
|
||
// v0.3.2: 聚合参数支持 CASE WHEN 表达式(如 SUM(CASE WHEN age > 18 THEN 1 ELSE 0 END))
|
||
const caseExpr = /^\s*CASE\b/i.test(col) ? parseCaseExpression(col) : null;
|
||
// v0.4.0: COUNT(DISTINCT col) 等去重聚合
|
||
const distinctArg = !caseExpr && /^\s*DISTINCT\s+/i.test(col);
|
||
const argCol = distinctArg ? col.replace(/^\s*DISTINCT\s+/i, '').trim() : col;
|
||
const rawValues = rows
|
||
.map((r) => (caseExpr ? evaluateCase(caseExpr, r) : r[argCol]))
|
||
.filter((v) => v !== null && v !== undefined);
|
||
// v0.4.0: COUNT 对原始值去重(任意类型);数值聚合在类型转换后去重
|
||
if (func === 'COUNT') {
|
||
if (argCol === '*') return rows.length;
|
||
if (distinctArg) {
|
||
return new Set(rawValues.map((v) => (typeof v === 'object' ? JSON.stringify(v) : String(v)))).size;
|
||
}
|
||
return rawValues.length;
|
||
}
|
||
const nums = rawValues.map(Number);
|
||
// v0.8.0: 数值型聚合一律走单次遍历归约。
|
||
//
|
||
// 此前 MIN/MAX 用 `Math.min(...distinctNums)` 展开实参:20 万行同组直接
|
||
// `RangeError: Maximum call stack size exceeded`(原生错误,调用方无法按 code 分类)。
|
||
// 同时把"空集/全 NULL"的返回值从 0 改为 null —— SQL 标准中 SUM/AVG/MIN/MAX
|
||
// 对空集返回 NULL,返回 0 会让 `SUM(x) = 0` 与"没有数据"不可区分。
|
||
const distinctNums = distinctArg ? Array.from(new Set(nums)) : nums;
|
||
switch (func) {
|
||
case 'SUM': return reduceNumeric(distinctNums, 'SUM');
|
||
case 'AVG': return reduceNumeric(distinctNums, 'AVG');
|
||
case 'MIN': return reduceNumeric(distinctNums, 'MIN');
|
||
case 'MAX': return reduceNumeric(distinctNums, 'MAX');
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
// ---- DISTINCT(优化:列值拼接代替 JSON.stringify) ----
|
||
|
||
private executeDistinct(rows: Record<string, unknown>[]): Record<string, unknown>[] {
|
||
const seen = new Set<string>();
|
||
return rows.filter((row) => {
|
||
// v0.7.4: 类型安全键编码(null 与 'null' 字符串、'\0' 分离)
|
||
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
|
||
if (seen.has(key)) return false;
|
||
seen.add(key);
|
||
return true;
|
||
});
|
||
}
|
||
|
||
// ===================================================================
|
||
// 其他语句
|
||
// ===================================================================
|
||
|
||
private async executeInsert(stmt: InsertStatement): Promise<string[]> {
|
||
const schema = await this.engine.getTableSchema(stmt.into);
|
||
if (!schema) throw new DatabaseError(`Table "${stmt.into}" does not exist`, 'TABLE_NOT_FOUND');
|
||
const colNames = stmt.columns ?? Object.keys(schema.columns);
|
||
|
||
// INSERT INTO ... SELECT ...(v0.3.0)
|
||
if (stmt.select) {
|
||
const selectRows = await this.executeSelectPart(stmt.select);
|
||
// v0.4.0 修复:源列顺序不能依赖行键(validateRow 会跳过 undefined 导致行键缺失/乱序)。
|
||
// 以 SELECT 列列表 / 源表 schema 列顺序为准,按位置对齐目标列,缺列不填。
|
||
let srcCols: string[] = [];
|
||
const sel = stmt.select;
|
||
if (sel.type === 'SELECT') {
|
||
if (sel.columns && sel.columns.length > 0 && sel.columns[0] !== '*') {
|
||
srcCols = sel.columns.map((c) => c.split('.').pop()!);
|
||
} else if (sel.from) {
|
||
const srcSchema = await this.engine.getTableSchema(sel.from);
|
||
srcCols = srcSchema ? Object.keys(srcSchema.columns) : [];
|
||
}
|
||
}
|
||
if (srcCols.length === 0 && selectRows.length > 0) {
|
||
srcCols = Object.keys(selectRows[0]);
|
||
}
|
||
const rows: Record<string, unknown>[] = selectRows.map((row) => {
|
||
const mapped: Record<string, unknown> = {};
|
||
for (let i = 0; i < colNames.length; i++) {
|
||
const src = i < srcCols.length ? srcCols[i] : null;
|
||
if (src && src in row) mapped[colNames[i]] = row[src];
|
||
}
|
||
return mapped;
|
||
});
|
||
return this.engine.insert(stmt.into, rows);
|
||
}
|
||
|
||
const rows: Record<string, unknown>[] = (stmt.values ?? []).map((vals: unknown[]) => {
|
||
const row: Record<string, unknown> = {};
|
||
for (let i = 0; i < colNames.length; i++) { if (i < vals.length) row[colNames[i]] = vals[i]; }
|
||
return row;
|
||
});
|
||
return this.engine.insert(stmt.into, rows);
|
||
}
|
||
|
||
private async executeUpdate(stmt: UpdateStatement): Promise<number> {
|
||
// v0.7.4: 先解析 WHERE 子查询 —— 此前直接 compileStatement 调引擎:
|
||
// 引擎层 matchWhere 的 $in/$nin 遇未解析的 $subquery 对象恒 false →
|
||
// 所有行不匹配,UPDATE 静默影响 0 行(与 queryStream v0.7.3 修复同类)。
|
||
await this.resolveWriteWhere(stmt);
|
||
const plan = compileStatement(stmt);
|
||
plan.where = stmt.where;
|
||
return this.engine.update(plan.table, plan, stmt.sets);
|
||
}
|
||
|
||
private async executeDelete(stmt: DeleteStatement): Promise<number> {
|
||
// v0.7.4: 同 executeUpdate —— DELETE 子查询 WHERE 此前静默删除 0 行
|
||
await this.resolveWriteWhere(stmt);
|
||
const plan = compileStatement(stmt);
|
||
plan.where = stmt.where;
|
||
return this.engine.delete(plan.table, plan);
|
||
}
|
||
|
||
/**
|
||
* v0.7.4: 写语句(UPDATE/DELETE)WHERE 的子查询解析。
|
||
* 非关联子查询($subquery)解析为具体值列表/标量;
|
||
* 关联引用($col / 关联 EXISTS)在写语句中无法逐行绑定外层上下文
|
||
* (引擎层 matchWhere 无 $col 绑定选项)→ 显式 NOT_SUPPORTED 而非静默 0 行。
|
||
*/
|
||
private async resolveWriteWhere(stmt: UpdateStatement | DeleteStatement): Promise<WhereCondition> {
|
||
const where = stmt.where;
|
||
if (!where || Object.keys(where).length === 0) return where ?? {};
|
||
if (this.hasCorrelatedRefs(where)) {
|
||
throw new DatabaseError(
|
||
'Correlated subqueries and column references are not supported in UPDATE/DELETE WHERE clauses',
|
||
'NOT_SUPPORTED',
|
||
);
|
||
}
|
||
stmt.where = await this.resolveSubqueries(where);
|
||
return stmt.where;
|
||
}
|
||
|
||
private async executeCreateTable(stmt: CreateTableStatement): Promise<void> {
|
||
// IF NOT EXISTS: 表已存在时静默返回
|
||
if (stmt.ifNotExists) {
|
||
const exists = await this.engine.hasTable(stmt.name);
|
||
if (exists) return;
|
||
}
|
||
// v0.7.1: Object.create(null) —— 防止 '__proto__' 列名触发原型 setter 静默丢列
|
||
// (createSchema 校验会显式拒绝该列名)
|
||
const columns: Record<string, any> = Object.create(null) as Record<string, any>;
|
||
for (const col of stmt.columns) columns[col.name] = astColumnToColumnDef(col);
|
||
return this.engine.createTable(createSchema(stmt.name, columns));
|
||
}
|
||
|
||
private async executeDropTable(stmt: DropTableStatement): Promise<void> {
|
||
if (stmt.ifExists) {
|
||
const exists = await this.engine.hasTable(stmt.name);
|
||
if (!exists) return; // IF EXISTS: 表不存在时静默返回
|
||
}
|
||
return this.engine.dropTable(stmt.name);
|
||
}
|
||
|
||
private async executeAlterTable(stmt: AlterTableStatement): Promise<void> {
|
||
const exists = await this.engine.hasTable(stmt.name);
|
||
if (!exists) throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||
const schema = await this.engine.getTableSchema(stmt.name);
|
||
if (!schema) return;
|
||
|
||
// v0.7.0: ALTER ADD 主键列防护 —— 复合主键不支持(与 createSchema 校验对齐),
|
||
// 避免绕过建表校验添加第二个主键列导致语义陷阱
|
||
if (stmt.action === 'ADD' && stmt.column.primaryKey) {
|
||
const hasPk = Object.values(schema.columns).some((c) => c.primaryKey);
|
||
if (hasPk) {
|
||
throw new DatabaseError(
|
||
`Composite primary keys are not supported yet: table "${stmt.name}" already has a primary key column`,
|
||
'SCHEMA_ERROR',
|
||
);
|
||
}
|
||
}
|
||
|
||
// v0.4.1: 引擎级 alterTable(Aria 需重写存储行 + 持久化 schema;其余引擎走通用引用路径)
|
||
if (typeof this.engine.alterTable === 'function') {
|
||
return this.engine.alterTable(stmt.name, stmt.action, { ...astColumnToColumnDef(stmt.column), name: stmt.column.name });
|
||
}
|
||
|
||
if (stmt.action === 'ADD') {
|
||
if (schema.columns[stmt.column.name]) {
|
||
throw new DatabaseError(`Column "${stmt.column.name}" already exists in table "${stmt.name}"`, 'COLUMN_EXISTS');
|
||
}
|
||
// 直接在 schema 引用上添加列(已有行的该列值为 undefined/default)
|
||
schema.columns[stmt.column.name] = astColumnToColumnDef(stmt.column);
|
||
} else if (stmt.action === 'DROP') {
|
||
if (!schema.columns[stmt.column.name]) {
|
||
throw new DatabaseError(`Column "${stmt.column.name}" does not exist in table "${stmt.name}"`, 'COLUMN_NOT_FOUND');
|
||
}
|
||
// 从 schema 引用上删除列定义(保留所有行数据)
|
||
delete schema.columns[stmt.column.name];
|
||
// 清除已有行中该列的值(MemoryEngine 的 find 返回引用,delete 直接生效)
|
||
const rows = await this.engine.find(stmt.name, { table: stmt.name });
|
||
const colName = stmt.column.name;
|
||
for (const row of rows) {
|
||
if (colName in row) delete row[colName];
|
||
}
|
||
}
|
||
}
|
||
|
||
private async executeTruncateTable(stmt: TruncateTableStatement): Promise<void> {
|
||
const exists = await this.engine.hasTable(stmt.name);
|
||
if (!exists) throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||
return this.engine.clear(stmt.name);
|
||
}
|
||
|
||
// ===================================================================
|
||
// CREATE INDEX / DROP INDEX(v0.3.0)
|
||
// ===================================================================
|
||
|
||
private async executeCreateIndex(stmt: CreateIndexStatement): Promise<void> {
|
||
const exists = await this.engine.hasTable(stmt.table);
|
||
if (!exists) throw new DatabaseError(`Table "${stmt.table}" does not exist`, 'TABLE_NOT_FOUND');
|
||
|
||
const schema = await this.engine.getTableSchema(stmt.table);
|
||
if (schema && !schema.columns[stmt.column]) {
|
||
throw new DatabaseError(`Column "${stmt.column}" does not exist in table "${stmt.table}"`, 'COLUMN_NOT_FOUND');
|
||
}
|
||
|
||
if (typeof this.engine.createIndex !== 'function') {
|
||
throw new DatabaseError(
|
||
`Engine "${this.engine.name}" does not support CREATE INDEX`,
|
||
'NOT_SUPPORTED',
|
||
);
|
||
}
|
||
return this.engine.createIndex(stmt.table, stmt.column, stmt.unique);
|
||
}
|
||
|
||
private async executeDropIndex(stmt: DropIndexStatement): Promise<void> {
|
||
if (typeof this.engine.dropIndex !== 'function') {
|
||
throw new DatabaseError(
|
||
`Engine "${this.engine.name}" does not support DROP INDEX`,
|
||
'NOT_SUPPORTED',
|
||
);
|
||
}
|
||
return this.engine.dropIndex(stmt.table, stmt.column, stmt.name);
|
||
}
|
||
|
||
// ===================================================================
|
||
// 事务语句(v0.3.0)
|
||
// ===================================================================
|
||
|
||
private async executeBegin(): Promise<void> {
|
||
return this.engine.beginTransaction();
|
||
}
|
||
|
||
private async executeCommit(): Promise<void> {
|
||
return this.engine.commitTransaction();
|
||
}
|
||
|
||
private async executeRollback(): Promise<void> {
|
||
return this.engine.rollbackTransaction();
|
||
}
|
||
|
||
// ===================================================================
|
||
// 维护语句(v0.5.1)
|
||
// ===================================================================
|
||
|
||
/** SAVEPOINT name / ROLLBACK TO SAVEPOINT name / RELEASE SAVEPOINT name */
|
||
private async executeSavepoint(stmt: import('./ast').SavepointStatement): Promise<void> {
|
||
const engine = this.engine as unknown as import('./ast').SavepointStatement & {
|
||
savepoint?: (name: string) => Promise<void>;
|
||
rollbackToSavepoint?: (name: string) => Promise<void>;
|
||
releaseSavepoint?: (name: string) => Promise<void>;
|
||
};
|
||
if (stmt.action === 'SAVE') {
|
||
if (typeof engine.savepoint !== 'function') {
|
||
throw new DatabaseError(`Engine "${this.engine.name}" does not support SAVEPOINT`, 'NOT_SUPPORTED');
|
||
}
|
||
return engine.savepoint(stmt.name);
|
||
}
|
||
if (stmt.action === 'ROLLBACK') {
|
||
if (typeof engine.rollbackToSavepoint !== 'function') {
|
||
throw new DatabaseError(`Engine "${this.engine.name}" does not support ROLLBACK TO SAVEPOINT`, 'NOT_SUPPORTED');
|
||
}
|
||
return engine.rollbackToSavepoint(stmt.name);
|
||
}
|
||
if (typeof engine.releaseSavepoint !== 'function') {
|
||
throw new DatabaseError(`Engine "${this.engine.name}" does not support RELEASE SAVEPOINT`, 'NOT_SUPPORTED');
|
||
}
|
||
return engine.releaseSavepoint(stmt.name);
|
||
}
|
||
|
||
/** ANALYZE TABLE name — 收集表统计信息 */
|
||
private async executeAnalyze(stmt: import('./ast').AnalyzeStatement): Promise<Record<string, unknown>> {
|
||
const engine = this.engine as unknown as import('./ast').AnalyzeStatement & {
|
||
analyzeTable?: (table: string) => Promise<Record<string, unknown>>;
|
||
};
|
||
if (typeof engine.analyzeTable !== 'function') {
|
||
throw new DatabaseError(`Engine "${this.engine.name}" does not support ANALYZE`, 'NOT_SUPPORTED');
|
||
}
|
||
const exists = await this.engine.hasTable(stmt.table);
|
||
if (!exists) throw new DatabaseError(`Table "${stmt.table}" does not exist`, 'TABLE_NOT_FOUND');
|
||
return engine.analyzeTable(stmt.table);
|
||
}
|
||
|
||
/** REINDEX TABLE name — 重建表二级索引 */
|
||
private async executeReindex(stmt: import('./ast').ReindexStatement): Promise<number> {
|
||
const engine = this.engine as unknown as import('./ast').ReindexStatement & {
|
||
reindexTable?: (table: string) => Promise<number>;
|
||
};
|
||
if (typeof engine.reindexTable !== 'function') {
|
||
throw new DatabaseError(`Engine "${this.engine.name}" does not support REINDEX`, 'NOT_SUPPORTED');
|
||
}
|
||
const exists = await this.engine.hasTable(stmt.table);
|
||
if (!exists) throw new DatabaseError(`Table "${stmt.table}" does not exist`, 'TABLE_NOT_FOUND');
|
||
return engine.reindexTable(stmt.table);
|
||
}
|
||
|
||
/** VACUUM — 压缩 LSM + 清理碎片 */
|
||
private async executeVacuum(): Promise<unknown> {
|
||
const engine = this.engine as unknown as import('./ast').VacuumStatement & {
|
||
vacuum?: () => Promise<unknown>;
|
||
};
|
||
if (typeof engine.vacuum !== 'function') {
|
||
throw new DatabaseError(`Engine "${this.engine.name}" does not support VACUUM`, 'NOT_SUPPORTED');
|
||
}
|
||
return engine.vacuum();
|
||
}
|
||
|
||
/** 列列表是否包含 CASE WHEN 表达式 */
|
||
private hasCaseColumn(columns: string[]): boolean {
|
||
return columns.some((col) => /^\s*CASE\b/i.test(col));
|
||
}
|
||
|
||
/**
|
||
* v0.3.3: ORDER BY 是否引用 SELECT 别名(如 `SELECT name AS n ... ORDER BY n`)。
|
||
* 别名列在引擎层投影前不存在,需投影后重新排序。
|
||
*/
|
||
private orderByUsesSelectAlias(stmt: SelectStatement): boolean {
|
||
if (!stmt.orderBy || stmt.orderBy.length === 0) return false;
|
||
const aliases = new Set<string>();
|
||
for (const col of stmt.columns) {
|
||
const m = col.match(/\s+AS\s+(\w+)$/i);
|
||
if (m) aliases.add(m[1]);
|
||
else if (/^\s*CASE\b/i.test(col)) {
|
||
const expr = parseCaseExpression(col);
|
||
if (expr?.alias) aliases.add(expr.alias);
|
||
}
|
||
}
|
||
if (aliases.size === 0) return false;
|
||
return stmt.orderBy.some((o) => aliases.has(o.column));
|
||
}
|
||
|
||
/** WHERE 是否包含 CASE WHEN 表达式键 */
|
||
private whereHasCase(where: WhereCondition): boolean {
|
||
for (const [key, value] of Object.entries(where)) {
|
||
if (key === '$and' || key === '$or') {
|
||
if ((value as WhereCondition[]).some((sub) => this.whereHasCase(sub))) return true;
|
||
continue;
|
||
}
|
||
if (key === '$not') {
|
||
if (this.whereHasCase(value as WhereCondition)) return true;
|
||
continue;
|
||
}
|
||
if (/^\s*CASE\b/i.test(key)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
getEngine(): IStorageEngine { return this.engine; }
|
||
|
||
/**
|
||
* v0.8.0: 校验 SELECT 列表中的**裸列引用**在结果行里确实存在,否则抛 COLUMN_NOT_FOUND。
|
||
*
|
||
* 为什么必须做:`SELECT bogus FROM t` 此前返回 `[{},{},...]`(行数对、内容空、无报错),
|
||
* 这是"静默错误结果"里最难被发现的一类 —— 调用方拿到的是结构正确但全空的表格。
|
||
*
|
||
* 判定规则(与 projectRow 的分类保持一致):
|
||
* - `*` 跳过;
|
||
* - 字符串/数字常量列跳过;
|
||
* - CASE 表达式跳过(其内部列引用由 evaluateCase 处理);
|
||
* - `expr AS alias`:字符串/数字常量跳过,否则取 `expr` 作为被引用列;
|
||
* - 其余视为裸列引用。
|
||
* 存在性检查允许两种形态:精确匹配,或**唯一**以 `.<col>` 结尾(JOIN 行以 `alias.col` 为键)。
|
||
* 若同一个后缀出现在多个表别名下则视为歧义,同样报错(符合"未限定列名歧义应报错"的语义)。
|
||
*
|
||
* 结果集为空时无法判定,此时跳过(空表 + 未知列不会误报)。
|
||
*/
|
||
private assertProjectionColumnsExist(
|
||
rows: Record<string, unknown>[],
|
||
columns: string[],
|
||
stmt: SelectStatement,
|
||
): void {
|
||
if (rows.length === 0 || columns.length === 0) return;
|
||
|
||
const available = new Set<string>();
|
||
for (const row of rows) {
|
||
for (const key of Object.keys(row)) available.add(key);
|
||
}
|
||
|
||
for (const raw of columns) {
|
||
const col = raw.trim();
|
||
if (col === '*') continue;
|
||
if (parseCaseExpression(col)) continue;
|
||
|
||
let reference = col;
|
||
const aliasMatch = col.match(/^(.+?)\s+AS\s+\w+$/i);
|
||
if (aliasMatch) reference = aliasMatch[1].trim();
|
||
|
||
// 常量列(字符串 / 数字 / 布尔 / NULL)
|
||
if (/^'.*'$/s.test(reference)) continue;
|
||
if (/^-?\d+(\.\d+)?$/.test(reference)) continue;
|
||
if (/^(TRUE|FALSE|NULL)$/i.test(reference)) continue;
|
||
// 聚合表达式(在 hasAggregate 分支已跳过,这里再兜一层防御)
|
||
if (/^(COUNT|SUM|AVG|MIN|MAX)\s*\(/i.test(reference)) continue;
|
||
|
||
if (available.has(reference)) continue;
|
||
|
||
const suffixMatches: string[] = [];
|
||
for (const key of available) {
|
||
if (key.endsWith(`.${reference}`)) suffixMatches.push(key);
|
||
}
|
||
if (suffixMatches.length === 1) continue;
|
||
|
||
if (suffixMatches.length > 1) {
|
||
const owners = suffixMatches.map((k) => k.slice(0, k.length - reference.length - 1)).sort();
|
||
throw new DatabaseError(
|
||
`Ambiguous column "${reference}" in SELECT list: present in ${owners.join(', ')}`,
|
||
'COLUMN_NOT_FOUND',
|
||
{ column: reference, tables: owners, from: stmt.from },
|
||
);
|
||
}
|
||
|
||
throw new DatabaseError(
|
||
`Unknown column "${reference}" in SELECT list`,
|
||
'COLUMN_NOT_FOUND',
|
||
{ column: reference, from: stmt.from, available: [...available].slice(0, 32) },
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 列投影(v0.3.1):普通列走 projectColumns,CASE WHEN 表达式逐行求值;
|
||
* v0.3.3: 支持 `col AS alias` 列别名
|
||
*/
|
||
private projectRow(row: Record<string, unknown>, columns: string[]): Record<string, unknown> {
|
||
const plain: string[] = [];
|
||
const aliasCols: { alias: string; source: string }[] = [];
|
||
const caseCols: { alias: string; expr: CaseExpression }[] = [];
|
||
const constCols: { key: string; value: unknown }[] = [];
|
||
// v0.7.3: 裸 '*' 与列表达式混合(SELECT *, name AS nick)→ 原行全部列为基
|
||
let hasStar = false;
|
||
for (const col of columns) {
|
||
if (col === '*') {
|
||
hasStar = true;
|
||
continue;
|
||
}
|
||
const expr = parseCaseExpression(col);
|
||
if (expr) {
|
||
caseCols.push({ alias: expr.alias ?? col, expr });
|
||
continue;
|
||
}
|
||
const m = col.match(/^(.+?)\s+AS\s+(\w+)$/i);
|
||
if (m) {
|
||
aliasCols.push({ alias: m[2], source: m[1].trim() });
|
||
continue;
|
||
}
|
||
// v0.4.0: 字符串常量列 SELECT 'lit' → 常量输出
|
||
const lit = col.match(/^'(.*)'$/s);
|
||
if (lit) {
|
||
// v0.7.3: SQL 标准 '' 转义还原(readString 已把 '' 合并为单个 ',
|
||
// 打包回列的文本中相邻两个 ' 即一个引号字面量)
|
||
const value = lit[1].replace(/''/g, "'");
|
||
constCols.push({ key: col, value });
|
||
continue;
|
||
}
|
||
// v0.8.0: 匿名常量列(数字/布尔/NULL)—— `SELECT 1 FROM t` 此前投影成 {}
|
||
// (键 '1' 在、值为 undefined,JSON 序列化后键消失)。SQLite/MySQL 用
|
||
// 表达式原文作列名,这里保持一致。
|
||
if (/^-?\d+(\.\d+)?$/.test(col) || /^(TRUE|FALSE|NULL)$/i.test(col)) {
|
||
constCols.push({ key: col, value: resolveAliasSource(col, row) });
|
||
continue;
|
||
}
|
||
plain.push(col);
|
||
}
|
||
// v0.7.3: hasStar 时以原行全部列为基(projectColumns 仅投影 plain 列,不含 * 的其余列)
|
||
const projected = hasStar
|
||
? { ...row }
|
||
: (plain.length > 0 ? projectColumns(row, plain) : {});
|
||
for (const { alias, source } of aliasCols) {
|
||
if (source === '*') {
|
||
Object.assign(projected, row);
|
||
} else {
|
||
projected[alias] = resolveAliasSource(source, row);
|
||
}
|
||
}
|
||
for (const { key, value } of constCols) {
|
||
projected[key] = value;
|
||
}
|
||
for (const { alias, expr } of caseCols) {
|
||
projected[alias] = evaluateCase(expr, row);
|
||
}
|
||
return projected;
|
||
}
|
||
|
||
// ===================================================================
|
||
// 无 GROUP BY 时的聚合计算
|
||
// ===================================================================
|
||
|
||
/** 检查 SELECT 列列表中是否包含聚合函数 */
|
||
private _hasAggregateColumn(columns: string[]): boolean {
|
||
return columns.some((col) => /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(col));
|
||
}
|
||
|
||
/** 计算单行聚合结果(无 GROUP BY) */
|
||
private computeSingleAggregate(rows: Record<string, unknown>[], stmt: SelectStatement): Record<string, unknown> {
|
||
const result: Record<string, unknown> = {};
|
||
for (const colExpr of stmt.columns) {
|
||
if (colExpr === '*') continue;
|
||
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
|
||
if (m) {
|
||
const [, func, arg, alias] = m;
|
||
result[alias || colExpr] = this.computeAggregate(func.toUpperCase(), rows, arg.trim());
|
||
} else {
|
||
// 非聚合列取第一行的值
|
||
result[colExpr] = rows.length > 0 ? rows[0][colExpr] : null;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ===================================================================
|
||
// 关联子查询 / 别名规范化(v0.3.0)
|
||
// ===================================================================
|
||
|
||
/** 剥离主表别名前缀:'u.id' → 'id'(键与 $col 值均处理,支持多层别名) */
|
||
private normalizeWhereColumns(where: WhereCondition, aliases: string[]): WhereCondition {
|
||
const normalized: WhereCondition = {};
|
||
for (const [key, value] of Object.entries(where)) {
|
||
if (key === '$and' || key === '$or') {
|
||
normalized[key] = (value as WhereCondition[]).map((sub) => this.normalizeWhereColumns(sub, aliases));
|
||
continue;
|
||
}
|
||
if (key === '$not') {
|
||
normalized.$not = this.normalizeWhereColumns(value as WhereCondition, aliases);
|
||
continue;
|
||
}
|
||
if (key === '$exists') {
|
||
normalized[key] = this.normalizeExistsValue(value, aliases);
|
||
continue;
|
||
}
|
||
const newKey = this.stripAlias(key, aliases);
|
||
normalized[newKey] = this.normalizeFieldValue(value, aliases);
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
private normalizeExistsValue(value: unknown, aliases: string[]): unknown {
|
||
if (typeof value !== 'object' || value === null) return value;
|
||
const v = value as Record<string, unknown>;
|
||
if (v.$subquery) {
|
||
const sub = v.$subquery as SelectStatement;
|
||
// 子查询 where 需同时识别:子查询自身别名 + 外层别名(关联引用)
|
||
const subAliases = [sub.alias ?? sub.from, ...aliases].filter(Boolean);
|
||
return { ...v, $subquery: { ...sub, where: this.normalizeWhereColumns(sub.where, subAliases) } };
|
||
}
|
||
return value;
|
||
}
|
||
|
||
private normalizeFieldValue(value: unknown, aliases: string[]): unknown {
|
||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return value;
|
||
const ops: Record<string, unknown> = {};
|
||
for (const [op, operand] of Object.entries(value as Record<string, unknown>)) {
|
||
if (op === '$and' || op === '$or') {
|
||
ops[op] = (operand as WhereCondition[]).map((sub) => this.normalizeWhereColumns(sub, aliases));
|
||
} else if (op === '$not' && typeof operand === 'object' && operand !== null) {
|
||
ops[op] = this.normalizeFieldValue(operand, aliases);
|
||
} else if (op === '$col') {
|
||
ops[op] = this.stripAlias(String(operand), aliases);
|
||
} else if (typeof operand === 'object' && operand !== null && !Array.isArray(operand) && '$col' in (operand as Record<string, unknown>)) {
|
||
// 操作符值中嵌套的列引用:{ $eq: { $col: 'u.id' } }
|
||
ops[op] = { $col: this.stripAlias(String((operand as Record<string, unknown>).$col), aliases) };
|
||
} else {
|
||
ops[op] = operand;
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
private stripAlias(col: string, aliases: string[]): string {
|
||
for (const alias of aliases) {
|
||
if (!alias) continue;
|
||
const prefix = `${alias}.`;
|
||
if (col.startsWith(prefix)) return col.slice(prefix.length);
|
||
}
|
||
return col;
|
||
}
|
||
|
||
/** WHERE 是否含关联引用($col 或关联 EXISTS)或 CASE WHEN 表达式键 */
|
||
private hasCorrelatedRefs(where: WhereCondition): boolean {
|
||
for (const [key, value] of Object.entries(where)) {
|
||
if (key === '$and' || key === '$or') {
|
||
if ((value as WhereCondition[]).some((sub) => this.hasCorrelatedRefs(sub))) return true;
|
||
continue;
|
||
}
|
||
if (key === '$not') {
|
||
if (this.hasCorrelatedRefs(value as WhereCondition)) return true;
|
||
continue;
|
||
}
|
||
if (key === '$exists') {
|
||
// 关联 EXISTS:子查询 where 含 $col 或主 where 含 $negate 未解析标记
|
||
if (typeof value === 'object' && value !== null && '$subquery' in (value as Record<string, unknown>)) {
|
||
return true; // 关联 EXISTS 统一走逐行求值
|
||
}
|
||
continue;
|
||
}
|
||
// v0.3.2: CASE WHEN 表达式键(逐行求值)
|
||
if (/^\s*CASE\b/i.test(key)) return true;
|
||
if (this.fieldHasColRef(value)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private fieldHasColRef(value: unknown): boolean {
|
||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
||
const ops = value as Record<string, unknown>;
|
||
if ('$col' in ops) return true;
|
||
if ('$and' in ops || '$or' in ops) {
|
||
const subs = (ops.$and ?? ops.$or) as WhereCondition[];
|
||
return subs.some((sub) => this.hasCorrelatedRefs(sub));
|
||
}
|
||
if ('$not' in ops && typeof ops.$not === 'object' && ops.$not !== null) {
|
||
return this.fieldHasColRef(ops.$not);
|
||
}
|
||
// 操作符值中嵌套的列引用:{ $eq: { $col: 'id' } }
|
||
for (const [, operand] of Object.entries(ops)) {
|
||
if (typeof operand === 'object' && operand !== null && !Array.isArray(operand)) {
|
||
if ('$col' in (operand as Record<string, unknown>)) return true;
|
||
if (this.fieldHasColRef(operand)) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** 移除关联 EXISTS 标记(引擎层先执行无 EXISTS 条件的查询) */
|
||
private stripCorrelatedExists(where: WhereCondition): WhereCondition {
|
||
const cleaned: WhereCondition = {};
|
||
for (const [key, value] of Object.entries(where)) {
|
||
if (key === '$and' || key === '$or') {
|
||
cleaned[key] = (value as WhereCondition[]).map((sub) => this.stripCorrelatedExists(sub));
|
||
continue;
|
||
}
|
||
if (key === '$not') {
|
||
const inner = this.stripCorrelatedExists(value as WhereCondition);
|
||
// 剥离后为空 → 条件恒真,删掉该键(避免引擎层执行 NOT(true) 过滤掉所有行)
|
||
if (Object.keys(inner).length > 0) cleaned.$not = inner;
|
||
continue;
|
||
}
|
||
if (key === '$exists') continue; // 逐行求值时单独处理
|
||
if (/^\s*CASE\b/i.test(key)) continue; // v0.3.2: CASE 键逐行求值
|
||
cleaned[key] = value;
|
||
}
|
||
return cleaned;
|
||
}
|
||
|
||
/** 逐行绑定外层行上下文,求值关联 EXISTS、$col 引用与 CASE WHEN 键 */
|
||
private async filterCorrelated(rows: Record<string, unknown>[], where: WhereCondition): Promise<Record<string, unknown>[]> {
|
||
const result: Record<string, unknown>[] = [];
|
||
for (const row of rows) {
|
||
// 1. CASE WHEN 键 → 布尔条件(同步)
|
||
let rowWhere = this.resolveCaseKeys(where, row);
|
||
// 2. $col 绑定 + 关联 EXISTS 求值(异步)
|
||
rowWhere = await this.resolveSubqueries(rowWhere, row);
|
||
if (matchWhere(row, rowWhere)) {
|
||
result.push(row);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/** 将 WHERE 中的 CASE WHEN 表达式键求值为布尔条件($caseResult) */
|
||
private resolveCaseKeys(where: WhereCondition, row: Record<string, unknown>): WhereCondition {
|
||
const resolved: WhereCondition = {};
|
||
for (const [key, value] of Object.entries(where)) {
|
||
if (key === '$and' || key === '$or') {
|
||
resolved[key] = (value as WhereCondition[]).map((sub) => this.resolveCaseKeys(sub, row));
|
||
continue;
|
||
}
|
||
if (key === '$not') {
|
||
resolved.$not = this.resolveCaseKeys(value as WhereCondition, row);
|
||
continue;
|
||
}
|
||
if (/^\s*CASE\b/i.test(key)) {
|
||
const expr = parseCaseExpression(key);
|
||
if (!expr) continue; // 解析失败视为不满足
|
||
const val = evaluateCase(expr, row);
|
||
if (this.caseConditionMatches(val, value)) {
|
||
resolved.$caseResult = true;
|
||
} else {
|
||
return { $caseResult: false };
|
||
}
|
||
continue;
|
||
}
|
||
resolved[key] = value;
|
||
}
|
||
return resolved;
|
||
}
|
||
|
||
/** CASE 求值结果与操作符条件比较 */
|
||
private caseConditionMatches(val: unknown, condition: unknown): boolean {
|
||
if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) {
|
||
return val === condition;
|
||
}
|
||
const ops = condition as Record<string, unknown>;
|
||
for (const [op, operand] of Object.entries(ops)) {
|
||
switch (op) {
|
||
case '$eq': if (val !== operand) return false; break;
|
||
case '$ne': if (val === operand) return false; break;
|
||
case '$gt': if (!((val as number) > (operand as number))) return false; break;
|
||
case '$gte': if (!((val as number) >= (operand as number))) return false; break;
|
||
case '$lt': if (!((val as number) < (operand as number))) return false; break;
|
||
case '$lte': if (!((val as number) <= (operand as number))) return false; break;
|
||
case '$in': if (!(Array.isArray(operand) && operand.includes(val))) return false; break;
|
||
case '$nin': if (Array.isArray(operand) && operand.includes(val)) return false; break;
|
||
default: break;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/** 将 where 中的 $col 引用替换为上下文行值 */
|
||
private bindColumnRefs(value: unknown, contextRow: Record<string, unknown>): unknown {
|
||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return value;
|
||
const ops: Record<string, unknown> = {};
|
||
for (const [op, operand] of Object.entries(value as Record<string, unknown>)) {
|
||
if (op === '$col') {
|
||
ops[op] = contextRow[String(operand)] ?? null;
|
||
} else if (op === '$and' || op === '$or') {
|
||
ops[op] = (operand as WhereCondition[]).map((sub) => this.bindWhereRefs(sub, contextRow));
|
||
} else if (op === '$not' && typeof operand === 'object' && operand !== null) {
|
||
ops[op] = this.bindColumnRefs(operand, contextRow);
|
||
} else if (typeof operand === 'object' && operand !== null && !Array.isArray(operand) && '$col' in (operand as Record<string, unknown>)) {
|
||
// 操作符值中嵌套的列引用:{ $eq: { $col: 'id' } } → { $eq: row['id'] }
|
||
ops[op] = contextRow[String((operand as Record<string, unknown>).$col)] ?? null;
|
||
} else {
|
||
ops[op] = operand;
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
private bindWhereRefs(where: WhereCondition, contextRow: Record<string, unknown>): WhereCondition {
|
||
const bound: WhereCondition = {};
|
||
for (const [key, value] of Object.entries(where)) {
|
||
if (key === '$and' || key === '$or') {
|
||
bound[key] = (value as WhereCondition[]).map((sub) => this.bindWhereRefs(sub, contextRow));
|
||
} else if (key === '$not') {
|
||
bound.$not = this.bindWhereRefs(value as WhereCondition, contextRow);
|
||
} else if (key === '$exists') {
|
||
bound[key] = value;
|
||
} else {
|
||
bound[key] = this.bindColumnRefs(value, contextRow);
|
||
}
|
||
}
|
||
return bound;
|
||
}
|
||
|
||
// ===================================================================
|
||
// 子查询解析
|
||
// ===================================================================
|
||
|
||
/**
|
||
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
|
||
* 将结果替换为具体值。
|
||
* @param contextRow 关联子查询的外层行上下文(用于绑定 $col 引用)
|
||
*/
|
||
private async resolveSubqueries(where: WhereCondition, contextRow?: Record<string, unknown>): Promise<WhereCondition> {
|
||
// 关联上下文:先把字段级的 $col 引用绑定为外层行值
|
||
if (contextRow) {
|
||
where = this.bindWhereRefs(where, contextRow);
|
||
}
|
||
const resolved: WhereCondition = {};
|
||
|
||
for (const [key, value] of Object.entries(where)) {
|
||
// 顶层 $exists(v0.3.0):执行子查询并解析为 boolean,由 where-matcher 消费
|
||
if (key === '$exists' && typeof value === 'object' && value !== null) {
|
||
const v = value as Record<string, unknown>;
|
||
const sub = v.$subquery as SelectStatement;
|
||
const negate = !!(v as Record<string, unknown>).$negate;
|
||
let subWhere = sub.where;
|
||
// 子查询内的关联引用(如 o.user_id = u.id 中的 u.id)绑定外层行
|
||
if (this.hasCorrelatedRefs(subWhere)) {
|
||
subWhere = this.bindWhereRefs(subWhere, contextRow ?? {});
|
||
}
|
||
const rows = await this.executeSelectPart({ ...sub, where: subWhere });
|
||
resolved.$exists = rows.length > 0 !== negate;
|
||
continue;
|
||
}
|
||
// 逻辑组合操作符
|
||
if (key === '$and' && Array.isArray(value)) {
|
||
resolved.$and = await Promise.all(
|
||
(value as WhereCondition[]).map((sub) => this.resolveSubqueries(sub, contextRow)),
|
||
);
|
||
continue;
|
||
}
|
||
if (key === '$or' && Array.isArray(value)) {
|
||
resolved.$or = await Promise.all(
|
||
(value as WhereCondition[]).map((sub) => this.resolveSubqueries(sub, contextRow)),
|
||
);
|
||
continue;
|
||
}
|
||
if (key === '$not' && typeof value === 'object' && value !== null) {
|
||
resolved.$not = await this.resolveSubqueries(value as WhereCondition, contextRow);
|
||
continue;
|
||
}
|
||
|
||
// 字段条件
|
||
if (typeof value === 'object' && value !== null) {
|
||
resolved[key] = await this.resolveOperatorSubqueries(value as Record<string, unknown>);
|
||
} else {
|
||
resolved[key] = value;
|
||
}
|
||
}
|
||
|
||
return resolved;
|
||
}
|
||
|
||
/**
|
||
* 解析操作符值中嵌套的子查询
|
||
*/
|
||
private async resolveOperatorSubqueries(ops: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||
const resolved: Record<string, unknown> = {};
|
||
|
||
for (const [op, operand] of Object.entries(ops)) {
|
||
// 处理嵌套 $and/$or(在字段级条件中)
|
||
if (op === '$and' && Array.isArray(operand)) {
|
||
resolved.$and = await Promise.all(
|
||
(operand as WhereCondition[]).map((sub) => this.resolveSubqueries(sub)),
|
||
);
|
||
continue;
|
||
}
|
||
if (op === '$or' && Array.isArray(operand)) {
|
||
resolved.$or = await Promise.all(
|
||
(operand as WhereCondition[]).map((sub) => this.resolveSubqueries(sub)),
|
||
);
|
||
continue;
|
||
}
|
||
if (op === '$not') {
|
||
resolved.$not = typeof operand === 'object' && operand !== null
|
||
? await this.resolveOperatorSubqueries(operand as Record<string, unknown>)
|
||
: operand;
|
||
continue;
|
||
}
|
||
|
||
// 子查询检测
|
||
if (typeof operand === 'object' && operand !== null && '$subquery' in (operand as Record<string, unknown>)) {
|
||
const subStmt = (operand as Record<string, unknown>).$subquery as SelectStatement;
|
||
const subResult = await this.executeSelect(subStmt);
|
||
|
||
if (op === '$in' || op === '$nin') {
|
||
// IN 子查询 → 提取第一列的值列表
|
||
const colName = Object.keys(subResult[0] || {})[0];
|
||
const values = subResult.map((row) => row[colName]);
|
||
resolved[op] = values;
|
||
} else {
|
||
// 标量子查询 → 取第一行第一列
|
||
if (subResult.length === 0) {
|
||
resolved[op] = null;
|
||
} else {
|
||
const colName = Object.keys(subResult[0])[0];
|
||
resolved[op] = subResult[0][colName];
|
||
}
|
||
}
|
||
} else {
|
||
resolved[op] = operand;
|
||
}
|
||
}
|
||
|
||
return resolved;
|
||
}
|
||
}
|