|
10 | 10 | import org.elasticsearch.xpack.esql.core.expression.Alias; |
11 | 11 | import org.elasticsearch.xpack.esql.core.expression.Attribute; |
12 | 12 | import org.elasticsearch.xpack.esql.core.expression.AttributeMap; |
| 13 | +import org.elasticsearch.xpack.esql.core.expression.AttributeSet; |
13 | 14 | import org.elasticsearch.xpack.esql.core.expression.Expression; |
14 | 15 | import org.elasticsearch.xpack.esql.core.expression.Expressions; |
15 | 16 | import org.elasticsearch.xpack.esql.core.expression.NamedExpression; |
@@ -64,46 +65,42 @@ protected LogicalPlan rule(Join join) { |
64 | 65 | // used in the original Project; any such conflict needs to be resolved by copying the attribute under a temporary name via an |
65 | 66 | // Eval - and using the attribute from said Eval in the new downstream Project. |
66 | 67 | Set<String> lookupFieldNames = new HashSet<>(Expressions.names(join.rightOutputFields())); |
67 | | - PushDownUtils.AttributeReplacement replacement = PushDownUtils.renameAttributesInExpressions(lookupFieldNames, newProjections); |
| 68 | + List<NamedExpression> finalProjections = new ArrayList<>(newProjections.size()); |
| 69 | + AttributeMap.Builder<Alias> aliasesForReplacedAttributesBuilder = AttributeMap.builder(); |
| 70 | + AttributeSet leftOutput = updatedJoin.left().outputSet(); |
68 | 71 |
|
69 | | - List<Expression> conflictFreeProjections = replacement.rewrittenExpressions(); |
70 | | - List<Alias> evalAliases = new ArrayList<>(replacement.replacedAttributes().values()); |
71 | | - |
72 | | - if (evalAliases.isEmpty()) { |
73 | | - // No name conflicts, so no eval needed. |
74 | | - return new Project(project.source(), updatedJoin.replaceLeft(project.child()), newProjections); |
75 | | - } |
| 72 | + for (NamedExpression proj : newProjections) { |
| 73 | + // TODO: add assert to Project that ensures Alias to attr or pure attr. |
| 74 | + Attribute coreAttr = (Attribute) (proj instanceof Alias as ? as.child() : proj); |
| 75 | + // Only fields from the left need to be protected from conflicts - because fields from the right shadow them. |
| 76 | + if (leftOutput.contains(coreAttr) == false || lookupFieldNames.contains(coreAttr.name()) == false) { |
| 77 | + finalProjections.add(proj); |
| 78 | + } else { |
| 79 | + // Conflict - the core attribute will be shadowed by the `LOOKUP JOIN` and we need to alias it in an upstream Eval. |
| 80 | + Alias renaming = aliasesForReplacedAttributesBuilder.computeIfAbsent(coreAttr, a -> { |
| 81 | + String tempName = TemporaryNameUtils.locallyUniqueTemporaryName(a.name(), "temp_name"); |
| 82 | + return new Alias(a.source(), tempName, a, null, true); |
| 83 | + }); |
76 | 84 |
|
77 | | - // The conflict free projections replaced any shadowed attributes by temporary attributes that we'll create in an Eval. |
78 | | - // That's good if the replaced attribute was in an Alias; if the projection was a mere attribute to begin with, we need to |
79 | | - // alias it back to the name/id that's expected in the original output. |
80 | | - List<NamedExpression> finalProjections = new ArrayList<>(conflictFreeProjections.size()); |
81 | | - for (int i = 0; i < newProjections.size(); i++) { |
82 | | - Expression conflictFreeProj = conflictFreeProjections.get(i); |
83 | | - if (conflictFreeProj instanceof Alias as) { |
84 | | - // Already aliased - keep it, it's fine if the child was rewritten. |
85 | | - finalProjections.add(as); |
86 | | - } else if (conflictFreeProj instanceof Attribute conflictFreeAttr) { |
87 | | - Attribute expectedOutputAttr = (Attribute) newProjections.get(i); |
88 | | - if (expectedOutputAttr.semanticEquals(conflictFreeAttr)) { |
89 | | - // no conflict, wasn't rewritten |
90 | | - finalProjections.add(expectedOutputAttr); |
| 85 | + Attribute renamedAttribute = renaming.toAttribute(); |
| 86 | + Alias renamedBack; |
| 87 | + if (proj instanceof Alias as) { |
| 88 | + renamedBack = new Alias(as.source(), as.name(), renamedAttribute, as.id(), as.synthetic()); |
91 | 89 | } else { |
92 | | - Alias renameBack = new Alias( |
93 | | - expectedOutputAttr.source(), |
94 | | - expectedOutputAttr.name(), |
95 | | - conflictFreeAttr, |
96 | | - expectedOutputAttr.id(), |
97 | | - expectedOutputAttr.synthetic() |
98 | | - ); |
99 | | - finalProjections.add(renameBack); |
| 90 | + // no alias - that means proj == coreAttr |
| 91 | + renamedBack = new Alias(coreAttr.source(), coreAttr.name(), renamedAttribute, coreAttr.id(), coreAttr.synthetic()); |
100 | 92 | } |
101 | | - } else { |
102 | | - throw new IllegalStateException("Resolving a list of projections must yield a list of projections again"); |
| 93 | + finalProjections.add(renamedBack); |
103 | 94 | } |
104 | 95 | } |
105 | 96 |
|
106 | | - Eval eval = new Eval(project.source(), project.child(), evalAliases); |
| 97 | + if (aliasesForReplacedAttributesBuilder.isEmpty()) { |
| 98 | + // No name conflicts, so no eval needed. |
| 99 | + return new Project(project.source(), updatedJoin.replaceLeft(project.child()), newProjections); |
| 100 | + } |
| 101 | + |
| 102 | + List<Alias> renamesForEval = new ArrayList<>(aliasesForReplacedAttributesBuilder.build().values()); |
| 103 | + Eval eval = new Eval(project.source(), project.child(), renamesForEval); |
107 | 104 | Join finalJoin = new Join(join.source(), eval, updatedJoin.right(), updatedJoin.config()); |
108 | 105 |
|
109 | 106 | return new Project(project.source(), finalJoin, finalProjections); |
|
0 commit comments