You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2014/07/28 20:56:27 UTC

[3/7] git commit: Refactor test infrastructure to allow testing against heuristic bushy-join optimizer.

Refactor test infrastructure to allow testing against heuristic bushy-join optimizer.

Add enum OptiqAssert.SchemaSpec, to allow more uniform use of various schemas in the test suite.


Project: http://git-wip-us.apache.org/repos/asf/incubator-optiq/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-optiq/commit/929f5310
Tree: http://git-wip-us.apache.org/repos/asf/incubator-optiq/tree/929f5310
Diff: http://git-wip-us.apache.org/repos/asf/incubator-optiq/diff/929f5310

Branch: refs/heads/master
Commit: 929f5310ad53b69a8917105501ad651656e09187
Parents: a461539
Author: Julian Hyde <jh...@apache.org>
Authored: Mon Jul 21 16:14:16 2014 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Mon Jul 28 11:12:52 2014 -0700

----------------------------------------------------------------------
 .../net/hydromatic/optiq/tools/Programs.java    |   7 +-
 .../org/eigenbase/rel/rules/LoptJoinTree.java   | 104 +++++++++++--------
 .../rel/rules/LoptOptimizeJoinRule.java         |  24 ++---
 .../rel/rules/OptimizeBushyJoinRule.java        |  58 +++++++++++
 .../net/hydromatic/optiq/test/OptiqAssert.java  | 104 ++++++++++++-------
 .../net/hydromatic/optiq/tools/PlannerTest.java |  72 ++++++++++---
 6 files changed, 253 insertions(+), 116 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/929f5310/core/src/main/java/net/hydromatic/optiq/tools/Programs.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/tools/Programs.java b/core/src/main/java/net/hydromatic/optiq/tools/Programs.java
index 53a6ac6..83c73cd 100644
--- a/core/src/main/java/net/hydromatic/optiq/tools/Programs.java
+++ b/core/src/main/java/net/hydromatic/optiq/tools/Programs.java
@@ -113,7 +113,8 @@ public class Programs {
    * {@link org.eigenbase.rel.rules.MultiJoinRel} and
    * {@link org.eigenbase.rel.rules.LoptOptimizeJoinRule})
    * if there are 6 or more joins (7 or more relations). */
-  public static Program heuristicJoinOrder(final Collection<RelOptRule> rules) {
+  public static Program heuristicJoinOrder(final Collection<RelOptRule> rules,
+      final boolean bushy) {
     return new Program() {
       public RelNode run(RelOptPlanner planner, RelNode rel,
           RelTraitSet requiredOutputTraits) {
@@ -140,7 +141,9 @@ public class Programs {
                   CommutativeJoinRule.INSTANCE,
                   PushJoinThroughJoinRule.LEFT,
                   PushJoinThroughJoinRule.RIGHT));
-          list.add(LoptOptimizeJoinRule.INSTANCE);
+          list.add(bushy
+              ? OptimizeBushyJoinRule.INSTANCE
+              : LoptOptimizeJoinRule.INSTANCE);
           final Program program2 = ofRules(list);
 
           program = sequence(program1, program2);

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/929f5310/core/src/main/java/org/eigenbase/rel/rules/LoptJoinTree.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/rel/rules/LoptJoinTree.java b/core/src/main/java/org/eigenbase/rel/rules/LoptJoinTree.java
index a274ab9..0e32b0e 100644
--- a/core/src/main/java/org/eigenbase/rel/rules/LoptJoinTree.java
+++ b/core/src/main/java/org/eigenbase/rel/rules/LoptJoinTree.java
@@ -21,6 +21,8 @@ import java.util.*;
 
 import org.eigenbase.rel.*;
 
+import com.google.common.base.Preconditions;
+
 /**
  * Utility class used to store a {@link JoinRelBase} tree and the factors that
  * make up the tree.
@@ -34,26 +36,26 @@ import org.eigenbase.rel.*;
 public class LoptJoinTree {
   //~ Instance fields --------------------------------------------------------
 
-  private BinaryTree factorTree;
-  private RelNode joinTree;
-  private boolean removableSelfJoin;
+  private final BinaryTree factorTree;
+  private final RelNode joinTree;
+  private final boolean removableSelfJoin;
 
   //~ Constructors -----------------------------------------------------------
 
   /**
-   * Creates a jointree consisting of a single node
+   * Creates a join-tree consisting of a single node.
    *
    * @param joinTree RelNode corresponding to the single node
    * @param factorId factor id of the node
    */
   public LoptJoinTree(RelNode joinTree, int factorId) {
     this.joinTree = joinTree;
-    factorTree = new BinaryTree(factorId, this);
+    this.factorTree = new Leaf(factorId, this);
     this.removableSelfJoin = false;
   }
 
   /**
-   * Associates the factor ids with a jointree
+   * Associates the factor ids with a join-tree.
    *
    * @param joinTree RelNodes corresponding to the join tree
    * @param factorTree tree of the factor ids
@@ -70,8 +72,8 @@ public class LoptJoinTree {
   }
 
   /**
-   * Associates the factor ids with a jointree given the factors corresponding
-   * to the left and right subtrees of the join
+   * Associates the factor ids with a join-tree given the factors corresponding
+   * to the left and right subtrees of the join.
    *
    * @param joinTree RelNodes corresponding to the join tree
    * @param leftFactorTree tree of the factor ids for left subtree
@@ -85,7 +87,7 @@ public class LoptJoinTree {
   }
 
   /**
-   * Associates the factor ids with a jointree given the factors corresponding
+   * Associates the factor ids with a join-tree given the factors corresponding
    * to the left and right subtrees of the join. Also indicates whether the
    * join is a removable self-join.
    *
@@ -99,7 +101,7 @@ public class LoptJoinTree {
       BinaryTree leftFactorTree,
       BinaryTree rightFactorTree,
       boolean removableSelfJoin) {
-    factorTree = new BinaryTree(leftFactorTree, rightFactorTree, this);
+    factorTree = new Node(leftFactorTree, rightFactorTree, this);
     this.joinTree = joinTree;
     this.removableSelfJoin = removableSelfJoin;
   }
@@ -111,17 +113,19 @@ public class LoptJoinTree {
   }
 
   public LoptJoinTree getLeft() {
+    final Node node = (Node) factorTree;
     return new LoptJoinTree(
         ((JoinRelBase) joinTree).getLeft(),
-        factorTree.getLeft(),
-        factorTree.getLeft().getParent().isRemovableSelfJoin());
+        node.getLeft(),
+        node.getLeft().getParent().isRemovableSelfJoin());
   }
 
   public LoptJoinTree getRight() {
+    final Node node = (Node) factorTree;
     return new LoptJoinTree(
         ((JoinRelBase) joinTree).getRight(),
-        factorTree.getRight(),
-        factorTree.getRight().getParent().isRemovableSelfJoin());
+        node.getRight(),
+        node.getRight().getParent().isRemovableSelfJoin());
   }
 
   public BinaryTree getFactorTree() {
@@ -142,55 +146,63 @@ public class LoptJoinTree {
    * Simple binary tree class that stores an id in the leaf nodes and keeps
    * track of the parent LoptJoinTree object associated with the binary tree.
    */
-  protected class BinaryTree {
-    private int id;
-    private BinaryTree left;
-    private BinaryTree right;
-    private LoptJoinTree parent;
+  protected abstract static class BinaryTree {
+    private final LoptJoinTree parent;
 
-    public BinaryTree(int rootId, LoptJoinTree parent) {
-      this.id = rootId;
-      this.left = null;
-      this.right = null;
-      this.parent = parent;
+    protected BinaryTree(LoptJoinTree parent) {
+      this.parent = Preconditions.checkNotNull(parent);
     }
 
-    public BinaryTree(
-        BinaryTree left,
-        BinaryTree right,
-        LoptJoinTree parent) {
-      this.left = left;
-      this.right = right;
-      this.parent = parent;
+    public LoptJoinTree getParent() {
+      return parent;
     }
 
-    public BinaryTree getLeft() {
-      return left;
-    }
+    public abstract void getTreeOrder(List<Integer> treeOrder);
+  }
 
-    public BinaryTree getRight() {
-      return right;
-    }
+  /** Binary tree node that has no children. */
+  protected static class Leaf extends BinaryTree {
+    private final int id;
 
-    public LoptJoinTree getParent() {
-      return parent;
+    public Leaf(int rootId, LoptJoinTree parent) {
+      super(parent);
+      this.id = rootId;
     }
 
     /**
      * @return the id associated with a leaf node in a binary tree
      */
     public int getId() {
-      assert left == null && right == null;
       return id;
     }
 
     public void getTreeOrder(List<Integer> treeOrder) {
-      if ((left == null) || (right == null)) {
-        treeOrder.add(id);
-      } else {
-        left.getTreeOrder(treeOrder);
-        right.getTreeOrder(treeOrder);
-      }
+      treeOrder.add(id);
+    }
+  }
+
+  /** Binary tree node that has two children. */
+  protected static class Node extends BinaryTree {
+    private BinaryTree left;
+    private BinaryTree right;
+
+    public Node(BinaryTree left, BinaryTree right, LoptJoinTree parent) {
+      super(parent);
+      this.left = Preconditions.checkNotNull(left);
+      this.right = Preconditions.checkNotNull(right);
+    }
+
+    public BinaryTree getLeft() {
+      return left;
+    }
+
+    public BinaryTree getRight() {
+      return right;
+    }
+
+    public void getTreeOrder(List<Integer> treeOrder) {
+      left.getTreeOrder(treeOrder);
+      right.getTreeOrder(treeOrder);
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/929f5310/core/src/main/java/org/eigenbase/rel/rules/LoptOptimizeJoinRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/rel/rules/LoptOptimizeJoinRule.java b/core/src/main/java/org/eigenbase/rel/rules/LoptOptimizeJoinRule.java
index 9e3d122..e758ee1 100644
--- a/core/src/main/java/org/eigenbase/rel/rules/LoptOptimizeJoinRule.java
+++ b/core/src/main/java/org/eigenbase/rel/rules/LoptOptimizeJoinRule.java
@@ -55,13 +55,13 @@ public class LoptOptimizeJoinRule extends RelOptRule {
 
   // implement RelOptRule
   public void onMatch(RelOptRuleCall call) {
-    MultiJoinRel multiJoinRel = (MultiJoinRel) call.rels[0];
-    LoptMultiJoin multiJoin = new LoptMultiJoin(multiJoinRel);
+    final MultiJoinRel multiJoinRel = call.rel(0);
+    final LoptMultiJoin multiJoin = new LoptMultiJoin(multiJoinRel);
 
     findRemovableOuterJoins(multiJoin);
 
-    RexBuilder rexBuilder = multiJoinRel.getCluster().getRexBuilder();
-    LoptSemiJoinOptimizer semiJoinOpt =
+    final RexBuilder rexBuilder = multiJoinRel.getCluster().getRexBuilder();
+    final LoptSemiJoinOptimizer semiJoinOpt =
         new LoptSemiJoinOptimizer(multiJoin, rexBuilder);
 
     // determine all possible semijoins
@@ -745,13 +745,10 @@ public class LoptOptimizeJoinRule extends RelOptRule {
 
       // can't add a null-generating factor if its dependent,
       // non-null generating factors haven't been added yet
-      if (multiJoin.isNullGenerating(factor)) {
-        BitSet tmp =
-            (BitSet) multiJoin.getOuterJoinFactors(factor).clone();
-        tmp.andNot(factorsAdded);
-        if (tmp.cardinality() != 0) {
-          continue;
-        }
+      if (multiJoin.isNullGenerating(factor)
+          && !BitSets.contains(factorsAdded,
+              multiJoin.getOuterJoinFactors(factor))) {
+        continue;
       }
 
       // determine the best weight between the current factor
@@ -1838,12 +1835,11 @@ public class LoptOptimizeJoinRule extends RelOptRule {
 
     if (selfJoin) {
       return !multiJoin.isLeftFactorInRemovableSelfJoin(
-          left.getFactorTree().getId());
+          ((LoptJoinTree.Leaf) left.getFactorTree()).getId());
     }
 
     Double leftRowCount = RelMetadataQuery.getRowCount(left.getJoinTree());
-    Double rightRowCount =
-        RelMetadataQuery.getRowCount(right.getJoinTree());
+    Double rightRowCount = RelMetadataQuery.getRowCount(right.getJoinTree());
 
     // The left side is smaller than the right if it has fewer rows,
     // or if it has the same number of rows as the right (excluding

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/929f5310/core/src/main/java/org/eigenbase/rel/rules/OptimizeBushyJoinRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/rel/rules/OptimizeBushyJoinRule.java b/core/src/main/java/org/eigenbase/rel/rules/OptimizeBushyJoinRule.java
new file mode 100644
index 0000000..297ce9c
--- /dev/null
+++ b/core/src/main/java/org/eigenbase/rel/rules/OptimizeBushyJoinRule.java
@@ -0,0 +1,58 @@
+/*
+// Licensed to Julian Hyde under one or more contributor license
+// agreements. See the NOTICE file distributed with this work for
+// additional information regarding copyright ownership.
+//
+// Julian Hyde 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.eigenbase.rel.rules;
+
+import java.util.*;
+
+import org.eigenbase.rel.*;
+import org.eigenbase.relopt.*;
+import org.eigenbase.rex.*;
+import org.eigenbase.util.Util;
+
+/**
+ * Planner rule that finds an approximately optimal ordering for join operators
+ * using a heuristic algorithm.
+ *
+ * <p>It is triggered by the pattern {@link ProjectRel} ({@link MultiJoinRel}).
+ *
+ * <p>It is similar to {@link org.eigenbase.rel.rules.LoptOptimizeJoinRule}.
+ * {@code LoptOptimizeJoinRule} is only capable of producing left-deep joins;
+ * this rule is capable of producing bushy joins.
+ */
+public class OptimizeBushyJoinRule extends RelOptRule {
+  public static final OptimizeBushyJoinRule INSTANCE =
+      new OptimizeBushyJoinRule(RelFactories.DEFAULT_JOIN_FACTORY);
+
+  private final RelFactories.JoinFactory joinFactory;
+
+  /** Creates an OptimizeBushyJoinRule. */
+  public OptimizeBushyJoinRule(RelFactories.JoinFactory joinFactory) {
+    super(operand(MultiJoinRel.class, any()));
+    this.joinFactory = joinFactory;
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    final MultiJoinRel multiJoinRel = call.rel(0);
+    final LoptMultiJoin multiJoin = new LoptMultiJoin(multiJoinRel);
+
+    final RexBuilder rexBuilder = multiJoinRel.getCluster().getRexBuilder();
+    Util.discard(multiJoin);
+  }
+}
+
+// End OptimizeBushyJoinRule.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/929f5310/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java b/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java
index 0a713c1..e23c6b5 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java
@@ -518,32 +518,37 @@ public class OptiqAssert {
     throw new AssertionError("method " + methodName + " not found");
   }
 
-  static OptiqConnection getConnection(String... schema)
-    throws ClassNotFoundException, SQLException {
-    final List<String> schemaList = Arrays.asList(schema);
-    Class.forName("net.hydromatic.optiq.jdbc.Driver");
-    String suffix = schemaList.contains("spark") ? "spark=true" : "";
-    Connection connection =
-        DriverManager.getConnection("jdbc:optiq:" + suffix);
-    OptiqConnection optiqConnection =
-        connection.unwrap(OptiqConnection.class);
-    SchemaPlus rootSchema = optiqConnection.getRootSchema();
-    if (schemaList.contains("hr")) {
-      rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
-    }
-    if (schemaList.contains("foodmart")) {
-      rootSchema.add("foodmart",
+  public static SchemaPlus addSchema(SchemaPlus rootSchema, SchemaSpec schema) {
+    switch (schema) {
+    case REFLECTIVE_FOODMART:
+      return rootSchema.add("foodmart",
           new ReflectiveSchema(new JdbcTest.FoodmartSchema()));
-    }
-    if (schemaList.contains("lingual")) {
-      rootSchema.add("SALES",
+    case JDBC_FOODMART:
+      final DataSource dataSource =
+          JdbcSchema.dataSource(
+              CONNECTION_SPEC.url,
+              CONNECTION_SPEC.driver,
+              CONNECTION_SPEC.username,
+              CONNECTION_SPEC.password);
+      return rootSchema.add("foodmart",
+          JdbcSchema.create(rootSchema, "foodmart", dataSource, null,
+              "foodmart"));
+    case CLONE_FOODMART:
+      SchemaPlus foodmart = rootSchema.getSubSchema("foodmart");
+      if (foodmart == null) {
+        foodmart = OptiqAssert.addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
+      }
+      return rootSchema.add("foodmart2", new CloneSchema(foodmart));
+    case HR:
+      return rootSchema.add("hr",
+          new ReflectiveSchema(new JdbcTest.HrSchema()));
+    case LINGUAL:
+      return rootSchema.add("SALES",
           new ReflectiveSchema(new JdbcTest.LingualSchema()));
-    }
-    if (schemaList.contains("post")) {
+    case POST:
       final SchemaPlus post = rootSchema.add("POST", new AbstractSchema());
       post.add("EMP",
-          ViewTable.viewMacro(
-              post,
+          ViewTable.viewMacro(post,
               "select * from (values\n"
               + "    ('Jane', 10, 'F'),\n"
               + "    ('Bob', 10, 'M'),\n"
@@ -557,8 +562,7 @@ public class OptiqAssert {
               + "  as t(ename, deptno, gender)",
               ImmutableList.<String>of()));
       post.add("DEPT",
-          ViewTable.viewMacro(
-              post,
+          ViewTable.viewMacro(post,
               "select * from (values\n"
               + "    (10, 'Sales'),\n"
               + "    (20, 'Marketing'),\n"
@@ -566,8 +570,7 @@ public class OptiqAssert {
               + "    (40, 'Empty')) as t(deptno, dname)",
               ImmutableList.<String>of()));
       post.add("EMPS",
-          ViewTable.viewMacro(
-              post,
+          ViewTable.viewMacro(post,
               "select * from (values\n"
               + "    (100, 'Fred',  10, CAST(NULL AS CHAR(1)), CAST(NULL AS VARCHAR(20)), 40,               25, TRUE,    FALSE, DATE '1996-08-03'),\n"
               + "    (110, 'Eric',  20, 'M',                   'San Francisco',           3,                80, UNKNOWN, FALSE, DATE '2001-01-01'),\n"
@@ -576,6 +579,33 @@ public class OptiqAssert {
               + "    (130, 'Alice', 40, 'F',                   'Vancouver',               2, CAST(NULL AS INT), FALSE,   TRUE,  DATE '2007-01-01'))\n"
               + " as t(empno, name, deptno, gender, city, empid, age, slacker, manager, joinedat)",
               ImmutableList.<String>of()));
+      return post;
+    default:
+      throw new AssertionError("unknown schema " + schema);
+    }
+  }
+
+  static OptiqConnection getConnection(String... schema)
+    throws ClassNotFoundException, SQLException {
+    final List<String> schemaList = Arrays.asList(schema);
+    Class.forName("net.hydromatic.optiq.jdbc.Driver");
+    String suffix = schemaList.contains("spark") ? "spark=true" : "";
+    Connection connection =
+        DriverManager.getConnection("jdbc:optiq:" + suffix);
+    OptiqConnection optiqConnection =
+        connection.unwrap(OptiqConnection.class);
+    SchemaPlus rootSchema = optiqConnection.getRootSchema();
+    if (schemaList.contains("hr")) {
+      addSchema(rootSchema, SchemaSpec.HR);
+    }
+    if (schemaList.contains("foodmart")) {
+      addSchema(rootSchema, SchemaSpec.REFLECTIVE_FOODMART);
+    }
+    if (schemaList.contains("lingual")) {
+      addSchema(rootSchema, SchemaSpec.LINGUAL);
+    }
+    if (schemaList.contains("post")) {
+      addSchema(rootSchema, SchemaSpec.POST);
     }
     if (schemaList.contains("metadata")) {
       // always present
@@ -602,18 +632,9 @@ public class OptiqAssert {
     OptiqConnection optiqConnection =
         connection.unwrap(OptiqConnection.class);
     final SchemaPlus rootSchema = optiqConnection.getRootSchema();
-    final DataSource dataSource =
-        JdbcSchema.dataSource(
-            CONNECTION_SPEC.url,
-            CONNECTION_SPEC.driver,
-            CONNECTION_SPEC.username,
-            CONNECTION_SPEC.password);
-    final SchemaPlus foodmart =
-        rootSchema.add("foodmart",
-            JdbcSchema.create(rootSchema, "foodmart", dataSource, null,
-                "foodmart"));
+    addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
     if (withClone) {
-      rootSchema.add("foodmart2", new CloneSchema(foodmart));
+      addSchema(rootSchema, SchemaSpec.CLONE_FOODMART);
     }
     optiqConnection.setSchema("foodmart2");
     return optiqConnection;
@@ -1177,6 +1198,15 @@ public class OptiqAssert {
       this.driver = driver;
     }
   }
+
+  public enum SchemaSpec {
+    REFLECTIVE_FOODMART,
+    JDBC_FOODMART,
+    CLONE_FOODMART,
+    HR,
+    LINGUAL,
+    POST
+  }
 }
 
 // End OptiqAssert.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/929f5310/core/src/test/java/net/hydromatic/optiq/tools/PlannerTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/tools/PlannerTest.java b/core/src/test/java/net/hydromatic/optiq/tools/PlannerTest.java
index 75282c7..77e734e 100644
--- a/core/src/test/java/net/hydromatic/optiq/tools/PlannerTest.java
+++ b/core/src/test/java/net/hydromatic/optiq/tools/PlannerTest.java
@@ -26,7 +26,6 @@ import net.hydromatic.optiq.prepare.OptiqPrepareImpl;
 import net.hydromatic.optiq.rules.java.EnumerableConvention;
 import net.hydromatic.optiq.rules.java.JavaRules;
 import net.hydromatic.optiq.rules.java.JavaRules.EnumerableProjectRel;
-import net.hydromatic.optiq.test.JdbcTest;
 import net.hydromatic.optiq.test.OptiqAssert;
 
 import org.eigenbase.rel.*;
@@ -143,10 +142,13 @@ public class PlannerTest {
         ImmutableList.of(stdOpTab,
             new ListSqlOperatorTable(
                 ImmutableList.<SqlOperator>of(new MyCountAggFunction()))));
-    Planner planner = Frameworks.getPlanner(Frameworks.newConfigBuilder() //
-        .defaultSchema(createHrSchema()) //
-        .operatorTable(opTab) //
-        .build());
+    final SchemaPlus rootSchema = Frameworks.createRootSchema(true);
+    final FrameworkConfig config = Frameworks.newConfigBuilder()
+        .defaultSchema(
+            OptiqAssert.addSchema(rootSchema, OptiqAssert.SchemaSpec.HR))
+        .operatorTable(opTab)
+        .build();
+    final Planner planner = Frameworks.getPlanner(config);
     SqlNode parse =
         planner.parse("select \"deptno\", my_count(\"empid\") from \"emps\"\n"
             + "group by \"deptno\"");
@@ -175,18 +177,16 @@ public class PlannerTest {
     }
   }
 
-  private SchemaPlus createHrSchema() {
-    return Frameworks.createRootSchema(true).add("hr",
-        new ReflectiveSchema(new JdbcTest.HrSchema()));
-  }
-
   private Planner getPlanner(List<RelTraitDef> traitDefs, Program... programs) {
-    return Frameworks.getPlanner(Frameworks.newConfigBuilder() //
-        .lex(Lex.ORACLE) //
-        .defaultSchema(createHrSchema()) //
-        .traitDefs(traitDefs) //
-        .programs(programs) //
-        .build());
+    final SchemaPlus rootSchema = Frameworks.createRootSchema(true);
+    final FrameworkConfig config = Frameworks.newConfigBuilder()
+        .lex(Lex.ORACLE)
+        .defaultSchema(
+            OptiqAssert.addSchema(rootSchema, OptiqAssert.SchemaSpec.HR))
+        .traitDefs(traitDefs)
+        .programs(programs)
+        .build();
+    return Frameworks.getPlanner(config);
   }
 
   /** Tests that planner throws an error if you pass to
@@ -443,7 +443,8 @@ public class PlannerTest {
           .append(i).append(".\"deptno\" = d")
           .append(i - 1).append(".\"deptno\"");
     }
-    Planner planner = getPlanner(null, Programs.heuristicJoinOrder(RULE_SET));
+    Planner planner =
+        getPlanner(null, Programs.heuristicJoinOrder(RULE_SET, false));
     SqlNode parse = planner.parse(buf.toString());
 
     SqlNode validate = planner.validate(parse);
@@ -455,6 +456,43 @@ public class PlannerTest {
         "EnumerableJoinRel(condition=[=($3, $0)], joinType=[inner])"));
   }
 
+  /** Plans a 4-table join query on the FoodMart schema. The ideal plan is not
+   * bushy, but nevertheless exercises the bushy-join heuristic optimizer. */
+  @Test public void testAlmostBushy() throws Exception {
+    final String sql = "select *\n"
+        + "from \"sales_fact_1997\" as s\n"
+        + "  join \"customer\" as c using (\"customer_id\")\n"
+        + "  join \"product\" as p using (\"product_id\")\n"
+        + "where c.\"city\" = 'San Francisco'\n"
+        + "and p.\"brand_name\" = 'Washington'";
+    final String expected = ""
+        + "EnumerableJoinRel(condition=[=($0, $38)], joinType=[inner])\n"
+        + "  EnumerableJoinRel(condition=[=($2, $8)], joinType=[inner])\n"
+        + "    EnumerableTableAccessRel(table=[[foodmart2, sales_fact_1997]])\n"
+        + "    EnumerableFilterRel(condition=[=($9, 'San Francisco')])\n"
+        + "      EnumerableTableAccessRel(table=[[foodmart2, customer]])\n"
+        + "  EnumerableFilterRel(condition=[=($2, 'Washington')])\n"
+        + "    EnumerableTableAccessRel(table=[[foodmart2, product]])\n";
+    final SchemaPlus rootSchema = Frameworks.createRootSchema(true);
+    final FrameworkConfig config = Frameworks.newConfigBuilder()
+        .lex(Lex.ORACLE)
+        .defaultSchema(
+            OptiqAssert.addSchema(rootSchema,
+                OptiqAssert.SchemaSpec.CLONE_FOODMART))
+        .traitDefs((List<RelTraitDef>) null)
+        .programs(Programs.heuristicJoinOrder(RULE_SET, true))
+        .build();
+    Planner planner = Frameworks.getPlanner(config);
+    SqlNode parse = planner.parse(sql);
+
+    SqlNode validate = planner.validate(parse);
+    RelNode convert = planner.convert(validate);
+    RelTraitSet traitSet = planner.getEmptyTraitSet()
+        .replace(EnumerableConvention.INSTANCE);
+    RelNode transform = planner.transform(0, traitSet, convert);
+    assertThat(toString(transform), containsString(expected));
+  }
+
   /**
    * Rule to convert a {@link EnumerableProjectRel} to an
    * {@link JdbcProjectRel}.