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/08/29 20:04:08 UTC

git commit: [OPTIQ-344] Lattice data structure

Repository: incubator-optiq
Updated Branches:
  refs/heads/master 793e5c4d2 -> acee9632f


[OPTIQ-344] Lattice data structure


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

Branch: refs/heads/master
Commit: acee9632f113d0523928add6032d3c4c41944624
Parents: 793e5c4
Author: Julian Hyde <jh...@apache.org>
Authored: Mon Jul 14 01:31:33 2014 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Fri Aug 29 02:15:14 2014 -0700

----------------------------------------------------------------------
 .../java/net/hydromatic/optiq/SchemaPlus.java   |   5 +
 .../main/java/net/hydromatic/optiq/Schemas.java |  72 ++++++
 .../net/hydromatic/optiq/impl/StarTable.java    |  25 +-
 .../net/hydromatic/optiq/jdbc/OptiqPrepare.java |  17 +-
 .../net/hydromatic/optiq/jdbc/OptiqSchema.java  |  62 +++++
 .../hydromatic/optiq/materialize/Lattice.java   | 226 +++++++++++++++++++
 .../net/hydromatic/optiq/model/JsonLattice.java |  39 ++++
 .../net/hydromatic/optiq/model/JsonSchema.java  |  12 +-
 .../hydromatic/optiq/model/ModelHandler.java    |  18 ++
 .../optiq/prepare/OptiqMaterializer.java        |  71 +++---
 .../optiq/prepare/OptiqPrepareImpl.java         |  37 ++-
 .../net/hydromatic/optiq/prepare/Prepare.java   |  29 ++-
 .../java/net/hydromatic/optiq/runtime/Hook.java |   9 +
 .../net/hydromatic/optiq/util/Compatible.java   |   9 +
 .../optiq/util/graph/DefaultDirectedGraph.java  |   7 +-
 .../org/eigenbase/relopt/RelOptLattice.java     |  57 +++++
 .../org/eigenbase/relopt/RelOptPlanner.java     |   8 +
 .../org/eigenbase/relopt/hep/HepPlanner.java    |   4 +
 .../relopt/volcano/VolcanoPlanner.java          |  44 +++-
 .../net/hydromatic/optiq/test/LatticeTest.java  | 186 +++++++++++++++
 .../net/hydromatic/optiq/test/ModelTest.java    |  47 ++++
 .../net/hydromatic/optiq/test/OptiqAssert.java  |  88 ++++++--
 .../net/hydromatic/optiq/test/OptiqSuite.java   |   1 +
 .../org/eigenbase/test/MockRelOptPlanner.java   |   4 +
 .../test/java/org/eigenbase/util/TestUtil.java  |  36 +++
 25 files changed, 1039 insertions(+), 74 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/SchemaPlus.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/SchemaPlus.java b/core/src/main/java/net/hydromatic/optiq/SchemaPlus.java
index 6a476df..91ca5ca 100644
--- a/core/src/main/java/net/hydromatic/optiq/SchemaPlus.java
+++ b/core/src/main/java/net/hydromatic/optiq/SchemaPlus.java
@@ -16,6 +16,8 @@
  */
 package net.hydromatic.optiq;
 
+import net.hydromatic.optiq.materialize.Lattice;
+
 import com.google.common.collect.ImmutableList;
 
 /**
@@ -57,6 +59,9 @@ public interface SchemaPlus extends Schema {
   /** Adds a function to this schema. */
   void add(String name, Function function);
 
+  /** Adds a lattice to this schema. */
+  void add(String name, Lattice lattice);
+
   boolean isMutable();
 
   /** Returns an underlying object. */

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/Schemas.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/Schemas.java b/core/src/main/java/net/hydromatic/optiq/Schemas.java
index 45f32eb..3f279c5 100644
--- a/core/src/main/java/net/hydromatic/optiq/Schemas.java
+++ b/core/src/main/java/net/hydromatic/optiq/Schemas.java
@@ -23,6 +23,7 @@ import net.hydromatic.linq4j.expressions.*;
 import net.hydromatic.optiq.config.OptiqConnectionConfig;
 import net.hydromatic.optiq.impl.java.JavaTypeFactory;
 import net.hydromatic.optiq.jdbc.*;
+import net.hydromatic.optiq.materialize.Lattice;
 
 import org.eigenbase.reltype.RelDataType;
 import org.eigenbase.reltype.RelDataTypeFactory;
@@ -30,6 +31,7 @@ import org.eigenbase.reltype.RelProtoDataType;
 import org.eigenbase.sql.type.SqlTypeUtil;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
 
 import java.lang.reflect.*;
 import java.sql.Connection;
@@ -39,6 +41,26 @@ import java.util.*;
  * Utility functions for schemas.
  */
 public final class Schemas {
+  private static final com.google.common.base.Function<OptiqSchema.LatticeEntry,
+      OptiqSchema.TableEntry> TO_TABLE_ENTRY =
+      new com.google.common.base.Function<OptiqSchema.LatticeEntry,
+          OptiqSchema.TableEntry>() {
+        public OptiqSchema.TableEntry apply(OptiqSchema.LatticeEntry entry) {
+          final OptiqSchema.TableEntry starTable = entry.getStarTable();
+          assert starTable.getTable().getJdbcTableType()
+              == Schema.TableType.STAR;
+          return entry.getStarTable();
+        }
+      };
+
+  private static final com.google.common.base.Function<OptiqSchema.LatticeEntry,
+      Lattice> TO_LATTICE =
+      new com.google.common.base.Function<OptiqSchema.LatticeEntry, Lattice>() {
+        public Lattice apply(OptiqSchema.LatticeEntry entry) {
+          return entry.getLattice();
+        }
+      };
+
   private Schemas() {
     throw new AssertionError("no instances!");
   }
@@ -178,6 +200,22 @@ public final class Schemas {
     }
   }
 
+  /** Parses and validates a SQL query and converts to relational algebra. For
+   * use within Optiq only. */
+  public static OptiqPrepare.ConvertResult convert(
+      final OptiqConnection connection, final OptiqSchema schema,
+      final List<String> schemaPath, final String sql) {
+    final OptiqPrepare prepare = OptiqPrepare.DEFAULT_FACTORY.apply();
+    final OptiqPrepare.Context context =
+        makeContext(connection, schema, schemaPath);
+    OptiqPrepare.Dummy.push(context);
+    try {
+      return prepare.convert(context, sql);
+    } finally {
+      OptiqPrepare.Dummy.pop(context);
+    }
+  }
+
   /** Prepares a SQL query for execution. For use within Optiq only. */
   public static OptiqPrepare.PrepareResult<Object> prepare(
       final OptiqConnection connection, final OptiqSchema schema,
@@ -262,6 +300,40 @@ public final class Schemas {
     };
   }
 
+  /** Returns the star tables defined in a schema.
+   *
+   * @param schema Schema */
+  public static List<OptiqSchema.TableEntry> getStarTables(OptiqSchema schema) {
+    final List<OptiqSchema.LatticeEntry> list = getLatticeEntries(schema);
+    return Lists.transform(list, TO_TABLE_ENTRY);
+  }
+
+  /** Returns the lattices defined in a schema.
+   *
+   * @param schema Schema */
+  public static List<Lattice> getLattices(OptiqSchema schema) {
+    final List<OptiqSchema.LatticeEntry> list = getLatticeEntries(schema);
+    return Lists.transform(list, TO_LATTICE);
+  }
+
+  /** Returns the lattices defined in a schema.
+   *
+   * @param schema Schema */
+  public static List<OptiqSchema.LatticeEntry> getLatticeEntries(
+      OptiqSchema schema) {
+    final List<OptiqSchema.LatticeEntry> list = Lists.newArrayList();
+    gatherLattices(schema, list);
+    return list;
+  }
+
+  private static void gatherLattices(OptiqSchema schema,
+      List<OptiqSchema.LatticeEntry> list) {
+    list.addAll(schema.getLatticeMap().values());
+    for (OptiqSchema subSchema : schema.getSubSchemaMap().values()) {
+      gatherLattices(subSchema, list);
+    }
+  }
+
   /** Dummy data context that has no variables. */
   private static class DummyDataContext implements DataContext {
     private final OptiqConnection connection;

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/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 33ee4d3..1e8b2a3 100644
--- a/core/src/main/java/net/hydromatic/optiq/impl/StarTable.java
+++ b/core/src/main/java/net/hydromatic/optiq/impl/StarTable.java
@@ -17,8 +17,10 @@
 package net.hydromatic.optiq.impl;
 
 import net.hydromatic.optiq.Table;
+import net.hydromatic.optiq.TranslatableTable;
 
-import org.eigenbase.relopt.RelOptUtil;
+import org.eigenbase.rel.*;
+import org.eigenbase.relopt.*;
 import org.eigenbase.reltype.RelDataType;
 import org.eigenbase.reltype.RelDataTypeFactory;
 import org.eigenbase.sql.validate.SqlValidatorUtil;
@@ -41,7 +43,7 @@ import java.util.List;
  * to a query on top of a star table. Queries that are candidates to map onto
  * the materialization are mapped onto the same star table.</p>
  */
-public class StarTable extends AbstractTable {
+public class StarTable extends AbstractTable implements TranslatableTable {
   // TODO: we'll also need a list of join conditions between tables. For now
   //  we assume that join conditions match
   public final ImmutableList<Table> tables;
@@ -79,6 +81,11 @@ public class StarTable extends AbstractTable {
         SqlValidatorUtil.uniquify(nameList));
   }
 
+  public RelNode toRel(RelOptTable.ToRelContext context, RelOptTable table) {
+    // Create a table scan of infinite cost.
+    return new StarTableScan(context.getCluster(), table);
+  }
+
   public StarTable add(Table table) {
     final List<Table> tables1 = new ArrayList<Table>(tables);
     tables1.add(table);
@@ -103,6 +110,20 @@ public class StarTable extends AbstractTable {
     throw new IllegalArgumentException("star table " + this
         + " does not contain table " + table);
   }
+
+  /** Relational expression that scans a {@link StarTable}.
+   *
+   * <p>It has infinite cost.
+   */
+  private static class StarTableScan extends TableAccessRelBase {
+    public StarTableScan(RelOptCluster cluster, RelOptTable relOptTable) {
+      super(cluster, cluster.traitSetOf(Convention.NONE), relOptTable);
+    }
+
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner) {
+      return planner.getCostFactory().makeInfiniteCost();
+    }
+  }
 }
 
 // End StarTable.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqPrepare.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqPrepare.java b/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqPrepare.java
index 4663ca9..f9f062c 100644
--- a/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqPrepare.java
+++ b/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqPrepare.java
@@ -60,8 +60,9 @@ public interface OptiqPrepare {
         }
       };
 
-  ParseResult parse(
-      Context context, String sql);
+  ParseResult parse(Context context, String sql);
+
+  ConvertResult convert(Context context, String sql);
 
   <T> PrepareResult<T> prepareSql(
       Context context,
@@ -202,6 +203,18 @@ public interface OptiqPrepare {
     }
   }
 
+  /** The result of parsing and validating a SQL query and converting it to
+   * relational algebra. */
+  public static class ConvertResult extends ParseResult {
+    public final RelNode relNode;
+
+    public ConvertResult(OptiqPrepareImpl prepare, SqlValidator validator,
+        String sql, SqlNode sqlNode, RelDataType rowType, RelNode relNode) {
+      super(prepare, validator, sql, sqlNode, rowType);
+      this.relNode = relNode;
+    }
+  }
+
   /** The result of preparing a query. It gives the Avatica driver framework
    * the information it needs to create a prepared statement, or to execute a
    * statement directly, without an explicit prepare step. */

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqSchema.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqSchema.java b/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqSchema.java
index 9d7fb80..b97b06b 100644
--- a/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqSchema.java
+++ b/core/src/main/java/net/hydromatic/optiq/jdbc/OptiqSchema.java
@@ -22,6 +22,8 @@ import net.hydromatic.linq4j.expressions.Expression;
 import net.hydromatic.optiq.*;
 import net.hydromatic.optiq.Table;
 import net.hydromatic.optiq.impl.MaterializedViewTable;
+import net.hydromatic.optiq.impl.StarTable;
+import net.hydromatic.optiq.materialize.Lattice;
 import net.hydromatic.optiq.util.Compatible;
 
 import org.eigenbase.util.Pair;
@@ -62,6 +64,8 @@ public class OptiqSchema {
       new TreeMap<String, TableEntry>(COMPARATOR);
   private final Multimap<String, FunctionEntry> functionMap =
       LinkedListMultimap.create();
+  private final NavigableMap<String, LatticeEntry> latticeMap =
+      new TreeMap<String, LatticeEntry>(COMPARATOR);
   private final NavigableSet<String> functionNames =
       new TreeSet<String>(COMPARATOR);
   private final NavigableMap<String, FunctionEntry> nullaryFunctionMap =
@@ -137,6 +141,15 @@ public class OptiqSchema {
     return entry;
   }
 
+  private LatticeEntry add(String name, Lattice lattice) {
+    if (latticeMap.containsKey(name)) {
+      throw new RuntimeException("Duplicate lattice '" + name + "'");
+    }
+    final LatticeEntryImpl entry = new LatticeEntryImpl(this, name, lattice);
+    latticeMap.put(name, entry);
+    return entry;
+  }
+
   public OptiqRootSchema root() {
     for (OptiqSchema schema = this;;) {
       if (schema.parent == null) {
@@ -301,6 +314,13 @@ public class OptiqSchema {
     return Compatible.INSTANCE.navigableMap(builder.build());
   }
 
+  /** Returns a collection of lattices.
+   *
+   * <p>All are explicit (defined using {@link #add(String, Lattice)}). */
+  public NavigableMap<String, LatticeEntry> getLatticeMap() {
+    return Compatible.INSTANCE.immutableNavigableMap(latticeMap);
+  }
+
   /** Returns the set of all table names. Includes implicit and explicit tables
    * and functions with zero parameters. */
   public NavigableSet<String> getTableNames() {
@@ -502,6 +522,17 @@ public class OptiqSchema {
     public abstract boolean isMaterialization();
   }
 
+  /** Membership of a lattice in a schema. */
+  public abstract static class LatticeEntry extends Entry {
+    public LatticeEntry(OptiqSchema schema, String name) {
+      super(schema, name);
+    }
+
+    public abstract Lattice getLattice();
+
+    public abstract TableEntry getStarTable();
+  }
+
   /** Implementation of {@link SchemaPlus} based on an {@code OptiqSchema}. */
   private class SchemaPlusImpl implements SchemaPlus {
     public OptiqSchema optiqSchema() {
@@ -591,6 +622,10 @@ public class OptiqSchema {
     public void add(String name, net.hydromatic.optiq.Function function) {
       OptiqSchema.this.add(name, function);
     }
+
+    public void add(String name, Lattice lattice) {
+      OptiqSchema.this.add(name, lattice);
+    }
   }
 
   /**
@@ -636,6 +671,33 @@ public class OptiqSchema {
     }
   }
 
+  /**
+   * Implementation of {@link LatticeEntry}
+   * where all properties are held in fields.
+   */
+  public static class LatticeEntryImpl extends LatticeEntry {
+    private final Lattice lattice;
+    private final OptiqSchema.TableEntry starTableEntry;
+
+    /** Creates a LatticeEntryImpl. */
+    public LatticeEntryImpl(OptiqSchema schema, String name, Lattice lattice) {
+      super(schema, name);
+      this.lattice = lattice;
+
+      // Star table has same name as lattice and is in same schema.
+      final StarTable starTable = lattice.createStarTable();
+      starTableEntry = schema.add(name, starTable);
+    }
+
+    public Lattice getLattice() {
+      return lattice;
+    }
+
+    public TableEntry getStarTable() {
+      return starTableEntry;
+    }
+  }
+
   /** Strategy for caching the value of an object and re-creating it if its
    * value is out of date as of a given timestamp.
    *

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/materialize/Lattice.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/materialize/Lattice.java b/core/src/main/java/net/hydromatic/optiq/materialize/Lattice.java
new file mode 100644
index 0000000..984b157
--- /dev/null
+++ b/core/src/main/java/net/hydromatic/optiq/materialize/Lattice.java
@@ -0,0 +1,226 @@
+/*
+ * 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 net.hydromatic.optiq.materialize;
+
+import net.hydromatic.optiq.Schemas;
+import net.hydromatic.optiq.Table;
+import net.hydromatic.optiq.impl.MaterializedViewTable;
+import net.hydromatic.optiq.impl.StarTable;
+import net.hydromatic.optiq.jdbc.OptiqPrepare;
+import net.hydromatic.optiq.jdbc.OptiqSchema;
+import net.hydromatic.optiq.util.graph.*;
+
+import org.eigenbase.rel.*;
+import org.eigenbase.relopt.RelOptUtil;
+import org.eigenbase.rex.*;
+import org.eigenbase.util.mapping.IntPair;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.*;
+
+import java.util.*;
+
+/**
+ * Structure that allows materialized views based upon a star schema to be
+ * recognized and recommended.
+ */
+public class Lattice {
+  public final ImmutableList<Node> nodes;
+
+  private Lattice(List<Node> nodes) {
+    this.nodes = ImmutableList.copyOf(nodes);
+
+    // Validate that nodes form a tree; each node except the first references
+    // a predecessor.
+    for (int i = 0; i < nodes.size(); i++) {
+      Node node = nodes.get(i);
+      if (i == 0) {
+        assert node.parent == null;
+      } else {
+        assert nodes.subList(0, i).contains(node.parent);
+      }
+    }
+  }
+
+  /** Creates a Lattice. */
+  public static Lattice create(OptiqSchema schema, String sql) {
+    OptiqPrepare.ConvertResult parsed =
+        Schemas.convert(MaterializedViewTable.MATERIALIZATION_CONNECTION,
+            schema, schema.path(null), sql);
+
+    // Walk the join tree.
+    List<RelNode> relNodes = Lists.newArrayList();
+    List<int[][]> tempLinks = Lists.newArrayList();
+    populate(relNodes, tempLinks, parsed.relNode);
+
+    // Build a graph.
+    final DirectedGraph<RelNode, Edge> graph =
+        DefaultDirectedGraph.create(Edge.FACTORY);
+    for (RelNode node : relNodes) {
+      graph.addVertex(node);
+    }
+    for (int[][] tempLink : tempLinks) {
+      final RelNode source = relNodes.get(tempLink[0][0]);
+      final RelNode target = relNodes.get(tempLink[1][0]);
+      Edge edge = graph.getEdge(source, target);
+      if (edge == null) {
+        edge = graph.addEdge(source, target);
+      }
+      edge.pairs.add(IntPair.of(tempLink[0][1], tempLink[1][1]));
+    }
+
+    // Convert the graph into a tree of nodes, each connected to a parent and
+    // with a join condition to that parent.
+    List<Node> nodes = Lists.newArrayList();
+    Node previous = null;
+    final Map<RelNode, Node> map = Maps.newIdentityHashMap();
+    for (RelNode relNode : TopologicalOrderIterator.of(graph)) {
+      final List<Edge> edges = graph.getInwardEdges(relNode);
+      Node node;
+      if (previous == null) {
+        if (!edges.isEmpty()) {
+          throw new RuntimeException("root node must not have relationships: "
+              + relNode);
+        }
+        node = new Node((TableAccessRelBase) relNode, null, null);
+      } else {
+        if (edges.size() != 1) {
+          throw new RuntimeException(
+              "child node must have precisely one parent: " + relNode);
+        }
+        final Edge edge = edges.get(0);
+        node =
+            new Node((TableAccessRelBase) relNode, map.get(edge.getSource()),
+                edge.pairs);
+      }
+      nodes.add(node);
+      map.put(relNode, node);
+      previous = node;
+    }
+    return new Lattice(nodes);
+  }
+
+  private static boolean populate(List<RelNode> nodes, List<int[][]> tempLinks,
+      RelNode rel) {
+    if (nodes.isEmpty() && rel instanceof ProjectRel) {
+      return populate(nodes, tempLinks, ((ProjectRel) rel).getChild());
+    }
+    if (rel instanceof TableAccessRelBase) {
+      nodes.add(rel);
+      return true;
+    }
+    if (rel instanceof JoinRel) {
+      JoinRel join = (JoinRel) rel;
+      if (join.getJoinType() != JoinRelType.INNER) {
+        throw new RuntimeException("only inner join allowed, but got "
+            + join.getJoinType());
+      }
+      populate(nodes, tempLinks, join.getLeft());
+      populate(nodes, tempLinks, join.getRight());
+      for (RexNode rex : RelOptUtil.conjunctions(join.getCondition())) {
+        tempLinks.add(grab(nodes, rex));
+      }
+      return true;
+    }
+    throw new RuntimeException("Invalid node type "
+        + rel.getClass().getSimpleName() + " in lattice query");
+  }
+
+  /** Converts an "t1.c1 = t2.c2" expression into two (input, field) pairs. */
+  private static int[][] grab(List<RelNode> leaves, RexNode rex) {
+    switch (rex.getKind()) {
+    case EQUALS:
+      break;
+    default:
+      throw new AssertionError("only equi-join allowed");
+    }
+    final List<RexNode> operands = ((RexCall) rex).getOperands();
+    return new int[][] {
+        inputField(leaves, operands.get(0)),
+        inputField(leaves, operands.get(1))};
+  }
+
+  /** Converts an expression into an (input, field) pair. */
+  private static int[] inputField(List<RelNode> leaves, RexNode rex) {
+    if (!(rex instanceof RexInputRef)) {
+      throw new RuntimeException("only equi-join of columns allowed: " + rex);
+    }
+    RexInputRef ref = (RexInputRef) rex;
+    int start = 0;
+    for (int i = 0; i < leaves.size(); i++) {
+      final RelNode leaf = leaves.get(i);
+      final int end = start + leaf.getRowType().getFieldCount();
+      if (ref.getIndex() < end) {
+        return new int[] {i, ref.getIndex() - start};
+      }
+      start = end;
+    }
+    throw new AssertionError("input not found");
+  }
+
+  public StarTable createStarTable() {
+    final List<Table> tables = Lists.newArrayList();
+    for (Node node : nodes) {
+      tables.add(node.scan.getTable().unwrap(Table.class));
+    }
+    return new StarTable(tables);
+  }
+
+  /** Source relation of a lattice.
+   *
+   * <p>Relations form a tree; all relations except the root relation
+   * (the fact table) have precisely one parent and an equi-join
+   * condition on one or more pairs of columns linking to it. */
+  public static class Node {
+    public final TableAccessRelBase scan;
+    public final Node parent;
+    public final ImmutableList<IntPair> link;
+
+    public Node(TableAccessRelBase scan, Node parent, List<IntPair> link) {
+      this.scan = Preconditions.checkNotNull(scan);
+      this.parent = parent;
+      this.link = link == null ? null : ImmutableList.copyOf(link);
+      assert (parent == null) == (link == null);
+    }
+  }
+
+  /** Edge in the temporary graph. */
+  private static class Edge extends DefaultEdge {
+    public static final DirectedGraph.EdgeFactory<RelNode, Edge> FACTORY =
+        new DirectedGraph.EdgeFactory<RelNode, Edge>() {
+          public Edge createEdge(RelNode source, RelNode target) {
+            return new Edge(source, target);
+          }
+        };
+
+    final List<IntPair> pairs = Lists.newArrayList();
+
+    public Edge(RelNode source, RelNode target) {
+      super(source, target);
+    }
+
+    public RelNode getTarget() {
+      return (RelNode) target;
+    }
+
+    public RelNode getSource() {
+      return (RelNode) source;
+    }
+  }
+}
+
+// End Lattice.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/model/JsonLattice.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/model/JsonLattice.java b/core/src/main/java/net/hydromatic/optiq/model/JsonLattice.java
new file mode 100644
index 0000000..60abce0
--- /dev/null
+++ b/core/src/main/java/net/hydromatic/optiq/model/JsonLattice.java
@@ -0,0 +1,39 @@
+/*
+ * 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 net.hydromatic.optiq.model;
+
+/**
+ * Element that describes a star schema and provides a framework for defining,
+ * recognizing, and recommending materialized views at various levels of
+ * aggregation.
+ *
+ * @see JsonRoot Description of schema elements
+ */
+public class JsonLattice {
+  public String name;
+  public String sql;
+
+  public void accept(ModelHandler handler) {
+    handler.visit(this);
+  }
+
+  @Override public String toString() {
+    return "JsonLattice(name=" + name + ", sql=" + sql + ")";
+  }
+}
+
+// End JsonLattice.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/model/JsonSchema.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/model/JsonSchema.java b/core/src/main/java/net/hydromatic/optiq/model/JsonSchema.java
index e1e7cc4..f0921dd 100644
--- a/core/src/main/java/net/hydromatic/optiq/model/JsonSchema.java
+++ b/core/src/main/java/net/hydromatic/optiq/model/JsonSchema.java
@@ -19,7 +19,8 @@ package net.hydromatic.optiq.model;
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
-import java.util.ArrayList;
+import com.google.common.collect.Lists;
+
 import java.util.List;
 
 /**
@@ -42,8 +43,10 @@ public abstract class JsonSchema {
    * string-list. */
   public List<Object> path;
 
-  public List<JsonMaterialization> materializations =
-      new ArrayList<JsonMaterialization>();
+  public final List<JsonMaterialization> materializations =
+      Lists.newArrayList();
+
+  public final List<JsonLattice> lattices = Lists.newArrayList();
 
   /** Whether to cache metadata (tables, functions and sub-schemas) generated
    * by this schema. Default value is {@code true}.
@@ -71,6 +74,9 @@ public abstract class JsonSchema {
   public abstract void accept(ModelHandler handler);
 
   public void visitChildren(ModelHandler modelHandler) {
+    for (JsonLattice jsonLattice : lattices) {
+      jsonLattice.accept(modelHandler);
+    }
     for (JsonMaterialization jsonMaterialization : materializations) {
       jsonMaterialization.accept(modelHandler);
     }

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/model/ModelHandler.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/model/ModelHandler.java b/core/src/main/java/net/hydromatic/optiq/model/ModelHandler.java
index 624e914..91cd1bf 100644
--- a/core/src/main/java/net/hydromatic/optiq/model/ModelHandler.java
+++ b/core/src/main/java/net/hydromatic/optiq/model/ModelHandler.java
@@ -21,6 +21,7 @@ import net.hydromatic.optiq.impl.*;
 import net.hydromatic.optiq.impl.jdbc.JdbcSchema;
 import net.hydromatic.optiq.jdbc.OptiqConnection;
 import net.hydromatic.optiq.jdbc.OptiqSchema;
+import net.hydromatic.optiq.materialize.Lattice;
 
 import org.eigenbase.util.Pair;
 import org.eigenbase.util.Util;
@@ -242,6 +243,23 @@ public class ModelHandler {
     }
   }
 
+  public void visit(JsonLattice jsonLattice) {
+    try {
+      final SchemaPlus schema = currentSchema();
+      if (!schema.isMutable()) {
+        throw new RuntimeException(
+            "Cannot define lattice; parent schema '"
+            + currentSchemaName()
+            + "' is not a SemiMutableSchema");
+      }
+      OptiqSchema optiqSchema = OptiqSchema.from(schema);
+      schema.add(jsonLattice.name,
+          Lattice.create(optiqSchema, jsonLattice.sql));
+    } catch (Exception e) {
+      throw new RuntimeException("Error instantiating " + jsonLattice, e);
+    }
+  }
+
   public void visit(JsonCustomTable jsonTable) {
     try {
       final SchemaPlus schema = currentMutableSchema("table");

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/prepare/OptiqMaterializer.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/prepare/OptiqMaterializer.java b/core/src/main/java/net/hydromatic/optiq/prepare/OptiqMaterializer.java
index cb35bcd..ba2ae80 100644
--- a/core/src/main/java/net/hydromatic/optiq/prepare/OptiqMaterializer.java
+++ b/core/src/main/java/net/hydromatic/optiq/prepare/OptiqMaterializer.java
@@ -30,11 +30,14 @@ import org.eigenbase.sql.parser.SqlParseException;
 import org.eigenbase.sql.parser.SqlParser;
 import org.eigenbase.sql2rel.SqlToRelConverter;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
 import java.util.*;
 
 /**
-* Context for populating a {@link Materialization}.
-*/
+ * Context for populating a {@link Materialization}.
+ */
 class OptiqMaterializer extends OptiqPrepareImpl.OptiqPreparingStmt {
   public OptiqMaterializer(OptiqPrepare.Context context,
       CatalogReader catalogReader, OptiqSchema schema,
@@ -76,16 +79,32 @@ class OptiqMaterializer extends OptiqPrepareImpl.OptiqPreparingStmt {
   }
 
   /** Converts a relational expression to use a
-   * {@link net.hydromatic.optiq.impl.StarTable} defined in {@code schema}.
+   * {@link StarTable} defined in {@code schema}.
    * Uses the first star table that fits. */
   private void useStar(OptiqSchema schema, Materialization materialization) {
-    List<OptiqSchema.TableEntry> starTables = getStarTables(schema);
+    for (Callback x : useStar(schema, materialization.queryRel)) {
+      // Success -- we found a star table that matches.
+      materialization.materialize(x.rel, x.starRelOptTable);
+      System.out.println("Materialization "
+          + materialization.materializedTable + " matched star table "
+          + x.starTable + "; query after re-write: "
+          + RelOptUtil.toString(materialization.queryRel));
+    }
+  }
+
+  /** Converts a relational expression to use a
+   * {@link net.hydromatic.optiq.impl.StarTable} defined in {@code schema}.
+   * Uses the first star table that fits. */
+  private Iterable<Callback> useStar(OptiqSchema schema, RelNode queryRel) {
+    List<OptiqSchema.TableEntry> starTables =
+        Schemas.getStarTables(schema.root());
     if (starTables.isEmpty()) {
       // Don't waste effort converting to leaf-join form.
-      return;
+      return ImmutableList.of();
     }
+    final List<Callback> list = Lists.newArrayList();
     final RelNode rel2 =
-        RelOptMaterialization.toLeafJoinForm(materialization.queryRel);
+        RelOptMaterialization.toLeafJoinForm(queryRel);
     for (OptiqSchema.TableEntry starTable : starTables) {
       final Table table = starTable.getTable();
       assert table instanceof StarTable;
@@ -95,31 +114,7 @@ class OptiqMaterializer extends OptiqPrepareImpl.OptiqPreparingStmt {
       final RelNode rel3 =
           RelOptMaterialization.tryUseStar(rel2, starRelOptTable);
       if (rel3 != null) {
-        // Success -- we found a star table that matches.
-        materialization.materialize(rel3, starRelOptTable);
-        System.out.println("Materialization "
-            + materialization.materializedTable + " matched star table "
-            + starTable + "; query after re-write: "
-            + RelOptUtil.toString(materialization.queryRel));
-        return;
-      }
-    }
-  }
-
-  /** Returns the star tables defined in a schema.
-   * @param schema Schema */
-  private List<OptiqSchema.TableEntry> getStarTables(OptiqSchema schema) {
-    final List<OptiqSchema.TableEntry> list =
-        new ArrayList<OptiqSchema.TableEntry>();
-    // TODO: Assumes that star tables are all defined in a schema called
-    // "mat". Instead, we should look for star tables that use a given set of
-    // tables, regardless of schema.
-    final OptiqSchema matSchema = schema.root().getSubSchema("mat", true);
-    if (matSchema != null) {
-      for (OptiqSchema.TableEntry tis : matSchema.tableMap.values()) {
-        if (tis.getTable().getJdbcTableType() == Schema.TableType.STAR) {
-          list.add(tis);
-        }
+        list.add(new Callback(rel3, starTable, starRelOptTable));
       }
     }
     return list;
@@ -169,6 +164,20 @@ class OptiqMaterializer extends OptiqPrepareImpl.OptiqPreparingStmt {
     }
   }
 
+  /** Called when we discover a star table that matches. */
+  static class Callback {
+    public final RelNode rel;
+    public final OptiqSchema.TableEntry starTable;
+    public final RelOptTableImpl starRelOptTable;
+
+    Callback(RelNode rel,
+        OptiqSchema.TableEntry starTable,
+        RelOptTableImpl starRelOptTable) {
+      this.rel = rel;
+      this.starTable = starTable;
+      this.starRelOptTable = starRelOptTable;
+    }
+  }
 }
 
 // End OptiqMaterializer.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/prepare/OptiqPrepareImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/prepare/OptiqPrepareImpl.java b/core/src/main/java/net/hydromatic/optiq/prepare/OptiqPrepareImpl.java
index b894962..4c19268 100644
--- a/core/src/main/java/net/hydromatic/optiq/prepare/OptiqPrepareImpl.java
+++ b/core/src/main/java/net/hydromatic/optiq/prepare/OptiqPrepareImpl.java
@@ -38,6 +38,7 @@ import net.hydromatic.optiq.tools.Frameworks;
 import org.eigenbase.rel.*;
 import org.eigenbase.rel.rules.*;
 import org.eigenbase.relopt.*;
+import org.eigenbase.relopt.hep.*;
 import org.eigenbase.relopt.volcano.VolcanoPlanner;
 import org.eigenbase.reltype.*;
 import org.eigenbase.rex.RexBuilder;
@@ -149,6 +150,15 @@ public class OptiqPrepareImpl implements OptiqPrepare {
 
   public ParseResult parse(
       Context context, String sql) {
+    return parse_(context, sql, false);
+  }
+
+  public ConvertResult convert(Context context, String sql) {
+    return (ConvertResult) parse_(context, sql, true);
+  }
+
+  /** Shared implementation for {@link #parse} and {@link #convert}. */
+  private ParseResult parse_(Context context, String sql, boolean convert) {
     final JavaTypeFactory typeFactory = context.getTypeFactory();
     OptiqCatalogReader catalogReader =
         new OptiqCatalogReader(
@@ -167,8 +177,24 @@ public class OptiqPrepareImpl implements OptiqPrepare {
         new OptiqSqlValidator(
             SqlStdOperatorTable.instance(), catalogReader, typeFactory);
     SqlNode sqlNode1 = validator.validate(sqlNode);
-    return new ParseResult(this, validator, sql, sqlNode1,
-        validator.getValidatedNodeType(sqlNode1));
+    if (!convert) {
+      return new ParseResult(this, validator, sql, sqlNode1,
+          validator.getValidatedNodeType(sqlNode1));
+    }
+    final OptiqPreparingStmt preparingStmt =
+        new OptiqPreparingStmt(
+            context,
+            catalogReader,
+            typeFactory,
+            context.getRootSchema(),
+            null,
+            new HepPlanner(new HepProgramBuilder().build()),
+            EnumerableConvention.INSTANCE);
+    final SqlToRelConverter converter =
+        preparingStmt.getSqlToRelConverter(validator, catalogReader);
+    final RelNode relNode = converter.convertQuery(sqlNode1, false, true);
+    return new ConvertResult(this, validator, sql, sqlNode1,
+        validator.getValidatedNodeType(sqlNode1), relNode);
   }
 
   /** Creates a collection of planner factories.
@@ -374,8 +400,10 @@ public class OptiqPrepareImpl implements OptiqPrepare {
       for (Prepare.Materialization materialization : materializations) {
         populateMaterializations(context, planner, materialization);
       }
+      final List<OptiqSchema.LatticeEntry> lattices =
+          Schemas.getLatticeEntries(rootSchema);
       preparedResult = preparingStmt.prepareSql(
-          sqlNode, Object.class, validator, true, materializations);
+          sqlNode, Object.class, validator, true, materializations, lattices);
       switch (sqlNode.getKind()) {
       case INSERT:
       case EXPLAIN:
@@ -643,7 +671,8 @@ public class OptiqPrepareImpl implements OptiqPrepare {
       rootRel = trimUnusedFields(rootRel);
 
       final List<Materialization> materializations = ImmutableList.of();
-      rootRel = optimize(resultType, rootRel, materializations);
+      final List<OptiqSchema.LatticeEntry> lattices = ImmutableList.of();
+      rootRel = optimize(resultType, rootRel, materializations, lattices);
 
       if (timingTracer != null) {
         timingTracer.traceTime("end optimization");

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/prepare/Prepare.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/prepare/Prepare.java b/core/src/main/java/net/hydromatic/optiq/prepare/Prepare.java
index 87903ed..8f17eb7 100644
--- a/core/src/main/java/net/hydromatic/optiq/prepare/Prepare.java
+++ b/core/src/main/java/net/hydromatic/optiq/prepare/Prepare.java
@@ -18,6 +18,7 @@ package net.hydromatic.optiq.prepare;
 
 import net.hydromatic.optiq.DataContext;
 import net.hydromatic.optiq.impl.StarTable;
+import net.hydromatic.optiq.impl.java.JavaTypeFactory;
 import net.hydromatic.optiq.jdbc.OptiqPrepare;
 import net.hydromatic.optiq.jdbc.OptiqSchema;
 import net.hydromatic.optiq.runtime.Bindable;
@@ -95,10 +96,12 @@ public abstract class Prepare {
    * @param rootRel root of a relational expression
    *
    * @param materializations Tables known to be populated with a given query
+   * @param lattices Lattices
    * @return an equivalent optimized relational expression
    */
   protected RelNode optimize(RelDataType logicalRowType, final RelNode rootRel,
-      final List<Materialization> materializations) {
+      final List<Materialization> materializations,
+      final List<OptiqSchema.LatticeEntry> lattices) {
     final RelOptPlanner planner = rootRel.getCluster().getPlanner();
 
     planner.setRoot(rootRel);
@@ -116,6 +119,16 @@ public abstract class Prepare {
               materialization.starRelOptTable));
     }
 
+    for (OptiqSchema.LatticeEntry lattice : lattices) {
+      final OptiqSchema.TableEntry starTable = lattice.getStarTable();
+      final JavaTypeFactory typeFactory = context.getTypeFactory();
+      final RelOptTableImpl starRelOptTable =
+          RelOptTableImpl.create(catalogReader,
+              starTable.getTable().getRowType(typeFactory), starTable);
+      planner.addLattice(
+          new RelOptLattice(lattice.getLattice(), starRelOptTable));
+    }
+
     final RelNode rootRel4 = program.run(planner, rootRel, desiredTraits);
     if (LOGGER.isLoggable(Level.FINE)) {
       LOGGER.fine(
@@ -160,14 +173,16 @@ public abstract class Prepare {
       Class runtimeContextClass,
       SqlValidator validator,
       boolean needsValidation,
-      List<Materialization> materializations) {
+      List<Materialization> materializations,
+      List<OptiqSchema.LatticeEntry> lattices) {
     return prepareSql(
         sqlQuery,
         sqlQuery,
         runtimeContextClass,
         validator,
         needsValidation,
-        materializations);
+        materializations,
+        lattices);
   }
 
   public PreparedResult prepareSql(
@@ -176,7 +191,8 @@ public abstract class Prepare {
       Class runtimeContextClass,
       SqlValidator validator,
       boolean needsValidation,
-      List<Materialization> materializations) {
+      List<Materialization> materializations,
+      List<OptiqSchema.LatticeEntry> lattices) {
     queryString = sqlQuery.toString();
 
     init(runtimeContextClass);
@@ -243,13 +259,14 @@ public abstract class Prepare {
       switch (explainDepth) {
       case PHYSICAL:
       default:
-        rootRel = optimize(rootRel.getRowType(), rootRel, materializations);
+        rootRel = optimize(rootRel.getRowType(), rootRel, materializations,
+            lattices);
         return createPreparedExplanation(
             null, parameterRowType, rootRel, explainAsXml, detailLevel);
       }
     }
 
-    rootRel = optimize(resultType, rootRel, materializations);
+    rootRel = optimize(resultType, rootRel, materializations, lattices);
 
     if (timingTracer != null) {
       timingTracer.traceTime("end optimization");

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/runtime/Hook.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/runtime/Hook.java b/core/src/main/java/net/hydromatic/optiq/runtime/Hook.java
index 8944578..c8c7a80 100644
--- a/core/src/main/java/net/hydromatic/optiq/runtime/Hook.java
+++ b/core/src/main/java/net/hydromatic/optiq/runtime/Hook.java
@@ -47,6 +47,9 @@ public enum Hook {
    * optimization. */
   TRIMMED,
 
+  /** Called by the planner after substituting a materialization. */
+  SUB,
+
   /** Called when a constant expression is being reduced. */
   EXPRESSION_REDUCER,
 
@@ -130,6 +133,12 @@ public enum Hook {
    * JDK 1.6.</p>
    */
   public interface Closeable /*extends AutoCloseable*/ {
+    /** Closeable that does nothing. */
+    Closeable EMPTY =
+        new Closeable() {
+          public void close() {}
+        };
+
     void close(); // override, removing "throws"
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/util/Compatible.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/util/Compatible.java b/core/src/main/java/net/hydromatic/optiq/util/Compatible.java
index b410fc1..0c8d09d 100644
--- a/core/src/main/java/net/hydromatic/optiq/util/Compatible.java
+++ b/core/src/main/java/net/hydromatic/optiq/util/Compatible.java
@@ -41,6 +41,10 @@ public interface Compatible {
    * {@link java.util.NavigableMap}. */
   <K, V> NavigableMap<K, V> navigableMap(ImmutableSortedMap<K, V> map);
 
+  /** Converts a {@link Map} to a {@link java.util.NavigableMap} that is
+   * immutable. */
+  <K, V> NavigableMap<K, V> immutableNavigableMap(NavigableMap<K, V> map);
+
   /** Creates the implementation of Compatible suitable for the
    * current environment. */
   class Factory {
@@ -71,6 +75,11 @@ public interface Compatible {
                 ImmutableSortedMap map = (ImmutableSortedMap) args[0];
                 return CompatibleGuava11.navigableMap(map);
               }
+              if (method.getName().equals("immutableNavigableMap")) {
+                Map map = (Map) args[0];
+                ImmutableSortedMap sortedMap = ImmutableSortedMap.copyOf(map);
+                return CompatibleGuava11.navigableMap(sortedMap);
+              }
               return null;
             }
           });

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/net/hydromatic/optiq/util/graph/DefaultDirectedGraph.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/net/hydromatic/optiq/util/graph/DefaultDirectedGraph.java b/core/src/main/java/net/hydromatic/optiq/util/graph/DefaultDirectedGraph.java
index 857a922..3bafdf6 100644
--- a/core/src/main/java/net/hydromatic/optiq/util/graph/DefaultDirectedGraph.java
+++ b/core/src/main/java/net/hydromatic/optiq/util/graph/DefaultDirectedGraph.java
@@ -37,7 +37,12 @@ public class DefaultDirectedGraph<V, E extends DefaultEdge>
   }
 
   public static <V> DefaultDirectedGraph<V, DefaultEdge> create() {
-    return new DefaultDirectedGraph<V, DefaultEdge>(DefaultEdge.<V>factory());
+    return create(DefaultEdge.<V>factory());
+  }
+
+  public static <V, E extends DefaultEdge> DefaultDirectedGraph<V, E> create(
+      EdgeFactory<V, E> edgeFactory) {
+    return new DefaultDirectedGraph<V, E>(edgeFactory);
   }
 
   @Override

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/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
new file mode 100644
index 0000000..b85234e
--- /dev/null
+++ b/core/src/main/java/org/eigenbase/relopt/RelOptLattice.java
@@ -0,0 +1,57 @@
+/*
+ * 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.relopt;
+
+import java.util.List;
+
+import org.eigenbase.rel.RelNode;
+
+import net.hydromatic.optiq.materialize.Lattice;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Use of a lattice by the query optimizer.
+ */
+public class RelOptLattice {
+  private final Lattice lattice;
+  private final RelOptTable starRelOptTable;
+  private final List<RelOptMaterialization> materializations =
+      Lists.newArrayList();
+
+  public RelOptLattice(Lattice lattice, RelOptTable starRelOptTable) {
+    this.lattice = lattice;
+    this.starRelOptTable = starRelOptTable;
+  }
+
+  public RelOptTable rootTable() {
+    return lattice.nodes.get(0).scan.getTable();
+  }
+
+  /** Rewrites a relational expression to use a lattice.
+   *
+   * <p>Returns null if a rewrite is not possible.
+   *
+   * @param node Relational expression
+   * @return Rewritten query
+   */
+  public RelNode rewrite(RelNode node) {
+    return RelOptMaterialization.tryUseStar(node, starRelOptTable);
+  }
+}
+
+// End RelOptLattice.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/main/java/org/eigenbase/relopt/RelOptPlanner.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/eigenbase/relopt/RelOptPlanner.java b/core/src/main/java/org/eigenbase/relopt/RelOptPlanner.java
index e7185d1..4c9f3c8 100644
--- a/core/src/main/java/org/eigenbase/relopt/RelOptPlanner.java
+++ b/core/src/main/java/org/eigenbase/relopt/RelOptPlanner.java
@@ -156,6 +156,14 @@ public interface RelOptPlanner {
   void addMaterialization(RelOptMaterialization materialization);
 
   /**
+   * Defines a lattice.
+   *
+   * <p>The lattice may have materializations; it is not necessary to call
+   * {@link #addMaterialization} for these; they are registered implicitly.
+   */
+  void addLattice(RelOptLattice lattice);
+
+  /**
    * Finds the most efficient expression to implement this query.
    *
    * @throws CannotPlanException if cannot find a plan

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/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 d0835dd..05b09c2 100644
--- a/core/src/main/java/org/eigenbase/relopt/hep/HepPlanner.java
+++ b/core/src/main/java/org/eigenbase/relopt/hep/HepPlanner.java
@@ -132,6 +132,10 @@ public class HepPlanner extends AbstractRelOptPlanner {
     // ignore - this planner does not support materializations
   }
 
+  public void addLattice(RelOptLattice lattice) {
+    // ignore - this planner does not support lattices
+  }
+
   // implement RelOptPlanner
   public boolean addRule(RelOptRule rule) {
     boolean added = allRules.add(rule);

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/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 8de7415..e8b740b 100644
--- a/core/src/main/java/org/eigenbase/relopt/volcano/VolcanoPlanner.java
+++ b/core/src/main/java/org/eigenbase/relopt/volcano/VolcanoPlanner.java
@@ -34,9 +34,11 @@ import org.eigenbase.util.*;
 import net.hydromatic.linq4j.expressions.Expressions;
 
 import net.hydromatic.optiq.prepare.OptiqPrepareImpl;
+import net.hydromatic.optiq.runtime.Hook;
 import net.hydromatic.optiq.runtime.Spaces;
 import net.hydromatic.optiq.util.graph.*;
 
+import com.google.common.base.Function;
 import com.google.common.collect.*;
 
 import static org.eigenbase.util.Stacks.*;
@@ -50,6 +52,13 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
 
   protected static final double COST_IMPROVEMENT = .5;
 
+  private static final Function<RelOptTable, List<String>> GET_QUALIFIED_NAME =
+      new Function<RelOptTable, List<String>>() {
+        public List<String> apply(RelOptTable relOptTable) {
+          return relOptTable.getQualifiedName();
+        }
+      };
+
   //~ Instance fields --------------------------------------------------------
 
   protected RelSubset root;
@@ -179,7 +188,9 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
   private boolean locked;
 
   private final List<RelOptMaterialization> materializations =
-      new ArrayList<RelOptMaterialization>();
+      Lists.newArrayList();
+
+  private final List<RelOptLattice> lattices = Lists.newArrayList();
 
   final Map<RelNode, Provenance> provenanceMap =
       new HashMap<RelNode, Provenance>();
@@ -261,6 +272,15 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     materializations.add(materialization);
   }
 
+  public void addLattice(RelOptLattice lattice) {
+    lattices.add(lattice);
+  }
+
+  private void useLattice(RelOptLattice lattice, RelNode rel) {
+    Hook.SUB.run(rel);
+    registerImpl(rel, root.set);
+  }
+
   private void useMaterialization(RelOptMaterialization materialization) {
     // Try to rewrite the original root query in terms of the materialized
     // query. If that is possible, register the remnant query as equivalent
@@ -271,6 +291,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
       // TODO: try to substitute other materializations in the remnant.
       // Useful for big queries, e.g.
       //   (t1 group by c1) join (t2 group by c2).
+      Hook.SUB.run(sub);
       registerImpl(sub, root.set);
       return;
     }
@@ -349,6 +370,23 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
         }
       }
     }
+
+    // Use a lattice if the query uses at least the central (fact) table of the
+    // lattice.
+    final List<Pair<RelOptLattice, RelNode>> latticeUses = Lists.newArrayList();
+    final Set<List<String>> queryTableNames =
+        Sets.newHashSet(Iterables.transform(queryTables, GET_QUALIFIED_NAME));
+    for (RelOptLattice lattice : lattices) {
+      if (queryTableNames.contains(lattice.rootTable().getQualifiedName())) {
+        RelNode rel2 = lattice.rewrite(originalRoot);
+        if (rel2 != null) {
+          latticeUses.add(Pair.of(lattice, rel2));
+        }
+      }
+    }
+    if (!latticeUses.isEmpty()) {
+      useLattice(latticeUses.get(0).left, latticeUses.get(0).right);
+    }
   }
 
   /**
@@ -587,8 +625,8 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
    * <p>Furthermore, after every 10 iterations without an implementable plan,
    * RelSubSets that contain only logical RelNodes are given an importance
    * boost via {@link #injectImportanceBoost()}. Once an implementable plan is
-   * found, the artificially raised importances are cleared ({@link
-   * #clearImportanceBoost()}).
+   * found, the artificially raised importance values are cleared (see
+   * {@link #clearImportanceBoost()}).
    *
    * @return the most efficient RelNode tree found for implementing the given
    * query

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/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
new file mode 100644
index 0000000..80b4cde
--- /dev/null
+++ b/core/src/test/java/net/hydromatic/optiq/test/LatticeTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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 net.hydromatic.optiq.test;
+
+import org.eigenbase.util.TestUtil;
+import org.eigenbase.util.Util;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Unit test for lattices.
+ */
+public class LatticeTest {
+  private OptiqAssert.AssertThat modelWithLattice(String name, String sql) {
+    return modelWithLattices(
+        "{ name: '" + name + "', sql: " + TestUtil.escapeString(sql) + "}");
+  }
+
+  private OptiqAssert.AssertThat modelWithLattices(String... lattices) {
+    final Class<JdbcTest.EmpDeptTableFactory> clazz =
+        JdbcTest.EmpDeptTableFactory.class;
+    return OptiqAssert.that().withModel(""
+        + "{\n"
+        + "  version: '1.0',\n"
+        + "   schemas: [\n"
+        + JdbcTest.FOODMART_SCHEMA
+        + ",\n"
+        + "     {\n"
+        + "       name: 'adhoc',\n"
+        + "       tables: [\n"
+        + "         {\n"
+        + "           name: 'EMPLOYEES',\n"
+        + "           type: 'custom',\n"
+        + "           factory: '"
+        + clazz.getName()
+        + "',\n"
+        + "           operand: {'foo': true, 'bar': 345}\n"
+        + "         }\n"
+        + "       ],\n"
+        + "       lattices: "
+        + Arrays.toString(lattices)
+        + "     }\n"
+        + "   ]\n"
+        + "}").withSchema("adhoc");
+  }
+
+  /** Tests that it's OK for a lattice to have the same name as a table in the
+   * schema. */
+  @Test public void testLatticeWithSameNameAsTable() {
+    modelWithLattice("EMPLOYEES", "select * from \"foodmart\".\"days\"")
+        .query("select count(*) from EMPLOYEES")
+        .returnsValue("4");
+  }
+
+  /** Tests that it's an error to have two lattices with the same name in a
+   * schema. */
+  @Test public void testTwoLatticesWithSameNameFails() {
+    modelWithLattices(
+        "{name: 'Lattice1', sql: 'select * from \"foodmart\".\"days\"'}",
+        "{name: 'Lattice1', sql: 'select * from \"foodmart\".\"time_by_day\"'}")
+        .connectThrows("Duplicate lattice 'Lattice1'");
+  }
+
+  /** Tests a lattice whose SQL is invalid. */
+  @Test public void testLatticeInvalidSqlFails() {
+    modelWithLattice("star", "select foo from nonexistent")
+        .connectThrows("Error instantiating JsonLattice(name=star, ")
+        .connectThrows("Table 'NONEXISTENT' not found");
+  }
+
+  /** Tests a lattice whose SQL is invalid because it contains a GROUP BY. */
+  @Test public void testLatticeSqlWithGroupByFails() {
+    modelWithLattice("star",
+        "select 1 from \"foodmart\".\"sales_fact_1997\" as s group by \"product_id\"")
+        .connectThrows("Invalid node type AggregateRel in lattice query");
+  }
+
+  /** Tests a lattice whose SQL is invalid because it contains a ORDER BY. */
+  @Test public void testLatticeSqlWithOrderByFails() {
+    modelWithLattice("star",
+        "select 1 from \"foodmart\".\"sales_fact_1997\" as s order by \"product_id\"")
+        .connectThrows("Invalid node type SortRel in lattice query");
+  }
+
+  /** Tests a lattice whose SQL is invalid because it contains a UNION ALL. */
+  @Test public void testLatticeSqlWithUnionFails() {
+    modelWithLattice("star",
+        "select 1 from \"foodmart\".\"sales_fact_1997\" as s\n"
+        + "union all\n"
+        + "select 1 from \"foodmart\".\"sales_fact_1997\" as s")
+        .connectThrows("Invalid node type UnionRel in lattice query");
+  }
+
+  /** Tests a lattice with valid join SQL. */
+  @Test public void testLatticeSqlWithJoin() {
+    foodmartModel()
+        .query("values 1")
+        .returnsValue("1");
+  }
+
+  /** Tests a lattice with invalid SQL (for a lattice). */
+  @Test public void testLatticeInvalidSql() {
+    modelWithLattice("star",
+        "select 1 from \"foodmart\".\"sales_fact_1997\" as s\n"
+        + "join \"foodmart\".\"product\" as p using (\"product_id\")\n"
+        + "join \"foodmart\".\"time_by_day\" as t on s.\"product_id\" = 100")
+        .connectThrows("only equi-join of columns allowed: 100");
+  }
+
+  /** Left join is invalid in a lattice. */
+  @Test public void testLatticeInvalidSql2() {
+    modelWithLattice("star",
+        "select 1 from \"foodmart\".\"sales_fact_1997\" as s\n"
+        + "join \"foodmart\".\"product\" as p using (\"product_id\")\n"
+        + "left join \"foodmart\".\"time_by_day\" as t on s.\"product_id\" = p.\"product_id\"")
+        .connectThrows("only inner join allowed, but got LEFT");
+  }
+
+  /** When a lattice is registered, there is a table with the same name.
+   * It can be used for explain, but not for queries. */
+  @Test public void testLatticeStarTable() {
+    try {
+      foodmartModel()
+          .query("select count(*) from \"adhoc\".\"star\"")
+          .convertContains(
+              "AggregateRel(group=[{}], EXPR$0=[COUNT()])\n"
+              + "  ProjectRel(DUMMY=[0])\n"
+              + "    ProjectRel\n"
+              + "      StarTableScan(table=[[adhoc, star]])\n");
+    } catch (RuntimeException e) {
+      assertThat(Util.getStackTrace(e), containsString("CannotPlanException"));
+    }
+  }
+
+  /** Tests that a 2-way join query can be mapped 4-way join lattice. */
+  @Test public void testLatticeRecognizeJoin() {
+    final AtomicInteger counter = new AtomicInteger();
+    foodmartModel()
+      .query(
+          "select s.\"unit_sales\", p.\"brand_name\"\n"
+          + "from \"foodmart\".\"sales_fact_1997\" as s\n"
+          + "join \"foodmart\".\"product\" as p using (\"product_id\")\n")
+        .substitutionMatches(
+            OptiqAssert.checkRel(
+                "ProjectRel(unit_sales=[$1], brand_name=[$3])\n"
+                + "  JoinRel(condition=[=($0, $2)], joinType=[inner])\n"
+                + "    ProjectRel(product_id=[$0], unit_sales=[$7])\n"
+                + "      ProjectRel($f0=[$0], $f1=[$1], $f2=[$2], $f3=[$3], $f4=[$4], $f5=[$5], $f6=[$6], $f7=[$7])\n"
+                + "        TableAccessRel(table=[[adhoc, star]])\n"
+                + "    ProjectRel(product_id=[$1], brand_name=[$2])\n"
+                + "      JdbcTableScan(table=[[foodmart, product]])\n",
+                counter));
+    assertThat(counter.intValue(), equalTo(1));
+  }
+
+  private OptiqAssert.AssertThat foodmartModel() {
+    return modelWithLattice("star",
+        "select 1 from \"foodmart\".\"sales_fact_1997\" as s\n"
+        + "join \"foodmart\".\"product\" as p using (\"product_id\")\n"
+        + "join \"foodmart\".\"time_by_day\" as t using (\"time_id\")\n"
+        + "join \"foodmart\".\"product_class\" as pc on p.\"product_class_id\" = pc.\"product_class_id\"");
+  }
+}
+
+// End LatticeTest.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/test/java/net/hydromatic/optiq/test/ModelTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/test/ModelTest.java b/core/src/test/java/net/hydromatic/optiq/test/ModelTest.java
index 25043d4..ec3aea4 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/ModelTest.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/ModelTest.java
@@ -175,6 +175,53 @@ public class ModelTest {
         "Cannot define materialization; parent schema 'adhoc' is not a "
         + "SemiMutableSchema");
   }
+
+  /** Tests a model containing a lattice. */
+  @Test public void testReadLattice() throws IOException {
+    final ObjectMapper mapper = mapper();
+    JsonRoot root = mapper.readValue(
+        "{\n"
+        + "  version: '1.0',\n"
+        + "   schemas: [\n"
+        + "     {\n"
+        + "       name: 'FoodMart',\n"
+        + "       tables: [\n"
+        + "         {\n"
+        + "           name: 'time_by_day',\n"
+        + "           columns: [\n"
+        + "             {\n"
+        + "               name: 'time_id'\n"
+        + "             }\n"
+        + "           ]\n"
+        + "         },\n"
+        + "         {\n"
+        + "           name: 'sales_fact_1997',\n"
+        + "           columns: [\n"
+        + "             {\n"
+        + "               name: 'time_id'\n"
+        + "             }\n"
+        + "           ]\n"
+        + "         }\n"
+        + "       ],\n"
+        + "       lattices: [\n"
+        + "         {\n"
+        + "           name: 'SalesStar',\n"
+        + "           sql: 'select * from sales_fact_1997'\n"
+        + "         }\n"
+        + "       ]\n"
+        + "     }\n"
+        + "   ]\n"
+        + "}",
+        JsonRoot.class);
+    assertEquals("1.0", root.version);
+    assertEquals(1, root.schemas.size());
+    final JsonMapSchema schema = (JsonMapSchema) root.schemas.get(0);
+    assertEquals("FoodMart", schema.name);
+    assertEquals(1, schema.lattices.size());
+    final JsonLattice lattice0 = schema.lattices.get(0);
+    assertEquals("SalesStar", lattice0.name);
+    assertEquals("select * from sales_fact_1997", lattice0.sql);
+  }
 }
 
 // End ModelTest.java

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/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 66b179e..b2fe555 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/OptiqAssert.java
@@ -19,6 +19,7 @@ package net.hydromatic.optiq.test;
 import net.hydromatic.linq4j.function.Function1;
 
 import net.hydromatic.optiq.*;
+import net.hydromatic.optiq.config.OptiqConnectionProperty;
 import net.hydromatic.optiq.impl.AbstractSchema;
 import net.hydromatic.optiq.impl.ViewTable;
 import net.hydromatic.optiq.impl.clone.CloneSchema;
@@ -44,6 +45,7 @@ import java.sql.*;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
 import javax.sql.DataSource;
 
 import static org.hamcrest.CoreMatchers.*;
@@ -147,6 +149,20 @@ public class OptiqAssert {
     return new AssertThat(Config.REGULAR);
   }
 
+  static Function1<RelNode, Void> checkRel(final String expected,
+      final AtomicInteger counter) {
+    return new Function1<RelNode, Void>() {
+      public Void apply(RelNode relNode) {
+        if (counter != null) {
+          counter.incrementAndGet();
+        }
+        String s = RelOptUtil.toString(relNode);
+        assertThat(s, containsString(expected));
+        return null;
+      }
+    };
+  }
+
   static Function1<Throwable, Void> checkException(
       final String expected) {
     return new Function1<Throwable, Void>() {
@@ -356,7 +372,8 @@ public class OptiqAssert {
     final List<Hook.Closeable> closeableList = Lists.newArrayList();
     try {
       ((OptiqConnection) connection).getProperties().setProperty(
-          "materializationsEnabled", Boolean.toString(materializationsEnabled));
+          OptiqConnectionProperty.MATERIALIZATIONS_ENABLED.camelName(),
+          Boolean.toString(materializationsEnabled));
       for (Pair<Hook, Function> hook : hooks) {
         closeableList.add(hook.left.addThread(hook.right));
       }
@@ -401,19 +418,34 @@ public class OptiqAssert {
       Connection connection,
       String sql,
       boolean materializationsEnabled,
-      final Function1<RelNode, Void> convertChecker) throws Exception {
+      final Function1<RelNode, Void> convertChecker,
+      final Function1<RelNode, Void> substitutionChecker) throws Exception {
     final String message =
         "With materializationsEnabled=" + materializationsEnabled;
-    Hook.Closeable closeable = Hook.TRIMMED.addThread(
-        new Function<RelNode, Object>() {
-          public Void apply(RelNode rel) {
-            convertChecker.apply(rel);
-            return null;
-          }
-        });
+    final Hook.Closeable closeable =
+        convertChecker == null
+            ? Hook.Closeable.EMPTY
+            : Hook.TRIMMED.addThread(
+                new Function<RelNode, Void>() {
+                  public Void apply(RelNode rel) {
+                    convertChecker.apply(rel);
+                    return null;
+                  }
+                });
+    final Hook.Closeable closeable2 =
+        substitutionChecker == null
+            ? Hook.Closeable.EMPTY
+            : Hook.SUB.addThread(
+                new Function<RelNode, Void>() {
+                  public Void apply(RelNode rel) {
+                    substitutionChecker.apply(rel);
+                    return null;
+                  }
+                });
     try {
       ((OptiqConnection) connection).getProperties().setProperty(
-          "materializationsEnabled", Boolean.toString(materializationsEnabled));
+          OptiqConnectionProperty.MATERIALIZATIONS_ENABLED.camelName(),
+          Boolean.toString(materializationsEnabled));
       PreparedStatement statement = connection.prepareStatement(sql);
       statement.close();
       connection.close();
@@ -421,6 +453,7 @@ public class OptiqAssert {
       throw new RuntimeException(message, e);
     } finally {
       closeable.close();
+      closeable2.close();
     }
   }
 
@@ -972,24 +1005,30 @@ public class OptiqAssert {
     /** Checks that when the query (which was set using
      * {@link AssertThat#query(String)}) is converted to a relational algebra
      * expression matching the given string. */
-    public AssertQuery convertContains(final String expected) {
-      return convertMatches(
-          new Function1<RelNode, Void>() {
-            public Void apply(RelNode relNode) {
-              String s = RelOptUtil.toString(relNode);
-              assertThat(s, containsString(expected));
-              return null;
-            }
-          });
+    public final AssertQuery convertContains(final String expected) {
+      return convertMatches(checkRel(expected, null));
     }
 
     public AssertQuery convertMatches(final Function1<RelNode, Void> checker) {
       try {
-        assertPrepare(createConnection(), sql, false, checker);
+        assertPrepare(createConnection(), sql, this.materializationsEnabled,
+            checker, null);
         return this;
       } catch (Exception e) {
-        throw new RuntimeException(
-            "exception while preparing [" + sql + "]", e);
+        throw new RuntimeException("exception while preparing [" + sql + "]",
+            e);
+      }
+    }
+
+    public AssertQuery substitutionMatches(
+        final Function1<RelNode, Void> checker) {
+      try {
+        assertPrepare(createConnection(), sql, materializationsEnabled,
+            null, checker);
+        return this;
+      } catch (Exception e) {
+        throw new RuntimeException("exception while preparing [" + sql + "]",
+            e);
       }
     }
 
@@ -1174,6 +1213,11 @@ public class OptiqAssert {
       return this;
     }
 
+    @Override public AssertQuery substitutionMatches(
+        Function1<RelNode, Void> checker) {
+      return this;
+    }
+
     @Override
     public AssertQuery planContains(String expected) {
       return this;

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/test/java/net/hydromatic/optiq/test/OptiqSuite.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/net/hydromatic/optiq/test/OptiqSuite.java b/core/src/test/java/net/hydromatic/optiq/test/OptiqSuite.java
index 6f9ac93..1e58866 100644
--- a/core/src/test/java/net/hydromatic/optiq/test/OptiqSuite.java
+++ b/core/src/test/java/net/hydromatic/optiq/test/OptiqSuite.java
@@ -85,6 +85,7 @@ import org.junit.runners.Suite;
     RelOptRulesTest.class,
     RexExecutorTest.class,
     MaterializationTest.class,
+    LatticeTest.class,
     SqlLimitsTest.class,
     LinqFrontJdbcBackTest.class,
     JdbcFrontLinqBackTest.class,

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/test/java/org/eigenbase/test/MockRelOptPlanner.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/eigenbase/test/MockRelOptPlanner.java b/core/src/test/java/org/eigenbase/test/MockRelOptPlanner.java
index fd8d965..1d46a40 100644
--- a/core/src/test/java/org/eigenbase/test/MockRelOptPlanner.java
+++ b/core/src/test/java/org/eigenbase/test/MockRelOptPlanner.java
@@ -62,6 +62,10 @@ public class MockRelOptPlanner extends AbstractRelOptPlanner {
     // ignore - this planner does not support materializations
   }
 
+  public void addLattice(RelOptLattice lattice) {
+    // ignore - this planner does not support lattices
+  }
+
   @Override public void clear() {
     super.clear();
     this.rule = null;

http://git-wip-us.apache.org/repos/asf/incubator-optiq/blob/acee9632/core/src/test/java/org/eigenbase/util/TestUtil.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/eigenbase/util/TestUtil.java b/core/src/test/java/org/eigenbase/util/TestUtil.java
index 34c6b4c..34dfd6a 100644
--- a/core/src/test/java/org/eigenbase/util/TestUtil.java
+++ b/core/src/test/java/org/eigenbase/util/TestUtil.java
@@ -134,6 +134,42 @@ public abstract class TestUtil {
     return buf.toString();
   }
 
+  /** Quotes a string for Java or JSON. */
+  public static String escapeString(String s) {
+    return escapeString(new StringBuilder(), s).toString();
+  }
+
+  /** Quotes a string for Java or JSON, into a builder. */
+  public static StringBuilder escapeString(StringBuilder buf, String s) {
+    buf.append('"');
+    int n = s.length();
+    char lastChar = 0;
+    for (int i = 0; i < n; ++i) {
+      char c = s.charAt(i);
+      switch (c) {
+      case '\\':
+        buf.append("\\\\");
+        break;
+      case '"':
+        buf.append("\\\"");
+        break;
+      case '\n':
+        buf.append("\\n");
+        break;
+      case '\r':
+        if (lastChar != '\n') {
+          buf.append("\\r");
+        }
+        break;
+      default:
+        buf.append(c);
+        break;
+      }
+      lastChar = c;
+    }
+    return buf.append('"');
+  }
+
   /**
    * Quotes a pattern.
    */