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 6ee472456..2a1ff321d 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 9d86c8f1e..03304000c 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -6,6 +6,7 @@ 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.JdbcConfiguration; @@ -22,6 +23,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; @@ -59,6 +61,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; @@ -73,6 +77,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(); @@ -229,15 +235,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 @@ -279,13 +286,13 @@ 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 @@ -319,13 +326,19 @@ public void setTypeMap(Map> map) throws SQLException { @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 @@ -563,10 +576,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 SQLFeatureNotSupportedException("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); } @@ -574,14 +599,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 ee76c143c..4f6a9d9f2 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,34 @@ 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 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 +785,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('['); + appendArrayElements((Object[]) ((Array) x).getArray(), listString); + listString.append(']'); return listString.toString(); + } else if (x instanceof Object[]) { + StringBuilder arrayString = new StringBuilder(); + arrayString.append('['); + appendArrayElements((Object[]) x, arrayString); + arrayString.append(']'); + return arrayString.toString(); } else if (x.getClass().isArray()) { StringBuilder listString = new StringBuilder(); - listString.append("["); - - + listString.append('['); 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(']'); 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('['); + 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(']'); 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.setLength(mapString.length() - 1); } - if (!tmpMap.isEmpty()) - mapString.delete(mapString.length() - 2, mapString.length()); - mapString.append("}"); + + 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 +862,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/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..6265881e8 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; @@ -261,39 +264,58 @@ 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(column.getArrayBaseColumn().getDataType())); + 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(column.getArrayBaseColumn().getDataType())), + 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 " + value + " 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; + } + + 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..e92d9b92e 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; @@ -19,17 +20,26 @@ import java.sql.Array; import java.sql.Connection; import java.sql.DatabaseMetaData; +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.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 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 +94,12 @@ 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(), }; for (Assert.ThrowingRunnable createStatement : createStatements) { @@ -100,14 +113,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 +174,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 +243,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 +367,106 @@ 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 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 41e5b5aa5..abd3ee5f2 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java @@ -2,11 +2,13 @@ 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.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; @@ -724,7 +726,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"); @@ -745,11 +747,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()); } } 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..c14e72dc0 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,83 @@ 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 org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.net.Inet6Address; +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"); + } }