From 496070fda69f66a857a1810cc3fa06905dd954e3 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Fri, 22 Mar 2019 20:42:54 +0200 Subject: [PATCH] SQL: CAST supports both SQL and ES types (#40365) Extend CAST to support all data types notations (whether SQL or ES specific) Fix #40282 (cherry picked from commit eb2ee8a344da946920598839a5db76c8bb9bc3fe) --- .../sql/functions/type-conversion.asciidoc | 5 + .../xpack/sql/parser/ExpressionBuilder.java | 43 +------ .../xpack/sql/type/DataType.java | 105 +++++++++++------- .../xpack/sql/parser/SqlParserTests.java | 5 + .../xpack/sql/type/DataTypesTests.java | 33 ++++++ 5 files changed, 113 insertions(+), 78 deletions(-) diff --git a/docs/reference/sql/functions/type-conversion.asciidoc b/docs/reference/sql/functions/type-conversion.asciidoc index a696183d911..ea23462dcca 100644 --- a/docs/reference/sql/functions/type-conversion.asciidoc +++ b/docs/reference/sql/functions/type-conversion.asciidoc @@ -38,6 +38,11 @@ include-tagged::{sql-specs}/docs.csv-spec[conversionIntToStringCast] include-tagged::{sql-specs}/docs.csv-spec[conversionStringToDateTimeCast] ---- +IMPORTANT: Both ANSI SQL and {es-sql} types are supported with the former taking +precedence. This only affects `FLOAT` which due naming conflict, is interpreted as ANSI SQL +and thus maps to `double` in {es} as oppose to `float`. +To obtain an {es} `float`, perform casting to its SQL equivalent, `real` type. + [[sql-functions-type-conversion-convert]] ==== `CONVERT` diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java index e829915c35f..2adeba21ba8 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/ExpressionBuilder.java @@ -387,44 +387,11 @@ abstract class ExpressionBuilder extends IdentifierBuilder { @Override public DataType visitPrimitiveDataType(PrimitiveDataTypeContext ctx) { - String type = visitIdentifier(ctx.identifier()).toLowerCase(Locale.ROOT); - - switch (type) { - case "bit": - case "bool": - case "boolean": - return DataType.BOOLEAN; - case "tinyint": - case "byte": - return DataType.BYTE; - case "smallint": - case "short": - return DataType.SHORT; - case "int": - case "integer": - return DataType.INTEGER; - case "long": - case "bigint": - return DataType.LONG; - case "real": - return DataType.FLOAT; - case "float": - case "double": - return DataType.DOUBLE; - case "date": - return DataType.DATE; - case "datetime": - case "timestamp": - return DataType.DATETIME; - case "char": - case "varchar": - case "string": - return DataType.KEYWORD; - case "ip": - return DataType.IP; - default: - throw new ParsingException(source(ctx), "Does not recognize type [{}]", ctx.getText()); - } + String type = visitIdentifier(ctx.identifier()); + DataType dataType = DataType.fromSqlOrEsType(type); + if (dataType == null) { + throw new ParsingException(source(ctx), "Does not recognize type [{}]", type); } + return dataType; } // diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java index bc2d5cc722c..f75d0a8f735 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java @@ -13,6 +13,7 @@ import java.sql.Types; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; /** * Elasticsearch SQL data types. @@ -73,58 +74,78 @@ public enum DataType { INTERVAL_MINUTE_TO_SECOND(ExtTypes.INTERVAL_MINUTE_TO_SECOND,Long.BYTES, 23, 23, false, false, false); // @formatter:on - private static final Map odbcToEs; + private static final Map ODBC_TO_ES = new HashMap<>(36); static { - odbcToEs = new HashMap<>(36); - // Numeric - odbcToEs.put("SQL_BIT", BOOLEAN); - odbcToEs.put("SQL_TINYINT", BYTE); - odbcToEs.put("SQL_SMALLINT", SHORT); - odbcToEs.put("SQL_INTEGER", INTEGER); - odbcToEs.put("SQL_BIGINT", LONG); - odbcToEs.put("SQL_FLOAT", FLOAT); - odbcToEs.put("SQL_REAL", FLOAT); - odbcToEs.put("SQL_DOUBLE", DOUBLE); - odbcToEs.put("SQL_DECIMAL", DOUBLE); - odbcToEs.put("SQL_NUMERIC", DOUBLE); + ODBC_TO_ES.put("SQL_BIT", BOOLEAN); + ODBC_TO_ES.put("SQL_TINYINT", BYTE); + ODBC_TO_ES.put("SQL_SMALLINT", SHORT); + ODBC_TO_ES.put("SQL_INTEGER", INTEGER); + ODBC_TO_ES.put("SQL_BIGINT", LONG); + ODBC_TO_ES.put("SQL_REAL", FLOAT); + ODBC_TO_ES.put("SQL_FLOAT", DOUBLE); + ODBC_TO_ES.put("SQL_DOUBLE", DOUBLE); + ODBC_TO_ES.put("SQL_DECIMAL", DOUBLE); + ODBC_TO_ES.put("SQL_NUMERIC", DOUBLE); // String - odbcToEs.put("SQL_GUID", KEYWORD); - odbcToEs.put("SQL_CHAR", KEYWORD); - odbcToEs.put("SQL_WCHAR", KEYWORD); - odbcToEs.put("SQL_VARCHAR", TEXT); - odbcToEs.put("SQL_WVARCHAR", TEXT); - odbcToEs.put("SQL_LONGVARCHAR", TEXT); - odbcToEs.put("SQL_WLONGVARCHAR", TEXT); + ODBC_TO_ES.put("SQL_GUID", KEYWORD); + ODBC_TO_ES.put("SQL_CHAR", KEYWORD); + ODBC_TO_ES.put("SQL_WCHAR", KEYWORD); + ODBC_TO_ES.put("SQL_VARCHAR", TEXT); + ODBC_TO_ES.put("SQL_WVARCHAR", TEXT); + ODBC_TO_ES.put("SQL_LONGVARCHAR", TEXT); + ODBC_TO_ES.put("SQL_WLONGVARCHAR", TEXT); // Binary - odbcToEs.put("SQL_BINARY", BINARY); - odbcToEs.put("SQL_VARBINARY", BINARY); - odbcToEs.put("SQL_LONGVARBINARY", BINARY); + ODBC_TO_ES.put("SQL_BINARY", BINARY); + ODBC_TO_ES.put("SQL_VARBINARY", BINARY); + ODBC_TO_ES.put("SQL_LONGVARBINARY", BINARY); // Date - odbcToEs.put("SQL_DATE", DATE); - odbcToEs.put("SQL_TIME", DATETIME); - odbcToEs.put("SQL_TIMESTAMP", DATETIME); + ODBC_TO_ES.put("SQL_DATE", DATE); + ODBC_TO_ES.put("SQL_TIME", DATETIME); + ODBC_TO_ES.put("SQL_TIMESTAMP", DATETIME); // Intervals - odbcToEs.put("SQL_INTERVAL_HOUR_TO_MINUTE", INTERVAL_HOUR_TO_MINUTE); - odbcToEs.put("SQL_INTERVAL_HOUR_TO_SECOND", INTERVAL_HOUR_TO_SECOND); - odbcToEs.put("SQL_INTERVAL_MINUTE_TO_SECOND", INTERVAL_MINUTE_TO_SECOND); - odbcToEs.put("SQL_INTERVAL_MONTH", INTERVAL_MONTH); - odbcToEs.put("SQL_INTERVAL_YEAR", INTERVAL_YEAR); - odbcToEs.put("SQL_INTERVAL_YEAR_TO_MONTH", INTERVAL_YEAR_TO_MONTH); - odbcToEs.put("SQL_INTERVAL_DAY", INTERVAL_DAY); - odbcToEs.put("SQL_INTERVAL_HOUR", INTERVAL_HOUR); - odbcToEs.put("SQL_INTERVAL_MINUTE", INTERVAL_MINUTE); - odbcToEs.put("SQL_INTERVAL_SECOND", INTERVAL_SECOND); - odbcToEs.put("SQL_INTERVAL_DAY_TO_HOUR", INTERVAL_DAY_TO_HOUR); - odbcToEs.put("SQL_INTERVAL_DAY_TO_MINUTE", INTERVAL_DAY_TO_MINUTE); - odbcToEs.put("SQL_INTERVAL_DAY_TO_SECOND", INTERVAL_DAY_TO_SECOND); + ODBC_TO_ES.put("SQL_INTERVAL_HOUR_TO_MINUTE", INTERVAL_HOUR_TO_MINUTE); + ODBC_TO_ES.put("SQL_INTERVAL_HOUR_TO_SECOND", INTERVAL_HOUR_TO_SECOND); + ODBC_TO_ES.put("SQL_INTERVAL_MINUTE_TO_SECOND", INTERVAL_MINUTE_TO_SECOND); + ODBC_TO_ES.put("SQL_INTERVAL_MONTH", INTERVAL_MONTH); + ODBC_TO_ES.put("SQL_INTERVAL_YEAR", INTERVAL_YEAR); + ODBC_TO_ES.put("SQL_INTERVAL_YEAR_TO_MONTH", INTERVAL_YEAR_TO_MONTH); + ODBC_TO_ES.put("SQL_INTERVAL_DAY", INTERVAL_DAY); + ODBC_TO_ES.put("SQL_INTERVAL_HOUR", INTERVAL_HOUR); + ODBC_TO_ES.put("SQL_INTERVAL_MINUTE", INTERVAL_MINUTE); + ODBC_TO_ES.put("SQL_INTERVAL_SECOND", INTERVAL_SECOND); + ODBC_TO_ES.put("SQL_INTERVAL_DAY_TO_HOUR", INTERVAL_DAY_TO_HOUR); + ODBC_TO_ES.put("SQL_INTERVAL_DAY_TO_MINUTE", INTERVAL_DAY_TO_MINUTE); + ODBC_TO_ES.put("SQL_INTERVAL_DAY_TO_SECOND", INTERVAL_DAY_TO_SECOND); } + private static final Map SQL_TO_ES = new HashMap<>(45); + static { + // first add ES types + for (DataType type : DataType.values()) { + if (type.isPrimitive()) { + SQL_TO_ES.put(type.name(), type); + } + } + + // reuse the ODBC definition (without SQL_) + // note that this will override existing types in particular FLOAT + for (Entry entry : ODBC_TO_ES.entrySet()) { + SQL_TO_ES.put(entry.getKey().substring(4), entry.getValue()); + } + + + // special ones + SQL_TO_ES.put("BOOL", DataType.BOOLEAN); + SQL_TO_ES.put("INT", DataType.INTEGER); + SQL_TO_ES.put("STRING", DataType.KEYWORD); + } + /** * Type's name used for error messages and column info for the clients */ @@ -234,9 +255,13 @@ public enum DataType { } public static DataType fromOdbcType(String odbcType) { - return odbcToEs.get(odbcType); + return ODBC_TO_ES.get(odbcType); } + public static DataType fromSqlOrEsType(String typeName) { + return SQL_TO_ES.get(typeName.toUpperCase(Locale.ROOT)); + } + /** * Creates returns DataType enum corresponding to the specified es type */ diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java index 45276b8d15e..d1e05b6ec53 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java @@ -81,6 +81,11 @@ public class SqlParserTests extends ESTestCase { assertEquals("CONVERT(POWER(languages, 2), SQL_DOUBLE)", f.sourceText()); } + public void testSelectCastToEsType() { + Cast f = singleProjection(project(parseStatement("SELECT CAST('0.' AS SCALED_FLOAT)")), Cast.class); + assertEquals("CAST('0.' AS SCALED_FLOAT)", f.sourceText()); + } + public void testSelectAddWithParanthesis() { Add f = singleProjection(project(parseStatement("SELECT (1 + 2)")), Add.class); assertEquals("1 + 2", f.sourceText()); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/type/DataTypesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/type/DataTypesTests.java index 7b38718dad7..47f01be9178 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/type/DataTypesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/type/DataTypesTests.java @@ -7,8 +7,13 @@ package org.elasticsearch.xpack.sql.type; import org.elasticsearch.test.ESTestCase; +import java.util.ArrayList; +import java.util.Arrays; import java.util.EnumSet; +import java.util.List; +import java.util.stream.Stream; +import static java.util.stream.Collectors.toList; import static org.elasticsearch.xpack.sql.type.DataType.DATETIME; import static org.elasticsearch.xpack.sql.type.DataType.FLOAT; import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_DAY; @@ -108,6 +113,34 @@ public class DataTypesTests extends ESTestCase { assertNull(compatibleInterval(INTERVAL_MINUTE_TO_SECOND, INTERVAL_MONTH)); } + public void testEsToDataType() throws Exception { + List types = new ArrayList<>(Arrays.asList("null", "boolean", "bool", + "byte", "tinyint", + "short", "smallint", + "integer", + "long", "bigint", + "double", "real", + "half_float", "scaled_float", "float", + "decimal", "numeric", + "keyword", "text", "varchar", + "date", "datetime", "timestamp", + "binary", "varbinary", + "ip", + "interval_year", "interval_month", "interval_year_to_month", + "interval_day", "interval_hour", "interval_minute", "interval_second", + "interval_day_to_hour", "interval_day_to_minute", "interval_day_to_second", + "interval_hour_to_minute", "interval_hour_to_second", + "interval_minute_to_second")); + + types.addAll(Stream.of(DataType.values()) + .filter(DataType::isPrimitive) + .map(DataType::name) + .collect(toList())); + String type = randomFrom(types.toArray(new String[0])); + DataType dataType = DataType.fromSqlOrEsType(type); + assertNotNull(dataType); + } + private DataType randomDataTypeNoDateTime() { return randomValueOtherThan(DataType.DATETIME, () -> randomFrom(DataType.values())); }