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/09/16 04:48:15 UTC

[1/6] calcite git commit: Fix up previous commit

Repository: calcite
Updated Branches:
  refs/heads/master 962eac566 -> 0938c7b6d


Fix up previous commit


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

Branch: refs/heads/master
Commit: baaf2562ea16cc7d11964afb04a4d68d2d1ade04
Parents: 962eac5
Author: Julian Hyde <jh...@apache.org>
Authored: Thu Sep 15 15:38:58 2016 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Sep 15 17:02:19 2016 -0700

----------------------------------------------------------------------
 .../apache/calcite/jdbc/CalciteMetaImpl.java    |  2 +-
 .../adapter/druid/DruidDateTimeUtils.java       | 33 +++++++--------
 .../calcite/adapter/druid/DruidQuery.java       | 42 ++++++++++----------
 .../calcite/adapter/druid/DruidRules.java       |  4 +-
 .../calcite/adapter/druid/DruidTable.java       |  6 +--
 5 files changed, 40 insertions(+), 47 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/baaf2562/core/src/main/java/org/apache/calcite/jdbc/CalciteMetaImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/jdbc/CalciteMetaImpl.java b/core/src/main/java/org/apache/calcite/jdbc/CalciteMetaImpl.java
index 434b1d4..168f356 100644
--- a/core/src/main/java/org/apache/calcite/jdbc/CalciteMetaImpl.java
+++ b/core/src/main/java/org/apache/calcite/jdbc/CalciteMetaImpl.java
@@ -452,7 +452,7 @@ public class CalciteMetaImpl extends MetaImpl {
 
   private ImmutableList<MetaTypeInfo> getAllDefaultType() {
     final ImmutableList.Builder<MetaTypeInfo> allTypeList =
-        new ImmutableList.Builder<>();
+        ImmutableList.builder();
     final CalciteConnectionImpl conn = (CalciteConnectionImpl) connection;
     final RelDataTypeSystem typeSystem = conn.typeFactory.getTypeSystem();
     for (SqlTypeName sqlTypeName : SqlTypeName.values()) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/baaf2562/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java
----------------------------------------------------------------------
diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java
index edfaad8..2921b9f 100644
--- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java
+++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java
@@ -60,12 +60,9 @@ public class DruidDateTimeUtils {
   }
 
   /**
-   * Given a list of predicates, it generates the equivalent Interval
-   * (if possible). It assumes that all the predicates in the input
-   * reference a single column : the timestamp column.
-   *
-   * @param conjs list of conditions to use for the transformation
-   * @return interval representing the conditions in the input list
+   * Generates a list of {@link Interval}s equivalent to a given
+   * expression. Assumes that all the predicates in the input
+   * reference a single column: the timestamp column.
    */
   public static List<Interval> createInterval(RelDataType type, RexNode e) {
     final List<Range> ranges = extractRanges(type, e, false);
@@ -90,9 +87,9 @@ public class DruidDateTimeUtils {
           return DruidTable.DEFAULT_INTERVAL;
         }
         long start = range.hasLowerBound() ? toLong(range.lowerEndpoint())
-          : DruidTable.DEFAULT_INTERVAL.getStartMillis();
+            : DruidTable.DEFAULT_INTERVAL.getStartMillis();
         long end = range.hasUpperBound() ? toLong(range.upperEndpoint())
-          : DruidTable.DEFAULT_INTERVAL.getEndMillis();
+            : DruidTable.DEFAULT_INTERVAL.getEndMillis();
         if (range.hasLowerBound() && range.lowerBoundType() == BoundType.OPEN) {
           start++;
         }
@@ -109,7 +106,7 @@ public class DruidDateTimeUtils {
   }
 
   protected static List<Range> extractRanges(RelDataType type, RexNode node,
-          boolean withNot) {
+      boolean withNot) {
     switch (node.getKind()) {
     case EQUALS:
     case LESS_THAN:
@@ -167,7 +164,7 @@ public class DruidDateTimeUtils {
   }
 
   protected static List<Range> leafToRanges(RelDataType type, RexCall call,
-          boolean withNot) {
+      boolean withNot) {
     switch (call.getKind()) {
     case EQUALS:
     case LESS_THAN:
@@ -177,16 +174,16 @@ public class DruidDateTimeUtils {
     {
       RexLiteral literal = null;
       if (call.getOperands().get(0) instanceof RexInputRef
-              && call.getOperands().get(1) instanceof RexLiteral) {
+          && call.getOperands().get(1) instanceof RexLiteral) {
         literal = extractLiteral(call.getOperands().get(1));
       } else if (call.getOperands().get(0) instanceof RexInputRef
-              && call.getOperands().get(1).getKind() == SqlKind.CAST) {
+          && call.getOperands().get(1).getKind() == SqlKind.CAST) {
         literal = extractLiteral(call.getOperands().get(1));
       } else if (call.getOperands().get(1) instanceof RexInputRef
-              && call.getOperands().get(0) instanceof RexLiteral) {
+          && call.getOperands().get(0) instanceof RexLiteral) {
         literal = extractLiteral(call.getOperands().get(0));
       } else if (call.getOperands().get(1) instanceof RexInputRef
-              && call.getOperands().get(0).getKind() == SqlKind.CAST) {
+          && call.getOperands().get(0).getKind() == SqlKind.CAST) {
         literal = extractLiteral(call.getOperands().get(0));
       }
       if (literal == null) {
@@ -230,10 +227,10 @@ public class DruidDateTimeUtils {
       boolean inverted = value1.compareTo(value2) > 0;
       if (!withNot) {
         return Arrays.<Range>asList(
-                inverted ? Range.closed(value2, value1) : Range.closed(value1, value2));
+            inverted ? Range.closed(value2, value1) : Range.closed(value1, value2));
       }
       return Arrays.<Range>asList(Range.lessThan(inverted ? value2 : value1),
-              Range.greaterThan(inverted ? value1 : value2));
+          Range.greaterThan(inverted ? value1 : value2));
     }
     case IN:
     {
@@ -249,7 +246,7 @@ public class DruidDateTimeUtils {
         }
         if (withNot) {
           ranges.addAll(
-                  Arrays.<Range>asList(Range.lessThan(element), Range.greaterThan(element)));
+              Arrays.<Range>asList(Range.lessThan(element), Range.greaterThan(element)));
         } else {
           ranges.add(Range.closed(element, element));
         }
@@ -433,7 +430,7 @@ public class DruidDateTimeUtils {
   }
 
   /**
-   * Extract granularity from a call FLOOR(<time> TO <timeunit>).
+   * Extracts granularity from a call {@code FLOOR(<time> TO <timeunit>)}.
    * Timeunit specifies the granularity. Returns null if it cannot
    * be inferred.
    *

http://git-wip-us.apache.org/repos/asf/calcite/blob/baaf2562/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java
----------------------------------------------------------------------
diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java
index cec8261..bb2a534 100644
--- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java
+++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java
@@ -97,7 +97,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
    * @param traitSet       Traits
    * @param table          Table
    * @param druidTable     Druid table
-   * @param interval       Interval for the query
+   * @param intervals      Intervals for the query
    * @param rels           Internal relational expressions
    */
   protected DruidQuery(RelOptCluster cluster, RelTraitSet traitSet,
@@ -127,10 +127,10 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
     for (RelNode rel : rels) {
       b.append(rel instanceof TableScan ? 's'
           : rel instanceof Project ? 'p'
-          : rel instanceof Filter ? 'f'
-          : rel instanceof Aggregate ? 'a'
-          : rel instanceof Sort ? 'l'
-          : '!');
+              : rel instanceof Filter ? 'f'
+                  : rel instanceof Aggregate ? 'a'
+                      : rel instanceof Sort ? 'l'
+                          : '!');
     }
     return b.toString();
   }
@@ -243,13 +243,13 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
   public static DruidQuery extendQuery(DruidQuery query, RelNode r) {
     final ImmutableList.Builder<RelNode> builder = ImmutableList.builder();
     return DruidQuery.create(query.getCluster(), r.getTraitSet(), query.getTable(),
-            query.druidTable, query.intervals, builder.addAll(query.rels).add(r).build());
+        query.druidTable, query.intervals, builder.addAll(query.rels).add(r).build());
   }
 
   /** Extends a DruidQuery. */
   public static DruidQuery extendQuery(DruidQuery query, List<Interval> intervals) {
     return DruidQuery.create(query.getCluster(), query.getTraitSet(), query.getTable(),
-            query.druidTable, intervals, query.rels);
+        query.druidTable, intervals, query.rels);
   }
 
   @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
@@ -259,8 +259,8 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
 
   @Override public RelDataType deriveRowType() {
     return getCluster().getTypeFactory().createStructType(
-            Pair.right(Util.last(rels).getRowType().getFieldList()),
-            getQuerySpec().fieldNames);
+        Pair.right(Util.last(rels).getRowType().getFieldList()),
+        getQuerySpec().fieldNames);
   }
 
   public TableScan getTableScan() {
@@ -296,7 +296,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
       } else if (rel instanceof Sort) {
         final Sort sort = (Sort) rel;
         for (Ord<RelFieldCollation> ord
-                : Ord.zip(sort.collation.getFieldCollations())) {
+            : Ord.zip(sort.collation.getFieldCollations())) {
           pw.item("sort" + ord.i, ord.e.getFieldIndex());
         }
         for (Ord<RelFieldCollation> ord
@@ -392,7 +392,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
     }
 
     return getQuery(rowType, filter, projects, groupSet, aggCalls, aggNames,
-            collationIndexes, collationDirections, fetch);
+        collationIndexes, collationDirections, fetch);
   }
 
   public QueryType getQueryType() {
@@ -451,7 +451,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
             // Reference, it could be to the timestamp column or any other dimension
             final RexInputRef ref = (RexInputRef) project;
             final String origin = druidTable.getRowType(getCluster().getTypeFactory())
-                    .getFieldList().get(ref.getIndex()).getName();
+                .getFieldList().get(ref.getIndex()).getName();
             if (origin.equals(druidTable.timestampFieldName)) {
               granularity = "none";
               builder.add(s);
@@ -465,7 +465,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
             // Call, check if we should infer granularity
             final RexCall call = (RexCall) project;
             final String funcGranularity =
-                    DruidDateTimeUtils.extractGranularity(call);
+                DruidDateTimeUtils.extractGranularity(call);
             if (funcGranularity != null) {
               granularity = funcGranularity;
               builder.add(s);
@@ -507,12 +507,12 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
       boolean sortsMetric = false;
       if (collationIndexes != null) {
         assert collationDirections != null;
-        ImmutableList.Builder<JsonCollation> colBuilder
-          = new ImmutableList.Builder<JsonCollation>();
+        ImmutableList.Builder<JsonCollation> colBuilder =
+            ImmutableList.builder();
         for (Pair<Integer, Direction> p : Pair.zip(collationIndexes, collationDirections)) {
           colBuilder.add(
-            new JsonCollation(fieldNames.get(p.left),
-                p.right == Direction.DESCENDING ? "descending" : "ascending"));
+              new JsonCollation(fieldNames.get(p.left),
+                  p.right == Direction.DESCENDING ? "descending" : "ascending"));
           if (p.left >= groupSet.cardinality() && p.right == Direction.DESCENDING) {
             // Currently only support for DESC in TopN
             sortsMetric = true;
@@ -619,7 +619,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
         generator.writeFieldName("pagingSpec");
         generator.writeStartObject();
         generator.writeNumberField("threshold", fetch != null ? fetch
-          : CalciteConnectionProperty.DRUID_FETCH.wrap(new Properties()).getInt());
+            : CalciteConnectionProperty.DRUID_FETCH.wrap(new Properties()).getInt());
         generator.writeEndObject();
 
         generator.writeFieldName("context");
@@ -789,7 +789,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
         if (druidTable.metricFieldNames.contains(fieldName)) {
           metrics.add(fieldName);
         } else if (!druidTable.timestampFieldName.equals(fieldName)
-                && !DruidTable.DEFAULT_TIMESTAMP_COLUMN.equals(fieldName)) {
+            && !DruidTable.DEFAULT_TIMESTAMP_COLUMN.equals(fieldName)) {
           dimensions.add(fieldName);
         }
       }
@@ -805,7 +805,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
           if (druidTable.metricFieldNames.contains(fieldName)) {
             metrics.add(fieldName);
           } else if (!druidTable.timestampFieldName.equals(fieldName)
-                  && !DruidTable.DEFAULT_TIMESTAMP_COLUMN.equals(fieldName)) {
+              && !DruidTable.DEFAULT_TIMESTAMP_COLUMN.equals(fieldName)) {
             dimensions.add(fieldName);
           }
         }
@@ -916,7 +916,7 @@ public class DruidQuery extends AbstractRelNode implements BindableRel {
 
     private static boolean containsLimit(QuerySpec querySpec) {
       return querySpec.queryString.contains("\"context\":{\""
-            + DRUID_QUERY_FETCH + "\":true");
+          + DRUID_QUERY_FETCH + "\":true");
     }
 
     private ColumnMetaData.Rep getPrimitive(RelDataTypeField field) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/baaf2562/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
----------------------------------------------------------------------
diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
index 3d1c23a..70917e8 100644
--- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
+++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
@@ -264,9 +264,7 @@ public class DruidRules {
                 final int index = positions.indexOf(ref.getIndex());
                 return rexBuilder.makeInputRef(belowTypes.get(index), index);
               }
-            }
-          )
-        );
+            }));
       }
       return Pair.of(aboveNodes, belowNodes);
     }

http://git-wip-us.apache.org/repos/asf/calcite/blob/baaf2562/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTable.java
----------------------------------------------------------------------
diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTable.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTable.java
index fce83a8..836941b 100644
--- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTable.java
+++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTable.java
@@ -50,10 +50,8 @@ import java.util.Set;
 public class DruidTable extends AbstractTable implements TranslatableTable {
 
   public static final String DEFAULT_TIMESTAMP_COLUMN = "__time";
-  public static final Interval DEFAULT_INTERVAL = new Interval(
-          new DateTime("1900-01-01"),
-          new DateTime("3000-01-01")
-  );
+  public static final Interval DEFAULT_INTERVAL =
+      new Interval(new DateTime("1900-01-01"), new DateTime("3000-01-01"));
 
   final DruidSchema schema;
   final String dataSource;


[6/6] calcite git commit: [CALCITE-1372] JDBC adapter generates SQL with wrong field names

Posted by jh...@apache.org.
[CALCITE-1372] JDBC adapter generates SQL with wrong field names

Clean up RelToSqlConverterTest, using a fluent API to invoke tests.


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

Branch: refs/heads/master
Commit: f9724c2739fe482d36cfdd5aa6cad207c7a8fcd4
Parents: baaf256
Author: Julian Hyde <jh...@apache.org>
Authored: Wed Sep 14 18:42:22 2016 +0200
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Sep 15 17:02:42 2016 -0700

----------------------------------------------------------------------
 .../calcite/rel/rel2sql/SqlImplementor.java     |  42 +-
 .../rel/rel2sql/RelToSqlConverterTest.java      | 504 ++++++++++---------
 .../apache/calcite/test/JdbcAdapterTest.java    |  14 +
 3 files changed, 303 insertions(+), 257 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/f9724c27/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 3fc030f..826ca6b 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -65,6 +65,7 @@ import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -74,6 +75,7 @@ import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -239,11 +241,11 @@ public abstract class SqlImplementor {
   /**
    * Converts a {@link RexNode} condition into a {@link SqlNode}.
    *
-   * @param node            condition Node
-   * @param leftContext     LeftContext
-   * @param rightContext    RightContext
-   * @param leftFieldCount  Number of field on left result
-   * @return SqlJoin which represent the condition
+   * @param node            Join condition
+   * @param leftContext     Left context
+   * @param rightContext    Right context
+   * @param leftFieldCount  Number of fields on left result
+   * @return SqlNode that represents the condition
    */
   public static SqlNode convertConditionToSqlNode(RexNode node,
       Context leftContext,
@@ -396,10 +398,10 @@ public abstract class SqlImplementor {
             alias3, aliasSet, SqlValidatorUtil.EXPR_SUGGESTER);
     if (aliases != null
         && !aliases.isEmpty()
-        && !dialect.hasImplicitTableAlias()) {
+        && (!dialect.hasImplicitTableAlias()
+          || aliases.size() > 1)) {
       return new Result(node, clauses, alias4, aliases);
     }
-
     final String alias5;
     if (alias2 == null
         || !alias2.equals(alias4)
@@ -415,12 +417,26 @@ public abstract class SqlImplementor {
   /** Creates a result based on a join. (Each join could contain one or more
    * relational expressions.) */
   public Result result(SqlNode join, Result leftResult, Result rightResult) {
-    final Map<String, RelDataType> aliases =
-        ImmutableMap.<String, RelDataType>builder()
-            .putAll(leftResult.aliases)
-            .putAll(rightResult.aliases)
-            .build();
-    return new Result(join, Expressions.list(Clause.FROM), null, aliases);
+    final ImmutableMap.Builder<String, RelDataType> builder =
+        ImmutableMap.builder();
+    collectAliases(builder, join,
+        Iterables.concat(leftResult.aliases.values(),
+            rightResult.aliases.values()).iterator());
+    return new Result(join, Expressions.list(Clause.FROM), null,
+        builder.build());
+  }
+
+  private void collectAliases(ImmutableMap.Builder<String, RelDataType> builder,
+      SqlNode node, Iterator<RelDataType> aliases) {
+    if (node instanceof SqlJoin) {
+      final SqlJoin join = (SqlJoin) node;
+      collectAliases(builder, join.getLeft(),  aliases);
+      collectAliases(builder, join.getRight(), aliases);
+    } else {
+      final String alias = SqlValidatorUtil.getAlias(node, -1);
+      assert alias != null;
+      builder.put(alias, aliases.next());
+    }
   }
 
   /** Wraps a node in a SELECT statement that has no clauses:

http://git-wip-us.apache.org/repos/asf/calcite/blob/f9724c27/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 85f4720..1fb9695 100644
--- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -42,292 +42,250 @@ import static org.junit.Assert.assertTrue;
  * Tests for utility {@link RelToSqlConverter}
  */
 public class RelToSqlConverterTest {
-  private Planner logicalPlanner = getPlanner(null);
-
-  private void checkRel2Sql(Planner planner, String query, String expectedQuery,
-      SqlDialect dialect) {
-    try {
-      SqlNode parse = planner.parse(query);
-      SqlNode validate = planner.validate(parse);
-      RelNode rel = planner.rel(validate).rel;
-      final RelToSqlConverter converter =
-          new RelToSqlConverter(dialect);
-      final SqlNode sqlNode = converter.visitChild(0, rel).asQuery();
-      assertThat(Util.toLinux(sqlNode.toSqlString(dialect).getSql()),
-          is(expectedQuery));
-    } catch (Exception e) {
-      assertTrue("Parsing failed throwing error: " + e.getMessage(), false);
-    }
-  }
-
-  private void checkRel2Sql(Planner planner, String query, String expectedQuery) {
-    checkRel2Sql(planner, query, expectedQuery, SqlDialect.CALCITE);
+  /** Initiates a test case with a given SQL query. */
+  private Sql sql(String sql) {
+    return new Sql(CalciteAssert.SchemaSpec.JDBC_FOODMART, sql,
+        SqlDialect.CALCITE);
   }
 
-  private Planner getPlanner(List<RelTraitDef> traitDefs, Program... programs) {
-    return getPlanner(traitDefs, SqlParser.Config.DEFAULT, programs);
-  }
-
-  private Planner getPlanner(List<RelTraitDef> traitDefs,
-      SqlParser.Config parserConfig, Program... programs) {
+  private static Planner getPlanner(List<RelTraitDef> traitDefs,
+      SqlParser.Config parserConfig, CalciteAssert.SchemaSpec schemaSpec,
+      Program... programs) {
     final SchemaPlus rootSchema = Frameworks.createRootSchema(true);
     final FrameworkConfig config = Frameworks.newConfigBuilder()
         .parserConfig(parserConfig)
-        .defaultSchema(
-            CalciteAssert.addSchema(rootSchema, CalciteAssert.SchemaSpec.JDBC_FOODMART))
+        .defaultSchema(CalciteAssert.addSchema(rootSchema, schemaSpec))
         .traitDefs(traitDefs)
         .programs(programs)
         .build();
     return Frameworks.getPlanner(config);
   }
 
-  @Test
-  public void testSimpleSelectStarFromProductTable() {
+  @Test public void testSimpleSelectStarFromProductTable() {
     String query = "select * from \"product\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT *\nFROM \"foodmart\".\"product\"");
+    sql(query).ok("SELECT *\nFROM \"foodmart\".\"product\"");
   }
 
-  @Test
-  public void testSimpleSelectQueryFromProductTable() {
+  @Test public void testSimpleSelectQueryFromProductTable() {
     String query = "select \"product_id\", \"product_class_id\" from \"product\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\", \"product_class_id\"\n"
-            + "FROM \"foodmart\".\"product\"");
+    final String expected = "SELECT \"product_id\", \"product_class_id\"\n"
+        + "FROM \"foodmart\".\"product\"";
+    sql(query).ok(expected);
   }
 
   //TODO: add test for query -> select * from product
 
-  @Test
-  public void testSelectQueryWithWhereClauseOfLessThan() {
+  @Test public void testSelectQueryWithWhereClauseOfLessThan() {
     String query =
         "select \"product_id\", \"shelf_width\"  from \"product\" where \"product_id\" < 10";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\", \"shelf_width\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "WHERE \"product_id\" < 10");
+    final String expected = "SELECT \"product_id\", \"shelf_width\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "WHERE \"product_id\" < 10";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithWhereClauseOfBasicOperators() {
+  @Test public void testSelectQueryWithWhereClauseOfBasicOperators() {
     String query = "select * from \"product\" "
         + "where (\"product_id\" = 10 OR \"product_id\" <= 5) "
         + "AND (80 >= \"shelf_width\" OR \"shelf_width\" > 30)";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT *\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "WHERE (\"product_id\" = 10 OR \"product_id\" <= 5) "
-            + "AND (80 >= \"shelf_width\" OR \"shelf_width\" > 30)");
+    final String expected = "SELECT *\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "WHERE (\"product_id\" = 10 OR \"product_id\" <= 5) "
+        + "AND (80 >= \"shelf_width\" OR \"shelf_width\" > 30)";
+    sql(query).ok(expected);
   }
 
 
-  @Test
-  public void testSelectQueryWithGroupBy() {
+  @Test public void testSelectQueryWithGroupBy() {
     String query = "select count(*) from \"product\" group by \"product_class_id\", \"product_id\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT COUNT(*)\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\", \"product_id\"");
+    final String expected = "SELECT COUNT(*)\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\", \"product_id\"";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithMinAggregateFunction() {
+  @Test public void testSelectQueryWithMinAggregateFunction() {
     String query = "select min(\"net_weight\") from \"product\" group by \"product_class_id\" ";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT MIN(\"net_weight\")\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\"");
+    final String expected = "SELECT MIN(\"net_weight\")\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\"";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithMinAggregateFunction1() {
+  @Test public void testSelectQueryWithMinAggregateFunction1() {
     String query = "select \"product_class_id\", min(\"net_weight\") from"
         + " \"product\" group by \"product_class_id\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_class_id\", MIN(\"net_weight\")\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\"");
+    final String expected = "SELECT \"product_class_id\", MIN(\"net_weight\")\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\"";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithSumAggregateFunction() {
+  @Test public void testSelectQueryWithSumAggregateFunction() {
     String query =
         "select sum(\"net_weight\") from \"product\" group by \"product_class_id\" ";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT SUM(\"net_weight\")\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\"");
-  }
-
-  @Test
-  public void testSelectQueryWithMultipleAggregateFunction() {
-    String query =
-        "select sum(\"net_weight\"), min(\"low_fat\"), count(*)"
-            + " from \"product\" group by \"product_class_id\" ";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT SUM(\"net_weight\"), MIN(\"low_fat\"), COUNT(*)\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\"");
-  }
-
-  @Test
-  public void testSelectQueryWithMultipleAggregateFunction1() {
-    String query =
-        "select \"product_class_id\", sum(\"net_weight\"), min(\"low_fat\"), count(*)"
-            + " from \"product\" group by \"product_class_id\" ";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_class_id\", SUM(\"net_weight\"), MIN(\"low_fat\"), COUNT(*)\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\"");
-  }
-
-  @Test
-  public void testSelectQueryWithGroupByAndProjectList() {
-    String query =
-        "select \"product_class_id\", \"product_id\", count(*) from \"product\" group "
-            + "by \"product_class_id\", \"product_id\"  ";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_class_id\", \"product_id\", COUNT(*)\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\", \"product_id\"");
-  }
-
-  @Test
-  public void testSelectQueryWithGroupByAndProjectList1() {
+    final String expected = "SELECT SUM(\"net_weight\")\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\"";
+    sql(query).ok(expected);
+  }
+
+  @Test public void testSelectQueryWithMultipleAggregateFunction() {
+    String query = "select sum(\"net_weight\"), min(\"low_fat\"), count(*)"
+        + " from \"product\" group by \"product_class_id\" ";
+    final String expected = "SELECT SUM(\"net_weight\"), MIN(\"low_fat\"),"
+        + " COUNT(*)\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\"";
+    sql(query).ok(expected);
+  }
+
+  @Test public void testSelectQueryWithMultipleAggregateFunction1() {
+    String query = "select \"product_class_id\","
+        + " sum(\"net_weight\"), min(\"low_fat\"), count(*)"
+        + " from \"product\" group by \"product_class_id\" ";
+    final String expected = "SELECT \"product_class_id\","
+        + " SUM(\"net_weight\"), MIN(\"low_fat\"), COUNT(*)\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\"";
+    sql(query).ok(expected);
+  }
+
+  @Test public void testSelectQueryWithGroupByAndProjectList() {
+    String query = "select \"product_class_id\", \"product_id\", count(*) "
+        + "from \"product\" group by \"product_class_id\", \"product_id\"  ";
+    final String expected = "SELECT \"product_class_id\", \"product_id\","
+        + " COUNT(*)\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\", \"product_id\"";
+    sql(query).ok(expected);
+  }
+
+  @Test public void testSelectQueryWithGroupByAndProjectList1() {
     String query =
         "select count(*)  from \"product\" group by \"product_class_id\", \"product_id\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT COUNT(*)\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\", \"product_id\"");
+
+    final String expected = "SELECT COUNT(*)\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\", \"product_id\"";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithGroupByHaving() {
+  @Test public void testSelectQueryWithGroupByHaving() {
     String query = "select count(*) from \"product\" group by \"product_class_id\","
         + " \"product_id\"  having \"product_id\"  > 10";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT COUNT(*)\n"
-            + "FROM (SELECT \"product_class_id\", \"product_id\", COUNT(*)\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "GROUP BY \"product_class_id\", \"product_id\") AS \"t0\"\n"
-            + "WHERE \"product_id\" > 10");
+    final String expected = "SELECT COUNT(*)\n"
+        + "FROM (SELECT \"product_class_id\", \"product_id\", COUNT(*)\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "GROUP BY \"product_class_id\", \"product_id\") AS \"t0\"\n"
+        + "WHERE \"product_id\" > 10";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithOrderByClause() {
+  @Test public void testSelectQueryWithOrderByClause() {
     String query = "select \"product_id\"  from \"product\" order by \"net_weight\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\", \"net_weight\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "ORDER BY \"net_weight\"");
+    final String expected = "SELECT \"product_id\", \"net_weight\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "ORDER BY \"net_weight\"";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithOrderByClause1() {
+  @Test public void testSelectQueryWithOrderByClause1() {
     String query =
         "select \"product_id\", \"net_weight\" from \"product\" order by \"net_weight\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\", \"net_weight\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "ORDER BY \"net_weight\"");
+    final String expected = "SELECT \"product_id\", \"net_weight\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "ORDER BY \"net_weight\"";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithTwoOrderByClause() {
+  @Test public void testSelectQueryWithTwoOrderByClause() {
     String query =
         "select \"product_id\"  from \"product\" order by \"net_weight\", \"gross_weight\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\", \"net_weight\", \"gross_weight\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "ORDER BY \"net_weight\", \"gross_weight\"");
+    final String expected = "SELECT \"product_id\", \"net_weight\","
+        + " \"gross_weight\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "ORDER BY \"net_weight\", \"gross_weight\"";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithAscDescOrderByClause() {
-    String query =
-        "select \"product_id\" from \"product\" order by \"net_weight\" asc, "
-            + "\"gross_weight\" desc, \"low_fat\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\", \"net_weight\", \"gross_weight\", \"low_fat\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "ORDER BY \"net_weight\", \"gross_weight\" DESC, \"low_fat\"");
+  @Test public void testSelectQueryWithAscDescOrderByClause() {
+    String query = "select \"product_id\" from \"product\" "
+        + "order by \"net_weight\" asc, \"gross_weight\" desc, \"low_fat\"";
+    final String expected = "SELECT"
+        + " \"product_id\", \"net_weight\", \"gross_weight\", \"low_fat\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "ORDER BY \"net_weight\", \"gross_weight\" DESC, \"low_fat\"";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithLimitClause() {
+  @Test public void testSelectQueryWithLimitClause() {
     String query = "select \"product_id\"  from \"product\" limit 100 offset 10";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT product_id\n"
-            + "FROM foodmart.product\n"
-            + "LIMIT 100\nOFFSET 10",
-        SqlDialect.DatabaseProduct.HIVE.getDialect());
+    final String expected = "SELECT product_id\n"
+        + "FROM foodmart.product\n"
+        + "LIMIT 100\nOFFSET 10";
+    sql(query).dialect(SqlDialect.DatabaseProduct.HIVE.getDialect())
+        .ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithLimitClauseWithoutOrder() {
+  @Test public void testSelectQueryWithLimitClauseWithoutOrder() {
     String query = "select \"product_id\"  from \"product\" limit 100 offset 10";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "OFFSET 10 ROWS\n"
-            + "FETCH NEXT 100 ROWS ONLY");
+    final String expected = "SELECT \"product_id\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "OFFSET 10 ROWS\n"
+        + "FETCH NEXT 100 ROWS ONLY";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithLimitOffsetClause() {
+  @Test public void testSelectQueryWithLimitOffsetClause() {
     String query = "select \"product_id\"  from \"product\" order by \"net_weight\" asc"
         + " limit 100 offset 10";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\", \"net_weight\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "ORDER BY \"net_weight\"\n"
-            + "OFFSET 10 ROWS\n"
-            + "FETCH NEXT 100 ROWS ONLY");
+    final String expected = "SELECT \"product_id\", \"net_weight\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "ORDER BY \"net_weight\"\n"
+        + "OFFSET 10 ROWS\n"
+        + "FETCH NEXT 100 ROWS ONLY";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithFetchOffsetClause() {
+  @Test public void testSelectQueryWithFetchOffsetClause() {
     String query = "select \"product_id\"  from \"product\" order by \"product_id\""
         + " offset 10 rows fetch next 100 rows only";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"product_id\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "ORDER BY \"product_id\"\n"
-            + "OFFSET 10 ROWS\n"
-            + "FETCH NEXT 100 ROWS ONLY");
+    final String expected = "SELECT \"product_id\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "ORDER BY \"product_id\"\n"
+        + "OFFSET 10 ROWS\n"
+        + "FETCH NEXT 100 ROWS ONLY";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryComplex() {
+  @Test public void testSelectQueryComplex() {
     String query =
         "select count(*), \"units_per_case\" from \"product\" where \"cases_per_pallet\" > 100 "
             + "group by \"product_id\", \"units_per_case\" order by \"units_per_case\" desc";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT COUNT(*), \"units_per_case\"\n"
-            + "FROM \"foodmart\".\"product\"\n"
-            + "WHERE \"cases_per_pallet\" > 100\n"
-            + "GROUP BY \"product_id\", \"units_per_case\"\n"
-            + "ORDER BY \"units_per_case\" DESC");
+    final String expected = "SELECT COUNT(*), \"units_per_case\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "WHERE \"cases_per_pallet\" > 100\n"
+        + "GROUP BY \"product_id\", \"units_per_case\"\n"
+        + "ORDER BY \"units_per_case\" DESC";
+    sql(query).ok(expected);
   }
 
-  @Test
-  public void testSelectQueryWithGroup() {
-    String query =
-        "select count(*), sum(\"employee_id\") from \"reserve_employee\" "
-            + "where \"hire_date\" > '2015-01-01' "
-            + "and (\"position_title\" = 'SDE' or \"position_title\" = 'SDM') "
-            + "group by \"store_id\", \"position_title\"";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT COUNT(*), SUM(\"employee_id\")\n"
-            + "FROM \"foodmart\".\"reserve_employee\"\n"
-            + "WHERE \"hire_date\" > '2015-01-01' "
-            + "AND (\"position_title\" = 'SDE' OR \"position_title\" = 'SDM')\n"
-            + "GROUP BY \"store_id\", \"position_title\"");
-  }
-
-  @Test
-  public void testSimpleJoin() {
+  @Test public void testSelectQueryWithGroup() {
+    String query = "select"
+        + " count(*), sum(\"employee_id\") from \"reserve_employee\" "
+        + "where \"hire_date\" > '2015-01-01' "
+        + "and (\"position_title\" = 'SDE' or \"position_title\" = 'SDM') "
+        + "group by \"store_id\", \"position_title\"";
+    final String expected = "SELECT COUNT(*), SUM(\"employee_id\")\n"
+        + "FROM \"foodmart\".\"reserve_employee\"\n"
+        + "WHERE \"hire_date\" > '2015-01-01' "
+        + "AND (\"position_title\" = 'SDE' OR \"position_title\" = 'SDM')\n"
+        + "GROUP BY \"store_id\", \"position_title\"";
+    sql(query).ok(expected);
+  }
+
+  @Test public void testSimpleJoin() {
     String query = "select *\n"
         + "from \"sales_fact_1997\" as s\n"
         + "  join \"customer\" as c using (\"customer_id\")\n"
@@ -335,28 +293,32 @@ public class RelToSqlConverterTest {
         + "  join \"product_class\" as pc using (\"product_class_id\")\n"
         + "where c.\"city\" = 'San Francisco'\n"
         + "and pc.\"product_department\" = 'Snacks'\n";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT *\nFROM \"foodmart\".\"sales_fact_1997\"\n"
-            + "INNER JOIN \"foodmart\".\"customer\" "
-            + "ON \"sales_fact_1997\".\"customer_id\" = \"customer\".\"customer_id\"\n"
-            + "INNER JOIN \"foodmart\".\"product\" "
-            + "ON \"sales_fact_1997\".\"product_id\" = \"product\".\"product_id\"\n"
-            + "INNER JOIN \"foodmart\".\"product_class\" "
-            + "ON \"product\".\"product_class_id\" = \"product_class\".\"product_class_id\"\n"
-            + "WHERE \"customer\".\"city\" = 'San Francisco' AND "
-            + "\"product_class\".\"product_department\" = 'Snacks'");
+    final String expected = "SELECT *\nFROM \"foodmart\".\"sales_fact_1997\"\n"
+        + "INNER JOIN \"foodmart\".\"customer\" "
+        + "ON \"sales_fact_1997\".\"customer_id\" = \"customer\""
+        + ".\"customer_id\"\n"
+        + "INNER JOIN \"foodmart\".\"product\" "
+        + "ON \"sales_fact_1997\".\"product_id\" = \"product\".\"product_id\"\n"
+        + "INNER JOIN \"foodmart\".\"product_class\" "
+        + "ON \"product\".\"product_class_id\" = \"product_class\""
+        + ".\"product_class_id\"\n"
+        + "WHERE \"customer\".\"city\" = 'San Francisco' AND "
+        + "\"product_class\".\"product_department\" = 'Snacks'";
+    sql(query).ok(expected);
   }
 
   @Test public void testSimpleIn() {
     String query = "select * from \"department\" where \"department_id\" in (\n"
         + "  select \"department_id\" from \"employee\"\n"
         + "  where \"store_id\" < 150)";
-    checkRel2Sql(logicalPlanner, query,
-        "SELECT \"department\".\"department_id\", \"department\".\"department_description\"\n"
-            + "FROM \"foodmart\".\"department\"\nINNER JOIN "
-            + "(SELECT \"department_id\"\nFROM \"foodmart\".\"employee\"\n"
-            + "WHERE \"store_id\" < 150\nGROUP BY \"department_id\") AS \"t1\" "
-            + "ON \"department\".\"department_id\" = \"t1\".\"department_id\"");
+    final String expected = "SELECT "
+        + "\"department\".\"department_id\", \"department\""
+        + ".\"department_description\"\n"
+        + "FROM \"foodmart\".\"department\"\nINNER JOIN "
+        + "(SELECT \"department_id\"\nFROM \"foodmart\".\"employee\"\n"
+        + "WHERE \"store_id\" < 150\nGROUP BY \"department_id\") AS \"t1\" "
+        + "ON \"department\".\"department_id\" = \"t1\".\"department_id\"";
+    sql(query).ok(expected);
   }
 
   /** Test case for
@@ -371,8 +333,7 @@ public class RelToSqlConverterTest {
         + "FROM foodmart.employee AS employee\n"
         + "INNER JOIN foodmart.department AS department "
         + "ON employee.department_id = department.department_id";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectSelfJoinStar() {
@@ -383,9 +344,7 @@ public class RelToSqlConverterTest {
         + "FROM foodmart.employee AS employee\n"
         + "INNER JOIN foodmart.employee AS employee0 "
         + "ON employee.department_id = employee0.department_id";
-    checkRel2Sql(logicalPlanner, query,
-        expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectJoin() {
@@ -397,8 +356,7 @@ public class RelToSqlConverterTest {
         + "FROM foodmart.employee AS employee\n"
         + "INNER JOIN foodmart.department AS department "
         + "ON employee.department_id = department.department_id";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectSelfJoin() {
@@ -410,8 +368,7 @@ public class RelToSqlConverterTest {
         + "FROM foodmart.employee AS employee\n"
         + "INNER JOIN foodmart.employee AS employee0 "
         + "ON employee.department_id = employee0.department_id";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectWhere() {
@@ -420,8 +377,7 @@ public class RelToSqlConverterTest {
     final String expected = "SELECT employee.employee_id\n"
         + "FROM foodmart.employee AS employee\n"
         + "WHERE employee.department_id < 1000";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectJoinWhere() {
@@ -435,8 +391,7 @@ public class RelToSqlConverterTest {
         + "INNER JOIN foodmart.department AS department "
         + "ON employee.department_id = department.department_id\n"
         + "WHERE employee.employee_id < 1000";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectSelfJoinWhere() {
@@ -450,8 +405,7 @@ public class RelToSqlConverterTest {
         + "INNER JOIN foodmart.employee AS employee0 "
         + "ON employee.department_id = employee0.department_id\n"
         + "WHERE employee0.employee_id < 2000";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectCast() {
@@ -460,8 +414,7 @@ public class RelToSqlConverterTest {
     final String expected = "SELECT reserve_employee.hire_date, "
         + "CAST(reserve_employee.hire_date AS VARCHAR(10))\n"
         + "FROM foodmart.reserve_employee AS reserve_employee";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectSelectQueryWithGroupByHaving() {
@@ -474,8 +427,7 @@ public class RelToSqlConverterTest {
         + "FROM foodmart.product AS product\n"
         + "GROUP BY product.product_class_id, product.product_id) AS t0\n"
         + "WHERE t0.product_id > 10";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
 
@@ -489,8 +441,7 @@ public class RelToSqlConverterTest {
         + "WHERE product.cases_per_pallet > 100\n"
         + "GROUP BY product.product_id, product.units_per_case\n"
         + "ORDER BY product.units_per_case DESC";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
   @Test public void testDb2DialectSelectQueryWithGroup() {
@@ -506,10 +457,75 @@ public class RelToSqlConverterTest {
         + "AND (reserve_employee.position_title = 'SDE' OR "
         + "reserve_employee.position_title = 'SDM')\n"
         + "GROUP BY reserve_employee.store_id, reserve_employee.position_title";
-    checkRel2Sql(logicalPlanner, query, expected,
-        DatabaseProduct.DB2.getDialect());
+    sql(query).dialect(DatabaseProduct.DB2.getDialect()).ok(expected);
   }
 
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-1372">[CALCITE-1372]
+   * JDBC adapter generates SQL with wrong field names</a>. */
+  @Test public void testJoinPlan2() {
+    final String sql = "SELECT v1.deptno, v2.deptno\n"
+        + "FROM dept v1 LEFT JOIN emp v2 ON v1.deptno = v2.deptno\n"
+        + "WHERE v2.job LIKE 'PRESIDENT'";
+    final String expected = "SELECT \"DEPT\".\"DEPTNO\","
+        + " \"EMP\".\"DEPTNO\" AS \"DEPTNO0\"\n"
+        + "FROM \"JDBC_SCOTT\".\"DEPT\"\n"
+        + "LEFT JOIN \"JDBC_SCOTT\".\"EMP\""
+        + " ON \"DEPT\".\"DEPTNO\" = \"EMP\".\"DEPTNO\"\n"
+        + "WHERE \"EMP\".\"JOB\" LIKE 'PRESIDENT'";
+    final String expected2 = "SELECT DEPT.DEPTNO, EMP.DEPTNO AS DEPTNO0\n"
+        + "FROM JDBC_SCOTT.DEPT AS DEPT\n"
+        + "LEFT JOIN JDBC_SCOTT.EMP AS EMP ON DEPT.DEPTNO = EMP.DEPTNO\n"
+        + "WHERE EMP.JOB LIKE 'PRESIDENT'";
+    sql(sql)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .ok(expected)
+        .dialect(DatabaseProduct.DB2.getDialect())
+        .ok(expected2);
+  }
+
+  /** Fluid interface to run tests. */
+  private static class Sql {
+    private CalciteAssert.SchemaSpec schemaSpec;
+    private final String sql;
+    private final SqlDialect dialect;
+
+    Sql(CalciteAssert.SchemaSpec schemaSpec, String sql, SqlDialect dialect) {
+      this.schemaSpec = schemaSpec;
+      this.sql = sql;
+      this.dialect = dialect;
+    }
+
+    Sql dialect(SqlDialect dialect) {
+      return new Sql(schemaSpec, sql, dialect);
+    }
+
+    Sql planner(Planner planner) {
+      return new Sql(schemaSpec, sql, dialect);
+    }
+
+    Sql ok(String expectedQuery) {
+      final Planner planner =
+          getPlanner(null, SqlParser.Config.DEFAULT, schemaSpec);
+      try {
+        SqlNode parse = planner.parse(sql);
+        SqlNode validate = planner.validate(parse);
+        RelNode rel = planner.rel(validate).rel;
+        final RelToSqlConverter converter =
+            new RelToSqlConverter(dialect);
+        final SqlNode sqlNode = converter.visitChild(0, rel).asQuery();
+        assertThat(Util.toLinux(sqlNode.toSqlString(dialect).getSql()),
+            is(expectedQuery));
+      } catch (Exception e) {
+        assertTrue("Parsing failed throwing error: " + e.getMessage(), false);
+      }
+      return this;
+    }
+
+    public Sql schema(CalciteAssert.SchemaSpec schemaSpec) {
+      return new Sql(schemaSpec, sql, dialect);
+    }
+  }
 }
 
 // End RelToSqlConverterTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/f9724c27/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
index 97da6c3..4b65017 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.calcite.test;
 
+import org.apache.calcite.config.Lex;
 import org.apache.calcite.jdbc.CalciteConnection;
 
 import com.google.common.base.Function;
@@ -319,6 +320,19 @@ public class JdbcAdapterTest {
   }
 
   /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-1372">[CALCITE-1372]
+   * JDBC adapter generates SQL with wrong field names</a>. */
+  @Test public void testJoinPlan2() {
+    final String sql = "SELECT v1.deptno, v2.deptno\n"
+        + "FROM Scott.dept v1 LEFT JOIN Scott.emp v2 ON v1.deptno = v2.deptno\n"
+        + "WHERE v2.job LIKE 'PRESIDENT'";
+    CalciteAssert.model(JdbcTest.SCOTT_MODEL)
+        .with(Lex.MYSQL)
+        .query(sql).runs()
+        .returnsCount(1);
+  }
+
+  /** Test case for
    * <a href="https://issues.apache.org/jira/browse/CALCITE-657">[CALCITE-657]
    * NullPointerException when executing JdbcAggregate implement method</a>. */
   @Test public void testJdbcAggregate() throws Exception {


[3/6] calcite git commit: [CALCITE-1208] Improve two-level column structure handling

Posted by jh...@apache.org.
[CALCITE-1208] Improve two-level column structure handling

Modify RelDataType interface, and RelDataTypeImpl.getField().


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

Branch: refs/heads/master
Commit: a859f93ff2fc800e3e4a481274e3daf19f72e8b2
Parents: 339ac13
Author: maryannxue <ma...@gmail.com>
Authored: Wed Apr 27 22:41:19 2016 -0400
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Sep 15 17:02:42 2016 -0700

----------------------------------------------------------------------
 .../apache/calcite/rel/type/RelDataType.java    |  13 ++
 .../calcite/rel/type/RelDataTypeImpl.java       |  43 +++++-
 .../apache/calcite/test/MockCatalogReader.java  | 131 +++++++++++++++++++
 .../calcite/test/SqlToRelConverterTest.java     |   4 +
 .../apache/calcite/test/SqlValidatorTest.java   | 101 ++++++++++++++
 5 files changed, 288 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/a859f93f/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java
index 9bd73f2..442538e 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java
@@ -36,6 +36,12 @@ public interface RelDataType /*extends Type*/ {
   int SCALE_NOT_SPECIFIED = Integer.MIN_VALUE;
   int PRECISION_NOT_SPECIFIED = -1;
 
+  enum StructKind {
+    FULLY_QUALIFIED,
+    PEEK_FIELDS,
+    PEEK_FIELDS_DEFAULT,
+  }
+
   //~ Methods ----------------------------------------------------------------
 
   /**
@@ -77,6 +83,13 @@ public interface RelDataType /*extends Type*/ {
   int getFieldCount();
 
   /**
+   * Gets the StructKind of a structured type.
+   *
+   * @return the StructKind that determines how its fields are resolved.
+   */
+  StructKind getStructKind();
+
+  /**
    * Looks up a field by name.
    *
    * <p>NOTE: Be careful choosing the value of {@code caseSensitive}:</p>

http://git-wip-us.apache.org/repos/asf/calcite/blob/a859f93f/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java
index 3134b15..0fc14f3 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java
@@ -84,6 +84,36 @@ public abstract class RelDataTypeImpl
         return field;
       }
     }
+    for (RelDataTypeField field : fieldList) {
+      RelDataType dataType = field.getType();
+      if (dataType.isStruct()
+          && dataType.getStructKind() == StructKind.PEEK_FIELDS_DEFAULT) {
+        for (RelDataTypeField subField : dataType.getFieldList()) {
+          if (Util.matches(caseSensitive, subField.getName(), fieldName)) {
+            return subField;
+          }
+        }
+      }
+    }
+    RelDataTypeField matched = null;
+    for (RelDataTypeField field : fieldList) {
+      RelDataType dataType = field.getType();
+      if (dataType.isStruct()
+          && dataType.getStructKind() == StructKind.PEEK_FIELDS) {
+        for (RelDataTypeField subField : dataType.getFieldList()) {
+          if (Util.matches(caseSensitive, subField.getName(), fieldName)) {
+            if (matched == null) {
+              matched = subField;
+            } else {
+              // TODO throw columnAmbiguous
+            }
+          }
+        }
+      }
+    }
+    if (matched != null) {
+      return matched;
+    }
     if (elideRecord) {
       final List<Slot> slots = Lists.newArrayList();
       getFieldRecurse(slots, this, 0, fieldName, caseSensitive);
@@ -119,13 +149,13 @@ public abstract class RelDataTypeImpl
     return null;
   }
 
-  private static void getFieldRecurse(List<Slot> slots, RelDataTypeImpl type,
+  private static void getFieldRecurse(List<Slot> slots, RelDataType type,
       int depth, String fieldName, boolean caseSensitive) {
     while (slots.size() <= depth) {
       slots.add(new Slot());
     }
     final Slot slot = slots.get(depth);
-    for (RelDataTypeField field : type.fieldList) {
+    for (RelDataTypeField field : type.getFieldList()) {
       if (Util.matches(caseSensitive, field.getName(), fieldName)) {
         slot.count++;
         slot.field = field;
@@ -133,9 +163,9 @@ public abstract class RelDataTypeImpl
     }
     // No point looking to depth + 1 if there is a hit at depth.
     if (slot.count == 0) {
-      for (RelDataTypeField field : type.fieldList) {
+      for (RelDataTypeField field : type.getFieldList()) {
         if (field.getType().isStruct()) {
-          getFieldRecurse(slots, (RelDataTypeImpl) field.getType(), depth + 1,
+          getFieldRecurse(slots, field.getType(), depth + 1,
               fieldName, caseSensitive);
         }
       }
@@ -158,6 +188,11 @@ public abstract class RelDataTypeImpl
     return fieldList.size();
   }
 
+  public StructKind getStructKind() {
+    assert isStruct() : this;
+    return StructKind.FULLY_QUALIFIED;
+  }
+
   // implement RelDataType
   public RelDataType getComponentType() {
     // this is not a collection type

http://git-wip-us.apache.org/repos/asf/calcite/blob/a859f93f/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java b/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
index 039a655..d61b440 100644
--- a/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
+++ b/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
@@ -34,10 +34,14 @@ import org.apache.calcite.rel.logical.LogicalProject;
 import org.apache.calcite.rel.logical.LogicalTableScan;
 import org.apache.calcite.rel.type.DynamicRecordTypeImpl;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataType.StructKind;
 import org.apache.calcite.rel.type.RelDataTypeComparability;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeFactory.FieldInfoBuilder;
+import org.apache.calcite.rel.type.RelDataTypeFamily;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.RelDataTypeFieldImpl;
+import org.apache.calcite.rel.type.RelDataTypePrecedenceList;
 import org.apache.calcite.rel.type.RelRecordType;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexInputRef;
@@ -50,8 +54,10 @@ import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.Schemas;
 import org.apache.calcite.schema.Table;
 import org.apache.calcite.sql.SqlAccessType;
+import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlFunctionCategory;
 import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlSyntax;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
@@ -79,6 +85,7 @@ import com.google.common.collect.Sets;
 
 import java.lang.reflect.Type;
 import java.math.BigDecimal;
+import java.nio.charset.Charset;
 import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -419,6 +426,32 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     emp20View.addColumn("SLACKER", booleanType);
     registerTable(emp20View);
 
+    MockSchema structTypeSchema = new MockSchema("STRUCT");
+    registerSchema(structTypeSchema);
+    MockTable structTypeTable = MockTable.create(
+        this, structTypeSchema, "T", false, 100);
+    structTypeTable.addColumn("K0", varchar20Type);
+    structTypeTable.addColumn("C1", varchar20Type);
+    FieldInfoBuilder builder = typeFactory.builder();
+    builder.add("C0", intType);
+    builder.add("C1", intType);
+    RelDataType f0Type = new DelegateStructType(
+        typeFactory.createStructType(builder), StructKind.PEEK_FIELDS_DEFAULT);
+    structTypeTable.addColumn("F0", f0Type);
+    builder = typeFactory.builder();
+    builder.add("C0", intTypeNull);
+    builder.add("C2", intType);
+    builder.add("A0", intType);
+    RelDataType f1Type = new DelegateStructType(
+        typeFactory.createStructType(builder), StructKind.PEEK_FIELDS);
+    structTypeTable.addColumn("F1", f1Type);
+    builder = typeFactory.builder();
+    builder.add("C3", intType);
+    builder.add("A0", booleanType);
+    RelDataType f2Type = new DelegateStructType(
+        typeFactory.createStructType(builder), StructKind.PEEK_FIELDS);
+    structTypeTable.addColumn("F2", f2Type);
+    registerTable(structTypeTable);
     return this;
   }
 
@@ -766,7 +799,105 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     }
   }
 
+  private static class DelegateStructType implements RelDataType {
+    private RelDataType delegate;
+    private StructKind structKind;
 
+    DelegateStructType(RelDataType delegate, StructKind structKind) {
+      assert delegate.isStruct();
+      this.delegate = delegate;
+      this.structKind = structKind;
+    }
+
+    @Override public boolean isStruct() {
+      return delegate.isStruct();
+    }
+
+    @Override public boolean isDynamicStruct() {
+      return delegate.isDynamicStruct();
+    }
+
+    @Override public List<RelDataTypeField> getFieldList() {
+      return delegate.getFieldList();
+    }
+
+    @Override public List<String> getFieldNames() {
+      return delegate.getFieldNames();
+    }
+
+    @Override public int getFieldCount() {
+      return delegate.getFieldCount();
+    }
+
+    @Override public StructKind getStructKind() {
+      return structKind;
+    }
+
+    @Override public RelDataTypeField getField(String fieldName,
+        boolean caseSensitive, boolean elideRecord) {
+      return delegate.getField(fieldName, caseSensitive, elideRecord);
+    }
+
+    @Override public boolean isNullable() {
+      return delegate.isNullable();
+    }
+
+    @Override public RelDataType getComponentType() {
+      return delegate.getComponentType();
+    }
+
+    @Override public RelDataType getKeyType() {
+      return delegate.getKeyType();
+    }
+
+    @Override public RelDataType getValueType() {
+      return delegate.getValueType();
+    }
+
+    @Override public Charset getCharset() {
+      return delegate.getCharset();
+    }
+
+    @Override public SqlCollation getCollation() {
+      return delegate.getCollation();
+    }
+
+    @Override public SqlIntervalQualifier getIntervalQualifier() {
+      return delegate.getIntervalQualifier();
+    }
+
+    @Override public int getPrecision() {
+      return delegate.getPrecision();
+    }
+
+    @Override public int getScale() {
+      return delegate.getScale();
+    }
+
+    @Override public SqlTypeName getSqlTypeName() {
+      return delegate.getSqlTypeName();
+    }
+
+    @Override public SqlIdentifier getSqlIdentifier() {
+      return delegate.getSqlIdentifier();
+    }
+
+    @Override public String getFullTypeString() {
+      return delegate.getFullTypeString();
+    }
+
+    @Override public RelDataTypeFamily getFamily() {
+      return delegate.getFamily();
+    }
+
+    @Override public RelDataTypePrecedenceList getPrecedenceList() {
+      return delegate.getPrecedenceList();
+    }
+
+    @Override public RelDataTypeComparability getComparability() {
+      return delegate.getComparability();
+    }
+  }
 }
 
 // End MockCatalogReader.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/a859f93f/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 0ab5260..7ec5087 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -1720,6 +1720,10 @@ public class SqlToRelConverterTest extends SqlToRelTestBase {
     sql(sql).decorrelate(true).ok();
   }
 
+  @Test public void testStructType() {
+    sql("select * from struct.t").convertsTo("");
+  }
+
   /**
    * Test case for
    * <a href="https://issues.apache.org/jira/browse/CALCITE-1150">[CALCITE-1150]

http://git-wip-us.apache.org/repos/asf/calcite/blob/a859f93f/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 7fff2d9..c48b528 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -8137,6 +8137,107 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
     //   12345678901234567890123456789012345678901234567890123456789012345
     //        check("SELECT count(0) FROM emp GROUP BY ()");
   }
+
+  @Test public void testStructType() {
+    // Table STRUCT.T is defined as:
+    // (K0 VARCHAR(20) NOT NULL, C1 VARCHAR(20) NOT NULL,
+    //   RecordType(C0 INTEGER NOT NULL, C1 INTEGER NOT NULL) F0,
+    //   RecordType(C0 INTEGER, C2 INTEGER NOT NULL, A0 INTEGER NOT NULL) F1,
+    //   RecordType(C3 INTEGER NOT NULL, A0 BOOLEAN NOT NULL) F2)
+    // , where F0 has a default struct priority.
+
+    check("select * from struct.t");
+
+    // Resolve K0 as top-level column K0.
+    checkResultType("select k0 from struct.t",
+        "RecordType(VARCHAR(20) NOT NULL K0) NOT NULL");
+
+    // Resolve C2 as secondary-level column F1.C2.
+    checkResultType("select c2 from struct.t",
+        "RecordType(INTEGER NOT NULL C2) NOT NULL");
+
+    // Resolve F1.C2 as fully qualified column F1.C2.
+    checkResultType("select c2 from struct.t",
+        "RecordType(INTEGER NOT NULL C2) NOT NULL");
+
+    // Resolve C1 as top-level column C1 as opposed to F0.C1.
+    checkResultType("select c1 from struct.t",
+        "RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+
+    // Resolve C0 as secondary-level column F0.C0 as opposed to F1.C0, since F0 has the
+    // default priority.
+    checkResultType("select c0 from struct.t",
+        "RecordType(INTEGER NOT NULL C0) NOT NULL");
+
+    // Resolve F1.C0 as fully qualified column F1.C0.
+    checkResultType("select f1.c0 from struct.t",
+        "RecordType(INTEGER C0) NOT NULL");
+
+    // Fail ambiguous column reference A0, since F1.A0 and F2.A0 both exist with the
+    // same resolving priority.
+    checkFails("select ^a0^ from struct.t",
+        "Column 'A0' is ambiguous");
+
+    // Resolve F2.A0 as fully qualified column F2.A0.
+    checkResultType("select f2.a0 from struct.t",
+        "RecordType(BOOLEAN NOT NULL C0) NOT NULL");
+
+    // Resolve T0.K0 as top-level column K0, since T0 is recognized as the table alias.
+    checkResultType("select t0.k0 from struct.t t0",
+        "RecordType(VARCHAR(20) NOT NULL K0) NOT NULL");
+
+    // Resolve T0.C2 as secondary-level column F1.C2, since T0 is recognized as the
+    // table alias here.
+    checkResultType("select t0.c2 from struct.t t0",
+        "RecordType(INTEGER NOT NULL C2) NOT NULL");
+
+    // Resolve F0.C2 as secondary-level column F1.C2, since F0 is recognized as the
+    // table alias here.
+    checkResultType("select f0.c2 from struct.t f0",
+        "RecordType(INTEGER NOT NULL C2) NOT NULL");
+
+    // Resolve F0.C1 as top-level column C1 as opposed to F0.C1, since F0 is recognized
+    // as the table alias here.
+    checkResultType("select f0.c1 from struct.t f0",
+        "RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+
+    // Resolve T.C1 as top-level column C1 as opposed to F0.C1, since T is recognized as
+    // the table name.
+    checkResultType("select t.c1 from struct.t f0",
+        "RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+
+    // Resolve STRUCT.T.C1 as top-level column C1 as opposed to F0.C1, since STRUCT.T is
+    // recognized as the schema and table name.
+    checkResultType("select struct.t.c1 from struct.t f0",
+        "RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+
+    // Resolve F0.F0.C1 as secondary-level column F0.C1, since the first F0 is recognized
+    // as the table alias here.
+    checkResultType("select f0.f0.c1 from struct.t f0",
+        "RecordType(INTEGER NOT NULL C1) NOT NULL");
+
+    // Resolve T.F0.C1 as secondary-level column F0.C1, since T is recognized as the table
+    // name.
+    checkResultType("select t.f0.c1 from struct.t f0",
+        "RecordType(INTEGER NOT NULL C1) NOT NULL");
+
+    // Resolve STRUCT.T.F0.C1 as secondary-level column F0.C1, since STRUCT.T is
+    // recognized as the schema and table name.
+    checkResultType("select struct.t.f0.c1 from struct.t f0",
+        "RecordType(INTEGER NOT NULL C1) NOT NULL");
+
+    // Resolve struct type F1 with wildcard.
+    checkResultType("select f1.* from struct.t",
+        "RecordType(INTEGER NOT NULL C0, INTEGER NOT NULL C2) NOT NULL");
+
+    // Fail non-existent column B0.
+    checkFails("select ^b0^ from struct.t",
+        "Column 'B0' not found in any table");
+
+    // Fail struct type with no wildcard.
+    checkFails("select ^f1^ from struct.t",
+        "Unknown identifier 'F1'");
+  }
 }
 
 // End SqlValidatorTest.java


[5/6] calcite git commit: Complete [CALCITE-1208] by giving "resolve" a call-back, so it can make multiple matches

Posted by jh...@apache.org.
Complete [CALCITE-1208] by giving "resolve" a call-back, so it can make multiple matches

Change parser to make 'SELECT t.*.c' illegal.

Address Jinfeng's review comments.

Add a test for converting a query with UNION in FROM clause.


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

Branch: refs/heads/master
Commit: 0938c7b6d767e3242874d87a30d9112512d9243a
Parents: a859f93
Author: Julian Hyde <jh...@apache.org>
Authored: Fri May 6 11:57:25 2016 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Sep 15 17:02:42 2016 -0700

----------------------------------------------------------------------
 core/src/main/codegen/templates/Parser.jj       |  25 +-
 .../calcite/prepare/CalciteCatalogReader.java   |  15 +-
 .../AggregateExpandDistinctAggregatesRule.java  |   2 +-
 .../calcite/rel/type/DynamicRecordTypeImpl.java |  41 ++-
 .../apache/calcite/rel/type/RelDataType.java    |  14 +-
 .../calcite/rel/type/RelDataTypeFactory.java    |  36 ++-
 .../rel/type/RelDataTypeFactoryImpl.java        |  61 +++-
 .../calcite/rel/type/RelDataTypeHolder.java     |  27 +-
 .../calcite/rel/type/RelDataTypeImpl.java       |  58 +---
 .../apache/calcite/rel/type/RelRecordType.java  |  36 ++-
 .../org/apache/calcite/rel/type/StructKind.java |  83 ++++++
 .../apache/calcite/runtime/CalciteResource.java |   3 +
 .../org/apache/calcite/runtime/FlatLists.java   |  41 +++
 .../org/apache/calcite/sql/SqlIdentifier.java   |  16 +
 .../calcite/sql/validate/CatalogScope.java      |  10 -
 .../calcite/sql/validate/DelegatingScope.java   | 265 +++++++++++++----
 .../apache/calcite/sql/validate/EmptyScope.java |   6 +-
 .../apache/calcite/sql/validate/ListScope.java  | 136 +++++----
 .../calcite/sql/validate/ParameterScope.java    |   9 -
 .../calcite/sql/validate/SqlQualified.java      |   5 +-
 .../sql/validate/SqlValidatorCatalogReader.java |   8 -
 .../calcite/sql/validate/SqlValidatorImpl.java  |  74 +++--
 .../calcite/sql/validate/SqlValidatorScope.java | 120 +++++++-
 .../calcite/sql/validate/SqlValidatorUtil.java  |  48 ++-
 .../apache/calcite/sql/validate/WithScope.java  |  15 +-
 .../calcite/sql2rel/SqlToRelConverter.java      |  61 ++--
 .../apache/calcite/util/ImmutableIntList.java   |   6 +-
 .../main/java/org/apache/calcite/util/Pair.java |   5 +
 .../calcite/runtime/CalciteResource.properties  |   1 +
 .../calcite/sql/parser/SqlParserTest.java       |  15 +
 .../apache/calcite/sql/test/SqlAdvisorTest.java |   2 +
 .../apache/calcite/test/MockCatalogReader.java  | 162 ++++++----
 .../calcite/test/SqlToRelConverterTest.java     |  10 +-
 .../apache/calcite/test/SqlValidatorTest.java   | 294 +++++++++++++------
 .../calcite/test/SqlToRelConverterTest.xml      |  31 ++
 .../adapter/splunk/SplunkPushDownRule.java      |  13 +-
 36 files changed, 1165 insertions(+), 589 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/codegen/templates/Parser.jj
----------------------------------------------------------------------
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 43d6cd5..2d97cf3 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -3686,20 +3686,19 @@ SqlIdentifier CompoundIdentifier() :
     }
     (
         <DOT>
-        (
-            p = Identifier() {
-                star = false;
-                list.add(p);
-                posList.add(getPos());
-            }
-        |
-            <STAR> {
-                star = true;
-                list.add("*");
-                posList.add(getPos());
-            }
-        )
+        p = Identifier() {
+            list.add(p);
+            posList.add(getPos());
+        }
     ) *
+    (
+        <DOT>
+        <STAR> {
+            star = true;
+            list.add("");
+            posList.add(getPos());
+        }
+    )?
     {
         SqlParserPos pos = SqlParserPos.sum(posList);
         if (star) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java b/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
index 9d8f3ef..0e14c83 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
@@ -74,7 +74,6 @@ public class CalciteCatalogReader implements Prepare.CatalogReader {
   final CalciteSchema rootSchema;
   final JavaTypeFactory typeFactory;
   private final List<String> defaultSchema;
-  private final boolean elideRecord = true;
   private final boolean caseSensitive;
 
   public CalciteCatalogReader(
@@ -203,27 +202,17 @@ public class CalciteCatalogReader implements Prepare.CatalogReader {
   }
 
   public RelDataTypeField field(RelDataType rowType, String alias) {
-    return SqlValidatorUtil.lookupField(caseSensitive, elideRecord, rowType,
-        alias);
-  }
-
-  public int fieldOrdinal(RelDataType rowType, String alias) {
-    RelDataTypeField field = field(rowType, alias);
-    return field != null ? field.getIndex() : -1;
+    return SqlValidatorUtil.lookupField(caseSensitive, rowType, alias);
   }
 
   public boolean matches(String string, String name) {
     return Util.matches(caseSensitive, string, name);
   }
 
-  public int match(List<String> strings, String name) {
-    return Util.findMatch(strings, name, caseSensitive);
-  }
-
   public RelDataType createTypeFromProjection(final RelDataType type,
       final List<String> columnNameList) {
     return SqlValidatorUtil.createTypeFromProjection(type, columnNameList,
-        typeFactory, caseSensitive, elideRecord);
+        typeFactory, caseSensitive);
   }
 
   public void lookupOperatorOverloads(final SqlIdentifier opName,

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java
index a5f27f7..de12e42 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java
@@ -571,7 +571,7 @@ public final class AggregateExpandDistinctAggregatesRule extends RelOptRule {
       List<Integer> argList) {
     ImmutableIntList list = ImmutableIntList.of();
     for (int arg : argList) {
-      list = list.add(remap(groupSet, arg));
+      list = list.append(remap(groupSet, arg));
     }
     return list;
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/type/DynamicRecordTypeImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/DynamicRecordTypeImpl.java b/core/src/main/java/org/apache/calcite/rel/type/DynamicRecordTypeImpl.java
index 21206ac..82b6714 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/DynamicRecordTypeImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/DynamicRecordTypeImpl.java
@@ -24,35 +24,34 @@ import java.util.Collections;
 import java.util.List;
 
 /**
- * Implementation of RelDataType for dynamic table. It's used in
- * Sql validation phase, where field list is mutable for getField() call.
+ * Implementation of {@link RelDataType} for a dynamic table.
  *
- * <p>After Sql validation, a normal RelDataTypeImpl with immutable field list
- * would take place of DynamicRecordTypeImpl instance for dynamic table. </p>
+ * <p>It's used during SQL validation, where the field list is mutable for
+ * the getField() call. After SQL validation, a normal {@link RelDataTypeImpl}
+ * with an immutable field list takes the place of the DynamicRecordTypeImpl
+ * instance.
  */
 public class DynamicRecordTypeImpl extends DynamicRecordType {
-
-  private final RelDataTypeFactory typeFactory;
   private final RelDataTypeHolder holder;
 
+  /** Creates a DynamicRecordTypeImpl. */
   public DynamicRecordTypeImpl(RelDataTypeFactory typeFactory) {
-    this.typeFactory = typeFactory;
-    this.holder = new RelDataTypeHolder();
-    this.holder.setRelDataTypeFactory(typeFactory);
+    this.holder = new RelDataTypeHolder(typeFactory);
     computeDigest();
   }
 
-  public List<RelDataTypeField> getFieldList() {
-    return holder.getFieldList(typeFactory);
+  @Override public List<RelDataTypeField> getFieldList() {
+    return holder.getFieldList();
   }
 
-  public int getFieldCount() {
+  @Override public int getFieldCount() {
     return holder.getFieldCount();
   }
 
-  public RelDataTypeField getField(String fieldName, boolean caseSensitive, boolean elideRecord) {
-    Pair<RelDataTypeField, Boolean> pair = holder.getFieldOrInsert(typeFactory, fieldName,
-        caseSensitive);
+  @Override public RelDataTypeField getField(String fieldName,
+      boolean caseSensitive, boolean elideRecord) {
+    final Pair<RelDataTypeField, Boolean> pair =
+        holder.getFieldOrInsert(fieldName, caseSensitive);
     // If a new field is added, we should re-compute the digest.
     if (pair.right) {
       computeDigest();
@@ -61,27 +60,27 @@ public class DynamicRecordTypeImpl extends DynamicRecordType {
     return pair.left;
   }
 
-  public List<String> getFieldNames() {
+  @Override public List<String> getFieldNames() {
     return holder.getFieldNames();
   }
 
-  public SqlTypeName getSqlTypeName() {
+  @Override public SqlTypeName getSqlTypeName() {
     return SqlTypeName.ROW;
   }
 
-  public RelDataTypePrecedenceList getPrecedenceList() {
+  @Override public RelDataTypePrecedenceList getPrecedenceList() {
     return new SqlTypeExplicitPrecedenceList(Collections.<SqlTypeName>emptyList());
   }
 
   protected void generateTypeString(StringBuilder sb, boolean withDetail) {
-    sb.append("(DynamicRecordRow" + getFieldNames() + ")");
+    sb.append("(DynamicRecordRow").append(getFieldNames()).append(")");
   }
 
-  public boolean isStruct() {
+  @Override public boolean isStruct() {
     return true;
   }
 
-  public RelDataTypeFamily getFamily() {
+  @Override public RelDataTypeFamily getFamily() {
     return getSqlTypeName().getFamily();
   }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java
index 442538e..7b18d67 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataType.java
@@ -36,12 +36,6 @@ public interface RelDataType /*extends Type*/ {
   int SCALE_NOT_SPECIFIED = Integer.MIN_VALUE;
   int PRECISION_NOT_SPECIFIED = -1;
 
-  enum StructKind {
-    FULLY_QUALIFIED,
-    PEEK_FIELDS,
-    PEEK_FIELDS_DEFAULT,
-  }
-
   //~ Methods ----------------------------------------------------------------
 
   /**
@@ -83,9 +77,10 @@ public interface RelDataType /*extends Type*/ {
   int getFieldCount();
 
   /**
-   * Gets the StructKind of a structured type.
+   * Returns the rule for resolving the fields of a structured type,
+   * or {@link StructKind#NONE} if this is not a structured type.
    *
-   * @return the StructKind that determines how its fields are resolved.
+   * @return the StructKind that determines how this type's fields are resolved
    */
   StructKind getStructKind();
 
@@ -242,9 +237,10 @@ public interface RelDataType /*extends Type*/ {
   RelDataTypeComparability getComparability();
 
   /**
-   *@return whether it has dynamic structure (for "schema-on-read" table)
+   * @return whether it has dynamic structure (for "schema-on-read" table)
    */
   boolean isDynamicStruct();
+
 }
 
 // End RelDataType.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
index 7c3e88c..1b3a1be 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
@@ -50,7 +50,7 @@ public interface RelDataTypeFactory {
   RelDataTypeSystem getTypeSystem();
 
   /**
-   * Creates a type which corresponds to a Java class.
+   * Creates a type that corresponds to a Java class.
    *
    * @param clazz the Java class used to define the type
    * @return canonical Java type descriptor
@@ -66,28 +66,37 @@ public interface RelDataTypeFactory {
   RelDataType createJoinType(RelDataType... types);
 
   /**
-   * Creates a type which represents a structured collection of fields, given
+   * Creates a type that represents a structured collection of fields, given
    * lists of the names and types of the fields.
    *
+   * @param  kind         Name resolution policy
    * @param typeList      types of the fields
    * @param fieldNameList names of the fields
    * @return canonical struct type descriptor
    */
+  RelDataType createStructType(StructKind kind,
+      List<RelDataType> typeList,
+      List<String> fieldNameList);
+
+  /** Creates a type that represents a structured collection of fields.
+   * Shorthand for <code>createStructType(StructKind.FULLY_QUALIFIED, typeList,
+   * fieldNameList)</code>. */
   RelDataType createStructType(
       List<RelDataType> typeList,
       List<String> fieldNameList);
 
   /**
-   * Creates a type which represents a structured collection of fields,
+   * Creates a type that represents a structured collection of fields,
    * obtaining the field information via a callback.
    *
    * @param fieldInfo callback for field information
    * @return canonical struct type descriptor
    */
+  @Deprecated // to be removed before 2.0
   RelDataType createStructType(FieldInfo fieldInfo);
 
   /**
-   * Creates a type which represents a structured collection of fieldList,
+   * Creates a type that represents a structured collection of fieldList,
    * obtaining the field information from a list of (name, type) pairs.
    *
    * @param fieldList List of (name, type) pairs
@@ -140,7 +149,7 @@ public interface RelDataTypeFactory {
   RelDataType copyType(RelDataType type);
 
   /**
-   * Creates a type which is the same as another type but with possibly
+   * Creates a type that is the same as another type but with possibly
    * different nullability. The output type may be identical to the input
    * type. For type systems without a concept of nullability, the return value
    * is always the same as the input.
@@ -156,7 +165,7 @@ public interface RelDataTypeFactory {
       boolean nullable);
 
   /**
-   * Creates a Type which is the same as another type but with possibly
+   * Creates a type that is the same as another type but with possibly
    * different charset or collation. For types without a concept of charset or
    * collation this function must throw an error.
    *
@@ -179,7 +188,7 @@ public interface RelDataTypeFactory {
   /**
    * Returns the most general of a set of types (that is, one type to which
    * they can all be cast), or null if conversion is not possible. The result
-   * may be a new type which is less restrictive than any of the input types,
+   * may be a new type that is less restrictive than any of the input types,
    * e.g. <code>leastRestrictive(INT, NUMERIC(3, 2))</code> could be
    * {@code NUMERIC(12, 2)}.
    *
@@ -288,8 +297,9 @@ public interface RelDataTypeFactory {
   //~ Inner Interfaces -------------------------------------------------------
 
   /**
-   * Callback which provides enough information to create fields.
+   * Callback that provides enough information to create fields.
    */
+  @Deprecated // to be removed before 2.0
   interface FieldInfo {
     /**
      * Returns the number of fields.
@@ -319,10 +329,11 @@ public interface RelDataTypeFactory {
    * Implementation of {@link FieldInfo} that provides a fluid API to build
    * a list of fields.
    */
+  @SuppressWarnings("deprecation")
   class FieldInfoBuilder implements FieldInfo {
     private final List<String> names = new ArrayList<>();
     private final List<RelDataType> types = new ArrayList<>();
-
+    private StructKind kind = StructKind.FULLY_QUALIFIED;
     private final RelDataTypeFactory typeFactory;
 
     /**
@@ -418,6 +429,11 @@ public interface RelDataTypeFactory {
       return this;
     }
 
+    public FieldInfoBuilder kind(StructKind kind) {
+      this.kind = kind;
+      return this;
+    }
+
     /**
      * Makes sure that field names are unique.
      */
@@ -435,7 +451,7 @@ public interface RelDataTypeFactory {
      * Creates a struct type with the current contents of this builder.
      */
     public RelDataType build() {
-      return typeFactory.createStructType(types, names);
+      return typeFactory.createStructType(kind, types, names);
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
index eaff896..774a728 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
@@ -22,7 +22,6 @@ import org.apache.calcite.sql.type.JavaToSqlTypeConversionRules;
 import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
-import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 
 import com.google.common.base.Preconditions;
@@ -41,6 +40,8 @@ import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import javax.annotation.Nonnull;
 
 /**
  * Abstract base for implementations of {@link RelDataTypeFactory}.
@@ -56,21 +57,20 @@ public abstract class RelDataTypeFactoryImpl implements RelDataTypeFactory {
           .softValues()
           .build(
               new CacheLoader<Object, RelDataType>() {
-                @Override public RelDataType load(Object key) {
-                  if (key instanceof RelDataType) {
-                    return (RelDataType) key;
+                @Override public RelDataType load(@Nonnull Object k) {
+                  if (k instanceof RelDataType) {
+                    return (RelDataType) k;
                   }
                   @SuppressWarnings("unchecked")
-                  final Pair<List<String>, List<RelDataType>> pair =
-                      (Pair<List<String>, List<RelDataType>>) key;
+                  final Key key = (Key) k;
                   final ImmutableList.Builder<RelDataTypeField> list =
                       ImmutableList.builder();
-                  for (int i = 0; i < pair.left.size(); i++) {
+                  for (int i = 0; i < key.names.size(); i++) {
                     list.add(
                         new RelDataTypeFieldImpl(
-                            pair.left.get(i), i, pair.right.get(i)));
+                            key.names.get(i), i, key.types.get(i)));
                   }
-                  return new RelRecordType(list.build());
+                  return new RelRecordType(key.kind, list.build());
                 }
               });
 
@@ -132,18 +132,24 @@ public abstract class RelDataTypeFactoryImpl implements RelDataTypeFactory {
         new RelCrossType(flattenedTypes, getFieldList(flattenedTypes)));
   }
 
-  // implement RelDataTypeFactory
   public RelDataType createStructType(
       final List<RelDataType> typeList,
       final List<String> fieldNameList) {
+    return createStructType(StructKind.FULLY_QUALIFIED, typeList,
+        fieldNameList);
+  }
+
+  public RelDataType createStructType(StructKind kind,
+      final List<RelDataType> typeList,
+      final List<String> fieldNameList) {
     assert typeList.size() == fieldNameList.size();
-    return canonize(fieldNameList, typeList);
+    return canonize(kind, fieldNameList, typeList);
   }
 
   // implement RelDataTypeFactory
   public RelDataType createStructType(
       final RelDataTypeFactory.FieldInfo fieldInfo) {
-    return canonize(
+    return canonize(StructKind.FULLY_QUALIFIED,
         new AbstractList<String>() {
           @Override public String get(int index) {
             return fieldInfo.getFieldName(index);
@@ -346,16 +352,16 @@ public abstract class RelDataTypeFactoryImpl implements RelDataTypeFactory {
    * key is more expensive, because it must be immutable and not hold
    * references into other data structures.</p>
    */
-  protected RelDataType canonize(
+  protected RelDataType canonize(final StructKind kind,
       final List<String> names,
       final List<RelDataType> types) {
-    final RelDataType type = CACHE.getIfPresent(Pair.of(names, types));
+    final RelDataType type = CACHE.getIfPresent(new Key(kind, names, types));
     if (type != null) {
       return type;
     }
     final ImmutableList<String> names2 = ImmutableList.copyOf(names);
     final ImmutableList<RelDataType> types2 = ImmutableList.copyOf(types);
-    return CACHE.getUnchecked(Pair.of(names2, types2));
+    return CACHE.getUnchecked(new Key(kind, names2, types2));
   }
 
   /**
@@ -646,6 +652,31 @@ public abstract class RelDataTypeFactoryImpl implements RelDataTypeFactory {
       return typeName;
     }
   }
+
+  /** Key to the data type cache. */
+  private static class Key {
+    private final StructKind kind;
+    private final List<String> names;
+    private final List<RelDataType> types;
+
+    Key(StructKind kind, List<String> names, List<RelDataType> types) {
+      this.kind = kind;
+      this.names = names;
+      this.types = types;
+    }
+
+    @Override public int hashCode() {
+      return Objects.hash(kind, names, types);
+    }
+
+    @Override public boolean equals(Object obj) {
+      return obj == this
+          || obj instanceof Key
+          && kind == ((Key) obj).kind
+          && names.equals(((Key) obj).names)
+          && types.equals(((Key) obj).types);
+    }
+  }
 }
 
 // End RelDataTypeFactoryImpl.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeHolder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeHolder.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeHolder.java
index 414045d..1a777ff 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeHolder.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeHolder.java
@@ -26,12 +26,15 @@ import java.util.List;
 /**
  * Holding the expandable list of fields for dynamic table.
  */
-public class RelDataTypeHolder {
-  List<RelDataTypeField> fields = new ArrayList<>();
+class RelDataTypeHolder {
+  private final List<RelDataTypeField> fields = new ArrayList<>();
+  private final RelDataTypeFactory typeFactory;
 
-  private RelDataTypeFactory typeFactory;
+  RelDataTypeHolder(RelDataTypeFactory typeFactory) {
+    this.typeFactory = typeFactory;
+  }
 
-  public List<RelDataTypeField> getFieldList(RelDataTypeFactory typeFactory) {
+  public List<RelDataTypeField> getFieldList() {
     return fields;
   }
 
@@ -43,15 +46,12 @@ public class RelDataTypeHolder {
    * Get field if exists, otherwise inserts a new field. The new field by default will have "any"
    * type, except for the dynamic star field.
    *
-   * @param typeFactory RelDataTypeFactory
    * @param fieldName Request field name
    * @param caseSensitive Case Sensitive
    * @return A pair of RelDataTypeField and Boolean. Boolean indicates whether a new field is added
    * to this holder.
    */
-  public Pair<RelDataTypeField, Boolean> getFieldOrInsert(RelDataTypeFactory typeFactory,
-      String fieldName, boolean caseSensitive) {
-
+  Pair<RelDataTypeField, Boolean> getFieldOrInsert(String fieldName, boolean caseSensitive) {
     // First check if this field name exists in our field list
     for (RelDataTypeField f : fields) {
       if (Util.matches(caseSensitive, f.getName(), fieldName)) {
@@ -75,16 +75,7 @@ public class RelDataTypeHolder {
   }
 
   public List<String> getFieldNames() {
-    List<String> fieldNames = new ArrayList<>();
-    for (RelDataTypeField f : fields) {
-      fieldNames.add(f.getName());
-    }
-
-    return fieldNames;
-  }
-
-  public void setRelDataTypeFactory(RelDataTypeFactory typeFactory) {
-    this.typeFactory = typeFactory;
+    return Pair.left(fields);
   }
 
 }

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java
index 0fc14f3..a559686 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeImpl.java
@@ -84,36 +84,6 @@ public abstract class RelDataTypeImpl
         return field;
       }
     }
-    for (RelDataTypeField field : fieldList) {
-      RelDataType dataType = field.getType();
-      if (dataType.isStruct()
-          && dataType.getStructKind() == StructKind.PEEK_FIELDS_DEFAULT) {
-        for (RelDataTypeField subField : dataType.getFieldList()) {
-          if (Util.matches(caseSensitive, subField.getName(), fieldName)) {
-            return subField;
-          }
-        }
-      }
-    }
-    RelDataTypeField matched = null;
-    for (RelDataTypeField field : fieldList) {
-      RelDataType dataType = field.getType();
-      if (dataType.isStruct()
-          && dataType.getStructKind() == StructKind.PEEK_FIELDS) {
-        for (RelDataTypeField subField : dataType.getFieldList()) {
-          if (Util.matches(caseSensitive, subField.getName(), fieldName)) {
-            if (matched == null) {
-              matched = subField;
-            } else {
-              // TODO throw columnAmbiguous
-            }
-          }
-        }
-      }
-    }
-    if (matched != null) {
-      return matched;
-    }
     if (elideRecord) {
       final List<Slot> slots = Lists.newArrayList();
       getFieldRecurse(slots, this, 0, fieldName, caseSensitive);
@@ -172,7 +142,6 @@ public abstract class RelDataTypeImpl
     }
   }
 
-  // implement RelDataType
   public List<RelDataTypeField> getFieldList() {
     assert isStruct();
     return fieldList;
@@ -182,18 +151,15 @@ public abstract class RelDataTypeImpl
     return Pair.left(fieldList);
   }
 
-  // implement RelDataType
   public int getFieldCount() {
     assert isStruct() : this;
     return fieldList.size();
   }
 
   public StructKind getStructKind() {
-    assert isStruct() : this;
-    return StructKind.FULLY_QUALIFIED;
+    return isStruct() ? StructKind.FULLY_QUALIFIED : StructKind.NONE;
   }
 
-  // implement RelDataType
   public RelDataType getComponentType() {
     // this is not a collection type
     return null;
@@ -209,13 +175,11 @@ public abstract class RelDataTypeImpl
     return null;
   }
 
-  // implement RelDataType
   public boolean isStruct() {
     return fieldList != null;
   }
 
-  // implement RelDataType
-  public boolean equals(Object obj) {
+  @Override public boolean equals(Object obj) {
     if (obj instanceof RelDataTypeImpl) {
       final RelDataTypeImpl that = (RelDataTypeImpl) obj;
       return this.digest.equals(that.digest);
@@ -223,52 +187,42 @@ public abstract class RelDataTypeImpl
     return false;
   }
 
-  // implement RelDataType
-  public int hashCode() {
+  @Override public int hashCode() {
     return digest.hashCode();
   }
 
-  // implement RelDataType
   public String getFullTypeString() {
     return digest;
   }
 
-  // implement RelDataType
   public boolean isNullable() {
     return false;
   }
 
-  // implement RelDataType
   public Charset getCharset() {
     return null;
   }
 
-  // implement RelDataType
   public SqlCollation getCollation() {
     return null;
   }
 
-  // implement RelDataType
   public SqlIntervalQualifier getIntervalQualifier() {
     return null;
   }
 
-  // implement RelDataType
   public int getPrecision() {
     return PRECISION_NOT_SPECIFIED;
   }
 
-  // implement RelDataType
   public int getScale() {
     return SCALE_NOT_SPECIFIED;
   }
 
-  // implement RelDataType
   public SqlTypeName getSqlTypeName() {
     return null;
   }
 
-  // implement RelDataType
   public SqlIdentifier getSqlIdentifier() {
     SqlTypeName typeName = getSqlTypeName();
     if (typeName == null) {
@@ -279,7 +233,6 @@ public abstract class RelDataTypeImpl
         SqlParserPos.ZERO);
   }
 
-  // implement RelDataType
   public RelDataTypeFamily getFamily() {
     // by default, put each type into its own family
     return this;
@@ -310,14 +263,12 @@ public abstract class RelDataTypeImpl
     digest = sb.toString();
   }
 
-  // implement RelDataType
-  public String toString() {
+  @Override public String toString() {
     StringBuilder sb = new StringBuilder();
     generateTypeString(sb, false);
     return sb.toString();
   }
 
-  // implement RelDataType
   public RelDataTypePrecedenceList getPrecedenceList() {
     // by default, make each type have a precedence list containing
     // only other types in the same family
@@ -336,7 +287,6 @@ public abstract class RelDataTypeImpl
     };
   }
 
-  // implement RelDataType
   public RelDataTypeComparability getComparability() {
     return RelDataTypeComparability.ALL;
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/type/RelRecordType.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelRecordType.java b/core/src/main/java/org/apache/calcite/rel/type/RelRecordType.java
index 8d09cf4..7ae4dda 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelRecordType.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelRecordType.java
@@ -19,6 +19,8 @@ package org.apache.calcite.rel.type;
 import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.sql.type.SqlTypeName;
 
+import com.google.common.base.Preconditions;
+
 import java.io.Serializable;
 import java.util.List;
 
@@ -26,37 +28,55 @@ import java.util.List;
  * RelRecordType represents a structured type having named fields.
  */
 public class RelRecordType extends RelDataTypeImpl implements Serializable {
+  /** Name resolution policy; usually {@link StructKind#FULLY_QUALIFIED}. */
+  private final StructKind kind;
+
   //~ Constructors -----------------------------------------------------------
 
   /**
    * Creates a <code>RecordType</code>. This should only be called from a
    * factory method.
    */
-  public RelRecordType(List<RelDataTypeField> fields) {
+  public RelRecordType(StructKind kind, List<RelDataTypeField> fields) {
     super(fields);
+    this.kind = Preconditions.checkNotNull(kind);
     computeDigest();
   }
 
+  public RelRecordType(List<RelDataTypeField> fields) {
+    this(StructKind.FULLY_QUALIFIED, fields);
+  }
+
   //~ Methods ----------------------------------------------------------------
 
-  // implement RelDataType
-  public SqlTypeName getSqlTypeName() {
+  @Override public SqlTypeName getSqlTypeName() {
     return SqlTypeName.ROW;
   }
 
-  // implement RelDataType
-  public boolean isNullable() {
+  @Override public boolean isNullable() {
     return false;
   }
 
-  // implement RelDataType
-  public int getPrecision() {
+  @Override public int getPrecision() {
     // REVIEW: angel 18-Aug-2005 Put in fake implementation for precision
     return 0;
   }
 
+  @Override public StructKind getStructKind() {
+    return kind;
+  }
+
   protected void generateTypeString(StringBuilder sb, boolean withDetail) {
-    sb.append("RecordType(");
+    sb.append("RecordType");
+    switch (kind) {
+    case PEEK_FIELDS:
+      sb.append(":peek");
+      break;
+    case PEEK_FIELDS_DEFAULT:
+      sb.append(":peek_default");
+      break;
+    }
+    sb.append("(");
     for (Ord<RelDataTypeField> ord : Ord.zip(fieldList)) {
       if (ord.i > 0) {
         sb.append(", ");

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/rel/type/StructKind.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/type/StructKind.java b/core/src/main/java/org/apache/calcite/rel/type/StructKind.java
new file mode 100644
index 0000000..b10f816
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/type/StructKind.java
@@ -0,0 +1,83 @@
+/*
+ * 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.type;
+
+/**
+ * Describes a policy for resolving fields in record types.
+ *
+ * <p>The usual value is {@link #FULLY_QUALIFIED}.
+ *
+ * <p>A field whose record type is labeled {@link #PEEK_FIELDS} can be omitted.
+ * In Phoenix, column families are represented by fields like this.
+ * {@link #PEEK_FIELDS_DEFAULT} is similar, but represents the default column
+ * family, so it will win in the event of a tie.
+ *
+ * <p>SQL usually disallows a record type. For instance,
+ *
+ * <blockquote>SELECT address.zip FROM Emp AS e</blockquote>
+ *
+ * is disallowed because {@code address} "looks like" a table alias. You'd have
+ * to write
+ *
+ * <blockquote>SELECT e.address.zip FROM Emp AS e</blockquote>
+ *
+ * <p>But if a table has one or more columns that are record-typed and are
+ * labeled {@link #PEEK_FIELDS} or {@link #PEEK_FIELDS_DEFAULT} we suspend that
+ * rule and would allow {@code address.zip}.
+ *
+ * <p>If there are multiple matches, we choose the one that is:
+ * <ol>
+ *   <li>Shorter. If you write {@code zipcode}, {@code address.zipcode} will
+ *   be preferred over {@code product.supplier.zipcode}.
+ *   <li>Uses as little skipping as possible. A match that is fully-qualified
+ *   will beat one that uses {@code PEEK_FIELDS_DEFAULT} at some point, which
+ *   will beat one that uses {@code PEEK_FIELDS} at some point.
+ * </ol>
+ */
+public enum StructKind {
+  /** This is not a structured type. */
+  NONE,
+
+  /** This is a traditional structured type, where each field must be
+   * referenced explicitly.
+   *
+   * <p>Also, when referencing a struct column, you
+   * need to qualify it with the table alias, per standard SQL. For instance,
+   * {@code SELECT c.address.zipcode FROM customer AS c}
+   * is valid but
+   * {@code SELECT address.zipcode FROM customer}
+   * it not valid.
+   */
+  FULLY_QUALIFIED,
+
+  /** As {@link #PEEK_FIELDS}, but takes priority if another struct-typed
+   * field also has a field of the name being sought.
+   *
+   * <p>In Phoenix, only one of a table's columns is labeled
+   * {@code PEEK_FIELDS_DEFAULT} - the default column family - but in principle
+   * there could be more than one. */
+  PEEK_FIELDS_DEFAULT,
+
+  /** If a field has this type, you can see its fields without qualifying them
+   * with the name of this field.
+   *
+   * <p>For example, if {@code address} is labeled {@code PEEK_FIELDS}, you
+   * could write {@code zipcode} as shorthand for {@code address.zipcode}. */
+  PEEK_FIELDS,
+}
+
+// End StructKind.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index b936859..f261659 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -607,6 +607,9 @@ public interface CalciteResource {
   @BaseMessage("Table ''{0}'' not found")
   ExInst<CalciteException> tableNotFound(String tableName);
 
+  @BaseMessage("Not a record type. The ''*'' operator requires a record")
+  ExInst<SqlValidatorException> starRequiresRecordType();
+
   @BaseMessage("FILTER expression must be of type BOOLEAN")
   ExInst<CalciteException> filterMustBeBoolean();
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/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 eba51b7..4ab4ef2 100644
--- a/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
+++ b/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
@@ -18,7 +18,10 @@ package org.apache.calcite.runtime;
 
 import org.apache.calcite.util.ImmutableNullableList;
 
+import com.google.common.collect.ImmutableList;
+
 import java.util.AbstractList;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
@@ -238,6 +241,17 @@ public class FlatLists {
     }
   }
 
+  /** Returns a list that consists of a given list plus an element. */
+  public static <E> List<E> append(List<E> list, E e) {
+    if (list instanceof AbstractFlatList) {
+      //noinspection unchecked
+      return ((AbstractFlatList) list).append(e);
+    }
+    final List<E> newList = new ArrayList<>(list);
+    newList.add(e);
+    return FlatLists.of(newList);
+  }
+
   /** Base class for flat lists. */
   public abstract static class AbstractFlatList<T>
       extends AbstractImmutableList<T> implements RandomAccess {
@@ -246,6 +260,9 @@ public class FlatLists {
       return Arrays.asList((T[]) toArray());
     }
 
+    /** Returns a list that consists of a this list's elements plus a given
+     * element. */
+    public abstract List<T> append(T e);
   }
 
   /**
@@ -348,6 +365,10 @@ public class FlatLists {
     public int compareTo(List o) {
       return ComparableListImpl.compare((List) this, o);
     }
+
+    public List<T> append(T e) {
+      return new Flat2List<T>(t0, e);
+    }
   }
 
   /**
@@ -469,6 +490,10 @@ public class FlatLists {
     public int compareTo(List o) {
       return ComparableListImpl.compare((List) this, o);
     }
+
+    public List<T> append(T e) {
+      return new Flat3List<T>(t0, t1, e);
+    }
   }
 
   /**
@@ -609,6 +634,10 @@ public class FlatLists {
     public int compareTo(List o) {
       return ComparableListImpl.compare((List) this, o);
     }
+
+    public List<T> append(T e) {
+      return new Flat4List<T>(t0, t1, t2, e);
+    }
   }
 
   /**
@@ -768,6 +797,10 @@ public class FlatLists {
     public int compareTo(List o) {
       return ComparableListImpl.compare((List) this, o);
     }
+
+    public List<T> append(T e) {
+      return new Flat5List<T>(t0, t1, t2, t3, e);
+    }
   }
 
   /**
@@ -946,6 +979,10 @@ public class FlatLists {
     public int compareTo(List o) {
       return ComparableListImpl.compare((List) this, o);
     }
+
+    public List<T> append(T e) {
+      return new Flat6List<T>(t0, t1, t2, t3, t4, e);
+    }
   }
 
   /**
@@ -1144,6 +1181,10 @@ public class FlatLists {
     public int compareTo(List o) {
       return ComparableListImpl.compare((List) this, o);
     }
+
+    public List<T> append(T e) {
+      return ImmutableList.of(t0, t1, t2, t3, t5, e);
+    }
   }
 
   /** Empty list that implements the {@link Comparable} interface. */

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/SqlIdentifier.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlIdentifier.java b/core/src/main/java/org/apache/calcite/sql/SqlIdentifier.java
index d9b44ba..d070150 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlIdentifier.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlIdentifier.java
@@ -30,6 +30,7 @@ import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -173,6 +174,21 @@ public class SqlIdentifier extends SqlNode {
     }
   }
 
+  /** Returns an identifier that is the same as this except with a component
+   * added at a given position. Does not modify this identifier. */
+  public SqlIdentifier add(int i, String name, SqlParserPos pos) {
+    final List<String> names2 = new ArrayList<>(names);
+    names2.add(i, name);
+    final List<SqlParserPos> pos2;
+    if (componentPositions == null) {
+      pos2 = null;
+    } else {
+      pos2 = new ArrayList<>(componentPositions);
+      pos2.add(i, pos);
+    }
+    return new SqlIdentifier(names2, collation, pos, pos2);
+  }
+
   /**
    * Returns the position of the <code>i</code>th component of a compound
    * identifier, or the position of the whole identifier if that information

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/CatalogScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/CatalogScope.java b/core/src/main/java/org/apache/calcite/sql/validate/CatalogScope.java
index c2ef43e..1420123 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/CatalogScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/CatalogScope.java
@@ -73,16 +73,6 @@ class CatalogScope extends DelegatingScope {
     throw new UnsupportedOperationException();
   }
 
-  public SqlValidatorNamespace resolve(List<String> names,
-      SqlValidatorScope[] ancestorOut, int[] offsetOut) {
-    final ImmutableList<String> nameList =
-        ImmutableList.<String>builder().addAll(this.names).addAll(names)
-            .build();
-    if (schemaNames.contains(nameList)) {
-      return new SchemaNamespace(validator, nameList);
-    }
-    return null;
-  }
 }
 
 // End CatalogScope.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java b/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
index d48bb50..491ea1d 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
@@ -19,6 +19,7 @@ package org.apache.calcite.sql.validate;
 import org.apache.calcite.rel.type.DynamicRecordType;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rel.type.StructKind;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlNode;
@@ -29,9 +30,13 @@ import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.util.Pair;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 
 import static org.apache.calcite.util.Static.RESOURCE;
 
@@ -74,11 +79,42 @@ public abstract class DelegatingScope implements SqlValidatorScope {
     throw new UnsupportedOperationException();
   }
 
-  public SqlValidatorNamespace resolve(
-      List<String> names,
-      SqlValidatorScope[] ancestorOut,
-      int[] offsetOut) {
-    return parent.resolve(names, ancestorOut, offsetOut);
+  public void resolve(List<String> names, boolean deep, Resolved resolved) {
+    parent.resolve(names, deep, resolved);
+  }
+
+  /** If a record type allows implicit references to fields, recursively looks
+   * into the fields. Otherwise returns immediately. */
+  void resolveInNamespace(SqlValidatorNamespace ns, List<String> names,
+      Path path, Resolved resolved) {
+    if (names.isEmpty()) {
+      resolved.found(ns, this, path);
+      return;
+    }
+    final RelDataType rowType = ns.getRowType();
+    if (rowType.isStruct()) {
+      final String name = names.get(0);
+      final RelDataTypeField field0 =
+          validator.catalogReader.field(rowType, name);
+      if (field0 != null) {
+        final SqlValidatorNamespace ns2 = ns.lookupChild(field0.getName());
+        final Step path2 = path.add(rowType, field0.getIndex(),
+            StructKind.FULLY_QUALIFIED);
+        resolveInNamespace(ns2, names.subList(1, names.size()), path2,
+            resolved);
+      } else {
+        for (RelDataTypeField field : rowType.getFieldList()) {
+          switch (field.getType().getStructKind()) {
+          case PEEK_FIELDS:
+          case PEEK_FIELDS_DEFAULT:
+            final Step path2 = path.add(rowType, field.getIndex(),
+                field.getType().getStructKind());
+            final SqlValidatorNamespace ns2 = ns.lookupChild(field.getName());
+            resolveInNamespace(ns2, names, path2, resolved);
+          }
+        }
+      }
+    }
   }
 
   protected void addColumnNames(
@@ -113,6 +149,11 @@ public abstract class DelegatingScope implements SqlValidatorScope {
     return parent.findQualifyingTableName(columnName, ctx);
   }
 
+  protected Map<String, SqlValidatorNamespace>
+  findQualifyingTables(String columnName) {
+    return ImmutableMap.of();
+  }
+
   public RelDataType resolveColumn(String name, SqlNode ctx) {
     return parent.resolveColumn(name, ctx);
   }
@@ -148,6 +189,7 @@ public abstract class DelegatingScope implements SqlValidatorScope {
       return SqlQualified.create(this, 1, null, identifier);
     }
 
+    final SqlIdentifier previous = identifier;
     String columnName;
     switch (identifier.names.size()) {
     case 1: {
@@ -159,63 +201,167 @@ public abstract class DelegatingScope implements SqlValidatorScope {
 
       final RelDataTypeField field =
           validator.catalogReader.field(namespace.getRowType(), columnName);
+      if (field != null) {
+        if (hasAmbiguousUnresolvedStar(namespace.getRowType(), field,
+            columnName)) {
+          throw validator.newValidationError(identifier,
+              RESOURCE.columnAmbiguous(columnName));
+        }
 
-      checkAmbiguousUnresolvedStar(namespace.getRowType(), field, identifier, columnName);
-
+        columnName = field.getName(); // use resolved field name
+      }
       // todo: do implicit collation here
       final SqlParserPos pos = identifier.getParserPosition();
-      SqlIdentifier expanded =
-          new SqlIdentifier(
-              ImmutableList.of(tableName, field.getName()),  // use resolved field name
-              null,
-              pos,
-              ImmutableList.of(SqlParserPos.ZERO, pos));
-      validator.setOriginal(expanded, identifier);
-      return SqlQualified.create(this, 1, namespace, expanded);
+      identifier =
+          new SqlIdentifier(ImmutableList.of(tableName, columnName), null,
+              pos, ImmutableList.of(SqlParserPos.ZERO, pos));
     }
-
+    // fall through
     default: {
       SqlValidatorNamespace fromNs = null;
+      Path fromPath = null;
+      final ResolvedImpl resolved = new ResolvedImpl();
       final int size = identifier.names.size();
       int i = size - 1;
       for (; i > 0; i--) {
         final SqlIdentifier prefix = identifier.getComponent(0, i);
-        fromNs = resolve(prefix.names, null, null);
-        if (fromNs != null) {
-          if (fromNs.getEnclosingNode() != null) {
-            String alias =
-                SqlValidatorUtil.getAlias(fromNs.getEnclosingNode(), -1);
-            if (alias != null
-                && !alias.equals(identifier.names.get(i - 1))) {
-              identifier = identifier.setName(i - 1, alias);
-            }
-          }
+        resolved.clear();
+        resolve(prefix.names, false, resolved);
+        if (resolved.count() == 1) {
+          final Resolve resolve = resolved.only();
+          fromNs = resolve.namespace;
+          fromPath = resolve.path;
           break;
         }
       }
       if (fromNs == null || fromNs instanceof SchemaNamespace) {
-        final SqlIdentifier prefix1 = identifier.skipLast(1);
-        throw validator.newValidationError(prefix1,
-            RESOURCE.tableNameNotFound(prefix1.toString()));
+        // Look for a column not qualified by a table alias.
+        columnName = identifier.names.get(0);
+        final Map<String, SqlValidatorNamespace> map =
+            findQualifyingTables(columnName);
+        switch (map.size()) {
+        default:
+          final SqlIdentifier prefix1 = identifier.skipLast(1);
+          throw validator.newValidationError(prefix1,
+              RESOURCE.tableNameNotFound(prefix1.toString()));
+        case 1:
+          fromPath = resolved.emptyPath();
+          fromNs = map.entrySet().iterator().next().getValue();
+        }
+
+        // Throw an error if the table was not found.
+        // If one or more of the child namespaces allows peeking
+        // (e.g. if they are Phoenix column families) then we relax the SQL
+        // standard requirement that record fields are qualified by table alias.
+        if (!hasLiberalChild()) {
+          final SqlIdentifier prefix1 = identifier.skipLast(1);
+          throw validator.newValidationError(prefix1,
+              RESOURCE.tableNameNotFound(prefix1.toString()));
+        }
+      }
+
+      // If a table alias is part of the identifier, make sure that the table
+      // alias uses the same case as it was defined. For example, in
+      //
+      //    SELECT e.empno FROM Emp as E
+      //
+      // change "e.empno" to "E.empno".
+      if (fromNs.getEnclosingNode() != null) {
+        String alias =
+            SqlValidatorUtil.getAlias(fromNs.getEnclosingNode(), -1);
+        if (alias != null
+            && i > 0
+            && !alias.equals(identifier.names.get(i - 1))) {
+          identifier = identifier.setName(i - 1, alias);
+        }
       }
       RelDataType fromRowType = fromNs.getRowType();
-      for (int j = i; j < size; j++) {
-        final SqlIdentifier last = identifier.getComponent(j);
-        columnName = last.getSimple();
-        final RelDataTypeField field =
-            validator.catalogReader.field(fromRowType, columnName);
-        if (field == null) {
-          throw validator.newValidationError(last,
-              RESOURCE.columnNotFoundInTable(columnName,
-                  identifier.getComponent(0, j).toString()));
+      if (fromPath.stepCount() > 1) {
+        for (Step p : fromPath.steps()) {
+          fromRowType = fromRowType.getFieldList().get(p.i).getType();
         }
+        ++i;
+      }
+      final SqlIdentifier suffix = identifier.getComponent(i, size);
+      resolved.clear();
+      resolveInNamespace(fromNs, suffix.names, resolved.emptyPath(), resolved);
+      final Path path;
+      switch (resolved.count()) {
+      case 0:
+        // Find the shortest suffix that also fails. Suppose we cannot resolve
+        // "a.b.c"; we find we cannot resolve "a.b" but can resolve "a". So,
+        // the error will be "Column 'a.b' not found".
+        int k = size - 1;
+        for (; k > i; --k) {
+          SqlIdentifier suffix2 = identifier.getComponent(i, k);
+          resolved.clear();
+          resolveInNamespace(fromNs, suffix2.names, resolved.emptyPath(),
+              resolved);
+          if (resolved.count() > 0) {
+            break;
+          }
+        }
+        final SqlIdentifier prefix = identifier.getComponent(0, i);
+        final SqlIdentifier suffix3 = identifier.getComponent(i, k + 1);
+        throw validator.newValidationError(suffix3,
+            RESOURCE.columnNotFoundInTable(suffix3.toString(), prefix.toString()));
+      case 1:
+        path = resolved.only().path;
+        break;
+      default:
+        final Comparator<Resolve> c =
+            new Comparator<Resolve>() {
+              public int compare(Resolve o1, Resolve o2) {
+                // Name resolution that uses fewer implicit steps wins.
+                int c = Integer.compare(worstKind(o1.path), worstKind(o2.path));
+                if (c != 0) {
+                  return c;
+                }
+                // Shorter path wins
+                return Integer.compare(o1.path.stepCount(), o2.path.stepCount());
+              }
+
+              private int worstKind(Path path) {
+                int kind = -1;
+                for (Step step : path.steps()) {
+                  kind = Math.max(kind, step.kind.ordinal());
+                }
+                return kind;
+              }
+            };
+        Collections.sort(resolved.resolves, c);
+        if (c.compare(resolved.resolves.get(0), resolved.resolves.get(1)) == 0) {
+          throw validator.newValidationError(suffix,
+              RESOURCE.columnAmbiguous(suffix.toString()));
+        }
+        path = resolved.resolves.get(0).path;
+      }
 
-        checkAmbiguousUnresolvedStar(fromRowType, field, identifier, columnName);
-
-        // normalize case to match definition, in a copy of the identifier
-        identifier = identifier.setName(j, field.getName());
-        fromRowType = field.getType();
+      // Normalize case to match definition, make elided fields explicit,
+      // and check that references to dynamic stars ("**") are unambiguous.
+      int k = i;
+      for (Step step : path.steps()) {
+        final RelDataTypeField field0 =
+            step.rowType.getFieldList().get(step.i);
+        final String fieldName = field0.getName();
+        switch (step.kind) {
+        case PEEK_FIELDS:
+        case PEEK_FIELDS_DEFAULT:
+          identifier = identifier.add(k, fieldName, SqlParserPos.ZERO);
+          break;
+        default:
+          final String name = identifier.names.get(k);
+          if (!fieldName.equals(name)) {
+            identifier = identifier.setName(k, fieldName);
+          }
+          if (hasAmbiguousUnresolvedStar(step.rowType, field0, name)) {
+            throw validator.newValidationError(identifier,
+                RESOURCE.columnAmbiguous(name));
+          }
+        }
+        ++k;
       }
+
       if (i > 1) {
         // Simplify overqualified identifiers.
         // For example, schema.emp.deptno becomes emp.deptno.
@@ -226,11 +372,19 @@ public abstract class DelegatingScope implements SqlValidatorScope {
         //   SELECT schema.emp.deptno FROM schema.emp AS e
         identifier = identifier.getComponent(i - 1, identifier.names.size());
       }
+
+      if (!previous.equals(identifier)) {
+        validator.setOriginal(identifier, previous);
+      }
       return SqlQualified.create(this, i, fromNs, identifier);
     }
     }
   }
 
+  protected boolean hasLiberalChild() {
+    return false;
+  }
+
   public void validateExpr(SqlNode expr) {
     // Do not delegate to parent. An expression valid in this scope may not
     // be valid in the parent scope.
@@ -248,27 +402,22 @@ public abstract class DelegatingScope implements SqlValidatorScope {
     return parent.getOrderList();
   }
 
-  private void checkAmbiguousUnresolvedStar(RelDataType fromRowType, RelDataTypeField field,
-      SqlIdentifier identifier, String columnName) {
-
-    if (field != null
-        && field.isDynamicStar()
+  /** Returns whether {@code rowType} contains more than one star column.
+   * Having more than one star columns implies ambiguous column. */
+  private boolean hasAmbiguousUnresolvedStar(RelDataType rowType,
+      RelDataTypeField field, String columnName) {
+    if (field.isDynamicStar()
         && !DynamicRecordType.isDynamicStarColName(columnName)) {
-      // Make sure fromRowType only contains one star column.
-      // Having more than one star columns implies ambiguous column.
       int count = 0;
-      for (RelDataTypeField possibleStar : fromRowType.getFieldList()) {
+      for (RelDataTypeField possibleStar : rowType.getFieldList()) {
         if (possibleStar.isDynamicStar()) {
-          count++;
+          if (++count > 1) {
+            return true;
+          }
         }
       }
-
-      if (count > 1) {
-        throw validator.newValidationError(identifier,
-            RESOURCE.columnAmbiguous(columnName));
-      }
     }
-
+    return false;
   }
 
   /**

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/EmptyScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/EmptyScope.java b/core/src/main/java/org/apache/calcite/sql/validate/EmptyScope.java
index 9da3bff..e7f0ff8 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/EmptyScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/EmptyScope.java
@@ -64,11 +64,7 @@ class EmptyScope implements SqlValidatorScope {
     throw new UnsupportedOperationException();
   }
 
-  public SqlValidatorNamespace resolve(
-      List<String> names,
-      SqlValidatorScope[] ancestorOut,
-      int[] offsetOut) {
-    return null;
+  public void resolve(List<String> names, boolean deep, Resolved resolved) {
   }
 
   public SqlValidatorNamespace getTableNamespace(List<String> names) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/ListScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/ListScope.java b/core/src/main/java/org/apache/calcite/sql/validate/ListScope.java
index 8567cc5..c4d292f 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/ListScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/ListScope.java
@@ -16,15 +16,21 @@
  */
 package org.apache.calcite.sql.validate;
 
+import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rel.type.StructKind;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 
+import com.google.common.collect.ImmutableList;
+
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import static org.apache.calcite.util.Static.RESOURCE;
 
@@ -39,7 +45,7 @@ public abstract class ListScope extends DelegatingScope {
    * List of child {@link SqlValidatorNamespace} objects and their names.
    */
   protected final List<Pair<String, SqlValidatorNamespace>> children =
-      new ArrayList<Pair<String, SqlValidatorNamespace>>();
+      new ArrayList<>();
 
   //~ Constructors -----------------------------------------------------------
 
@@ -63,57 +69,30 @@ public abstract class ListScope extends DelegatingScope {
     return Pair.right(children);
   }
 
-  protected SqlValidatorNamespace getChild(String alias) {
-    if (alias == null) {
-      if (children.size() != 1) {
-        throw Util.newInternal(
-            "no alias specified, but more than one table in from list");
-      }
-      return children.get(0).right;
-    } else {
-      final int i = validator.catalogReader.match(Pair.left(children), alias);
-      if (i >= 0) {
-        return children.get(i).right;
-      }
-      return null;
-    }
-  }
-
-  /** Returns a child namespace that matches a fully-qualified list of names.
-   * This will be a schema-qualified table, for example
-   *
-   * <blockquote><pre>SELECT sales.emp.empno FROM sales.emp</pre></blockquote>
-   */
-  protected SqlValidatorNamespace getChild(List<String> names) {
-    int i = findChild(names);
-    return i < 0 ? null : children.get(i).right;
-  }
-
-  protected int findChild(List<String> names) {
-    for (int i = 0; i < children.size(); i++) {
-      Pair<String, SqlValidatorNamespace> child = children.get(i);
-      if (child.left != null) {
-        if (validator.catalogReader.matches(child.left, Util.last(names))) {
-          if (names.size() == 1) {
-            return i;
-          }
-        } else {
+  private int findChild(List<String> names) {
+    for (Ord<Pair<String, SqlValidatorNamespace>> child : Ord.zip(children)) {
+      String lastName = Util.last(names);
+      if (child.e.left != null) {
+        if (!validator.catalogReader.matches(child.e.left, lastName)) {
           // Alias does not match last segment. Don't consider the
           // fully-qualified name. E.g.
           //    SELECT sales.emp.name FROM sales.emp AS otherAlias
           continue;
         }
+        if (names.size() == 1) {
+          return child.i;
+        }
       }
 
       // Look up the 2 tables independently, in case one is qualified with
       // catalog & schema and the other is not.
-      final SqlValidatorTable table = child.right.getTable();
+      final SqlValidatorTable table = child.e.right.getTable();
       if (table != null) {
         final SqlValidatorTable table2 =
             validator.catalogReader.getTable(names);
         if (table2 != null
             && table.getQualifiedName().equals(table2.getQualifiedName())) {
-          return i;
+          return child.i;
         }
       }
     }
@@ -134,47 +113,84 @@ public abstract class ListScope extends DelegatingScope {
     parent.findAliases(result);
   }
 
-  public Pair<String, SqlValidatorNamespace>
+  @Override public Pair<String, SqlValidatorNamespace>
   findQualifyingTableName(final String columnName, SqlNode ctx) {
-    int count = 0;
-    Pair<String, SqlValidatorNamespace> tableName = null;
-    for (Pair<String, SqlValidatorNamespace> child : children) {
-      final RelDataType rowType = child.right.getRowType();
-      if (validator.catalogReader.field(rowType, columnName) != null) {
-        tableName = child;
-        count++;
-      }
-    }
-    switch (count) {
+    final Map<String, SqlValidatorNamespace> map =
+        findQualifyingTables(columnName);
+    switch (map.size()) {
     case 0:
       return parent.findQualifyingTableName(columnName, ctx);
     case 1:
-      return tableName;
+      return Pair.of(map.entrySet().iterator().next());
     default:
       throw validator.newValidationError(ctx,
           RESOURCE.columnAmbiguous(columnName));
     }
   }
 
-  public SqlValidatorNamespace resolve(
-      List<String> names,
-      SqlValidatorScope[] ancestorOut,
-      int[] offsetOut) {
+  @Override public Map<String, SqlValidatorNamespace>
+  findQualifyingTables(String columnName) {
+    final Map<String, SqlValidatorNamespace> map = new HashMap<>();
+    for (Pair<String, SqlValidatorNamespace> child : children) {
+      final ResolvedImpl resolved = new ResolvedImpl();
+      resolve(ImmutableList.of(child.left, columnName), true, resolved);
+      if (resolved.count() > 0) {
+        map.put(child.getKey(), child.getValue());
+      }
+    }
+    return map;
+  }
+
+  @Override protected boolean hasLiberalChild() {
+    for (Pair<String, SqlValidatorNamespace> child : children) {
+      final RelDataType rowType = child.right.getRowType();
+      switch (rowType.getStructKind()) {
+      case PEEK_FIELDS:
+      case PEEK_FIELDS_DEFAULT:
+        return true;
+      }
+      for (RelDataTypeField field : rowType.getFieldList()) {
+        switch (field.getType().getStructKind()) {
+        case PEEK_FIELDS:
+        case PEEK_FIELDS_DEFAULT:
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override public void resolve(List<String> names, boolean deep,
+      Resolved resolved) {
     // First resolve by looking through the child namespaces.
     final int i = findChild(names);
     if (i >= 0) {
-      if (ancestorOut != null) {
-        ancestorOut[0] = this;
+      final Step path =
+          resolved.emptyPath().add(null, i, StructKind.FULLY_QUALIFIED);
+      resolved.found(children.get(i).right, this, path);
+      return;
+    }
+
+    // Recursively look deeper into the record-valued fields of the namespace,
+    // if it allows skipping fields.
+    if (deep) {
+      for (Ord<Pair<String, SqlValidatorNamespace>> child : Ord.zip(children)) {
+        // If identifier starts with table alias, remove the alias.
+        final List<String> names2 =
+            validator.catalogReader.matches(child.e.left, names.get(0))
+                ? names.subList(1, names.size())
+                : names;
+        resolveInNamespace(child.e.right, names2, resolved.emptyPath(),
+            resolved);
       }
-      if (offsetOut != null) {
-        offsetOut[0] = i;
+      if (resolved.count() > 0) {
+        return;
       }
-      return children.get(i).right;
     }
 
     // Then call the base class method, which will delegate to the
     // parent scope.
-    return parent.resolve(names, ancestorOut, offsetOut);
+    super.resolve(names, deep, resolved);
   }
 
   public RelDataType resolveColumn(String columnName, SqlNode ctx) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/ParameterScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/ParameterScope.java b/core/src/main/java/org/apache/calcite/sql/validate/ParameterScope.java
index f1a4e91..c9a0484 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/ParameterScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/ParameterScope.java
@@ -20,7 +20,6 @@ import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlIdentifier;
 
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -57,14 +56,6 @@ public class ParameterScope extends EmptyScope {
     return this;
   }
 
-  public SqlValidatorNamespace resolve(
-      List<String> names,
-      SqlValidatorScope[] ancestorOut,
-      int[] offsetOut) {
-    assert names.size() == 1;
-    final RelDataType type = nameToTypeMap.get(names.get(0));
-    return new ParameterNamespace(validator, type);
-  }
 }
 
 // End ParameterScope.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/SqlQualified.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlQualified.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlQualified.java
index 067c324..55c1d01 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlQualified.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlQualified.java
@@ -61,8 +61,11 @@ public class SqlQualified {
       return identifier.names;
     }
     final ImmutableList.Builder<String> builder = ImmutableList.builder();
+    final SqlValidatorScope.ResolvedImpl resolved =
+        new SqlValidatorScope.ResolvedImpl();
+    scope.resolve(Util.skipLast(identifier.names), false, resolved);
     SqlValidatorNamespace namespace =
-        scope.resolve(Util.skipLast(identifier.names), null, null);
+        resolved.count() == 1 ? resolved.only().namespace : null;
     builder.add(identifier.names.get(0));
     for (String name : Util.skip(identifier.names)) {
       if (namespace != null) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorCatalogReader.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorCatalogReader.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorCatalogReader.java
index b157c71..b43f85c 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorCatalogReader.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorCatalogReader.java
@@ -80,16 +80,8 @@ public interface SqlValidatorCatalogReader {
    */
   RelDataTypeField field(RelDataType rowType, String alias);
 
-  /**
-   * Finds the ordinal of a field with a given name, using the case-sensitivity
-   * of the current session.
-   */
-  int fieldOrdinal(RelDataType rowType, String alias);
-
   boolean matches(String string, String name);
 
-  int match(List<String> strings, String name);
-
   RelDataType createTypeFromProjection(RelDataType type,
       List<String> columnNameList);
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 44377d1..f4bbb79 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -445,7 +445,6 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
     switch (identifier.names.size()) {
     case 1:
       for (Pair<String, SqlValidatorNamespace> p : scope.children) {
-
         if (p.right.getRowType().isDynamicStruct()) {
           // don't expand star if the underneath table is dynamic.
           // Treat this star as a special field in validation/conversion and
@@ -482,33 +481,22 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
                 includeSystemVars);
           }
         }
-
       }
       return true;
+
     default:
       final SqlIdentifier prefixId = identifier.skipLast(1);
-      final SqlValidatorNamespace fromNs;
-      if (prefixId.names.size() == 1) {
-        String tableName = prefixId.names.get(0);
-        final SqlValidatorNamespace childNs = scope.getChild(tableName);
-        if (childNs == null) {
-          // e.g. "select r.* from e"
-          throw newValidationError(identifier.getComponent(0),
-              RESOURCE.unknownIdentifier(tableName));
-        }
-        final SqlNode from = childNs.getNode();
-        fromNs = getNamespace(from);
-      } else {
-        fromNs = scope.getChild(prefixId.names);
-        if (fromNs == null) {
-          // e.g. "select s.t.* from e"
-          throw newValidationError(prefixId,
-              RESOURCE.unknownIdentifier(prefixId.toString()));
-        }
-      }
-      assert fromNs != null;
+      final SqlValidatorScope.ResolvedImpl resolved =
+          new SqlValidatorScope.ResolvedImpl();
+      scope.resolve(prefixId.names, true, resolved);
+      if (resolved.count() == 0) {
+        // e.g. "select s.t.* from e"
+        // or "select r.* from e"
+        throw newValidationError(prefixId,
+            RESOURCE.unknownIdentifier(prefixId.toString()));
+      }
+      final SqlValidatorNamespace fromNs = resolved.only().namespace;
       final RelDataType rowType = fromNs.getRowType();
-
       if (rowType.isDynamicStruct()) {
         // don't expand star if the underneath table is dynamic.
         addToSelectList(
@@ -518,7 +506,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
             prefixId.plus(DynamicRecordType.DYNAMIC_STAR_PREFIX, startPosition),
             scope,
             includeSystemVars);
-      } else {
+      } else if (rowType.isStruct()) {
         for (RelDataTypeField field : rowType.getFieldList()) {
           String columnName = field.getName();
 
@@ -531,8 +519,9 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
               scope,
               includeSystemVars);
         }
+      } else {
+        throw newValidationError(prefixId, RESOURCE.starRequiresRecordType());
       }
-
       return true;
     }
   }
@@ -701,7 +690,12 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       SqlValidatorNamespace ns = null;
       for (String name : subNames) {
         if (ns == null) {
-          ns = scope.resolve(ImmutableList.of(name), null, null);
+          final SqlValidatorScope.ResolvedImpl resolved =
+              new SqlValidatorScope.ResolvedImpl();
+          scope.resolve(ImmutableList.of(name), false, resolved);
+          if (resolved.count() == 1) {
+            ns = resolved.only().namespace;
+          }
         } else {
           ns = ns.lookupChild(name);
         }
@@ -932,10 +926,11 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       final SqlValidatorScope parentScope =
           ((DelegatingScope) scope).getParent();
       if (id.isSimple()) {
-        SqlValidatorNamespace ns =
-            parentScope.resolve(id.names, null, null);
-        if (ns != null) {
-          return ns;
+        final SqlValidatorScope.ResolvedImpl resolved =
+            new SqlValidatorScope.ResolvedImpl();
+        parentScope.resolve(id.names, false, resolved);
+        if (resolved.count() == 1) {
+          return resolved.only().namespace;
         }
       }
     }
@@ -4439,7 +4434,6 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       }
 
       // Resolve the longest prefix of id that we can
-      SqlValidatorNamespace resolvedNs;
       int i;
       for (i = id.names.size() - 1; i > 0; i--) {
         // REVIEW jvs 9-June-2005: The name resolution rules used
@@ -4450,10 +4444,16 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
         // we could do a better job if they were looked up via
         // resolveColumn.
 
-        resolvedNs = scope.resolve(id.names.subList(0, i), null, null);
-        if (resolvedNs != null) {
+        final SqlValidatorScope.ResolvedImpl resolved =
+            new SqlValidatorScope.ResolvedImpl();
+        scope.resolve(id.names.subList(0, i), false, resolved);
+        if (resolved.count() == 1) {
           // There's a namespace with the name we seek.
-          type = resolvedNs.getRowType();
+          final SqlValidatorScope.Resolve resolve = resolved.only();
+          type = resolve.namespace.getRowType();
+          for (SqlValidatorScope.Step p : Util.skip(resolve.path.steps())) {
+            type = type.getFieldList().get(p.i).getType();
+          }
           break;
         }
       }
@@ -4525,9 +4525,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
   private static class Expander extends SqlScopedShuttle {
     private final SqlValidatorImpl validator;
 
-    public Expander(
-        SqlValidatorImpl validator,
-        SqlValidatorScope scope) {
+    Expander(SqlValidatorImpl validator, SqlValidatorScope scope) {
       super(scope);
       this.validator = validator;
     }
@@ -4549,7 +4547,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       // SqlIdentifier "col_name" would be resolved to a dynamic star field in dynTable's rowType.
       // Expand such SqlIdentifier to ITEM operator.
       if (DynamicRecordType.isDynamicStarColName(Util.last(fqId.names))
-        && !DynamicRecordType.isDynamicStarColName(Util.last(id.names))) {
+          && !DynamicRecordType.isDynamicStarColName(Util.last(id.names))) {
         SqlNode[] inputs = new SqlNode[2];
         inputs[0] = fqId;
         inputs[1] = SqlLiteral.createCharString(

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorScope.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorScope.java
index ee72ffb..c30b9ff 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorScope.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.sql.validate;
 
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.StructKind;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlNode;
@@ -24,6 +25,11 @@ import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlWindow;
 import org.apache.calcite.util.Pair;
 
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
@@ -53,14 +59,11 @@ public interface SqlValidatorScope {
   /**
    * Looks up a node with a given name. Returns null if none is found.
    *
-   * @param names       Name of node to find
-   * @param ancestorOut If not null, writes the ancestor scope here
-   * @param offsetOut   If not null, writes the offset within the ancestor here
+   * @param names       Name of node to find, maybe partially or fully qualified
+   * @param deep        Whether to look more than one level deep
+   * @param resolved    Callback wherein to write the match(es) we find
    */
-  SqlValidatorNamespace resolve(
-      List<String> names,
-      SqlValidatorScope[] ancestorOut,
-      int[] offsetOut);
+  void resolve(List<String> names, boolean deep, Resolved resolved);
 
   /**
    * Finds the table alias which is implicitly qualifying an unqualified
@@ -172,6 +175,109 @@ public interface SqlValidatorScope {
   /** Converts the type of an expression to nullable, if the context
    * warrants it. */
   RelDataType nullifyType(SqlNode node, RelDataType type);
+
+  /** Callback from
+   * {@link SqlValidatorScope#resolve(List, boolean, Resolved)}. */
+  interface Resolved {
+    void found(SqlValidatorNamespace namespace, SqlValidatorScope scope,
+        Path path);
+    int count();
+    Path emptyPath();
+  }
+
+  /** A sequence of steps by which an identifier was resolved. */
+  abstract class Path {
+    /** Creates a path which consists of this path plus one additional step. */
+    Step add(RelDataType rowType, int i, StructKind kind) {
+      return new Step(this, rowType, i, kind);
+    }
+
+    /** Number of steps in this path. */
+    public int stepCount() {
+      return 0;
+    }
+
+    /** Returns the steps in this path. */
+    public List<Step> steps() {
+      ImmutableList.Builder<Step> paths = new ImmutableList.Builder<>();
+      build(paths);
+      return paths.build();
+    }
+
+    protected void build(ImmutableList.Builder<Step> paths) {
+    }
+  }
+
+  /** A path that has no steps. */
+  class EmptyPath extends Path {
+  }
+
+  /** A step in resolving an identifier. */
+  class Step extends Path {
+    final Path parent;
+    final RelDataType rowType;
+    public final int i;
+    final StructKind kind;
+
+    Step(Path parent, RelDataType rowType, int i, StructKind kind) {
+      this.parent = Preconditions.checkNotNull(parent);
+      this.rowType = rowType; // may be null
+      this.i = i;
+      this.kind = Preconditions.checkNotNull(kind);
+    }
+
+    @Override public int stepCount() {
+      return 1 + parent.stepCount();
+    }
+
+    protected void build(ImmutableList.Builder<Step> paths) {
+      parent.build(paths);
+      paths.add(this);
+    }
+  }
+
+  /** Default implementation of
+   * {@link org.apache.calcite.sql.validate.SqlValidatorScope.Resolved}. */
+  class ResolvedImpl implements Resolved {
+    final List<Resolve> resolves = new ArrayList<>();
+    private final EmptyPath emptyPath = new EmptyPath();
+
+    public void found(SqlValidatorNamespace namespace, SqlValidatorScope scope, Path path) {
+      resolves.add(new Resolve(namespace, scope, path));
+    }
+
+    public int count() {
+      return resolves.size();
+    }
+
+    public Path emptyPath() {
+      return emptyPath;
+    }
+
+    public Resolve only() {
+      return Iterables.getOnlyElement(resolves);
+    }
+
+    /** Resets all state. */
+    public void clear() {
+      resolves.clear();
+    }
+
+  }
+
+  /** A match found when looking up a name. */
+  class Resolve {
+    public final SqlValidatorNamespace namespace;
+    public final SqlValidatorScope scope;
+    public final Path path;
+
+    Resolve(SqlValidatorNamespace namespace, SqlValidatorScope scope,
+        Path path) {
+      this.namespace = Preconditions.checkNotNull(namespace);
+      this.scope = scope;
+      this.path = path;
+    }
+  }
 }
 
 // End SqlValidatorScope.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
index 249f678..249f5f5 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
@@ -109,14 +109,13 @@ public class SqlValidatorUtil {
    * Looks up a field with a given name, returning null if not found.
    *
    * @param caseSensitive Whether match is case-sensitive
-   * @param elideRecord Whether to find fields nested within records
    * @param rowType    Row type
    * @param columnName Field name
    * @return Field, or null if not found
    */
   public static RelDataTypeField lookupField(boolean caseSensitive,
-      boolean elideRecord, final RelDataType rowType, String columnName) {
-    return rowType.getField(columnName, caseSensitive, elideRecord);
+      final RelDataType rowType, String columnName) {
+    return rowType.getField(columnName, caseSensitive, false);
   }
 
   public static void checkCharsetAndCollateConsistentIfCharType(
@@ -445,16 +444,15 @@ public class SqlValidatorUtil {
       SqlValidatorScope scope,
       List<String> names) {
     assert names.size() > 0;
-    SqlValidatorNamespace namespace = null;
-    for (int i = 0; i < names.size(); i++) {
-      String name = names.get(i);
-      if (i == 0) {
-        namespace = scope.resolve(ImmutableList.of(name), null, null);
-      } else {
-        namespace = namespace.lookupChild(name);
-      }
+    final SqlValidatorScope.ResolvedImpl resolved =
+        new SqlValidatorScope.ResolvedImpl();
+    scope.resolve(ImmutableList.of(names.get(0)), false, resolved);
+    assert resolved.count() == 1;
+    SqlValidatorNamespace namespace = resolved.only().namespace;
+    for (String name : Util.skip(names)) {
+      namespace = namespace.lookupChild(name);
+      assert namespace != null;
     }
-    assert namespace != null;
     return namespace;
   }
 
@@ -524,14 +522,14 @@ public class SqlValidatorUtil {
 
   public static RelDataType createTypeFromProjection(RelDataType type,
       List<String> columnNameList, RelDataTypeFactory typeFactory,
-      boolean caseSensitive, boolean elideRecord) {
+      boolean caseSensitive) {
     // If the names in columnNameList and type have case-sensitive differences,
     // the resulting type will use those from type. These are presumably more
     // canonical.
     final List<RelDataTypeField> fields =
         new ArrayList<>(columnNameList.size());
     for (String name : columnNameList) {
-      RelDataTypeField field = type.getField(name, caseSensitive, elideRecord);
+      RelDataTypeField field = type.getField(name, caseSensitive, false);
       fields.add(type.getFieldList().get(field.getIndex()));
     }
     return typeFactory.createStructType(fields);
@@ -657,26 +655,24 @@ public class SqlValidatorUtil {
       String originalRelName = expr.names.get(0);
       String originalFieldName = expr.names.get(1);
 
-      int[] nsIndexes = {-1};
-      final SqlValidatorScope[] ancestorScopes = {null};
-      SqlValidatorNamespace foundNs =
-          scope.resolve(
-              ImmutableList.of(originalRelName),
-              ancestorScopes,
-              nsIndexes);
+      final SqlValidatorScope.ResolvedImpl resolved =
+          new SqlValidatorScope.ResolvedImpl();
+      scope.resolve(ImmutableList.of(originalRelName), false, resolved);
 
-      assert foundNs != null;
-      assert nsIndexes.length == 1;
-      int childNamespaceIndex = nsIndexes[0];
+      assert resolved.count() == 1;
+      final SqlValidatorScope.Resolve resolve = resolved.only();
+      final SqlValidatorNamespace foundNs = resolve.namespace;
+      final int childNamespaceIndex = resolve.path.steps().get(0).i;
 
       int namespaceOffset = 0;
 
       if (childNamespaceIndex > 0) {
         // If not the first child, need to figure out the width of
         // output types from all the preceding namespaces
-        assert ancestorScopes[0] instanceof ListScope;
+        final SqlValidatorScope ancestorScope = resolve.scope;
+        assert ancestorScope instanceof ListScope;
         List<SqlValidatorNamespace> children =
-            ((ListScope) ancestorScopes[0]).getChildren();
+            ((ListScope) ancestorScope).getChildren();
 
         for (int j = 0; j < childNamespaceIndex; j++) {
           namespaceOffset +=

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/sql/validate/WithScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/WithScope.java b/core/src/main/java/org/apache/calcite/sql/validate/WithScope.java
index 9141f83..42c8b99 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/WithScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/WithScope.java
@@ -16,6 +16,7 @@
  */
 package org.apache.calcite.sql.validate;
 
+import org.apache.calcite.rel.type.StructKind;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlWithItem;
 
@@ -53,14 +54,16 @@ class WithScope extends ListScope {
     return super.getTableNamespace(names);
   }
 
-  @Override public SqlValidatorNamespace resolve(List<String> names,
-      SqlValidatorScope[] ancestorOut,
-      int[] offsetOut) {
+  @Override public void resolve(List<String> names, boolean deep, Resolved resolved) {
     if (names.size() == 1
-        && names.get(0).equals(withItem.name.getSimple())) {
-      return validator.getNamespace(withItem);
+        && names.equals(withItem.name.names)) {
+      final SqlValidatorNamespace ns = validator.getNamespace(withItem);
+      final Step path = resolved.emptyPath()
+          .add(ns.getRowType(), 0, StructKind.FULLY_QUALIFIED);
+      resolved.found(ns, null, path);
+      return;
     }
-    return super.resolve(names, ancestorOut, offsetOut);
+    super.resolve(names, deep, resolved);
   }
 }
 


[4/6] calcite git commit: Complete [CALCITE-1208] by giving "resolve" a call-back, so it can make multiple matches

Posted by jh...@apache.org.
http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/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 0efdaf2..d380c1b 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -2203,20 +2203,15 @@ public class SqlToRelConverter {
       String originalRelName = lookup.getOriginalRelName();
       String originalFieldName = fieldAccess.getField().getName();
 
-      int[] nsIndexes = {-1};
-      final SqlValidatorScope[] ancestorScopes = {null};
-      SqlValidatorNamespace foundNs =
-          lookup.bb.scope.resolve(
-              ImmutableList.of(originalRelName),
-              ancestorScopes,
-              nsIndexes);
-
-      assert foundNs != null;
-      assert nsIndexes.length == 1;
-
-      int childNamespaceIndex = nsIndexes[0];
-
-      SqlValidatorScope ancestorScope = ancestorScopes[0];
+      SqlValidatorScope.ResolvedImpl resolved =
+          new SqlValidatorScope.ResolvedImpl();
+      lookup.bb.scope.resolve(ImmutableList.of(originalRelName), false,
+          resolved);
+      assert resolved.count() == 1;
+      final SqlValidatorScope.Resolve resolve = resolved.only();
+      final SqlValidatorNamespace foundNs = resolve.namespace;
+      final int childNamespaceIndex = resolve.path.steps().get(0).i;
+      final SqlValidatorScope ancestorScope = resolve.scope;
       boolean correlInCurrentScope = ancestorScope == bb.scope;
 
       if (!correlInCurrentScope) {
@@ -2309,18 +2304,12 @@ public class SqlToRelConverter {
       DeferredLookup lookup = mapCorrelToDeferred.get(correlName);
       String originalRelName = lookup.getOriginalRelName();
 
-      int[] nsIndexes = {-1};
-      final SqlValidatorScope[] ancestorScopes = {null};
-      SqlValidatorNamespace foundNs =
-          lookup.bb.scope.resolve(
-              ImmutableList.of(originalRelName),
-              ancestorScopes,
-              nsIndexes);
-
-      assert foundNs != null;
-      assert nsIndexes.length == 1;
+      final SqlValidatorScope.ResolvedImpl resolved =
+          new SqlValidatorScope.ResolvedImpl();
+      lookup.bb.scope.resolve(ImmutableList.of(originalRelName), false,
+          resolved);
 
-      SqlValidatorScope ancestorScope = ancestorScopes[0];
+      SqlValidatorScope ancestorScope = resolved.only().scope;
 
       // If the correlated reference is in a scope that's "above" the
       // subquery, then this is a correlated subquery.
@@ -3900,24 +3889,24 @@ public class SqlToRelConverter {
         }
         return Pair.of(node, null);
       }
-      int[] offsets = {-1};
-      final SqlValidatorScope[] ancestorScopes = {null};
-      SqlValidatorNamespace foundNs =
-          scope.resolve(qualified.prefix(), ancestorScopes, offsets);
-      if (foundNs == null) {
+      final SqlValidatorScope.ResolvedImpl resolved =
+          new SqlValidatorScope.ResolvedImpl();
+      scope.resolve(qualified.prefix(), false, resolved);
+      if (!(resolved.count() == 1)) {
         return null;
       }
+      final SqlValidatorScope.Resolve resolve = resolved.only();
+      final SqlValidatorNamespace foundNs = resolve.namespace;
 
       // Found in current query's from list.  Find which from item.
       // We assume that the order of the from clause items has been
       // preserved.
-      SqlValidatorScope ancestorScope = ancestorScopes[0];
+      final SqlValidatorScope ancestorScope = resolve.scope;
       boolean isParent = ancestorScope != scope;
       if ((inputs != null) && !isParent) {
-        int offset = offsets[0];
         final LookupContext rels =
             new LookupContext(this, inputs, systemFieldList.size());
-        final RexNode node = lookup(offset, rels);
+        final RexNode node = lookup(resolve.path.steps().get(0).i, rels);
         if (node == null) {
           return null;
         } else {
@@ -3932,20 +3921,20 @@ public class SqlToRelConverter {
             new DeferredLookup(this, qualified.identifier.names.get(0));
         final CorrelationId correlId = cluster.createCorrel();
         mapCorrelToDeferred.put(correlId, lookup);
-        if (offsets[0] < 0) {
+        if (resolve.path.steps().get(0).i < 0) {
           return Pair.of(rexBuilder.makeCorrel(foundNs.getRowType(), correlId),
               null);
         } else {
           final RelDataTypeFactory.FieldInfoBuilder builder =
               typeFactory.builder();
-          final ListScope ancestorScope1 = (ListScope) ancestorScopes[0];
+          final ListScope ancestorScope1 = (ListScope) resolve.scope;
           final ImmutableMap.Builder<String, Integer> fields =
               ImmutableMap.builder();
           int i = 0;
           int offset = 0;
           for (SqlValidatorNamespace c : ancestorScope1.getChildren()) {
             builder.addAll(c.getRowType().getFieldList());
-            if (i == offsets[0]) {
+            if (i == resolve.path.steps().get(0).i) {
               for (RelDataTypeField field : c.getRowType().getFieldList()) {
                 fields.put(field.getName(), field.getIndex() + offset);
               }

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java b/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java
index 78b3dbe..90cbdb0 100644
--- a/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java
+++ b/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java
@@ -223,8 +223,12 @@ public class ImmutableIntList extends FlatLists.AbstractFlatList<Integer> {
     return -1;
   }
 
+  @Override public ImmutableIntList append(Integer e) {
+    return append((int) e);
+  }
+
   /** Returns a copy of this list with one element added. */
-  public ImmutableIntList add(int element) {
+  public ImmutableIntList append(int element) {
     if (ints.length == 0) {
       return of(element);
     }

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/java/org/apache/calcite/util/Pair.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/Pair.java b/core/src/main/java/org/apache/calcite/util/Pair.java
index a3d509a..bb2521e 100644
--- a/core/src/main/java/org/apache/calcite/util/Pair.java
+++ b/core/src/main/java/org/apache/calcite/util/Pair.java
@@ -73,6 +73,11 @@ public class Pair<T1, T2>
     return new Pair<>(left, right);
   }
 
+  /** Creates a {@code Pair} from a {@link java.util.Map.Entry}. */
+  public static <K, V> Pair<K, V> of(Map.Entry<K, V> entry) {
+    return of(entry.getKey(), entry.getValue());
+  }
+
   //~ Methods ----------------------------------------------------------------
 
   public boolean equals(Object obj) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
----------------------------------------------------------------------
diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index bab7a4a..729fa13 100644
--- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -198,6 +198,7 @@ ModifiableViewMustBeBasedOnSingleTable=Modifiable view must be based on a single
 MoreThanOneMappedColumn=View is not modifiable. More than one expression maps to column ''{0}'' of base table ''{1}''
 NoValueSuppliedForViewColumn=View is not modifiable. No value is supplied for NOT NULL column ''{0}'' of base table ''{1}''
 TableNotFound=Table ''{0}'' not found
+StarRequiresRecordType=Not a record type. The ''*'' operator requires a record
 FilterMustBeBoolean=FILTER expression must be of type BOOLEAN
 CannotStreamResultsForNonStreamingInputs=Cannot stream results of a query with no streaming inputs: ''{0}''. At least one input should be convertible to a stream
 SelectMissingFrom=SELECT must have a FROM clause

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 7a3ea1a..a65f797 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -2126,6 +2126,11 @@ public class SqlParserTest {
                 + "FROM `EMP`");
   }
 
+  @Test public void testTableStarColumnFails() {
+    sql("select emp.*^.^xx from emp")
+        .fails("(?s).*Encountered \".\" .*");
+  }
+
   @Test public void testNotExists() {
     check(
         "select * from dept where not not exists (select * from emp) and true",
@@ -2639,6 +2644,16 @@ public class SqlParserTest {
             + "FROM `EMP`");
   }
 
+  @Test public void testCompoundStar() {
+    final String sql = "select sales.emp.address.zipcode,\n"
+        + " sales.emp.address.*\n"
+        + "from sales.emp";
+    final String expected = "SELECT `SALES`.`EMP`.`ADDRESS`.`ZIPCODE`,"
+        + " `SALES`.`EMP`.`ADDRESS`.*\n"
+        + "FROM `SALES`.`EMP`";
+    sql(sql).ok(expected);
+  }
+
   @Test public void testSelectDistinct() {
     check(
         "select distinct foo from bar",

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
index 2a3c02f..03200f0 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
@@ -81,7 +81,9 @@ public class SqlAdvisorTest extends SqlValidatorTestCase {
   private static final List<String> SCHEMAS =
       Arrays.asList(
           "CATALOG(CATALOG)",
+          "SCHEMA(CATALOG.DYNAMIC)",
           "SCHEMA(CATALOG.SALES)",
+          "SCHEMA(CATALOG.STRUCT)",
           "SCHEMA(CATALOG.CUSTOMER)");
 
   private static final List<String> AB_TABLES =

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java b/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
index d61b440..23719d4 100644
--- a/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
+++ b/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
@@ -34,15 +34,14 @@ import org.apache.calcite.rel.logical.LogicalProject;
 import org.apache.calcite.rel.logical.LogicalTableScan;
 import org.apache.calcite.rel.type.DynamicRecordTypeImpl;
 import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataType.StructKind;
 import org.apache.calcite.rel.type.RelDataTypeComparability;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.rel.type.RelDataTypeFactory.FieldInfoBuilder;
 import org.apache.calcite.rel.type.RelDataTypeFamily;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.RelDataTypeFieldImpl;
 import org.apache.calcite.rel.type.RelDataTypePrecedenceList;
 import org.apache.calcite.rel.type.RelRecordType;
+import org.apache.calcite.rel.type.StructKind;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexNode;
@@ -115,7 +114,6 @@ public class MockCatalogReader implements Prepare.CatalogReader {
 
   protected final RelDataTypeFactory typeFactory;
   private final boolean caseSensitive;
-  private final boolean elideRecord = true;
   private final Map<List<String>, MockTable> tables;
   protected final Map<String, MockSchema> schemas;
   private RelDataType addressType;
@@ -165,7 +163,16 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     final RelDataType booleanType =
         typeFactory.createSqlType(SqlTypeName.BOOLEAN);
     final RelDataType rectilinearCoordType =
-        typeFactory.builder().add("X", intType).add("Y", intType).build();
+        typeFactory.builder()
+            .add("X", intType)
+            .add("Y", intType)
+            .build();
+    final RelDataType rectilinearPeekCoordType =
+        typeFactory.builder()
+            .add("X", intType)
+            .add("Y", intType)
+            .kind(StructKind.PEEK_FIELDS)
+            .build();
     final RelDataType empRecordType =
         typeFactory.builder()
             .add("EMPNO", intType)
@@ -183,8 +190,8 @@ public class MockCatalogReader implements Prepare.CatalogReader {
             Arrays.asList(
                 new RelDataTypeFieldImpl("STREET", 0, varchar20Type),
                 new RelDataTypeFieldImpl("CITY", 1, varchar20Type),
-                new RelDataTypeFieldImpl("ZIP", 1, intType),
-                new RelDataTypeFieldImpl("STATE", 1, varchar20Type)),
+                new RelDataTypeFieldImpl("ZIP", 2, intType),
+                new RelDataTypeFieldImpl("STATE", 3, varchar20Type)),
             RelDataTypeComparability.NONE);
 
     // Register "SALES" schema.
@@ -235,7 +242,8 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     registerTable(deptNestedTable);
 
     // Register "BONUS" table.
-    MockTable bonusTable = MockTable.create(this, salesSchema, "BONUS", false, 0);
+    MockTable bonusTable =
+        MockTable.create(this, salesSchema, "BONUS", false, 0);
     bonusTable.addColumn("ENAME", varchar20Type);
     bonusTable.addColumn("JOB", varchar10Type);
     bonusTable.addColumn("SAL", intType);
@@ -243,7 +251,8 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     registerTable(bonusTable);
 
     // Register "SALGRADE" table.
-    MockTable salgradeTable = MockTable.create(this, salesSchema, "SALGRADE", false, 5);
+    MockTable salgradeTable =
+        MockTable.create(this, salesSchema, "SALGRADE", false, 5);
     salgradeTable.addColumn("GRADE", intType);
     salgradeTable.addColumn("LOSAL", intType);
     salgradeTable.addColumn("HISAL", intType);
@@ -257,6 +266,20 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     contactAddressTable.addColumn("MAILING_ADDRESS", addressType);
     registerTable(contactAddressTable);
 
+    // Register "DYNAMIC" schema.
+    MockSchema dynamicSchema = new MockSchema("DYNAMIC");
+    registerSchema(dynamicSchema);
+
+    MockTable nationTable =
+        new MockDynamicTable(this, dynamicSchema.getCatalogName(),
+            dynamicSchema.getName(), "NATION", false, 100);
+    registerTable(nationTable);
+
+    MockTable customerTable =
+        new MockDynamicTable(this, dynamicSchema.getCatalogName(),
+            dynamicSchema.getName(), "CUSTOMER", false, 100);
+    registerTable(customerTable);
+
     // Register "CUSTOMER" schema.
     MockSchema customerSchema = new MockSchema("CUSTOMER");
     registerSchema(customerSchema);
@@ -271,6 +294,16 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     contactTable.addColumn("COORD", rectilinearCoordType);
     registerTable(contactTable);
 
+    // Register "CONTACT_PEEK" table. The
+    MockTable contactPeekTable =
+        MockTable.create(this, customerSchema, "CONTACT_PEEK", false, 1000);
+    contactPeekTable.addColumn("CONTACTNO", intType);
+    contactPeekTable.addColumn("FNAME", varchar10Type);
+    contactPeekTable.addColumn("LNAME", varchar10Type);
+    contactPeekTable.addColumn("EMAIL", varchar20Type);
+    contactPeekTable.addColumn("COORD", rectilinearPeekCoordType);
+    registerTable(contactPeekTable);
+
     // Register "ACCOUNT" table.
     MockTable accountTable = MockTable.create(this, customerSchema, "ACCOUNT",
         false, 457);
@@ -428,28 +461,28 @@ public class MockCatalogReader implements Prepare.CatalogReader {
 
     MockSchema structTypeSchema = new MockSchema("STRUCT");
     registerSchema(structTypeSchema);
-    MockTable structTypeTable = MockTable.create(
-        this, structTypeSchema, "T", false, 100);
+    MockTable structTypeTable = MockTable.create(this, structTypeSchema, "T",
+        false, 100);
     structTypeTable.addColumn("K0", varchar20Type);
     structTypeTable.addColumn("C1", varchar20Type);
-    FieldInfoBuilder builder = typeFactory.builder();
-    builder.add("C0", intType);
-    builder.add("C1", intType);
-    RelDataType f0Type = new DelegateStructType(
-        typeFactory.createStructType(builder), StructKind.PEEK_FIELDS_DEFAULT);
+    final RelDataType f0Type = typeFactory.builder()
+        .add("C0", intType)
+        .add("C1", intType)
+        .kind(StructKind.PEEK_FIELDS_DEFAULT)
+        .build();
     structTypeTable.addColumn("F0", f0Type);
-    builder = typeFactory.builder();
-    builder.add("C0", intTypeNull);
-    builder.add("C2", intType);
-    builder.add("A0", intType);
-    RelDataType f1Type = new DelegateStructType(
-        typeFactory.createStructType(builder), StructKind.PEEK_FIELDS);
+    final RelDataType f1Type = typeFactory.builder()
+        .add("C0", intTypeNull)
+        .add("C2", intType)
+        .add("A0", intType)
+        .kind(StructKind.PEEK_FIELDS)
+        .build();
     structTypeTable.addColumn("F1", f1Type);
-    builder = typeFactory.builder();
-    builder.add("C3", intType);
-    builder.add("A0", booleanType);
-    RelDataType f2Type = new DelegateStructType(
-        typeFactory.createStructType(builder), StructKind.PEEK_FIELDS);
+    final RelDataType f2Type = typeFactory.builder()
+        .add("C3", intType)
+        .add("A0", booleanType)
+        .kind(StructKind.PEEK_FIELDS)
+        .build();
     structTypeTable.addColumn("F2", f2Type);
     registerTable(structTypeTable);
     return this;
@@ -562,27 +595,17 @@ public class MockCatalogReader implements Prepare.CatalogReader {
   }
 
   public RelDataTypeField field(RelDataType rowType, String alias) {
-    return SqlValidatorUtil.lookupField(caseSensitive, elideRecord, rowType,
-        alias);
-  }
-
-  public int fieldOrdinal(RelDataType rowType, String alias) {
-    final RelDataTypeField field = field(rowType, alias);
-    return field != null ? field.getIndex() : -1;
+    return SqlValidatorUtil.lookupField(caseSensitive, rowType, alias);
   }
 
   public boolean matches(String string, String name) {
     return Util.matches(caseSensitive, string, name);
   }
 
-  public int match(List<String> strings, String name) {
-    return Util.findMatch(strings, name, caseSensitive);
-  }
-
   public RelDataType createTypeFromProjection(final RelDataType type,
       final List<String> columnNameList) {
     return SqlValidatorUtil.createTypeFromProjection(type, columnNameList,
-        typeFactory, caseSensitive, elideRecord);
+        typeFactory, caseSensitive);
   }
 
   private static List<RelCollation> deduceMonotonicity(
@@ -646,6 +669,7 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     private List<RelCollation> collationList;
     protected final List<String> names;
     private final Set<String> monotonicColumnSet = Sets.newHashSet();
+    private StructKind kind = StructKind.FULLY_QUALIFIED;
 
     public MockTable(MockCatalogReader catalogReader, String catalogName,
         String schemaName, String name, boolean stream, double rowCount) {
@@ -732,7 +756,8 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     }
 
     public void onRegister(RelDataTypeFactory typeFactory) {
-      rowType = typeFactory.createStructType(columnList);
+      rowType = typeFactory.createStructType(kind, Pair.right(columnList),
+          Pair.left(columnList));
       collationList = deduceMonotonicity(this);
     }
 
@@ -771,6 +796,14 @@ public class MockCatalogReader implements Prepare.CatalogReader {
       table.onRegister(catalogReader.typeFactory);
       return table;
     }
+
+    public void setKind(StructKind kind) {
+      this.kind = kind;
+    }
+
+    public StructKind getKind() {
+      return kind;
+    }
   }
 
   /**
@@ -778,13 +811,13 @@ public class MockCatalogReader implements Prepare.CatalogReader {
    * {@link org.apache.calcite.prepare.Prepare.PreparingTable} with dynamic record type.
    */
   public static class MockDynamicTable extends MockTable {
-    public MockDynamicTable(MockCatalogReader catalogReader, String catalogName,
+    MockDynamicTable(MockCatalogReader catalogReader, String catalogName,
         String schemaName, String name, boolean stream, double rowCount) {
       super(catalogReader, catalogName, schemaName, name, stream, rowCount);
     }
 
     public void onRegister(RelDataTypeFactory typeFactory) {
-      rowType =  new DynamicRecordTypeImpl(typeFactory);
+      rowType = new DynamicRecordTypeImpl(typeFactory);
     }
 
     /**
@@ -799,6 +832,7 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     }
   }
 
+  /** Struct type based on another struct type. */
   private static class DelegateStructType implements RelDataType {
     private RelDataType delegate;
     private StructKind structKind;
@@ -809,92 +843,92 @@ public class MockCatalogReader implements Prepare.CatalogReader {
       this.structKind = structKind;
     }
 
-    @Override public boolean isStruct() {
+    public boolean isStruct() {
       return delegate.isStruct();
     }
 
-    @Override public boolean isDynamicStruct() {
+    public boolean isDynamicStruct() {
       return delegate.isDynamicStruct();
     }
 
-    @Override public List<RelDataTypeField> getFieldList() {
+    public List<RelDataTypeField> getFieldList() {
       return delegate.getFieldList();
     }
 
-    @Override public List<String> getFieldNames() {
+    public List<String> getFieldNames() {
       return delegate.getFieldNames();
     }
 
-    @Override public int getFieldCount() {
+    public int getFieldCount() {
       return delegate.getFieldCount();
     }
 
-    @Override public StructKind getStructKind() {
+    public StructKind getStructKind() {
       return structKind;
     }
 
-    @Override public RelDataTypeField getField(String fieldName,
-        boolean caseSensitive, boolean elideRecord) {
+    public RelDataTypeField getField(String fieldName, boolean caseSensitive,
+        boolean elideRecord) {
       return delegate.getField(fieldName, caseSensitive, elideRecord);
     }
 
-    @Override public boolean isNullable() {
+    public boolean isNullable() {
       return delegate.isNullable();
     }
 
-    @Override public RelDataType getComponentType() {
+    public RelDataType getComponentType() {
       return delegate.getComponentType();
     }
 
-    @Override public RelDataType getKeyType() {
+    public RelDataType getKeyType() {
       return delegate.getKeyType();
     }
 
-    @Override public RelDataType getValueType() {
+    public RelDataType getValueType() {
       return delegate.getValueType();
     }
 
-    @Override public Charset getCharset() {
+    public Charset getCharset() {
       return delegate.getCharset();
     }
 
-    @Override public SqlCollation getCollation() {
+    public SqlCollation getCollation() {
       return delegate.getCollation();
     }
 
-    @Override public SqlIntervalQualifier getIntervalQualifier() {
+    public SqlIntervalQualifier getIntervalQualifier() {
       return delegate.getIntervalQualifier();
     }
 
-    @Override public int getPrecision() {
+    public int getPrecision() {
       return delegate.getPrecision();
     }
 
-    @Override public int getScale() {
+    public int getScale() {
       return delegate.getScale();
     }
 
-    @Override public SqlTypeName getSqlTypeName() {
+    public SqlTypeName getSqlTypeName() {
       return delegate.getSqlTypeName();
     }
 
-    @Override public SqlIdentifier getSqlIdentifier() {
+    public SqlIdentifier getSqlIdentifier() {
       return delegate.getSqlIdentifier();
     }
 
-    @Override public String getFullTypeString() {
+    public String getFullTypeString() {
       return delegate.getFullTypeString();
     }
 
-    @Override public RelDataTypeFamily getFamily() {
+    public RelDataTypeFamily getFamily() {
       return delegate.getFamily();
     }
 
-    @Override public RelDataTypePrecedenceList getPrecedenceList() {
+    public RelDataTypePrecedenceList getPrecedenceList() {
       return delegate.getPrecedenceList();
     }
 
-    @Override public RelDataTypeComparability getComparability() {
+    public RelDataTypeComparability getComparability() {
       return delegate.getComparability();
     }
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 7ec5087..9953355 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -1721,7 +1721,7 @@ public class SqlToRelConverterTest extends SqlToRelTestBase {
   }
 
   @Test public void testStructType() {
-    sql("select * from struct.t").convertsTo("");
+    sql("select * from struct.t").convertsTo("${plan}");
   }
 
   /**
@@ -1908,6 +1908,14 @@ public class SqlToRelConverterTest extends SqlToRelTestBase {
         });
   }
 
+  @Test public void testUnionInFrom() {
+    final String sql = "select x0, x1 from (\n"
+        + "  select 'a' as x0, 'a' as x1, 'a' as x2 from emp\n"
+        + "  union all\n"
+        + "  select 'bb' as x0, 'bb' as x1, 'bb' as x2 from dept)";
+    sql(sql).ok();
+  }
+
   /**
    * Visitor that checks that every {@link RelNode} in a tree is valid.
    *

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index c48b528..e1afc68 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -4615,9 +4615,10 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
     check("select emp.* from emp");
 
     // Error message could be better (EMPNO does exist, but it's a column).
-    checkFails(
-        "select ^empno^ .  * from emp",
-        "Unknown identifier 'EMPNO'");
+    sql("select ^empno^ .  * from emp")
+        .fails("Not a record type. The '\\*' operator requires a record");
+    sql("select ^emp.empno^ .  * from emp")
+        .fails("Not a record type. The '\\*' operator requires a record");
   }
 
   /**
@@ -4636,10 +4637,10 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
   }
 
   @Test public void testNonLocalStar() {
-    // MySQL allows this but we can't, currently
+    // MySQL allows this, and now so do we
     sql("select * from emp e where exists (\n"
-        + "  select ^e^.* from dept where dept.deptno = e.deptno)")
-        .fails("Unknown identifier 'E'");
+        + "  select e.* from dept where dept.deptno = e.deptno)")
+        .type(EMP_RECORD_TYPE);
   }
 
   /**
@@ -4656,10 +4657,9 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
   }
 
   @Test public void testStarDotIdFails() {
-    // Parser allows a star inside (not at end of) compound identifier, but
-    // validator does not
+    // Fails in parser
     sql("select emp.^*^.foo from emp")
-        .fails("Column '\\*' not found in table 'EMP'");
+        .fails("(?s).*Encountered \".\" at .*");
     // Parser does not allow star dot identifier.
     sql("select ^*^.foo from emp")
         .fails("(?s).*Encountered \".\" at .*");
@@ -6950,6 +6950,21 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         "DISTINCT/ALL not allowed with COALESCE function");
   }
 
+  @Test public void testColumnNotFound() {
+    sql("select ^b0^ from sales.emp")
+        .fails("Column 'B0' not found in any table");
+  }
+
+  @Test public void testColumnNotFound2() {
+    sql("select ^b0^ from sales.emp, sales.dept")
+        .fails("Column 'B0' not found in any table");
+  }
+
+  @Test public void testColumnNotFound3() {
+    sql("select e.^b0^ from sales.emp as e")
+        .fails("Column 'B0' not found in table 'E'");
+  }
+
   @Test public void testSelectDistinct() {
     check("SELECT DISTINCT deptno FROM emp");
     check("SELECT DISTINCT deptno, sal FROM emp");
@@ -7151,14 +7166,28 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
    * <a href="https://issues.apache.org/jira/browse/CALCITE-497">[CALCITE-497]
    * Support optional qualifier for column name references</a>. */
   @Test public void testRecordTypeElided() {
-    checkResultType(
-        "SELECT contact.x, contact.coord.y FROM customer.contact",
-        "RecordType(INTEGER NOT NULL X, INTEGER NOT NULL Y) NOT NULL");
+    sql("SELECT contact.^x^, contact.coord.y FROM customer.contact")
+        .fails("Column 'X' not found in table 'CONTACT'");
+
+    // Fully qualified works.
+    sql("SELECT contact.coord.x, contact.coord.y FROM customer.contact")
+        .type("RecordType(INTEGER NOT NULL X, INTEGER NOT NULL Y) NOT NULL");
+
+    // Because the type of CONTACT_PEEK.COORD is marked "peek", the validator
+    // can see through it.
+    sql("SELECT c.x, c.coord.y FROM customer.contact_peek as c")
+        .type("RecordType(INTEGER NOT NULL X, INTEGER NOT NULL Y) NOT NULL");
+    sql("SELECT c.coord.x, c.coord.y FROM customer.contact_peek as c")
+        .type("RecordType(INTEGER NOT NULL X, INTEGER NOT NULL Y) NOT NULL");
+    sql("SELECT x, c.coord.y FROM customer.contact_peek as c")
+        .type("RecordType(INTEGER NOT NULL X, INTEGER NOT NULL Y) NOT NULL");
 
     // Qualifying with schema is OK.
-    checkResultType(
-        "SELECT customer.contact.x, customer.contact.email, contact.coord.y FROM customer.contact",
-        "RecordType(INTEGER NOT NULL X, VARCHAR(20) NOT NULL EMAIL, INTEGER NOT NULL Y) NOT NULL");
+    final String sql = "SELECT customer.contact_peek.x,\n"
+        + " customer.contact_peek.email, contact_peek.coord.y\n"
+        + "FROM customer.contact_peek";
+    sql(sql).type("RecordType(INTEGER NOT NULL X, VARCHAR(20) NOT NULL EMAIL,"
+        + " INTEGER NOT NULL Y) NOT NULL");
   }
 
   @Test public void testSample() {
@@ -8131,112 +8160,195 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         .fails(cannotStreamResultsForNonStreamingInputs("PRODUCTS, SUPPLIERS"));
   }
 
-  @Test public void testNew() {
+  @Test public void testDummy() {
     // (To debug individual statements, paste them into this method.)
-    //            1         2         3         4         5         6
-    //   12345678901234567890123456789012345678901234567890123456789012345
-    //        check("SELECT count(0) FROM emp GROUP BY ()");
   }
 
   @Test public void testStructType() {
-    // Table STRUCT.T is defined as:
-    // (K0 VARCHAR(20) NOT NULL, C1 VARCHAR(20) NOT NULL,
-    //   RecordType(C0 INTEGER NOT NULL, C1 INTEGER NOT NULL) F0,
-    //   RecordType(C0 INTEGER, C2 INTEGER NOT NULL, A0 INTEGER NOT NULL) F1,
-    //   RecordType(C3 INTEGER NOT NULL, A0 BOOLEAN NOT NULL) F2)
-    // , where F0 has a default struct priority.
+    // Table STRUCT.T is defined as: (
+    //   K0 VARCHAR(20) NOT NULL,
+    //   C1 VARCHAR(20) NOT NULL,
+    //   RecordType:PEEK_FIELDS_DEFAULT(
+    //     C0 INTEGER NOT NULL,
+    //     C1 INTEGER NOT NULL) F0,
+    //   RecordType:PEEK_FIELDS(
+    //      C0 INTEGER,
+    //      C2 INTEGER NOT NULL,
+    //      A0 INTEGER NOT NULL) F1,
+    //   RecordType:PEEK_FIELDS(
+    //      C3 INTEGER NOT NULL,
+    //      A0 BOOLEAN NOT NULL) F2)
+    //
+    // The labels 'PEEK_FIELDS_DEFAULT' and 'PEEK_FIELDS' mean that F0, F1 and
+    // F2 can all be transparent. F0 has default struct priority; F1 and F2 have
+    // lower priority.
 
-    check("select * from struct.t");
+    sql("select * from struct.t").ok();
 
     // Resolve K0 as top-level column K0.
-    checkResultType("select k0 from struct.t",
-        "RecordType(VARCHAR(20) NOT NULL K0) NOT NULL");
+    sql("select k0 from struct.t")
+        .type("RecordType(VARCHAR(20) NOT NULL K0) NOT NULL");
 
     // Resolve C2 as secondary-level column F1.C2.
-    checkResultType("select c2 from struct.t",
-        "RecordType(INTEGER NOT NULL C2) NOT NULL");
+    sql("select c2 from struct.t")
+        .type("RecordType(INTEGER NOT NULL C2) NOT NULL");
 
     // Resolve F1.C2 as fully qualified column F1.C2.
-    checkResultType("select c2 from struct.t",
-        "RecordType(INTEGER NOT NULL C2) NOT NULL");
+    sql("select f1.c2 from struct.t")
+        .type("RecordType(INTEGER NOT NULL C2) NOT NULL");
 
     // Resolve C1 as top-level column C1 as opposed to F0.C1.
-    checkResultType("select c1 from struct.t",
-        "RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+    sql("select c1 from struct.t")
+        .type("RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
 
-    // Resolve C0 as secondary-level column F0.C0 as opposed to F1.C0, since F0 has the
-    // default priority.
-    checkResultType("select c0 from struct.t",
-        "RecordType(INTEGER NOT NULL C0) NOT NULL");
+    // Resolve C0 as secondary-level column F0.C0 as opposed to F1.C0, since F0
+    // has the default priority.
+    sql("select c0 from struct.t")
+        .type("RecordType(INTEGER NOT NULL C0) NOT NULL");
 
-    // Resolve F1.C0 as fully qualified column F1.C0.
-    checkResultType("select f1.c0 from struct.t",
-        "RecordType(INTEGER C0) NOT NULL");
+    // Resolve F1.C0 as fully qualified column F1.C0 (as evidenced by "INTEGER"
+    // rather than "INTEGER NOT NULL")
+    sql("select f1.c0 from struct.t")
+        .type("RecordType(INTEGER C0) NOT NULL");
 
-    // Fail ambiguous column reference A0, since F1.A0 and F2.A0 both exist with the
-    // same resolving priority.
-    checkFails("select ^a0^ from struct.t",
-        "Column 'A0' is ambiguous");
+    // Fail ambiguous column reference A0, since F1.A0 and F2.A0 both exist with
+    // the same resolving priority.
+    sql("select ^a0^ from struct.t")
+        .fails("Column 'A0' is ambiguous");
 
     // Resolve F2.A0 as fully qualified column F2.A0.
-    checkResultType("select f2.a0 from struct.t",
-        "RecordType(BOOLEAN NOT NULL C0) NOT NULL");
+    sql("select f2.a0 from struct.t")
+        .type("RecordType(BOOLEAN NOT NULL A0) NOT NULL");
 
-    // Resolve T0.K0 as top-level column K0, since T0 is recognized as the table alias.
-    checkResultType("select t0.k0 from struct.t t0",
-        "RecordType(VARCHAR(20) NOT NULL K0) NOT NULL");
+    // Resolve T0.K0 as top-level column K0, since T0 is recognized as the table
+    // alias.
+    sql("select t0.k0 from struct.t t0")
+        .type("RecordType(VARCHAR(20) NOT NULL K0) NOT NULL");
+
+    // Resolve T0.C2 as secondary-level column F1.C2, since T0 is recognized as
+    // the table alias here.
+    sql("select t0.c2 from struct.t t0")
+        .type("RecordType(INTEGER NOT NULL C2) NOT NULL");
+
+    // Resolve F0.C2 as secondary-level column F1.C2, since F0 is recognized as
+    // the table alias here.
+    sql("select f0.c2 from struct.t f0")
+        .type("RecordType(INTEGER NOT NULL C2) NOT NULL");
+
+    // Resolve F0.C1 as top-level column C1 as opposed to F0.C1, since F0 is
+    // recognized as the table alias here.
+    sql("select f0.c1 from struct.t f0")
+        .type("RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+
+    // Resolve C1 as inner INTEGER column not top-level VARCHAR column.
+    sql("select f0.f0.c1 from struct.t f0")
+        .type("RecordType(INTEGER NOT NULL C1) NOT NULL");
+
+    // Resolve T.C1 as top-level column C1 as opposed to F0.C1, since T is
+    // recognized as the table name.
+    sql("select t.c1 from struct.t")
+        .type("RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+
+    // Alias "f0" obscures table name "t"
+    sql("select ^t^.c1 from struct.t f0")
+        .fails("Table 'T' not found");
+
+    // Resolve STRUCT.T.C1 as top-level column C1 as opposed to F0.C1, since
+    // STRUCT.T is recognized as the schema and table name.
+    sql("select struct.t.c1 from struct.t")
+        .type("RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+
+    // Table alias "f0" obscures table name "struct.t"
+    sql("select ^struct.t^.c1 from struct.t f0")
+        .fails("Table 'STRUCT.T' not found");
+
+    // Resolve F0.F0.C1 as secondary-level column F0.C1, since the first F0 is
+    // recognized as the table alias here.
+    sql("select f0.f0.c1 from struct.t f0")
+        .type("RecordType(INTEGER NOT NULL C1) NOT NULL");
+
+    // Resolve T.F0.C1 as secondary-level column F0.C1, since T is recognized as
+    // the table name.
+    sql("select t.f0.c1 from struct.t")
+        .type("RecordType(INTEGER NOT NULL C1) NOT NULL");
 
-    // Resolve T0.C2 as secondary-level column F1.C2, since T0 is recognized as the
-    // table alias here.
-    checkResultType("select t0.c2 from struct.t t0",
-        "RecordType(INTEGER NOT NULL C2) NOT NULL");
+    // Table alias obscures
+    sql("select ^t.f0^.c1 from struct.t f0")
+        .fails("Table 'T.F0' not found");
 
-    // Resolve F0.C2 as secondary-level column F1.C2, since F0 is recognized as the
-    // table alias here.
-    checkResultType("select f0.c2 from struct.t f0",
-        "RecordType(INTEGER NOT NULL C2) NOT NULL");
+    // Resolve STRUCT.T.F0.C1 as secondary-level column F0.C1, since STRUCT.T is
+    // recognized as the schema and table name.
+    sql("select struct.t.f0.c1 from struct.t")
+        .type("RecordType(INTEGER NOT NULL C1) NOT NULL");
 
-    // Resolve F0.C1 as top-level column C1 as opposed to F0.C1, since F0 is recognized
-    // as the table alias here.
-    checkResultType("select f0.c1 from struct.t f0",
-        "RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+    // Table alias "f0" obscures table name "struct.t"
+    sql("select ^struct.t.f0^.c1 from struct.t f0")
+        .fails("Table 'STRUCT.T.F0' not found");
 
-    // Resolve T.C1 as top-level column C1 as opposed to F0.C1, since T is recognized as
-    // the table name.
-    checkResultType("select t.c1 from struct.t f0",
-        "RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+    // Resolve struct type F1 with wildcard.
+    sql("select f1.* from struct.t")
+        .type("RecordType(INTEGER C0, INTEGER NOT NULL C2,"
+            + " INTEGER NOT NULL A0) NOT NULL");
 
-    // Resolve STRUCT.T.C1 as top-level column C1 as opposed to F0.C1, since STRUCT.T is
-    // recognized as the schema and table name.
-    checkResultType("select struct.t.c1 from struct.t f0",
-        "RecordType(VARCHAR(20) NOT NULL C1) NOT NULL");
+    // Fail non-existent column B0.
+    sql("select ^b0^ from struct.t")
+        .fails("Column 'B0' not found in any table");
 
-    // Resolve F0.F0.C1 as secondary-level column F0.C1, since the first F0 is recognized
-    // as the table alias here.
-    checkResultType("select f0.f0.c1 from struct.t f0",
-        "RecordType(INTEGER NOT NULL C1) NOT NULL");
+    // It's OK to reference a record type.
+    //
+    // This is admittedly a bit strange for Phoenix users. We model a column
+    // family as a column whose type is a record, but Phoenix users would
+    // rarely if ever want to use a column family as a record.
+    sql("select f1 from struct.t")
+        .type("RecordType(RecordType:peek(INTEGER C0, INTEGER NOT NULL C2,"
+            + " INTEGER NOT NULL A0) NOT NULL F1) NOT NULL");
+
+    // If we fail to find a column, give an error based on the shortest prefix
+    // that fails.
+    sql("select t.^f0.notFound^.a.b.c.d from struct.t")
+        .fails("Column 'F0\\.NOTFOUND' not found in table 'T'");
+    sql("select t.^f0.notFound^ from struct.t")
+        .fails("Column 'F0\\.NOTFOUND' not found in table 'T'");
+    sql("select t.^f0.c1.notFound^ from struct.t")
+        .fails("Column 'F0\\.C1\\.NOTFOUND' not found in table 'T'");
+  }
 
-    // Resolve T.F0.C1 as secondary-level column F0.C1, since T is recognized as the table
-    // name.
-    checkResultType("select t.f0.c1 from struct.t f0",
-        "RecordType(INTEGER NOT NULL C1) NOT NULL");
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-1150">[CALCITE-1150]
+   * Dynamic Table / Dynamic Star support</a>. */
+  @Test public void testAmbiguousDynamicStar() throws Exception {
+    final String sql = "select ^n_nation^\n"
+        + "from (select * from \"DYNAMIC\".NATION),\n"
+        + " (select * from \"DYNAMIC\".CUSTOMER)";
+    sql(sql).fails("Column 'N_NATION' is ambiguous");
+  }
 
-    // Resolve STRUCT.T.F0.C1 as secondary-level column F0.C1, since STRUCT.T is
-    // recognized as the schema and table name.
-    checkResultType("select struct.t.f0.c1 from struct.t f0",
-        "RecordType(INTEGER NOT NULL C1) NOT NULL");
+  @Test public void testAmbiguousDynamicStar2() throws Exception {
+    final String sql = "select ^n_nation^\n"
+        + "from (select * from \"DYNAMIC\".NATION, \"DYNAMIC\".CUSTOMER)";
+    sql(sql).fails("Column 'N_NATION' is ambiguous");
+  }
 
-    // Resolve struct type F1 with wildcard.
-    checkResultType("select f1.* from struct.t",
-        "RecordType(INTEGER NOT NULL C0, INTEGER NOT NULL C2) NOT NULL");
+  @Test public void testAmbiguousDynamicStar3() throws Exception {
+    final String sql = "select ^nc.n_nation^\n"
+        + "from (select * from \"DYNAMIC\".NATION, \"DYNAMIC\".CUSTOMER) as nc";
+    sql(sql).fails("Column 'N_NATION' is ambiguous");
+  }
 
-    // Fail non-existent column B0.
-    checkFails("select ^b0^ from struct.t",
-        "Column 'B0' not found in any table");
+  @Test public void testAmbiguousDynamicStar4() throws Exception {
+    final String sql = "select n.n_nation\n"
+        + "from (select * from \"DYNAMIC\".NATION) as n,\n"
+        + " (select * from \"DYNAMIC\".CUSTOMER)";
+    sql(sql).type("RecordType(ANY N_NATION) NOT NULL");
+  }
 
-    // Fail struct type with no wildcard.
-    checkFails("select ^f1^ from struct.t",
-        "Unknown identifier 'F1'");
+  /** When resolve column reference, regular field has higher priority than
+   * dynamic star columns. */
+  @Test public void testDynamicStar2() throws Exception {
+    final String sql = "select newid from (\n"
+        + "  select *, NATION.N_NATION + 100 as newid\n"
+        + "  from \"DYNAMIC\".NATION, \"DYNAMIC\".CUSTOMER)";
+    sql(sql).type("RecordType(ANY NEWID) NOT NULL");
   }
 }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
----------------------------------------------------------------------
diff --git a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index 6713fd4..3c29e67 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -2469,6 +2469,19 @@ LogicalTableModify(table=[[CATALOG, SALES, EMP]], operation=[INSERT], updateColu
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testStructType">
+        <Resource name="sql">
+            <![CDATA[select * from struct.t]]>
+        </Resource>
+        <Resource name="plan">
+            <![CDATA[
+LogicalProject(K0=[$0], C1=[$1], F0=[$2], F1=[$3], F2=[$4])
+  LogicalProject(K0=[$0], C1=[$1], F0=[$2], F03=[$3], F1=[$4], F15=[$5], F16=[$6], F2=[$7], F28=[$8])
+    LogicalProject(K0=[$0], C1=[$1], C0=[$2.C0], C13=[$2.C1], C04=[$3.C0], C2=[$3.C2], A0=[$3.A0], C3=[$4.C3], A08=[$4.A0])
+      LogicalTableScan(table=[[CATALOG, STRUCT, T]])
+]]>
+        </Resource>
+    </TestCase>
     <TestCase name="testWindowAggWithGroupBy">
         <Resource name="sql">
             <![CDATA[select min(deptno), rank() over (order by empno),
@@ -3073,6 +3086,24 @@ LogicalAggregate(group=[{}], EXPR$0=[MIN($0)])
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testUnionInFrom">
+        <Resource name="sql">
+            <![CDATA[select x0, x1 from (
+  select 'a' as x0, 'a' as x1, 'a' as x2 from emp
+  union all
+  select 'bb' as x0, 'bb' as x1, 'bb' as x2 from dept)]]>
+        </Resource>
+        <Resource name="plan">
+            <![CDATA[
+LogicalProject(X0=[$0], X1=[$1])
+  LogicalUnion(all=[true])
+    LogicalProject(X0=['a'], X1=['a'], X2=['a'])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalProject(X0=['bb'], X1=['bb'], X2=['bb'])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+        </Resource>
+    </TestCase>
     <TestCase name="testWhereInCorrelated">
         <Resource name="sql">
             <![CDATA[select empno from emp as e

http://git-wip-us.apache.org/repos/asf/calcite/blob/0938c7b6/splunk/src/main/java/org/apache/calcite/adapter/splunk/SplunkPushDownRule.java
----------------------------------------------------------------------
diff --git a/splunk/src/main/java/org/apache/calcite/adapter/splunk/SplunkPushDownRule.java b/splunk/src/main/java/org/apache/calcite/adapter/splunk/SplunkPushDownRule.java
index 31e73d8..fdf5b9d 100644
--- a/splunk/src/main/java/org/apache/calcite/adapter/splunk/SplunkPushDownRule.java
+++ b/splunk/src/main/java/org/apache/calcite/adapter/splunk/SplunkPushDownRule.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.adapter.splunk;
 
 import org.apache.calcite.adapter.splunk.util.StringUtils;
+import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
@@ -25,7 +26,6 @@ import org.apache.calcite.rel.logical.LogicalFilter;
 import org.apache.calcite.rel.logical.LogicalProject;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
-import org.apache.calcite.rel.type.RelRecordType;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
@@ -189,6 +189,7 @@ public class SplunkPushDownRule
       LogicalProject bottomProj,
       RelDataType topRow,
       RelDataType bottomRow) {
+    final RelOptCluster cluster = splunkRel.getCluster();
     StringBuilder updateSearchStr = new StringBuilder(splunkRel.search);
 
     if (!toAppend.isEmpty()) {
@@ -237,7 +238,7 @@ public class SplunkPushDownRule
         if (!bottomFields.get(rif.getIndex()).getName()
             .equals(topFields.get(i).getName())) {
           renames.add(
-              new Pair<String, String>(
+              Pair.of(
                   bottomFields.get(rif.getIndex()).getName(),
                   topFields.get(i).getName()));
           field = topFields.get(i);
@@ -254,12 +255,13 @@ public class SplunkPushDownRule
       }
     }
 
-    RelDataType resultType = new RelRecordType(newFields);
+    RelDataType resultType =
+        cluster.getTypeFactory().createStructType(newFields);
     String searchWithFilter = updateSearchStr.toString();
 
     RelNode rel =
         new SplunkTableScan(
-            splunkRel.getCluster(),
+            cluster,
             splunkRel.getTable(),
             splunkRel.splunkTable,
             searchWithFilter,
@@ -267,8 +269,7 @@ public class SplunkPushDownRule
             splunkRel.latest,
             resultType.getFieldNames());
 
-    LOGGER.debug(
-        "end of appendSearchString fieldNames: {}",
+    LOGGER.debug("end of appendSearchString fieldNames: {}",
         rel.getRowType().getFieldNames());
     return rel;
   }


[2/6] calcite git commit: [CALCITE-1095] NOT precedence

Posted by jh...@apache.org.
[CALCITE-1095] NOT precedence

Previously prefix operators such as NOT were not handled in the same
way as infix and postfix operators. With a new precedence climbing
parser, we can handle all operators uniformly.

Document precedence of all operators, adjusting some to match the SQL
standard; for example, LIKE now has slightly higher precedence than =,
and NOT has lower precedence than =.

Add a test to ensure that operators' real rankings match the documentation.


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

Branch: refs/heads/master
Commit: 339ac13eb23b5aac6cadc16c42e36deebbcd1e39
Parents: f9724c2
Author: Julian Hyde <jh...@apache.org>
Authored: Thu Feb 18 11:14:52 2016 -0800
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Sep 15 17:02:42 2016 -0700

----------------------------------------------------------------------
 core/src/main/codegen/templates/Parser.jj       |  69 +--
 .../apache/calcite/sql/SqlPrefixOperator.java   |   2 +-
 .../apache/calcite/sql/SqlSpecialOperator.java  |  40 +-
 .../calcite/sql/fun/SqlBetweenOperator.java     |  42 +-
 .../apache/calcite/sql/fun/SqlInOperator.java   |   2 +-
 .../apache/calcite/sql/fun/SqlItemOperator.java |  19 +-
 .../apache/calcite/sql/fun/SqlLikeOperator.java |  22 +-
 .../calcite/sql/fun/SqlStdOperatorTable.java    |  26 +-
 .../calcite/sql/parser/SqlParserUtil.java       | 387 ++++++++--------
 .../calcite/util/PrecedenceClimbingParser.java  | 447 +++++++++++++++++++
 .../main/java/org/apache/calcite/util/Util.java |   2 +-
 .../calcite/sql/parser/SqlParserTest.java       | 104 +++--
 .../apache/calcite/sql/test/SqlTesterImpl.java  |  26 +-
 .../org/apache/calcite/test/CalciteSuite.java   |   2 +
 .../apache/calcite/test/SqlValidatorTest.java   | 181 ++++++++
 .../util/PrecedenceClimbingParserTest.java      | 181 ++++++++
 core/src/test/resources/sql/operator.iq         |  69 +++
 site/_docs/reference.md                         |  18 +
 18 files changed, 1310 insertions(+), 329 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/codegen/templates/Parser.jj
----------------------------------------------------------------------
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 50f762c..43d6cd5 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -2556,13 +2556,30 @@ SqlNode Expression(ExprContext exprContext) :
 
 // TODO jvs 15-Nov-2003:  ANY/ALL
 
+void Expression2b(ExprContext exprContext, List<Object> list) :
+{
+    SqlNode e;
+    SqlOperator op;
+}
+{
+    (
+        op = PrefixRowOperator() {
+            checkNonQueryExpression(exprContext);
+            list.add(new SqlParserUtil.ToTreeListItem(op, getPos()));
+        }
+    )*
+    e = Expression3(exprContext) {
+        list.add(e);
+    }
+}
+
 /**
  * Parses a binary row expression, or a parenthesized expression of any
  * kind.
  *
  * <p>The result is as a flat list of operators and operands. The top-level
  * call to get an expression should call {@link #Expression}, but lower-level
- * calls should call this, to give the parser the opos to associate
+ * calls should call this, to give the parser the opportunity to associate
  * operator calls.
  *
  * <p>For example 'a = b like c = d' should come out '((a = b) like c) = d'
@@ -2572,17 +2589,15 @@ SqlNode Expression(ExprContext exprContext) :
  */
 List<Object> Expression2(ExprContext exprContext) :
 {
-    List<Object> list, list2;
+    final List<Object> list = new ArrayList();
+    List<Object> list2;
     SqlNodeList nodeList;
     SqlNode e;
     SqlOperator op;
-    SqlParserPos pos;
+    SqlParserPos pos = getPos();
 }
 {
-    e = Expression3(exprContext)
-    {
-        list = startList(e);
-    }
+    Expression2b(exprContext, list)
     (
         (
             LOOKAHEAD(2)
@@ -2599,7 +2614,7 @@ List<Object> Expression2(ExprContext exprContext) :
                         op = SqlStdOperatorTable.NOT_IN;
                         pos = getPos();
                     }
-                    |
+                |
                     <IN>
                     {
                         op = SqlStdOperatorTable.IN;
@@ -2622,7 +2637,7 @@ List<Object> Expression2(ExprContext exprContext) :
                         list.add(nodeList);
                     }
                 }
-                |
+            |
                 LOOKAHEAD(2)
                 {
                     checkNonQueryExpression(exprContext);
@@ -2635,10 +2650,10 @@ List<Object> Expression2(ExprContext exprContext) :
                     }
                     [
                         <SYMMETRIC> { op = SqlStdOperatorTable.SYMMETRIC_NOT_BETWEEN; }
-                        |
+                    |
                         <ASYMMETRIC>
                     ]
-                    |
+                |
                     <BETWEEN>
                     {
                         op = SqlStdOperatorTable.BETWEEN;
@@ -2646,7 +2661,7 @@ List<Object> Expression2(ExprContext exprContext) :
                     }
                     [
                         <SYMMETRIC> { op = SqlStdOperatorTable.SYMMETRIC_BETWEEN; }
-                        |
+                    |
                         <ASYMMETRIC>
                     ]
                 )
@@ -2655,7 +2670,7 @@ List<Object> Expression2(ExprContext exprContext) :
                     list.add(new SqlParserUtil.ToTreeListItem(op, pos));
                     list.add(e);
                 }
-                |
+            |
                 {
                     checkNonQueryExpression(exprContext);
                     pos = getPos();
@@ -2664,12 +2679,12 @@ List<Object> Expression2(ExprContext exprContext) :
                     <NOT>
                     (
                         <LIKE> { op = SqlStdOperatorTable.NOT_LIKE; }
-                        |
+                    |
                         <SIMILAR> <TO> { op = SqlStdOperatorTable.NOT_SIMILAR_TO; }
                     )
-                    |
+                |
                     <LIKE> { op = SqlStdOperatorTable.LIKE; }
-                    |
+                |
                     <SIMILAR> <TO> { op = SqlStdOperatorTable.SIMILAR_TO; }
                 )
                 list2 = Expression2(ExprContext.ACCEPT_SUBQUERY)
@@ -2688,17 +2703,14 @@ List<Object> Expression2(ExprContext exprContext) :
                         list.add(e);
                     }
                 ]
-                |
+            |
                 LOOKAHEAD(3) op = BinaryRowOperator()
                 {
                     checkNonQueryExpression(exprContext);
-                }
-                e = Expression3(ExprContext.ACCEPT_SUBQUERY)
-                {
                     list.add(new SqlParserUtil.ToTreeListItem(op, getPos()));
-                    list.add(e);
                 }
-                |
+                Expression2b(ExprContext.ACCEPT_SUBQUERY, list)
+            |
                 <LBRACKET>
                 e = Expression(ExprContext.ACCEPT_SUBQUERY)
                 <RBRACKET>
@@ -2708,7 +2720,7 @@ List<Object> Expression2(ExprContext exprContext) :
                             SqlStdOperatorTable.ITEM, getPos()));
                     list.add(e);
                 }
-                |
+            |
                 {
                     checkNonQueryExpression(exprContext);
                 }
@@ -2766,17 +2778,6 @@ SqlNode Expression3(ExprContext exprContext) :
         return SqlStdOperatorTable.ROW.createCall(list);
     }
     |
-    op = PrefixRowOperator()
-    {
-        prefixRowOpPos = getPos();
-        checkNonQueryExpression(exprContext);
-    }
-    e = Expression3(ExprContext.ACCEPT_SUBQUERY)
-    {
-        SqlParserPos callPos = prefixRowOpPos.plus(e.getParserPosition());
-        return op.createCall(callPos, e);
-    }
-    |
     {
         pos = getPos();
     }

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java
index 11eeb26..af44dd2 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java
@@ -42,7 +42,7 @@ public class SqlPrefixOperator extends SqlOperator {
     super(
         name,
         kind,
-        leftPrec(0, true),
+        leftPrec(prec, true),
         rightPrec(prec, true),
         returnTypeInference,
         operandTypeInference,

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java
index 85da998..83049ac 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java
@@ -16,12 +16,14 @@
  */
 package org.apache.calcite.sql;
 
+import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlOperandTypeChecker;
 import org.apache.calcite.sql.type.SqlOperandTypeInference;
 import org.apache.calcite.sql.type.SqlReturnTypeInference;
+import org.apache.calcite.util.PrecedenceClimbingParser;
 import org.apache.calcite.util.Util;
 
-import java.util.List;
+import com.google.common.base.Predicate;
 
 /**
  * Generic operator for nodes with special syntax.
@@ -81,11 +83,43 @@ public class SqlSpecialOperator extends SqlOperator {
    *     {@link SqlNode}
    * @return ordinal of the node which replaced the expression
    */
-  public int reduceExpr(
+  public ReduceResult reduceExpr(
       int ordinal,
-      List<Object> list) {
+      TokenSequence list) {
     throw Util.needToImplement(this);
   }
+
+  /** List of tokens: the input to a parser. Every token is either an operator
+   * ({@link SqlOperator}) or an expression ({@link SqlNode}), and every token
+   * has a position. */
+  public interface TokenSequence {
+    int size();
+    SqlOperator op(int i);
+    SqlParserPos pos(int i);
+    boolean isOp(int i);
+    SqlNode node(int i);
+    void replaceSublist(int start, int end, SqlNode e);
+
+    /** Creates a parser whose token sequence is a copy of a subset of this
+     * token sequence. */
+    PrecedenceClimbingParser parser(int start,
+        Predicate<PrecedenceClimbingParser.Token> predicate);
+  }
+
+  /** Result of applying
+   * {@link org.apache.calcite.util.PrecedenceClimbingParser.Special#apply}.
+   * Tells the caller which range of tokens to replace, and with what. */
+  public class ReduceResult {
+    public final int startOrdinal;
+    public final int endOrdinal;
+    public final SqlNode node;
+
+    public ReduceResult(int startOrdinal, int endOrdinal, SqlNode node) {
+      this.startOrdinal = startOrdinal;
+      this.endOrdinal = endOrdinal;
+      this.node = node;
+    }
+  }
 }
 
 // End SqlSpecialOperator.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java
index 86660a2..bf23c3f 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java
@@ -109,7 +109,7 @@ public class SqlBetweenOperator extends SqlInfixOperator {
   //~ Constructors -----------------------------------------------------------
 
   public SqlBetweenOperator(Flag flag, boolean negated) {
-    super(negated ? NOT_BETWEEN_NAMES : BETWEEN_NAMES, SqlKind.BETWEEN, 30,
+    super(negated ? NOT_BETWEEN_NAMES : BETWEEN_NAMES, SqlKind.BETWEEN, 32,
         null, InferTypes.FIRST_KNOWN, OTC_CUSTOM);
     this.flag = flag;
     this.negated = negated;
@@ -191,12 +191,8 @@ public class SqlBetweenOperator extends SqlInfixOperator {
     writer.endList(frame);
   }
 
-  public int reduceExpr(
-      int opOrdinal,
-      List<Object> list) {
-    final SqlParserUtil.ToTreeListItem betweenNode =
-        (SqlParserUtil.ToTreeListItem) list.get(opOrdinal);
-    SqlOperator op = betweenNode.getOperator();
+  public ReduceResult reduceExpr(int opOrdinal, TokenSequence list) {
+    SqlOperator op = list.op(opOrdinal);
     assert op == this;
 
     // Break the expression up into expressions. For example, a simple
@@ -208,26 +204,18 @@ public class SqlBetweenOperator extends SqlInfixOperator {
     //    |_____|       |_____|   |_____|
     //     exp0          exp1      exp2
     // Create the expression between 'BETWEEN' and 'AND'.
-    final SqlParserPos pos =
-        ((SqlNode) list.get(opOrdinal + 1)).getParserPosition();
     SqlNode exp1 =
         SqlParserUtil.toTreeEx(list, opOrdinal + 1, 0, SqlKind.AND);
     if ((opOrdinal + 2) >= list.size()) {
-      SqlParserPos lastPos =
-          ((SqlNode) list.get(list.size() - 1)).getParserPosition();
+      SqlParserPos lastPos = list.pos(list.size() - 1);
       final int line = lastPos.getEndLineNum();
       final int col = lastPos.getEndColumnNum() + 1;
       SqlParserPos errPos = new SqlParserPos(line, col, line, col);
       throw SqlUtil.newContextException(errPos, RESOURCE.betweenWithoutAnd());
     }
-    final Object o = list.get(opOrdinal + 2);
-    if (!(o instanceof SqlParserUtil.ToTreeListItem)) {
-      SqlParserPos errPos = ((SqlNode) o).getParserPosition();
-      throw SqlUtil.newContextException(errPos, RESOURCE.betweenWithoutAnd());
-    }
-    if (((SqlParserUtil.ToTreeListItem) o).getOperator().getKind()
-        != SqlKind.AND) {
-      SqlParserPos errPos = ((SqlParserUtil.ToTreeListItem) o).getPos();
+    if (!list.isOp(opOrdinal + 2)
+        || list.op(opOrdinal + 2).getKind() != SqlKind.AND) {
+      SqlParserPos errPos = list.pos(opOrdinal + 2);
       throw SqlUtil.newContextException(errPos, RESOURCE.betweenWithoutAnd());
     }
 
@@ -240,30 +228,22 @@ public class SqlBetweenOperator extends SqlInfixOperator {
     //   (a BETWEEN b AND c + d) OR e
     // because OR has lower precedence than BETWEEN.
     SqlNode exp2 =
-        SqlParserUtil.toTreeEx(
-            list,
+        SqlParserUtil.toTreeEx(list,
             opOrdinal + 3,
             getRightPrec(),
             SqlKind.OTHER);
 
     // Create the call.
-    SqlNode exp0 = (SqlNode) list.get(opOrdinal - 1);
+    SqlNode exp0 = list.node(opOrdinal - 1);
     SqlCall newExp =
         createCall(
-            betweenNode.getPos(),
+            list.pos(opOrdinal),
             exp0,
             exp1,
             exp2);
 
     // Replace all of the matched nodes with the single reduced node.
-    SqlParserUtil.replaceSublist(
-        list,
-        opOrdinal - 1,
-        opOrdinal + 4,
-        newExp);
-
-    // Return the ordinal of the new current node.
-    return opOrdinal - 1;
+    return new ReduceResult(opOrdinal - 1, opOrdinal + 4, newExp);
   }
 
   //~ Inner Classes ----------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java
index a20cea3..716902b 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java
@@ -66,7 +66,7 @@ public class SqlInOperator extends SqlBinaryOperator {
     super(
         isNotIn ? "NOT IN" : "IN",
         SqlKind.IN,
-        30,
+        32,
         true,
         ReturnTypes.BOOLEAN_NULLABLE,
         InferTypes.FIRST_KNOWN,

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
index 6b07a35..7cd0e1e 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
@@ -26,15 +26,12 @@ import org.apache.calcite.sql.SqlOperandCountRange;
 import org.apache.calcite.sql.SqlOperatorBinding;
 import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.SqlWriter;
-import org.apache.calcite.sql.parser.SqlParserUtil;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.SqlOperandCountRanges;
 import org.apache.calcite.sql.type.SqlSingleOperandTypeChecker;
 import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
 
-import java.util.List;
-
 /**
  * The item operator {@code [ ... ]}, used to access a given element of an
  * array or map. For example, {@code myArray[3]} or {@code "myMap['foo']"}.
@@ -51,22 +48,18 @@ class SqlItemOperator extends SqlSpecialOperator {
     super("ITEM", SqlKind.OTHER_FUNCTION, 100, true, null, null, null);
   }
 
-  @Override public int reduceExpr(int ordinal, List<Object> list) {
-    SqlNode left = (SqlNode) list.get(ordinal - 1);
-    SqlNode right = (SqlNode) list.get(ordinal + 1);
-    final SqlParserUtil.ToTreeListItem treeListItem =
-        (SqlParserUtil.ToTreeListItem) list.get(ordinal);
-    SqlParserUtil.replaceSublist(
-        list,
-        ordinal - 1,
+  @Override public ReduceResult reduceExpr(int ordinal,
+      TokenSequence list) {
+    SqlNode left = list.node(ordinal - 1);
+    SqlNode right = list.node(ordinal + 1);
+    return new ReduceResult(ordinal - 1,
         ordinal + 2,
         createCall(
             left.getParserPosition()
                 .plus(right.getParserPosition())
-                .plus(treeListItem.getPos()),
+                .plus(list.pos(ordinal)),
             left,
             right));
-    return ordinal - 1;
   }
 
   @Override public void unparse(

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java
index 1de1407..9a0187c 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java
@@ -33,8 +33,6 @@ import org.apache.calcite.sql.type.SqlOperandCountRanges;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.util.Util;
 
-import java.util.List;
-
 /**
  * An operator describing the <code>LIKE</code> and <code>SIMILAR</code>
  * operators.
@@ -76,7 +74,7 @@ public class SqlLikeOperator extends SqlSpecialOperator {
     super(
         name,
         kind,
-        30,
+        32,
         false,
         ReturnTypes.BOOLEAN_NULLABLE,
         InferTypes.FIRST_KNOWN,
@@ -148,16 +146,15 @@ public class SqlLikeOperator extends SqlSpecialOperator {
     writer.endList(frame);
   }
 
-  public int reduceExpr(
+  public ReduceResult reduceExpr(
       final int opOrdinal,
-      List<Object> list) {
+      TokenSequence list) {
     // Example:
     //   a LIKE b || c ESCAPE d || e AND f
     // |  |    |      |      |      |
     //  exp0    exp1          exp2
-    SqlNode exp0 = (SqlNode) list.get(opOrdinal - 1);
-    SqlOperator op =
-        ((SqlParserUtil.ToTreeListItem) list.get(opOrdinal)).getOperator();
+    SqlNode exp0 = list.node(opOrdinal - 1);
+    SqlOperator op = list.op(opOrdinal);
     assert op instanceof SqlLikeOperator;
     SqlNode exp1 =
         SqlParserUtil.toTreeEx(
@@ -167,10 +164,8 @@ public class SqlLikeOperator extends SqlSpecialOperator {
             SqlKind.ESCAPE);
     SqlNode exp2 = null;
     if ((opOrdinal + 2) < list.size()) {
-      final Object o = list.get(opOrdinal + 2);
-      if (o instanceof SqlParserUtil.ToTreeListItem) {
-        final SqlOperator op2 =
-            ((SqlParserUtil.ToTreeListItem) o).getOperator();
+      if (list.isOp(opOrdinal + 2)) {
+        final SqlOperator op2 = list.op(opOrdinal + 2);
         if (op2.getKind() == SqlKind.ESCAPE) {
           exp2 =
               SqlParserUtil.toTreeEx(
@@ -191,8 +186,7 @@ public class SqlLikeOperator extends SqlSpecialOperator {
       end = opOrdinal + 2;
     }
     SqlCall call = createCall(SqlParserPos.ZERO, operands);
-    SqlParserUtil.replaceSublist(list, opOrdinal - 1, end, call);
-    return opOrdinal - 1;
+    return new ReduceResult(opOrdinal - 1, end, call);
   }
 }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
index e94748f..d0e60d2 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
@@ -142,7 +142,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlBinaryOperator(
           "AND",
           SqlKind.AND,
-          28,
+          24,
           true,
           ReturnTypes.BOOLEAN_NULLABLE_OPTIMIZED,
           InferTypes.BOOLEAN,
@@ -431,7 +431,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlBinaryOperator(
           "OR",
           SqlKind.OR,
-          26,
+          22,
           true,
           ReturnTypes.BOOLEAN_NULLABLE_OPTIMIZED,
           InferTypes.BOOLEAN,
@@ -553,7 +553,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NOT NULL",
           SqlKind.IS_NOT_NULL,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.VARCHAR_1024,
           OperandTypes.ANY);
@@ -562,7 +562,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NULL",
           SqlKind.IS_NULL,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.VARCHAR_1024,
           OperandTypes.ANY);
@@ -571,7 +571,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NOT TRUE",
           SqlKind.IS_NOT_TRUE,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -580,7 +580,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS TRUE",
           SqlKind.IS_TRUE,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -589,7 +589,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NOT FALSE",
           SqlKind.IS_NOT_FALSE,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -598,7 +598,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS FALSE",
           SqlKind.IS_FALSE,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -607,7 +607,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NOT UNKNOWN",
           SqlKind.IS_NOT_NULL,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -616,7 +616,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS UNKNOWN",
           SqlKind.IS_NULL,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -625,7 +625,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS A SET",
           SqlKind.OTHER,
-          30,
+          28,
           ReturnTypes.BOOLEAN,
           null,
           OperandTypes.MULTISET);
@@ -657,7 +657,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPrefixOperator(
           "NOT",
           SqlKind.NOT,
-          30,
+          26,
           ReturnTypes.ARG0,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -1054,7 +1054,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
    * SIMILAR TO expression.
    */
   public static final SqlSpecialOperator ESCAPE =
-      new SqlSpecialOperator("Escape", SqlKind.ESCAPE, 30);
+      new SqlSpecialOperator("ESCAPE", SqlKind.ESCAPE, 0);
 
   public static final SqlCaseOperator CASE = SqlCaseOperator.INSTANCE;
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
index 03b0ac3..3c605a5 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
@@ -20,25 +20,32 @@ import org.apache.calcite.avatica.util.Casing;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.runtime.CalciteContextException;
 import org.apache.calcite.sql.SqlBinaryOperator;
-import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlIntervalLiteral;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLiteral;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlNumericLiteral;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlPostfixOperator;
+import org.apache.calcite.sql.SqlPrefixOperator;
 import org.apache.calcite.sql.SqlSpecialOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.util.PrecedenceClimbingParser;
 import org.apache.calcite.util.SaffronProperties;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.trace.CalciteTrace;
 
+import com.google.common.base.Predicate;
+
 import org.slf4j.Logger;
 
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.nio.charset.Charset;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.StringTokenizer;
@@ -99,6 +106,7 @@ public final class SqlParserUtil {
   /**
    * @deprecated this method is not localized for Farrago standards
    */
+  @Deprecated // to be removed before 2.0
   public static java.sql.Date parseDate(String s) {
     return java.sql.Date.valueOf(s);
   }
@@ -106,6 +114,7 @@ public final class SqlParserUtil {
   /**
    * @deprecated Does not parse SQL:99 milliseconds
    */
+  @Deprecated // to be removed before 2.0
   public static java.sql.Time parseTime(String s) {
     return java.sql.Time.valueOf(s);
   }
@@ -113,6 +122,7 @@ public final class SqlParserUtil {
   /**
    * @deprecated this method is not localized for Farrago standards
    */
+  @Deprecated // to be removed before 2.0
   public static java.sql.Timestamp parseTimestamp(String s) {
     return java.sql.Timestamp.valueOf(s);
   }
@@ -227,6 +237,7 @@ public final class SqlParserUtil {
    * Parses a Binary string. SQL:99 defines a binary string as a hexstring
    * with EVEN nbr of hex digits.
    */
+  @Deprecated // to be removed before 2.0
   public static byte[] parseBinaryString(String s) {
     s = s.replaceAll(" ", "");
     s = s.replaceAll("\n", "");
@@ -491,6 +502,7 @@ public final class SqlParserUtil {
     return new ParsedCollation(charset, locale, strength);
   }
 
+  @Deprecated // to be removed before 2.0
   public static String[] toStringArray(List<String> list) {
     return list.toArray(new String[list.size()]);
   }
@@ -503,6 +515,7 @@ public final class SqlParserUtil {
     return list.toArray();
   }
 
+  @Deprecated // to be removed before 2.0
   public static String rightTrim(
       String s,
       char c) {
@@ -541,8 +554,14 @@ public final class SqlParserUtil {
    * taking operator precedence and associativity into account.
    */
   public static SqlNode toTree(List<Object> list) {
+    if (list.size() == 1
+        && list.get(0) instanceof SqlNode) {
+      // Short-cut for the simple common case
+      return (SqlNode) list.get(0);
+    }
     LOGGER.trace("Attempting to reduce {}", list);
-    final SqlNode node = toTreeEx(list, 0, 0, SqlKind.OTHER);
+    final OldTokenSequenceImpl tokenSequence = new OldTokenSequenceImpl(list);
+    final SqlNode node = toTreeEx(tokenSequence, 0, 0, SqlKind.OTHER);
     LOGGER.debug("Reduced {}", node);
     return node;
   }
@@ -563,182 +582,58 @@ public final class SqlParserUtil {
    *                    we encounter a token of this kind.
    * @return the root node of the tree which the list condenses into
    */
-  public static SqlNode toTreeEx(
-      List<Object> list,
-      int start,
-      int minPrec,
-      SqlKind stopperKind) {
-// Make several passes over the list, and each pass, coalesce the
-// expressions with the highest precedence.
-  outer:
-    while (true) {
-      final int count = list.size();
-      if (count <= (start + 1)) {
-        break;
-      }
-      int i = start + 1;
-      while (i < count) {
-        SqlOperator previous;
-        SqlOperator current = ((ToTreeListItem) list.get(i)).op;
-        SqlParserPos currentPos = ((ToTreeListItem) list.get(i)).pos;
-        if ((stopperKind != SqlKind.OTHER)
-            && (current.getKind() == stopperKind)) {
-          break outer;
-        }
-        SqlOperator next;
-        int previousRight;
-        int left = current.getLeftPrec();
-        int right = current.getRightPrec();
-        if (left < minPrec) {
-          break outer;
-        }
-        int nextLeft;
-        if (current instanceof SqlBinaryOperator) {
-          if (i == (start + 1)) {
-            previous = null;
-            previousRight = 0;
-          } else {
-            previous = ((ToTreeListItem) list.get(i - 2)).op;
-            previousRight = previous.getRightPrec();
-          }
-          if (i == (count - 2)) {
-            next = null;
-            nextLeft = 0;
-          } else {
-            next = ((ToTreeListItem) list.get(i + 2)).op;
-            nextLeft = next.getLeftPrec();
-            if ((next.getKind() == stopperKind)
-                && (stopperKind != SqlKind.OTHER)) {
-              // Suppose we're looking at 'AND' in
-              //    a BETWEEN b OR c AND d
-              //
-              // Because 'AND' is our stopper token, we still
-              // want to reduce 'b OR c', even though 'AND' has
-              // higher precedence than 'OR'.
-              nextLeft = 0;
+  public static SqlNode toTreeEx(SqlSpecialOperator.TokenSequence list,
+      int start, final int minPrec, final SqlKind stopperKind) {
+    final Predicate<PrecedenceClimbingParser.Token> predicate =
+        new Predicate<PrecedenceClimbingParser.Token>() {
+          public boolean apply(PrecedenceClimbingParser.Token t) {
+            if (t instanceof PrecedenceClimbingParser.Op) {
+              final SqlOperator op = ((ToTreeListItem) t.o).op;
+              return stopperKind != SqlKind.OTHER
+                  && op.kind == stopperKind
+                  || minPrec > 0
+                  && op.getLeftPrec() < minPrec;
+            } else {
+              return false;
             }
           }
-          if ((previousRight < left) && (right >= nextLeft)) {
-            // For example,
-            //    i:  0 1 2 3 4 5 6 7 8
-            // list:  a + b * c * d + e
-            // prec: 0 1 2 3 4 3 4 1 2 0
-            //
-            // At i == 3, we have the first '*' operator, and its
-            // surrounding precedences obey the relation 2 < 3 and
-            // 4 >= 3, so we can reduce (b * c) to a single node.
-            SqlNode leftExp = (SqlNode) list.get(i - 1);
-
-            // For example,
-            //    i:  0 1 2 3 4 5 6 7 8
-            // list:  a + b * c * d + e
-            // prec: 0 1 2 3 4 3 4 1 2 0
-            //
-            // At i == 3, we have the first '*' operator, and its
-            // surrounding precedences obey the relation 2 < 3 and
-            // 4 >= 3, so we can reduce (b * c) to a single node.
-            SqlNode rightExp = (SqlNode) list.get(i + 1);
-            SqlParserPos callPos =
-                currentPos.plusAll(
-                    new SqlNode[]{leftExp, rightExp});
-            final SqlCall newExp =
-                current.createCall(callPos, leftExp, rightExp);
-            LOGGER.debug("Reduced infix: {}", newExp);
-
-            // Replace elements {i - 1, i, i + 1} with the new
-            // expression.
-            replaceSublist(list, i - 1, i + 2, newExp);
-            break;
-          }
-          i += 2;
-        } else if (current instanceof SqlPostfixOperator) {
-          if (i == (start + 1)) {
-            previous = null;
-            previousRight = 0;
-          } else {
-            previous = ((ToTreeListItem) list.get(i - 2)).op;
-            previousRight = previous.getRightPrec();
-          }
-          if (previousRight < left) {
-            // For example,
-            //    i:  0 1 2 3 4 5 6 7 8
-            // list:  a + b * c ! + d
-            // prec: 0 1 2 3 4 3 0 2
-            //
-            // At i == 3, we have the postfix '!' operator. Its
-            // high precedence determines that it binds with 'b *
-            // c'. The precedence of the following '+' operator is
-            // irrelevant.
-            SqlNode leftExp = (SqlNode) list.get(i - 1);
-
-            SqlParserPos callPos =
-                currentPos.plusAll(new SqlNode[]{leftExp});
-            final SqlCall newExp =
-                current.createCall(callPos, leftExp);
-            LOGGER.debug("Reduced postfix: {}", newExp);
-
-            // Replace elements {i - 1, i} with the new expression.
-            list.remove(i);
-            list.set(i - 1, newExp);
-            break;
-          }
-          ++i;
-
-          //
-        } else if (current instanceof SqlSpecialOperator) {
-          SqlSpecialOperator specOp = (SqlSpecialOperator) current;
-
-          // We decide to reduce a special operator only on the basis
-          // of what's to the left of it. The operator then decides
-          // how far to the right to chew off.
-          if (i == (start + 1)) {
-            previous = null;
-            previousRight = 0;
-          } else {
-            previous = ((ToTreeListItem) list.get(i - 2)).op;
-            previousRight = previous.getRightPrec();
-          }
-          int nextOrdinal = i + 2;
-          if (i == (count - 2)) {
-            next = null;
-            nextLeft = 0;
-          } else {
-            // find next op
-            next = null;
-            nextLeft = 0;
-            for (; nextOrdinal < count; nextOrdinal++) {
-              Object listItem = list.get(nextOrdinal);
-              if (listItem instanceof ToTreeListItem) {
-                next = ((ToTreeListItem) listItem).op;
-                nextLeft = next.getLeftPrec();
-                if ((stopperKind != SqlKind.OTHER)
-                    && (next.getKind() == stopperKind)) {
-                  break outer;
-                } else {
-                  break;
-                }
-              }
-            }
-          }
-          if (nextLeft < minPrec) {
-            break outer;
-          }
-          if ((previousRight < left) && (right >= nextLeft)) {
-            i = specOp.reduceExpr(i, list);
-            LOGGER.debug("Reduced special op: {}", list.get(i));
-            break;
-          }
-          i = nextOrdinal;
-        } else {
-          throw Util.newInternal("Unexpected operator type: " + current);
-        }
-      }
+        };
+    PrecedenceClimbingParser parser = list.parser(start, predicate);
+    final int beforeSize = parser.all().size();
+    parser.partialParse();
+    final int afterSize = parser.all().size();
+    final SqlNode node = convert(parser.all().get(0));
+    list.replaceSublist(start, start + beforeSize - afterSize + 1, node);
+    return node;
+  }
 
-      // Require the list shrinks each time around -- otherwise we will
-      // never terminate.
-      assert list.size() < count;
+  private static SqlNode convert(PrecedenceClimbingParser.Token token) {
+    switch (token.type) {
+    case ATOM:
+      return (SqlNode) token.o;
+    case CALL:
+      final PrecedenceClimbingParser.Call call =
+          (PrecedenceClimbingParser.Call) token;
+      final List<SqlNode> list = new ArrayList<>();
+      for (PrecedenceClimbingParser.Token arg : call.args) {
+        list.add(convert(arg));
+      }
+      final ToTreeListItem item = (ToTreeListItem) call.op.o;
+      if (item.op == SqlStdOperatorTable.UNARY_MINUS
+          && list.size() == 1
+          && list.get(0) instanceof SqlNumericLiteral) {
+        return SqlLiteral.createNegative((SqlNumericLiteral) list.get(0),
+            item.pos.plusAll(list));
+      }
+      if (item.op == SqlStdOperatorTable.UNARY_PLUS
+          && list.size() == 1
+          && list.get(0) instanceof SqlNumericLiteral) {
+        return list.get(0);
+      }
+      return item.op.createCall(item.pos.plusAll(list), list);
+    default:
+      throw new AssertionError(token);
     }
-    return (SqlNode) list.get(start);
   }
 
   /**
@@ -810,6 +705,10 @@ public final class SqlParserUtil {
       this.pos = pos;
     }
 
+    public String toString() {
+      return op.toString();
+    }
+
     public SqlOperator getOperator() {
       return op;
     }
@@ -834,6 +733,146 @@ public final class SqlParserUtil {
       this.pos = pos;
     }
   }
+
+  /** Implementation of {@link SqlSpecialOperator.TokenSequence} based on an
+   * existing parser. */
+  private static class TokenSequenceImpl
+      implements SqlSpecialOperator.TokenSequence {
+    final List<PrecedenceClimbingParser.Token> list;
+    final PrecedenceClimbingParser parser;
+
+    private TokenSequenceImpl(PrecedenceClimbingParser parser) {
+      this.parser = parser;
+      this.list = parser.all();
+    }
+
+    public PrecedenceClimbingParser parser(int start, Predicate
+        <PrecedenceClimbingParser.Token> predicate) {
+      return parser.copy(start, predicate);
+    }
+
+    public int size() {
+      return list.size();
+    }
+
+    public SqlOperator op(int i) {
+      return ((ToTreeListItem) list.get(i).o).getOperator();
+    }
+
+    private static SqlParserPos pos(PrecedenceClimbingParser.Token token) {
+      switch (token.type) {
+      case ATOM:
+        return ((SqlNode) token.o).getParserPosition();
+      case CALL:
+        final PrecedenceClimbingParser.Call call =
+            (PrecedenceClimbingParser.Call) token;
+        SqlParserPos pos = ((ToTreeListItem) call.op.o).pos;
+        for (PrecedenceClimbingParser.Token arg : call.args) {
+          pos = pos.plus(pos(arg));
+        }
+        return pos;
+      default:
+        return ((ToTreeListItem) token.o).getPos();
+      }
+    }
+
+    public SqlParserPos pos(int i) {
+      return pos(list.get(i));
+    }
+
+    public boolean isOp(int i) {
+      return list.get(i).o instanceof ToTreeListItem;
+    }
+
+    public SqlNode node(int i) {
+      return convert(list.get(i));
+    }
+
+    public void replaceSublist(int start, int end, SqlNode e) {
+      SqlParserUtil.replaceSublist(list, start, end, parser.atom(e));
+    }
+  }
+
+  /** Implementation of {@link SqlSpecialOperator.TokenSequence}. */
+  private static class OldTokenSequenceImpl
+      implements SqlSpecialOperator.TokenSequence {
+    final List<Object> list;
+
+    private OldTokenSequenceImpl(List<Object> list) {
+      this.list = list;
+    }
+
+    @Override public PrecedenceClimbingParser parser(int start,
+        Predicate<PrecedenceClimbingParser.Token> predicate) {
+      final PrecedenceClimbingParser.Builder builder =
+          new PrecedenceClimbingParser.Builder();
+      for (Object o : Util.skip(list, start)) {
+        if (o instanceof ToTreeListItem) {
+          final ToTreeListItem item = (ToTreeListItem) o;
+          final SqlOperator op = item.getOperator();
+          if (op instanceof SqlPrefixOperator) {
+            builder.prefix(item, op.getLeftPrec());
+          } else if (op instanceof SqlPostfixOperator) {
+            builder.postfix(item, op.getRightPrec());
+          } else if (op instanceof SqlBinaryOperator) {
+            builder.infix(item, op.getLeftPrec(),
+                op.getLeftPrec() < op.getRightPrec());
+          } else if (op instanceof SqlSpecialOperator) {
+            builder.special(item, op.getLeftPrec(), op.getRightPrec(),
+                new PrecedenceClimbingParser.Special() {
+                  public PrecedenceClimbingParser.Result apply(
+                      PrecedenceClimbingParser parser,
+                      PrecedenceClimbingParser.SpecialOp op) {
+                    final List<PrecedenceClimbingParser.Token> tokens =
+                        parser.all();
+                    final SqlSpecialOperator op1 =
+                        (SqlSpecialOperator) ((ToTreeListItem) op.o).op;
+                    SqlSpecialOperator.ReduceResult r =
+                        op1.reduceExpr(tokens.indexOf(op),
+                            new TokenSequenceImpl(parser));
+                    return new PrecedenceClimbingParser.Result(
+                        tokens.get(r.startOrdinal),
+                        tokens.get(r.endOrdinal - 1),
+                        parser.atom(r.node));
+                  }
+                });
+          } else {
+            throw new AssertionError();
+          }
+        } else {
+          builder.atom(o);
+        }
+      }
+      return builder.build();
+    }
+
+    public int size() {
+      return list.size();
+    }
+
+    public SqlOperator op(int i) {
+      return ((ToTreeListItem) list.get(i)).op;
+    }
+
+    public SqlParserPos pos(int i) {
+      final Object o = list.get(i);
+      return o instanceof ToTreeListItem
+          ? ((ToTreeListItem) o).pos
+          : ((SqlNode) o).getParserPosition();
+    }
+
+    public boolean isOp(int i) {
+      return list.get(i) instanceof ToTreeListItem;
+    }
+
+    public SqlNode node(int i) {
+      return (SqlNode) list.get(i);
+    }
+
+    public void replaceSublist(int start, int end, SqlNode e) {
+      SqlParserUtil.replaceSublist(list, start, end, e);
+    }
+  }
 }
 
 // End SqlParserUtil.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/util/PrecedenceClimbingParser.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/PrecedenceClimbingParser.java b/core/src/main/java/org/apache/calcite/util/PrecedenceClimbingParser.java
new file mode 100644
index 0000000..68b8be5
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/util/PrecedenceClimbingParser.java
@@ -0,0 +1,447 @@
+/*
+ * 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.util;
+
+import org.apache.calcite.linq4j.Ord;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parser that takes a collection of tokens (atoms and operators)
+ * and groups them together according to the operators' precedence
+ * and associativity.
+ */
+public class PrecedenceClimbingParser {
+  private Token first;
+  private Token last;
+
+  private PrecedenceClimbingParser(List<Token> tokens) {
+    Token p = null;
+    for (Token token : tokens) {
+      if (p != null) {
+        p.next = token;
+      } else {
+        first = token;
+      }
+      token.previous = p;
+      token.next = null;
+      p = token;
+    }
+    last = p;
+  }
+
+  public Token atom(Object o) {
+    return new Token(Type.ATOM, o, -1, -1);
+  }
+
+  public Call call(Op op, ImmutableList<Token> args) {
+    return new Call(op, args);
+  }
+
+  public Op infix(Object o, int precedence, boolean left) {
+    return new Op(Type.INFIX, o, precedence * 2 + (left ? 0 : 1),
+        precedence * 2 + (left ? 1 : 0));
+  }
+
+  public Op prefix(Object o, int precedence) {
+    return new Op(Type.PREFIX, o, -1, precedence * 2);
+  }
+
+  public Op postfix(Object o, int precedence) {
+    return new Op(Type.POSTFIX, o, precedence * 2, -1);
+  }
+
+  public SpecialOp special(Object o, int leftPrec, int rightPrec,
+      Special special) {
+    return new SpecialOp(o, leftPrec * 2, rightPrec * 2, special);
+  }
+
+  public Token parse() {
+    partialParse();
+    if (first != last) {
+      throw new AssertionError("could not find next operator to reduce: "
+          + this);
+    }
+    return first;
+  }
+
+  public void partialParse() {
+    for (;;) {
+      Op op = highest();
+      if (op == null) {
+        return;
+      }
+      final Token t;
+      switch (op.type) {
+      case POSTFIX:
+        t = call(op, ImmutableList.of(op.previous));
+        replace(t, op.previous.previous, op.next);
+        break;
+      case PREFIX:
+        t = call(op, ImmutableList.of(op.next));
+        replace(t, op.previous, op.next.next);
+        break;
+      case INFIX:
+        t = call(op, ImmutableList.of(op.previous, op.next));
+        replace(t, op.previous.previous, op.next.next);
+        break;
+      case SPECIAL:
+        Result r = ((SpecialOp) op).special.apply(this, (SpecialOp) op);
+        Preconditions.checkNotNull(r);
+        replace(r.replacement, r.first.previous, r.last.next);
+        break;
+      default:
+        throw new AssertionError();
+      }
+      // debug: System.out.println(this);
+    }
+  }
+
+  @Override public String toString() {
+    return Util.commaList(all());
+  }
+
+  /** Returns a list of all tokens. */
+  public List<Token> all() {
+    return new TokenList();
+  }
+
+  private void replace(Token t, Token previous, Token next) {
+    t.previous = previous;
+    t.next = next;
+    if (previous == null) {
+      first = t;
+    } else {
+      previous.next = t;
+    }
+    if (next == null) {
+      last = t;
+    } else {
+      next.previous = t;
+    }
+  }
+
+  private Op highest() {
+    int p = -1;
+    Op highest = null;
+    for (Token t = first; t != null; t = t.next) {
+      if ((t.left > p || t.right > p)
+          && (t.left < 0 || t.left >= prevRight(t.previous))
+          && (t.right < 0 || t.right >= nextLeft(t.next))) {
+        p = Math.max(t.left, t.right);
+        highest = (Op) t;
+      }
+    }
+    return highest;
+  }
+
+  /** Returns the right precedence of the preceding operator token. */
+  private int prevRight(Token token) {
+    for (; token != null; token = token.previous) {
+      if (token.type == Type.POSTFIX) {
+        return Integer.MAX_VALUE;
+      }
+      if (token.right >= 0) {
+        return token.right;
+      }
+    }
+    return -1;
+  }
+
+  /** Returns the left precedence of the following operator token. */
+  private int nextLeft(Token token) {
+    for (; token != null; token = token.next) {
+      if (token.type == Type.PREFIX) {
+        return Integer.MAX_VALUE;
+      }
+      if (token.left >= 0) {
+        return token.left;
+      }
+    }
+    return -1;
+  }
+
+  public String print(Token token) {
+    return token.toString();
+  }
+
+  public PrecedenceClimbingParser copy(int start, Predicate<Token> predicate) {
+    final List<Token> tokens = new ArrayList<>();
+    for (Token token : Util.skip(all(), start)) {
+      if (predicate.apply(token)) {
+        break;
+      }
+      tokens.add(token.copy());
+    }
+    return new PrecedenceClimbingParser(tokens);
+  }
+
+  /** Token type. */
+  public enum Type {
+    ATOM,
+    CALL,
+    PREFIX,
+    INFIX,
+    POSTFIX,
+    SPECIAL
+  }
+
+  /** A token: either an atom, a call to an operator with arguments,
+   * or an unmatched operator. */
+  public static class Token {
+    Token previous;
+    Token next;
+    public final Type type;
+    public final Object o;
+    final int left;
+    final int right;
+
+    Token(Type type, Object o, int left, int right) {
+      this.type = type;
+      this.o = o;
+      this.left = left;
+      this.right = right;
+    }
+
+    @Override public String toString() {
+      return o.toString();
+    }
+
+    protected StringBuilder print(StringBuilder b) {
+      return b.append(o);
+    }
+
+    public Token copy() {
+      return new Token(type, o, left, right);
+    }
+  }
+
+  /** An operator token. */
+  public static class Op extends Token {
+    Op(Type type, Object o, int left, int right) {
+      super(type, o, left, right);
+    }
+
+    @Override public Token copy() {
+      return new Op(type, o, left, right);
+    }
+  }
+
+  /** An token corresponding to a special operator. */
+  public static class SpecialOp extends Op {
+    public final Special special;
+
+    SpecialOp(Object o, int left, int right, Special special) {
+      super(Type.SPECIAL, o, left, right);
+      this.special = special;
+    }
+
+    @Override public Token copy() {
+      return new SpecialOp(o, left, right, special);
+    }
+  }
+
+
+  /** A token that is a call to an operator with arguments. */
+  public static class Call extends Token {
+    public final Op op;
+    public final ImmutableList<Token> args;
+
+    Call(Op op, ImmutableList<Token> args) {
+      super(Type.CALL, null, -1, -1);
+      this.op = op;
+      this.args = args;
+    }
+
+    @Override public Token copy() {
+      return new Call(op, args);
+    }
+
+    @Override public String toString() {
+      return print(new StringBuilder()).toString();
+    }
+
+    protected StringBuilder print(StringBuilder b) {
+      switch (op.type) {
+      case PREFIX:
+        b.append('(');
+        printOp(b, false, true);
+        args.get(0).print(b);
+        return b.append(')');
+      case POSTFIX:
+        b.append('(');
+        args.get(0).print(b);
+        return printOp(b, true, false).append(')');
+      case INFIX:
+        b.append('(');
+        args.get(0).print(b);
+        printOp(b, true, true);
+        args.get(1).print(b);
+        return b.append(')');
+      case SPECIAL:
+        printOp(b, false, false)
+            .append('(');
+        for (Ord<Token> arg : Ord.zip(args)) {
+          if (arg.i > 0) {
+            b.append(", ");
+          }
+          arg.e.print(b);
+        }
+        return b.append(')');
+      default:
+        throw new AssertionError();
+      }
+    }
+
+    private StringBuilder printOp(StringBuilder b, boolean leftSpace,
+        boolean rightSpace) {
+      String s = op.o.toString();
+      if (leftSpace) {
+        b.append(' ');
+      }
+      b.append(s);
+      if (rightSpace) {
+        b.append(' ');
+      }
+      return b;
+    }
+  }
+
+  /** Callback defining the behavior of a special function. */
+  public interface Special {
+    /** Given an occurrence of this operator, identifies the range of tokens to
+     * be collapsed into a call of this operator, and the arguments to that
+     * call. */
+    Result apply(PrecedenceClimbingParser parser, SpecialOp op);
+  }
+
+  /** Result of a call to {@link Special#apply}. */
+  public static class Result {
+    final Token first;
+    final Token last;
+    final Token replacement;
+
+    public Result(Token first, Token last, Token replacement) {
+      this.first = first;
+      this.last = last;
+      this.replacement = replacement;
+    }
+  }
+
+  /** Fluent helper to build a parser containing a list of tokens. */
+  public static class Builder {
+    final List<Token> tokens = new ArrayList<>();
+    private final PrecedenceClimbingParser dummy =
+        new PrecedenceClimbingParser(ImmutableList.<Token>of());
+
+    private Builder add(Token t) {
+      tokens.add(t);
+      return this;
+    }
+
+    public Builder atom(Object o) {
+      return add(dummy.atom(o));
+    }
+
+    public Builder call(Op op, Token arg0, Token arg1) {
+      return add(dummy.call(op, ImmutableList.of(arg0, arg1)));
+    }
+
+    public Builder infix(Object o, int precedence, boolean left) {
+      return add(dummy.infix(o, precedence, left));
+    }
+
+    public Builder prefix(Object o, int precedence) {
+      return add(dummy.prefix(o, precedence));
+    }
+
+    public Builder postfix(Object o, int precedence) {
+      return add(dummy.postfix(o, precedence));
+    }
+
+    public Builder special(Object o, int leftPrec, int rightPrec,
+        Special special) {
+      return add(dummy.special(o, leftPrec, rightPrec, special));
+    }
+
+    public PrecedenceClimbingParser build() {
+      return new PrecedenceClimbingParser(tokens);
+    }
+  }
+
+  /** List view onto the tokens in a parser. The view is semi-mutable; it
+   * supports {@link List#remove(int)} but not {@link List#set} or
+   * {@link List#add}. */
+  private class TokenList extends AbstractList<Token> {
+    @Override public Token get(int index) {
+      for (Token t = first; t != null; t = t.next) {
+        if (index-- == 0) {
+          return t;
+        }
+      }
+      throw new IndexOutOfBoundsException();
+    }
+
+    @Override public int size() {
+      int n = 0;
+      for (Token t = first; t != null; t = t.next) {
+        ++n;
+      }
+      return n;
+    }
+
+    @Override public Token remove(int index) {
+      Token t = get(index);
+      if (t.previous == null) {
+        first = t.next;
+      } else {
+        t.previous.next = t.next;
+      }
+      if (t.next == null) {
+        last = t.previous;
+      } else {
+        t.next.previous = t.previous;
+      }
+      return t;
+    }
+
+    @Override public Token set(int index, Token element) {
+      final Token t = get(index);
+      element.previous = t.previous;
+      if (t.previous == null) {
+        first = element;
+      } else {
+        t.previous.next = element;
+      }
+      element.next = t.next;
+      if (t.next == null) {
+        last = element;
+      } else {
+        t.next.previous = element;
+      }
+      return t;
+    }
+  }
+}
+
+// End PrecedenceClimbingParser.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/util/Util.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/Util.java b/core/src/main/java/org/apache/calcite/util/Util.java
index 0eee37b..c01c772 100644
--- a/core/src/main/java/org/apache/calcite/util/Util.java
+++ b/core/src/main/java/org/apache/calcite/util/Util.java
@@ -2044,7 +2044,7 @@ public class Util {
 
   /** Returns all but the first {@code n} elements of a list. */
   public static <E> List<E> skip(List<E> list, int fromIndex) {
-    return list.subList(fromIndex, list.size());
+    return fromIndex == 0 ? list : list.subList(fromIndex, list.size());
   }
 
   public static List<Integer> range(final int end) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 590f0ee..7a3ea1a 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -744,6 +744,56 @@ public class SqlParserTest {
             + "WHERE (1 AND TRUE)");
   }
 
+  @Test public void testLessThanAssociativity() {
+    checkExp("NOT a = b", "(NOT (`A` = `B`))");
+
+    // comparison operators are left-associative
+    checkExp("x < y < z", "((`X` < `Y`) < `Z`)");
+    checkExp("x < y <= z = a", "(((`X` < `Y`) <= `Z`) = `A`)");
+    checkExp("a = x < y <= z = a", "((((`A` = `X`) < `Y`) <= `Z`) = `A`)");
+
+    // IS NULL has lower precedence than comparison
+    checkExp("a = x is null", "((`A` = `X`) IS NULL)");
+    checkExp("a = x is not null", "((`A` = `X`) IS NOT NULL)");
+
+    // BETWEEN, IN, LIKE have higher precedence than comparison
+    checkExp("a = x between y = b and z = c",
+        "((`A` = (`X` BETWEEN ASYMMETRIC (`Y` = `B`) AND `Z`)) = `C`)");
+    checkExp("a = x like y = b",
+        "((`A` = (`X` LIKE `Y`)) = `B`)");
+    checkExp("a = x not like y = b",
+        "((`A` = (`X` NOT LIKE `Y`)) = `B`)");
+    checkExp("a = x similar to y = b",
+        "((`A` = (`X` SIMILAR TO `Y`)) = `B`)");
+    checkExp("a = x not similar to y = b",
+        "((`A` = (`X` NOT SIMILAR TO `Y`)) = `B`)");
+    checkExp("a = x not in (y, z) = b",
+        "((`A` = (`X` NOT IN (`Y`, `Z`))) = `B`)");
+
+    // LIKE has higher precedence than IS NULL
+    checkExp("a like b is null", "((`A` LIKE `B`) IS NULL)");
+    checkExp("a not like b is not null",
+        "((`A` NOT LIKE `B`) IS NOT NULL)");
+
+    // = has higher precedence than NOT
+    checkExp("NOT a = b", "(NOT (`A` = `B`))");
+    checkExp("NOT a = NOT b", "(NOT (`A` = (NOT `B`)))");
+
+    // IS NULL has higher precedence than NOT
+    checkExp("NOT a IS NULL", "(NOT (`A` IS NULL))");
+    checkExp("NOT a = b IS NOT NULL", "(NOT ((`A` = `B`) IS NOT NULL))");
+
+    // NOT has higher precedence than AND, which  has higher precedence than OR
+    checkExp("NOT a AND NOT b", "((NOT `A`) AND (NOT `B`))");
+    checkExp("NOT a OR NOT b", "((NOT `A`) OR (NOT `B`))");
+    checkExp("NOT a = b AND NOT c = d OR NOT e = f",
+        "(((NOT (`A` = `B`)) AND (NOT (`C` = `D`))) OR (NOT (`E` = `F`)))");
+    checkExp("NOT a = b OR NOT c = d AND NOT e = f",
+        "((NOT (`A` = `B`)) OR ((NOT (`C` = `D`)) AND (NOT (`E` = `F`))))");
+    checkExp("NOT NOT a = b OR NOT NOT c = d",
+        "((NOT (NOT (`A` = `B`))) OR (NOT (NOT (`C` = `D`))))");
+  }
+
   @Test public void testIsBooleans() {
     String[] inOuts = {"NULL", "TRUE", "FALSE", "UNKNOWN"};
 
@@ -752,7 +802,7 @@ public class SqlParserTest {
           "select * from t where nOt fAlSe Is " + inOut,
           "SELECT *\n"
               + "FROM `T`\n"
-              + "WHERE ((NOT FALSE) IS " + inOut + ")");
+              + "WHERE (NOT (FALSE IS " + inOut + "))");
 
       check(
           "select * from t where c1=1.1 IS NOT " + inOut,
@@ -771,7 +821,7 @@ public class SqlParserTest {
     check("select 1 from t where not true is unknown",
         "SELECT 1\n"
             + "FROM `T`\n"
-            + "WHERE ((NOT TRUE) IS UNKNOWN)");
+            + "WHERE (NOT (TRUE IS UNKNOWN))");
 
     check(
         "select * from t where x is unknown is not unknown is false is not false"
@@ -781,11 +831,16 @@ public class SqlParserTest {
             + "WHERE ((((((((`X` IS UNKNOWN) IS NOT UNKNOWN) IS FALSE) IS NOT FALSE) IS TRUE) IS NOT TRUE) IS NULL) IS NOT NULL)");
 
     // combine IS postfix operators with infix (AND) and prefix (NOT) ops
-    check(
-        "select * from t where x is unknown is false and x is unknown is true or not y is unknown is not null",
-        "SELECT *\n"
-            + "FROM `T`\n"
-            + "WHERE ((((`X` IS UNKNOWN) IS FALSE) AND ((`X` IS UNKNOWN) IS TRUE)) OR (((NOT `Y`) IS UNKNOWN) IS NOT NULL))");
+    final String sql = "select * from t "
+        + "where x is unknown is false "
+        + "and x is unknown is true "
+        + "or not y is unknown is not null";
+    final String expected = "SELECT *\n"
+        + "FROM `T`\n"
+        + "WHERE ((((`X` IS UNKNOWN) IS FALSE)"
+        + " AND ((`X` IS UNKNOWN) IS TRUE))"
+        + " OR (NOT ((`Y` IS UNKNOWN) IS NOT NULL)))";
+    check(sql, expected);
   }
 
   @Test public void testEqualNotEqual() {
@@ -871,10 +926,10 @@ public class SqlParserTest {
         "values a between b and c + 2 or d and e",
         "VALUES (ROW(((`A` BETWEEN ASYMMETRIC `B` AND (`C` + 2)) OR (`D` AND `E`))))");
 
-    // '=' and BETWEEN have same precedence, and are left-assoc
+    // '=' has slightly lower precedence than BETWEEN; both are left-assoc
     check(
         "values x = a between b and c = d = e",
-        "VALUES (ROW(((((`X` = `A`) BETWEEN ASYMMETRIC `B` AND `C`) = `D`) = `E`)))");
+        "VALUES (ROW((((`X` = (`A` BETWEEN ASYMMETRIC `B` AND `C`)) = `D`) = `E`)))");
 
     // AND doesn't match BETWEEN if it's between parentheses!
     check(
@@ -1147,7 +1202,7 @@ public class SqlParserTest {
     // LIKE has same precedence as '='; LIKE is right-assoc, '=' is left
     check(
         "values a = b like c = d",
-        "VALUES (ROW(((`A` = `B`) LIKE (`C` = `D`))))");
+        "VALUES (ROW(((`A` = (`B` LIKE `C`)) = `D`)))");
 
     // Nested LIKE
     check(
@@ -1228,13 +1283,13 @@ public class SqlParserTest {
   @Test public void testFoo() {
   }
 
-  @Test public void testArthimeticOperators() {
+  @Test public void testArithmeticOperators() {
     checkExp("1-2+3*4/5/6-7", "(((1 - 2) + (((3 * 4) / 5) / 6)) - 7)");
     checkExp("power(2,3)", "POWER(2, 3)");
     checkExp("aBs(-2.3e-2)", "ABS(-2.3E-2)");
     checkExp("MOD(5             ,\t\f\r\n2)", "MOD(5, 2)");
     checkExp("ln(5.43  )", "LN(5.43)");
-    checkExp("log10(- -.2  )", "LOG10((- -0.2))");
+    checkExp("log10(- -.2  )", "LOG10(0.2)");
   }
 
   @Test public void testExists() {
@@ -2297,14 +2352,14 @@ public class SqlParserTest {
     check(
         "values (- -1\n"
             + ")",
-        "VALUES (ROW((- -1)))");
+        "VALUES (ROW(1))");
 
     check(
         "values (--1+\n"
             + "2)",
         "VALUES (ROW(2))");
 
-    // end of multiline commment without start
+    // end of multiline comment without start
     if (Bug.FRG73_FIXED) {
       checkFails("values (1 */ 2)", "xx");
     }
@@ -2380,7 +2435,7 @@ public class SqlParserTest {
     checkExp("1", "1");
     checkExp("+1.", "1");
     checkExp("-1", "-1");
-    checkExp("- -1", "(- -1)");
+    checkExp("- -1", "1");
     checkExp("1.0", "1.0");
     checkExp("-3.2", "-3.2");
     checkExp("1.", "1");
@@ -2437,7 +2492,11 @@ public class SqlParserTest {
   }
 
   @Test public void testPrecedence2() {
-    checkExp("- - 1", "(- -1)"); // two prefices
+    checkExp("- - 1", "1"); // special case for unary minus
+  }
+
+  @Test public void testPrecedence2b() {
+    checkExp("not not 1", "(NOT (NOT 1))"); // two prefixes
   }
 
   @Test public void testPrecedence3() {
@@ -2821,7 +2880,7 @@ public class SqlParserTest {
         "select ^cursor^(select * from emps) from emps",
         "CURSOR expression encountered in illegal context");
     checkFails(
-        "call p(^cursor^(select * from emps))",
+        "call list(^cursor^(select * from emps))",
         "CURSOR expression encountered in illegal context");
     checkFails(
         "select f(^cursor^(select * from emps)) from emps",
@@ -6887,11 +6946,7 @@ public class SqlParserTest {
       try {
         sqlNode = parseStmt(sql);
       } catch (SqlParseException e) {
-        e.printStackTrace();
-        String message = "Received error while parsing SQL '" + sql
-            + "'; error is:\n"
-            + e.toString();
-        throw new AssertionError(message);
+        throw new RuntimeException("Error while parsing SQL: " + sql, e);
       }
       return sqlNode;
     }
@@ -6912,10 +6967,7 @@ public class SqlParserTest {
       try {
         sqlNode = parseExpression(sql);
       } catch (SqlParseException e) {
-        String message = "Received error while parsing SQL '" + sql
-            + "'; error is:\n"
-            + e.toString();
-        throw new RuntimeException(message, e);
+        throw new RuntimeException("Error while parsing expression: " + sql, e);
       }
       return sqlNode;
     }

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java b/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java
index f9f1bd1..3c55d6d 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java
@@ -113,23 +113,16 @@ public class SqlTesterImpl implements SqlTester {
     } catch (SqlParseException e) {
       String errMessage = e.getMessage();
       if (expectedMsgPattern == null) {
-        e.printStackTrace();
-        throw new AssertionError(
-            "Error while parsing query [" + sap.sql + "]");
-      } else if (
-          (null == errMessage)
-              || !errMessage.matches(expectedMsgPattern)) {
-        e.printStackTrace();
-        throw new AssertionError(
-            "Error did not match expected ["
-                + expectedMsgPattern + "] while parsing query ["
-                + sap.sql + "]");
+        throw new RuntimeException("Error while parsing query:" + sap.sql, e);
+      } else if (errMessage == null
+          || !errMessage.matches(expectedMsgPattern)) {
+        throw new RuntimeException("Error did not match expected ["
+            + expectedMsgPattern + "] while parsing query ["
+            + sap.sql + "]", e);
       }
       return;
     } catch (Throwable e) {
-      e.printStackTrace();
-      throw new AssertionError(
-          "Error while parsing query [" + sap.sql + "]");
+      throw new RuntimeException("Error while parsing query: " + sap.sql, e);
     }
 
     Throwable thrown = null;
@@ -163,11 +156,8 @@ public class SqlTesterImpl implements SqlTester {
     SqlNode sqlNode;
     try {
       sqlNode = parseQuery(sql);
-    } catch (SqlParseException e) {
-      throw new RuntimeException("Error while parsing query [" + sql + "]", e);
     } catch (Throwable e) {
-      e.printStackTrace();
-      throw new AssertionError("Error while parsing query [" + sql + "]");
+      throw new RuntimeException("Error while parsing query: " + sql, e);
     }
     return validator.validate(sqlNode);
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
index 7211e05..7152553 100644
--- a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
+++ b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
@@ -53,6 +53,7 @@ import org.apache.calcite.util.ChunkListTest;
 import org.apache.calcite.util.ImmutableBitSetTest;
 import org.apache.calcite.util.PartiallyOrderedSetTest;
 import org.apache.calcite.util.PermutationTestCase;
+import org.apache.calcite.util.PrecedenceClimbingParserTest;
 import org.apache.calcite.util.ReflectVisitorTest;
 import org.apache.calcite.util.UtilTest;
 import org.apache.calcite.util.graph.DirectedGraphTest;
@@ -81,6 +82,7 @@ import org.junit.runners.Suite;
     RelOptUtilTest.class,
     RelCollationTest.class,
     UtilTest.class,
+    PrecedenceClimbingParserTest.class,
     MappingTest.class,
     CalciteResourceTest.class,
     FilteratorTest.class,

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 5d2ef4a..7fff2d9 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -23,6 +23,7 @@ import org.apache.calcite.config.Lex;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.test.SqlTester;
@@ -37,6 +38,7 @@ import org.apache.calcite.util.ImmutableBitSet;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
 
 import org.junit.BeforeClass;
 import org.junit.Ignore;
@@ -46,6 +48,7 @@ import org.slf4j.LoggerFactory;
 
 import java.nio.charset.Charset;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Locale;
 
@@ -7623,6 +7626,184 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
     }
   }
 
+  private static int prec(SqlOperator op) {
+    return Math.max(op.getLeftPrec(), op.getRightPrec());
+  }
+
+  /** Tests that operators, sorted by precedence, are in a sane order. Each
+   * operator has a {@link SqlOperator#getLeftPrec() left} and
+   * {@link SqlOperator#getRightPrec()} right} precedence, but we would like
+   * the order to remain the same even if we tweak particular operators'
+   * precedences. If you need to update the expected output, you might also
+   * need to change
+   * <a href="http://calcite.apache.org/docs/reference.html#operator-precedence">
+   * the documentation</a>. */
+  @Test public void testOperatorsSortedByPrecedence() {
+    final StringBuilder b = new StringBuilder();
+    final Comparator<SqlOperator> comparator =
+        new Comparator<SqlOperator>() {
+          public int compare(SqlOperator o1, SqlOperator o2) {
+            int c = Integer.compare(prec(o1), prec(o2));
+            if (c != 0) {
+              return -c;
+            }
+            c = o1.getName().compareTo(o2.getName());
+            if (c != 0) {
+              return c;
+            }
+            return o1.getSyntax().compareTo(o2.getSyntax());
+          }
+        };
+    final List<SqlOperator> operators =
+        SqlStdOperatorTable.instance().getOperatorList();
+    int p = -1;
+    for (SqlOperator op : Ordering.from(comparator).sortedCopy(operators)) {
+      final String type;
+      switch (op.getSyntax()) {
+      case FUNCTION:
+      case FUNCTION_ID:
+      case FUNCTION_STAR:
+      case INTERNAL:
+        continue;
+      case PREFIX:
+        type = "pre";
+        break;
+      case POSTFIX:
+        type = "post";
+        break;
+      case BINARY:
+        if (op.getLeftPrec() < op.getRightPrec()) {
+          type = "left";
+        } else {
+          type = "right";
+        }
+        break;
+      default:
+        if (op instanceof SqlSpecialOperator) {
+          type = "-";
+        } else {
+          continue;
+        }
+      }
+      if (prec(op) != p) {
+        b.append('\n');
+        p = prec(op);
+      }
+      b.append(op.getName())
+          .append(' ')
+          .append(type)
+          .append('\n');
+    }
+    final String expected = "\n"
+        + "ARRAY -\n"
+        + "ARRAY -\n"
+        + "COLUMN_LIST -\n"
+        + "CURSOR -\n"
+        + "LATERAL -\n"
+        + "MAP -\n"
+        + "MAP -\n"
+        + "MULTISET -\n"
+        + "MULTISET -\n"
+        + "ROW -\n"
+        + "TABLE -\n"
+        + "UNNEST -\n"
+        + "\n"
+        + "CURRENT_VALUE -\n"
+        + "DEFAULT -\n"
+        + "ITEM -\n"
+        + "NEXT_VALUE -\n"
+        + "\n"
+        + "$LiteralChain -\n"
+        + "+ pre\n"
+        + "- pre\n"
+        + ". left\n"
+        + "\n"
+        + "* left\n"
+        + "/ left\n"
+        + "/INT left\n"
+        + "|| left\n"
+        + "\n"
+        + "+ left\n"
+        + "- left\n"
+        + "- -\n"
+        + "DATETIME_PLUS -\n"
+        + "EXISTS pre\n"
+        + "\n"
+        + "BETWEEN ASYMMETRIC -\n"
+        + "BETWEEN SYMMETRIC -\n"
+        + "IN left\n"
+        + "LIKE -\n"
+        + "NOT BETWEEN ASYMMETRIC -\n"
+        + "NOT BETWEEN SYMMETRIC -\n"
+        + "NOT IN left\n"
+        + "NOT LIKE -\n"
+        + "NOT SIMILAR TO -\n"
+        + "SIMILAR TO -\n"
+        + "\n"
+        + "$IS_DIFFERENT_FROM left\n"
+        + "< left\n"
+        + "<= left\n"
+        + "<> left\n"
+        + "= left\n"
+        + "> left\n"
+        + ">= left\n"
+        + "IS DISTINCT FROM left\n"
+        + "IS NOT DISTINCT FROM left\n"
+        + "MEMBER OF left\n"
+        + "OVERLAPS -\n"
+        + "SUBMULTISET OF left\n"
+        + "\n"
+        + "IS A SET post\n"
+        + "IS FALSE post\n"
+        + "IS NOT FALSE post\n"
+        + "IS NOT NULL post\n"
+        + "IS NOT TRUE post\n"
+        + "IS NOT UNKNOWN post\n"
+        + "IS NULL post\n"
+        + "IS TRUE post\n"
+        + "IS UNKNOWN post\n"
+        + "\n"
+        + "NOT pre\n"
+        + "\n"
+        + "AND left\n"
+        + "\n"
+        + "OR left\n"
+        + "\n"
+        + "=> -\n"
+        + "AS -\n"
+        + "DESC post\n"
+        + "OVER left\n"
+        + "TABLESAMPLE -\n"
+        + "\n"
+        + "INTERSECT left\n"
+        + "INTERSECT ALL left\n"
+        + "MULTISET INTERSECT left\n"
+        + "MULTISET INTERSECT ALL left\n"
+        + "NULLS FIRST post\n"
+        + "NULLS LAST post\n"
+        + "\n"
+        + "EXCEPT left\n"
+        + "EXCEPT ALL left\n"
+        + "MULTISET EXCEPT left\n"
+        + "MULTISET EXCEPT ALL left\n"
+        + "MULTISET UNION left\n"
+        + "MULTISET UNION ALL left\n"
+        + "UNION left\n"
+        + "UNION ALL left\n"
+        + "\n"
+        + "$throw -\n"
+        + "EXTRACT_DATE -\n"
+        + "FILTER left\n"
+        + "Reinterpret -\n"
+        + "TABLE pre\n"
+        + "VALUES -\n"
+        + "\n"
+        + "CALL pre\n"
+        + "ESCAPE -\n"
+        + "NEW pre\n";
+    assertThat(b.toString(), is(expected));
+  }
+
   /** Tests that it is an error to insert into the same column twice, even using
    * case-insensitive matching. */
   @Test public void testCaseInsensitiveInsert() {

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/util/PrecedenceClimbingParserTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/util/PrecedenceClimbingParserTest.java b/core/src/test/java/org/apache/calcite/util/PrecedenceClimbingParserTest.java
new file mode 100644
index 0000000..3fbeac4
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/util/PrecedenceClimbingParserTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.util;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Unit test for {@link PrecedenceClimbingParser}.
+ */
+public class PrecedenceClimbingParserTest {
+  @Test public void testBasic() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .infix("+", 1, true)
+        .prefix("-", 3)
+        .atom("b")
+        .infix("*", 2, true)
+        .atom("c")
+        .postfix("!", 4)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("(a + ((- b) * (c !)))"));
+  }
+
+  @Test public void testRepeatedPrefixPostfix() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .prefix("+", 3)
+        .prefix("-", 3)
+        .prefix("+", 3)
+        .prefix("+", 3)
+        .atom("a")
+        .postfix("!", 4)
+        .infix("+", 1, true)
+        .prefix("-", 3)
+        .prefix("-", 3)
+        .atom("b")
+        .postfix("!", 4)
+        .postfix("!", 4)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token),
+        is("((+ (- (+ (+ (a !))))) + (- (- ((b !) !))))"));
+  }
+
+  @Test public void testAtom() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("a"));
+  }
+
+  @Test public void testOnlyPrefix() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .prefix("-", 3)
+        .prefix("-", 3)
+        .atom(1)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("(- (- 1))"));
+  }
+
+  @Test public void testOnlyPostfix() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom(1)
+        .postfix("!", 33333)
+        .postfix("!", 33333)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("((1 !) !)"));
+  }
+
+  @Test public void testLeftAssociative() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .infix("*", 2, true)
+        .atom("b")
+        .infix("+", 1, true)
+        .atom("c")
+        .infix("+", 1, true)
+        .atom("d")
+        .infix("+", 1, true)
+        .atom("e")
+        .infix("*", 2, true)
+        .atom("f")
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("((((a * b) + c) + d) + (e * f))"));
+  }
+
+  @Test public void testRightAssociative() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .infix("^", 3, false)
+        .atom("b")
+        .infix("^", 3, false)
+        .atom("c")
+        .infix("^", 3, false)
+        .atom("d")
+        .infix("+", 1, true)
+        .atom("e")
+        .infix("*", 2, true)
+        .atom("f")
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("((a ^ (b ^ (c ^ d))) + (e * f))"));
+  }
+
+  @Test public void testSpecial() {
+    // price > 5 and price between 1 + 2 and 3 * 4 and price is null
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("price")
+        .infix(">", 4, true)
+        .atom("5")
+        .infix("and", 2, true)
+        .atom("price")
+        .special("between", 3, 3,
+            new PrecedenceClimbingParser.Special() {
+              public PrecedenceClimbingParser.Result apply(
+                  PrecedenceClimbingParser parser,
+                  PrecedenceClimbingParser.SpecialOp op) {
+                return new PrecedenceClimbingParser.Result(op.previous,
+                    op.next.next.next,
+                    parser.call(op,
+                        ImmutableList.of(op.previous, op.next,
+                            op.next.next.next)));
+              }
+            })
+        .atom("1")
+        .infix("+", 5, true)
+        .atom("2")
+        .infix("and", 2, true)
+        .atom("3")
+        .infix("*", 6, true)
+        .atom("4")
+        .infix("and", 2, true)
+        .atom("price")
+        .postfix("is null", 4)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token),
+        is("(((price > 5) and between(price, (1 + 2), (3 * 4)))"
+            + " and (price is null))"));
+  }
+
+  @Test public void testEqualPrecedence() {
+    // LIKE has same precedence as '='; LIKE is right-assoc, '=' is left
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .infix("=", 3, true)
+        .atom("b")
+        .infix("like", 3, false)
+        .atom("c")
+        .infix("=", 3, true)
+        .atom("d")
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("(((a = b) like c) = d)"));
+  }
+}
+
+// End PrecedenceClimbingParserTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/resources/sql/operator.iq
----------------------------------------------------------------------
diff --git a/core/src/test/resources/sql/operator.iq b/core/src/test/resources/sql/operator.iq
new file mode 100644
index 0000000..626d31d
--- /dev/null
+++ b/core/src/test/resources/sql/operator.iq
@@ -0,0 +1,69 @@
+# operator.iq - expressions involving operators
+#
+# 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.
+#
+!use scott
+!set outputformat mysql
+
+# [CALCITE-1095] NOT precedence
+select * from "scott".emp where not sal > 1300;
++-------+--------+----------+------+------------+---------+---------+--------+
+| EMPNO | ENAME  | JOB      | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO |
++-------+--------+----------+------+------------+---------+---------+--------+
+|  7369 | SMITH  | CLERK    | 7902 | 1980-12-17 |  800.00 |         |     20 |
+|  7521 | WARD   | SALESMAN | 7698 | 1981-02-22 | 1250.00 |  500.00 |     30 |
+|  7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 |     30 |
+|  7876 | ADAMS  | CLERK    | 7788 | 1987-05-23 | 1100.00 |         |     20 |
+|  7900 | JAMES  | CLERK    | 7698 | 1981-12-03 |  950.00 |         |     30 |
+|  7934 | MILLER | CLERK    | 7782 | 1982-01-23 | 1300.00 |         |     10 |
++-------+--------+----------+------+------------+---------+---------+--------+
+(6 rows)
+
+!ok
+
+select count(*) as c from "scott".emp where not ename = 'SMITH';
++----+
+| C  |
++----+
+| 13 |
++----+
+(1 row)
+
+!ok
+
+select count(*) as c from "scott".emp where not not ename = 'SMITH';
++---+
+| C |
++---+
+| 1 |
++---+
+(1 row)
+
+!ok
+
+select * from "scott".emp where not sal > 1300 and not sal < 1200;
++-------+--------+----------+------+------------+---------+---------+--------+
+| EMPNO | ENAME  | JOB      | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO |
++-------+--------+----------+------+------------+---------+---------+--------+
+|  7521 | WARD   | SALESMAN | 7698 | 1981-02-22 | 1250.00 |  500.00 |     30 |
+|  7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 |     30 |
+|  7934 | MILLER | CLERK    | 7782 | 1982-01-23 | 1300.00 |         |     10 |
++-------+--------+----------+------+------------+---------+---------+--------+
+(3 rows)
+
+!ok
+
+# End operator.iq

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/site/_docs/reference.md
----------------------------------------------------------------------
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 8f9fd16..428ef83 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -831,6 +831,24 @@ Note:
 
 ## Operators and functions
 
+### Operator precedence
+
+The operator precedence and associativity, highest to lowest.
+
+| Operator                                          | Associativity
+|:------------------------------------------------- |:-------------
+| .                                                 | left
+| [ ] (array element)                               | left
+| + - (unary plus, minus)                           | right
+| * /                                               | left
+| + -                                               | left
+| BETWEEN, IN, LIKE, SIMILAR                        | -
+| < > = <= >= <>                                    | left
+| IS NULL, IS FALSE, IS NOT TRUE etc.               | -
+| NOT                                               | right
+| AND                                               | left
+| OR                                                | left
+
 ### Comparison operators
 
 | Operator syntax                                   | Description