From 8d138651d180346feae141a47935edd27bc95ce3 Mon Sep 17 00:00:00 2001 From: kishansinghifs1 Date: Fri, 10 Jul 2026 22:03:12 +0530 Subject: [PATCH 1/2] fix: ensure nullable binary arrays return boxed type arrays instead of Object[] in BinaryStreamReader --- CHANGELOG.md | 2 + .../internal/BinaryStreamReader.java | 37 +++++++++- .../internal/BinaryStreamReaderTests.java | 71 +++++++++++++++++++ 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef68d17e6..21bb6bfb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Bug Fixes +- **[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 container query parameters being sent unquoted, so `Client.query(sql, params, settings)` binding a `List` (or an array/`Map`) to a placeholder like `{ids:Array(Date)}` was rejected by the server with `CANNOT_PARSE_INPUT_ASSERTION_FAILED`. Parameter values are now formatted by diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java index dd70d7ea4..98b116dd0 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java @@ -625,8 +625,13 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException { public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws IOException { ArrayValue array; - if (itemTypeColumn.isNullable() - || itemTypeColumn.getDataType() == ClickHouseDataType.Variant + if (itemTypeColumn.isNullable()) { + Class itemClass = resolveNullableArrayItemClass(itemTypeColumn.getDataType()); + array = new ArrayValue(itemClass, len); + for (int i = 0; i < len; i++) { + array.set(i, readValue(itemTypeColumn)); + } + } else if (itemTypeColumn.getDataType() == ClickHouseDataType.Variant || itemTypeColumn.getDataType() == ClickHouseDataType.Dynamic || itemTypeColumn.getDataType() == ClickHouseDataType.Geometry) { array = new ArrayValue(Object.class, len); @@ -667,6 +672,34 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws return array; } + /** + * Resolves the Java class that {@link #readValue} actually returns for a given data type + * so that it can be used as the component type of a nullable array. + * + *

For unsigned integer types, {@code readValue} widens the value (e.g. UInt8 → Short, + * UInt32 → Long), so we use {@link ClickHouseDataType#getWiderObjectClass()} which mirrors + * that widening. For Enum types, {@code readValue} returns {@link EnumValue} rather than the + * declared {@code String.class}. All other types use {@link ClickHouseDataType#getObjectClass()}. + * + * @param dataType the element data type of the array + * @return the Java class to use as the array component type; never {@code null} + */ + private static Class resolveNullableArrayItemClass(ClickHouseDataType dataType) { + switch (dataType) { + case UInt8: + case UInt16: + case UInt32: + case UInt64: + return dataType.getWiderObjectClass(); + case Enum8: + case Enum16: + return EnumValue.class; + default: + Class cls = dataType.getObjectClass(); + return cls == null ? Object.class : cls; + } + } + public void skipValue(ClickHouseColumn column) throws IOException { readValue(column, null); } diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java index 0d94e0a5f..a3a930379 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java @@ -1,6 +1,7 @@ package com.clickhouse.client.api.data_formats.internal; import com.clickhouse.data.ClickHouseColumn; +import com.clickhouse.data.format.BinaryStreamUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -204,4 +205,74 @@ public void testReadNullVariantReturnsNull() throws Exception { Assert.assertNull(reader.readValue(column)); } + + @Test + public void testNullableArrayValueUsesBoxedComponentType() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BinaryStreamUtils.writeVarInt(baos, 2); + BinaryStreamUtils.writeNonNull(baos); + BinaryStreamUtils.writeFloat64(baos, 1.0); + BinaryStreamUtils.writeNonNull(baos); + BinaryStreamUtils.writeFloat64(baos, 2.0); + + BinaryStreamReader reader = new BinaryStreamReader( + new ByteArrayInputStream(baos.toByteArray()), + TimeZone.getTimeZone("UTC"), + null, + new BinaryStreamReader.CachingByteBufferAllocator(), + false, + null); + + BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) reader.readValue( + ClickHouseColumn.of("v", "Array(Nullable(Float64))")); + + Assert.assertEquals(array.getArray().getClass().getComponentType(), Double.class); + } + + @Test + public void testNullableUnsignedArrayUsesWidenedType() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BinaryStreamUtils.writeVarInt(baos, 2); + BinaryStreamUtils.writeNonNull(baos); + BinaryStreamUtils.writeUnsignedInt8(baos, 10); + BinaryStreamUtils.writeNonNull(baos); + BinaryStreamUtils.writeUnsignedInt8(baos, 20); + + BinaryStreamReader reader = new BinaryStreamReader( + new ByteArrayInputStream(baos.toByteArray()), + TimeZone.getTimeZone("UTC"), + null, + new BinaryStreamReader.CachingByteBufferAllocator(), + false, + null); + + BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) reader.readValue( + ClickHouseColumn.of("v", "Array(Nullable(UInt8))")); + + Assert.assertEquals(array.getArray().getClass().getComponentType(), Short.class); + } + + @Test + public void testNullableEnumArrayUsesEnumValueType() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BinaryStreamUtils.writeVarInt(baos, 2); + BinaryStreamUtils.writeNonNull(baos); + baos.write(1); // enum ordinal for 'a' + BinaryStreamUtils.writeNonNull(baos); + baos.write(2); // enum ordinal for 'b' + + BinaryStreamReader reader = new BinaryStreamReader( + new ByteArrayInputStream(baos.toByteArray()), + TimeZone.getTimeZone("UTC"), + null, + new BinaryStreamReader.CachingByteBufferAllocator(), + false, + null); + + BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) reader.readValue( + ClickHouseColumn.of("v", "Array(Nullable(Enum8('a'=1,'b'=2)))")); + + Assert.assertEquals(array.getArray().getClass().getComponentType(), + BinaryStreamReader.EnumValue.class); + } } From f3e22cf1613da2101440fcf143d8450a93de1506 Mon Sep 17 00:00:00 2001 From: kishansinghifs1 Date: Mon, 13 Jul 2026 09:42:12 +0530 Subject: [PATCH 2/2] refactor: unify and improve array component type resolution for primitive and nullable types in BinaryStreamReader --- .../internal/BinaryStreamReader.java | 74 +++++++++++++------ .../internal/BinaryStreamReaderTests.java | 30 ++++++++ 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java index 98b116dd0..a113d12bf 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java @@ -609,8 +609,8 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException { ArrayValue array; ClickHouseColumn itemTypeColumn = column.getNestedColumns().get(0); if (len == 0) { - Class itemClass = itemTypeColumn.getDataType().getPrimitiveClass(); - array = new ArrayValue(itemClass == null ? Object.class : itemClass, 0); + Class itemClass = resolveArrayItemClass(itemTypeColumn); + array = new ArrayValue(itemClass, 0); } else if (column.getArrayNestedLevel() == 1) { array = readArrayItem(itemTypeColumn, len); } else { @@ -626,7 +626,7 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException { public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws IOException { ArrayValue array; if (itemTypeColumn.isNullable()) { - Class itemClass = resolveNullableArrayItemClass(itemTypeColumn.getDataType()); + Class itemClass = resolveArrayItemClass(itemTypeColumn); array = new ArrayValue(itemClass, len); for (int i = 0; i < len; i++) { array.set(i, readValue(itemTypeColumn)); @@ -673,30 +673,60 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws } /** - * Resolves the Java class that {@link #readValue} actually returns for a given data type - * so that it can be used as the component type of a nullable array. + * Resolves the Java class that {@link #readValue} actually returns for a given column + * so that it can be used as the component type of an array. * *

For unsigned integer types, {@code readValue} widens the value (e.g. UInt8 → Short, - * UInt32 → Long), so we use {@link ClickHouseDataType#getWiderObjectClass()} which mirrors - * that widening. For Enum types, {@code readValue} returns {@link EnumValue} rather than the - * declared {@code String.class}. All other types use {@link ClickHouseDataType#getObjectClass()}. + * UInt32 → Long), so we use {@link ClickHouseDataType#getWiderObjectClass()} or + * {@link ClickHouseDataType#getWiderPrimitiveClass()} which mirrors that widening. + * For Enum types, {@code readValue} returns {@link EnumValue} rather than the + * declared {@code String.class}. All other types use {@link ClickHouseDataType#getObjectClass()} + * or {@link ClickHouseDataType#getPrimitiveClass()}. * - * @param dataType the element data type of the array + * @param itemTypeColumn the element column of the array * @return the Java class to use as the array component type; never {@code null} */ - private static Class resolveNullableArrayItemClass(ClickHouseDataType dataType) { - switch (dataType) { - case UInt8: - case UInt16: - case UInt32: - case UInt64: - return dataType.getWiderObjectClass(); - case Enum8: - case Enum16: - return EnumValue.class; - default: - Class cls = dataType.getObjectClass(); - return cls == null ? Object.class : cls; + private static Class resolveArrayItemClass(ClickHouseColumn itemTypeColumn) { + ClickHouseDataType dataType = itemTypeColumn.getDataType(); + if (itemTypeColumn.isNullable()) { + switch (dataType) { + case UInt8: + case UInt16: + case UInt32: + case UInt64: + return dataType.getWiderObjectClass(); + case Enum8: + case Enum16: + return EnumValue.class; + default: + Class cls = dataType.getObjectClass(); + return cls == null ? Object.class : cls; + } + } else { + switch (dataType) { + case UInt8: + case UInt16: + case UInt32: + case UInt64: + return dataType.getWiderPrimitiveClass(); + case Enum8: + case Enum16: + return EnumValue.class; + case Variant: + case Dynamic: + case Geometry: + return Object.class; + case Array: + return ArrayValue.class; + case Tuple: + case Nested: + return Object[].class; + case Map: + return Map.class; + default: + Class cls = dataType.getPrimitiveClass(); + return cls == null ? Object.class : cls; + } } } diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java index a3a930379..4b089a8ea 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java @@ -275,4 +275,34 @@ public void testNullableEnumArrayUsesEnumValueType() throws Exception { Assert.assertEquals(array.getArray().getClass().getComponentType(), BinaryStreamReader.EnumValue.class); } + + @Test + public void testEmptyArrayTypes() throws Exception { + assertEmptyArrayComponentType("Array(UInt8)", short.class); + assertEmptyArrayComponentType("Array(Nullable(UInt8))", Short.class); + assertEmptyArrayComponentType("Array(String)", String.class); + assertEmptyArrayComponentType("Array(Nullable(String))", String.class); + assertEmptyArrayComponentType("Array(Enum8('a'=1))", BinaryStreamReader.EnumValue.class); + assertEmptyArrayComponentType("Array(Nullable(Enum8('a'=1)))", BinaryStreamReader.EnumValue.class); + assertEmptyArrayComponentType("Array(Variant(Int32, String))", Object.class); + assertEmptyArrayComponentType("Array(Array(String))", BinaryStreamReader.ArrayValue.class); + } + + private void assertEmptyArrayComponentType(String columnType, Class expectedComponentType) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BinaryStreamUtils.writeVarInt(baos, 0); + + BinaryStreamReader reader = new BinaryStreamReader( + new ByteArrayInputStream(baos.toByteArray()), + TimeZone.getTimeZone("UTC"), + null, + new BinaryStreamReader.CachingByteBufferAllocator(), + false, + null); + + BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) reader.readValue( + ClickHouseColumn.of("v", columnType)); + + Assert.assertEquals(array.getArray().getClass().getComponentType(), expectedComponentType, "Failed for " + columnType); + } }