Skip to content

Commit 682746c

Browse files
Fix client-v2 Native reader misreading multi-row Array columns
NativeFormatReader.readBlock() read an Array column's cumulative row offsets but then used the first row's offset as the element count for every row, truncating later rows and desyncing the columns that follow the array in the same block. Compute each row's length from the delta between consecutive offsets instead. Also make BinaryStreamReader.readArrayItem handle len == 0 (empty array rows) without reading a phantom element and indexing a zero-length array, mirroring the existing len == 0 guard in readArray. Fixes: #2955
1 parent dd09060 commit 682746c

4 files changed

Lines changed: 67 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919

2020
### Bug Fixes
2121

22+
- **[client-v2]** Fixed the `Native` format reader (`NativeFormatReader`) misreading `Array` columns in multi-row
23+
results whose rows have different lengths. Native encodes an array column as cumulative row offsets followed by the
24+
flattened elements, but the reader used the first row's offset as the element count for every row — truncating later
25+
rows and desyncing the columns that follow the array in the same block. Each row's length is now derived from the
26+
difference between consecutive offsets, and empty array rows (`len == 0`) no longer read a phantom element. Results
27+
with uniform array lengths were unaffected. (https://github.com/ClickHouse/clickhouse-java/issues/2955)
28+
2229
- **[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)
2330

2431
- **[client-v2]** Fixed `Float32`/`Float64` columns throwing `ClassCastException` when a value of a

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,18 @@ private boolean readBlock() throws IOException {
9393

9494
List<Object> values = new ArrayList<>(nRows);
9595
if (column.isArray()) {
96-
int[] sizes = new int[nRows];
96+
// Native encodes an Array column as nRows cumulative offsets followed by the
97+
// flattened elements; each row's element count is the delta between consecutive
98+
// offsets, not the first offset.
99+
long[] offsets = new long[nRows];
97100
for (int j = 0; j < nRows; j++) {
98-
sizes[j] = Math.toIntExact(binaryStreamReader.readLongLE());
101+
offsets[j] = binaryStreamReader.readLongLE();
99102
}
103+
long prevOffset = 0;
100104
for (int j = 0; j < nRows; j++) {
101-
values.add(binaryStreamReader.readArrayItem(column.getNestedColumns().get(0), sizes[0]));
105+
int len = Math.toIntExact(offsets[j] - prevOffset);
106+
values.add(binaryStreamReader.readArrayItem(column.getNestedColumns().get(0), len));
107+
prevOffset = offsets[j];
102108
}
103109
} else {
104110
for (int j = 0; j < nRows; j++) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,11 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException {
655655
}
656656

657657
public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws IOException {
658+
if (len == 0) {
659+
// Nothing to read for an empty array; typing it via resolveArrayItemClass avoids the
660+
// primitive branch below reading a phantom element and indexing a zero-length array.
661+
return new ArrayValue(resolveArrayItemClass(itemTypeColumn), 0);
662+
}
658663
ArrayValue array;
659664
if (itemTypeColumn.isNullable()) {
660665
Class<?> itemClass = resolveArrayItemClass(itemTypeColumn);

client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,52 @@ public void testReadingArrayInNative() throws Exception {
482482
}
483483
}
484484

485+
@DataProvider(name = "multiRowArrayCases")
486+
Object[][] getMultiRowArrayCases() {
487+
String nonUniform = "SELECT id, arr, tag FROM values("
488+
+ "'id UInt32, arr Array(Int32), tag Int32', "
489+
+ "(1, [10], 100), (2, [20, 21], 200), (3, [], 300), (4, [30, 31, 32], 400), (5, [40], 500)"
490+
+ ") ORDER BY id";
491+
List<Object[]> nonUniformRows = Arrays.asList(
492+
new Object[]{1L, Arrays.asList(10), 100},
493+
new Object[]{2L, Arrays.asList(20, 21), 200},
494+
new Object[]{3L, Collections.emptyList(), 300},
495+
new Object[]{4L, Arrays.asList(30, 31, 32), 400},
496+
new Object[]{5L, Arrays.asList(40), 500});
497+
498+
String uniform = "SELECT id, arr, tag FROM values("
499+
+ "'id UInt32, arr Array(Int32), tag Int32', "
500+
+ "(1, [10, 11], 100), (2, [20, 21], 200), (3, [30, 31], 300)"
501+
+ ") ORDER BY id";
502+
List<Object[]> uniformRows = Arrays.asList(
503+
new Object[]{1L, Arrays.asList(10, 11), 100},
504+
new Object[]{2L, Arrays.asList(20, 21), 200},
505+
new Object[]{3L, Arrays.asList(30, 31), 300});
506+
507+
return new Object[][]{
508+
{ClickHouseFormat.Native, nonUniform, nonUniformRows},
509+
{ClickHouseFormat.Native, uniform, uniformRows},
510+
{ClickHouseFormat.RowBinaryWithNamesAndTypes, nonUniform, nonUniformRows},
511+
};
512+
}
513+
514+
@Test(groups = {"integration"}, dataProvider = "multiRowArrayCases")
515+
public void testReadingMultiRowArrays(ClickHouseFormat format, String sql, List<Object[]> expectedRows)
516+
throws Exception {
517+
QuerySettings settings = new QuerySettings().setFormat(format);
518+
try (QueryResponse response = client.query(sql, settings).get()) {
519+
ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response);
520+
for (Object[] expected : expectedRows) {
521+
Map<String, Object> record = reader.next();
522+
Assert.assertNotNull(record, "Expected a row for id " + expected[0]);
523+
Assert.assertEquals(record.get("id"), expected[0]);
524+
Assert.assertEquals(((BinaryStreamReader.ArrayValue) record.get("arr")).asList(), expected[1]);
525+
Assert.assertEquals(record.get("tag"), expected[2]);
526+
}
527+
Assert.assertNull(reader.next());
528+
}
529+
}
530+
485531
@Test(groups = {"integration"})
486532
public void testBinaryStreamReader() throws Exception {
487533
final String table = "dynamic_schema_test_table";

0 commit comments

Comments
 (0)