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/09/12 10:06:44 UTC

[2/2] git commit: Now, a lattice can materialize an aggregate-join and use it in a subsequent query.

Now, a lattice can materialize an aggregate-join and use it in a subsequent query.

Add AggregateFilterTransposeRule, to help crystallize a query into a form that matches an aggregate table.

Minor fixes and tweaks to BreadthFirstIterator, DepthFirstIterator, ArrayTable, StarTable, Mappings.


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

Branch: refs/heads/master
Commit: 1d2cb011856d5d7c43cd55df6d0ceb71812ff24e
Parents: fbac0d6
Author: Julian Hyde <jh...@apache.org>
Authored: Mon Sep 8 18:05:28 2014 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Fri Sep 12 00:33:24 2014 -0700

----------------------------------------------------------------------
 core/pom.xml                                    |   2 +-
 .../net/hydromatic/optiq/impl/StarTable.java    |   5 +
 .../hydromatic/optiq/impl/clone/ArrayTable.java |   2 +-
 .../optiq/impl/clone/CloneSchema.java           |  10 +-
 .../hydromatic/optiq/rules/java/JavaRules.java  |   4 -
 .../optiq/util/graph/BreadthFirstIterator.java  |  32 +++-
 .../optiq/util/graph/DepthFirstIterator.java    |  37 ++--
 .../rel/rules/AggregateFilterTransposeRule.java | 139 ++++++++++++++
 .../rel/rules/AggregateStarTableRule.java       |   1 -
 .../org/eigenbase/relopt/RelOptLattice.java     |   8 +-
 .../eigenbase/relopt/RelOptMaterialization.java | 187 +++++++++++++------
 .../java/org/eigenbase/relopt/RelOptUtil.java   |  14 ++
 .../org/eigenbase/relopt/hep/HepPlanner.java    |   8 +-
 .../relopt/volcano/VolcanoPlanner.java          |   3 +
 .../eigenbase/rex/RexPermuteInputsShuttle.java  |  31 ++-
 .../main/java/org/eigenbase/rex/RexUtil.java    |  15 +-
 .../sql2rel/RelStructuredTypeFlattener.java     |   5 +-
 .../org/eigenbase/util/mapping/Mappings.java    |  59 ++++--
 .../net/hydromatic/optiq/test/FoodmartTest.java |   2 +-
 .../net/hydromatic/optiq/test/JdbcTest.java     |  28 ++-
 .../net/hydromatic/optiq/test/LatticeTest.java  |  13 +-
 .../optiq/test/LinqFrontJdbcBackTest.java       |   2 +-
 .../net/hydromatic/optiq/test/OptiqAssert.java  |  80 ++++----
 .../optiq/util/graph/DirectedGraphTest.java     |  21 ++-
 .../org/eigenbase/util/mapping/MappingTest.java |  11 +-
 25 files changed, 536 insertions(+), 183 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/pom.xml
----------------------------------------------------------------------
diff --git a/core/pom.xml b/core/pom.xml
index 93ca43d..c1957f3 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -55,7 +55,7 @@ limitations under the License.
           <threadCount>1</threadCount>
           <perCoreThreadCount>true</perCoreThreadCount>
           <parallel>both</parallel>
-          <argLine>-Xmx1024m -XX:MaxPermSize=256m</argLine>
+          <argLine>-Xmx1536m -XX:MaxPermSize=256m</argLine>
         </configuration>
       </plugin>
       <plugin>

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/net/hydromatic/optiq/impl/StarTable.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/impl/StarTable.java b/core/src/main/java/net/hydromatic/optiq/impl/StarTable.java
index 37d3dc9..c4284e0 100644
--- a/core/src/main/java/net/hydromatic/optiq/impl/StarTable.java
+++ b/core/src/main/java/net/hydromatic/optiq/impl/StarTable.java
@@ -16,6 +16,7 @@
  */
 package net.hydromatic.optiq.impl;
 
+import net.hydromatic.optiq.Schema;
 import net.hydromatic.optiq.Table;
 import net.hydromatic.optiq.TranslatableTable;
 import net.hydromatic.optiq.materialize.Lattice;
@@ -66,6 +67,10 @@ public class StarTable extends AbstractTable implements TranslatableTable {
     return new StarTable(lattice, ImmutableList.copyOf(tables));
   }
 
+  @Override public Schema.TableType getJdbcTableType() {
+    return Schema.TableType.STAR;
+  }
+
   public RelDataType getRowType(RelDataTypeFactory typeFactory) {
     final List<RelDataType> typeList = new ArrayList<RelDataType>();
     final List<String> nameList = new ArrayList<String>();

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/net/hydromatic/optiq/impl/clone/ArrayTable.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/impl/clone/ArrayTable.java b/core/src/main/java/net/hydromatic/optiq/impl/clone/ArrayTable.java
index 5d445aa..8b56b7b 100644
--- a/core/src/main/java/net/hydromatic/optiq/impl/clone/ArrayTable.java
+++ b/core/src/main/java/net/hydromatic/optiq/impl/clone/ArrayTable.java
@@ -576,7 +576,7 @@ class ArrayTable extends AbstractQueryableTable {
 
     public Object getObject(Object dataSet, int ordinal) {
       Pair<Object, Integer> pair = (Pair<Object, Integer>) dataSet;
-      return pair.getValue();
+      return pair.left;
     }
 
     public int getInt(Object dataSet, int ordinal) {

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/net/hydromatic/optiq/impl/clone/CloneSchema.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/impl/clone/CloneSchema.java b/core/src/main/java/net/hydromatic/optiq/impl/clone/CloneSchema.java
index 3d534a6..e3fa950 100644
--- a/core/src/main/java/net/hydromatic/optiq/impl/clone/CloneSchema.java
+++ b/core/src/main/java/net/hydromatic/optiq/impl/clone/CloneSchema.java
@@ -62,10 +62,12 @@ public class CloneSchema extends AbstractSchema {
   protected Map<String, Table> getTableMap() {
     final Map<String, Table> map = new LinkedHashMap<String, Table>();
     for (String name : sourceSchema.getTableNames()) {
-      final QueryableTable sourceTable =
-          (QueryableTable) sourceSchema.getTable(name);
-      map.put(name,
-          createCloneTable(MATERIALIZATION_CONNECTION, sourceTable, name));
+      final Table table = sourceSchema.getTable(name);
+      if (table instanceof QueryableTable) {
+        final QueryableTable sourceTable = (QueryableTable) table;
+        map.put(name,
+            createCloneTable(MATERIALIZATION_CONNECTION, sourceTable, name));
+      }
     }
     return map;
   }

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/net/hydromatic/optiq/rules/java/JavaRules.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/rules/java/JavaRules.java b/core/src/main/java/net/hydromatic/optiq/rules/java/JavaRules.java
index 35d3f9f..d9ead6c 100644
--- a/core/src/main/java/net/hydromatic/optiq/rules/java/JavaRules.java
+++ b/core/src/main/java/net/hydromatic/optiq/rules/java/JavaRules.java
@@ -742,8 +742,6 @@ public class JavaRules {
   public static class EnumerableCalcRel
       extends CalcRelBase
       implements EnumerableRel {
-    private final RexProgram program;
-
     public EnumerableCalcRel(
         RelOptCluster cluster,
         RelTraitSet traitSet,
@@ -754,8 +752,6 @@ public class JavaRules {
       super(cluster, traitSet, child, rowType, program, collationList);
       assert getConvention() instanceof EnumerableConvention;
       assert !program.containsAggs();
-      this.program = program;
-      this.rowType = program.getOutputRowType();
     }
 
     @Override public EnumerableCalcRel copy(RelTraitSet traitSet, RelNode child,

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/net/hydromatic/optiq/util/graph/BreadthFirstIterator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/util/graph/BreadthFirstIterator.java b/core/src/main/java/net/hydromatic/optiq/util/graph/BreadthFirstIterator.java
index 2dcdc58..0f7589b 100644
--- a/core/src/main/java/net/hydromatic/optiq/util/graph/BreadthFirstIterator.java
+++ b/core/src/main/java/net/hydromatic/optiq/util/graph/BreadthFirstIterator.java
@@ -16,11 +16,10 @@
  */
 package net.hydromatic.optiq.util.graph;
 
-import org.eigenbase.util.ChunkList;
-
+import java.util.ArrayDeque;
+import java.util.Deque;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Set;
 
 /**
@@ -32,12 +31,12 @@ import java.util.Set;
 public class BreadthFirstIterator<V, E extends DefaultEdge>
     implements Iterator<V> {
   private final DirectedGraph<V, E> graph;
-  private final List<V> list = new ChunkList<V>();
+  private final Deque<V> deque = new ArrayDeque<V>();
   private final Set<V> set = new HashSet<V>();
 
   public BreadthFirstIterator(DirectedGraph<V, E> graph, V root) {
     this.graph = graph;
-    this.list.add(root);
+    this.deque.add(root);
   }
 
   public static <V, E extends DefaultEdge> Iterable<V> of(
@@ -49,16 +48,33 @@ public class BreadthFirstIterator<V, E extends DefaultEdge>
     };
   }
 
+  /** Populates a set with the nodes reachable from a given node. */
+  public static <V, E extends DefaultEdge> void reachable(Set<V> set,
+      final DirectedGraph<V, E> graph, final V root) {
+    final Deque<V> deque = new ArrayDeque<V>();
+    deque.add(root);
+    set.add(root);
+    while (!deque.isEmpty()) {
+      V v = deque.removeFirst();
+      for (E e : graph.getOutwardEdges(v)) {
+        @SuppressWarnings("unchecked") V target = (V) e.target;
+        if (set.add(target)) {
+          deque.addLast(target);
+        }
+      }
+    }
+  }
+
   public boolean hasNext() {
-    return !list.isEmpty();
+    return !deque.isEmpty();
   }
 
   public V next() {
-    V v = list.remove(0);
+    V v = deque.removeFirst();
     for (E e : graph.getOutwardEdges(v)) {
       @SuppressWarnings("unchecked") V target = (V) e.target;
       if (set.add(target)) {
-        list.add(target);
+        deque.addLast(target);
       }
     }
     return v;

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/net/hydromatic/optiq/util/graph/DepthFirstIterator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/util/graph/DepthFirstIterator.java b/core/src/main/java/net/hydromatic/optiq/util/graph/DepthFirstIterator.java
index 9652e10..c6cd7cf 100644
--- a/core/src/main/java/net/hydromatic/optiq/util/graph/DepthFirstIterator.java
+++ b/core/src/main/java/net/hydromatic/optiq/util/graph/DepthFirstIterator.java
@@ -16,6 +16,9 @@
  */
 package net.hydromatic.optiq.util.graph;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
 import java.util.*;
 
 /**
@@ -26,33 +29,37 @@ import java.util.*;
  */
 public class DepthFirstIterator<V, E extends DefaultEdge>
     implements Iterator<V> {
-
   private final Iterator<V> iterator;
 
   public DepthFirstIterator(DirectedGraph<V, E> graph, V start) {
     // Dumb implementation that builds the list first.
-    List<V> list = buildList(graph, start);
-    iterator = list.iterator();
+    iterator = buildList(graph, start).iterator();
+  }
+
+  private static <V, E extends DefaultEdge> List<V> buildList(
+      DirectedGraph<V, E> graph, V start) {
+    final List<V> list = Lists.newArrayList();
+    buildListRecurse(list, Sets.<V>newHashSet(), graph, start);
+    return list;
   }
 
+  /** Creates an iterable over the vertices in the given graph in a depth-first
+   * iteration order. */
   public static <V, E extends DefaultEdge> Iterable<V> of(
-      final DirectedGraph<V, E> graph, final V start) {
-    return new Iterable<V>() {
-      public Iterator<V> iterator() {
-        return new DepthFirstIterator<V, E>(graph, start);
-      }
-    };
+      DirectedGraph<V, E> graph, V start) {
+    // Doesn't actually return a DepthFirstIterator, but a list with the same
+    // contents, which is more efficient.
+    return buildList(graph, start);
   }
 
-  private List<V> buildList(DirectedGraph<V, E> graph, V start) {
-    final List<V> list = new ArrayList<V>();
+  /** Populates a collection with the nodes reachable from a given node. */
+  public static <V, E extends DefaultEdge> void reachable(Collection<V> list,
+      final DirectedGraph<V, E> graph, final V start) {
     buildListRecurse(list, new HashSet<V>(), graph, start);
-    return list;
   }
 
-  private void buildListRecurse(List<V> list,
-      Set<V> activeVertices,
-      DirectedGraph<V, E> graph,
+  private static <V, E extends DefaultEdge> void buildListRecurse(
+      Collection<V> list, Set<V> activeVertices, DirectedGraph<V, E> graph,
       V start) {
     if (!activeVertices.add(start)) {
       return;

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/rel/rules/AggregateFilterTransposeRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/rel/rules/AggregateFilterTransposeRule.java b/core/src/main/java/org/eigenbase/rel/rules/AggregateFilterTransposeRule.java
new file mode 100644
index 0000000..44a5818
--- /dev/null
+++ b/core/src/main/java/org/eigenbase/rel/rules/AggregateFilterTransposeRule.java
@@ -0,0 +1,139 @@
+/*
+ * 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.eigenbase.rel.rules;
+
+import java.util.BitSet;
+import java.util.List;
+
+import org.eigenbase.rel.AggregateCall;
+import org.eigenbase.rel.AggregateRelBase;
+import org.eigenbase.rel.Aggregation;
+import org.eigenbase.rel.FilterRelBase;
+import org.eigenbase.rel.RelNode;
+import org.eigenbase.rel.metadata.RelMetadataQuery;
+import org.eigenbase.relopt.RelOptRule;
+import org.eigenbase.relopt.RelOptRuleCall;
+import org.eigenbase.relopt.RelOptUtil;
+import org.eigenbase.relopt.SubstitutionVisitor;
+import org.eigenbase.rex.RexNode;
+import org.eigenbase.rex.RexUtil;
+import org.eigenbase.util.mapping.Mappings;
+
+import net.hydromatic.optiq.util.BitSets;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+
+/**
+ * Planner rule that matches an {@link org.eigenbase.rel.AggregateRelBase}
+ * on a {@link org.eigenbase.rel.FilterRelBase} and transposes them,
+ * pushing the aggregate below the filter.
+ *
+ * <p>In some cases, it is necessary to split the aggregate.
+ *
+ * <p>This rule does not directly improve performance. The aggregate will
+ * have to process more rows, to produce aggregated rows that will be thrown
+ * away. The rule might be beneficial if the predicate is very expensive to
+ * evaluate. The main use of the rule is to match a query that has a filter
+ * under an aggregate to an existing aggregate table.
+ */
+public class AggregateFilterTransposeRule extends RelOptRule {
+  public static final AggregateFilterTransposeRule INSTANCE =
+      new AggregateFilterTransposeRule();
+
+  private AggregateFilterTransposeRule() {
+    super(
+        operand(AggregateRelBase.class,
+            operand(FilterRelBase.class, any())));
+  }
+
+  public void onMatch(RelOptRuleCall call) {
+    final AggregateRelBase aggregate = call.rel(0);
+    final FilterRelBase filter = call.rel(1);
+
+    // Do the columns used by the filter appear in the output of the aggregate?
+    final BitSet filterColumns =
+        RelOptUtil.InputFinder.bits(filter.getCondition());
+    final BitSet newGroupSet =
+        BitSets.union(aggregate.getGroupSet(), filterColumns);
+    final RelNode input = filter.getChild();
+    final Boolean unique =
+        RelMetadataQuery.areColumnsUnique(input, newGroupSet);
+    if (unique != null && unique) {
+      // The input is already unique on the grouping columns, so there's little
+      // advantage of aggregating again. More important, without this check,
+      // the rule fires forever: A-F => A-F-A => A-A-F-A => A-A-A-F-A => ...
+      return;
+    }
+    final AggregateRelBase newAggregate =
+        aggregate.copy(aggregate.getTraitSet(), input, newGroupSet,
+            aggregate.getAggCallList());
+    final Mappings.TargetMapping mapping = Mappings.target(
+        new Function<Integer, Integer>() {
+          public Integer apply(Integer a0) {
+            return BitSets.toList(newGroupSet).indexOf(a0);
+          }
+        },
+        input.getRowType().getFieldCount(),
+        newGroupSet.cardinality());
+    final RexNode newCondition =
+        RexUtil.apply(mapping, filter.getCondition());
+    final FilterRelBase newFilter = filter.copy(filter.getTraitSet(),
+        newAggregate, newCondition);
+    if (BitSets.contains(aggregate.getGroupSet(), filterColumns)) {
+      // Everything needed by the filter is returned by the aggregate.
+      assert newGroupSet.equals(aggregate.getGroupSet());
+      call.transformTo(newFilter);
+    } else {
+      // The filter needs at least one extra column.
+      // Now aggregate it away.
+      final BitSet topGroupSet = new BitSet();
+      for (int c : BitSets.toIter(aggregate.getGroupSet())) {
+        topGroupSet.set(BitSets.toList(newGroupSet).indexOf(c));
+      }
+      final List<AggregateCall> topAggCallList = Lists.newArrayList();
+      final int offset = newGroupSet.cardinality()
+          - aggregate.getGroupSet().cardinality();
+      assert offset > 0;
+      for (AggregateCall aggregateCall : aggregate.getAggCallList()) {
+        final List<Integer> args = Lists.newArrayList();
+        for (int arg : aggregateCall.getArgList()) {
+          args.add(arg + offset);
+        }
+        final Aggregation rollup =
+            SubstitutionVisitor.getRollup(aggregateCall.getAggregation());
+        if (rollup == null) {
+          // This aggregate cannot be rolled up.
+          return;
+        }
+        if (aggregateCall.isDistinct()) {
+          // Cannot roll up distinct.
+          return;
+        }
+        topAggCallList.add(
+            new AggregateCall(rollup, aggregateCall.isDistinct(), args,
+                aggregateCall.type, aggregateCall.name));
+      }
+      final AggregateRelBase topAggregate =
+          aggregate.copy(aggregate.getTraitSet(), newFilter, topGroupSet,
+              topAggCallList);
+      call.transformTo(topAggregate);
+    }
+  }
+}
+
+// End AggregateFilterTransposeRule.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/rel/rules/AggregateStarTableRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/rel/rules/AggregateStarTableRule.java b/core/src/main/java/org/eigenbase/rel/rules/AggregateStarTableRule.java
index 2edadfe..5f61fd6 100644
--- a/core/src/main/java/org/eigenbase/rel/rules/AggregateStarTableRule.java
+++ b/core/src/main/java/org/eigenbase/rel/rules/AggregateStarTableRule.java
@@ -98,7 +98,6 @@ public class AggregateStarTableRule extends RelOptRule {
     if (aggregateTable == null) {
       return;
     }
-    System.out.println(aggregateTable);
     final double rowCount = aggregate.getRows();
     final RelOptTable aggregateRelOptTable =
         RelOptTableImpl.create(table.getRelOptSchema(),

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/relopt/RelOptLattice.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/relopt/RelOptLattice.java b/core/src/main/java/org/eigenbase/relopt/RelOptLattice.java
index a41202b..0606282 100644
--- a/core/src/main/java/org/eigenbase/relopt/RelOptLattice.java
+++ b/core/src/main/java/org/eigenbase/relopt/RelOptLattice.java
@@ -23,6 +23,7 @@ import org.eigenbase.rel.AggregateCall;
 import org.eigenbase.rel.RelNode;
 import org.eigenbase.reltype.RelDataTypeField;
 import org.eigenbase.sql.SqlDialect;
+import org.eigenbase.util.Util;
 import org.eigenbase.util.mapping.IntPair;
 
 import net.hydromatic.optiq.config.OptiqConnectionConfig;
@@ -117,7 +118,12 @@ public class RelOptLattice {
       if (k++ > 0) {
         buf.append(", ");
       }
-      dialect.quoteIdentifier(buf, field.getName());
+      final List<String> identifiers = lattice.columns.get(i);
+      dialect.quoteIdentifier(buf, identifiers);
+      if (!Util.last(identifiers).equals(field.getName())) {
+        buf.append(" AS ");
+        dialect.quoteIdentifier(buf, field.getName());
+      }
     }
     buf.append("\nFROM ");
     for (Lattice.Node node : usedNodes) {

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/relopt/RelOptMaterialization.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/relopt/RelOptMaterialization.java b/core/src/main/java/org/eigenbase/relopt/RelOptMaterialization.java
index 2cf1792..0b0686d 100644
--- a/core/src/main/java/org/eigenbase/relopt/RelOptMaterialization.java
+++ b/core/src/main/java/org/eigenbase/relopt/RelOptMaterialization.java
@@ -16,10 +16,18 @@
  */
 package org.eigenbase.relopt;
 
+import java.util.List;
+
 import org.eigenbase.rel.*;
 import org.eigenbase.rel.metadata.DefaultRelMetadataProvider;
+import org.eigenbase.rel.rules.AggregateFilterTransposeRule;
+import org.eigenbase.rel.rules.AggregateProjectMergeRule;
 import org.eigenbase.rel.rules.MergeProjectRule;
 import org.eigenbase.rel.rules.PullUpProjectsAboveJoinRule;
+import org.eigenbase.rel.rules.PushFilterPastJoinRule;
+import org.eigenbase.rel.rules.PushProjectPastFilterRule;
+import org.eigenbase.rex.RexNode;
+import org.eigenbase.rex.RexUtil;
 import org.eigenbase.sql.SqlExplainLevel;
 import org.eigenbase.util.Util;
 import org.eigenbase.util.mapping.Mappings;
@@ -30,7 +38,9 @@ import net.hydromatic.optiq.prepare.OptiqPrepareImpl;
 import net.hydromatic.optiq.tools.Program;
 import net.hydromatic.optiq.tools.Programs;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 
 /**
  * Records that a particular query is materialized by a particular table.
@@ -69,7 +79,7 @@ public class RelOptMaterialization {
       final RelOptTable starRelOptTable) {
     final StarTable starTable = starRelOptTable.unwrap(StarTable.class);
     assert starTable != null;
-    return rel.accept(
+    RelNode rel2 = rel.accept(
         new RelShuttleImpl() {
           @Override
           public RelNode visit(TableAccessRelBase scan) {
@@ -98,87 +108,147 @@ public class RelOptMaterialization {
                 return rel;
               }
               join = (JoinRel) rel;
-              final RelNode left = join.getLeft();
-              final RelNode right = join.getRight();
-              try {
-                if (left instanceof TableAccessRelBase
-                    && right instanceof TableAccessRelBase) {
-                  match(left, null, right, null, join.getCluster());
-                }
-                if (isProjectedTable(left)
-                    && right instanceof TableAccessRelBase) {
-                  final ProjectRel leftProject = (ProjectRel) left;
-                  match(leftProject.getChild(), leftProject.getMapping(), right,
-                      null, join.getCluster());
-                }
-                if (left instanceof TableAccessRelBase
-                    && isProjectedTable(right)) {
-                  final ProjectRel rightProject = (ProjectRel) right;
-                  match(left, null, rightProject.getChild(),
-                      rightProject.getMapping(), join.getCluster());
-                }
-                if (isProjectedTable(left)
-                    && isProjectedTable(right)) {
-                  final ProjectRel leftProject = (ProjectRel) left;
-                  final ProjectRel rightProject = (ProjectRel) right;
-                  match(leftProject.getChild(), leftProject.getMapping(),
-                      rightProject.getChild(), rightProject.getMapping(),
-                      join.getCluster());
+              final ProjectFilterTable left =
+                  ProjectFilterTable.of(join.getLeft());
+              if (left != null) {
+                final ProjectFilterTable right =
+                    ProjectFilterTable.of(join.getRight());
+                if (right != null) {
+                  try {
+                    match(left, right, join.getCluster());
+                  } catch (Util.FoundOne e) {
+                    return (RelNode) e.getNode();
+                  }
                 }
-              } catch (Util.FoundOne e) {
-                return (RelNode) e.getNode();
               }
             }
           }
 
-          private boolean isProjectedTable(RelNode rel) {
-            return rel instanceof ProjectRel
-                && ((ProjectRel) rel).isMapping()
-                && ((ProjectRel) rel).getChild() instanceof TableAccessRelBase;
-          }
-
           /** Throws a {@link org.eigenbase.util.Util.FoundOne} containing a
            * {@link org.eigenbase.rel.TableAccessRel} on success.
            * (Yes, an exception for normal operation.) */
-          private void match(RelNode left, Mappings.TargetMapping leftMapping,
-              RelNode right, Mappings.TargetMapping rightMapping,
+          private void match(ProjectFilterTable left, ProjectFilterTable right,
               RelOptCluster cluster) {
-            if (leftMapping == null) {
-              leftMapping =
-                  Mappings.createIdentity(left.getRowType().getFieldCount());
-            }
-            if (rightMapping == null) {
-              rightMapping =
-                  Mappings.createIdentity(right.getRowType().getFieldCount());
-            }
+            final Mappings.TargetMapping leftMapping = left.mapping();
+            final Mappings.TargetMapping rightMapping = right.mapping();
             final RelOptTable leftRelOptTable = left.getTable();
             final Table leftTable = leftRelOptTable.unwrap(Table.class);
+            final int leftCount = leftRelOptTable.getRowType().getFieldCount();
             final RelOptTable rightRelOptTable = right.getTable();
             final Table rightTable = rightRelOptTable.unwrap(Table.class);
             if (leftTable instanceof StarTable
                 && ((StarTable) leftTable).tables.contains(rightTable)) {
+              final int offset =
+                  ((StarTable) leftTable).columnOffset(rightTable);
               Mappings.TargetMapping mapping =
                   Mappings.merge(leftMapping,
-                      Mappings.offset(rightMapping,
-                          ((StarTable) leftTable).columnOffset(rightTable),
-                          leftRelOptTable.getRowType().getFieldCount()));
-              throw new Util.FoundOne(
-                  RelOptUtil.createProject(
-                      new TableAccessRel(cluster, leftRelOptTable),
-                      Mappings.asList(mapping.inverse())));
+                      Mappings.offsetTarget(
+                          Mappings.offsetSource(rightMapping, offset),
+                          leftMapping.getTargetCount()));
+              final RelNode project = RelOptUtil.createProject(
+                  new TableAccessRel(cluster, leftRelOptTable),
+                  Mappings.asList(mapping.inverse()));
+              final List<RexNode> conditions = Lists.newArrayList();
+              if (left.condition != null) {
+                conditions.add(RexUtil.apply(mapping, left.condition));
+              }
+              if (right.condition != null) {
+                conditions.add(
+                    RexUtil.apply(mapping,
+                        RexUtil.shift(right.condition, offset)));
+              }
+              final RelNode filter =
+                  RelOptUtil.createFilter(project, conditions);
+              throw new Util.FoundOne(filter);
             }
             if (rightTable instanceof StarTable
                 && ((StarTable) rightTable).tables.contains(leftTable)) {
-              assert false; // TODO:
+              final int offset =
+                  ((StarTable) rightTable).columnOffset(leftTable);
               Mappings.TargetMapping mapping =
-                  Mappings.append(leftMapping, rightMapping);
-              throw new Util.FoundOne(
-                  RelOptUtil.createProject(
-                      new TableAccessRel(cluster, rightRelOptTable),
-                      Mappings.asList(mapping.inverse())));
+                  Mappings.merge(
+                      Mappings.offsetSource(leftMapping, offset),
+                      Mappings.offsetTarget(rightMapping, leftCount));
+              final RelNode project = RelOptUtil.createProject(
+                  new TableAccessRel(cluster, rightRelOptTable),
+                  Mappings.asList(mapping.inverse()));
+              final List<RexNode> conditions = Lists.newArrayList();
+              if (left.condition != null) {
+                conditions.add(RexUtil.apply(mapping, left.condition));
+              }
+              if (right.condition != null) {
+                conditions.add(
+                    RexUtil.apply(mapping,
+                        RexUtil.shift(right.condition, offset)));
+              }
+              final RelNode filter =
+                  RelOptUtil.createFilter(project, conditions);
+              throw new Util.FoundOne(filter);
             }
           }
         });
+    if (rel2 == rel) {
+      return rel;
+    }
+    final Program program = Programs.hep(
+        ImmutableList.of(PushProjectPastFilterRule.INSTANCE,
+            AggregateProjectMergeRule.INSTANCE,
+            AggregateFilterTransposeRule.INSTANCE),
+        false,
+        new DefaultRelMetadataProvider());
+    return program.run(null, rel2, null);
+  }
+
+  /** A table scan and optional project mapping and filter condition. */
+  private static class ProjectFilterTable {
+    final RexNode condition;
+    final Mappings.TargetMapping mapping;
+    final TableAccessRelBase scan;
+
+    private ProjectFilterTable(RexNode condition,
+        Mappings.TargetMapping mapping, TableAccessRelBase scan) {
+      this.condition = condition;
+      this.mapping = mapping;
+      this.scan = Preconditions.checkNotNull(scan);
+    }
+
+    static ProjectFilterTable of(RelNode node) {
+      if (node instanceof FilterRelBase) {
+        final FilterRelBase filter = (FilterRelBase) node;
+        return of2(filter.getCondition(), filter.getChild());
+      } else {
+        return of2(null, node);
+      }
+    }
+
+    private static ProjectFilterTable of2(RexNode condition, RelNode node) {
+      if (node instanceof ProjectRelBase) {
+        final ProjectRelBase project = (ProjectRelBase) node;
+        return of3(condition, project.getMapping(), project.getChild());
+      } else {
+        return of3(condition, null, node);
+      }
+    }
+
+    private static ProjectFilterTable of3(RexNode condition,
+        Mappings.TargetMapping mapping, RelNode node) {
+      if (node instanceof TableAccessRelBase) {
+        return new ProjectFilterTable(condition, mapping,
+            (TableAccessRelBase) node);
+      } else {
+        return null;
+      }
+    }
+
+    public Mappings.TargetMapping mapping() {
+      return mapping != null
+          ? mapping
+          : Mappings.createIdentity(scan.getRowType().getFieldCount());
+    }
+
+    public RelOptTable getTable() {
+      return scan.getTable();
+    }
   }
 
   /**
@@ -191,6 +261,7 @@ public class RelOptMaterialization {
         ImmutableList.of(
             PullUpProjectsAboveJoinRule.RIGHT_PROJECT,
             PullUpProjectsAboveJoinRule.LEFT_PROJECT,
+            PushFilterPastJoinRule.PushFilterIntoJoinRule.FILTER_ON_JOIN,
             MergeProjectRule.INSTANCE),
         false,
         new DefaultRelMetadataProvider());

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/relopt/RelOptUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/relopt/RelOptUtil.java b/core/src/main/java/org/eigenbase/relopt/RelOptUtil.java
index eae8c2f..aee380d 100644
--- a/core/src/main/java/org/eigenbase/relopt/RelOptUtil.java
+++ b/core/src/main/java/org/eigenbase/relopt/RelOptUtil.java
@@ -454,6 +454,20 @@ public abstract class RelOptUtil {
     return new FilterRel(child.getCluster(), child, condition);
   }
 
+  /** Creates a filter, or returns the original relational expression if the
+   * condition is trivial. */
+  public static RelNode createFilter(RelNode child,
+      Iterable<? extends RexNode> conditions) {
+    final RelOptCluster cluster = child.getCluster();
+    final RexNode condition =
+        RexUtil.composeConjunction(cluster.getRexBuilder(), conditions, true);
+    if (condition == null) {
+      return child;
+    } else {
+      return createFilter(child, condition);
+    }
+  }
+
   /**
    * Creates a filter which will remove rows containing NULL values.
    *

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/relopt/hep/HepPlanner.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/relopt/hep/HepPlanner.java b/core/src/main/java/org/eigenbase/relopt/hep/HepPlanner.java
index ab94fe7..e06f8c1 100644
--- a/core/src/main/java/org/eigenbase/relopt/hep/HepPlanner.java
+++ b/core/src/main/java/org/eigenbase/relopt/hep/HepPlanner.java
@@ -869,12 +869,8 @@ public class HepPlanner extends AbstractRelOptPlanner {
 
     // Yer basic mark-and-sweep.
     Set<HepRelVertex> rootSet = new HashSet<HepRelVertex>();
-    Iterator<HepRelVertex> iter =
-        new DepthFirstIterator<HepRelVertex, DefaultEdge>(
-            graph,
-            root);
-    while (iter.hasNext()) {
-      rootSet.add(iter.next());
+    if (graph.vertexSet().contains(root)) {
+      BreadthFirstIterator.reachable(rootSet, graph, root);
     }
 
     if (rootSet.size() == graph.vertexSet().size()) {

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/relopt/volcano/VolcanoPlanner.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/relopt/volcano/VolcanoPlanner.java b/core/src/main/java/org/eigenbase/relopt/volcano/VolcanoPlanner.java
index 23f37c0..03afff5 100644
--- a/core/src/main/java/org/eigenbase/relopt/volcano/VolcanoPlanner.java
+++ b/core/src/main/java/org/eigenbase/relopt/volcano/VolcanoPlanner.java
@@ -403,6 +403,9 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
       if (queryTableNames.contains(lattice.rootTable().getQualifiedName())) {
         RelNode rel2 = lattice.rewrite(leafJoinRoot.get());
         if (rel2 != null) {
+          if (OptiqPrepareImpl.DEBUG) {
+            System.out.println("use lattice:\n" + RelOptUtil.toString(rel2));
+          }
           latticeUses.add(Pair.of(lattice, rel2));
         }
       }

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/rex/RexPermuteInputsShuttle.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/rex/RexPermuteInputsShuttle.java b/core/src/main/java/org/eigenbase/rex/RexPermuteInputsShuttle.java
index 52c380a..f9d7b1e 100644
--- a/core/src/main/java/org/eigenbase/rex/RexPermuteInputsShuttle.java
+++ b/core/src/main/java/org/eigenbase/rex/RexPermuteInputsShuttle.java
@@ -16,24 +16,25 @@
  */
 package org.eigenbase.rex;
 
-import java.util.ArrayList;
 import java.util.List;
 
 import org.eigenbase.rel.RelNode;
 import org.eigenbase.reltype.RelDataTypeField;
 import org.eigenbase.util.mapping.Mappings;
 
+import com.google.common.collect.ImmutableList;
+
 /**
  * Shuttle which applies a permutation to its input fields.
  *
  * @see RexPermutationShuttle
+ * @see RexUtil#apply(org.eigenbase.util.mapping.Mappings.TargetMapping, RexNode)
  */
 public class RexPermuteInputsShuttle extends RexShuttle {
   //~ Instance fields --------------------------------------------------------
 
   private final Mappings.TargetMapping mapping;
-  private final List<RelDataTypeField> fields =
-      new ArrayList<RelDataTypeField>();
+  private final ImmutableList<RelDataTypeField> fields;
 
   //~ Constructors -----------------------------------------------------------
 
@@ -51,14 +52,34 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
+    this(mapping, fields(inputs));
+  }
+
+  private RexPermuteInputsShuttle(
+      Mappings.TargetMapping mapping,
+      ImmutableList<RelDataTypeField> fields) {
     this.mapping = mapping;
+    this.fields = fields;
+  }
+
+  /** Creates a shuttle with an empty field list. It cannot handle GET calls but
+   * otherwise works OK. */
+  public static RexPermuteInputsShuttle of(Mappings.TargetMapping mapping) {
+    return new RexPermuteInputsShuttle(mapping,
+        ImmutableList.<RelDataTypeField>of());
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  private static ImmutableList<RelDataTypeField> fields(RelNode[] inputs) {
+    final ImmutableList.Builder<RelDataTypeField> fields =
+        ImmutableList.builder();
     for (RelNode input : inputs) {
       fields.addAll(input.getRowType().getFieldList());
     }
+    return fields.build();
   }
 
-  //~ Methods ----------------------------------------------------------------
-
   @Override
   public RexNode visitInputRef(RexInputRef local) {
     final int index = local.getIndex();

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/rex/RexUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/rex/RexUtil.java b/core/src/main/java/org/eigenbase/rex/RexUtil.java
index 5ed708d..af365e1 100644
--- a/core/src/main/java/org/eigenbase/rex/RexUtil.java
+++ b/core/src/main/java/org/eigenbase/rex/RexUtil.java
@@ -607,12 +607,16 @@ public class RexUtil {
     }
   }
 
-  /** Flattens a list of AND nodes. */
+  /** Flattens a list of AND nodes.
+   *
+   * <p>Treats null nodes as literal TRUE (i.e. ignores them). */
   public static ImmutableList<RexNode> flattenAnd(
       Iterable<? extends RexNode> nodes) {
     final ImmutableList.Builder<RexNode> builder = ImmutableList.builder();
     for (RexNode node : nodes) {
-      addAnd(builder, node);
+      if (node != null) {
+        addAnd(builder, node);
+      }
     }
     return builder.build();
   }
@@ -784,6 +788,13 @@ public class RexUtil {
   }
 
   /**
+   * Applies a mapping to an expression.
+   */
+  public static RexNode apply(Mappings.TargetMapping mapping, RexNode node) {
+    return node.accept(RexPermuteInputsShuttle.of(mapping));
+  }
+
+  /**
    * Applies a shuttle to an array of expressions. Creates a copy first.
    *
    * @param shuttle Shuttle

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/sql2rel/RelStructuredTypeFlattener.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/sql2rel/RelStructuredTypeFlattener.java b/core/src/main/java/org/eigenbase/sql2rel/RelStructuredTypeFlattener.java
index 5fa73a9..eacbbac 100644
--- a/core/src/main/java/org/eigenbase/sql2rel/RelStructuredTypeFlattener.java
+++ b/core/src/main/java/org/eigenbase/sql2rel/RelStructuredTypeFlattener.java
@@ -28,8 +28,7 @@ import org.eigenbase.sql.type.*;
 import org.eigenbase.util.*;
 import org.eigenbase.util.mapping.Mappings;
 
-import net.hydromatic.linq4j.function.Function1;
-
+import com.google.common.base.Function;
 import com.google.common.collect.*;
 
 // TODO jvs 10-Feb-2005:  factor out generic rewrite helper, with the
@@ -256,7 +255,7 @@ public class RelStructuredTypeFlattener implements ReflectiveVisitor {
   private Mappings.TargetMapping getNewForOldInputMapping(RelNode oldRel) {
     final RelNode newRel = getNewForOldRel(oldRel);
     return Mappings.target(
-        new Function1<Integer, Integer>() {
+        new Function<Integer, Integer>() {
           public Integer apply(Integer oldInput) {
             return getNewForOldInput(oldInput);
           }

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/main/java/org/eigenbase/util/mapping/Mappings.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/util/mapping/Mappings.java b/core/src/main/java/org/eigenbase/util/mapping/Mappings.java
index 0266e0b..ea5a6fd 100644
--- a/core/src/main/java/org/eigenbase/util/mapping/Mappings.java
+++ b/core/src/main/java/org/eigenbase/util/mapping/Mappings.java
@@ -20,10 +20,10 @@ import java.util.*;
 
 import org.eigenbase.util.*;
 
-import net.hydromatic.linq4j.function.Function1;
-
 import net.hydromatic.optiq.util.BitSets;
 
+import com.google.common.base.Function;
+
 /**
  * Utility functions related to mappings.
  *
@@ -270,12 +270,11 @@ public abstract class Mappings {
   }
 
   public static TargetMapping target(
-      Function1<Integer, Integer> function,
+      Function<Integer, Integer> function,
       int sourceCount,
       int targetCount) {
     final PartialFunctionImpl mapping =
-        new PartialFunctionImpl(
-            sourceCount, targetCount, MappingType.FUNCTION);
+        new PartialFunctionImpl(sourceCount, targetCount, MappingType.FUNCTION);
     for (int source = 0; source < sourceCount; source++) {
       Integer target = function.apply(source);
       if (target != null) {
@@ -285,6 +284,16 @@ public abstract class Mappings {
     return mapping;
   }
 
+  public static Mapping target(Iterable<IntPair> pairs, int sourceCount,
+      int targetCount) {
+    final PartialFunctionImpl mapping =
+        new PartialFunctionImpl(sourceCount, targetCount, MappingType.FUNCTION);
+    for (IntPair pair : pairs) {
+      mapping.set(pair.source, pair.target);
+    }
+    return mapping;
+  }
+
   public static Mapping source(List<Integer> targets, int targetCount) {
     final int sourceCount = targets.size();
     final PartialFunctionImpl mapping =
@@ -465,7 +474,7 @@ public abstract class Mappings {
       int t = s < s0 ? mapping0.getTargetOpt(s) : -1;
       if (t >= 0) {
         mapping.set(s, t);
-        assert mapping1.getTargetOpt(s) < 0;
+        assert s >= s1 || mapping1.getTargetOpt(s) < 0;
       } else {
         t = s < s1 ? mapping1.getTargetOpt(s) : -1;
         if (t >= 0) {
@@ -510,7 +519,7 @@ public abstract class Mappings {
       throw new IllegalArgumentException("new source count too low");
     }
     return target(
-        new Function1<Integer, Integer>() {
+        new Function<Integer, Integer>() {
           public Integer apply(Integer source) {
             int source2 = source - offset;
             return source2 < 0 || source2 >= mapping.getSourceCount()
@@ -556,7 +565,7 @@ public abstract class Mappings {
       throw new IllegalArgumentException("new target count too low");
     }
     return target(
-        new Function1<Integer, Integer>() {
+        new Function<Integer, Integer>() {
           public Integer apply(Integer source) {
             int target = mapping.getTargetOpt(source);
             return target < 0 ? null : target + offset;
@@ -587,7 +596,7 @@ public abstract class Mappings {
       throw new IllegalArgumentException("new source count too low");
     }
     return target(
-        new Function1<Integer, Integer>() {
+        new Function<Integer, Integer>() {
           public Integer apply(Integer source) {
             final int source2 = source - offset;
             if (source2 < 0 || source2 >= mapping.getSourceCount()) {
@@ -619,6 +628,35 @@ public abstract class Mappings {
     return true;
   }
 
+  /** Inverts an {@link java.lang.Iterable} over
+   * {@link org.eigenbase.util.mapping.IntPair}s. */
+  public static Iterable<IntPair> invert(final Iterable<IntPair> pairs) {
+    return new Iterable<IntPair>() {
+      public Iterator<IntPair> iterator() {
+        return invert(pairs.iterator());
+      }
+    };
+  }
+
+  /** Inverts an {@link java.util.Iterator} over
+   * {@link org.eigenbase.util.mapping.IntPair}s. */
+  public static Iterator<IntPair> invert(final Iterator<IntPair> pairs) {
+    return new Iterator<IntPair>() {
+      public boolean hasNext() {
+        return pairs.hasNext();
+      }
+
+      public IntPair next() {
+        final IntPair pair = pairs.next();
+        return IntPair.of(pair.target, pair.source);
+      }
+
+      public void remove() {
+        throw new UnsupportedOperationException("remove");
+      }
+    };
+  }
+
   //~ Inner Interfaces -------------------------------------------------------
 
   /**
@@ -1492,8 +1530,7 @@ public abstract class Mappings {
     }
 
     public Mapping inverse() {
-      // todo: implement
-      throw new UnsupportedOperationException();
+      return target(invert(this), targetCount, sourceCount);
     }
 
     public void set(int source, int target) {

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/test/java/net/hydromatic/optiq/test/FoodmartTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/test/FoodmartTest.java b/core/src/test/java/net/hydromatic/optiq/test/FoodmartTest.java
index ff8432c..852a106 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/FoodmartTest.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/FoodmartTest.java
@@ -184,7 +184,7 @@ public class FoodmartTest {
     /** Returns the singleton instance of the query set. It is backed by a
      * soft reference, so it may be freed if memory is short and no one is
      * using it. */
-    public static FoodMartQuerySet instance() throws IOException {
+    public static synchronized FoodMartQuerySet instance() throws IOException {
       final SoftReference<FoodMartQuerySet> refLocal = ref;
       if (refLocal != null) {
         final FoodMartQuerySet set = refLocal.get();

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/test/java/net/hydromatic/optiq/test/JdbcTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/test/JdbcTest.java b/core/src/test/java/net/hydromatic/optiq/test/JdbcTest.java
index 2efcbaa..6730332 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/JdbcTest.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/JdbcTest.java
@@ -956,7 +956,8 @@ public class JdbcTest {
 
   @Test public void testCloneSchema()
       throws ClassNotFoundException, SQLException {
-    final OptiqConnection connection = OptiqAssert.getConnection(false);
+    final OptiqConnection connection =
+        OptiqAssert.getConnection(OptiqAssert.SchemaSpec.JDBC_FOODMART);
     final SchemaPlus rootSchema = connection.getRootSchema();
     final SchemaPlus foodmart = rootSchema.getSubSchema("foodmart");
     rootSchema.add("foodmart2", new CloneSchema(foodmart));
@@ -2054,6 +2055,28 @@ public class JdbcTest {
         .runs();
   }
 
+  /** Tests that a relatively complex query on the foodmart schema creates
+   * an in-memory aggregate table and then uses it. */
+  @Test public void testFoodmartLattice() throws IOException {
+    // 8: select ... from customer, sales, time ... group by ...
+    final FoodmartTest.FoodmartQuery query =
+        FoodmartTest.FoodMartQuerySet.instance().queries.get(8);
+    OptiqAssert.that()
+        .with(OptiqAssert.Config.JDBC_FOODMART_WITH_LATTICE)
+        .pooled()
+        .withSchema("foodmart")
+        .query(query.sql)
+        .enableMaterializations(true)
+        .explainContains(
+            "EnumerableCalcRel(expr#0..8=[{inputs}], c0=[$t3], c1=[$t2], c2=[$t1], c3=[$t0], c4=[$t8], c5=[$t8], c6=[$t6], c7=[$t4], c8=[$t7], c9=[$t5])\n"
+            + "  EnumerableSortRel(sort0=[$3], sort1=[$2], sort2=[$1], sort3=[$8], dir0=[ASC-nulls-last], dir1=[ASC-nulls-last], dir2=[ASC-nulls-last], dir3=[ASC-nulls-last])\n"
+            + "    EnumerableCalcRel(expr#0..8=[{inputs}], expr#9=['%Jeanne%'], expr#10=[LIKE($t8, $t9)], proj#0..8=[{exprs}], $condition=[$t10])\n"
+            + "      EnumerableAggregateRel(group=[{0, 1, 2, 3, 4, 5, 6, 7, 8}])\n"
+            + "        EnumerableCalcRel(expr#0..9=[{inputs}], expr#10=[CAST($t0):INTEGER], expr#11=[1997], expr#12=[=($t10, $t11)], $f0=[$t1], $f1=[$t2], $f2=[$t3], $f3=[$t4], $f4=[$t5], $f5=[$t6], $f6=[$t7], $f7=[$t8], $f8=[$t9], $f9=[$t0], $condition=[$t12])\n"
+            + "          EnumerableTableAccessRel(table=[[foodmart, m{12, 18, 27, 28, 30, 35, 36, 37, 40, 46}]])")
+        .runs();
+  }
+
   /** Test case for (not yet fixed)
    * <a href="https://issues.apache.org/jira/browse/OPTIQ-99">OPTIQ-99</a>,
    * "Recognize semi-join that has high selectivity and push it down". */
@@ -5847,7 +5870,8 @@ public class JdbcTest {
   }
 
   @Test public void testSchemaCaching() throws Exception {
-    final OptiqConnection connection = OptiqAssert.getConnection(false);
+    final OptiqConnection connection =
+        OptiqAssert.getConnection(OptiqAssert.SchemaSpec.JDBC_FOODMART);
     final SchemaPlus rootSchema = connection.getRootSchema();
 
     // create schema "/a"

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/test/java/net/hydromatic/optiq/test/LatticeTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/test/LatticeTest.java b/core/src/test/java/net/hydromatic/optiq/test/LatticeTest.java
index c11b574..703ad9b 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/LatticeTest.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/LatticeTest.java
@@ -198,15 +198,12 @@ public class LatticeTest {
                 assertThat(s,
                     anyOf(
                         containsString(
-                            "AggregateRel(group=[{0, 1}])\n"
-                            + "  ProjectRel(brand_name=[$10], customer_id=[$2])\n"
-                            + "    ProjectRel($f0=[$0], $f1=[$1], $f2=[$2], $f3=[$3], $f4=[$4], $f5=[$5], $f6=[$6], $f7=[$7], $f8=[$8], $f9=[$9], $f10=[$10], $f11=[$11], $f12=[$12], $f13=[$13], $f14=[$14], $f15=[$15], $f16=[$16], $f17=[$17], $f18=[$18], $f19=[$19], $f20=[$20], $f21=[$21], $f22=[$22])\n"
-                            + "      TableAccessRel(table=[[adhoc, star]])\n"),
+                            "ProjectRel($f0=[$1], $f1=[$0])\n"
+                            + "  AggregateRel(group=[{2, 10}])\n"
+                            + "    TableAccessRel(table=[[adhoc, star]])\n"),
                         containsString(
-                            "AggregateRel(group=[{0, 1}])\n"
-                            + "  ProjectRel(customer_id=[$2], brand_name=[$10])\n"
-                            + "    ProjectRel($f0=[$0], $f1=[$1], $f2=[$2], $f3=[$3], $f4=[$4], $f5=[$5], $f6=[$6], $f7=[$7], $f8=[$8], $f9=[$9], $f10=[$10], $f11=[$11], $f12=[$12], $f13=[$13], $f14=[$14], $f15=[$15], $f16=[$16], $f17=[$17], $f18=[$18], $f19=[$19], $f20=[$20], $f21=[$21], $f22=[$22])\n"
-                            + "      TableAccessRel(table=[[adhoc, star]])\n")));
+                            "AggregateRel(group=[{2, 10}])\n"
+                            + "  TableAccessRel(table=[[adhoc, star]])\n")));
                 return null;
               }
             });

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/test/java/net/hydromatic/optiq/test/LinqFrontJdbcBackTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/test/LinqFrontJdbcBackTest.java b/core/src/test/java/net/hydromatic/optiq/test/LinqFrontJdbcBackTest.java
index 56146d1..a7218e2 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/LinqFrontJdbcBackTest.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/LinqFrontJdbcBackTest.java
@@ -37,7 +37,7 @@ public class LinqFrontJdbcBackTest {
   @Test public void testTableWhere() throws SQLException,
       ClassNotFoundException {
     final OptiqConnection connection =
-        OptiqAssert.getConnection(false);
+        OptiqAssert.getConnection(OptiqAssert.SchemaSpec.JDBC_FOODMART);
     final SchemaPlus schema =
         connection.getRootSchema().getSubSchema("foodmart");
     ParameterExpression c =

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/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 5a5b9a7..24909e8 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java
@@ -25,6 +25,8 @@ import net.hydromatic.optiq.impl.java.ReflectiveSchema;
 import net.hydromatic.optiq.impl.jdbc.JdbcSchema;
 import net.hydromatic.optiq.jdbc.MetaImpl;
 import net.hydromatic.optiq.jdbc.OptiqConnection;
+import net.hydromatic.optiq.jdbc.OptiqSchema;
+import net.hydromatic.optiq.materialize.Lattice;
 import net.hydromatic.optiq.runtime.Hook;
 
 import org.eigenbase.rel.RelNode;
@@ -566,6 +568,7 @@ public class OptiqAssert {
   }
 
   public static SchemaPlus addSchema(SchemaPlus rootSchema, SchemaSpec schema) {
+    SchemaPlus foodmart;
     switch (schema) {
     case REFLECTIVE_FOODMART:
       return rootSchema.add("foodmart",
@@ -580,8 +583,21 @@ public class OptiqAssert {
       return rootSchema.add("foodmart",
           JdbcSchema.create(rootSchema, "foodmart", dataSource, null,
               "foodmart"));
+    case JDBC_FOODMART_WITH_LATTICE:
+      foodmart = rootSchema.getSubSchema("foodmart");
+      if (foodmart == null) {
+        foodmart = OptiqAssert.addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
+      }
+      foodmart.add("lattice",
+          Lattice.create(foodmart.unwrap(OptiqSchema.class),
+              "select 1 from \"foodmart\".\"sales_fact_1997\" as s\n"
+              + "join \"foodmart\".\"time_by_day\" as t using (\"time_id\")\n"
+              + "join \"foodmart\".\"customer\" as c using (\"customer_id\")\n"
+              + "join \"foodmart\".\"product\" as p using (\"product_id\")\n"
+              + "join \"foodmart\".\"product_class\" as pc on p.\"product_class_id\" = pc.\"product_class_id\""));
+      return foodmart;
     case CLONE_FOODMART:
-      SchemaPlus foodmart = rootSchema.getSubSchema("foodmart");
+      foodmart = rootSchema.getSubSchema("foodmart");
       if (foodmart == null) {
         foodmart = OptiqAssert.addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
       }
@@ -666,22 +682,30 @@ public class OptiqAssert {
    * uses the connection as its own provider. The connection contains a
    * schema called "foodmart" backed by a JDBC connection to MySQL.
    *
-   * @param withClone Whether to create a "foodmart2" schema as in-memory
-   *     clone
+   * @param schemaSpec Schema specification; whether to create a "foodmart2"
+   *     schema as in-memory clone
    * @return Connection
    * @throws ClassNotFoundException
    * @throws java.sql.SQLException
    */
-  static OptiqConnection getConnection(boolean withClone)
+  static OptiqConnection getConnection(SchemaSpec schemaSpec)
       throws ClassNotFoundException, SQLException {
     Class.forName("net.hydromatic.optiq.jdbc.Driver");
     Connection connection = DriverManager.getConnection("jdbc:optiq:");
     OptiqConnection optiqConnection =
         connection.unwrap(OptiqConnection.class);
     final SchemaPlus rootSchema = optiqConnection.getRootSchema();
-    addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
-    if (withClone) {
-      addSchema(rootSchema, SchemaSpec.CLONE_FOODMART);
+    switch (schemaSpec) {
+    case JDBC_FOODMART:
+      addSchema(rootSchema, schemaSpec);
+      break;
+    case CLONE_FOODMART:
+    case JDBC_FOODMART_WITH_LATTICE:
+      addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
+      addSchema(rootSchema, schemaSpec);
+      break;
+    default:
+      throw new AssertionError("unknown schema " + schemaSpec);
     }
     optiqConnection.setSchema("foodmart2");
     return optiqConnection;
@@ -884,37 +908,6 @@ public class OptiqAssert {
     OptiqConnection createConnection() throws Exception;
   }
 
-  private static class MemoizingConnectionFactory implements ConnectionFactory {
-    private final Supplier<OptiqConnection> supplier;
-
-    public MemoizingConnectionFactory(final ConnectionFactory factory) {
-      super();
-      this.supplier = Suppliers.memoize(
-          new Supplier<OptiqConnection>() {
-            public OptiqConnection get() {
-              try {
-                return factory.createConnection();
-              } catch (Exception e) {
-                throw new RuntimeException(e);
-              }
-            }
-          });
-    }
-
-    public OptiqConnection createConnection() throws Exception {
-      try {
-        return supplier.get();
-      } catch (RuntimeException e) {
-        if (e.getClass() == RuntimeException.class
-            && e.getCause() instanceof Exception
-            && e.getCause() != e) {
-          throw (Exception) e.getCause();
-        }
-        throw e;
-      }
-    }
-  }
-
   private static class PoolingConnectionFactory implements ConnectionFactory {
     private final ConnectionFactory factory;
 
@@ -966,9 +959,11 @@ public class OptiqAssert {
       case LINGUAL:
         return getConnection("lingual");
       case JDBC_FOODMART:
-        return getConnection(false);
+        return getConnection(OptiqAssert.SchemaSpec.JDBC_FOODMART);
       case FOODMART_CLONE:
-        return getConnection(true);
+        return getConnection(SchemaSpec.CLONE_FOODMART);
+      case JDBC_FOODMART_WITH_LATTICE:
+        return getConnection(SchemaSpec.JDBC_FOODMART_WITH_LATTICE);
       case SPARK:
         return getConnection("spark");
       default:
@@ -1261,6 +1256,10 @@ public class OptiqAssert {
      * database. */
     FOODMART_CLONE,
 
+    /** Configuration that contains an in-memory clone of the FoodMart
+     * database, plus a lattice to enable on-the-fly materializations. */
+    JDBC_FOODMART_WITH_LATTICE,
+
     /** Configuration that includes the metadata schema. */
     REGULAR_PLUS_METADATA,
 
@@ -1351,6 +1350,7 @@ public class OptiqAssert {
     REFLECTIVE_FOODMART,
     JDBC_FOODMART,
     CLONE_FOODMART,
+    JDBC_FOODMART_WITH_LATTICE,
     HR,
     LINGUAL,
     POST

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/test/java/net/hydromatic/optiq/util/graph/DirectedGraphTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/util/graph/DirectedGraphTest.java b/core/src/test/java/net/hydromatic/optiq/util/graph/DirectedGraphTest.java
index 9d5ff6a..6ff7d35 100644
--- a/core/src/test/java/net/hydromatic/optiq/util/graph/DirectedGraphTest.java
+++ b/core/src/test/java/net/hydromatic/optiq/util/graph/DirectedGraphTest.java
@@ -18,6 +18,7 @@ package net.hydromatic.optiq.util.graph;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 
 import org.hamcrest.CoreMatchers;
 
@@ -25,6 +26,7 @@ import org.junit.Test;
 
 import java.util.*;
 
+import static org.hamcrest.CoreMatchers.equalTo;
 import static org.junit.Assert.*;
 
 /**
@@ -117,7 +119,10 @@ public class DirectedGraphTest {
     for (String s : DepthFirstIterator.of(graph, "A")) {
       list.add(s);
     }
-    assertEquals("[A, B, C, D, E, C, D, F]", list.toString());
+    assertThat(list.toString(), equalTo("[A, B, C, D, E, C, D, F]"));
+    list.clear();
+    DepthFirstIterator.reachable(list, graph, "A");
+    assertThat(list.toString(), equalTo("[A, B, C, D, E, C, D, F]"));
   }
 
   /** Unit test for {@link DepthFirstIterator}. */
@@ -274,9 +279,10 @@ public class DirectedGraphTest {
    * {@link net.hydromatic.optiq.util.graph.BreadthFirstIterator}. */
   @Test public void testBreadthFirstIterator() {
     DefaultDirectedGraph<String, DefaultEdge> graph = createDag();
-    assertThat(getA(graph, "A"),
-        CoreMatchers.<List<String>>equalTo(
-            ImmutableList.of("A", "B", "E", "C", "F", "D")));
+    final List<String> expected =
+        ImmutableList.of("A", "B", "E", "C", "F", "D");
+    assertThat(getA(graph, "A"), equalTo(expected));
+    assertThat(Lists.newArrayList(getB(graph, "A")), equalTo(expected));
   }
 
   private List<String> getA(DefaultDirectedGraph<String, DefaultEdge> graph,
@@ -288,6 +294,13 @@ public class DirectedGraphTest {
     return list;
   }
 
+  private Set<String> getB(DefaultDirectedGraph<String, DefaultEdge> graph,
+      String root) {
+    final Set<String> list = new LinkedHashSet<String>();
+    BreadthFirstIterator.reachable(list, graph, root);
+    return list;
+  }
+
 }
 
 // End DirectedGraphTest.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/1d2cb011/core/src/test/java/org/eigenbase/util/mapping/MappingTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/eigenbase/util/mapping/MappingTest.java b/core/src/test/java/org/eigenbase/util/mapping/MappingTest.java
index ea57ebb..65a9d61 100644
--- a/core/src/test/java/org/eigenbase/util/mapping/MappingTest.java
+++ b/core/src/test/java/org/eigenbase/util/mapping/MappingTest.java
@@ -143,13 +143,10 @@ public class MappingTest {
     final List<Integer> integers = Mappings.asList(mapping);
     assertThat(integers, equalTo(targets));
 
-    try {
-      final Mapping inverse = mapping.inverse();
-      fail("expected exception, got " + inverse);
-    } catch (UnsupportedOperationException e) {
-      // ok... but we'd prefer if that inverse succeeds if the mapping is
-      // invertible
-    }
+    final Mapping inverse = mapping.inverse();
+    assertThat(inverse.toString(),
+        equalTo(
+            "[size=5, sourceCount=10, targetCount=5, elements=[1:1, 3:0, 4:2, 5:3, 8:4]]"));
   }
 
   /** Unit test for {@link Mappings#target(List, int)}. */