Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 type is " + array.getClass(), e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,109 @@ public void testGetStringArrayAndGetObjectArrayWhenValueIsList() throws Exceptio
}
}

@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, "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<GenericRecord> records = client.queryAll("SELECT * FROM " + table + " ORDER BY id");
Assert.assertEquals(records.size(), 2);

for (GenericRecord record : records) {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) 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);
}
}
}

@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<String, Object> obj9000 = Collections.singletonMap("d", 9000L);
Map<String, Object> obj42 = Collections.singletonMap("d", 42L);
Map<String, Object> 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 + " ( ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,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;
Expand Down Expand Up @@ -227,4 +229,83 @@ void testLapsTime() throws Exception {
}
}
}

@Test(groups = {"integration"})
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("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());
}
}
}
}

@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());
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,80 @@ public void testArrayOfMaps() throws Exception {
}
}

/**
* Verifies that Array(Map(LowCardinality(String), String)) with empty maps decodes correctly.
* Regression test for <a href="https://github.com/ClickHouse/clickhouse-java/issues/2657">#2657</a>
*/
@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<String, String> 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<String, String> firstMap = (Map<String, String>) maps[0];
Assert.assertTrue(firstMap.isEmpty(), "First map should be empty on attempt " + attempt);

@SuppressWarnings("unchecked")
Map<String, String> secondMap = (Map<String, String>) maps[1];
Assert.assertEquals(secondMap, expectedNonEmptyMap, "Second map mismatch on attempt " + attempt);

for (int i = 2; i < 8; i++) {
@SuppressWarnings("unchecked")
Map<String, String> emptyMap = (Map<String, String>) 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, "
Expand Down
37 changes: 37 additions & 0 deletions jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading