diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java index d6dd53594..b234dfce5 100644 --- a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java +++ b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java @@ -1175,6 +1175,21 @@ public ClickHouseValue newValue(ClickHouseDataConfig config) { return value; } + + /** + * Returns column effective data type. In case of SimpleAggregateFunction + * returns type of the first column. + * + * @return ClickHouseDataType + */ + public ClickHouseDataType getEffectiveDataType() { + ClickHouseDataType columnDataType = getDataType(); + if (columnDataType.equals(ClickHouseDataType.SimpleAggregateFunction)){ + columnDataType = getNestedColumns().get(0).getDataType(); + } + return columnDataType; + } + @Override public int hashCode() { final int prime = 31; diff --git a/client-v2/src/main/java/com/clickhouse/client/api/DataTypeUtils.java b/client-v2/src/main/java/com/clickhouse/client/api/DataTypeUtils.java index b9f0218cc..6840e8101 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/DataTypeUtils.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/DataTypeUtils.java @@ -3,11 +3,18 @@ import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; import com.clickhouse.data.ClickHouseDataType; +import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; import java.util.Objects; import static com.clickhouse.client.api.data_formats.internal.BinaryStreamReader.BASES; @@ -139,4 +146,77 @@ public static Instant instantFromTime64Integer(int precision, long value) { return Instant.ofEpochSecond(value, nanoSeconds); } + + public static LocalDateTime localTimeFromTime64Integer(int precision, long value) { + int nanoSeconds = 0; + if (precision > 0) { + int factor = BinaryStreamReader.BASES[precision]; + nanoSeconds = Math.abs((int) (value % factor)); // nanoseconds are stored separately and only positive values accepted + value /= factor; + + if (nanoSeconds > 0L) { + nanoSeconds *= BASES[9 - precision]; + } + + } + + return LocalDateTime.ofEpochSecond(value, nanoSeconds, ZoneOffset.UTC); + } + + /** + * Converts a {@link Duration} to a time string in the format {@code [-]HH:mm:ss[.nnnnnnnnn]}. + *

+ * Unlike standard time formats, hours can exceed 24 and can be negative. + * The precision parameter controls the number of fractional second digits (0-9). + * + * @param duration the duration to convert + * @param precision the number of fractional second digits (0-9) + * @return a string representation like {@code -999:59:59.123456789} + * @throws NullPointerException if {@code duration} is null + */ + public static String durationToTimeString(Duration duration, int precision) { + Objects.requireNonNull(duration, "Duration required for durationToTimeString"); + + boolean negative = duration.isNegative(); + if (negative) { + duration = duration.negated(); + } + + long totalSeconds = duration.getSeconds(); + int nanos = duration.getNano(); + + long hours = totalSeconds / 3600; + int minutes = (int) ((totalSeconds % 3600) / 60); + int seconds = (int) (totalSeconds % 60); + + StringBuilder sb = new StringBuilder(); + if (negative) { + sb.append('-'); + } + sb.append(hours); + sb.append(':'); + if (minutes < 10) { + sb.append('0'); + } + sb.append(minutes); + sb.append(':'); + if (seconds < 10) { + sb.append('0'); + } + sb.append(seconds); + + if (precision > 0 && precision <= 9) { + sb.append('.'); + // Format nanos with leading zeros, then truncate to precision + String nanosStr = String.format("%09d", nanos); + sb.append(nanosStr, 0, precision); + } + + return sb.toString(); + } + + public static Duration localDateTimeToDuration(LocalDateTime localDateTime) { + return Duration.ofSeconds(localDateTime.toEpochSecond(ZoneOffset.UTC)) + .plusNanos(localDateTime.getNano()); + } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java index 7d475e9a1..df6979412 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java @@ -15,6 +15,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.time.temporal.TemporalAmount; @@ -551,6 +552,10 @@ public interface ClickHouseBinaryFormatReader extends AutoCloseable { LocalDate getLocalDate(int index); + LocalTime getLocalTime(String colName); + + LocalTime getLocalTime(int index); + LocalDateTime getLocalDateTime(String colName); LocalDateTime getLocalDateTime(int index); diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 9aa5c7aeb..03af34781 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -2,7 +2,6 @@ import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.ClientException; -import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.internal.DataTypeConverter; import com.clickhouse.client.api.internal.MapUtils; @@ -35,7 +34,9 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.TemporalAmount; @@ -309,8 +310,6 @@ protected void setSchema(TableSchema schema) { case Enum16: case Variant: case Dynamic: - case Time: - case Time64: this.convertions[i] = NumberConverter.NUMBER_CONVERTERS; break; default: @@ -330,12 +329,32 @@ public TableSchema getSchema() { @Override public String getString(String colName) { - return dataTypeConverter.convertToString(readValue(colName), schema.getColumnByName(colName)); + return getString(schema.nameToColumnIndex(colName)); } @Override public String getString(int index) { - return getString(schema.columnIndexToName(index)); + ClickHouseColumn column = schema.getColumnByIndex(index); + Object value; + switch (column.getEffectiveDataType()) { + case Date: + case Date32: + value = getLocalDate(index); + break; + case Time: + case Time64: + value = getLocalTime(index); + break; + case DateTime: + case DateTime32: + case DateTime64: + value = getLocalDateTime(index); + break; + default: + value = readValue(index); + } + + return dataTypeConverter.convertToString(value, column); } private T readNumberValue(String colName, NumberConverter.NumberType targetType) { @@ -344,6 +363,9 @@ private T readNumberValue(String colName, NumberConverter.NumberType targetT if (converter != null) { Object value = readValue(colName); if (value == null) { + if (targetType == NumberConverter.NumberType.BigInteger || targetType == NumberConverter.NumberType.BigDecimal) { + return null; + } throw new NullValueException("Column " + colName + " has null value and it cannot be cast to " + targetType.getTypeName()); } @@ -401,59 +423,18 @@ public BigDecimal getBigDecimal(String colName) { @Override public Instant getInstant(String colName) { - int colIndex = schema.nameToIndex(colName); - ClickHouseColumn column = schema.getColumns().get(colIndex); - ClickHouseDataType columnDataType = column.getDataType(); - if (columnDataType.equals(ClickHouseDataType.SimpleAggregateFunction)){ - columnDataType = column.getNestedColumns().get(0).getDataType(); - } - switch (columnDataType) { - case Date: - case Date32: - LocalDate data = readValue(colName); - return data.atStartOfDay().toInstant(ZoneOffset.UTC); - case DateTime: - case DateTime64: - Object colValue = readValue(colName); - if (colValue instanceof LocalDateTime) { - LocalDateTime dateTime = (LocalDateTime) colValue; - return dateTime.toInstant(column.getTimeZone().toZoneId().getRules().getOffset(dateTime)); - } else { - ZonedDateTime dateTime = (ZonedDateTime) colValue; - return dateTime.toInstant(); - } - case Time: - return Instant.ofEpochSecond(getLong(colName)); - case Time64: - return DataTypeUtils.instantFromTime64Integer(column.getScale(), getLong(colName)); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); - } + return getInstant(getSchema().nameToColumnIndex(colName)); } @Override public ZonedDateTime getZonedDateTime(String colName) { - int colIndex = schema.nameToIndex(colName); - ClickHouseColumn column = schema.getColumns().get(colIndex); - ClickHouseDataType columnDataType = column.getDataType(); - if (columnDataType.equals(ClickHouseDataType.SimpleAggregateFunction)){ - columnDataType = column.getNestedColumns().get(0).getDataType(); - } - switch (columnDataType) { - case DateTime: - case DateTime64: - case Date: - case Date32: - return readValue(colName); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); - } + return getZonedDateTime(schema.nameToColumnIndex(colName)); } @Override public Duration getDuration(String colName) { TemporalAmount temporalAmount = getTemporalAmount(colName); - return Duration.from(temporalAmount); + return temporalAmount == null ? null : Duration.from(temporalAmount); } @Override @@ -463,12 +444,14 @@ public TemporalAmount getTemporalAmount(String colName) { @Override public Inet4Address getInet4Address(String colName) { - return InetAddressConverter.convertToIpv4(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : InetAddressConverter.convertToIpv4((java.net.InetAddress) val); } @Override public Inet6Address getInet6Address(String colName) { - return InetAddressConverter.convertToIpv6(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : InetAddressConverter.convertToIpv6((java.net.InetAddress) val); } @Override @@ -478,28 +461,35 @@ public UUID getUUID(String colName) { @Override public ClickHouseGeoPointValue getGeoPoint(String colName) { - return ClickHouseGeoPointValue.of(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : ClickHouseGeoPointValue.of((double[]) val); } @Override public ClickHouseGeoRingValue getGeoRing(String colName) { - return ClickHouseGeoRingValue.of(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : ClickHouseGeoRingValue.of((double[][]) val); } @Override public ClickHouseGeoPolygonValue getGeoPolygon(String colName) { - return ClickHouseGeoPolygonValue.of(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : ClickHouseGeoPolygonValue.of((double[][][]) val); } @Override public ClickHouseGeoMultiPolygonValue getGeoMultiPolygon(String colName) { - return ClickHouseGeoMultiPolygonValue.of(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : ClickHouseGeoMultiPolygonValue.of((double[][][][]) val); } @Override public List getList(String colName) { Object value = readValue(colName); + if (value == null) { + return null; + } if (value instanceof BinaryStreamReader.ArrayValue) { return ((BinaryStreamReader.ArrayValue) value).asList(); } else if (value instanceof List) { @@ -513,6 +503,9 @@ public List getList(String colName) { private T getPrimitiveArray(String colName, Class componentType) { try { Object value = readValue(colName); + if (value == null) { + return null; + } if (value instanceof BinaryStreamReader.ArrayValue) { BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) value; if (array.itemType.isPrimitive()) { @@ -600,6 +593,9 @@ public short[] getShortArray(String colName) { @Override public String[] getStringArray(String colName) { Object value = readValue(colName); + if (value == null) { + return null; + } if (value instanceof BinaryStreamReader.ArrayValue) { BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) value; int length = array.length; @@ -671,12 +667,63 @@ public BigDecimal getBigDecimal(int index) { @Override public Instant getInstant(int index) { - return getInstant(schema.columnIndexToName(index)); + ClickHouseColumn column = schema.getColumnByIndex(index); + switch (column.getEffectiveDataType()) { + case Date: + case Date32: + LocalDate date = getLocalDate(index); + return date == null ? null : date.atStartOfDay(ZoneId.of("UTC")).toInstant(); + case Time: + case Time64: + LocalDateTime dt = getLocalDateTime(index); + return dt == null ? null : dt.toInstant(ZoneOffset.UTC); + case DateTime: + case DateTime64: + case DateTime32: + ZonedDateTime zdt = readValue(index); + return zdt.toInstant(); + case Dynamic: + case Variant: + Object value = readValue(index); + Instant instant = objectToInstant(value); + if (value == null || instant != null) { + return instant; + } + break; + } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); + } + + static Instant objectToInstant(Object value) { + if (value instanceof LocalDateTime) { + LocalDateTime dateTime = (LocalDateTime) value; + return Instant.from(dateTime.atZone(ZoneId.of("UTC"))); + } else if (value instanceof ZonedDateTime) { + ZonedDateTime dateTime = (ZonedDateTime) value; + return dateTime.toInstant(); + } + return null; } @Override public ZonedDateTime getZonedDateTime(int index) { - return readValue(index); + ClickHouseColumn column = schema.getColumnByIndex(index); + switch (column.getEffectiveDataType()) { + case DateTime: + case DateTime64: + case DateTime32: + return readValue(index); + case Dynamic: + case Variant: + Object value = readValue(index); + if (value == null) { + return null; + } else if (value instanceof ZonedDateTime) { + return (ZonedDateTime) value; + } + break; + } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); } @Override @@ -691,12 +738,14 @@ public TemporalAmount getTemporalAmount(int index) { @Override public Inet4Address getInet4Address(int index) { - return InetAddressConverter.convertToIpv4(readValue(index)); + Object val = readValue(index); + return val == null ? null : InetAddressConverter.convertToIpv4((java.net.InetAddress) val); } @Override public Inet6Address getInet6Address(int index) { - return InetAddressConverter.convertToIpv6(readValue(index)); + Object val = readValue(index); + return val == null ? null : InetAddressConverter.convertToIpv6((java.net.InetAddress) val); } @Override @@ -782,6 +831,9 @@ public Object[] getTuple(String colName) { @Override public byte getEnum8(String colName) { BinaryStreamReader.EnumValue enumValue = readValue(colName); + if (enumValue == null) { + throw new NullValueException("Column " + colName + " has null value and it cannot be cast to byte"); + } return enumValue.byteValue(); } @@ -793,6 +845,9 @@ public byte getEnum8(int index) { @Override public short getEnum16(String colName) { BinaryStreamReader.EnumValue enumValue = readValue(colName); + if (enumValue == null) { + throw new NullValueException("Column " + colName + " has null value and it cannot be cast to short"); + } return enumValue.shortValue(); } @@ -803,45 +858,148 @@ public short getEnum16(int index) { @Override public LocalDate getLocalDate(String colName) { - Object value = readValue(colName); - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toLocalDate(); + return getLocalDate(schema.nameToColumnIndex(colName)); + } + + @Override + public LocalDate getLocalDate(int index) { + ClickHouseColumn column = schema.getColumnByIndex(index); + switch(column.getEffectiveDataType()) { + case Date: + case Date32: + return readValue(index); + case DateTime: + case DateTime32: + case DateTime64: + ZonedDateTime zdt = readValue(index); + return zdt == null ? null : zdt.toLocalDate(); + case Dynamic: + case Variant: + Object value = readValue(index); + LocalDate localDate = objectToLocalDate(value); + if (value == null || localDate != null) { + return localDate; + } + break; } - return (LocalDate) value; + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); + } + static LocalDate objectToLocalDate(Object value) { + if (value instanceof LocalDate) { + return (LocalDate) value; + } else if (value instanceof ZonedDateTime) { + return ((ZonedDateTime)value).toLocalDate(); + } else if (value instanceof LocalDateTime) { + return ((LocalDateTime)value).toLocalDate(); + } + return null; } @Override - public LocalDate getLocalDate(int index) { - return getLocalDate(schema.columnIndexToName(index)); + public LocalTime getLocalTime(String colName) { + return getLocalTime(schema.nameToColumnIndex(colName)); } @Override - public LocalDateTime getLocalDateTime(String colName) { - Object value = readValue(colName); - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toLocalDateTime(); + public LocalTime getLocalTime(int index) { + ClickHouseColumn column = schema.getColumnByIndex(index); + switch(column.getEffectiveDataType()) { + case Time: + case Time64: + LocalDateTime dt = readValue(index); + return dt == null ? null : dt.toLocalTime(); + case DateTime: + case DateTime32: + case DateTime64: + ZonedDateTime zdt = readValue(index); + return zdt == null ? null : zdt.toLocalTime(); + case Dynamic: + case Variant: + Object value = readValue(index); + LocalTime localTime = objectToLocalTime(value); + if (value == null || localTime != null) { + return localTime; + } + break; } - return (LocalDateTime) value; + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalTime"); + } + + static LocalTime objectToLocalTime(Object value) { + if (value instanceof LocalDateTime) { + return ((LocalDateTime)value).toLocalTime(); + } else if (value instanceof ZonedDateTime) { + return ((ZonedDateTime)value).toLocalTime(); + } + return null; + } + + @Override + public LocalDateTime getLocalDateTime(String colName) { + return getLocalDateTime(schema.nameToColumnIndex(colName)); } @Override public LocalDateTime getLocalDateTime(int index) { - return getLocalDateTime(schema.columnIndexToName(index)); + ClickHouseColumn column = schema.getColumnByIndex(index); + switch(column.getEffectiveDataType()) { + case Time: + case Time64: + return readValue(index); + case DateTime: + case DateTime32: + case DateTime64: + ZonedDateTime zdt = readValue(index); + return zdt == null ? null : zdt.toLocalDateTime(); + case Dynamic: + case Variant: + Object value = readValue(index); + LocalDateTime ldt = objectToLocalDateTime(value); + if (value == null || ldt != null) { + return ldt; + } + break; + + } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDateTime"); + } + + static LocalDateTime objectToLocalDateTime(Object value) { + if (value instanceof LocalDateTime) { + return (LocalDateTime) value; + } else if (value instanceof ZonedDateTime) { + return ((ZonedDateTime)value).toLocalDateTime(); + } + + return null; } @Override public OffsetDateTime getOffsetDateTime(String colName) { - Object value = readValue(colName); - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toOffsetDateTime(); - } - return (OffsetDateTime) value; + return getOffsetDateTime(schema.nameToColumnIndex(colName)); } @Override public OffsetDateTime getOffsetDateTime(int index) { - return getOffsetDateTime(schema.columnIndexToName(index)); + ClickHouseColumn column = schema.getColumnByIndex(index); + switch(column.getEffectiveDataType()) { + case DateTime: + case DateTime32: + case DateTime64: + ZonedDateTime zdt = readValue(index); + return zdt == null ? null : zdt.toOffsetDateTime(); + case Dynamic: + case Variant: + Object value = readValue(index); + if (value == null) { + return null; + } else if (value instanceof ZonedDateTime) { + return ((ZonedDateTime) value).toOffsetDateTime(); + } + + } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to OffsetDateTime"); } @Override diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecord.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecord.java index 8b534b4fd..e06b5225a 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecord.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecord.java @@ -375,6 +375,16 @@ public LocalDate getLocalDate(int index) { return reader.getLocalDate(index); } + @Override + public LocalTime getLocalTime(String colName) { + return reader.getLocalTime(colName); + } + + @Override + public LocalTime getLocalTime(int index) { + return reader.getLocalTime(index); + } + @Override public LocalDateTime getLocalDateTime(String colName) { return reader.getLocalDateTime(colName); 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 c1c4faadf..a51dfae85 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 @@ -1,6 +1,7 @@ package com.clickhouse.client.api.data_formats.internal; import com.clickhouse.client.api.ClientException; +import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.data.ClickHouseEnum; @@ -21,6 +22,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.Period; import java.time.ZonedDateTime; import java.time.temporal.TemporalAmount; @@ -179,9 +181,9 @@ public T readValue(ClickHouseColumn column, Class typeHint) throws IOExce return (T) new EnumValue(name == null ? "" : name, enum16Val); } case Date: - return convertDateTime(readDate(timezone), typeHint); + return (T) readDateAsLocalDate(); case Date32: - return convertDateTime(readDate32(timezone), typeHint); + return (T) readDate32AaLocalDate(); case DateTime: return convertDateTime(readDateTime32(timezone), typeHint); case DateTime32: @@ -189,9 +191,9 @@ public T readValue(ClickHouseColumn column, Class typeHint) throws IOExce case DateTime64: return convertDateTime(readDateTime64(scale, timezone), typeHint); case Time: - return (T) (Integer) readIntLE(); + return (T) readTime(); case Time64: - return (T) (Long) (readLongLE()); + return (T) readTime64(scale); case IntervalYear: case IntervalQuarter: case IntervalMonth: @@ -969,6 +971,22 @@ public ZonedDateTime readDate32(TimeZone tz) return readDate32(input, bufferAllocator.allocate(INT32_SIZE), tz); } + public LocalDate readDateAsLocalDate() throws IOException { + return LocalDate.ofEpochDay(readUnsignedShortLE()); + } + + public LocalDate readDate32AaLocalDate() throws IOException { + return LocalDate.ofEpochDay(readIntLE()); + } + + public LocalDateTime readTime() throws IOException { + return DataTypeUtils.localTimeFromTime64Integer(0, readIntLE()); + } + + public LocalDateTime readTime64(int precision) throws IOException { + return DataTypeUtils.localTimeFromTime64Integer(precision, readLongLE()); + } + /** * Reads a date32 from input stream. * diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java index d30277b7a..7c33cece6 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java @@ -21,6 +21,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -77,6 +78,9 @@ private T readNumberValue(String colName, NumberConverter.NumberType targetT if (converter != null) { Object value = readValue(colName); if (value == null) { + if (targetType == NumberConverter.NumberType.BigInteger || targetType == NumberConverter.NumberType.BigDecimal) { + return null; + } throw new NullValueException("Column " + colName + " has null value and it cannot be cast to " + targetType.getTypeName()); } @@ -136,41 +140,42 @@ public BigDecimal getBigDecimal(String colName) { @Override public Instant getInstant(String colName) { ClickHouseColumn column = schema.getColumnByName(colName); - switch (column.getDataType()) { + int colIndex = column.getColumnIndex(); + switch (column.getEffectiveDataType()) { case Date: case Date32: - LocalDate data = readValue(colName); - return data.atStartOfDay().toInstant(ZoneOffset.UTC); - case DateTime: - case DateTime64: - LocalDateTime dateTime = readValue(colName); - return dateTime.toInstant(column.getTimeZone().toZoneId().getRules().getOffset(dateTime)); + LocalDate date = getLocalDate(colIndex); + return date == null ? null : Instant.from(date); case Time: - return Instant.ofEpochSecond(getLong(colName)); case Time64: - return DataTypeUtils.instantFromTime64Integer(column.getScale(), getLong(colName)); - + LocalDateTime time = getLocalDateTime(colName); + return time == null ? null : time.toInstant(ZoneOffset.UTC); + case DateTime: + case DateTime64: + case DateTime32: + ZonedDateTime zdt = getZonedDateTime(colName); + return zdt == null ? null : zdt.toInstant(); + case Dynamic: + case Variant: + Object value = readValue(colName); + Instant instant = AbstractBinaryFormatReader.objectToInstant(value); + if (value == null || instant != null) { + return instant; + } + break; } throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); } @Override public ZonedDateTime getZonedDateTime(String colName) { - ClickHouseColumn column = schema.getColumnByName(colName); - switch (column.getDataType()) { - case DateTime: - case DateTime64: - case Date: - case Date32: - return readValue(colName); - } - - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); + return getZonedDateTime(schema.nameToColumnIndex(colName)); } @Override public Duration getDuration(String colName) { - return readValue(colName); + TemporalAmount temporalAmount = readValue(colName); + return temporalAmount == null ? null : Duration.from(temporalAmount); } @Override @@ -180,12 +185,14 @@ public TemporalAmount getTemporalAmount(String colName) { @Override public Inet4Address getInet4Address(String colName) { - return InetAddressConverter.convertToIpv4(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : InetAddressConverter.convertToIpv4((java.net.InetAddress) val); } @Override public Inet6Address getInet6Address(String colName) { - return InetAddressConverter.convertToIpv6(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : InetAddressConverter.convertToIpv6((java.net.InetAddress) val); } @Override @@ -195,28 +202,35 @@ public UUID getUUID(String colName) { @Override public ClickHouseGeoPointValue getGeoPoint(String colName) { - return ClickHouseGeoPointValue.of(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : ClickHouseGeoPointValue.of((double[]) val); } @Override public ClickHouseGeoRingValue getGeoRing(String colName) { - return ClickHouseGeoRingValue.of(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : ClickHouseGeoRingValue.of((double[][]) val); } @Override public ClickHouseGeoPolygonValue getGeoPolygon(String colName) { - return ClickHouseGeoPolygonValue.of(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : ClickHouseGeoPolygonValue.of((double[][][]) val); } @Override public ClickHouseGeoMultiPolygonValue getGeoMultiPolygon(String colName) { - return ClickHouseGeoMultiPolygonValue.of(readValue(colName)); + Object val = readValue(colName); + return val == null ? null : ClickHouseGeoMultiPolygonValue.of((double[][][][]) val); } @Override public List getList(String colName) { Object value = readValue(colName); + if (value == null) { + return null; + } if (value instanceof BinaryStreamReader.ArrayValue) { return ((BinaryStreamReader.ArrayValue) value).asList(); } else if (value instanceof List) { @@ -229,6 +243,9 @@ public List getList(String colName) { private T getPrimitiveArray(String colName) { BinaryStreamReader.ArrayValue array = readValue(colName); + if (array == null) { + return null; + } if (array.itemType.isPrimitive()) { return (T) array.array; } else { @@ -273,7 +290,22 @@ public short[] getShortArray(String colName) { @Override public String[] getStringArray(String colName) { - return getPrimitiveArray(colName); + Object value = readValue(colName); + if (value == null) { + return null; + } + if (value instanceof BinaryStreamReader.ArrayValue) { + BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) value; + int length = array.length; + if (!array.itemType.equals(String.class)) + throw new ClientException("Not A String type."); + String [] values = new String[length]; + for (int i = 0; i < length; i++) { + values[i] = (String)((BinaryStreamReader.ArrayValue) value).get(i); + } + return values; + } + throw new ClientException("Not ArrayValue type."); } @Override @@ -338,7 +370,23 @@ public Instant getInstant(int index) { @Override public ZonedDateTime getZonedDateTime(int index) { - return getZonedDateTime(schema.columnIndexToName(index)); + ClickHouseColumn column = schema.getColumnByIndex(index); + switch (column.getEffectiveDataType()) { + case DateTime: + case DateTime64: + case DateTime32: + return readValue(index); + case Dynamic: + case Variant: + Object value = readValue(index); + if (value == null) { + return null; + } else if (value instanceof ZonedDateTime) { + return (ZonedDateTime) value; + } + break; + } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); } @Override @@ -353,12 +401,14 @@ public TemporalAmount getTemporalAmount(int index) { @Override public Inet4Address getInet4Address(int index) { - return InetAddressConverter.convertToIpv4(readValue(index)); + Object val = readValue(index); + return val == null ? null : InetAddressConverter.convertToIpv4((java.net.InetAddress) val); } @Override public Inet6Address getInet6Address(int index) { - return InetAddressConverter.convertToIpv6(readValue(index)); + Object val = readValue(index); + return val == null ? null : InetAddressConverter.convertToIpv6((java.net.InetAddress) val); } @Override @@ -443,22 +493,36 @@ public Object[] getTuple(String colName) { @Override public byte getEnum8(String colName) { - return readValue(colName); + Object val = readValue(colName); + if (val == null) { + throw new NullValueException("Column " + colName + " has null value and it cannot be cast to byte"); + } + if (val instanceof BinaryStreamReader.EnumValue) { + return ((BinaryStreamReader.EnumValue) val).byteValue(); + } + return (byte) val; } @Override public byte getEnum8(int index) { - return readValue(index); + return getEnum8(schema.columnIndexToName(index)); } @Override public short getEnum16(String colName) { - return readValue(colName); + Object val = readValue(colName); + if (val == null) { + throw new NullValueException("Column " + colName + " has null value and it cannot be cast to short"); + } + if (val instanceof BinaryStreamReader.EnumValue) { + return ((BinaryStreamReader.EnumValue) val).shortValue(); + } + return (short) val; } @Override public short getEnum16(int index) { - return readValue(index); + return getEnum16(schema.columnIndexToName(index)); } @Override @@ -468,21 +532,81 @@ public LocalDate getLocalDate(int index) { @Override public LocalDate getLocalDate(String colName) { - Object value = readValue(colName); - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toLocalDate(); + ClickHouseColumn column = schema.getColumnByName(colName); + switch(column.getEffectiveDataType()) { + case Date: + case Date32: + return (LocalDate) getObject(colName); + case DateTime: + case DateTime32: + case DateTime64: + LocalDateTime dt = getLocalDateTime(colName); + return dt == null ? null : dt.toLocalDate(); + case Dynamic: + case Variant: + Object value = getObject(colName); + LocalDate localDate = AbstractBinaryFormatReader.objectToLocalDate(value); + if (value == null || localDate != null) { + return localDate; + } + break; + } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); + } + + @Override + public LocalTime getLocalTime(String colName) { + ClickHouseColumn column = schema.getColumnByName(colName); + switch(column.getEffectiveDataType()) { + case Time: + case Time64: + LocalDateTime val = (LocalDateTime) getObject(colName); + return val == null ? null : val.toLocalTime(); + case DateTime: + case DateTime32: + case DateTime64: + LocalDateTime dt = getLocalDateTime(colName); + return dt == null ? null : dt.toLocalTime(); + case Dynamic: + case Variant: + Object value = getObject(colName); + LocalTime localTime = AbstractBinaryFormatReader.objectToLocalTime(value); + if (value == null || localTime != null) { + return localTime; + } + break; } - return (LocalDate) value; + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalTime"); + } + @Override + public LocalTime getLocalTime(int index) { + return getLocalTime(schema.columnIndexToName(index)); } @Override public LocalDateTime getLocalDateTime(String colName) { - Object value = readValue(colName); - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toLocalDateTime(); + ClickHouseColumn column = schema.getColumnByName(colName); + switch(column.getEffectiveDataType()) { + case Time: + case Time64: + // Types present wide range of value so LocalDateTime let to access to actual value + return (LocalDateTime) getObject(colName); + case DateTime: + case DateTime32: + case DateTime64: + ZonedDateTime val = (ZonedDateTime) readValue(colName); + return val == null ? null : val.toLocalDateTime(); + case Dynamic: + case Variant: + Object value = getObject(colName); + LocalDateTime localDateTime = AbstractBinaryFormatReader.objectToLocalDateTime(value); + if (value == null || localDateTime != null) { + return localDateTime; + } + break; } - return (LocalDateTime) value; + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDateTime"); } @Override @@ -492,11 +616,18 @@ public LocalDateTime getLocalDateTime(int index) { @Override public OffsetDateTime getOffsetDateTime(String colName) { - Object value = readValue(colName); - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toOffsetDateTime(); + ClickHouseColumn column = schema.getColumnByName(colName); + switch(column.getEffectiveDataType()) { + case DateTime: + case DateTime32: + case DateTime64: + case Dynamic: + case Variant: + ZonedDateTime val = getZonedDateTime(colName); + return val == null ? null : val.toOffsetDateTime(); + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to OffsetDataTime"); } - return (OffsetDateTime) value; } @Override diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/GenericRecord.java b/client-v2/src/main/java/com/clickhouse/client/api/query/GenericRecord.java index 9f43ea24d..e50dc82ee 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/query/GenericRecord.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/query/GenericRecord.java @@ -506,6 +506,10 @@ public interface GenericRecord { LocalDate getLocalDate(int index); + LocalTime getLocalTime(String colName); + + LocalTime getLocalTime(int index); + LocalDateTime getLocalDateTime(String colName); LocalDateTime getLocalDateTime(int index); diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java new file mode 100644 index 000000000..befa6ee87 --- /dev/null +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java @@ -0,0 +1,455 @@ +package com.clickhouse.client.api.data_formats.internal; + +import com.clickhouse.client.BaseIntegrationTest; +import com.clickhouse.client.ClickHouseNode; +import com.clickhouse.client.ClickHouseProtocol; +import com.clickhouse.client.ClickHouseServerForTest; +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.command.CommandSettings; +import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; +import com.clickhouse.client.api.enums.Protocol; +import com.clickhouse.client.api.query.GenericRecord; +import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.data.ClickHouseVersion; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.List; + +@Test(groups = {"integration"}) +public class BaseReaderTests extends BaseIntegrationTest { + + private Client client; + + @BeforeMethod(groups = {"integration"}) + public void setUp() { + client = newClient().build(); + } + + @AfterMethod(groups = {"integration"}) + public void tearDown() { + if (client != null) { + client.close(); + } + } + + @Test(groups = {"integration"}) + public void testReadingLocalDateFromDynamic() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_local_date_from_dynamic"; + final LocalDate expectedDate = LocalDate.of(2025, 7, 15); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Dynamic"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_dynamic_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '" + expectedDate + "'::Date)").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + LocalDate actualDate = reader.getLocalDate("field"); + Assert.assertEquals(actualDate, expectedDate); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + LocalDate actualDate = records.get(0).getLocalDate("field"); + Assert.assertEquals(actualDate, expectedDate); + } + + @Test(groups = {"integration"}) + public void testReadingLocalDateTimeFromDynamic() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_local_datetime_from_dynamic"; + final LocalDateTime expectedDateTime = LocalDateTime.of(2025, 7, 15, 14, 30, 45); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Dynamic"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_dynamic_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '" + expectedDateTime + "'::DateTime64(3))").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + LocalDateTime actualDateTime = reader.getLocalDateTime("field"); + Assert.assertEquals(actualDateTime, expectedDateTime); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + LocalDateTime actualDateTime = records.get(0).getLocalDateTime("field"); + Assert.assertEquals(actualDateTime, expectedDateTime); + } + + @Test(groups = {"integration"}) + public void testReadingLocalTimeFromDynamic() throws Exception { + if (isVersionMatch("(,25.5]")) { + return; + } + + final String table = "test_reading_local_time_from_dynamic"; + final LocalTime expectedTime = LocalTime.of(14, 30, 45, 123000000); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Dynamic"), + (CommandSettings) new CommandSettings() + .serverSetting("allow_experimental_dynamic_type", "1") + .serverSetting("allow_experimental_time_time64_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '14:30:45.123'::Time64(3))", + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_time_time64_type", "1")).get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + LocalTime actualTime = reader.getLocalTime("field"); + Assert.assertEquals(actualTime, expectedTime); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + LocalTime actualTime = records.get(0).getLocalTime("field"); + Assert.assertEquals(actualTime, expectedTime); + } + + @Test(groups = {"integration"}) + public void testReadingZonedDateTimeFromDynamic() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_zoned_datetime_from_dynamic"; + final ZoneId zoneId = ZoneId.of("Europe/Berlin"); + final ZonedDateTime expectedZonedDateTime = ZonedDateTime.of(2025, 7, 15, 14, 30, 45, 0, zoneId); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Dynamic"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_dynamic_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '2025-07-15 14:30:45'::DateTime64(3, 'Europe/Berlin'))").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + ZonedDateTime actualZonedDateTime = reader.getZonedDateTime("field"); + Assert.assertEquals(actualZonedDateTime, expectedZonedDateTime); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + ZonedDateTime actualZonedDateTime = records.get(0).getZonedDateTime("field"); + Assert.assertEquals(actualZonedDateTime, expectedZonedDateTime); + } + + @Test(groups = {"integration"}) + public void testReadingInstantFromDynamic() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_instant_from_dynamic"; + final Instant expectedInstant = Instant.parse("2025-07-15T12:30:45.123Z"); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Dynamic"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_dynamic_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '2025-07-15 12:30:45.123'::DateTime64(3, 'UTC'))").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + Instant actualInstant = reader.getInstant("field"); + Assert.assertEquals(actualInstant, expectedInstant); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + Instant actualInstant = records.get(0).getInstant("field"); + Assert.assertEquals(actualInstant, expectedInstant); + } + + @Test(groups = {"integration"}) + public void testReadingOffsetDateTimeFromDynamic() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_offset_datetime_from_dynamic"; + final OffsetDateTime expectedOffsetDateTime = OffsetDateTime.of(2025, 7, 15, 14, 30, 45, 0, ZoneOffset.ofHours(2)); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Dynamic"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_dynamic_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '2025-07-15 14:30:45'::DateTime64(3, 'Europe/Berlin'))").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + OffsetDateTime actualOffsetDateTime = reader.getOffsetDateTime("field"); + Assert.assertEquals(actualOffsetDateTime, expectedOffsetDateTime); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + OffsetDateTime actualOffsetDateTime = records.get(0).getOffsetDateTime("field"); + Assert.assertEquals(actualOffsetDateTime, expectedOffsetDateTime); + } + + + @Test(groups = {"integration"}) + public void testReadingLocalDateFromVariant() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_local_date_from_variant"; + final LocalDate expectedDate = LocalDate.of(2025, 7, 15); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Variant(Date, String)"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_variant_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '" + expectedDate + "'::Date)").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + LocalDate actualDate = reader.getLocalDate("field"); + Assert.assertEquals(actualDate, expectedDate); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + LocalDate actualDate = records.get(0).getLocalDate("field"); + Assert.assertEquals(actualDate, expectedDate); + } + + @Test(groups = {"integration"}) + public void testReadingLocalDateTimeFromVariant() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_local_datetime_from_variant"; + final LocalDateTime expectedDateTime = LocalDateTime.of(2025, 7, 15, 14, 30, 45); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Variant(DateTime64(3), String)"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_variant_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '" + expectedDateTime + "'::DateTime64(3))").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + LocalDateTime actualDateTime = reader.getLocalDateTime("field"); + Assert.assertEquals(actualDateTime, expectedDateTime); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + LocalDateTime actualDateTime = records.get(0).getLocalDateTime("field"); + Assert.assertEquals(actualDateTime, expectedDateTime); + } + + @Test(groups = {"integration"}) + public void testReadingLocalTimeFromVariant() throws Exception { + if (isVersionMatch("(,25.5]")) { + return; + } + + final String table = "test_reading_local_time_from_variant"; + final LocalTime expectedTime = LocalTime.of(14, 30, 45, 123000000); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Variant(Time64(3), String)"), + (CommandSettings) new CommandSettings() + .serverSetting("allow_experimental_variant_type", "1") + .serverSetting("allow_experimental_time_time64_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '14:30:45.123'::Time64(3))", (CommandSettings) new CommandSettings() + .serverSetting("allow_experimental_time_time64_type", "1")).get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + LocalTime actualTime = reader.getLocalTime("field"); + Assert.assertEquals(actualTime, expectedTime); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + LocalTime actualTime = records.get(0).getLocalTime("field"); + Assert.assertEquals(actualTime, expectedTime); + } + + @Test(groups = {"integration"}) + public void testReadingZonedDateTimeFromVariant() throws Exception { + if (isVersionMatch("(,25.3]")) { + return; + } + + final String table = "test_reading_zoned_datetime_from_variant"; + final ZoneId zoneId = ZoneId.of("Europe/Berlin"); + final ZonedDateTime expectedZonedDateTime = ZonedDateTime.of(2025, 7, 15, 14, 30, 45, 0, zoneId); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Variant(DateTime64(3, 'Europe/Berlin'), String)"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_variant_type", "1") + .serverSetting("allow_experimental_time_time64_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '2025-07-15 14:30:45'::DateTime64(3, 'Europe/Berlin'))").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + ZonedDateTime actualZonedDateTime = reader.getZonedDateTime("field"); + Assert.assertEquals(actualZonedDateTime, expectedZonedDateTime); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + ZonedDateTime actualZonedDateTime = records.get(0).getZonedDateTime("field"); + Assert.assertEquals(actualZonedDateTime, expectedZonedDateTime); + } + + @Test(groups = {"integration"}) + public void testReadingInstantFromVariant() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_instant_from_variant"; + final Instant expectedInstant = Instant.parse("2025-07-15T12:30:45.123Z"); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Variant(DateTime64(3, 'UTC'), String)"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_variant_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '2025-07-15 12:30:45.123'::DateTime64(3, 'UTC'))").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + Instant actualInstant = reader.getInstant("field"); + Assert.assertEquals(actualInstant, expectedInstant); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + Instant actualInstant = records.get(0).getInstant("field"); + Assert.assertEquals(actualInstant, expectedInstant); + } + + @Test(groups = {"integration"}) + public void testReadingOffsetDateTimeFromVariant() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_reading_offset_datetime_from_variant"; + final OffsetDateTime expectedOffsetDateTime = OffsetDateTime.of(2025, 7, 15, 14, 30, 45, 0, ZoneOffset.ofHours(2)); + + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, "id Int32", "field Variant(DateTime64(3, 'Europe/Berlin'), String)"), + (CommandSettings) new CommandSettings().serverSetting("allow_experimental_variant_type", "1")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, '2025-07-15 14:30:45'::DateTime64(3, 'Europe/Berlin'))").get(); + + // Test with RowBinaryWithNamesAndTypesFormatReader via query() + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + OffsetDateTime actualOffsetDateTime = reader.getOffsetDateTime("field"); + Assert.assertEquals(actualOffsetDateTime, expectedOffsetDateTime); + } + + // Test with GenericRecord via queryAll() + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + OffsetDateTime actualOffsetDateTime = records.get(0).getOffsetDateTime("field"); + Assert.assertEquals(actualOffsetDateTime, expectedOffsetDateTime); + } + + + public static String tableDefinition(String table, String... columns) { + StringBuilder sb = new StringBuilder(); + sb.append("CREATE TABLE " + table + " ( "); + Arrays.stream(columns).forEach(s -> { + sb.append(s).append(", "); + }); + sb.setLength(sb.length() - 2); + sb.append(") Engine = MergeTree ORDER BY ()"); + return sb.toString(); + } + + + private boolean isVersionMatch(String versionExpression) { + List serverVersion = client.queryAll("SELECT version()"); + return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); + } + + private Client.Builder newClient() { + ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); + return new Client.Builder() + .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), isCloud()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()); + } + +} \ No newline at end of file diff --git a/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java index 6cd659c43..bb606592b 100644 --- a/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java @@ -5,6 +5,7 @@ import com.clickhouse.client.ClickHouseProtocol; import com.clickhouse.client.ClickHouseServerForTest; import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.ClientException; import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; @@ -35,8 +36,13 @@ import java.math.RoundingMode; import java.sql.Connection; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.Period; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; @@ -438,6 +444,9 @@ public void testVariantWithTime64Types() throws Exception { if (isVersionMatch("(,25.5]")) { return; // time64 was introduced in 25.6 } + + LocalDateTime epochZero = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC); + testVariantWith("Time", new String[]{"field Variant(Time, String)"}, new Object[]{ "30:33:30", @@ -445,17 +454,27 @@ public void testVariantWithTime64Types() throws Exception { }, new String[]{ "30:33:30", - "360630", // Time stored as integer by default + epochZero.plusHours(100).plusMinutes(10).plusSeconds(30).toString() }); - testVariantWith("Time64", new String[]{"field Variant(Time64, String)"}, + testVariantWith("Time64", new String[]{"field Variant(Time64(0), String)"}, new Object[]{ "30:33:30", TimeUnit.HOURS.toSeconds(100) + TimeUnit.MINUTES.toSeconds(10) + 30 }, new String[]{ "30:33:30", - "360630", + epochZero.plusHours(100).plusMinutes(10).plusSeconds(30).toString() + }); + + testVariantWith("Time64", new String[]{"field Variant(Time64, String)"}, + new Object[]{ + "30:33:30", + TimeUnit.HOURS.toMillis(100) + TimeUnit.MINUTES.toMillis(10) + TimeUnit.SECONDS.toMillis(30) + }, + new String[]{ + "30:33:30", + epochZero.plusHours(100).plusMinutes(10).plusSeconds(30).toString() }); } @@ -559,7 +578,7 @@ public void testDynamicWithPrimitives() throws Exception { case Decimal128: case Decimal256: BigDecimal tmpDec = row.getBigDecimal("field").stripTrailingZeros(); - if (tmpDec.divide((BigDecimal)value, RoundingMode.FLOOR).equals(BigDecimal.ONE)) { + if (tmpDec.divide((BigDecimal)value, RoundingMode.UNNECESSARY).equals(BigDecimal.ONE)) { continue; } strValue = tmpDec.toPlainString(); @@ -688,13 +707,12 @@ public void testDynamicWithTime64Types() throws Exception { Instant maxTime64 = Instant.ofEpochSecond(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59, 123456789); - long maxTime64Value = maxTime64.getEpochSecond() * 1_000_000_000 + maxTime64.getNano(); testDynamicWith("Time64", new Object[]{ maxTime64, }, new String[]{ - String.valueOf(maxTime64Value) + LocalDateTime.ofInstant(maxTime64, ZoneId.of("UTC")).toString() }); } @@ -848,100 +866,61 @@ public void testTimeDataType() throws Exception { GenericRecord record = records.get(0); Assert.assertEquals(record.getInteger("o_num"), 1); - Assert.assertEquals(record.getInteger("time"), TimeUnit.HOURS.toSeconds(999)); + Assert.assertEquals(record.getLocalDateTime("time").toEpochSecond(ZoneOffset.UTC), TimeUnit.HOURS.toSeconds(999)); Assert.assertEquals(record.getInstant("time"), Instant.ofEpochSecond(TimeUnit.HOURS.toSeconds(999))); record = records.get(1); Assert.assertEquals(record.getInteger("o_num"), 2); - Assert.assertEquals(record.getInteger("time"), TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59); + Assert.assertEquals(record.getLocalDateTime("time").toEpochSecond(ZoneOffset.UTC), TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59); Assert.assertEquals(record.getInstant("time"), Instant.ofEpochSecond(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); record = records.get(2); Assert.assertEquals(record.getInteger("o_num"), 3); - Assert.assertEquals(record.getInteger("time"), 0); + Assert.assertEquals(record.getLocalDateTime("time").toEpochSecond(ZoneOffset.UTC), 0); Assert.assertEquals(record.getInstant("time"), Instant.ofEpochSecond(0)); record = records.get(3); Assert.assertEquals(record.getInteger("o_num"), 4); - Assert.assertEquals(record.getInteger("time"), - (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); + Assert.assertEquals(record.getLocalDateTime("time").toEpochSecond(ZoneOffset.UTC), - (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); Assert.assertEquals(record.getInstant("time"), Instant.ofEpochSecond(- (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59))); } - @Test(groups = {"integration"}) - public void testTime64() throws Exception { + @Test(groups = {"integration"}, dataProvider = "testTimeData") + public void testTime(String column, String value, LocalDateTime expectedDt) throws Exception { if (isVersionMatch("(,25.5]")) { return; // time64 was introduced in 25.6 } - String table = "data_type_tests_time64"; - client.execute("DROP TABLE IF EXISTS " + table).get(); - client.execute(tableDefinition(table, "o_num UInt32", "t_sec Time64(0)", "t_ms Time64(3)", "t_us Time64(6)", "t_ns Time64(9)"), - (CommandSettings) new CommandSettings().serverSetting("allow_experimental_time_time64_type", "1")).get(); - - String[][] values = new String[][] { - {"00:01:00.123", "00:01:00.123", "00:01:00.123456", "00:01:00.123456789"}, - {"-00:01:00.123", "-00:01:00.123", "-00:01:00.123456", "-00:01:00.123456789"}, - {"-999:59:59.999", "-999:59:59.999", "-999:59:59.999999", "-999:59:59.999999999"}, - {"999:59:59.999", "999:59:59.999", "999:59:59.999999", "999:59:59.999999999"}, - }; - - Long[][] expectedValues = new Long[][] { - {timeToSec(0, 1,0), timeToMs(0, 1,0) + 123, timeToUs(0, 1,0) + 123456, timeToNs(0, 1,0) + 123456789}, - {-timeToSec(0, 1,0), -(timeToMs(0, 1,0) + 123), -(timeToUs(0, 1,0) + 123456), -(timeToNs(0, 1,0) + 123456789)}, - {-timeToSec(999,59, 59), -(timeToMs(999,59, 59) + 999), - -(timeToUs(999, 59, 59) + 999999), -(timeToNs(999, 59, 59) + 999999999)}, - {timeToSec(999,59, 59), timeToMs(999,59, 59) + 999, - timeToUs(999, 59, 59) + 999999, timeToNs(999, 59, 59) + 999999999}, - }; - - String[][] expectedInstantStrings = new String[][] { - {"1970-01-01T00:01:00Z", - "1970-01-01T00:01:00.123Z", - "1970-01-01T00:01:00.123456Z", - "1970-01-01T00:01:00.123456789Z"}, - - {"1969-12-31T23:59:00Z", - "1969-12-31T23:58:59.877Z", - "1969-12-31T23:58:59.876544Z", - "1969-12-31T23:58:59.876543211Z"}, - - {"1969-11-20T08:00:01Z", - "1969-11-20T08:00:00.001Z", - "1969-11-20T08:00:00.000001Z", - "1969-11-20T08:00:00.000000001Z"}, - - - {"1970-02-11T15:59:59Z", - "1970-02-11T15:59:59.999Z", - "1970-02-11T15:59:59.999999Z", - "1970-02-11T15:59:59.999999999Z"}, - }; - - for (int i = 0; i < values.length; i++) { - StringBuilder insertSQL = new StringBuilder("INSERT INTO " + table + " VALUES (" + i + ", "); - for (int j = 0; j < values[i].length; j++) { - insertSQL.append("'").append(values[i][j]).append("', "); - } - insertSQL.setLength(insertSQL.length() - 2); - insertSQL.append(");"); - - client.query(insertSQL.toString()).get().close(); - - List records = client.queryAll("SELECT * FROM " + table); + QuerySettings settings = new QuerySettings().serverSetting("allow_experimental_time_time64_type", "1"); + List records = client.queryAll("SELECT \'" + value + "\'::" + column, settings); + LocalDateTime dt = records.get(0).getLocalDateTime(1); + Assert.assertEquals(dt, expectedDt); + } - GenericRecord record = records.get(0); - Assert.assertEquals(record.getInteger("o_num"), i); - for (int j = 0; j < values[i].length; j++) { - Assert.assertEquals(record.getLong(j + 2), expectedValues[i][j], "failed at value " +j); - Instant actualInstant = record.getInstant(j + 2); - Assert.assertEquals(actualInstant.toString(), expectedInstantStrings[i][j], "failed at value " +j); - } + @DataProvider + public static Object[][] testTimeData() { - client.execute("TRUNCATE TABLE " + table).get(); - } + return new Object[][] { + {"Time64", "00:01:00.123", LocalDateTime.parse("1970-01-01T00:01:00.123")}, + {"Time64(3)","00:01:00.123", LocalDateTime.parse("1970-01-01T00:01:00.123")}, + {"Time64(6)","00:01:00.123456", LocalDateTime.parse("1970-01-01T00:01:00.123456")}, + {"Time64(9)","00:01:00.123456789", LocalDateTime.parse("1970-01-01T00:01:00.123456789")}, + {"Time64","-00:01:00.123", LocalDateTime.parse("1969-12-31T23:59:00.123")}, + {"Time64(3)","-00:01:00.123", LocalDateTime.parse("1969-12-31T23:59:00.123")}, + {"Time64(6)","-00:01:00.123456", LocalDateTime.parse("1969-12-31T23:59:00.123456")}, + {"Time64(9)","-00:01:00.123456789", LocalDateTime.parse("1969-12-31T23:59:00.123456789")}, + {"Time64","-999:59:59.999", LocalDateTime.parse("1969-11-20T08:00:01.999")}, + {"Time64(3)","-999:59:59.999", LocalDateTime.parse("1969-11-20T08:00:01.999")}, + {"Time64(6)","-999:59:59.999999", LocalDateTime.parse("1969-11-20T08:00:01.999999")}, + {"Time64(9)","-999:59:59.999999999", LocalDateTime.parse("1969-11-20T08:00:01.999999999")}, + {"Time64","999:59:59.999", LocalDateTime.parse("1970-02-11T15:59:59.999")}, + {"Time64(3)","999:59:59.999", LocalDateTime.parse("1970-02-11T15:59:59.999")}, + {"Time64(6)","999:59:59.999999", LocalDateTime.parse("1970-02-11T15:59:59.999999")}, + {"Time64(9)","999:59:59.999999999", LocalDateTime.parse("1970-02-11T15:59:59.999999999")}, + }; } - + private static long timeToSec(int hours, int minutes, int seconds) { return TimeUnit.HOURS.toSeconds(hours) + TimeUnit.MINUTES.toSeconds(minutes) + seconds; } @@ -1084,6 +1063,88 @@ public static Object[][] testDataTypesAsStringDP() { }; } + @Test(groups = {"integration"}) + public void testDates() throws Exception { + LocalDate date = LocalDate.of(2024, 1, 15); + + String sql = "SELECT toDate('" + date + "') AS d, toDate32('" + date + "') AS d32"; + ZoneId laZone = ZoneId.of("America/Los_Angeles"); + ZoneId tokyoZone = ZoneId.of("Asia/Tokyo"); + ZoneId utcZone = ZoneId.of("UTC"); + + // Los Angeles + LocalDate laDate; + LocalDate laDate32; + QuerySettings laSettings = new QuerySettings() + .setUseServerTimeZone(true) + .serverSetting("session_timezone", laZone.getId()); + try (QueryResponse response = client.query(sql, laSettings).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + + laDate = reader.getLocalDate("d"); + laDate32 = reader.getLocalDate("d32"); + + Assert.assertThrows(ClientException.class, () -> reader.getZonedDateTime("d")); + Assert.assertThrows(ClientException.class, () -> reader.getZonedDateTime("d32")); + Assert.assertThrows(ClientException.class, () -> reader.getLocalDateTime("d")); + Assert.assertThrows(ClientException.class, () -> reader.getLocalDateTime("d32")); + Assert.assertThrows(ClientException.class, () -> reader.getOffsetDateTime("d")); + Assert.assertThrows(ClientException.class, () -> reader.getOffsetDateTime("d32")); + + } + + // Tokyo + LocalDate tokyoDate; + LocalDate tokyoDate32; + QuerySettings tokyoSettings = new QuerySettings() + .setUseServerTimeZone(true) + .serverSetting("session_timezone", tokyoZone.getId()); + try (QueryResponse response = client.query(sql, tokyoSettings).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + reader.next(); + tokyoDate = reader.getLocalDate("d"); + tokyoDate32 = reader.getLocalDate("d32"); + } + + + // Check dates - should be equal + Assert.assertEquals(laDate, date); + Assert.assertEquals(laDate32, date); + Assert.assertEquals(tokyoDate, date); + Assert.assertEquals(tokyoDate32, date); + + + // Verify with session time differ from use timezone - no effect on date + try (Client tzClient = newClient() + .useTimeZone(utcZone.getId()) + .build()) { + QuerySettings tzSettings = new QuerySettings() + .serverSetting("session_timezone", laZone.getId()); + try (QueryResponse response = tzClient.query(sql, tzSettings).get()) { + ClickHouseBinaryFormatReader reader = tzClient.newBinaryFormatReader(response); + reader.next(); + Assert.assertEquals(reader.getLocalDate("d"), date); + Assert.assertEquals(reader.getLocalDate("d32"), date); + } + + LocalDate minDate = LocalDate.of(1970, 1, 1); + LocalDate maxDate = LocalDate.of(2149, 6, 6); + LocalDate minDate32 = LocalDate.of(1900, 1, 1); + LocalDate maxDate32 = LocalDate.of(2299, 12, 31); + String extremesSql = "SELECT toDate('" + minDate + "') AS d_min, toDate('" + maxDate + "') AS d_max, " + + "toDate32('" + minDate32 + "') AS d32_min, toDate32('" + maxDate32 + "') AS d32_max"; + try (QueryResponse response = tzClient.query(extremesSql, tzSettings).get()) { + ClickHouseBinaryFormatReader reader = tzClient.newBinaryFormatReader(response); + reader.next(); + Assert.assertEquals(reader.getLocalDate("d_min"), minDate); + Assert.assertEquals(reader.getLocalDate("d_max"), maxDate); + Assert.assertEquals(reader.getLocalDate("d32_min"), minDate32); + Assert.assertEquals(reader.getLocalDate("d32_max"), maxDate32); + } + } + } + public static String tableDefinition(String table, String... columns) { StringBuilder sb = new StringBuilder(); sb.append("CREATE TABLE " + table + " ( "); @@ -1099,4 +1160,14 @@ private boolean isVersionMatch(String versionExpression) { List serverVersion = client.queryAll("SELECT version()"); return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); } + + private Client.Builder newClient() { + ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); + return new Client.Builder() + .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), isCloud()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .compressClientRequest(useClientCompression) + .useHttpCompression(useHttpCompression); + } } diff --git a/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java b/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java index c1119a834..1a0ee2287 100644 --- a/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java @@ -25,6 +25,7 @@ import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; +import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -394,8 +395,8 @@ public void writeDatetimeTests() throws Exception { new Field("datetime", ZonedDateTime.now()), new Field("datetime_nullable"), new Field("datetime_default").set(ZonedDateTime.parse("2020-01-01T00:00:00+00:00[UTC]")), //DateTime new Field("datetime32", ZonedDateTime.now()), new Field("datetime32_nullable"), new Field("datetime32_default").set(ZonedDateTime.parse("2020-01-01T00:00:00+00:00[UTC]")), //DateTime new Field("datetime64", ZonedDateTime.now()), new Field("datetime64_nullable"), new Field("datetime64_default").set(ZonedDateTime.parse("2025-01-01T00:00:00+00:00[UTC]")), //DateTime64 - new Field("date", ZonedDateTime.parse("2021-01-01T00:00:00+00:00[UTC]")), new Field("date_nullable"), new Field("date_default").set(ZonedDateTime.parse("2020-01-01T00:00:00+00:00[UTC]").toEpochSecond()), //Date - new Field("date32", ZonedDateTime.parse("2021-01-01T00:00:00+00:00[UTC]")), new Field("date32_nullable"), new Field("date32_default").set(ZonedDateTime.parse("2025-01-01T00:00:00+00:00[UTC]").toEpochSecond()) //Date + new Field("date", LocalDate.parse("2021-01-01")), new Field("date_nullable"), new Field("date_default").set(LocalDate.parse("2020-01-01")), //Date + new Field("date32", LocalDate.parse("2021-01-01")), new Field("date32_nullable"), new Field("date32_default").set(LocalDate.parse("2025-01-01")) //Date } }; diff --git a/client-v2/src/test/java/com/clickhouse/client/insert/InsertTests.java b/client-v2/src/test/java/com/clickhouse/client/insert/InsertTests.java index bfbf8434c..ee3239d1d 100644 --- a/client-v2/src/test/java/com/clickhouse/client/insert/InsertTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/insert/InsertTests.java @@ -778,7 +778,7 @@ public void testPOJOWithDynamicType() throws Exception { if (item.rowId == 3) { assertEquals(((ZonedDateTime) item.getNullableAny()).toLocalDateTime(), data.get(i++).getNullableAny()); } else if (item.rowId == 5) { - assertEquals(((ZonedDateTime) item.getNullableAny()).toLocalDate(), data.get(i++).getNullableAny()); + assertEquals(item.getNullableAny(), data.get(i++).getNullableAny()); } else { assertEquals(item, data.get(i++)); } diff --git a/client-v2/src/test/java/com/clickhouse/client/internal/SmallTests.java b/client-v2/src/test/java/com/clickhouse/client/internal/SmallTests.java index 42be10846..57ef3cb5c 100644 --- a/client-v2/src/test/java/com/clickhouse/client/internal/SmallTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/internal/SmallTests.java @@ -1,8 +1,49 @@ package com.clickhouse.client.internal; +import org.testng.annotations.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.concurrent.TimeUnit; + /** * Tests playground */ public class SmallTests { + + @Test + public void testInstantVsLocalTime() { + + // Date + LocalDate longBeforeEpoch = LocalDate.ofEpochDay(-47482); + LocalDate beforeEpoch = LocalDate.ofEpochDay(-1); + LocalDate epoch = LocalDate.ofEpochDay(0); + LocalDate dateMaxValue = LocalDate.ofEpochDay(65535); + LocalDate date32MaxValue = LocalDate.ofEpochDay(47482); + + System.out.println(longBeforeEpoch); + System.out.println(beforeEpoch); + System.out.println(epoch); + System.out.println(date32MaxValue); + System.out.println(dateMaxValue); + + System.out.println(); + + // Time + + LocalDateTime beforeEpochTime = LocalDateTime.ofEpochSecond(-999, 0, ZoneOffset.UTC); + LocalDateTime epochTime = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC); + LocalDateTime maxTime = LocalDateTime.ofEpochSecond(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59, + 123999999, ZoneOffset.UTC); + + System.out.println(beforeEpochTime); + System.out.println("before time: " + (beforeEpochTime.getSecond())); + System.out.println(epochTime); + System.out.println(maxTime); + System.out.println(maxTime.getDayOfYear()); + } } diff --git a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java index b8b1da74d..41a942b9a 100644 --- a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java @@ -839,98 +839,6 @@ public void testConversionOfIpAddresses() throws Exception { Assert.assertThrows(() -> record.getInet4Address(2)); } - @Test(groups = {"integration"}) - public void testDateTimeDataTypes() { - final List columns = Arrays.asList( - "min_date Date", - "max_date Date", - "min_dateTime DateTime", - "max_dateTime DateTime", - "min_dateTime64 DateTime64", - "max_dateTime64 DateTime64", - "min_dateTime64_6 DateTime64(6)", - "max_dateTime64_6 DateTime64(6)", - "min_dateTime64_9 DateTime64(9)", - "max_dateTime64_9 DateTime64(9)" - ); - - final LocalDate minDate = LocalDate.parse("1970-01-01"); - final LocalDate maxDate = LocalDate.parse("2149-06-06"); - final LocalDateTime minDateTime = LocalDateTime.parse("1970-01-01T01:02:03"); - final LocalDateTime maxDateTime = LocalDateTime.parse("2106-02-07T06:28:15"); - final LocalDateTime minDateTime64 = LocalDateTime.parse("1970-01-01T01:02:03.123"); - final LocalDateTime maxDateTime64 = LocalDateTime.parse("2106-02-07T06:28:15.123"); - final LocalDateTime minDateTime64_6 = LocalDateTime.parse("1970-01-01T01:02:03.123456"); - final LocalDateTime maxDateTime64_6 = LocalDateTime.parse("2106-02-07T06:28:15.123456"); - final LocalDateTime minDateTime64_9 = LocalDateTime.parse("1970-01-01T01:02:03.123456789"); - final LocalDateTime maxDateTime64_9 = LocalDateTime.parse("2106-02-07T06:28:15.123456789"); - final List> valueGenerators = Arrays.asList( - () -> sq(minDate.toString()), - () -> sq(maxDate.toString()), - () -> sq(minDateTime.format(DataTypeUtils.DATETIME_FORMATTER)), - () -> sq(maxDateTime.format(DataTypeUtils.DATETIME_FORMATTER)), - () -> sq(minDateTime64.format(DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER)), - () -> sq(maxDateTime64.format(DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER)), - () -> sq(minDateTime64_6.format(DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER)), - () -> sq(maxDateTime64_6.format(DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER)), - () -> sq(minDateTime64_9.format(DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER)), - () -> sq(maxDateTime64_9.format(DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER)) - ); - - final List> verifiers = new ArrayList<>(); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("min_date"), "No value for column min_date found"); - Assert.assertEquals(r.getLocalDate("min_date"), minDate); - Assert.assertEquals(r.getLocalDate(1), minDate); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("max_date"), "No value for column max_date found"); - Assert.assertEquals(r.getLocalDate("max_date"), maxDate); - Assert.assertEquals(r.getLocalDate(2), maxDate); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("min_dateTime"), "No value for column min_dateTime found"); - Assert.assertEquals(r.getLocalDateTime("min_dateTime"), minDateTime); - Assert.assertEquals(r.getLocalDateTime(3), minDateTime); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("max_dateTime"), "No value for column max_dateTime found"); - Assert.assertEquals(r.getLocalDateTime("max_dateTime"), maxDateTime); - Assert.assertEquals(r.getLocalDateTime(4), maxDateTime); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("min_dateTime64"), "No value for column min_dateTime64 found"); - Assert.assertEquals(r.getLocalDateTime("min_dateTime64"), minDateTime64); - Assert.assertEquals(r.getLocalDateTime(5), minDateTime64); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("max_dateTime64"), "No value for column max_dateTime64 found"); - Assert.assertEquals(r.getLocalDateTime("max_dateTime64"), maxDateTime64); - Assert.assertEquals(r.getLocalDateTime(6), maxDateTime64); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("min_dateTime64_6"), "No value for column min_dateTime64_6 found"); - Assert.assertEquals(r.getLocalDateTime("min_dateTime64_6"), minDateTime64_6); - Assert.assertEquals(r.getLocalDateTime(7), minDateTime64_6); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("max_dateTime64_6"), "No value for column max_dateTime64_6 found"); - Assert.assertEquals(r.getLocalDateTime("max_dateTime64_6"), maxDateTime64_6); - Assert.assertEquals(r.getLocalDateTime(8), maxDateTime64_6); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("min_dateTime64_9"), "No value for column min_dateTime64_9 found"); - Assert.assertEquals(r.getLocalDateTime("min_dateTime64_9"), minDateTime64_9); - Assert.assertEquals(r.getLocalDateTime(9), minDateTime64_9); - }); - verifiers.add(r -> { - Assert.assertTrue(r.hasValue("max_dateTime64_9"), "No value for column max_dateTime64_9 found"); - Assert.assertEquals(r.getLocalDateTime("max_dateTime64_9"), maxDateTime64_9); - Assert.assertEquals(r.getLocalDateTime(10), maxDateTime64_9); - }); - - testDataTypes(columns, valueGenerators, verifiers); - } private Consumer createNumberVerifier(String columnName, int columnIndex, int bits, boolean isSigned, diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java index b85d438fa..3793bcf16 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -34,10 +34,15 @@ import java.sql.Time; import java.sql.Timestamp; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Collections; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; public class ResultSetImpl implements ResultSet, JdbcV2Wrapper { @@ -1012,8 +1017,8 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException { public Date getDate(String columnLabel, Calendar cal) throws SQLException { checkClosed(); try { - ZonedDateTime zdt = reader.getZonedDateTime(columnLabel); - if (zdt == null) { + LocalDate ld = reader.getLocalDate(columnLabel); + if (ld == null) { wasNull = true; return null; } @@ -1021,9 +1026,20 @@ public Date getDate(String columnLabel, Calendar cal) throws SQLException { Calendar c = (Calendar) (cal != null ? cal : defaultCalendar).clone(); c.clear(); - c.set(zdt.getYear(), zdt.getMonthValue() - 1, zdt.getDayOfMonth(), 0, 0, 0); + c.set(ld.getYear(), ld.getMonthValue() - 1, ld.getDayOfMonth(), 0, 0, 0); return new Date(c.getTimeInMillis()); } catch (Exception e) { + ClickHouseColumn column = getSchema().getColumnByName(columnLabel); + switch (column.getEffectiveDataType()) { + case Date: + case Date32: + case DateTime64: + case DateTime: + case DateTime32: + break; + default: + throw new SQLException("Value of " + column.getEffectiveDataType() + " type cannot be converted to Date value"); + } throw ExceptionUtils.toSqlState(String.format("Method: getDate(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1038,36 +1054,28 @@ public Time getTime(String columnLabel, Calendar cal) throws SQLException { checkClosed(); try { + LocalDateTime ld = reader.getLocalDateTime(columnLabel); + if (ld == null) { + wasNull = true; + return null; + } + wasNull = false; + Calendar c = cal != null ? cal : defaultCalendar; + long time = ld.atZone(c.getTimeZone().toZoneId()).toEpochSecond() * 1000 + TimeUnit.NANOSECONDS.toMillis(ld.getNano()); + return new Time(time); + } catch (Exception e) { ClickHouseColumn column = getSchema().getColumnByName(columnLabel); - switch (column.getDataType()) { + switch (column.getEffectiveDataType()) { case Time: case Time64: - Instant instant = reader.getInstant(columnLabel); - if (instant == null) { - wasNull = true; - return null; - } - wasNull = false; - return new Time(instant.getEpochSecond() * 1000L + instant.getNano() / 1_000_000); + case DateTime64: case DateTime: case DateTime32: - case DateTime64: - ZonedDateTime zdt = reader.getZonedDateTime(columnLabel); - if (zdt == null) { - wasNull = true; - return null; - } - wasNull = false; - - Calendar c = (Calendar) (cal != null ? cal : defaultCalendar).clone(); - c.clear(); - c.set(1970, Calendar.JANUARY, 1, zdt.getHour(), zdt.getMinute(), zdt.getSecond()); - return new Time(c.getTimeInMillis()); + break; default: - throw new SQLException("Column \"" + columnLabel + "\" is not a time type."); + throw new SQLException("Value of " + column.getEffectiveDataType() + " type cannot be converted to Time value"); } - } catch (Exception e) { throw ExceptionUtils.toSqlState(String.format("Method: getTime(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1095,6 +1103,16 @@ public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLExcept timestamp.setNanos(zdt.getNano()); return timestamp; } catch (Exception e) { + ClickHouseColumn column = getSchema().getColumnByName(columnLabel); + switch (column.getEffectiveDataType()) { + case DateTime64: + case DateTime: + case DateTime32: + break; + default: + throw new SQLException("Value of " + column.getEffectiveDataType() + " type cannot be converted to Timestamp value"); + } + throw ExceptionUtils.toSqlState(String.format("Method: getTimestamp(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1495,6 +1513,15 @@ public T getObjectImpl(String columnLabel, Class type, Map type, ClickHouseColumn colum return convertObject(value, type, column); } - public static Object convertObject(Object value, Class type, ClickHouseColumn column) throws SQLException { + static Object convertObject(Object value, Class type, ClickHouseColumn column) throws SQLException { if (value == null || type == null) { return value; } @@ -359,6 +360,8 @@ public static Object convertObject(Object value, Class type, ClickHouseColumn return Double.parseDouble(value.toString()); } else if (type == java.math.BigDecimal.class) { return new java.math.BigDecimal(value.toString()); + } else if (type == Duration.class && value instanceof LocalDateTime) { + return DataTypeUtils.localDateTimeToDuration((LocalDateTime) value); } else if (value instanceof TemporalAccessor) { TemporalAccessor temporalValue = (TemporalAccessor) value; if (type == LocalDate.class) { @@ -367,6 +370,8 @@ public static Object convertObject(Object value, Class type, ClickHouseColumn return LocalDateTime.from(temporalValue); } else if (type == OffsetDateTime.class) { return OffsetDateTime.from(temporalValue); + } else if (type == LocalTime.class) { + return LocalTime.from(temporalValue); } else if (type == ZonedDateTime.class) { return ZonedDateTime.from(temporalValue); } else if (type == Instant.class) { diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java new file mode 100644 index 000000000..7cb29d587 --- /dev/null +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -0,0 +1,125 @@ +package com.clickhouse.jdbc; + + +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.DataTypeUtils; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; +import java.util.Calendar; +import java.util.Properties; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +@Test(groups = {"integration"}) +public class JDBCDateTimeTests extends JdbcIntegrationTest { + + + + @Test(groups = {"integration"}) + void testDaysBeforeBirthdayParty() throws SQLException { + + LocalDate now = LocalDate.now(); + int daysBeforeParty = 10; + LocalDate birthdate = now.plusDays(daysBeforeParty); + + + Properties props = new Properties(); + props.put(ClientConfigProperties.USE_TIMEZONE.getKey(), "Asia/Tokyo"); + props.put(ClientConfigProperties.serverSetting("session_timezone"), "Asia/Tokyo"); + try (Connection conn = getJdbcConnection(props); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("CREATE TABLE test_days_before_birthday_party (id Int32, birthdate Date32) Engine MergeTree ORDER BY()"); + + final String birthdateStr = birthdate.format(DataTypeUtils.DATE_FORMATTER); + stmt.executeUpdate("INSERT INTO test_days_before_birthday_party VALUES (1, '" + birthdateStr + "')"); + + try (ResultSet rs = stmt.executeQuery("SELECT id, birthdate, birthdate::String, timezone() FROM test_days_before_birthday_party")) { + Assert.assertTrue(rs.next()); + + LocalDate dateFromDb = rs.getObject(2, LocalDate.class); + Assert.assertEquals(dateFromDb, birthdate); + Assert.assertEquals(now.toEpochDay() - dateFromDb.toEpochDay(), -daysBeforeParty); + Assert.assertEquals(rs.getString(4), "Asia/Tokyo"); + + + Assert.assertEquals(rs.getString(2), rs.getString(3)); + + java.sql.Date sqlDate = rs.getDate(2); // in local timezone + + String zoneId = "Asia/Tokyo"; + Calendar tzCalendar = Calendar.getInstance(TimeZone.getTimeZone(ZoneId.of(zoneId))); // TimeZone.getTimeZone() doesn't throw exception but fallback to GMT + java.sql.Date tzSqlDate = rs.getDate(2, tzCalendar); // Calendar tells from what timezone convert to local + Assert.assertEquals(Math.abs(sqlDate.toLocalDate().toEpochDay() - tzSqlDate.toLocalDate().toEpochDay()), 1, + "tzCalendar " + tzCalendar + " default " + Calendar.getInstance().getTimeZone().getID()); + } + } + } + + @Test(groups = {"integration"}) + void testWalkTime() throws SQLException { + if (isVersionMatch("(,25.5]")) { + return; // time64 was introduced in 25.6 + } + int hours = 100; + Duration walkTime = Duration.ZERO.plusHours(hours).plusMinutes(59).plusSeconds(59).plusMillis(300); + System.out.println(walkTime); + + Properties props = new Properties(); + props.put(ClientConfigProperties.USE_TIMEZONE.getKey(), "Asia/Tokyo"); + props.put(ClientConfigProperties.serverSetting("session_timezone"), "Asia/Tokyo"); + props.put(ClientConfigProperties.serverSetting("allow_experimental_time_time64_type"), "1"); + try (Connection conn = getJdbcConnection(props); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("CREATE TABLE test_walk_time (id Int32, walk_time Time64(3)) Engine MergeTree ORDER BY()"); + + final String walkTimeStr = DataTypeUtils.durationToTimeString(walkTime, 3); + System.out.println(walkTimeStr); + stmt.executeUpdate("INSERT INTO test_walk_time VALUES (1, '" + walkTimeStr + "')"); + + try (ResultSet rs = stmt.executeQuery("SELECT id, walk_time, walk_time::String, timezone() FROM test_walk_time")) { + Assert.assertTrue(rs.next()); + + LocalTime dbTime = rs.getObject(2, LocalTime.class); + Assert.assertEquals(dbTime.getHour(), hours % 24); // LocalTime is only 24 hours and will truncate big hour values + Assert.assertEquals(dbTime.getMinute(), 59); + Assert.assertEquals(dbTime.getSecond(), 59); + Assert.assertEquals(dbTime.getNano(), TimeUnit.MILLISECONDS.toNanos(300)); + + LocalDateTime utDateTime = rs.getObject(2, LocalDateTime.class); // LocalDateTime covers all range + Assert.assertEquals(utDateTime.getYear(), 1970); + Assert.assertEquals(utDateTime.getMonth(), Month.JANUARY); + Assert.assertEquals(utDateTime.getDayOfMonth(), 1 + (hours / 24)); + + Assert.assertEquals(utDateTime.getHour(), walkTime.toHours() % 24); // LocalTime is only 24 hours and will truncate big hour values + Assert.assertEquals(utDateTime.getMinute(), 59); + Assert.assertEquals(utDateTime.getSecond(), 59); + Assert.assertEquals(utDateTime.getNano(), TimeUnit.MILLISECONDS.toNanos(300)); + + Duration dbDuration = rs.getObject(2, Duration.class); + Assert.assertEquals(dbDuration, walkTime); + + java.sql.Time sqlTime = rs.getTime(2); + Assert.assertEquals(sqlTime.toLocalTime(), dbTime.truncatedTo(ChronoUnit.SECONDS)); // java.sql.Time accepts milliseconds but converts to LD with seconds precision. + } + } + } + + +} diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java similarity index 95% rename from jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java rename to jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java index 7a535de4c..e3784f17a 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -47,6 +47,7 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; @@ -64,8 +65,8 @@ import static org.testng.Assert.assertTrue; @Test(groups = { "integration" }) -public class DataTypeTests extends JdbcIntegrationTest { - private static final Logger log = LoggerFactory.getLogger(DataTypeTests.class); +public class JdbcDataTypeTests extends JdbcIntegrationTest { + private static final Logger log = LoggerFactory.getLogger(JdbcDataTypeTests.class); @BeforeClass(groups = { "integration" }) public static void setUp() throws SQLException { @@ -459,28 +460,25 @@ public void testDecimalTypes() throws SQLException { } @Test(groups = { "integration" }) - public void testDateTypes() throws SQLException { - runQuery("CREATE TABLE test_dates (order Int8, " - + "date Date, date32 Date32, " + + public void testDateTimeTypes() throws SQLException { + runQuery("CREATE TABLE test_datetimes (order Int8, " + "dateTime DateTime, dateTime32 DateTime32, " + "dateTime643 DateTime64(3), dateTime646 DateTime64(6), dateTime649 DateTime64(9)" + ") ENGINE = MergeTree ORDER BY ()"); // Insert minimum values - insertData("INSERT INTO test_dates VALUES ( 1, '1970-01-01', '1970-01-01', " + + insertData("INSERT INTO test_datetimes VALUES ( 1, " + "'1970-01-01 00:00:00', '1970-01-01 00:00:00', " + "'1970-01-01 00:00:00.000', '1970-01-01 00:00:00.000000', '1970-01-01 00:00:00.000000000' )"); // Insert maximum values - insertData("INSERT INTO test_dates VALUES ( 2, '2149-06-06', '2299-12-31', " + + insertData("INSERT INTO test_datetimes VALUES ( 2," + "'2106-02-07 06:28:15', '2106-02-07 06:28:15', " + "'2261-12-31 23:59:59.999', '2261-12-31 23:59:59.999999', '2261-12-31 23:59:59.999999999' )"); // Insert random (valid) values final ZoneId zoneId = ZoneId.of("America/Los_Angeles"); final LocalDateTime now = LocalDateTime.now(zoneId); - final Date date = Date.valueOf(now.toLocalDate()); - final Date date32 = Date.valueOf(now.toLocalDate()); final java.sql.Timestamp dateTime = Timestamp.valueOf(now); dateTime.setNanos(0); final java.sql.Timestamp dateTime32 = Timestamp.valueOf(now); @@ -493,14 +491,12 @@ public void testDateTypes() throws SQLException { dateTime649.setNanos(333333333); try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_dates VALUES ( 4, ?, ?, ?, ?, ?, ?, ?)")) { - stmt.setDate(1, date); - stmt.setDate(2, date32); - stmt.setTimestamp(3, dateTime); - stmt.setTimestamp(4, dateTime32); - stmt.setTimestamp(5, dateTime643); - stmt.setTimestamp(6, dateTime646); - stmt.setTimestamp(7, dateTime649); + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_datetimes VALUES ( 4, ?, ?, ?, ?, ?)")) { + stmt.setTimestamp(1, dateTime); + stmt.setTimestamp(2, dateTime32); + stmt.setTimestamp(3, dateTime643); + stmt.setTimestamp(4, dateTime646); + stmt.setTimestamp(5, dateTime649); stmt.executeUpdate(); } } @@ -508,10 +504,8 @@ public void testDateTypes() throws SQLException { // Check the results try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) { assertTrue(rs.next()); - assertEquals(rs.getDate("date"), Date.valueOf("1970-01-01")); - assertEquals(rs.getDate("date32"), Date.valueOf("1970-01-01")); assertEquals(rs.getTimestamp("dateTime").toString(), "1970-01-01 00:00:00.0"); assertEquals(rs.getTimestamp("dateTime32").toString(), "1970-01-01 00:00:00.0"); assertEquals(rs.getTimestamp("dateTime643").toString(), "1970-01-01 00:00:00.0"); @@ -519,8 +513,6 @@ public void testDateTypes() throws SQLException { assertEquals(rs.getTimestamp("dateTime649").toString(), "1970-01-01 00:00:00.0"); assertTrue(rs.next()); - assertEquals(rs.getDate("date"), Date.valueOf("2149-06-06")); - assertEquals(rs.getDate("date32"), Date.valueOf("2299-12-31")); assertEquals(rs.getTimestamp("dateTime").toString(), "2106-02-07 06:28:15.0"); assertEquals(rs.getTimestamp("dateTime32").toString(), "2106-02-07 06:28:15.0"); assertEquals(rs.getTimestamp("dateTime643").toString(), "2261-12-31 23:59:59.999"); @@ -528,8 +520,6 @@ public void testDateTypes() throws SQLException { assertEquals(rs.getTimestamp("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); assertTrue(rs.next()); - assertEquals(rs.getDate("date").toString(), date.toString()); - assertEquals(rs.getDate("date32").toString(), date32.toString()); assertEquals(rs.getTimestamp("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); assertEquals(rs.getTimestamp("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); assertEquals(rs.getTimestamp("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); @@ -550,10 +540,8 @@ public void testDateTypes() throws SQLException { // Check the results try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) { assertTrue(rs.next()); - assertEquals(rs.getObject("date"), Date.valueOf("1970-01-01")); - assertEquals(rs.getObject("date32"), Date.valueOf("1970-01-01")); assertEquals(rs.getObject("dateTime").toString(), "1970-01-01 00:00:00.0"); assertEquals(rs.getObject("dateTime32").toString(), "1970-01-01 00:00:00.0"); assertEquals(rs.getObject("dateTime643").toString(), "1970-01-01 00:00:00.0"); @@ -561,8 +549,7 @@ public void testDateTypes() throws SQLException { assertEquals(rs.getObject("dateTime649").toString(), "1970-01-01 00:00:00.0"); assertTrue(rs.next()); - assertEquals(rs.getObject("date"), Date.valueOf("2149-06-06")); - assertEquals(rs.getObject("date32"), Date.valueOf("2299-12-31")); + assertEquals(rs.getObject("dateTime").toString(), "2106-02-07 06:28:15.0"); assertEquals(rs.getObject("dateTime32").toString(), "2106-02-07 06:28:15.0"); assertEquals(rs.getObject("dateTime643").toString(), "2261-12-31 23:59:59.999"); @@ -570,8 +557,6 @@ public void testDateTypes() throws SQLException { assertEquals(rs.getObject("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); assertTrue(rs.next()); - assertEquals(rs.getObject("date").toString(), date.toString()); - assertEquals(rs.getObject("date32").toString(), date32.toString()); assertEquals(rs.getObject("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); assertEquals(rs.getObject("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); @@ -586,11 +571,10 @@ public void testDateTypes() throws SQLException { try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) + ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) { assertTrue(rs.next()); - assertEquals(rs.getString("date"), "1970-01-01"); - assertEquals(rs.getString("date32"), "1970-01-01"); + assertEquals(rs.getString("dateTime"), "1970-01-01 00:00:00"); assertEquals(rs.getString("dateTime32"), "1970-01-01 00:00:00"); assertEquals(rs.getString("dateTime643"), "1970-01-01 00:00:00"); @@ -598,8 +582,6 @@ public void testDateTypes() throws SQLException { assertEquals(rs.getString("dateTime649"), "1970-01-01 00:00:00"); assertTrue(rs.next()); - assertEquals(rs.getString("date"), "2149-06-06"); - assertEquals(rs.getString("date32"), "2299-12-31"); assertEquals(rs.getString("dateTime"), "2106-02-07 06:28:15"); assertEquals(rs.getString("dateTime32"), "2106-02-07 06:28:15"); assertEquals(rs.getString("dateTime643"), "2261-12-31 23:59:59.999"); @@ -608,12 +590,6 @@ public void testDateTypes() throws SQLException { ZoneId tzServer = ZoneId.of(((ConnectionImpl) conn).getClient().getServerTimeZone()); assertTrue(rs.next()); - assertEquals( - rs.getString("date"), - Instant.ofEpochMilli(date.getTime()).atZone(tzServer).toLocalDate().toString()); - assertEquals( - rs.getString("date32"), - Instant.ofEpochMilli(date32.getTime()).atZone(tzServer).toLocalDate().toString()); assertEquals( rs.getString("dateTime"), DataTypeUtils.DATETIME_FORMATTER.format( @@ -636,6 +612,99 @@ public void testDateTypes() throws SQLException { } } + @Test(groups = { "integration" }) + public void testDateTypes() throws SQLException { + runQuery("CREATE TABLE test_dates (order Int8, " + + "date Date, date32 Date32" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert minimum values + insertData("INSERT INTO test_dates VALUES ( 1, '1970-01-01', '1970-01-01')"); + + // Insert maximum values + insertData("INSERT INTO test_dates VALUES ( 2, '2149-06-06', '2299-12-31')"); + + // Insert random (valid) values + final ZoneId zoneId = ZoneId.of("America/Los_Angeles"); + final LocalDateTime now = LocalDateTime.now(zoneId); + final Date date = Date.valueOf(now.toLocalDate()); + final Date date32 = Date.valueOf(now.toLocalDate()); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_dates VALUES ( 3, ?, ?)")) { + stmt.setDate(1, date); + stmt.setDate(2, date32); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getDate("date"), Date.valueOf("1970-01-01")); + assertEquals(rs.getDate("date32"), Date.valueOf("1970-01-01")); + + assertTrue(rs.next()); + assertEquals(rs.getDate("date"), Date.valueOf("2149-06-06")); + assertEquals(rs.getDate("date32"), Date.valueOf("2299-12-31")); + + assertTrue(rs.next()); + assertEquals(rs.getDate("date").toString(), date.toString()); + assertEquals(rs.getDate("date32").toString(), date32.toString()); + + assertFalse(rs.next()); + } + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("date"), Date.valueOf("1970-01-01")); + assertEquals(rs.getObject("date32"), Date.valueOf("1970-01-01")); + + assertTrue(rs.next()); + assertEquals(rs.getObject("date"), Date.valueOf("2149-06-06")); + assertEquals(rs.getObject("date32"), Date.valueOf("2299-12-31")); + + assertTrue(rs.next()); + assertEquals(rs.getObject("date").toString(), date.toString()); + assertEquals(rs.getObject("date32").toString(), date32.toString()); + + assertFalse(rs.next()); + } + } + } + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) + { + assertTrue(rs.next()); + assertEquals(rs.getString("date"), "1970-01-01"); + assertEquals(rs.getString("date32"), "1970-01-01"); + + assertTrue(rs.next()); + assertEquals(rs.getString("date"), "2149-06-06"); + assertEquals(rs.getString("date32"), "2299-12-31"); + + ZoneId tzServer = ZoneId.of(((ConnectionImpl) conn).getClient().getServerTimeZone()); + assertTrue(rs.next()); + assertEquals( + rs.getString("date"), + Instant.ofEpochMilli(date.getTime()).atZone(tzServer).toLocalDate().toString()); + assertEquals( + rs.getString("date32"), + Instant.ofEpochMilli(date32.getTime()).atZone(tzServer).toLocalDate().toString()); + + assertFalse(rs.next()); + } + } + @Test(groups = { "integration" }) public void testTimeTypes() throws SQLException { @@ -658,29 +727,43 @@ public void testTimeTypes() throws SQLException { try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_time64")) { assertTrue(rs.next()); assertEquals(rs.getInt("order"), 1); - assertEquals(rs.getInt("time"), -(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); - assertEquals(rs.getLong("time64"), -((TimeUnit.HOURS.toNanos(999) + TimeUnit.MINUTES.toNanos(59) + TimeUnit.SECONDS.toNanos(59)) + 999999999)); + // Negative values + // Negative value cannot be returned as Time without being truncated + assertTrue(rs.getTime("time").getTime() < 0); + assertTrue(rs.getTime("time64").getTime() < 0); + LocalDateTime negativeTime = rs.getObject("time", LocalDateTime.class); + assertEquals(negativeTime.toEpochSecond(ZoneOffset.UTC), -(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); + LocalDateTime negativeTime64 = rs.getObject("time64", LocalDateTime.class); + assertEquals(negativeTime64.toEpochSecond(ZoneOffset.UTC), -(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59), "value " + negativeTime64); + assertEquals(negativeTime64.getNano(), 999_999_999); // nanoseconds are stored separately and only positive values accepted + + // Positive values assertTrue(rs.next()); assertEquals(rs.getInt("order"), 2); - assertEquals(rs.getInt("time"), (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); - assertEquals(rs.getLong("time64"), (TimeUnit.HOURS.toNanos(999) + TimeUnit.MINUTES.toNanos(59) + TimeUnit.SECONDS.toNanos(59)) + 999999999); + LocalDateTime positiveTime = rs.getObject("time", LocalDateTime.class); + assertEquals(positiveTime.toEpochSecond(ZoneOffset.UTC), (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); + LocalDateTime positiveTime64 = rs.getObject("time64", LocalDateTime.class); + assertEquals(positiveTime64.toEpochSecond(ZoneOffset.UTC), (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); + assertEquals(positiveTime64.getNano(), 999_999_999); + + // Time is stored as UTC (server timezone) + assertEquals(rs.getTime("time", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime(), + (TimeUnit.HOURS.toMillis(999) + TimeUnit.MINUTES.toMillis(59) + TimeUnit.SECONDS.toMillis(59))); - Time time = rs.getTime("time"); - assertEquals(time.getTime(), rs.getInt("time") * 1000L); // time is in seconds - assertEquals(time.getTime(), rs.getObject("time", Time.class).getTime()); - Time time64 = rs.getTime("time64"); - assertEquals(time64.getTime(), rs.getLong("time64") / 1_000_000); // time64 is in nanoseconds - assertEquals(time64, rs.getObject("time64", Time.class)); + // java.sql.Time max resolution is milliseconds + assertEquals(rs.getTime("time64", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime(), + (TimeUnit.HOURS.toMillis(999) + TimeUnit.MINUTES.toMillis(59) + TimeUnit.SECONDS.toMillis(59) + 999)); + + assertEquals(rs.getTime("time"), rs.getObject("time", Time.class)); + assertEquals(rs.getTime("time64"), rs.getObject("time64", Time.class)); // time has no date part and cannot be converted to Date or Timestamp for (String col : Arrays.asList("time", "time64")) { assertThrows(SQLException.class, () -> rs.getDate(col)); assertThrows(SQLException.class, () -> rs.getTimestamp(col)); - assertThrows(SQLException.class, () -> rs.getObject(col, Date.class)); assertThrows(SQLException.class, () -> rs.getObject(col, Timestamp.class)); - // LocalTime conversion is not supported - assertThrows(SQLException.class, () -> rs.getObject(col, LocalTime.class)); + assertThrows(SQLException.class, () -> rs.getObject(col, Date.class)); } assertFalse(rs.next()); } @@ -1745,7 +1828,7 @@ public void testTypeConversions() throws Exception { assertEquals(rs.getObject(4), Date.valueOf("2024-12-01")); assertEquals(rs.getString(4), "2024-12-01");//Underlying object is ZonedDateTime assertEquals(rs.getObject(4, LocalDate.class), LocalDate.of(2024, 12, 1)); - assertEquals(rs.getObject(4, ZonedDateTime.class), ZonedDateTime.of(2024, 12, 1, 0, 0, 0, 0, ZoneId.of("UTC"))); + assertThrows(SQLException.class, () -> rs.getObject(4, ZonedDateTime.class)); // Date cannot be presented as time assertEquals(String.valueOf(rs.getObject(4, new HashMap>(){{put(JDBCType.DATE.getName(), LocalDate.class);}})), "2024-12-01"); assertEquals(rs.getTimestamp(5).toString(), "2024-12-01 12:34:56.0"); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcIntegrationTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcIntegrationTest.java index 85d76b218..69bc2a11e 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcIntegrationTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcIntegrationTest.java @@ -5,12 +5,14 @@ import com.clickhouse.client.ClickHouseServerForTest; import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.query.GenericRecord; +import com.clickhouse.data.ClickHouseVersion; import com.clickhouse.logging.Logger; import com.clickhouse.logging.LoggerFactory; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; +import java.util.List; import java.util.Properties; public abstract class JdbcIntegrationTest extends BaseIntegrationTest { @@ -91,4 +93,8 @@ protected String getServerVersion() { return null; } } + + protected boolean isVersionMatch(String versionExpression) { + return ClickHouseVersion.of(getServerVersion()).check(versionExpression); + } }