diff --git a/docs/features.md b/docs/features.md index b8b2b24a8..a9c6e405a 100644 --- a/docs/features.md +++ b/docs/features.md @@ -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>)` 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. diff --git a/history/latest/3178.md b/history/latest/3178.md new file mode 100644 index 000000000..a3a6ef8fd --- /dev/null +++ b/history/latest/3178.md @@ -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) 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 97c323911..d49a69be6 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 @@ -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); } @@ -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; 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 f00057bf5..0b37a4bbd 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -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 ()");