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 2016/05/06 06:32:11 UTC

[1/3] calcite git commit: [CALCITE-1216] Rule to convert Filter-on-Scan to materialized view (Amogh Margoor)

Repository: calcite
Updated Branches:
  refs/heads/master 6031b7a47 -> 19361b94a


[CALCITE-1216] Rule to convert Filter-on-Scan to materialized view (Amogh Margoor)

Close apache/calcite#224

This will allow join queries with filter to be optimized by Materialized
Views. It waits for predicate tot be pushed across join onto table scan,
and then check if it can possible do any substitution for it.


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

Branch: refs/heads/master
Commit: bf098dd6737bdc78169961e076ba53bb30ba76d7
Parents: 354e824
Author: Amogh Margoor <am...@qubole.com>
Authored: Sun May 1 00:16:22 2016 -0400
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu May 5 22:07:12 2016 -0700

----------------------------------------------------------------------
 .../adapter/enumerable/EnumerableTableScan.java |  10 ++
 .../calcite/plan/SubstitutionVisitor.java       |   2 +-
 .../calcite/plan/volcano/VolcanoPlanner.java    |  76 ++++++++------
 .../calcite/prepare/CalcitePrepareImpl.java     |   5 +-
 .../apache/calcite/prepare/RelOptTableImpl.java |  10 ++
 .../rules/MaterializedViewFilterScanRule.java   | 101 +++++++++++++++++++
 .../calcite/test/MaterializationTest.java       |  66 +++++++-----
 7 files changed, 212 insertions(+), 58 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/bf098dd6/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScan.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScan.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScan.java
index 9364fa6..d7529ce 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScan.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScan.java
@@ -88,6 +88,16 @@ public class EnumerableTableScan
     return new EnumerableTableScan(cluster, traitSet, relOptTable, elementType);
   }
 
+  @Override public boolean equals(Object obj) {
+    return obj == this
+        || obj instanceof EnumerableTableScan
+        && table.equals(((EnumerableTableScan) obj).table);
+  }
+
+  @Override public int hashCode() {
+    return table.hashCode();
+  }
+
   /** Returns whether EnumerableTableScan can generate code to handle a
    * particular variant of the Table SPI. */
   public static boolean canHandle(Table table) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/bf098dd6/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java b/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
index 6045df4..5110da0 100644
--- a/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
+++ b/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
@@ -1616,7 +1616,7 @@ public class SubstitutionVisitor {
     @Override public boolean equals(Object obj) {
       return obj == this
           || obj instanceof MutableScan
-          && rel == ((MutableScan) obj).rel;
+          && rel.equals(((MutableScan) obj).rel);
     }
 
     @Override public int hashCode() {

http://git-wip-us.apache.org/repos/asf/calcite/blob/bf098dd6/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
index baa918b..4cda68e 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
@@ -344,6 +344,10 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     return root;
   }
 
+  public ImmutableList<RelOptMaterialization> getMaterializations() {
+    return ImmutableList.copyOf(materializations);
+  }
+
   @Override public void addMaterialization(
       RelOptMaterialization materialization) {
     materializations.add(materialization);
@@ -453,6 +457,42 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     // graph will contain
     //   (T, Emps), (T, Depts), (T2, T)
     // and therefore we can deduce T2 uses Emps.
+    final List<RelOptMaterialization> applicableMaterializations =
+        getApplicableMaterializations(originalRoot, materializations);
+    useMaterializations(originalRoot, applicableMaterializations);
+    final Set<RelOptTable> queryTables = findTables(originalRoot);
+
+    // 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));
+    // Remember leaf-join form of root so we convert at most once.
+    final Supplier<RelNode> leafJoinRoot = Suppliers.memoize(
+        new Supplier<RelNode>() {
+          public RelNode get() {
+            return RelOptMaterialization.toLeafJoinForm(originalRoot);
+          }
+        });
+    for (RelOptLattice lattice : latticeByName.values()) {
+      if (queryTableNames.contains(lattice.rootTable().getQualifiedName())) {
+        RelNode rel2 = lattice.rewrite(leafJoinRoot.get());
+        if (rel2 != null) {
+          if (CalcitePrepareImpl.DEBUG) {
+            System.out.println("use lattice:\n"
+                + RelOptUtil.toString(rel2));
+          }
+          latticeUses.add(Pair.of(lattice, rel2));
+        }
+      }
+    }
+    if (!latticeUses.isEmpty()) {
+      useLattice(latticeUses.get(0).left, latticeUses.get(0).right);
+    }
+  }
+
+  public static List<RelOptMaterialization> getApplicableMaterializations(RelNode root,
+      List<RelOptMaterialization> materializations) {
     DirectedGraph<List<String>, DefaultEdge> usesGraph =
         DefaultDirectedGraph.create();
     final Map<List<String>, RelOptMaterialization> qnameMap = new HashMap<>();
@@ -476,51 +516,23 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     // actually use.)
     final Graphs.FrozenGraph<List<String>, DefaultEdge> frozenGraph =
         Graphs.makeImmutable(usesGraph);
-    final Set<RelOptTable> queryTables = findTables(originalRoot);
+    final Set<RelOptTable> queryTablesUsed = findTables(root);
     final List<RelOptMaterialization> applicableMaterializations = Lists.newArrayList();
     for (List<String> qname : TopologicalOrderIterator.of(usesGraph)) {
       RelOptMaterialization materialization = qnameMap.get(qname);
       if (materialization != null
-          && usesTable(materialization.table, queryTables, frozenGraph)) {
+          && usesTable(materialization.table, queryTablesUsed, frozenGraph)) {
         applicableMaterializations.add(materialization);
       }
     }
-    useMaterializations(originalRoot, applicableMaterializations);
-
-    // 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));
-    // Remember leaf-join form of root so we convert at most once.
-    final Supplier<RelNode> leafJoinRoot = Suppliers.memoize(
-        new Supplier<RelNode>() {
-          public RelNode get() {
-            return RelOptMaterialization.toLeafJoinForm(originalRoot);
-          }
-        });
-    for (RelOptLattice lattice : latticeByName.values()) {
-      if (queryTableNames.contains(lattice.rootTable().getQualifiedName())) {
-        RelNode rel2 = lattice.rewrite(leafJoinRoot.get());
-        if (rel2 != null) {
-          if (CalcitePrepareImpl.DEBUG) {
-            System.out.println("use lattice:\n"
-                + RelOptUtil.toString(rel2));
-          }
-          latticeUses.add(Pair.of(lattice, rel2));
-        }
-      }
-    }
-    if (!latticeUses.isEmpty()) {
-      useLattice(latticeUses.get(0).left, latticeUses.get(0).right);
-    }
+    return applicableMaterializations;
   }
 
   /**
    * Returns whether {@code table} uses one or more of the tables in
    * {@code usedTables}.
    */
-  private boolean usesTable(
+  private static boolean usesTable(
       RelOptTable table,
       Set<RelOptTable> usedTables,
       Graphs.FrozenGraph<List<String>, DefaultEdge> usesGraph) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/bf098dd6/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
index 177cd0b..e766fb0 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
@@ -84,6 +84,7 @@ import org.apache.calcite.rel.rules.JoinAssociateRule;
 import org.apache.calcite.rel.rules.JoinCommuteRule;
 import org.apache.calcite.rel.rules.JoinPushExpressionsRule;
 import org.apache.calcite.rel.rules.JoinPushThroughJoinRule;
+import org.apache.calcite.rel.rules.MaterializedViewFilterScanRule;
 import org.apache.calcite.rel.rules.ProjectFilterTransposeRule;
 import org.apache.calcite.rel.rules.ProjectMergeRule;
 import org.apache.calcite.rel.rules.ProjectTableScanRule;
@@ -491,7 +492,9 @@ public class CalcitePrepareImpl implements CalcitePrepare {
     for (RelOptRule rule : DEFAULT_RULES) {
       planner.addRule(rule);
     }
-
+    if (prepareContext.config().materializationsEnabled()) {
+      planner.addRule(MaterializedViewFilterScanRule.INSTANCE);
+    }
     if (ENABLE_BINDABLE) {
       for (RelOptRule rule : Bindables.RULES) {
         planner.addRule(rule);

http://git-wip-us.apache.org/repos/asf/calcite/blob/bf098dd6/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java b/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java
index 3a77256..e3ce221 100644
--- a/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java
+++ b/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java
@@ -205,6 +205,16 @@ public class RelOptTableImpl implements Prepare.PreparingTable {
     throw new RuntimeException("Cannot extend " + table); // TODO: user error
   }
 
+  @Override public boolean equals(Object obj) {
+    return obj instanceof RelOptTableImpl
+        && this.rowType.equals(((RelOptTableImpl) obj).getRowType())
+        && this.table == ((RelOptTableImpl) obj).table;
+  }
+
+  @Override public int hashCode() {
+    return (this.table == null)
+        ? super.hashCode() : this.table.hashCode();
+  }
   public double getRowCount() {
     if (rowCount != null) {
       return rowCount;

http://git-wip-us.apache.org/repos/asf/calcite/blob/bf098dd6/core/src/main/java/org/apache/calcite/rel/rules/MaterializedViewFilterScanRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/MaterializedViewFilterScanRule.java b/core/src/main/java/org/apache/calcite/rel/rules/MaterializedViewFilterScanRule.java
new file mode 100644
index 0000000..852d724
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/rules/MaterializedViewFilterScanRule.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.rel.rules;
+
+import org.apache.calcite.plan.MaterializedViewSubstitutionVisitor;
+import org.apache.calcite.plan.RelOptMaterialization;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.hep.HepPlanner;
+import org.apache.calcite.plan.hep.HepProgram;
+import org.apache.calcite.plan.hep.HepProgramBuilder;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.core.RelFactories;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.tools.RelBuilderFactory;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Planner rule that converts
+ * a {@link org.apache.calcite.rel.core.Filter}
+ * on a {@link org.apache.calcite.rel.core.TableScan}
+ * to a {@link org.apache.calcite.rel.core.Filter} on Materialized View
+ */
+public class MaterializedViewFilterScanRule extends RelOptRule {
+  public static final MaterializedViewFilterScanRule INSTANCE =
+      new MaterializedViewFilterScanRule(RelFactories.LOGICAL_BUILDER);
+
+  private final HepProgram program = new HepProgramBuilder()
+      .addRuleInstance(FilterProjectTransposeRule.INSTANCE)
+      .addRuleInstance(ProjectMergeRule.INSTANCE)
+      .build();
+
+  //~ Constructors -----------------------------------------------------------
+
+  /** Creates a MaterializedViewFilterScanRule. */
+  protected MaterializedViewFilterScanRule(RelBuilderFactory relBuilderFactory) {
+    super(operand(Filter.class, operand(TableScan.class, null, none())),
+        relBuilderFactory, "MaterializedViewFilterScanRule");
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  public void onMatch(RelOptRuleCall call) {
+    final Filter filter = call.rel(0);
+    final TableScan scan = call.rel(1);
+    apply(call, filter, scan);
+  }
+
+  protected void apply(RelOptRuleCall call, Filter filter, TableScan scan) {
+    RelOptPlanner planner = call.getPlanner();
+    List<RelOptMaterialization> materializations =
+        (planner instanceof VolcanoPlanner)
+            ? ((VolcanoPlanner) planner).getMaterializations()
+            : ImmutableList.<RelOptMaterialization>of();
+    if (!materializations.isEmpty()) {
+      RelNode root = filter.copy(filter.getTraitSet(),
+          Collections.singletonList((RelNode) scan));
+      List<RelOptMaterialization> applicableMaterializations =
+          VolcanoPlanner.getApplicableMaterializations(root, materializations);
+      for (RelOptMaterialization materialization : applicableMaterializations) {
+        if (RelOptUtil.areRowTypesEqual(scan.getRowType(),
+            materialization.queryRel.getRowType(), false)) {
+          RelNode target = materialization.queryRel;
+          final HepPlanner hepPlanner =
+              new HepPlanner(program, planner.getContext());
+          hepPlanner.setRoot(target);
+          target = hepPlanner.findBestExp();
+          List<RelNode> subs = new MaterializedViewSubstitutionVisitor(target, root)
+              .go(materialization.tableRel);
+          for (RelNode s : subs) {
+            call.transformTo(s);
+          }
+        }
+      }
+    }
+  }
+}
+
+// End MaterializedViewFilterScanRule.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/bf098dd6/core/src/test/java/org/apache/calcite/test/MaterializationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/MaterializationTest.java b/core/src/test/java/org/apache/calcite/test/MaterializationTest.java
index 8b7aaf5..ba9a05e 100644
--- a/core/src/test/java/org/apache/calcite/test/MaterializationTest.java
+++ b/core/src/test/java/org/apache/calcite/test/MaterializationTest.java
@@ -254,8 +254,7 @@ public class MaterializationTest {
    * FilterToProjectUnifyRule.invert(MutableRel, MutableRel, MutableProject)
    * works incorrectly</a>. */
   @Test public void testFilterQueryOnProjectView8() {
-    try {
-      Prepare.THREAD_TRIM.set(true);
+    try (final TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
       MaterializationService.setThreadLocal();
       final String m = "select \"salary\", \"commission\",\n"
           + "\"deptno\", \"empid\", \"name\" from \"emps\"";
@@ -296,8 +295,6 @@ public class MaterializationTest {
           .enableMaterializations(true)
           .explainMatches("", CONTAINS_M0)
           .sameResultWithMaterializationsDisabled();
-    } finally {
-      Prepare.THREAD_TRIM.set(false);
     }
   }
 
@@ -905,8 +902,7 @@ public class MaterializationTest {
   }
 
   @Test public void testViewSchemaPath() {
-    try {
-      Prepare.THREAD_TRIM.set(true);
+    try (final TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
       MaterializationService.setThreadLocal();
       final String m = "select empno, deptno from emp";
       final String q = "select deptno from scott.emp";
@@ -942,8 +938,6 @@ public class MaterializationTest {
           .enableMaterializations(true)
           .explainMatches("", CONTAINS_M0)
           .sameResultWithMaterializationsDisabled();
-    } finally {
-      Prepare.THREAD_TRIM.set(false);
     }
   }
 
@@ -951,8 +945,7 @@ public class MaterializationTest {
     String q = "select *\n"
         + "from (select * from \"emps\" where \"empid\" < 300)\n"
         + "join (select * from \"emps\" where \"empid\" < 200) using (\"empid\")";
-    try {
-      Prepare.THREAD_TRIM.set(true);
+    try (final TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
       MaterializationService.setThreadLocal();
       CalciteAssert.that()
           .withMaterializations(JdbcTest.HR_MODEL,
@@ -973,8 +966,6 @@ public class MaterializationTest {
             }
           })
           .sameResultWithMaterializationsDisabled();
-    } finally {
-      Prepare.THREAD_TRIM.set(false);
     }
   }
 
@@ -982,8 +973,7 @@ public class MaterializationTest {
     String q = "select *\n"
         + "from (select * from \"emps\" where \"empid\" < 300)\n"
         + "join (select \"deptno\", count(*) as c from \"emps\" group by \"deptno\") using (\"deptno\")";
-    try {
-      Prepare.THREAD_TRIM.set(true);
+    try (final TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
       MaterializationService.setThreadLocal();
       CalciteAssert.that()
           .withMaterializations(JdbcTest.HR_MODEL,
@@ -994,8 +984,42 @@ public class MaterializationTest {
           .explainContains("EnumerableTableScan(table=[[hr, m0]])")
           .explainContains("EnumerableTableScan(table=[[hr, m1]])")
           .sameResultWithMaterializationsDisabled();
-    } finally {
-      Prepare.THREAD_TRIM.set(false);
+    }
+  }
+
+  @Test public void testMaterializationOnJoinQuery() {
+    final String q = "select *\n"
+        + "from \"emps\"\n"
+        + "join \"depts\" using (\"deptno\") where \"empid\" < 300 ";
+    try (final TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
+      MaterializationService.setThreadLocal();
+      CalciteAssert.that()
+          .withMaterializations(JdbcTest.HR_MODEL,
+              "m0", "select * from \"emps\" where \"empid\" < 500")
+          .query(q)
+          .enableMaterializations(true)
+          .explainContains("EnumerableTableScan(table=[[hr, m0]])")
+          .sameResultWithMaterializationsDisabled();
+    }
+  }
+
+  @Ignore("Creating mv for depts considering all its column throws exception")
+  @Test public void testMultiMaterializationOnJoinQuery() {
+    final String q = "select *\n"
+        + "from \"emps\"\n"
+        + "join \"depts\" using (\"deptno\") where \"empid\" < 300 "
+        + "and \"depts\".\"deptno\" > 200";
+    try (final TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
+      MaterializationService.setThreadLocal();
+      CalciteAssert.that()
+          .withMaterializations(JdbcTest.HR_MODEL,
+              "m0", "select * from \"emps\" where \"empid\" < 500",
+              "m1", "select * from \"depts\" where \"deptno\" > 100")
+          .query(q)
+          .enableMaterializations(true)
+          .explainContains("EnumerableTableScan(table=[[hr, m0]])")
+          .explainContains("EnumerableTableScan(table=[[hr, m1]])")
+          .sameResultWithMaterializationsDisabled();
     }
   }
 
@@ -1014,8 +1038,7 @@ public class MaterializationTest {
       {{"hr", "m1"}, {"hr", "m0"}},
       {{"hr", "m1"}, {"hr", "m1"}}};
 
-    try {
-      Prepare.THREAD_TRIM.set(true);
+    try (final TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
       MaterializationService.setThreadLocal();
       final List<List<List<String>>> substitutedNames = new ArrayList<>();
       CalciteAssert.that()
@@ -1034,8 +1057,6 @@ public class MaterializationTest {
           .sameResultWithMaterializationsDisabled();
       Collections.sort(substitutedNames, CASE_INSENSITIVE_LIST_LIST_COMPARATOR);
       assertThat(substitutedNames, is(list3(expectedNames)));
-    } finally {
-      Prepare.THREAD_TRIM.set(false);
     }
   }
 
@@ -1061,8 +1082,7 @@ public class MaterializationTest {
       {{"hr", "m2"}, {"hr", "m1"}},
       {{"hr", "m2"}, {"hr", "m2"}}};
 
-    try {
-      Prepare.THREAD_TRIM.set(true);
+    try (final TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
       MaterializationService.setThreadLocal();
       final List<List<List<String>>> substitutedNames = new ArrayList<>();
       CalciteAssert.that()
@@ -1082,8 +1102,6 @@ public class MaterializationTest {
           .sameResultWithMaterializationsDisabled();
       Collections.sort(substitutedNames, CASE_INSENSITIVE_LIST_LIST_COMPARATOR);
       assertThat(substitutedNames, is(list3(expectedNames)));
-    } finally {
-      Prepare.THREAD_TRIM.set(false);
     }
   }
 


[3/3] calcite git commit: Site: add committer, and post slides/video from Polyalgebra talk

Posted by jh...@apache.org.
Site: add committer, and post slides/video from Polyalgebra talk

Prevent jekyll from adding whitespace each time a committer is added.


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

Branch: refs/heads/master
Commit: 19361b94a4e44601e182f2d2298bc216c017bd81
Parents: bf098dd
Author: Julian Hyde <jh...@apache.org>
Authored: Mon Apr 4 16:38:19 2016 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu May 5 22:12:24 2016 -0700

----------------------------------------------------------------------
 site/_config.yml              |  2 +-
 site/_data/contributors.yml   |  6 ++++++
 site/_includes/news_item.html | 28 +++++++++++++++-------------
 site/_layouts/news_item.html  | 28 +++++++++++++++-------------
 site/community/index.md       | 14 +++++++++-----
 5 files changed, 46 insertions(+), 32 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/19361b94/site/_config.yml
----------------------------------------------------------------------
diff --git a/site/_config.yml b/site/_config.yml
index 8c78d7e..1086414 100644
--- a/site/_config.yml
+++ b/site/_config.yml
@@ -20,7 +20,7 @@ excerpt_separator: ""
 repository: https://github.com/apache/calcite
 destination: target
 exclude: [README.md,Gemfile*]
-keep_files: [".git", ".svn", "apidocs", "testapidocs", "avatica"]
+keep_files: [".git", ".svn", "apidocs", "testapidocs", "avatica", "docs/cassandra.html"]
 
 collections:
   docs:

http://git-wip-us.apache.org/repos/asf/calcite/blob/19361b94/site/_data/contributors.yml
----------------------------------------------------------------------
diff --git a/site/_data/contributors.yml b/site/_data/contributors.yml
index 98f8bf9..8752770 100644
--- a/site/_data/contributors.yml
+++ b/site/_data/contributors.yml
@@ -72,6 +72,12 @@
   githubId: maryannxue
   org: Intel
   role: Committer
+- name: Michael Mior
+  apacheId: mmior
+  githubId: michaelmior
+  org: University of Waterloo
+  role: Committer
+  homepage: http://michael.mior.ca/
 - name: Nick Dimiduk
   apacheId: ndimiduk
   githubId: ndimiduk

http://git-wip-us.apache.org/repos/asf/calcite/blob/19361b94/site/_includes/news_item.html
----------------------------------------------------------------------
diff --git a/site/_includes/news_item.html b/site/_includes/news_item.html
index 34eea0b..7b48ea6 100644
--- a/site/_includes/news_item.html
+++ b/site/_includes/news_item.html
@@ -31,20 +31,22 @@ limitations under the License.
     </span>
     {% capture homepage %}http://people.apache.org/~{{ post.author }}{% endcapture %}
     {% capture avatar %}http://people.apache.org/~{{ post.author }}/{{ post.author }}.jpg{% endcapture %}
-    {% for c in site.data.contributors %}
-      {% if c.apacheId == post.author %}
-        {% if c.homepage %}
-          {% assign homepage = c.homepage %}
-        {% else %}
-          {% capture homepage %}http://github.com/{{ c.githubId }}{% endcapture %}
+    {% capture discard %}
+      {% for c in site.data.contributors %}
+        {% if c.apacheId == post.author %}
+          {% if c.homepage %}
+            {% assign homepage = c.homepage %}
+          {% else %}
+            {% capture homepage %}http://github.com/{{ c.githubId }}{% endcapture %}
+          {% endif %}
+          {% if c.avatar %}
+            {% assign avatar = c.avatar %}
+          {% else %}
+            {% capture avatar %}http://github.com/{{ c.githubId }}.png{% endcapture %}
+          {% endif %}
         {% endif %}
-        {% if c.avatar %}
-          {% assign avatar = c.avatar %}
-        {% else %}
-          {% capture avatar %}http://github.com/{{ c.githubId }}.png{% endcapture %}
-        {% endif %}
-      {% endif %}
-    {% endfor %}
+      {% endfor %}
+    {% endcapture %}{% assign discard = nil %}
     <a href="{{ homepage }}" class="post-author">
       <img src="{{ avatar }}"
            class="avatar" alt="{{ post.author }} avatar"

http://git-wip-us.apache.org/repos/asf/calcite/blob/19361b94/site/_layouts/news_item.html
----------------------------------------------------------------------
diff --git a/site/_layouts/news_item.html b/site/_layouts/news_item.html
index e91e9e5..5fd0ecc 100644
--- a/site/_layouts/news_item.html
+++ b/site/_layouts/news_item.html
@@ -18,20 +18,22 @@ layout: news
     </span>
     {% capture homepage %}http://people.apache.org/~{{ page.author }}{% endcapture %}
     {% capture avatar %}http://people.apache.org/~{{ page.author }}/{{ page.author }}.jpg{% endcapture %}
-    {% for c in site.data.contributors %}
-      {% if c.apacheId == page.author %}
-        {% if c.homepage %}
-          {% assign homepage = c.homepage %}
-        {% else %}
-          {% capture homepage %}http://github.com/{{ c.githubId }}{% endcapture %}
+    {% capture discard %}
+      {% for c in site.data.contributors %}
+        {% if c.apacheId == page.author %}
+          {% if c.homepage %}
+            {% assign homepage = c.homepage %}
+          {% else %}
+            {% capture homepage %}http://github.com/{{ c.githubId }}{% endcapture %}
+          {% endif %}
+          {% if c.avatar %}
+            {% assign avatar = c.avatar %}
+          {% else %}
+            {% capture avatar %}http://github.com/{{ c.githubId }}.png{% endcapture %}
+          {% endif %}
         {% endif %}
-        {% if c.avatar %}
-          {% assign avatar = c.avatar %}
-        {% else %}
-          {% capture avatar %}http://github.com/{{ c.githubId }}.png{% endcapture %}
-        {% endif %}
-      {% endif %}
-    {% endfor %}
+      {% endfor %}
+    {% endcapture %}{% assign discard = nil %}
     <a href="{{ homepage }}" class="post-author">
       <img src="{{ avatar }}"
            class="avatar" alt="{{ page.author }} avatar"

http://git-wip-us.apache.org/repos/asf/calcite/blob/19361b94/site/community/index.md
----------------------------------------------------------------------
diff --git a/site/community/index.md b/site/community/index.md
index 61c5cbb..7e8bec0 100644
--- a/site/community/index.md
+++ b/site/community/index.md
@@ -26,13 +26,9 @@ limitations under the License.
 
 # Upcoming talks
 
-* 2016/03/30 <a href="http://conferences.oreilly.com/strata/hadoop-big-data-ca/public/schedule/detail/48180">Strata + Hadoop World</a>, San Jose (developer showcase)
-* 2016/04/13 <a href="http://hadoopsummit.org/dublin/agenda/">Hadoop Summit</a>, Dublin
-* 2016/04/13 [Hortonworks User Group](http://www.meetup.com/Washington-DC-Hortonworks-User-Group-Meetup/events/229494421/), Baltimore
-* 2016/04/20 [Hortonworks User Group](http://www.meetup.com/Washington-DC-Hortonworks-User-Group-Meetup/events/229668371/), Herndon
-* 2016/04/26 <a href="http://kafka-summit.org/schedule/">Kafka Summit</a>, San Francisco
 * 2016/05/10 <a href="http://events.linuxfoundation.org/events/apache-big-data-north-america/program/schedule">ApacheCon Big Data North America</a>, Vancouver
 * 2016/05/24 <a href="http://www-conf.slac.stanford.edu/xldb2016/Program.asp">XLDB</a>, Palo Alto
+* 2016/06/29 <a href="http://hadoopsummit.org/san-jose/agenda/">Hadoop Summit</a>, San Jose
 
 # Project Members
 
@@ -116,6 +112,14 @@ At Samza meetup, Mountain View, CA, 2016
 
 <iframe src="//www.slideshare.net/slideshow/embed_code/key/rzaptOy3H8K6Gz" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe>
 
+## Planning with Polyalgebra: Bringing together relational, complex and machine learning algebra
+
+As Hadoop Summit, Dublin, 2016
+[[video](https://www.youtube.com/watch?v=fHZqbe3iPMc)],
+[[slides](http://www.slideshare.net/julianhyde/planning-with-polyalgebra-bringing-together-relational-complex-and-machine-learning-algebra)].
+
+{% oembed https://www.youtube.com/watch?v=fHZqbe3iPMc %}
+
 ## More talks
 
 * <a href="https://github.com/julianhyde/share/blob/master/slides/hive-cbo-seattle-2014.pdf?raw=true">Cost-based optimization in Hive 0.14</a> (Seattle, 2014)


[2/3] calcite git commit: [CALCITE-855] UNNEST with multiple arguments

Posted by jh...@apache.org.
[CALCITE-855] UNNEST with multiple arguments

Change a few FlatList contructor methods to return ComparableList.


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

Branch: refs/heads/master
Commit: 354e8240d49de42bd6972d985fea692e4785e8b0
Parents: 6031b7a
Author: Julian Hyde <jh...@apache.org>
Authored: Wed May 4 15:25:00 2016 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu May 5 22:07:12 2016 -0700

----------------------------------------------------------------------
 .../adapter/enumerable/EnumerableUncollect.java |  58 +++------
 .../org/apache/calcite/runtime/FlatLists.java   |  29 +++--
 .../apache/calcite/runtime/SqlFunctions.java    | 129 ++++++++++++++++++-
 .../apache/calcite/sql/SqlUnnestOperator.java   |  36 +++---
 .../sql/type/ComparableOperandTypeChecker.java  |  18 +--
 .../sql/type/CompositeOperandTypeChecker.java   |  30 ++++-
 .../type/CompositeSingleOperandTypeChecker.java |   2 +-
 .../apache/calcite/sql/type/OperandTypes.java   |  24 +++-
 .../sql/type/SameOperandTypeChecker.java        |  12 +-
 .../calcite/sql2rel/SqlToRelConverter.java      |  20 ++-
 .../org/apache/calcite/util/BuiltInMethod.java  |   2 +-
 .../java/org/apache/calcite/test/JdbcTest.java  |  38 ++++++
 .../java/org/apache/calcite/util/UtilTest.java  |  55 ++++++--
 .../linq4j/CartesianProductEnumerator.java      |  14 +-
 .../java/org/apache/calcite/linq4j/Linq4j.java  |  23 +++-
 15 files changed, 360 insertions(+), 130 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java
index 20bbaf2..0dfa57f 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollect.java
@@ -19,15 +19,15 @@ package org.apache.calcite.adapter.enumerable;
 import org.apache.calcite.linq4j.tree.BlockBuilder;
 import org.apache.calcite.linq4j.tree.Expression;
 import org.apache.calcite.linq4j.tree.Expressions;
-import org.apache.calcite.linq4j.tree.ParameterExpression;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Uncollect;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.util.BuiltInMethod;
-import org.apache.calcite.util.ImmutableIntList;
+import org.apache.calcite.util.IntList;
 
-import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -83,48 +83,24 @@ public class EnumerableUncollect extends Uncollect implements EnumerableRel {
             JavaRowFormat.LIST);
 
     // final Enumerable<List<Employee>> child = <<child adapter>>;
-    // return child.selectMany(LIST_TO_ENUMERABLE);
+    // return child.selectMany(FLAT_PRODUCT);
     final Expression child_ =
         builder.append(
             "child", result.block);
-    final Expression lambda;
-    if (withOrdinality) {
-      final BlockBuilder builder2 = new BlockBuilder();
-      final ParameterExpression o_ = Expressions.parameter(Modifier.FINAL,
-          result.physType.getJavaRowType(),
-          "o");
-      final Expression list_ = builder2.append("list",
-          Expressions.new_(ArrayList.class));
-      final ParameterExpression i_ = Expressions.parameter(int.class, "i");
-      final BlockBuilder builder3 = new BlockBuilder();
-      final Expression v_ =
-          builder3.append("v",
-              Expressions.call(o_, BuiltInMethod.LIST_GET.method, i_));
-      final List<Expression> expressions = new ArrayList<>();
-      final PhysType componentPhysType = result.physType.component(0);
-      final int fieldCount = componentPhysType.getRowType().getFieldCount();
-      expressions.addAll(
-          componentPhysType.accessors(v_,
-              ImmutableIntList.identity(fieldCount)));
-      expressions.add(Expressions.add(i_, Expressions.constant(1)));
-      builder3.add(
-          Expressions.statement(
-              Expressions.call(list_, BuiltInMethod.COLLECTION_ADD.method,
-                  physType.record(expressions))));
-      builder2.add(
-          Expressions.for_(
-              Expressions.declare(0, i_, Expressions.constant(0)),
-              Expressions.lessThan(i_,
-                  Expressions.call(o_, BuiltInMethod.COLLECTION_SIZE.method)),
-              Expressions.postIncrementAssign(i_),
-              builder3.toBlock()));
-      builder2.add(
-          Expressions.return_(null,
-              Expressions.call(BuiltInMethod.AS_ENUMERABLE2.method, list_)));
-      lambda = Expressions.lambda(builder2.toBlock(), o_);
-    } else {
-      lambda = Expressions.call(BuiltInMethod.LIST_TO_ENUMERABLE.method);
+    final List<Integer> fieldCounts = new ArrayList<>();
+    for (RelDataTypeField field : child.getRowType().getFieldList()) {
+      final RelDataType type = field.getType();
+      final RelDataType elementType = type.getComponentType();
+      if (elementType.isStruct()) {
+        fieldCounts.add(elementType.getFieldCount());
+      } else {
+        fieldCounts.add(-1);
+      }
     }
+    final Expression lambda =
+        Expressions.call(BuiltInMethod.FLAT_PRODUCT.method,
+            Expressions.constant(IntList.toArray(fieldCounts)),
+            Expressions.constant(withOrdinality));
     builder.add(
         Expressions.return_(null,
             Expressions.call(child_,

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/runtime/FlatLists.java b/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
index 5e8d046..eba51b7 100644
--- a/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
+++ b/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
@@ -37,7 +37,7 @@ public class FlatLists {
       new ComparableEmptyList();
 
   /** Creates a flat list with 0 elements. */
-  public static <T> List<T> of() {
+  public static <T> ComparableList<T> of() {
     //noinspection unchecked
     return COMPARABLE_EMPTY_LIST;
   }
@@ -135,23 +135,24 @@ public class FlatLists {
    * @param t Array of members of list
    * @return List containing the given members
    */
-  private static <T extends Comparable> List<T> flatList_(T[] t, boolean copy) {
+  private static <T extends Object & Comparable> ComparableList<T>
+  flatList_(T[] t, boolean copy) {
     switch (t.length) {
     case 0:
       //noinspection unchecked
       return COMPARABLE_EMPTY_LIST;
     case 1:
-      return Collections.singletonList(t[0]);
+      return new Flat1List<>(t[0]);
     case 2:
-      return new Flat2List<T>(t[0], t[1]);
+      return new Flat2List<>(t[0], t[1]);
     case 3:
-      return new Flat3List<T>(t[0], t[1], t[2]);
+      return new Flat3List<>(t[0], t[1], t[2]);
     case 4:
-      return new Flat4List<T>(t[0], t[1], t[2], t[3]);
+      return new Flat4List<>(t[0], t[1], t[2], t[3]);
     case 5:
-      return new Flat5List<T>(t[0], t[1], t[2], t[3], t[4]);
+      return new Flat5List<>(t[0], t[1], t[2], t[3], t[4]);
     case 6:
-      return new Flat6List<T>(t[0], t[1], t[2], t[3], t[4], t[5]);
+      return new Flat6List<>(t[0], t[1], t[2], t[3], t[4], t[5]);
     default:
       // REVIEW: AbstractList contains a modCount field; we could
       //   write our own implementation and reduce creation overhead a
@@ -202,12 +203,21 @@ public class FlatLists {
    * @return List containing the given members
    */
   public static <T> List<T> of(List<T> t) {
+    return of_(t);
+  }
+
+  public static <T extends Comparable> ComparableList<T>
+  ofComparable(List<T> t) {
+    return of_(t);
+  }
+
+  private static <T> ComparableList<T> of_(List<T> t) {
     switch (t.size()) {
     case 0:
       //noinspection unchecked
       return COMPARABLE_EMPTY_LIST;
     case 1:
-      return Collections.singletonList(t.get(0));
+      return new Flat1List<>(t.get(0));
     case 2:
       return new Flat2List<>(t.get(0), t.get(1));
     case 3:
@@ -1239,6 +1249,7 @@ public class FlatLists {
       return a.compareTo(b);
     }
   }
+
 }
 
 // End FlatLists.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index d9657b2..eed92f5 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -19,7 +19,10 @@ package org.apache.calcite.runtime;
 import org.apache.calcite.DataContext;
 import org.apache.calcite.avatica.util.ByteString;
 import org.apache.calcite.avatica.util.DateTimeUtils;
+import org.apache.calcite.linq4j.AbstractEnumerable;
+import org.apache.calcite.linq4j.CartesianProductEnumerator;
 import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Enumerator;
 import org.apache.calcite.linq4j.Linq4j;
 import org.apache.calcite.linq4j.function.Deterministic;
 import org.apache.calcite.linq4j.function.Function1;
@@ -32,6 +35,8 @@ import java.math.MathContext;
 import java.sql.SQLException;
 import java.sql.Timestamp;
 import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -67,6 +72,28 @@ public class SqlFunctions {
         }
       };
 
+  private static final Function1<Object[], Enumerable<Object[]>>
+  ARRAY_CARTESIAN_PRODUCT =
+      new Function1<Object[], Enumerable<Object[]>>() {
+        public Enumerable<Object[]> apply(Object[] lists) {
+          final List<Enumerator<Object>> enumerators = new ArrayList<>();
+          for (Object list : lists) {
+            enumerators.add(Linq4j.enumerator((List) list));
+          }
+          final Enumerator<List<Object>> product = Linq4j.product(enumerators);
+          return new AbstractEnumerable<Object[]>() {
+            public Enumerator<Object[]> enumerator() {
+              return Linq4j.transform(product,
+                  new Function1<List<Object>, Object[]>() {
+                    public Object[] apply(List<Object> list) {
+                      return list.toArray();
+                    }
+                  });
+            }
+          };
+        }
+      };
+
   /** Holds, for each thread, a map from sequence name to sequence current
    * value.
    *
@@ -1423,15 +1450,109 @@ public class SqlFunctions {
     }
   }
 
-  /** Returns a lambda that converts a list to an enumerable. */
-  public static <E> Function1<List<E>, Enumerable<E>> listToEnumerable() {
-    //noinspection unchecked
-    return (Function1<List<E>, Enumerable<E>>) (Function1) LIST_AS_ENUMERABLE;
+  public static <E extends Comparable>
+  Function1<Object, Enumerable<FlatLists.ComparableList<E>>>
+  flatProduct(final int[] fieldCounts, final boolean withOrdinality) {
+    if (fieldCounts.length == 1) {
+      if (!withOrdinality) {
+        //noinspection unchecked
+        return (Function1) LIST_AS_ENUMERABLE;
+      } else {
+        return new Function1<Object, Enumerable<FlatLists.ComparableList<E>>>() {
+          public Enumerable<FlatLists.ComparableList<E>> apply(Object row) {
+            return p2(new Object[] {row}, fieldCounts, true);
+          }
+        };
+      }
+    }
+    return new Function1<Object, Enumerable<FlatLists.ComparableList<E>>>() {
+      public Enumerable<FlatLists.ComparableList<E>> apply(Object lists) {
+        return p2((Object[]) lists, fieldCounts, withOrdinality);
+      }
+    };
+  }
+
+  private static <E extends Comparable>
+  Enumerable<FlatLists.ComparableList<E>> p2(Object[] lists, int[] fieldCounts,
+      boolean withOrdinality) {
+    final List<Enumerator<List<E>>> enumerators = new ArrayList<>();
+    int totalFieldCount = 0;
+    for (int i = 0; i < lists.length; i++) {
+      int fieldCount = fieldCounts[i];
+      if (fieldCount < 0) {
+        ++totalFieldCount;
+        @SuppressWarnings("unchecked")
+        List<E> list = (List<E>) lists[i];
+        enumerators.add(
+            Linq4j.transform(
+                Linq4j.enumerator(list),
+                new Function1<E, List<E>>() {
+                  public List<E> apply(E a0) {
+                    return FlatLists.of(a0);
+                  }
+                }));
+      } else {
+        totalFieldCount += fieldCount;
+        @SuppressWarnings("unchecked")
+        List<List<E>> list = (List<List<E>>) lists[i];
+        enumerators.add(Linq4j.enumerator(list));
+      }
+    }
+    if (withOrdinality) {
+      ++totalFieldCount;
+    }
+    return product(enumerators, totalFieldCount, withOrdinality);
   }
 
   public static Object[] array(Object... args) {
     return args;
   }
+
+  /** Similar to {@link Linq4j#product(Iterable)} but each resulting list
+   * implements {@link FlatLists.ComparableList}. */
+  public static <E extends Comparable>
+  Enumerable<FlatLists.ComparableList<E>>
+  product(final List<Enumerator<List<E>>> enumerators, final int fieldCount,
+      final boolean withOrdinality) {
+    return new AbstractEnumerable<FlatLists.ComparableList<E>>() {
+      public Enumerator<FlatLists.ComparableList<E>> enumerator() {
+        return new ProductComparableListEnumerator<>(enumerators, fieldCount,
+            withOrdinality);
+      }
+    };
+  }
+
+  /** Enumerates over the cartesian product of the given lists, returning
+   * a comparable list for each row. */
+  private static class ProductComparableListEnumerator<E extends Comparable>
+      extends CartesianProductEnumerator<List<E>, FlatLists.ComparableList<E>> {
+    final E[] flatElements;
+    final List<E> list;
+    private final boolean withOrdinality;
+    private int ordinality;
+
+    ProductComparableListEnumerator(List<Enumerator<List<E>>> enumerators,
+        int fieldCount, boolean withOrdinality) {
+      super(enumerators);
+      this.withOrdinality = withOrdinality;
+      flatElements = (E[]) new Comparable[fieldCount];
+      list = Arrays.asList(flatElements);
+    }
+
+    public FlatLists.ComparableList<E> current() {
+      int i = 0;
+      for (Object element : (Object[]) elements) {
+        final List list2 = (List) element;
+        Object[] a = list2.toArray();
+        System.arraycopy(a, 0, flatElements, i, a.length);
+        i += a.length;
+      }
+      if (withOrdinality) {
+        flatElements[i] = (E) new Integer(++ordinality); // 1-based
+      }
+      return FlatLists.ofComparable(list);
+    }
+  }
 }
 
 // End SqlFunctions.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java
index 096f361..b3d0a1b 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUnnestOperator.java
@@ -21,7 +21,9 @@ import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.type.ArraySqlType;
 import org.apache.calcite.sql.type.MultisetSqlType;
 import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.SqlOperandCountRanges;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.Util;
 
 /**
  * The <code>UNNEST</code> operator.
@@ -44,33 +46,33 @@ public class SqlUnnestOperator extends SqlFunctionalOperator {
         true,
         null,
         null,
-        OperandTypes.SCALAR_OR_RECORD_COLLECTION);
+        OperandTypes.repeat(SqlOperandCountRanges.from(1),
+            OperandTypes.SCALAR_OR_RECORD_COLLECTION));
     this.withOrdinality = withOrdinality;
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public RelDataType inferReturnType(
-      SqlOperatorBinding opBinding) {
-    RelDataType type = opBinding.getOperandType(0);
-    if (type.isStruct()) {
-      type = type.getFieldList().get(0).getType();
-    }
-    assert type instanceof ArraySqlType || type instanceof MultisetSqlType;
-    if (withOrdinality) {
-      final RelDataTypeFactory.FieldInfoBuilder builder =
-          opBinding.getTypeFactory().builder();
+  @Override public RelDataType inferReturnType(SqlOperatorBinding opBinding) {
+    final RelDataTypeFactory.FieldInfoBuilder builder =
+        opBinding.getTypeFactory().builder();
+    for (Integer operand : Util.range(opBinding.getOperandCount())) {
+      RelDataType type = opBinding.getOperandType(operand);
+      if (type.isStruct()) {
+        type = type.getFieldList().get(0).getType();
+      }
+      assert type instanceof ArraySqlType || type instanceof MultisetSqlType;
       if (type.getComponentType().isStruct()) {
         builder.addAll(type.getComponentType().getFieldList());
       } else {
-        builder.add(SqlUtil.deriveAliasFromOrdinal(0), type.getComponentType());
+        builder.add(SqlUtil.deriveAliasFromOrdinal(operand),
+            type.getComponentType());
       }
-      return builder
-          .add(ORDINALITY_COLUMN_NAME, SqlTypeName.INTEGER)
-          .build();
-    } else {
-      return type.getComponentType();
     }
+    if (withOrdinality) {
+      builder.add(ORDINALITY_COLUMN_NAME, SqlTypeName.INTEGER);
+    }
+    return builder.build();
   }
 
   @Override public void unparse(SqlWriter writer, SqlCall call, int leftPrec,

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/sql/type/ComparableOperandTypeChecker.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/type/ComparableOperandTypeChecker.java b/core/src/main/java/org/apache/calcite/sql/type/ComparableOperandTypeChecker.java
index 3370b5f..0849b58 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/ComparableOperandTypeChecker.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/ComparableOperandTypeChecker.java
@@ -19,14 +19,10 @@ package org.apache.calcite.sql.type;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeComparability;
 import org.apache.calcite.sql.SqlCallBinding;
-import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlOperatorBinding;
-import org.apache.calcite.sql.SqlUtil;
 
 import com.google.common.base.Preconditions;
 
-import java.util.Collections;
-
 /**
  * Type checking strategy which verifies that types have the required attributes
  * to be used as arguments to comparison operators.
@@ -99,14 +95,7 @@ public class ComparableOperandTypeChecker extends SameOperandTypeChecker {
     boolean b = true;
     for (int i = 0; i < nOperands; ++i) {
       RelDataType type = callBinding.getOperandType(i);
-      boolean result;
-      if (type.getComparability().ordinal()
-          < requiredComparability.ordinal()) {
-        result = false;
-      } else {
-        result = true;
-      }
-      if (!result) {
+      if (type.getComparability().ordinal() < requiredComparability.ordinal()) {
         b = false;
       }
     }
@@ -116,9 +105,8 @@ public class ComparableOperandTypeChecker extends SameOperandTypeChecker {
     return b;
   }
 
-  public String getAllowedSignatures(SqlOperator op, String opName) {
-    return SqlUtil.getAliasedSignature(op, opName,
-        Collections.nCopies(nOperands, "COMPARABLE_TYPE"));
+  @Override protected String getTypeName() {
+    return "COMPARABLE_TYPE";
   }
 
   @Override public Consistency getConsistency() {

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/sql/type/CompositeOperandTypeChecker.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/type/CompositeOperandTypeChecker.java b/core/src/main/java/org/apache/calcite/sql/type/CompositeOperandTypeChecker.java
index 9bfea87..cb88823 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/CompositeOperandTypeChecker.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/CompositeOperandTypeChecker.java
@@ -20,6 +20,7 @@ import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlOperandCountRange;
 import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.util.Util;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -76,11 +77,12 @@ import javax.annotation.Nullable;
  * AND composition, only the first rule is used for signature generation.
  */
 public class CompositeOperandTypeChecker implements SqlOperandTypeChecker {
+  private final SqlOperandCountRange range;
   //~ Enums ------------------------------------------------------------------
 
   /** How operands are composed. */
   public enum Composition {
-    AND, OR, SEQUENCE
+    AND, OR, SEQUENCE, REPEAT
   }
 
   //~ Instance fields --------------------------------------------------------
@@ -98,11 +100,14 @@ public class CompositeOperandTypeChecker implements SqlOperandTypeChecker {
   CompositeOperandTypeChecker(
       Composition composition,
       ImmutableList<? extends SqlOperandTypeChecker> allowedRules,
-      @Nullable String allowedSignatures) {
+      @Nullable String allowedSignatures,
+      @Nullable SqlOperandCountRange range) {
     this.allowedRules = Preconditions.checkNotNull(allowedRules);
     this.composition = Preconditions.checkNotNull(composition);
     this.allowedSignatures = allowedSignatures;
-    assert allowedRules.size() > 1;
+    this.range = range;
+    assert (range != null) == (composition == Composition.REPEAT);
+    assert allowedRules.size() + (range == null ? 0 : 1) > 1;
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -147,6 +152,8 @@ public class CompositeOperandTypeChecker implements SqlOperandTypeChecker {
 
   public SqlOperandCountRange getOperandCountRange() {
     switch (composition) {
+    case REPEAT:
+      return range;
     case SEQUENCE:
       return SqlOperandCountRanges.of(allowedRules.size());
     case AND:
@@ -253,6 +260,23 @@ public class CompositeOperandTypeChecker implements SqlOperandTypeChecker {
 
   private boolean check(SqlCallBinding callBinding) {
     switch (composition) {
+    case REPEAT:
+      if (!range.isValidCount(callBinding.getOperandCount())) {
+        return false;
+      }
+      for (int operand : Util.range(callBinding.getOperandCount())) {
+        for (SqlOperandTypeChecker rule : allowedRules) {
+          if (!((SqlSingleOperandTypeChecker) rule).checkSingleOperandType(
+              callBinding,
+              callBinding.getCall().operand(operand),
+              0,
+              false)) {
+            return false;
+          }
+        }
+      }
+      return true;
+
     case SEQUENCE:
       if (callBinding.getOperandCount() != allowedRules.size()) {
         return false;

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/sql/type/CompositeSingleOperandTypeChecker.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/type/CompositeSingleOperandTypeChecker.java b/core/src/main/java/org/apache/calcite/sql/type/CompositeSingleOperandTypeChecker.java
index de8c303..7142605 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/CompositeSingleOperandTypeChecker.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/CompositeSingleOperandTypeChecker.java
@@ -41,7 +41,7 @@ public class CompositeSingleOperandTypeChecker
       CompositeOperandTypeChecker.Composition composition,
       ImmutableList<? extends SqlSingleOperandTypeChecker> allowedRules,
       String allowedSignatures) {
-    super(composition, allowedRules, allowedSignatures);
+    super(composition, allowedRules, allowedSignatures, null);
   }
 
   //~ Methods ----------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index eafd7c6..1d43ba0 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -87,17 +87,16 @@ public abstract class OperandTypes {
   public static SqlOperandTypeChecker or(SqlOperandTypeChecker... rules) {
     return new CompositeOperandTypeChecker(
         CompositeOperandTypeChecker.Composition.OR,
-        ImmutableList.copyOf(rules), null);
+        ImmutableList.copyOf(rules), null, null);
   }
 
   /**
-   * Creates a single-operand checker that passes if any one of the rules
-   * passes.
+   * Creates a checker that passes if all of the rules pass.
    */
   public static SqlOperandTypeChecker and(SqlOperandTypeChecker... rules) {
     return new CompositeOperandTypeChecker(
         CompositeOperandTypeChecker.Composition.AND,
-        ImmutableList.copyOf(rules), null);
+        ImmutableList.copyOf(rules), null, null);
   }
 
   /**
@@ -112,8 +111,8 @@ public abstract class OperandTypes {
   }
 
   /**
-   * Creates a single-operand checker that passes if any one of the rules
-   * passes.
+   * Creates a single-operand checker that passes if all of the rules
+   * pass.
    */
   public static SqlSingleOperandTypeChecker and(
       SqlSingleOperandTypeChecker... rules) {
@@ -129,7 +128,18 @@ public abstract class OperandTypes {
       SqlSingleOperandTypeChecker... rules) {
     return new CompositeOperandTypeChecker(
         CompositeOperandTypeChecker.Composition.SEQUENCE,
-        ImmutableList.copyOf(rules), allowedSignatures);
+        ImmutableList.copyOf(rules), allowedSignatures, null);
+  }
+
+  /**
+   * Creates a checker that passes if all of the rules pass for each operand,
+   * using a given operand count strategy.
+   */
+  public static SqlOperandTypeChecker repeat(SqlOperandCountRange range,
+      SqlSingleOperandTypeChecker... rules) {
+    return new CompositeOperandTypeChecker(
+        CompositeOperandTypeChecker.Composition.REPEAT,
+        ImmutableList.copyOf(rules), null, range);
   }
 
   // ----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/sql/type/SameOperandTypeChecker.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SameOperandTypeChecker.java b/core/src/main/java/org/apache/calcite/sql/type/SameOperandTypeChecker.java
index b88ea2e..2bb75e0 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SameOperandTypeChecker.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SameOperandTypeChecker.java
@@ -134,12 +134,18 @@ public class SameOperandTypeChecker implements SqlSingleOperandTypeChecker {
     }
   }
 
-  // implement SqlOperandTypeChecker
   public String getAllowedSignatures(SqlOperator op, String opName) {
+    final String typeName = getTypeName();
     return SqlUtil.getAliasedSignature(op, opName,
         nOperands == -1
-            ? ImmutableList.of("EQUIVALENT_TYPE", "EQUIVALENT_TYPE", "...")
-            : Collections.nCopies(nOperands, "EQUIVALENT_TYPE"));
+            ? ImmutableList.of(typeName, typeName, "...")
+            : Collections.nCopies(nOperands, typeName));
+  }
+
+  /** Override to change the behavior of
+   * {@link #getAllowedSignatures(SqlOperator, String)}. */
+  protected String getTypeName() {
+    return "EQUIVALENT_TYPE";
   }
 
   public boolean checkSingleOperandType(

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index e0e8325..556839b 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -2052,19 +2052,25 @@ public class SqlToRelConverter {
 
     case UNNEST:
       call = (SqlCall) from;
-      final SqlNode node = call.operand(0);
+      final List<SqlNode> nodes = call.getOperandList();
       final SqlUnnestOperator operator = (SqlUnnestOperator) call.getOperator();
-      replaceSubqueries(bb, node, RelOptUtil.Logic.TRUE_FALSE_UNKNOWN);
-      final RelNode childRel =
+      for (SqlNode node : nodes) {
+        replaceSubqueries(bb, node, RelOptUtil.Logic.TRUE_FALSE_UNKNOWN);
+      }
+      final List<RexNode> exprs = new ArrayList<>();
+      final List<String> fieldNames = new ArrayList<>();
+      for (Ord<SqlNode> node : Ord.zip(nodes)) {
+        exprs.add(bb.convertExpression(node.e));
+        fieldNames.add(validator.deriveAlias(node.e, node.i));
+      }
+      final RelNode input =
           RelOptUtil.createProject(
               (null != bb.root) ? bb.root : LogicalValues.createOneRow(cluster),
-              Collections.singletonList(bb.convertExpression(node)),
-              Collections.singletonList(validator.deriveAlias(node, 0)),
-              true);
+              exprs, fieldNames, true);
 
       Uncollect uncollect =
           new Uncollect(cluster, cluster.traitSetOf(Convention.NONE),
-              childRel, operator.withOrdinality);
+              input, operator.withOrdinality);
       bb.setRoot(uncollect, true);
       return;
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index fd5f78f..c069b91 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -176,6 +176,7 @@ public enum BuiltInMethod {
   FUNCTION1_APPLY(Function1.class, "apply", Object.class),
   ARRAYS_AS_LIST(Arrays.class, "asList", Object[].class),
   ARRAY(SqlFunctions.class, "array", Object[].class),
+  FLAT_PRODUCT(SqlFunctions.class, "flatProduct", int[].class, boolean.class),
   LIST_N(FlatLists.class, "copyOf", Comparable[].class),
   LIST2(FlatLists.class, "of", Object.class, Object.class),
   LIST3(FlatLists.class, "of", Object.class, Object.class, Object.class),
@@ -191,7 +192,6 @@ public enum BuiltInMethod {
   AS_ENUMERABLE(Linq4j.class, "asEnumerable", Object[].class),
   AS_ENUMERABLE2(Linq4j.class, "asEnumerable", Iterable.class),
   ENUMERABLE_TO_LIST(ExtendedEnumerable.class, "toList"),
-  LIST_TO_ENUMERABLE(SqlFunctions.class, "listToEnumerable"),
   AS_LIST(Primitive.class, "asList", Object.class),
   ENUMERATOR_CURRENT(Enumerator.class, "current"),
   ENUMERATOR_MOVE_NEXT(Enumerator.class, "moveNext"),

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/test/java/org/apache/calcite/test/JdbcTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index fb5eb8a..21c8ce1 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -2248,6 +2248,44 @@ public class JdbcTest {
             "name=Sales; empid=150; deptno=10; name0=Sebastian; salary=7000.0; commission=null");
   }
 
+  @Test public void testUnnestArrayScalarArray() {
+    CalciteAssert.hr()
+        .query("select d.\"name\", e.*\n"
+            + "from \"hr\".\"depts\" as d,\n"
+            + " UNNEST(d.\"employees\", array[1, 2]) as e")
+        .returnsUnordered(
+            "name=HR; empid=200; deptno=20; name0=Eric; salary=8000.0; commission=500; EXPR$1=1",
+            "name=HR; empid=200; deptno=20; name0=Eric; salary=8000.0; commission=500; EXPR$1=2",
+            "name=Sales; empid=100; deptno=10; name0=Bill; salary=10000.0; commission=1000; EXPR$1=1",
+            "name=Sales; empid=100; deptno=10; name0=Bill; salary=10000.0; commission=1000; EXPR$1=2",
+            "name=Sales; empid=150; deptno=10; name0=Sebastian; salary=7000.0; commission=null; EXPR$1=1",
+            "name=Sales; empid=150; deptno=10; name0=Sebastian; salary=7000.0; commission=null; EXPR$1=2");
+  }
+
+  @Test public void testUnnestArrayScalarArrayAliased() {
+    CalciteAssert.hr()
+        .query("select d.\"name\", e.*\n"
+            + "from \"hr\".\"depts\" as d,\n"
+            + " UNNEST(d.\"employees\", array[1, 2]) as e (ei, d, n, s, c, i)\n"
+            + "where ei + i > 151")
+        .returnsUnordered(
+            "name=HR; EI=200; D=20; N=Eric; S=8000.0; C=500; I=1",
+            "name=HR; EI=200; D=20; N=Eric; S=8000.0; C=500; I=2",
+            "name=Sales; EI=150; D=10; N=Sebastian; S=7000.0; C=null; I=2");
+  }
+
+  @Test public void testUnnestArrayScalarArrayWithOrdinal() {
+    CalciteAssert.hr()
+        .query("select d.\"name\", e.*\n"
+            + "from \"hr\".\"depts\" as d,\n"
+            + " UNNEST(d.\"employees\", array[1, 2]) with ordinality as e (ei, d, n, s, c, i, o)\n"
+            + "where ei + i > 151")
+        .returnsUnordered(
+            "name=HR; EI=200; D=20; N=Eric; S=8000.0; C=500; I=1; O=2",
+            "name=HR; EI=200; D=20; N=Eric; S=8000.0; C=500; I=2; O=4",
+            "name=Sales; EI=150; D=10; N=Sebastian; S=7000.0; C=null; I=2; O=5");
+  }
+
   private CalciteAssert.AssertQuery withFoodMartQuery(int id)
       throws IOException {
     final FoodmartTest.FoodMartQuerySet set =

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/core/src/test/java/org/apache/calcite/util/UtilTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/util/UtilTest.java b/core/src/test/java/org/apache/calcite/util/UtilTest.java
index 548b538..9afa6b6 100644
--- a/core/src/test/java/org/apache/calcite/util/UtilTest.java
+++ b/core/src/test/java/org/apache/calcite/util/UtilTest.java
@@ -19,12 +19,16 @@ package org.apache.calcite.util;
 import org.apache.calcite.avatica.AvaticaUtils;
 import org.apache.calcite.avatica.util.Spaces;
 import org.apache.calcite.examples.RelBuilderExample;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.linq4j.Linq4j;
 import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.linq4j.function.Function1;
 import org.apache.calcite.linq4j.function.Parameter;
 import org.apache.calcite.runtime.ConsList;
 import org.apache.calcite.runtime.FlatLists;
 import org.apache.calcite.runtime.Resources;
+import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.runtime.Utilities;
 import org.apache.calcite.sql.SqlDialect;
 import org.apache.calcite.sql.util.SqlBuilder;
@@ -365,7 +369,7 @@ public class UtilTest {
    * Tests {@link org.apache.calcite.util.CastingList} and {@link Util#cast}.
    */
   @Test public void testCastingList() {
-    final List<Number> numberList = new ArrayList<Number>();
+    final List<Number> numberList = new ArrayList<>();
     numberList.add(new Integer(1));
     numberList.add(null);
     numberList.add(new Integer(2));
@@ -703,7 +707,7 @@ public class UtilTest {
 
     List<String> listEmpty = Collections.emptyList();
     List<String> listAbc = Arrays.asList("a", "b", "c");
-    List<String> listEmpty2 = new ArrayList<String>();
+    List<String> listEmpty2 = new ArrayList<>();
 
     // Made up of three lists, two of which are empty
     list = CompositeList.of(listEmpty, listAbc, listEmpty2);
@@ -763,7 +767,7 @@ public class UtilTest {
             "Hello, {0}, what a nice {1}.", "world", "day"));
 
     // Our extended message format. First, just strings.
-    final HashMap<Object, Object> map = new HashMap<Object, Object>();
+    final HashMap<Object, Object> map = new HashMap<>();
     map.put("person", "world");
     map.put("time", "day");
     assertEquals(
@@ -813,7 +817,7 @@ public class UtilTest {
         Collections.<String>emptyList(), template2.getParameterNames());
     assertEquals(
         "Don't expand this {brace}.",
-        template2.format(Collections.<Object, Object>emptyMap()));
+        template2.format(Collections.emptyMap()));
 
     // Empty template.
     assertEquals("", Template.formatByName("", map));
@@ -904,7 +908,7 @@ public class UtilTest {
    */
   @Test public void testPairAdjacents() {
     List<String> strings = Arrays.asList("a", "b", "c");
-    List<String> result = new ArrayList<String>();
+    List<String> result = new ArrayList<>();
     for (Pair<String, String> pair : Pair.adjacents(strings)) {
       result.add(pair.toString());
     }
@@ -929,7 +933,7 @@ public class UtilTest {
    */
   @Test public void testPairFirstAnd() {
     List<String> strings = Arrays.asList("a", "b", "c");
-    List<String> result = new ArrayList<String>();
+    List<String> result = new ArrayList<>();
     for (Pair<String, String> pair : Pair.firstAnd(strings)) {
       result.add(pair.toString());
     }
@@ -1045,13 +1049,13 @@ public class UtilTest {
   }
 
   private List<Integer> checkIntegerIntervalSet(String s, int... ints) {
-    List<Integer> list = new ArrayList<Integer>();
+    List<Integer> list = new ArrayList<>();
     final Set<Integer> set = IntegerIntervalSet.of(s);
     assertEquals(set.size(), ints.length);
     for (Integer integer : set) {
       list.add(integer);
     }
-    assertEquals(new HashSet<Integer>(IntList.asList(ints)), set);
+    assertEquals(new HashSet<>(IntList.asList(ints)), set);
     return list;
   }
 
@@ -1210,6 +1214,41 @@ public class UtilTest {
     }
   }
 
+  private <E> List<E> l1(E e) {
+    return Collections.singletonList(e);
+  }
+
+  private <E> List<E> l2(E e0, E e1) {
+    return Arrays.asList(e0, e1);
+  }
+
+  private <E> List<E> l3(E e0, E e1, E e2) {
+    return Arrays.asList(e0, e1, e2);
+  }
+
+  @Test public void testFlatListProduct() {
+    final List<Enumerator<List<String>>> list = new ArrayList<>();
+    list.add(Linq4j.enumerator(l2(l1("a"), l1("b"))));
+    list.add(Linq4j.enumerator(l3(l2("x", "p"), l2("y", "q"), l2("z", "r"))));
+    final Enumerable<FlatLists.ComparableList<String>> product =
+        SqlFunctions.product(list, 3, false);
+    int n = 0;
+    FlatLists.ComparableList<String> previous = FlatLists.of();
+    for (FlatLists.ComparableList<String> strings : product) {
+      if (n++ == 1) {
+        assertThat(strings.size(), is(3));
+        assertThat(strings.get(0), is("a"));
+        assertThat(strings.get(1), is("y"));
+        assertThat(strings.get(2), is("q"));
+      }
+      if (previous != null) {
+        assertTrue(previous.compareTo(strings) < 0);
+      }
+      previous = strings;
+    }
+    assertThat(n, is(6));
+  }
+
   /**
    * Unit test for {@link AvaticaUtils#toCamelCase(String)}.
    */

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/linq4j/src/main/java/org/apache/calcite/linq4j/CartesianProductEnumerator.java
----------------------------------------------------------------------
diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/CartesianProductEnumerator.java b/linq4j/src/main/java/org/apache/calcite/linq4j/CartesianProductEnumerator.java
index 9bf3da1..1f871e3 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/CartesianProductEnumerator.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/CartesianProductEnumerator.java
@@ -16,29 +16,25 @@
  */
 package org.apache.calcite.linq4j;
 
-import java.util.Arrays;
 import java.util.List;
 
 /**
  * Enumerator over the cartesian product of enumerators.
  *
- * @param <T> Element type
+ * @param <T> Input element type
+ * @param <E> Element type
  */
-class CartesianProductEnumerator<T> implements Enumerator<List<T>> {
+public abstract class CartesianProductEnumerator<T, E> implements Enumerator<E> {
   private final List<Enumerator<T>> enumerators;
-  private final T[] elements;
+  protected final T[] elements;
   private boolean first = true;
 
-  public CartesianProductEnumerator(List<Enumerator<T>> enumerators) {
+  protected CartesianProductEnumerator(List<Enumerator<T>> enumerators) {
     this.enumerators = enumerators;
     //noinspection unchecked
     this.elements = (T[]) new Object[enumerators.size()];
   }
 
-  public List<T> current() {
-    return Arrays.asList(elements.clone());
-  }
-
   public boolean moveNext() {
     if (first) {
       int i = 0;

http://git-wip-us.apache.org/repos/asf/calcite/blob/354e8240/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java
----------------------------------------------------------------------
diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java b/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java
index c0d8d75..3dde65c 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java
@@ -384,7 +384,7 @@ public abstract class Linq4j {
    */
   public static <T> Enumerator<List<T>> product(
       List<Enumerator<T>> enumerators) {
-    return new CartesianProductEnumerator<>(enumerators);
+    return new CartesianProductListEnumerator<>(enumerators);
   }
 
   /** Returns the cartesian product of an iterable of iterables. */
@@ -397,7 +397,7 @@ public abstract class Linq4j {
           enumerators.add(iterableEnumerator(iterable));
         }
         return enumeratorIterator(
-            new CartesianProductEnumerator<>(enumerators));
+            new CartesianProductListEnumerator<>(enumerators));
       }
     };
   }
@@ -447,7 +447,7 @@ public abstract class Linq4j {
     Iterator<? extends T> iterator;
     T current;
 
-    public IterableEnumerator(Iterable<? extends T> iterable) {
+    IterableEnumerator(Iterable<? extends T> iterable) {
       this.iterable = iterable;
       iterator = iterable.iterator();
       current = (T) DUMMY;
@@ -659,7 +659,7 @@ public abstract class Linq4j {
     private final Enumerator<T> enumerator;
     boolean hasNext;
 
-    public EnumeratorIterator(Enumerator<T> enumerator) {
+    EnumeratorIterator(Enumerator<T> enumerator) {
       this.enumerator = enumerator;
       hasNext = enumerator.moveNext();
     }
@@ -688,7 +688,7 @@ public abstract class Linq4j {
     private final List<? extends V> list;
     int i = -1;
 
-    public ListEnumerator(List<? extends V> list) {
+    ListEnumerator(List<? extends V> list) {
       this.list = list;
     }
 
@@ -707,6 +707,19 @@ public abstract class Linq4j {
     public void close() {
     }
   }
+
+  /** Enumerates over the cartesian product of the given lists, returning
+   * a list for each row. */
+  private static class CartesianProductListEnumerator<E>
+      extends CartesianProductEnumerator<E, List<E>> {
+    CartesianProductListEnumerator(List<Enumerator<E>> enumerators) {
+      super(enumerators);
+    }
+
+    public List<E> current() {
+      return Arrays.asList(elements.clone());
+    }
+  }
 }
 
 // End Linq4j.java