From 3e72e308b21f86eefda4933da3d9a831fc69f6e5 Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:49:17 +0000 Subject: [PATCH 1/5] Fix client-v2: serialize Nested columns in RowBinary writer RowBinaryFormatWriter threw `UnsupportedOperationException: Unsupported data type: Nested` when inserting into a table whose schema keeps an un-flattened `Nested(...)` column (created with `flatten_nested = 0`), because SerializerUtils.serializeData had no case for the Nested type and fell through to serializePrimitiveData's default throw. A `Nested(f1 T1, ..., fN TN)` column has the same RowBinary layout as `Array(Tuple(T1, ..., TN))` - a var-uint element count followed by that many tuples - which is exactly how BinaryStreamReader.readNested reads it. serializeData now serializes it symmetrically. Fixes: https://github.com/ClickHouse/clickhouse-java/issues/2477 --- CHANGELOG.md | 2 ++ .../internal/SerializerUtils.java | 22 +++++++++++++ .../internal/SerializerUtilsTest.java | 31 +++++++++++++++++++ .../datatypes/RowBinaryFormatWriterTest.java | 21 +++++++++++++ 4 files changed, 76 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd7bba1f..753d2d196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Bug Fixes +- **[client-v2]** Fixed `RowBinaryFormatWriter` throwing `UnsupportedOperationException: Unsupported data type: Nested` when inserting into a table with an un-flattened `Nested(...)` column (created with `flatten_nested = 0`). The `RowBinary` writer now serializes a `Nested(f1 T1, ..., fN TN)` column the same way it is read — identically to `Array(Tuple(T1, ..., TN))`. (https://github.com/ClickHouse/clickhouse-java/issues/2477) + - **[client-v2]** Fixed binary array decoding for nullable element types so `Array(Nullable(Float64))` and similar columns now return boxed arrays such as `Double[]` instead of `Object[]`. This keeps null-supporting arrays aligned with their element type while preserving the existing `Object[]` fallback for Variant/Dynamic/Geometry arrays. (https://github.com/ClickHouse/clickhouse-java/issues/2846) - **[client-v2]** Fixed `Float32`/`Float64` columns throwing `ClassCastException` when a value of a diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java index 6ff4b4b79..d3d7a43bb 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java @@ -72,6 +72,9 @@ public static void serializeData(OutputStream stream, Object value, ClickHouseCo case Map: serializeMapData(stream, value, column); break; + case Nested: + serializeNestedTypeData(stream, value, column); + break; case AggregateFunction: serializeAggregateFunction(stream, value, column); break; @@ -130,6 +133,25 @@ private static void serializeNestedData(OutputStream stream, Object value, Click serializeData(stream, value, column); } + /** + * Serializes a {@code Nested} column. In {@code RowBinary} a {@code Nested(f1 T1, ..., fN TN)} + * column has the same layout as {@code Array(Tuple(T1, ..., TN))}: a var-uint element count + * followed by that many tuples, each carrying the N field values in declaration order. The + * value is therefore a list (or array) of tuples, matching what + * {@link BinaryStreamReader#readNested(ClickHouseColumn)} produces on read. + */ + private static void serializeNestedTypeData(OutputStream stream, Object value, ClickHouseColumn column) throws IOException { + if (value == null) { + writeVarInt(stream, 0); + return; + } + List tuples = convertArrayValueToList(value); + writeVarInt(stream, tuples.size()); + for (Object tuple : tuples) { + serializeTupleData(stream, tuple, column); + } + } + private static final Map, ClickHouseColumn> PREDEFINED_TYPE_COLUMNS = getPredefinedTypeColumnsMap(); private static Map, ClickHouseColumn> getPredefinedTypeColumnsMap() { diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java index 265cd1cfb..51172d212 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java @@ -217,6 +217,37 @@ private Object[][] nestedNullableData() throws Exception { }; } + @Test(dataProvider = "rowBinaryTypeData") + public void testRowBinaryTypeRoundTrip(String typeName, Object value) throws Exception { + ClickHouseColumn column = ClickHouseColumn.of("v", typeName); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + SerializerUtils.serializeData(out, value, column); + + Object actual = newReader(out.toByteArray()).readValue(column); + Assert.assertEquals(normalize(actual), normalize(value)); + } + + @DataProvider(name = "rowBinaryTypeData") + private Object[][] rowBinaryTypeData() { + return new Object[][] { + // A Nested(...) column has the same RowBinary layout as Array(Tuple(...)): a + // var-uint row count followed by that many tuples. Two rows detect a wrong count + // or a dropped field byte, which would shift every following tuple. + {"Nested(a Int32, b String)", + Arrays.asList(Arrays.asList(1, "x"), Arrays.asList(2, "y"))}, + + // A Nullable field in the MIDDLE of the nested tuple, with a trailing fixed-width + // Float64: a dropped null-marker byte misaligns the Float64 and is caught. The + // second row exercises the null branch of that field. + {"Nested(a Int32, b Nullable(String), c Float64)", + Arrays.asList(Arrays.asList(7, "opt", 9.5d), Arrays.asList(7, null, 8.5d))}, + + // An empty Nested serializes as a zero-length array. + {"Nested(a Int32, b String)", Arrays.asList()}, + }; + } + // Normalizes Tuple (Object[]) and Array (ArrayValue / List) results to nested Lists so // round-tripped values compare structurally regardless of the container representation the // reader returns. diff --git a/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java b/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java index 1a0ee2287..bf7ff3067 100644 --- a/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java @@ -534,6 +534,27 @@ public void writeNestedTests() throws Exception { writeTest(tableName, tableCreate, rows); } + @Test (groups = { "integration" }) + public void writeNestedTypeTests() throws Exception { + String tableName = "rowBinaryFormatWriterTest_writeNestedTypeTests_" + UUID.randomUUID().toString().replace('-', '_'); + // flatten_nested = 0 keeps the column typed as Nested(...) in the schema instead of + // expanding it into parallel Array(...) sub-columns; the un-flattened Nested type is the + // one that has to be serialized as Array(Tuple(...)). + String tableCreate = "CREATE TABLE \"" + tableName + "\" " + + " (id Int32, " + + " n Nested(a UInt32, b Nullable(String)) " + + " ) Engine = MergeTree ORDER BY id SETTINGS flatten_nested = 0"; + + List nested = Arrays.asList(Arrays.asList(10L, "x"), Arrays.asList(20L, null)); + Field[][] rows = new Field[][] {{ + new Field("id", 1), //Row ID + new Field("n", nested).set(nested) //Nested + } + }; + + writeTest(tableName, tableCreate, rows); + } + @Test (groups = { "integration" }) public void writeNullableTests() throws Exception { String tableName = "rowBinaryFormatWriterTest_writeNullableTests_" + UUID.randomUUID().toString().replace('-', '_'); From e0f467482567482e6e5f867598915ae40f5eeec5 Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:31:08 +0000 Subject: [PATCH 2/5] test(client-v2): cover Object[][] (array-shaped) Nested values in round-trip Adds an Object[][] "matrix array" case to the Nested RowBinary round-trip DataProvider, exercising convertArrayValueToList's array branch and serializeTupleData's array branch (the existing cases used List shapes only). Documents that array-shaped Nested values round-trip correctly. --- .../api/data_formats/internal/SerializerUtilsTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java index 51172d212..46f269b58 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java @@ -237,6 +237,13 @@ private Object[][] rowBinaryTypeData() { {"Nested(a Int32, b String)", Arrays.asList(Arrays.asList(1, "x"), Arrays.asList(2, "y"))}, + // Same value as arrays instead of Lists: an Object[][] of Object[] rows (a + // "matrix" array). convertArrayValueToList takes the array branch and each row is + // serialized by serializeTupleData's array branch, producing the same bytes as the + // List-shaped case above. + {"Nested(a Int32, b String)", + new Object[][] {{1, "x"}, {2, "y"}}}, + // A Nullable field in the MIDDLE of the nested tuple, with a trailing fixed-width // Float64: a dropped null-marker byte misaligns the Float64 and is caught. The // second row exercises the null branch of that field. From 5112ee38c500647a579ad37beea5c47cfaef2a3b Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:02:09 +0000 Subject: [PATCH 3/5] Support Nested columns in jdbc-v2 java.sql.Array getResultSet A `java.sql.Array` wrapping an un-flattened `Nested(f1 T1, ..., fN TN)` column (tables created with `flatten_nested = 0`) threw from `getResultSet()`: `ArrayResultSet` derived the VALUE column as the first nested field instead of the element `Tuple(...)`, so reading each element tried to coerce the tuple `Object[]` to that field's scalar type. Resolve the element as the tuple for Nested columns (shared `elementColumn` helper, also used by the getArray element path). Insert (createArrayOf/setArray/ setObject) and read (getArray/getObject/getResultSet) of Nested columns are covered by a new jdbc-v2 integration test. Fixes: https://github.com/ClickHouse/clickhouse-java/issues/2477 --- CHANGELOG.md | 2 + docs/features.md | 1 + .../clickhouse/jdbc/types/ArrayResultSet.java | 30 +++++-- .../clickhouse/jdbc/JdbcDataTypeTests.java | 86 +++++++++++++++++++ 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 753d2d196..d303d4ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - **[client-v2]** Fixed `RowBinaryFormatWriter` throwing `UnsupportedOperationException: Unsupported data type: Nested` when inserting into a table with an un-flattened `Nested(...)` column (created with `flatten_nested = 0`). The `RowBinary` writer now serializes a `Nested(f1 T1, ..., fN TN)` column the same way it is read — identically to `Array(Tuple(T1, ..., TN))`. (https://github.com/ClickHouse/clickhouse-java/issues/2477) +- **[jdbc-v2]** Fixed `java.sql.Array#getResultSet()` on an un-flattened `Nested(...)` column throwing `Value of class [Ljava.lang.Object; cannot be converted to class java.lang.Byte`. The array `ResultSet` now exposes each nested row as its `Tuple(f1 T1, ..., fN TN)` element type instead of the first nested field, so `getObject` returns the whole tuple. Insert (via `Connection#createArrayOf` + `setArray` or `setObject`) and read (via `getArray`/`getObject`) of `Nested(...)` columns are covered by tests. (https://github.com/ClickHouse/clickhouse-java/issues/2477) + - **[client-v2]** Fixed binary array decoding for nullable element types so `Array(Nullable(Float64))` and similar columns now return boxed arrays such as `Double[]` instead of `Object[]`. This keeps null-supporting arrays aligned with their element type while preserving the existing `Object[]` fallback for Variant/Dynamic/Geometry arrays. (https://github.com/ClickHouse/clickhouse-java/issues/2846) - **[client-v2]** Fixed `Float32`/`Float64` columns throwing `ClassCastException` when a value of a diff --git a/docs/features.md b/docs/features.md index 4237a80cd..1a753e9ee 100644 --- a/docs/features.md +++ b/docs/features.md @@ -75,6 +75,7 @@ Compatibility-sensitive traits: - Type mapping and conversions: Maps ClickHouse types to JDBC types and Java classes, including date/time handling and `java.time` support. - 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`, `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. - 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. - Client info propagation: Supports JDBC client info such as `ApplicationName` and forwards it to the underlying client name. - Wrapper support: Implements standard JDBC `Wrapper` and `unwrap` behavior on major JDBC objects. diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index 9965978dd..97da74a06 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -64,8 +64,7 @@ public ArrayResultSet(Object array, ClickHouseColumn column) { this.column = column; this.columnCount = 2; // INDEX, VALUE - List nestedColumns = column.getNestedColumns(); - ClickHouseColumn valueColumn = column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : nestedColumns.get(0); + ClickHouseColumn valueColumn = elementColumn(column); this.metadata = new ResultSetMetaDataImpl(Arrays.asList(INDEX_COLUMN, ClickHouseColumn.parse(VALUE_COLUMN + " " + valueColumn.getOriginalTypeName()).get(0)) , "", "", "", JdbcUtils.DATA_TYPE_CLASS_MAP, java.util.Collections.emptyMap()); @@ -74,6 +73,29 @@ public ArrayResultSet(Object array, ClickHouseColumn column) { indexConverterMap = defaultValueConverters.getConvertersForType(Integer.class); } + /** + * Resolves the type of a single element of {@code column} (an array-like column). A + * {@code Nested(f1 T1, ..., fN TN)} column is read as an array whose elements are + * {@code Tuple(f1 T1, ..., fN TN)}, so its element is that tuple rather than the first nested + * field; every other array-like column keeps its existing base/nested-column resolution. + */ + private static ClickHouseColumn elementColumn(ClickHouseColumn column) { + List nestedColumns = column.getNestedColumns(); + if (column.isNested()) { + StringBuilder tupleType = new StringBuilder(ClickHouseDataType.Tuple.name()).append('('); + for (int i = 0; i < nestedColumns.size(); i++) { + ClickHouseColumn field = nestedColumns.get(i); + if (i > 0) { + tupleType.append(", "); + } + tupleType.append(field.getColumnName()).append(' ').append(field.getOriginalTypeName()); + } + tupleType.append(')'); + return ClickHouseColumn.of(VALUE_COLUMN, tupleType.toString()); + } + return column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : nestedColumns.get(0); + } + @Override public boolean next() throws SQLException { if (pos + 1 >= length || length == 0) { @@ -139,9 +161,7 @@ private Object getValueAsObject(int columnIndex, Class type, Object defaultVa } if (type == Array.class) { - ClickHouseColumn nestedColumn = - column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : column.getNestedColumns().get(0); - return new com.clickhouse.jdbc.types.Array(nestedColumn, JdbcUtils.arrayToObjectArray(value)); + return new com.clickhouse.jdbc.types.Array(elementColumn(column), JdbcUtils.arrayToObjectArray(value)); } else { Map, Function> valueConverterMap = initValueConverterMapIfNeeded(value); return convertValue(value, type, valueConverterMap); 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 ed0b00cb9..a97f9bcba 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -1516,6 +1516,92 @@ public void testArrayTypes() throws SQLException { } } + @Test(groups = { "integration" }) + public void testNestedType() throws SQLException { + runQuery("DROP TABLE IF EXISTS test_nested_jdbc"); + runQuery("CREATE TABLE test_nested_jdbc (order Int8, " + + "n Nested(a Int8, b Nullable(String)), " + + "tail Int32" + + ") ENGINE = MergeTree ORDER BY (order) SETTINGS flatten_nested = 0"); + + // A null in the Nullable field exercises null propagation through every read path. + Tuple[] nested = new Tuple[] { + new Tuple((byte) 1, "x"), + new Tuple((byte) 2, null), + }; + Tuple[] empty = new Tuple[0]; + + // Row 1: insert the Nested column through java.sql.Array (Connection#createArrayOf + setArray). + try (Connection conn = getJdbcConnection(); + PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_nested_jdbc VALUES (1, ?, 100)")) { + stmt.setArray(1, conn.createArrayOf("Tuple(Int8, String)", nested)); + stmt.executeUpdate(); + } + + // Row 2: insert the same Nested value as a plain Java array of tuples (setObject). + try (Connection conn = getJdbcConnection(); + PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_nested_jdbc VALUES (2, ?, 200)")) { + stmt.setObject(1, nested); + stmt.executeUpdate(); + } + + // Row 3: an empty Nested value, so the read paths cover a zero-row array. + try (Connection conn = getJdbcConnection(); + PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_nested_jdbc VALUES (3, ?, 300)")) { + stmt.setObject(1, empty); + stmt.executeUpdate(); + } + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT order, n, tail FROM test_nested_jdbc ORDER BY order")) { + // Row 1 read through getArray + assertTrue(rs.next()); + assertEquals(rs.getByte("order"), (byte) 1); + assertNestedEquals(rs.getArray("n"), nested); + assertEquals(rs.getInt("tail"), 100); + + // Row 2 read through getObject + assertTrue(rs.next()); + assertEquals(rs.getByte("order"), (byte) 2); + assertNestedEquals((Array) rs.getObject("n"), nested); + assertEquals(rs.getInt("tail"), 200); + + // Row 3: empty Nested -> empty array and an empty getResultSet(). + assertTrue(rs.next()); + assertEquals(rs.getByte("order"), (byte) 3); + assertNestedEquals(rs.getArray("n"), empty); + assertEquals(rs.getInt("tail"), 300); + + assertFalse(rs.next()); + } + } + + private static void assertNestedEquals(Array nestedColumn, Tuple[] expected) throws SQLException { + // getArray() yields one element per nested row, each an Object[] of the tuple field values. + Object[] rows = (Object[]) nestedColumn.getArray(); + assertEquals(rows.length, expected.length); + for (int i = 0; i < expected.length; i++) { + assertTupleEquals((Object[]) rows[i], expected[i]); + } + + // getResultSet() exposes the same rows as (INDEX, VALUE) pairs. + try (ResultSet ars = nestedColumn.getResultSet()) { + int seen = 0; + while (ars.next()) { + assertEquals(ars.getInt(1), seen + 1); + assertTupleEquals((Object[]) ars.getObject(2), expected[seen]); + seen++; + } + assertEquals(seen, expected.length); + } + } + + private static void assertTupleEquals(Object[] actual, Tuple expected) { + assertEquals(String.valueOf(actual[0]), String.valueOf(expected.getValue(0))); + assertEquals(actual[1], expected.getValue(1)); // Nullable(String): null stays null + } + @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 ()"); From 9c0fed6d09faeb8290d3ea4fada4e633942124e7 Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Sun, 26 Jul 2026 12:50:13 +0000 Subject: [PATCH 4/5] test(jdbc-v2): declare Nullable field in Nested createArrayOf element type testNestedType inserts into Nested(a Int8, b Nullable(String)); align the createArrayOf element type to Tuple(Int8, Nullable(String)) so the declared element type honestly matches the column. Rendering is unchanged (the array's base data type is Tuple either way, and a null field renders as NULL value-driven), so this only makes the declared type honest and removes brittleness if createArrayOf ever validates element types more strictly. --- .../src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a97f9bcba..bcab0f373 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -1534,7 +1534,7 @@ public void testNestedType() throws SQLException { // Row 1: insert the Nested column through java.sql.Array (Connection#createArrayOf + setArray). try (Connection conn = getJdbcConnection(); PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_nested_jdbc VALUES (1, ?, 100)")) { - stmt.setArray(1, conn.createArrayOf("Tuple(Int8, String)", nested)); + stmt.setArray(1, conn.createArrayOf("Tuple(Int8, Nullable(String))", nested)); stmt.executeUpdate(); } From 2576802bb6c7225741526d1a806a0a1a707e701b Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:19:09 +0000 Subject: [PATCH 5/5] Address review: frame Nested as new-type support + more integration tests - CHANGELOG: reclassify the Nested change as a New Features entry (new type support), consolidating the client-v2 write and jdbc-v2 exposure. - docs/features.md: add a client-v2 entry describing how un-flattened Nested writes are exposed (Client#insert / RowBinaryFormatWriter#setValue). - client-v2 tests: add two integration tests covering un-flattened Nested with array / nested-array / tuple / nullable fields and with varied scalar type families, each with a fixed-width trailing field. Refs: https://github.com/ClickHouse/clickhouse-java/issues/2477 --- CHANGELOG.md | 13 +++-- .../datatypes/RowBinaryFormatWriterTest.java | 55 +++++++++++++++++++ docs/features.md | 1 + 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f0e4230f..4530b79df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,13 +16,18 @@ and the comma-separated `ssl_cipher_suites` connection property (client-v2 and jdbc-v2) restrict the cipher suites enabled on secure connections; when unset, the transport defaults are used. Cipher-suite selection is independent of the trust configuration and `ssl_mode`. (https://github.com/ClickHouse/clickhouse-java/issues/2882) +- **[client-v2, jdbc-v2]** Added support for un-flattened `Nested(...)` columns (tables created with + `flatten_nested = 0`). Previously the `RowBinary` writer threw `UnsupportedOperationException: Unsupported + data type: Nested` when inserting such a column. `client-v2` now serializes a `Nested(f1 T1, ..., fN TN)` + column the same way it is read — identically to `Array(Tuple(T1, ..., TN))` (a var-uint row count followed + by one tuple per nested row) — so it can be written through the insert path / `RowBinaryFormatWriter`. In + `jdbc-v2` an un-flattened `Nested(...)` column is exposed as a JDBC `ARRAY` whose element type is + `Tuple(f1 T1, ..., fN TN)`: it can be inserted through `Connection#createArrayOf`/`setArray` or `setObject` + and read back through `getArray`/`getObject`, and `java.sql.Array#getResultSet()` iterates the nested rows + as `(INDEX, VALUE)` pairs where each `VALUE` is the tuple. (https://github.com/ClickHouse/clickhouse-java/issues/2477) ### Bug Fixes -- **[client-v2]** Fixed `RowBinaryFormatWriter` throwing `UnsupportedOperationException: Unsupported data type: Nested` when inserting into a table with an un-flattened `Nested(...)` column (created with `flatten_nested = 0`). The `RowBinary` writer now serializes a `Nested(f1 T1, ..., fN TN)` column the same way it is read — identically to `Array(Tuple(T1, ..., TN))`. (https://github.com/ClickHouse/clickhouse-java/issues/2477) - -- **[jdbc-v2]** Fixed `java.sql.Array#getResultSet()` on an un-flattened `Nested(...)` column throwing `Value of class [Ljava.lang.Object; cannot be converted to class java.lang.Byte`. The array `ResultSet` now exposes each nested row as its `Tuple(f1 T1, ..., fN TN)` element type instead of the first nested field, so `getObject` returns the whole tuple. Insert (via `Connection#createArrayOf` + `setArray` or `setObject`) and read (via `getArray`/`getObject`) of `Nested(...)` columns are covered by tests. (https://github.com/ClickHouse/clickhouse-java/issues/2477) - - **[client-v2]** Fixed scalar `String` query parameters containing a tab (`0x09`), newline (`0x0a`) or backslash being mishandled through the server's `param_` interface. A `{name:String}` parameter value is parsed by the server with `deserializeTextEscaped`, which treated a raw tab or diff --git a/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java b/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java index 3f060052a..2c5116d68 100644 --- a/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/datatypes/RowBinaryFormatWriterTest.java @@ -783,6 +783,61 @@ public void writeNestedTypeTests() throws Exception { writeTest(tableName, tableCreate, rows); } + @Test (groups = { "integration" }) + public void writeNestedWithArrayAndComplexFieldsTests() throws Exception { + String tableName = "rowBinaryFormatWriterTest_writeNestedComplex_" + UUID.randomUUID().toString().replace('-', '_'); + // Un-flattened Nested whose fields cover arrays, nested arrays, an inner tuple and a + // nullable, with a fixed-width Float64 last so a dropped/extra byte shifts the trailing + // field and is detected. Serialized as Array(Tuple(...)) per nested row. + String tableCreate = "CREATE TABLE \"" + tableName + "\" " + + " (id Int32, " + + " n Nested(a UInt32, tags Array(String), matrix Array(Array(Int32)), " + + " pair Tuple(Int8, String), score Nullable(Float64), tail Float64) " + + " ) Engine = MergeTree ORDER BY id SETTINGS flatten_nested = 0"; + + List nestedMulti = Arrays.asList( + Arrays.asList(10L, Arrays.asList("x", "y"), Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3)), + Arrays.asList(1, "p"), 1.5, 9.5), + Arrays.asList(20L, Collections.emptyList(), Arrays.asList(Arrays.asList(4, 5)), + Arrays.asList(2, "q"), null, 8.5)); + List nestedEmpty = Collections.emptyList(); + List nestedSingle = Arrays.asList( + Arrays.asList(30L, Arrays.asList("z"), Arrays.asList(Arrays.asList(7, 8, 9)), + Arrays.asList(3, "r"), 2.5, 7.5)); + + Field[][] rows = new Field[][] { + {new Field("id", 1), new Field("n", nestedMulti)}, + {new Field("id", 2), new Field("n", nestedEmpty)}, + {new Field("id", 3), new Field("n", nestedSingle)}, + }; + + writeTest(tableName, tableCreate, rows); + } + + @Test (groups = { "integration" }) + public void writeNestedWithVariedScalarTypesTests() throws Exception { + String tableName = "rowBinaryFormatWriterTest_writeNestedScalars_" + UUID.randomUUID().toString().replace('-', '_'); + // Un-flattened Nested spanning several scalar type families (unsigned, temporal, UUID, + // low-cardinality, boolean) with a fixed-width Float64 last. + String tableCreate = "CREATE TABLE \"" + tableName + "\" " + + " (id Int32, " + + " n Nested(u UInt64, when Date, guid UUID, lc LowCardinality(String), flag Bool, tail Float64) " + + " ) Engine = MergeTree ORDER BY id SETTINGS flatten_nested = 0"; + + UUID g1 = UUID.fromString("00112233-4455-6677-8899-aabbccddeeff"); + UUID g2 = UUID.fromString("ffeeddcc-bbaa-9988-7766-554433221100"); + List nested = Arrays.asList( + Arrays.asList(123456789L, LocalDate.of(2023, 1, 2), g1, "cat", true, 1.25), + Arrays.asList(0L, LocalDate.of(1970, 1, 1), g2, "dog", false, 2.5)); + + Field[][] rows = new Field[][] {{ + new Field("id", 1), //Row ID + new Field("n", nested) //Nested + }}; + + writeTest(tableName, tableCreate, rows); + } + @Test (groups = { "integration" }) public void writeNullableTests() throws Exception { String tableName = "rowBinaryFormatWriterTest_writeNullableTests_" + UUID.randomUUID().toString().replace('-', '_'); diff --git a/docs/features.md b/docs/features.md index f4aac09b2..922826545 100644 --- a/docs/features.md +++ b/docs/features.md @@ -23,6 +23,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t - Data type conversion: Maps ClickHouse types to Java values for binary reads, POJO binding, and SQL parameter formatting, including date/time handling. - BFloat16 type support: For ClickHouse `24.11+`, reads and writes the `BFloat16` type through generic records, binary readers, POJO binding, and `Nullable`/`Dynamic`/`Variant` wrappers. `BFloat16` maps to the Java `float` type: a read widens the stored 16-bit value losslessly, while a write keeps the high 16 bits of the `float`. In `jdbc-v2`, `BFloat16` maps to `java.sql.Types.FLOAT` / `java.lang.Float` and is read and written through the standard `getFloat`/`setFloat` and `getObject` accessors. - Geometry type support: For ClickHouse `25.11+`, where `Geometry` changed from a string alias to `Variant(Point, Ring, LineString, MultiLineString, Polygon, MultiPolygon)`, the client reads and writes `Geometry` values through generic records, binary readers, POJO binding, and SQL parameter formatting, using Java array dimensionality to represent the geometry shape. +- Nested type support: Un-flattened `Nested(f1 T1, ..., fN TN)` columns (tables created with `flatten_nested = 0`) can be written through the insert path (`Client#insert`) using `RowBinaryFormatWriter#setValue`, and are read back through the binary readers and generic records. The column is serialized the same way it is read — identically to `Array(Tuple(T1, ..., TN))`, a var-uint row count followed by one tuple per nested row — so the value supplied for the column is a `List` (or array) of tuples, one tuple per nested row, each carrying the N field values in declaration order. - Insert APIs: Supports inserting registered POJOs, raw streams, and callback-driven writers, with optional column lists and format selection. - Insert controls: Supports insert-specific settings such as deduplication token, query id, compression behavior, and request headers. - Command execution: Executes DDL or other non-result commands and exposes response summaries and operation metrics.