SQL: Consolidate JDBC type handling (elastic/x-pack-elasticsearch#3761)

Consolidates handling of JDBC types conversion into a single file that should simplify maintaining consistency between type handling. Also separates the types that are handled as part of Elasticsearch output and types that are handled as user-supplied parameters.

relates elastic/x-pack-elasticsearch#3556

Original commit: elastic/x-pack-elasticsearch@d251fce66b
This commit is contained in:
Igor Motov 2018-01-29 16:35:42 -05:00 committed by GitHub
parent 07658cc04f
commit 1627ec1378
12 changed files with 308 additions and 337 deletions

View File

@ -22,6 +22,9 @@ import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.List;
import static java.sql.JDBCType.INTEGER;
import static java.sql.JDBCType.SMALLINT;
/**
* Implementation of {@link DatabaseMetaData} for Elasticsearch. Draws inspiration
* from <a href="https://www.postgresql.org/docs/9.0/static/information-schema.html">
@ -640,11 +643,11 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
"PROCEDURE_CAT",
"PROCEDURE_SCHEM",
"PROCEDURE_NAME",
"NUM_INPUT_PARAMS", int.class,
"NUM_OUTPUT_PARAMS", int.class,
"NUM_RESULT_SETS", int.class,
"NUM_INPUT_PARAMS", INTEGER,
"NUM_OUTPUT_PARAMS", INTEGER,
"NUM_RESULT_SETS", INTEGER,
"REMARKS",
"PROCEDURE_TYPE", short.class,
"PROCEDURE_TYPE", SMALLINT,
"SPECIFIC_NAME");
}
@ -657,20 +660,20 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
"PROCEDURE_SCHEM",
"PROCEDURE_NAME",
"COLUMN_NAME",
"COLUMN_TYPE", short.class,
"DATA_TYPE", int.class,
"COLUMN_TYPE", SMALLINT,
"DATA_TYPE", INTEGER,
"TYPE_NAME",
"PRECISION", int.class,
"LENGTH", int.class,
"SCALE", short.class,
"RADIX", short.class,
"NULLABLE", short.class,
"PRECISION", INTEGER,
"LENGTH", INTEGER,
"SCALE", SMALLINT,
"RADIX", SMALLINT,
"NULLABLE", SMALLINT,
"REMARKS",
"COLUMN_DEF",
"SQL_DATA_TYPE", int.class,
"SQL_DATETIME_SUB", int.class,
"CHAR_OCTET_LENGTH", int.class,
"ORDINAL_POSITION", int.class,
"SQL_DATA_TYPE", INTEGER,
"SQL_DATETIME_SUB", INTEGER,
"CHAR_OCTET_LENGTH", INTEGER,
"ORDINAL_POSITION", INTEGER,
"IS_NULLABLE",
"SPECIFIC_NAME");
}
@ -739,7 +742,6 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern)
throws SQLException {
PreparedStatement ps = con.prepareStatement("SYS COLUMNS TABLES LIKE ? LIKE ?");
ps.setString(1, tableNamePattern != null ? tableNamePattern.trim() : "%");
ps.setString(2, columnNamePattern != null ? columnNamePattern.trim() : "%");
@ -865,9 +867,9 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
"TYPE_SCHEM",
"TYPE_NAME",
"CLASS_NAME",
"DATA_TYPE", int.class,
"DATA_TYPE", INTEGER,
"REMARKS",
"BASE_TYPE", short.class);
"BASE_TYPE", SMALLINT);
}
@Override
@ -926,23 +928,23 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
"TYPE_SCHEM",
"TYPE_NAME",
"ATTR_NAME",
"DATA_TYPE", int.class,
"DATA_TYPE", INTEGER,
"ATTR_TYPE_NAME",
"ATTR_SIZE", int.class,
"DECIMAL_DIGITS", int.class,
"NUM_PREC_RADIX", int.class,
"NULLABLE", int.class,
"ATTR_SIZE", INTEGER,
"DECIMAL_DIGITS", INTEGER,
"NUM_PREC_RADIX", INTEGER,
"NULLABLE", INTEGER,
"REMARKS",
"ATTR_DEF",
"SQL_DATA_TYPE", int.class,
"SQL_DATETIME_SUB", int.class,
"CHAR_OCTET_LENGTH", int.class,
"ORDINAL_POSITION", int.class,
"SQL_DATA_TYPE", INTEGER,
"SQL_DATETIME_SUB", INTEGER,
"CHAR_OCTET_LENGTH", INTEGER,
"ORDINAL_POSITION", INTEGER,
"IS_NULLABLE",
"SCOPE_CATALOG",
"SCOPE_SCHEMA",
"SCOPE_TABLE",
"SOURCE_DATA_TYPE", short.class);
"SOURCE_DATA_TYPE", SMALLINT);
}
@Override
@ -1018,7 +1020,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
"FUNCTION_SCHEM",
"FUNCTION_NAME",
"REMARKS",
"FUNCTION_TYPE", short.class,
"FUNCTION_TYPE", SMALLINT,
"SPECIFIC_NAME");
}
@ -1031,16 +1033,16 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
"FUNCTION_SCHEM",
"FUNCTION_NAME",
"COLUMN_NAME",
"DATA_TYPE", int.class,
"DATA_TYPE", INTEGER,
"TYPE_NAME",
"PRECISION", int.class,
"LENGTH", int.class,
"SCALE", short.class,
"RADIX", short.class,
"NULLABLE", short.class,
"PRECISION", INTEGER,
"LENGTH", INTEGER,
"SCALE", SMALLINT,
"RADIX", SMALLINT,
"NULLABLE", SMALLINT,
"REMARKS",
"CHAR_OCTET_LENGTH", int.class,
"ORDINAL_POSITION", int.class,
"CHAR_OCTET_LENGTH", INTEGER,
"ORDINAL_POSITION", INTEGER,
"IS_NULLABLE",
"SPECIFIC_NAME");
}
@ -1054,10 +1056,10 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
"TABLE_SCHEM",
"TABLE_NAME",
"COLUMN_NAME",
"DATA_TYPE", int.class,
"COLUMN_SIZE", int.class,
"DECIMAL_DIGITS", int.class,
"NUM_PREC_RADIX", int.class,
"DATA_TYPE", INTEGER,
"COLUMN_SIZE", INTEGER,
"DECIMAL_DIGITS", INTEGER,
"NUM_PREC_RADIX", INTEGER,
"REMARKS",
"COLUMN_USAGE",
"IS_NULLABLE");
@ -1078,8 +1080,8 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
JDBCType type = JDBCType.VARCHAR;
if (i + 1 < cols.length) {
// check if the next item it's a type
if (cols[i + 1] instanceof Class) {
type = JDBCType.valueOf(JdbcUtils.fromClass((Class<?>) cols[i + 1]));
if (cols[i + 1] instanceof JDBCType) {
type = (JDBCType) cols[i + 1];
i++;
}
// it's not, use the default and move on

View File

@ -32,7 +32,7 @@ class JdbcParameterMetaData implements ParameterMetaData, JdbcWrapper {
@Override
public boolean isSigned(int param) throws SQLException {
return JdbcUtils.isSigned(paramInfo(param).type.getVendorTypeNumber().intValue());
return TypeConverter.isSigned(paramInfo(param).type);
}
@Override
@ -49,7 +49,7 @@ class JdbcParameterMetaData implements ParameterMetaData, JdbcWrapper {
@Override
public int getParameterType(int param) throws SQLException {
return paramInfo(param).type.getVendorTypeNumber().intValue();
return paramInfo(param).type.getVendorTypeNumber();
}
@Override
@ -59,7 +59,7 @@ class JdbcParameterMetaData implements ParameterMetaData, JdbcWrapper {
@Override
public String getParameterClassName(int param) throws SQLException {
return JdbcUtils.classOf(paramInfo(param).type.getVendorTypeNumber()).getName();
return TypeConverter.classNameOf(paramInfo(param).type);
}
@Override

View File

@ -369,12 +369,7 @@ class JdbcResultSet implements ResultSet, JdbcWrapper {
JDBCType columnType = cursor.columns().get(columnIndex - 1).type;
T t = TypeConverter.convert(val, columnType, type);
if (t != null || type == null) {
return t;
}
throw new SQLException("Conversion from type [" + columnType + "] to [" + type.getName() + "] not supported");
return TypeConverter.convert(val, columnType, type);
}
@Override

View File

@ -62,7 +62,7 @@ class JdbcResultSetMetaData implements ResultSetMetaData, JdbcWrapper {
@Override
public boolean isSigned(int column) throws SQLException {
return JdbcUtils.isSigned(getColumnType(column));
return TypeConverter.isSigned(column(column).type);
}
@Override
@ -137,7 +137,7 @@ class JdbcResultSetMetaData implements ResultSetMetaData, JdbcWrapper {
@Override
public String getColumnClassName(int column) throws SQLException {
return JdbcUtils.classOf(column(column).type.getVendorTypeNumber()).getName();
return TypeConverter.classNameOf(column(column).type);
}
private void checkOpen() throws SQLException {

View File

@ -1,169 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.jdbc.jdbc;
import org.elasticsearch.xpack.sql.jdbc.JdbcSQLException;
import java.math.BigDecimal;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.JDBCType;
import java.sql.Time;
import java.sql.Timestamp;
import static java.sql.Types.BIGINT;
import static java.sql.Types.BINARY;
import static java.sql.Types.BIT;
import static java.sql.Types.BLOB;
import static java.sql.Types.BOOLEAN;
import static java.sql.Types.CHAR;
import static java.sql.Types.CLOB;
import static java.sql.Types.DATE;
import static java.sql.Types.DECIMAL;
import static java.sql.Types.DOUBLE;
import static java.sql.Types.FLOAT;
import static java.sql.Types.INTEGER;
import static java.sql.Types.LONGVARBINARY;
import static java.sql.Types.LONGVARCHAR;
import static java.sql.Types.NULL;
import static java.sql.Types.NUMERIC;
import static java.sql.Types.REAL;
import static java.sql.Types.SMALLINT;
import static java.sql.Types.TIME;
import static java.sql.Types.TIMESTAMP;
import static java.sql.Types.TIMESTAMP_WITH_TIMEZONE;
import static java.sql.Types.TINYINT;
import static java.sql.Types.VARBINARY;
import static java.sql.Types.VARCHAR;
public abstract class JdbcUtils {
public static int fromClass(Class<?> clazz) throws JdbcSQLException {
if (clazz == null) {
return NULL;
}
if (clazz == String.class) {
return VARCHAR;
}
if (clazz == Boolean.class || clazz == boolean.class) {
return BOOLEAN;
}
if (clazz == Byte.class || clazz == byte.class) {
return TINYINT;
}
if (clazz == Short.class || clazz == short.class) {
return SMALLINT;
}
if (clazz == Integer.class || clazz == int.class) {
return INTEGER;
}
if (clazz == Long.class || clazz == long.class) {
return BIGINT;
}
if (clazz == Float.class || clazz == float.class) {
return REAL;
}
if (clazz == Double.class || clazz == double.class) {
return DOUBLE;
}
if (clazz == Void.class || clazz == void.class) {
return NULL;
}
if (clazz == byte[].class) {
return VARBINARY;
}
if (clazz == Date.class) {
return DATE;
}
if (clazz == Time.class) {
return TIME;
}
if (clazz == Timestamp.class) {
return TIMESTAMP;
}
if (clazz == Blob.class) {
return BLOB;
}
if (clazz == Clob.class) {
return CLOB;
}
if (clazz == BigDecimal.class) {
return DECIMAL;
}
throw new JdbcSQLException("Unrecognized class [" + clazz + "]");
}
// see javax.sql.rowset.RowSetMetaDataImpl
// and https://db.apache.org/derby/docs/10.5/ref/rrefjdbc20377.html
public static Class<?> classOf(int jdbcType) throws JdbcSQLException {
switch (jdbcType) {
case NUMERIC:
case DECIMAL:
return BigDecimal.class;
case BOOLEAN:
case BIT:
return Boolean.class;
case TINYINT:
return Byte.class;
case SMALLINT:
return Short.class;
case INTEGER:
return Integer.class;
case BIGINT:
return Long.class;
case REAL:
return Float.class;
case FLOAT:
case DOUBLE:
return Double.class;
case BINARY:
case VARBINARY:
case LONGVARBINARY:
return byte[].class;
case CHAR:
case VARCHAR:
case LONGVARCHAR:
return String.class;
case DATE:
return Date.class;
case TIME:
return Time.class;
case TIMESTAMP:
return Timestamp.class;
case BLOB:
return Blob.class;
case CLOB:
return Clob.class;
case TIMESTAMP_WITH_TIMEZONE:
return Long.class;
default:
throw new JdbcSQLException("Unsupported JDBC type " + jdbcType + ", " + type(jdbcType).getName() + "");
}
}
static boolean isSigned(int type) {
switch (type) {
case BIGINT:
case DECIMAL:
case DOUBLE:
case FLOAT:
case INTEGER:
case SMALLINT:
case REAL:
case NUMERIC:
return true;
default:
return false;
}
}
static JDBCType type(int jdbcType) {
return JDBCType.valueOf(jdbcType);
}
}

View File

@ -5,6 +5,11 @@
*/
package org.elasticsearch.xpack.sql.jdbc.jdbc;
import org.elasticsearch.xpack.sql.jdbc.JdbcSQLException;
import java.math.BigDecimal;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.JDBCType;
import java.sql.SQLException;
@ -30,10 +35,28 @@ import static java.util.Calendar.MONTH;
import static java.util.Calendar.SECOND;
import static java.util.Calendar.YEAR;
abstract class TypeConverter {
/**
* Conversion utilities for conversion of JDBC types to Java type and back
* <p>
* The following JDBC types are supported as part of Elasticsearch Response. See org.elasticsearch.xpack.sql.type.DataType for details.
* <p>
* NULL, BOOLEAN, TINYINT, SMALLINT, INTEGER, BIGINT, DOUBLE, REAL, FLOAT, VARCHAR, VARBINARY and TIMESTAMP
* <p>
* The following additional types are also supported as parameters:
* <p>
* NUMERIC, DECIMAL, BIT, BINARY, LONGVARBINARY, CHAR, LONGVARCHAR, DATE, TIME, BLOB, CLOB, TIMESTAMP_WITH_TIMEZONE
*/
final class TypeConverter {
private TypeConverter() {
}
private static final long DAY_IN_MILLIS = 60 * 60 * 24;
/**
* Converts millisecond after epoc to date
*/
static Date convertDate(Long millis, Calendar cal) {
return dateTimeConvert(millis, cal, c -> {
c.set(HOUR_OF_DAY, 0);
@ -44,6 +67,9 @@ abstract class TypeConverter {
});
}
/**
* Converts millisecond after epoc to time
*/
static Time convertTime(Long millis, Calendar cal) {
return dateTimeConvert(millis, cal, c -> {
c.set(ERA, GregorianCalendar.AD);
@ -54,10 +80,11 @@ abstract class TypeConverter {
});
}
/**
* Converts millisecond after epoc to timestamp
*/
static Timestamp convertTimestamp(Long millis, Calendar cal) {
return dateTimeConvert(millis, cal, c -> {
return new Timestamp(c.getTimeInMillis());
});
return dateTimeConvert(millis, cal, c -> new Timestamp(c.getTimeInMillis()));
}
private static <T> T dateTimeConvert(Long millis, Calendar c, Function<Calendar, T> creator) {
@ -66,20 +93,23 @@ abstract class TypeConverter {
}
long initial = c.getTimeInMillis();
try {
c.setTimeInMillis(millis.longValue());
c.setTimeInMillis(millis);
return creator.apply(c);
} finally {
c.setTimeInMillis(initial);
}
}
/**
* Converts object val from columnType to type
*/
@SuppressWarnings("unchecked")
static <T> T convert(Object val, JDBCType columnType, Class<T> type) throws SQLException {
if (type == null) {
return (T) asNative(val, columnType);
return (T) convert(val, columnType);
}
if (type == String.class) {
return (T) asString(asNative(val, columnType));
return (T) asString(convert(val, columnType));
}
if (type == Boolean.class) {
return (T) asBoolean(val, columnType);
@ -132,43 +162,145 @@ abstract class TypeConverter {
if (type == OffsetDateTime.class) {
return (T) asOffsetDateTime(val, columnType);
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [" + type.getName() + "] not supported");
}
// keep in check with JdbcUtils#columnType
static Object asNative(Object v, JDBCType columnType) {
switch (columnType) {
/**
* Translates numeric JDBC type into corresponding Java class
* <p>
* See {@link javax.sql.rowset.RowSetMetaDataImpl#getColumnClassName} and
* https://db.apache.org/derby/docs/10.5/ref/rrefjdbc20377.html
*/
public static String classNameOf(JDBCType jdbcType) throws JdbcSQLException {
switch (jdbcType) {
// ES - supported types
case BOOLEAN:
return Boolean.class.getName();
case TINYINT: // BYTE DataType
return Byte.class.getName();
case SMALLINT: // SHORT DataType
return Short.class.getName();
case INTEGER:
return Integer.class.getName();
case BIGINT: // LONG DataType
return Long.class.getName();
case DOUBLE:
return Double.class.getName();
case REAL: // FLOAT DataType
return Float.class.getName();
case FLOAT: // HALF_FLOAT DataType
return Double.class.getName(); // TODO: Is this correct?
case VARCHAR: // KEYWORD or TEXT DataType
return String.class.getName();
case VARBINARY: // BINARY DataType
return byte[].class.getName();
case TIMESTAMP: // DATE DataType
return Timestamp.class.getName();
// Parameters data types that cannot be returned by ES but can appear in client - supplied parameters
case NUMERIC:
case DECIMAL:
return BigDecimal.class.getName();
case BIT:
return Boolean.class.getName();
case BINARY:
case LONGVARBINARY:
return byte[].class.getName();
case CHAR:
case LONGVARCHAR:
return String.class.getName();
case DATE:
return Date.class.getName();
case TIME:
return Time.class.getName();
case BLOB:
return Blob.class.getName();
case CLOB:
return Clob.class.getName();
case TIMESTAMP_WITH_TIMEZONE:
return Long.class.getName();
default:
throw new JdbcSQLException("Unsupported JDBC type [" + jdbcType + "]");
}
}
/**
* Converts the object from JSON representation to the specified JDBCType
* <p>
* The returned types needs to correspond to ES-portion of classes returned by {@link TypeConverter#classNameOf}
*/
static Object convert(Object v, JDBCType columnType) throws SQLException {
switch (columnType) {
case NULL:
return null;
case BOOLEAN:
case BINARY:
case VARBINARY:
case LONGVARBINARY:
case CHAR:
case VARCHAR:
case LONGVARCHAR:
return v;
return v; // These types are already represented correctly in JSON
case TINYINT:
return ((Number) v).byteValue();
return ((Number) v).byteValue(); // Parser might return it as integer or long - need to update to the correct type
case SMALLINT:
return ((Number) v).shortValue();
return ((Number) v).shortValue(); // Parser might return it as integer or long - need to update to the correct type
case INTEGER:
return ((Number) v).intValue();
case BIGINT:
return ((Number) v).longValue();
case FLOAT:
case DOUBLE:
return doubleValue(v);
return doubleValue(v); // Double might be represented as string for infinity and NaN values
case REAL:
return floatValue(v);
return floatValue(v); // Float might be represented as string for infinity and NaN values
case TIMESTAMP:
return ((Number) v).longValue();
// since the date is already in UTC_CALENDAR just do calendar math
case DATE:
return new Date(utcMillisRemoveTime(((Number) v).longValue()));
case TIME:
return new Time(utcMillisRemoveDate(((Number) v).longValue()));
default:
return null;
throw new SQLException("Unexpected column type [" + columnType.getName() + "]");
}
}
/**
* Returns true if the type represents a signed number, false otherwise
* <p>
* It needs to support both params and column types
*/
static boolean isSigned(JDBCType type) throws SQLException {
switch (type) {
// ES Supported types
case BIGINT:
case DOUBLE:
case FLOAT:
case INTEGER:
case TINYINT:
case SMALLINT:
return true;
case NULL:
case BOOLEAN:
case VARCHAR:
case VARBINARY:
case TIMESTAMP:
return false;
// Parameter types
case REAL:
case DECIMAL:
case NUMERIC:
return true;
case BIT:
case BINARY:
case LONGVARBINARY:
case CHAR:
case LONGVARCHAR:
case DATE:
case TIME:
case BLOB:
case CLOB:
case TIMESTAMP_WITH_TIMEZONE:
return false;
default:
throw new SQLException("Unexpected column or parameter type [" + type + "]");
}
}
@ -208,9 +340,8 @@ abstract class TypeConverter {
return nativeValue == null ? null : String.valueOf(nativeValue);
}
private static Boolean asBoolean(Object val, JDBCType columnType) {
private static Boolean asBoolean(Object val, JDBCType columnType) throws SQLException {
switch (columnType) {
case BIT:
case BOOLEAN:
case TINYINT:
case SMALLINT:
@ -221,13 +352,13 @@ abstract class TypeConverter {
case DOUBLE:
return Boolean.valueOf(Integer.signum(((Number) val).intValue()) == 0);
default:
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Boolean] not supported");
}
}
private static Byte asByte(Object val, JDBCType columnType) throws SQLException {
switch (columnType) {
case BIT:
case BOOLEAN:
return Byte.valueOf(((Boolean) val).booleanValue() ? (byte) 1 : (byte) 0);
case TINYINT:
@ -242,12 +373,11 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Byte] not supported");
}
private static Short asShort(Object val, JDBCType columnType) throws SQLException {
switch (columnType) {
case BIT:
case BOOLEAN:
return Short.valueOf(((Boolean) val).booleanValue() ? (short) 1 : (short) 0);
case TINYINT:
@ -262,12 +392,11 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Short] not supported");
}
private static Integer asInteger(Object val, JDBCType columnType) throws SQLException {
switch (columnType) {
case BIT:
case BOOLEAN:
return Integer.valueOf(((Boolean) val).booleanValue() ? 1 : 0);
case TINYINT:
@ -282,12 +411,11 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Integer] not supported");
}
private static Long asLong(Object val, JDBCType columnType) throws SQLException {
switch (columnType) {
case BIT:
case BOOLEAN:
return Long.valueOf(((Boolean) val).booleanValue() ? 1 : 0);
case TINYINT:
@ -309,12 +437,11 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Long] not supported");
}
private static Float asFloat(Object val, JDBCType columnType) throws SQLException {
switch (columnType) {
case BIT:
case BOOLEAN:
return Float.valueOf(((Boolean) val).booleanValue() ? 1 : 0);
case TINYINT:
@ -329,12 +456,11 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Float] not supported");
}
private static Double asDouble(Object val, JDBCType columnType) throws SQLException {
switch (columnType) {
case BIT:
case BOOLEAN:
return Double.valueOf(((Boolean) val).booleanValue() ? 1 : 0);
case TINYINT:
@ -349,7 +475,7 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Double] not supported");
}
private static Date asDate(Object val, JDBCType columnType) throws SQLException {
@ -364,7 +490,7 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Date] not supported");
}
private static Time asTime(Object val, JDBCType columnType) throws SQLException {
@ -379,7 +505,7 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Time] not supported");
}
private static Timestamp asTimestamp(Object val, JDBCType columnType) throws SQLException {
@ -394,12 +520,13 @@ abstract class TypeConverter {
default:
}
return null;
throw new SQLException("Conversion from type [" + columnType + "] to [Timestamp] not supported");
}
private static byte[] asByteArray(Object val, JDBCType columnType) {
throw new UnsupportedOperationException();
}
private static LocalDate asLocalDate(Object val, JDBCType columnType) {
throw new UnsupportedOperationException();
}

View File

@ -13,7 +13,6 @@ import org.elasticsearch.xpack.sql.plugin.AbstractSqlRequest;
import org.elasticsearch.xpack.sql.plugin.SqlQueryResponse;
import org.joda.time.DateTime;
import java.io.IOException;
import java.sql.JDBCType;
import static org.hamcrest.Matchers.instanceOf;
@ -22,7 +21,7 @@ import static org.hamcrest.Matchers.instanceOf;
public class TypeConverterTests extends ESTestCase {
public void testFloatAsNative() throws IOException {
public void testFloatAsNative() throws Exception {
assertThat(convertAsNative(42.0f, JDBCType.REAL), instanceOf(Float.class));
assertThat(convertAsNative(42.0, JDBCType.REAL), instanceOf(Float.class));
assertEquals(42.0f, (float) convertAsNative(42.0, JDBCType.REAL), 0.001f);
@ -31,7 +30,7 @@ public class TypeConverterTests extends ESTestCase {
assertEquals(Float.POSITIVE_INFINITY, convertAsNative(Float.POSITIVE_INFINITY, JDBCType.REAL));
}
public void testDoubleAsNative() throws IOException {
public void testDoubleAsNative() throws Exception {
JDBCType type = randomFrom(JDBCType.FLOAT, JDBCType.DOUBLE);
assertThat(convertAsNative(42.0, type), instanceOf(Double.class));
assertEquals(42.0f, (double) convertAsNative(42.0, type), 0.001f);
@ -40,13 +39,13 @@ public class TypeConverterTests extends ESTestCase {
assertEquals(Double.POSITIVE_INFINITY, convertAsNative(Double.POSITIVE_INFINITY, type));
}
public void testTimestampAsNative() throws IOException {
public void testTimestampAsNative() throws Exception {
DateTime now = DateTime.now();
assertThat(convertAsNative(now, JDBCType.TIMESTAMP), instanceOf(Long.class));
assertEquals(now.getMillis(), convertAsNative(now, JDBCType.TIMESTAMP));
}
private Object convertAsNative(Object value, JDBCType type) throws IOException {
private Object convertAsNative(Object value, JDBCType type) throws Exception {
// Simulate sending over XContent
XContentBuilder builder = JsonXContent.contentBuilder();
builder.startObject();
@ -55,7 +54,7 @@ public class TypeConverterTests extends ESTestCase {
builder.endObject();
builder.close();
Object copy = XContentHelper.convertToMap(builder.bytes(), false, builder.contentType()).v2().get("value");
return TypeConverter.asNative(copy, type);
return TypeConverter.convert(copy, type);
}
}

View File

@ -132,7 +132,10 @@ public class SysColumns extends Command {
type.size,
// no DECIMAL support
null,
// RADIX
// RADIX - Determines how numbers returned by COLUMN_SIZE and DECIMAL_DIGITS should be interpreted.
// 10 means they represent the number of decimal digits allowed for the column.
// 2 means they represent the number of bits allowed for the column.
// null means radix is not applicable for the given type.
type.isInteger ? Integer.valueOf(10) : type.isRational ? Integer.valueOf(2) : null,
// everything is nullable
DatabaseMetaData.columnNullable,

View File

@ -81,7 +81,7 @@ public class SysTypes extends Command {
// everything is searchable,
DatabaseMetaData.typeSearchable,
// only numerics are signed
t.isNumeric() ? !t.isSigned : null,
t.isSigned(),
//no fixed precision scale SQL_FALSE
0,
null,

View File

@ -13,23 +13,23 @@ import java.util.Locale;
*/
public enum DataType {
// @formatter:off
// jdbc type, size, defPrecision, dispSize, sig, int, rat, docvals
// jdbc type, size, defPrecision, dispSize, int, rat, docvals
NULL( JDBCType.NULL, 0, 0, 0),
UNSUPPORTED( JDBCType.OTHER, 0, 0, 0),
BOOLEAN( JDBCType.BOOLEAN, 1, 1, 1),
BYTE( JDBCType.TINYINT, Byte.BYTES, 3, 5, true, true, false, true),
SHORT( JDBCType.SMALLINT, Short.BYTES, 5, 6, true, true, false, true),
INTEGER( JDBCType.INTEGER, Integer.BYTES, 10, 11, true, true, false, true),
LONG( JDBCType.BIGINT, Long.BYTES, 19, 20, true, true, false, true),
BYTE( JDBCType.TINYINT, Byte.BYTES, 3, 5, true, false, true),
SHORT( JDBCType.SMALLINT, Short.BYTES, 5, 6, true, false, true),
INTEGER( JDBCType.INTEGER, Integer.BYTES, 10, 11, true, false, true),
LONG( JDBCType.BIGINT, Long.BYTES, 19, 20, true, false, true),
// 53 bits defaultPrecision ~ 16(15.95) decimal digits (53log10(2)),
DOUBLE( JDBCType.DOUBLE, Double.BYTES, 16, 25, true, false, true, true),
DOUBLE( JDBCType.DOUBLE, Double.BYTES, 16, 25, false, true, true),
// 24 bits defaultPrecision - 24*log10(2) =~ 7 (7.22)
FLOAT( JDBCType.REAL, Float.BYTES, 7, 15, true, false, true, true),
HALF_FLOAT( JDBCType.FLOAT, Double.BYTES, 16, 25, true, false, true, true),
FLOAT( JDBCType.REAL, Float.BYTES, 7, 15, false, true, true),
HALF_FLOAT( JDBCType.FLOAT, Double.BYTES, 16, 25, false, true, true),
// precision is based on long
SCALED_FLOAT(JDBCType.FLOAT, Double.BYTES, 19, 25, true, false, true, true),
SCALED_FLOAT(JDBCType.FLOAT, Double.BYTES, 19, 25, false, true, true),
KEYWORD( JDBCType.VARCHAR, Integer.MAX_VALUE, 256, 0),
TEXT( JDBCType.VARCHAR, Integer.MAX_VALUE, Integer.MAX_VALUE, 0, false, false, false, false),
TEXT( JDBCType.VARCHAR, Integer.MAX_VALUE, Integer.MAX_VALUE, 0, false, false, false),
OBJECT( JDBCType.STRUCT, -1, 0, 0),
NESTED( JDBCType.STRUCT, -1, 0, 0),
BINARY( JDBCType.VARBINARY, -1, Integer.MAX_VALUE, 0),
@ -71,11 +71,6 @@ public enum DataType {
*/
public final int displaySize;
/**
* True if the type represents a signed number
*/
public final boolean isSigned;
/**
* True if the type represents an integer number
*/
@ -91,21 +86,20 @@ public enum DataType {
*/
public final boolean defaultDocValues;
DataType(JDBCType jdbcType, int size, int defaultPrecision, int displaySize, boolean isSigned, boolean isInteger, boolean isRational,
DataType(JDBCType jdbcType, int size, int defaultPrecision, int displaySize, boolean isInteger, boolean isRational,
boolean defaultDocValues) {
this.esType = name().toLowerCase(Locale.ROOT);
this.jdbcType = jdbcType;
this.size = size;
this.defaultPrecision = defaultPrecision;
this.displaySize = displaySize;
this.isSigned = isSigned;
this.isInteger = isInteger;
this.isRational = isRational;
this.defaultDocValues = defaultDocValues;
}
DataType(JDBCType jdbcType, int size, int defaultPrecision, int displaySize) {
this(jdbcType, size, defaultPrecision, displaySize, false, false, false, true);
this(jdbcType, size, defaultPrecision, displaySize, false, false, true);
}
public String sqlName() {
@ -116,6 +110,14 @@ public enum DataType {
return isInteger || isRational;
}
/**
* Returns true if value is signed, false if it is unsigned or null if the type doesn't represent a number
*/
public Boolean isSigned() {
// For now all numeric values that es supports are signed
return isNumeric() ? true : null;
}
public boolean isString() {
return this == KEYWORD || this == TEXT;
}

View File

@ -16,7 +16,8 @@ import java.util.function.Function;
import java.util.function.LongFunction;
/**
* Conversions from one data type to another.
* Conversions from one Elasticsearch data type to another Elasticsearch data types.
* <p>
* This class throws {@link SqlIllegalArgumentException} to differentiate between validation
* errors inside SQL as oppose to the rest of ES.
*/
@ -24,6 +25,13 @@ public abstract class DataTypeConversion {
private static final DateTimeFormatter UTC_DATE_FORMATTER = ISODateTimeFormat.dateTimeNoMillis().withZoneUTC();
/**
* Returns the type compatible with both left and right types
* <p>
* If one of the types is null - returns another type
* If both types are numeric - returns type with the highest precision int < long < float < double
* If one of the types is string and another numeric - returns numeric
*/
public static DataType commonType(DataType left, DataType right) {
if (left == right) {
return left;
@ -65,46 +73,30 @@ public abstract class DataTypeConversion {
return null;
}
public static boolean canConvert(DataType from, DataType to) { // TODO it'd be cleaner and more right to fetch the conversion
/**
* Returns true if the from type can be converted to the to type, false - otherwise
*/
public static boolean canConvert(DataType from, DataType to) {
// Special handling for nulls and if conversion is not requires
if (from == to || from == DataType.NULL) {
return true;
}
// only primitives are supported so far
if (!from.isPrimitive() || !to.isPrimitive()) {
return false;
}
if (from.getClass() == to.getClass()) {
return true;
}
if (from == DataType.NULL) {
return true;
}
// anything can be converted to String
if (to.isString()) {
return true;
}
// also anything can be converted into a bool
if (to == DataType.BOOLEAN) {
return true;
}
// numeric conversion
if ((from.isString() || from == DataType.BOOLEAN || from == DataType.DATE || from.isNumeric()) && to.isNumeric()) {
return true;
}
// date conversion
if ((from == DataType.DATE || from.isString() || from.isNumeric()) && to == DataType.DATE) {
return true;
}
return false;
return from.isPrimitive() && to.isPrimitive() && conversion(from, to) != null;
}
/**
* Get the conversion from one type to another.
*/
public static Conversion conversionFor(DataType from, DataType to) {
Conversion conversion = conversion(from, to);
if (conversion == null) {
throw new SqlIllegalArgumentException("cannot convert from [" + from + "] to [" + to + "]");
}
return conversion;
}
private static Conversion conversion(DataType from, DataType to) {
switch (to) {
case KEYWORD:
case TEXT:
@ -126,8 +118,9 @@ public abstract class DataTypeConversion {
case BOOLEAN:
return conversionToBoolean(from);
default:
throw new SqlIllegalArgumentException("cannot convert from [" + from + "] to [" + to + "]");
return null;
}
}
private static Conversion conversionToString(DataType from) {
@ -150,7 +143,7 @@ public abstract class DataTypeConversion {
if (from.isString()) {
return Conversion.STRING_TO_LONG;
}
throw new SqlIllegalArgumentException("cannot convert from [" + from + "] to [Long]");
return null;
}
private static Conversion conversionToInt(DataType from) {
@ -166,7 +159,7 @@ public abstract class DataTypeConversion {
if (from.isString()) {
return Conversion.STRING_TO_INT;
}
throw new SqlIllegalArgumentException("cannot convert from [" + from + "] to [Integer]");
return null;
}
private static Conversion conversionToShort(DataType from) {
@ -182,7 +175,7 @@ public abstract class DataTypeConversion {
if (from.isString()) {
return Conversion.STRING_TO_SHORT;
}
throw new SqlIllegalArgumentException("cannot convert [" + from + "] to [Short]");
return null;
}
private static Conversion conversionToByte(DataType from) {
@ -198,7 +191,7 @@ public abstract class DataTypeConversion {
if (from.isString()) {
return Conversion.STRING_TO_BYTE;
}
throw new SqlIllegalArgumentException("cannot convert [" + from + "] to [Byte]");
return null;
}
private static Conversion conversionToFloat(DataType from) {
@ -214,7 +207,7 @@ public abstract class DataTypeConversion {
if (from.isString()) {
return Conversion.STRING_TO_FLOAT;
}
throw new SqlIllegalArgumentException("cannot convert [" + from + "] to [Float]");
return null;
}
private static Conversion conversionToDouble(DataType from) {
@ -230,7 +223,7 @@ public abstract class DataTypeConversion {
if (from.isString()) {
return Conversion.STRING_TO_DOUBLE;
}
throw new SqlIllegalArgumentException("cannot convert [" + from + "] to [Double]");
return null;
}
private static Conversion conversionToDate(DataType from) {
@ -246,7 +239,7 @@ public abstract class DataTypeConversion {
if (from.isString()) {
return Conversion.STRING_TO_DATE;
}
throw new SqlIllegalArgumentException("cannot convert [" + from + "] to [Date]");
return null;
}
private static Conversion conversionToBoolean(DataType from) {
@ -256,7 +249,7 @@ public abstract class DataTypeConversion {
if (from.isString()) {
return Conversion.STRING_TO_BOOLEAN;
}
throw new SqlIllegalArgumentException("cannot convert [" + from + "] to [Boolean]");
return null;
}
public static byte safeToByte(long x) {
@ -295,9 +288,14 @@ public abstract class DataTypeConversion {
return Booleans.parseBoolean(lowVal);
}
/**
* Converts arbitrary object to the desired data type.
* <p>
* Throws SqlIllegalArgumentException if such conversion is not possible
*/
public static Object convert(Object value, DataType dataType) {
DataType detectedType = DataTypes.fromJava(value);
if (detectedType.equals(dataType) || value == null) {
if (detectedType == dataType || value == null) {
return value;
}
return conversionFor(detectedType, dataType).convert(value);

View File

@ -177,4 +177,18 @@ public class DataTypeConversionTests extends ESTestCase {
assertEquals("[" + Short.MAX_VALUE + "] out of [Byte] range", e.getMessage());
}
}
public void testCommonType() {
assertEquals(DataType.BOOLEAN, DataTypeConversion.commonType(DataType.BOOLEAN, DataType.NULL));
assertEquals(DataType.BOOLEAN, DataTypeConversion.commonType(DataType.NULL, DataType.BOOLEAN));
assertEquals(DataType.BOOLEAN, DataTypeConversion.commonType(DataType.BOOLEAN, DataType.BOOLEAN));
assertEquals(DataType.NULL, DataTypeConversion.commonType(DataType.NULL, DataType.NULL));
assertEquals(DataType.INTEGER, DataTypeConversion.commonType(DataType.INTEGER, DataType.KEYWORD));
assertEquals(DataType.LONG, DataTypeConversion.commonType(DataType.TEXT, DataType.LONG));
assertEquals(null, DataTypeConversion.commonType(DataType.TEXT, DataType.KEYWORD));
assertEquals(DataType.SHORT, DataTypeConversion.commonType(DataType.SHORT, DataType.BYTE));
assertEquals(DataType.FLOAT, DataTypeConversion.commonType(DataType.BYTE, DataType.FLOAT));
assertEquals(DataType.FLOAT, DataTypeConversion.commonType(DataType.FLOAT, DataType.INTEGER));
assertEquals(DataType.DOUBLE, DataTypeConversion.commonType(DataType.DOUBLE, DataType.FLOAT));
}
}