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 69cbc9a7f..0b62b2411 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -6,10 +6,11 @@ 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; import com.clickhouse.jdbc.internal.SqlParser; import com.clickhouse.jdbc.metadata.DatabaseMetaDataImpl; @@ -39,12 +40,10 @@ 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); @@ -60,6 +59,8 @@ 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; @@ -68,6 +69,8 @@ public class ConnectionImpl implements Connection, JdbcV2Wrapper { private Executor networkTimeoutExecutor; + private final FeatureManager featureManager; + public ConnectionImpl(String url, Properties info) throws SQLException { try { this.url = url;//Raw URL @@ -75,6 +78,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(); @@ -114,6 +119,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) { @@ -168,10 +174,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; } @@ -185,10 +188,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 @@ -199,16 +199,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 @@ -234,15 +230,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 @@ -259,9 +256,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 @@ -284,89 +279,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 @@ -422,10 +405,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; } @@ -465,9 +445,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; } @@ -475,30 +453,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; } @@ -566,12 +535,30 @@ public Properties getClientInfo() throws SQLException { return clientInfo; } + /** + * Creating multilevel arrays may be confusing. + * Spec doesn't tell much about it so there may be different variants. + * Note: createArrayOf() expect type name be for element of the array and for + * Array(Array(Int8)) it should be Int8 according to spec. However element type + * of 1st level array is Array(Int8) + * @param typeName the SQL name of the type the elements of the array map to. The typeName is a + * database-specific name which may be the name of a built-in type, a user-defined type or a standard SQL type supported by this database. This + * is the value returned by {@code Array.getBaseTypeName} + * + * @param elements the elements that populate the returned object + * @return + * @throws SQLException + */ @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + ensureOpen(); + if (typeName == null) { + throw new SQLFeatureNotSupportedException("typeName cannot be null"); + } + + ClickHouseColumn column = ClickHouseColumn.of("array", typeName); 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(column, elements); } catch (Exception e) { throw new SQLException("Failed to create array", ExceptionUtils.SQL_STATE_CLIENT_ERROR, e); } @@ -579,14 +566,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(); 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 705779668..3e1d1a7e6 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; @@ -58,6 +59,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Stack; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -757,32 +759,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) { @@ -790,106 +797,71 @@ 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) { - 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("]"); - - return listString.toString(); + return QUOTE + ((InetAddress) x).getHostAddress() + QUOTE; + } else if (x instanceof java.sql.Array) { + com.clickhouse.jdbc.types.Array array = (com.clickhouse.jdbc.types.Array) x; + int nestedLevel = Math.max(1, array.getNestedLevel()); + return encodeArray((Object[]) array.getArray(), nestedLevel, array.getBaseDataType()); + } 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 encodeTuple(((Tuple)x).getValues()); + } else if (x instanceof Struct) { + return encodeTuple(((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 @@ -899,6 +871,105 @@ private static String encodeObject(Object x, Long length) throws SQLException { } } + private void appendArrayElements(Object[] array, StringBuilder sb) throws SQLException { + appendArrayElements(array, sb, null); + } + + private void appendArrayElements(Object[] array, StringBuilder sb, ClickHouseDataType elementType) throws SQLException { + if (array == null) { + return; + } + for (Object item : array) { + if (elementType == ClickHouseDataType.Tuple && item != null && item.getClass().isArray()) { + sb.append(encodeTuple((Object[]) item)); + } else { + sb.append(encodeObject(item)).append(','); + } + } + if (array.length > 0) { + sb.setLength(sb.length() - 1); + } + } + + public String encodeArray(Object[] elements, int levels, ClickHouseDataType elementType) throws SQLException { + if (elements == null) { + return "[]"; + } + + StringBuilder arraySb = new StringBuilder(); + Stack stack = new Stack<>(); + ArrayProcessingCursor cursor = new ArrayProcessingCursor(elements, 0, levels); + + arraySb.append(O_BRACKET); + while (cursor != null) { + if (cursor.pos >= cursor.array.length) { + if (cursor.array.length > 0) { + arraySb.setLength(arraySb.length() - 1); + } + arraySb.append(C_BRACKET); + cursor = stack.isEmpty() ? null : stack.pop(); + if (cursor != null) { + arraySb.append(','); + } + continue; + } + + Object element = cursor.array[cursor.pos]; + if (element == null) { + if (cursor.level == 1) { + arraySb.append("NULL"); + } else { + arraySb.append("[]"); + } + arraySb.append(','); + cursor.pos++; + } else if (cursor.arrayObjAsTuple) { + arraySb.append(encodeTuple((Object[]) ((Array)element).getArray())).append(','); + cursor.pos++; + } else if (cursor.arrayAsTuple) { + arraySb.append(encodeTuple((Object[]) element)).append(','); + cursor.pos++; + } else if (cursor.level == 1 && elementType == ClickHouseDataType.Tuple && element instanceof Array ) { + cursor.arrayObjAsTuple = true; + } else if (cursor.level == 1 && elementType == ClickHouseDataType.Tuple && element instanceof Object[] ) { + cursor.arrayAsTuple = true; + } else if (cursor.level == 1) { + arraySb.append(encodeObject(element)).append(','); + cursor.pos++; + } else { + cursor.pos++; + stack.push(cursor); + cursor = new ArrayProcessingCursor((Object[]) element, 0, cursor.level - 1); + arraySb.append(O_BRACKET); + } + } + + return arraySb.toString(); + } + + private static final class ArrayProcessingCursor { + Object[] array; // current array + int pos; // processing position + int level; + boolean arrayAsTuple = false; + boolean arrayObjAsTuple = false; + public ArrayProcessingCursor(Object[] array, int pos, int level) { + this.array = array; + this.pos = pos; + this.level = level; + } + } + + private String encodeTuple(Object[] array) throws SQLException { + StringBuilder sb = new StringBuilder(); + sb.append('('); + if (array != null) { + appendArrayElements(array, sb); + } + sb.append(')'); + return sb.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 3a86591ea..172f1c3be 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; import org.slf4j.Logger; @@ -12,6 +14,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; @@ -227,6 +230,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) { @@ -262,39 +267,59 @@ 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(column, convertedValues); } - return new Array(((BinaryStreamReader.ArrayValue) value).asList(), "Object", JDBCType.JAVA_OBJECT.getVendorTypeNumber()); + return new Array(column, arrayValue.getArrayOfObjects()); } 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(); + Object[] convertedValues = convertList((List) value, JdbcUtils.convertToJavaClass(baseType)); + return new Array(column, convertedValues); } - return new Array((List) value, "Object", JDBCType.JAVA_OBJECT.getVendorTypeNumber()); + // base type is unknown. all objects should be converted + return new Array(column, ((List) value).toArray()); } 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..610501b69 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 @@ -1,43 +1,61 @@ package com.clickhouse.jdbc.types; +import com.clickhouse.data.ClickHouseColumn; +import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.jdbc.internal.ExceptionUtils; +import com.clickhouse.jdbc.internal.JdbcUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.sql.JDBCType; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; -import java.util.List; import java.util.Map; 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 final ClickHouseColumn column; + private Object[] array; + private final int type; //java.sql.Types + private final String elementTypeName; + private boolean valid; + private final ClickHouseDataType baseDataType; + + public Array(ClickHouseColumn column, Object[] elements) throws SQLException { + this.column = column; + this.array = elements; + ClickHouseColumn baseColumn = (this.column.isArray() ? this.column.getArrayBaseColumn() : this.column); + this.baseDataType = baseColumn.getDataType(); + this.elementTypeName = baseColumn.getOriginalTypeName(); + this.type = JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(baseDataType, JDBCType.OTHER).getVendorTypeNumber(); + this.valid = true; + } - this.array = list.toArray(); - this.type = itemType; - this.typeName = itemTypeName; + public ClickHouseDataType getBaseDataType() { + return baseDataType; + } + + public int getNestedLevel() { + return column.getArrayNestedLevel(); } @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 +66,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 (array == null || 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 +109,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 a742c811a..076418a3a 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,25 +16,41 @@ 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.Collections; +import java.util.List; import java.util.Properties; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +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.assertTrue; @@ -89,9 +106,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) { @@ -105,14 +126,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()) { @@ -174,23 +187,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" }) @@ -244,14 +256,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" }) @@ -370,17 +380,245 @@ 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()) { + + final String baseType = "Tuple(String, Int8)"; + final String tableName = "array_create_test"; + final String arrayType = "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[] { 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[]) elements[0]; + Object[] storedTuple2 = (Object[]) elements[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 testCreateArrayVariants() throws Exception { + try (Connection conn = getJdbcConnection()) { + + // it is valid + { + Array array = conn.createArrayOf("Nullable(String)", (Object[]) null); + assertNull(array.getArray()); + assertThrows(SQLException.class, () -> array.getArray(10, 10)); + assertThrows(SQLFeatureNotSupportedException.class, () -> array.getArray(10, 10, Collections.emptyMap())); + + try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Nullable(String)) as value")) { + stmt.setArray(1, array); + try (ResultSet rs = stmt.executeQuery()) { + rs.next(); + assertEquals(rs.getMetaData().getColumnTypeName(1), "Array(Nullable(String))"); + assertEquals(rs.getArray(1).getArray(), new String[] {}); +// assertEquals(rs.getArray(1).getArray().getClass(), String[].class); // TODO: fix + } + } + } + + // array of nullables + { + String[] strings = new String[] {"one", null, "five"}; + Array array = conn.createArrayOf("Nullable(String)", strings); + assertNotNull(array.getArray()); + + try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Nullable(String)) as value")) { + stmt.setArray(1, array); + try (ResultSet rs = stmt.executeQuery()) { + rs.next(); + assertEquals(rs.getMetaData().getColumnTypeName(1), "Array(Nullable(String))"); + } + } + } + + // multi-level array + { + Object[][] table = new Object[][] { + {1, 2 ,3, 4, 5}, + {10, 20, 30, 40, 50, }, + }; + Array array = conn.createArrayOf("Array(Array(Int32))", table); + + } + + + // array of tuples + { + Object[][] tuples = new Object[][] { + {"tuple1", 10}, + {"tuple2", 20}, + }; + Array array = conn.createArrayOf("Tuple(String, Int32)", tuples); + assertNotNull(array.getArray()); + + try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Tuple(String, Int32)) as value")) { + stmt.setArray(1, array); + try (ResultSet rs = stmt.executeQuery()) { + rs.next(); + assertEquals(rs.getMetaData().getColumnTypeName(1), "Array(Tuple(String, Int32))"); + } + } + } + + { + Array tuple1 = conn.createArrayOf("String", new String[] {"one", "two"}); + Array tuple2 = conn.createArrayOf("String", new String[] {"three", "four"}); + Array array = conn.createArrayOf("Tuple(String, String)", new Object[] {tuple1, tuple2}); + + try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Tuple(String, String)) as value")) { + stmt.setArray(1, array); + try (ResultSet rs = stmt.executeQuery()) { + rs.next(); + assertEquals(rs.getMetaData().getColumnTypeName(1), "Array(Tuple(String, String))"); + } + } + } + } } @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..f707ccc2f 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()); } } @@ -914,7 +918,8 @@ public void testArrayTypes() throws SQLException { try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_arrays VALUES ( 1, ?, ?, ?, ?)")) { stmt.setArray(1, conn.createArrayOf("Int8", array)); stmt.setArray(2, conn.createArrayOf("String", arraystr)); - stmt.setArray(3, conn.createArrayOf("Tuple", arraytuple)); + stmt.setArray(3, conn.createArrayOf("Tuple(Int8, String)", arraytuple)); + stmt.setArray(3, conn.createArrayOf("Tuple(Int8, String)", arraytuple)); stmt.setArray(4, conn.createArrayOf("Date", arraydate)); stmt.executeUpdate(); } @@ -998,6 +1003,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 aabf55bc3..b3cda1247 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -1,5 +1,6 @@ package com.clickhouse.jdbc; +import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.data.ClickHouseVersion; import com.clickhouse.data.Tuple; @@ -276,6 +277,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]"); + } + } } } @@ -1522,4 +1533,71 @@ public void testParameterCount() throws Exception { } } } + + @Test + public void testEncodingArray() throws Exception { + try (Connection conn = getJdbcConnection();) { + try (PreparedStatementImpl stmt = (PreparedStatementImpl) conn.prepareStatement("SELECT ?")) { + + { + Object[] array1 = new Object[]{1, 2, 3}; + ClickHouseColumn col1 = ClickHouseColumn.of("v", "Array(Int8)"); + assertEquals(stmt.encodeArray(array1, col1.getArrayNestedLevel(), col1.getArrayBaseColumn().getDataType()), + "[1,2,3]"); + } { + Object[] array1 = new Object[]{1, 2, 3}; + Object[] array2 = new Object[]{4, 5, 6}; + Object[] array3 = new Object[]{array1, array2}; + ClickHouseColumn col1 = ClickHouseColumn.of("v", "Array(Array(Int8))"); + assertEquals(stmt.encodeArray(array3, col1.getArrayNestedLevel(), col1.getArrayBaseColumn().getDataType()), + "[[1,2,3],[4,5,6]]"); + } + { + Object[] array1 = new Object[]{1, 2, 3}; + Object[] array2 = new Object[]{4, null, 6}; + Object[] array3 = new Object[]{null, array1, array2}; + ClickHouseColumn col1 = ClickHouseColumn.of("v", "Array(Array(Int8))"); + assertEquals(stmt.encodeArray(array3, col1.getArrayNestedLevel(), col1.getArrayBaseColumn().getDataType()), + "[[],[1,2,3],[4,NULL,6]]"); + } + { + Object[] array1 = new Object[]{1, 2, 3}; + Object[] array2 = new Object[]{4, null, 6}; + Object[] array3 = new Object[]{null, array1, array2}; + Object[] array4 = new Object[]{7, null, 9}; + Object[] array5 = new Object[]{10, null, 12}; + Object[] array6 = new Object[]{null, array4, array5}; + + Object[] array7 = new Object[]{null, array3, array6}; + ClickHouseColumn col1 = ClickHouseColumn.of("v", "Array(Array(Array(Int8)))"); + assertEquals(stmt.encodeArray(array7, col1.getArrayNestedLevel(), col1.getArrayBaseColumn().getDataType()), + "[[],[[],[1,2,3],[4,NULL,6]],[[],[7,NULL,9],[10,NULL,12]]]"); + } + + + { + Object[] array1 = new Object[]{1, 2, 3}; + Object[] array2 = new Object[]{4, 5, 6}; + Object[] array3 = new Object[]{array1, array2}; + ClickHouseColumn col1 = ClickHouseColumn.of("v", "Array(Tuple(Int8, Int8, Int8))"); + assertEquals(stmt.encodeArray(array3, col1.getArrayNestedLevel(), col1.getArrayBaseColumn().getDataType()), + "[(1,2,3),(4,5,6)]"); + } + + { + Object[] array1 = new Object[]{1, 2, 3}; + Object[] array2 = new Object[]{4, 5, 6}; + Object[] array3 = new Object[]{null, array1, array2, new Object[0]}; + Object[] array4 = new Object[]{7, 8, 9}; + Object[] array5 = new Object[]{10, 11, 12}; + Object[] array6 = new Object[]{null, array4, array5}; + + Object[] array7 = new Object[]{null, array3, array6, new Object[0]}; + ClickHouseColumn col1 = ClickHouseColumn.of("v", "Array(Array(Tuple(Int8, Int8, Int8)))"); + assertEquals(stmt.encodeArray(array7, col1.getArrayNestedLevel(), col1.getArrayBaseColumn().getDataType()), + "[[],[NULL,(1,2,3),(4,5,6),()],[NULL,(7,8,9),(10,11,12)],[]]"); + } + } + } + } } 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"); + } }