From cf6a962a586bb14bfade61c12252fb54e95d05dd Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 21 Jan 2026 15:19:47 -0800 Subject: [PATCH 01/13] reorganized date/time tests to be in one place. Fixed Date/Date32 to be stored as Integer --- .../com/clickhouse/data/ClickHouseColumn.java | 15 ++ .../internal/AbstractBinaryFormatReader.java | 26 +-- .../internal/BinaryStreamReader.java | 4 +- .../client/datatypes/DataTypeTests.java | 92 +++++++++++ .../clickhouse/client/query/QueryTests.java | 92 ----------- ...aTypeTests.java => JdbcDataTypeTests.java} | 153 +++++++++++++----- 6 files changed, 236 insertions(+), 146 deletions(-) rename jdbc-v2/src/test/java/com/clickhouse/jdbc/{DataTypeTests.java => JdbcDataTypeTests.java} (97%) 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/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 9aa5c7aeb..fc6eb0a39 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 @@ -309,6 +309,8 @@ protected void setSchema(TableSchema schema) { case Enum16: case Variant: case Dynamic: + case Date: + case Date32: case Time: case Time64: this.convertions[i] = NumberConverter.NUMBER_CONVERTERS; @@ -442,8 +444,7 @@ public ZonedDateTime getZonedDateTime(String colName) { switch (columnDataType) { case DateTime: case DateTime64: - case Date: - case Date32: + case DateTime32: return readValue(colName); default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); @@ -803,17 +804,24 @@ public short getEnum16(int index) { @Override public LocalDate getLocalDate(String colName) { - Object value = readValue(colName); - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toLocalDate(); - } - return (LocalDate) value; - + return getLocalDate(schema.nameToColumnIndex(colName)); } @Override public LocalDate getLocalDate(int index) { - return getLocalDate(schema.columnIndexToName(index)); + ClickHouseColumn column = schema.getColumnByIndex(index); + switch(column.getEffectiveDataType()) { + case Date: + case Date32: + return LocalDate.ofEpochDay(getLong(index)); + case DateTime: + case DateTime32: + case DateTime64: + ZonedDateTime zdt = readValue(index); + return zdt.toLocalDate(); + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); + } } @Override 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..9150578fe 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 @@ -179,9 +179,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) (Integer) readUnsignedShortLE(); // days since Unix Epoch case Date32: - return convertDateTime(readDate32(timezone), typeHint); + return (T) (Integer) readIntLE(); // days, 0 - Unix Epoch, -1 -before, +1 - after case DateTime: return convertDateTime(readDateTime32(timezone), typeHint); case DateTime32: 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 9ae67693d..b0007da71 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; @@ -34,8 +35,11 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; @@ -1083,6 +1087,84 @@ 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")); + + } + + // 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 + " ( "); @@ -1098,4 +1180,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/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/test/java/com/clickhouse/jdbc/DataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java similarity index 97% 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 4216eb061..548dd0675 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -42,7 +42,6 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneId; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -63,8 +62,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 { @@ -439,28 +438,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); @@ -473,14 +469,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(); } } @@ -488,10 +482,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"); @@ -499,8 +491,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"); @@ -508,8 +498,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()); @@ -530,10 +518,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"); @@ -541,8 +527,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"); @@ -550,8 +535,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()); @@ -566,11 +549,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"); @@ -578,8 +560,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"); @@ -588,12 +568,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( @@ -616,6 +590,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 { From 9dc8649491d98be6154fde773d1e825dba9b02aa Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 22 Jan 2026 17:12:59 -0800 Subject: [PATCH 02/13] Reworked reading date/time values so they read as Local time data. This reduces number of conversions and makes logic simple --- .../clickhouse/client/api/DataTypeUtils.java | 23 +++ .../ClickHouseBinaryFormatReader.java | 5 + .../internal/AbstractBinaryFormatReader.java | 161 +++++++++++------- .../internal/BinaryReaderBackedRecord.java | 10 ++ .../internal/BinaryStreamReader.java | 26 ++- .../internal/MapBackedRecord.java | 126 +++++++++++--- .../client/api/query/GenericRecord.java | 4 + .../client/datatypes/DataTypeTests.java | 130 ++++++-------- .../datatypes/RowBinaryFormatWriterTest.java | 5 +- .../clickhouse/client/insert/InsertTests.java | 2 +- .../client/internal/SmallTests.java | 41 +++++ 11 files changed, 363 insertions(+), 170 deletions(-) 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..236674bc9 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 @@ -4,7 +4,11 @@ import com.clickhouse.data.ClickHouseDataType; 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; @@ -139,4 +143,23 @@ 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 = (int) (value % factor); + value /= factor; + if (nanoSeconds < 0) { + nanoSeconds += factor; + value--; + } + if (nanoSeconds > 0L) { + nanoSeconds *= BASES[9 - precision]; + } + + } + + return LocalDateTime.ofEpochSecond(value, nanoSeconds, ZoneOffset.UTC); + } } 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 fc6eb0a39..f0f9be311 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 @@ -35,6 +35,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; @@ -332,12 +333,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) { @@ -403,52 +424,12 @@ 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 DateTime32: - return readValue(colName); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); - } + return getZonedDateTime(schema.nameToColumnIndex(colName)); } @Override @@ -672,12 +653,43 @@ 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 Instant.from(date); + case Time: + case Time64: + LocalDateTime dt = getLocalDateTime(index); + return dt.toInstant(ZoneOffset.UTC); + case DateTime: + case DateTime64: + Object colValue = readValue(index); + 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(); + } + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); + } } @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); + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); + } } @Override @@ -813,7 +825,7 @@ public LocalDate getLocalDate(int index) { switch(column.getEffectiveDataType()) { case Date: case Date32: - return LocalDate.ofEpochDay(getLong(index)); + return readValue(index); case DateTime: case DateTime32: case DateTime64: @@ -825,31 +837,62 @@ public LocalDate getLocalDate(int index) { } @Override - public LocalDateTime getLocalDateTime(String colName) { - Object value = readValue(colName); - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toLocalDateTime(); + public LocalTime getLocalTime(String colName) { + return getLocalTime(schema.nameToColumnIndex(colName)); + } + + @Override + public LocalTime getLocalTime(int index) { + ClickHouseColumn column = schema.getColumnByIndex(index); + switch(column.getEffectiveDataType()) { + case Time: + case Time64: + LocalDateTime dt = readValue(index); + return dt.toLocalTime(); + case DateTime: + case DateTime32: + case DateTime64: + ZonedDateTime zdt = readValue(index); + return zdt.toLocalTime(); + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); } - return (LocalDateTime) value; + } + + @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 DateTime: + case DateTime32: + case DateTime64: + return ((ZonedDateTime)readValue(index)).toLocalDateTime(); + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDateTime"); + } } @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: + return ((ZonedDateTime)readValue(index)).toOffsetDateTime(); + default: + 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 9150578fe..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 (T) (Integer) readUnsignedShortLE(); // days since Unix Epoch + return (T) readDateAsLocalDate(); case Date32: - return (T) (Integer) readIntLE(); // days, 0 - Unix Epoch, -1 -before, +1 - after + 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..f00d4bc23 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; @@ -136,36 +137,35 @@ 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 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.toInstant(ZoneOffset.UTC); + case DateTime: + case DateTime64: + return getZonedDateTime(colName).toInstant(); + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); } - 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()) { + switch (column.getEffectiveDataType()) { case DateTime: case DateTime64: - case Date: - case Date32: - return readValue(colName); + Object colValue = readValue(column.getColumnIndex()); + return (ZonedDateTime) colValue; } - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); } @Override @@ -468,21 +468,85 @@ 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.toLocalDate(); + case Dynamic: + case Variant: + Object value = getObject(colName); + if (value instanceof LocalDate) { + return (LocalDate) value; + } else { + throw new ClientException("Dynamic/Variant value of " + (value == null? "null" : value.getClass()) + " cannot be converted to LocalDate"); + } + default: + 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: + return ((LocalDateTime) getObject(colName)).toLocalTime(); + case DateTime: + case DateTime32: + case DateTime64: + LocalDateTime dt = getLocalDateTime(colName); + return dt.toLocalTime(); + case Dynamic: + case Variant: + Object value = getObject(colName); + if (value instanceof LocalDateTime) { + return ((LocalDateTime) value).toLocalTime(); + } else { + throw new ClientException("Dynamic/Variant value of " + (value == null? "null" : value.getClass()) + " cannot be converted to LocalTime"); + } + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalTime"); } - return (LocalDate) value; + } + @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: + return ((ZonedDateTime)readValue(colName)).toLocalDateTime(); + case Dynamic: + case Variant: + Object value = getObject(colName); + if (value instanceof LocalDateTime) { + return (LocalDateTime) value; + } else if (value instanceof ZonedDateTime) { + return ((ZonedDateTime)value).toLocalDateTime(); + } else { + throw new ClientException("Dynamic/Variant value of " + (value == null? "null" : value.getClass()) + " cannot be converted to LocalDateTime"); + + } + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDateTime"); } - return (LocalDateTime) value; } @Override @@ -492,11 +556,17 @@ 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: + return getZonedDateTime(colName).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/datatypes/DataTypeTests.java b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java index b0007da71..acfd8219d 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 @@ -37,8 +37,10 @@ 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; @@ -441,6 +443,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", @@ -448,17 +453,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() }); } @@ -691,13 +706,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() }); } @@ -851,100 +865,60 @@ 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); + List records = client.queryAll("SELECT \'" + value + "\'::" + column); + 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:58:59.877")}, + {"Time64(3)","-00:01:00.123", LocalDateTime.parse("1969-12-31T23:58:59.877")}, + {"Time64(6)","-00:01:00.123456", LocalDateTime.parse("1969-12-31T23:58:59.876544")}, + {"Time64(9)","-00:01:00.123456789", LocalDateTime.parse("1969-12-31T23:58:59.876543211")}, + {"Time64","-999:59:59.999", LocalDateTime.parse("1969-11-20T08:00:00.001")}, + {"Time64(3)","-999:59:59.999", LocalDateTime.parse("1969-11-20T08:00:00.001")}, + {"Time64(6)","-999:59:59.999999", LocalDateTime.parse("1969-11-20T08:00:00.000001")}, + {"Time64(9)","-999:59:59.999999999", LocalDateTime.parse("1969-11-20T08:00:00.000000001")}, + {"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; } @@ -1111,6 +1085,10 @@ public void testDates() throws Exception { 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")); } 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()); + } } From afa4000376e9002bb8ce55007d5b89d73d45fcaa Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 27 Jan 2026 23:09:04 -0800 Subject: [PATCH 03/13] Fixed reading time values in client and updated jdbc. Local times and dates are used for simpler reference --- .../clickhouse/client/api/DataTypeUtils.java | 7 +- .../internal/AbstractBinaryFormatReader.java | 66 +++++++--- .../internal/MapBackedRecord.java | 113 +++++++++++++----- .../client/datatypes/DataTypeTests.java | 18 +-- .../com/clickhouse/jdbc/ResultSetImpl.java | 83 +++++++++---- .../clickhouse/jdbc/internal/JdbcUtils.java | 2 +- .../clickhouse/jdbc/JdbcDataTypeTests.java | 44 ++++--- 7 files changed, 236 insertions(+), 97 deletions(-) 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 236674bc9..64f25e96f 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 @@ -148,12 +148,9 @@ public static LocalDateTime localTimeFromTime64Integer(int precision, long value int nanoSeconds = 0; if (precision > 0) { int factor = BinaryStreamReader.BASES[precision]; - nanoSeconds = (int) (value % factor); + nanoSeconds = Math.abs((int) (value % factor)); // nanoseconds are stored separately and only positive values accepted value /= factor; - if (nanoSeconds < 0) { - nanoSeconds += factor; - value--; - } + if (nanoSeconds > 0L) { nanoSeconds *= BASES[9 - precision]; } 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 f0f9be311..083e771f5 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 @@ -367,6 +367,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()); } @@ -435,7 +438,7 @@ public ZonedDateTime getZonedDateTime(String colName) { @Override public Duration getDuration(String colName) { TemporalAmount temporalAmount = getTemporalAmount(colName); - return Duration.from(temporalAmount); + return temporalAmount == null ? null : Duration.from(temporalAmount); } @Override @@ -445,12 +448,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 @@ -460,28 +465,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) { @@ -495,6 +507,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()) { @@ -582,6 +597,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; @@ -659,14 +677,17 @@ public Instant getInstant(int index) { case Date: case Date32: LocalDate date = getLocalDate(index); - return Instant.from(date); + return date == null ? null : Instant.from(date); case Time: case Time64: LocalDateTime dt = getLocalDateTime(index); - return dt.toInstant(ZoneOffset.UTC); + return dt == null ? null : dt.toInstant(ZoneOffset.UTC); case DateTime: case DateTime64: Object colValue = readValue(index); + if (colValue == null) { + return null; + } if (colValue instanceof LocalDateTime) { LocalDateTime dateTime = (LocalDateTime) colValue; return dateTime.toInstant(column.getTimeZone().toZoneId().getRules().getOffset(dateTime)); @@ -704,12 +725,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 @@ -795,6 +818,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(); } @@ -806,6 +832,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(); } @@ -830,7 +859,7 @@ public LocalDate getLocalDate(int index) { case DateTime32: case DateTime64: ZonedDateTime zdt = readValue(index); - return zdt.toLocalDate(); + return zdt == null ? null : zdt.toLocalDate(); default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); } @@ -848,12 +877,12 @@ public LocalTime getLocalTime(int index) { case Time: case Time64: LocalDateTime dt = readValue(index); - return dt.toLocalTime(); + return dt == null ? null : dt.toLocalTime(); case DateTime: case DateTime32: case DateTime64: ZonedDateTime zdt = readValue(index); - return zdt.toLocalTime(); + return zdt == null ? null : zdt.toLocalTime(); default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); } @@ -868,10 +897,14 @@ public LocalDateTime getLocalDateTime(String colName) { public LocalDateTime getLocalDateTime(int index) { ClickHouseColumn column = schema.getColumnByIndex(index); switch(column.getEffectiveDataType()) { + case Time: + case Time64: + return readValue(index); case DateTime: case DateTime32: case DateTime64: - return ((ZonedDateTime)readValue(index)).toLocalDateTime(); + ZonedDateTime zdt = readValue(index); + return zdt == null ? null : zdt.toLocalDateTime(); default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDateTime"); } @@ -889,7 +922,8 @@ public OffsetDateTime getOffsetDateTime(int index) { case DateTime: case DateTime32: case DateTime64: - return ((ZonedDateTime)readValue(index)).toOffsetDateTime(); + ZonedDateTime zdt = readValue(index); + return zdt == null ? null : zdt.toOffsetDateTime(); default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to OffsetDateTime"); } 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 f00d4bc23..e505aa7a0 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 @@ -78,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()); } @@ -142,14 +145,15 @@ public Instant getInstant(String colName) { case Date: case Date32: LocalDate date = getLocalDate(colIndex); - return Instant.from(date); + return date == null ? null : Instant.from(date); case Time: case Time64: LocalDateTime time = getLocalDateTime(colName); - return time.toInstant(ZoneOffset.UTC); + return time == null ? null : time.toInstant(ZoneOffset.UTC); case DateTime: case DateTime64: - return getZonedDateTime(colName).toInstant(); + ZonedDateTime zdt = getZonedDateTime(colName); + return zdt == null ? null : zdt.toInstant(); default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); } @@ -161,8 +165,7 @@ public ZonedDateTime getZonedDateTime(String colName) { switch (column.getEffectiveDataType()) { case DateTime: case DateTime64: - Object colValue = readValue(column.getColumnIndex()); - return (ZonedDateTime) colValue; + return readValue(column.getColumnIndex()); } throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); @@ -170,7 +173,8 @@ public ZonedDateTime getZonedDateTime(String colName) { @Override public Duration getDuration(String colName) { - return readValue(colName); + TemporalAmount temporalAmount = readValue(colName); + return temporalAmount == null ? null : Duration.from(temporalAmount); } @Override @@ -180,12 +184,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 +201,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 +242,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 +289,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 @@ -353,12 +384,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 +476,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 @@ -477,14 +524,17 @@ public LocalDate getLocalDate(String colName) { case DateTime32: case DateTime64: LocalDateTime dt = getLocalDateTime(colName); - return dt.toLocalDate(); + return dt == null ? null : dt.toLocalDate(); case Dynamic: case Variant: Object value = getObject(colName); + if (value == null) { + return null; + } if (value instanceof LocalDate) { return (LocalDate) value; } else { - throw new ClientException("Dynamic/Variant value of " + (value == null? "null" : value.getClass()) + " cannot be converted to LocalDate"); + throw new ClientException("Dynamic/Variant value of " + value.getClass() + " cannot be converted to LocalDate"); } default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); @@ -497,19 +547,23 @@ public LocalTime getLocalTime(String colName) { switch(column.getEffectiveDataType()) { case Time: case Time64: - return ((LocalDateTime) getObject(colName)).toLocalTime(); + LocalDateTime val = (LocalDateTime) getObject(colName); + return val == null ? null : val.toLocalTime(); case DateTime: case DateTime32: case DateTime64: LocalDateTime dt = getLocalDateTime(colName); - return dt.toLocalTime(); + return dt == null ? null : dt.toLocalTime(); case Dynamic: case Variant: Object value = getObject(colName); + if (value == null) { + return null; + } if (value instanceof LocalDateTime) { return ((LocalDateTime) value).toLocalTime(); } else { - throw new ClientException("Dynamic/Variant value of " + (value == null? "null" : value.getClass()) + " cannot be converted to LocalTime"); + throw new ClientException("Dynamic/Variant value of " + value.getClass() + " cannot be converted to LocalTime"); } default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalTime"); @@ -532,16 +586,20 @@ public LocalDateTime getLocalDateTime(String colName) { case DateTime: case DateTime32: case DateTime64: - return ((ZonedDateTime)readValue(colName)).toLocalDateTime(); + ZonedDateTime val = (ZonedDateTime) readValue(colName); + return val == null ? null : val.toLocalDateTime(); case Dynamic: case Variant: Object value = getObject(colName); + if (value == null) { + return null; + } if (value instanceof LocalDateTime) { return (LocalDateTime) value; } else if (value instanceof ZonedDateTime) { return ((ZonedDateTime)value).toLocalDateTime(); } else { - throw new ClientException("Dynamic/Variant value of " + (value == null? "null" : value.getClass()) + " cannot be converted to LocalDateTime"); + throw new ClientException("Dynamic/Variant value of " + value.getClass() + " cannot be converted to LocalDateTime"); } default: @@ -563,7 +621,8 @@ public OffsetDateTime getOffsetDateTime(String colName) { case DateTime64: case Dynamic: case Variant: - return getZonedDateTime(colName).toOffsetDateTime(); + ZonedDateTime val = getZonedDateTime(colName); + return val == null ? null : val.toOffsetDateTime(); default: throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to OffsetDataTime"); } 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 acfd8219d..bba104252 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 @@ -577,7 +577,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(); @@ -904,14 +904,14 @@ public static Object[][] testTimeData() { {"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:58:59.877")}, - {"Time64(3)","-00:01:00.123", LocalDateTime.parse("1969-12-31T23:58:59.877")}, - {"Time64(6)","-00:01:00.123456", LocalDateTime.parse("1969-12-31T23:58:59.876544")}, - {"Time64(9)","-00:01:00.123456789", LocalDateTime.parse("1969-12-31T23:58:59.876543211")}, - {"Time64","-999:59:59.999", LocalDateTime.parse("1969-11-20T08:00:00.001")}, - {"Time64(3)","-999:59:59.999", LocalDateTime.parse("1969-11-20T08:00:00.001")}, - {"Time64(6)","-999:59:59.999999", LocalDateTime.parse("1969-11-20T08:00:00.000001")}, - {"Time64(9)","-999:59:59.999999999", LocalDateTime.parse("1969-11-20T08:00:00.000000001")}, + {"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")}, 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..7a878cfff 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,34 @@ 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; + + if (ld.getYear() != 1970 && ld.getMonth() != Month.JANUARY && ld.getDayOfMonth() != 1) { + final String msg = "Time value '" + ld + "' is before Epoch and cannot be returned as java.sql.Time. Use getObject() to get LocalDateTime instead."; + throw new SQLException(msg); + } + + 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 +1109,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 +1519,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; } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java index 548dd0675..205874e3d 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -42,9 +42,11 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.ZoneOffset; 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; @@ -705,29 +707,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 + assertThrows(SQLException.class, () -> rs.getTime("time")); + assertThrows(SQLException.class, () -> rs.getTime("time64")); + 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 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)); + // 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))); + + // 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()); } @@ -1656,7 +1672,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"); From b01a1810203988fa2413bd3f92947ddb883f1834 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 28 Jan 2026 14:01:50 -0800 Subject: [PATCH 04/13] fix test by adding allowing time64 type --- .../java/com/clickhouse/client/datatypes/DataTypeTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 bba104252..67c5d3bdf 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 @@ -891,7 +891,8 @@ public void testTime(String column, String value, LocalDateTime expectedDt) thro return; // time64 was introduced in 25.6 } - List records = client.queryAll("SELECT \'" + value + "\'::" + column); + 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); } From d1853738624c41eee08fd0eb8a9095f85283a8d7 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 30 Jan 2026 11:16:37 -0800 Subject: [PATCH 05/13] added dedicated test for time values --- .../clickhouse/jdbc/JDBCDateTimeTests.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java 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..599d58298 --- /dev/null +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -0,0 +1,54 @@ +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.time.LocalDate; +import java.util.Properties; + +@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()"); + + 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"); + } + + } + + + } + +} From b6303f40ac2badbfeee8a6521e09ddcbb1c234b3 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sat, 31 Jan 2026 20:52:26 -0800 Subject: [PATCH 06/13] added Date and Time tests --- .../clickhouse/client/api/DataTypeUtils.java | 60 +++++++++++++++++ .../clickhouse/jdbc/internal/JdbcUtils.java | 5 ++ .../clickhouse/jdbc/JDBCDateTimeTests.java | 67 ++++++++++++++++++- 3 files changed, 131 insertions(+), 1 deletion(-) 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 64f25e96f..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,6 +3,7 @@ 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; @@ -12,6 +13,8 @@ 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; @@ -159,4 +162,61 @@ public static LocalDateTime localTimeFromTime64Integer(int precision, long value 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/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index 8c8fed08e..57a074684 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -18,6 +18,7 @@ import java.sql.SQLException; import java.sql.SQLType; import java.sql.Time; +import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -359,6 +360,8 @@ static Object convertObject(Object value, Class type, ClickHouseColumn column 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 @@ static Object convertObject(Object value, Class type, ClickHouseColumn column 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 index 599d58298..6d19bc405 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -10,8 +10,19 @@ 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.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 { @@ -44,11 +55,65 @@ void testDaysBeforeBirthdayParty() throws SQLException { 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 + + Calendar tzCalendar = Calendar.getInstance(TimeZone.getTimeZone(TimeZone.getDefault().getRawOffset() >= 0 ? "America/Los Angeles" : "Asia/Tokyo")); + 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); + } } + } + @Test(groups = {"integration"}) + void testWalkTime() throws SQLException { + 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"); + try (Connection conn = getJdbcConnection(props); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("CREATE TABLE test_walk_time (id Int32, walk_time Time64(3)) Engine MergeTree()"); + + 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. + } + } } + } From 5ce72d4ecabc0fdb3f5b72d3e40efb084004773a Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 1 Feb 2026 08:27:22 -0800 Subject: [PATCH 07/13] fixed tests for different versions --- .../java/com/clickhouse/jdbc/JDBCDateTimeTests.java | 12 ++++++++---- .../com/clickhouse/jdbc/JdbcIntegrationTest.java | 6 ++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java index 6d19bc405..2f1e9f0d0 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -43,7 +43,7 @@ void testDaysBeforeBirthdayParty() throws SQLException { try (Connection conn = getJdbcConnection(props); Statement stmt = conn.createStatement()) { - stmt.executeUpdate("CREATE TABLE test_days_before_birthday_party (id Int32, birthdate Date32) Engine MergeTree()"); + 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 + "')"); @@ -63,14 +63,17 @@ void testDaysBeforeBirthdayParty() throws SQLException { Calendar tzCalendar = Calendar.getInstance(TimeZone.getTimeZone(TimeZone.getDefault().getRawOffset() >= 0 ? "America/Los Angeles" : "Asia/Tokyo")); 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); + 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); @@ -78,10 +81,11 @@ void testWalkTime() throws SQLException { 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()"); + 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); 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); + } } From 85f4942899e6b81d6c0cfbfa4026ab3497d135a4 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 1 Feb 2026 10:36:07 -0800 Subject: [PATCH 08/13] fixed setting timezone in test --- .../test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java index 2f1e9f0d0..3b5ea1ae8 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -16,6 +16,7 @@ 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; @@ -61,10 +62,11 @@ void testDaysBeforeBirthdayParty() throws SQLException { java.sql.Date sqlDate = rs.getDate(2); // in local timezone - Calendar tzCalendar = Calendar.getInstance(TimeZone.getTimeZone(TimeZone.getDefault().getRawOffset() >= 0 ? "America/Los Angeles" : "Asia/Tokyo")); + String zoneId = TimeZone.getDefault().getRawOffset() >= 0 ? "America/Los Angeles" : "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()); + Assert.assertEquals(Math.abs(sqlDate.toLocalDate().toEpochDay() - tzSqlDate.toLocalDate().toEpochDay()), 1, + "tzCalendar " + tzCalendar + " default " + Calendar.getInstance().getTimeZone().getID()); } } } From b8aacc42def2e889e900a703fd502d74bf87df60 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 1 Feb 2026 10:44:01 -0800 Subject: [PATCH 09/13] fixed condition for check that Time overlaps 24 hours span --- jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7a878cfff..66d8c27ad 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -1061,7 +1061,7 @@ public Time getTime(String columnLabel, Calendar cal) throws SQLException { } wasNull = false; - if (ld.getYear() != 1970 && ld.getMonth() != Month.JANUARY && ld.getDayOfMonth() != 1) { + if (ld.getYear() != 1970 || ld.getMonth() != Month.JANUARY || ld.getDayOfMonth() != 1) { final String msg = "Time value '" + ld + "' is before Epoch and cannot be returned as java.sql.Time. Use getObject() to get LocalDateTime instead."; throw new SQLException(msg); } From 12847f72f3dbc178b6e71205dbfbdae618200a22 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 1 Feb 2026 12:44:47 -0800 Subject: [PATCH 10/13] removed throwing on time when value is over 24 hours to let getting Time value and actual time from it --- .../internal/AbstractBinaryFormatReader.java | 6 +----- .../internal/MapBackedRecord.java | 19 ++++++++++--------- .../com/clickhouse/jdbc/ResultSetImpl.java | 6 ------ .../clickhouse/jdbc/JdbcDataTypeTests.java | 4 ++-- 4 files changed, 13 insertions(+), 22 deletions(-) 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 083e771f5..aaf0f93c3 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 @@ -310,10 +310,6 @@ protected void setSchema(TableSchema schema) { case Enum16: case Variant: case Dynamic: - case Date: - case Date32: - case Time: - case Time64: this.convertions[i] = NumberConverter.NUMBER_CONVERTERS; break; default: @@ -884,7 +880,7 @@ public LocalTime getLocalTime(int index) { ZonedDateTime zdt = readValue(index); return zdt == null ? null : zdt.toLocalTime(); default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalTime"); } } 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 e505aa7a0..eeca84261 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 @@ -161,14 +161,7 @@ public Instant getInstant(String colName) { @Override public ZonedDateTime getZonedDateTime(String colName) { - ClickHouseColumn column = schema.getColumnByName(colName); - switch (column.getEffectiveDataType()) { - case DateTime: - case DateTime64: - return readValue(column.getColumnIndex()); - } - - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); + return getZonedDateTime(schema.nameToColumnIndex(colName)); } @Override @@ -369,7 +362,15 @@ 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); + default: + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); + } } @Override 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 66d8c27ad..3793bcf16 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -1060,12 +1060,6 @@ public Time getTime(String columnLabel, Calendar cal) throws SQLException { return null; } wasNull = false; - - if (ld.getYear() != 1970 || ld.getMonth() != Month.JANUARY || ld.getDayOfMonth() != 1) { - final String msg = "Time value '" + ld + "' is before Epoch and cannot be returned as java.sql.Time. Use getObject() to get LocalDateTime instead."; - throw new SQLException(msg); - } - Calendar c = cal != null ? cal : defaultCalendar; long time = ld.atZone(c.getTimeZone().toZoneId()).toEpochSecond() * 1000 + TimeUnit.NANOSECONDS.toMillis(ld.getNano()); return new Time(time); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java index 48cdb48bd..e3784f17a 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -730,8 +730,8 @@ public void testTimeTypes() throws SQLException { // Negative values // Negative value cannot be returned as Time without being truncated - assertThrows(SQLException.class, () -> rs.getTime("time")); - assertThrows(SQLException.class, () -> rs.getTime("time64")); + 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); From baf3a553c99297b2838733d272231bc28422c0a8 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 1 Feb 2026 15:24:16 -0800 Subject: [PATCH 11/13] fixed getting Date/Time values from Dynamic and Variant columns --- .../internal/AbstractBinaryFormatReader.java | 125 ++++-- .../internal/MapBackedRecord.java | 68 +-- .../internal/BaseReaderTests.java | 399 ++++++++++++++++++ .../clickhouse/jdbc/JDBCDateTimeTests.java | 2 +- 4 files changed, 534 insertions(+), 60 deletions(-) create mode 100644 client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java 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 aaf0f93c3..8abb9b4eb 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; @@ -37,6 +36,7 @@ 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; @@ -667,33 +667,40 @@ public BigDecimal getBigDecimal(int index) { @Override public Instant getInstant(int index) { - ClickHouseColumn column = schema.getColumnByIndex(index); switch (column.getEffectiveDataType()) { case Date: case Date32: LocalDate date = getLocalDate(index); - return date == null ? null : Instant.from(date); + 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: - Object colValue = readValue(index); - if (colValue == null) { - return null; - } - 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 DateTime32: + case Dynamic: + case Variant: + Object value = readValue(index); + Instant instant = objectToInstant(value, column); + if (value == null || instant != null) { + return instant; } - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); + break; + } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); + } + + static Instant objectToInstant(Object value, ClickHouseColumn column) { + if (value instanceof LocalDateTime) { + LocalDateTime dateTime = (LocalDateTime) value; + return dateTime.toInstant(column.getTimeZone().toZoneId().getRules().getOffset(dateTime)); + } else if (value instanceof ZonedDateTime) { + ZonedDateTime dateTime = (ZonedDateTime) value; + return dateTime.toInstant(); } + return null; } @Override @@ -704,9 +711,17 @@ public ZonedDateTime getZonedDateTime(int index) { case DateTime64: case DateTime32: return readValue(index); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); + 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 @@ -856,9 +871,27 @@ public LocalDate getLocalDate(int index) { case DateTime64: ZonedDateTime zdt = readValue(index); return zdt == null ? null : zdt.toLocalDate(); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); + case Dynamic: + case Variant: + Object value = readValue(index); + LocalDate localDate = objectToLocalDate(value); + if (value == null || localDate != null) { + return localDate; + } + break; + } + 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 @@ -879,9 +912,25 @@ public LocalTime getLocalTime(int index) { case DateTime64: ZonedDateTime zdt = readValue(index); return zdt == null ? null : zdt.toLocalTime(); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalTime"); + case Dynamic: + case Variant: + Object value = readValue(index); + LocalTime localTime = objectToLocalTime(value); + if (value == null || localTime != null) { + return localTime; + } + break; + } + 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 @@ -901,9 +950,27 @@ public LocalDateTime getLocalDateTime(int index) { case DateTime64: ZonedDateTime zdt = readValue(index); return zdt == null ? null : zdt.toLocalDateTime(); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDateTime"); + 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 @@ -920,9 +987,17 @@ public OffsetDateTime getOffsetDateTime(int index) { case DateTime64: ZonedDateTime zdt = readValue(index); return zdt == null ? null : zdt.toOffsetDateTime(); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to OffsetDateTime"); + 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/MapBackedRecord.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java index eeca84261..884cb53e9 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 @@ -154,9 +154,16 @@ public Instant getInstant(String colName) { case DateTime64: ZonedDateTime zdt = getZonedDateTime(colName); return zdt == null ? null : zdt.toInstant(); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); + case Dynamic: + case Variant: + Object value = readValue(colName); + Instant instant = AbstractBinaryFormatReader.objectToInstant(value, column); + if (value == null || instant != null) { + return instant; + } + break; } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); } @Override @@ -368,9 +375,17 @@ public ZonedDateTime getZonedDateTime(int index) { case DateTime64: case DateTime32: return readValue(index); - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to ZonedDateTime"); + 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 @@ -529,17 +544,13 @@ public LocalDate getLocalDate(String colName) { case Dynamic: case Variant: Object value = getObject(colName); - if (value == null) { - return null; + LocalDate localDate = AbstractBinaryFormatReader.objectToLocalDate(value); + if (value == null || localDate != null) { + return localDate; } - if (value instanceof LocalDate) { - return (LocalDate) value; - } else { - throw new ClientException("Dynamic/Variant value of " + value.getClass() + " cannot be converted to LocalDate"); - } - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); + break; } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDate"); } @Override @@ -558,17 +569,13 @@ public LocalTime getLocalTime(String colName) { case Dynamic: case Variant: Object value = getObject(colName); - if (value == null) { - return null; - } - if (value instanceof LocalDateTime) { - return ((LocalDateTime) value).toLocalTime(); - } else { - throw new ClientException("Dynamic/Variant value of " + value.getClass() + " cannot be converted to LocalTime"); + LocalTime localTime = AbstractBinaryFormatReader.objectToLocalTime(value); + if (value == null || localTime != null) { + return localTime; } - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalTime"); + break; } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalTime"); } @Override @@ -592,20 +599,13 @@ public LocalDateTime getLocalDateTime(String colName) { case Dynamic: case Variant: Object value = getObject(colName); - if (value == null) { - return null; - } - if (value instanceof LocalDateTime) { - return (LocalDateTime) value; - } else if (value instanceof ZonedDateTime) { - return ((ZonedDateTime)value).toLocalDateTime(); - } else { - throw new ClientException("Dynamic/Variant value of " + value.getClass() + " cannot be converted to LocalDateTime"); - + LocalDateTime localDateTime = AbstractBinaryFormatReader.objectToLocalDateTime(value); + if (value == null || localDateTime != null) { + return localDateTime; } - default: - throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDateTime"); + break; } + throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to LocalDateTime"); } @Override 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..61df10e25 --- /dev/null +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java @@ -0,0 +1,399 @@ +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 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 { + 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 { + 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 { + 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))").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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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))").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 { + 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")).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 { + 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 { + 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 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/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java index 3b5ea1ae8..9fb23612f 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -62,7 +62,7 @@ void testDaysBeforeBirthdayParty() throws SQLException { java.sql.Date sqlDate = rs.getDate(2); // in local timezone - String zoneId = TimeZone.getDefault().getRawOffset() >= 0 ? "America/Los Angeles" : "Asia/Tokyo"; + String zoneId = TimeZone.getDefault().getRawOffset() >= 0 ? "America/Los_Angeles" : "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, From 9b8614d2bafcb97423a39333c5f8e912a11f77f9 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 1 Feb 2026 17:52:07 -0800 Subject: [PATCH 12/13] fixed issues with instant and tests --- .../internal/AbstractBinaryFormatReader.java | 8 ++- .../internal/MapBackedRecord.java | 3 +- .../internal/BaseReaderTests.java | 62 ++++++++++++++++++- 3 files changed, 66 insertions(+), 7 deletions(-) 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 8abb9b4eb..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 @@ -680,10 +680,12 @@ public Instant getInstant(int index) { 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, column); + Instant instant = objectToInstant(value); if (value == null || instant != null) { return instant; } @@ -692,10 +694,10 @@ public Instant getInstant(int index) { throw new ClientException("Column of type " + column.getDataType() + " cannot be converted to Instant"); } - static Instant objectToInstant(Object value, ClickHouseColumn column) { + static Instant objectToInstant(Object value) { if (value instanceof LocalDateTime) { LocalDateTime dateTime = (LocalDateTime) value; - return dateTime.toInstant(column.getTimeZone().toZoneId().getRules().getOffset(dateTime)); + return Instant.from(dateTime.atZone(ZoneId.of("UTC"))); } else if (value instanceof ZonedDateTime) { ZonedDateTime dateTime = (ZonedDateTime) value; return dateTime.toInstant(); 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 884cb53e9..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 @@ -152,12 +152,13 @@ public Instant getInstant(String 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, column); + Instant instant = AbstractBinaryFormatReader.objectToInstant(value); if (value == null || instant != null) { return instant; } 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 index 61df10e25..befa6ee87 100644 --- 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 @@ -10,6 +10,7 @@ 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; @@ -45,6 +46,10 @@ public void tearDown() { @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); @@ -72,6 +77,10 @@ public void testReadingLocalDateFromDynamic() throws Exception { @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); @@ -99,6 +108,10 @@ public void testReadingLocalDateTimeFromDynamic() throws Exception { @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); @@ -108,7 +121,8 @@ public void testReadingLocalTimeFromDynamic() throws Exception { .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))").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()) { @@ -128,6 +142,10 @@ public void testReadingLocalTimeFromDynamic() throws Exception { @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); @@ -156,6 +174,10 @@ public void testReadingZonedDateTimeFromDynamic() throws Exception { @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"); @@ -183,6 +205,10 @@ public void testReadingInstantFromDynamic() throws Exception { @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)); @@ -211,6 +237,10 @@ public void testReadingOffsetDateTimeFromDynamic() throws Exception { @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); @@ -238,6 +268,10 @@ public void testReadingLocalDateFromVariant() throws Exception { @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); @@ -265,6 +299,10 @@ public void testReadingLocalDateTimeFromVariant() throws Exception { @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); @@ -274,7 +312,8 @@ public void testReadingLocalTimeFromVariant() throws Exception { .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))").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()) { @@ -294,13 +333,18 @@ public void testReadingLocalTimeFromVariant() throws Exception { @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")).get(); + (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(); @@ -322,6 +366,10 @@ public void testReadingZonedDateTimeFromVariant() throws Exception { @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"); @@ -349,6 +397,10 @@ public void testReadingInstantFromVariant() throws Exception { @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)); @@ -387,6 +439,10 @@ public static String tableDefinition(String table, String... columns) { } + 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); From b23d218beb7b58696269e1e5003bace45f860629 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 1 Feb 2026 20:46:08 -0800 Subject: [PATCH 13/13] fixed issues with instant and tests --- .../src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java index 9fb23612f..7cb29d587 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -62,7 +62,7 @@ void testDaysBeforeBirthdayParty() throws SQLException { java.sql.Date sqlDate = rs.getDate(2); // in local timezone - String zoneId = TimeZone.getDefault().getRawOffset() >= 0 ? "America/Los_Angeles" : "Asia/Tokyo"; + 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,