/** * 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): 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): 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): 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 { 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[]> { 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[] = 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(); const result: Record[] = []; 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[]> { return part.type === 'SELECT_UNION' ? this.executeSelectUnion(part) : this.executeSelect(part); } /** 将 UNION 右侧行投影为左侧列结构(按位置取值) */ private projectUnionRow(row: Record, leftCols: string[]): Record { if (leftCols.length === 0) return row; const values = Object.values(row); const projected: Record = {}; 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> { 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[]> { const shape = this.analyzeSelect(stmt); const { hasGroupBy, hasAggregate, isJoinQuery, needsRawRows, orderByAlias, hasSelectAlias, limitPushdownSafe } = shape; let rows: Record[]; 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 })._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[]): Promise[]> { const mainAlias = stmt.alias ?? stmt.from; // v0.4.0: 派生表行源已预加载(行带别名前缀) let mainRows: Record[]; 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, alias: string): Record { const prefixed: Record = {}; 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 | 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[], join: JoinClause, joinAlias: string, mainAlias: string, ): Promise[] | 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; if ('$eq' in c && typeof c.$eq === 'object' && c.$eq !== null && '$col' in (c.$eq as Record)) { refCol = String((c.$eq as Record).$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[]>(); 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 = {}; for (const key of Object.keys(schema.columns)) nullRight[key] = null; const result: Record[] = []; 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[], rightRows: Record[], join: JoinClause, ): Record[] { if (join.type === 'CROSS') { const result: Record[] = []; for (const l of leftRows) for (const r of rightRows) result.push({ ...l, ...r }); return result; } const result: Record[] = []; 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 = {}; 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 = {}; for (const key of Object.keys(leftRows[0] ?? {})) nullLeft[key] = null; result.push({ ...nullLeft, ...r }); } } } return result; } // ---- GROUP BY ---- private executeGroupBy(rows: Record[], stmt: SelectStatement): Record[] { const groups = new Map[]>(); 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[] = []; // v0.4.0: 聚合表达式键 → 输出键 映射(HAVING SUM(...) 引用表达式时归一为别名键) const aliasMap = new Map(); for (const groupRows of groups.values()) { const aggregated: Record = {}; 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 })._aggAliasMap = aliasMap; return result; } /** * 计算单个聚合值。 * * v0.8.0: 返回类型放宽为 unknown —— SUM/AVG/MIN/MAX 对空集返回 null(SQL 标准), * COUNT 仍返回 number。最小/最大改为单次遍历(不再展开实参,消除栈溢出)。 */ private computeAggregate(func: string, rows: Record[], 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[]): Record[] { const seen = new Set(); 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 { 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[] = selectRows.map((row) => { const mapped: Record = {}; 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[] = (stmt.values ?? []).map((vals: unknown[]) => { const row: Record = {}; 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 { // 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 { // 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 { 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 { // 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 = Object.create(null) as Record; 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 { 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 { 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 { 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 { 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 { 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 { return this.engine.beginTransaction(); } private async executeCommit(): Promise { return this.engine.commitTransaction(); } private async executeRollback(): Promise { return this.engine.rollbackTransaction(); } // =================================================================== // 维护语句(v0.5.1) // =================================================================== /** SAVEPOINT name / ROLLBACK TO SAVEPOINT name / RELEASE SAVEPOINT name */ private async executeSavepoint(stmt: import('./ast').SavepointStatement): Promise { const engine = this.engine as unknown as import('./ast').SavepointStatement & { savepoint?: (name: string) => Promise; rollbackToSavepoint?: (name: string) => Promise; releaseSavepoint?: (name: string) => Promise; }; 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> { const engine = this.engine as unknown as import('./ast').AnalyzeStatement & { analyzeTable?: (table: string) => Promise>; }; 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 { const engine = this.engine as unknown as import('./ast').ReindexStatement & { reindexTable?: (table: string) => Promise; }; 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 { const engine = this.engine as unknown as import('./ast').VacuumStatement & { vacuum?: () => Promise; }; 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(); 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` 作为被引用列; * - 其余视为裸列引用。 * 存在性检查允许两种形态:精确匹配,或**唯一**以 `.` 结尾(JOIN 行以 `alias.col` 为键)。 * 若同一个后缀出现在多个表别名下则视为歧义,同样报错(符合"未限定列名歧义应报错"的语义)。 * * 结果集为空时无法判定,此时跳过(空表 + 未知列不会误报)。 */ private assertProjectionColumnsExist( rows: Record[], columns: string[], stmt: SelectStatement, ): void { if (rows.length === 0 || columns.length === 0) return; const available = new Set(); 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, columns: string[]): Record { 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[], stmt: SelectStatement): Record { const result: Record = {}; 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; 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 = {}; for (const [op, operand] of Object.entries(value as Record)) { 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)) { // 操作符值中嵌套的列引用:{ $eq: { $col: 'u.id' } } ops[op] = { $col: this.stripAlias(String((operand as Record).$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)) { 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; 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)) 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[], where: WhereCondition): Promise[]> { const result: Record[] = []; 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): 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; 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): unknown { if (typeof value !== 'object' || value === null || Array.isArray(value)) return value; const ops: Record = {}; for (const [op, operand] of Object.entries(value as Record)) { 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)) { // 操作符值中嵌套的列引用:{ $eq: { $col: 'id' } } → { $eq: row['id'] } ops[op] = contextRow[String((operand as Record).$col)] ?? null; } else { ops[op] = operand; } } return ops; } private bindWhereRefs(where: WhereCondition, contextRow: Record): 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): Promise { // 关联上下文:先把字段级的 $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; const sub = v.$subquery as SelectStatement; const negate = !!(v as Record).$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); } else { resolved[key] = value; } } return resolved; } /** * 解析操作符值中嵌套的子查询 */ private async resolveOperatorSubqueries(ops: Record): Promise> { const resolved: Record = {}; 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) : operand; continue; } // 子查询检测 if (typeof operand === 'object' && operand !== null && '$subquery' in (operand as Record)) { const subStmt = (operand as Record).$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; } }