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);
+ }
}