Skip to content

Commit 8d13865

Browse files
fix: ensure nullable binary arrays return boxed type arrays instead of Object[] in BinaryStreamReader
1 parent 8ce79b1 commit 8d13865

3 files changed

Lines changed: 108 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Bug Fixes
66

7+
- **[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)
8+
79
- **[client-v2]** Fixed container query parameters being sent unquoted, so `Client.query(sql, params, settings)` binding
810
a `List<LocalDate>` (or an array/`Map`) to a placeholder like `{ids:Array(Date)}` was rejected by the server with
911
`CANNOT_PARSE_INPUT_ASSERTION_FAILED`. Parameter values are now formatted by

client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -625,8 +625,13 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException {
625625

626626
public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws IOException {
627627
ArrayValue array;
628-
if (itemTypeColumn.isNullable()
629-
|| itemTypeColumn.getDataType() == ClickHouseDataType.Variant
628+
if (itemTypeColumn.isNullable()) {
629+
Class<?> itemClass = resolveNullableArrayItemClass(itemTypeColumn.getDataType());
630+
array = new ArrayValue(itemClass, len);
631+
for (int i = 0; i < len; i++) {
632+
array.set(i, readValue(itemTypeColumn));
633+
}
634+
} else if (itemTypeColumn.getDataType() == ClickHouseDataType.Variant
630635
|| itemTypeColumn.getDataType() == ClickHouseDataType.Dynamic
631636
|| itemTypeColumn.getDataType() == ClickHouseDataType.Geometry) {
632637
array = new ArrayValue(Object.class, len);
@@ -667,6 +672,34 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws
667672
return array;
668673
}
669674

675+
/**
676+
* Resolves the Java class that {@link #readValue} actually returns for a given data type
677+
* so that it can be used as the component type of a nullable array.
678+
*
679+
* <p>For unsigned integer types, {@code readValue} widens the value (e.g. UInt8 → Short,
680+
* UInt32 → Long), so we use {@link ClickHouseDataType#getWiderObjectClass()} which mirrors
681+
* that widening. For Enum types, {@code readValue} returns {@link EnumValue} rather than the
682+
* declared {@code String.class}. All other types use {@link ClickHouseDataType#getObjectClass()}.
683+
*
684+
* @param dataType the element data type of the array
685+
* @return the Java class to use as the array component type; never {@code null}
686+
*/
687+
private static Class<?> resolveNullableArrayItemClass(ClickHouseDataType dataType) {
688+
switch (dataType) {
689+
case UInt8:
690+
case UInt16:
691+
case UInt32:
692+
case UInt64:
693+
return dataType.getWiderObjectClass();
694+
case Enum8:
695+
case Enum16:
696+
return EnumValue.class;
697+
default:
698+
Class<?> cls = dataType.getObjectClass();
699+
return cls == null ? Object.class : cls;
700+
}
701+
}
702+
670703
public void skipValue(ClickHouseColumn column) throws IOException {
671704
readValue(column, null);
672705
}

client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReaderTests.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.clickhouse.client.api.data_formats.internal;
22

33
import com.clickhouse.data.ClickHouseColumn;
4+
import com.clickhouse.data.format.BinaryStreamUtils;
45

56
import java.io.ByteArrayInputStream;
67
import java.io.ByteArrayOutputStream;
@@ -204,4 +205,74 @@ public void testReadNullVariantReturnsNull() throws Exception {
204205

205206
Assert.assertNull(reader.readValue(column));
206207
}
208+
209+
@Test
210+
public void testNullableArrayValueUsesBoxedComponentType() throws Exception {
211+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
212+
BinaryStreamUtils.writeVarInt(baos, 2);
213+
BinaryStreamUtils.writeNonNull(baos);
214+
BinaryStreamUtils.writeFloat64(baos, 1.0);
215+
BinaryStreamUtils.writeNonNull(baos);
216+
BinaryStreamUtils.writeFloat64(baos, 2.0);
217+
218+
BinaryStreamReader reader = new BinaryStreamReader(
219+
new ByteArrayInputStream(baos.toByteArray()),
220+
TimeZone.getTimeZone("UTC"),
221+
null,
222+
new BinaryStreamReader.CachingByteBufferAllocator(),
223+
false,
224+
null);
225+
226+
BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) reader.readValue(
227+
ClickHouseColumn.of("v", "Array(Nullable(Float64))"));
228+
229+
Assert.assertEquals(array.getArray().getClass().getComponentType(), Double.class);
230+
}
231+
232+
@Test
233+
public void testNullableUnsignedArrayUsesWidenedType() throws Exception {
234+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
235+
BinaryStreamUtils.writeVarInt(baos, 2);
236+
BinaryStreamUtils.writeNonNull(baos);
237+
BinaryStreamUtils.writeUnsignedInt8(baos, 10);
238+
BinaryStreamUtils.writeNonNull(baos);
239+
BinaryStreamUtils.writeUnsignedInt8(baos, 20);
240+
241+
BinaryStreamReader reader = new BinaryStreamReader(
242+
new ByteArrayInputStream(baos.toByteArray()),
243+
TimeZone.getTimeZone("UTC"),
244+
null,
245+
new BinaryStreamReader.CachingByteBufferAllocator(),
246+
false,
247+
null);
248+
249+
BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) reader.readValue(
250+
ClickHouseColumn.of("v", "Array(Nullable(UInt8))"));
251+
252+
Assert.assertEquals(array.getArray().getClass().getComponentType(), Short.class);
253+
}
254+
255+
@Test
256+
public void testNullableEnumArrayUsesEnumValueType() throws Exception {
257+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
258+
BinaryStreamUtils.writeVarInt(baos, 2);
259+
BinaryStreamUtils.writeNonNull(baos);
260+
baos.write(1); // enum ordinal for 'a'
261+
BinaryStreamUtils.writeNonNull(baos);
262+
baos.write(2); // enum ordinal for 'b'
263+
264+
BinaryStreamReader reader = new BinaryStreamReader(
265+
new ByteArrayInputStream(baos.toByteArray()),
266+
TimeZone.getTimeZone("UTC"),
267+
null,
268+
new BinaryStreamReader.CachingByteBufferAllocator(),
269+
false,
270+
null);
271+
272+
BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) reader.readValue(
273+
ClickHouseColumn.of("v", "Array(Nullable(Enum8('a'=1,'b'=2)))"));
274+
275+
Assert.assertEquals(array.getArray().getClass().getComponentType(),
276+
BinaryStreamReader.EnumValue.class);
277+
}
207278
}

0 commit comments

Comments
 (0)