This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 9b89eee2ff2 branch-4.2: [fix](MV) Accept constant query outputs when
matching a materialized view #69787 (#70021)
9b89eee2ff2 is described below
commit 9b89eee2ff202c9fbf8bcfff3dc38752349303b6
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Nov 3 17:07:17 2026 +0800
branch-4.2: [fix](MV) Accept constant query outputs when matching a
materialized view #69787 (#70021)
Cherry-picked from #69787
Co-authored-by: minghong <[email protected]>
---
.../org/apache/doris/nereids/CascadesContext.java | 12 +-
.../mv/AbstractMaterializedViewRule.java | 5 +-
.../exploration/mv/MaterializedViewUtils.java | 207 +++++++++++++++-
.../mv/MaterializedViewUtilsAlignTest.java | 273 +++++++++++++++++++++
4 files changed, 488 insertions(+), 9 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/CascadesContext.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/CascadesContext.java
index 0215f007022..95d8d60574a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/CascadesContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/CascadesContext.java
@@ -42,6 +42,7 @@ import org.apache.doris.nereids.properties.PhysicalProperties;
import org.apache.doris.nereids.rules.RuleFactory;
import org.apache.doris.nereids.rules.RuleSet;
import org.apache.doris.nereids.rules.exploration.mv.MaterializationContext;
+import org.apache.doris.nereids.rules.exploration.mv.MaterializedViewUtils;
import org.apache.doris.nereids.trees.expressions.Alias;
import org.apache.doris.nereids.trees.expressions.CTEId;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -276,15 +277,22 @@ public class CascadesContext implements ScheduleContext {
if (!statementContext.getRewrittenPlansByMv().isEmpty()) {
// copy tmp plan for mv rewrite firstly
for (Plan rewrittenPlan : rewrittenPlansByMv) {
+ // the rewritten plan may define a constant output with
another exprId than the origin plan, align
+ // the outputs before the comparison, only a same constant is
allowed to change the exprId
+ Plan alignedPlan =
MaterializedViewUtils.normalizeSinkExpressions(rewrittenPlan, plan);
+ if (alignedPlan != null &&
!alignedPlan.getLogicalProperties().equals(plan.getLogicalProperties())) {
+ alignedPlan =
MaterializedViewUtils.alignPlanOutputWithOrigin(rewrittenPlan, plan);
+ }
// aggregate_without_roll_up query_13_0 cause error into
targetGroup but differ in logical properties
// tmp rewritten plan output is different from final rewritten
plan output
- if
(!rewrittenPlan.getLogicalProperties().equals(plan.getLogicalProperties())) {
+ if (alignedPlan == null
+ ||
!alignedPlan.getLogicalProperties().equals(plan.getLogicalProperties())) {
LOG.error("rewritten plan in rbo logical properties are "
+ "different from original plan, query id
is {}",
getConnectContext().getQueryIdentifier());
continue;
}
- this.memo.copyIn(rewrittenPlan, this.memo.getRoot(), false);
+ this.memo.copyIn(alignedPlan, this.memo.getRoot(), false);
}
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java
index 768801f8bf6..ba4b0a1b370 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java
@@ -595,7 +595,10 @@ public abstract class AbstractMaterializedViewRule
implements ExplorationRuleFac
List<Expression> rewrittenExpressions = new ArrayList<>();
for (int exprIndex = 0; exprIndex < sourceShuttledExpressions.size();
exprIndex++) {
Expression expressionShuttledToRewrite =
sourceShuttledExpressions.get(exprIndex);
- if (expressionShuttledToRewrite instanceof Literal) {
+ if (expressionShuttledToRewrite.isConstant()) {
+ // a constant expression needs no counterpart in the view:
e.g. the query column is pinned to a
+ // literal by a predicate, or it is materialized as a constant
above an aggregate, so the view does
+ // not have to output it
rewrittenExpressions.add(expressionShuttledToRewrite);
continue;
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtils.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtils.java
index 71d36282c34..19ad9c0052b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtils.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtils.java
@@ -456,29 +456,224 @@ public class MaterializedViewUtils {
if (rewrittenPlan.getOutput().size() !=
originPlan.getOutput().size()) {
return null;
}
- if
(rewrittenPlan.getLogicalProperties().equals(originPlan.getLogicalProperties()))
{
- return rewrittenPlan;
- }
List<NamedExpression> rewrittenPlanOutputExprList =
rewrittenPlan.getOutputExprs();
List<? extends NamedExpression> originPlanOutputExprList =
originPlan.getOutput();
if (rewrittenPlanOutputExprList.size() !=
originPlanOutputExprList.size()) {
return null;
}
+ List<Slot> childOutputs = rewrittenPlan.child().getOutput();
+ Map<ExprId, Slot> childOutputByExprId = Maps.newHashMap();
+ for (Slot childOutput : childOutputs) {
+ childOutputByExprId.putIfAbsent(childOutput.getExprId(),
childOutput);
+ }
+ boolean childOutputsPositional = childOutputs.size() ==
rewrittenPlanOutputExprList.size();
+ // a rule can change the exprIds of the child outputs without
updating the sink outputs,
+ // e.g. when a group by key is eliminated by a uniform
predicate. The stale sink outputs
+ // have to be replaced by the output which the child produces
at the same position.
+ boolean outputsResolvable = true;
+ for (NamedExpression rewrittenExpression :
rewrittenPlanOutputExprList) {
+ if (!isExpressionResolvable(rewrittenExpression,
childOutputByExprId)) {
+ outputsResolvable = false;
+ break;
+ }
+ }
+ if (outputsResolvable
+ &&
rewrittenPlan.getLogicalProperties().equals(originPlan.getLogicalProperties()))
{
+ return rewrittenPlan;
+ }
+ if (!outputsResolvable && !childOutputsPositional) {
+ // the sink outputs reference slots which the child does
not produce and they can not
+ // be mapped to the child outputs by position, the
rewritten plan is not usable
+ return null;
+ }
List<NamedExpression> normalizedOutputExprList = new
ArrayList<>();
for (int i = 0; i < rewrittenPlanOutputExprList.size(); i++) {
NamedExpression rewrittenExpression =
rewrittenPlanOutputExprList.get(i);
+ if (childOutputsPositional &&
!isExpressionResolvable(rewrittenExpression, childOutputByExprId)) {
+ rewrittenExpression = childOutputs.get(i);
+ }
NamedExpression originalExpression =
originPlanOutputExprList.get(i);
normalizedOutputExprList.add(normalizeExpression(originalExpression,
rewrittenExpression, true));
}
- LogicalProject<Plan> project = new
LogicalProject<>(normalizedOutputExprList,
- rewrittenPlan.child());
- return rewrittenPlan.withChildren(project);
+ // Set the outputs on the sink. An output exprId which the
child does not produce yet is
+ // materialized by a project between the sink and the child,
see withSinkOutputs.
+ return withSinkOutputs(rewrittenPlan,
normalizedOutputExprList);
}
}, null);
}
+ /**
+ * Set the outputs on the sink and keep the output exprIds resolvable for
the physical translation.
+ *
+ * <p>A sink output whose exprId the child does not produce yet, e.g. the
origin exprId of a constant
+ * which the view rewrite materialized with a fresh exprId, is
materialized by a project between the
+ * sink and its child. The outputs which the child already produces are
passed through with the
+ * nullable property of the child, so a rule which merges the project with
the project of the child,
+ * e.g. MergeProjectsCBO, derives the same nullable property and the plan
keeps the logical properties
+ * of the group it is inserted into.
+ */
+ private static Plan withSinkOutputs(LogicalSink<? extends Plan> sink,
List<NamedExpression> sinkOutputs) {
+ Plan child = sink.child();
+ Map<ExprId, Slot> childOutputs = Maps.newHashMap();
+ for (Slot childOutput : child.getOutput()) {
+ childOutputs.putIfAbsent(childOutput.getExprId(), childOutput);
+ }
+ boolean needProject = false;
+ List<NamedExpression> projectOutputs = new
ArrayList<>(sinkOutputs.size());
+ for (NamedExpression sinkOutput : sinkOutputs) {
+ Slot childOutput = childOutputs.get(sinkOutput.getExprId());
+ if (childOutput == null) {
+ // the child does not produce the exprId, materialize it by
the output expression
+ projectOutputs.add(sinkOutput);
+ needProject = true;
+ } else {
+ // the child already produces the exprId, pass it through with
the nullable property of
+ // the child, the changed nullable property of the sink output
is only applied on the sink
+ projectOutputs.add(childOutput);
+ }
+ }
+ if (!needProject) {
+ return sink.withOutputExprs(sinkOutputs);
+ }
+ // The sink outputs reference the outputs of the project, not the
expressions below the project,
+ // otherwise a sink which validates its output expressions, e.g. a
result sink, would still see a
+ // slot which the sink child does not produce.
+ List<NamedExpression> projectSinkOutputs = sinkOutputs.stream()
+ .map(output -> (NamedExpression) output.toSlot())
+ .collect(ImmutableList.toImmutableList());
+ return sink.withOutputExprs(projectSinkOutputs)
+ .withChildren(new LogicalProject<>(projectOutputs, child));
+ }
+
+ private static boolean isExpressionResolvable(Expression expression,
Map<ExprId, Slot> childOutputs) {
+ for (Slot inputSlot : expression.getInputSlots()) {
+ if (!childOutputs.containsKey(inputSlot.getExprId())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Align the outputs of a plan rewritten by a materialized view with the
origin query plan, so that both plans
+ * can be alternatives of one memo group.
+ *
+ * <p>An output keeps its exprId. The exprId of the origin output may only
be substituted
+ * <ul>
+ * <li>when both plans define that output as the same constant: a
column which a predicate pins to a
+ * literal can be materialized with a fresh exprId, and the two plans
then still compute the same value,
+ * or</li>
+ * <li>when the rewritten output describes the same column as the
origin one: the name and the type have
+ * to match and the qualifier has to be kept by both sides and be
equal, so that a same named column of
+ * another relation (e.g. the two relations of a self join) is not
accepted.</li>
+ * </ul>
+ * Otherwise the two plans are not known to produce the same columns (e.g.
a self join can expose two same
+ * named columns of two relations) and the rewritten plan must not be used.
+ */
+ public static Plan alignPlanOutputWithOrigin(Plan rewrittenPlan, Plan
originPlan) {
+ List<Slot> originOutputs = originPlan.getOutput();
+ List<Slot> rewrittenOutputs = rewrittenPlan instanceof LogicalSink
+ ? ((LogicalSink<?>) rewrittenPlan).child().getOutput()
+ : rewrittenPlan.getOutput();
+ if (originOutputs.size() != rewrittenOutputs.size()) {
+ return null;
+ }
+ Map<ExprId, Expression> originOutputDefinitions =
collectNamedExpressionDefinitions(originPlan);
+ Map<ExprId, Expression> rewrittenOutputDefinitions =
collectNamedExpressionDefinitions(rewrittenPlan);
+ List<NamedExpression> alignedOutputs = new
ArrayList<>(originOutputs.size());
+ for (int i = 0; i < originOutputs.size(); i++) {
+ Slot originSlot = originOutputs.get(i);
+ Slot rewrittenSlot = rewrittenOutputs.get(i);
+ if (rewrittenSlot.getExprId().equals(originSlot.getExprId())) {
+ if (rewrittenSlot instanceof SlotReference &&
rewrittenSlot.nullable() != originSlot.nullable()) {
+ alignedOutputs.add(((SlotReference)
rewrittenSlot).withNullable(originSlot.nullable()));
+ } else {
+ alignedOutputs.add(rewrittenSlot);
+ }
+ continue;
+ }
+ // the slot itself is not a constant, the definition of its exprId
is
+ Expression originDefinition =
originOutputDefinitions.get(originSlot.getExprId());
+ Expression rewrittenDefinition =
rewrittenOutputDefinitions.get(rewrittenSlot.getExprId());
+ if (!isSameConstant(rewrittenDefinition, originDefinition)
+ && !isSameColumn(rewrittenSlot, originSlot)) {
+ return null;
+ }
+ Expression inner = rewrittenSlot.nullable() ==
originSlot.nullable()
+ ? rewrittenSlot
+ : (originSlot.nullable() ? new Nullable(rewrittenSlot) :
new NonNullable(rewrittenSlot));
+ alignedOutputs.add(new Alias(originSlot.getExprId(), inner,
originSlot.getName()));
+ }
+ if (rewrittenPlan instanceof LogicalSink) {
+ // Only the outputs of the sink have to be aligned. If an aligned
exprId is not produced by the
+ // child yet, withSinkOutputs materializes it, otherwise a sink
which translates its outputs by
+ // exprId, e.g. a file sink, can not resolve the slot.
+ return withSinkOutputs((LogicalSink<?>) rewrittenPlan,
alignedOutputs);
+ }
+ return new LogicalProject<>(alignedOutputs, rewrittenPlan);
+ }
+
+ /**
+ * Whether the rewritten output describes the same column as the origin
one. The rewritten plan is built by the
+ * materialized view rewrite which keeps the position of the query
outputs, so the name, the type, the qualifier
+ * and the sub column path have to match. The qualifier has to be kept by
both sides: a missing qualifier can not
+ * rule out that the slot is the same named column of another relation,
e.g. the two relations of a self join, so
+ * an output without a qualifier is rejected and the rewritten plan is not
used. This also rejects an output which
+ * is added or removed and shifts the columns.
+ */
+ private static boolean isSameColumn(Slot rewrittenSlot, Slot originSlot) {
+ if (!rewrittenSlot.getName().equalsIgnoreCase(originSlot.getName())
+ ||
!rewrittenSlot.getDataType().equals(originSlot.getDataType())) {
+ return false;
+ }
+ List<String> rewrittenQualifier = rewrittenSlot.getQualifier();
+ List<String> originQualifier = originSlot.getQualifier();
+ if (rewrittenQualifier.isEmpty() ||
!rewrittenQualifier.equals(originQualifier)) {
+ return false;
+ }
+ // the sub column path is part of the column identity, e.g.
variant['a'] and variant['b'] are
+ // different columns although they have the same name, type and
qualifier
+ List<String> rewrittenSubPath = rewrittenSlot instanceof SlotReference
+ ? ((SlotReference) rewrittenSlot).getSubPath() :
ImmutableList.of();
+ List<String> originSubPath = originSlot instanceof SlotReference
+ ? ((SlotReference) originSlot).getSubPath() :
ImmutableList.of();
+ return rewrittenSubPath.equals(originSubPath);
+ }
+
+ private static Map<ExprId, Expression>
collectNamedExpressionDefinitions(Plan plan) {
+ Map<ExprId, Expression> definitions = Maps.newHashMap();
+ plan.foreach(node -> {
+ for (Expression expression : ((Plan) node).getExpressions()) {
+ if (!(expression instanceof NamedExpression)) {
+ continue;
+ }
+ NamedExpression namedExpression = (NamedExpression) expression;
+ Expression existing =
definitions.get(namedExpression.getExprId());
+ // prefer the definition which materializes the value, a plain
slot reference only consumes it
+ if (existing == null || (!existing.isConstant() &&
namedExpression.isConstant())) {
+ definitions.put(namedExpression.getExprId(),
namedExpression);
+ }
+ }
+ return false;
+ });
+ return definitions;
+ }
+
+ private static boolean isSameConstant(Expression rewrittenDefinition,
Expression originDefinition) {
+ if (rewrittenDefinition == null || originDefinition == null
+ || !rewrittenDefinition.isConstant() ||
!originDefinition.isConstant()) {
+ return false;
+ }
+ Expression rewrittenConstant = rewrittenDefinition instanceof Alias
+ ? rewrittenDefinition.child(0) : rewrittenDefinition;
+ Expression originConstant = originDefinition instanceof Alias
+ ? originDefinition.child(0) : originDefinition;
+ return
rewrittenConstant.getDataType().equals(originConstant.getDataType())
+ && rewrittenConstant.equals(originConstant);
+ }
+
/**
* Normalize expression with query, keep the consistency of exprId and
nullable props with
* query
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtilsAlignTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtilsAlignTest.java
new file mode 100644
index 00000000000..666e38b2811
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtilsAlignTest.java
@@ -0,0 +1,273 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.exploration.mv;
+
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.trees.plans.logical.LogicalFileSink;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
+import org.apache.doris.nereids.types.IntegerType;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Test for {@link MaterializedViewUtils#alignPlanOutputWithOrigin} and
+ * {@link MaterializedViewUtils#normalizeSinkExpressions}.
+ */
+public class MaterializedViewUtilsAlignTest {
+
+ @Test
+ public void testSameConstantWithDifferentExprIdIsAccepted() {
+ ExprId originExprId = new ExprId(10);
+ Plan origin = project(new Alias(originExprId, new IntegerLiteral(1),
"k1"));
+ Plan rewritten = project(new Alias(new ExprId(11), new
IntegerLiteral(1), "k1"));
+
+ Plan aligned =
MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten, origin);
+
+ Assertions.assertNotNull(aligned);
+ Assertions.assertEquals(originExprId,
aligned.getOutput().get(0).getExprId());
+ Assertions.assertEquals("k1", aligned.getOutput().get(0).getName());
+ }
+
+ @Test
+ public void testSameNameDifferentConstantIsRejected() {
+ Plan origin = project(new Alias(new ExprId(10), new IntegerLiteral(1),
"k1"));
+ Plan rewritten = project(new Alias(new ExprId(11), new
IntegerLiteral(2), "k1"));
+
+
Assertions.assertNull(MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten,
origin));
+ }
+
+ @Test
+ public void testSameColumnWithSameQualifierIsAccepted() {
+ ExprId originExprId = new ExprId(10);
+ Plan origin = project(slot(originExprId, ImmutableList.of("a")));
+ Plan rewritten = project(slot(new ExprId(11), ImmutableList.of("a")));
+
+ Plan aligned =
MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten, origin);
+
+ Assertions.assertNotNull(aligned);
+ Assertions.assertEquals(originExprId,
aligned.getOutput().get(0).getExprId());
+ Assertions.assertEquals("k1", aligned.getOutput().get(0).getName());
+ }
+
+ @Test
+ public void testSameNameColumnOfDifferentRelationIsRejected() {
+ Plan origin = project(slot(new ExprId(10), ImmutableList.of("a")));
+ Plan rewritten = project(slot(new ExprId(11), ImmutableList.of("b")));
+
+
Assertions.assertNull(MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten,
origin));
+ }
+
+ @Test
+ public void testSameNameColumnWithoutQualifierIsRejected() {
+ // The rewritten output lost the qualifier, so it can not be told
apart from the same named column
+ // of another relation, e.g. the other relation of a self join.
+ Plan origin = project(slot(new ExprId(10), ImmutableList.of("a")));
+ Plan rewritten = project(slot(new ExprId(11), ImmutableList.of()));
+
+
Assertions.assertNull(MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten,
origin));
+ }
+
+ @Test
+ public void testSameNameColumnWithoutQualifierOnBothSidesIsRejected() {
+ // Both sides lost the qualifier, so the outputs can still be the same
named columns of two
+ // different relations.
+ Plan origin = project(slot(new ExprId(10), ImmutableList.of()));
+ Plan rewritten = project(slot(new ExprId(11), ImmutableList.of()));
+
+
Assertions.assertNull(MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten,
origin));
+ }
+
+ @Test
+ public void testSameNameColumnOfDifferentSubPathIsRejected() {
+ // variant['a'] and variant['b'] have the same name, type and
qualifier but are different columns.
+ Plan origin = project(slot(new ExprId(10),
ImmutableList.of("a")).withSubPath(ImmutableList.of("a")));
+ Plan rewritten = project(slot(new ExprId(11),
ImmutableList.of("a")).withSubPath(ImmutableList.of("b")));
+
+
Assertions.assertNull(MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten,
origin));
+ }
+
+ @Test
+ public void testSameNameColumnOfSameSubPathIsAccepted() {
+ ExprId originExprId = new ExprId(10);
+ Plan origin = project(slot(originExprId,
ImmutableList.of("a")).withSubPath(ImmutableList.of("b")));
+ Plan rewritten = project(slot(new ExprId(11),
ImmutableList.of("a")).withSubPath(ImmutableList.of("b")));
+
+ Plan aligned =
MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten, origin);
+
+ Assertions.assertNotNull(aligned);
+ Assertions.assertEquals(originExprId,
aligned.getOutput().get(0).getExprId());
+ }
+
+ @Test
+ public void testFileSinkOutputWithFreshExprIdIsMaterialized() {
+ ExprId originExprId = new ExprId(10);
+ ExprId rewrittenExprId = new ExprId(11);
+ SlotReference rewrittenSlot = slot(rewrittenExprId,
ImmutableList.of("a"));
+ LogicalFileSink<Plan> rewritten =
fileSink(ImmutableList.of(rewrittenSlot), project(rewrittenSlot));
+ LogicalFileSink<Plan> origin =
fileSink(ImmutableList.of(slot(originExprId, ImmutableList.of("a"))),
+ oneRow());
+
+ Plan normalized =
MaterializedViewUtils.normalizeSinkExpressions(rewritten, origin);
+
+ Assertions.assertNotNull(normalized);
+ Assertions.assertEquals(originExprId,
normalized.getOutput().get(0).getExprId());
+ Assertions.assertTrue(normalized.child(0) instanceof LogicalProject);
+ assertSinkOutputsResolvable(normalized);
+ assertSinkOutputExpressionsResolvable(normalized);
+ }
+
+ @Test
+ public void testSinkWithOnlyNullablePropertyChangedDoesNotAddProject() {
+ ExprId exprId = new ExprId(10);
+ SlotReference childOutput = new SlotReference(exprId, "k1",
IntegerType.INSTANCE, true,
+ ImmutableList.of("a"));
+ LogicalOneRowRelation child = new LogicalOneRowRelation(new
RelationId(1), ImmutableList.of(childOutput));
+ LogicalFileSink<Plan> rewritten =
fileSink(ImmutableList.of(childOutput), child);
+ LogicalFileSink<Plan> origin = fileSink(ImmutableList.of(
+ new SlotReference(exprId, "k1", IntegerType.INSTANCE, false,
ImmutableList.of("a"))), oneRow());
+
+ Plan normalized =
MaterializedViewUtils.normalizeSinkExpressions(rewritten, origin);
+
+ Assertions.assertNotNull(normalized);
+ Assertions.assertSame(child, normalized.child(0));
+ Assertions.assertFalse(normalized.getOutput().get(0).nullable());
+ assertSinkOutputExpressionsResolvable(normalized);
+ }
+
+ @Test
+ public void
testMaterializedProjectKeepsChildNullableForPassThroughOutput() {
+ ExprId originFirst = new ExprId(10);
+ ExprId rewrittenFirst = new ExprId(11);
+ ExprId shared = new ExprId(12);
+ SlotReference childFirst = slot(rewrittenFirst, ImmutableList.of("a"));
+ SlotReference childShared = new SlotReference(shared, "k2",
IntegerType.INSTANCE, true,
+ ImmutableList.of("a"));
+ LogicalOneRowRelation child = new LogicalOneRowRelation(new
RelationId(1),
+ ImmutableList.of(childFirst, childShared));
+ LogicalFileSink<Plan> rewritten =
fileSink(ImmutableList.of(childFirst, childShared), child);
+ LogicalFileSink<Plan> origin = fileSink(ImmutableList.of(
+ slot(originFirst, ImmutableList.of("a")),
+ new SlotReference(shared, "k2", IntegerType.INSTANCE, false,
ImmutableList.of("a"))), oneRow());
+
+ Plan normalized =
MaterializedViewUtils.normalizeSinkExpressions(rewritten, origin);
+
+ Assertions.assertNotNull(normalized);
+ Assertions.assertEquals(originFirst,
normalized.getOutput().get(0).getExprId());
+ Assertions.assertFalse(normalized.getOutput().get(1).nullable());
+ Plan project = normalized.child(0);
+ Assertions.assertTrue(project instanceof LogicalProject);
+ // the materialized project passes the output which the child already
produces through with the
+ // nullable property of the child, so merging the projects does not
change the logical properties
+ Assertions.assertTrue(project.getOutput().get(1).nullable());
+ assertSinkOutputsResolvable(normalized);
+ assertSinkOutputExpressionsResolvable(normalized);
+ }
+
+ @Test
+ public void testAlignSinkMaterializesOutputOfFreshExprId() {
+ ExprId originExprId = new ExprId(10);
+ ExprId rewrittenExprId = new ExprId(11);
+ SlotReference rewrittenSlot = slot(rewrittenExprId,
ImmutableList.of("a"));
+ LogicalFileSink<Plan> rewritten =
fileSink(ImmutableList.of(rewrittenSlot),
+ new LogicalOneRowRelation(new RelationId(1),
ImmutableList.of(rewrittenSlot)));
+ LogicalFileSink<Plan> origin =
fileSink(ImmutableList.of(slot(originExprId, ImmutableList.of("a"))),
+ oneRow());
+
+ Plan aligned =
MaterializedViewUtils.alignPlanOutputWithOrigin(rewritten, origin);
+
+ Assertions.assertNotNull(aligned);
+ Assertions.assertEquals(originExprId,
aligned.getOutput().get(0).getExprId());
+ assertSinkOutputsResolvable(aligned);
+ assertSinkOutputExpressionsResolvable(aligned);
+ }
+
+ @Test
+ public void testStaleSinkOutputUsesChildOutput() {
+ ExprId originExprId = new ExprId(11);
+ ExprId staleExprId = new ExprId(2);
+ SlotReference childOutput = slot(originExprId, ImmutableList.of("a"));
+ LogicalOneRowRelation child = new LogicalOneRowRelation(new
RelationId(1), ImmutableList.of(childOutput));
+ // the sink still outputs the exprId from before a rule changed the
child output, the child does
+ // not produce this exprId any more
+ LogicalFileSink<Plan> rewritten =
fileSink(ImmutableList.of(slot(staleExprId, ImmutableList.of("a"))), child);
+ LogicalFileSink<Plan> origin =
fileSink(ImmutableList.of(slot(originExprId, ImmutableList.of("a"))), oneRow());
+
+ Plan normalized =
MaterializedViewUtils.normalizeSinkExpressions(rewritten, origin);
+
+ Assertions.assertNotNull(normalized);
+ Assertions.assertEquals(originExprId,
normalized.getOutput().get(0).getExprId());
+ Assertions.assertSame(child, normalized.child(0));
+ assertSinkOutputsResolvable(normalized);
+ assertSinkOutputExpressionsResolvable(normalized);
+ }
+
+ private static void assertSinkOutputsResolvable(Plan sinkPlan) {
+ // PhysicalFileSink resolves every output exprId in the child, see
+ // PhysicalPlanTranslator.visitPhysicalFileSink and
translateOutputExprs
+ Set<ExprId> childOutputExprIds = sinkPlan.child(0).getOutput().stream()
+ .map(Slot::getExprId).collect(Collectors.toSet());
+ for (Slot sinkOutput : sinkPlan.getOutput()) {
+
Assertions.assertTrue(childOutputExprIds.contains(sinkOutput.getExprId()),
+ "the child must produce the sink output exprId " +
sinkOutput.getExprId());
+ }
+ }
+
+ private static void assertSinkOutputExpressionsResolvable(Plan sinkPlan) {
+ Set<ExprId> childOutputExprIds = sinkPlan.child(0).getOutput().stream()
+ .map(Slot::getExprId).collect(Collectors.toSet());
+ for (NamedExpression output : ((LogicalSink<?>)
sinkPlan).getOutputExprs()) {
+ for (Slot inputSlot : output.getInputSlots()) {
+
Assertions.assertTrue(childOutputExprIds.contains(inputSlot.getExprId()),
+ "the sink output expression references a slot not
produced by the child: " + inputSlot);
+ }
+ }
+ }
+
+ private static SlotReference slot(ExprId exprId, ImmutableList<String>
qualifier) {
+ return new SlotReference(exprId, "k1", IntegerType.INSTANCE, false,
qualifier);
+ }
+
+ private static LogicalFileSink<Plan> fileSink(List<NamedExpression>
outputs, Plan child) {
+ return new
LogicalFileSink<>("file:///tmp/materialized_view_align_test", "csv",
+ ImmutableMap.of(), outputs, child);
+ }
+
+ private static LogicalOneRowRelation oneRow() {
+ return new LogicalOneRowRelation(new RelationId(1),
ImmutableList.of());
+ }
+
+ private static Plan project(NamedExpression... projects) {
+ return new LogicalProject<>(ImmutableList.copyOf(projects), oneRow());
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]