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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### 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 binary varint decoding for length and count fields so overflowing or overlong values fail with an `IOException` instead of being decoded into corrupted or negative `int` values. (https://github.com/ClickHouse/clickhouse-java/issues/2902)

- **[client-v2]** Fixed container query parameters being sent unquoted, so `Client.query(sql, params, settings)` binding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()) {
Comment thread
cursor[bot] marked this conversation as resolved.
Class<?> itemClass = resolveArrayItemClass(itemTypeColumn);
array = new ArrayValue(itemClass, len);
for (int i = 0; i < len; i++) {
array.set(i, readValue(itemTypeColumn));
}
Comment thread
cursor[bot] marked this conversation as resolved.
} else if (itemTypeColumn.getDataType() == ClickHouseDataType.Variant
|| itemTypeColumn.getDataType() == ClickHouseDataType.Dynamic
|| itemTypeColumn.getDataType() == ClickHouseDataType.Geometry) {
array = new ArrayValue(Object.class, len);
Expand Down Expand Up @@ -667,6 +672,64 @@ public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws
return 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.
*
* <p>For unsigned integer types, {@code readValue} widens the value (e.g. UInt8 → Short,
* 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 itemTypeColumn the element column of the array
* @return the Java class to use as the array component type; never {@code null}
*/
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;
}
}
}

public void skipValue(ClickHouseColumn column) throws IOException {
readValue(column, null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -206,24 +207,102 @@ public void testReadNullVariantReturnsNull() throws Exception {
}

@Test
public void testReadVarIntReadsMaxInt() throws IOException {
Assert.assertEquals(readVarInt((byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x07),
Integer.MAX_VALUE);
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 testReadVarIntRejectsOverflow() {
Assert.assertThrows(IOException.class,
() -> readVarInt((byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x08));
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 testReadVarIntRejectsOverlongValue() {
Assert.assertThrows(IOException.class,
() -> readVarInt((byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01));
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);
}

@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 static int readVarInt(byte... bytes) throws IOException {
return BinaryStreamReader.readVarInt(new ByteArrayInputStream(bytes));
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);
}
}
Loading