Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -801,10 +801,26 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws
}
ArrayValue array;
if (itemTypeColumn.isNullable()) {
Class<?> itemClass = resolveArrayItemClass(itemTypeColumn);
// Type the array by the values actually read: the declared type does not always tell
// which class readValue returns (UInt64 -> BigInteger, DateTime64 -> ZonedDateTime).
Object[] values = new Object[len];
Class<?> itemClass = null;
for (int i = 0; i < len; i++) {
Object value = readValue(itemTypeColumn);
values[i] = value;
if (value != null) {
itemClass = itemClass == null ? value.getClass() : commonSuperclass(itemClass, value.getClass());
}
}
if (itemClass == null) {
itemClass = resolveArrayItemClass(itemTypeColumn);
} else if (itemClass.isArray() && itemClass.getComponentType().isPrimitive()) {
// ArrayValue allocates Object[][] for an array class, which cannot hold Point's double[]
itemClass = Object.class;
}
array = new ArrayValue(itemClass, len);
for (int i = 0; i < len; i++) {
array.set(i, readValue(itemTypeColumn));
array.set(i, values[i]);
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}
} else if (itemTypeColumn.getDataType() == ClickHouseDataType.Variant
|| itemTypeColumn.getDataType() == ClickHouseDataType.Dynamic
Expand Down Expand Up @@ -868,8 +884,15 @@ private static Class<?> resolveArrayItemClass(ClickHouseColumn itemTypeColumn) {
case UInt8:
case UInt16:
case UInt32:
case UInt64:
return dataType.getWiderObjectClass();
case UInt64:
return BigInteger.class;
case DateTime:
case DateTime32:
case DateTime64:
return ZonedDateTime.class;
case Tuple:
return Object[].class;
case Enum8:
case Enum16:
return EnumValue.class;
Expand Down Expand Up @@ -905,6 +928,18 @@ private static Class<?> resolveArrayItemClass(ClickHouseColumn itemTypeColumn) {
}
}

/**
* Returns the most specific class that both classes extend, so that an array of it can hold values
* of both. For example, IPv6 values can be read as {@link Inet4Address} or {@link Inet6Address}.
*/
private static Class<?> commonSuperclass(Class<?> a, Class<?> b) {
Class<?> cls = a;
while (!cls.isAssignableFrom(b)) {
cls = cls.getSuperclass();
}
return cls;
}

/**
* Reads a value nested inside a container (Array, Map, Tuple, Nested, Variant, JSON). Strings are always
* decoded into {@link String} here, regardless of the binary-string feature flag, because nested types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;

Expand Down Expand Up @@ -372,4 +377,130 @@ public void testNullableArraysWithMixedTypes() throws Exception {
Assert.assertNull(mixedList.get(3));
Assert.assertEquals(mixedList.get(4), 5L);
}

@Test
public void testNullableArraysOfTypesReadAsOtherClasses() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
String[] names = new String[]{"uint64_arr", "datetime64_arr", "ipv6_arr", "all_null_uint64_arr"};
String[] types = new String[]{"Array(Nullable(UInt64))", "Array(Nullable(DateTime64(3, 'UTC')))",
"Array(Nullable(IPv6))", "Array(Nullable(UInt64))"};

BinaryStreamUtils.writeVarInt(out, names.length);
for (String name : names) {
BinaryStreamUtils.writeString(out, name);
}
for (String type : types) {
BinaryStreamUtils.writeString(out, type);
}

// Array(Nullable(UInt64)): [0, NULL, 18446744073709551615]
BigInteger maxUInt64 = new BigInteger("18446744073709551615");
BinaryStreamUtils.writeVarInt(out, 3);
BinaryStreamUtils.writeNonNull(out);
BinaryStreamUtils.writeUnsignedInt64(out, BigInteger.ZERO);
BinaryStreamUtils.writeNull(out);
BinaryStreamUtils.writeNonNull(out);
BinaryStreamUtils.writeUnsignedInt64(out, maxUInt64);

// Array(Nullable(DateTime64(3, 'UTC'))): ['2026-01-01 13:34:56.780', NULL]
LocalDateTime dateTime = LocalDateTime.of(2026, 1, 1, 13, 34, 56, 780_000_000);
BinaryStreamUtils.writeVarInt(out, 2);
BinaryStreamUtils.writeNonNull(out);
BinaryStreamUtils.writeDateTime64(out, dateTime, 3, TimeZone.getTimeZone("UTC"));
BinaryStreamUtils.writeNull(out);

// Array(Nullable(IPv6)): ['::ffff:1.2.3.4', NULL, '2001:db8::1']
// An IPv4-mapped address is read as Inet4Address, the other one as Inet6Address.
BinaryStreamUtils.writeVarInt(out, 3);
BinaryStreamUtils.writeNonNull(out);
out.write(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xff, (byte) 0xff, 1, 2, 3, 4});
BinaryStreamUtils.writeNull(out);
BinaryStreamUtils.writeNonNull(out);
out.write(InetAddress.getByName("2001:db8::1").getAddress());

// Array(Nullable(UInt64)): [NULL, NULL]
BinaryStreamUtils.writeVarInt(out, 2);
BinaryStreamUtils.writeNull(out);
BinaryStreamUtils.writeNull(out);

InputStream in = new ByteArrayInputStream(out.toByteArray());
QuerySettings querySettings = new QuerySettings().setUseTimeZone(TimeZone.getTimeZone("UTC").toZoneId().getId());
RowBinaryWithNamesAndTypesFormatReader reader =
new RowBinaryWithNamesAndTypesFormatReader(in, querySettings, new BinaryStreamReader.CachingByteBufferAllocator());

reader.next();

List<Object> uint64List = reader.getList("uint64_arr");
Assert.assertEquals(uint64List, Arrays.asList(BigInteger.ZERO, null, maxUInt64));

List<Object> dateTimeList = reader.getList("datetime64_arr");
Assert.assertEquals(dateTimeList, Arrays.asList(dateTime.atZone(ZoneId.of("UTC")), null));

List<Object> ipv6List = reader.getList("ipv6_arr");
Assert.assertEquals(ipv6List, Arrays.asList(InetAddress.getByName("1.2.3.4"), null,
InetAddress.getByName("2001:db8::1")));

List<Object> allNullList = reader.getList("all_null_uint64_arr");
Assert.assertEquals(allNullList, Arrays.asList(null, null));
}

@Test
public void testNullableArraysOfArrayBackedTypes() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
String[] names = new String[]{"point_arr", "ring_arr", "tuple_arr"};
String[] types = new String[]{"Array(Nullable(Point))", "Array(Nullable(Ring))",
"Array(Nullable(Tuple(Int32, Int32)))"};

BinaryStreamUtils.writeVarInt(out, names.length);
for (String name : names) {
BinaryStreamUtils.writeString(out, name);
}
for (String type : types) {
BinaryStreamUtils.writeString(out, type);
}

// Array(Nullable(Point)): [(1.5, 2.5), NULL]
BinaryStreamUtils.writeVarInt(out, 2);
BinaryStreamUtils.writeNonNull(out);
BinaryStreamUtils.writeFloat64(out, 1.5);
BinaryStreamUtils.writeFloat64(out, 2.5);
BinaryStreamUtils.writeNull(out);

// Array(Nullable(Ring)): [[(1.5, 2.5)], NULL]
BinaryStreamUtils.writeVarInt(out, 2);
BinaryStreamUtils.writeNonNull(out);
BinaryStreamUtils.writeVarInt(out, 1);
BinaryStreamUtils.writeFloat64(out, 1.5);
BinaryStreamUtils.writeFloat64(out, 2.5);
BinaryStreamUtils.writeNull(out);

// Array(Nullable(Tuple(Int32, Int32))): [(1, 2), NULL]
BinaryStreamUtils.writeVarInt(out, 2);
BinaryStreamUtils.writeNonNull(out);
BinaryStreamUtils.writeInt32(out, 1);
BinaryStreamUtils.writeInt32(out, 2);
BinaryStreamUtils.writeNull(out);

InputStream in = new ByteArrayInputStream(out.toByteArray());
QuerySettings querySettings = new QuerySettings().setUseTimeZone(TimeZone.getTimeZone("UTC").toZoneId().getId());
RowBinaryWithNamesAndTypesFormatReader reader =
new RowBinaryWithNamesAndTypesFormatReader(in, querySettings, new BinaryStreamReader.CachingByteBufferAllocator());

reader.next();

List<Object> points = reader.getList("point_arr");
Assert.assertEquals(points.size(), 2);
Assert.assertEquals((double[]) points.get(0), new double[]{1.5, 2.5});
Assert.assertNull(points.get(1));

List<Object> rings = reader.getList("ring_arr");
Assert.assertEquals(rings.size(), 2);
Assert.assertEquals(((double[][]) rings.get(0))[0], new double[]{1.5, 2.5});
Assert.assertNull(rings.get(1));

List<Object> tuples = reader.getList("tuple_arr");
Assert.assertEquals(tuples.size(), 2);
Assert.assertEquals((Object[]) tuples.get(0), new Object[]{1, 2});
Assert.assertNull(tuples.get(1));
}
}
8 changes: 8 additions & 0 deletions history/latest/3283.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- **[client-v2]** Fixed reading `Array(Nullable(T))` columns whose element values are read as a different class than
the one declared for `T`. Reading failed with `IllegalArgumentException: array element type mismatch` for
`Array(Nullable(UInt64))` (values are `BigInteger`, the array was `UnsignedLong[]`), for
`Array(Nullable(DateTime64))` and `Array(Nullable(DateTime))` (values are `ZonedDateTime`, the array was
`LocalDateTime[]`), and for `Array(Nullable(IPv6))` holding IPv4-mapped addresses (read as `Inet4Address`). The
array is now typed by the values that were read. An array of only `NULL` elements gets the same type as one with
values for `UInt64` (`BigInteger`), `DateTime`/`DateTime64` (`ZonedDateTime`) and `Tuple` (`Object[]`).
(https://github.com/ClickHouse/clickhouse-java/issues/3283, https://github.com/ClickHouse/clickhouse-java/issues/3287)