diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java index c2a8a1ef0..601e479ca 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java @@ -32,6 +32,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TimeZone; import java.util.UUID; @@ -667,7 +668,7 @@ public static class ArrayValue { int nextPos = 0; - ArrayValue(Class itemType, int length) { + public ArrayValue(Class itemType, int length) { this.itemType = itemType; this.length = length; @@ -721,6 +722,34 @@ public synchronized List asList() { } return (List) list; } + + /** + * Returns internal array. This method is only useful to work with array of primitives (int[], boolean[]). + * Otherwise use {@link #getArrayOfObjects()} + * + * @return + */ + public Object getArray() { + return array; + } + + /** + * Returns array of objects. + * If item type is primitive then all elements will be converted into objects. + * + * @return + */ + public Object[] getArrayOfObjects() { + if (itemType.isPrimitive()) { + Object[] result = new Object[length]; + for (int i = 0; i < length; i++) { + result[i] = Array.get(array, i); + } + return result; + } else { + return (Object[]) array; + } + } } public static class EnumValue extends Number { diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java index 81c1f32c5..6f22eb24b 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java @@ -3,9 +3,11 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.lang.reflect.Array; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.TimeZone; import org.testng.Assert; @@ -176,4 +178,16 @@ private Object[][] provideDateTimeTestData() { }; } + @Test + public void testArrayValue() throws Exception { + BinaryStreamReader.ArrayValue array = new BinaryStreamReader.ArrayValue(int.class, 10); + + for (int i = 0; i < array.length(); i++) { + array.set(i, i); + } + + int[] array1 = (int[]) array.getArray(); + Object[] array2 = array.getArrayOfObjects(); + Assert.assertEquals(array1.length, array2.length); + } } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java index efcb8b7e2..40d45cbf4 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -6,8 +6,10 @@ import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.client.api.query.GenericRecord; import com.clickhouse.client.api.query.QuerySettings; +import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.jdbc.internal.ExceptionUtils; +import com.clickhouse.jdbc.internal.FeatureManager; import com.clickhouse.jdbc.internal.JdbcConfiguration; import com.clickhouse.jdbc.internal.JdbcUtils; import com.clickhouse.jdbc.internal.ParsedPreparedStatement; @@ -22,6 +24,7 @@ import java.sql.Clob; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.JDBCType; import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -34,16 +37,13 @@ import java.sql.Statement; import java.sql.Struct; import java.time.Duration; -import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.Executor; -import java.util.stream.Collectors; public class ConnectionImpl implements Connection, JdbcV2Wrapper { private static final Logger log = LoggerFactory.getLogger(ConnectionImpl.class); @@ -59,12 +59,16 @@ public class ConnectionImpl implements Connection, JdbcV2Wrapper { private String schema; private String appName; private QuerySettings defaultQuerySettings; + private boolean readOnly; + private int holdability; private final DatabaseMetaDataImpl metadata; protected final Calendar defaultCalendar; private final SqlParser sqlParser; + private final FeatureManager featureManager; + public ConnectionImpl(String url, Properties info) throws SQLException { try { this.url = url;//Raw URL @@ -72,6 +76,8 @@ public ConnectionImpl(String url, Properties info) throws SQLException { this.onCluster = false; this.cluster = null; this.appName = ""; + this.readOnly = false; + this.holdability = ResultSet.HOLD_CURSORS_OVER_COMMIT; String clientName = "ClickHouse JDBC Driver V2/" + Driver.driverVersion; Map clientProperties = config.getClientProperties(); @@ -111,6 +117,7 @@ public ConnectionImpl(String url, Properties info) throws SQLException { this.defaultCalendar = Calendar.getInstance(); this.sqlParser = new SqlParser(); + this.featureManager = new FeatureManager(this.config); } catch (SQLException e) { throw e; } catch (Exception e) { @@ -161,10 +168,7 @@ public PreparedStatement prepareStatement(String sql) throws SQLException { @Override public CallableStatement prepareCall(String sql) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("CallableStatement not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("prepareCall(String sql)"); return null; } @@ -178,10 +182,7 @@ public String nativeSQL(String sql) throws SQLException { @Override public void setAutoCommit(boolean autoCommit) throws SQLException { ensureOpen(); - - if (!config.isIgnoreUnsupportedRequests() && !autoCommit) { - throw new SQLFeatureNotSupportedException("setAutoCommit = false not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("setAutoCommit(false)", !autoCommit); } @Override @@ -192,16 +193,12 @@ public boolean getAutoCommit() throws SQLException { @Override public void commit() throws SQLException { - if (!config.isIgnoreUnsupportedRequests() ) { - throw new SQLFeatureNotSupportedException("Commit/Rollback not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("commit()"); } @Override public void rollback() throws SQLException { - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Commit/Rollback not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("rollback()"); } @Override @@ -228,15 +225,16 @@ public DatabaseMetaData getMetaData() throws SQLException { @Override public void setReadOnly(boolean readOnly) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests() && readOnly) { - throw new SQLFeatureNotSupportedException("read-only=true unsupported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + // This method is just a hint for the driver. Documentation doesn't tell to block update operations. + // Currently, we do not use this hint but some connection pools may use this property. + // So we just save and return + this.readOnly = readOnly; } @Override public boolean isReadOnly() throws SQLException { ensureOpen(); - return false; + return readOnly; } @Override @@ -253,9 +251,7 @@ public String getCatalog() throws SQLException { @Override public void setTransactionIsolation(int level) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests() && TRANSACTION_NONE != level) { - throw new SQLFeatureNotSupportedException("setTransactionIsolation not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("setTransactionIsolation(TRANSACTION_NONE)", TRANSACTION_NONE != level); } @Override @@ -278,89 +274,77 @@ public void clearWarnings() throws SQLException { @Override public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { ensureOpen(); - return createStatement(resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT); + return createStatement(resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT); } @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { ensureOpen(); - return prepareStatement(sql, resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT); + return prepareStatement(sql, resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT); } @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("CallableStatement not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("prepareCall(String sql, int resultSetType, int resultSetConcurrency)"); return null; } @Override public Map> getTypeMap() throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("getTypeMap not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - - return null; + featureManager.unsupportedFeatureThrow("getTypeMap()"); + return Collections.emptyMap(); } @Override public void setTypeMap(Map> map) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("setTypeMap not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("setTypeMap(Map>)"); } @Override public void setHoldability(int holdability) throws SQLException { ensureOpen(); - //TODO: Should this be supported? + if (holdability != ResultSet.HOLD_CURSORS_OVER_COMMIT && holdability != ResultSet.CLOSE_CURSORS_AT_COMMIT) { + throw new SQLException("Only ResultSet.HOLD_CURSORS_OVER_COMMIT and ResultSet.CLOSE_CURSORS_AT_COMMIT allowed for holdability"); + } + // we do not support transactions and almost always use auto-commit. + // holdability regulates is result set is open or closed on commit. + // currently we ignore value and always set what we support. + this.holdability = ResultSet.HOLD_CURSORS_OVER_COMMIT; } @Override public int getHoldability() throws SQLException { ensureOpen(); - return ResultSet.HOLD_CURSORS_OVER_COMMIT;//TODO: Check if this is correct + return holdability; } @Override public Savepoint setSavepoint() throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Savepoint not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("setSavepoint()"); return null; } @Override public Savepoint setSavepoint(String name) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Savepoint not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("setSavepoint(String name)"); return null; } @Override public void rollback(Savepoint savepoint) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Commit/Rollback not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("rollback(Savepoint savepoint)"); } @Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Savepoint not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("releaseSavepoint(Savepoint savepoint)"); } @Override @@ -416,10 +400,7 @@ public PreparedStatement prepareStatement(String sql, int resultSetType, int res @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("CallableStatement not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability)"); return null; } @@ -459,9 +440,7 @@ public PreparedStatement prepareStatement(String sql, String[] columnNames) thro @Override public Clob createClob() throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Clob not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("createClob()"); return null; } @@ -469,30 +448,21 @@ public Clob createClob() throws SQLException { @Override public Blob createBlob() throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Blob not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("createBlob()"); return null; } @Override public NClob createNClob() throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("NClob not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("createNClob()"); return null; } @Override public SQLXML createSQLXML() throws SQLException { ensureOpen(); - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("SQLXML not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("createSQLXML()"); return null; } @@ -562,10 +532,22 @@ public Properties getClientInfo() throws SQLException { @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + ensureOpen(); + if (typeName == null) { + throw new SQLFeatureNotSupportedException("typeName cannot be null"); + } + + + int parentPos = typeName.indexOf('('); + int endPos = parentPos == -1 ? typeName.length() : parentPos; + String clickhouseDataTypeName = (typeName.substring(0, endPos)).trim(); + ClickHouseDataType dataType = ClickHouseDataType.valueOf(clickhouseDataTypeName); + if (dataType.equals(ClickHouseDataType.Array)) { + throw new SQLException("Array cannot be a base type. In case of nested array provide most deep element type name."); + } try { - List list = - (elements == null || elements.length == 0) ? Collections.emptyList() : Arrays.stream(elements, 0, elements.length).collect(Collectors.toList()); - return new com.clickhouse.jdbc.types.Array(list, typeName, JdbcUtils.convertToSqlType(ClickHouseDataType.valueOf(typeName)).getVendorTypeNumber()); + return new com.clickhouse.jdbc.types.Array(elements, typeName, + JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(dataType, JDBCType.OTHER).getVendorTypeNumber()); } catch (Exception e) { throw new SQLException("Failed to create array", ExceptionUtils.SQL_STATE_CLIENT_ERROR, e); } @@ -573,14 +555,19 @@ public Array createArrayOf(String typeName, Object[] elements) throws SQLExcepti @Override public Struct createStruct(String typeName, Object[] attributes) throws SQLException { - //TODO: Should this be supported? - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("createStruct not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); + ensureOpen(); + if (typeName == null) { + throw new SQLFeatureNotSupportedException("typeName cannot be null"); + } + ClickHouseColumn column = ClickHouseColumn.of("v", typeName); + if (column.getDataType().equals(ClickHouseDataType.Tuple)) { + return new com.clickhouse.jdbc.types.Struct(column, attributes); + } else { + throw new SQLException("Only Tuple datatype is supported for Struct", ExceptionUtils.SQL_STATE_CLIENT_ERROR); } - - return null; } + @Override public void setSchema(String schema) throws SQLException { ensureOpen(); @@ -596,26 +583,19 @@ public String getSchema() throws SQLException { @Override public void abort(Executor executor) throws SQLException { - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("abort not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("abort()"); } @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { //TODO: Should this be supported? - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("setNetworkTimeout not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } + featureManager.unsupportedFeatureThrow("setNetworkTimeout()"); } @Override public int getNetworkTimeout() throws SQLException { //TODO: Should this be supported? - if (!config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("getNetworkTimeout not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); - } - + featureManager.unsupportedFeatureThrow("getNetworkTimeout()"); return -1; } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java index ee76c143c..3ded15e92 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -39,6 +39,7 @@ import java.sql.SQLType; import java.sql.SQLXML; import java.sql.Statement; +import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; import java.time.Instant; @@ -749,32 +750,37 @@ public final int executeUpdate(String sql, String[] columnNames) throws SQLExcep "executeUpdate(String, String[]) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); } - private static String encodeObject(Object x) throws SQLException { + private String encodeObject(Object x) throws SQLException { return encodeObject(x, null); } + + private static final char QUOTE = '\''; - private static String encodeObject(Object x, Long length) throws SQLException { + private static final char O_BRACKET = '['; + private static final char C_BRACKET = ']'; + + private String encodeObject(Object x, Long length) throws SQLException { LOG.trace("Encoding object: {}", x); try { if (x == null) { return "NULL"; } else if (x instanceof String) { - return "'" + SQLUtils.escapeSingleQuotes((String) x) + "'"; + return QUOTE + SQLUtils.escapeSingleQuotes((String) x) + QUOTE; } else if (x instanceof Boolean) { return (Boolean) x ? "1" : "0"; } else if (x instanceof Date) { - return "'" + DataTypeUtils.DATE_FORMATTER.format(((Date) x).toLocalDate()) + "'"; + return QUOTE + DataTypeUtils.DATE_FORMATTER.format(((Date) x).toLocalDate()) + QUOTE; } else if (x instanceof LocalDate) { - return "'" + DataTypeUtils.DATE_FORMATTER.format((LocalDate) x) + "'"; + return QUOTE + DataTypeUtils.DATE_FORMATTER.format((LocalDate) x) + QUOTE; } else if (x instanceof Time) { - return "'" + TIME_FORMATTER.format(((Time) x).toLocalTime()) + "'"; + return QUOTE + TIME_FORMATTER.format(((Time) x).toLocalTime()) + QUOTE; } else if (x instanceof LocalTime) { - return "'" + TIME_FORMATTER.format((LocalTime) x) + "'"; + return QUOTE + TIME_FORMATTER.format((LocalTime) x) + QUOTE; } else if (x instanceof Timestamp) { - return "'" + DATETIME_FORMATTER.format(((Timestamp) x).toLocalDateTime()) + "'"; + return QUOTE + DATETIME_FORMATTER.format(((Timestamp) x).toLocalDateTime()) + QUOTE; } else if (x instanceof LocalDateTime) { - return "'" + DATETIME_FORMATTER.format((LocalDateTime) x) + "'"; + return QUOTE + DATETIME_FORMATTER.format((LocalDateTime) x) + QUOTE; } else if (x instanceof OffsetDateTime) { return encodeObject(((OffsetDateTime) x).toInstant()); } else if (x instanceof ZonedDateTime) { @@ -782,106 +788,74 @@ private static String encodeObject(Object x, Long length) throws SQLException { } else if (x instanceof Instant) { return "fromUnixTimestamp64Nano(" + (((Instant) x).getEpochSecond() * 1_000_000_000L + ((Instant) x).getNano()) + ")"; } else if (x instanceof InetAddress) { - return "'" + ((InetAddress) x).getHostAddress() + "'"; - } else if (x instanceof Array) { + return QUOTE + ((InetAddress) x).getHostAddress() + QUOTE; + } else if (x instanceof java.sql.Array) { StringBuilder listString = new StringBuilder(); - listString.append("["); - int i = 0; - for (Object item : (Object[]) ((Array) x).getArray()) { - if (i > 0) { - listString.append(", "); - } - listString.append(encodeObject(item)); - i++; - } - listString.append("]"); + listString.append(O_BRACKET); + appendArrayElements((Object[]) ((Array) x).getArray(), listString); + listString.append(C_BRACKET); return listString.toString(); + } else if (x instanceof Object[]) { + StringBuilder arrayString = new StringBuilder(); + arrayString.append(O_BRACKET); + appendArrayElements((Object[]) x, arrayString); + arrayString.append(C_BRACKET); + return arrayString.toString(); } else if (x.getClass().isArray()) { StringBuilder listString = new StringBuilder(); - listString.append("["); - - + listString.append(O_BRACKET); if (x.getClass().getComponentType().isPrimitive()) { int len = java.lang.reflect.Array.getLength(x); for (int i = 0; i < len; i++) { - if (i > 0) { - listString.append(", "); - } - listString.append(encodeObject(java.lang.reflect.Array.get(x, i))); + listString.append(encodeObject(java.lang.reflect.Array.get(x, i))).append(','); } - } else { - int i = 0; - for (Object item : (Object[]) x) { - if (i > 0) { - listString.append(", "); - } - listString.append(encodeObject(item)); - i++; + if (len > 0) { + listString.setLength(listString.length() - 1); } + } else { + appendArrayElements((Object[]) x, listString); } - listString.append("]"); + listString.append(C_BRACKET); return listString.toString(); } else if (x instanceof Collection) { StringBuilder listString = new StringBuilder(); - listString.append("["); - for (Object item : (Collection) x) { - listString.append(encodeObject(item)).append(", "); + listString.append(O_BRACKET); + Collection collection = (Collection) x; + for (Object item : collection) { + listString.append(encodeObject(item)).append(','); } - if (listString.length() > 1) { - listString.delete(listString.length() - 2, listString.length()); + if (!collection.isEmpty()) { + listString.setLength(listString.length() - 1); } - listString.append("]"); + listString.append(C_BRACKET); return listString.toString(); } else if (x instanceof Map) { Map tmpMap = (Map) x; StringBuilder mapString = new StringBuilder(); - mapString.append("{"); + mapString.append('{'); for (Object key : tmpMap.keySet()) { - mapString.append(encodeObject(key)).append(": ").append(encodeObject(tmpMap.get(key))).append(", "); + mapString.append(encodeObject(key)).append(": ").append(encodeObject(tmpMap.get(key))).append(','); } - if (!tmpMap.isEmpty()) - mapString.delete(mapString.length() - 2, mapString.length()); - mapString.append("}"); + if (!tmpMap.isEmpty()) { + mapString.setLength(mapString.length() - 1); + } + + mapString.append('}'); return mapString.toString(); } else if (x instanceof Reader) { return encodeCharacterStream((Reader) x, length); } else if (x instanceof InputStream) { return encodeCharacterStream((InputStream) x, length); - } else if (x instanceof Object[]) { - StringBuilder arrayString = new StringBuilder(); - arrayString.append("["); - int i = 0; - for (Object item : (Object[]) x) { - if (i > 0) { - arrayString.append(", "); - } - arrayString.append(encodeObject(item)); - i++; - } - arrayString.append("]"); - - return arrayString.toString(); } else if (x instanceof Tuple) { - StringBuilder tupleString = new StringBuilder(); - tupleString.append("("); - Tuple t = (Tuple) x; - Object [] values = t.getValues(); - int i = 0; - for (Object item : values) { - if (i > 0) { - tupleString.append(", "); - } - tupleString.append(encodeObject(item)); - i++; - } - tupleString.append(")"); - return tupleString.toString(); + return arrayToTuple(((Tuple)x).getValues()); + } else if (x instanceof Struct) { + return arrayToTuple(((Struct)x).getAttributes()); } else if (x instanceof UUID) { - return "'" + ((UUID) x).toString() + "'"; + return QUOTE + ((UUID) x).toString() + QUOTE; } return SQLUtils.escapeSingleQuotes(x.toString()); //Escape single quotes @@ -891,6 +865,23 @@ private static String encodeObject(Object x, Long length) throws SQLException { } } + private void appendArrayElements(Object[] array, StringBuilder sb) throws SQLException { + for (Object item : array) { + sb.append(encodeObject(item)).append(','); + } + if (array.length > 0) { + sb.setLength(sb.length() - 1); + } + } + + private String arrayToTuple(Object[] array) throws SQLException { + StringBuilder tupleString = new StringBuilder(); + tupleString.append('('); + appendArrayElements(array, tupleString); + tupleString.append(')'); + return tupleString.toString(); + } + private static String encodeCharacterStream(InputStream stream, Long length) throws SQLException { return encodeCharacterStream(new InputStreamReader(stream, StandardCharsets.UTF_8), length); } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/FeatureManager.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/FeatureManager.java index 25670c41e..68d21f6eb 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/FeatureManager.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/FeatureManager.java @@ -11,10 +11,14 @@ public FeatureManager(JdbcConfiguration configuration) { this.configuration = configuration; } - public void unsupportedFeatureThrow(String methodName) throws SQLException { - if (!configuration.isIgnoreUnsupportedRequests()) { + public void unsupportedFeatureThrow(String featureName) throws SQLException { + unsupportedFeatureThrow(featureName, true); + } + + public void unsupportedFeatureThrow(String methodName, boolean doCheck) throws SQLException { + if (doCheck && !configuration.isIgnoreUnsupportedRequests()) { throw new SQLFeatureNotSupportedException(methodName + " is not supported.", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); } } -} +} \ No newline at end of file diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java index 49f6d7515..73de6bbfc 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java @@ -61,7 +61,7 @@ public boolean isIgnoreUnsupportedRequests() { * @param info - Driver and Client properties. */ public JdbcConfiguration(String url, Properties info) throws SQLException { - this.disableFrameworkDetection = Boolean.parseBoolean(info.getProperty("disable_frameworks_detection", "false")); + this.disableFrameworkDetection = info != null && Boolean.parseBoolean(info.getProperty("disable_frameworks_detection", "false")); this.clientProperties = new HashMap<>(); this.driverProperties = new HashMap<>(); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index 9752d98ab..39403376f 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -1,9 +1,11 @@ package com.clickhouse.jdbc.internal; import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; +import com.clickhouse.client.api.data_formats.internal.InetAddressConverter; import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.data.Tuple; +import com.clickhouse.data.format.BinaryStreamUtils; import com.clickhouse.jdbc.types.Array; import com.google.common.collect.ImmutableMap; @@ -11,6 +13,7 @@ import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; +import java.net.InetAddress; import java.sql.Date; import java.sql.JDBCType; import java.sql.SQLException; @@ -226,6 +229,8 @@ public static Object convert(Object value, Class type, ClickHouseColumn colum try { if (type.isInstance(value)) { return value; + } else if (type != java.sql.Array.class && value instanceof List) { + return convertList((List) value, type); } else if (type == String.class) { return value.toString(); } else if (type == Boolean.class || type == boolean.class) { @@ -261,39 +266,60 @@ public static Object convert(Object value, Class type, ClickHouseColumn colum } else if (type == java.sql.Time.class && value instanceof TemporalAccessor) { return java.sql.Time.valueOf(LocalTime.from((TemporalAccessor) value)); } else if (type == java.sql.Array.class && value instanceof BinaryStreamReader.ArrayValue) {//It's cleaner to use getList but this handles the more generic getObject + BinaryStreamReader.ArrayValue arrayValue = (BinaryStreamReader.ArrayValue) value; if (column != null && column.getArrayBaseColumn() != null) { - return new Array(convertList(((BinaryStreamReader.ArrayValue) value).asList(), JdbcUtils.convertToJavaClass(column.getArrayBaseColumn().getDataType())), "Object", JDBCType.JAVA_OBJECT.getVendorTypeNumber()); + ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType(); + Object[] convertedValues = convertArray(arrayValue.getArrayOfObjects(), + JdbcUtils.convertToJavaClass(baseType)); + return new Array(convertedValues, baseType.getName(), baseType.getVendorTypeNumber()); } - return new Array(((BinaryStreamReader.ArrayValue) value).asList(), "Object", JDBCType.JAVA_OBJECT.getVendorTypeNumber()); + return new Array(arrayValue.getArrayOfObjects(), "Unknown", JDBCType.OTHER.getVendorTypeNumber()); } else if (type == java.sql.Array.class && value instanceof List) { if (column != null && column.getArrayBaseColumn() != null) { - return new Array(convertList(((List) value), JdbcUtils.convertToJavaClass(column.getArrayBaseColumn().getDataType())), "Object", JDBCType.JAVA_OBJECT.getVendorTypeNumber()); + ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType(); + return new Array(convertList((List) value, JdbcUtils.convertToJavaClass(baseType)), + baseType.getName(), JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(baseType, JDBCType.OTHER).getVendorTypeNumber()); } - return new Array((List) value, "Object", JDBCType.JAVA_OBJECT.getVendorTypeNumber()); + // base type is unknown. all objects should be converted + return new Array(((List) value).toArray(), "Unknown", JDBCType.OTHER.getVendorTypeNumber()); } else if (type == Inet4Address.class && value instanceof Inet6Address) { // Convert Inet6Address to Inet4Address - return Inet4Address.getByName(value.toString()); + return InetAddressConverter.convertToIpv4((InetAddress) value); } else if (type == Inet6Address.class && value instanceof Inet4Address) { // Convert Inet4Address to Inet6Address - return Inet6Address.getByName(value.toString()); + return InetAddressConverter.convertToIpv6((InetAddress) value); } else if (type == Tuple.class && value.getClass().isArray()) { return new Tuple(true, value); } } catch (Exception e) { - throw new SQLException("Failed to convert " + value + " to " + type.getName(), ExceptionUtils.SQL_STATE_DATA_EXCEPTION); + throw new SQLException("Failed to convert from " + value.getClass().getName() + " to " + type.getName(), ExceptionUtils.SQL_STATE_DATA_EXCEPTION, e); } throw new SQLException("Unsupported conversion from " + value.getClass().getName() + " to " + type.getName(), ExceptionUtils.SQL_STATE_DATA_EXCEPTION); } - public static List convertList(List values, Class type) throws SQLException { + public static Object[] convertList(List values, Class type) throws SQLException { + if (values == null) { + return null; + } + if (values.isEmpty()) { + return new Object[0]; + } + + Object[] convertedValues = new Object[values.size()]; + for (int i = 0; i < values.size(); i++) { + convertedValues[i] = convert(values.get(i), type); + } + return convertedValues; + } + + public static Object[] convertArray(Object[] values, Class type) throws SQLException { if (values == null || type == null) { return values; } - - List convertedValues = new ArrayList<>(values.size()); - for (Object value : values) { - convertedValues.add(convert(value, type)); + Object[] convertedValues = new Object[values.length]; + for (int i = 0; i < values.length; i++) { + convertedValues[i] = convert(values[i], type); } return convertedValues; } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java index e463a8fb5..c3bfa9541 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java @@ -12,32 +12,47 @@ public class Array implements java.sql.Array { private static final Logger log = LoggerFactory.getLogger(Array.class); - Object[] array; - int type; //java.sql.Types - String typeName; - public Array(List list, String itemTypeName, int itemType) throws SQLException { - if (list == null) { - throw ExceptionUtils.toSqlState(new IllegalArgumentException("List cannot be null")); - } + private Object[] array; + private final int type; //java.sql.Types + private final String elementTypeName; + private boolean valid; + + /** + * @deprecated this constructor should not be used. Elements array should be constructed externally. + */ + public Array(List list, String elementTypeName, int itemType) throws SQLException { + this(list.toArray(), elementTypeName, itemType); + } - this.array = list.toArray(); + public Array(Object[] elements, String elementTypeName, int itemType) throws SQLException { + if (elements == null) { + throw ExceptionUtils.toSqlState(new IllegalArgumentException("Array cannot be null")); + } + if (elementTypeName == null) { + throw ExceptionUtils.toSqlState(new IllegalArgumentException("Array element type name cannot be null")); + } + this.array = elements; this.type = itemType; - this.typeName = itemTypeName; + this.elementTypeName = elementTypeName; + this.valid = true; } @Override public String getBaseTypeName() throws SQLException { - return typeName; + ensureValid(); + return elementTypeName; } @Override public int getBaseType() throws SQLException { + ensureValid(); return type; } @Override public Object getArray() throws SQLException { + ensureValid(); return array; } @@ -48,14 +63,20 @@ public Object getArray(Map> map) throws SQLException { @Override public Object getArray(long index, int count) throws SQLException { - try { - Object[] smallerArray = new Object[count]; - System.arraycopy(array, (int) index, smallerArray, 0, count); - return smallerArray; - } catch (Exception e) { - log.error("Failed to get array", e); - throw new SQLException(e.getMessage(), ExceptionUtils.SQL_STATE_CLIENT_ERROR, e); + ensureValid(); + if (index < 0) { + throw new SQLException("Index cannot be negative"); + } + if (count < 0) { + throw new SQLException("Count cannot be negative"); } + if (count > (array.length - index)) { + throw new SQLException("Not enough elements after index " + index); + } + + Object[] smallerArray = new Object[count]; + System.arraycopy(array, (int) index, smallerArray, 0, count); + return smallerArray; } @Override @@ -85,6 +106,13 @@ public ResultSet getResultSet(long index, int count, Map> map) @Override public void free() throws SQLException { + valid = false; array = null; } + + private void ensureValid() throws SQLException { + if (!valid) { + throw ExceptionUtils.toSqlState(new SQLFeatureNotSupportedException("Array is not valid. Possible free() was called.")); + } + } } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Struct.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Struct.java new file mode 100644 index 000000000..874dfdf09 --- /dev/null +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Struct.java @@ -0,0 +1,39 @@ +package com.clickhouse.jdbc.types; + +import com.clickhouse.data.ClickHouseColumn; +import com.clickhouse.jdbc.internal.ExceptionUtils; + +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Map; + +public class Struct implements java.sql.Struct { + + private final Object[] attributes; + + private final ClickHouseColumn column; + + public Struct(ClickHouseColumn column, Object[] attributes) { + this.column = column; + this.attributes = attributes; + } + + @Override + public String getSQLTypeName() throws SQLException { + return column.getOriginalTypeName(); + } + + @Override + public Object[] getAttributes() throws SQLException { + return attributes; + } + + @Override + public Object[] getAttributes(Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getAttributes(Map>) is not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); + } + + public ClickHouseColumn getColumn() { + return column; + } +} diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java index 42374f757..683237344 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java @@ -5,6 +5,7 @@ import com.clickhouse.client.ClickHouseServerForTest; import com.clickhouse.client.api.Client; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.client.api.ServerException; import com.clickhouse.client.api.internal.ServerSettings; import com.github.tomakehurst.wiremock.WireMockServer; @@ -15,21 +16,36 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.math.BigDecimal; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.nio.charset.StandardCharsets; import java.sql.Array; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.Date; +import java.sql.JDBCType; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; +import java.sql.Struct; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.Base64; +import java.util.List; import java.util.Properties; import java.util.UUID; +import java.util.function.BiConsumer; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertThrows; import static org.testng.Assert.fail; @@ -84,9 +100,13 @@ public void testCreateUnsupportedStatements() throws Throwable { () -> conn.prepareStatement("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE), () -> conn.prepareStatement("SELECT 1", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY), () -> conn.prepareStatement("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT), + () -> conn.prepareCall("SELECT 1"), + () -> conn.prepareCall("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY), + () -> conn.prepareCall("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT), conn::setSavepoint, () -> conn.setSavepoint("save point"), - () -> conn.createStruct("simple", null), + conn::createSQLXML, + () -> conn.setAutoCommit(false) }; for (Assert.ThrowingRunnable createStatement : createStatements) { @@ -100,14 +120,6 @@ public void testCreateUnsupportedStatements() throws Throwable { } } - @Test(groups = { "integration" }) - public void prepareCallTest() throws SQLException { - Connection localConnection = this.getJdbcConnection(); - assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareCall("SELECT 1")); - assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareCall("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)); - assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareCall("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT)); - } - @Test(groups = { "integration" }) public void nativeSQLTest() throws SQLException { try (Connection conn = this.getJdbcConnection()) { @@ -169,23 +181,22 @@ public void closeTest() throws SQLException { @Test(groups = { "integration" }) public void getMetaDataTest() throws SQLException { - Connection localConnection = this.getJdbcConnection(); - DatabaseMetaData metaData = localConnection.getMetaData(); - Assert.assertNotNull(metaData); - Assert.assertEquals(metaData.getConnection(), localConnection); + try (Connection localConnection = this.getJdbcConnection()) { + DatabaseMetaData metaData = localConnection.getMetaData(); + Assert.assertNotNull(metaData); + Assert.assertEquals(metaData.getConnection(), localConnection); + } } @Test(groups = { "integration" }) public void setReadOnlyTest() throws SQLException { - Connection localConnection = this.getJdbcConnection(); - localConnection.setReadOnly(false); - assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.setReadOnly(true)); - } - - @Test(groups = { "integration" }) - public void isReadOnlyTest() throws SQLException { - Connection localConnection = this.getJdbcConnection(); - Assert.assertFalse(localConnection.isReadOnly()); + try (Connection conn = this.getJdbcConnection()) { + assertFalse(conn.isReadOnly()); + conn.setReadOnly(true); + Assert.assertTrue(conn.isReadOnly()); + conn.setReadOnly(false); + Assert.assertFalse(conn.isReadOnly()); + } } @Test(groups = { "integration" }) @@ -239,14 +250,12 @@ public void setTypeMapTest() throws SQLException { @Test(groups = { "integration" }) public void setHoldabilityTest() throws SQLException { - Connection localConnection = this.getJdbcConnection(); - localConnection.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);//No-op - } - - @Test(groups = { "integration" }) - public void getHoldabilityTest() throws SQLException { - Connection localConnection = this.getJdbcConnection(); - Assert.assertEquals(localConnection.getHoldability(), ResultSet.HOLD_CURSORS_OVER_COMMIT); + try (Connection conn = this.getJdbcConnection()) { + Assert.assertEquals(conn.getHoldability(), ResultSet.HOLD_CURSORS_OVER_COMMIT); + conn.setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT); + Assert.assertEquals(conn.getHoldability(), ResultSet.HOLD_CURSORS_OVER_COMMIT); + assertThrows(SQLException.class, () -> conn.setHoldability(-1)); + } } @Test(groups = { "integration" }) @@ -365,17 +374,170 @@ public static Object[][] setAndGetClientInfoTestDataProvider() { } @Test(groups = { "integration" }) - public void createArrayOfTest() throws SQLException { - Connection localConnection = this.getJdbcConnection(); - Array array = localConnection.createArrayOf("Int8", new Object[] { 1, 2, 3 }); - Assert.assertNotNull(array); - Assert.assertEquals(array.getArray(), new Object[] { 1, 2, 3 }); + public void testCreateArray() throws SQLException { + try (Connection conn = getJdbcConnection()) { + + Assert.expectThrows(SQLException.class, () -> conn.createArrayOf("Array()", new Integer[] {1})); + + + final String baseType = "Tuple(String, Int8)"; + final String tableName = "array_create_test"; + final String arrayType = "Array(Array(" + baseType + "))"; + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE TABLE " +tableName + " (v1 " + arrayType + ") ENGINE MergeTree ORDER BY ()"); + + + Struct tuple1 = conn.createStruct(baseType, new Object[]{"v1", (byte)10}); + Struct tuple2 = conn.createStruct(baseType, new Object[]{"v2", (byte)20}); + + Struct[][] srcArray = new Struct[][] { + new Struct[] { tuple1}, + new Struct[] { tuple1, tuple2}, + }; + + Array arrayValue = conn.createArrayOf("Tuple(String, Int8)", srcArray ); + assertEquals(arrayValue.getBaseTypeName(), baseType); + assertEquals(arrayValue.getBaseType(), JDBCType.OTHER.getVendorTypeNumber()); + assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getArray(null)); + assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getArray(0, 1, null)); + assertThrows(SQLFeatureNotSupportedException.class, arrayValue::getResultSet); + assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getResultSet(0, 1)); + assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getResultSet(null)); + assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getResultSet(0, 1, null)); + + Assert.expectThrows(SQLException.class, () -> arrayValue.getArray(-1, 1)); + Assert.expectThrows(SQLException.class, () -> arrayValue.getArray(0, -1)); + Assert.expectThrows(SQLException.class, () -> arrayValue.getArray(0, 3)); + Assert.expectThrows(SQLException.class, () -> arrayValue.getArray(1, 2)); + + Object[] subArray = (Object[]) arrayValue.getArray(1, 1); + Assert.assertEquals(subArray.length, 1); + + try (PreparedStatement pStmt = conn.prepareStatement("INSERT INTO " + tableName + " (v1) VALUES (?)")) { + pStmt.setArray(1, arrayValue); + pStmt.executeUpdate(); + pStmt.setObject(1, arrayValue); + pStmt.executeUpdate(); + } finally { + arrayValue.free(); + arrayValue.free(); // just to check that operation idempotent + assertThrows(SQLException.class, () -> arrayValue.getArray(1, 1)); + assertThrows(SQLException.class, arrayValue::getArray); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName)) { + Assert.assertTrue(rs.next()); + Array array1 = rs.getArray(1); + Object[] elements = (Object[]) array1.getArray(); + Object[] storedTuple1 = (Object[]) ((List)elements[0]).get(0); + Object[] storedTuple2 = (Object[]) ((List)elements[1]).get(1); + Assert.assertEquals(storedTuple1, tuple1.getAttributes()); + Assert.assertEquals(storedTuple2, tuple2.getAttributes()); + + Array array2 = (Array) rs.getObject(1); + Assert.assertEquals(array2.getArray(), elements); + } + } + } + } + + @Test(groups = {"integration"}) + public void testCreateArrayDifferentTypes() throws Exception { + try (Connection conn = getJdbcConnection()) { + + BiConsumer verification = (type, arr) -> { + Array array; + try { + array = conn.createArrayOf(type, arr); + Object[] wrappedArray = (Object[]) array.getArray(); + assertEquals(wrappedArray.length, arr.length); + assertEquals(wrappedArray, arr); + } catch (SQLException e) { + fail("Failed to create array of type " + type + " with " + Arrays.toString(arr), e); + throw new RuntimeException(e); + } + }; + + + verification.accept("Int8", new Byte[] {1, 2, 3}); + verification.accept("Int16", new Short[] {Short.MIN_VALUE, -1, 0, 1, Short.MAX_VALUE}); + verification.accept("Int32", new Integer[] {Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE}); + verification.accept("Int64", new Long[] {Long.MIN_VALUE, -1L, 0L, 1L, Long.MAX_VALUE}); + verification.accept("UInt8", new Byte[] {0, 1, Byte.MAX_VALUE}); + verification.accept("UInt16", new Short[] {0, 1, Short.MAX_VALUE}); + verification.accept("UInt32", new Long[] {0L, 1L, (long)Integer.MAX_VALUE}); + verification.accept("UInt64", new Long[] {0L, 1L, Long.MAX_VALUE}); + verification.accept("Float32", new Float[] {-1.0F, 0.0F, 1.0F}); + verification.accept("Float64", new Double[] {-1.0D, 0.0D, 1.0D}); + verification.accept("Date", new Date[] { + Date.valueOf(LocalDate.now()), + Date.valueOf(LocalDate.of(2022, 1, 1)), + Date.valueOf(LocalDate.of(2021, 12, 31)) + }); + verification.accept("DateTime", new Timestamp[] { + Timestamp.valueOf(LocalDateTime.now()), + Timestamp.valueOf(LocalDateTime.of(2022, 1, 1, 0, 0, 0)), + Timestamp.valueOf(LocalDateTime.of(2021, 12, 31, 23, 59, 59)) + }); + verification.accept("Decimal(10, 2)", new BigDecimal[] { + new BigDecimal("123.45"), + new BigDecimal("-12345.67"), + new BigDecimal("0.00") + }); + verification.accept("String", new String[] { + "", + "Hello", + " hello " + }); + verification.accept("FixedString(5)", new String[] { + "12345", + "abcde", + " 123" + }); + verification.accept("IPv4", new Inet4Address[] { + (Inet4Address) Inet4Address.getByName("127.0.0.1"), + (Inet4Address) Inet4Address.getByName("192.168.0.1"), + }); + verification.accept("IPv6", new Inet6Address[] { + (Inet6Address) Inet6Address.getByName("::1"), + (Inet6Address) Inet6Address.getByName("2001:db8::1"), + }); + } } @Test(groups = { "integration" }) - public void createStructTest() throws SQLException { - Connection localConnection = this.getJdbcConnection(); - assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.createStruct("type-name", new Object[] { 1, 2, 3 })); + public void testCreateStruct() throws SQLException { + try (Connection conn = this.getJdbcConnection()) { + final String tableName = "test_struct_tuple"; + final String tupleType = "Tuple(Int8, String, DateTime64)"; + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE TABLE " + tableName +" (v1 " + tupleType + ") ENGINE MergeTree ORDER BY ()"); + + final java.sql.Timestamp timePart = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + timePart.setNanos(333000000); + + Struct tupleValue = conn.createStruct(tupleType, new Object[] {120, "test tuple value", timePart}); + assertEquals(tupleValue.getSQLTypeName(), tupleType); + assertThrows(SQLFeatureNotSupportedException.class, () -> tupleValue.getAttributes(null)); + assertNotNull(((com.clickhouse.jdbc.types.Struct) tupleValue).getColumn()); + + + try (PreparedStatement pStmt = conn.prepareStatement("INSERT INTO " + tableName + " VALUES (?)")) { + pStmt.setObject(1, tupleValue); + pStmt.executeUpdate(); + } + + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName)) { + Assert.assertTrue(rs.next()); + Object[] tuple = (Object[]) rs.getObject(1); + Assert.assertEquals(tuple[0], (byte)120); + Assert.assertEquals(tuple[1], "test tuple value"); + Assert.assertEquals(DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER.format((TemporalAccessor) tuple[2]), + timePart.toString()); + } + } + } } @Test(groups = { "integration" }) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java index ad9400028..ac09f34e7 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java @@ -2,12 +2,14 @@ import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.DataTypeUtils; +import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; import com.clickhouse.client.api.internal.ServerSettings; import com.clickhouse.client.api.sql.SQLUtils; import com.clickhouse.data.ClickHouseVersion; import com.clickhouse.data.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -726,7 +728,7 @@ public void testIpAddressTypes() throws SQLException, UnknownHostException { long seed = System.currentTimeMillis(); Random rand = new Random(seed); - InetAddress ipv4AddressByIp = Inet4Address.getByName(rand.nextInt(256) + "." + rand.nextInt(256) + "." + rand.nextInt(256) + "." + rand.nextInt(256)); + InetAddress ipv4AddressByIp = Inet4Address.getByName("90.176.75.97"); InetAddress ipv4AddressByName = Inet4Address.getByName("www.example.com"); InetAddress ipv6Address = Inet6Address.getByName("2001:adb8:85a3:1:2:8a2e:370:7334"); InetAddress ipv4AsIpv6 = Inet4Address.getByName("90.176.75.97"); @@ -747,11 +749,13 @@ public void testIpAddressTypes() throws SQLException, UnknownHostException { try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_ips ORDER BY order")) { assertTrue(rs.next()); assertEquals(rs.getObject("ipv4_ip"), ipv4AddressByIp); + assertEquals(rs.getObject("ipv4_ip", Inet6Address.class).toString(), "/0:0:0:0:0:ffff:5ab0:4b61"); assertEquals(rs.getString("ipv4_ip"), ipv4AddressByIp.toString()); assertEquals(rs.getObject("ipv4_name"), ipv4AddressByName); assertEquals(rs.getObject("ipv6"), ipv6Address); assertEquals(rs.getString("ipv6"), ipv6Address.toString()); assertEquals(rs.getObject("ipv4_as_ipv6"), ipv4AsIpv6); + assertEquals(rs.getObject("ipv4_as_ipv6", Inet4Address.class), ipv4AsIpv6); assertFalse(rs.next()); } } @@ -998,6 +1002,51 @@ public void testArrayTypes() throws SQLException { } } + @Test(groups = { "integration" }) + public void testNestedArrays() throws Exception { + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Array(Int32)) as value")) { + Integer[][] srcArray = new Integer[][] { + {1, 2, 3}, + {4, 5, 6} + }; + Array array = conn.createArrayOf("Int32", srcArray); + stmt.setArray(1, array); + + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + Array arrayHolder = (Array) rs.getObject(1); + Object[] dbArray = (Object[]) arrayHolder.getArray(); + for (int i = 0; i < dbArray.length; i++) { + Object[] nestedArray = (Object[]) dbArray[i]; + for (int j = 0; j < nestedArray.length; j++) { + assertEquals((Integer) nestedArray[j], (Integer)srcArray[i][j]); + } + } + } + + Integer[] simpleArray = new Integer[] {1, 2, 3}; + Array array1 = conn.createArrayOf("Int32", simpleArray); + Array array2 = conn.createArrayOf("Int32", simpleArray); + + Array[] multiLevelArray = new Array[] {array1, array2}; + Array array3 = conn.createArrayOf("Int32", multiLevelArray); + stmt.setArray(1, array3); + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + Array arrayHolder = (Array) rs.getObject(1); + Object[] dbArray = (Object[]) arrayHolder.getArray(); + for (int i = 0; i < dbArray.length; i++) { + Object[] nestedArray = (Object[]) dbArray[i]; + for (int j = 0; j < nestedArray.length; j++) { + assertEquals((Integer) nestedArray[j], (Integer)simpleArray[j]); + } + } + } + } + } + } + @Test(groups = { "integration" }) public void testMapTypes() throws SQLException { runQuery("CREATE TABLE test_maps (order Int8, " diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java index 88363c6a0..dc6e32388 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -276,6 +276,16 @@ public void testPrimitiveArrays() throws Exception { assertFalse(rs.next()); } } + + try (PreparedStatement stmt = conn.prepareStatement("SELECT ?")) { + stmt.setObject(1, new Object[] {1, 2, 3}); + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + Array a1 = rs.getArray(1); + assertNotNull(a1); + assertEquals(Arrays.deepToString((Object[]) a1.getArray()), "[1, 2, 3]"); + } + } } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java index b083135c3..ef6b63a84 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java @@ -1,5 +1,81 @@ package com.clickhouse.jdbc.internal; +import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; +import com.clickhouse.data.ClickHouseColumn; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + public class JdbcUtilsTest { + + @Test(groups = {"unit"}) + public void testConvertPrimitiveTypes() throws SQLException { + assertEquals(JdbcUtils.convert(1, int.class), 1); + assertEquals(JdbcUtils.convert(1L, long.class), 1L); + assertEquals(JdbcUtils.convert("1", String.class), "1"); + assertEquals(JdbcUtils.convert(1.0f, float.class), 1.0f); + assertEquals(JdbcUtils.convert(1.0, double.class), 1.0); + assertEquals(JdbcUtils.convert(true, boolean.class), true); + assertEquals(JdbcUtils.convert((short) 1, short.class), (short) 1); + assertEquals(JdbcUtils.convert((byte) 1, byte.class), (byte) 1); + assertEquals(JdbcUtils.convert(1.0d, BigDecimal.class), BigDecimal.valueOf(1.0d)); + } + + + @Test(groups = {"unit"}) + public void testConvertToArray() throws Exception { + ClickHouseColumn column = ClickHouseColumn.of("arr", "Array(Int32)"); + BinaryStreamReader.ArrayValue arrayValue = new BinaryStreamReader.ArrayValue(int.class, 2); + arrayValue.set(0, 1); + arrayValue.set(1, 2); + java.sql.Array array = (java.sql.Array) JdbcUtils.convert(arrayValue, java.sql.Array.class, column); + Object arr = array.getArray(); + assertEquals(array.getBaseTypeName(), "Int32"); + assertEquals(arr.getClass().getComponentType(), Object.class); + Object[] arrs = (Object[]) arr; + assertEquals(arrs[0], 1); + assertEquals(arrs[1], 2); + } + + + @Test(groups = {"unit"}) + public void testConvertArray() throws Exception { + Object[] src = {1, 2, 3}; + Object[] dst = JdbcUtils.convertArray(src, int.class); + assertEquals(dst.length, src.length); + assertEquals(dst[0], src[0]); + assertEquals(dst[1], src[1]); + assertEquals(dst[2], src[2]); + + assertNull(JdbcUtils.convertArray(null, int.class)); + assertEquals(JdbcUtils.convertArray(new Integer[] { 1, 2}, null), new Integer[] { 1, 2}); + } + + + @Test(groups = {"unit"}) + public void testConvertList() throws Exception { + ClickHouseColumn column = ClickHouseColumn.of("arr", "Array(Int32)"); + List src = Arrays.asList(1, 2, 3); + Object[] dst = JdbcUtils.convertList(src, Integer.class); + assertEquals(dst.length, src.size()); + assertEquals(dst[0], src.get(0)); + assertEquals(dst[1], src.get(1)); + assertEquals(dst[2], src.get(2)); + + assertNull(JdbcUtils.convertList(null, Integer.class)); + } + + + @Test(groups = {"unit"}) + public void testConvertToInetAddress() throws Exception { + ClickHouseColumn column = ClickHouseColumn.of("ip", "IPv4"); + assertEquals(JdbcUtils.convert(java.net.InetAddress.getByName("192.168.0.1"), java.net.Inet6Address.class, column).toString(), "/0:0:0:0:0:ffff:c0a8:1"); + } }