UNPKG

rawsql-ts

Version:

High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

1,148 lines (1,147 loc) 57.7 kB
import { DistinctOn, JoinOnClause, SubQuerySource, TableSource } from "../models/Clause"; import { BinarySelectQuery, SimpleSelectQuery } from "../models/SelectQuery"; import { ArrayExpression, ArrayQueryExpression, BetweenExpression, BinaryExpression, CaseExpression, CastExpression, ColumnReference, FunctionCall, InlineQuery, JsonPredicateExpression, ParameterExpression, TupleExpression, TypeValue, UnaryExpression, ValueList } from "../models/ValueComponent"; import { SelectQueryParser } from "../parsers/SelectQueryParser"; import { formatSqlComponent, hasSqlComponentFormatOverride } from "./SqlComponentFormatter"; import { SelectOutputCollector } from "./SelectOutputCollector"; import { dedupeTopLevelAndConditions, dedupeWhereTopLevelAndConditions } from "./TopLevelAndConditionDeduper"; import { appendUnique, cloneColumnReference, cloneValueComponent, collectTopLevelAndTerms, columnReferenceText, identifiersEqual, normalizeIdentifier, rebuildWhereWithoutTerms, sameColumnReference, unwrapParens } from "./PredicateExpressionUtils"; const SUPPORTED_OPERATORS = new Set(["=", "<>", "!=", "<", "<=", ">", ">=", "like", "ilike", "in"]); const VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON = "Condition contains a function call; volatile and expression predicates are not moved in the safe-only implementation."; export class ParameterConditionPlacementOptimizer { plan(input, options = {}) { var _a, _b, _c; const parsed = this.parseInput(input, options); const warnings = [...parsed.warnings]; const errors = [...parsed.errors]; if (!parsed.query) { return this.buildResult({ query: null, sql: parsed.sql, applied: [], skipped: [], warnings, errors, dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : true, formatterGeneratedSource: parsed.formatterGeneratedSource }); } if (!(parsed.query instanceof SimpleSelectQuery)) { warnings.push({ code: "UNSUPPORTED_ROOT_QUERY", message: "Parameter condition placement currently supports only SimpleSelectQuery roots." }); return this.buildResult({ query: parsed.query, sql: parsed.sql, applied: [], skipped: [], warnings, errors, dryRun: (_b = options.dryRun) !== null && _b !== void 0 ? _b : true, formatterGeneratedSource: parsed.formatterGeneratedSource }); } const query = parsed.query; const applied = []; const skipped = []; const movedTerms = []; for (const term of query.whereClause ? collectTopLevelAndTerms(query.whereClause.condition) : []) { const candidate = this.analyzeCandidate(term, options); if (!candidate) { continue; } if ("code" in candidate) { skipped.push(this.makeSkipped(term, candidate, options)); continue; } const target = this.resolveTarget(query, candidate); if ("code" in target) { skipped.push(this.makeSkipped(term, target, options)); continue; } let appliedReason = ""; if (target.kind === "join_on") { this.appendJoinOnCondition(target.join, candidate.expression, options); appliedReason = target.reason; } else if (target.kind === "simple") { const targetColumns = this.resolveTargetColumns(query, target.query, candidate.references); if ("code" in targetColumns) { skipped.push(this.makeSkipped(term, targetColumns, options)); continue; } const placement = this.resolveTargetPlacement(target.query, targetColumns); if ("code" in placement) { skipped.push(this.makeSkipped(term, placement, options)); continue; } const movedCondition = this.rebaseCondition(candidate.expression, targetColumns, options); target.query.appendWhere(movedCondition); dedupeWhereTopLevelAndConditions(target.query, options); appliedReason = placement.reason; } else { const placements = []; let skip = null; for (const branch of target.branches) { const placement = this.resolveTargetPlacement(branch.query, branch.targetColumns); if ("code" in placement) { skip = placement; break; } placements.push(placement); } if (skip) { skipped.push(this.makeSkipped(term, skip, options)); continue; } for (const branch of target.branches) { const movedCondition = this.rebaseCondition(candidate.expression, branch.targetColumns, options); branch.query.appendWhere(movedCondition); dedupeWhereTopLevelAndConditions(branch.query, options); } appliedReason = placements.some(item => /group by/i.test(item.reason)) ? "Condition is distributed to every UNION branch by output column position; grouped branches only receive GROUP BY-key predicates." : "Condition is distributed to every UNION branch by output column position before unsafe query boundaries."; } movedTerms.push(term); applied.push({ kind: "move_condition", conditionSql: candidate.conditionSql, fromScopeId: "scope:root", toScopeId: target.scopeId, reason: appliedReason, parameterNames: candidate.parameterNames, columnReferences: candidate.references.map(columnReferenceText) }); } rebuildWhereWithoutTerms(query, new Set(movedTerms)); const sql = applied.length > 0 || hasSqlComponentFormatOverride(options) ? formatSqlComponent(query, options) : parsed.sql; return this.buildResult({ query, sql, applied, skipped, warnings, errors, dryRun: (_c = options.dryRun) !== null && _c !== void 0 ? _c : true, formatterGeneratedSource: parsed.formatterGeneratedSource }); } optimize(input, options = {}) { var _a; return this.plan(input, Object.assign(Object.assign({}, options), { dryRun: (_a = options.dryRun) !== null && _a !== void 0 ? _a : false })); } parseInput(input, options) { const warnings = []; const errors = []; try { const sourceSql = typeof input === "string" ? input : formatSqlComponent(input, options); if (typeof input !== "string" && options.cloneInput === false) { return { query: input, sql: sourceSql, formatterGeneratedSource: false, warnings, errors }; } if (typeof input !== "string") { warnings.push({ code: "AST_INPUT_FORMATTED", message: "AST input is cloned through formatter output so the caller-owned query is not mutated." }); } return { query: SelectQueryParser.parse(sourceSql), sql: sourceSql, formatterGeneratedSource: typeof input !== "string", warnings, errors }; } catch (error) { const detail = error instanceof Error ? error.message : String(error); errors.push({ code: "PARSE_FAILED", message: "Parameter condition optimization could not parse the input SQL.", detail }); return { query: null, sql: typeof input === "string" ? input : "", formatterGeneratedSource: typeof input !== "string", warnings, errors }; } } analyzeCandidate(expression, options) { const parameterNames = this.collectParameterNames(expression); if (parameterNames.length === 0) { return null; } const unsupported = this.findUnsupportedExpression(expression); if (unsupported) { return unsupported; } const columnReferences = this.collectColumnReferences(expression); if (columnReferences.length === 0) { return { code: "AMBIGUOUS_COLUMN_REFERENCE", reason: "Parameter condition has no column reference to anchor the move." }; } const unsupportedShape = this.findUnsupportedParameterPredicateShape(expression); if (unsupportedShape) { return unsupportedShape; } return { expression, conditionSql: formatSqlComponent(expression, options), parameterNames, references: columnReferences }; } findUnsupportedExpression(expression) { let found = null; const visit = (value) => { if (found) { return; } const candidate = unwrapParens(value); if (candidate instanceof BinaryExpression) { visit(candidate.left); visit(candidate.right); return; } if (candidate instanceof FunctionCall) { found = { code: "FUNCTION_PREDICATE_UNSUPPORTED", reason: VOLATILE_OR_UNSUPPORTED_FUNCTION_REASON }; return; } if (candidate instanceof CaseExpression) { found = { code: "CASE_PREDICATE_UNSUPPORTED", reason: "CASE predicates are not moved in the first safe-only implementation." }; return; } if (candidate instanceof InlineQuery || candidate instanceof ArrayQueryExpression) { found = { code: "SUBQUERY_PREDICATE_UNSUPPORTED", reason: "Subquery predicates are not moved in the first safe-only implementation." }; return; } if (candidate instanceof UnaryExpression) { visit(candidate.expression); return; } if (candidate instanceof CastExpression) { visit(candidate.input); return; } if (candidate instanceof JsonPredicateExpression) { visit(candidate.expression); return; } if (candidate instanceof ArrayExpression) { visit(candidate.expression); return; } if (candidate instanceof ValueList) { candidate.values.forEach(visit); return; } if (candidate instanceof TupleExpression) { candidate.values.forEach(visit); return; } if (candidate instanceof TypeValue && candidate.argument) { visit(candidate.argument); } }; visit(expression); return found; } findUnsupportedParameterPredicateShape(expression) { const visit = (value) => { var _a; const candidate = unwrapParens(value); if (candidate instanceof BetweenExpression) { return null; } if (candidate instanceof BinaryExpression) { const operator = candidate.operator.value.trim().toLowerCase(); if (operator === "and" || operator === "or") { return (_a = visit(candidate.left)) !== null && _a !== void 0 ? _a : visit(candidate.right); } if (!SUPPORTED_OPERATORS.has(operator)) { return { code: "UNSUPPORTED_OPERATOR", reason: `Operator '${candidate.operator.value}' is not supported for safe-only parameter condition placement.` }; } if (operator === "in" && !this.isSupportedInPredicate(candidate)) { return { code: "UNSUPPORTED_IN_PREDICATE", reason: "Only simple column IN (:parameter) predicates are moved in the safe-only implementation." }; } return null; } if (candidate instanceof UnaryExpression) { const operator = candidate.operator.value.trim().toLowerCase(); if (operator === "not") { return visit(candidate.expression); } } return { code: "UNSUPPORTED_PARAMETER_CONDITION", reason: "Only simple binary, BETWEEN, and whole OR/AND/NOT parameter predicates are moved in the safe-only implementation." }; }; return visit(expression); } isSupportedInPredicate(expression) { const left = unwrapParens(expression.left); const right = unwrapParens(expression.right); if (!(left instanceof ColumnReference)) { return false; } if (right instanceof ParameterExpression) { return true; } if (!(right instanceof ValueList)) { return false; } return right.values.length > 0 && right.values.every(value => unwrapParens(value) instanceof ParameterExpression); } resolveTarget(root, candidate) { var _a; const boundary = this.findRootQueryBoundary(root); if (boundary) { return boundary; } if (!root.fromClause) { return { code: "NO_FROM_CLAUSE", reason: "Condition has no FROM source that can receive the predicate safely." }; } const bindings = []; for (const reference of candidate.references) { const binding = this.resolveSourceBinding(root, root, reference); if ("code" in binding) { return binding; } if (!bindings.some(item => item.source === binding.source)) { bindings.push(binding); } } if (bindings.length !== 1) { return { code: "MULTIPLE_SOURCE_REFERENCES", reason: "Parameter condition references multiple source query blocks; moving it may change semantics." }; } const binding = bindings[0]; if ((_a = binding.join) === null || _a === void 0 ? void 0 : _a.lateral) { return { code: "LATERAL_JOIN_BOUNDARY", reason: "Condition crosses a LATERAL JOIN boundary; moving it may change semantics." }; } const nullableSide = this.findNullableSideBoundary(root.fromClause, binding); if (nullableSide) { return nullableSide; } const upstream = this.resolveUpstreamQuery(root, binding, candidate.references); if ("code" in upstream) { if (upstream.code === "NO_SAFE_UPSTREAM_QUERY") { const joinOnTarget = this.resolveBaseTableJoinOnTarget(root, binding); if (!("code" in joinOnTarget)) { return joinOnTarget; } return joinOnTarget; } return upstream; } return upstream; } findRootQueryBoundary(query) { if (this.hasDistinctOnBoundary(query)) { return { code: "DISTINCT_BOUNDARY", reason: "Condition crosses DISTINCT ON boundary; moving it may change semantics." }; } if (this.hasWindowUsage(query)) { return { code: "WINDOW_BOUNDARY", reason: "Condition crosses WINDOW boundary; moving it may change semantics." }; } return null; } resolveTargetPlacement(query, targetColumns) { const hasOrdinaryDistinct = this.hasOrdinaryDistinct(query); if (this.hasDistinctOnBoundary(query)) { return { code: "DISTINCT_BOUNDARY", reason: "Condition crosses DISTINCT ON boundary; moving it may change semantics." }; } if (this.hasWindowUsage(query)) { return { code: "WINDOW_BOUNDARY", reason: "Condition crosses WINDOW boundary; moving it may change semantics." }; } if (query.limitClause || query.offsetClause || query.fetchClause) { return { code: "ROW_LIMIT_BOUNDARY", reason: "Condition crosses LIMIT/OFFSET/FETCH boundary; moving it may change row selection semantics." }; } if (query.fromClause && this.hasOuterJoin(query.fromClause)) { return { code: "OUTER_JOIN_BOUNDARY", reason: "Target query contains an OUTER JOIN boundary that is not moved across in the safe-only implementation." }; } if (query.groupByClause) { const allReferencesAreGroupKeys = targetColumns.every(item => this.isGroupKeyColumn(query, item.targetColumn)); if (!allReferencesAreGroupKeys) { return { code: "GROUP_BY_BOUNDARY", reason: "Condition references a target column that is not proven to be a GROUP BY key." }; } return { reason: "Condition references only GROUP BY keys; it is moved into pre-aggregation WHERE." }; } if (query.havingClause) { return { code: "GROUP_BY_BOUNDARY", reason: "Condition crosses HAVING aggregation boundary; moving it may change semantics." }; } if (hasOrdinaryDistinct) { return { reason: "Condition references a direct ordinary DISTINCT output column; it is moved into the DISTINCT input WHERE." }; } return { reason: "All referenced columns resolve to a single direct upstream output before unsafe query boundaries." }; } resolveSourceBinding(contextRoot, query, column) { const fromClause = query.fromClause; if (!fromClause) { return { code: "NO_FROM_CLAUSE", reason: "Condition has no FROM source that can receive the predicate safely." }; } const bindings = this.getSourceBindings(fromClause); const namespace = column.getNamespace(); const columnName = column.column.name; if (namespace) { const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace)); if (matches.length !== 1) { return { code: "AMBIGUOUS_COLUMN_SOURCE", reason: `Column source alias '${column.getNamespace()}' is not uniquely resolvable.` }; } const matchCount = this.getOutputColumnMatchCount(contextRoot, matches[0], columnName); if (matchCount === 0 && this.isBaseTableBinding(contextRoot, matches[0])) { return matches[0]; } if (matchCount === 0) { return { code: "COLUMN_NOT_AVAILABLE_UPSTREAM", reason: `Column '${columnReferenceText(column)}' is not a direct output of the referenced source.` }; } if (matchCount > 1) { return { code: "AMBIGUOUS_COLUMN_REFERENCE", reason: `Column '${columnReferenceText(column)}' resolves to multiple outputs in the referenced source.` }; } return matches[0]; } const matches = []; for (const binding of bindings) { const matchCount = this.getOutputColumnMatchCount(contextRoot, binding, columnName); if (matchCount > 1) { return { code: "AMBIGUOUS_COLUMN_REFERENCE", reason: `Column '${columnName}' resolves to multiple outputs in source '${binding.alias}'.` }; } if (matchCount === 1) { matches.push(binding); } } if (matches.length !== 1) { return { code: "AMBIGUOUS_COLUMN_REFERENCE", reason: matches.length === 0 ? `Column '${columnName}' is not uniquely resolvable to a safe upstream source.` : `Column '${columnName}' is ambiguous across source query blocks.` }; } return matches[0]; } resolveUpstreamQuery(root, binding, references) { const source = binding.source.datasource; if (source instanceof SubQuerySource) { if (source.query instanceof SimpleSelectQuery) { return { kind: "simple", query: source.query, scopeId: `subquery:${binding.alias}`, sourceBinding: binding }; } if (source.query instanceof BinarySelectQuery) { return this.resolveUnionTarget(root, source.query, `subquery:${binding.alias}`, references); } return { code: "UNION_BOUNDARY", reason: "Condition would need distribution into a UNION or non-simple subquery, which is unsupported." }; } if (!(source instanceof TableSource)) { return { code: "UNSUPPORTED_SOURCE", reason: "Only CTE and simple derived-table sources can receive moved parameter conditions." }; } const cteName = source.table.name; const commonTable = this.findCte(root, cteName); if (!commonTable) { return { code: "NO_SAFE_UPSTREAM_QUERY", reason: "The referenced source is a base table, so there is no upstream query block to move into." }; } const referenceCount = this.countTableSourceReferences(root, cteName); if (referenceCount !== 1) { return { code: "CTE_REUSE_UNSUPPORTED", reason: `CTE '${cteName}' is referenced ${referenceCount} times; moving a predicate into it may affect other consumers.` }; } if (commonTable.query instanceof BinarySelectQuery) { return this.resolveUnionTarget(root, commonTable.query, `cte:${commonTable.getSourceAliasName()}`, references); } if (!(commonTable.query instanceof SimpleSelectQuery)) { return { code: "UNSUPPORTED_CTE_QUERY", reason: "Writable or non-select CTE bodies are not moved into by parameter condition placement." }; } return { kind: "simple", query: commonTable.query, scopeId: `cte:${commonTable.getSourceAliasName()}`, sourceBinding: binding, cteName: commonTable.getSourceAliasName() }; } resolveUnionTarget(root, query, scopeId, references) { const branches = this.collectUnionBranches(query); if ("code" in branches) { return branches; } const outputIndexes = new Map(); for (const reference of references) { const outputIndex = this.resolveUnionOutputIndex(root, branches[0], reference.column.name); if ("code" in outputIndex) { return outputIndex; } outputIndexes.set(reference, outputIndex.index); } const branchTargets = []; for (const branch of branches) { const targetColumns = []; for (const [reference, outputIndex] of outputIndexes.entries()) { const targetColumn = this.resolveTargetColumnByOutputIndex(root, branch, outputIndex, reference); if ("code" in targetColumn) { return targetColumn; } targetColumns.push(targetColumn); } branchTargets.push(this.resolveDeepestBranchTarget(root, branch, targetColumns)); } return { kind: "union", scopeId, branches: branchTargets }; } resolveTargetColumns(root, query, references) { const resolved = []; for (const reference of references) { const matches = this.collectDirectOutputMatches(root, query, reference.column.name); if (matches.length !== 1) { return { code: "AMBIGUOUS_TARGET_COLUMN", reason: matches.length === 0 ? `Target query does not expose '${reference.column.name}' as a direct output column.` : `Target query exposes multiple '${reference.column.name}' columns.` }; } const value = matches[0].value; if (!(value instanceof ColumnReference)) { return { code: "EXPRESSION_OUTPUT_UNSUPPORTED", reason: `Target output '${reference.column.name}' is an expression, not a direct column reference.` }; } const sourceResolution = this.verifyColumnResolvableInQuery(query, value); if (sourceResolution) { return sourceResolution; } resolved.push({ sourceColumn: reference, targetColumn: value }); } return resolved; } resolveBaseTableJoinOnTarget(root, binding) { var _a, _b, _c; if (!root.fromClause || !this.isBaseTableBinding(root, binding)) { return { code: "NO_SAFE_UPSTREAM_QUERY", reason: "The referenced source is a base table, so there is no upstream query block to move into." }; } if (this.hasOuterJoin(root.fromClause)) { return { code: "OUTER_JOIN_BOUNDARY", reason: "Condition crosses an OUTER JOIN boundary; moving it into JOIN ON may change semantics." }; } const join = binding.isPrimary ? (_b = (_a = root.fromClause.joins) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null : binding.join; if (join === null || join === void 0 ? void 0 : join.lateral) { return { code: "LATERAL_JOIN_BOUNDARY", reason: "Condition crosses a LATERAL JOIN boundary; moving it into JOIN ON may change semantics." }; } if (!join || !this.isInnerJoin(join)) { return { code: "NO_SAFE_JOIN_ON_TARGET", reason: "Base-table conditions are moved only into INNER JOIN ON clauses in the safe-only implementation." }; } if (!(join.condition instanceof JoinOnClause)) { return { code: "NO_SAFE_JOIN_ON_TARGET", reason: "Base-table conditions are moved only into existing JOIN ON clauses, not USING or conditionless joins." }; } return { kind: "join_on", join, scopeId: `join_on:${(_c = join.source.getAliasName()) !== null && _c !== void 0 ? _c : "unknown"}`, reason: binding.isPrimary ? "Condition references the primary source of an INNER JOIN; it is moved into the first JOIN ON clause." : "Condition references the joined source of an INNER JOIN; it is moved into that JOIN ON clause." }; } appendJoinOnCondition(join, expression, options) { if (!(join.condition instanceof JoinOnClause)) { return; } join.condition.condition = new BinaryExpression(join.condition.condition, "and", cloneValueComponent(expression, options)); join.condition.condition = dedupeTopLevelAndConditions(join.condition.condition, options); } resolveTargetColumnByOutputIndex(root, query, outputIndex, sourceColumn) { const output = this.collectSelectOutputs(root, query)[outputIndex]; if (!output) { return { code: "UNION_COLUMN_MISMATCH", reason: `UNION branch does not expose column '${sourceColumn.column.name}' at output position ${outputIndex + 1}.` }; } else if (identifiersEqual(output.name, sourceColumn.column.name)) { const matches = this.collectDirectOutputMatches(root, query, sourceColumn.column.name); if (matches.length > 1) { return { code: "AMBIGUOUS_TARGET_COLUMN", reason: `UNION branch exposes multiple '${sourceColumn.column.name}' columns.` }; } } if (!(output.value instanceof ColumnReference)) { return { code: "EXPRESSION_OUTPUT_UNSUPPORTED", reason: `UNION branch output at position ${outputIndex + 1} for '${sourceColumn.column.name}' is an expression, not a direct column reference.` }; } const sourceResolution = this.verifyColumnResolvableInQuery(query, output.value); if (sourceResolution) { return sourceResolution; } return { sourceColumn, targetColumn: output.value }; } resolveDeepestBranchTarget(contextRoot, query, targetColumns, visited = new Set()) { if (visited.has(query) || targetColumns.length === 0) { return { query, targetColumns: [...targetColumns] }; } const nextVisited = new Set(visited); nextVisited.add(query); const bindings = []; for (const item of targetColumns) { const binding = this.resolveSourceBinding(contextRoot, query, item.targetColumn); if ("code" in binding) { return { query, targetColumns: [...targetColumns] }; } if (!bindings.some(existing => existing.source === binding.source)) { bindings.push(binding); } } if (bindings.length !== 1) { return { query, targetColumns: [...targetColumns] }; } const binding = bindings[0]; const nullableSide = query.fromClause ? this.findNullableSideBoundary(query.fromClause, binding) : null; if (nullableSide) { return { query, targetColumns: [...targetColumns] }; } const upstream = this.resolveUpstreamQuery(contextRoot, binding, targetColumns.map(item => item.targetColumn)); if ("code" in upstream) { return { query, targetColumns: [...targetColumns] }; } if (upstream.kind !== "simple") { return { query, targetColumns: [...targetColumns] }; } const upstreamColumns = this.resolveTargetColumns(contextRoot, upstream.query, targetColumns.map(item => item.targetColumn)); if ("code" in upstreamColumns) { return { query, targetColumns: [...targetColumns] }; } const placement = this.resolveTargetPlacement(upstream.query, upstreamColumns); if ("code" in placement) { return { query, targetColumns: [...targetColumns] }; } const rebasedColumns = upstreamColumns.map((item, index) => ({ sourceColumn: targetColumns[index].sourceColumn, targetColumn: item.targetColumn })); return this.resolveDeepestBranchTarget(contextRoot, upstream.query, rebasedColumns, nextVisited); } verifyColumnResolvableInQuery(query, column) { if (!query.fromClause) { return { code: "NO_TARGET_FROM_CLAUSE", reason: "Target query has no FROM clause where the moved column can be resolved." }; } const bindings = this.getSourceBindings(query.fromClause); const namespace = column.getNamespace(); if (namespace) { const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace)); return matches.length === 1 ? null : { code: "AMBIGUOUS_TARGET_COLUMN", reason: `Target column '${columnReferenceText(column)}' is not uniquely resolvable in the destination query.` }; } if (bindings.length === 1) { return null; } return { code: "AMBIGUOUS_TARGET_COLUMN", reason: `Unqualified target column '${column.column.name}' is ambiguous in a multi-source destination query.` }; } isGroupKeyColumn(query, column) { const groupBy = query.groupByClause; if (!groupBy || groupBy.mode || groupBy.grouping.length === 0) { return false; } return groupBy.grouping.some(grouping => { const candidate = unwrapParens(grouping); return candidate instanceof ColumnReference && this.sameResolvableColumnInQuery(query, candidate, column); }); } sameResolvableColumnInQuery(query, left, right) { if (sameColumnReference(left, right)) { return true; } if (!identifiersEqual(left.column.name, right.column.name)) { return false; } const leftSource = this.resolveColumnSourceAlias(query, left); const rightSource = this.resolveColumnSourceAlias(query, right); return leftSource !== null && leftSource === rightSource; } resolveColumnSourceAlias(query, column) { if (!query.fromClause) { return null; } const bindings = this.getSourceBindings(query.fromClause); const namespace = column.getNamespace(); if (namespace) { const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace)); return matches.length === 1 ? matches[0].alias : null; } return bindings.length === 1 ? bindings[0].alias : null; } rebaseCondition(expression, targetColumns, options) { const cloned = cloneValueComponent(expression, options); for (const reference of this.collectColumnReferences(cloned)) { const target = targetColumns.find(item => sameColumnReference(reference, item.sourceColumn)); if (target) { reference.qualifiedName = cloneColumnReference(target.targetColumn).qualifiedName; } } return cloned; } getSourceBindings(fromClause) { var _a, _b, _c; const bindings = [{ source: fromClause.source, alias: (_a = fromClause.source.getAliasName()) !== null && _a !== void 0 ? _a : "", join: null, joinIndex: -1, isPrimary: true }]; for (let index = 0; index < ((_b = fromClause.joins) !== null && _b !== void 0 ? _b : []).length; index += 1) { const join = fromClause.joins[index]; bindings.push({ source: join.source, alias: (_c = join.source.getAliasName()) !== null && _c !== void 0 ? _c : "", join, joinIndex: index, isPrimary: false }); } return bindings; } isBaseTableBinding(root, binding) { const source = binding.source.datasource; return source instanceof TableSource && !this.findCte(root, source.table.name); } isInnerJoin(join) { const joinType = join.joinType.value.trim().toLowerCase(); return joinType === "join" || joinType === "inner join"; } findNullableSideBoundary(fromClause, binding) { var _a; const joins = (_a = fromClause.joins) !== null && _a !== void 0 ? _a : []; if (binding.isPrimary) { return this.hasLaterJoinThatNullsPriorSources(joins, -1) ? { code: "OUTER_JOIN_NULLABLE_SIDE", reason: "Condition crosses OUTER JOIN nullable side; moving it may change semantics." } : null; } const join = binding.join; if (!join) { return null; } const joinType = join.joinType.value.toLowerCase(); if (joinType.includes("left")) { return { code: "OUTER_JOIN_NULLABLE_SIDE", reason: "Condition crosses LEFT JOIN nullable side; moving it may change semantics." }; } if (joinType.includes("full")) { return { code: "OUTER_JOIN_NULLABLE_SIDE", reason: "Condition crosses FULL JOIN nullable side; moving it may change semantics." }; } if (this.hasLaterJoinThatNullsPriorSources(joins, binding.joinIndex)) { return { code: "OUTER_JOIN_NULLABLE_SIDE", reason: "Condition crosses later RIGHT/FULL JOIN nullable side; moving it may change semantics." }; } return null; } hasLaterJoinThatNullsPriorSources(joins, sourceJoinIndex) { return joins .slice(sourceJoinIndex + 1) .some(join => { const joinType = join.joinType.value.toLowerCase(); return joinType.includes("right") || joinType.includes("full"); }); } hasOuterJoin(fromClause) { var _a; return ((_a = fromClause.joins) !== null && _a !== void 0 ? _a : []).some(join => { const joinType = join.joinType.value.toLowerCase(); return joinType.includes("left") || joinType.includes("right") || joinType.includes("full") || joinType.includes("outer"); }); } hasDistinctOnBoundary(query) { return query.selectClause.distinct instanceof DistinctOn; } hasOrdinaryDistinct(query) { return query.selectClause.distinct !== null && !this.hasDistinctOnBoundary(query); } getOutputColumnMatchCount(root, binding, columnName) { const target = this.resolveSourceQueryForColumns(root, binding.source); if (!target) { return 0; } if (target instanceof BinarySelectQuery) { const branches = this.collectUnionBranches(target); if ("code" in branches) { return 0; } return this.collectDirectOutputMatches(root, branches[0], columnName).length; } if (!(target instanceof SimpleSelectQuery)) { return 0; } return this.collectDirectOutputMatches(root, target, columnName, true).length; } collectUnionBranches(query) { const branches = []; const visit = (select) => { var _a; if (select instanceof SimpleSelectQuery) { branches.push(select); return null; } if (!(select instanceof BinarySelectQuery)) { return { code: "UNION_BOUNDARY", reason: "Condition would need distribution into a non-simple UNION branch, which is unsupported." }; } const operator = select.operator.value.trim().toLowerCase(); if (operator !== "union" && operator !== "union all") { return { code: "UNION_BOUNDARY", reason: `Condition would need distribution through '${select.operator.value}', which is unsupported.` }; } return (_a = visit(select.left)) !== null && _a !== void 0 ? _a : visit(select.right); }; const skip = visit(query); return skip !== null && skip !== void 0 ? skip : branches; } resolveUnionOutputIndex(root, firstBranch, outputColumnName) { const matches = []; this.collectSelectOutputs(root, firstBranch).forEach((item, index) => { if (identifiersEqual(item.name, outputColumnName)) { matches.push(index); } }); if (matches.length !== 1) { return { code: "AMBIGUOUS_TARGET_COLUMN", reason: matches.length === 0 ? `UNION output does not expose '${outputColumnName}' from its first branch.` : `UNION first branch exposes multiple '${outputColumnName}' columns.` }; } return { index: matches[0] }; } collectSelectOutputs(root, query) { var _a, _b, _c, _d; const commonTables = [ ...((_b = (_a = query.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []), ...((_d = (_c = root.withClause) === null || _c === void 0 ? void 0 : _c.tables) !== null && _d !== void 0 ? _d : []) ]; const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null); return collector.collect(query); } collectDirectOutputMatches(root, query, columnName, includeUnprovenPotential = false) { const collectedMatches = this.collectSelectOutputs(root, query).filter(item => identifiersEqual(item.name, columnName)); if (collectedMatches.length > 0) { return this.hasExplicitOutputMatch(query, columnName) ? [...collectedMatches, ...this.inferWildcardOutputMatches(root, query, columnName, true)] : collectedMatches; } return this.inferWildcardOutputMatches(root, query, columnName, includeUnprovenPotential); } hasExplicitOutputMatch(query, columnName) { return query.selectClause.items.some(item => { if (item.identifier) { return identifiersEqual(item.identifier.name, columnName); } return item.value instanceof ColumnReference && item.value.column.name !== "*" && identifiersEqual(item.value.column.name, columnName); }); } inferWildcardOutputMatches(root, query, columnName, includeUnprovenPotential) { const matches = []; query.selectClause.items.forEach((item, outputIndex) => { if (item.identifier || !(item.value instanceof ColumnReference) || item.value.column.name !== "*") { return; } const targetColumn = this.inferWildcardTargetColumn(root, query, item.value, columnName, includeUnprovenPotential); if (!targetColumn) { return; } if (targetColumn === "ambiguous") { matches.push(this.createInferredOutputColumn(columnName, new ColumnReference(null, columnName), outputIndex), this.createInferredOutputColumn(columnName, new ColumnReference(null, columnName), outputIndex)); return; } matches.push(this.createInferredOutputColumn(columnName, targetColumn, outputIndex)); }); return matches; } inferWildcardTargetColumn(root, query, wildcard, columnName, includeUnprovenPotential) { if (!query.fromClause) { return null; } const bindings = this.getSourceBindings(query.fromClause); if (wildcard.namespaces === null) { if (bindings.length !== 1) { return "ambiguous"; } return this.resolveWildcardColumnFromBinding(root, query, bindings[0], columnName, includeUnprovenPotential); } const namespace = wildcard.getNamespace(); const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace)); if (matches.length === 0) { return null; } return matches.length === 1 ? this.resolveWildcardColumnFromBinding(root, query, matches[0], columnName, includeUnprovenPotential) : "ambiguous"; } resolveWildcardColumnFromBinding(root, query, binding, columnName, includeUnprovenPotential) { const matches = this.collectSourceOutputMatches(root, query, binding.source, columnName); if (matches.length > 1) { return "ambiguous"; } if (matches.length === 1) { const value = matches[0].value; return value instanceof ColumnReference ? value : null; } return includeUnprovenPotential && binding.source.datasource instanceof TableSource ? this.createColumnForBinding(binding, columnName) : null; } collectSourceOutputMatches(root, query, source, columnName) { var _a, _b, _c, _d; const commonTables = [ ...((_b = (_a = query.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []), ...((_d = (_c = root.withClause) === null || _c === void 0 ? void 0 : _c.tables) !== null && _d !== void 0 ? _d : []) ]; const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null); return collector.collect(source).filter(item => identifiersEqual(item.name, columnName)); } createColumnForBinding(binding, columnName) { return new ColumnReference(binding.alias ? [binding.alias] : null, columnName); } createInferredOutputColumn(name, value, outputIndex) { return { name, value, outputIndex, sourceAlias: value.getNamespace() || null, sourceName: null, sourceColumnName: name }; } resolveSourceQueryForColumns(root, source) { var _a; if (source.datasource instanceof SubQuerySource) { return source.datasource.query; } if (source.datasource instanceof TableSource) { const cteQuery = (_a = this.findCte(root, source.datasource.table.name)) === null || _a === void 0 ? void 0 : _a.query; return cteQuery instanceof SimpleSelectQuery || cteQuery instanceof BinarySelectQuery ? cteQuery : null; } return null; } findCte(root, name) { var _a, _b; const normalized = normalizeIdentifier(name); const matches = ((_b = (_a = root.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []) .filter(table => normalizeIdentifier(table.getSourceAliasName()) === normalized); return matches.length === 1 ? matches[0] : null; } countTableSourceReferences(query, tableName) { const normalized = normalizeIdentifier(tableName); let count = 0; const visitSelect = (select) => { var _a, _b; if (select instanceof BinarySelectQuery) { visitSelect(select.left); visitSelect(select.right); return; } if (!(select instanceof SimpleSelectQuery)) { return; } if (select.fromClause) { for (const binding of this.getSourceBindings(select.fromClause)) { const source = binding.source.datasource; if (source instanceof TableSource && normalizeIdentifier(source.table.name) === normalized) { count += 1; } if (source instanceof SubQuerySource) { visitSelect(source.query); } } } for (const cte of (_b = (_a = select.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []) { if (cte.query instanceof SimpleSelectQuery || cte.query instanceof BinarySelectQuery) { visitSelect(cte.query); } } }; visitSelect(query); return count; } hasWindowUsage(query) { if (query.windowClause) { return true; } let found = false; const visit = (value) => { if (found) { return; } const candidate = unwrapParens(value); if (candidate instanceof FunctionCall) { if (candidate.over) { found = true; return; } if (candidate.argument) { visit(candidate.argument); } if (candidate.filterCondition) { visit(candidate.filterCondition); }