Skip to content

Commit 39fed41

Browse files
authored
Merge pull request #2920 from kishansinghifs1/fix-nullable-binary-arrays
fix: ensure nullable binary arrays return boxed type arrays instead of Object[]
2 parents b8a8824 + 14e450c commit 39fed41

3 files changed

Lines changed: 159 additions & 15 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 `Float32`/`Float64` columns throwing `ClassCastException` when a value of a
810
non-matching boxed numeric type was supplied through the `Object`-typed insert surface — for example a
911
`Double` (the natural type of a Java literal like `1.5`) for a `Float32` column, or a `Float` for a

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

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,8 +609,8 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException {
609609
ArrayValue array;
610610
ClickHouseColumn itemTypeColumn = column.getNestedColumns().get(0);
611611
if (len == 0) {
612-
Class<?> itemClass = itemTypeColumn.getDataType().getPrimitiveClass();
613-
array = new ArrayValue(itemClass == null ? Object.class : itemClass, 0);
612+
Class<?> itemClass = resolveArrayItemClass(itemTypeColumn);
613+
array = new ArrayValue(itemClass, 0);
614614
} else if (column.getArrayNestedLevel() == 1) {
615615
array = readArrayItem(itemTypeColumn, len);
616616
} else {
@@ -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 = resolveArrayItemClass(itemTypeColumn);
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,64 @@ 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 column
677+
* so that it can be used as the component type of an 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()} or
681+
* {@link ClickHouseDataType#getWiderPrimitiveClass()} which mirrors that widening.
682+
* For Enum types, {@code readValue} returns {@link EnumValue} rather than the
683+
* declared {@code String.class}. All other types use {@link ClickHouseDataType#getObjectClass()}
684+
* or {@link ClickHouseDataType#getPrimitiveClass()}.
685+
*
686+
* @param itemTypeColumn the element column of the array
687+
* @return the Java class to use as the array component type; never {@code null}
688+
*/
689+
private static Class<?> resolveArrayItemClass(ClickHouseColumn itemTypeColumn) {
690+
ClickHouseDataType dataType = itemTypeColumn.getDataType();
691+
if (itemTypeColumn.isNullable()) {
692+
switch (dataType) {
693+
case UInt8:
694+
case UInt16:
695+
case UInt32:
696+
case UInt64:
697+
return dataType.getWiderObjectClass();
698+
case Enum8:
699+
case Enum16:
700+
return EnumValue.class;
701+
default:
702+
Class<?> cls = dataType.getObjectClass();
703+
return cls == null ? Object.class : cls;
704+
}
705+
} else {
706+
switch (dataType) {
707+
case UInt8:
708+
case UInt16:
709+
case UInt32:
710+
case UInt64:
711+
return dataType.getWiderPrimitiveClass();
712+
case Enum8:
713+
case Enum16:
714+
return EnumValue.class;
715+
case Variant:
716+
case Dynamic:
717+
case Geometry:
718+
return Object.class;
719+
case Array:
720+
return ArrayValue.class;
721+
case Tuple:
722+
case Nested:
723+
return Object[].class;
724+
case Map:
725+
return Map.class;
726+
default:
727+
Class<?> cls = dataType.getPrimitiveClass();
728+
return cls == null ? Object.class : cls;
729+
}
730+
}
731+
}
732+
670733
public void skipValue(ClickHouseColumn column) throws IOException {
671734
readValue(column, null);
672735
}

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

Lines changed: 90 additions & 11 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;
@@ -206,24 +207,102 @@ public void testReadNullVariantReturnsNull() throws Exception {
206207
}
207208

208209
@Test
209-
public void testReadVarIntReadsMaxInt() throws IOException {
210-
Assert.assertEquals(readVarInt((byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x07),
211-
Integer.MAX_VALUE);
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);
212230
}
213231

214232
@Test
215-
public void testReadVarIntRejectsOverflow() {
216-
Assert.assertThrows(IOException.class,
217-
() -> readVarInt((byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x08));
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);
218253
}
219254

220255
@Test
221-
public void testReadVarIntRejectsOverlongValue() {
222-
Assert.assertThrows(IOException.class,
223-
() -> readVarInt((byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01));
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+
}
278+
279+
@Test
280+
public void testEmptyArrayTypes() throws Exception {
281+
assertEmptyArrayComponentType("Array(UInt8)", short.class);
282+
assertEmptyArrayComponentType("Array(Nullable(UInt8))", Short.class);
283+
assertEmptyArrayComponentType("Array(String)", String.class);
284+
assertEmptyArrayComponentType("Array(Nullable(String))", String.class);
285+
assertEmptyArrayComponentType("Array(Enum8('a'=1))", BinaryStreamReader.EnumValue.class);
286+
assertEmptyArrayComponentType("Array(Nullable(Enum8('a'=1)))", BinaryStreamReader.EnumValue.class);
287+
assertEmptyArrayComponentType("Array(Variant(Int32, String))", Object.class);
288+
assertEmptyArrayComponentType("Array(Array(String))", BinaryStreamReader.ArrayValue.class);
224289
}
225290

226-
private static int readVarInt(byte... bytes) throws IOException {
227-
return BinaryStreamReader.readVarInt(new ByteArrayInputStream(bytes));
291+
private void assertEmptyArrayComponentType(String columnType, Class<?> expectedComponentType) throws Exception {
292+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
293+
BinaryStreamUtils.writeVarInt(baos, 0);
294+
295+
BinaryStreamReader reader = new BinaryStreamReader(
296+
new ByteArrayInputStream(baos.toByteArray()),
297+
TimeZone.getTimeZone("UTC"),
298+
null,
299+
new BinaryStreamReader.CachingByteBufferAllocator(),
300+
false,
301+
null);
302+
303+
BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) reader.readValue(
304+
ClickHouseColumn.of("v", columnType));
305+
306+
Assert.assertEquals(array.getArray().getClass().getComponentType(), expectedComponentType, "Failed for " + columnType);
228307
}
229308
}

0 commit comments

Comments
 (0)