Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Compatibility-sensitive traits:
- QBit type mapping: For ClickHouse `25.10+`, JDBC exposes the experimental `QBit(element_type, dimension)` type as `ARRAY`, returning the vector as a `java.sql.Array` of the element type from `getObject()`/`getArray()`. Supported element types are `BFloat16` and `Float32` (both `java.lang.Float`) and `Float64` (`java.lang.Double`). The `allow_experimental_qbit_type` server setting is required to create a `QBit` column.
- Custom result-set type map: `ResultSet#getObject(int|String, Map<String, Class<?>>)` accepts both ClickHouse type names and JDBC `SQLType` names as map keys. Only unwrapped type names are used — `Nullable(...)` and `LowCardinality(...)` wrappers are stripped before lookup, so a key like `"Int32"` matches both `Int32` and `Nullable(Int32)` columns, and keys like `"Nullable(Int32)"` are not recognized. Lookup order is `ClickHouseColumn#getDataType().name()` (e.g. `"Int32"`, `"String"`, `"DateTime"`) then `SQLType.getName()` (e.g. `"INTEGER"`, `"VARCHAR"`, `"TIMESTAMP"`); a missing entry leaves the value uncoerced (read as-is). The feature is supported for primitive ClickHouse types only — `Array`, `Tuple`, `Map`, `Nested`, and geometry types (`Point`, `Ring`, `LineString`, `MultiPoint`, `Polygon`, `MultiPolygon`, `MultiLineString`, `Geometry`) bypass the type map and are returned in their native form.
- Arrays and tuples: Supports JDBC arrays plus ClickHouse tuple values through custom `Array` and `Struct` implementations.
- Nested columns: Un-flattened `Nested(f1 T1, ..., fN TN)` columns (tables created with `flatten_nested = 0`) are exposed as JDBC `ARRAY` whose element type is `Tuple(f1 T1, ..., fN TN)`. They can be inserted through `Connection#createArrayOf`/`setArray` or `setObject` (a Java array of tuples) and read back through `getArray`/`getObject`; `java.sql.Array#getResultSet()` iterates the nested rows as `(INDEX, VALUE)` pairs where each `VALUE` is the tuple.
- Nested columns: Un-flattened `Nested(f1 T1, ..., fN TN)` columns (tables created with `flatten_nested = 0`) are exposed as JDBC `ARRAY` whose element type is `Tuple(f1 T1, ..., fN TN)`. They can be inserted through `Connection#createArrayOf`/`setArray` or `setObject` (a Java array of tuples) and read back through `getArray`/`getObject`; `java.sql.Array#getResultSet()` iterates the nested rows as `(INDEX, VALUE)` pairs where each `VALUE` is the tuple. An `Array(Nested(...))` column is read as a JDBC `ARRAY` whose elements are the nested values, each an array of tuples (`Object[]` of `Object[]`); its `getResultSet()` returns each element as a `java.sql.Array` equal to a top-level `Nested(...)` value.
- Geometry type mapping: For ClickHouse `25.11+`, where `Geometry` changed from a string alias to `Variant(Point, Ring, LineString, MultiLineString, Polygon, MultiPolygon)`, JDBC exposes `Geometry` as `ARRAY`, returns nested Java arrays from `getObject()`/`getArray()`, and accepts `Struct` or nested `Array` inputs for prepared-statement inserts depending on the geometry shape. For ClickHouse `26.8+` the variant also carries `MultiPoint`.
- MultiPoint type mapping: For ClickHouse `26.8+`, JDBC exposes `MultiPoint` as `ARRAY` with type name `MultiPoint`, returns `double[][]` from `getObject()` and a `java.sql.Array` from `getArray()`, and accepts a nested `Array` (`createArrayOf("Array(Point)", ...)`) for prepared-statement inserts, the same as `Ring` and `LineString`.
- Client info propagation: Supports JDBC client info such as `ApplicationName` and forwards it to the underlying client name.
Expand Down
7 changes: 7 additions & 0 deletions history/latest/3178.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- **[jdbc-v2]** Fixed reading an `Array(Nested(...))` column through `ResultSet#getObject`/`getArray`, which failed
with `IllegalStateException: There is a child array at depth 0 where it is not expected`. A `Nested(...)` value is
itself an array of tuples, but the conversion counted only the `Array` levels of the column. The `Nested` level is
now counted too: the column is returned as a `java.sql.Array` whose elements are the nested values (each an
`Object[]` of tuples), the same as an `Array(Array(Tuple(...)))` column, and `java.sql.Array#getResultSet()` returns
each element as a `java.sql.Array` equal to a top-level `Nested(...)` value.
(https://github.com/ClickHouse/clickhouse-java/issues/3178)
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,14 @@ public static Object convert(Object value, Class<?> type, ClickHouseColumn colum
if (value instanceof List<?>) {
List<?> listValue = (List<?>) value;
if (type != java.sql.Array.class) {
return convertList(listValue, type, column.getArrayNestedLevel());
return convertList(listValue, type, getArrayDimensions(column));
}

if (column != null && column.getArrayBaseColumn() != null) {
ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType();
ClickHouseColumn baseColumn = column.getArrayBaseColumn();
ClickHouseDataType baseType = baseColumn.isNested() ? ClickHouseDataType.Tuple : baseColumn.getDataType();
Object[] convertedValues = convertList(listValue, convertToJavaClass(baseType),
column.getArrayNestedLevel());
getArrayDimensions(column));
return new Array(column, convertedValues);
}

Expand Down Expand Up @@ -346,6 +347,15 @@ public static Object convert(Object value, Class<?> type, ClickHouseColumn colum
return convertObject(value, type, column);
}

/**
* Returns the number of array dimensions of an array column value. A {@code Nested(...)} base column is an
* array of tuples, so it adds one more dimension to the {@code Array} levels of the column.
*/
private static int getArrayDimensions(ClickHouseColumn column) {
ClickHouseColumn baseColumn = column.getArrayBaseColumn();
return column.getArrayNestedLevel() + (baseColumn != null && baseColumn.isNested() ? 1 : 0);
}

static Object convertObject(Object value, Class<?> type, ClickHouseColumn column) throws SQLException {
if (value == null || type == null) {
return value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1898,6 +1898,76 @@ private static void assertTupleEquals(Object[] actual, Tuple expected) {
assertEquals(actual[1], expected.getValue(1)); // Nullable(String): null stays null
}

@Test(groups = { "integration" })
public void testArrayOfNestedType() throws SQLException {
runQuery("DROP TABLE IF EXISTS test_array_of_nested_jdbc");
runQuery("CREATE TABLE test_array_of_nested_jdbc (order Int8, "
+ "an Array(Nested(a Int8, b Nullable(String))), "
+ "aan Array(Array(Nested(a Int8, b Nullable(String)))), "
+ "aat Array(Array(Tuple(a Int8, b Nullable(String)))), "
+ "tail Int32"
+ ") ENGINE = MergeTree ORDER BY (order)");
runQuery("INSERT INTO test_array_of_nested_jdbc VALUES "
+ "(1, [[(1, 'x'), (2, NULL)], [], [(3, 'y')]], [[[(1, 'x'), (2, NULL)]], [], [[], [(3, 'y')]]], "
+ "[[(1, 'x'), (2, NULL)], [], [(3, 'y')]], 100), "
+ "(2, [], [], [], 200)");

Tuple[][] an = new Tuple[][] {
{ new Tuple((byte) 1, "x"), new Tuple((byte) 2, null) },
{},
{ new Tuple((byte) 3, "y") },
};
Tuple[][][] aan = new Tuple[][][] {
{ { new Tuple((byte) 1, "x"), new Tuple((byte) 2, null) } },
{},
{ {}, { new Tuple((byte) 3, "y") } },
};

try (Connection conn = getJdbcConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT order, an, aan, aat, tail FROM test_array_of_nested_jdbc ORDER BY order")) {
assertTrue(rs.next());
assertEquals(rs.getByte("order"), (byte) 1);
assertArrayOfNestedEquals(rs.getArray("an"), an);
assertArrayOfNestedEquals((Array) rs.getObject("an"), an);
Object[] aanValue = (Object[]) rs.getArray("aan").getArray();
assertEquals(aanValue.length, aan.length);
try (ResultSet ars = rs.getArray("aan").getResultSet()) {
for (Tuple[][] expected : aan) {
assertTrue(ars.next());
assertArrayOfNestedEquals((Array) ars.getObject(2), expected);
}
assertFalse(ars.next());
}
assertArrayOfNestedEquals(rs.getArray("aat"), an);
assertTrue(Arrays.deepEquals((Object[]) rs.getArray("an").getArray(), (Object[]) rs.getArray("aat").getArray()));
assertTrue(Arrays.deepEquals(rs.getObject("an", Object[].class), rs.getObject("aat", Object[].class)));
assertEquals(rs.getInt("tail"), 100);

assertTrue(rs.next());
assertEquals(rs.getByte("order"), (byte) 2);
assertEquals(((Object[]) rs.getArray("an").getArray()).length, 0);
assertEquals(((Object[]) rs.getArray("aan").getArray()).length, 0);
assertEquals(((Object[]) rs.getArray("aat").getArray()).length, 0);
assertEquals(rs.getInt("tail"), 200);

assertFalse(rs.next());
}
}

private static void assertArrayOfNestedEquals(Array array, Tuple[][] expected) throws SQLException {
Object[] elements = (Object[]) array.getArray();
assertEquals(elements.length, expected.length);
try (ResultSet ars = array.getResultSet()) {
for (int i = 0; i < expected.length; i++) {
assertTrue(ars.next());
assertEquals(((Object[]) elements[i]).length, expected[i].length);
assertNestedEquals((Array) ars.getObject(2), expected[i]);
}
assertFalse(ars.next());
}
}

@Test(groups = { "integration" })
public void testStringsUsedAsBytes() throws Exception {
runQuery("CREATE TABLE test_strings_as_bytes (order Int8, str String, fixed FixedString(10)) ENGINE = MergeTree ORDER BY ()");
Expand Down
Loading