Skip to content

Revert use file schema in parquet pruning - #16086

Merged
alamb merged 6 commits into
apache:mainfrom
pydantic:revert-use-file-schema
May 21, 2025
Merged

Revert use file schema in parquet pruning#16086
alamb merged 6 commits into
apache:mainfrom
pydantic:revert-use-file-schema

Conversation

@adriangb

@adriangbadriangb commented May 19, 2025

Copy link
Copy Markdown
Contributor

@github-actionsgithub-actionsBot added optimizer Optimizer rules core Core DataFusion crate datasource Changes to the datasource crate labels May 19, 2025
Comment on lines +900 to +910
// TODO: this is failing on main, and has been for a long time!
// See <comment on PR>
// // Predicate should prune no row groups
// let filter = col("c1").eq(lit(ScalarValue::UInt64(Some(1))));
// let read = RoundTrip::new()
// .with_predicate(filter)
// .with_schema(schema)
// .round_trip_to_batches(vec![batch])
// .await
// .unwrap();
// assert_eq!(read.len(), 1);

@adriangbadriangbMay 19, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This has been failing at least as far back as v46.0.0 with this diff:

diff --git a/datafusion/core/src/datasource/file_format/parquet.rs b/datafusion/core/src/datasource/file_format/parquet.rs
index 3b71593b3..60b403aff 100644
--- a/datafusion/core/src/datasource/file_format/parquet.rs+++ b/datafusion/core/src/datasource/file_format/parquet.rs@@ -67,13 +67,14 @@ pub(crate) mod test_util {
.into_iter()
.zip(tmp_files.into_iter())
.map(|(batch, mut output)| {
- let builder = parquet::file::properties::WriterProperties::builder();- let props = if multi_page {+ let mut builder = parquet::file::properties::WriterProperties::builder();+ builder = if multi_page {
builder.set_data_page_row_count_limit(ROWS_PER_PAGE)
} else {
builder
- }- .build();+ };+ builder = builder.set_bloom_filter_enabled(true);+ let props = builder.build();
let mut writer = parquet::arrow::ArrowWriter::try_new(
&mut output,
diff --git a/datafusion/core/src/datasource/physical_plan/parquet.rs b/datafusion/core/src/datasource/physical_plan/parquet.rs
index 888f3ad9e..240d84783 100644
--- a/datafusion/core/src/datasource/physical_plan/parquet.rs+++ b/datafusion/core/src/datasource/physical_plan/parquet.rs@@ -489,6 +489,37 @@ mod tests {
assert_eq!(read.len(), 0);
}
++ #[tokio::test]+ async fn evolved_schema_column_type_filter_ints() {+ // The table and filter have a common data type, but the file schema differs+ let c1: ArrayRef = Arc::new(Int8Array::from(vec![Some(1), Some(2)]));+ let batch = create_batch(vec![("c1", c1.clone())]);++ let schema =+ Arc::new(Schema::new(vec![Field::new("c1", DataType::UInt64, false)]));++ // // Predicate should prune all row groups+ // let filter = col("c1").eq(lit(ScalarValue::UInt64(Some(5))));+ // let read = RoundTrip::new()+ // .with_predicate(filter)+ // .with_schema(schema.clone())+ // .round_trip_to_batches(vec![batch.clone()])+ // .await+ // .unwrap();+ // assert_eq!(read.len(), 0);++ // Predicate should prune no row groups+ let filter = col("c1").eq(lit(ScalarValue::UInt64(Some(1))));+ let read = RoundTrip::new()+ .with_predicate(filter)+ .with_schema(schema)+ .round_trip_to_batches(vec![batch])+ .await+ .unwrap();+ assert_eq!(read.len(), 1);+ }+
#[tokio::test]
async fn evolved_schema_disjoint_schema_filter() {
let c1: ArrayRef =

Comment threaddatafusion/core/src/datasource/physical_plan/parquet.rs Outdated
@xudong963
xudong963 self-requested a review May 19, 2025 06:26
@github-actionsgithub-actionsBot removed the optimizer Optimizer rules label May 19, 2025
@xudong963

Copy link
Copy Markdown
Member

Thank you @adriangb , I'll review the PR tomorrow carefully and try to verify if this is related to the issue we encountered during upgrading DF47

Comment on lines +165 to 167
} else {
source = source.with_pushdown_filters(false);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is just to be explicit

Comment on lines +175 to 179
if self.bloom_filters {
source = source.with_bloom_filter_on_read(true);
} else {
source = source.with_bloom_filter_on_read(false);
}

@adriangbadriangbMay 19, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Otherwise bloom filters will pick up the slack and do pruning even if the types mismatch and stats / page / row pruning fails (bloom filters use &str for both Utf8 and Utf8View so they don't care). Only one of the tests actually relies on bloom filters.

@adriangb

Copy link
Copy Markdown
ContributorAuthor

Thank you @adriangb , I'll review the PR tomorrow carefully and try to verify if this is related to the issue we encountered during upgrading DF47

Thank you 🙏🏻 and sorry for any inconvenience this has caused!

@adriangb
adriangbforce-pushed the revert-use-file-schema branch from 348760b to 455582bCompareMay 19, 2025 20:38

@alambalamb left a comment

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.

Thank you @adriangb and @xudong963

pub predicate: Option<Arc<dyn PhysicalExpr>>,
/// Schema of the output table
pub table_schema: SchemaRef,
/// Schema of the output table without partition columns.

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.

I verified that when the code changes are reverted this test fails:

cargo test --all-features -p datafusion -- parquet
...
---- datasource::physical_plan::parquet::tests::evolved_schema_column_type_filter_ints stdout ----
thread 'datasource::physical_plan::parquet::tests::evolved_schema_column_type_filter_ints' panicked at datafusion/core/src/datasource/physical_plan/parquet.rs:927:9:
assertion `left == right` failed
left: 1
right: 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
---- datasource::physical_plan::parquet::tests::evolved_schema_column_type_filter_strings stdout ----
thread 'datasource::physical_plan::parquet::tests::evolved_schema_column_type_filter_strings' panicked at datafusion/core/src/datasource/physical_plan/parquet.rs:885:9:
assertion `left == right` failed
left: 1
right: 0
failures:
datasource::physical_plan::parquet::tests::evolved_schema_column_type_filter_ints
datasource::physical_plan::parquet::tests::evolved_schema_column_type_filter_strings

@xudong963xudong963May 21, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think they're related to the PR: #16133

Sorry, I misread @alamb 's message

let (pruning_predicate, page_pruning_predicate) = build_pruning_predicates(
predicate.as_ref(),
&physical_file_schema,
&logical_file_schema,

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.

This is the actual change (from physical to logical schema) -- I am calling this out because it took me a while to spot it (at first I thought this was only a name change)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes sorry for also changing the names in this PR - it was just too confusing to even work on this PR without giving things a new name. Let me know if you have any suggestions for better names, happy to accept them, naming here seems to be 1/2 of the problem. You know what they say, there's 3 hard problems...

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.

no worries -- I think the new names are much clearer to me

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.

Sorry, coming to this too late, but I'm curious why the logical schema? To me pruning implies a physical layer kind of thing. I ask because now I'm having issues in #15821 trying to build pruning predicates for columns that don't exist in the physical schema.

This may just mean #15821 is operating at too low a level and needs to operate up here where we a) know it's parquet and b) have the physical schema.

@alamb@adriangb

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I would like it to operate at the physical layer and maybe we can get back there through future work.

My immediate goal merging this PR was to fix the pretty bad regression I had introduced. A step backwards to avoid running into a wall.

But I also am not sure that we can even make it operate at the physical layer properly: there's a lot of logic that goes into determining what casts to do and such that happens at the logical layer. We'd have to re-create all of that at the physical layer because if the types of the columns change everything has to be re-evaluated.

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.

I think the key observation is that the predicates are expressed in terms of the table schema (not the file schema).

Today there is a bunch of logic to translate table schema -> file schema for data and statistics.

So in order to evaluate predicates on the physical schema we would need to translate the expression as well, which I think is what @adriangb is saying above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep precisely. And that requires re evaluating cast rules, etc which is... not easy.

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.

Ok, thanks for the explanation!

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.

let schema = Arc::new(Schema::new(vec![Field::new("c1", DataType::Utf8, false)]));

// Predicate should prune all row groups
let filter = col("c1").eq(lit(ScalarValue::Utf8(Some("aaa".to_string()))));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the filter should be constructed by Utf8View, it's a logical expr.

let filter = col("c1").eq(lit(ScalarValue::Utf8View(Some("aaa".to_string()))));

Then, the test will fail because https://github.com/apache/datafusion/pull/16086/files#diff-c8eebe4fb9ee7662170c6ce507ad2be92c0229b3a91a2f4431a9bfc7185a0eb9L148 uses file_schema to transfer logical expr to physical expr. And #16133 will fix it

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.

Upon more reflection, I think the predicate needs to be created using the logical schema today

@adriangb@etseidl and myself are discussing potentially changing that here:

@xudong963xudong963 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you @adriangb

Your test evolved_schema_column_type_filter_strings is close to my real case, my real case is that file_schema is view but table_schame is utf8.

I constructed a similar test that matches my cases:

let c1:ArrayRef =
Arc::new(StringArray::from(vec![Some("foo"),Some("bar")]));let batch = create_batch(vec![("c1", c1.clone())]);let schema = Arc::new(Schema::new(vec![Field::new("c1",DataType::Utf8View,false)]));// Predicate should prune all row groupslet filter = col("c1").eq(lit(ScalarValue::Utf8(Some("aaa".to_string()))));

With the fix: #16133 (Why with the fix? The above test I posted is using table schema to build filter), I tested the test in branch-46, branch-47, main and your branch:
branch-46: ✅
branch-47: ❌
main: ❌
your branch: ✅

@alamb
alamb merged commit ca55f1c into apache:mainMay 21, 2025
@alamb

Copy link
Copy Markdown
Contributor

Thanks again @xudong963 and @adriangb

@xudong963

Copy link
Copy Markdown
Member

Fyi, we've upgraded to DF47 with the fix successfully.

@adriangb

Copy link
Copy Markdown
ContributorAuthor

Great to hear! Sorry for the inconvenience...

@xudong963

Copy link
Copy Markdown
Member

Great to hear! Sorry for the inconvenience...

No problem!

@alamb

Copy link
Copy Markdown
Contributor

FWIW @phillipleblanc also hit this as well in SpiceAI. See this for more details

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coreCore DataFusion cratedatasourceChanges to the datasource crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parquet predicate filters fail with "Invalid comparison operation: Utf8View <= Utf8"

4 participants

@adriangb@xudong963@alamb@etseidl