Skip to content

Arrow: Fix ClassCastException in vectorized reader on int-to-long pro…#16343

Open
xndai wants to merge 1 commit into
apache:mainfrom
xndai:iceberg-16341
Open

Arrow: Fix ClassCastException in vectorized reader on int-to-long pro…#16343
xndai wants to merge 1 commit into
apache:mainfrom
xndai:iceberg-16341

Conversation

@xndai
Copy link
Copy Markdown

@xndai xndai commented May 14, 2026

…motion with INT logical type

Fix ClassCastException: BigIntVector cannot be cast to IntVector when reading Parquet files with INT(32, true) logical type annotation after promoting a column from int to long.

The vectorized reader's LogicalTypeVisitor now allocates vectors based on the Parquet physical type instead of deriving them from the (potentially promoted) Iceberg schema type.

Root Cause:
In VectorizedArrowReader.allocateFieldVector(), the Arrow field was created from the Iceberg schema type (which reflects the promoted LongType), producing a BigIntVector. The LogicalTypeVisitor then cast this vector to IntVector based on the Parquet file's INT(32) logical type, causing the mismatch.

The non-vectorized reader (BaseParquetReaders) already handles this correctly by checking the expected Iceberg type and using IntAsLongReader for promotion. The vectorized reader relies on the accessor layer for widening (IntAccessor.getLong() widens int to long), so the fix ensures the vector matches the physical data layout.

Tests:

  • testIntToLongPromotionWithLogicalType: verifies reading after promotion when file has INT(32, true) annotation (the reported crash)
  • testIntToLongPromotionWithoutLogicalType: verifies reading after promotion when file has bare INT32

Fixes #16341

@github-actions github-actions Bot added the arrow label May 14, 2026
Copy link
Copy Markdown
Contributor

@CTTY CTTY left a comment

Choose a reason for hiding this comment

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

LGTM! just one minor comment

// Iceberg has no unsigned integer type. Reading UINT32 into a 32-bit signed value would
// silently produce negative results for inputs above Integer.MAX_VALUE. UINT8 and UINT16
// both fit losslessly in a signed int32 and are allowed, matching the policy in
// BaseParquetReaders for the non-vectorized path.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why do we remove this comment? this still looks relevant

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I thought the check below was self explanatory. I add them back.

Comment on lines +501 to +507
IntVector intVector = (IntVector) vector;
for (int i = 0; i < root.getRowCount(); i++) {
assertThat(intVector.get(i))
.as("Row %d value should be read correctly", rowIndex)
.isEqualTo(values.get(rowIndex));
rowIndex++;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Testing if the accessor gives back the values correctly is missing

Suggested change
IntVector intVector = (IntVector) vector;
for (int i = 0; i < root.getRowCount(); i++) {
assertThat(intVector.get(i))
.as("Row %d value should be read correctly", rowIndex)
.isEqualTo(values.get(rowIndex));
rowIndex++;
}
ColumnVector columnVector = batch.column(0);
for (int i = 0; i < root.getRowCount(); i++) {
assertThat(columnVector.getLong(i))
.as("Row %d value should be read correctly", rowIndex)
.isEqualTo((long) values.get(i));
}

Comment on lines +491 to +492
int totalRows = 0;
int rowIndex = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If the test data is so small and batch size so large no need for tracking the rows

Comment on lines +564 to +585
int totalRows = 0;
int rowIndex = 0;
int[] expectedValues = new int[] {1, 2, 3, Integer.MAX_VALUE};
try (VectorizedTableScanIterable vectorizedReader =
new VectorizedTableScanIterable(table.newScan(), 1024, false)) {
for (ColumnarBatch batch : vectorizedReader) {
VectorSchemaRoot root = batch.createVectorSchemaRootFromVectors();
FieldVector vector = root.getVector("col");
assertThat(vector)
.as("Vector should be IntVector matching the physical Parquet type")
.isInstanceOf(IntVector.class);
IntVector intVector = (IntVector) vector;
for (int i = 0; i < root.getRowCount(); i++) {
assertThat(intVector.get(i))
.as("Row %d value should be read correctly", rowIndex)
.isEqualTo(expectedValues[rowIndex]);
rowIndex++;
}
totalRows += root.getRowCount();
root.close();
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

My comments for the other test apply here too.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I miss a test case where the table is written with values larger than Integer.MAX_VALUE after the type promotion and reuseContainers is true for the table scan .

Comment on lines 1153 to 1156
// Perform a type promotion
// TODO: The read Arrow vector should of type BigInt (promoted type) but it is Int (old type).
Table tableLatest = tables.load(tableLocation);
tableLatest.updateSchema().updateColumn("int_promotion", Types.LongType.get()).commit();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This type promotion is now tested separately and the TODO is not true anymore. So these lines are with the "int_promotion" column can be deleted.

…motion with INT logical type

Fix ClassCastException: BigIntVector cannot be cast to IntVector when reading
Parquet files with INT(32, true) logical type annotation after promoting a
column from int to long.

The vectorized reader's LogicalTypeVisitor now allocates vectors based on the
Parquet physical type instead of deriving them from the (potentially promoted)
Iceberg schema type.

Root Cause:
In VectorizedArrowReader.allocateFieldVector(), the Arrow field was created
from the Iceberg schema type (which reflects the promoted LongType), producing
a BigIntVector. The LogicalTypeVisitor then cast this vector to IntVector based
on the Parquet file's INT(32) logical type, causing the mismatch.

The non-vectorized reader (BaseParquetReaders) already handles this correctly
by checking the expected Iceberg type and using IntAsLongReader for promotion.
The vectorized reader relies on the accessor layer for widening
(IntAccessor.getLong() widens int to long), so the fix ensures the vector
matches the physical data layout.

Tests:
- testIntToLongPromotionWithLogicalType: verifies reading after promotion when
  file has INT(32, true) annotation (the reported crash)
- testIntToLongPromotionWithoutLogicalType: verifies reading after promotion
  when file has bare INT32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vectorized reader throws ClassCastException on int-to-long promotion when Parquet file has INT(32) logical type annotation

3 participants