Skip to content

Commit 4cd30d7

Browse files
adriangbclaude
andcommitted
fix(parquet): no scan equivalences from optional conjuncts
`ParquetSource::exact_filter` (#25780) returns the conjuncts that every output row satisfies. An optional conjunct is not one of them: the scan does not evaluate it after the decode (this PR), and it drops it when the `RowFilter` cannot evaluate it. `exact_filter` now skips optional conjuncts, so that the scan claims no equivalence from them. Test: `exact_filter_excludes_optional_conjuncts` (failed before this change: `a@0 = 1 AND Optional(b@1 = 2)` with `pushdown_filters = false`). PR: #25722 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 07096a0 commit 4cd30d7

1 file changed

Lines changed: 52 additions & 1 deletion

File tree

‎datafusion/datasource-parquet/src/source.rs‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use datafusion_datasource::file_scan_config::FileScanConfig;
5050
use datafusion_functions::core::file_row_index::FileRowIndexFunc;
5151
use datafusion_physical_expr::expressions::{Column, DynamicFilterTracking};
5252
use datafusion_physical_expr::projection::ProjectionExprs;
53-
use datafusion_physical_expr::utils::split_conjunction;
53+
use datafusion_physical_expr::utils::{is_optional_filter, split_conjunction};
5454
use datafusion_physical_expr::{EquivalenceProperties, conjunction};
5555
use datafusion_physical_expr_adapter::DefaultPhysicalExprAdapterFactory;
5656
use datafusion_physical_expr_adapter::rewrite::{
@@ -719,6 +719,10 @@ impl FileSource for ParquetSource {
719719
/// A pruning-only predicate (see
720720
/// [`FileSource::try_pushdown_pruning_filters`]) is used only to prune:
721721
/// a `FilterExec` above the scan applies it. Thus it is not exact.
722+
///
723+
/// Optional conjuncts (see `split_optional`) are not exact either: the
724+
/// scan does not evaluate them after the decode, and it drops them when
725+
/// the `RowFilter` cannot evaluate them.
722726
fn exact_filter(&self) -> Option<Arc<dyn PhysicalExpr>> {
723727
if self.pruning_only_predicate {
724728
return None;
@@ -727,6 +731,7 @@ impl FileSource for ParquetSource {
727731
let pushable_schema = self.table_schema.schema_without_virtual_columns();
728732
let exact = split_conjunction(predicate)
729733
.into_iter()
734+
.filter(|expr| !is_optional_filter(expr))
730735
.filter(|expr| can_expr_be_pushed_down_with_schemas(expr, pushable_schema))
731736
.cloned()
732737
.collect::<Vec<_>>();
@@ -2253,6 +2258,52 @@ mod tests {
22532258
);
22542259
}
22552260

2261+
/// An optional conjunct is not exact: the scan does not evaluate it
2262+
/// after the decode, and drops it when the `RowFilter` cannot evaluate
2263+
/// it. Thus it must not give equivalences.
2264+
#[test]
2265+
fn exact_filter_excludes_optional_conjuncts() {
2266+
use arrow::datatypes::{DataType, Field, Schema};
2267+
use datafusion_common::config::ConfigOptions;
2268+
use datafusion_expr::{col, lit as logical_lit};
2269+
use datafusion_physical_expr::expressions::OptionalFilterPhysicalExpr;
2270+
use datafusion_physical_expr::planner::logical2physical;
2271+
2272+
let schema = Arc::new(Schema::new(vec![
2273+
Field::new("a", DataType::Int64, false),
2274+
Field::new("b", DataType::Int64, false),
2275+
]));
2276+
let required = logical2physical(&col("a").eq(logical_lit(1i64)), &schema);
2277+
let optional: Arc<dyn PhysicalExpr> = Arc::new(OptionalFilterPhysicalExpr::new(
2278+
logical2physical(&col("b").eq(logical_lit(2i64)), &schema),
2279+
));
2280+
for pushdown_filters in [false, true] {
2281+
let mut config = ConfigOptions::default();
2282+
config.execution.parquet.pushdown_filters = pushdown_filters;
2283+
let prop = ParquetSource::new(Arc::clone(&schema))
2284+
.try_pushdown_filters(
2285+
vec![Arc::clone(&required), Arc::clone(&optional)],
2286+
&config,
2287+
)
2288+
.unwrap();
2289+
let source = prop.updated_node.unwrap();
2290+
let source = source.downcast_ref::<ParquetSource>().unwrap();
2291+
assert_eq!(
2292+
source.exact_filter().unwrap().to_string(),
2293+
"a@0 = 1",
2294+
"pushdown_filters = {pushdown_filters}"
2295+
);
2296+
}
2297+
2298+
// A predicate with only optional conjuncts is not exact.
2299+
let prop = ParquetSource::new(Arc::clone(&schema))
2300+
.try_pushdown_filters(vec![Arc::clone(&optional)], &ConfigOptions::default())
2301+
.unwrap();
2302+
let source = prop.updated_node.unwrap();
2303+
let source = source.downcast_ref::<ParquetSource>().unwrap();
2304+
assert!(source.exact_filter().is_none());
2305+
}
2306+
22562307
#[test]
22572308
fn test_try_pushdown_filters_rejects_virtual_column_refs() {
22582309
// Virtual columns are produced by the reader and cannot be referenced

0 commit comments

Comments
 (0)