Skip to content

Commit ea2321b

Browse files
committed
fix projection flattening with .toAttribute
1 parent 4bded60 commit ea2321b

File tree

7 files changed

+85
-16
lines changed

7 files changed

+85
-16
lines changed

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,11 @@ public static Range rangeOf(Expression value, Expression lower, boolean includeL
253253
}
254254

255255
public static EsRelation relation() {
256-
return new EsRelation(EMPTY, new EsIndex(randomAlphaOfLength(8), emptyMap()), IndexMode.STANDARD);
256+
return relation(IndexMode.STANDARD);
257+
}
258+
259+
public static EsRelation relation(IndexMode mode) {
260+
return new EsRelation(EMPTY, new EsIndex(randomAlphaOfLength(8), emptyMap()), mode);
257261
}
258262

259263
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public static Tuple<List<Alias>, List<NamedExpression>> aliasedNulls(
6565
}
6666
// otherwise point to it since this avoids creating field copies
6767
else {
68-
projection = new Alias(attr.source(), attr.name(), nullAlias.toAttribute(), nullAlias.id());
68+
projection = new Alias(attr.source(), attr.name(), nullAlias.toAttribute(), attr.id());
6969
}
7070
} else {
7171
projection = attr;

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import org.elasticsearch.xpack.esql.core.expression.AttributeMap;
1111
import org.elasticsearch.xpack.esql.core.expression.AttributeSet;
1212
import org.elasticsearch.xpack.esql.core.expression.Expression;
13-
import org.elasticsearch.xpack.esql.core.expression.Expressions;
1413
import org.elasticsearch.xpack.esql.optimizer.LogicalOptimizerContext;
1514
import org.elasticsearch.xpack.esql.optimizer.rules.RuleUtils;
1615
import org.elasticsearch.xpack.esql.optimizer.rules.logical.OptimizerRules;
@@ -59,6 +58,6 @@ private static LogicalPlan replaceJoin(Join join) {
5958
}
6059
var aliasedNulls = RuleUtils.aliasedNulls(joinRightOutput, a -> true);
6160
var eval = new Eval(join.source(), join.left(), aliasedNulls.v1());
62-
return new Project(join.source(), eval, join.computeOutput(join.left().output(), Expressions.asAttributes(aliasedNulls.v2())));
61+
return new Project(join.source(), eval, join.computeOutputExpressions(join.left().output(), aliasedNulls.v2()));
6362
}
6463
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Lookup.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.elasticsearch.xpack.esql.core.capabilities.Resolvables;
1616
import org.elasticsearch.xpack.esql.core.expression.Attribute;
1717
import org.elasticsearch.xpack.esql.core.expression.Expression;
18+
import org.elasticsearch.xpack.esql.core.expression.Expressions;
1819
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
1920
import org.elasticsearch.xpack.esql.core.tree.Source;
2021
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
@@ -139,7 +140,7 @@ public List<Attribute> output() {
139140
if (localRelation == null) {
140141
throw new IllegalStateException("Cannot determine output of LOOKUP with unresolved table");
141142
}
142-
lazyOutput = Join.computeOutput(child().output(), localRelation.output(), joinConfig());
143+
lazyOutput = Expressions.asAttributes(Join.computeOutputExpressions(child().output(), localRelation.output(), joinConfig()));
143144
}
144145
return lazyOutput;
145146
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/InlineJoin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public Join replaceChildren(LogicalPlan left, LogicalPlan right) {
200200
}
201201

202202
@Override
203-
public List<Attribute> computeOutput(List<Attribute> left, List<Attribute> right) {
203+
List<Attribute> computeOutput(List<Attribute> left, List<Attribute> right) {
204204
JoinType joinType = config().type();
205205
List<Attribute> output;
206206
if (LEFT.equals(joinType)) {

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.elasticsearch.xpack.esql.core.expression.AttributeSet;
1919
import org.elasticsearch.xpack.esql.core.expression.Expression;
2020
import org.elasticsearch.xpack.esql.core.expression.Expressions;
21+
import org.elasticsearch.xpack.esql.core.expression.NamedExpression;
2122
import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute;
2223
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
2324
import org.elasticsearch.xpack.esql.core.tree.Source;
@@ -64,7 +65,7 @@
6465
import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG;
6566
import static org.elasticsearch.xpack.esql.core.type.DataType.UNSUPPORTED;
6667
import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION;
67-
import static org.elasticsearch.xpack.esql.expression.NamedExpressions.mergeOutputAttributes;
68+
import static org.elasticsearch.xpack.esql.expression.NamedExpressions.mergeOutputExpressions;
6869
import static org.elasticsearch.xpack.esql.plan.logical.join.JoinTypes.LEFT;
6970
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.commonType;
7071

@@ -210,30 +211,38 @@ public List<Attribute> rightOutputFields() {
210211
return rightOutputFields;
211212
}
212213

213-
public List<Attribute> computeOutput(List<Attribute> left, List<Attribute> right) {
214-
return computeOutput(left, right, config);
214+
List<Attribute> computeOutput(List<Attribute> left, List<Attribute> right) {
215+
return Expressions.asAttributes(computeOutputExpressions(left, right, config));
216+
}
217+
218+
public List<NamedExpression> computeOutputExpressions(List<? extends NamedExpression> left, List<? extends NamedExpression> right) {
219+
return computeOutputExpressions(left, right, config);
215220
}
216221

217222
/**
218223
* Combine the two lists of attributes into one.
219224
* In case of (name) conflicts, specify which sides wins, that is overrides the other column - the left or the right.
220225
*/
221-
public static List<Attribute> computeOutput(List<Attribute> leftOutput, List<Attribute> rightOutput, JoinConfig config) {
226+
public static List<NamedExpression> computeOutputExpressions(
227+
List<? extends NamedExpression> leftOutput,
228+
List<? extends NamedExpression> rightOutput,
229+
JoinConfig config
230+
) {
222231
JoinType joinType = config.type();
223-
List<Attribute> output;
232+
List<NamedExpression> output;
224233
// TODO: make the other side nullable
225234
if (LEFT.equals(joinType)) {
226235
if (config.joinOnConditions() == null) {
227236
// right side becomes nullable and overrides left except for join keys, which we preserve from the left
228237
AttributeSet rightKeys = AttributeSet.of(config.rightFields());
229-
List<Attribute> rightOutputWithoutMatchFields = rightOutput.stream()
230-
.filter(attr -> rightKeys.contains(attr) == false)
238+
List<? extends NamedExpression> rightOutputWithoutMatchFields = rightOutput.stream()
239+
.filter(ne -> rightKeys.contains(ne.toAttribute()) == false)
231240
.toList();
232-
output = mergeOutputAttributes(rightOutputWithoutMatchFields, leftOutput);
241+
output = mergeOutputExpressions(rightOutputWithoutMatchFields, leftOutput);
233242
} else {
234243
// We don't allow any attributes in the joinOnConditions that don't have unique names
235244
// so right always overwrites left in case of name clashes
236-
output = mergeOutputAttributes(rightOutput, leftOutput);
245+
output = mergeOutputExpressions(rightOutput, leftOutput);
237246
}
238247

239248
} else {

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161
import org.elasticsearch.xpack.esql.plan.logical.Project;
6262
import org.elasticsearch.xpack.esql.plan.logical.Row;
6363
import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan;
64+
import org.elasticsearch.xpack.esql.plan.logical.join.Join;
65+
import org.elasticsearch.xpack.esql.plan.logical.join.JoinConfig;
66+
import org.elasticsearch.xpack.esql.plan.logical.join.JoinTypes;
6467
import org.elasticsearch.xpack.esql.plan.logical.local.EmptyLocalSupplier;
6568
import org.elasticsearch.xpack.esql.plan.logical.local.EsqlProject;
6669
import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation;
@@ -96,6 +99,7 @@
9699
import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning;
97100
import static org.elasticsearch.xpack.esql.core.tree.Source.EMPTY;
98101
import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER;
102+
import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
99103
import static org.elasticsearch.xpack.esql.optimizer.rules.logical.OptimizerRules.TransformDirection.DOWN;
100104
import static org.elasticsearch.xpack.esql.optimizer.rules.logical.OptimizerRules.TransformDirection.UP;
101105
import static org.hamcrest.Matchers.contains;
@@ -1019,6 +1023,58 @@ public List<Attribute> output() {
10191023
assertThat(e.getMessage(), containsString("Output has changed from"));
10201024
}
10211025

1026+
/**
1027+
* Input:
1028+
* Project[[key{f}#2, int{f}#3, field1{f}#7, field2{f}#8]]
1029+
* \_Join[LEFT,[key{f}#2],[key{f}#6],null]
1030+
* |_EsRelation[JLfQlKmn][key{f}#2, int{f}#3, field1{f}#4, field2{f}#5]
1031+
* \_EsRelation[HQtEBOWq][LOOKUP][key{f}#6, field1{f}#7, field2{f}#8]
1032+
*
1033+
* Output:
1034+
* Project[[key{r}#2, int{f}#3, field1{r}#7, field1{r}#7 AS field2#8]]
1035+
* \_Eval[[null[KEYWORD] AS key#2, null[INTEGER] AS field1#7]]
1036+
* \_EsRelation[JLfQlKmn][key{f}#2, int{f}#3, field1{f}#4, field2{f}#5]
1037+
*/
1038+
public void testPruneLeftJoinOnNullMatchingFieldAndShadowingAttributes() {
1039+
var keyLeft = getFieldAttribute("key", KEYWORD);
1040+
var intFieldLeft = getFieldAttribute("int");
1041+
var fieldLeft1 = getFieldAttribute("field1");
1042+
var fieldLeft2 = getFieldAttribute("field2");
1043+
var leftRelation = EsqlTestUtils.relation(IndexMode.STANDARD)
1044+
.withAttributes(List.of(keyLeft, intFieldLeft, fieldLeft1, fieldLeft2));
1045+
1046+
var keyRight = getFieldAttribute("key", KEYWORD);
1047+
var fieldRight1 = getFieldAttribute("field1");
1048+
var fieldRight2 = getFieldAttribute("field2");
1049+
var rightRelation = EsqlTestUtils.relation(IndexMode.LOOKUP).withAttributes(List.of(keyRight, fieldRight1, fieldRight2));
1050+
1051+
JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(keyLeft), List.of(keyRight), null);
1052+
var join = new Join(EMPTY, leftRelation, rightRelation, joinConfig);
1053+
var project = new Project(EMPTY, join, List.of(keyLeft, intFieldLeft, fieldRight1, fieldRight2));
1054+
1055+
var testStats = statsForMissingField("key");
1056+
var localPlan = localPlan(project, testStats);
1057+
1058+
var projectOut = as(localPlan, Project.class);
1059+
var projectionsOut = projectOut.projections();
1060+
assertThat(Expressions.names(projectionsOut), contains("key", "int", "field1", "field2"));
1061+
assertThat(projectionsOut.get(0).id(), is(keyLeft.id()));
1062+
assertThat(projectionsOut.get(1).id(), is(intFieldLeft.id()));
1063+
assertThat(projectionsOut.get(2).id(), is(fieldRight1.id())); // id must remain from the RHS.
1064+
var aliasField2 = as(projectionsOut.get(3), Alias.class); // the projection must contain an alias ...
1065+
assertThat(aliasField2.id(), is(fieldRight2.id())); // ... with the same id as the original field.
1066+
1067+
var eval = as(projectOut.child(), Eval.class);
1068+
assertThat(Expressions.names(eval.fields()), contains("key", "field1"));
1069+
var keyEval = as(Alias.unwrap(eval.fields().get(0)), Literal.class);
1070+
assertThat(keyEval.value(), is(nullValue()));
1071+
assertThat(keyEval.dataType(), is(KEYWORD));
1072+
var field1Eval = as(Alias.unwrap(eval.fields().get(1)), Literal.class);
1073+
assertThat(field1Eval.value(), is(nullValue()));
1074+
assertThat(field1Eval.dataType(), is(INTEGER));
1075+
var source = as(eval.child(), EsRelation.class);
1076+
}
1077+
10221078
private IsNotNull isNotNull(Expression field) {
10231079
return new IsNotNull(EMPTY, field);
10241080
}
@@ -1078,6 +1134,6 @@ protected List<String> filteredWarnings() {
10781134
}
10791135

10801136
public static EsRelation relation() {
1081-
return new EsRelation(EMPTY, new EsIndex(randomAlphaOfLength(8), emptyMap()), randomFrom(IndexMode.values()));
1137+
return EsqlTestUtils.relation(randomFrom(IndexMode.values()));
10821138
}
10831139
}

0 commit comments

Comments
 (0)