From e6a74a3d446726ff5f92954a536844ca6048dc18 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 26 Feb 2026 14:49:37 -0800 Subject: [PATCH 01/10] Added verification that DESCRIBE can be read. Prev no result set returned --- .../com/clickhouse/jdbc/StatementTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index fd0bd2f4a..3aba8b652 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -1285,6 +1285,43 @@ public void testResponseWithDuplicateColumns() throws Exception { } } + @Test(groups = {"integration"}) + public void testDescribeStatement() throws Exception { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + boolean isResultSet = stmt.execute("DESCRIBE table (SELECT 10, 'message', 30)"); + Assert.assertTrue(isResultSet); + try (ResultSet rs = stmt.getResultSet()) { + Object[][] expected = new Object[][] { + {"10", "UInt8"}, + {"'message'", "String"}, + {"30", "UInt8"}, + }; + + for (Object[] objects : expected) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getString("name"), objects[0]); + Assert.assertEquals(rs.getString("type"), objects[1]); + } + } + } + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + boolean isResultSet = stmt.execute("DESCRIBE TABLE (SELECT numbers.number FROM system.numbers)"); + Assert.assertTrue(isResultSet); + try (ResultSet rs = stmt.getResultSet()) { + Object[][] expected = new Object[][] { + {"number", "UInt64"}, + }; + + for (Object[] objects : expected) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getString("name"), objects[0]); + Assert.assertEquals(rs.getString("type"), objects[1]); + } + } + } + } + private static String getDBName(Statement stmt) throws SQLException { try (ResultSet rs = stmt.executeQuery("SELECT database()")) { rs.next(); From 0c8e1ae8271dc1e0c086e6fb22778d8e7cd3fac7 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 26 Feb 2026 15:04:48 -0800 Subject: [PATCH 02/10] Add verification for selecting dates in range --- .../clickhouse/jdbc/JDBCDateTimeTests.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) 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 d18f4656e..ec9ce52dc 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -6,6 +6,7 @@ import org.testng.Assert; import org.testng.annotations.Test; +import java.sql.Array; import java.sql.Connection; import java.sql.Date; import java.sql.PreparedStatement; @@ -227,4 +228,42 @@ void testLapsTime() throws Exception { } } } + + @Test + void testDateInRange() throws Exception { + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("DROP TABLE IF EXISTS test_date_in_range"); + stmt.executeUpdate("CREATE TABLE test_date_in_range ( id UInt32, d Date) Engine MergeTree ORDER BY()"); + stmt.executeUpdate("INSERT INTO test_date_in_range VALUES (1, '2025-01-01') , (2, '2025-02-01') , (3, '2025-02-03')"); + + try (PreparedStatement pStmt = conn.prepareStatement("SELECT * FROM test_date_in_range WHERE d IN (?) ORDER BY id")){ + pStmt.setDate(1, Date.valueOf("2025-02-01")); + try (ResultSet rs = pStmt.executeQuery()) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 2); + Assert.assertFalse(rs.next()); + } + + pStmt.setObject(1, LocalDate.parse("2025-02-01")); + try (ResultSet rs = pStmt.executeQuery()) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 2); + Assert.assertFalse(rs.next()); + } + + Array range = conn.createArrayOf("Array(Date)", new Object[] {Date.valueOf("2025-02-01"), + Date.valueOf("2025-02-03")}); + pStmt.setArray(1, range); + try (ResultSet rs = pStmt.executeQuery()) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 2); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 3); + Assert.assertFalse(rs.next()); + } + } + } + } } From 16dd59b3bac80e31d78031b5ce7fc25172315c60 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 27 Feb 2026 11:33:14 -0800 Subject: [PATCH 03/10] Added test to verify JSON keys not leaking between map instances --- .../client/datatypes/DataTypeTests.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) 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 423cb3390..7558195a4 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 @@ -1529,6 +1529,47 @@ public void testGetStringArrayAndGetObjectArrayWhenValueIsList() throws Exceptio } } + /** + * Regression test for https://github.com/ClickHouse/clickhouse-go/issues/1775 + * When scanning JSON rows into a map, keys that are absent in a given row must not + * appear in that row's result, even when another row in the same result set contains + * those keys. + */ + @Test(groups = {"integration"}) + public void testJSONScanDoesNotLeakKeysAcrossRows() throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_json_no_key_leak"; + final CommandSettings cmdSettings = (CommandSettings) new CommandSettings() + .serverSetting("enable_json_type", "1") + .serverSetting("allow_experimental_json_type", "1"); + + client.execute("DROP TABLE IF EXISTS " + table).get().close(); + client.execute(tableDefinition(table, "data JSON"), cmdSettings).get().close(); + client.execute("INSERT INTO " + table + " VALUES ('{\"a\": \"foo\"}'::JSON), ('{\"b\": \"bar\"}'::JSON)").get().close(); + + List records = client.queryAll("SELECT * FROM " + table + " ORDER BY data"); + Assert.assertEquals(records.size(), 2); + + for (GenericRecord record : records) { + @SuppressWarnings("unchecked") + Map data = (Map) record.getObject("data"); + Assert.assertNotNull(data, "JSON column should not be null"); + + if (data.containsKey("a")) { + Assert.assertFalse(data.containsKey("b"), + "Row with key 'a' should not contain key 'b', but got: " + data); + } else if (data.containsKey("b")) { + Assert.assertFalse(data.containsKey("a"), + "Row with key 'b' should not contain key 'a', but got: " + data); + } else { + Assert.fail("Expected row to contain either key 'a' or 'b', but got: " + data); + } + } + } + public static String tableDefinition(String table, String... columns) { StringBuilder sb = new StringBuilder(); sb.append("CREATE TABLE " + table + " ( "); From 3ffa77ef3eb03fc5b9a77677434f92810496d60b Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 27 Feb 2026 12:49:41 -0800 Subject: [PATCH 04/10] Verified that column names are cleaned from table names in case of join --- .../metadata/ResultSetMetaDataImplTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImplTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImplTest.java index 7e387d76a..a24e1d197 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImplTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImplTest.java @@ -236,6 +236,48 @@ public void testGetColumnScale() throws Exception { } } + @Test(groups = { "integration" }) + public void testColumnNamesStrippedFromTablePrefix() throws Exception { + final String t1 = "rsmd_test_prefix_t1"; + final String t2 = "rsmd_test_prefix_t2"; + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate("DROP TABLE IF EXISTS " + t1); + stmt.executeUpdate("DROP TABLE IF EXISTS " + t2); + stmt.executeUpdate("CREATE TABLE " + t1 + " (id Int32, val String) ENGINE = MergeTree ORDER BY id"); + stmt.executeUpdate("CREATE TABLE " + t2 + " (id Int32, name String) ENGINE = MergeTree ORDER BY id"); + stmt.executeUpdate("INSERT INTO " + t1 + " VALUES (1, 'test_val')"); + stmt.executeUpdate("INSERT INTO " + t2 + " VALUES (1, 'test_name')"); + + // JOIN: ClickHouse includes table name prefix in column metadata (e.g., "t1.id") + try (ResultSet rs = stmt.executeQuery( + "SELECT " + t1 + ".id, " + t2 + ".name FROM " + t1 + " JOIN " + t2 + " ON " + t1 + ".id = " + t2 + ".id")) { + ResultSetMetaData rsmd = rs.getMetaData(); + assertEquals(rsmd.getColumnCount(), 2); + assertEquals(rsmd.getColumnName(1), "id"); + assertEquals(rsmd.getColumnName(2), "name"); + assertEquals(rsmd.getColumnLabel(1), "id"); + assertEquals(rsmd.getColumnLabel(2), "name"); + assertTrue(rs.next()); + assertEquals(rs.getInt("id"), 1); + assertEquals(rs.getString("name"), "test_name"); + assertFalse(rs.next()); + } + + // Alias dot notation: SELECT t.col FROM table AS t + try (ResultSet rs = stmt.executeQuery("SELECT t.id, t.val FROM " + t1 + " AS t")) { + ResultSetMetaData rsmd = rs.getMetaData(); + assertEquals(rsmd.getColumnCount(), 2); + assertEquals(rsmd.getColumnName(1), "id"); + assertEquals(rsmd.getColumnName(2), "val"); + assertTrue(rs.next()); + assertEquals(rs.getInt("id"), 1); + assertEquals(rs.getString("val"), "test_val"); + assertFalse(rs.next()); + } + } + } + static void assertColumnNames(ResultSet rs, String... names) throws Exception { ResultSetMetaData metadata = rs.getMetaData(); assertEquals(names.length, metadata.getColumnCount()); From 7ac43e2334b17c2c9d82631a4a51bfc4c3209aac Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 27 Feb 2026 13:51:48 -0800 Subject: [PATCH 05/10] Added test with timestamps in range --- .../jdbc/PreparedStatementImpl.java | 12 +++++- .../clickhouse/jdbc/JDBCDateTimeTests.java | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java index a29a220f1..37559b83d 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -135,6 +135,7 @@ private String buildSQL() throws SQLException { public ResultSet executeQuery() throws SQLException { ensureOpen(); String buildSQL = buildSQL(); + System.out.println(buildSQL); return super.executeQueryImpl(buildSQL, localSettings); } @@ -743,6 +744,10 @@ private String encodeObject(Object x) throws SQLException { private static final char QUOTE = '\''; + /** Matches a datetime string that ends with a decimal point followed only by zeros, e.g. "2024-09-10 22:58:20.0". + * Such strings are produced by {@link Timestamp#toString()} and are rejected by ClickHouse DateTime. */ + private static final Pattern TRAILING_ZERO_FRACTION = Pattern.compile("(\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2})\\.0+$"); + private static final char O_BRACKET = '['; private static final char C_BRACKET = ']'; @@ -753,7 +758,12 @@ private String encodeObject(Object x, Long length) throws SQLException { if (x == null) { return "NULL"; } else if (x instanceof String) { - return QUOTE + SQLUtils.escapeSingleQuotes((String) x) + QUOTE; + String s = (String) x; + Matcher m = TRAILING_ZERO_FRACTION.matcher(s); + if (m.matches()) { + s = m.group(1); + } + return QUOTE + SQLUtils.escapeSingleQuotes(s) + QUOTE; } else if (x instanceof Boolean) { return (Boolean) x ? "1" : "0"; } else if (x instanceof Date) { 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 ec9ce52dc..ce17cfc30 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -21,6 +21,7 @@ import java.time.Month; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Calendar; import java.util.Properties; @@ -266,4 +267,45 @@ void testDateInRange() throws Exception { } } } + + @Test(groups = {"integration"}) + void testTimestampInRange() throws Exception { + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("DROP TABLE IF EXISTS test_timestamp_in_range"); + stmt.executeUpdate("CREATE TABLE test_timestamp_in_range (id UInt32, ts DateTime) Engine MergeTree ORDER BY()"); + stmt.executeUpdate("INSERT INTO test_timestamp_in_range VALUES " + + "(1, '2025-01-01 08:00:00'), (2, '2025-01-01 12:00:00'), (3, '2025-01-01 18:00:00'), (4, '2025-01-02 00:00:00')"); + + ZoneId utc = ZoneId.of("UTC"); + try (PreparedStatement pStmt = conn.prepareStatement("SELECT * FROM test_timestamp_in_range WHERE ts BETWEEN ? AND ? ORDER BY id")) { + ZonedDateTime start = ZonedDateTime.of(2025, 1, 1, 10, 0, 0, 0, utc); + ZonedDateTime end = ZonedDateTime.of(2025, 1, 1, 20, 0, 0, 0, utc); + pStmt.setObject(1, start); + pStmt.setObject(2, end); + try (ResultSet rs = pStmt.executeQuery()) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 2); + Assert.assertEquals(rs.getObject(2, ZonedDateTime.class), ZonedDateTime.of(2025, 1, 1, 12, 0, 0, 0, utc)); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 3); + Assert.assertEquals(rs.getObject(2, ZonedDateTime.class), ZonedDateTime.of(2025, 1, 1, 18, 0, 0, 0, utc)); + Assert.assertFalse(rs.next()); + } + } + + try (PreparedStatement pStmt = conn.prepareStatement("SELECT * FROM test_timestamp_in_range WHERE ts BETWEEN ? AND ? ORDER BY id")) { + pStmt.setObject(1, ZonedDateTime.of(2025, 1, 1, 0, 0, 0, 0, utc)); + pStmt.setObject(2, ZonedDateTime.of(2025, 1, 1, 12, 0, 0, 0, utc)); + try (ResultSet rs = pStmt.executeQuery()) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 1); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 2); + Assert.assertFalse(rs.next()); + } + } + } + } } From ceba4b9c07d82dc6c287689a7e362d9a56223309 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 27 Feb 2026 14:14:49 -0800 Subject: [PATCH 06/10] Fixed reading arrays when first element is empty collection like Map, List --- .../internal/BinaryStreamReader.java | 6 +- .../clickhouse/jdbc/JdbcDataTypeTests.java | 74 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) 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 f74898268..0e76393a9 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 @@ -648,6 +648,10 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws itemClass = float.class; } else if (firstValue instanceof Double) { itemClass = double.class; + } else if (firstValue instanceof Map) { + itemClass = Map.class; + } else if (firstValue instanceof List) { + itemClass = List.class; } array = new ArrayValue(itemClass, len); @@ -701,7 +705,7 @@ public void set(int index, Object value) { Array.set(array, index, value); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Failed to set value at index: " + index + - " value " + value + " of class " + value.getClass().getName(), e); + " value " + value + " of class " + value.getClass().getName() + " when array is type of " + array.getClass(), e); } } 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 50014398a..9b1df664d 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -1443,6 +1443,80 @@ public void testArrayOfMaps() throws Exception { } } + /** + * Verifies that Array(Map(LowCardinality(String), String)) with empty maps decodes correctly. + * Regression test for #2657 + */ + @Test(groups = {"integration"}) + public void testArrayOfMapsWithLowCardinalityAndEmptyMaps() throws Exception { + runQuery("CREATE TABLE test_array_map_lc_empty (" + + "StartedDateTime DateTime, " + + "traits Array(Map(LowCardinality(String), String))" + + ") ENGINE = MergeTree ORDER BY StartedDateTime"); + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("INSERT INTO test_array_map_lc_empty (StartedDateTime, traits) VALUES (" + + "'2025-11-11 00:00:01', " + + "[" + + " map(), " + + " map(" + + " 'RandomKey1','Value1'," + + " 'RandomKey2','Value2'," + + " 'RandomKey3','Value3'," + + " 'RandomKey4','Value4'," + + " 'RandomKey5','Value5'," + + " 'RandomKey6','Value6'," + + " 'RandomKey7','Value7'," + + " 'RandomKey8','Value8'" + + " ), " + + " map(), map(), map(), map(), map(), map()" + + "]" + + ")"); + + Map expectedNonEmptyMap = new HashMap<>(); + expectedNonEmptyMap.put("RandomKey1", "Value1"); + expectedNonEmptyMap.put("RandomKey2", "Value2"); + expectedNonEmptyMap.put("RandomKey3", "Value3"); + expectedNonEmptyMap.put("RandomKey4", "Value4"); + expectedNonEmptyMap.put("RandomKey5", "Value5"); + expectedNonEmptyMap.put("RandomKey6", "Value6"); + expectedNonEmptyMap.put("RandomKey7", "Value7"); + expectedNonEmptyMap.put("RandomKey8", "Value8"); + + // Run multiple iterations because the bug is intermittent + for (int attempt = 0; attempt < 10; attempt++) { + try (ResultSet rs = stmt.executeQuery("SELECT traits FROM test_array_map_lc_empty")) { + Assert.assertTrue(rs.next(), "Expected a row on attempt " + attempt); + + Array traitsArray = rs.getArray(1); + Assert.assertEquals(traitsArray.getBaseTypeName(), "Map(LowCardinality(String), String)"); + + Object[] maps = (Object[]) traitsArray.getArray(); + Assert.assertEquals(maps.length, 8, "Expected 8 maps in array on attempt " + attempt); + + @SuppressWarnings("unchecked") + Map firstMap = (Map) maps[0]; + Assert.assertTrue(firstMap.isEmpty(), "First map should be empty on attempt " + attempt); + + @SuppressWarnings("unchecked") + Map secondMap = (Map) maps[1]; + Assert.assertEquals(secondMap, expectedNonEmptyMap, "Second map mismatch on attempt " + attempt); + + for (int i = 2; i < 8; i++) { + @SuppressWarnings("unchecked") + Map emptyMap = (Map) maps[i]; + Assert.assertTrue(emptyMap.isEmpty(), + "Map at index " + i + " should be empty on attempt " + attempt); + } + + Assert.assertFalse(rs.next()); + } + } + } + } + @Test(groups = { "integration" }) public void testNullableTypesSimpleStatement() throws SQLException { runQuery("CREATE TABLE test_nullable (order Int8, " From 3a60b7f55c7daf79f896bec80fc7e4e74177acb5 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 27 Feb 2026 14:32:57 -0800 Subject: [PATCH 07/10] verified json is read correctly --- .../client/datatypes/DataTypeTests.java | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) 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 7558195a4..f7b5e5be5 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 @@ -1529,12 +1529,6 @@ public void testGetStringArrayAndGetObjectArrayWhenValueIsList() throws Exceptio } } - /** - * Regression test for https://github.com/ClickHouse/clickhouse-go/issues/1775 - * When scanning JSON rows into a map, keys that are absent in a given row must not - * appear in that row's result, even when another row in the same result set contains - * those keys. - */ @Test(groups = {"integration"}) public void testJSONScanDoesNotLeakKeysAcrossRows() throws Exception { if (isVersionMatch("(,24.8]")) { @@ -1570,6 +1564,74 @@ public void testJSONScanDoesNotLeakKeysAcrossRows() throws Exception { } } + @Test(groups = {"integration"}, dataProvider = "testJSONSubPathAccess_dp") + public void testJSONSubPathAccess(String query, Object[] expectedValues) throws Exception { + if (isVersionMatch("(,24.8]")) { + return; + } + + final String table = "test_json_sub_path_access"; + client.execute("DROP TABLE IF EXISTS " + table).get().close(); + + CommandSettings jsonSettings = (CommandSettings) new CommandSettings() + .serverSetting("enable_json_type", "1") + .serverSetting("allow_experimental_json_type", "1"); + client.execute("CREATE TABLE " + table + " (`i` Int64, `j` JSON) ENGINE = MergeTree ORDER BY i", + jsonSettings).get().close(); + client.execute("INSERT INTO " + table + " VALUES " + + "(1, '{\"m\":{\"a\":[{\"d\": 9000}]}}'), " + + "(2, '{\"m\":{\"a\":[{\"d\": 42}, {\"d\": 7}]}}')", jsonSettings).get().close(); + + String fullQuery = query.replace("${table}", table); + try (QueryResponse response = client.query(fullQuery).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + int rowIndex = 0; + while (reader.next() != null) { + Assert.assertTrue(rowIndex < expectedValues.length, + "More rows returned than expected for query: " + fullQuery); + Object actual = reader.readValue(1); + if (actual instanceof BinaryStreamReader.ArrayValue) { + actual = ((BinaryStreamReader.ArrayValue) actual).asList(); + } + Assert.assertEquals(actual, expectedValues[rowIndex], + "Mismatch at row " + rowIndex + " for query: " + fullQuery); + rowIndex++; + } + Assert.assertEquals(rowIndex, expectedValues.length, + "Row count mismatch for query: " + fullQuery); + } + } + + @DataProvider + public Object[][] testJSONSubPathAccess_dp() { + Map obj9000 = Collections.singletonMap("d", 9000L); + Map obj42 = Collections.singletonMap("d", 42L); + Map obj7 = Collections.singletonMap("d", 7L); + + return new Object[][] { + // gh#2703: backtick-quoted JSON path with array element access + { + "SELECT j.`m`.`a`[].`d`[1] FROM ${table} ORDER BY i", + new Object[] { 9000L, 42L } + }, + // dot-notation sub-path access + { + "SELECT j.m.a FROM ${table} ORDER BY i", + new Object[] { Arrays.asList(obj9000), Arrays.asList(obj42, obj7) } + }, + // array element access with index + { + "SELECT j.m.a[].d[1] FROM ${table} ORDER BY i", + new Object[] { 9000L, 42L } + }, + // sub-column access to nested field + { + "SELECT j.m.a[].d FROM ${table} ORDER BY i", + new Object[] { Arrays.asList(9000L), Arrays.asList(42L, 7L) } + }, + }; + } + public static String tableDefinition(String table, String... columns) { StringBuilder sb = new StringBuilder(); sb.append("CREATE TABLE " + table + " ( "); From 85a27df78c8e63ec4aad336ae0c9baec3de8adb7 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 27 Feb 2026 14:35:55 -0800 Subject: [PATCH 08/10] fixed type in 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 ce17cfc30..5f49b4825 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -254,7 +254,7 @@ void testDateInRange() throws Exception { Assert.assertFalse(rs.next()); } - Array range = conn.createArrayOf("Array(Date)", new Object[] {Date.valueOf("2025-02-01"), + Array range = conn.createArrayOf("Date", new Object[] {Date.valueOf("2025-02-01"), Date.valueOf("2025-02-03")}); pStmt.setArray(1, range); try (ResultSet rs = pStmt.executeQuery()) { From 50bbce4fd8d9f8dd2518711701440c724a8a7f1e Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sat, 28 Feb 2026 08:02:09 -0800 Subject: [PATCH 09/10] removed unwanted change. Fixed tests --- .../clickhouse/client/datatypes/DataTypeTests.java | 6 +++--- .../com/clickhouse/jdbc/PreparedStatementImpl.java | 13 ++----------- 2 files changed, 5 insertions(+), 14 deletions(-) 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 f7b5e5be5..2045a0d29 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 @@ -1541,10 +1541,10 @@ public void testJSONScanDoesNotLeakKeysAcrossRows() throws Exception { .serverSetting("allow_experimental_json_type", "1"); client.execute("DROP TABLE IF EXISTS " + table).get().close(); - client.execute(tableDefinition(table, "data JSON"), cmdSettings).get().close(); - client.execute("INSERT INTO " + table + " VALUES ('{\"a\": \"foo\"}'::JSON), ('{\"b\": \"bar\"}'::JSON)").get().close(); + client.execute(tableDefinition(table, "id UInt32", "data JSON"), cmdSettings).get().close(); + client.execute("INSERT INTO " + table + " VALUES (1, '{\"a\": \"foo\"}'::JSON), (2, '{\"b\": \"bar\"}'::JSON)", cmdSettings).get().close(); - List records = client.queryAll("SELECT * FROM " + table + " ORDER BY data"); + List records = client.queryAll("SELECT * FROM " + table + " ORDER BY id"); Assert.assertEquals(records.size(), 2); for (GenericRecord record : records) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java index 37559b83d..6913f5d3b 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -135,7 +135,6 @@ private String buildSQL() throws SQLException { public ResultSet executeQuery() throws SQLException { ensureOpen(); String buildSQL = buildSQL(); - System.out.println(buildSQL); return super.executeQueryImpl(buildSQL, localSettings); } @@ -485,6 +484,7 @@ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws S ensureOpen(); TimeZone tz = (cal == null ? defaultCalendar : cal).getTimeZone(); values[parameterIndex - 1] = encodeObject(DataTypeUtils.toZonedDateTime(x, tz)); + values[parameterIndex - 1] = encodeObject(DataTypeUtils.toZonedDateTime(x, tz)); } @Override @@ -744,10 +744,6 @@ private String encodeObject(Object x) throws SQLException { private static final char QUOTE = '\''; - /** Matches a datetime string that ends with a decimal point followed only by zeros, e.g. "2024-09-10 22:58:20.0". - * Such strings are produced by {@link Timestamp#toString()} and are rejected by ClickHouse DateTime. */ - private static final Pattern TRAILING_ZERO_FRACTION = Pattern.compile("(\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2})\\.0+$"); - private static final char O_BRACKET = '['; private static final char C_BRACKET = ']'; @@ -758,12 +754,7 @@ private String encodeObject(Object x, Long length) throws SQLException { if (x == null) { return "NULL"; } else if (x instanceof String) { - String s = (String) x; - Matcher m = TRAILING_ZERO_FRACTION.matcher(s); - if (m.matches()) { - s = m.group(1); - } - return QUOTE + SQLUtils.escapeSingleQuotes(s) + QUOTE; + return QUOTE + SQLUtils.escapeSingleQuotes((String) x) + QUOTE; } else if (x instanceof Boolean) { return (Boolean) x ? "1" : "0"; } else if (x instanceof Date) { From ae2c1e4bdd0bad35e5af8a1d55b174d8b70a5b0b Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sat, 28 Feb 2026 09:01:00 -0800 Subject: [PATCH 10/10] Fixed issues --- .../client/api/data_formats/internal/BinaryStreamReader.java | 2 +- .../main/java/com/clickhouse/jdbc/PreparedStatementImpl.java | 1 - .../src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) 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 0e76393a9..04acf3a2b 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 @@ -705,7 +705,7 @@ public void set(int index, Object value) { Array.set(array, index, value); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Failed to set value at index: " + index + - " value " + value + " of class " + value.getClass().getName() + " when array is type of " + array.getClass(), e); + " value " + value + " of class " + value.getClass().getName() + " when array type is " + array.getClass(), e); } } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java index 6913f5d3b..a29a220f1 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -484,7 +484,6 @@ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws S ensureOpen(); TimeZone tz = (cal == null ? defaultCalendar : cal).getTimeZone(); values[parameterIndex - 1] = encodeObject(DataTypeUtils.toZonedDateTime(x, tz)); - values[parameterIndex - 1] = encodeObject(DataTypeUtils.toZonedDateTime(x, tz)); } @Override 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 5f49b4825..1c74cb4bb 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java @@ -230,7 +230,7 @@ void testLapsTime() throws Exception { } } - @Test + @Test(groups = {"integration"}) void testDateInRange() throws Exception { try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) {