Skip to content

Commit f0f296a

Browse files
authored
Merge pull request #2765 from ClickHouse/02/26/26/issue_verification
[jdbc-v2,client-v2] Issue Verification
2 parents 6a9468d + ae2c1e4 commit f0f296a

File tree

6 files changed

+342
-1
lines changed

6 files changed

+342
-1
lines changed

client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,10 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws
648648
itemClass = float.class;
649649
} else if (firstValue instanceof Double) {
650650
itemClass = double.class;
651+
} else if (firstValue instanceof Map) {
652+
itemClass = Map.class;
653+
} else if (firstValue instanceof List) {
654+
itemClass = List.class;
651655
}
652656

653657
array = new ArrayValue(itemClass, len);
@@ -701,7 +705,7 @@ public void set(int index, Object value) {
701705
Array.set(array, index, value);
702706
} catch (IllegalArgumentException e) {
703707
throw new IllegalArgumentException("Failed to set value at index: " + index +
704-
" value " + value + " of class " + value.getClass().getName(), e);
708+
" value " + value + " of class " + value.getClass().getName() + " when array type is " + array.getClass(), e);
705709
}
706710
}
707711

client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,109 @@ public void testGetStringArrayAndGetObjectArrayWhenValueIsList() throws Exceptio
15291529
}
15301530
}
15311531

1532+
@Test(groups = {"integration"})
1533+
public void testJSONScanDoesNotLeakKeysAcrossRows() throws Exception {
1534+
if (isVersionMatch("(,24.8]")) {
1535+
return;
1536+
}
1537+
1538+
final String table = "test_json_no_key_leak";
1539+
final CommandSettings cmdSettings = (CommandSettings) new CommandSettings()
1540+
.serverSetting("enable_json_type", "1")
1541+
.serverSetting("allow_experimental_json_type", "1");
1542+
1543+
client.execute("DROP TABLE IF EXISTS " + table).get().close();
1544+
client.execute(tableDefinition(table, "id UInt32", "data JSON"), cmdSettings).get().close();
1545+
client.execute("INSERT INTO " + table + " VALUES (1, '{\"a\": \"foo\"}'::JSON), (2, '{\"b\": \"bar\"}'::JSON)", cmdSettings).get().close();
1546+
1547+
List<GenericRecord> records = client.queryAll("SELECT * FROM " + table + " ORDER BY id");
1548+
Assert.assertEquals(records.size(), 2);
1549+
1550+
for (GenericRecord record : records) {
1551+
@SuppressWarnings("unchecked")
1552+
Map<String, Object> data = (Map<String, Object>) record.getObject("data");
1553+
Assert.assertNotNull(data, "JSON column should not be null");
1554+
1555+
if (data.containsKey("a")) {
1556+
Assert.assertFalse(data.containsKey("b"),
1557+
"Row with key 'a' should not contain key 'b', but got: " + data);
1558+
} else if (data.containsKey("b")) {
1559+
Assert.assertFalse(data.containsKey("a"),
1560+
"Row with key 'b' should not contain key 'a', but got: " + data);
1561+
} else {
1562+
Assert.fail("Expected row to contain either key 'a' or 'b', but got: " + data);
1563+
}
1564+
}
1565+
}
1566+
1567+
@Test(groups = {"integration"}, dataProvider = "testJSONSubPathAccess_dp")
1568+
public void testJSONSubPathAccess(String query, Object[] expectedValues) throws Exception {
1569+
if (isVersionMatch("(,24.8]")) {
1570+
return;
1571+
}
1572+
1573+
final String table = "test_json_sub_path_access";
1574+
client.execute("DROP TABLE IF EXISTS " + table).get().close();
1575+
1576+
CommandSettings jsonSettings = (CommandSettings) new CommandSettings()
1577+
.serverSetting("enable_json_type", "1")
1578+
.serverSetting("allow_experimental_json_type", "1");
1579+
client.execute("CREATE TABLE " + table + " (`i` Int64, `j` JSON) ENGINE = MergeTree ORDER BY i",
1580+
jsonSettings).get().close();
1581+
client.execute("INSERT INTO " + table + " VALUES " +
1582+
"(1, '{\"m\":{\"a\":[{\"d\": 9000}]}}'), " +
1583+
"(2, '{\"m\":{\"a\":[{\"d\": 42}, {\"d\": 7}]}}')", jsonSettings).get().close();
1584+
1585+
String fullQuery = query.replace("${table}", table);
1586+
try (QueryResponse response = client.query(fullQuery).get()) {
1587+
ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response);
1588+
int rowIndex = 0;
1589+
while (reader.next() != null) {
1590+
Assert.assertTrue(rowIndex < expectedValues.length,
1591+
"More rows returned than expected for query: " + fullQuery);
1592+
Object actual = reader.readValue(1);
1593+
if (actual instanceof BinaryStreamReader.ArrayValue) {
1594+
actual = ((BinaryStreamReader.ArrayValue) actual).asList();
1595+
}
1596+
Assert.assertEquals(actual, expectedValues[rowIndex],
1597+
"Mismatch at row " + rowIndex + " for query: " + fullQuery);
1598+
rowIndex++;
1599+
}
1600+
Assert.assertEquals(rowIndex, expectedValues.length,
1601+
"Row count mismatch for query: " + fullQuery);
1602+
}
1603+
}
1604+
1605+
@DataProvider
1606+
public Object[][] testJSONSubPathAccess_dp() {
1607+
Map<String, Object> obj9000 = Collections.singletonMap("d", 9000L);
1608+
Map<String, Object> obj42 = Collections.singletonMap("d", 42L);
1609+
Map<String, Object> obj7 = Collections.singletonMap("d", 7L);
1610+
1611+
return new Object[][] {
1612+
// gh#2703: backtick-quoted JSON path with array element access
1613+
{
1614+
"SELECT j.`m`.`a`[].`d`[1] FROM ${table} ORDER BY i",
1615+
new Object[] { 9000L, 42L }
1616+
},
1617+
// dot-notation sub-path access
1618+
{
1619+
"SELECT j.m.a FROM ${table} ORDER BY i",
1620+
new Object[] { Arrays.asList(obj9000), Arrays.asList(obj42, obj7) }
1621+
},
1622+
// array element access with index
1623+
{
1624+
"SELECT j.m.a[].d[1] FROM ${table} ORDER BY i",
1625+
new Object[] { 9000L, 42L }
1626+
},
1627+
// sub-column access to nested field
1628+
{
1629+
"SELECT j.m.a[].d FROM ${table} ORDER BY i",
1630+
new Object[] { Arrays.asList(9000L), Arrays.asList(42L, 7L) }
1631+
},
1632+
};
1633+
}
1634+
15321635
public static String tableDefinition(String table, String... columns) {
15331636
StringBuilder sb = new StringBuilder();
15341637
sb.append("CREATE TABLE " + table + " ( ");

jdbc-v2/src/test/java/com/clickhouse/jdbc/JDBCDateTimeTests.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.testng.Assert;
77
import org.testng.annotations.Test;
88

9+
import java.sql.Array;
910
import java.sql.Connection;
1011
import java.sql.Date;
1112
import java.sql.PreparedStatement;
@@ -20,6 +21,7 @@
2021
import java.time.Month;
2122
import java.time.ZoneId;
2223
import java.time.ZoneOffset;
24+
import java.time.ZonedDateTime;
2325
import java.time.temporal.ChronoUnit;
2426
import java.util.Calendar;
2527
import java.util.Properties;
@@ -227,4 +229,83 @@ void testLapsTime() throws Exception {
227229
}
228230
}
229231
}
232+
233+
@Test(groups = {"integration"})
234+
void testDateInRange() throws Exception {
235+
try (Connection conn = getJdbcConnection();
236+
Statement stmt = conn.createStatement()) {
237+
238+
stmt.executeUpdate("DROP TABLE IF EXISTS test_date_in_range");
239+
stmt.executeUpdate("CREATE TABLE test_date_in_range ( id UInt32, d Date) Engine MergeTree ORDER BY()");
240+
stmt.executeUpdate("INSERT INTO test_date_in_range VALUES (1, '2025-01-01') , (2, '2025-02-01') , (3, '2025-02-03')");
241+
242+
try (PreparedStatement pStmt = conn.prepareStatement("SELECT * FROM test_date_in_range WHERE d IN (?) ORDER BY id")){
243+
pStmt.setDate(1, Date.valueOf("2025-02-01"));
244+
try (ResultSet rs = pStmt.executeQuery()) {
245+
Assert.assertTrue(rs.next());
246+
Assert.assertEquals(rs.getInt(1), 2);
247+
Assert.assertFalse(rs.next());
248+
}
249+
250+
pStmt.setObject(1, LocalDate.parse("2025-02-01"));
251+
try (ResultSet rs = pStmt.executeQuery()) {
252+
Assert.assertTrue(rs.next());
253+
Assert.assertEquals(rs.getInt(1), 2);
254+
Assert.assertFalse(rs.next());
255+
}
256+
257+
Array range = conn.createArrayOf("Date", new Object[] {Date.valueOf("2025-02-01"),
258+
Date.valueOf("2025-02-03")});
259+
pStmt.setArray(1, range);
260+
try (ResultSet rs = pStmt.executeQuery()) {
261+
Assert.assertTrue(rs.next());
262+
Assert.assertEquals(rs.getInt(1), 2);
263+
Assert.assertTrue(rs.next());
264+
Assert.assertEquals(rs.getInt(1), 3);
265+
Assert.assertFalse(rs.next());
266+
}
267+
}
268+
}
269+
}
270+
271+
@Test(groups = {"integration"})
272+
void testTimestampInRange() throws Exception {
273+
try (Connection conn = getJdbcConnection();
274+
Statement stmt = conn.createStatement()) {
275+
276+
stmt.executeUpdate("DROP TABLE IF EXISTS test_timestamp_in_range");
277+
stmt.executeUpdate("CREATE TABLE test_timestamp_in_range (id UInt32, ts DateTime) Engine MergeTree ORDER BY()");
278+
stmt.executeUpdate("INSERT INTO test_timestamp_in_range VALUES " +
279+
"(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')");
280+
281+
ZoneId utc = ZoneId.of("UTC");
282+
try (PreparedStatement pStmt = conn.prepareStatement("SELECT * FROM test_timestamp_in_range WHERE ts BETWEEN ? AND ? ORDER BY id")) {
283+
ZonedDateTime start = ZonedDateTime.of(2025, 1, 1, 10, 0, 0, 0, utc);
284+
ZonedDateTime end = ZonedDateTime.of(2025, 1, 1, 20, 0, 0, 0, utc);
285+
pStmt.setObject(1, start);
286+
pStmt.setObject(2, end);
287+
try (ResultSet rs = pStmt.executeQuery()) {
288+
Assert.assertTrue(rs.next());
289+
Assert.assertEquals(rs.getInt(1), 2);
290+
Assert.assertEquals(rs.getObject(2, ZonedDateTime.class), ZonedDateTime.of(2025, 1, 1, 12, 0, 0, 0, utc));
291+
Assert.assertTrue(rs.next());
292+
Assert.assertEquals(rs.getInt(1), 3);
293+
Assert.assertEquals(rs.getObject(2, ZonedDateTime.class), ZonedDateTime.of(2025, 1, 1, 18, 0, 0, 0, utc));
294+
Assert.assertFalse(rs.next());
295+
}
296+
}
297+
298+
try (PreparedStatement pStmt = conn.prepareStatement("SELECT * FROM test_timestamp_in_range WHERE ts BETWEEN ? AND ? ORDER BY id")) {
299+
pStmt.setObject(1, ZonedDateTime.of(2025, 1, 1, 0, 0, 0, 0, utc));
300+
pStmt.setObject(2, ZonedDateTime.of(2025, 1, 1, 12, 0, 0, 0, utc));
301+
try (ResultSet rs = pStmt.executeQuery()) {
302+
Assert.assertTrue(rs.next());
303+
Assert.assertEquals(rs.getInt(1), 1);
304+
Assert.assertTrue(rs.next());
305+
Assert.assertEquals(rs.getInt(1), 2);
306+
Assert.assertFalse(rs.next());
307+
}
308+
}
309+
}
310+
}
230311
}

jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,80 @@ public void testArrayOfMaps() throws Exception {
14431443
}
14441444
}
14451445

1446+
/**
1447+
* Verifies that Array(Map(LowCardinality(String), String)) with empty maps decodes correctly.
1448+
* Regression test for <a href="https://github.com/ClickHouse/clickhouse-java/issues/2657">#2657</a>
1449+
*/
1450+
@Test(groups = {"integration"})
1451+
public void testArrayOfMapsWithLowCardinalityAndEmptyMaps() throws Exception {
1452+
runQuery("CREATE TABLE test_array_map_lc_empty ("
1453+
+ "StartedDateTime DateTime, "
1454+
+ "traits Array(Map(LowCardinality(String), String))"
1455+
+ ") ENGINE = MergeTree ORDER BY StartedDateTime");
1456+
1457+
try (Connection conn = getJdbcConnection();
1458+
Statement stmt = conn.createStatement()) {
1459+
1460+
stmt.executeUpdate("INSERT INTO test_array_map_lc_empty (StartedDateTime, traits) VALUES ("
1461+
+ "'2025-11-11 00:00:01', "
1462+
+ "["
1463+
+ " map(), "
1464+
+ " map("
1465+
+ " 'RandomKey1','Value1',"
1466+
+ " 'RandomKey2','Value2',"
1467+
+ " 'RandomKey3','Value3',"
1468+
+ " 'RandomKey4','Value4',"
1469+
+ " 'RandomKey5','Value5',"
1470+
+ " 'RandomKey6','Value6',"
1471+
+ " 'RandomKey7','Value7',"
1472+
+ " 'RandomKey8','Value8'"
1473+
+ " ), "
1474+
+ " map(), map(), map(), map(), map(), map()"
1475+
+ "]"
1476+
+ ")");
1477+
1478+
Map<String, String> expectedNonEmptyMap = new HashMap<>();
1479+
expectedNonEmptyMap.put("RandomKey1", "Value1");
1480+
expectedNonEmptyMap.put("RandomKey2", "Value2");
1481+
expectedNonEmptyMap.put("RandomKey3", "Value3");
1482+
expectedNonEmptyMap.put("RandomKey4", "Value4");
1483+
expectedNonEmptyMap.put("RandomKey5", "Value5");
1484+
expectedNonEmptyMap.put("RandomKey6", "Value6");
1485+
expectedNonEmptyMap.put("RandomKey7", "Value7");
1486+
expectedNonEmptyMap.put("RandomKey8", "Value8");
1487+
1488+
// Run multiple iterations because the bug is intermittent
1489+
for (int attempt = 0; attempt < 10; attempt++) {
1490+
try (ResultSet rs = stmt.executeQuery("SELECT traits FROM test_array_map_lc_empty")) {
1491+
Assert.assertTrue(rs.next(), "Expected a row on attempt " + attempt);
1492+
1493+
Array traitsArray = rs.getArray(1);
1494+
Assert.assertEquals(traitsArray.getBaseTypeName(), "Map(LowCardinality(String), String)");
1495+
1496+
Object[] maps = (Object[]) traitsArray.getArray();
1497+
Assert.assertEquals(maps.length, 8, "Expected 8 maps in array on attempt " + attempt);
1498+
1499+
@SuppressWarnings("unchecked")
1500+
Map<String, String> firstMap = (Map<String, String>) maps[0];
1501+
Assert.assertTrue(firstMap.isEmpty(), "First map should be empty on attempt " + attempt);
1502+
1503+
@SuppressWarnings("unchecked")
1504+
Map<String, String> secondMap = (Map<String, String>) maps[1];
1505+
Assert.assertEquals(secondMap, expectedNonEmptyMap, "Second map mismatch on attempt " + attempt);
1506+
1507+
for (int i = 2; i < 8; i++) {
1508+
@SuppressWarnings("unchecked")
1509+
Map<String, String> emptyMap = (Map<String, String>) maps[i];
1510+
Assert.assertTrue(emptyMap.isEmpty(),
1511+
"Map at index " + i + " should be empty on attempt " + attempt);
1512+
}
1513+
1514+
Assert.assertFalse(rs.next());
1515+
}
1516+
}
1517+
}
1518+
}
1519+
14461520
@Test(groups = { "integration" })
14471521
public void testNullableTypesSimpleStatement() throws SQLException {
14481522
runQuery("CREATE TABLE test_nullable (order Int8, "

jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,43 @@ public void testResponseWithDuplicateColumns() throws Exception {
12851285
}
12861286
}
12871287

1288+
@Test(groups = {"integration"})
1289+
public void testDescribeStatement() throws Exception {
1290+
try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) {
1291+
boolean isResultSet = stmt.execute("DESCRIBE table (SELECT 10, 'message', 30)");
1292+
Assert.assertTrue(isResultSet);
1293+
try (ResultSet rs = stmt.getResultSet()) {
1294+
Object[][] expected = new Object[][] {
1295+
{"10", "UInt8"},
1296+
{"'message'", "String"},
1297+
{"30", "UInt8"},
1298+
};
1299+
1300+
for (Object[] objects : expected) {
1301+
Assert.assertTrue(rs.next());
1302+
Assert.assertEquals(rs.getString("name"), objects[0]);
1303+
Assert.assertEquals(rs.getString("type"), objects[1]);
1304+
}
1305+
}
1306+
}
1307+
1308+
try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) {
1309+
boolean isResultSet = stmt.execute("DESCRIBE TABLE (SELECT numbers.number FROM system.numbers)");
1310+
Assert.assertTrue(isResultSet);
1311+
try (ResultSet rs = stmt.getResultSet()) {
1312+
Object[][] expected = new Object[][] {
1313+
{"number", "UInt64"},
1314+
};
1315+
1316+
for (Object[] objects : expected) {
1317+
Assert.assertTrue(rs.next());
1318+
Assert.assertEquals(rs.getString("name"), objects[0]);
1319+
Assert.assertEquals(rs.getString("type"), objects[1]);
1320+
}
1321+
}
1322+
}
1323+
}
1324+
12881325
private static String getDBName(Statement stmt) throws SQLException {
12891326
try (ResultSet rs = stmt.executeQuery("SELECT database()")) {
12901327
rs.next();

0 commit comments

Comments
 (0)