This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new ee3692cf3142 fix(flink): avoid AIOOBE in NestedColumnReader across the
read batch boundary (#19210)
ee3692cf3142 is described below
commit ee3692cf3142c293b2ec36e520c252936a8c7af3
Author: ericyuan915 <[email protected]>
AuthorDate: Tue Jul 7 04:18:50 2026 -0700
fix(flink): avoid AIOOBE in NestedColumnReader across the read batch
boundary (#19210)
NestedColumnReader#readRow collapses a present row whose children are all
null into a NULL row. That loop iterated up to rowPosition.getPositionsCount(),
which on a full, non-final vectorized batch is one larger than the materialized
child vectors: the Dremel level stream carries a one-record lookahead
(NestedPrimitiveColumnReader#readAndNewVector reads one value past the batch,
and #getLevelDelegation keeps that trailing level for the next batch). Reading
a nested ROW column from a C [...]
Clamp the collapse loop to the shortest vector it indexes (the row vector
and every child), unwrapping ParquetDecimalVector (a non-AbstractHeapVector
DECIMAL child) via a new vectorLength helper; the phantom trailing position is
never surfaced downstream (ParquetColumnarRowSplitReader caps the batch at
num). Applied identically to all five flink modules
(1.18.x/1.19.x/1.20.x/2.0.x/2.1.x). Adds integration test
ITTestHoodieDataSource#testParquetNestedRowExceedingReadBatch covering both
[...]
closes #19208
---
.../apache/hudi/table/ITTestHoodieDataSource.java | 119 +++++++++++++++++++++
.../cow/vector/reader/NestedColumnReader.java | 33 +++++-
.../cow/vector/reader/NestedColumnReader.java | 33 +++++-
.../cow/vector/reader/NestedColumnReader.java | 33 +++++-
.../cow/vector/reader/NestedColumnReader.java | 33 +++++-
.../cow/vector/reader/NestedColumnReader.java | 33 +++++-
6 files changed, 279 insertions(+), 5 deletions(-)
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestHoodieDataSource.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestHoodieDataSource.java
index 9cf41d1e47fd..4d03c3d9cc82 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestHoodieDataSource.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestHoodieDataSource.java
@@ -119,6 +119,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertLinesMatch;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -2474,6 +2475,124 @@ public class ITTestHoodieDataSource {
assertRowsEqualsUnordered(expected, result);
}
+ @Test
+ void testParquetNestedRowExceedingReadBatch() {
+ // Regression for NestedColumnReader#readRow throwing
ArrayIndexOutOfBoundsException when a COW
+ // base file holds more rows than the 2048-row vectorized read batch
+ // (RecordIterators.DEFAULT_BATCH_SIZE) and a nested ROW column is read.
On a full, non-final
+ // batch the Dremel level stream carries a one-record lookahead, so
NestedPositionUtil
+ // #calculateRowOffsets returns positionsCount = batchSize + 1 = 2049
while the materialized
+ // child column vectors are sized to their value count = 2048. The
Hudi-specific null-row-collapse
+ // loop iterates to positionsCount and reads child.isNullAt(2048), one
past a length-2048 vector.
+ //
+ // Two conditions are both required to surface it, and drove this schema
and data:
+ // 1. The bad index is only reached through AbstractHeapVector#isNullAt,
which short-circuits to
+ // false without touching isNull[] when the vector has no nulls. So a
child vector must
+ // actually carry a null. Odd-id rows therefore store a present ROW
with all-null children
+ // (row(null, ...)); the row stays present (its own isNullAt(2048)
short-circuits) but the
+ // child leaf vectors get noNulls=false and overrun at the phantom
index. Half the rows are
+ // null-children so the first full batch is guaranteed to contain them
regardless of how
+ // bulk_insert orders keys.
+ // 2. The nullable leaves must be *direct* children of the collapsed row.
A sub-row child would
+ // be renewed to positionsCount (length 2049) and not overrun, so the
two nested rows are
+ // top-level columns: f_scalar row(f0 int, f1 varchar(10)) covers
heap-vector children, and
+ // f_dec row(d decimal(10, 2)) covers a decimal child, whose
ParquetDecimalVector is not an
+ // AbstractHeapVector and must be unwrapped by
NestedColumnReader#vectorLength.
+ // See ITTestHoodieDataSource#testParquetNullChildColumnsRowTypes for the
collapse behaviour.
+ TableEnvironment tableEnv = batchTableEnv;
+
+ // More rows than one 2048-row read batch, so the first batch is full and
non-final -- that is
+ // what makes the level stream carry the trailing lookahead that
overshoots the vectors. The
+ // rows are generated by cross joining two small VALUES lists rather than
a single 2000+-row
+ // VALUES literal: Calcite plans the latter pathologically slowly (minutes
to hours), while two
+ // ~50-element lists plan instantly and the row count is simply their
product.
+ final int outer = 43;
+ final int inner = 50;
+ final int numRows = outer * inner; // 2150 > 2048
+
+ String hoodieTableDDL = sql("t1")
+ .field("f_int int")
+ .field("f_scalar row(f0 int, f1 varchar(10))")
+ .field("f_dec row(d decimal(10, 2))")
+ .pkField("f_int")
+ .noPartition()
+ .option(FlinkOptions.PATH, tempFile.getAbsolutePath())
+ .option(FlinkOptions.OPERATION, "bulk_insert")
+ // Single write task => all rows land in one base file, so one read
split crosses the
+ // 2048-row batch boundary.
+ .option(FlinkOptions.WRITE_TASKS, 1)
+ .end();
+ tableEnv.executeSql(hoodieTableDDL);
+
+ // id = blk * inner + pos is unique over blk in [0, outer), pos in [0,
inner) => 0 .. numRows-1.
+ // Both nested rows stay present; even ids get populated leaves, odd ids
get all-null leaves
+ // (which the reader collapses back to a NULL row). Each ROW is cast to
its named type so the
+ // query output type matches the sink column exactly.
+ String insert = "insert into t1 select\n"
+ + " g.id,\n"
+ + " cast(row(\n"
+ + " case when mod(g.id, 2) = 0 then g.id else cast(null as int)
end,\n"
+ + " case when mod(g.id, 2) = 0 then concat('v', cast(g.id as
varchar)) else cast(null as varchar(10)) end\n"
+ + " ) as row<f0 int, f1 varchar(10)>),\n"
+ + " cast(row(\n"
+ + " case when mod(g.id, 2) = 0 then cast(g.id as decimal(10, 2))
else cast(null as decimal(10, 2)) end\n"
+ + " ) as row<d decimal(10, 2)>)\n"
+ + "from (\n"
+ + " select blk.b * " + inner + " + pos.p as id\n"
+ + " from (values " + valuesList(outer) + ") as blk(b)\n"
+ + " cross join (values " + valuesList(inner) + ") as pos(p)\n"
+ + ") g";
+ execInsertSql(tableEnv, insert);
+
+ List<Row> result = CollectionUtil.iterableToList(
+ () -> tableEnv.sqlQuery("select * from t1").execute().collect());
+
+ // The read completes (no AIOOBE across the batch boundary) and every row
is returned. Without
+ // the fix the vectorized read throws while materializing the first full
batch, so this fails.
+ assertEquals(numRows, result.size());
+
+ // bulk_insert does not preserve order, so index by pk.
+ Map<Integer, Row> byId = new HashMap<>();
+ for (Row r : result) {
+ byId.put((Integer) r.getField(0), r);
+ }
+ // Populated rows (even id) round-trip both nested rows -- one from the
first (full) batch and
+ // one with a large id past the boundary.
+ assertPopulatedRow(byId.get(0), 0);
+ assertPopulatedRow(byId.get(numRows - 2), numRows - 2);
+ // All-null-children rows (odd id) collapse both nested rows back to NULL,
including a large id.
+ assertCollapsedRow(byId.get(1));
+ assertCollapsedRow(byId.get(numRows - 1));
+ }
+
+ /** Builds the VALUES row list {@code (0), (1), ..., (n-1)} for the
generator cross join. */
+ private static String valuesList(int n) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < n; i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append('(').append(i).append(')');
+ }
+ return sb.toString();
+ }
+
+ /** Asserts the row keyed by an even {@code id} round-trips its populated
nested rows. */
+ private static void assertPopulatedRow(Row row, int id) {
+ assertNotNull(row, "row with pk " + id + " was not read back");
+ Row scalar = (Row) row.getField(1);
+ assertEquals(id, scalar.getField(0));
+ assertEquals("v" + id, scalar.getField(1));
+ assertNotNull(((Row) row.getField(2)).getField(0)); // decimal leaf
present, not null
+ }
+
+ /** Asserts the row keyed by an odd {@code id} had both all-null nested rows
collapsed to NULL. */
+ private static void assertCollapsedRow(Row row) {
+ assertNotNull(row, "expected an odd-id row to be read back");
+ assertNull(row.getField(1)); // f_scalar collapsed to null
+ assertNull(row.getField(2)); // f_dec collapsed to null
+ }
+
@ParameterizedTest
@ValueSource(strings = {"insert", "upsert", "bulk_insert"})
void testParquetNullChildColumnsRowTypes(String operation) {
diff --git
a/hudi-flink-datasource/hudi-flink1.18.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
b/hudi-flink-datasource/hudi-flink1.18.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
index 7ec70490dc45..60575f148cc4 100644
---
a/hudi-flink-datasource/hudi-flink1.18.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
+++
b/hudi-flink-datasource/hudi-flink1.18.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
@@ -22,6 +22,7 @@ import
org.apache.hudi.table.format.cow.utils.NestedPositionUtil;
import org.apache.hudi.table.format.cow.vector.HeapArrayVector;
import org.apache.hudi.table.format.cow.vector.HeapMapColumnVector;
import org.apache.hudi.table.format.cow.vector.HeapRowColumnVector;
+import org.apache.hudi.table.format.cow.vector.ParquetDecimalVector;
import org.apache.hudi.table.format.cow.vector.position.CollectionPosition;
import org.apache.hudi.table.format.cow.vector.position.LevelDelegation;
import org.apache.hudi.table.format.cow.vector.position.RowPosition;
@@ -155,7 +156,21 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
// legacy RowColumnReader (deleted alongside the Dremel rewire) and
existing Hudi tables rely
// on it. Diverges from Flink 2.1, which would surface it as Row(null,
null). Pinned by the
// integration test
ITTestHoodieDataSource#testParquetNullChildColumnsRowTypes.
- int rowCount = rowPosition.getPositionsCount();
+ // positionsCount comes from the Dremel definition/repetition level stream
+ // (NestedPositionUtil#calculateRowOffsets). On a full, non-final batch
that stream carries a
+ // one-record lookahead (NestedPrimitiveColumnReader#readAndNewVector
reads one value past the
+ // batch in its do/while, and #getLevelDelegation keeps that trailing
level for the next batch),
+ // so positionsCount can be one larger than the materialized vector
lengths. When inside==true
+ // the row vector is renewed to positionsCount but its children are sized
to their value count;
+ // when inside==false the row vector keeps its batch capacity. Either way,
iterating all the way
+ // to positionsCount can read one element past a shorter vector and throw
+ // ArrayIndexOutOfBoundsException. Clamp to the shortest vector this loop
indexes -- the phantom
+ // trailing position is never surfaced downstream
(ParquetColumnarRowSplitReader caps the batch
+ // at num).
+ int rowCount = Math.min(rowPosition.getPositionsCount(),
heapRowVector.getLen());
+ for (WritableColumnVector child : finalChildrenVectors) {
+ rowCount = Math.min(rowCount, vectorLength(child));
+ }
for (int j = 0; j < rowCount; j++) {
if (heapRowVector.isNullAt(j)) {
continue;
@@ -271,6 +286,22 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
return Tuple2.of(reader.getLevelDelegation(), writableColumnVector);
}
+ /**
+ * The length of the {@code isNull}-backed storage that {@code vector} (a
row child) is indexed
+ * against by the null-collapse loop in {@link #readRow}. Every row child is
an {@link
+ * AbstractHeapVector} (nested rows/arrays/maps and all non-decimal
primitives) or a {@link
+ * ParquetDecimalVector} wrapping one (DECIMAL leaves; see {@code
+ * NestedPrimitiveColumnReader#fillColumnVector}); unwrapping the latter
yields an {@code
+ * AbstractHeapVector} in all cases.
+ */
+ private static int vectorLength(ColumnVector vector) {
+ ColumnVector storage =
+ vector instanceof ParquetDecimalVector
+ ? ((ParquetDecimalVector) vector).getVector()
+ : vector;
+ return ((AbstractHeapVector) storage).getLen();
+ }
+
private static void setFieldNullFlag(boolean[] nullFlags, AbstractHeapVector
vector) {
for (int index = 0; index < vector.getLen() && index < nullFlags.length;
index++) {
if (nullFlags[index]) {
diff --git
a/hudi-flink-datasource/hudi-flink1.19.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
b/hudi-flink-datasource/hudi-flink1.19.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
index 7ec70490dc45..60575f148cc4 100644
---
a/hudi-flink-datasource/hudi-flink1.19.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
+++
b/hudi-flink-datasource/hudi-flink1.19.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
@@ -22,6 +22,7 @@ import
org.apache.hudi.table.format.cow.utils.NestedPositionUtil;
import org.apache.hudi.table.format.cow.vector.HeapArrayVector;
import org.apache.hudi.table.format.cow.vector.HeapMapColumnVector;
import org.apache.hudi.table.format.cow.vector.HeapRowColumnVector;
+import org.apache.hudi.table.format.cow.vector.ParquetDecimalVector;
import org.apache.hudi.table.format.cow.vector.position.CollectionPosition;
import org.apache.hudi.table.format.cow.vector.position.LevelDelegation;
import org.apache.hudi.table.format.cow.vector.position.RowPosition;
@@ -155,7 +156,21 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
// legacy RowColumnReader (deleted alongside the Dremel rewire) and
existing Hudi tables rely
// on it. Diverges from Flink 2.1, which would surface it as Row(null,
null). Pinned by the
// integration test
ITTestHoodieDataSource#testParquetNullChildColumnsRowTypes.
- int rowCount = rowPosition.getPositionsCount();
+ // positionsCount comes from the Dremel definition/repetition level stream
+ // (NestedPositionUtil#calculateRowOffsets). On a full, non-final batch
that stream carries a
+ // one-record lookahead (NestedPrimitiveColumnReader#readAndNewVector
reads one value past the
+ // batch in its do/while, and #getLevelDelegation keeps that trailing
level for the next batch),
+ // so positionsCount can be one larger than the materialized vector
lengths. When inside==true
+ // the row vector is renewed to positionsCount but its children are sized
to their value count;
+ // when inside==false the row vector keeps its batch capacity. Either way,
iterating all the way
+ // to positionsCount can read one element past a shorter vector and throw
+ // ArrayIndexOutOfBoundsException. Clamp to the shortest vector this loop
indexes -- the phantom
+ // trailing position is never surfaced downstream
(ParquetColumnarRowSplitReader caps the batch
+ // at num).
+ int rowCount = Math.min(rowPosition.getPositionsCount(),
heapRowVector.getLen());
+ for (WritableColumnVector child : finalChildrenVectors) {
+ rowCount = Math.min(rowCount, vectorLength(child));
+ }
for (int j = 0; j < rowCount; j++) {
if (heapRowVector.isNullAt(j)) {
continue;
@@ -271,6 +286,22 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
return Tuple2.of(reader.getLevelDelegation(), writableColumnVector);
}
+ /**
+ * The length of the {@code isNull}-backed storage that {@code vector} (a
row child) is indexed
+ * against by the null-collapse loop in {@link #readRow}. Every row child is
an {@link
+ * AbstractHeapVector} (nested rows/arrays/maps and all non-decimal
primitives) or a {@link
+ * ParquetDecimalVector} wrapping one (DECIMAL leaves; see {@code
+ * NestedPrimitiveColumnReader#fillColumnVector}); unwrapping the latter
yields an {@code
+ * AbstractHeapVector} in all cases.
+ */
+ private static int vectorLength(ColumnVector vector) {
+ ColumnVector storage =
+ vector instanceof ParquetDecimalVector
+ ? ((ParquetDecimalVector) vector).getVector()
+ : vector;
+ return ((AbstractHeapVector) storage).getLen();
+ }
+
private static void setFieldNullFlag(boolean[] nullFlags, AbstractHeapVector
vector) {
for (int index = 0; index < vector.getLen() && index < nullFlags.length;
index++) {
if (nullFlags[index]) {
diff --git
a/hudi-flink-datasource/hudi-flink1.20.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
b/hudi-flink-datasource/hudi-flink1.20.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
index 7ec70490dc45..60575f148cc4 100644
---
a/hudi-flink-datasource/hudi-flink1.20.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
+++
b/hudi-flink-datasource/hudi-flink1.20.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
@@ -22,6 +22,7 @@ import
org.apache.hudi.table.format.cow.utils.NestedPositionUtil;
import org.apache.hudi.table.format.cow.vector.HeapArrayVector;
import org.apache.hudi.table.format.cow.vector.HeapMapColumnVector;
import org.apache.hudi.table.format.cow.vector.HeapRowColumnVector;
+import org.apache.hudi.table.format.cow.vector.ParquetDecimalVector;
import org.apache.hudi.table.format.cow.vector.position.CollectionPosition;
import org.apache.hudi.table.format.cow.vector.position.LevelDelegation;
import org.apache.hudi.table.format.cow.vector.position.RowPosition;
@@ -155,7 +156,21 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
// legacy RowColumnReader (deleted alongside the Dremel rewire) and
existing Hudi tables rely
// on it. Diverges from Flink 2.1, which would surface it as Row(null,
null). Pinned by the
// integration test
ITTestHoodieDataSource#testParquetNullChildColumnsRowTypes.
- int rowCount = rowPosition.getPositionsCount();
+ // positionsCount comes from the Dremel definition/repetition level stream
+ // (NestedPositionUtil#calculateRowOffsets). On a full, non-final batch
that stream carries a
+ // one-record lookahead (NestedPrimitiveColumnReader#readAndNewVector
reads one value past the
+ // batch in its do/while, and #getLevelDelegation keeps that trailing
level for the next batch),
+ // so positionsCount can be one larger than the materialized vector
lengths. When inside==true
+ // the row vector is renewed to positionsCount but its children are sized
to their value count;
+ // when inside==false the row vector keeps its batch capacity. Either way,
iterating all the way
+ // to positionsCount can read one element past a shorter vector and throw
+ // ArrayIndexOutOfBoundsException. Clamp to the shortest vector this loop
indexes -- the phantom
+ // trailing position is never surfaced downstream
(ParquetColumnarRowSplitReader caps the batch
+ // at num).
+ int rowCount = Math.min(rowPosition.getPositionsCount(),
heapRowVector.getLen());
+ for (WritableColumnVector child : finalChildrenVectors) {
+ rowCount = Math.min(rowCount, vectorLength(child));
+ }
for (int j = 0; j < rowCount; j++) {
if (heapRowVector.isNullAt(j)) {
continue;
@@ -271,6 +286,22 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
return Tuple2.of(reader.getLevelDelegation(), writableColumnVector);
}
+ /**
+ * The length of the {@code isNull}-backed storage that {@code vector} (a
row child) is indexed
+ * against by the null-collapse loop in {@link #readRow}. Every row child is
an {@link
+ * AbstractHeapVector} (nested rows/arrays/maps and all non-decimal
primitives) or a {@link
+ * ParquetDecimalVector} wrapping one (DECIMAL leaves; see {@code
+ * NestedPrimitiveColumnReader#fillColumnVector}); unwrapping the latter
yields an {@code
+ * AbstractHeapVector} in all cases.
+ */
+ private static int vectorLength(ColumnVector vector) {
+ ColumnVector storage =
+ vector instanceof ParquetDecimalVector
+ ? ((ParquetDecimalVector) vector).getVector()
+ : vector;
+ return ((AbstractHeapVector) storage).getLen();
+ }
+
private static void setFieldNullFlag(boolean[] nullFlags, AbstractHeapVector
vector) {
for (int index = 0; index < vector.getLen() && index < nullFlags.length;
index++) {
if (nullFlags[index]) {
diff --git
a/hudi-flink-datasource/hudi-flink2.0.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
b/hudi-flink-datasource/hudi-flink2.0.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
index 7ec70490dc45..60575f148cc4 100644
---
a/hudi-flink-datasource/hudi-flink2.0.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
+++
b/hudi-flink-datasource/hudi-flink2.0.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
@@ -22,6 +22,7 @@ import
org.apache.hudi.table.format.cow.utils.NestedPositionUtil;
import org.apache.hudi.table.format.cow.vector.HeapArrayVector;
import org.apache.hudi.table.format.cow.vector.HeapMapColumnVector;
import org.apache.hudi.table.format.cow.vector.HeapRowColumnVector;
+import org.apache.hudi.table.format.cow.vector.ParquetDecimalVector;
import org.apache.hudi.table.format.cow.vector.position.CollectionPosition;
import org.apache.hudi.table.format.cow.vector.position.LevelDelegation;
import org.apache.hudi.table.format.cow.vector.position.RowPosition;
@@ -155,7 +156,21 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
// legacy RowColumnReader (deleted alongside the Dremel rewire) and
existing Hudi tables rely
// on it. Diverges from Flink 2.1, which would surface it as Row(null,
null). Pinned by the
// integration test
ITTestHoodieDataSource#testParquetNullChildColumnsRowTypes.
- int rowCount = rowPosition.getPositionsCount();
+ // positionsCount comes from the Dremel definition/repetition level stream
+ // (NestedPositionUtil#calculateRowOffsets). On a full, non-final batch
that stream carries a
+ // one-record lookahead (NestedPrimitiveColumnReader#readAndNewVector
reads one value past the
+ // batch in its do/while, and #getLevelDelegation keeps that trailing
level for the next batch),
+ // so positionsCount can be one larger than the materialized vector
lengths. When inside==true
+ // the row vector is renewed to positionsCount but its children are sized
to their value count;
+ // when inside==false the row vector keeps its batch capacity. Either way,
iterating all the way
+ // to positionsCount can read one element past a shorter vector and throw
+ // ArrayIndexOutOfBoundsException. Clamp to the shortest vector this loop
indexes -- the phantom
+ // trailing position is never surfaced downstream
(ParquetColumnarRowSplitReader caps the batch
+ // at num).
+ int rowCount = Math.min(rowPosition.getPositionsCount(),
heapRowVector.getLen());
+ for (WritableColumnVector child : finalChildrenVectors) {
+ rowCount = Math.min(rowCount, vectorLength(child));
+ }
for (int j = 0; j < rowCount; j++) {
if (heapRowVector.isNullAt(j)) {
continue;
@@ -271,6 +286,22 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
return Tuple2.of(reader.getLevelDelegation(), writableColumnVector);
}
+ /**
+ * The length of the {@code isNull}-backed storage that {@code vector} (a
row child) is indexed
+ * against by the null-collapse loop in {@link #readRow}. Every row child is
an {@link
+ * AbstractHeapVector} (nested rows/arrays/maps and all non-decimal
primitives) or a {@link
+ * ParquetDecimalVector} wrapping one (DECIMAL leaves; see {@code
+ * NestedPrimitiveColumnReader#fillColumnVector}); unwrapping the latter
yields an {@code
+ * AbstractHeapVector} in all cases.
+ */
+ private static int vectorLength(ColumnVector vector) {
+ ColumnVector storage =
+ vector instanceof ParquetDecimalVector
+ ? ((ParquetDecimalVector) vector).getVector()
+ : vector;
+ return ((AbstractHeapVector) storage).getLen();
+ }
+
private static void setFieldNullFlag(boolean[] nullFlags, AbstractHeapVector
vector) {
for (int index = 0; index < vector.getLen() && index < nullFlags.length;
index++) {
if (nullFlags[index]) {
diff --git
a/hudi-flink-datasource/hudi-flink2.1.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
b/hudi-flink-datasource/hudi-flink2.1.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
index 00abceeee4fa..7be03dacc218 100644
---
a/hudi-flink-datasource/hudi-flink2.1.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
+++
b/hudi-flink-datasource/hudi-flink2.1.x/src/main/java/org/apache/hudi/table/format/cow/vector/reader/NestedColumnReader.java
@@ -22,6 +22,7 @@ import
org.apache.hudi.table.format.cow.utils.NestedPositionUtil;
import org.apache.hudi.table.format.cow.vector.HeapArrayVector;
import org.apache.hudi.table.format.cow.vector.HeapMapColumnVector;
import org.apache.hudi.table.format.cow.vector.HeapRowColumnVector;
+import org.apache.hudi.table.format.cow.vector.ParquetDecimalVector;
import org.apache.hudi.table.format.cow.vector.position.CollectionPosition;
import org.apache.hudi.table.format.cow.vector.position.LevelDelegation;
import org.apache.hudi.table.format.cow.vector.position.RowPosition;
@@ -158,7 +159,21 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
// legacy RowColumnReader (deleted alongside the Dremel rewire) and
existing Hudi tables rely
// on it. Diverges from Flink 2.1, which would surface it as Row(null,
null). Pinned by the
// integration test
ITTestHoodieDataSource#testParquetNullChildColumnsRowTypes.
- int rowCount = rowPosition.getPositionsCount();
+ // positionsCount comes from the Dremel definition/repetition level stream
+ // (NestedPositionUtil#calculateRowOffsets). On a full, non-final batch
that stream carries a
+ // one-record lookahead (NestedPrimitiveColumnReader#readAndNewVector
reads one value past the
+ // batch in its do/while, and #getLevelDelegation keeps that trailing
level for the next batch),
+ // so positionsCount can be one larger than the materialized vector
lengths. When inside==true
+ // the row vector is renewed to positionsCount but its children are sized
to their value count;
+ // when inside==false the row vector keeps its batch capacity. Either way,
iterating all the way
+ // to positionsCount can read one element past a shorter vector and throw
+ // ArrayIndexOutOfBoundsException. Clamp to the shortest vector this loop
indexes -- the phantom
+ // trailing position is never surfaced downstream
(ParquetColumnarRowSplitReader caps the batch
+ // at num).
+ int rowCount = Math.min(rowPosition.getPositionsCount(),
heapRowVector.getLen());
+ for (WritableColumnVector child : finalChildrenVectors) {
+ rowCount = Math.min(rowCount, vectorLength(child));
+ }
for (int j = 0; j < rowCount; j++) {
if (heapRowVector.isNullAt(j)) {
continue;
@@ -274,6 +289,22 @@ public class NestedColumnReader implements
ColumnReader<WritableColumnVector> {
return Tuple2.of(reader.getLevelDelegation(), writableColumnVector);
}
+ /**
+ * The length of the {@code isNull}-backed storage that {@code vector} (a
row child) is indexed
+ * against by the null-collapse loop in {@link #readRow}. Every row child is
an {@link
+ * AbstractHeapVector} (nested rows/arrays/maps and all non-decimal
primitives) or a {@link
+ * ParquetDecimalVector} wrapping one (DECIMAL leaves; see {@code
+ * NestedPrimitiveColumnReader#fillColumnVector}); unwrapping the latter
yields an {@code
+ * AbstractHeapVector} in all cases.
+ */
+ private static int vectorLength(ColumnVector vector) {
+ ColumnVector storage =
+ vector instanceof ParquetDecimalVector
+ ? ((ParquetDecimalVector) vector).getVector()
+ : vector;
+ return ((AbstractHeapVector) storage).getLen();
+ }
+
private static void setFieldNullFlag(boolean[] nullFlags, AbstractHeapVector
vector) {
for (int index = 0; index < vector.getLen() && index < nullFlags.length;
index++) {
if (nullFlags[index]) {