This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new b3af0c0 fix(scan): prune OR predicates using stats (#426)
b3af0c0 is described below
commit b3af0c05554a6aab895d05f8bdc9ad5bc6d6bfb4
Author: QuakeWang <[email protected]>
AuthorDate: Wed Jul 1 14:55:39 2026 +0800
fix(scan): prune OR predicates using stats (#426)
---
crates/integration_tests/tests/read_tables.rs | 9 +--
crates/paimon/src/predicate_stats.rs | 5 +-
crates/paimon/src/table/stats_filter.rs | 11 ++-
crates/paimon/src/table/table_scan.rs | 97 ++++++++++++++++++++++++++-
4 files changed, 114 insertions(+), 8 deletions(-)
diff --git a/crates/integration_tests/tests/read_tables.rs
b/crates/integration_tests/tests/read_tables.rs
index e03a123..ac45170 100644
--- a/crates/integration_tests/tests/read_tables.rs
+++ b/crates/integration_tests/tests/read_tables.rs
@@ -752,7 +752,7 @@ async fn
test_read_multi_partitioned_table_or_of_mixed_ands_prunes_partitions()
Predicate::and(vec![
pb.equal("dt", Datum::String("2024-01-01".into())).unwrap(),
pb.equal("hr", Datum::Int(10)).unwrap(),
- pb.greater_than("id", Datum::Int(10)).unwrap(),
+ pb.greater_than("id", Datum::Int(0)).unwrap(),
]),
Predicate::and(vec![
pb.equal("dt", Datum::String("2024-01-01".into())).unwrap(),
@@ -780,8 +780,9 @@ async fn
test_read_multi_partitioned_table_or_of_mixed_ands_prunes_partitions()
);
}
-/// A directly mixed OR like `dt = '...' OR id > 10` is still not safely
-/// splittable into a partition predicate, so no partitions should be pruned.
+/// A directly mixed OR like `dt = '...' OR id > 0` is still not safely
+/// splittable into a partition predicate. The data predicate branch may match
+/// all provisioned files, so this isolates partition projection behavior.
#[tokio::test]
async fn test_read_partitioned_table_mixed_or_filter_preserves_all() {
use paimon::spec::{Datum, Predicate, PredicateBuilder};
@@ -793,7 +794,7 @@ async fn
test_read_partitioned_table_mixed_or_filter_preserves_all() {
let filter = Predicate::or(vec![
pb.equal("dt", Datum::String("2024-01-01".into())).unwrap(),
- pb.greater_than("id", Datum::Int(10)).unwrap(),
+ pb.greater_than("id", Datum::Int(0)).unwrap(),
]);
let (plan, batches) = scan_and_read_with_filter(&table, filter).await;
diff --git a/crates/paimon/src/predicate_stats.rs
b/crates/paimon/src/predicate_stats.rs
index e0efd67..fc1a79a 100644
--- a/crates/paimon/src/predicate_stats.rs
+++ b/crates/paimon/src/predicate_stats.rs
@@ -300,7 +300,10 @@ fn predicate_may_match_with_schema<T: StatsAccessor>(
Predicate::And(children) => children
.iter()
.all(|child| predicate_may_match_with_schema(child, stats,
field_mapping, file_fields)),
- Predicate::Or(_) | Predicate::Not(_) => true,
+ Predicate::Or(children) => children
+ .iter()
+ .any(|child| predicate_may_match_with_schema(child, stats,
field_mapping, file_fields)),
+ Predicate::Not(_) => true,
Predicate::Leaf {
index,
data_type,
diff --git a/crates/paimon/src/table/stats_filter.rs
b/crates/paimon/src/table/stats_filter.rs
index 68a4d65..999213d 100644
--- a/crates/paimon/src/table/stats_filter.rs
+++ b/crates/paimon/src/table/stats_filter.rs
@@ -375,7 +375,16 @@ fn data_evolution_predicate_may_match(
row_count,
)
}),
- Predicate::Or(_) | Predicate::Not(_) => true,
+ Predicate::Or(children) => children.iter().any(|child| {
+ data_evolution_predicate_may_match(
+ child,
+ table_fields,
+ field_sources,
+ file_stats,
+ row_count,
+ )
+ }),
+ Predicate::Not(_) => true,
Predicate::Leaf {
index,
data_type,
diff --git a/crates/paimon/src/table/table_scan.rs
b/crates/paimon/src/table/table_scan.rs
index bd847d1..27bc8d8 100644
--- a/crates/paimon/src/table/table_scan.rs
+++ b/crates/paimon/src/table/table_scan.rs
@@ -843,7 +843,10 @@ mod tests {
use crate::table::bucket_filter::{compute_target_buckets,
extract_predicate_for_keys};
use crate::table::partition_filter::PartitionFilter;
use crate::table::source::{DataSplit, DataSplitBuilder, DeletionFile};
- use crate::table::stats_filter::{data_file_matches_predicates,
group_by_overlapping_row_id};
+ use crate::table::stats_filter::{
+ data_evolution_group_matches_predicates, data_file_matches_predicates,
+ group_by_overlapping_row_id,
+ };
use crate::table::Table;
use crate::Error;
use chrono::{DateTime, Utc};
@@ -1327,7 +1330,7 @@ mod tests {
}
#[test]
- fn test_data_file_matches_unsupported_predicate_fails_open() {
+ fn test_data_file_matches_or_prunes_when_no_child_matches() {
let fields = int_field();
let file = test_data_file_meta(
int_stats_row(Some(10)),
@@ -1341,6 +1344,29 @@ mod tests {
pb.greater_than("id", Datum::Int(25)).unwrap(),
]);
+ assert!(!data_file_matches_predicates(
+ &file,
+ &[predicate],
+ TEST_SCHEMA_ID,
+ &test_schema_fields(),
+ ));
+ }
+
+ #[test]
+ fn test_data_file_matches_or_keeps_when_any_child_matches() {
+ let fields = int_field();
+ let file = test_data_file_meta(
+ int_stats_row(Some(10)),
+ int_stats_row(Some(20)),
+ vec![Some(0)],
+ 5,
+ );
+ let pb = PredicateBuilder::new(&fields);
+ let predicate = Predicate::or(vec![
+ pb.less_than("id", Datum::Int(15)).unwrap(),
+ pb.greater_than("id", Datum::Int(25)).unwrap(),
+ ]);
+
assert!(data_file_matches_predicates(
&file,
&[predicate],
@@ -1349,6 +1375,73 @@ mod tests {
));
}
+ #[test]
+ fn test_data_file_matches_not_fails_open() {
+ let fields = int_field();
+ let file = test_data_file_meta(
+ int_stats_row(Some(10)),
+ int_stats_row(Some(20)),
+ vec![Some(0)],
+ 5,
+ );
+ let predicate = Predicate::negate(
+ PredicateBuilder::new(&fields)
+ .less_than("id", Datum::Int(5))
+ .unwrap(),
+ );
+
+ assert!(data_file_matches_predicates(
+ &file,
+ &[predicate],
+ TEST_SCHEMA_ID,
+ &test_schema_fields(),
+ ));
+ }
+
+ #[test]
+ fn test_data_evolution_group_matches_or_prunes_when_no_child_matches() {
+ let fields = int_field();
+ let file = test_data_file_meta(
+ int_stats_row(Some(10)),
+ int_stats_row(Some(20)),
+ vec![Some(0)],
+ 5,
+ );
+ let pb = PredicateBuilder::new(&fields);
+ let predicate = Predicate::or(vec![
+ pb.less_than("id", Datum::Int(5)).unwrap(),
+ pb.greater_than("id", Datum::Int(25)).unwrap(),
+ ]);
+
+ assert!(!data_evolution_group_matches_predicates(
+ &[file],
+ &[predicate],
+ &fields,
+ ));
+ }
+
+ #[test]
+ fn test_data_evolution_group_matches_or_keeps_when_any_child_matches() {
+ let fields = int_field();
+ let file = test_data_file_meta(
+ int_stats_row(Some(10)),
+ int_stats_row(Some(20)),
+ vec![Some(0)],
+ 5,
+ );
+ let pb = PredicateBuilder::new(&fields);
+ let predicate = Predicate::or(vec![
+ pb.less_than("id", Datum::Int(15)).unwrap(),
+ pb.greater_than("id", Datum::Int(25)).unwrap(),
+ ]);
+
+ assert!(data_evolution_group_matches_predicates(
+ &[file],
+ &[predicate],
+ &fields,
+ ));
+ }
+
#[test]
fn test_data_file_matches_corrupt_stats_fails_open() {
let fields = int_field();