From e69f82042311887115dfb642fe942efcd2c0792b Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Mon, 16 Apr 2018 19:47:38 +0300 Subject: [PATCH] SQL: Expand SYS TABLES behavior to support ODBC spec (elastic/x-pack-elasticsearch#4375) Add behavior for enumerating catalogs, table types, etc when the main param is SQL_ALL_* while all the rest are empty strings Fix elastic/x-pack-elasticsearch#4334 Original commit: elastic/x-pack-elasticsearch@28e7b15904f39013c2a87a4c95c9f3263ee05048 --- .../sql/analysis/index/IndexResolver.java | 10 ++- .../xpack/sql/parser/CommandBuilder.java | 15 ++-- .../logical/command/sys/SysTableTypes.java | 3 +- .../plan/logical/command/sys/SysTables.java | 33 +++++++++ .../xpack/sql/util/StringUtils.java | 7 +- .../logical/command/sys/SysTablesTests.java | 68 ++++++++++++++++--- 6 files changed, 116 insertions(+), 20 deletions(-) diff --git a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolver.java b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolver.java index e9ae4ae59a5..1800c170b7c 100644 --- a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolver.java +++ b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolver.java @@ -44,7 +44,11 @@ public class IndexResolver { public enum IndexType { INDEX("BASE TABLE"), - ALIAS("ALIAS"); + ALIAS("ALIAS"), + // value for user types unrecognized + UNKNOWN("UKNOWN"); + + public static final EnumSet VALID = EnumSet.of(INDEX, ALIAS); private final String toSql; @@ -59,13 +63,13 @@ public class IndexResolver { public static IndexType from(String name) { if (name != null) { name = name.toUpperCase(Locale.ROOT); - for (IndexType type : IndexType.values()) { + for (IndexType type : IndexType.VALID) { if (type.toSql.equals(name)) { return type; } } } - return null; + return IndexType.UNKNOWN; } } diff --git a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/CommandBuilder.java b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/CommandBuilder.java index ac34890c376..fb08d08fcb9 100644 --- a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/CommandBuilder.java +++ b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/CommandBuilder.java @@ -35,6 +35,7 @@ import org.elasticsearch.xpack.sql.plan.logical.command.sys.SysTables; import org.elasticsearch.xpack.sql.plan.logical.command.sys.SysTypes; import org.elasticsearch.xpack.sql.plugin.SqlTypedParamValue; import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.util.StringUtils; import java.util.ArrayList; import java.util.EnumSet; @@ -150,13 +151,19 @@ abstract class CommandBuilder extends LogicalPlanBuilder { for (StringContext string : ctx.string()) { String value = string(string); if (value != null) { - IndexType type = IndexType.from(value); - if (type == null) { - throw new ParsingException(source(ctx), "Invalid table type [{}]", value); + // check special ODBC wildcard case + if (value.equals(StringUtils.SQL_WILDCARD) && ctx.string().size() == 1) { + // since % is the same as not specifying a value, choose + // https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/value-list-arguments?view=ssdt-18vs2017 + // that is skip the value + } else { + IndexType type = IndexType.from(value); + types.add(type); } - types.add(type); } } + + // if the ODBC enumeration is specified, skip validation EnumSet set = types.isEmpty() ? null : EnumSet.copyOf(types); return new SysTables(source(ctx), visitPattern(ctx.clusterPattern), visitPattern(ctx.tablePattern), set); } diff --git a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTableTypes.java b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTableTypes.java index 6f395b3ef34..ff6789bc373 100644 --- a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTableTypes.java +++ b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTableTypes.java @@ -16,7 +16,6 @@ import org.elasticsearch.xpack.sql.tree.Location; import org.elasticsearch.xpack.sql.tree.NodeInfo; import java.util.List; -import java.util.stream.Stream; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; @@ -43,7 +42,7 @@ public class SysTableTypes extends Command { @Override public final void execute(SqlSession session, ActionListener listener) { - listener.onResponse(Rows.of(output(), Stream.of(IndexType.values()) + listener.onResponse(Rows.of(output(), IndexType.VALID.stream() .map(t -> singletonList(t.toSql())) .collect(toList()))); } diff --git a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTables.java b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTables.java index 50c165cc75c..2b8e5e8527c 100644 --- a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTables.java +++ b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTables.java @@ -15,7 +15,9 @@ import org.elasticsearch.xpack.sql.session.SchemaRowSet; import org.elasticsearch.xpack.sql.session.SqlSession; import org.elasticsearch.xpack.sql.tree.Location; import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.util.CollectionUtils; +import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -24,6 +26,7 @@ import java.util.regex.Pattern; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import static org.elasticsearch.xpack.sql.util.StringUtils.EMPTY; +import static org.elasticsearch.xpack.sql.util.StringUtils.SQL_WILDCARD; public class SysTables extends Command { @@ -62,6 +65,36 @@ public class SysTables extends Command { public final void execute(SqlSession session, ActionListener listener) { String cluster = session.indexResolver().clusterName(); + // first check if where dealing with ODBC enumeration + // namely one param specified with '%', everything else empty string + // https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqltables-function?view=ssdt-18vs2017#comments + + if (clusterPattern != null && clusterPattern.pattern().equals(SQL_WILDCARD)) { + if (pattern != null && pattern.pattern().isEmpty() && CollectionUtils.isEmpty(types)) { + Object[] enumeration = new Object[10]; + // send only the cluster, everything else null + enumeration[0] = cluster; + listener.onResponse(Rows.singleton(output(), enumeration)); + return; + } + } + + // if no types were specified (the parser takes care of the % case) + if (CollectionUtils.isEmpty(types)) { + if (clusterPattern != null && clusterPattern.pattern().isEmpty()) { + List> values = new ArrayList<>(); + // send only the types, everything else null + for (IndexType type : IndexType.VALID) { + Object[] enumeration = new Object[10]; + enumeration[3] = type.toSql(); + values.add(asList(enumeration)); + } + listener.onResponse(Rows.of(output(), values)); + return; + } + } + + String cRegex = clusterPattern != null ? clusterPattern.asJavaRegex() : null; // if the catalog doesn't match, don't return any results diff --git a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/util/StringUtils.java b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/util/StringUtils.java index b591db7a6bc..e8bb9368d69 100644 --- a/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/util/StringUtils.java +++ b/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/util/StringUtils.java @@ -26,6 +26,7 @@ public abstract class StringUtils { public static final String EMPTY = ""; public static final String NEW_LINE = "\n"; + public static final String SQL_WILDCARD = "%"; //CamelCase to camel_case public static String camelCaseToUnderscore(String string) { @@ -82,7 +83,7 @@ public abstract class StringUtils { else { switch (curr) { case '%': - regex.append(escaped ? "%" : ".*"); + regex.append(escaped ? SQL_WILDCARD : ".*"); break; case '_': regex.append(escaped ? "_" : "."); @@ -145,7 +146,7 @@ public abstract class StringUtils { } else { switch (curr) { case '%': - wildcard.append(escaped ? "%" : "*"); + wildcard.append(escaped ? SQL_WILDCARD : "*"); break; case '_': wildcard.append(escaped ? "_" : "?"); @@ -191,7 +192,7 @@ public abstract class StringUtils { } else { switch (curr) { case '%': - wildcard.append(escaped ? "%" : "*"); + wildcard.append(escaped ? SQL_WILDCARD : "*"); break; case '_': wildcard.append(escaped ? "_" : "*"); diff --git a/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTablesTests.java b/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTablesTests.java index 212327b7268..66cd934da38 100644 --- a/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTablesTests.java +++ b/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTablesTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.xpack.sql.analysis.index.IndexResolver; import org.elasticsearch.xpack.sql.analysis.index.IndexResolver.IndexInfo; import org.elasticsearch.xpack.sql.analysis.index.IndexResolver.IndexType; import org.elasticsearch.xpack.sql.expression.function.FunctionRegistry; -import org.elasticsearch.xpack.sql.parser.ParsingException; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.plan.logical.command.Command; import org.elasticsearch.xpack.sql.plugin.SqlTypedParamValue; @@ -26,6 +25,7 @@ import org.elasticsearch.xpack.sql.type.EsField; import org.elasticsearch.xpack.sql.type.TypesTests; import org.joda.time.DateTimeZone; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -41,6 +41,8 @@ import static org.mockito.Mockito.when; public class SysTablesTests extends ESTestCase { + private static final String CLUSTER_NAME = "cluster"; + private final SqlParser parser = new SqlParser(); private final Map mapping = TypesTests.loadMapping("mapping-multi-field-with-nested.json", true); private final IndexInfo index = new IndexInfo("test", IndexType.INDEX); @@ -137,25 +139,75 @@ public class SysTablesTests extends ESTestCase { } public void testSysTablesWithInvalidType() throws Exception { - ParsingException pe = expectThrows(ParsingException.class, () -> sql("SYS TABLES LIKE 'test' TYPE 'QUE HORA ES'")); - assertEquals("line 1:2: Invalid table type [QUE HORA ES]", pe.getMessage()); + executeCommand("SYS TABLES LIKE 'test' TYPE 'QUE HORA ES'", r -> { + assertEquals(0, r.size()); + }, new IndexInfo[0]); + } + + public void testSysTablesCatalogEnumeration() throws Exception { + executeCommand("SYS TABLES CATALOG LIKE '%' LIKE ''", r -> { + assertEquals(1, r.size()); + assertEquals(CLUSTER_NAME, r.column(0)); + // everything else should be null + for (int i = 1; i < 10; i++) { + assertNull(r.column(i)); + } + }, new IndexInfo[0]); + } + + public void testSysTablesTypesEnumeration() throws Exception { + executeCommand("SYS TABLES CATALOG LIKE '' LIKE '' TYPE '%'", r -> { + assertEquals(2, r.size()); + + Iterator it = IndexType.VALID.iterator(); + + for (int t = 0; t < r.size(); t++) { + assertEquals(it.next().toSql(), r.column(3)); + + // everything else should be null + for (int i = 0; i < 10; i++) { + if (i != 3) { + assertNull(r.column(i)); + } + } + + r.advanceRow(); + } + }, new IndexInfo[0]); + } + + public void testSysTablesTypesEnumerationWoString() throws Exception { + executeCommand("SYS TABLES CATALOG LIKE '' LIKE '' ", r -> { + assertEquals(2, r.size()); + + Iterator it = IndexType.VALID.iterator(); + + for (int t = 0; t < r.size(); t++) { + assertEquals(it.next().toSql(), r.column(3)); + + // everything else should be null + for (int i = 0; i < 10; i++) { + if (i != 3) { + assertNull(r.column(i)); + } + } + + r.advanceRow(); + } + }, new IndexInfo[0]); } private SqlTypedParamValue param(Object value) { return new SqlTypedParamValue(value, DataTypes.fromJava(value)); } - private Tuple sql(String sql) { - return sql(sql, emptyList()); - } - private Tuple sql(String sql, List params) { EsIndex test = new EsIndex("test", mapping); Analyzer analyzer = new Analyzer(new FunctionRegistry(), IndexResolution.valid(test), DateTimeZone.UTC); Command cmd = (Command) analyzer.analyze(parser.createStatement(sql, params), true); IndexResolver resolver = mock(IndexResolver.class); - when(resolver.clusterName()).thenReturn("cluster"); + when(resolver.clusterName()).thenReturn(CLUSTER_NAME); SqlSession session = new SqlSession(null, null, null, resolver, null, null, null); return new Tuple<>(cmd, session);