You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hive.apache.org by jc...@apache.org on 2015/10/09 10:02:35 UTC

hive git commit: HIVE-11976: Extend CBO rules to being able to apply rules only once on a given operator (Jesus Camacho Rodriguez, reviewed by Laljo John Pullokkaran)

Repository: hive
Updated Branches:
  refs/heads/master aded0d32d -> 5201f188b


HIVE-11976: Extend CBO rules to being able to apply rules only once on a given operator (Jesus Camacho Rodriguez, reviewed by Laljo John Pullokkaran)


Project: http://git-wip-us.apache.org/repos/asf/hive/repo
Commit: http://git-wip-us.apache.org/repos/asf/hive/commit/5201f188
Tree: http://git-wip-us.apache.org/repos/asf/hive/tree/5201f188
Diff: http://git-wip-us.apache.org/repos/asf/hive/diff/5201f188

Branch: refs/heads/master
Commit: 5201f188bc6c808c2a9f3d100118340af3ebd7c4
Parents: aded0d3
Author: Jesus Camacho Rodriguez <jc...@apache.org>
Authored: Mon Oct 5 12:12:39 2015 +0100
Committer: Jesus Camacho Rodriguez <jc...@apache.org>
Committed: Fri Oct 9 09:01:48 2015 +0100

----------------------------------------------------------------------
 .../ql/optimizer/calcite/HiveConfigContext.java |  37 ----
 .../calcite/HiveHepPlannerContext.java          |  37 ++++
 .../calcite/HiveVolcanoPlannerContext.java      |  37 ++++
 .../calcite/cost/HiveVolcanoPlanner.java        |   6 +-
 .../calcite/rules/HivePreFilteringRule.java     |  42 ++++-
 .../calcite/rules/HiveRulesRegistry.java        |  44 +++++
 .../hadoop/hive/ql/parse/CalcitePlanner.java    |  11 +-
 .../calcite/TestCBORuleFiredOnlyOnce.java       | 168 +++++++++++++++++++
 8 files changed, 332 insertions(+), 50 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/hive/blob/5201f188/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveConfigContext.java
----------------------------------------------------------------------
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveConfigContext.java b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveConfigContext.java
deleted file mode 100644
index 0e559e0..0000000
--- a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveConfigContext.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * 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.hadoop.hive.ql.optimizer.calcite;
-
-import org.apache.calcite.plan.Context;
-import org.apache.hadoop.hive.ql.optimizer.calcite.cost.HiveAlgorithmsConf;
-
-
-public class HiveConfigContext implements Context {
-  private HiveAlgorithmsConf config;
-
-  public HiveConfigContext(HiveAlgorithmsConf config) {
-    this.config = config;
-  }
-
-  public <T> T unwrap(Class<T> clazz) {
-    if (clazz.isInstance(config)) {
-      return clazz.cast(config);
-    }
-    return null;
-  }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/hive/blob/5201f188/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveHepPlannerContext.java
----------------------------------------------------------------------
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveHepPlannerContext.java b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveHepPlannerContext.java
new file mode 100644
index 0000000..ad79aee
--- /dev/null
+++ b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveHepPlannerContext.java
@@ -0,0 +1,37 @@
+/**
+ * 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.hadoop.hive.ql.optimizer.calcite;
+
+import org.apache.calcite.plan.Context;
+import org.apache.hadoop.hive.ql.optimizer.calcite.rules.HiveRulesRegistry;
+
+
+public class HiveHepPlannerContext implements Context {
+  private HiveRulesRegistry registry;
+
+  public HiveHepPlannerContext(HiveRulesRegistry registry) {
+    this.registry = registry;
+  }
+
+  public <T> T unwrap(Class<T> clazz) {
+    if (clazz.isInstance(registry)) {
+      return clazz.cast(registry);
+    }
+    return null;
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/hive/blob/5201f188/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveVolcanoPlannerContext.java
----------------------------------------------------------------------
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveVolcanoPlannerContext.java b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveVolcanoPlannerContext.java
new file mode 100644
index 0000000..8859fc2
--- /dev/null
+++ b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/HiveVolcanoPlannerContext.java
@@ -0,0 +1,37 @@
+/**
+ * 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.hadoop.hive.ql.optimizer.calcite;
+
+import org.apache.calcite.plan.Context;
+import org.apache.hadoop.hive.ql.optimizer.calcite.cost.HiveAlgorithmsConf;
+
+
+public class HiveVolcanoPlannerContext implements Context {
+  private HiveAlgorithmsConf config;
+
+  public HiveVolcanoPlannerContext(HiveAlgorithmsConf config) {
+    this.config = config;
+  }
+
+  public <T> T unwrap(Class<T> clazz) {
+    if (clazz.isInstance(config)) {
+      return clazz.cast(config);
+    }
+    return null;
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/hive/blob/5201f188/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/cost/HiveVolcanoPlanner.java
----------------------------------------------------------------------
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/cost/HiveVolcanoPlanner.java b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/cost/HiveVolcanoPlanner.java
index a39ded2..8610edc 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/cost/HiveVolcanoPlanner.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/cost/HiveVolcanoPlanner.java
@@ -22,7 +22,7 @@ import org.apache.calcite.plan.ConventionTraitDef;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.volcano.VolcanoPlanner;
 import org.apache.calcite.rel.RelCollationTraitDef;
-import org.apache.hadoop.hive.ql.optimizer.calcite.HiveConfigContext;
+import org.apache.hadoop.hive.ql.optimizer.calcite.HiveVolcanoPlannerContext;
 
 /**
  * Refinement of {@link org.apache.calcite.plan.volcano.VolcanoPlanner} for Hive.
@@ -35,11 +35,11 @@ public class HiveVolcanoPlanner extends VolcanoPlanner {
   private static final boolean ENABLE_COLLATION_TRAIT = true;
 
   /** Creates a HiveVolcanoPlanner. */
-  public HiveVolcanoPlanner(HiveConfigContext conf) {
+  public HiveVolcanoPlanner(HiveVolcanoPlannerContext conf) {
     super(HiveCost.FACTORY, conf);
   }
 
-  public static RelOptPlanner createPlanner(HiveConfigContext conf) {
+  public static RelOptPlanner createPlanner(HiveVolcanoPlannerContext conf) {
     final VolcanoPlanner planner = new HiveVolcanoPlanner(conf);
     planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
     if (ENABLE_COLLATION_TRAIT) {

http://git-wip-us.apache.org/repos/asf/hive/blob/5201f188/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HivePreFilteringRule.java
----------------------------------------------------------------------
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HivePreFilteringRule.java b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HivePreFilteringRule.java
index 3e2311c..349c7f8 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HivePreFilteringRule.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HivePreFilteringRule.java
@@ -76,14 +76,38 @@ public class HivePreFilteringRule extends RelOptRule {
     this.filterFactory = HiveFilter.DEFAULT_FILTER_FACTORY;
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override
+  public boolean matches(RelOptRuleCall call) {
     final Filter filter = call.rel(0);
     final RelNode filterChild = call.rel(1);
 
-    // 0. If the filter is already on top of a TableScan,
-    //    we can bail out
+    // If the filter is already on top of a TableScan,
+    // we can bail out
     if (filterChild instanceof TableScan) {
-      return;
+      return false;
+    }
+
+    HiveRulesRegistry registry = call.getPlanner().
+            getContext().unwrap(HiveRulesRegistry.class);
+
+    // If this operator has been visited already by the rule,
+    // we do not need to apply the optimization
+    if (registry != null && registry.getVisited(this).contains(filter)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public void onMatch(RelOptRuleCall call) {
+    final Filter filter = call.rel(0);
+
+    // 0. Register that we have visited this operator in this rule
+    HiveRulesRegistry registry = call.getPlanner().
+            getContext().unwrap(HiveRulesRegistry.class);
+    if (registry != null) {
+      registry.registerVisited(this, filter);
     }
 
     final RexBuilder rexBuilder = filter.getCluster().getRexBuilder();
@@ -114,7 +138,7 @@ public class HivePreFilteringRule extends RelOptRule {
     }
 
     // 3. If the new conjuncts are already present in the plan, we bail out
-    final RelOptPredicateList predicates = RelMetadataQuery.getPulledUpPredicates(filter);
+    final RelOptPredicateList predicates = RelMetadataQuery.getPulledUpPredicates(filter.getInput());
     final List<RexNode> newConjuncts = new ArrayList<>();
     for (RexNode commonOperand : commonOperands) {
       boolean found = false;
@@ -137,9 +161,15 @@ public class HivePreFilteringRule extends RelOptRule {
             RexUtil.composeConjunction(rexBuilder, newConjuncts, false));
 
     // 5. We create the new filter that might be pushed down
-    RelNode newFilter = filterFactory.createFilter(filterChild, newCondition);
+    RelNode newFilter = filterFactory.createFilter(filter.getInput(), newCondition);
     RelNode newTopFilter = filterFactory.createFilter(newFilter, condition);
 
+    // 6. We register both so we do not fire the rule on them again
+    if (registry != null) {
+      registry.registerVisited(this, newFilter);
+      registry.registerVisited(this, newTopFilter);
+    }
+
     call.transformTo(newTopFilter);
 
   }

http://git-wip-us.apache.org/repos/asf/hive/blob/5201f188/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HiveRulesRegistry.java
----------------------------------------------------------------------
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HiveRulesRegistry.java b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HiveRulesRegistry.java
new file mode 100644
index 0000000..18a065e
--- /dev/null
+++ b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/calcite/rules/HiveRulesRegistry.java
@@ -0,0 +1,44 @@
+/**
+ * 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.hadoop.hive.ql.optimizer.calcite.rules;
+
+import java.util.Set;
+
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.rel.RelNode;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+
+public class HiveRulesRegistry {
+
+  private SetMultimap<RelOptRule, RelNode> registry;
+
+  public HiveRulesRegistry() {
+    this.registry = HashMultimap.create();
+  }
+
+  public void registerVisited(RelOptRule rule, RelNode operator) {
+    this.registry.put(rule, operator);
+  }
+
+  public Set<RelNode> getVisited(RelOptRule rule) {
+    return this.registry.get(rule);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/hive/blob/5201f188/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java
----------------------------------------------------------------------
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java b/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java
index e68b385..61ee2bd 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java
@@ -63,7 +63,6 @@ import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.metadata.CachingRelMetadataProvider;
 import org.apache.calcite.rel.metadata.ChainedRelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelMetadataProvider;
-import org.apache.calcite.rel.rules.AggregateJoinTransposeRule;
 import org.apache.calcite.rel.rules.FilterAggregateTransposeRule;
 import org.apache.calcite.rel.rules.FilterProjectTransposeRule;
 import org.apache.calcite.rel.rules.JoinToMultiJoinRule;
@@ -118,9 +117,10 @@ import org.apache.hadoop.hive.ql.metadata.VirtualColumn;
 import org.apache.hadoop.hive.ql.optimizer.calcite.CalciteSemanticException;
 import org.apache.hadoop.hive.ql.optimizer.calcite.CalciteSemanticException.UnsupportedFeature;
 import org.apache.hadoop.hive.ql.optimizer.calcite.HiveCalciteUtil;
-import org.apache.hadoop.hive.ql.optimizer.calcite.HiveConfigContext;
 import org.apache.hadoop.hive.ql.optimizer.calcite.HiveDefaultRelMetadataProvider;
+import org.apache.hadoop.hive.ql.optimizer.calcite.HiveHepPlannerContext;
 import org.apache.hadoop.hive.ql.optimizer.calcite.HiveTypeSystemImpl;
+import org.apache.hadoop.hive.ql.optimizer.calcite.HiveVolcanoPlannerContext;
 import org.apache.hadoop.hive.ql.optimizer.calcite.RelOptHiveTable;
 import org.apache.hadoop.hive.ql.optimizer.calcite.TraitsUtil;
 import org.apache.hadoop.hive.ql.optimizer.calcite.cost.HiveAlgorithmsConf;
@@ -151,6 +151,7 @@ import org.apache.hadoop.hive.ql.optimizer.calcite.rules.HivePartitionPruneRule;
 import org.apache.hadoop.hive.ql.optimizer.calcite.rules.HivePreFilteringRule;
 import org.apache.hadoop.hive.ql.optimizer.calcite.rules.HiveProjectMergeRule;
 import org.apache.hadoop.hive.ql.optimizer.calcite.rules.HiveRelFieldTrimmer;
+import org.apache.hadoop.hive.ql.optimizer.calcite.rules.HiveRulesRegistry;
 import org.apache.hadoop.hive.ql.optimizer.calcite.rules.HiveWindowingFixRule;
 import org.apache.hadoop.hive.ql.optimizer.calcite.translator.ASTConverter;
 import org.apache.hadoop.hive.ql.optimizer.calcite.translator.HiveOpConverter;
@@ -841,7 +842,7 @@ public class CalcitePlanner extends SemanticAnalyzer {
       final Double maxMemory = (double) HiveConf.getLongVar(
               conf, HiveConf.ConfVars.HIVECONVERTJOINNOCONDITIONALTASKTHRESHOLD);
       HiveAlgorithmsConf algorithmsConf = new HiveAlgorithmsConf(maxSplitSize, maxMemory);
-      HiveConfigContext confContext = new HiveConfigContext(algorithmsConf);
+      HiveVolcanoPlannerContext confContext = new HiveVolcanoPlannerContext(algorithmsConf);
       RelOptPlanner planner = HiveVolcanoPlanner.createPlanner(confContext);
       final RelOptQuery query = new RelOptQuery(planner);
       final RexBuilder rexBuilder = cluster.getRexBuilder();
@@ -1061,7 +1062,9 @@ public class CalcitePlanner extends SemanticAnalyzer {
           programBuilder.addRuleInstance(r);
       }
 
-      HepPlanner planner = new HepPlanner(programBuilder.build());
+      HiveRulesRegistry registry = new HiveRulesRegistry();
+      HiveHepPlannerContext context = new HiveHepPlannerContext(registry);
+      HepPlanner planner = new HepPlanner(programBuilder.build(), context);
       List<RelMetadataProvider> list = Lists.newArrayList();
       list.add(mdProvider);
       planner.registerMetadataProviders(list);

http://git-wip-us.apache.org/repos/asf/hive/blob/5201f188/ql/src/test/org/apache/hadoop/hive/ql/optimizer/calcite/TestCBORuleFiredOnlyOnce.java
----------------------------------------------------------------------
diff --git a/ql/src/test/org/apache/hadoop/hive/ql/optimizer/calcite/TestCBORuleFiredOnlyOnce.java b/ql/src/test/org/apache/hadoop/hive/ql/optimizer/calcite/TestCBORuleFiredOnlyOnce.java
new file mode 100644
index 0000000..f1d8d1d
--- /dev/null
+++ b/ql/src/test/org/apache/hadoop/hive/ql/optimizer/calcite/TestCBORuleFiredOnlyOnce.java
@@ -0,0 +1,168 @@
+/**
+ * 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.hadoop.hive.ql.optimizer.calcite;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.hep.HepMatchOrder;
+import org.apache.calcite.plan.hep.HepPlanner;
+import org.apache.calcite.plan.hep.HepProgramBuilder;
+import org.apache.calcite.rel.AbstractRelNode;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.metadata.CachingRelMetadataProvider;
+import org.apache.calcite.rel.metadata.ChainedRelMetadataProvider;
+import org.apache.calcite.rel.metadata.RelMetadataProvider;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rel.type.RelRecordType;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.ql.optimizer.calcite.rules.HiveRulesRegistry;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class TestCBORuleFiredOnlyOnce {
+
+
+  @Test
+  public void testRuleFiredOnlyOnce() {
+
+    HiveConf conf = new HiveConf();
+
+    // Create HepPlanner
+    HepProgramBuilder programBuilder = new HepProgramBuilder();
+    programBuilder.addMatchOrder(HepMatchOrder.TOP_DOWN);
+    programBuilder = programBuilder.addRuleCollection(
+            ImmutableList.<RelOptRule>of(DummyRule.INSTANCE));
+
+    // Create rules registry to not trigger a rule more than once
+    HiveRulesRegistry registry = new HiveRulesRegistry();
+    HiveHepPlannerContext context = new HiveHepPlannerContext(registry);
+    HepPlanner planner = new HepPlanner(programBuilder.build(), context);
+
+    // Cluster
+    RexBuilder rexBuilder = new RexBuilder(new JavaTypeFactoryImpl());
+    RelOptCluster cluster = RelOptCluster.create(planner, rexBuilder);
+
+    // Create MD provider
+    HiveDefaultRelMetadataProvider mdProvider = new HiveDefaultRelMetadataProvider(conf);
+    List<RelMetadataProvider> list = Lists.newArrayList();
+    list.add(mdProvider.getMetadataProvider());
+    planner.registerMetadataProviders(list);
+    RelMetadataProvider chainedProvider = ChainedRelMetadataProvider.of(list);
+
+    final RelNode node = new DummyNode(cluster, cluster.traitSet());
+
+    node.getCluster().setMetadataProvider(
+        new CachingRelMetadataProvider(chainedProvider, planner));
+
+    planner.setRoot(node);
+
+    planner.findBestExp();
+
+    // Matches 3 times: 2 times the original node, 1 time the new node created by the rule
+    assertEquals(3, DummyRule.INSTANCE.numberMatches);
+    // It is fired only once: on the original node
+    assertEquals(1, DummyRule.INSTANCE.numberOnMatch);
+  }
+
+  public static class DummyRule extends RelOptRule {
+
+    public static final DummyRule INSTANCE =
+            new DummyRule();
+
+    public int numberMatches;
+    public int numberOnMatch;
+
+    private DummyRule() {
+      super(operand(RelNode.class, any()));
+      numberMatches = 0;
+      numberOnMatch = 0;
+    }
+
+    @Override
+    public boolean matches(RelOptRuleCall call) {
+      final RelNode node = call.rel(0);
+
+      numberMatches++;
+
+      HiveRulesRegistry registry = call.getPlanner().
+              getContext().unwrap(HiveRulesRegistry.class);
+
+      // If this operator has been visited already by the rule,
+      // we do not need to apply the optimization
+      if (registry != null && registry.getVisited(this).contains(node)) {
+        return false;
+      }
+
+      return true;
+    }
+
+    @Override
+    public void onMatch(RelOptRuleCall call) {
+      final RelNode node = call.rel(0);
+
+      numberOnMatch++;
+
+      // If we have fired it already once, we return and the test will fail
+      if (numberOnMatch > 1) {
+        return;
+      }
+
+      // Register that we have visited this operator in this rule
+      HiveRulesRegistry registry = call.getPlanner().
+              getContext().unwrap(HiveRulesRegistry.class);
+      if (registry != null) {
+        registry.registerVisited(this, node);
+      }
+
+      // We create a new op if it is the first time we fire the rule
+      final RelNode newNode = new DummyNode(node.getCluster(), node.getTraitSet());
+      // We register it so we do not fire the rule on it again
+      if (registry != null) {
+        registry.registerVisited(this, newNode);
+      }
+
+      call.transformTo(newNode);
+
+    }
+  }
+
+  public static class DummyNode extends AbstractRelNode {
+
+    protected DummyNode(RelOptCluster cluster, RelTraitSet traits) {
+      super(cluster, cluster.traitSet());
+    }
+
+    @Override
+    protected RelDataType deriveRowType() {
+      return new RelRecordType(Lists.<RelDataTypeField>newArrayList());
+    }
+  }
+
+
+}