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 2015/01/01 17:15:14 UTC

[1/2] incubator-calcite git commit: [CALCITE-356] Allow column references of the form schema.table.column

Repository: incubator-calcite
Updated Branches:
  refs/heads/master 0fda7afc8 -> 6c88546ea


http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 069ba82..ee72ffb 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
@@ -22,7 +22,9 @@ import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlWindow;
+import org.apache.calcite.util.Pair;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -51,12 +53,12 @@ public interface SqlValidatorScope {
   /**
    * Looks up a node with a given name. Returns null if none is found.
    *
-   * @param name        Name of node to find
+   * @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
    */
   SqlValidatorNamespace resolve(
-      String name,
+      List<String> names,
       SqlValidatorScope[] ancestorOut,
       int[] offsetOut);
 
@@ -72,10 +74,9 @@ public interface SqlValidatorScope {
    *
    * @param columnName Column name
    * @param ctx        Validation context, to appear in any error thrown
-   * @return Table alias
+   * @return Table alias and namespace
    */
-  String findQualifyingTableName(
-      String columnName,
+  Pair<String, SqlValidatorNamespace> findQualifyingTableName(String columnName,
       SqlNode ctx);
 
   /**
@@ -91,14 +92,16 @@ public interface SqlValidatorScope {
    *
    * @param result a list of monikers to add the result to
    */
-  void findAliases(List<SqlMoniker> result);
+  void findAliases(Collection<SqlMoniker> result);
 
   /**
    * Converts an identifier into a fully-qualified identifier. For example,
    * the "empno" in "select empno from emp natural join dept" becomes
    * "emp.empno".
+   *
+   * @return A qualified identifier, never null
    */
-  SqlIdentifier fullyQualify(SqlIdentifier identifier);
+  SqlQualified fullyQualify(SqlIdentifier identifier);
 
   /**
    * Registers a relation in this scope.

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 2ae4af5..b872b1d 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
@@ -268,7 +268,7 @@ public class SqlValidatorUtil {
     for (int i = 0; i < names.size(); i++) {
       String name = names.get(i);
       if (i == 0) {
-        namespace = scope.resolve(name, null, null);
+        namespace = scope.resolve(ImmutableList.of(name), null, null);
       } else {
         namespace = namespace.lookupChild(name);
       }
@@ -283,15 +283,17 @@ public class SqlValidatorUtil {
       List<SqlMoniker> hints) {
     // Assume that the last name is 'dummy' or similar.
     List<String> subNames = Util.skipLast(names);
-    hints.addAll(catalogReader.getAllSchemaObjectNames(subNames));
 
-    // If the name has length 0, try prepending the name of the default
-    // schema. So, the empty name would yield a list of tables in the
-    // default schema, as well as a list of schemas from the above code.
-    if (subNames.size() == 0) {
-      hints.addAll(
-          catalogReader.getAllSchemaObjectNames(
-              catalogReader.getSchemaName()));
+    // Try successively with catalog.schema, catalog and no prefix
+    List<String> x = catalogReader.getSchemaName();
+    for (;;) {
+      final List<String> names2 =
+          ImmutableList.<String>builder().addAll(x).addAll(subNames).build();
+      hints.addAll(catalogReader.getAllSchemaObjectNames(names2));
+      if (x.isEmpty()) {
+        break;
+      }
+      x = Util.skipLast(x);
     }
   }
 
@@ -478,7 +480,7 @@ public class SqlValidatorUtil {
       final SqlValidatorScope[] ancestorScopes = {null};
       SqlValidatorNamespace foundNs =
           scope.resolve(
-              originalRelName,
+              ImmutableList.of(originalRelName),
               ancestorScopes,
               nsIndexes);
 
@@ -607,7 +609,7 @@ public class SqlValidatorUtil {
     }
 
     public SqlNode visit(SqlIdentifier id) {
-      return getScope().fullyQualify(id);
+      return getScope().fullyQualify(id).identifier;
     }
 
     public SqlNode visit(SqlDataTypeSpec type) {

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorWithHints.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorWithHints.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorWithHints.java
index d131d94..d796f1e 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorWithHints.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorWithHints.java
@@ -31,7 +31,7 @@ public interface SqlValidatorWithHints extends SqlValidator {
   //~ Methods ----------------------------------------------------------------
 
   /**
-   * Looks up completion hints for a syntatically correct SQL statement that
+   * Looks up completion hints for a syntactically correct SQL statement that
    * has been parsed into an expression tree. (Note this should be called
    * after {@link #validate(org.apache.calcite.sql.SqlNode)}.
    *
@@ -53,7 +53,7 @@ public interface SqlValidatorWithHints extends SqlValidator {
    * Parser Position in a parsed expression tree Note: call this only after
    * {@link #validate} has been called.
    *
-   * @param topNode top of expression tree in which to lookup the qualfied
+   * @param topNode top of expression tree in which to lookup the qualified
    *                name for the SqlIdentifier
    * @param pos indicates the position of the {@link SqlIdentifier} in
    *                the SQL statement we want to get the qualified

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 1866d7d..b650402 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
@@ -53,13 +53,13 @@ class WithScope extends ListScope {
     return super.getTableNamespace(names);
   }
 
-  @Override public SqlValidatorNamespace resolve(String name,
+  @Override public SqlValidatorNamespace resolve(List<String> names,
       SqlValidatorScope[] ancestorOut,
       int[] offsetOut) {
-    if (name.equals(withItem.name.getSimple())) {
+    if (names.equals(withItem.name.getSimple())) {
       return validator.getNamespace(withItem);
     }
-    return super.resolve(name, ancestorOut, offsetOut);
+    return super.resolve(names, ancestorOut, offsetOut);
   }
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 09b074d..33fc891 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -123,6 +123,7 @@ import org.apache.calcite.sql.validate.ListScope;
 import org.apache.calcite.sql.validate.ParameterScope;
 import org.apache.calcite.sql.validate.SelectScope;
 import org.apache.calcite.sql.validate.SqlMonotonicity;
+import org.apache.calcite.sql.validate.SqlQualified;
 import org.apache.calcite.sql.validate.SqlUserDefinedTableFunction;
 import org.apache.calcite.sql.validate.SqlUserDefinedTableMacro;
 import org.apache.calcite.sql.validate.SqlValidator;
@@ -1233,7 +1234,7 @@ public class SqlToRelConverter {
       boolean isExists) {
     SqlCall call = (SqlBasicCall) subQuery.node;
     if (subqueryConverter.canConvertSubquery()
-        && isSubqNonCorrelated(converted, bb)) {
+        && isSubQueryNonCorrelated(converted, bb)) {
       // First check if the subquery has already been converted
       // because it's a nested subquery.  If so, don't re-evaluate
       // it again.
@@ -2070,7 +2071,7 @@ public class SqlToRelConverter {
         final SqlValidatorScope[] ancestorScopes = {null};
         SqlValidatorNamespace foundNs =
             lookup.bb.scope.resolve(
-                originalRelName,
+                ImmutableList.of(originalRelName),
                 ancestorScopes,
                 nsIndexes);
 
@@ -2368,7 +2369,7 @@ public class SqlToRelConverter {
    *             blackboard of the parent query of this subquery
    * @return true if the subquery is non-correlated.
    */
-  private boolean isSubqNonCorrelated(RelNode subq, Blackboard bb) {
+  private boolean isSubQueryNonCorrelated(RelNode subq, Blackboard bb) {
     Set<String> correlatedVariables = RelOptUtil.getVariablesUsed(subq);
     for (String correlName : correlatedVariables) {
       DeferredLookup lookup = mapCorrelToDeferred.get(correlName);
@@ -2378,7 +2379,7 @@ public class SqlToRelConverter {
       final SqlValidatorScope[] ancestorScopes = {null};
       SqlValidatorNamespace foundNs =
           lookup.bb.scope.resolve(
-              originalRelName,
+              ImmutableList.of(originalRelName),
               ancestorScopes,
               nsIndexes);
 
@@ -3302,12 +3303,14 @@ public class SqlToRelConverter {
           + "' is not a group expr");
     }
 
-    SqlValidatorNamespace namespace = null;
+    final SqlQualified qualified;
     if (bb.scope != null) {
-      identifier = bb.scope.fullyQualify(identifier);
-      namespace = bb.scope.resolve(identifier.names.get(0), null, null);
+      qualified = bb.scope.fullyQualify(identifier);
+      identifier = qualified.identifier;
+    } else {
+      qualified = SqlQualified.create(null, 1, null, identifier);
     }
-    RexNode e = bb.lookupExp(identifier.names.get(0));
+    RexNode e = bb.lookupExp(qualified);
     final String correlationName;
     if (e instanceof RexCorrelVariable) {
       correlationName = ((RexCorrelVariable) e).getName();
@@ -3315,11 +3318,7 @@ public class SqlToRelConverter {
       correlationName = null;
     }
 
-    for (String name : Util.skip(identifier.names)) {
-      if (namespace != null) {
-        name = namespace.translate(name);
-        namespace = null;
-      }
+    for (String name : qualified.suffixTranslated()) {
       final boolean caseSensitive = true; // name already fully-qualified
       e = rexBuilder.makeFieldAccess(e, name, caseSensitive);
     }
@@ -3970,23 +3969,23 @@ public class SqlToRelConverter {
     /**
      * Returns an expression with which to reference a from-list item.
      *
-     * @param name the alias of the from item
+     * @param qualified the alias of the from item
      * @return a {@link RexFieldAccess} or {@link RexRangeRef}, or null if
      * not found
      */
-    RexNode lookupExp(String name) {
-      if (nameToNodeMap != null) {
-        RexNode node = nameToNodeMap.get(name);
+    RexNode lookupExp(SqlQualified qualified) {
+      if (nameToNodeMap != null && qualified.prefixLength == 1) {
+        RexNode node = nameToNodeMap.get(qualified.identifier.names.get(0));
         if (node == null) {
-          throw Util.newInternal("Unknown identifier '" + name
-              + "' encountered while expanding expression" + node);
+          throw Util.newInternal("Unknown identifier '" + qualified.identifier
+              + "' encountered while expanding expression");
         }
         return node;
       }
       int[] offsets = {-1};
       final SqlValidatorScope[] ancestorScopes = {null};
       SqlValidatorNamespace foundNs =
-          scope.resolve(name, ancestorScopes, offsets);
+          scope.resolve(qualified.prefix(), ancestorScopes, offsets);
       if (foundNs == null) {
         return null;
       }
@@ -4007,7 +4006,8 @@ public class SqlToRelConverter {
         // e.g. "select from emp as emp join emp.getDepts() as dept".
         // Create a temporary expression.
         assert isParent;
-        DeferredLookup lookup = new DeferredLookup(this, name);
+        DeferredLookup lookup =
+            new DeferredLookup(this, qualified.identifier.names.get(0));
         String correlName = createCorrel();
         mapCorrelToDeferred.put(correlName, lookup);
         final RelDataType rowType = foundNs.getRowType();

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/util/Bug.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/Bug.java b/core/src/main/java/org/apache/calcite/util/Bug.java
index 6d9db47..c91cde4 100644
--- a/core/src/main/java/org/apache/calcite/util/Bug.java
+++ b/core/src/main/java/org/apache/calcite/util/Bug.java
@@ -108,13 +108,6 @@ public abstract class Bug {
   public static final boolean FRG78_FIXED = false;
 
   /**
-   * Whether <a href="http://issues.eigenbase.org/browse/FRG-140">issue
-   * FRG-140: validator does not accept column qualified by schema name</a> is
-   * fixed.
-   */
-  public static final boolean FRG140_FIXED = false;
-
-  /**
    * Whether <a href="http://issues.eigenbase.org/browse/FRG-187">issue
    * FRG-187: FarragoAutoVmOperatorTest.testOverlapsOperator fails</a> is
    * fixed.

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 a100315..80fe7ee 100644
--- a/core/src/main/java/org/apache/calcite/util/Util.java
+++ b/core/src/main/java/org/apache/calcite/util/Util.java
@@ -2019,23 +2019,27 @@ public class Util {
     return -1;
   }
 
-  public static int match2(List<String> strings, String name,
-      boolean caseSensitive1) {
-    if (caseSensitive1) {
-      return strings.indexOf(name);
+  /** Looks for a string within a list of strings, using a given
+   * case-sensitivity policy, and returns the position at which the first match
+   * is found, or -1 if there are no matches. */
+  public static int findMatch(List<String> strings, String seek,
+      boolean caseSensitive) {
+    if (caseSensitive) {
+      return strings.indexOf(seek);
     }
     for (int i = 0; i < strings.size(); i++) {
       String s = strings.get(i);
-      if (s.equalsIgnoreCase(name)) {
+      if (s.equalsIgnoreCase(seek)) {
         return i;
       }
     }
     return -1;
   }
 
-  public static boolean match(boolean caseSensitive, String name1,
-      String name0) {
-    return caseSensitive ? name0.equals(name1) : name0.equalsIgnoreCase(name1);
+  /** Returns whether a name matches another according to a given
+   * case-sensitivity policy. */
+  public static boolean matches(boolean caseSensitive, String s0, String s1) {
+    return caseSensitive ? s1.equals(s0) : s1.equalsIgnoreCase(s0);
   }
 
   /** Returns whether one list is a prefix of another. */

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 d9a5e0f..adcaf93 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
@@ -1385,6 +1385,27 @@ public class SqlParserTest {
             + "`DEPT`");
   }
 
+  @Test public void testSchemaTableStar() {
+    sql("select schem.emp.*, emp.empno * dept.deptno\n"
+            + "from schem.emp, dept")
+        .ok("SELECT `SCHEM`.`EMP`.*, (`EMP`.`EMPNO` * `DEPT`.`DEPTNO`)\n"
+                + "FROM `SCHEM`.`EMP`,\n"
+                + "`DEPT`");
+  }
+
+  @Test public void testCatalogSchemaTableStar() {
+    sql("select cat.schem.emp.* from cat.schem.emp")
+        .ok("SELECT `CAT`.`SCHEM`.`EMP`.*\n"
+                + "FROM `CAT`.`SCHEM`.`EMP`");
+  }
+
+  @Test public void testAliasedStar() {
+    // OK in parser; validator will give error
+    sql("select emp.* as foo from emp")
+        .ok("SELECT `EMP`.* AS `FOO`\n"
+                + "FROM `EMP`");
+  }
+
   @Test public void testNotExists() {
     check(
         "select * from dept where not not exists (select * from emp) and true",

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 bd88a5c..65e24a6 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
@@ -63,28 +63,19 @@ public class SqlAdvisorTest extends SqlValidatorTestCase {
           "KEYWORD(TABLE)",
           "KEYWORD(UNNEST)");
 
-  protected static final List<String> AGG_KEYWORDS =
-      Arrays.asList(
-          "KEYWORD(SELECT)",
-          "KEYWORD(TABLE)",
-          "KEYWORD(VALUES)",
-          "KEYWORD())",
-          "KEYWORD(*)",
-          "KEYWORD(ALL)",
-          "KEYWORD(DISTINCT)");
-
   protected static final List<String> SALES_TABLES =
       Arrays.asList(
-          "TABLE(EMP)",
-          "TABLE(EMP_ADDRESS)",
-          "TABLE(DEPT)",
-          "TABLE(BONUS)",
-          "TABLE(SALGRADE)");
+          "TABLE(CATALOG.SALES.EMP)",
+          "TABLE(CATALOG.SALES.EMP_ADDRESS)",
+          "TABLE(CATALOG.SALES.DEPT)",
+          "TABLE(CATALOG.SALES.BONUS)",
+          "TABLE(CATALOG.SALES.SALGRADE)");
 
   private static final List<String> SCHEMAS =
       Arrays.asList(
-          "SCHEMA(SALES)",
-          "SCHEMA(CUSTOMER)");
+          "CATALOG(CATALOG)",
+          "SCHEMA(CATALOG.SALES)",
+          "SCHEMA(CATALOG.CUSTOMER)");
 
   private static final List<String> AB_TABLES =
       Arrays.asList(

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/test/java/org/apache/calcite/test/JdbcTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 52b3b01..561ede1 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -377,34 +377,35 @@ public class JdbcTest {
    */
   @Test public void testSqlAdvisorGetHintsFunction()
       throws SQLException, ClassNotFoundException {
-    String res = adviseSql("select e.e^ from \"emps\" e");
-    assertThat(res,
-        equalTo("id=e; names=null; type=MATCH\n"
-            + "id=empid; names=[empid]; type=COLUMN\n"));
+    adviseSql("select e.e^ from \"emps\" e",
+        CalciteAssert.checkResultUnordered(
+            "id=e; names=null; type=MATCH",
+            "id=empid; names=[empid]; type=COLUMN"));
   }
+
   /**
    * Tests {@link org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction}.
    */
   @Test public void testSqlAdvisorSchemaNames()
       throws SQLException, ClassNotFoundException {
-    String res = adviseSql("select empid from \"emps\" e, ^");
-    assertThat(res,
-        equalTo("id=; names=null; type=MATCH\n"
-            + "id=(; names=[(]; type=KEYWORD\n"
-            + "id=LATERAL; names=[LATERAL]; type=KEYWORD\n"
-            + "id=TABLE; names=[TABLE]; type=KEYWORD\n"
-            + "id=UNNEST; names=[UNNEST]; type=KEYWORD\n"
-            + "id=hr; names=[hr]; type=SCHEMA\n"
-            + "id=metadata; names=[metadata]; type=SCHEMA\n"
-            + "id=s; names=[s]; type=SCHEMA\n"
-            + "id=hr.dependents; names=[hr, dependents]; type=TABLE\n"
-            + "id=hr.depts; names=[hr, depts]; type=TABLE\n"
-            + "id=hr.emps; names=[hr, emps]; type=TABLE\n"
-            + "id=hr.locations; names=[hr, locations]; type=TABLE\n"));
-  }
-
-  private String adviseSql(String sql) throws ClassNotFoundException,
-      SQLException {
+    adviseSql("select empid from \"emps\" e, ^",
+        CalciteAssert.checkResultUnordered(
+            "id=; names=null; type=MATCH",
+            "id=(; names=[(]; type=KEYWORD",
+            "id=LATERAL; names=[LATERAL]; type=KEYWORD",
+            "id=TABLE; names=[TABLE]; type=KEYWORD",
+            "id=UNNEST; names=[UNNEST]; type=KEYWORD",
+            "id=hr; names=[hr]; type=SCHEMA",
+            "id=metadata; names=[metadata]; type=SCHEMA",
+            "id=s; names=[s]; type=SCHEMA",
+            "id=hr.dependents; names=[hr, dependents]; type=TABLE",
+            "id=hr.depts; names=[hr, depts]; type=TABLE",
+            "id=hr.emps; names=[hr, emps]; type=TABLE",
+            "id=hr.locations; names=[hr, locations]; type=TABLE"));
+  }
+
+  private void adviseSql(String sql, Function<ResultSet, Void> checker)
+      throws ClassNotFoundException, SQLException {
     Properties info = new Properties();
     info.put("lex", "JAVA");
     info.put("quoting", "DOUBLE_QUOTE");
@@ -424,7 +425,10 @@ public class JdbcTest {
     SqlParserUtil.StringAndPos sap = SqlParserUtil.findPos(sql);
     ps.setString(1, sap.sql);
     ps.setInt(2, sap.cursor);
-    return CalciteAssert.toString(ps.executeQuery());
+    final ResultSet resultSet = ps.executeQuery();
+    checker.apply(resultSet);
+    resultSet.close();
+    connection.close();
   }
 
   /**

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 f070f3d..adf4565 100644
--- a/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
+++ b/core/src/test/java/org/apache/calcite/test/MockCatalogReader.java
@@ -262,16 +262,26 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     List<SqlMoniker> result;
     switch (names.size()) {
     case 0:
+      // looking for catalog and schema names
+      return ImmutableList.<SqlMoniker>builder()
+          .add(new SqlMonikerImpl(DEFAULT_CATALOG, SqlMonikerType.CATALOG))
+          .addAll(getAllSchemaObjectNames(ImmutableList.of(DEFAULT_CATALOG)))
+          .build();
+    case 1:
       // looking for schema names
       result = new ArrayList<SqlMoniker>();
       for (MockSchema schema : schemas.values()) {
-        result.add(
-            new SqlMonikerImpl(schema.name, SqlMonikerType.SCHEMA));
+        final String catalogName = names.get(0);
+        if (schema.getCatalogName().equals(catalogName)) {
+          final ImmutableList<String> names1 =
+              ImmutableList.of(catalogName, schema.name);
+          result.add(new SqlMonikerImpl(names1, SqlMonikerType.SCHEMA));
+        }
       }
       return result;
-    case 1:
+    case 2:
       // looking for table names in the given schema
-      MockSchema schema = schemas.get(names.get(0));
+      MockSchema schema = schemas.get(names.get(1));
       if (schema == null) {
         return Collections.emptyList();
       }
@@ -279,7 +289,8 @@ public class MockCatalogReader implements Prepare.CatalogReader {
       for (String tableName : schema.tableNames) {
         result.add(
             new SqlMonikerImpl(
-                tableName,
+                ImmutableList.of(schema.getCatalogName(), schema.name,
+                    tableName),
                 SqlMonikerType.TABLE));
       }
       return result;
@@ -289,7 +300,31 @@ public class MockCatalogReader implements Prepare.CatalogReader {
   }
 
   public List<String> getSchemaName() {
-    return Collections.singletonList(DEFAULT_SCHEMA);
+    return ImmutableList.of(DEFAULT_CATALOG, DEFAULT_SCHEMA);
+  }
+
+  private MockSchema getMockSchema(List<String> names) {
+    return schemas.get(names.get(0));
+  }
+
+  public List<SqlMoniker> getAllSchemaObjectNames2(List<String> names) {
+    List<SqlMoniker> result = new ArrayList<SqlMoniker>();
+    if (names.isEmpty()) {
+      for (MockSchema schema : schemas.values()) {
+        result.add(
+            new SqlMonikerImpl(schema.name, SqlMonikerType.SCHEMA));
+      }
+    }
+    // looking for table names in the given schema
+    MockSchema schema = getMockSchema(names);
+    if (schema != null) {
+      for (String tableName : schema.tableNames) {
+        result.add(
+            new SqlMonikerImpl(
+                tableName, SqlMonikerType.TABLE));
+      }
+    }
+    return result;
   }
 
   public RelDataTypeField field(RelDataType rowType, String alias) {
@@ -301,8 +336,12 @@ public class MockCatalogReader implements Prepare.CatalogReader {
     return field != null ? field.getIndex() : -1;
   }
 
+  public boolean matches(String string, String name) {
+    return Util.matches(caseSensitive, string, name);
+  }
+
   public int match(List<String> strings, String name) {
-    return Util.match2(strings, name, caseSensitive);
+    return Util.findMatch(strings, name, caseSensitive);
   }
 
   public RelDataType createTypeFromProjection(final RelDataType type,

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 125d393..a1becf6 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -4451,6 +4451,43 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         "Unknown identifier 'EMPNO'");
   }
 
+  @Test public void testStarAliasFails() {
+    sql("select emp.^*^ AS x from emp")
+        .fails("Unknown field '\\*'");
+  }
+
+  @Test public void testNonLocalStar() {
+    // MySQL allows this but we can't, currently
+    sql("select * from emp e where exists (\n"
+        + "  select ^e^.* from dept where dept.deptno = e.deptno)")
+        .fails("Unknown identifier 'E'");
+  }
+
+  /**
+   * Parser allows "*" in FROM clause because "*" can occur in any identifier.
+   * But validator must not.
+   *
+   * <p>See also
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-546">[CALCITE-546]
+   * "Allow table, column and field called '*'"</a> (not yet fixed).
+   */
+  @Test public void testStarInFromFails() {
+    sql("select emp.empno AS x from ^sales.*^")
+        .fails("Table 'SALES.\\*' not found");
+    sql("select emp.empno from emp where emp.^*^ is not null")
+        .fails("Unknown field '\\*'");
+  }
+
+  @Test public void testStarDotIdFails() {
+    // Parser allows a star inside (not at end of) compound identifier, but
+    // validator does not
+    sql("select emp.^*^.foo from emp")
+        .fails("Column '\\*' not found in table 'EMP'");
+    // Parser does not allow star dot identifier.
+    sql("select ^*^.foo from emp")
+        .fails("(?s).*Encountered \".\" at .*");
+  }
+
   @Test public void testAsColumnList() {
     check("select d.a, b from dept as d(a, b)");
     checkFails(
@@ -4600,6 +4637,55 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         + "  select 1 from emp where emp.empno = emp.deptno)");
   }
 
+  @Test public void testSchemaTableStar() {
+    sql("select ^sales.e^.* from sales.emp as e")
+        .fails("Unknown identifier 'SALES\\.E'");
+    sql("select sales.dept.* from sales.dept")
+        .type("RecordType(INTEGER NOT NULL DEPTNO,"
+            + " VARCHAR(10) NOT NULL NAME) NOT NULL");
+    sql("select sales.emp.* from emp").ok();
+    sql("select sales.emp.* from emp as emp").ok();
+    // MySQL gives: "Unknown table 'emp'"
+    // (consistent with MySQL)
+    sql("select ^sales.emp^.* from emp as e")
+        .fails("Unknown identifier 'SALES.EMP'");
+  }
+
+  @Test public void testSchemaTableColumn() {
+    sql("select emp.empno from sales.emp").ok();
+    sql("select sales.emp.empno from sales.emp").ok();
+    sql("select sales.emp.empno from sales.emp\n"
+        + "where sales.emp.deptno > 0").ok();
+    sql("select 1 from sales.emp where sales.emp.^bad^ < 0")
+        .fails("Column 'BAD' not found in table 'SALES.EMP'");
+    sql("select ^sales.bad^.empno from sales.emp\n"
+        + "where sales.emp.deptno > 0")
+        .fails("Table 'SALES\\.BAD' not found");
+    sql("select sales.emp.deptno from sales.emp").ok();
+    sql("select 1 from sales.emp where sales.emp.deptno = 10").ok();
+    sql("select 1 from sales.emp order by sales.emp.deptno").ok();
+    // alias does not hide the fully-qualified name if same
+    // (consistent with MySQL)
+    sql("select sales.emp.deptno from sales.emp as emp").ok();
+    // alias hides the fully-qualified name
+    // (consistent with MySQL)
+    sql("select ^sales.emp^.deptno from sales.emp as e")
+        .fails("Table 'SALES\\.EMP' not found");
+    sql("select sales.emp.deptno from sales.emp, ^sales.emp^")
+        .fails("Duplicate relation name 'EMP' in FROM clause");
+    // Table exists but not used in FROM clause
+    sql("select ^sales.emp^.deptno from sales.dept as d1, sales.dept")
+        .fails("Table 'SALES.EMP' not found");
+    // Table does not exist
+    sql("select ^sales.bad^.deptno from sales.dept as d1, sales.dept")
+        .fails("Table 'SALES.BAD' not found");
+  }
+
+  @Ignore("does not work yet")
+  @Test public void testSchemaTableColumnInGroupBy() {
+    sql("select 1 from sales.emp group by sales.emp.deptno").ok(); // TODO:
+  }
+
   @Test public void testInvalidGroupBy() {
     sql("select ^empno^, deptno from emp group by deptno")
         .fails("Expression 'EMPNO' is not being grouped");
@@ -6397,11 +6483,9 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         "RecordType(INTEGER NOT NULL X, INTEGER NOT NULL Y) NOT NULL");
 
     // Qualifying with schema is OK.
-    if (Bug.FRG140_FIXED) {
-      checkResultType(
-          "SELECT customer.contact.coord.x, customer.contact.email, contact.coord.y FROM customer.contact",
-          "RecordType(INTEGER NOT NULL X, INTEGER NOT NULL Y) NOT NULL");
-    }
+    checkResultType(
+        "SELECT customer.contact.coord.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");
   }
 
   @Test public void testSample() {

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/test/resources/sql/misc.oq
----------------------------------------------------------------------
diff --git a/core/src/test/resources/sql/misc.oq b/core/src/test/resources/sql/misc.oq
index ae1b2ea..21eace7 100644
--- a/core/src/test/resources/sql/misc.oq
+++ b/core/src/test/resources/sql/misc.oq
@@ -18,6 +18,21 @@
 !use post
 !set outputformat mysql
 
+# [CALCITE-356] Allow column references of the form schema.table.column
+select "hr"."emps"."empid"
+from "hr"."emps";
++-------+
+| empid |
++-------+
+|   100 |
+|   110 |
+|   150 |
+|   200 |
++-------+
+(4 rows)
+
+!ok
+
 # CALCITE-307 CAST(timestamp AS DATE) gives ClassCastException
 # Based on DRILL-1051
 !if (false) {


[2/2] incubator-calcite git commit: [CALCITE-356] Allow column references of the form schema.table.column

Posted by jh...@apache.org.
[CALCITE-356] Allow column references of the form schema.table.column

SqlValidatorScope.fullyQualify now returns a new class SqlQualified, which contains more information than just name components after an identifier has been fully-qualified in a scope. We plan to cache these, at which point we should be able to make sql-to-rel translation faster and more predictable, since it builds on work already done by the validator.


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

Branch: refs/heads/master
Commit: 6c88546ea8a0deb544df57536ed24466fbb46b85
Parents: 0fda7af
Author: Julian Hyde <jh...@apache.org>
Authored: Thu Dec 18 13:24:37 2014 -0800
Committer: julianhyde <jh...@apache.org>
Committed: Wed Dec 31 20:35:59 2014 -0800

----------------------------------------------------------------------
 core/src/main/codegen/templates/Parser.jj       |  36 +--
 .../calcite/prepare/CalciteCatalogReader.java   |   6 +-
 .../calcite/rel/type/RelDataTypeImpl.java       |   2 +-
 .../java/org/apache/calcite/sql/SqlCall.java    |   3 +-
 .../org/apache/calcite/sql/SqlIdentifier.java   |  47 ++-
 .../java/org/apache/calcite/sql/SqlNode.java    |   5 +-
 .../org/apache/calcite/sql/SqlOperator.java     |   3 +-
 .../java/org/apache/calcite/sql/SqlUtil.java    |  99 ++++++
 .../apache/calcite/sql/advise/SqlAdvisor.java   |  60 +++-
 .../calcite/sql/advise/SqlSimpleParser.java     |   8 +-
 .../apache/calcite/sql/parser/SqlParserPos.java | 116 ++++---
 .../apache/calcite/sql/validate/AggChecker.java |   4 +-
 .../sql/validate/AggregatingSelectScope.java    |   4 +-
 .../calcite/sql/validate/CatalogScope.java      |  88 ++++++
 .../calcite/sql/validate/DelegatingScope.java   |  76 +++--
 .../apache/calcite/sql/validate/EmptyScope.java |  13 +-
 .../apache/calcite/sql/validate/ListScope.java  |  59 +++-
 .../calcite/sql/validate/OrderByScope.java      |   4 +-
 .../calcite/sql/validate/ParameterScope.java    |  10 +-
 .../calcite/sql/validate/SchemaNamespace.java   |  60 ++++
 .../calcite/sql/validate/SelectScope.java       |   2 +-
 .../apache/calcite/sql/validate/SqlMoniker.java |  18 ++
 .../sql/validate/SqlMonikerComparator.java      |  39 ---
 .../calcite/sql/validate/SqlMonikerImpl.java    |  19 +-
 .../calcite/sql/validate/SqlMonikerType.java    |   2 +-
 .../calcite/sql/validate/SqlQualified.java      |  86 ++++++
 .../sql/validate/SqlValidatorCatalogReader.java |   2 +
 .../calcite/sql/validate/SqlValidatorImpl.java  | 306 ++++++++++---------
 .../calcite/sql/validate/SqlValidatorScope.java |  17 +-
 .../calcite/sql/validate/SqlValidatorUtil.java  |  24 +-
 .../sql/validate/SqlValidatorWithHints.java     |   4 +-
 .../apache/calcite/sql/validate/WithScope.java  |   6 +-
 .../calcite/sql2rel/SqlToRelConverter.java      |  42 +--
 .../main/java/org/apache/calcite/util/Bug.java  |   7 -
 .../main/java/org/apache/calcite/util/Util.java |  20 +-
 .../calcite/sql/parser/SqlParserTest.java       |  21 ++
 .../apache/calcite/sql/test/SqlAdvisorTest.java |  25 +-
 .../java/org/apache/calcite/test/JdbcTest.java  |  50 +--
 .../apache/calcite/test/MockCatalogReader.java  |  53 +++-
 .../apache/calcite/test/SqlValidatorTest.java   |  94 +++++-
 core/src/test/resources/sql/misc.oq             |  15 +
 41 files changed, 1078 insertions(+), 477 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 4b28e3b..5f540e7 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -1411,21 +1411,9 @@ SqlNode SelectItem() :
 SqlNode SelectExpression() :
 {
     SqlNode e;
-    String id;
-    SqlParserPos pos, starPos;
+    SqlParserPos pos;
 }
 {
-    LOOKAHEAD(3)
-    id = Identifier() { pos = getPos(); } <DOT> <STAR>
-    {
-        starPos = getPos();
-        return new SqlIdentifier(
-            ImmutableList.of(id, "*"),
-            null,
-            pos.plus(starPos),
-            ImmutableList.of(pos, starPos));
-    }
-    |
     <STAR>
     {
         pos = getPos();
@@ -3504,17 +3492,21 @@ SqlIdentifier CompoundIdentifier() :
         list.add(p);
     }
     (
-        <DOT> p = Identifier()
-        {
-            list.add(p);
-            posList.add(getPos());
-        }
+        <DOT>
+        (
+            p = Identifier() {
+                list.add(p);
+                posList.add(getPos());
+            }
+        |
+            <STAR> {
+                list.add("*");
+                posList.add(getPos());
+            }
+        )
     ) *
     {
-        SqlParserPos[] componentPositions =
-            (SqlParserPos []) posList.toArray(
-                new SqlParserPos[posList.size()]);
-        SqlParserPos pos = SqlParserPos.sum(componentPositions);
+        SqlParserPos pos = SqlParserPos.sum(posList);
         return new SqlIdentifier(list, null, pos, posList);
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 0f4b4f5..15cf374 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
@@ -209,8 +209,12 @@ public class CalciteCatalogReader implements Prepare.CatalogReader,
     return field != null ? field.getIndex() : -1;
   }
 
+  public boolean matches(String string, String name) {
+    return Util.matches(caseSensitive, string, name);
+  }
+
   public int match(List<String> strings, String name) {
-    return Util.match2(strings, name, caseSensitive);
+    return Util.findMatch(strings, name, caseSensitive);
   }
 
   public RelDataType createTypeFromProjection(final RelDataType type,

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 d917667..3b13b9b 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
@@ -79,7 +79,7 @@ public abstract class RelDataTypeImpl
   // implement RelDataType
   public RelDataTypeField getField(String fieldName, boolean caseSensitive) {
     for (RelDataTypeField field : fieldList) {
-      if (Util.match(caseSensitive, field.getName(), fieldName)) {
+      if (Util.matches(caseSensitive, field.getName(), fieldName)) {
         return field;
       }
     }

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/SqlCall.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlCall.java b/core/src/main/java/org/apache/calcite/sql/SqlCall.java
index 07f5f5b..7548e01 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlCall.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlCall.java
@@ -26,6 +26,7 @@ import org.apache.calcite.sql.validate.SqlValidatorImpl;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -110,7 +111,7 @@ public abstract class SqlCall extends SqlNode {
       SqlValidator validator,
       SqlValidatorScope scope,
       SqlParserPos pos,
-      List<SqlMoniker> hintList) {
+      Collection<SqlMoniker> hintList) {
     for (SqlNode operand : getOperandList()) {
       if (operand instanceof SqlIdentifier) {
         SqlIdentifier id = (SqlIdentifier) operand;

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 c772331..c5e2420 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlIdentifier.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlIdentifier.java
@@ -19,10 +19,9 @@ package org.apache.calcite.sql;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.util.SqlVisitor;
 import org.apache.calcite.sql.validate.SqlMonotonicity;
+import org.apache.calcite.sql.validate.SqlQualified;
 import org.apache.calcite.sql.validate.SqlValidator;
-import org.apache.calcite.sql.validate.SqlValidatorNamespace;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
-import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.util.Util;
 
 import com.google.common.collect.ImmutableList;
@@ -168,8 +167,41 @@ public class SqlIdentifier extends SqlNode {
    * available.
    */
   public SqlIdentifier getComponent(int ordinal) {
-    return new SqlIdentifier(names.get(ordinal),
-        getComponentParserPosition(ordinal));
+    return getComponent(ordinal, ordinal + 1);
+  }
+
+  public SqlIdentifier getComponent(int from, int to) {
+    final SqlParserPos pos;
+    final ImmutableList<SqlParserPos> pos2;
+    if (componentPositions == null) {
+      pos2 = null;
+      pos = this.pos;
+    } else {
+      pos2 = componentPositions.subList(from, to);
+      pos = SqlParserPos.sum(pos2);
+    }
+    return new SqlIdentifier(names.subList(from, to), collation, pos, pos2);
+  }
+
+  /**
+   * Creates an identifier that consists of this identifier plus a name segment.
+   * Does not modify this identifier.
+   */
+  public SqlIdentifier plus(String name, SqlParserPos pos) {
+    final ImmutableList<String> names =
+        ImmutableList.<String>builder().addAll(this.names).add(name).build();
+    final ImmutableList.Builder<SqlParserPos> builder = ImmutableList.builder();
+    final ImmutableList<SqlParserPos> componentPositions =
+        builder.addAll(this.componentPositions).add(pos).build();
+    final SqlParserPos pos2 =
+        SqlParserPos.sum(builder.add(this.pos).build());
+    return new SqlIdentifier(names, collation, pos2, componentPositions);
+  }
+
+  /** Creates an identifier that consists of all but the last {@code n}
+   * name segments of this one. */
+  public SqlIdentifier skipLast(int n) {
+    return getComponent(0, names.size() - n);
   }
 
   public void unparse(
@@ -270,10 +302,9 @@ public class SqlIdentifier extends SqlNode {
     if (call != null) {
       return call.getMonotonicity(scope);
     }
-    final SqlIdentifier fqId = scope.fullyQualify(this);
-    final SqlValidatorNamespace ns =
-        SqlValidatorUtil.lookup(scope, Util.skipLast(fqId.names));
-    return ns.resolve().getMonotonicity(Util.last(fqId.names));
+    final SqlQualified qualified = scope.fullyQualify(this);
+    final SqlIdentifier fqId = qualified.identifier;
+    return qualified.namespace.resolve().getMonotonicity(Util.last(fqId.names));
   }
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/SqlNode.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlNode.java b/core/src/main/java/org/apache/calcite/sql/SqlNode.java
index 75e3979..6708ba5 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlNode.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlNode.java
@@ -26,6 +26,7 @@ import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
 import org.apache.calcite.util.Util;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 
@@ -43,7 +44,7 @@ public abstract class SqlNode implements Cloneable {
 
   //~ Instance fields --------------------------------------------------------
 
-  private final SqlParserPos pos;
+  protected final SqlParserPos pos;
 
   //~ Constructors -----------------------------------------------------------
 
@@ -212,7 +213,7 @@ public abstract class SqlNode implements Cloneable {
       SqlValidator validator,
       SqlValidatorScope scope,
       SqlParserPos pos,
-      List<SqlMoniker> hintList) {
+      Collection<SqlMoniker> hintList) {
     // no valid options
   }
 

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/SqlOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlOperator.java
index cab1ecc..08325ab 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlOperator.java
@@ -30,6 +30,7 @@ import org.apache.calcite.sql.validate.SqlValidatorScope;
 import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.util.Util;
 
+import java.util.Arrays;
 import java.util.List;
 
 import static org.apache.calcite.util.Static.RESOURCE;
@@ -232,7 +233,7 @@ public abstract class SqlOperator {
       SqlLiteral functionQualifier,
       SqlParserPos pos,
       SqlNode... operands) {
-    pos = pos.plusAll(operands);
+    pos = pos.plusAll(Arrays.asList(operands));
     return new SqlBasicCall(this, operands, pos, false, functionQualifier);
   }
 

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
index e2430e7..4ee7ece 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
@@ -27,12 +27,15 @@ import org.apache.calcite.sql.parser.SqlParserPos;
 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.sql.util.SqlBasicVisitor;
 import org.apache.calcite.util.BarfingInvocationHandler;
 import org.apache.calcite.util.ConversionUtil;
 import org.apache.calcite.util.NlsString;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
 import java.nio.charset.Charset;
@@ -764,6 +767,23 @@ public abstract class SqlUtil {
     }
   }
 
+  /** Returns a list of ancestors of {@code predicate} within a given
+   * {@code SqlNode} tree.
+   *
+   * <p>The first element of the list is {@code root}, and the last is
+   * the node that matched {@code predicate}. Throws if no node matches.
+   */
+  public static ImmutableList<SqlNode> getAncestry(SqlNode root,
+      Predicate<SqlNode> predicate, Predicate<SqlNode> postPredicate) {
+    try {
+      new Genealogist(predicate, postPredicate).visitChild(root);
+      throw new AssertionError("not found: " + predicate + " in " + root);
+    } catch (Util.FoundOne e) {
+      //noinspection unchecked
+      return (ImmutableList<SqlNode>) e.getNode();
+    }
+  }
+
   //~ Inner Classes ----------------------------------------------------------
 
   /**
@@ -792,6 +812,85 @@ public abstract class SqlUtil {
       return identifierQuoteString;
     }
   }
+
+  /** Walks over a {@link org.apache.calcite.sql.SqlNode} tree and returns the
+   * ancestry stack when it finds a given node. */
+  private static class Genealogist extends SqlBasicVisitor<Void> {
+    private final List<SqlNode> ancestors = Lists.newArrayList();
+    private final Predicate<SqlNode> predicate;
+    private final Predicate<SqlNode> postPredicate;
+
+    Genealogist(Predicate<SqlNode> predicate,
+        Predicate<SqlNode> postPredicate) {
+      this.predicate = predicate;
+      this.postPredicate = postPredicate;
+    }
+
+    private Void check(SqlNode node) {
+      preCheck(node);
+      postCheck(node);
+      return null;
+    }
+
+    private Void preCheck(SqlNode node) {
+      if (predicate.apply(node)) {
+        throw new Util.FoundOne(ImmutableList.copyOf(ancestors));
+      }
+      return null;
+    }
+
+    private Void postCheck(SqlNode node) {
+      if (postPredicate.apply(node)) {
+        throw new Util.FoundOne(ImmutableList.copyOf(ancestors));
+      }
+      return null;
+    }
+
+    private void visitChild(SqlNode node) {
+      if (node == null) {
+        return;
+      }
+      ancestors.add(node);
+      node.accept(this);
+      ancestors.remove(ancestors.size() - 1);
+    }
+
+    @Override public Void visit(SqlIdentifier id) {
+      return check(id);
+    }
+
+    @Override public Void visit(SqlCall call) {
+      preCheck(call);
+      for (SqlNode node : call.getOperandList()) {
+        visitChild(node);
+      }
+      return postCheck(call);
+    }
+
+    @Override public Void visit(SqlIntervalQualifier intervalQualifier) {
+      return check(intervalQualifier);
+    }
+
+    @Override public Void visit(SqlLiteral literal) {
+      return check(literal);
+    }
+
+    @Override public Void visit(SqlNodeList nodeList) {
+      preCheck(nodeList);
+      for (SqlNode node : nodeList) {
+        visitChild(node);
+      }
+      return postCheck(nodeList);
+    }
+
+    @Override public Void visit(SqlDynamicParam param) {
+      return check(param);
+    }
+
+    @Override public Void visit(SqlDataTypeSpec type) {
+      return check(type);
+    }
+  }
 }
 
 // End SqlUtil.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisor.java b/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisor.java
index 818d200..06d10c0 100644
--- a/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisor.java
+++ b/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisor.java
@@ -20,6 +20,8 @@ import org.apache.calcite.runtime.CalciteContextException;
 import org.apache.calcite.runtime.CalciteException;
 import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.parser.SqlAbstractParserImpl;
 import org.apache.calcite.sql.parser.SqlParseException;
 import org.apache.calcite.sql.parser.SqlParser;
@@ -31,6 +33,10 @@ import org.apache.calcite.sql.validate.SqlValidatorWithHints;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.trace.CalciteTrace;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -46,12 +52,13 @@ public class SqlAdvisor {
   //~ Static fields/initializers ---------------------------------------------
 
   public static final Logger LOGGER = CalciteTrace.PARSER_LOGGER;
+  private static final String HINT_TOKEN = "_suggest_";
+  private static final String UPPER_HINT_TOKEN = HINT_TOKEN.toUpperCase();
 
   //~ Instance fields --------------------------------------------------------
 
   // Flags indicating precision/scale combinations
   private final SqlValidatorWithHints validator;
-  private final String hintToken = "_suggest_";
 
   //~ Constructors -----------------------------------------------------------
 
@@ -68,7 +75,7 @@ public class SqlAdvisor {
   //~ Methods ----------------------------------------------------------------
 
   /**
-   * Gets completion hints for a partially completed or syntatically incorrect
+   * Gets completion hints for a partially completed or syntactically incorrect
    * sql statement with cursor pointing to the position where completion hints
    * are requested.
    *
@@ -79,8 +86,8 @@ public class SqlAdvisor {
    * whitespace, the replaced string is empty. The replaced string is never
    * null.
    *
-   * @param sql      A partial or syntatically incorrect sql statement for which
-   *                 to retrieve completion hints
+   * @param sql      A partial or syntactically incorrect sql statement for
+   *                 which to retrieve completion hints
    * @param cursor   to indicate the 0-based cursor position in the query at
    * @param replaced String which is being replaced (output)
    * @return completion hints
@@ -164,7 +171,7 @@ public class SqlAdvisor {
 
   public List<SqlMoniker> getCompletionHints0(String sql, int cursor) {
     String simpleSql = simplifySql(sql, cursor);
-    int idx = simpleSql.indexOf(hintToken);
+    int idx = simpleSql.indexOf(HINT_TOKEN);
     if (idx < 0) {
       return Collections.emptyList();
     }
@@ -173,10 +180,10 @@ public class SqlAdvisor {
   }
 
   /**
-   * Gets completion hints for a syntatically correct sql statement with dummy
+   * Gets completion hints for a syntactically correct sql statement with dummy
    * SqlIdentifier
    *
-   * @param sql A syntatically correct sql statement for which to retrieve
+   * @param sql A syntactically correct sql statement for which to retrieve
    *            completion hints
    * @param pos to indicate the line and column position in the query at which
    *            completion hints need to be retrieved. For example, "select
@@ -205,6 +212,12 @@ public class SqlAdvisor {
         + sql.substring(x);
     tryParse(sql, hintList);
 
+    final SqlMoniker star =
+        new SqlMonikerImpl(ImmutableList.of("*"), SqlMonikerType.KEYWORD);
+    if (hintList.contains(star) && !isSelectListItem(sqlNode, pos)) {
+      hintList.remove(star);
+    }
+
     // Add the identifiers which are expected at the point of interest.
     try {
       validator.validate(sqlNode);
@@ -221,6 +234,29 @@ public class SqlAdvisor {
     return hintList;
   }
 
+  private static boolean isSelectListItem(SqlNode root,
+      final SqlParserPos pos) {
+    List<SqlNode> nodes = SqlUtil.getAncestry(root,
+        new Predicate<SqlNode>() {
+
+          public boolean apply(SqlNode input) {
+            return input instanceof SqlIdentifier
+                && Util.last(((SqlIdentifier) input).names)
+                    .equals(UPPER_HINT_TOKEN);
+          }
+        },
+        new Predicate<SqlNode>() {
+          public boolean apply(SqlNode input) {
+            return input.getParserPosition().startsAt(pos);
+          }
+        });
+    assert nodes.get(0) == root;
+    nodes = Lists.reverse(nodes);
+    return nodes.size() > 2
+        && nodes.get(2) instanceof SqlSelect
+        && nodes.get(1) == ((SqlSelect) nodes.get(2)).getSelectList();
+  }
+
   /**
    * Tries to parse a SQL statement.
    *
@@ -289,11 +325,11 @@ public class SqlAdvisor {
    * Attempts to complete and validate a given partially completed sql
    * statement, and returns whether it is valid.
    *
-   * @param sql A partial or syntatically incorrect sql statement to validate
+   * @param sql A partial or syntactically incorrect sql statement to validate
    * @return whether SQL statement is valid
    */
   public boolean isValid(String sql) {
-    SqlSimpleParser simpleParser = new SqlSimpleParser(hintToken);
+    SqlSimpleParser simpleParser = new SqlSimpleParser(HINT_TOKEN);
     String simpleSql = simpleParser.simplifySql(sql);
     SqlNode sqlNode;
     try {
@@ -351,16 +387,16 @@ public class SqlAdvisor {
   }
 
   /**
-   * Turns a partially completed or syntatically incorrect sql statement into
+   * Turns a partially completed or syntactically incorrect sql statement into
    * a simplified, valid one that can be passed into getCompletionHints()
    *
-   * @param sql    A partial or syntatically incorrect sql statement
+   * @param sql    A partial or syntactically incorrect sql statement
    * @param cursor to indicate column position in the query at which
    *               completion hints need to be retrieved.
    * @return a completed, valid (and possibly simplified SQL statement
    */
   public String simplifySql(String sql, int cursor) {
-    SqlSimpleParser parser = new SqlSimpleParser(hintToken);
+    SqlSimpleParser parser = new SqlSimpleParser(HINT_TOKEN);
     return parser.simplifySql(sql, cursor);
   }
 

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/advise/SqlSimpleParser.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/advise/SqlSimpleParser.java b/core/src/main/java/org/apache/calcite/sql/advise/SqlSimpleParser.java
index 0bf8ce2..0ad0d32 100644
--- a/core/src/main/java/org/apache/calcite/sql/advise/SqlSimpleParser.java
+++ b/core/src/main/java/org/apache/calcite/sql/advise/SqlSimpleParser.java
@@ -105,7 +105,7 @@ public class SqlSimpleParser {
    * Turns a partially completed or syntactically incorrect sql statement into
    * a simplified, valid one that can be passed into getCompletionHints().
    *
-   * @param sql    A partial or syntatically incorrect sql statement
+   * @param sql    A partial or syntactically incorrect sql statement
    * @param cursor to indicate column position in the query at which
    *               completion hints need to be retrieved.
    * @return a completed, valid (and possibly simplified SQL statement
@@ -123,10 +123,10 @@ public class SqlSimpleParser {
   }
 
   /**
-   * Turns a partially completed or syntatically incorrect sql statement into
+   * Turns a partially completed or syntactically incorrect sql statement into
    * a simplified, valid one that can be validated
    *
-   * @param sql A partial or syntatically incorrect sql statement
+   * @param sql A partial or syntactically incorrect sql statement
    * @return a completed, valid (and possibly simplified) SQL statement
    */
   public String simplifySql(String sql) {
@@ -140,7 +140,7 @@ public class SqlSimpleParser {
       list.add(token);
     }
 
-    // Gather consecutive subsequences of tokens into subqueries.
+    // Gather consecutive sub-sequences of tokens into subqueries.
     List<Token> outList = new ArrayList<Token>();
     consumeQuery(list.listIterator(), outList);
 

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java
index 95d704c..5028282 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java
@@ -18,7 +18,11 @@ package org.apache.calcite.sql.parser;
 
 import org.apache.calcite.sql.SqlNode;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+
 import java.io.Serializable;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 
@@ -125,14 +129,14 @@ public class SqlParserPos implements Serializable {
     return endColumnNumber;
   }
 
-  // implements Object
-  public String toString() {
+  @Override public String toString() {
     return RESOURCE.parserContext(lineNumber, columnNumber).str();
   }
 
   /**
-   * Combines this parser position with another to create a position which
-   * spans from the first point in the first to the last point in the other.
+   * Combines this parser position with another to create a
+   * position that spans from the first point in the first to the last point
+   * in the other.
    */
   public SqlParserPos plus(SqlParserPos pos) {
     return new SqlParserPos(
@@ -144,32 +148,40 @@ public class SqlParserPos implements Serializable {
 
   /**
    * Combines this parser position with an array of positions to create a
-   * position which spans from the first point in the first to the last point
+   * position that spans from the first point in the first to the last point
    * in the other.
    */
   public SqlParserPos plusAll(SqlNode[] nodes) {
-    int line = getLineNum();
-    int column = getColumnNum();
-    int endLine = getEndLineNum();
-    int endColumn = getEndColumnNum();
-    return sum(nodes, line, column, endLine, endColumn);
+    return plusAll(Arrays.asList(nodes));
   }
 
   /**
    * Combines this parser position with a list of positions.
    */
   public SqlParserPos plusAll(Collection<SqlNode> nodeList) {
-    final SqlNode[] nodes = nodeList.toArray(new SqlNode[nodeList.size()]);
-    return plusAll(nodes);
+    int line = getLineNum();
+    int column = getColumnNum();
+    int endLine = getEndLineNum();
+    int endColumn = getEndColumnNum();
+    return sum(toPos(nodeList), line, column, endLine, endColumn);
   }
 
   /**
    * Combines the parser positions of an array of nodes to create a position
    * which spans from the beginning of the first to the end of the last.
    */
-  public static SqlParserPos sum(
-      SqlNode[] nodes) {
-    return sum(nodes, Integer.MAX_VALUE, Integer.MAX_VALUE, -1, -1);
+  public static SqlParserPos sum(SqlNode[] nodes) {
+    final Iterable<SqlParserPos> poses = toPos(Arrays.asList(nodes));
+    return sum(poses, Integer.MAX_VALUE, Integer.MAX_VALUE, -1, -1);
+  }
+
+  private static Iterable<SqlParserPos> toPos(Iterable<SqlNode> nodes) {
+    return Iterables.transform(nodes,
+        new Function<SqlNode, SqlParserPos>() {
+          public SqlParserPos apply(SqlNode input) {
+            return input.getParserPosition();
+          }
+        });
   }
 
   /**
@@ -181,56 +193,10 @@ public class SqlParserPos implements Serializable {
   }
 
   /**
-   * Computes the parser position which is the sum of the positions of an
-   * array of parse tree nodes and of a parser position represented by (line,
-   * column, endLine, endColumn).
-   *
-   * @param nodes     Array of parse tree nodes
-   * @param line      Start line
-   * @param column    Start column
-   * @param endLine   End line
-   * @param endColumn End column
-   * @return Sum of parser positions
-   */
-  private static SqlParserPos sum(
-      SqlNode[] nodes,
-      int line,
-      int column,
-      int endLine,
-      int endColumn) {
-    int testLine;
-    int testColumn;
-    for (SqlNode node : nodes) {
-      if (node == null) {
-        continue;
-      }
-      SqlParserPos pos = node.getParserPosition();
-      if (pos.equals(SqlParserPos.ZERO)) {
-        continue;
-      }
-      testLine = pos.getLineNum();
-      testColumn = pos.getColumnNum();
-      if (testLine < line || testLine == line && testColumn < column) {
-        line = testLine;
-        column = testColumn;
-      }
-
-      testLine = pos.getEndLineNum();
-      testColumn = pos.getEndColumnNum();
-      if (testLine > endLine || testLine == endLine && testColumn > endColumn) {
-        endLine = testLine;
-        endColumn = testColumn;
-      }
-    }
-    return new SqlParserPos(line, column, endLine, endColumn);
-  }
-
-  /**
    * Combines an array of parser positions to create a position which spans
    * from the beginning of the first to the end of the last.
    */
-  public static SqlParserPos sum(
-      SqlParserPos[] poses) {
+  public static SqlParserPos sum(Iterable<SqlParserPos> poses) {
     return sum(poses, Integer.MAX_VALUE, Integer.MAX_VALUE, -1, -1);
   }
 
@@ -247,7 +213,7 @@ public class SqlParserPos implements Serializable {
    * @return Sum of parser positions
    */
   private static SqlParserPos sum(
-      SqlParserPos[] poses,
+      Iterable<SqlParserPos> poses,
       int line,
       int column,
       int endLine,
@@ -255,7 +221,7 @@ public class SqlParserPos implements Serializable {
     int testLine;
     int testColumn;
     for (SqlParserPos pos : poses) {
-      if (pos == null) {
+      if (pos == null || pos.equals(SqlParserPos.ZERO)) {
         continue;
       }
       testLine = pos.getLineNum();
@@ -274,6 +240,28 @@ public class SqlParserPos implements Serializable {
     }
     return new SqlParserPos(line, column, endLine, endColumn);
   }
+
+  public boolean overlaps(SqlParserPos pos) {
+    return startsBefore(pos) && endsAfter(pos)
+        || pos.startsBefore(this) && pos.endsAfter(this);
+  }
+
+  private boolean startsBefore(SqlParserPos pos) {
+    return lineNumber < pos.lineNumber
+        || lineNumber == pos.lineNumber
+        && columnNumber <= pos.columnNumber;
+  }
+
+  private boolean endsAfter(SqlParserPos pos) {
+    return endLineNumber > pos.endLineNumber
+        || endLineNumber == pos.endLineNumber
+        && endColumnNumber >= pos.endColumnNumber;
+  }
+
+  public boolean startsAt(SqlParserPos pos) {
+    return lineNumber == pos.lineNumber
+        && columnNumber == pos.columnNumber;
+  }
 }
 
 // End SqlParserPos.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java b/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java
index 1c32594..69bf51c 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java
@@ -99,8 +99,8 @@ class AggChecker extends SqlBasicVisitor<Void> {
     // it fully-qualified.
     // TODO: It would be better if we always compared fully-qualified
     // to fully-qualified.
-    final SqlIdentifier fqId = Stacks.peek(scopes).fullyQualify(id);
-    if (isGroupExpr(fqId)) {
+    final SqlQualified fqId = Stacks.peek(scopes).fullyQualify(id);
+    if (isGroupExpr(fqId.identifier)) {
       return null;
     }
     SqlNode originalExpr = validator.getOriginal(id);

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java b/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java
index 4584d6f..58fda81 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java
@@ -88,8 +88,8 @@ public class AggregatingSelectScope
       // We deep-copy the group-list in case subsequent validation
       // modifies it and makes it no longer equivalent. While copying,
       // we fully qualify all identifiers.
-      final SqlNodeList groupList =
-          SqlValidatorUtil.DeepCopier.copy(parent, select.getGroup());
+      final SqlNodeList groupList = select.getGroup();
+//          SqlValidatorUtil.DeepCopier.copy(parent, select.getGroup());
       for (SqlNode groupExpr : groupList) {
         SqlValidatorUtil.analyzeGroupItem(this, temporaryGroupExprList,
             groupExprProjection, builder, groupExpr);

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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
new file mode 100644
index 0000000..c2ef43e
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/validate/CatalogScope.java
@@ -0,0 +1,88 @@
+/*
+ * 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.sql.validate;
+
+import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.linq4j.function.Function1;
+import org.apache.calcite.linq4j.function.Predicate1;
+import org.apache.calcite.sql.SqlNode;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Implementation of {@link SqlValidatorScope} that can see all schemas in the
+ * current catalog.
+ *
+ * <p>Occurs near the root of the scope stack; its parent is typically
+ * {@link EmptyScope}.
+ *
+ * <p>Helps resolve {@code schema.table.column} column references, such as
+ *
+ * <blockquote><pre>select sales.emp.empno from sales.emp</pre></blockquote>
+ */
+class CatalogScope extends DelegatingScope {
+  /** Fully-qualified name of the catalog. Typically empty or ["CATALOG"]. */
+  final ImmutableList<String> names;
+  private final Set<List<String>> schemaNames;
+
+  //~ Constructors -----------------------------------------------------------
+
+  CatalogScope(SqlValidatorScope parent, List<String> names) {
+    super(parent);
+    this.names = ImmutableList.copyOf(names);
+    this.schemaNames =
+        Linq4j.asEnumerable(
+            validator.getCatalogReader()
+                .getAllSchemaObjectNames(ImmutableList.<String>of()))
+            .where(
+                new Predicate1<SqlMoniker>() {
+                  public boolean apply(SqlMoniker input) {
+                    return input.getType() == SqlMonikerType.SCHEMA;
+                  }
+                })
+            .select(
+                new Function1<SqlMoniker, List<String>>() {
+                  public List<String> apply(SqlMoniker input) {
+                    return input.getFullyQualifiedNames();
+                  }
+                })
+            .into(Sets.<List<String>>newHashSet());
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  public SqlNode getNode() {
+    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/incubator-calcite/blob/6c88546e/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 e23d886..72c533e 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
@@ -25,9 +25,11 @@ import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlWindow;
 import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.util.Pair;
 
 import com.google.common.collect.ImmutableList;
 
+import java.util.Collection;
 import java.util.List;
 
 import static org.apache.calcite.util.Static.RESOURCE;
@@ -72,10 +74,10 @@ public abstract class DelegatingScope implements SqlValidatorScope {
   }
 
   public SqlValidatorNamespace resolve(
-      String name,
+      List<String> names,
       SqlValidatorScope[] ancestorOut,
       int[] offsetOut) {
-    return parent.resolve(name, ancestorOut, offsetOut);
+    return parent.resolve(names, ancestorOut, offsetOut);
   }
 
   protected void addColumnNames(
@@ -101,11 +103,12 @@ public abstract class DelegatingScope implements SqlValidatorScope {
     parent.findAllColumnNames(result);
   }
 
-  public void findAliases(List<SqlMoniker> result) {
+  public void findAliases(Collection<SqlMoniker> result) {
     parent.findAliases(result);
   }
 
-  public String findQualifyingTableName(String columnName, SqlNode ctx) {
+  public Pair<String, SqlValidatorNamespace>
+  findQualifyingTableName(String columnName, SqlNode ctx) {
     return parent.findQualifyingTableName(columnName, ctx);
   }
 
@@ -139,19 +142,19 @@ public abstract class DelegatingScope implements SqlValidatorScope {
    *
    * <p>If the identifier cannot be resolved, throws. Never returns null.
    */
-  public SqlIdentifier fullyQualify(SqlIdentifier identifier) {
+  public SqlQualified fullyQualify(SqlIdentifier identifier) {
     if (identifier.isStar()) {
-      return identifier;
+      return SqlQualified.create(this, 1, null, identifier);
     }
 
-    String tableName;
     String columnName;
-
     switch (identifier.names.size()) {
     case 1:
       columnName = identifier.names.get(0);
-      tableName =
+      final Pair<String, SqlValidatorNamespace> pair =
           findQualifyingTableName(columnName, identifier);
+      final String tableName = pair.left;
+      final SqlValidatorNamespace namespace = pair.right;
 
       // todo: do implicit collation here
       final SqlParserPos pos = identifier.getParserPosition();
@@ -162,32 +165,39 @@ public abstract class DelegatingScope implements SqlValidatorScope {
               pos,
               ImmutableList.of(SqlParserPos.ZERO, pos));
       validator.setOriginal(expanded, identifier);
-      return expanded;
-
-    case 2:
-      tableName = identifier.names.get(0);
-      final SqlValidatorNamespace fromNs = resolve(tableName, null, null);
-      if (fromNs == null) {
-        throw validator.newValidationError(identifier.getComponent(0),
-            RESOURCE.tableNameNotFound(tableName));
-      }
-      columnName = identifier.names.get(1);
-      final RelDataType fromRowType = fromNs.getRowType();
-      final RelDataTypeField field =
-          validator.catalogReader.field(fromRowType, columnName);
-      if (field != null) {
-        identifier.setName(1, field.getName()); // normalize case to match defn
-        return identifier; // it was fine already
-      } else {
-        throw validator.newValidationError(identifier.getComponent(1),
-            RESOURCE.columnNotFoundInTable(columnName, tableName));
-      }
+      return SqlQualified.create(this, 1, namespace, expanded);
 
     default:
-      // NOTE jvs 26-May-2004:  lengths greater than 2 are possible
-      // for row and structured types
-      assert identifier.names.size() > 0;
-      return identifier;
+      SqlValidatorNamespace fromNs = null;
+      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) {
+          break;
+        }
+      }
+      if (fromNs == null || fromNs instanceof SchemaNamespace) {
+        final SqlIdentifier prefix1 = identifier.skipLast(1);
+        throw validator.newValidationError(prefix1,
+            RESOURCE.tableNameNotFound(prefix1.toString()));
+      }
+      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()));
+        }
+        identifier.setName(j, field.getName()); // normalize case to match defn
+        fromRowType = field.getType();
+      }
+      return SqlQualified.create(this, i, fromNs, identifier);
     }
   }
 

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 6376bd7..9da3bff 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
@@ -25,7 +25,9 @@ import org.apache.calcite.sql.SqlLiteral;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlWindow;
+import org.apache.calcite.util.Pair;
 
+import java.util.Collection;
 import java.util.List;
 
 import static org.apache.calcite.util.Static.RESOURCE;
@@ -54,8 +56,8 @@ class EmptyScope implements SqlValidatorScope {
     return validator;
   }
 
-  public SqlIdentifier fullyQualify(SqlIdentifier identifier) {
-    return identifier;
+  public SqlQualified fullyQualify(SqlIdentifier identifier) {
+    return SqlQualified.create(this, 1, null, identifier);
   }
 
   public SqlNode getNode() {
@@ -63,7 +65,7 @@ class EmptyScope implements SqlValidatorScope {
   }
 
   public SqlValidatorNamespace resolve(
-      String name,
+      List<String> names,
       SqlValidatorScope[] ancestorOut,
       int[] offsetOut) {
     return null;
@@ -86,7 +88,7 @@ class EmptyScope implements SqlValidatorScope {
   public void findAllTableNames(List<SqlMoniker> result) {
   }
 
-  public void findAliases(List<SqlMoniker> result) {
+  public void findAliases(Collection<SqlMoniker> result) {
   }
 
   public RelDataType resolveColumn(String name, SqlNode ctx) {
@@ -101,7 +103,8 @@ class EmptyScope implements SqlValidatorScope {
     // valid
   }
 
-  public String findQualifyingTableName(String columnName, SqlNode ctx) {
+  public Pair<String, SqlValidatorNamespace>
+  findQualifyingTableName(String columnName, SqlNode ctx) {
     throw validator.newValidationError(ctx,
         RESOURCE.columnNotFound(columnName));
   }

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 80af917..8567cc5 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
@@ -23,6 +23,7 @@ import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 import static org.apache.calcite.util.Static.RESOURCE;
@@ -78,6 +79,47 @@ public abstract class ListScope extends DelegatingScope {
     }
   }
 
+  /** 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 {
+          // 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;
+        }
+      }
+
+      // 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();
+      if (table != null) {
+        final SqlValidatorTable table2 =
+            validator.catalogReader.getTable(names);
+        if (table2 != null
+            && table.getQualifiedName().equals(table2.getQualifiedName())) {
+          return i;
+        }
+      }
+    }
+    return -1;
+  }
+
   public void findAllColumnNames(List<SqlMoniker> result) {
     for (Pair<String, SqlValidatorNamespace> pair : children) {
       addColumnNames(pair.right, result);
@@ -85,22 +127,21 @@ public abstract class ListScope extends DelegatingScope {
     parent.findAllColumnNames(result);
   }
 
-  public void findAliases(List<SqlMoniker> result) {
+  public void findAliases(Collection<SqlMoniker> result) {
     for (Pair<String, SqlValidatorNamespace> pair : children) {
       result.add(new SqlMonikerImpl(pair.left, SqlMonikerType.TABLE));
     }
     parent.findAliases(result);
   }
 
-  public String findQualifyingTableName(
-      final String columnName,
-      SqlNode ctx) {
+  public Pair<String, SqlValidatorNamespace>
+  findQualifyingTableName(final String columnName, SqlNode ctx) {
     int count = 0;
-    String tableName = null;
+    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.left;
+        tableName = child;
         count++;
       }
     }
@@ -116,11 +157,11 @@ public abstract class ListScope extends DelegatingScope {
   }
 
   public SqlValidatorNamespace resolve(
-      String name,
+      List<String> names,
       SqlValidatorScope[] ancestorOut,
       int[] offsetOut) {
     // First resolve by looking through the child namespaces.
-    final int i = validator.catalogReader.match(Pair.left(children), name);
+    final int i = findChild(names);
     if (i >= 0) {
       if (ancestorOut != null) {
         ancestorOut[0] = this;
@@ -133,7 +174,7 @@ public abstract class ListScope extends DelegatingScope {
 
     // Then call the base class method, which will delegate to the
     // parent scope.
-    return parent.resolve(name, ancestorOut, offsetOut);
+    return parent.resolve(names, ancestorOut, offsetOut);
   }
 
   public RelDataType resolveColumn(String columnName, SqlNode ctx) {

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java b/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java
index ea38156..55b3af9 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java
@@ -65,7 +65,7 @@ public class OrderByScope extends DelegatingScope {
     addColumnNames(ns, result);
   }
 
-  public SqlIdentifier fullyQualify(SqlIdentifier identifier) {
+  public SqlQualified fullyQualify(SqlIdentifier identifier) {
     // If it's a simple identifier, look for an alias.
     if (identifier.isSimple()
         && validator.getConformance().isSortByAlias()) {
@@ -74,7 +74,7 @@ public class OrderByScope extends DelegatingScope {
           validator.getNamespace(select);
       final RelDataType rowType = selectNs.getRowType();
       if (validator.catalogReader.field(rowType, name) != null) {
-        return identifier;
+        return SqlQualified.create(this, 1, selectNs, identifier);
       }
     }
     return super.fullyQualify(identifier);

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 6a5d86c..f1a4e91 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,6 +20,7 @@ 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;
 
 /**
@@ -48,8 +49,8 @@ public class ParameterScope extends EmptyScope {
 
   //~ Methods ----------------------------------------------------------------
 
-  public SqlIdentifier fullyQualify(SqlIdentifier identifier) {
-    return identifier;
+  public SqlQualified fullyQualify(SqlIdentifier identifier) {
+    return SqlQualified.create(this, 1, null, identifier);
   }
 
   public SqlValidatorScope getOperandScope(SqlCall call) {
@@ -57,10 +58,11 @@ public class ParameterScope extends EmptyScope {
   }
 
   public SqlValidatorNamespace resolve(
-      String name,
+      List<String> names,
       SqlValidatorScope[] ancestorOut,
       int[] offsetOut) {
-    final RelDataType type = nameToTypeMap.get(name);
+    assert names.size() == 1;
+    final RelDataType type = nameToTypeMap.get(names.get(0));
     return new ParameterNamespace(validator, type);
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/SchemaNamespace.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SchemaNamespace.java b/core/src/main/java/org/apache/calcite/sql/validate/SchemaNamespace.java
new file mode 100644
index 0000000..e8347c4
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SchemaNamespace.java
@@ -0,0 +1,60 @@
+/*
+ * 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.sql.validate;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.util.Util;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/** Namespace based on a schema.
+ *
+ * <p>The visible names are tables and sub-schemas.
+ */
+class SchemaNamespace extends AbstractNamespace {
+  /** The path of this schema. */
+  private final ImmutableList<String> names;
+
+  /** Creates a SchemaNamespace. */
+  SchemaNamespace(SqlValidatorImpl validator, ImmutableList<String> names) {
+    super(validator, null);
+    this.names = Preconditions.checkNotNull(names);
+  }
+
+  protected RelDataType validateImpl() {
+    final RelDataTypeFactory.FieldInfoBuilder builder =
+        validator.getTypeFactory().builder();
+    for (SqlMoniker moniker
+        : validator.catalogReader.getAllSchemaObjectNames(names)) {
+      final List<String> names1 = moniker.getFullyQualifiedNames();
+      final SqlValidatorTable table = validator.catalogReader.getTable(names1);
+      builder.add(Util.last(names1), table.getRowType());
+    }
+    return builder.build();
+  }
+
+  public SqlNode getNode() {
+    return null;
+  }
+}
+
+// End SchemaNamespace.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/SelectScope.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SelectScope.java b/core/src/main/java/org/apache/calcite/sql/validate/SelectScope.java
index 76ddeb1..ea6db19 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SelectScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SelectScope.java
@@ -209,7 +209,7 @@ public class SelectScope extends ListScope {
         final SelectScope parentScope = (SelectScope) walker;
         return parentScope.existingWindowName(winName);
       }
-      walker = parent;
+      walker = ((DelegatingScope) walker).parent;
     }
 
     return false;

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/SqlMoniker.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlMoniker.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlMoniker.java
index 72f8ffd..66c47f5 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlMoniker.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlMoniker.java
@@ -18,12 +18,30 @@ package org.apache.calcite.sql.validate;
 
 import org.apache.calcite.sql.SqlIdentifier;
 
+import com.google.common.collect.Ordering;
+
+import java.util.Comparator;
 import java.util.List;
 
 /**
  * An interface of an object identifier that represents a SqlIdentifier
  */
 public interface SqlMoniker {
+  Comparator<SqlMoniker> COMPARATOR =
+      new Comparator<SqlMoniker>() {
+        final Ordering<Iterable<String>> listOrdering =
+            Ordering.<String>natural().lexicographical();
+
+        public int compare(SqlMoniker o1, SqlMoniker o2) {
+          int c = o1.getType().compareTo(o2.getType());
+          if (c == 0) {
+            c = listOrdering.compare(o1.getFullyQualifiedNames(),
+                o2.getFullyQualifiedNames());
+          }
+          return c;
+        }
+      };
+
   //~ Methods ----------------------------------------------------------------
 
   /**

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerComparator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerComparator.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerComparator.java
deleted file mode 100644
index 85eb1c0..0000000
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerComparator.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.sql.validate;
-
-import java.util.Comparator;
-
-/**
- * A general-purpose implementation of {@link Comparator} to compare
- * {@link SqlMoniker} values.
- */
-public class SqlMonikerComparator implements Comparator<SqlMoniker> {
-  //~ Methods ----------------------------------------------------------------
-
-  public int compare(SqlMoniker m1, SqlMoniker m2) {
-    if (m1.getType().ordinal() > m2.getType().ordinal()) {
-      return 1;
-    } else if (m1.getType().ordinal() < m2.getType().ordinal()) {
-      return -1;
-    } else {
-      return m1.toString().compareTo(m2.toString());
-    }
-  }
-}
-
-// End SqlMonikerComparator.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerImpl.java
index c7eebd8..d14f76c 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerImpl.java
@@ -20,6 +20,7 @@ import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.util.Util;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 import java.util.List;
@@ -39,13 +40,8 @@ public class SqlMonikerImpl implements SqlMoniker {
    * Creates a moniker with an array of names.
    */
   public SqlMonikerImpl(List<String> names, SqlMonikerType type) {
-    assert names != null;
-    assert type != null;
-    for (String name : names) {
-      assert name != null;
-    }
     this.names = ImmutableList.copyOf(names);
-    this.type = type;
+    this.type = Preconditions.checkNotNull(type);
   }
 
   /**
@@ -57,6 +53,17 @@ public class SqlMonikerImpl implements SqlMoniker {
 
   //~ Methods ----------------------------------------------------------------
 
+  @Override public boolean equals(Object obj) {
+    return this == obj
+        || obj instanceof SqlMonikerImpl
+        && type == ((SqlMonikerImpl) obj).type
+        && names.equals(((SqlMonikerImpl) obj).names);
+  }
+
+  @Override public int hashCode() {
+    return Util.hash(type.ordinal(), names);
+  }
+
   public SqlMonikerType getType() {
     return type;
   }

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerType.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerType.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerType.java
index 4b64859..7bf7a3e 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerType.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlMonikerType.java
@@ -22,7 +22,7 @@ package org.apache.calcite.sql.validate;
  * <p>Used in {@link SqlMoniker}.
  */
 public enum SqlMonikerType {
-  COLUMN, TABLE, VIEW, SCHEMA, REPOSITORY, FUNCTION, KEYWORD
+  COLUMN, TABLE, VIEW, SCHEMA, CATALOG, REPOSITORY, FUNCTION, KEYWORD
 }
 
 // End SqlMonikerType.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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
new file mode 100644
index 0000000..4b99845
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlQualified.java
@@ -0,0 +1,86 @@
+/*
+ * 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.sql.validate;
+
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.util.Util;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * Fully-qualified identifier.
+ *
+ * <p>The result of calling
+ * {@link org.apache.calcite.sql.validate.SqlValidatorScope#fullyQualify(org.apache.calcite.sql.SqlIdentifier)},
+ * a fully-qualified identifier contains the name (in correct case),
+ * parser position, type, and scope of each component of the identifier.
+ *
+ * <p>It is immutable.
+ */
+public class SqlQualified {
+  private final SqlValidatorScope scope;
+  public final int prefixLength;
+  public final SqlValidatorNamespace namespace;
+  public final SqlIdentifier identifier;
+
+  private SqlQualified(SqlValidatorScope scope, int prefixLength,
+      SqlValidatorNamespace namespace, SqlIdentifier identifier) {
+    this.scope = scope;
+    this.prefixLength = prefixLength;
+    this.namespace = namespace;
+    this.identifier = identifier;
+  }
+
+  public static SqlQualified create(SqlValidatorScope scope, int prefixLength,
+      SqlValidatorNamespace namespace, SqlIdentifier identifier) {
+    return new SqlQualified(scope, prefixLength, namespace, identifier);
+  }
+
+  public List<String> translatedNames() {
+    if (scope == null) {
+      return identifier.names;
+    }
+    final ImmutableList.Builder<String> builder = ImmutableList.builder();
+    SqlValidatorNamespace namespace =
+        scope.resolve(Util.skipLast(identifier.names), null, null);
+    builder.add(identifier.names.get(0));
+    for (String name : Util.skip(identifier.names)) {
+      if (namespace != null) {
+        name = namespace.translate(name);
+        namespace = null;
+      }
+      builder.add(name);
+    }
+    return builder.build();
+  }
+
+  public final List<String> prefix() {
+    return identifier.names.subList(0, prefixLength);
+  }
+
+  public final List<String> suffix() {
+    return Util.skip(identifier.names, prefixLength);
+  }
+
+  public final List<String> suffixTranslated() {
+    return Util.skip(translatedNames(), prefixLength);
+  }
+}
+
+// End SqlQualified.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 915d9ec..4a1c16b 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
@@ -86,6 +86,8 @@ public interface SqlValidatorCatalogReader {
    */
   int fieldOrdinal(RelDataType rowType, String alias);
 
+  boolean matches(String string, String name);
+
   int match(List<String> strings, String name);
 
   RelDataType createTypeFromProjection(RelDataType type,

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/6c88546e/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 cabc4d8..6e82f4c 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
@@ -74,7 +74,6 @@ import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.util.SqlShuttle;
 import org.apache.calcite.sql.util.SqlVisitor;
 import org.apache.calcite.util.BitString;
-import org.apache.calcite.util.Bug;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.trace.CalciteTrace;
@@ -92,6 +91,7 @@ import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.GregorianCalendar;
 import java.util.HashMap;
@@ -398,66 +398,9 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       List<Map.Entry<String, RelDataType>> types,
       final boolean includeSystemVars) {
     final SelectScope scope = (SelectScope) getWhereScope(select);
-    if (selectItem instanceof SqlIdentifier) {
-      SqlIdentifier identifier = (SqlIdentifier) selectItem;
-      if ((identifier.names.size() == 1)
-          && identifier.names.get(0).equals("*")) {
-        SqlParserPos starPosition = identifier.getParserPosition();
-        for (Pair<String, SqlValidatorNamespace> p : scope.children) {
-          final SqlNode from = p.right.getNode();
-          final SqlValidatorNamespace fromNs = getNamespace(from, scope);
-          assert fromNs != null;
-          final RelDataType rowType = fromNs.getRowType();
-          for (RelDataTypeField field : rowType.getFieldList()) {
-            String columnName = field.getName();
-
-            // TODO: do real implicit collation here
-            final SqlNode exp =
-                new SqlIdentifier(
-                    ImmutableList.of(p.left, columnName),
-                    starPosition);
-            addToSelectList(
-                selectItems,
-                aliases,
-                types,
-                exp,
-                scope,
-                includeSystemVars);
-          }
-        }
-        return true;
-      } else if (identifier.names.size() == 2
-          && identifier.names.get(1).equals("*")) {
-        final String tableName = identifier.names.get(0);
-        SqlParserPos starPosition = identifier.getParserPosition();
-        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();
-        final SqlValidatorNamespace fromNs = getNamespace(from);
-        assert fromNs != null;
-        final RelDataType rowType = fromNs.getRowType();
-        for (RelDataTypeField field : rowType.getFieldList()) {
-          String columnName = field.getName();
-
-          // TODO: do real implicit collation here
-          final SqlIdentifier exp =
-              new SqlIdentifier(
-                  ImmutableList.of(tableName, columnName),
-                  starPosition);
-          addToSelectList(
-              selectItems,
-              aliases,
-              types,
-              exp,
-              scope,
-              includeSystemVars);
-        }
-        return true;
-      }
+    if (expandStar(selectItems, aliases, types, includeSystemVars, scope,
+        selectItem)) {
+      return true;
     }
 
     // Expand the select item: fully-qualify columns, and convert
@@ -495,8 +438,84 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
     return false;
   }
 
+  private boolean expandStar(List<SqlNode> selectItems, Set<String> aliases,
+      List<Map.Entry<String, RelDataType>> types, boolean includeSystemVars,
+      SelectScope scope, SqlNode node) {
+    if (!(node instanceof SqlIdentifier)) {
+      return false;
+    }
+    final SqlIdentifier identifier = (SqlIdentifier) node;
+    if (!identifier.isStar()) {
+      return false;
+    }
+    final SqlParserPos starPosition = identifier.getParserPosition();
+    switch (identifier.names.size()) {
+    case 1:
+      for (Pair<String, SqlValidatorNamespace> p : scope.children) {
+        final SqlNode from = p.right.getNode();
+        final SqlValidatorNamespace fromNs = getNamespace(from, scope);
+        assert fromNs != null;
+        final RelDataType rowType = fromNs.getRowType();
+        for (RelDataTypeField field : rowType.getFieldList()) {
+          String columnName = field.getName();
+
+          // TODO: do real implicit collation here
+          final SqlNode exp =
+              new SqlIdentifier(
+                  ImmutableList.of(p.left, columnName),
+                  starPosition);
+          addToSelectList(
+              selectItems,
+              aliases,
+              types,
+              exp,
+              scope,
+              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 RelDataType rowType = fromNs.getRowType();
+      for (RelDataTypeField field : rowType.getFieldList()) {
+        String columnName = field.getName();
+
+        // TODO: do real implicit collation here
+        addToSelectList(
+            selectItems,
+            aliases,
+            types,
+            prefixId.plus(columnName, starPosition),
+            scope,
+            includeSystemVars);
+      }
+      return true;
+    }
+  }
+
   public SqlNode validate(SqlNode topNode) {
     SqlValidatorScope scope = new EmptyScope(this);
+    scope = new CatalogScope(scope, ImmutableList.of("CATALOG"));
     final SqlNode topNode2 = validateScopedExpression(topNode, scope);
     final RelDataType type = getValidatedNodeType(topNode2);
     Util.discard(type);
@@ -520,16 +539,17 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
     if (ns == null) {
       throw Util.newInternal("Not a query: " + outermostNode);
     }
-    List<SqlMoniker> hintList = new ArrayList<SqlMoniker>();
+    Collection<SqlMoniker> hintList = Sets.newTreeSet(SqlMoniker.COMPARATOR);
     lookupSelectHints(ns, pos, hintList);
-    return hintList;
+    return ImmutableList.copyOf(hintList);
   }
 
   public SqlMoniker lookupQualifiedName(SqlNode topNode, SqlParserPos pos) {
     final String posString = pos.toString();
     IdInfo info = idPositions.get(posString);
     if (info != null) {
-      return new SqlIdentifierMoniker(info.scope.fullyQualify(info.id));
+      final SqlQualified qualified = info.scope.fullyQualify(info.id);
+      return new SqlIdentifierMoniker(qualified.identifier);
     } else {
       return null;
     }
@@ -548,7 +568,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
   void lookupSelectHints(
       SqlSelect select,
       SqlParserPos pos,
-      List<SqlMoniker> hintList) {
+      Collection<SqlMoniker> hintList) {
     IdInfo info = idPositions.get(pos.toString());
     if ((info == null) || (info.scope == null)) {
       SqlNode fromNode = select.getFrom();
@@ -563,7 +583,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
   private void lookupSelectHints(
       SqlValidatorNamespace ns,
       SqlParserPos pos,
-      List<SqlMoniker> hintList) {
+      Collection<SqlMoniker> hintList) {
     final SqlNode node = ns.getNode();
     if (node instanceof SqlSelect) {
       lookupSelectHints((SqlSelect) node, pos, hintList);
@@ -574,7 +594,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       SqlNode node,
       SqlValidatorScope scope,
       SqlParserPos pos,
-      List<SqlMoniker> hintList) {
+      Collection<SqlMoniker> hintList) {
     final SqlValidatorNamespace ns = getNamespace(node);
     if (ns.isWrapperFor(IdentifierNamespace.class)) {
       IdentifierNamespace idNs = ns.unwrap(IdentifierNamespace.class);
@@ -610,7 +630,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       SqlJoin join,
       SqlValidatorScope scope,
       SqlParserPos pos,
-      List<SqlMoniker> hintList) {
+      Collection<SqlMoniker> hintList) {
     SqlNode left = join.getLeft();
     SqlNode right = join.getRight();
     SqlNode condition = join.getCondition();
@@ -648,7 +668,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       SqlValidatorScope scope,
       List<String> names,
       SqlParserPos pos,
-      List<SqlMoniker> hintList) {
+      Collection<SqlMoniker> hintList) {
     // Remove the last part of name - it is a dummy
     List<String> subNames = Util.skipLast(names);
 
@@ -657,7 +677,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       SqlValidatorNamespace ns = null;
       for (String name : subNames) {
         if (ns == null) {
-          ns = scope.resolve(name, null, null);
+          ns = scope.resolve(ImmutableList.of(name), null, null);
         } else {
           ns = ns.lookupChild(name);
         }
@@ -705,7 +725,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
   private static void findAllValidUdfNames(
       List<String> names,
       SqlValidator validator,
-      List<SqlMoniker> result) {
+      Collection<SqlMoniker> result) {
     List<SqlMoniker> objNames = new ArrayList<SqlMoniker>();
     SqlValidatorUtil.getSchemaObjectMonikers(
         validator.getCatalogReader(),
@@ -721,7 +741,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
   private static void findAllValidFunctionNames(
       List<String> names,
       SqlValidator validator,
-      List<SqlMoniker> result,
+      Collection<SqlMoniker> result,
       SqlParserPos pos) {
     // a function name can only be 1 part
     if (names.size() > 1) {
@@ -878,7 +898,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
           ((DelegatingScope) scope).getParent();
       if (id.isSimple()) {
         SqlValidatorNamespace ns =
-            parentScope.resolve(id.getSimple(), null, null);
+            parentScope.resolve(id.names, null, null);
         if (ns != null) {
           return ns;
         }
@@ -2523,11 +2543,11 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
   }
 
   public void validateIdentifier(SqlIdentifier id, SqlValidatorScope scope) {
-    final SqlIdentifier fqId = scope.fullyQualify(id);
+    final SqlQualified fqId = scope.fullyQualify(id);
     if (expandColumnReferences) {
       // NOTE jvs 9-Apr-2007: this doesn't cover ORDER BY, which has its
       // own ideas about qualification.
-      id.assignNamesFrom(fqId);
+      id.assignNamesFrom(fqId.identifier);
     } else {
       Util.discard(fqId);
     }
@@ -3157,10 +3177,9 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
 
     // Validate SELECT list. Expand terms of the form "*" or "TABLE.*".
     final SqlValidatorScope selectScope = getSelectScope(select);
-    final List<SqlNode> expandedSelectItems = new ArrayList<SqlNode>();
-    final Set<String> aliases = new HashSet<String>();
-    final List<Map.Entry<String, RelDataType>> fieldList =
-        new ArrayList<Map.Entry<String, RelDataType>>();
+    final List<SqlNode> expandedSelectItems = Lists.newArrayList();
+    final Set<String> aliases = Sets.newHashSet();
+    final List<Map.Entry<String, RelDataType>> fieldList = Lists.newArrayList();
 
     for (int i = 0; i < selectItems.size(); i++) {
       SqlNode selectItem = selectItems.get(i);
@@ -3848,26 +3867,21 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       final List<SqlNode> selectList = scope.getExpandedSelectList();
       final SqlNode selectItem = stripAs(selectList.get(i));
       if (selectItem instanceof SqlIdentifier) {
-        SqlIdentifier id = (SqlIdentifier) selectItem;
-        SqlValidatorNamespace namespace = null;
-        List<String> origin = new ArrayList<String>();
-        for (String name : id.names) {
+        final SqlQualified qualified =
+            scope.fullyQualify((SqlIdentifier) selectItem);
+        SqlValidatorNamespace namespace = qualified.namespace;
+        final SqlValidatorTable table = namespace.getTable();
+        if (table == null) {
+          return null;
+        }
+        final List<String> origin =
+            Lists.newArrayList(table.getQualifiedName());
+        for (String name : qualified.suffix()) {
+          namespace = namespace.lookupChild(name);
           if (namespace == null) {
-            namespace = scope.resolve(name, null, null);
-            final SqlValidatorTable table = namespace.getTable();
-            if (table != null) {
-              origin.addAll(table.getQualifiedName());
-            } else {
-              return null;
-            }
-          } else {
-            namespace = namespace.lookupChild(name);
-            if (namespace != null) {
-              origin.add(name);
-            } else {
-              return null;
-            }
+            return null;
           }
+          origin.add(name);
         }
         return origin;
       }
@@ -4030,56 +4044,56 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
 
       RelDataType type = null;
       if (!(scope instanceof EmptyScope)) {
-        id = scope.fullyQualify(id);
+        id = scope.fullyQualify(id).identifier;
       }
-      for (Ord<String> ord : Ord.zip(id.names)) {
-        String name = ord.e;
-        if (ord.i == 0) {
-          // REVIEW jvs 9-June-2005: The name resolution rules used
-          // here are supposed to match SQL:2003 Part 2 Section 6.6
-          // (identifier chain), but we don't currently have enough
-          // information to get everything right.  In particular,
-          // routine parameters are currently looked up via resolve;
-          // we could do a better job if they were looked up via
-          // resolveColumn.
-
-          // TODO jvs 9-June-2005:  Support schema-qualified table
-          // names here (FRG-140).  This was illegal in SQL-92, but
-          // became legal in SQL:1999.  (SQL:2003 Part 2 Section
-          // 6.6 Syntax Rule 8.b.vi)
-          Util.discard(Bug.FRG140_FIXED);
-
-          SqlValidatorNamespace resolvedNs =
-              scope.resolve(name, null, null);
-
-          if (resolvedNs != null) {
-            // There's a namespace with the name we seek.
-            type = resolvedNs.getRowType();
-          }
 
-          // Give precedence to namespace found, unless there
-          // are no more identifier components.
-          if (type == null || id.names.size() == 1) {
-            // See if there's a column with the name we seek in
-            // precisely one of the namespaces in this scope.
-            RelDataType colType = scope.resolveColumn(name, id);
-            if (colType != null) {
-              type = colType;
-            }
-          }
+      // 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
+        // here are supposed to match SQL:2003 Part 2 Section 6.6
+        // (identifier chain), but we don't currently have enough
+        // information to get everything right.  In particular,
+        // routine parameters are currently looked up via resolve;
+        // 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) {
+          // There's a namespace with the name we seek.
+          type = resolvedNs.getRowType();
+          break;
+        }
+      }
 
-          if (type == null) {
-            throw newValidationError(id.getComponent(ord.i),
-                RESOURCE.unknownIdentifier(name));
-          }
-        } else {
-          final RelDataTypeField field = catalogReader.field(type, name);
-          if (field == null) {
-            throw newValidationError(id.getComponent(ord.i),
-                RESOURCE.unknownField(name));
-          }
-          type = field.getType();
+      // Give precedence to namespace found, unless there
+      // are no more identifier components.
+      if (type == null || id.names.size() == 1) {
+        // See if there's a column with the name we seek in
+        // precisely one of the namespaces in this scope.
+        RelDataType colType = scope.resolveColumn(id.names.get(0), id);
+        if (colType != null) {
+          type = colType;
+        }
+        ++i;
+      }
+
+      if (type == null) {
+        final SqlIdentifier last = id.getComponent(i - 1, i);
+        throw newValidationError(last,
+            RESOURCE.unknownIdentifier(last.toString()));
+      }
+
+      // Resolve rest of identifier
+      for (; i < id.names.size(); i++) {
+        String name = id.names.get(i);
+        final RelDataTypeField field = catalogReader.field(type, name);
+        if (field == null) {
+          throw newValidationError(id.getComponent(i),
+              RESOURCE.unknownField(name));
         }
+        type = field.getType();
       }
       type =
           SqlTypeUtil.addCharsetAndCollation(
@@ -4129,7 +4143,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       if (call != null) {
         return call.accept(this);
       }
-      final SqlIdentifier fqId = getScope().fullyQualify(id);
+      final SqlIdentifier fqId = getScope().fullyQualify(id).identifier;
       validator.setOriginal(fqId, id);
       return fqId;
     }
@@ -4213,7 +4227,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       SqlNode expr = expandedSelectList.get(ordinal);
       expr = stripAs(expr);
       if (expr instanceof SqlIdentifier) {
-        expr = getScope().fullyQualify((SqlIdentifier) expr);
+        expr = getScope().fullyQualify((SqlIdentifier) expr).identifier;
       }
 
       // Create a copy of the expression with the position of the order
@@ -4239,7 +4253,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       }
 
       // No match. Return identifier unchanged.
-      return getScope().fullyQualify(id);
+      return getScope().fullyQualify(id).identifier;
     }
 
     protected SqlNode visitScoped(SqlCall call) {