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); + } }