[CALCITE-2434] SqlAdvisor: support hints for nested tables/schemas

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

Branch: refs/heads/master
Commit: 1ed6b75717d154b046f0ba3df30e88f91811536c
Parents: 89e22e3
Author: Vladimir Sitnikov <[email protected]>
Authored: Thu Aug 2 15:17:04 2018 +0300
Committer: Vladimir Sitnikov <[email protected]>
Committed: Wed Sep 5 15:24:24 2018 +0300

----------------------------------------------------------------------
 .../calcite/jdbc/CalciteConnectionImpl.java     |  12 +-
 .../apache/calcite/sql/advise/SqlAdvisor.java   | 143 +++++++++--
 .../sql/advise/SqlAdvisorGetHintsFunction2.java | 120 +++++++++
 .../calcite/sql/advise/SqlAdvisorHint2.java     |  40 +++
 .../main/java/org/apache/calcite/util/Util.java |  23 ++
 .../apache/calcite/sql/test/SqlAdvisorTest.java | 255 +++++++++++++++++--
 .../apache/calcite/sql/test/SqlTestFactory.java |   1 +
 .../org/apache/calcite/test/CalciteSuite.java   |   1 +
 .../java/org/apache/calcite/test/JdbcTest.java  | 104 --------
 .../apache/calcite/test/SqlAdvisorJdbcTest.java | 172 +++++++++++++
 10 files changed, 718 insertions(+), 153 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java 
b/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java
index 5db3421..b7df1c6 100644
--- a/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java
+++ b/core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java
@@ -57,6 +57,7 @@ import org.apache.calcite.server.CalciteServerStatement;
 import org.apache.calcite.sql.advise.SqlAdvisor;
 import org.apache.calcite.sql.advise.SqlAdvisorValidator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParser;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 import org.apache.calcite.sql.validate.SqlValidatorWithHints;
 import org.apache.calcite.tools.RelRunner;
@@ -459,7 +460,16 @@ abstract class CalciteConnectionImpl
           new CalciteCatalogReader(rootSchema,
               schemaPath, typeFactory, con.config()),
           typeFactory, SqlConformanceEnum.DEFAULT);
-      return new SqlAdvisor(validator);
+      final CalciteConnectionConfig config = con.config();
+      // This duplicates 
org.apache.calcite.prepare.CalcitePrepareImpl.prepare2_
+      final SqlParser.Config parserConfig = SqlParser.configBuilder()
+          .setQuotedCasing(config.quotedCasing())
+          .setUnquotedCasing(config.unquotedCasing())
+          .setQuoting(config.quoting())
+          .setConformance(config.conformance())
+          .setCaseSensitive(config.caseSensitive())
+          .build();
+      return new SqlAdvisor(validator, parserConfig);
     }
 
     public SchemaPlus getRootSchema() {

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/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 69f3d59..2da1ca4 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
@@ -16,6 +16,7 @@
  */
 package org.apache.calcite.sql.advise;
 
+import org.apache.calcite.avatica.util.Casing;
 import org.apache.calcite.runtime.CalciteContextException;
 import org.apache.calcite.runtime.CalciteException;
 import org.apache.calcite.sql.SqlIdentifier;
@@ -26,6 +27,7 @@ import org.apache.calcite.sql.parser.SqlAbstractParserImpl;
 import org.apache.calcite.sql.parser.SqlParseException;
 import org.apache.calcite.sql.parser.SqlParser;
 import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.parser.SqlParserUtil;
 import org.apache.calcite.sql.validate.SqlMoniker;
 import org.apache.calcite.sql.validate.SqlMonikerImpl;
 import org.apache.calcite.sql.validate.SqlMonikerType;
@@ -64,13 +66,19 @@ public class SqlAdvisor {
   private final SqlValidatorWithHints validator;
   private final SqlParser.Config parserConfig;
 
+  // Cache for getPreferredCasing
+  private String prevWord;
+  private Casing prevPreferredCasing;
+
   //~ Constructors -----------------------------------------------------------
 
   /**
    * Creates a SqlAdvisor with a validator instance
    *
    * @param validator Validator
+   * @deprecated use {@link #SqlAdvisor(SqlValidatorWithHints, 
SqlParser.Config)}
    */
+  @Deprecated
   public SqlAdvisor(
       SqlValidatorWithHints validator) {
     this(validator, SqlParser.Config.DEFAULT);
@@ -164,32 +172,28 @@ public class SqlAdvisor {
     final List<SqlMoniker> completionHints =
         getCompletionHints0(sql, wordStart);
 
+    if (quoted) {
+      word = word.substring(1);
+    }
+
+    if (word.isEmpty()) {
+      return completionHints;
+    }
+
     // If cursor was part of the way through a word, only include hints
     // which start with that word in the result.
-    final List<SqlMoniker> result;
-    if (word.length() > 0) {
-      result = new ArrayList<SqlMoniker>();
-      if (quoted) {
-        // Quoted identifier. Case-sensitive match.
-        word = word.substring(1);
-        for (SqlMoniker hint : completionHints) {
-          String cname = hint.toString();
-          if (cname.startsWith(word)) {
-            result.add(hint);
-          }
-        }
-      } else {
-        // Regular identifier. Case-insensitive match.
-        for (SqlMoniker hint : completionHints) {
-          String cname = hint.toString();
-          if ((cname.length() >= word.length())
-              && cname.substring(0, word.length()).equalsIgnoreCase(word)) {
-            result.add(hint);
-          }
-        }
+    final List<SqlMoniker> result = new ArrayList<>();
+    Casing preferredCasing = getPreferredCasing(word);
+
+    boolean ignoreCase = preferredCasing != Casing.UNCHANGED;
+    for (SqlMoniker hint : completionHints) {
+      List<String> names = hint.getFullyQualifiedNames();
+      // For now we treat only simple cases where the added name is the last
+      // See [CALCITE-2439] Smart complete for SqlAdvisor
+      String cname = Util.last(names);
+      if (cname.regionMatches(ignoreCase, 0, word, 0, word.length())) {
+        result.add(hint);
       }
-    } else {
-      result = completionHints;
     }
 
     return result;
@@ -206,6 +210,91 @@ public class SqlAdvisor {
   }
 
   /**
+   * Returns casing which is preferred for replacement.
+   * For instance, {@code en => ename, EN => ENAME}.
+   * When input has mixed case, {@code Casing.UNCHANGED} is returned.
+   * @param word input word
+   * @return preferred casing when replacing input word
+   */
+  private Casing getPreferredCasing(String word) {
+    if (word == prevWord) {
+      return prevPreferredCasing;
+    }
+    boolean hasLower = false;
+    boolean hasUpper = false;
+    int i = 0;
+    while (i < word.length() && !(hasLower && hasUpper)) {
+      int codePoint = word.codePointAt(i);
+      hasLower |= Character.isLowerCase(codePoint);
+      hasUpper |= Character.isUpperCase(codePoint);
+      i += Character.charCount(codePoint);
+    }
+    Casing preferredCasing;
+    if (hasUpper && !hasLower) {
+      preferredCasing = Casing.TO_UPPER;
+    } else if (!hasUpper && hasLower) {
+      preferredCasing = Casing.TO_LOWER;
+    } else {
+      preferredCasing = Casing.UNCHANGED;
+    }
+    prevWord = word;
+    prevPreferredCasing = preferredCasing;
+    return preferredCasing;
+  }
+
+  public String getReplacement(SqlMoniker hint, String word) {
+    Casing preferredCasing = getPreferredCasing(word);
+    return getReplacement(hint, !word.isEmpty() && word.charAt(0) == 
quoteStart(), preferredCasing);
+  }
+
+  public String getReplacement(SqlMoniker hint, boolean quoted, Casing 
preferredCasing) {
+    String name = Util.last(hint.getFullyQualifiedNames());
+    boolean isKeyword = hint.getType() == SqlMonikerType.KEYWORD;
+    // If replacement has mixed case, we need to quote it (or not depending
+    // on quotedCasing/unquotedCasing
+    quoted &= !isKeyword;
+
+    StringBuilder sb =
+        new StringBuilder(name.length() + (quoted ? 2 : 0));
+
+    if (!isKeyword && !Util.isValidJavaIdentifier(name)) {
+      // needs quotes ==> quoted
+      quoted = true;
+    }
+    String idToAppend = name;
+
+    if (!quoted) {
+      // id ==preferredCasing==> preferredId ==unquotedCasing==> recasedId
+      // if recasedId matches id, then use preferredId
+      String preferredId = applyCasing(name, preferredCasing);
+      if (isKeyword || matchesUnquoted(name, preferredId)) {
+        idToAppend = preferredId;
+      } else {
+        // Check if we can use unquoted identifier as is: for instance, 
unquotedCasing==UNCHANGED
+        quoted = !matchesUnquoted(name, idToAppend);
+      }
+    }
+    if (quoted) {
+      sb.append(quoteStart());
+    }
+    sb.append(idToAppend);
+    if (quoted) {
+      sb.append(quoteEnd());
+    }
+
+    return sb.toString();
+  }
+
+  private boolean matchesUnquoted(String name, String idToAppend) {
+    String recasedId = applyCasing(idToAppend, parserConfig.unquotedCasing());
+    return recasedId.regionMatches(!parserConfig.caseSensitive(), 0, name, 0, 
name.length());
+  }
+
+  private String applyCasing(String value, Casing casing) {
+    return SqlParserUtil.strip(value, null, null, null, casing);
+  }
+
+  /**
    * Gets completion hints for a syntactically correct sql statement with dummy
    * SqlIdentifier
    *
@@ -240,7 +329,9 @@ public class SqlAdvisor {
 
     final SqlMoniker star =
         new SqlMonikerImpl(ImmutableList.of("*"), SqlMonikerType.KEYWORD);
-    if (hintList.contains(star) && !isSelectListItem(sqlNode, pos)) {
+    String hintToken =
+        parserConfig.unquotedCasing() == Casing.TO_UPPER ? UPPER_HINT_TOKEN : 
HINT_TOKEN;
+    if (hintList.contains(star) && !isSelectListItem(sqlNode, pos, hintToken)) 
{
       hintList.remove(star);
     }
 
@@ -261,10 +352,10 @@ public class SqlAdvisor {
   }
 
   private static boolean isSelectListItem(SqlNode root,
-      final SqlParserPos pos) {
+      final SqlParserPos pos, String hintToken) {
     List<SqlNode> nodes = SqlUtil.getAncestry(root,
         input -> input instanceof SqlIdentifier
-            && ((SqlIdentifier) input).names.contains(UPPER_HINT_TOKEN),
+            && ((SqlIdentifier) input).names.contains(hintToken),
         input -> Objects.requireNonNull(input).getParserPosition()
             .startsAt(pos));
     assert nodes.get(0) == root;

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorGetHintsFunction2.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorGetHintsFunction2.java
 
b/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorGetHintsFunction2.java
new file mode 100644
index 0000000..acc2059
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorGetHintsFunction2.java
@@ -0,0 +1,120 @@
+/*
+ * 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.advise;
+
+import org.apache.calcite.DataContext;
+import org.apache.calcite.adapter.enumerable.CallImplementor;
+import org.apache.calcite.adapter.enumerable.NullPolicy;
+import org.apache.calcite.adapter.enumerable.RexImpTable;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.linq4j.tree.Types;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.schema.FunctionParameter;
+import org.apache.calcite.schema.ImplementableFunction;
+import org.apache.calcite.schema.TableFunction;
+import org.apache.calcite.schema.impl.ReflectiveFunctionBase;
+import org.apache.calcite.sql.validate.SqlMoniker;
+import org.apache.calcite.util.BuiltInMethod;
+
+import com.google.common.collect.Iterables;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Table function that returns completion hints for a given SQL statement.
+ * This function adds replacement string column to previously existed
+ * {@link SqlAdvisorGetHintsFunction}
+ */
+public class SqlAdvisorGetHintsFunction2
+    implements TableFunction, ImplementableFunction {
+  private static final Expression ADVISOR =
+      Expressions.convert_(
+          Expressions.call(DataContext.ROOT,
+              BuiltInMethod.DATA_CONTEXT_GET.method,
+              
Expressions.constant(DataContext.Variable.SQL_ADVISOR.camelName)),
+          SqlAdvisor.class);
+
+  private static final Method GET_COMPLETION_HINTS =
+      Types.lookupMethod(SqlAdvisorGetHintsFunction2.class, 
"getCompletionHints",
+          SqlAdvisor.class, String.class, int.class);
+
+  private static final CallImplementor IMPLEMENTOR =
+      RexImpTable.createImplementor(
+          (translator, call, operands) ->
+              Expressions.call(GET_COMPLETION_HINTS,
+                  Iterables.concat(Collections.singleton(ADVISOR), operands)),
+          NullPolicy.ANY, false);
+
+  private static final List<FunctionParameter> PARAMETERS =
+      ReflectiveFunctionBase.builder()
+          .add(String.class, "sql")
+          .add(int.class, "pos")
+          .build();
+
+  public CallImplementor getImplementor() {
+    return IMPLEMENTOR;
+  }
+
+  public RelDataType getRowType(RelDataTypeFactory typeFactory,
+      List<Object> arguments) {
+    return typeFactory.createJavaType(SqlAdvisorHint2.class);
+  }
+
+  public Type getElementType(List<Object> arguments) {
+    return SqlAdvisorHint2.class;
+  }
+
+  public List<FunctionParameter> getParameters() {
+    return PARAMETERS;
+  }
+
+  /**
+   * Returns completion hints for a given SQL statement.
+   *
+   * <p>Typically this is called from generated code
+   * (via {@link SqlAdvisorGetHintsFunction2#IMPLEMENTOR}).
+   *
+   * @param advisor Advisor to produce completion hints
+   * @param sql     SQL to complete
+   * @param pos     Cursor position in SQL
+   * @return the table that contains completion hints for a given SQL statement
+   */
+  public static Enumerable<SqlAdvisorHint2> getCompletionHints(
+      final SqlAdvisor advisor, final String sql, final int pos) {
+    final String[] replaced = {null};
+    final List<SqlMoniker> hints = advisor.getCompletionHints(sql,
+        pos, replaced);
+    final List<SqlAdvisorHint2> res = new ArrayList<>(hints.size() + 1);
+    res.add(new SqlAdvisorHint2(replaced[0], null, "MATCH", null));
+
+    String word = replaced[0];
+    for (SqlMoniker hint : hints) {
+      res.add(new SqlAdvisorHint2(hint, advisor.getReplacement(hint, word)));
+    }
+    return Linq4j.asEnumerable(res).asQueryable();
+  }
+}
+
+// End SqlAdvisorGetHintsFunction2.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorHint2.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorHint2.java 
b/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorHint2.java
new file mode 100644
index 0000000..599348f
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorHint2.java
@@ -0,0 +1,40 @@
+/*
+ * 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.advise;
+
+import org.apache.calcite.sql.validate.SqlMoniker;
+
+/**
+ * This class is used to return values for
+ * {@link SqlAdvisor#getCompletionHints (String, int, String[])}.
+ */
+public class SqlAdvisorHint2 extends SqlAdvisorHint {
+  /** Replacement string */
+  public final String replacement;
+
+  public SqlAdvisorHint2(String id, String[] names, String type, String 
replacement) {
+    super(id, names, type);
+    this.replacement = replacement;
+  }
+
+  public SqlAdvisorHint2(SqlMoniker id, String replacement) {
+    super(id);
+    this.replacement = replacement;
+  }
+}
+
+// End SqlAdvisorHint2.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/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 1139643..32e5e7b 100644
--- a/core/src/main/java/org/apache/calcite/util/Util.java
+++ b/core/src/main/java/org/apache/calcite/util/Util.java
@@ -707,6 +707,29 @@ public class Util {
     return buf.toString();
   }
 
+  /**
+   * Returns true when input string is a valid Java identifier.
+   * @param s input string
+   * @return true when input string is a valid Java identifier
+   */
+  public static boolean isValidJavaIdentifier(String s) {
+    if (s.isEmpty()) {
+      return false;
+    }
+    if (!Character.isJavaIdentifierStart(s.codePointAt(0))) {
+      return false;
+    }
+    int i = 0;
+    while (i < s.length()) {
+      int codePoint = s.codePointAt(i);
+      if (!Character.isJavaIdentifierPart(codePoint)) {
+        return false;
+      }
+      i += Character.charCount(codePoint);
+    }
+    return true;
+  }
+
   public static String toLinux(String s) {
     return s.replaceAll("\r\n", "\n");
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/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 fdd0881..1e3b959 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
@@ -27,6 +27,8 @@ import org.apache.calcite.sql.validate.SqlMonikerType;
 import org.apache.calcite.test.SqlValidatorTestCase;
 import org.apache.calcite.test.WithLex;
 
+import com.google.common.collect.ImmutableMap;
+
 import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
@@ -37,8 +39,10 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeSet;
 
 import static org.junit.Assert.assertNotNull;
@@ -250,6 +254,11 @@ public class SqlAdvisorTest extends SqlValidatorTestCase {
           "COLUMN(DEPTNO)",
           "COLUMN(SLACKER)");
 
+  private static final List<String> EMP_COLUMNS_E =
+      Arrays.asList(
+          "COLUMN(EMPNO)",
+          "COLUMN(ENAME)");
+
   private static final List<String> DEPT_COLUMNS =
       Arrays.asList(
           "COLUMN(DEPTNO)",
@@ -448,11 +457,24 @@ public class SqlAdvisorTest extends SqlValidatorTestCase {
   protected void assertComplete(
       String sql,
       List<String>... expectedResults) {
+    assertComplete(sql, null, expectedResults);
+  }
+
+  protected void assertComplete(
+      String sql,
+      Map<String, String> replacements,
+      List<String>... expectedResults) {
     List<String> expectedList = plus(expectedResults);
     String expected = toString(new TreeSet<>(expectedList));
-    assertComplete(sql, expected, null);
+    assertComplete(sql, expected, null, replacements);
   }
 
+  protected void assertComplete(
+      String sql,
+      String expectedResults,
+      String expectedWord) {
+    assertComplete(sql, expectedResults, expectedWord, null);
+  }
   /**
    * Tests that a given SQL which may be invalid or incomplete simplifies
    * itself and yields the salesTables set of completion hints. This is an
@@ -466,20 +488,46 @@ public class SqlAdvisorTest extends SqlValidatorTestCase {
   protected void assertComplete(
       String sql,
       String expectedResults,
-      String expectedWord) {
+      String expectedWord,
+      Map<String, String> replacements) {
     SqlAdvisor advisor = tester.getFactory().createAdvisor();
 
     SqlParserUtil.StringAndPos sap = SqlParserUtil.findPos(sql);
     final String[] replaced = {null};
     List<SqlMoniker> results =
         advisor.getCompletionHints(sap.sql, sap.cursor, replaced);
-    assertNotNull(replaced[0]);
-    assertNotNull(results);
-    Assert.assertEquals(
+    Assert.assertEquals("Completion hints for " + sql,
         expectedResults, convertCompletionHints(results));
     if (expectedWord != null) {
-      Assert.assertEquals(expectedWord, replaced[0]);
+      Assert.assertEquals("replaced[0] for " + sql, expectedWord, replaced[0]);
+    } else {
+      assertNotNull(replaced[0]);
+    }
+    assertReplacements(sql, replacements, advisor, replaced[0], results);
+  }
+
+  private void assertReplacements(String sql, Map<String, String> 
replacements, SqlAdvisor advisor,
+      String word, List<SqlMoniker> results) {
+    if (replacements == null) {
+      return;
+    }
+    Set<String> missingReplacemenets = new HashSet<>(replacements.keySet());
+    for (SqlMoniker result : results) {
+      String id = result.id();
+      String expectedReplacement = replacements.get(id);
+      if (expectedReplacement == null) {
+        continue;
+      }
+      missingReplacemenets.remove(id);
+      String actualReplacement = advisor.getReplacement(result, word);
+      Assert.assertEquals(sql + ", replacement of " + word + " with " + id,
+          expectedReplacement, actualReplacement);
     }
+    if (missingReplacemenets.isEmpty()) {
+      return;
+    }
+    Assert.fail("Sql " + sql + " did not produce replacement hints " + 
missingReplacemenets);
+
   }
 
   protected void assertEquals(
@@ -500,6 +548,9 @@ public class SqlAdvisorTest extends SqlValidatorTestCase {
   }
 
   private String convertCompletionHints(List<SqlMoniker> hints) {
+    if (hints == null) {
+      return "<<NULL>>";
+    }
     List<String> list = new ArrayList<String>();
     for (SqlMoniker hint : hints) {
       if (hint.getType() != SqlMonikerType.FUNCTION) {
@@ -650,14 +701,22 @@ public class SqlAdvisorTest extends SqlValidatorTestCase {
     sql =
         "select a.empno, b.deptno from sales.emp a, sales.dept b "
             + "where b.deptno=a.^";
-    assertComplete(sql, EMP_COLUMNS); // where list
+    assertComplete(sql, ImmutableMap.of("COLUMN(COMM)", "COMM"),
+        EMP_COLUMNS); // where list
+
+    sql =
+        "select a.empno, b.deptno from sales.emp a, sales.dept b "
+            + "where b.deptno=a.e^";
+    assertComplete(sql, ImmutableMap.of("COLUMN(ENAME)", "ename"),
+        EMP_COLUMNS_E); // where list
 
     // hints contain no columns, only table aliases, because there are >1
     // aliases
     sql =
         "select a.empno, b.deptno from sales.emp a, sales.dept b "
             + "where ^dummy=1";
-    assertHint(sql, AB_TABLES, EXPR_KEYWORDS); // where list
+    assertComplete(sql, ImmutableMap.of("KEYWORD(CURRENT_TIMESTAMP)", 
"CURRENT_TIMESTAMP"),
+        AB_TABLES, EXPR_KEYWORDS); // where list
 
     sql =
         "select a.empno, b.deptno from sales.emp a, sales.dept b "
@@ -1173,7 +1232,20 @@ public class SqlAdvisorTest extends SqlValidatorTestCase 
{
 
   @Test public void testPartialIdentifier() {
     String sql = "select * from emp where e^ and emp.deptno = 10";
-    final String expected =
+    String expected =
+        "COLUMN(EMPNO)\n"
+            + "COLUMN(ENAME)\n"
+            + "KEYWORD(ELEMENT)\n"
+            + "KEYWORD(EXISTS)\n"
+            + "KEYWORD(EXP)\n"
+            + "KEYWORD(EXTRACT)\n"
+            + "TABLE(EMP)\n";
+    assertComplete(sql, expected, "e",
+        ImmutableMap.of("KEYWORD(EXISTS)", "exists",
+            "TABLE(EMP)", "emp"));
+
+    sql = "select * from emp where \"e^ and emp.deptno = 10";
+    expected =
         "COLUMN(EMPNO)\n"
             + "COLUMN(ENAME)\n"
             + "KEYWORD(ELEMENT)\n"
@@ -1181,7 +1253,22 @@ public class SqlAdvisorTest extends SqlValidatorTestCase 
{
             + "KEYWORD(EXP)\n"
             + "KEYWORD(EXTRACT)\n"
             + "TABLE(EMP)\n";
-    assertComplete(sql, expected, "e");
+    assertComplete(sql, expected, "\"e",
+        ImmutableMap.of("KEYWORD(EXISTS)", "exists",
+            "TABLE(EMP)", "\"EMP\""));
+
+    sql = "select * from emp where E^ and emp.deptno = 10";
+    expected =
+        "COLUMN(EMPNO)\n"
+            + "COLUMN(ENAME)\n"
+            + "KEYWORD(ELEMENT)\n"
+            + "KEYWORD(EXISTS)\n"
+            + "KEYWORD(EXP)\n"
+            + "KEYWORD(EXTRACT)\n"
+            + "TABLE(EMP)\n";
+    assertComplete(sql, expected, "E",
+        ImmutableMap.of("KEYWORD(EXISTS)", "EXISTS",
+            "TABLE(EMP)", "EMP"));
 
     // cursor in middle of word and at end
     sql = "select * from emp where e^";
@@ -1189,35 +1276,57 @@ public class SqlAdvisorTest extends 
SqlValidatorTestCase {
 
     // longer completion
     sql = "select * from emp where em^";
-    assertComplete(sql, EMPNO_EMP, null);
+    assertComplete(sql, EMPNO_EMP, null, ImmutableMap.of("COLUMN(EMPNO)", 
"empno"));
 
     // word after punctuation
     sql = "select deptno,em^ from emp where 1+2<3+4";
-    assertComplete(sql, EMPNO_EMP, null);
+    assertComplete(sql, EMPNO_EMP, null, ImmutableMap.of("COLUMN(EMPNO)", 
"empno"));
 
     // inside double-quotes, no terminating double-quote.
     // Only identifiers should be suggested (no keywords),
     // and suggestion should include double-quotes
     sql = "select deptno,\"EM^ from emp where 1+2<3+4";
-    assertComplete(sql, EMPNO_EMP, "\"EM");
+    assertComplete(sql, EMPNO_EMP, "\"EM", ImmutableMap.of("COLUMN(EMPNO)", 
"\"EMPNO\""));
 
-    // inside double-quotes, match is case-sensitive
+    // inside double-quotes, match is case-insensitive as well
     sql = "select deptno,\"em^ from emp where 1+2<3+4";
-    assertComplete(sql, "", "\"em");
+    assertComplete(sql, EMPNO_EMP, "\"em", ImmutableMap.of("COLUMN(EMPNO)", 
"\"EMPNO\""));
+
+    // when input strings has mixed casing, match should be case-sensitive
+    sql = "select deptno,eM^ from emp where 1+2<3+4";
+    assertComplete(sql, "", "eM");
+
+    // when input strings has mixed casing, match should be case-sensitive
+    sql = "select deptno,\"eM^ from emp where 1+2<3+4";
+    assertComplete(sql, "", "\"eM");
 
     // eat up following double-quote
     sql = "select deptno,\"EM^ps\" from emp where 1+2<3+4";
-    assertComplete(sql, EMPNO_EMP, "\"EM");
+    assertComplete(sql, EMPNO_EMP, "\"EM", ImmutableMap.of("COLUMN(EMPNO)", 
"\"EMPNO\""));
 
     // closing double-quote is at very end of string
     sql = "select * from emp where 5 = \"EM^xxx\"";
-    assertComplete(sql, EMPNO_EMP, "\"EM");
+    assertComplete(sql, EMPNO_EMP, "\"EM", ImmutableMap.of("COLUMN(EMPNO)", 
"\"EMPNO\""));
 
     // just before dot
     sql = "select emp.^name from emp";
     assertComplete(sql, EMP_COLUMNS, STAR_KEYWORD);
   }
 
+  @Test @WithLex(Lex.JAVA) public void testAdviceKeywordsJava() {
+    String sql;
+    sql = "select deptno, exi^ from emp where 1+2<3+4";
+    assertComplete(sql, "KEYWORD(EXISTS)\n", "exi",
+        ImmutableMap.of("KEYWORD(EXISTS)", "exists"));
+  }
+
+  @Test @WithLex(Lex.JAVA) public void testAdviceMixedCase() {
+    String sql;
+    sql = "select is^ from (select 1 isOne from emp)";
+    assertComplete(sql, "COLUMN(isOne)\n", "is",
+        ImmutableMap.of("COLUMN(isOne)", "isOne"));
+  }
+
   @Test public void testInsert() throws Exception {
     String sql;
     sql = "insert into emp(empno, mgr) select ^ from dept a";
@@ -1245,20 +1354,122 @@ public class SqlAdvisorTest extends 
SqlValidatorTestCase {
     sql = "select * from sales.n^";
     assertComplete(
         sql,
-        Collections.singletonList("SCHEMA(CATALOG.SALES.NEST)"));
+        "SCHEMA(CATALOG.SALES.NEST)\n",
+        "n",
+        ImmutableMap.of("SCHEMA(CATALOG.SALES.NEST)", "nest"));
 
-    sql = "select * from sales.n^asfasdf";
+    sql = "select * from sales.\"n^asfasdf";
     assertComplete(
         sql,
-        Collections.singletonList("SCHEMA(CATALOG.SALES.NEST)"));
+        "SCHEMA(CATALOG.SALES.NEST)\n",
+        "\"n",
+        ImmutableMap.of("SCHEMA(CATALOG.SALES.NEST)", "\"NEST\""));
 
     sql = "select * from sales.n^est";
     assertComplete(
         sql,
-        Collections.singletonList("SCHEMA(CATALOG.SALES.NEST)"));
+        "SCHEMA(CATALOG.SALES.NEST)\n",
+        "n",
+        ImmutableMap.of("SCHEMA(CATALOG.SALES.NEST)", "nest"));
 
     sql = "select * from sales.nu^";
-    assertComplete(sql, "", null);
+    assertComplete(sql, "", "nu");
+  }
+
+  @Ignore("The set of completion results is empty")
+  @Test public void testNestTable1() throws Exception {
+    String sql;
+    // select scott.emp.deptno from scott.emp; # valid
+    sql = "select catalog.sales.emp.em^ from catalog.sales.emp";
+    assertComplete(
+        sql,
+        "COLUMN(EMPNO)\n",
+        "em",
+        ImmutableMap.of("COLUMN(EMPNO)", "empno"));
+
+    sql = "select catalog.sales.em^ from catalog.sales.emp";
+    assertComplete(
+        sql,
+        "TABLE(EMP)\n",
+        "em",
+        ImmutableMap.of("TABLE(EMP)", "emp"));
+  }
+
+  @Test public void testNestTable2() throws Exception {
+    String sql;
+    // select scott.emp.deptno from scott.emp as e; # not valid
+    sql = "select catalog.sales.emp.em^ from catalog.sales.emp as e";
+    assertComplete(
+        sql,
+        "",
+        "em");
+  }
+
+
+  @Ignore("The set of completion results is empty")
+  @Test public void testNestTable3() throws Exception {
+    String sql;
+    // select scott.emp.deptno from emp; # valid
+    sql = "select catalog.sales.emp.em^ from emp";
+    assertComplete(
+        sql,
+        "COLUMN(EMPNO)\n",
+        "em",
+        ImmutableMap.of("COLUMN(EMP)", "empno"));
+
+    sql = "select catalog.sales.em^ from emp";
+    assertComplete(
+        sql,
+        "TABLE(EMP)\n",
+        "em",
+        ImmutableMap.of("TABLE(EMP)", "emp"));
+  }
+
+  @Test public void testNestTable4() throws Exception {
+    String sql;
+    // select scott.emp.deptno from emp as emp; # not valid
+    sql = "select catalog.sales.emp.em^ from catalog.sales.emp as emp";
+    assertComplete(
+        sql,
+        "",
+        "em");
+  }
+
+  @Test public void testNestTableSchemaMustMatch() throws Exception {
+    String sql;
+    // select foo.emp.deptno from emp; # not valid
+    sql = "select sales.nest.em^ from catalog.sales.emp_r";
+    assertComplete(
+        sql,
+        "",
+        "em");
+  }
+
+  @WithLex(Lex.SQL_SERVER) @Test public void testNestSchemaSqlServer() throws 
Exception {
+    String sql;
+    sql = "select * from SALES.N^";
+    assertComplete(
+        sql,
+        "SCHEMA(CATALOG.SALES.NEST)\n",
+        "N",
+        ImmutableMap.of("SCHEMA(CATALOG.SALES.NEST)", "NEST"));
+
+    sql = "select * from SALES.[n^asfasdf";
+    assertComplete(
+        sql,
+        "SCHEMA(CATALOG.SALES.NEST)\n",
+        "[n",
+        ImmutableMap.of("SCHEMA(CATALOG.SALES.NEST)", "[NEST]"));
+
+    sql = "select * from SALES.[N^est";
+    assertComplete(
+        sql,
+        "SCHEMA(CATALOG.SALES.NEST)\n",
+        "[N",
+        ImmutableMap.of("SCHEMA(CATALOG.SALES.NEST)", "[NEST]"));
+
+    sql = "select * from SALES.NU^";
+    assertComplete(sql, "", "NU");
   }
 
   @Test public void testUnion() throws Exception {

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/core/src/test/java/org/apache/calcite/sql/test/SqlTestFactory.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlTestFactory.java 
b/core/src/test/java/org/apache/calcite/sql/test/SqlTestFactory.java
index c3bc4a6..c3e94cf 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlTestFactory.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlTestFactory.java
@@ -121,6 +121,7 @@ public class SqlTestFactory {
         .setUnquotedCasing((Casing) options.get("unquotedCasing"))
         .setQuotedCasing((Casing) options.get("quotedCasing"))
         .setConformance((SqlConformance) options.get("conformance"))
+        .setCaseSensitive((boolean) options.get("caseSensitive"))
         .build();
   }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java 
b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
index 7042010..5a8da4c 100644
--- a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
+++ b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
@@ -165,6 +165,7 @@ import org.junit.runners.Suite;
     ProfilerTest.class,
     LatticeTest.class,
     ReflectiveSchemaTest.class,
+    SqlAdvisorJdbcTest.class,
     JdbcTest.class,
     CoreQuidemTest.class,
     CalciteRemoteDriverTest.class,

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/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 70ffe1d..11551e1 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -69,7 +69,6 @@ import org.apache.calcite.schema.SchemaFactory;
 import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.Table;
 import org.apache.calcite.schema.TableFactory;
-import org.apache.calcite.schema.TableFunction;
 import org.apache.calcite.schema.TableMacro;
 import org.apache.calcite.schema.TranslatableTable;
 import org.apache.calcite.schema.impl.AbstractSchema;
@@ -84,10 +83,8 @@ import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlSpecialOperator;
-import org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction;
 import org.apache.calcite.sql.parser.SqlParser;
 import org.apache.calcite.sql.parser.SqlParserPos;
-import org.apache.calcite.sql.parser.SqlParserUtil;
 import org.apache.calcite.sql.parser.impl.SqlParserImpl;
 import org.apache.calcite.util.Bug;
 import org.apache.calcite.util.JsonBuilder;
@@ -400,107 +397,6 @@ public class JdbcTest {
     }
   }
 
-
-
-  /**
-   * Tests {@link org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction}.
-   */
-  @Test public void testSqlAdvisorGetHintsFunction()
-      throws SQLException, ClassNotFoundException {
-    adviseSql("select e.e^ from \"emps\" e",
-        CalciteAssert.checkResultUnordered(
-            "id=e; names=null; type=MATCH",
-            "id=empid; names=[empid]; type=COLUMN"));
-  }
-
-  @Test public void testSqlAdvisorNonExistingColumn()
-      throws SQLException, ClassNotFoundException {
-    adviseSql("select e.empdid_wrong_name.^ from \"hr\".\"emps\" e",
-        CalciteAssert.checkResultUnordered(
-            "id=*; names=[*]; type=KEYWORD",
-            "id=; names=null; type=MATCH"));
-  }
-
-  @Test public void testSqlAdvisorNonStructColumn()
-      throws SQLException, ClassNotFoundException {
-    adviseSql("select e.\"empid\".^ from \"hr\".\"emps\" e",
-        CalciteAssert.checkResultUnordered(
-            "id=*; names=[*]; type=KEYWORD",
-            "id=; names=null; type=MATCH"));
-  }
-
-  @Test public void testSqlAdvisorSubSchema()
-      throws SQLException, ClassNotFoundException {
-    adviseSql("select * from \"hr\".^.test_test_test",
-        CalciteAssert.checkResultUnordered(
-            "id=; names=null; type=MATCH",
-            "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",
-            "id=hr; names=[hr]; type=SCHEMA"));
-  }
-
-  @Test public void testSqlAdvisorTableInSchema()
-      throws SQLException, ClassNotFoundException {
-    adviseSql("select * from \"hr\".^",
-        CalciteAssert.checkResultUnordered(
-            "id=; names=null; type=MATCH",
-            "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",
-            "id=hr; names=[hr]; type=SCHEMA"));
-  }
-
-  /**
-   * Tests {@link org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction}.
-   */
-  @Test public void testSqlAdvisorSchemaNames()
-      throws SQLException, ClassNotFoundException {
-    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, Consumer<ResultSet> checker)
-      throws ClassNotFoundException, SQLException {
-    Properties info = new Properties();
-    info.put("lex", "JAVA");
-    info.put("quoting", "DOUBLE_QUOTE");
-    Connection connection =
-        DriverManager.getConnection("jdbc:calcite:", info);
-    CalciteConnection calciteConnection =
-        connection.unwrap(CalciteConnection.class);
-    SchemaPlus rootSchema = calciteConnection.getRootSchema();
-    rootSchema.add("hr", new ReflectiveSchema(new HrSchema()));
-    SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
-    calciteConnection.setSchema("hr");
-    final TableFunction table =
-        new SqlAdvisorGetHintsFunction();
-    schema.add("get_hints", table);
-    PreparedStatement ps = connection.prepareStatement("select *\n"
-        + "from table(\"s\".\"get_hints\"(?, ?)) as t(id, names, type)");
-    SqlParserUtil.StringAndPos sap = SqlParserUtil.findPos(sql);
-    ps.setString(1, sap.sql);
-    ps.setInt(2, sap.cursor);
-    final ResultSet resultSet = ps.executeQuery();
-    checker.accept(resultSet);
-    resultSet.close();
-    connection.close();
-  }
-
   /**
    * Tests a relation that is accessed via method syntax.
    *

http://git-wip-us.apache.org/repos/asf/calcite/blob/1ed6b757/core/src/test/java/org/apache/calcite/test/SqlAdvisorJdbcTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/SqlAdvisorJdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlAdvisorJdbcTest.java
new file mode 100644
index 0000000..907ec63
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/test/SqlAdvisorJdbcTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.test;
+
+import org.apache.calcite.adapter.java.ReflectiveSchema;
+import org.apache.calcite.jdbc.CalciteConnection;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.TableFunction;
+import org.apache.calcite.schema.impl.AbstractSchema;
+import org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction;
+import org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction2;
+import org.apache.calcite.sql.parser.SqlParserUtil;
+
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Properties;
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link org.apache.calcite.sql.advise.SqlAdvisor}.
+ */
+public class SqlAdvisorJdbcTest {
+
+  private void adviseSql(int apiVersion, String sql, Consumer<ResultSet> 
checker)
+      throws SQLException {
+    Properties info = new Properties();
+    if (apiVersion == 1) {
+      info.put("lex", "JAVA");
+      info.put("quoting", "DOUBLE_QUOTE");
+    } else if (apiVersion == 2) {
+      info.put("lex", "SQL_SERVER");
+      info.put("quoting", "BRACKET");
+    }
+    Connection connection =
+        DriverManager.getConnection("jdbc:calcite:", info);
+    CalciteConnection calciteConnection =
+        connection.unwrap(CalciteConnection.class);
+    SchemaPlus rootSchema = calciteConnection.getRootSchema();
+    rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
+    SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
+    calciteConnection.setSchema("hr");
+    final TableFunction getHints =
+        apiVersion == 1 ? new SqlAdvisorGetHintsFunction() : new 
SqlAdvisorGetHintsFunction2();
+    schema.add("get_hints", getHints);
+    String getHintsSql;
+    if (apiVersion == 1) {
+      getHintsSql = "select id, names, type from table(\"s\".\"get_hints\"(?, 
?)) as t";
+    } else {
+      getHintsSql = "select id, names, type, replacement from 
table([s].[get_hints](?, ?)) as t";
+    }
+
+    PreparedStatement ps = connection.prepareStatement(getHintsSql);
+    SqlParserUtil.StringAndPos sap = SqlParserUtil.findPos(sql);
+    ps.setString(1, sap.sql);
+    ps.setInt(2, sap.cursor);
+    final ResultSet resultSet = ps.executeQuery();
+    checker.accept(resultSet);
+    resultSet.close();
+    connection.close();
+  }
+
+  @Test public void testSqlAdvisorGetHintsFunction()
+      throws SQLException, ClassNotFoundException {
+    adviseSql(1, "select e.e^ from \"emps\" e",
+        CalciteAssert.checkResultUnordered(
+            "id=e; names=null; type=MATCH",
+            "id=empid; names=[empid]; type=COLUMN"));
+  }
+
+  @Test public void testSqlAdvisorGetHintsFunction2()
+      throws SQLException, ClassNotFoundException {
+    adviseSql(2, "select [e].e^ from [emps] e",
+        CalciteAssert.checkResultUnordered(
+            "id=e; names=null; type=MATCH; replacement=null",
+            "id=empid; names=[empid]; type=COLUMN; replacement=empid"));
+  }
+
+  @Test public void testSqlAdvisorNonExistingColumn()
+      throws SQLException, ClassNotFoundException {
+    adviseSql(1, "select e.empdid_wrong_name.^ from \"hr\".\"emps\" e",
+        CalciteAssert.checkResultUnordered(
+            "id=*; names=[*]; type=KEYWORD",
+            "id=; names=null; type=MATCH"));
+  }
+
+  @Test public void testSqlAdvisorNonStructColumn()
+      throws SQLException, ClassNotFoundException {
+    adviseSql(1, "select e.\"empid\".^ from \"hr\".\"emps\" e",
+        CalciteAssert.checkResultUnordered(
+            "id=*; names=[*]; type=KEYWORD",
+            "id=; names=null; type=MATCH"));
+  }
+
+  @Test public void testSqlAdvisorSubSchema()
+      throws SQLException, ClassNotFoundException {
+    adviseSql(1, "select * from \"hr\".^.test_test_test",
+        CalciteAssert.checkResultUnordered(
+            "id=; names=null; type=MATCH",
+            "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",
+            "id=hr; names=[hr]; type=SCHEMA"));
+  }
+
+  @Test public void testSqlAdvisorSubSchema2()
+      throws SQLException, ClassNotFoundException {
+    adviseSql(2, "select * from [hr].^.test_test_test",
+        CalciteAssert.checkResultUnordered(
+            "id=; names=null; type=MATCH; replacement=null",
+            "id=hr.dependents; names=[hr, dependents]; type=TABLE; 
replacement=dependents",
+            "id=hr.depts; names=[hr, depts]; type=TABLE; replacement=depts",
+            "id=hr.emps; names=[hr, emps]; type=TABLE; replacement=emps",
+            "id=hr.locations; names=[hr, locations]; type=TABLE; 
replacement=locations",
+            "id=hr; names=[hr]; type=SCHEMA; replacement=hr"));
+  }
+
+  @Test public void testSqlAdvisorTableInSchema()
+      throws SQLException, ClassNotFoundException {
+    adviseSql(1, "select * from \"hr\".^",
+        CalciteAssert.checkResultUnordered(
+            "id=; names=null; type=MATCH",
+            "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",
+            "id=hr; names=[hr]; type=SCHEMA"));
+  }
+
+  /**
+   * Tests {@link org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction}.
+   */
+  @Test public void testSqlAdvisorSchemaNames()
+      throws SQLException, ClassNotFoundException {
+    adviseSql(1, "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"));
+  }
+
+}
+
+// End SqlAdvisorJdbcTest.java

Reply via email to