This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 5faefa9bf0 fix(parquet): keep virtual columns in the schema reported
with a schema hint (#11047)
5faefa9bf0 is described below
commit 5faefa9bf022471856b7a3e0413a743e53980d8c
Author: Bharadwaj Pendyala <[email protected]>
AuthorDate: Mon Sep 21 04:40:02 2026 -0500
fix(parquet): keep virtual columns in the schema reported with a schema
hint (#11047)
# Which issue does this PR close?
- Closes #11046.
# Rationale for this change
`ArrowReaderMetadata::with_supplied_schema` already treats virtual
columns as extra fields on top of the hint. It passes them to
`parquet_to_arrow_field_levels_with_virtual`, and its own length check
reads `supplied_schema.fields().len() + virtual_columns.len()`. Then it
returns `schema: supplied_schema`, which doesn't have them.
So the two branches of `try_new` disagree. Without a hint you get a
schema with the virtual fields in it; add a hint and they vanish, even
though the reader still decodes them. On the file from the issue that
means `metadata.schema()` reports one field while every batch that
metadata produces has two:
```
metadata.schema().fields() [value]
batch.schema().fields() [value, row_number]
```
@limenilbuz asked for either of two behaviours: include the virtual
columns in the reported schema, or stop erroring when the hint itself
contains them. This does the first. The second is a change to what
`with_schema` accepts, and the length check here already assumes virtual
fields live outside the hint, so the first is the one that makes the
function agree with itself.
# What changes are included in this PR?
The returned schema is now the supplied fields followed by the virtual
fields, keeping the supplied schema's key/value metadata. When no
virtual columns are requested the supplied schema is returned untouched,
so nothing changes for that path.
`parquet_to_arrow_field_levels_with_virtual` appends virtual columns to
the root in the order given and clones them unchanged
(`parquet/src/arrow/schema/mod.rs:223`), so appending them here in the
same order lines the reported schema up with `field_levels`.
# Are these changes tested?
Yes. `test_supplied_schema_keeps_virtual_columns` builds metadata from a
hint plus two virtual fields and checks the field order, that the hint's
schema metadata survives, and that `metadata.schema()` agrees with the
fields of the batch the reader emits. It fails on `f9e02ba` with:
```
left: [Field { name: "value", data_type: Int64 }]
right: [Field { name: "value", .. }, Field { name: "row_number", .. },
Field { name: "row_group_index", .. }]
```
`cargo test -p parquet --lib` is 1381 passed, 0 failed. `cargo fmt --all
-- --check` and `cargo clippy -p parquet --all-targets` are both clean.
# Are there any user-facing changes?
`ArrowReaderMetadata::schema()`, and the builder schema derived from it,
gain the virtual fields when a hint and virtual columns are combined.
That's the fix, but it is a field-count change on a public accessor, so
it's worth calling out. Physical column indices are unaffected and no
crate in the tree combines those two options.
One thing I left alone: with an explicit projection the async reader's
`schema()` drops virtual fields while the batches still carry them. That
reproduces with and without a schema hint, so it's a separate bug from
this one and I didn't touch it here.
---
parquet/src/arrow/arrow_reader/mod.rs | 13 +++++-
.../arrow/arrow_reader/tests/virtual_columns.rs | 46 ++++++++++++++++++++++
2 files changed, 58 insertions(+), 1 deletion(-)
diff --git a/parquet/src/arrow/arrow_reader/mod.rs
b/parquet/src/arrow/arrow_reader/mod.rs
index 5736ff7c01..73bcb2048a 100644
--- a/parquet/src/arrow/arrow_reader/mod.rs
+++ b/parquet/src/arrow/arrow_reader/mod.rs
@@ -1127,9 +1127,20 @@ impl ArrowReaderMetadata {
)));
}
+ // `fields` is the supplied fields followed by the virtual columns, so
the reported
+ // schema has to be built from it or the virtual columns go missing.
+ let schema = if virtual_columns.is_empty() {
+ supplied_schema
+ } else {
+ Arc::new(Schema::new_with_metadata(
+ fields,
+ supplied_schema.metadata().clone(),
+ ))
+ };
+
Ok(Self {
metadata,
- schema: supplied_schema,
+ schema,
fields: field_levels.levels.map(Arc::new),
})
}
diff --git a/parquet/src/arrow/arrow_reader/tests/virtual_columns.rs
b/parquet/src/arrow/arrow_reader/tests/virtual_columns.rs
index 9dbdcae6f9..c46a1b89a3 100644
--- a/parquet/src/arrow/arrow_reader/tests/virtual_columns.rs
+++ b/parquet/src/arrow/arrow_reader/tests/virtual_columns.rs
@@ -18,6 +18,7 @@
//! Generated row numbers and row-group indices, including ordering and
filtering.
use super::*;
+use std::collections::HashMap;
#[test]
fn test_read_row_numbers() {
@@ -68,6 +69,51 @@ fn test_read_row_numbers() {
);
}
+#[test]
+fn test_supplied_schema_keeps_virtual_columns() {
+ let file = write_parquet_from_iter(vec![(
+ "value",
+ Arc::new(Int64Array::from(vec![1, 2, 3])) as ArrayRef,
+ )]);
+ let supplied_fields = Fields::from(vec![Field::new("value",
ArrowDataType::Int64, false)]);
+ let row_number_field = Arc::new(
+ Field::new("row_number", ArrowDataType::Int64,
false).with_extension_type(RowNumber),
+ );
+ let row_group_index_field = Arc::new(
+ Field::new("row_group_index", ArrowDataType::Int64, false)
+ .with_extension_type(RowGroupIndex),
+ );
+ let supplied_metadata = HashMap::from([("k".to_string(),
"v".to_string())]);
+
+ let options = ArrowReaderOptions::new()
+ .with_schema(Arc::new(Schema::new_with_metadata(
+ supplied_fields,
+ supplied_metadata.clone(),
+ )))
+ .with_virtual_columns(vec![
+ row_number_field.clone(),
+ row_group_index_field.clone(),
+ ])
+ .unwrap();
+ let metadata = ArrowReaderMetadata::load(&file, options).unwrap();
+
+ let expected = Fields::from(vec![
+ Arc::new(Field::new("value", ArrowDataType::Int64, false)),
+ row_number_field,
+ row_group_index_field,
+ ]);
+ assert_eq!(metadata.schema().fields(), &expected);
+ assert_eq!(metadata.schema().metadata(), &supplied_metadata);
+
+ let batch = ParquetRecordBatchReaderBuilder::new_with_metadata(file,
metadata.clone())
+ .build()
+ .unwrap()
+ .next()
+ .unwrap()
+ .unwrap();
+ assert_eq!(batch.schema().fields(), metadata.schema().fields());
+}
+
#[test]
fn test_read_only_row_numbers() {
let file = write_parquet_from_iter(vec![(