Files
MetonaSqlark/src/query/executor.ts
T
thzxx 752bdea97d fix(A5/A6/A7/A16/A18/A24/A28): 查询层语义根治 —— LIMIT 双重应用、未知列静默、聚合崩溃与空集语义
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 语义以表达式原文为键。
2026-09-14 21:19:49 +08:00

1688 lines
72 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
}
}
// ===================================================================
// UNIONv0.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) {
// 非 JOINWHERE 在 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 对空集返回 nullSQL 标准),
* 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/DELETEWHERE 的子查询解析。
* 非关联子查询($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: 引擎级 alterTableAria 需重写存储行 + 持久化 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 INDEXv0.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):普通列走 projectColumnsCASE 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' 在、值为 undefinedJSON 序列化后键消失)。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)) {
// 顶层 $existsv0.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;
}
}