Arrow: Fix ClassCastException in vectorized reader on int-to-long pro…#16343
Arrow: Fix ClassCastException in vectorized reader on int-to-long pro…#16343xndai wants to merge 1 commit into
Conversation
CTTY
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
why do we remove this comment? this still looks relevant
There was a problem hiding this comment.
I thought the check below was self explanatory. I add them back.
| 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++; | ||
| } |
There was a problem hiding this comment.
Testing if the accessor gives back the values correctly is missing
| 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)); | |
| } |
| int totalRows = 0; | ||
| int rowIndex = 0; |
There was a problem hiding this comment.
If the test data is so small and batch size so large no need for tracking the rows
| 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(); | ||
| } | ||
| } |
There was a problem hiding this comment.
My comments for the other test apply here too.
There was a problem hiding this comment.
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 .
| // 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(); |
There was a problem hiding this comment.
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
…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:
Fixes #16341