[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
