You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by da...@apache.org on 2019/05/29 02:07:32 UTC

[calcite] branch master updated: [CALCITE-3055] Use pair of relNode's rowType and digest as unique key for cache in RelOptPlanner (KazydubB)

This is an automated email from the ASF dual-hosted git repository.

danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/master by this push:
     new ac40d69  [CALCITE-3055] Use pair of relNode's rowType and digest as unique key for cache in RelOptPlanner (KazydubB)
ac40d69 is described below

commit ac40d6951bc8c475ca6804be6d878107cc2ebb13
Author: Bohdan Kazydub <bo...@gmail.com>
AuthorDate: Thu May 23 17:21:56 2019 +0300

    [CALCITE-3055] Use pair of relNode's rowType and digest as unique key for cache in RelOptPlanner (KazydubB)
    
    Currently RelNode's digest can not uniquely identify itself,
    two relational expressions are equivalent only if their digests and row
    types are the same.
    
    * Add unit test and update code for testing dynamic tables to reflect real use-cases;
    * Add row type as part of cache key for HepPlanner.
    
    close apache/calcite#1230
---
 .../java/org/apache/calcite/plan/RelOptNode.java   |   4 +-
 .../org/apache/calcite/plan/hep/HepPlanner.java    |  27 +++---
 .../calcite/plan/volcano/VolcanoPlanner.java       |  43 ++++-----
 .../apache/calcite/rel/rules/CalcMergeRule.java    |   3 +-
 .../org/apache/calcite/test/HepPlannerTest.java    |  12 +++
 .../org/apache/calcite/test/RelOptRulesTest.java   |  44 +++++++++
 .../org/apache/calcite/test/SqlToRelTestBase.java  |  37 +++++++-
 .../calcite/test/catalog/MockCatalogReader.java    | 105 +++++++++++++++++----
 .../test/catalog/MockCatalogReaderDynamic.java     |  36 +++++--
 .../org/apache/calcite/test/HepPlannerTest.xml     |  14 +++
 .../org/apache/calcite/test/RelOptRulesTest.xml    |  23 +++++
 11 files changed, 282 insertions(+), 66 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptNode.java b/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
index 85da2bc..9df7fba 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
@@ -34,8 +34,8 @@ public interface RelOptNode {
 
   /**
    * Returns a string which concisely describes the definition of this
-   * relational expression. Two relational expressions are equivalent if and
-   * only if their digests are the same.
+   * relational expression. Two relational expressions are equivalent if
+   * their digests and {@link #getRowType()} are the same.
    *
    * <p>The digest does not contain the relational expression's identity --
    * that would prevent similar relational expressions from ever comparing
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java b/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
index c062814..9ae96f8 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
@@ -37,6 +37,7 @@ import org.apache.calcite.rel.convert.TraitMatchingRule;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.metadata.RelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.graph.BreadthFirstIterator;
@@ -76,7 +77,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
 
   private RelTraitSet requestedRootTraits;
 
-  private final Map<String, HepRelVertex> mapDigestToVertex = new HashMap<>();
+  private final Map<Pair<String, RelDataType>, HepRelVertex> mapDigestToVertex = new HashMap<>();
 
   // NOTE jvs 24-Apr-2006:  We use LinkedHashSet
   // in order to provide deterministic behavior.
@@ -493,8 +494,8 @@ public class HepPlanner extends AbstractRelOptPlanner {
 
   /** Returns whether the vertex is valid. */
   private boolean belongsToDag(HepRelVertex vertex) {
-    String digest = vertex.getCurrentRel().getDigest();
-    return mapDigestToVertex.get(digest) != null;
+    Pair<String, RelDataType> key = key(vertex.getCurrentRel());
+    return mapDigestToVertex.get(key) != null;
   }
 
   private HepRelVertex applyRule(
@@ -822,8 +823,8 @@ public class HepPlanner extends AbstractRelOptPlanner {
     // try to find equivalent rel only if DAG is allowed
     if (!noDag) {
       // Now, check if an equivalent vertex already exists in graph.
-      String digest = rel.getDigest();
-      HepRelVertex equivVertex = mapDigestToVertex.get(digest);
+      Pair<String, RelDataType> key = key(rel);
+      HepRelVertex equivVertex = mapDigestToVertex.get(key);
       if (equivVertex != null) {
         // Use existing vertex.
         return equivVertex;
@@ -889,11 +890,10 @@ public class HepPlanner extends AbstractRelOptPlanner {
       // reachable from here.
       notifyDiscard(vertex.getCurrentRel());
     }
-    String oldDigest = vertex.getCurrentRel().toString();
-    if (mapDigestToVertex.get(oldDigest) == vertex) {
-      mapDigestToVertex.remove(oldDigest);
+    Pair<String, RelDataType> oldKey = key(vertex.getCurrentRel());
+    if (mapDigestToVertex.get(oldKey) == vertex) {
+      mapDigestToVertex.remove(oldKey);
     }
-    String newDigest = rel.getDigest();
     // When a transformation happened in one rule apply, support
     // vertex2 replace vertex1, but the current relNode of
     // vertex1 and vertex2 is same,
@@ -901,7 +901,8 @@ public class HepPlanner extends AbstractRelOptPlanner {
     // otherwise the digest will be removed wrongly in the mapDigestToVertex
     //  when collectGC
     // so it must update the digest that map to vertex
-    mapDigestToVertex.put(newDigest, vertex);
+    Pair<String, RelDataType> newKey = key(rel);
+    mapDigestToVertex.put(newKey, vertex);
     if (rel != vertex.getCurrentRel()) {
       vertex.replaceRel(rel);
     }
@@ -911,6 +912,10 @@ public class HepPlanner extends AbstractRelOptPlanner {
         false);
   }
 
+  private static Pair<String, RelDataType> key(RelNode rel) {
+    return Pair.of(rel.getDigest(), rel.getRowType());
+  }
+
   private RelNode buildFinalPlan(HepRelVertex vertex) {
     RelNode rel = vertex.getCurrentRel();
 
@@ -966,7 +971,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
     graphSizeLastGC = graph.vertexSet().size();
 
     // Clean up digest map too.
-    Iterator<Map.Entry<String, HepRelVertex>> digestIter =
+    Iterator<Map.Entry<Pair<String, RelDataType>, HepRelVertex>> digestIter =
         mapDigestToVertex.entrySet().iterator();
     while (digestIter.hasNext()) {
       HepRelVertex vertex = digestIter.next().getValue();
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
index dca0342..c70f2fe 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
@@ -60,6 +60,7 @@ import org.apache.calcite.rel.rules.ProjectRemoveRule;
 import org.apache.calcite.rel.rules.SemiJoinRule;
 import org.apache.calcite.rel.rules.SortRemoveRule;
 import org.apache.calcite.rel.rules.UnionToDistinctRule;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.util.Litmus;
@@ -153,7 +154,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
    * {@code Project(child=rel#1, a=null)} where a is a null INTEGER or a
    * null VARCHAR(10).
    */
-  private final Map<String, RelNode> mapDigestToRel =
+  private final Map<Pair<String, RelDataType>, RelNode> mapDigestToRel =
       new HashMap<>();
 
   /**
@@ -1188,6 +1189,11 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     }
   }
 
+  /** Computes the key for {@link #mapDigestToRel}. */
+  private static Pair<String, RelDataType> key(RelNode rel) {
+    return Pair.of(rel.getDigest(), rel.getRowType());
+  }
+
   public String toDot() {
     StringWriter sw = new StringWriter();
     PrintWriter pw = new PrintWriter(sw);
@@ -1430,11 +1436,14 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
   void rename(RelNode rel) {
     final String oldDigest = rel.getDigest();
     if (fixUpInputs(rel)) {
-      final RelNode removed = mapDigestToRel.remove(oldDigest);
+      final Pair<String, RelDataType> oldKey =
+          Pair.of(oldDigest, rel.getRowType());
+      final RelNode removed = mapDigestToRel.remove(oldKey);
       assert removed == rel;
       final String newDigest = rel.recomputeDigest();
       LOGGER.trace("Rename #{} from '{}' to '{}'", rel.getId(), oldDigest, newDigest);
-      final RelNode equivRel = mapDigestToRel.put(newDigest, rel);
+      final Pair<String, RelDataType> key = key(rel);
+      final RelNode equivRel = mapDigestToRel.put(key, rel);
       if (equivRel != null) {
         assert equivRel != rel;
 
@@ -1443,14 +1452,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
         LOGGER.trace("After renaming rel#{} it is now equivalent to rel#{}",
             rel.getId(), equivRel.getId());
 
-        assert RelOptUtil.equal(
-            "rel rowtype",
-            rel.getRowType(),
-            "equivRel rowtype",
-            equivRel.getRowType(),
-            Litmus.THROW);
-
-        mapDigestToRel.put(newDigest, equivRel);
+        mapDigestToRel.put(key, equivRel);
 
         RelSubset equivRelSubset = getSubset(equivRel);
         ruleQueue.recompute(equivRelSubset, true);
@@ -1493,16 +1495,11 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     // Is there an equivalent relational expression? (This might have
     // just occurred because the relational expression's child was just
     // found to be equivalent to another set.)
-    RelNode equivRel = mapDigestToRel.get(rel.getDigest());
+    final Pair<String, RelDataType> key = key(rel);
+    RelNode equivRel = mapDigestToRel.get(key);
     if (equivRel != null && equivRel != rel) {
       assert equivRel.getClass() == rel.getClass();
       assert equivRel.getTraitSet().equals(rel.getTraitSet());
-      assert RelOptUtil.equal(
-          "rel rowtype",
-          rel.getRowType(),
-          "equivRel rowtype",
-          equivRel.getRowType(),
-          Litmus.THROW);
 
       RelSubset equivRelSubset = getSubset(equivRel);
       ruleQueue.recompute(equivRelSubset, true);
@@ -1703,7 +1700,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
 
     // If it is equivalent to an existing expression, return the set that
     // the equivalent expression belongs to.
-    String key = rel.getDigest();
+    Pair<String, RelDataType> key = key(rel);
     RelNode equivExp = mapDigestToRel.get(key);
     if (equivExp == null) {
       // do nothing
@@ -1741,15 +1738,9 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
         // expression.
         if (fixUpInputs(rel)) {
           rel.recomputeDigest();
-          key = rel.getDigest();
+          key = key(rel);
           RelNode equivRel = mapDigestToRel.get(key);
           if ((equivRel != rel) && (equivRel != null)) {
-            assert RelOptUtil.equal(
-                "rel rowtype",
-                rel.getRowType(),
-                "equivRel rowtype",
-                equivRel.getRowType(),
-                Litmus.THROW);
 
             // make sure this bad rel didn't get into the
             // set in any way (fixupInputs will do this but it
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CalcMergeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/CalcMergeRule.java
index 0173c3f..9ed05f3 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CalcMergeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CalcMergeRule.java
@@ -85,7 +85,8 @@ public class CalcMergeRule extends RelOptRule {
             bottomCalc.getInput(),
             mergedProgram);
 
-    if (newCalc.getDigest().equals(bottomCalc.getDigest())) {
+    if (newCalc.getDigest().equals(bottomCalc.getDigest())
+        && newCalc.getRowType().equals(bottomCalc.getRowType())) {
       // newCalc is equivalent to bottomCalc, which means that topCalc
       // must be trivial. Take it out of the game.
       call.getPlanner().setImportance(topCalc, 0.0);
diff --git a/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java b/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
index 7a99d0b..1506cf9 100644
--- a/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
@@ -295,6 +295,18 @@ public class HepPlannerTest extends RelOptTestBase {
     planner.findBestExp();
   }
 
+  @Test public void testRelNodeCacheWithDigest() {
+    HepProgramBuilder programBuilder = HepProgram.builder();
+    HepPlanner planner =
+        new HepPlanner(
+            programBuilder.build());
+    String query = "(select n_nationkey from SALES.CUSTOMER) union all\n"
+        + "(select n_name from CUSTOMER_MODIFIABLEVIEW)";
+    Tester tester = createDynamicTester()
+        .withDecorrelation(true);
+    checkPlanning(tester, programBuilder.build(), planner, query, true);
+  }
+
   @Test public void testRuleApplyCount() {
     final long applyTimes1 = checkRuleApplyCount(HepMatchOrder.ARBITRARY);
     assertThat(applyTimes1, is(316L));
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 87f1761..d1e3899 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -16,9 +16,13 @@
  */
 package org.apache.calcite.test;
 
+import org.apache.calcite.adapter.enumerable.EnumerableConvention;
+import org.apache.calcite.adapter.enumerable.EnumerableRules;
 import org.apache.calcite.config.CalciteConnectionConfigImpl;
 import org.apache.calcite.plan.Context;
 import org.apache.calcite.plan.Contexts;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptUtil;
@@ -28,6 +32,7 @@ import org.apache.calcite.plan.hep.HepMatchOrder;
 import org.apache.calcite.plan.hep.HepPlanner;
 import org.apache.calcite.plan.hep.HepProgram;
 import org.apache.calcite.plan.hep.HepProgramBuilder;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
 import org.apache.calcite.prepare.Prepare;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollationTraitDef;
@@ -135,7 +140,11 @@ import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql2rel.SqlToRelConverter;
 import org.apache.calcite.test.catalog.MockCatalogReader;
+import org.apache.calcite.tools.Program;
+import org.apache.calcite.tools.Programs;
 import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.tools.RuleSet;
+import org.apache.calcite.tools.RuleSets;
 import org.apache.calcite.util.ImmutableBitSet;
 
 import com.google.common.collect.ImmutableList;
@@ -146,6 +155,7 @@ import org.junit.Test;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Properties;
@@ -5472,6 +5482,40 @@ public class RelOptRulesTest extends RelOptTestBase {
         "select e.sal + b.comm from emp e inner join bonus b "
             + "on (e.ename || e.job) IS NOT DISTINCT FROM (b.ename || b.job) and e.deptno = 10");
   }
+
+  @Test public void testDynamicStarWithUnion() {
+    String sql = "(select n_nationkey from SALES.CUSTOMER) union all\n"
+        + "(select n_name from CUSTOMER_MODIFIABLEVIEW)";
+
+    VolcanoPlanner planner = new VolcanoPlanner(null, null);
+    planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+
+    Tester dynamicTester = createDynamicTester().withDecorrelation(true)
+        .withClusterFactory(
+            relOptCluster -> RelOptCluster.create(planner, relOptCluster.getRexBuilder()));
+
+    RelRoot root = dynamicTester.convertSqlToRel(sql);
+
+    String planBefore = NL + RelOptUtil.toString(root.rel);
+    getDiffRepos().assertEquals("planBefore", "${planBefore}", planBefore);
+
+    RuleSet ruleSet =
+        RuleSets.ofList(
+            EnumerableRules.ENUMERABLE_PROJECT_RULE,
+            EnumerableRules.ENUMERABLE_TABLE_SCAN_RULE,
+            EnumerableRules.ENUMERABLE_UNION_RULE);
+    Program program = Programs.of(ruleSet);
+
+    RelTraitSet toTraits =
+        root.rel.getCluster().traitSet()
+            .replace(0, EnumerableConvention.INSTANCE);
+
+    RelNode relAfter = program.run(planner, root.rel, toTraits,
+        Collections.emptyList(), Collections.emptyList());
+
+    String planAfter = NL + RelOptUtil.toString(relAfter);
+    getDiffRepos().assertEquals("planAfter", "${planAfter}", planAfter);
+  }
 }
 
 // End RelOptRulesTest.java
diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
index e4d9d46..00c3a20 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
@@ -46,6 +46,7 @@ import org.apache.calcite.schema.ColumnStrategy;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlOperatorTable;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParseException;
 import org.apache.calcite.sql.parser.SqlParser;
 import org.apache.calcite.sql.test.SqlTestFactory;
 import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
@@ -632,7 +633,9 @@ public abstract class SqlToRelTestBase {
       if (clusterFactory != null) {
         cluster = clusterFactory.apply(cluster);
       }
-      return new SqlToRelConverter(null, validator, catalogReader, cluster,
+      RelOptTable.ViewExpander viewExpander =
+          new MockViewExpander(validator, catalogReader, cluster, config);
+      return new SqlToRelConverter(viewExpander, validator, catalogReader, cluster,
           StandardConvertletTable.INSTANCE, config);
     }
 
@@ -848,6 +851,38 @@ public abstract class SqlToRelTestBase {
       return true;
     }
   }
+
+  /**
+   * {@link RelOptTable.ViewExpander} implementation for testing usage.
+   */
+  private static class MockViewExpander implements RelOptTable.ViewExpander {
+    private final SqlValidator validator;
+    private final Prepare.CatalogReader catalogReader;
+    private final RelOptCluster cluster;
+    private final SqlToRelConverter.Config config;
+
+    MockViewExpander(SqlValidator validator, Prepare.CatalogReader catalogReader,
+        RelOptCluster cluster, SqlToRelConverter.Config config) {
+      this.validator = validator;
+      this.catalogReader = catalogReader;
+      this.cluster = cluster;
+      this.config = config;
+    }
+
+    @Override public RelRoot expandView(RelDataType rowType, String queryString,
+        List<String> schemaPath, List<String> viewPath) {
+      try {
+        SqlNode parsedNode = SqlParser.create(queryString).parseStmt();
+        SqlNode validatedNode = validator.validate(parsedNode);
+        SqlToRelConverter converter = new SqlToRelConverter(
+            this, validator, catalogReader, cluster,
+            StandardConvertletTable.INSTANCE, config);
+        return converter.convertQuery(validatedNode, false, true);
+      } catch (SqlParseException e) {
+        throw new RuntimeException("Error happened while expanding view.", e);
+      }
+    }
+  }
 }
 
 // End SqlToRelTestBase.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
index d7f303e..bba9b2f 100644
--- a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
+++ b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
@@ -16,10 +16,12 @@
  */
 package org.apache.calcite.test.catalog;
 
+import org.apache.calcite.adapter.java.AbstractQueryableTable;
 import org.apache.calcite.adapter.java.JavaTypeFactory;
 import org.apache.calcite.config.CalciteConnectionConfig;
 import org.apache.calcite.jdbc.CalcitePrepare;
 import org.apache.calcite.jdbc.CalciteSchema;
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
 import org.apache.calcite.linq4j.QueryProvider;
 import org.apache.calcite.linq4j.Queryable;
 import org.apache.calcite.linq4j.tree.Expression;
@@ -45,8 +47,8 @@ import org.apache.calcite.rel.type.RelDataTypeFamily;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.RelDataTypeImpl;
 import org.apache.calcite.rel.type.RelDataTypePrecedenceList;
+import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.rel.type.RelProtoDataType;
-import org.apache.calcite.rel.type.RelRecordType;
 import org.apache.calcite.rel.type.StructKind;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexInputRef;
@@ -60,9 +62,11 @@ import org.apache.calcite.schema.Schemas;
 import org.apache.calcite.schema.Statistic;
 import org.apache.calcite.schema.StreamableTable;
 import org.apache.calcite.schema.Table;
+import org.apache.calcite.schema.TranslatableTable;
 import org.apache.calcite.schema.Wrapper;
 import org.apache.calcite.schema.impl.AbstractSchema;
 import org.apache.calcite.schema.impl.ModifiableViewTable;
+import org.apache.calcite.schema.impl.ViewTable;
 import org.apache.calcite.schema.impl.ViewTableMacro;
 import org.apache.calcite.sql.SqlAccessType;
 import org.apache.calcite.sql.SqlCall;
@@ -93,6 +97,7 @@ import java.lang.reflect.Type;
 import java.nio.charset.Charset;
 import java.util.AbstractList;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -203,6 +208,21 @@ public abstract class MockCatalogReader extends CalciteCatalogReader {
     }
   }
 
+  void registerTable(MockDynamicTable table) {
+    registerTable(table.names, table);
+  }
+
+  void reregisterTable(MockDynamicTable table) {
+    List<String> names = table.names;
+    assert names.get(0).equals(DEFAULT_CATALOG);
+    List<String> schemaPath = Util.skipLast(names);
+    String tableName = Util.last(names);
+    CalciteSchema schema = SqlValidatorUtil.getSchema(rootSchema,
+        schemaPath, SqlNameMatchers.withCaseSensitive(true));
+    schema.removeTable(tableName);
+    schema.add(tableName, table);
+  }
+
   private void registerTable(final List<String> names, final Table table) {
     assert names.get(0).equals(DEFAULT_CATALOG);
     final List<String> schemaPath = Util.skipLast(names);
@@ -690,6 +710,52 @@ public abstract class MockCatalogReader extends CalciteCatalogReader {
   }
 
   /**
+   * Mock implementation of {@link Prepare.AbstractPreparingTable} which holds {@link ViewTable}
+   * and delegates {@link MockTable#toRel} call to the view.
+   */
+  public static class MockRelViewTable extends MockTable {
+    private final ViewTable viewTable;
+
+    private MockRelViewTable(ViewTable viewTable,
+        MockCatalogReader catalogReader, String catalogName, String schemaName, String name,
+        boolean stream, double rowCount, ColumnResolver resolver,
+        InitializerExpressionFactory initializerExpressionFactory) {
+      super(catalogReader, ImmutableList.of(catalogName, schemaName, name),
+          stream, false, rowCount, resolver, initializerExpressionFactory);
+      this.viewTable = viewTable;
+    }
+
+    public static MockRelViewTable create(ViewTable viewTable,
+        MockCatalogReader catalogReader, String catalogName, String schemaName, String name,
+        boolean stream, double rowCount, ColumnResolver resolver) {
+      Table underlying = viewTable.unwrap(Table.class);
+      InitializerExpressionFactory initializerExpressionFactory =
+          underlying instanceof Wrapper
+              ? ((Wrapper) underlying).unwrap(InitializerExpressionFactory.class)
+              : NullInitializerExpressionFactory.INSTANCE;
+      return new MockRelViewTable(viewTable,
+          catalogReader, catalogName, schemaName, name, stream, rowCount,
+          resolver, Util.first(initializerExpressionFactory,
+          NullInitializerExpressionFactory.INSTANCE));
+    }
+
+    @Override public RelDataType getRowType() {
+      return viewTable.getRowType(catalogReader.typeFactory);
+    }
+
+    @Override public RelNode toRel(RelOptTable.ToRelContext context) {
+      return viewTable.toRel(context, this);
+    }
+
+    @Override public <T> T unwrap(Class<T> clazz) {
+      if (clazz.isInstance(viewTable)) {
+        return clazz.cast(viewTable);
+      }
+      return super.unwrap(clazz);
+    }
+  }
+
+  /**
    * Mock implementation of
    * {@link org.apache.calcite.prepare.Prepare.PreparingTable} for views.
    */
@@ -827,29 +893,30 @@ public abstract class MockCatalogReader extends CalciteCatalogReader {
   }
 
   /**
-   * Mock implementation of
-   * {@link org.apache.calcite.prepare.Prepare.PreparingTable} with dynamic record type.
+   * Mock implementation of {@link AbstractQueryableTable} with dynamic record type.
    */
-  public static class MockDynamicTable extends MockTable {
-    public MockDynamicTable(MockCatalogReader catalogReader, String catalogName,
-        String schemaName, String name, boolean stream, double rowCount) {
-      super(catalogReader, catalogName, schemaName, name, stream, false,
-          rowCount, null, NullInitializerExpressionFactory.INSTANCE);
+  public static class MockDynamicTable
+      extends AbstractQueryableTable implements TranslatableTable {
+    private final DynamicRecordTypeImpl rowType;
+    protected final List<String> names;
+
+    MockDynamicTable(String catalogName, String schemaName, String name) {
+      super(Object.class);
+      this.names = Arrays.asList(catalogName, schemaName, name);
+      this.rowType = new DynamicRecordTypeImpl(new JavaTypeFactoryImpl(RelDataTypeSystem.DEFAULT));
     }
 
-    public void onRegister(RelDataTypeFactory typeFactory) {
-      rowType = new DynamicRecordTypeImpl(typeFactory);
+    @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+      return rowType;
     }
 
-    /**
-     * Recreates an immutable rowType, if the table has Dynamic Record Type,
-     * when converts table to Rel.
-     */
-    public RelNode toRel(ToRelContext context) {
-      if (rowType.isDynamicStruct()) {
-        rowType = new RelRecordType(rowType.getFieldList());
-      }
-      return super.toRel(context);
+    @Override public <T> Queryable<T> asQueryable(QueryProvider queryProvider,
+        SchemaPlus schema, String tableName) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override public RelNode toRel(RelOptTable.ToRelContext context, RelOptTable relOptTable) {
+      return LogicalTableScan.create(context.getCluster(), relOptTable);
     }
   }
 
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderDynamic.java b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderDynamic.java
index 7a3660b..8f129b2 100644
--- a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderDynamic.java
+++ b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderDynamic.java
@@ -18,8 +18,16 @@ package org.apache.calcite.test.catalog;
 
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.schema.TableMacro;
+import org.apache.calcite.schema.TranslatableTable;
+import org.apache.calcite.schema.impl.ViewTable;
 import org.apache.calcite.sql.type.SqlTypeName;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
 /**
  * Registers dynamic tables.
  *
@@ -44,14 +52,15 @@ public class MockCatalogReaderDynamic extends MockCatalogReader {
     MockSchema schema = new MockSchema("SALES");
     registerSchema(schema);
 
-    MockTable nationTable =
-        new MockDynamicTable(this, schema.getCatalogName(),
-            schema.getName(), "NATION", false, 100);
+    MockDynamicTable nationTable =
+        new MockDynamicTable(schema.getCatalogName(),
+            schema.getName(), "NATION");
     registerTable(nationTable);
 
-    MockTable customerTable =
-        new MockDynamicTable(this, schema.getCatalogName(),
-            schema.getName(), "CUSTOMER", false, 100);
+    Supplier<MockDynamicTable> customerTableSupplier = () ->
+        new MockDynamicTable(schema.getCatalogName(), schema.getName(), "CUSTOMER");
+
+    MockDynamicTable customerTable = customerTableSupplier.get();
     registerTable(customerTable);
 
     // CREATE TABLE "REGION" - static table with known schema.
@@ -67,6 +76,21 @@ public class MockCatalogReaderDynamic extends MockCatalogReader {
     regionTable.addColumn("R_COMMENT", varcharType);
     registerTable(regionTable);
 
+    List<String> custModifiableViewNames = Arrays.asList(
+        schema.getCatalogName(), schema.getName(), "CUSTOMER_MODIFIABLEVIEW");
+    TableMacro custModifiableViewMacro = MockModifiableViewRelOptTable.viewMacro(rootSchema,
+        "select n_name from SALES.CUSTOMER", custModifiableViewNames.subList(0, 2),
+        Collections.singletonList(custModifiableViewNames.get(2)), true);
+    TranslatableTable empModifiableView = custModifiableViewMacro.apply(Collections.emptyList());
+    MockTable mockCustViewTable = MockRelViewTable.create(
+        (ViewTable) empModifiableView, this,
+        custModifiableViewNames.get(0), custModifiableViewNames.get(1),
+        custModifiableViewNames.get(2), false, 20, null);
+    registerTable(mockCustViewTable);
+
+    // re-registers customer table to clear its row type after view registration
+    reregisterTable(customerTableSupplier.get());
+
     return this;
   }
 }
diff --git a/core/src/test/resources/org/apache/calcite/test/HepPlannerTest.xml b/core/src/test/resources/org/apache/calcite/test/HepPlannerTest.xml
index 21c5bda..a03acbe 100644
--- a/core/src/test/resources/org/apache/calcite/test/HepPlannerTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/HepPlannerTest.xml
@@ -216,4 +216,18 @@ LogicalCalc(expr#0..1=[{inputs}], expr#2=[UPPER($t1)], expr#3=[20], expr#4=[=($t
 ]]>
     </Resource>
   </TestCase>
+  <TestCase name="testRelNodeCacheWithDigest">
+    <Resource name="sql">
+      <![CDATA[(select n_nationkey from SALES.CUSTOMER) union all (select n_name from CUSTOMER_MODIFIABLEVIEW)]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalUnion(all=[true])
+  LogicalProject(N_NATIONKEY=[$0])
+    LogicalTableScan(table=[[CATALOG, SALES, CUSTOMER]])
+  LogicalProject(N_NAME=[$1])
+    LogicalTableScan(table=[[CATALOG, SALES, CUSTOMER]])
+]]>
+    </Resource>
+  </TestCase>
 </Root>
diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 71fef63..98077f0 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -10853,4 +10853,27 @@ LogicalProject(EXPR$0=[+($0, $3)])
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testDynamicStarWithUnion">
+        <Resource name="sql">
+            <![CDATA[(select n_nationkey from SALES.CUSTOMER) union all (select n_name from CUSTOMER_MODIFIABLEVIEW)]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalUnion(all=[true])
+  LogicalProject(N_NATIONKEY=[$0])
+    LogicalTableScan(table=[[CATALOG, SALES, CUSTOMER]])
+  LogicalProject(N_NAME=[$1])
+    LogicalTableScan(table=[[CATALOG, SALES, CUSTOMER]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+EnumerableUnion(all=[true])
+  EnumerableProject(N_NATIONKEY=[$0])
+    EnumerableTableScan(table=[[CATALOG, SALES, CUSTOMER]])
+  EnumerableProject(N_NAME=[$1])
+    EnumerableTableScan(table=[[CATALOG, SALES, CUSTOMER]])
+]]>
+        </Resource>
+    </TestCase>
 </Root>