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
JavaScript
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);
}