calcite-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jh...@apache.org
Subject [1/3] calcite git commit: [CALCITE-1148] Fix RelTrait conversion (e.g. distribution, collation) (Minji Kim)
Date Fri, 08 Jul 2016 18:51:47 GMT
Repository: calcite
Updated Branches:
  refs/heads/master d9eb43832 -> d485eab4a


[CALCITE-1148] Fix RelTrait conversion (e.g. distribution, collation) (Minji Kim)

In the current calcite, trait conversion is not handled properly, e.g.
collation/distribution traits are not converted (shown by the tests).
This patch fixes this issue.

For each RelCollationTrait, introduce a new API, canConvert() which
should return true if the conversion from a trait to the other is
possible.

For each Convention, introduce two new APIs, canConvertConvention()
returns true if the convernsion is possible, and useAbstractConverters()
returns true if the trait conversion should be handle via
AbstractConverters.  By default, both functions return false.

In RelSet, when adding a new RelSubset, if the convention returns false
for useAbstractConverters(), we do not add AbstractConverters.  Even if
convention.useAbstractConverters() return true, we only add
AbstractConverters if the AbstractConverters can convert (i.e. if
RelCollationTrait.canConvert() returns true) and the conversion is
needed (i.e. if RelTrait.satisfies() returns false).

Added test cases.

Close apache/calcite#210


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

Branch: refs/heads/master
Commit: a3bc0d8ea3c1f320b96a4ab9641becee90a388bc
Parents: 5bc0e0e
Author: Minji Kim <minji@dremio.com>
Authored: Tue Mar 8 21:31:20 2016 -0800
Committer: Julian Hyde <jhyde@apache.org>
Committed: Thu Jul 7 23:44:51 2016 -0700

----------------------------------------------------------------------
 .../enumerable/EnumerableConvention.java        |  10 +
 .../calcite/interpreter/BindableConvention.java |  10 +
 .../interpreter/InterpretableConvention.java    |  10 +
 .../org/apache/calcite/plan/Convention.java     |  34 +++
 .../apache/calcite/plan/ConventionTraitDef.java |   4 +-
 .../org/apache/calcite/plan/RelTraitDef.java    |  17 ++
 .../org/apache/calcite/plan/volcano/RelSet.java |  84 +++++-
 .../calcite/plan/volcano/VolcanoPlanner.java    |  19 --
 .../calcite/rel/RelCollationTraitDef.java       |  15 +-
 .../plan/volcano/CollationConversionTest.java   | 271 ++++++++++++++++++
 .../calcite/plan/volcano/ComboRuleTest.java     | 158 +++++++++++
 .../calcite/plan/volcano/PlannerTests.java      | 207 ++++++++++++++
 .../plan/volcano/TraitConversionTest.java       | 282 +++++++++++++++++++
 .../plan/volcano/VolcanoPlannerTest.java        | 236 +---------------
 .../plan/volcano/VolcanoPlannerTraitTest.java   |   8 +-
 .../org/apache/calcite/test/CalciteSuite.java   |   6 +
 .../apache/calcite/test/JdbcAdapterTest.java    | 136 ++++-----
 .../java/org/apache/calcite/test/JdbcTest.java  |   1 +
 18 files changed, 1190 insertions(+), 318 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableConvention.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableConvention.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableConvention.java
index 946afae..ab3976f 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableConvention.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableConvention.java
@@ -21,6 +21,7 @@ import org.apache.calcite.plan.ConventionTraitDef;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTrait;
 import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.plan.RelTraitSet;
 
 /**
  * Family of calling conventions that return results as an
@@ -54,6 +55,15 @@ public enum EnumerableConvention implements Convention {
   }
 
   public void register(RelOptPlanner planner) {}
+
+  public boolean canConvertConvention(Convention toConvention) {
+    return false;
+  }
+
+  public boolean useAbstractConvertersForConversion(RelTraitSet fromTraits,
+      RelTraitSet toTraits) {
+    return false;
+  }
 }
 
 // End EnumerableConvention.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/interpreter/BindableConvention.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/interpreter/BindableConvention.java b/core/src/main/java/org/apache/calcite/interpreter/BindableConvention.java
index 46dad77..94a35e8 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/BindableConvention.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/BindableConvention.java
@@ -21,6 +21,7 @@ import org.apache.calcite.plan.ConventionTraitDef;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTrait;
 import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.plan.RelTraitSet;
 
 /**
  * Calling convention that returns results as an
@@ -59,6 +60,15 @@ public enum BindableConvention implements Convention {
   }
 
   public void register(RelOptPlanner planner) {}
+
+  public boolean canConvertConvention(Convention toConvention) {
+    return false;
+  }
+
+  public boolean useAbstractConvertersForConversion(RelTraitSet fromTraits,
+      RelTraitSet toTraits) {
+    return false;
+  }
 }
 
 // End BindableConvention.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/interpreter/InterpretableConvention.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/interpreter/InterpretableConvention.java b/core/src/main/java/org/apache/calcite/interpreter/InterpretableConvention.java
index e0e2fde..5927b7e 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/InterpretableConvention.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/InterpretableConvention.java
@@ -22,6 +22,7 @@ import org.apache.calcite.plan.ConventionTraitDef;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTrait;
 import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.plan.RelTraitSet;
 
 /**
  * Calling convention that returns results as an
@@ -53,6 +54,15 @@ public enum InterpretableConvention implements Convention {
   }
 
   public void register(RelOptPlanner planner) {}
+
+  public boolean canConvertConvention(Convention toConvention) {
+    return false;
+  }
+
+  public boolean useAbstractConvertersForConversion(RelTraitSet fromTraits,
+      RelTraitSet toTraits) {
+    return false;
+  }
 }
 
 // End InterpretableConvention.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/plan/Convention.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/Convention.java b/core/src/main/java/org/apache/calcite/plan/Convention.java
index 46750c2..248a98f 100644
--- a/core/src/main/java/org/apache/calcite/plan/Convention.java
+++ b/core/src/main/java/org/apache/calcite/plan/Convention.java
@@ -38,6 +38,31 @@ public interface Convention extends RelTrait {
   String getName();
 
   /**
+   * Returns whether we should convert from this convention to
+   * {@code toConvention}. Used by {@link ConventionTraitDef}.
+   *
+   * @param toConvention Desired convention to convert to
+   * @return Whether we should convert from this convention to toConvention
+   */
+  boolean canConvertConvention(Convention toConvention);
+
+  /**
+   * Returns whether we should convert from this trait set to the other trait
+   * set.
+   *
+   * <p>The convention decides whether it wants to handle other trait
+   * conversions, e.g. collation, distribution, etc.  For a given convention, we
+   * will only add abstract converters to handle the trait (convention,
+   * collation, distribution, etc.) conversions if this function returns true.
+   *
+   * @param fromTraits Traits of the RelNode that we are converting from
+   * @param toTraits Target traits
+   * @return Whether we should add converters
+   */
+  boolean useAbstractConvertersForConversion(RelTraitSet fromTraits,
+      RelTraitSet toTraits);
+
+  /**
    * Default implementation.
    */
   class Impl implements Convention {
@@ -70,6 +95,15 @@ public interface Convention extends RelTrait {
     public RelTraitDef getTraitDef() {
       return ConventionTraitDef.INSTANCE;
     }
+
+    public boolean canConvertConvention(Convention toConvention) {
+      return false;
+    }
+
+    public boolean useAbstractConvertersForConversion(RelTraitSet fromTraits,
+        RelTraitSet toTraits) {
+      return false;
+    }
   }
 }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/plan/ConventionTraitDef.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/ConventionTraitDef.java b/core/src/main/java/org/apache/calcite/plan/ConventionTraitDef.java
index 579e217..0ecca03 100644
--- a/core/src/main/java/org/apache/calcite/plan/ConventionTraitDef.java
+++ b/core/src/main/java/org/apache/calcite/plan/ConventionTraitDef.java
@@ -195,8 +195,8 @@ public class ConventionTraitDef extends RelTraitDef<Convention> {
       Convention fromConvention,
       Convention toConvention) {
     ConversionData conversionData = getConversionData(planner);
-    return conversionData.getShortestPath(fromConvention, toConvention)
-        != null;
+    return fromConvention.canConvertConvention(toConvention)
+        || conversionData.getShortestPath(fromConvention, toConvention) != null;
   }
 
   private ConversionData getConversionData(RelOptPlanner planner) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/plan/RelTraitDef.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/RelTraitDef.java b/core/src/main/java/org/apache/calcite/plan/RelTraitDef.java
index 1f26b38..809dd21 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelTraitDef.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelTraitDef.java
@@ -177,6 +177,23 @@ public abstract class RelTraitDef<T extends RelTrait> {
       T toTrait);
 
   /**
+   * Tests whether the given RelTrait can be converted to another RelTrait.
+   *
+   * @param planner   the planner requesting the conversion test
+   * @param fromTrait the RelTrait to convert from
+   * @param toTrait   the RelTrait to convert to
+   * @param fromRel   the RelNode to convert from (with fromTrait)
+   * @return true if fromTrait can be converted to toTrait
+   */
+  public boolean canConvert(
+      RelOptPlanner planner,
+      T fromTrait,
+      T toTrait,
+      RelNode fromRel) {
+    return canConvert(planner, fromTrait, toTrait);
+  }
+
+  /**
    * Provides notification of the registration of a particular
    * {@link ConverterRule} with a {@link RelOptPlanner}. The default
    * implementation does nothing.

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/plan/volcano/RelSet.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/RelSet.java b/core/src/main/java/org/apache/calcite/plan/volcano/RelSet.java
index 7cee6b6..098fc6a 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/RelSet.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/RelSet.java
@@ -20,6 +20,7 @@ import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptListener;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.plan.RelTraitDef;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.CorrelationId;
@@ -148,6 +149,80 @@ class RelSet {
     return subset;
   }
 
+  private void addAbstractConverters(
+      VolcanoPlanner planner, RelOptCluster cluster, RelSubset subset, boolean subsetToOthers) {
+    // Converters from newly introduced subset to all the remaining one (vice versa), only if
+    // we can convert.  No point adding converters if it is not possible.
+    for (RelSubset other : subsets) {
+
+      assert other.getTraitSet().size() == subset.getTraitSet().size();
+
+      if ((other == subset)
+        || (subsetToOthers
+          && !subset.getConvention().useAbstractConvertersForConversion(
+            subset.getTraitSet(), other.getTraitSet()))
+        || (!subsetToOthers
+          && !other.getConvention().useAbstractConvertersForConversion(
+            other.getTraitSet(), subset.getTraitSet()))) {
+        continue;
+      }
+
+      final ImmutableList<RelTrait> difference =
+          subset.getTraitSet().difference(other.getTraitSet());
+
+      boolean addAbstractConverter = true;
+      int numTraitNeedConvert = 0;
+
+      for (RelTrait curOtherTrait : difference) {
+        RelTraitDef traitDef = curOtherTrait.getTraitDef();
+        RelTrait curRelTrait = subset.getTraitSet().getTrait(traitDef);
+
+        assert curRelTrait.getTraitDef() == traitDef;
+
+        if (curRelTrait == null) {
+          addAbstractConverter = false;
+          break;
+        }
+
+        boolean canConvert = false;
+        boolean needConvert = false;
+        if (subsetToOthers) {
+          // We can convert from subset to other.  So, add converter with subset as child and
+          // traitset as the other's traitset.
+          canConvert = traitDef.canConvert(
+              cluster.getPlanner(), curRelTrait, curOtherTrait, subset);
+          needConvert = !curRelTrait.satisfies(curOtherTrait);
+        } else {
+          // We can convert from others to subset.
+          canConvert = traitDef.canConvert(
+              cluster.getPlanner(), curOtherTrait, curRelTrait, other);
+          needConvert = !curOtherTrait.satisfies(curRelTrait);
+        }
+
+        if (!canConvert) {
+          addAbstractConverter = false;
+          break;
+        }
+
+        if (needConvert) {
+          numTraitNeedConvert++;
+        }
+      }
+
+      if (addAbstractConverter && numTraitNeedConvert > 0) {
+        if (subsetToOthers) {
+          final AbstractConverter converter =
+              new AbstractConverter(cluster, subset, null, other.getTraitSet());
+          planner.register(converter, other);
+        } else {
+          final AbstractConverter converter =
+              new AbstractConverter(cluster, other, null, subset.getTraitSet());
+          planner.register(converter, subset);
+        }
+      }
+    }
+  }
+
   RelSubset getOrCreateSubset(
       RelOptCluster cluster,
       RelTraitSet traits) {
@@ -158,12 +233,13 @@ class RelSet {
       final VolcanoPlanner planner =
           (VolcanoPlanner) cluster.getPlanner();
 
+      addAbstractConverters(planner, cluster, subset, true);
+
+      // Need to first add to subset before adding the abstract converters (for others->subset)
+      // since otherwise during register() the planner will try to add this subset again.
       subsets.add(subset);
 
-      if (planner.root != null
-          && planner.root.set == this) {
-        planner.ensureRootConverters();
-      }
+      addAbstractConverters(planner, cluster, subset, false);
 
       if (planner.listener != null) {
         postEquivalenceEvent(planner, subset);

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
----------------------------------------------------------------------
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 4cda68e..45aea2b 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
@@ -710,25 +710,6 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     }
   }
 
-  public boolean canConvert(RelTraitSet fromTraits, RelTraitSet toTraits) {
-    assert fromTraits.size() >= toTraits.size();
-
-    boolean canConvert = true;
-    for (int i = 0; (i < toTraits.size()) && canConvert; i++) {
-      RelTrait fromTrait = fromTraits.getTrait(i);
-      RelTrait toTrait = toTraits.getTrait(i);
-
-      assert fromTrait.getTraitDef() == toTrait.getTraitDef();
-      assert traitDefs.contains(fromTrait.getTraitDef());
-      assert traitDefs.contains(toTrait.getTraitDef());
-
-      canConvert =
-          fromTrait.getTraitDef().canConvert(this, fromTrait, toTrait);
-    }
-
-    return canConvert;
-  }
-
   public RelNode changeTraits(final RelNode rel, RelTraitSet toTraits) {
     assert !rel.getTraitSet().equals(toTraits);
     assert toTraits.allSimple();

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/main/java/org/apache/calcite/rel/RelCollationTraitDef.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/RelCollationTraitDef.java b/core/src/main/java/org/apache/calcite/rel/RelCollationTraitDef.java
index aa3e8bf..4050fa6 100644
--- a/core/src/main/java/org/apache/calcite/rel/RelCollationTraitDef.java
+++ b/core/src/main/java/org/apache/calcite/rel/RelCollationTraitDef.java
@@ -82,7 +82,20 @@ public class RelCollationTraitDef extends RelTraitDef<RelCollation> {
   }
 
   public boolean canConvert(
-      RelOptPlanner planner, RelCollation fromTrait, RelCollation toTrait) {
+       RelOptPlanner planner, RelCollation fromTrait, RelCollation toTrait) {
+    return false;
+  }
+
+  @Override public boolean canConvert(RelOptPlanner planner,
+      RelCollation fromTrait, RelCollation toTrait, RelNode fromRel) {
+    // Returns true only if we can convert.  In this case, we can only convert
+    // if the fromTrait (the input) has fields that the toTrait wants to sort.
+    for (RelFieldCollation field : toTrait.getFieldCollations()) {
+      int index = field.getFieldIndex();
+      if (index >= fromRel.getRowType().getFieldCount()) {
+        return false;
+      }
+    }
     return true;
   }
 }

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/plan/volcano/CollationConversionTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/CollationConversionTest.java b/core/src/test/java/org/apache/calcite/plan/volcano/CollationConversionTest.java
new file mode 100644
index 0000000..ff8b50d
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/CollationConversionTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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.calcite.plan.volcano;
+
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.volcano.AbstractConverter.ExpandConversionRule;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollationImpl;
+import org.apache.calcite.rel.RelFieldCollation;
+import org.apache.calcite.rel.RelFieldCollation.Direction;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rex.RexNode;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.apache.calcite.plan.volcano.PlannerTests.PHYS_CALLING_CONVENTION;
+import static org.apache.calcite.plan.volcano.PlannerTests.TestLeafRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.TestSingleRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.newCluster;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit test for {@link org.apache.calcite.rel.RelCollationTraitDef}.
+ */
+public class CollationConversionTest {
+  private static final TestRelCollationImpl LEAF_COLLATION =
+      new TestRelCollationImpl(
+          ImmutableList.of(new RelFieldCollation(0, Direction.CLUSTERED)));
+
+  private static final TestRelCollationImpl ROOT_COLLATION =
+      new TestRelCollationImpl(ImmutableList.of(new RelFieldCollation(0)));
+
+  private static final TestRelCollationTraitDef COLLATION_TRAIT_DEF =
+      new TestRelCollationTraitDef();
+
+  @Test public void testCollationConversion() {
+    final VolcanoPlanner planner = new VolcanoPlanner();
+    planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+    planner.addRelTraitDef(COLLATION_TRAIT_DEF);
+
+    planner.addRule(new SingleNodeRule());
+    planner.addRule(new LeafTraitRule());
+    planner.addRule(ExpandConversionRule.INSTANCE);
+
+    final RelOptCluster cluster = newCluster(planner);
+    final NoneLeafRel leafRel = new NoneLeafRel(cluster, "a");
+    final NoneSingleRel singleRel = new NoneSingleRel(cluster, leafRel);
+    final RelNode convertedRel =
+        planner.changeTraits(singleRel,
+            cluster.traitSetOf(PHYS_CALLING_CONVENTION).plus(ROOT_COLLATION));
+    planner.setRoot(convertedRel);
+    RelNode result = planner.chooseDelegate().findBestExp();
+    assertTrue(result instanceof RootSingleRel);
+    assertTrue(result.getTraitSet().contains(ROOT_COLLATION));
+    assertTrue(result.getTraitSet().contains(PHYS_CALLING_CONVENTION));
+
+    final RelNode input = result.getInput(0);
+    assertTrue(input instanceof PhysicalSort);
+    assertTrue(result.getTraitSet().contains(ROOT_COLLATION));
+    assertTrue(input.getTraitSet().contains(PHYS_CALLING_CONVENTION));
+
+    final RelNode input2 = input.getInput(0);
+    assertTrue(input2 instanceof LeafRel);
+    assertTrue(input2.getTraitSet().contains(LEAF_COLLATION));
+    assertTrue(input.getTraitSet().contains(PHYS_CALLING_CONVENTION));
+  }
+
+  /** Converts a NoneSingleRel to RootSingleRel. */
+  private class SingleNodeRule extends RelOptRule {
+    SingleNodeRule() {
+      super(operand(NoneSingleRel.class, any()));
+    }
+
+    public Convention getOutConvention() {
+      return PHYS_CALLING_CONVENTION;
+    }
+
+    public void onMatch(RelOptRuleCall call) {
+      NoneSingleRel single = call.rel(0);
+      RelNode input = single.getInput();
+      RelNode physInput =
+          convert(input,
+              single.getTraitSet()
+                  .replace(PHYS_CALLING_CONVENTION)
+                  .plus(ROOT_COLLATION));
+      call.transformTo(
+          new RootSingleRel(
+              single.getCluster(),
+              physInput));
+    }
+  }
+
+  /** Root node with physical convention and ROOT_COLLATION trait. */
+  private class RootSingleRel extends TestSingleRel {
+    RootSingleRel(RelOptCluster cluster, RelNode input) {
+      super(cluster,
+          cluster.traitSetOf(PHYS_CALLING_CONVENTION).plus(ROOT_COLLATION),
+          input);
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeTinyCost();
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      return new RootSingleRel(getCluster(), sole(inputs));
+    }
+  }
+
+  /** Converts a {@link NoneLeafRel} (with none convention) to {@link LeafRel}
+   * (with physical convention). */
+  private class LeafTraitRule extends RelOptRule {
+    LeafTraitRule() {
+      super(operand(NoneLeafRel.class, any()));
+    }
+
+    public Convention getOutConvention() {
+      return PHYS_CALLING_CONVENTION;
+    }
+
+    public void onMatch(RelOptRuleCall call) {
+      NoneLeafRel leafRel = call.rel(0);
+      call.transformTo(new LeafRel(leafRel.getCluster(), leafRel.label));
+    }
+  }
+
+  /** Leaf node with physical convention and LEAF_COLLATION trait. */
+  private class LeafRel extends TestLeafRel {
+    LeafRel(RelOptCluster cluster, String label) {
+      super(cluster,
+          cluster.traitSetOf(PHYS_CALLING_CONVENTION).plus(LEAF_COLLATION),
+          label);
+    }
+
+    public RelOptCost computeSelfCost(
+        RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeTinyCost();
+    }
+
+    public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      return new LeafRel(getCluster(), label);
+    }
+  }
+
+  /** Leaf node with none convention and LEAF_COLLATION trait. */
+  private class NoneLeafRel extends TestLeafRel {
+    NoneLeafRel(RelOptCluster cluster, String label) {
+      super(cluster, cluster.traitSetOf(Convention.NONE).plus(LEAF_COLLATION),
+          label);
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(Convention.NONE, LEAF_COLLATION);
+      assert inputs.isEmpty();
+      return this;
+    }
+  }
+
+  /** A single-input node with none convention and LEAF_COLLATION trait. */
+  private static class NoneSingleRel extends TestSingleRel {
+    NoneSingleRel(RelOptCluster cluster, RelNode input) {
+      super(cluster, cluster.traitSetOf(Convention.NONE).plus(LEAF_COLLATION),
+          input);
+    }
+
+    public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(Convention.NONE, LEAF_COLLATION);
+      return new NoneSingleRel(getCluster(), sole(inputs));
+    }
+  }
+
+  /** Dummy collation trait implementation for the test. */
+  private static class TestRelCollationImpl extends RelCollationImpl {
+    TestRelCollationImpl(ImmutableList<RelFieldCollation> fieldCollations) {
+      super(fieldCollations);
+    }
+
+    @Override public RelTraitDef getTraitDef() {
+      return COLLATION_TRAIT_DEF;
+    }
+  }
+
+  /** Dummy collation trait def implementation for the test (uses
+   * {@link PhysicalSort} below). */
+  private static class TestRelCollationTraitDef
+      extends RelTraitDef<RelCollation> {
+    public Class<RelCollation> getTraitClass() {
+      return RelCollation.class;
+    }
+
+    public String getSimpleName() {
+      return "testsort";
+    }
+
+    @Override public boolean multiple() {
+      return true;
+    }
+
+    public RelCollation getDefault() {
+      return LEAF_COLLATION;
+    }
+
+    public RelNode convert(RelOptPlanner planner, RelNode rel,
+        RelCollation toCollation, boolean allowInfiniteCostConverters) {
+      if (toCollation.getFieldCollations().isEmpty()) {
+        // An empty sort doesn't make sense.
+        return null;
+      }
+
+      return new PhysicalSort(rel.getCluster(),
+          rel.getTraitSet().replace(toCollation), rel, toCollation, null, null);
+    }
+
+    public boolean canConvert(RelOptPlanner planner, RelCollation fromTrait,
+        RelCollation toTrait) {
+      return true;
+    }
+  }
+
+  /** Physical sort node (not logical). */
+  private static class PhysicalSort extends Sort {
+    PhysicalSort(RelOptCluster cluster, RelTraitSet traits, RelNode input,
+        RelCollation collation, RexNode offset, RexNode fetch) {
+      super(cluster, traits, input, collation, offset, fetch);
+    }
+
+    public Sort copy(RelTraitSet traitSet, RelNode newInput,
+        RelCollation newCollation, RexNode offset, RexNode fetch) {
+      return new PhysicalSort(getCluster(), traitSet, newInput, newCollation,
+          offset, fetch);
+    }
+
+    public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeTinyCost();
+    }
+  }
+}
+
+// End CollationConversionTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/plan/volcano/ComboRuleTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/ComboRuleTest.java b/core/src/test/java/org/apache/calcite/plan/volcano/ComboRuleTest.java
new file mode 100644
index 0000000..3cd8195
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/ComboRuleTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.calcite.plan.volcano;
+
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptRuleOperand;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.apache.calcite.plan.volcano.PlannerTests.GoodSingleRule;
+import static org.apache.calcite.plan.volcano.PlannerTests.NoneLeafRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.NoneSingleRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.PHYS_CALLING_CONVENTION;
+import static org.apache.calcite.plan.volcano.PlannerTests.PhysLeafRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.PhysSingleRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.TestSingleRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.newCluster;
+
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * Unit test for {@link VolcanoPlanner}
+ */
+public class ComboRuleTest {
+
+  @Test public void testCombo() {
+    VolcanoPlanner planner = new VolcanoPlanner();
+    planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+
+    planner.addRule(new ComboRule());
+    planner.addRule(new AddIntermediateNodeRule());
+    planner.addRule(new GoodSingleRule());
+
+    RelOptCluster cluster = newCluster(planner);
+    NoneLeafRel leafRel = new NoneLeafRel(cluster, "a");
+    NoneSingleRel singleRel = new NoneSingleRel(cluster, leafRel);
+    NoneSingleRel singleRel2 = new NoneSingleRel(cluster, singleRel);
+    RelNode convertedRel =
+        planner.changeTraits(singleRel2,
+            cluster.traitSetOf(PHYS_CALLING_CONVENTION));
+    planner.setRoot(convertedRel);
+    RelNode result = planner.chooseDelegate().findBestExp();
+    assertTrue(result instanceof IntermediateNode);
+  }
+
+  /** Intermediate node, the cost decreases as it is pushed up the tree
+   * (more inputs it has, cheaper it gets). */
+  private static class IntermediateNode extends TestSingleRel {
+    final int nodesBelowCount;
+
+    IntermediateNode(RelOptCluster cluster, RelNode input, int nodesBelowCount) {
+      super(cluster, cluster.traitSetOf(PHYS_CALLING_CONVENTION), input);
+      this.nodesBelowCount = nodesBelowCount;
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeCost(100, 100, 100)
+          .multiplyBy(1.0 / nodesBelowCount);
+    }
+
+    public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(PHYS_CALLING_CONVENTION);
+      return new IntermediateNode(getCluster(), sole(inputs), nodesBelowCount);
+    }
+  }
+
+  /** Rule that adds an intermediate node above the {@link PhysLeafRel}. */
+  private static class AddIntermediateNodeRule extends RelOptRule {
+    AddIntermediateNodeRule() {
+      super(operand(NoneLeafRel.class, any()));
+    }
+
+    public Convention getOutConvention() {
+      return PHYS_CALLING_CONVENTION;
+    }
+
+    public void onMatch(RelOptRuleCall call) {
+      NoneLeafRel leaf = call.rel(0);
+
+      RelNode physLeaf = new PhysLeafRel(leaf.getCluster(), leaf.label);
+      RelNode intermediateNode = new IntermediateNode(physLeaf.getCluster(), physLeaf, 1);
+
+      call.transformTo(intermediateNode);
+    }
+  }
+
+  /** Matches {@link PhysSingleRel}-{@link IntermediateNode}-Any
+   * and converts to {@link IntermediateNode}-{@link PhysSingleRel}-Any. */
+  private static class ComboRule extends RelOptRule {
+    ComboRule() {
+      super(createOperand());
+    }
+
+    private static RelOptRuleOperand createOperand() {
+      RelOptRuleOperand input = operand(RelNode.class, any());
+      input = operand(IntermediateNode.class, some(input));
+      input = operand(PhysSingleRel.class, some(input));
+      return input;
+    }
+
+    @Override public Convention getOutConvention() {
+      return PHYS_CALLING_CONVENTION;
+    }
+
+    @Override public boolean matches(RelOptRuleCall call) {
+      if (call.rels.length < 3) {
+        return false;
+      }
+
+      if (call.rel(0) instanceof PhysSingleRel
+          && call.rel(1) instanceof IntermediateNode
+          && call.rel(2) instanceof RelNode) {
+        return true;
+      }
+      return false;
+    }
+
+    @Override public void onMatch(RelOptRuleCall call) {
+      List<RelNode> newInputs = ImmutableList.of(call.rel(2));
+      IntermediateNode oldInter = call.rel(1);
+      RelNode physRel = call.rel(0).copy(call.rel(0).getTraitSet(), newInputs);
+      RelNode converted = new IntermediateNode(physRel.getCluster(), physRel,
+          oldInter.nodesBelowCount + 1);
+      call.transformTo(converted);
+    }
+  }
+}
+
+// End ComboRuleTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/plan/volcano/PlannerTests.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/PlannerTests.java b/core/src/test/java/org/apache/calcite/plan/volcano/PlannerTests.java
new file mode 100644
index 0000000..713e5c1
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/PlannerTests.java
@@ -0,0 +1,207 @@
+/*
+ * 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.calcite.plan.volcano;
+
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.AbstractRelNode;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.SingleRel;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
+
+import java.util.List;
+
+/**
+ * Common classes and utility methods for Volcano planner tests.
+ */
+class PlannerTests {
+
+  private PlannerTests() {}
+
+  /**
+   * Private calling convention representing a physical implementation.
+   */
+  static final Convention PHYS_CALLING_CONVENTION =
+      new Convention.Impl("PHYS", RelNode.class) {
+        @Override public boolean canConvertConvention(Convention toConvention) {
+          return true;
+        }
+
+        @Override public boolean useAbstractConvertersForConversion(
+            RelTraitSet fromTraits, RelTraitSet toTraits) {
+          return true;
+        }
+      };
+
+  static RelOptCluster newCluster(VolcanoPlanner planner) {
+    final RelDataTypeFactory typeFactory =
+        new SqlTypeFactoryImpl(org.apache.calcite.rel.type.RelDataTypeSystem.DEFAULT);
+    return RelOptCluster.create(planner, new RexBuilder(typeFactory));
+  }
+
+  /** Leaf relational expression. */
+  abstract static class TestLeafRel extends AbstractRelNode {
+    final String label;
+
+    TestLeafRel(RelOptCluster cluster, RelTraitSet traits, String label) {
+      super(cluster, traits);
+      this.label = label;
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeInfiniteCost();
+    }
+
+    @Override protected RelDataType deriveRowType() {
+      final RelDataTypeFactory typeFactory = getCluster().getTypeFactory();
+      return typeFactory.builder()
+          .add("this", typeFactory.createJavaType(Void.TYPE))
+          .build();
+    }
+
+    @Override public RelWriter explainTerms(RelWriter pw) {
+      return super.explainTerms(pw).item("label", label);
+    }
+  }
+
+  /** Relational expression with one input. */
+  abstract static class TestSingleRel extends SingleRel {
+    TestSingleRel(RelOptCluster cluster, RelTraitSet traits, RelNode input) {
+      super(cluster, traits, input);
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeInfiniteCost();
+    }
+
+    @Override protected RelDataType deriveRowType() {
+      return getInput().getRowType();
+    }
+  }
+
+  /** Relational expression with one input and convention NONE. */
+  static class NoneSingleRel extends TestSingleRel {
+    NoneSingleRel(RelOptCluster cluster, RelNode input) {
+      super(cluster, cluster.traitSetOf(Convention.NONE), input);
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(Convention.NONE);
+      return new NoneSingleRel(getCluster(), sole(inputs));
+    }
+  }
+
+  /** Relational expression with zero inputs and convention NONE. */
+  static class NoneLeafRel extends TestLeafRel {
+    NoneLeafRel(RelOptCluster cluster, String label) {
+      super(cluster, cluster.traitSetOf(Convention.NONE), label);
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(Convention.NONE);
+      assert inputs.isEmpty();
+      return this;
+    }
+  }
+
+  /** Relational expression with zero inputs and convention PHYS. */
+  static class PhysLeafRel extends TestLeafRel {
+    PhysLeafRel(RelOptCluster cluster, String label) {
+      super(cluster, cluster.traitSetOf(PHYS_CALLING_CONVENTION), label);
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeTinyCost();
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(PHYS_CALLING_CONVENTION);
+      assert inputs.isEmpty();
+      return this;
+    }
+  }
+
+  /** Relational expression with one input and convention PHYS. */
+  static class PhysSingleRel extends TestSingleRel {
+    PhysSingleRel(RelOptCluster cluster, RelNode input) {
+      super(cluster, cluster.traitSetOf(PHYS_CALLING_CONVENTION), input);
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeTinyCost();
+    }
+
+    public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(PHYS_CALLING_CONVENTION);
+      return new PhysSingleRel(getCluster(), sole(inputs));
+    }
+  }
+
+  /** Planner rule that converts {@link NoneLeafRel} to PHYS convention. */
+  static class PhysLeafRule extends RelOptRule {
+    PhysLeafRule() {
+      super(operand(NoneLeafRel.class, any()));
+    }
+
+    @Override public Convention getOutConvention() {
+      return PHYS_CALLING_CONVENTION;
+    }
+
+    public void onMatch(RelOptRuleCall call) {
+      NoneLeafRel leafRel = call.rel(0);
+      call.transformTo(
+          new PhysLeafRel(leafRel.getCluster(), leafRel.label));
+    }
+  }
+
+  /** Planner rule that matches a {@link NoneSingleRel} and succeeds. */
+  static class GoodSingleRule extends RelOptRule {
+    GoodSingleRule() {
+      super(operand(NoneSingleRel.class, any()));
+    }
+
+    @Override public Convention getOutConvention() {
+      return PHYS_CALLING_CONVENTION;
+    }
+
+    public void onMatch(RelOptRuleCall call) {
+      NoneSingleRel single = call.rel(0);
+      RelNode input = single.getInput();
+      RelNode physInput =
+          convert(input,
+              single.getTraitSet().replace(PHYS_CALLING_CONVENTION));
+      call.transformTo(
+          new PhysSingleRel(single.getCluster(), physInput));
+    }
+  }
+}
+
+// End PlannerTests.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/plan/volcano/TraitConversionTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/TraitConversionTest.java b/core/src/test/java/org/apache/calcite/plan/volcano/TraitConversionTest.java
new file mode 100644
index 0000000..b3ae329
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/TraitConversionTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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.calcite.plan.volcano;
+
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.volcano.AbstractConverter.ExpandConversionRule;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.apache.calcite.plan.volcano.PlannerTests.PHYS_CALLING_CONVENTION;
+import static org.apache.calcite.plan.volcano.PlannerTests.TestLeafRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.TestSingleRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.newCluster;
+
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * Unit test for {@link org.apache.calcite.rel.RelDistributionTraitDef}.
+ */
+public class TraitConversionTest {
+
+  private static final ConvertRelDistributionTraitDef NEW_TRAIT_DEF_INSTANCE =
+      new ConvertRelDistributionTraitDef();
+  private static final SimpleDistribution SIMPLE_DISTRIBUTION_ANY =
+      new SimpleDistribution("ANY");
+  private static final SimpleDistribution SIMPLE_DISTRIBUTION_RANDOM =
+      new SimpleDistribution("RANDOM");
+  private static final SimpleDistribution SIMPLE_DISTRIBUTION_SINGLETON =
+      new SimpleDistribution("SINGLETON");
+
+  @Test public void testTraitConversion() {
+    final VolcanoPlanner planner = new VolcanoPlanner();
+    planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+    planner.addRelTraitDef(NEW_TRAIT_DEF_INSTANCE);
+
+    planner.addRule(new RandomSingleTraitRule());
+    planner.addRule(new SingleLeafTraitRule());
+    planner.addRule(ExpandConversionRule.INSTANCE);
+
+    final RelOptCluster cluster = newCluster(planner);
+    final NoneLeafRel leafRel = new NoneLeafRel(cluster, "a");
+    final NoneSingleRel singleRel = new NoneSingleRel(cluster, leafRel);
+    final RelNode convertedRel =
+        planner.changeTraits(singleRel,
+            cluster.traitSetOf(PHYS_CALLING_CONVENTION));
+    planner.setRoot(convertedRel);
+    final RelNode result = planner.chooseDelegate().findBestExp();
+
+    assertTrue(result instanceof RandomSingleRel);
+    assertTrue(result.getTraitSet().contains(PHYS_CALLING_CONVENTION));
+    assertTrue(result.getTraitSet().contains(SIMPLE_DISTRIBUTION_RANDOM));
+
+    final RelNode input = result.getInput(0);
+    assertTrue(input instanceof BridgeRel);
+    assertTrue(input.getTraitSet().contains(PHYS_CALLING_CONVENTION));
+    assertTrue(input.getTraitSet().contains(SIMPLE_DISTRIBUTION_RANDOM));
+
+    final RelNode input2 = input.getInput(0);
+    assertTrue(input2 instanceof SingletonLeafRel);
+    assertTrue(input2.getTraitSet().contains(PHYS_CALLING_CONVENTION));
+    assertTrue(input2.getTraitSet().contains(SIMPLE_DISTRIBUTION_SINGLETON));
+  }
+
+  /** Converts a {@link NoneSingleRel} (none convention, distribution any)
+   * to {@link RandomSingleRel} (physical convention, distribution random). */
+  private static class RandomSingleTraitRule extends RelOptRule {
+    RandomSingleTraitRule() {
+      super(operand(NoneSingleRel.class, any()));
+    }
+
+    @Override public Convention getOutConvention() {
+      return PHYS_CALLING_CONVENTION;
+    }
+
+    public void onMatch(RelOptRuleCall call) {
+      NoneSingleRel single = call.rel(0);
+      RelNode input = single.getInput();
+      RelNode physInput =
+          convert(input,
+              single.getTraitSet()
+                  .replace(PHYS_CALLING_CONVENTION)
+                  .plus(SIMPLE_DISTRIBUTION_RANDOM));
+      call.transformTo(
+          new RandomSingleRel(
+              single.getCluster(),
+              physInput));
+    }
+  }
+
+  /** Rel with physical convention and random distribution. */
+  private static class RandomSingleRel extends TestSingleRel {
+    RandomSingleRel(RelOptCluster cluster, RelNode input) {
+      super(cluster,
+          cluster.traitSetOf(PHYS_CALLING_CONVENTION)
+              .plus(SIMPLE_DISTRIBUTION_RANDOM), input);
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeTinyCost();
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      return new RandomSingleRel(getCluster(), sole(inputs));
+    }
+  }
+
+  /** Converts {@link NoneLeafRel} (none convention, any distribution) to
+   * {@link SingletonLeafRel} (physical convention, singleton distribution). */
+  private static class SingleLeafTraitRule extends RelOptRule {
+    SingleLeafTraitRule() {
+      super(operand(NoneLeafRel.class, any()));
+    }
+
+    @Override public Convention getOutConvention() {
+      return PHYS_CALLING_CONVENTION;
+    }
+
+    public void onMatch(RelOptRuleCall call) {
+      NoneLeafRel leafRel = call.rel(0);
+      call.transformTo(
+          new SingletonLeafRel(leafRel.getCluster(), leafRel.label));
+    }
+  }
+
+  /** Rel with singleton distribution, physical convention. */
+  private static class SingletonLeafRel extends TestLeafRel {
+    SingletonLeafRel(RelOptCluster cluster, String label) {
+      super(cluster,
+          cluster.traitSetOf(PHYS_CALLING_CONVENTION)
+              .plus(SIMPLE_DISTRIBUTION_SINGLETON), label);
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeTinyCost();
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      return new SingletonLeafRel(getCluster(), label);
+    }
+  }
+
+  /** Bridges the {@link SimpleDistribution}, difference between
+   * {@link SingletonLeafRel} and {@link RandomSingleRel}. */
+  private static class BridgeRel extends TestSingleRel {
+    BridgeRel(RelOptCluster cluster, RelNode input) {
+      super(cluster,
+          cluster.traitSetOf(PHYS_CALLING_CONVENTION)
+              .plus(SIMPLE_DISTRIBUTION_RANDOM), input);
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
+        RelMetadataQuery mq) {
+      return planner.getCostFactory().makeTinyCost();
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      return new BridgeRel(getCluster(), sole(inputs));
+    }
+  }
+
+  /** Dummy distribution for test (simplified version of RelDistribution). */
+  private static class SimpleDistribution implements RelTrait {
+    private final String name;
+
+    SimpleDistribution(String name) {
+      this.name = name;
+    }
+
+    @Override public String toString() {
+      return name;
+    }
+
+    @Override public RelTraitDef getTraitDef() {
+      return NEW_TRAIT_DEF_INSTANCE;
+    }
+
+    @Override public boolean satisfies(RelTrait trait) {
+      return trait == this || trait == SIMPLE_DISTRIBUTION_ANY;
+
+    }
+
+    @Override public void register(RelOptPlanner planner) {}
+  }
+
+  /**
+   * Dummy distribution trait def for test (handles conversion of SimpleDistribution)
+   */
+  private static class ConvertRelDistributionTraitDef
+      extends RelTraitDef<SimpleDistribution> {
+
+    @Override public Class<SimpleDistribution> getTraitClass() {
+      return SimpleDistribution.class;
+    }
+
+    @Override public String toString() {
+      return getSimpleName();
+    }
+
+    @Override public String getSimpleName() {
+      return "ConvertRelDistributionTraitDef";
+    }
+
+    @Override public RelNode convert(RelOptPlanner planner, RelNode rel,
+        SimpleDistribution toTrait, boolean allowInfiniteCostConverters) {
+      if (toTrait == SIMPLE_DISTRIBUTION_ANY) {
+        return rel;
+      }
+
+      return new BridgeRel(rel.getCluster(), rel);
+    }
+
+    @Override public boolean canConvert(RelOptPlanner planner,
+        SimpleDistribution fromTrait, SimpleDistribution toTrait) {
+      return (fromTrait == toTrait)
+          || (toTrait == SIMPLE_DISTRIBUTION_ANY)
+          || (fromTrait == SIMPLE_DISTRIBUTION_SINGLETON
+          && toTrait == SIMPLE_DISTRIBUTION_RANDOM);
+
+    }
+
+    @Override public SimpleDistribution getDefault() {
+      return SIMPLE_DISTRIBUTION_ANY;
+    }
+  }
+
+  /** Any distribution and none convention. */
+  private static class NoneLeafRel extends TestLeafRel {
+    NoneLeafRel(RelOptCluster cluster, String label) {
+      super(cluster, cluster.traitSetOf(Convention.NONE), label);
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(Convention.NONE, SIMPLE_DISTRIBUTION_ANY);
+      assert inputs.isEmpty();
+      return this;
+    }
+  }
+
+  /** Rel with any distribution and none convention. */
+  private static class NoneSingleRel extends TestSingleRel {
+    NoneSingleRel(RelOptCluster cluster, RelNode input) {
+      super(cluster, cluster.traitSetOf(Convention.NONE), input);
+    }
+
+    @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+      assert traitSet.comprises(Convention.NONE, SIMPLE_DISTRIBUTION_ANY);
+      return new NoneSingleRel(getCluster(), sole(inputs));
+    }
+  }
+}
+
+// End TraitConversionTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
index aea52f6..041eb2c 100644
--- a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
@@ -20,28 +20,17 @@ import org.apache.calcite.adapter.enumerable.EnumerableConvention;
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.ConventionTraitDef;
 import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptListener;
-import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.AbstractRelNode;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.RelWriter;
-import org.apache.calcite.rel.SingleRel;
 import org.apache.calcite.rel.convert.ConverterImpl;
 import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.rel.logical.LogicalProject;
-import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.rules.ProjectRemoveRule;
-import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.rel.type.RelDataTypeSystem;
-import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexInputRef;
-import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
 import org.apache.calcite.util.Util;
 
 import com.google.common.collect.ImmutableList;
@@ -54,7 +43,18 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
+import static org.apache.calcite.plan.volcano.PlannerTests.GoodSingleRule;
+import static org.apache.calcite.plan.volcano.PlannerTests.NoneLeafRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.NoneSingleRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.PHYS_CALLING_CONVENTION;
+import static org.apache.calcite.plan.volcano.PlannerTests.PhysLeafRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.PhysLeafRule;
+import static org.apache.calcite.plan.volcano.PlannerTests.PhysSingleRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.TestSingleRel;
+import static org.apache.calcite.plan.volcano.PlannerTests.newCluster;
+
 import static org.hamcrest.CoreMatchers.equalTo;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThat;
@@ -64,30 +64,11 @@ import static org.junit.Assert.assertTrue;
  * Unit test for {@link VolcanoPlanner the optimizer}.
  */
 public class VolcanoPlannerTest {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /**
-   * Private calling convention representing a physical implementation.
-   */
-  private static final Convention PHYS_CALLING_CONVENTION =
-      new Convention.Impl(
-          "PHYS",
-          RelNode.class);
-
-  //~ Constructors -----------------------------------------------------------
 
   public VolcanoPlannerTest() {
   }
 
   //~ Methods ----------------------------------------------------------------
-
-  static RelOptCluster newCluster(VolcanoPlanner planner) {
-    final RelDataTypeFactory typeFactory =
-        new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
-    return RelOptCluster.create(planner,
-        new RexBuilder(typeFactory));
-  }
-
   /**
    * Tests transformation of a leaf from NONE to PHYS.
    */
@@ -311,7 +292,7 @@ public class VolcanoPlannerTest {
     PhysLeafRel resultLeaf = (PhysLeafRel) result;
     assertEquals(
         "c",
-        resultLeaf.getLabel());
+        resultLeaf.label);
   }
 
   /**
@@ -347,7 +328,7 @@ public class VolcanoPlannerTest {
     PhysLeafRel resultLeaf = (PhysLeafRel) result;
     assertEquals(
         "c",
-        resultLeaf.getLabel());
+        resultLeaf.label);
   }
 
   /**
@@ -479,149 +460,6 @@ public class VolcanoPlannerTest {
 
   //~ Inner Classes ----------------------------------------------------------
 
-  /** Leaf relational expression. */
-  private abstract static class TestLeafRel extends AbstractRelNode {
-    private String label;
-
-    protected TestLeafRel(
-        RelOptCluster cluster,
-        RelTraitSet traits,
-        String label) {
-      super(cluster, traits);
-      this.label = label;
-    }
-
-    public String getLabel() {
-      return label;
-    }
-
-    // implement RelNode
-    public RelOptCost computeSelfCost(RelOptPlanner planner,
-        RelMetadataQuery mq) {
-      return planner.getCostFactory().makeInfiniteCost();
-    }
-
-    // implement RelNode
-    protected RelDataType deriveRowType() {
-      final RelDataTypeFactory typeFactory = getCluster().getTypeFactory();
-      return typeFactory.builder()
-          .add("this", typeFactory.createJavaType(Void.TYPE))
-          .build();
-    }
-
-    public RelWriter explainTerms(RelWriter pw) {
-      return super.explainTerms(pw)
-          .item("label", label);
-    }
-  }
-
-  /** Relational expression with one input. */
-  private abstract static class TestSingleRel extends SingleRel {
-    protected TestSingleRel(
-        RelOptCluster cluster,
-        RelTraitSet traits,
-        RelNode child) {
-      super(cluster, traits, child);
-    }
-
-    // implement RelNode
-    public RelOptCost computeSelfCost(RelOptPlanner planner,
-        RelMetadataQuery mq) {
-      return planner.getCostFactory().makeInfiniteCost();
-    }
-
-    // implement RelNode
-    protected RelDataType deriveRowType() {
-      return getInput().getRowType();
-    }
-  }
-
-  /** Relational expression with one input and convention NONE. */
-  private static class NoneSingleRel extends TestSingleRel {
-    protected NoneSingleRel(
-        RelOptCluster cluster,
-        RelNode child) {
-      super(
-          cluster,
-          cluster.traitSetOf(Convention.NONE),
-          child);
-    }
-
-    public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
-      assert traitSet.comprises(Convention.NONE);
-      return new NoneSingleRel(
-          getCluster(),
-          sole(inputs));
-    }
-  }
-
-  /** Relational expression with zero inputs and convention NONE. */
-  private static class NoneLeafRel extends TestLeafRel {
-    protected NoneLeafRel(
-        RelOptCluster cluster,
-        String label) {
-      super(
-          cluster,
-          cluster.traitSetOf(Convention.NONE),
-          label);
-    }
-
-    public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
-      assert traitSet.comprises(Convention.NONE);
-      assert inputs.isEmpty();
-      return this;
-    }
-  }
-
-  /** Relational expression with zero inputs and convention PHYS. */
-  private static class PhysLeafRel extends TestLeafRel {
-    PhysLeafRel(
-        RelOptCluster cluster,
-        String label) {
-      super(
-          cluster,
-          cluster.traitSetOf(PHYS_CALLING_CONVENTION),
-          label);
-    }
-
-    // implement RelNode
-    public RelOptCost computeSelfCost(RelOptPlanner planner,
-        RelMetadataQuery mq) {
-      return planner.getCostFactory().makeTinyCost();
-    }
-
-    public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
-      assert traitSet.comprises(PHYS_CALLING_CONVENTION);
-      assert inputs.isEmpty();
-      return this;
-    }
-  }
-
-  /** Relational expression with one input and convention PHYS. */
-  private static class PhysSingleRel extends TestSingleRel {
-    PhysSingleRel(
-        RelOptCluster cluster,
-        RelNode child) {
-      super(
-          cluster,
-          cluster.traitSetOf(PHYS_CALLING_CONVENTION),
-          child);
-    }
-
-    // implement RelNode
-    public RelOptCost computeSelfCost(RelOptPlanner planner,
-        RelMetadataQuery mq) {
-      return planner.getCostFactory().makeTinyCost();
-    }
-
-    public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
-      assert traitSet.comprises(PHYS_CALLING_CONVENTION);
-      return new PhysSingleRel(
-          getCluster(),
-          sole(inputs));
-    }
-  }
-
   /** Converter from PHYS to ENUMERABLE convention. */
   class PhysToIteratorConverter extends ConverterImpl {
     public PhysToIteratorConverter(
@@ -642,54 +480,6 @@ public class VolcanoPlannerTest {
     }
   }
 
-  /** Planner rule that converts {@link NoneLeafRel} to PHYS
-   * convention. */
-  private static class PhysLeafRule extends RelOptRule {
-    PhysLeafRule() {
-      super(operand(NoneLeafRel.class, any()));
-    }
-
-    // implement RelOptRule
-    public Convention getOutConvention() {
-      return PHYS_CALLING_CONVENTION;
-    }
-
-    // implement RelOptRule
-    public void onMatch(RelOptRuleCall call) {
-      NoneLeafRel leafRel = call.rel(0);
-      call.transformTo(
-          new PhysLeafRel(
-              leafRel.getCluster(),
-              leafRel.getLabel()));
-    }
-  }
-
-  /** Planner rule that matches a {@link NoneSingleRel} and succeeds. */
-  private static class GoodSingleRule extends RelOptRule {
-    GoodSingleRule() {
-      super(operand(NoneSingleRel.class, any()));
-    }
-
-    // implement RelOptRule
-    public Convention getOutConvention() {
-      return PHYS_CALLING_CONVENTION;
-    }
-
-    // implement RelOptRule
-    public void onMatch(RelOptRuleCall call) {
-      NoneSingleRel singleRel = call.rel(0);
-      RelNode childRel = singleRel.getInput();
-      RelNode physInput =
-          convert(
-              childRel,
-              singleRel.getTraitSet().replace(PHYS_CALLING_CONVENTION));
-      call.transformTo(
-          new PhysSingleRel(
-              singleRel.getCluster(),
-              physInput));
-    }
-  }
-
   /** Rule that matches a {@link RelSubset}. */
   private static class SubsetRule extends RelOptRule {
     private final List<String> buf;

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTraitTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTraitTest.java b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTraitTest.java
index c8530a2..78a0b7c 100644
--- a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTraitTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTraitTest.java
@@ -49,6 +49,8 @@ import org.junit.Test;
 
 import java.util.List;
 
+import static org.apache.calcite.plan.volcano.PlannerTests.newCluster;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -119,7 +121,7 @@ public class VolcanoPlannerTraitTest {
     planner.addRule(new PhysLeafRule());
     planner.addRule(new IterSingleRule());
 
-    RelOptCluster cluster = VolcanoPlannerTest.newCluster(planner);
+    RelOptCluster cluster = newCluster(planner);
 
     NoneLeafRel noneLeafRel =
         RelOptUtil.addTrait(
@@ -170,7 +172,7 @@ public class VolcanoPlannerTraitTest {
     planner.addRule(new IterSingleRule());
     planner.addRule(new IterSinglePhysMergeRule());
 
-    RelOptCluster cluster = VolcanoPlannerTest.newCluster(planner);
+    RelOptCluster cluster = newCluster(planner);
 
     NoneLeafRel noneLeafRel =
         RelOptUtil.addTrait(
@@ -207,7 +209,7 @@ public class VolcanoPlannerTraitTest {
     planner.addRule(new PhysLeafRule());
     planner.addRule(new IterSingleRule2());
 
-    RelOptCluster cluster = VolcanoPlannerTest.newCluster(planner);
+    RelOptCluster cluster = newCluster(planner);
 
     NoneLeafRel noneLeafRel =
         RelOptUtil.addTrait(

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
index c1320d2..988f5cc 100644
--- a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
+++ b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
@@ -21,6 +21,9 @@ import org.apache.calcite.jdbc.CalciteRemoteDriverTest;
 import org.apache.calcite.plan.RelOptPlanReaderTest;
 import org.apache.calcite.plan.RelOptUtilTest;
 import org.apache.calcite.plan.RelWriterTest;
+import org.apache.calcite.plan.volcano.CollationConversionTest;
+import org.apache.calcite.plan.volcano.ComboRuleTest;
+import org.apache.calcite.plan.volcano.TraitConversionTest;
 import org.apache.calcite.plan.volcano.TraitPropagationTest;
 import org.apache.calcite.plan.volcano.VolcanoPlannerTest;
 import org.apache.calcite.plan.volcano.VolcanoPlannerTraitTest;
@@ -125,6 +128,9 @@ import org.junit.runners.Suite;
     EnumerableCorrelateTest.class,
     LookupOperatorOverloadsTest.class,
     LexCaseSensitiveTest.class,
+    CollationConversionTest.class,
+    TraitConversionTest.class,
+    ComboRuleTest.class,
 
     // slow tests (above 1s)
     UdfTest.class,

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
index 8f4e5f3..97da6c3 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
@@ -103,20 +103,20 @@ public class JdbcAdapterTest {
             + "from scott.emp e inner join scott.dept d \n"
             + "on e.deptno = d.deptno")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
-            + "  JdbcProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2], DNAME=[$4])\n"
-            + "    JdbcJoin(condition=[=($2, $3)], joinType=[inner])\n"
-            + "      JdbcProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7])\n"
-            + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
+            + "  JdbcProject(EMPNO=[$2], ENAME=[$3], DEPTNO=[$4], DNAME=[$1])\n"
+            + "    JdbcJoin(condition=[=($4, $0)], joinType=[inner])\n"
             + "      JdbcProject(DEPTNO=[$0], DNAME=[$1])\n"
-            + "        JdbcTableScan(table=[[SCOTT, DEPT]])")
+            + "        JdbcTableScan(table=[[SCOTT, DEPT]])\n"
+            + "      JdbcProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7])\n"
+            + "        JdbcTableScan(table=[[SCOTT, EMP]])")
         .runs()
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.HSQLDB)
-        .planHasSql("SELECT \"t\".\"EMPNO\", \"t\".\"ENAME\", "
-            + "\"t\".\"DEPTNO\", \"t0\".\"DNAME\"\n"
-            + "FROM (SELECT \"EMPNO\", \"ENAME\", \"DEPTNO\"\n"
-            + "FROM \"SCOTT\".\"EMP\") AS \"t\"\n"
-            + "INNER JOIN (SELECT \"DEPTNO\", \"DNAME\"\n"
-            + "FROM \"SCOTT\".\"DEPT\") AS \"t0\" "
+        .planHasSql("SELECT \"t0\".\"EMPNO\", \"t0\".\"ENAME\", "
+            + "\"t0\".\"DEPTNO\", \"t\".\"DNAME\"\n"
+            + "FROM (SELECT \"DEPTNO\", \"DNAME\"\n"
+            + "FROM \"SCOTT\".\"DEPT\") AS \"t\"\n"
+            + "INNER JOIN (SELECT \"EMPNO\", \"ENAME\", \"DEPTNO\"\n"
+            + "FROM \"SCOTT\".\"EMP\") AS \"t0\" "
             + "ON \"t\".\"DEPTNO\" = \"t0\".\"DEPTNO\"");
   }
 
@@ -129,17 +129,19 @@ public class JdbcAdapterTest {
             + "from scott.emp e inner join scott.salgrade s \n"
             + "on e.sal > s.losal and e.sal < s.hisal")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
-            + "  JdbcProject(EMPNO=[$0], ENAME=[$1], GRADE=[$3])\n"
-            + "    JdbcJoin(condition=[AND(>($2, $4), <($2, $5))], joinType=[inner])\n"
+            + "  JdbcProject(EMPNO=[$3], ENAME=[$4], GRADE=[$0])\n"
+            + "    JdbcJoin(condition=[AND(>($5, $1), <($5, $2))], joinType=[inner])\n"
+            + "      JdbcTableScan(table=[[SCOTT, SALGRADE]])\n"
             + "      JdbcProject(EMPNO=[$0], ENAME=[$1], SAL=[$5])\n"
-            + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
-            + "      JdbcTableScan(table=[[SCOTT, SALGRADE]])")
+            + "        JdbcTableScan(table=[[SCOTT, EMP]])")
         .runs()
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.HSQLDB)
         .planHasSql("SELECT \"t\".\"EMPNO\", \"t\".\"ENAME\", "
-            + "\"SALGRADE\".\"GRADE\"\n"
-            + "FROM (SELECT \"EMPNO\", \"ENAME\", \"SAL\"\nFROM \"SCOTT\".\"EMP\") AS \"t\"\n"
-            + "INNER JOIN \"SCOTT\".\"SALGRADE\" ON \"t\".\"SAL\" > \"SALGRADE\".\"LOSAL\" AND \"t\".\"SAL\" < \"SALGRADE\".\"HISAL\"");
+            + "\"SALGRADE\".\"GRADE\"\nFROM \"SCOTT\".\"SALGRADE\"\n"
+            + "INNER JOIN (SELECT \"EMPNO\", \"ENAME\", \"SAL\"\n"
+            + "FROM \"SCOTT\".\"EMP\") AS \"t\" "
+            + "ON \"SALGRADE\".\"LOSAL\" < \"t\".\"SAL\" "
+            + "AND \"SALGRADE\".\"HISAL\" > \"t\".\"SAL\"");
   }
 
   @Test public void testNonEquiJoinReverseConditionPlan() {
@@ -148,18 +150,18 @@ public class JdbcAdapterTest {
             + "from scott.emp e inner join scott.salgrade s \n"
             + "on s.losal <= e.sal and s.hisal >= e.sal")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
-            + "  JdbcProject(EMPNO=[$0], ENAME=[$1], GRADE=[$3])\n"
-            + "    JdbcJoin(condition=[AND(<=($4, $2), >=($5, $2))], joinType=[inner])\n"
+            + "  JdbcProject(EMPNO=[$3], ENAME=[$4], GRADE=[$0])\n"
+            + "    JdbcJoin(condition=[AND(<=($1, $5), >=($2, $5))], joinType=[inner])\n"
+            + "      JdbcTableScan(table=[[SCOTT, SALGRADE]])\n"
             + "      JdbcProject(EMPNO=[$0], ENAME=[$1], SAL=[$5])\n"
-            + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
-            + "      JdbcTableScan(table=[[SCOTT, SALGRADE]])")
+            + "        JdbcTableScan(table=[[SCOTT, EMP]])")
         .runs()
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.HSQLDB)
         .planHasSql("SELECT \"t\".\"EMPNO\", \"t\".\"ENAME\", "
-            + "\"SALGRADE\".\"GRADE\"\n"
-            + "FROM (SELECT \"EMPNO\", \"ENAME\", \"SAL\"\n"
-            + "FROM \"SCOTT\".\"EMP\") AS \"t\"\n"
-            + "INNER JOIN \"SCOTT\".\"SALGRADE\" ON \"t\".\"SAL\" >= \"SALGRADE\".\"LOSAL\" AND \"t\".\"SAL\" <= \"SALGRADE\".\"HISAL\"");
+            + "\"SALGRADE\".\"GRADE\"\nFROM \"SCOTT\".\"SALGRADE\"\n"
+            + "INNER JOIN (SELECT \"EMPNO\", \"ENAME\", \"SAL\"\n"
+            + "FROM \"SCOTT\".\"EMP\") AS \"t\" "
+            + "ON \"SALGRADE\".\"LOSAL\" <= \"t\".\"SAL\" AND \"SALGRADE\".\"HISAL\" >= \"t\".\"SAL\"");
   }
 
   @Test public void testMixedJoinPlan() {
@@ -168,20 +170,21 @@ public class JdbcAdapterTest {
             + "from scott.emp e inner join scott.emp m on  \n"
             + "e.mgr = m.empno and e.sal > m.sal")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
-            + "  JdbcProject(EMPNO=[$0], ENAME=[$1], EMPNO0=[$0], ENAME0=[$1])\n"
-            + "    JdbcJoin(condition=[AND(=($2, $4), >($3, $5))], joinType=[inner])\n"
-            + "      JdbcProject(EMPNO=[$0], ENAME=[$1], MGR=[$3], SAL=[$5])\n"
-            + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
+            + "  JdbcProject(EMPNO=[$2], ENAME=[$3], EMPNO0=[$2], ENAME0=[$3])\n"
+            + "    JdbcJoin(condition=[AND(=($4, $0), >($5, $1))], joinType=[inner])\n"
             + "      JdbcProject(EMPNO=[$0], SAL=[$5])\n"
+            + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
+            + "      JdbcProject(EMPNO=[$0], ENAME=[$1], MGR=[$3], SAL=[$5])\n"
             + "        JdbcTableScan(table=[[SCOTT, EMP]])")
         .runs()
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.HSQLDB)
-        .planHasSql("SELECT \"t\".\"EMPNO\", \"t\".\"ENAME\", "
-            + "\"t\".\"EMPNO\" AS \"EMPNO0\", \"t\".\"ENAME\" AS \"ENAME0\"\n"
-            + "FROM (SELECT \"EMPNO\", \"ENAME\", \"MGR\", \"SAL\"\n"
+        .planHasSql("SELECT \"t0\".\"EMPNO\", \"t0\".\"ENAME\", "
+            + "\"t0\".\"EMPNO\" AS \"EMPNO0\", \"t0\".\"ENAME\" AS \"ENAME0\"\n"
+            + "FROM (SELECT \"EMPNO\", \"SAL\"\n"
             + "FROM \"SCOTT\".\"EMP\") AS \"t\"\n"
-            + "INNER JOIN (SELECT \"EMPNO\", \"SAL\"\n"
-            + "FROM \"SCOTT\".\"EMP\") AS \"t0\" ON \"t\".\"MGR\" = \"t0\".\"EMPNO\" AND \"t\".\"SAL\" > \"t0\".\"SAL\"");
+            + "INNER JOIN (SELECT \"EMPNO\", \"ENAME\", \"MGR\", \"SAL\"\n"
+            + "FROM \"SCOTT\".\"EMP\") AS \"t0\" "
+            + "ON \"t\".\"EMPNO\" = \"t0\".\"MGR\" AND \"t\".\"SAL\" < \"t0\".\"SAL\"");
   }
 
   @Test public void testMixedJoinWithOrPlan() {
@@ -190,20 +193,22 @@ public class JdbcAdapterTest {
             + "from scott.emp e inner join scott.emp m on  \n"
             + "e.mgr = m.empno and (e.sal > m.sal or m.hiredate > e.hiredate)")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
-            + "  JdbcProject(EMPNO=[$0], ENAME=[$1], EMPNO0=[$0], ENAME0=[$1])\n"
-            + "    JdbcJoin(condition=[AND(=($2, $5), OR(>($4, $7), >($6, $3)))], joinType=[inner])\n"
-            + "      JdbcProject(EMPNO=[$0], ENAME=[$1], MGR=[$3], HIREDATE=[$4], SAL=[$5])\n"
-            + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
+            + "  JdbcProject(EMPNO=[$3], ENAME=[$4], EMPNO0=[$3], ENAME0=[$4])\n"
+            + "    JdbcJoin(condition=[AND(=($5, $0), OR(>($7, $2), >($1, $6)))], joinType=[inner])\n"
             + "      JdbcProject(EMPNO=[$0], HIREDATE=[$4], SAL=[$5])\n"
+            + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
+            + "      JdbcProject(EMPNO=[$0], ENAME=[$1], MGR=[$3], HIREDATE=[$4], SAL=[$5])\n"
             + "        JdbcTableScan(table=[[SCOTT, EMP]])")
         .runs()
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.HSQLDB)
-        .planHasSql("SELECT \"t\".\"EMPNO\", \"t\".\"ENAME\", "
-            + "\"t\".\"EMPNO\" AS \"EMPNO0\", \"t\".\"ENAME\" AS \"ENAME0\"\n"
-            + "FROM (SELECT \"EMPNO\", \"ENAME\", \"MGR\", \"HIREDATE\", \"SAL\"\n"
+        .planHasSql("SELECT \"t0\".\"EMPNO\", \"t0\".\"ENAME\", "
+            + "\"t0\".\"EMPNO\" AS \"EMPNO0\", \"t0\".\"ENAME\" AS \"ENAME0\"\n"
+            + "FROM (SELECT \"EMPNO\", \"HIREDATE\", \"SAL\"\n"
             + "FROM \"SCOTT\".\"EMP\") AS \"t\"\n"
-            + "INNER JOIN (SELECT \"EMPNO\", \"HIREDATE\", \"SAL\"\n"
-            + "FROM \"SCOTT\".\"EMP\") AS \"t0\" ON \"t\".\"MGR\" = \"t0\".\"EMPNO\" AND (\"t\".\"SAL\" > \"t0\".\"SAL\" OR \"t\".\"HIREDATE\" < \"t0\".\"HIREDATE\")");
+            + "INNER JOIN (SELECT \"EMPNO\", \"ENAME\", \"MGR\", \"HIREDATE\", \"SAL\"\n"
+            + "FROM \"SCOTT\".\"EMP\") AS \"t0\" "
+            + "ON \"t\".\"EMPNO\" = \"t0\".\"MGR\" "
+            + "AND (\"t\".\"SAL\" < \"t0\".\"SAL\" OR \"t\".\"HIREDATE\" > \"t0\".\"HIREDATE\")");
   }
 
   @Test public void testJoin3TablesPlan() {
@@ -240,20 +245,19 @@ public class JdbcAdapterTest {
             + "from scott.emp e,scott.dept d \n"
             + "where e.deptno = d.deptno")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
-            + "  JdbcProject(EMPNO=[$2], ENAME=[$3], DEPTNO=[$0], DNAME=[$1])\n"
-            + "    JdbcJoin(condition=[=($4, $0)], joinType=[inner])\n"
-            + "      JdbcProject(DEPTNO=[$0], DNAME=[$1])\n"
-            + "        JdbcTableScan(table=[[SCOTT, DEPT]])\n"
+            + "  JdbcProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$3], DNAME=[$4])\n"
+            + "    JdbcJoin(condition=[=($2, $3)], joinType=[inner])\n"
             + "      JdbcProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7])\n"
-            + "        JdbcTableScan(table=[[SCOTT, EMP]])")
+            + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
+            + "      JdbcProject(DEPTNO=[$0], DNAME=[$1])\n"
+            + "        JdbcTableScan(table=[[SCOTT, DEPT]])")
         .runs()
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.HSQLDB)
-        .planHasSql("SELECT \"t0\".\"EMPNO\", \"t0\".\"ENAME\", "
-            + "\"t\".\"DEPTNO\", \"t\".\"DNAME\"\n"
-            + "FROM (SELECT \"DEPTNO\", \"DNAME\"\n"
-            + "FROM \"SCOTT\".\"DEPT\") AS \"t\"\n"
-            + "INNER JOIN (SELECT \"EMPNO\", \"ENAME\", \"DEPTNO\"\n"
-            + "FROM \"SCOTT\".\"EMP\") AS \"t0\" ON \"t\".\"DEPTNO\" = \"t0\".\"DEPTNO\"");
+        .planHasSql("SELECT \"t\".\"EMPNO\", \"t\".\"ENAME\", "
+            + "\"t0\".\"DEPTNO\", \"t0\".\"DNAME\"\n"
+            + "FROM (SELECT \"EMPNO\", \"ENAME\", \"DEPTNO\"\nFROM \"SCOTT\".\"EMP\") AS \"t\"\n"
+            + "INNER JOIN (SELECT \"DEPTNO\", \"DNAME\"\n"
+            + "FROM \"SCOTT\".\"DEPT\") AS \"t0\" ON \"t\".\"DEPTNO\" = \"t0\".\"DEPTNO\"");
   }
 
   // JdbcJoin not used for this
@@ -280,22 +284,22 @@ public class JdbcAdapterTest {
             + "where e.deptno = d.deptno \n"
             + "and e.deptno=20")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
-            + "  JdbcProject(EMPNO=[$2], ENAME=[$3], DEPTNO=[$0], DNAME=[$1])\n"
-            + "    JdbcJoin(condition=[=($4, $0)], joinType=[inner])\n"
-            + "      JdbcProject(DEPTNO=[$0], DNAME=[$1])\n"
-            + "        JdbcTableScan(table=[[SCOTT, DEPT]])\n"
+            + "  JdbcProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$3], DNAME=[$4])\n"
+            + "    JdbcJoin(condition=[=($2, $3)], joinType=[inner])\n"
             + "      JdbcProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7])\n"
             + "        JdbcFilter(condition=[=(CAST($7):INTEGER, 20)])\n"
-            + "          JdbcTableScan(table=[[SCOTT, EMP]])")
+            + "          JdbcTableScan(table=[[SCOTT, EMP]])\n"
+            + "      JdbcProject(DEPTNO=[$0], DNAME=[$1])\n"
+            + "        JdbcTableScan(table=[[SCOTT, DEPT]])")
         .runs()
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.HSQLDB)
-        .planHasSql("SELECT \"t1\".\"EMPNO\", \"t1\".\"ENAME\", "
-            + "\"t\".\"DEPTNO\", \"t\".\"DNAME\"\n"
-            + "FROM (SELECT \"DEPTNO\", \"DNAME\"\n"
-            + "FROM \"SCOTT\".\"DEPT\") AS \"t\"\n"
-            + "INNER JOIN (SELECT \"EMPNO\", \"ENAME\", \"DEPTNO\"\n"
+        .planHasSql("SELECT \"t0\".\"EMPNO\", \"t0\".\"ENAME\", "
+            + "\"t1\".\"DEPTNO\", \"t1\".\"DNAME\"\n"
+            + "FROM (SELECT \"EMPNO\", \"ENAME\", \"DEPTNO\"\n"
             + "FROM \"SCOTT\".\"EMP\"\n"
-            + "WHERE CAST(\"DEPTNO\" AS INTEGER) = 20) AS \"t1\" ON \"t\".\"DEPTNO\" = \"t1\".\"DEPTNO\"");
+            + "WHERE CAST(\"DEPTNO\" AS INTEGER) = 20) AS \"t0\"\n"
+            + "INNER JOIN (SELECT \"DEPTNO\", \"DNAME\"\n"
+            + "FROM \"SCOTT\".\"DEPT\") AS \"t1\" ON \"t0\".\"DEPTNO\" = \"t1\".\"DEPTNO\"");
   }
 
   /** Test case for

http://git-wip-us.apache.org/repos/asf/calcite/blob/a3bc0d8e/core/src/test/java/org/apache/calcite/test/JdbcTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 172e66a..2c0b513 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -1941,6 +1941,7 @@ public class JdbcTest {
     //   12    36
     //   13   116 - OOM did not complete
     checkJoinNWay(1);
+    checkJoinNWay(3);
     checkJoinNWay(6);
   }
 


Mime
View raw message