/** * metona-sqlark Query Builder — 链式查询构建器 * @module query/builder * * ============================================================================ * v0.8.0(B-3):**只产出 AST,执行一律交给 Executor** * ============================================================================ * 修复前每个 builder 的 `execute()` 都有自己的执行动作: * - `SelectQueryBuilder`:**无 JOIN 时直接调 `engine.find`**,只有 JOIN 才走 * executor; * - `UpdateQueryBuilder` / `DeleteQueryBuilder`:直接调 `engine.update/delete`。 * * 于是"同一条语义"在 builder 路径与 SQL 路径上走两条管线,规则各写一份: * - SELECT 的常量列/别名/CASE 投影在 builder 路径不存在 * (`db.table('t').select(['t.n'])` 的输出键、`SELECT 1` 之类行为不同); * - `maxRowsPerQuery`、`distinct`、`groupBy`/`having`、`fromSubquery` 等阶段 * 在直通路径上完全不经过; * - 未解析的 `$subquery`/`$col` 在直通路径上无人解析 → 引擎判 UNKNOWN → * **静默 0 行**(这正是 v0.7.4 要为此加"显式拒绝"防御的原因)。 * * 现在 builder 只负责"拼 AST",执行统一走 `executor.execute(ast)`; * 需要直通时由 executor 内部判断(它本来就是唯一知道"能不能下推"的地方)。 */ import type { WhereCondition, OrderBy, SortDirection } from '../constants'; import type { SelectStatement, UpdateStatement, DeleteStatement, JoinType, JoinClause } from './ast'; import { QueryExecutor } from './executor'; // --------------------------------------------------------------------------- // SelectQueryBuilder // --------------------------------------------------------------------------- export class SelectQueryBuilder { private _where: WhereCondition = {}; private _orderBy: OrderBy[] = []; private _limit?: number; private _offset?: number; private _joins: JoinClause[] = []; private _alias?: string; private _executor: QueryExecutor; constructor( private tableName: string, executor: QueryExecutor, private _columns: string[] = ['*'], ) { this._executor = executor; } /** 主表别名 */ as(alias: string): this { this._alias = alias; return this; } /** INNER JOIN */ innerJoin(table: string, on: WhereCondition, alias?: string): this { return this._addJoin('INNER', table, on, alias); } /** LEFT JOIN */ leftJoin(table: string, on: WhereCondition, alias?: string): this { return this._addJoin('LEFT', table, on, alias); } /** RIGHT JOIN */ rightJoin(table: string, on: WhereCondition, alias?: string): this { return this._addJoin('RIGHT', table, on, alias); } /** CROSS JOIN */ crossJoin(table: string, alias?: string): this { return this._addJoin('CROSS', table, {}, alias); } /** 通用 JOIN */ join(table: string, on: WhereCondition, alias?: string): this { return this._addJoin('INNER', table, on, alias); } private _addJoin(type: JoinType, table: string, on: WhereCondition, alias?: string): this { this._joins.push({ type, table, on, alias }); return this; } /** 添加过滤条件 */ where(condition: WhereCondition): this { this._where = { ...this._where, ...condition }; return this; } /** 排序 */ orderBy(column: string, direction: SortDirection = 'asc'): this { this._orderBy.push({ column, direction }); return this; } /** 限制返回条数 */ limit(n: number): this { this._limit = n; return this; } /** 偏移量 */ offset(n: number): this { this._offset = n; return this; } /** * 执行查询 —— 拼出 AST 后交给 Executor(**唯一**执行管线)。 * * 注意不要再为"无 JOIN"加一条 `engine.find` 快路:那条路会绕过 * 投影/列校验/LIMIT 下推判定/maxRowsPerQuery,从而与 `db.query()` 给出不同结果 * (B-3 修复前的实际状态)。executor 自己会在安全时下推到引擎,不需要 builder 代劳。 */ async execute(): Promise[]> { return this._executor.execute(this.toAST()) as Promise[]>; } /** 获取 AST */ toAST(): SelectStatement { return { type: 'SELECT', columns: this._columns, from: this.tableName, alias: this._alias, joins: this._joins.length > 0 ? [...this._joins] : undefined, where: this._where, orderBy: this._orderBy.length > 0 ? this._orderBy : undefined, limit: this._limit, offset: this._offset, }; } } // --------------------------------------------------------------------------- // UpdateQueryBuilder // --------------------------------------------------------------------------- export class UpdateQueryBuilder { private _where: WhereCondition = {}; constructor( private tableName: string, private _updates: Record, private executor: QueryExecutor, /** * TABLE API 的生命周期回调(`beforeUpdate` / `afterUpdate` / onWrite 广播)。 * * 为什么由 `Table` 注入而不是 builder 自己触发:builder 的职责是**产出 AST**, * 它不该知道钩子/广播的存在(否则又会像修复前那样"builder 顺带把写入也做了", * 从而绕过 Executor)。钩子被包裹在**唯一管线**之外, * 顺序与修复前完全一致:before → executor → onWrite → after。 * * 回调接收**实际执行的语句**(含 builder 上累积的 where), * 而不是构造 builder 时的空 where —— 后者会让 `beforeUpdate` 的 * `query.where` 永远是 `{}`(钩子拿不到过滤条件,等于信息缺失)。 */ private hooks?: { before?: (stmt: UpdateStatement) => Promise; after?: (count: number, stmt: UpdateStatement) => Promise; }, ) {} where(condition: WhereCondition): this { this._where = { ...this._where, ...condition }; return this; } /** * 执行更新 —— 走 Executor(唯一的写管线)。 * * 修复前这里直接调 `engine.update`:`$subquery` / `$col` / `$exists` 无人解析, * 引擎层 matchWhere 判 UNKNOWN → **静默影响 0 行**(返回 0 且无报错)。 * 引擎层为此加过"检测未解析标记就抛 NOT_SUPPORTED"的防御 —— 那是把 * "管线缺失"暴露成用户错误;正确做法是把请求送进唯一管线。 */ async execute(): Promise { const stmt = this.toAST(); await this.hooks?.before?.(stmt); const count = await this.executor.execute(stmt) as number; await this.hooks?.after?.(count, stmt); return count; } toAST(): UpdateStatement { return { type: 'UPDATE', table: this.tableName, sets: this._updates, where: this._where }; } } // --------------------------------------------------------------------------- // DeleteQueryBuilder // --------------------------------------------------------------------------- export class DeleteQueryBuilder { private _where: WhereCondition = {}; constructor( private tableName: string, private executor: QueryExecutor, /** TABLE API 生命周期回调,语义见 `UpdateQueryBuilder` 的说明 */ private hooks?: { before?: (stmt: DeleteStatement) => Promise; after?: (count: number, stmt: DeleteStatement) => Promise; }, ) {} where(condition: WhereCondition): this { this._where = { ...this._where, ...condition }; return this; } /** 执行删除 —— 走 Executor(同 UpdateQueryBuilder.execute 的理由) */ async execute(): Promise { const stmt = this.toAST(); await this.hooks?.before?.(stmt); const count = await this.executor.execute(stmt) as number; await this.hooks?.after?.(count, stmt); return count; } toAST(): DeleteStatement { return { type: 'DELETE', from: this.tableName, where: this._where }; } }