Skip to content

Commit c2c401b

Browse files
committed
Implement algo
1 parent c11e139 commit c2c401b

File tree

2 files changed

+93
-23
lines changed

2 files changed

+93
-23
lines changed

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownJoinPastProject.java

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,23 @@
77

88
package org.elasticsearch.xpack.esql.optimizer.rules.logical;
99

10+
import org.elasticsearch.xpack.esql.core.expression.Alias;
11+
import org.elasticsearch.xpack.esql.core.expression.Attribute;
12+
import org.elasticsearch.xpack.esql.core.expression.AttributeMap;
13+
import org.elasticsearch.xpack.esql.core.expression.Expression;
14+
import org.elasticsearch.xpack.esql.core.expression.Expressions;
15+
import org.elasticsearch.xpack.esql.core.expression.NamedExpression;
16+
import org.elasticsearch.xpack.esql.plan.logical.Eval;
1017
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
1118
import org.elasticsearch.xpack.esql.plan.logical.Project;
12-
import org.elasticsearch.xpack.esql.plan.logical.Eval;
1319
import org.elasticsearch.xpack.esql.plan.logical.join.Join;
1420
import org.elasticsearch.xpack.esql.plan.logical.join.JoinTypes;
1521

22+
import java.util.ArrayList;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Set;
26+
1627
/**
1728
* If a {@link Project} is found in the left child of a left {@link Join}, perform it after. Due to requiring the projected attributes
1829
* later, field extractions can also happen later, making joins cheapter to execute on data nodes.
@@ -26,20 +37,76 @@
2637
public final class PushDownJoinPastProject extends OptimizerRules.OptimizerRule<Join> {
2738
@Override
2839
protected LogicalPlan rule(Join join) {
29-
if (join.left() instanceof Project project && join.config().type() == JoinTypes.LEFT)
30-
{
31-
// 1. Propagate any renames into the Join, as we will remove the upstream Project.
32-
// E.g. `RENAME field AS key | LOOKUP JOIN idx ON key` -> `LOOKUP JOIN idx ON field | ...`
33-
// 2. Construct the downstream Project using the Join's output.
34-
// Use trivial aliases for now, so we can easily update the expressions.
35-
// This is like adding a `RENAME field1 AS field1, field2 AS field2, ...` after the Join, where we can easily change the
36-
// referenced field names to deal with name conflicts.
37-
// 3. Propagate any renames from the upstream Project into the new downstream Project.
38-
// 4. Look for name conflicts: any name shadowed by the `LOOKUP JOIN` that was used in the original Project needs to be
39-
// aliased temporarily. Add an Eval upstream from the `LOOKUP JOIN` and propagate its renames into the new downstream
40-
// Project.
41-
// 5. Remove any trivial aliases from the new downstream Project - `RENAME field AS field` just becomes (the equivalent of)
42-
// `KEEP field`.
40+
if (join.left() instanceof Project project && join.config().type() == JoinTypes.LEFT) {
41+
AttributeMap.Builder<Expression> aliasBuilder = AttributeMap.builder();
42+
project.forEachExpression(Alias.class, a -> aliasBuilder.put(a.toAttribute(), a.child()));
43+
var aliasesFromProject = aliasBuilder.build();
44+
45+
// Propagate any renames into the Join, as we will remove the upstream Project.
46+
// E.g. `RENAME field AS key | LOOKUP JOIN idx ON key` -> `LOOKUP JOIN idx ON field | ...`
47+
Join updatedJoin = PushDownUtils.resolveRenamesFromMap(join, aliasesFromProject);
48+
49+
// Construct the expressions for the new downstream Project using the Join's output.
50+
// We need to carry over RENAMEs/aliases from the original upstream Project.
51+
List<Attribute> originalOutput = join.output();
52+
List<NamedExpression> newProjections = new ArrayList<>(originalOutput.size());
53+
for (Attribute attr : originalOutput) {
54+
Attribute resolved = (Attribute) aliasesFromProject.resolve(attr, attr);
55+
if (attr.semanticEquals(resolved)) {
56+
newProjections.add(attr);
57+
} else {
58+
Alias renamed = new Alias(attr.source(), attr.name(), resolved, attr.id(), attr.synthetic());
59+
newProjections.add(renamed);
60+
}
61+
}
62+
63+
// This doesn't deal with name conflicts yet. Any name shadowed by a lookup field from the `LOOKUP JOIN` could still have been
64+
// used in the original Project; any such conflict needs to be resolved by copying the attribute under a temporary name via an
65+
// Eval - and using the attribute from said Eval in the new downstream Project.
66+
Set<String> lookupFieldNames = new HashSet<>(Expressions.names(join.rightOutputFields()));
67+
PushDownUtils.AttributeReplacement replacement = PushDownUtils.renameAttributesInExpressions(lookupFieldNames, newProjections);
68+
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+
}
76+
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);
91+
} 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);
100+
}
101+
} else {
102+
throw new IllegalStateException("Resolving a list of projections must yield a list of projections again");
103+
}
104+
}
105+
106+
Eval eval = new Eval(project.source(), project.child(), evalAliases);
107+
Join finalJoin = new Join(join.source(), eval, updatedJoin.right(), updatedJoin.config());
108+
109+
return new Project(project.source(), finalJoin, finalProjections);
43110
}
44111

45112
return join;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownUtils.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.ArrayList;
2727
import java.util.HashMap;
2828
import java.util.HashSet;
29-
import java.util.LinkedHashSet;
3029
import java.util.List;
3130
import java.util.Map;
3231
import java.util.Set;
@@ -55,7 +54,7 @@ class PushDownUtils {
5554
public static <Plan extends UnaryPlan & GeneratingPlan<Plan>> LogicalPlan pushGeneratingPlanPastProjectAndOrderBy(Plan generatingPlan) {
5655
LogicalPlan child = generatingPlan.child();
5756
if (child instanceof OrderBy orderBy) {
58-
Set<String> generatedFieldNames = new LinkedHashSet<>(Expressions.names(generatingPlan.generatedAttributes()));
57+
Set<String> generatedFieldNames = new HashSet<>(Expressions.names(generatingPlan.generatedAttributes()));
5958

6059
// Look for attributes in the OrderBy's expressions and create aliases with temporary names for them.
6160
AttributeReplacement nonShadowedOrders = renameAttributesInExpressions(generatedFieldNames, orderBy.order());
@@ -91,8 +90,7 @@ public static <Plan extends UnaryPlan & GeneratingPlan<Plan>> LogicalPlan pushGe
9190

9291
List<Attribute> generatedAttributes = generatingPlan.generatedAttributes();
9392

94-
@SuppressWarnings("unchecked")
95-
Plan generatingPlanWithResolvedExpressions = (Plan) resolveRenamesFromProject(generatingPlan, project);
93+
Plan generatingPlanWithResolvedExpressions = resolveRenamesFromProject(generatingPlan, project);
9694

9795
Set<String> namesReferencedInRenames = new HashSet<>();
9896
for (NamedExpression ne : project.projections()) {
@@ -145,7 +143,7 @@ public static <Plan extends UnaryPlan & GeneratingPlan<Plan>> LogicalPlan pushGe
145143
* Returns the rewritten expressions and a map with an alias for each replaced attribute; the rewritten expressions reference
146144
* these aliases.
147145
*/
148-
private static AttributeReplacement renameAttributesInExpressions(
146+
public static AttributeReplacement renameAttributesInExpressions(
149147
Set<String> attributeNamesToRename,
150148
List<? extends Expression> expressions
151149
) {
@@ -198,13 +196,18 @@ public static Project pushDownPastProject(UnaryPlan parent) {
198196
}
199197
}
200198

201-
private static UnaryPlan resolveRenamesFromProject(UnaryPlan plan, Project project) {
199+
public static <P extends LogicalPlan> P resolveRenamesFromProject(P plan, Project project) {
202200
AttributeMap.Builder<Expression> aliasBuilder = AttributeMap.builder();
203201
project.forEachExpression(Alias.class, a -> aliasBuilder.put(a.toAttribute(), a.child()));
204202
var aliases = aliasBuilder.build();
205203

206-
return (UnaryPlan) plan.transformExpressionsOnly(ReferenceAttribute.class, r -> aliases.resolve(r, r));
204+
return resolveRenamesFromMap(plan, aliases);
205+
}
206+
207+
@SuppressWarnings("unchecked")
208+
public static <P extends LogicalPlan> P resolveRenamesFromMap(P plan, AttributeMap<Expression> map) {
209+
return (P) plan.transformExpressionsOnly(ReferenceAttribute.class, r -> map.resolve(r, r));
207210
}
208211

209-
private record AttributeReplacement(List<Expression> rewrittenExpressions, AttributeMap<Alias> replacedAttributes) {}
212+
public record AttributeReplacement(List<Expression> rewrittenExpressions, AttributeMap<Alias> replacedAttributes) {}
210213
}

0 commit comments

Comments
 (0)