|
10 | 10 | import org.elasticsearch.common.util.Maps; |
11 | 11 | import org.elasticsearch.index.IndexMode; |
12 | 12 | import org.elasticsearch.xpack.esql.core.expression.Alias; |
| 13 | +import org.elasticsearch.xpack.esql.core.expression.Attribute; |
13 | 14 | import org.elasticsearch.xpack.esql.core.expression.AttributeSet; |
14 | 15 | import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; |
15 | 16 | import org.elasticsearch.xpack.esql.core.expression.Literal; |
16 | 17 | import org.elasticsearch.xpack.esql.core.expression.NamedExpression; |
| 18 | +import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute; |
17 | 19 | import org.elasticsearch.xpack.esql.core.type.DataType; |
18 | 20 | import org.elasticsearch.xpack.esql.core.type.PotentiallyUnmappedKeywordEsField; |
19 | 21 | import org.elasticsearch.xpack.esql.optimizer.LocalLogicalOptimizerContext; |
|
22 | 24 | import org.elasticsearch.xpack.esql.plan.logical.Eval; |
23 | 25 | import org.elasticsearch.xpack.esql.plan.logical.Filter; |
24 | 26 | import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; |
| 27 | +import org.elasticsearch.xpack.esql.plan.logical.MvExpand; |
25 | 28 | import org.elasticsearch.xpack.esql.plan.logical.OrderBy; |
26 | 29 | import org.elasticsearch.xpack.esql.plan.logical.Project; |
27 | 30 | import org.elasticsearch.xpack.esql.plan.logical.RegexExtract; |
@@ -81,7 +84,7 @@ else if (plan instanceof Project project) { |
81 | 84 | Alias nullAlias = nullLiteral.get(f.dataType()); |
82 | 85 | // save the first field as null (per datatype) |
83 | 86 | if (nullAlias == null) { |
84 | | - // In case of batch executions on data nodes and join exists, SearchStats may not always be available for all, |
| 87 | + // In case of batch executions on data nodes and join exists, SearchStats may not always be available for all |
85 | 88 | // fields, creating a new alias for null with the same id as the field id can potentially cause planEval to add a |
86 | 89 | // duplicated ChannelSet to a layout, and Layout.builder().build() could throw a NullPointerException. |
87 | 90 | // As a workaround, assign a new alias id to the null alias when join exists and SearchStats is not available. |
@@ -117,14 +120,40 @@ else if (plan instanceof Project project) { |
117 | 120 | ? f |
118 | 121 | : Literal.of(f, null) |
119 | 122 | ); |
| 123 | + } else if (plan instanceof MvExpand m) { |
| 124 | + NamedExpression target = m.target(); |
| 125 | + AttributeSet joinAttributes = joinAttributes(m); |
| 126 | + if (joinAttributes.isEmpty() == false // rewrite only when there is join, TODO do we want to rewrite when there is no join? |
| 127 | + && target instanceof FieldAttribute f |
| 128 | + && stats.exists(f.fieldName()) == false |
| 129 | + && joinAttributes.contains(f) == false |
| 130 | + && f.field() instanceof PotentiallyUnmappedKeywordEsField == false) { |
| 131 | + // Replace missing target field with null. |
| 132 | + Alias alias = new Alias(f.source(), f.name(), Literal.of(f, null)); |
| 133 | + NamedExpression nullTarget = alias.toAttribute(); |
| 134 | + plan = new Eval(m.source(), m.child(), List.of(alias)); |
| 135 | + // The expanded reference is built on top of target field with the same name, and the parent plans all reference to the |
| 136 | + // expanded reference other than the target field, keep expanded's id unchanged, otherwise the parent plans cannot find |
| 137 | + // it. |
| 138 | + Attribute nullExpanded = new ReferenceAttribute( |
| 139 | + nullTarget.source(), |
| 140 | + nullTarget.name(), |
| 141 | + nullTarget.dataType(), |
| 142 | + nullTarget.nullable(), |
| 143 | + m.expanded().id(), |
| 144 | + false |
| 145 | + ); |
| 146 | + plan = new MvExpand(m.source(), plan, nullTarget, nullExpanded); |
| 147 | + } |
120 | 148 | } |
121 | | - |
122 | 149 | return plan; |
123 | 150 | } |
124 | 151 |
|
125 | | - private AttributeSet joinAttributes(Project project) { |
| 152 | + private AttributeSet joinAttributes(LogicalPlan plan) { |
126 | 153 | var attributes = new AttributeSet(); |
127 | | - project.forEachDown(Join.class, j -> j.right().forEachDown(EsRelation.class, p -> attributes.addAll(p.output()))); |
| 154 | + if (plan instanceof Project || plan instanceof MvExpand) { |
| 155 | + plan.forEachDown(Join.class, j -> j.right().forEachDown(EsRelation.class, p -> attributes.addAll(p.output()))); |
| 156 | + } |
128 | 157 | return attributes; |
129 | 158 | } |
130 | 159 | } |
0 commit comments