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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<LocalDate>` (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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty nullable array typing

Medium Severity

When a nullable array has length zero, readArray still uses getPrimitiveClass (e.g. double for Float64) instead of the boxed types used in readArrayItem. The same column can yield primitive empty arrays but boxed non-empty arrays.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8d13865. Configure here.

Class<?> itemClass = resolveNullableArrayItemClass(itemTypeColumn.getDataType());
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,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.
*
* <p>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);
}
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 @@ -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);
}
}