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@28e7b15904
This commit is contained in:
Costin Leau 2018-04-16 19:47:38 +03:00 committed by GitHub
parent bf5f7e1847
commit e69f820423
6 changed files with 116 additions and 20 deletions

View File

@ -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<IndexType> 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;
}
}

View File

@ -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) {
// 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);
if (type == null) {
throw new ParsingException(source(ctx), "Invalid table type [{}]", value);
}
types.add(type);
}
}
}
// if the ODBC enumeration is specified, skip validation
EnumSet<IndexType> set = types.isEmpty() ? null : EnumSet.copyOf(types);
return new SysTables(source(ctx), visitPattern(ctx.clusterPattern), visitPattern(ctx.tablePattern), set);
}

View File

@ -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<SchemaRowSet> 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())));
}

View File

@ -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<SchemaRowSet> 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<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

View File

@ -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 ? "_" : "*");

View File

@ -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<String, EsField> 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<IndexType> 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<IndexType> 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<Command, SqlSession> sql(String sql) {
return sql(sql, emptyList());
}
private Tuple<Command, SqlSession> sql(String sql, List<SqlTypedParamValue> 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);