From 6c3349d0a298493245f2bf4c7dbd920524f4e0a7 Mon Sep 17 00:00:00 2001 From: Zhichun Wu Date: Fri, 9 Sep 2022 17:10:57 +0800 Subject: [PATCH] Refactor JdbcTypeMapping by making it extensible --- .../clickhouse/client/ClickHouseUtils.java | 15 - .../com/clickhouse/jdbc/ClickHouseArray.java | 3 +- .../clickhouse/jdbc/ClickHouseConnection.java | 15 +- .../jdbc/ClickHouseDatabaseMetaData.java | 10 +- .../clickhouse/jdbc/ClickHouseResultSet.java | 11 +- .../jdbc/ClickHouseResultSetMetaData.java | 18 +- .../java/com/clickhouse/jdbc/JdbcConfig.java | 71 +- .../com/clickhouse/jdbc/JdbcTypeMapping.java | 640 +++++++++++++----- .../internal/ClickHouseParameterMetaData.java | 15 +- .../internal/ClickHouseStatementImpl.java | 14 +- .../internal/InputBasedPreparedStatement.java | 3 +- .../internal/SqlBasedPreparedStatement.java | 12 +- .../internal/TableBasedPreparedStatement.java | 14 +- .../jdbc/ClickHousePreparedStatementTest.java | 2 +- .../jdbc/ClickHouseStatementTest.java | 24 + 15 files changed, 638 insertions(+), 229 deletions(-) diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseUtils.java b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseUtils.java index adeb32b3a..350aa4ae2 100644 --- a/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseUtils.java +++ b/clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseUtils.java @@ -7,7 +7,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; -import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URLDecoder; @@ -18,7 +17,6 @@ import java.nio.file.Paths; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.LinkedHashMap; @@ -1342,19 +1340,6 @@ public static int readParameters(String args, int startIndex, int len, List T[] toArray(Class clazz, Collection list) { - int size = list == null ? 0 : list.size(); - T[] array = (T[]) Array.newInstance(clazz, size); - if (size > 0) { - int i = 0; - for (T t : list) { - array[i++] = t; - } - } - return array; - } - private ClickHouseUtils() { } } diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseArray.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseArray.java index 8b2dcd2c0..47e014dcc 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseArray.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseArray.java @@ -39,8 +39,7 @@ public String getBaseTypeName() throws SQLException { public int getBaseType() throws SQLException { ensureValid(); - // don't really want to pass type mapping to Array object... - return JdbcTypeMapping.toJdbcType(null, getBaseColumn()); + return resultSet.mapper.toSqlType(getBaseColumn(), resultSet.defaultTypeMap); } @Override diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseConnection.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseConnection.java index ae4491e35..ac00d76b5 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseConnection.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseConnection.java @@ -229,12 +229,21 @@ default PreparedStatement prepareStatement(String sql, int resultSetType, int re */ JdbcConfig getJdbcConfig(); + /** + * Gets JDBC type mapping. Same as {@code getJdbcConfig().getMapper()}. + * + * @return non-null JDBC type mapping + */ + default JdbcTypeMapping getJdbcTypeMapping() { + return getJdbcConfig().getDialect(); + } + /** * Gets max insert block size. Pay attention that INSERT into one partition in - * one table of - * MergeTree family up to max_insert_size rows is transactional. + * one table of MergeTree family up to max_insert_block_size rows is + * transactional. * - * @return + * @return value of max_insert_block_size */ long getMaxInsertBlockSize(); diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseDatabaseMetaData.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseDatabaseMetaData.java index c6b32d506..bc75be099 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseDatabaseMetaData.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseDatabaseMetaData.java @@ -40,7 +40,6 @@ public class ClickHouseDatabaseMetaData extends JdbcWrapper implements DatabaseM "REMOTE TABLE", "TABLE", "VIEW", "SYSTEM TABLE", "TEMPORARY TABLE" }; private final ClickHouseConnection connection; - private final Map> typeMaps; protected ResultSet empty(String columns) throws SQLException { return fixed(columns, null); @@ -86,7 +85,6 @@ protected ResultSet query(String sql, ClickHouseRecordTransformer func, boolean public ClickHouseDatabaseMetaData(ClickHouseConnection connection) throws SQLException { this.connection = ClickHouseChecker.nonNull(connection, "Connection"); - this.typeMaps = connection.getTypeMap(); } @Override @@ -830,7 +828,8 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa String typeName = r.getValue("TYPE_NAME").asString(); try { ClickHouseColumn column = ClickHouseColumn.of("", typeName); - r.getValue("DATA_TYPE").update(JdbcTypeMapping.toJdbcType(typeMaps, column)); + r.getValue("DATA_TYPE") + .update(connection.getJdbcTypeMapping().toSqlType(column, connection.getTypeMap())); r.getValue("COLUMN_SIZE").update( column.getPrecision() > 0 ? column.getPrecision() : column.getDataType().getByteLength()); if (column.isNullable()) { @@ -913,7 +912,7 @@ public ResultSet getCrossReference(String parentCatalog, String parentSchema, St + "DELETE_RULE Int16, FK_NAME Nullable(String), PK_NAME Nullable(String), DEFERRABILITY Int16"); } - private Object[] toTypeRow(String typeName, String aliasTo) { + private Object[] toTypeRow(String typeName, String aliasTo) throws SQLException { ClickHouseDataType type; try { type = ClickHouseDataType.of(typeName); @@ -976,7 +975,8 @@ private Object[] toTypeRow(String typeName, String aliasTo) { break; } return new Object[] { typeName, - JdbcTypeMapping.toJdbcType(typeMaps, ClickHouseColumn.of("", type, false, false, new String[0])), + connection.getJdbcTypeMapping().toSqlType(ClickHouseColumn.of("", type, false, false, new String[0]), + connection.getTypeMap()), type.getMaxPrecision(), prefix, suffix, params, nullable, type.isCaseSensitive() ? 1 : 0, searchable, type.getMaxPrecision() > 0 && !type.isSigned() ? 1 : 0, money, 0, aliasTo == null || aliasTo.isEmpty() ? type.name() : aliasTo, type.getMinScale(), type.getMaxScale(), 0, diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseResultSet.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseResultSet.java index 3e0197bdf..8d03df4c9 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseResultSet.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseResultSet.java @@ -61,6 +61,7 @@ public class ClickHouseResultSet extends AbstractResultSet { protected final boolean nullAsDefault; protected final ClickHouseResultSetMetaData metaData; + protected final JdbcTypeMapping mapper; protected final Map> defaultTypeMap; // only for testing purpose @@ -74,11 +75,12 @@ public class ClickHouseResultSet extends AbstractResultSet { this.wrapObject = false; this.defaultCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + this.mapper = JdbcTypeMapping.getDefaultMapping(); this.defaultTypeMap = Collections.emptyMap(); this.currentRow = null; try { this.columns = response.getColumns(); - this.metaData = new ClickHouseResultSetMetaData(database, table, columns, defaultTypeMap); + this.metaData = new ClickHouseResultSetMetaData(database, table, columns, this.mapper, defaultTypeMap); this.rowCursor = response.records().iterator(); } catch (Exception e) { @@ -109,13 +111,14 @@ public ClickHouseResultSet(String database, String table, ClickHouseStatement st this.wrapObject = statement.getConnection().getJdbcConfig().useWrapperObject(); this.defaultCalendar = conn.getDefaultCalendar(); + this.mapper = statement.getConnection().getJdbcTypeMapping(); Map> typeMap = conn.getTypeMap(); this.defaultTypeMap = typeMap != null && !typeMap.isEmpty() ? Collections.unmodifiableMap(typeMap) : Collections.emptyMap(); this.currentRow = null; try { this.columns = response.getColumns(); - this.metaData = new ClickHouseResultSetMetaData(database, table, columns, defaultTypeMap); + this.metaData = new ClickHouseResultSetMetaData(database, table, columns, this.mapper, defaultTypeMap); this.rowCursor = response.records().iterator(); } catch (Exception e) { @@ -137,7 +140,7 @@ protected void ensureRead(int columnIndex) throws SQLException { throw new SQLException("No data available for reading", SqlExceptionUtils.SQL_STATE_NO_DATA); } else if (columnIndex < 1 || columnIndex > columns.size()) { throw SqlExceptionUtils.clientError(ClickHouseUtils - .format("Column index must between 1 and %d but we got %d", columns.size(), columnIndex)); + .format("Column index must between 1 and %d but we got %d", columns.size() + 1, columnIndex)); } } @@ -480,7 +483,7 @@ public Object getObject(int columnIndex, Map> map) throws SQLEx value = javaType != null ? v.asObject(javaType) : v.asObject(); } else if (c.isArray()) { value = new ClickHouseArray(this, columnIndex); - } else if (c.isTuple() || c.isNested()) { + } else if (c.isTuple() || c.isNested() || c.isMap()) { value = new ClickHouseStruct(c.getDataType().name(), v.asArray()); } else { value = javaType != null ? v.asObject(javaType) : v.asObject(); diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseResultSetMetaData.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseResultSetMetaData.java index 4209f0649..8e7b28305 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseResultSetMetaData.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHouseResultSetMetaData.java @@ -10,25 +10,26 @@ public class ClickHouseResultSetMetaData extends JdbcWrapper implements ResultSetMetaData { public static ResultSetMetaData of(String database, String table, List columns, - Map> typeMap) - throws SQLException { + JdbcTypeMapping mapper, Map> typeMap) throws SQLException { if (database == null || table == null || columns == null) { throw SqlExceptionUtils.clientError("Non-null database, table, and column list are required"); } - return new ClickHouseResultSetMetaData(database, table, columns, typeMap); + return new ClickHouseResultSetMetaData(database, table, columns, mapper, typeMap); } private final String database; private final String table; private final List columns; + private final JdbcTypeMapping mapper; private final Map> typeMap; protected ClickHouseResultSetMetaData(String database, String table, List columns, - Map> typeMap) { + JdbcTypeMapping mapper, Map> typeMap) { this.database = database; this.table = table; this.columns = columns; + this.mapper = mapper; this.typeMap = typeMap; } @@ -39,7 +40,8 @@ protected List getColumns() { protected ClickHouseColumn getColumn(int index) throws SQLException { if (index < 1 || index > columns.size()) { throw SqlExceptionUtils.clientError( - ClickHouseUtils.format("Column index must between 1 and %d but we got %d", columns.size(), index)); + ClickHouseUtils.format("Column index must between 1 and %d but we got %d", columns.size() + 1, + index)); } return columns.get(index - 1); } @@ -121,12 +123,12 @@ public String getCatalogName(int column) throws SQLException { @Override public int getColumnType(int column) throws SQLException { - return JdbcTypeMapping.toJdbcType(typeMap, getColumn(column)); + return mapper.toSqlType(getColumn(column), typeMap); } @Override public String getColumnTypeName(int column) throws SQLException { - return getColumn(column).getOriginalTypeName(); + return mapper.toNativeType(getColumn(column)); } @Override @@ -146,6 +148,6 @@ public boolean isDefinitelyWritable(int column) throws SQLException { @Override public String getColumnClassName(int column) throws SQLException { - return JdbcTypeMapping.toJavaClass(typeMap, getColumn(column)).getCanonicalName(); + return mapper.toJavaClass(getColumn(column), typeMap).getCanonicalName(); } } diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcConfig.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcConfig.java index 39283ffe0..3ec83902d 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcConfig.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcConfig.java @@ -10,7 +10,7 @@ import java.util.Map.Entry; import com.clickhouse.client.ClickHouseChecker; -import com.clickhouse.client.ClickHouseUtils; +import com.clickhouse.client.config.ClickHouseOption; import com.clickhouse.client.logging.Logger; import com.clickhouse.client.logging.LoggerFactory; @@ -23,6 +23,7 @@ public class JdbcConfig { public static final String PROP_AUTO_COMMIT = "autoCommit"; public static final String PROP_CREATE_DATABASE = "createDatabaseIfNotExist"; public static final String PROP_CONTINUE_BATCH = "continueBatchOnError"; + public static final String PROP_DIALECT = "dialect"; public static final String PROP_FETCH_SIZE = "fetchSize"; public static final String PROP_JDBC_COMPLIANT = "jdbcCompliant"; public static final String PROP_NAMED_PARAM = "namedParameter"; @@ -37,6 +38,7 @@ public class JdbcConfig { private static final String DEFAULT_AUTO_COMMIT = BOOLEAN_TRUE; private static final String DEFAULT_CREATE_DATABASE = BOOLEAN_FALSE; private static final String DEFAULT_CONTINUE_BATCH = BOOLEAN_FALSE; + private static final String DEFAULT_DIALECT = ""; private static final String DEFAULT_FETCH_SIZE = "0"; private static final String DEFAULT_JDBC_COMPLIANT = BOOLEAN_TRUE; private static final String DEFAULT_NAMED_PARAM = BOOLEAN_FALSE; @@ -45,31 +47,55 @@ public class JdbcConfig { private static final String DEFAULT_TYPE_MAP = ""; private static final String DEFAULT_WRAPPER_OBJ = BOOLEAN_FALSE; - static boolean extractBooleanValue(Properties props, String key, String defaultValue) { + static String removeAndGetPropertyValue(Properties props, String key) { if (props == null || props.isEmpty() || key == null || key.isEmpty()) { - return Boolean.parseBoolean(defaultValue); + return null; } - Object value = props.remove(key); - return Boolean.parseBoolean(value != null ? value.toString() : defaultValue); + // Remove JDBC-specific options so that they won't be treated as server settings + // at later stage. Default properties won't be used for the same reason. + Object raw = props.remove(key); + return raw == null ? null : raw.toString(); + } + + static boolean extractBooleanValue(Properties props, String key, String defaultValue) { + String value = removeAndGetPropertyValue(props, key); + return Boolean.parseBoolean(value != null ? value : defaultValue); } static int extractIntValue(Properties props, String key, String defaultValue) { - if (props == null || props.isEmpty() || key == null || key.isEmpty()) { - return Integer.parseInt(defaultValue); + String value = removeAndGetPropertyValue(props, key); + return Integer.parseInt(value != null ? value : defaultValue); + } + + // TODO return JdbcDialect + static JdbcTypeMapping extractDialectValue(Properties props, String key, String defaultValue) { + String value = removeAndGetPropertyValue(props, key); + if (value == null) { + value = defaultValue; } - Object value = props.remove(key); - return Integer.parseInt(value != null ? value.toString() : defaultValue); + JdbcTypeMapping mapper; + if (ClickHouseChecker.isNullOrBlank(value)) { + mapper = JdbcTypeMapping.getDefaultMapping(); + } else if ("ansi".equalsIgnoreCase(value)) { + mapper = JdbcTypeMapping.getAnsiMapping(); + } else { + try { + Class clazz = JdbcConfig.class.getClassLoader().loadClass(value); + mapper = (JdbcTypeMapping) clazz.getConstructor().newInstance(); + } catch (Throwable t) { + log.warn("Failed to load custom JDBC type mapping [%s], due to: %s", value, t.getMessage()); + mapper = JdbcTypeMapping.getDefaultMapping(); + } + } + return mapper; } static Map> extractTypeMapValue(Properties props, String key, String defaultValue) { - String value = null; - if (props == null || props.isEmpty() || key == null || key.isEmpty()) { + String value = removeAndGetPropertyValue(props, key); + if (value == null) { value = defaultValue; - } else { - Object v = props.remove(key); - value = v != null ? v.toString() : defaultValue; } if (ClickHouseChecker.isNullOrBlank(value)) { @@ -78,7 +104,7 @@ static Map> extractTypeMapValue(Properties props, String key, S Map> map = new LinkedHashMap<>(); ClassLoader loader = JdbcConfig.class.getClassLoader(); - for (Entry e : ClickHouseUtils.getKeyValuePairs(value).entrySet()) { + for (Entry e : ClickHouseOption.toKeyValuePairs(value).entrySet()) { Class clazz = null; try { clazz = loader.loadClass(e.getValue()); @@ -119,6 +145,10 @@ public static List getDriverProperties() { info.description = "Whether to enable JDBC-compliant features like fake transaction and standard UPDATE and DELETE statements."; list.add(info); + info = new DriverPropertyInfo(PROP_DIALECT, DEFAULT_DIALECT); + info.description = "Dialect mainly for data type mapping, can be set to ansi or a full qualified class name implementing JdbcTypeMapping."; + list.add(info); + info = new DriverPropertyInfo(PROP_NAMED_PARAM, DEFAULT_NAMED_PARAM); info.choices = new String[] { BOOLEAN_TRUE, BOOLEAN_FALSE }; info.description = "Whether to use named parameter(e.g. :ts(DateTime64(6)) or :value etc.) instead of standard JDBC question mark placeholder."; @@ -150,6 +180,7 @@ public static List getDriverProperties() { private final boolean continueBatch; private final int fetchSize; private final boolean jdbcCompliant; + private final JdbcTypeMapping dialect; private final boolean namedParameter; private final int nullAsDefault; private final boolean txSupport; @@ -168,6 +199,7 @@ public JdbcConfig(Properties props) { this.autoCommit = extractBooleanValue(props, PROP_AUTO_COMMIT, DEFAULT_AUTO_COMMIT); this.createDb = extractBooleanValue(props, PROP_CREATE_DATABASE, DEFAULT_CREATE_DATABASE); this.continueBatch = extractBooleanValue(props, PROP_CONTINUE_BATCH, DEFAULT_CONTINUE_BATCH); + this.dialect = extractDialectValue(props, PROP_DIALECT, DEFAULT_DIALECT); this.fetchSize = extractIntValue(props, PROP_FETCH_SIZE, DEFAULT_FETCH_SIZE); this.jdbcCompliant = extractBooleanValue(props, PROP_JDBC_COMPLIANT, DEFAULT_JDBC_COMPLIANT); this.namedParameter = extractBooleanValue(props, PROP_NAMED_PARAM, DEFAULT_NAMED_PARAM); @@ -215,6 +247,15 @@ public int getFetchSize() { return fetchSize; } + /** + * Gets JDBC dialect. + * + * @return non-null JDBC dialect + */ + public JdbcTypeMapping getDialect() { + return dialect; + } + /** * Gets custom type map. * diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcTypeMapping.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcTypeMapping.java index be25192dd..945bcfd39 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcTypeMapping.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/JdbcTypeMapping.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Date; +import java.sql.JDBCType; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; @@ -11,13 +12,311 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; +import java.util.List; import java.util.Map; +import java.util.TimeZone; import com.clickhouse.client.ClickHouseColumn; import com.clickhouse.client.ClickHouseDataType; +import com.clickhouse.client.ClickHouseUtils; -public final class JdbcTypeMapping { - static Class getCustomJavaClass(Map> typeMap, ClickHouseColumn column) { +/** + * This class defines mappings among {@link Types}, {@link JDBCType}, + * {@link ClickHouseDataType}, {@link ClickHouseColumn}, and {@link Class}. It + * does not impact serialization and deserialization, which is handled + * separately by {@link com.clickhouse.client.ClickHouseDataProcessor}. + */ +public class JdbcTypeMapping { + static final class AnsiTypeMapping extends JdbcTypeMapping { + static String toAnsiSqlType(ClickHouseDataType dataType, int precision, int scale, TimeZone tz) { + final String typeName; + switch (dataType) { + case Bool: + typeName = "BOOLEAN"; // or BIT(1)? + break; + case Date: + case Date32: + typeName = "DATE"; + break; + case DateTime: + case DateTime32: + case DateTime64: + typeName = (scale <= 0 ? new StringBuilder("TIMESTAMP") + : new StringBuilder("TIMESTAMP(").append(scale).append(')')) + .append(tz != null ? " WITH TIMEZONE" : "").toString(); + break; + case Int8: + typeName = "BYTE"; // NON-standard + break; + case UInt8: + case Int16: + typeName = "SMALLINT"; + break; + case UInt16: + case Int32: + typeName = "INTEGER"; + break; + case UInt32: + case Int64: + case IntervalYear: + case IntervalQuarter: + case IntervalMonth: + case IntervalWeek: + case IntervalDay: + case IntervalHour: + case IntervalMinute: + case IntervalSecond: + typeName = "BIGINT"; + break; + case UInt64: + case Int128: + case UInt128: + case Int256: + case UInt256: + case Decimal: + case Decimal32: + case Decimal64: + case Decimal128: + case Decimal256: + typeName = new StringBuilder("DECIMAL(").append(precision).append(',') + .append(scale).append(')').toString(); + break; + case Float32: + typeName = "REAL"; + break; + case Float64: + typeName = "DOUBLE PRECISION"; + break; + case Point: + case Ring: + case Polygon: + case MultiPolygon: + typeName = "ARRAY"; + break; + case Enum: + case Enum8: + case Enum16: + case IPv4: + case IPv6: + case JSON: + case Object: + case FixedString: + case String: + case UUID: + typeName = "VARCHAR"; + break; + default: + typeName = "BINARY"; + break; + } + return typeName; + } + + static String toAnsiSqlType(ClickHouseColumn column, StringBuilder builder) { + final ClickHouseDataType dataType = column.getDataType(); + final String sqlType; + + if (dataType == ClickHouseDataType.SimpleAggregateFunction) { + sqlType = column.hasNestedColumn() ? toAnsiSqlType(column.getNestedColumns().get(0), builder) + : "BINARY"; + } else if (column.isArray()) { + sqlType = builder.append("ARRAY").append('(') + .append(toAnsiSqlType(column.getArrayBaseColumn(), builder)) + .append(')').toString(); + } else if (column.isMap()) { + return builder.append("MAP").append('(').append(toAnsiSqlType(column.getKeyInfo(), builder)).append(',') + .append(toAnsiSqlType(column.getValueInfo(), builder)) + .append(')').toString(); + } else if (column.isNested() || column.isTuple()) { + builder.append("STRUCT").append('('); + for (ClickHouseColumn c : column.getNestedColumns()) { + builder.append(toAnsiSqlType(c, builder)).append(','); + } + builder.setLength(builder.length() - 1); + sqlType = builder.append(')').toString(); + } else { + sqlType = toAnsiSqlType(dataType, column.getPrecision(), column.getScale(), + column.getTimeZone()); + } + return sqlType; + } + + @Override + protected int getSqlType(Class javaClass) { // and purpose(e.g. for read or write?) + final int sqlType; + if (javaClass == boolean.class || javaClass == Boolean.class) { + sqlType = Types.BOOLEAN; + } else if (javaClass == byte.class || javaClass == Byte.class) { + sqlType = Types.TINYINT; + } else if (javaClass == short.class || javaClass == Short.class || javaClass == int.class + || javaClass == Integer.class) { + sqlType = Types.INTEGER; + } else if (javaClass == long.class || javaClass == Long.class) { + sqlType = Types.BIGINT; + } else if (javaClass == float.class || javaClass == Float.class) { + sqlType = Types.FLOAT; + } else if (javaClass == double.class || javaClass == Double.class) { + sqlType = Types.DOUBLE; + } else if (javaClass == BigInteger.class || javaClass == BigDecimal.class) { + sqlType = Types.DECIMAL; + } else if (javaClass == Date.class || javaClass == LocalDate.class) { + sqlType = Types.DATE; + } else if (javaClass == Time.class || javaClass == LocalTime.class) { + sqlType = Types.TIME; + } else if (javaClass == Timestamp.class || javaClass == LocalDateTime.class + || javaClass == OffsetDateTime.class || javaClass == ZonedDateTime.class) { + sqlType = Types.TIMESTAMP; + } else if (javaClass == String.class || javaClass == byte[].class + || Enum.class.isAssignableFrom(javaClass)) { + sqlType = Types.VARCHAR; + } else if (javaClass.isArray()) { // could be Nested type + sqlType = Types.ARRAY; + } else if (List.class.isAssignableFrom(javaClass) || Map.class.isAssignableFrom(javaClass)) { + sqlType = Types.STRUCT; + } else { + sqlType = Types.OTHER; + } + return sqlType; + } + + @Override + public String toNativeType(ClickHouseColumn column) { + return toAnsiSqlType(column, new StringBuilder()); + } + + @Override + public int toSqlType(ClickHouseColumn column, Map> typeMap) { + Class javaClass = getCustomJavaClass(column, typeMap); + if (javaClass != null) { + return getSqlType(javaClass); + } + + int sqlType = Types.OTHER; + switch (column.getDataType()) { + case Bool: + sqlType = Types.BOOLEAN; + break; + case Int8: + sqlType = Types.TINYINT; + break; + case UInt8: + case Int16: + case UInt16: + case Int32: + sqlType = Types.INTEGER; + break; + case UInt32: + case IntervalYear: + case IntervalQuarter: + case IntervalMonth: + case IntervalWeek: + case IntervalDay: + case IntervalHour: + case IntervalMinute: + case IntervalSecond: + case Int64: + sqlType = Types.BIGINT; + break; + case Float32: + sqlType = Types.FLOAT; + break; + case Float64: + sqlType = Types.DOUBLE; + break; + case UInt64: + case Int128: + case UInt128: + case Int256: + case UInt256: + case Decimal: + case Decimal32: + case Decimal64: + case Decimal128: + case Decimal256: + sqlType = Types.DECIMAL; + break; + case Date: + case Date32: + sqlType = Types.DATE; + break; + case DateTime: + case DateTime32: + case DateTime64: + sqlType = Types.TIMESTAMP; + break; + case Enum: + case Enum8: + case Enum16: + case IPv4: + case IPv6: + case FixedString: + case JSON: + case Object: + case String: + case UUID: + sqlType = Types.VARCHAR; + break; + case Point: + case Ring: + case Polygon: + case MultiPolygon: + case Array: + sqlType = Types.ARRAY; + break; + case Map: // Map + case Nested: // Object[][] + case Tuple: // List + sqlType = Types.STRUCT; + break; + case Nothing: + sqlType = Types.NULL; + break; + default: + break; + } + + return sqlType; + } + } + + /** + * Inner class for static initialization. + */ + static final class InstanceHolder { + private static final JdbcTypeMapping defaultMapping = ClickHouseUtils + .getService(JdbcTypeMapping.class, JdbcTypeMapping::new); + private static final JdbcTypeMapping ansiMapping = new AnsiTypeMapping(); + + private InstanceHolder() { + } + } + + /** + * Gets default type mapping. + * + * @return non-null type mapping + */ + public static JdbcTypeMapping getDefaultMapping() { + return InstanceHolder.defaultMapping; + } + + /** + * Gets ANSI type mapping. + * + * @return non-null type mapping + */ + public static JdbcTypeMapping getAnsiMapping() { + return InstanceHolder.ansiMapping; + } + + /** + * Gets custom Java class for the given column. + * + * @param column non-null column definition + * @param typeMap column type to Java class map, could be null + * @return custom Java class which may or may not be null + */ + protected Class getCustomJavaClass(ClickHouseColumn column, Map> typeMap) { if (typeMap != null && !typeMap.isEmpty()) { Class javaClass = typeMap.get(column.getOriginalTypeName()); if (javaClass == null) { @@ -31,13 +330,97 @@ static Class getCustomJavaClass(Map> typeMap, ClickHouseColu } /** - * Gets corresponding JDBC type for the given Java class. + * Gets corresponding {@link ClickHouseDataType} of the given {@link Types}. + * + * @param sqlType generic SQL types defined in JDBC + * @return non-null ClickHouse data type + */ + protected ClickHouseDataType getDataType(int sqlType) { + ClickHouseDataType dataType; + + switch (sqlType) { + case Types.BOOLEAN: + dataType = ClickHouseDataType.UInt8; + break; + case Types.TINYINT: + dataType = ClickHouseDataType.Int8; + break; + case Types.SMALLINT: + dataType = ClickHouseDataType.Int16; + break; + case Types.INTEGER: + dataType = ClickHouseDataType.Int32; + break; + case Types.BIGINT: + dataType = ClickHouseDataType.Int64; + break; + case Types.NUMERIC: + dataType = ClickHouseDataType.Int256; + break; + case Types.FLOAT: + case Types.REAL: + dataType = ClickHouseDataType.Float32; + break; + case Types.DOUBLE: + dataType = ClickHouseDataType.Float64; + break; + case Types.DECIMAL: + dataType = ClickHouseDataType.Decimal; + break; + case Types.BIT: + case Types.BLOB: + case Types.BINARY: + case Types.CHAR: + case Types.CLOB: + case Types.JAVA_OBJECT: + case Types.LONGNVARCHAR: + case Types.LONGVARBINARY: + case Types.LONGVARCHAR: + case Types.NCHAR: + case Types.NCLOB: + case Types.NVARCHAR: + case Types.OTHER: + case Types.SQLXML: + case Types.VARBINARY: + case Types.VARCHAR: + dataType = ClickHouseDataType.String; + break; + case Types.DATE: + dataType = ClickHouseDataType.Date; + break; + case Types.TIME: + case Types.TIME_WITH_TIMEZONE: + case Types.TIMESTAMP: + case Types.TIMESTAMP_WITH_TIMEZONE: + dataType = ClickHouseDataType.DateTime; + break; + case Types.ARRAY: + dataType = ClickHouseDataType.Array; + break; + case Types.STRUCT: + dataType = ClickHouseDataType.Tuple; + break; + case Types.DATALINK: + case Types.DISTINCT: + case Types.REF: + case Types.REF_CURSOR: + case Types.ROWID: + case Types.NULL: + default: + dataType = ClickHouseDataType.Nothing; + break; + } + return dataType; + } + + /** + * Gets corresponding {@link Types} for the given Java class. * * @param javaClass non-null Java class - * @return JDBC type + * @return generic SQL type defined in JDBC */ - public static int toJdbcType(Class javaClass) { - int sqlType = Types.OTHER; + protected int getSqlType(Class javaClass) { // and purpose(e.g. for read or write?) + final int sqlType; if (javaClass == boolean.class || javaClass == Boolean.class) { sqlType = Types.BOOLEAN; } else if (javaClass == byte.class || javaClass == Byte.class) { @@ -66,27 +449,114 @@ public static int toJdbcType(Class javaClass) { sqlType = Types.TIMESTAMP_WITH_TIMEZONE; } else if (javaClass == String.class || javaClass == byte[].class || Enum.class.isAssignableFrom(javaClass)) { sqlType = Types.VARCHAR; - } else if (javaClass.isArray()) { + } else if (javaClass.isArray()) { // could be Nested type sqlType = Types.ARRAY; + } else if (List.class.isAssignableFrom(javaClass) || Map.class.isAssignableFrom(javaClass)) { + sqlType = Types.STRUCT; + } else { + sqlType = Types.OTHER; } return sqlType; } /** - * Gets corresponding JDBC type for the given column. + * Converts {@link JDBCType} to ClickHouse column. + * + * @param jdbcType JDBC type + * @param scaleOrLength scale or length + * @return non-null ClickHouse column + */ + public ClickHouseColumn toColumn(JDBCType jdbcType, int scaleOrLength) { + Integer type = jdbcType.getVendorTypeNumber(); + return toColumn(type != null ? type : Types.OTHER, scaleOrLength); + } + + /** + * Converts {@link Types} to ClickHouse column. + * + * @param sqlType generic SQL types defined in JDBC + * @param scaleOrLength scale or length + * @return non-null ClickHouse column + */ + public ClickHouseColumn toColumn(int sqlType, int scaleOrLength) { + ClickHouseDataType dataType = getDataType(sqlType); + ClickHouseColumn column = null; + if (scaleOrLength > 0) { + if (sqlType == Types.BIT && scaleOrLength == 1) { + dataType = ClickHouseDataType.UInt8; + } else if (sqlType == Types.NUMERIC || sqlType == Types.DECIMAL) { + for (ClickHouseDataType t : new ClickHouseDataType[] {}) { + if (scaleOrLength <= t.getMaxScale() / 2) { + column = ClickHouseColumn.of("", t, false, t.getMaxPrecision() - t.getMaxScale(), + scaleOrLength); + break; + } + } + } else if (dataType == ClickHouseDataType.Date) { + if (scaleOrLength > 2) { + dataType = ClickHouseDataType.Date32; + } + } else if (dataType == ClickHouseDataType.DateTime) { + column = ClickHouseColumn.of("", ClickHouseDataType.DateTime64, false, 0, scaleOrLength); + } else if (dataType == ClickHouseDataType.String) { + column = ClickHouseColumn.of("", ClickHouseDataType.FixedString, false, scaleOrLength, 0); + } + } + + return column == null ? ClickHouseColumn.of("", dataType, false, false) : column; + } + + /** + * Converts {@link ClickHouseColumn} to {@link Class}. * - * @param typeMap type mappings, could be null * @param column non-null column definition - * @return JDBC type + * @param typeMap optional custom type mapping + * @return non-null Java class */ - public static int toJdbcType(Map> typeMap, ClickHouseColumn column) { - Class javaClass = getCustomJavaClass(typeMap, column); + public Class toJavaClass(ClickHouseColumn column, Map> typeMap) { + Class clazz = getCustomJavaClass(column, typeMap); + if (clazz != null) { + return clazz; + } + + ClickHouseDataType type = column.getDataType(); + switch (type) { + case DateTime: + case DateTime32: + case DateTime64: + clazz = column.getTimeZone() != null ? OffsetDateTime.class : LocalDateTime.class; + break; + default: + clazz = type.getObjectClass(); + break; + } + return clazz; + } + + /** + * Converts {@link ClickHouseColumn} to native type. + * + * @param column non-null column definition + * @return non-null native type + */ + public String toNativeType(ClickHouseColumn column) { + return column.getOriginalTypeName(); + } + + /** + * Converts {@link ClickHouseColumn} to generic SQL type defined in JDBC. + * + * @param column non-null column definition + * @param typeMap optional custom mapping + * @return generic SQL type defined in JDBC + */ + public int toSqlType(ClickHouseColumn column, Map> typeMap) { + Class javaClass = getCustomJavaClass(column, typeMap); if (javaClass != null) { - return toJdbcType(javaClass); + return getSqlType(javaClass); } int sqlType = Types.OTHER; - switch (column.getDataType()) { case Bool: sqlType = Types.BOOLEAN; @@ -149,6 +619,8 @@ public static int toJdbcType(Map> typeMap, ClickHouseColumn col case IPv4: case IPv6: case FixedString: + case JSON: + case Object: case String: case UUID: sqlType = Types.VARCHAR; @@ -160,152 +632,18 @@ public static int toJdbcType(Map> typeMap, ClickHouseColumn col case Array: sqlType = Types.ARRAY; break; - case Tuple: - case Nested: + case Map: // Map + case Nested: // Object[][] + case Tuple: // List sqlType = Types.STRUCT; break; case Nothing: sqlType = Types.NULL; break; - case Map: default: break; } return sqlType; } - - /** - * Gets Java class for the given column. - * - * @param typeMap type mappings, could be null - * @param column non-null column definition - * @return Java class for the column - */ - public static Class toJavaClass(Map> typeMap, ClickHouseColumn column) { - Class clazz = getCustomJavaClass(typeMap, column); - if (clazz != null) { - return clazz; - } - - ClickHouseDataType type = column.getDataType(); - switch (type) { - case DateTime: - case DateTime32: - case DateTime64: - clazz = column.getTimeZone() != null ? OffsetDateTime.class : LocalDateTime.class; - break; - default: - clazz = type.getObjectClass(); - break; - } - return clazz; - } - - public static ClickHouseColumn fromJdbcType(int jdbcType, int scaleOrLength) { - ClickHouseDataType dataType = fromJdbcType(jdbcType); - ClickHouseColumn column = null; - if (scaleOrLength > 0) { - if (jdbcType == Types.NUMERIC || jdbcType == Types.DECIMAL) { - for (ClickHouseDataType t : new ClickHouseDataType[] {}) { - if (scaleOrLength <= t.getMaxScale() / 2) { - column = ClickHouseColumn.of("", t, false, t.getMaxPrecision() - t.getMaxScale(), - scaleOrLength); - break; - } - } - } else if (dataType == ClickHouseDataType.Date) { - if (scaleOrLength > 2) { - dataType = ClickHouseDataType.Date32; - } - } else if (dataType == ClickHouseDataType.DateTime) { - column = ClickHouseColumn.of("", ClickHouseDataType.DateTime64, false, 0, scaleOrLength); - } else if (dataType == ClickHouseDataType.String) { - column = ClickHouseColumn.of("", ClickHouseDataType.FixedString, false, scaleOrLength, 0); - } - } - - return column == null ? ClickHouseColumn.of("", dataType, false, false) : column; - } - - public static ClickHouseDataType fromJdbcType(int jdbcType) { - ClickHouseDataType dataType; - - switch (jdbcType) { - case Types.BIT: - case Types.BOOLEAN: - dataType = ClickHouseDataType.UInt8; - break; - case Types.TINYINT: - dataType = ClickHouseDataType.Int8; - break; - case Types.SMALLINT: - dataType = ClickHouseDataType.Int16; - break; - case Types.INTEGER: - dataType = ClickHouseDataType.Int32; - break; - case Types.BIGINT: - dataType = ClickHouseDataType.Int64; - break; - case Types.NUMERIC: - dataType = ClickHouseDataType.Int256; - break; - case Types.FLOAT: - case Types.REAL: - dataType = ClickHouseDataType.Float32; - break; - case Types.DOUBLE: - dataType = ClickHouseDataType.Float64; - break; - case Types.DECIMAL: - dataType = ClickHouseDataType.Decimal; - break; - case Types.BLOB: - case Types.BINARY: - case Types.CHAR: - case Types.CLOB: - case Types.JAVA_OBJECT: - case Types.LONGNVARCHAR: - case Types.LONGVARBINARY: - case Types.LONGVARCHAR: - case Types.NCHAR: - case Types.NCLOB: - case Types.NVARCHAR: - case Types.OTHER: - case Types.SQLXML: - case Types.VARBINARY: - case Types.VARCHAR: - dataType = ClickHouseDataType.String; - break; - case Types.DATE: - dataType = ClickHouseDataType.Date; - break; - case Types.TIME: - case Types.TIME_WITH_TIMEZONE: - case Types.TIMESTAMP: - case Types.TIMESTAMP_WITH_TIMEZONE: - dataType = ClickHouseDataType.DateTime; - break; - case Types.ARRAY: - dataType = ClickHouseDataType.Array; - break; - case Types.STRUCT: - dataType = ClickHouseDataType.Nested; - break; - case Types.DATALINK: - case Types.DISTINCT: - case Types.REF: - case Types.REF_CURSOR: - case Types.ROWID: - case Types.NULL: - default: - dataType = ClickHouseDataType.Nothing; - break; - } - return dataType; - } - - private JdbcTypeMapping() { - } } diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseParameterMetaData.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseParameterMetaData.java index dbf41af10..b0aa2d499 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseParameterMetaData.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseParameterMetaData.java @@ -4,6 +4,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.List; +import java.util.Map; import com.clickhouse.client.ClickHouseChecker; import com.clickhouse.client.ClickHouseColumn; @@ -14,9 +15,15 @@ public class ClickHouseParameterMetaData extends JdbcWrapper implements ParameterMetaData { protected final List params; + protected final JdbcTypeMapping mapper; + protected final Map> typeMap; - protected ClickHouseParameterMetaData(List params) { + protected ClickHouseParameterMetaData(List params, JdbcTypeMapping mapper, + Map> typeMap) { this.params = ClickHouseChecker.nonNull(params, "Parameters"); + + this.mapper = mapper; + this.typeMap = typeMap; } protected ClickHouseColumn getParameter(int param) throws SQLException { @@ -64,19 +71,19 @@ public int getScale(int param) throws SQLException { @Override public int getParameterType(int param) throws SQLException { ClickHouseColumn p = getParameter(param); - return p != null ? JdbcTypeMapping.toJdbcType(null, p) : Types.OTHER; + return p != null ? mapper.toSqlType(p, typeMap) : Types.OTHER; } @Override public String getParameterTypeName(int param) throws SQLException { ClickHouseColumn p = getParameter(param); - return p != null ? p.getOriginalTypeName() : ""; + return p != null ? mapper.toNativeType(p) : ""; } @Override public String getParameterClassName(int param) throws SQLException { ClickHouseColumn p = getParameter(param); - return (p != null ? p.getObjectClass() : Object.class).getName(); + return (p != null ? mapper.toJavaClass(p, typeMap) : Object.class).getName(); } @Override diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseStatementImpl.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseStatementImpl.java index 343a0d816..8bcea8735 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseStatementImpl.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseStatementImpl.java @@ -38,6 +38,7 @@ import com.clickhouse.jdbc.ClickHouseConnection; import com.clickhouse.jdbc.ClickHouseResultSet; import com.clickhouse.jdbc.ClickHouseStatement; +import com.clickhouse.jdbc.JdbcTypeMapping; import com.clickhouse.jdbc.SqlExceptionUtils; import com.clickhouse.jdbc.JdbcWrapper; import com.clickhouse.jdbc.parser.ClickHouseSqlStatement; @@ -72,6 +73,8 @@ public class ClickHouseStatementImpl extends JdbcWrapper private ClickHouseResultSet currentResult; private long currentUpdateCount; + protected final JdbcTypeMapping mapper; + protected ClickHouseSqlStatement[] parsedStmts; protected ClickHouseDeserializer deserializer; protected ClickHouseSerializer serializer; @@ -149,7 +152,7 @@ protected ClickHouseResponse executeStatement(String stmt, Map(); ClickHouseConfig c = request.getConfig(); @@ -279,7 +284,8 @@ public void optionChanged(ClickHouseRequest source, ClickHouseOption option, if (option == ClickHouseClientOption.FORMAT) { this.deserializer = ClickHouseDataStreamFactory.getInstance().getDeserializer(request.getFormat()); - this.serializer = ClickHouseDataStreamFactory.getInstance().getSerializer(request.getInputFormat()); + this.serializer = ClickHouseDataStreamFactory.getInstance() + .getSerializer(request.getFormat().defaultInputFormat()); } } @@ -604,7 +610,7 @@ public long[] executeLargeBatch() throws SQLException { int i = 0; for (ClickHouseSqlStatement s : batchStmts) { try (ClickHouseResponse r = executeStatement(s, null, null, null); ResultSet rs = updateResult(s, r)) { - if (currentResult != null) { + if (rs != null) { throw SqlExceptionUtils.queryInBatchError(results); } results[i] = currentUpdateCount <= 0L ? 0L : currentUpdateCount; diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/InputBasedPreparedStatement.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/InputBasedPreparedStatement.java index 14e5d261a..f4663f149 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/InputBasedPreparedStatement.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/InputBasedPreparedStatement.java @@ -72,7 +72,8 @@ protected InputBasedPreparedStatement(ClickHouseConnectionImpl connection, Click list.add(col); i++; } - paramMetaData = new ClickHouseParameterMetaData(Collections.unmodifiableList(list)); + paramMetaData = new ClickHouseParameterMetaData(Collections.unmodifiableList(list), mapper, + connection.getTypeMap()); flags = new boolean[size]; counter = 0; diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/SqlBasedPreparedStatement.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/SqlBasedPreparedStatement.java index 45eb10968..f98d92dba 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/SqlBasedPreparedStatement.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/SqlBasedPreparedStatement.java @@ -37,7 +37,6 @@ import com.clickhouse.client.logging.LoggerFactory; import com.clickhouse.jdbc.ClickHousePreparedStatement; import com.clickhouse.jdbc.JdbcParameterizedQuery; -import com.clickhouse.jdbc.JdbcTypeMapping; import com.clickhouse.jdbc.SqlExceptionUtils; import com.clickhouse.jdbc.parser.ClickHouseSqlStatement; @@ -103,7 +102,8 @@ protected SqlBasedPreparedStatement(ClickHouseConnectionImpl connection, ClickHo for (int i = 1; i <= tlen; i++) { list.add(ClickHouseColumn.of("parameter" + i, ClickHouseDataType.JSON, true)); } - paramMetaData = new ClickHouseParameterMetaData(Collections.unmodifiableList(list)); + paramMetaData = new ClickHouseParameterMetaData(Collections.unmodifiableList(list), mapper, + connection.getTypeMap()); batch = new LinkedList<>(); builder = new StringBuilder(); if ((insertValuesQuery = prefix) != null) { @@ -148,8 +148,7 @@ protected long[] executeAny(boolean asBatch) throws SQLException { long rows = 0L; try { r = executeStatement(builder.toString(), null, null, null); - updateResult(parsedStmt, r); - if (asBatch && getResultSet() != null) { + if (updateResult(parsedStmt, r) != null && asBatch) { throw SqlExceptionUtils.queryInBatchError(results); } rows = r.getSummary().getWrittenRows(); @@ -196,8 +195,7 @@ protected long[] executeAny(boolean asBatch) throws SQLException { preparedQuery.apply(builder, params); try { r = executeStatement(builder.toString(), null, null, null); - updateResult(parsedStmt, r); - if (asBatch && getResultSet() != null) { + if (updateResult(parsedStmt, r) != null && asBatch) { throw SqlExceptionUtils.queryInBatchError(results); } int count = getUpdateCount(); @@ -598,7 +596,7 @@ public void setObject(int parameterIndex, Object x, int targetSqlType, int scale int idx = toArrayIndex(parameterIndex); ClickHouseValue value = templates[idx]; if (value == null) { - value = ClickHouseValues.newValue(getConfig(), JdbcTypeMapping.fromJdbcType(targetSqlType, scaleOrLength)); + value = ClickHouseValues.newValue(getConfig(), mapper.toColumn(targetSqlType, scaleOrLength)); templates[idx] = value; } diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/TableBasedPreparedStatement.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/TableBasedPreparedStatement.java index e3ef390f2..55accbfb7 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/TableBasedPreparedStatement.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/TableBasedPreparedStatement.java @@ -60,7 +60,8 @@ protected TableBasedPreparedStatement(ClickHouseConnectionImpl connection, Click for (String name : set) { list.add(ClickHouseColumn.of(name, ClickHouseDataType.JSON, false)); } - paramMetaData = new ClickHouseParameterMetaData(Collections.unmodifiableList(list)); + paramMetaData = new ClickHouseParameterMetaData(Collections.unmodifiableList(list), mapper, + connection.getTypeMap()); batch = new LinkedList<>(); } @@ -100,7 +101,7 @@ public long[] executeAny(boolean asBatch) throws SQLException { for (List list : batch) { try (ClickHouseResponse r = executeStatement(sql, null, list, null); ResultSet rs = updateResult(parsedStmt, r)) { - if (asBatch && getResultSet() != null) { + if (asBatch && rs != null) { throw SqlExceptionUtils.queryInBatchError(results); } long rows = getLargeUpdateCount(); @@ -234,8 +235,7 @@ public boolean execute() throws SQLException { } ClickHouseSqlStatement stmt = new ClickHouseSqlStatement(getSql()); - updateResult(parsedStmt, executeStatement(stmt, null, Arrays.asList(values), null)); - return getResultSet() != null; + return updateResult(parsedStmt, executeStatement(stmt, null, Arrays.asList(values), null)) != null; } @Override @@ -243,11 +243,7 @@ public void addBatch() throws SQLException { ensureOpen(); ensureParams(); - List list = new ArrayList<>(values.length); - for (ClickHouseExternalTable v : values) { - list.add(v); - } - batch.add(Collections.unmodifiableList(list)); + batch.add(Collections.unmodifiableList(Arrays.asList(values))); clearParameters(); } diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHousePreparedStatementTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHousePreparedStatementTest.java index 9fd4b6054..23f01605b 100644 --- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHousePreparedStatementTest.java +++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHousePreparedStatementTest.java @@ -1541,7 +1541,7 @@ public void testGetParameterMetaData() throws SQLException { "parameter mete data should be singleton"); Assert.assertEquals(ps.getParameterMetaData().getParameterCount(), 3); Assert.assertEquals(ps.getParameterMetaData().getParameterMode(3), ParameterMetaData.parameterModeIn); - Assert.assertEquals(ps.getParameterMetaData().getParameterType(3), Types.OTHER); + Assert.assertEquals(ps.getParameterMetaData().getParameterType(3), Types.VARCHAR); Assert.assertEquals(ps.getParameterMetaData().getPrecision(3), 0); Assert.assertEquals(ps.getParameterMetaData().getScale(3), 0); Assert.assertEquals(ps.getParameterMetaData().getParameterClassName(3), Object.class.getName()); diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseStatementTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseStatementTest.java index 71ebefd20..b2ce89071 100644 --- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseStatementTest.java +++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseStatementTest.java @@ -61,6 +61,30 @@ private Object[][] getConnectionProperties() { new Object[] { emptyProps }, new Object[] { sessionProps } }; } + @Test(groups = "integration") + public void testDialect() throws SQLException { + Properties props = new Properties(); + String sql = "select cast(1 as UInt64) a, cast([1, 2] as Array(Int8)) b"; + try (ClickHouseConnection conn = newConnection(props); + ClickHouseStatement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql);) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getMetaData().getColumnTypeName(1), ClickHouseDataType.UInt64.name()); + Assert.assertEquals(rs.getMetaData().getColumnTypeName(2), "Array(Int8)"); + Assert.assertFalse(rs.next()); + } + + props.setProperty(JdbcConfig.PROP_DIALECT, "ansi"); + try (ClickHouseConnection conn = newConnection(props); + ClickHouseStatement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql);) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getMetaData().getColumnTypeName(1), "DECIMAL(20,0)"); + Assert.assertEquals(rs.getMetaData().getColumnTypeName(2), "ARRAY(BYTE)"); + Assert.assertFalse(rs.next()); + } + } + @Test(groups = "integration") public void testJdbcEscapeSyntax() throws SQLException { try (ClickHouseConnection conn = newConnection(new Properties());