Skip to main content

datafusion_physical_plan/
filter.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::collections::hash_map::Entry;
19use std::collections::{HashMap, HashSet};
20use std::pin::Pin;
21use std::sync::Arc;
22use std::task::{Context, Poll, ready};
23
24use datafusion_physical_expr::projection::{ProjectionRef, combine_projections};
25use itertools::Itertools;
26
27use super::{
28    ColumnStatistics, DisplayAs, ExecutionPlanProperties, PlanProperties,
29    RecordBatchStream, SendableRecordBatchStream, Statistics,
30};
31use crate::coalesce::{LimitedBatchCoalescer, PushBatchStatus};
32use crate::common::can_project;
33use crate::execution_plan::{CardinalityEffect, replace_children_if_necessary};
34use crate::filter_pushdown::{
35    ChildFilterDescription, ChildPushdownResult, FilterDescription, FilterPushdownPhase,
36    FilterPushdownPropagation, PushedDown,
37};
38use crate::limit::LocalLimitExec;
39use crate::metrics::{MetricBuilder, MetricType};
40use crate::projection::{
41    EmbeddedProjection, ProjectionExec, ProjectionExpr, make_with_child,
42    try_embed_projection, update_expr,
43};
44use crate::statistics::{ChildStats, StatisticsArgs, StatisticsContext};
45use crate::stream::EmptyRecordBatchStream;
46use crate::{ChildrenPropertiesMode, ReplaceChildrenOptions, validate_child_count};
47use crate::{
48    DisplayFormatType, ExecutionPlan,
49    metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet, RatioMetrics},
50};
51
52use arrow::compute::filter_record_batch;
53use arrow::datatypes::{DataType, SchemaRef};
54use arrow::record_batch::RecordBatch;
55use datafusion_common::cast::as_boolean_array;
56use datafusion_common::config::ConfigOptions;
57use datafusion_common::stats::Precision;
58use datafusion_common::tree_node::TreeNodeRecursion;
59use datafusion_common::{
60    DataFusionError, Result, ScalarValue, internal_err, plan_err, project_schema,
61};
62use datafusion_execution::TaskContext;
63use datafusion_expr::Operator;
64use datafusion_physical_expr::equivalence::ProjectionMapping;
65use datafusion_physical_expr::expressions::{
66    BinaryExpr, Column, IsNotNullExpr, Literal, lit,
67};
68use datafusion_physical_expr::intervals::utils::check_support;
69use datafusion_physical_expr::utils::{collect_columns, reassign_expr_columns};
70use datafusion_physical_expr::{
71    AcrossPartitions, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr, analyze,
72    conjunction, split_conjunction,
73};
74
75use datafusion_physical_expr_common::physical_expr::fmt_sql;
76use futures::stream::{Stream, StreamExt};
77use log::trace;
78
79const FILTER_EXEC_DEFAULT_SELECTIVITY: u8 = 20;
80const FILTER_EXEC_DEFAULT_BATCH_SIZE: usize = 8192;
81
82/// FilterExec evaluates a boolean predicate against all input batches to determine which rows to
83/// include in its output batches.
84#[derive(Debug, Clone)]
85pub struct FilterExec {
86    /// The expression to filter on. This expression must evaluate to a boolean value.
87    predicate: Arc<dyn PhysicalExpr>,
88    /// The input plan
89    input: Arc<dyn ExecutionPlan>,
90    /// Execution metrics
91    metrics: ExecutionPlanMetricsSet,
92    /// Selectivity for statistics. 0 = no rows, 100 = all rows
93    default_selectivity: u8,
94    /// Properties equivalence properties, partitioning, etc.
95    cache: Arc<PlanProperties>,
96    /// The projection indices of the columns in the output schema of join
97    projection: Option<ProjectionRef>,
98    /// Target batch size for output batches
99    batch_size: usize,
100    /// Number of rows to fetch
101    fetch: Option<usize>,
102}
103
104/// Builder for [`FilterExec`] to set optional parameters
105pub struct FilterExecBuilder {
106    predicate: Arc<dyn PhysicalExpr>,
107    input: Arc<dyn ExecutionPlan>,
108    projection: Option<ProjectionRef>,
109    default_selectivity: u8,
110    batch_size: usize,
111    fetch: Option<usize>,
112}
113
114impl FilterExecBuilder {
115    /// Create a new builder with required parameters (predicate and input)
116    pub fn new(predicate: Arc<dyn PhysicalExpr>, input: Arc<dyn ExecutionPlan>) -> Self {
117        Self {
118            predicate,
119            input,
120            projection: None,
121            default_selectivity: FILTER_EXEC_DEFAULT_SELECTIVITY,
122            batch_size: FILTER_EXEC_DEFAULT_BATCH_SIZE,
123            fetch: None,
124        }
125    }
126
127    /// Set the input execution plan
128    pub fn with_input(mut self, input: Arc<dyn ExecutionPlan>) -> Self {
129        self.input = input;
130        self
131    }
132
133    /// Set the predicate expression
134    pub fn with_predicate(mut self, predicate: Arc<dyn PhysicalExpr>) -> Self {
135        self.predicate = predicate;
136        self
137    }
138
139    /// Set the projection, composing with any existing projection.
140    ///
141    /// If a projection is already set, the new projection indices are mapped
142    /// through the existing projection. For example, if the current projection
143    /// is `[0, 2, 3]` and `apply_projection(Some(vec![0, 2]))` is called, the
144    /// resulting projection will be `[0, 3]` (indices 0 and 2 of `[0, 2, 3]`).
145    ///
146    /// If no projection is currently set, the new projection is used directly.
147    /// If `None` is passed, the projection is cleared.
148    pub fn apply_projection(self, projection: Option<Vec<usize>>) -> Result<Self> {
149        let projection = projection.map(Into::into);
150        self.apply_projection_by_ref(projection.as_ref())
151    }
152
153    /// The same as [`Self::apply_projection`] but takes projection shared reference.
154    pub fn apply_projection_by_ref(
155        mut self,
156        projection: Option<&ProjectionRef>,
157    ) -> Result<Self> {
158        // Check if the projection is valid against current output schema
159        can_project(&self.input.schema(), projection.map(AsRef::as_ref))?;
160        self.projection = combine_projections(projection, self.projection.as_ref())?;
161        Ok(self)
162    }
163
164    /// Set the default selectivity
165    pub fn with_default_selectivity(mut self, default_selectivity: u8) -> Self {
166        self.default_selectivity = default_selectivity;
167        self
168    }
169
170    /// Set the batch size
171    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
172        self.batch_size = batch_size;
173        self
174    }
175
176    /// Set the fetch limit
177    pub fn with_fetch(mut self, fetch: Option<usize>) -> Self {
178        self.fetch = fetch;
179        self
180    }
181
182    /// Build the FilterExec, computing properties once with all configured parameters
183    pub fn build(self) -> Result<FilterExec> {
184        // Validate predicate type
185        match self.predicate.data_type(self.input.schema().as_ref())? {
186            DataType::Boolean => {}
187            other => {
188                return plan_err!(
189                    "Filter predicate must return BOOLEAN values, got {other:?}"
190                );
191            }
192        }
193
194        // Validate selectivity
195        if self.default_selectivity > 100 {
196            return plan_err!(
197                "Default filter selectivity value needs to be less than or equal to 100"
198            );
199        }
200
201        // Validate projection if provided
202        can_project(&self.input.schema(), self.projection.as_deref())?;
203
204        // Compute properties once with all parameters
205        let cache = FilterExec::compute_properties(
206            &self.input,
207            &self.predicate,
208            self.default_selectivity,
209            self.projection.as_deref(),
210        )?;
211
212        Ok(FilterExec {
213            predicate: self.predicate,
214            input: self.input,
215            metrics: ExecutionPlanMetricsSet::new(),
216            default_selectivity: self.default_selectivity,
217            cache: Arc::new(cache),
218            projection: self.projection,
219            batch_size: self.batch_size,
220            fetch: self.fetch,
221        })
222    }
223}
224
225impl From<&FilterExec> for FilterExecBuilder {
226    fn from(exec: &FilterExec) -> Self {
227        Self {
228            predicate: Arc::clone(&exec.predicate),
229            input: Arc::clone(&exec.input),
230            projection: exec.projection.clone(),
231            default_selectivity: exec.default_selectivity,
232            batch_size: exec.batch_size,
233            fetch: exec.fetch,
234            // We could cache / copy over PlanProperties
235            // here but that would require invalidating them in FilterExecBuilder::apply_projection, etc.
236            // and currently every call to this method ends up invalidating them anyway.
237            // If useful this can be added in the future as a non-breaking change.
238        }
239    }
240}
241
242impl FilterExec {
243    /// Create a FilterExec on an input using the builder pattern
244    pub fn try_new(
245        predicate: Arc<dyn PhysicalExpr>,
246        input: Arc<dyn ExecutionPlan>,
247    ) -> Result<Self> {
248        FilterExecBuilder::new(predicate, input).build()
249    }
250
251    /// Get a batch size
252    pub fn batch_size(&self) -> usize {
253        self.batch_size
254    }
255
256    /// Set the default selectivity
257    pub fn with_default_selectivity(
258        mut self,
259        default_selectivity: u8,
260    ) -> Result<Self, DataFusionError> {
261        if default_selectivity > 100 {
262            return plan_err!(
263                "Default filter selectivity value needs to be less than or equal to 100"
264            );
265        }
266        self.default_selectivity = default_selectivity;
267        Ok(self)
268    }
269
270    /// Return new instance of [FilterExec] with the given projection.
271    ///
272    /// # Deprecated
273    /// Use [`FilterExecBuilder::apply_projection`] instead
274    #[deprecated(
275        since = "52.0.0",
276        note = "Use FilterExecBuilder::apply_projection instead"
277    )]
278    pub fn with_projection(&self, projection: Option<Vec<usize>>) -> Result<Self> {
279        let builder = FilterExecBuilder::from(self);
280        builder.apply_projection(projection)?.build()
281    }
282
283    /// Set the batch size
284    pub fn with_batch_size(&self, batch_size: usize) -> Result<Self> {
285        Ok(Self {
286            predicate: Arc::clone(&self.predicate),
287            input: Arc::clone(&self.input),
288            metrics: self.metrics.clone(),
289            default_selectivity: self.default_selectivity,
290            cache: Arc::clone(&self.cache),
291            projection: self.projection.clone(),
292            batch_size,
293            fetch: self.fetch,
294        })
295    }
296
297    /// The expression to filter on. This expression must evaluate to a boolean value.
298    pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
299        &self.predicate
300    }
301
302    /// The input plan
303    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
304        &self.input
305    }
306
307    /// The default selectivity
308    pub fn default_selectivity(&self) -> u8 {
309        self.default_selectivity
310    }
311
312    /// Projection
313    pub fn projection(&self) -> &Option<ProjectionRef> {
314        &self.projection
315    }
316
317    /// Calculates `Statistics` for `FilterExec` by applying the filter's
318    /// selectivity (default, or estimated from interval analysis) to the input
319    /// statistics.
320    ///
321    /// The estimated output row count is used to keep the per-column statistics
322    /// consistent with it:
323    /// - null and distinct counts are capped at the estimated row count;
324    /// - byte sizes (per column and total) are scaled by the selectivity, and
325    ///   are an exact zero when the row count is an exact zero;
326    /// - a column constrained to a single value (`col = literal`, or an
327    ///   interval that collapses to one point) gets a distinct count of 1;
328    /// - a column in a null-rejecting conjunct gets a null count of 0.
329    ///
330    /// When interval analysis applies, min/max are also tightened to the
331    /// surviving value range.
332    ///
333    /// A contradictory predicate (e.g. `a = 1 AND a = 2`) yields zero rows and
334    /// empty-column statistics.
335    pub(crate) fn statistics_helper(
336        schema: &SchemaRef,
337        input_stats: Statistics,
338        predicate: &Arc<dyn PhysicalExpr>,
339        default_selectivity: u8,
340    ) -> Result<Statistics> {
341        let (eq_columns, is_infeasible) = collect_equality_columns(predicate);
342
343        let input_num_rows = input_stats.num_rows;
344        let input_total_byte_size = input_stats.total_byte_size;
345
346        let (selectivity, num_rows, column_statistics) = if is_infeasible {
347            // Contradictory predicate: no rows survive. Row-bounded counts are
348            // zero; value statistics are undefined on an empty column.
349            let mut cs = input_stats.to_inexact().column_statistics;
350            for col_stat in &mut cs {
351                col_stat.distinct_count = Precision::Exact(0);
352                col_stat.null_count = Precision::Exact(0);
353                col_stat.min_value = Precision::Absent;
354                col_stat.max_value = Precision::Absent;
355                col_stat.sum_value = Precision::Absent;
356                col_stat.byte_size = Precision::Exact(0);
357            }
358            (0.0, Precision::Exact(0), cs)
359        } else {
360            let null_rejecting_columns = collect_null_rejecting_columns(predicate);
361
362            if check_support(predicate, schema) {
363                let input_analysis_ctx = AnalysisContext::try_from_statistics(
364                    schema,
365                    &input_stats.column_statistics,
366                )?;
367                let analysis_ctx = analyze(predicate, input_analysis_ctx, schema)?;
368                let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
369                let filtered_num_rows =
370                    input_num_rows.with_estimated_selectivity(selectivity);
371                let cs = collect_new_statistics(
372                    schema,
373                    &input_stats.column_statistics,
374                    analysis_ctx.boundaries,
375                    selectivity,
376                    &null_rejecting_columns,
377                    filtered_num_rows,
378                );
379                (selectivity, filtered_num_rows, cs)
380            } else {
381                // Without interval boundaries, use the default selectivity and
382                // apply the row-count constraints that still follow from the
383                // filter predicate.
384                let selectivity = default_selectivity as f64 / 100.0;
385                let filtered_num_rows =
386                    input_num_rows.with_estimated_selectivity(selectivity);
387                let mut cs = input_stats.to_inexact().column_statistics;
388                for (idx, col_stat) in cs.iter_mut().enumerate() {
389                    col_stat.byte_size = scale_byte_size_at_rows(
390                        col_stat.byte_size,
391                        selectivity,
392                        filtered_num_rows,
393                    );
394                    col_stat.null_count = if null_rejecting_columns.contains(&idx) {
395                        Precision::Exact(0)
396                    } else {
397                        cap_at_rows(col_stat.null_count, filtered_num_rows)
398                    };
399                    col_stat.distinct_count = if eq_columns.contains(&idx) {
400                        distinct_count_for_singleton_domain(filtered_num_rows)
401                    } else {
402                        cap_at_rows(col_stat.distinct_count, filtered_num_rows)
403                    };
404                }
405                (selectivity, filtered_num_rows, cs)
406            }
407        };
408
409        let total_byte_size =
410            scale_byte_size_at_rows(input_total_byte_size, selectivity, num_rows);
411
412        Ok(Statistics {
413            num_rows,
414            total_byte_size,
415            column_statistics,
416        })
417    }
418
419    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
420    fn compute_properties(
421        input: &Arc<dyn ExecutionPlan>,
422        predicate: &Arc<dyn PhysicalExpr>,
423        default_selectivity: u8,
424        projection: Option<&[usize]>,
425    ) -> Result<PlanProperties> {
426        // Combine the equal predicates with the input equivalence properties
427        // to construct the equivalence properties:
428        let schema = input.schema();
429        let stats = Self::statistics_helper(
430            &schema,
431            Arc::unwrap_or_clone(
432                StatisticsContext::new()
433                    .compute(input.as_ref(), &StatisticsArgs::new())?,
434            ),
435            predicate,
436            default_selectivity,
437        )?;
438        let mut eq_properties = input.equivalence_properties().clone();
439        let (equal_pairs, _) = collect_columns_from_predicate_inner(predicate);
440        for (lhs, rhs) in equal_pairs {
441            eq_properties.add_equal_conditions(Arc::clone(lhs), Arc::clone(rhs))?
442        }
443        // Add the columns that have only one viable value (singleton) after
444        // filtering to constants.
445        let constants = collect_columns(predicate)
446            .into_iter()
447            .filter(|column| stats.column_statistics[column.index()].is_singleton())
448            .map(|column| {
449                let value = stats.column_statistics[column.index()]
450                    .min_value
451                    .get_value();
452                let expr = Arc::new(column) as _;
453                ConstExpr::new(expr, AcrossPartitions::Uniform(value.cloned()))
454            });
455        // This is for statistics
456        eq_properties.add_constants(constants)?;
457        // This is for logical constant (for example: a = '1', then a could be marked as a constant)
458        // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
459        eq_properties.add_constants(ConstExpr::collect_predicate_constants(
460            input.equivalence_properties(),
461            predicate,
462        ))?;
463
464        let mut output_partitioning = input.output_partitioning().clone();
465        // If contains projection, update the PlanProperties.
466        if let Some(projection) = projection {
467            let schema = eq_properties.schema();
468            let projection_mapping = ProjectionMapping::from_indices(projection, schema)?;
469            let out_schema = project_schema(schema, Some(&projection))?;
470            output_partitioning =
471                output_partitioning.project(&projection_mapping, &eq_properties);
472            eq_properties = eq_properties.project(&projection_mapping, out_schema);
473        }
474
475        Ok(PlanProperties::new(
476            eq_properties,
477            output_partitioning,
478            input.pipeline_behavior(),
479            input.boundedness(),
480        ))
481    }
482}
483
484impl DisplayAs for FilterExec {
485    fn fmt_as(
486        &self,
487        t: DisplayFormatType,
488        f: &mut std::fmt::Formatter,
489    ) -> std::fmt::Result {
490        match t {
491            DisplayFormatType::Default | DisplayFormatType::Verbose => {
492                let display_projections = if let Some(projection) =
493                    self.projection.as_ref()
494                {
495                    format!(
496                        ", projection=[{}]",
497                        projection
498                            .iter()
499                            .map(|index| format!(
500                                "{}@{}",
501                                self.input.schema().fields().get(*index).unwrap().name(),
502                                index
503                            ))
504                            .collect::<Vec<_>>()
505                            .join(", ")
506                    )
507                } else {
508                    "".to_string()
509                };
510                let fetch = self
511                    .fetch
512                    .map_or_else(|| "".to_string(), |f| format!(", fetch={f}"));
513                write!(
514                    f,
515                    "FilterExec: {}{}{}",
516                    self.predicate, display_projections, fetch
517                )
518            }
519            DisplayFormatType::TreeRender => {
520                if let Some(fetch) = self.fetch {
521                    writeln!(f, "fetch={fetch}")?;
522                }
523                write!(f, "predicate={}", fmt_sql(self.predicate.as_ref()))
524            }
525        }
526    }
527}
528
529impl ExecutionPlan for FilterExec {
530    fn name(&self) -> &'static str {
531        "FilterExec"
532    }
533
534    /// Return a reference to Any that can be used for downcasting
535    fn properties(&self) -> &Arc<PlanProperties> {
536        &self.cache
537    }
538
539    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
540        vec![&self.input]
541    }
542
543    fn apply_expressions(
544        &self,
545        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
546    ) -> Result<TreeNodeRecursion> {
547        crate::apply_expression_roots([&self.predicate], f)
548    }
549
550    fn maintains_input_order(&self) -> Vec<bool> {
551        // Tell optimizer this operator doesn't reorder its input
552        vec![true]
553    }
554
555    fn replace_children(
556        self: Arc<Self>,
557        mut children: Vec<Arc<dyn ExecutionPlan>>,
558        options: ReplaceChildrenOptions,
559    ) -> Result<Arc<dyn ExecutionPlan>> {
560        validate_child_count!(self, children);
561        match options.children_properties {
562            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
563                input: children.swap_remove(0),
564                metrics: ExecutionPlanMetricsSet::new(),
565                ..Self::clone(&*self)
566            })),
567            ChildrenPropertiesMode::Recompute => {
568                let new_input = children.swap_remove(0);
569                FilterExecBuilder::from(&*self)
570                    .with_input(new_input)
571                    .build()
572                    .map(|e| Arc::new(e) as _)
573            }
574        }
575    }
576
577    fn with_new_children(
578        self: Arc<Self>,
579        children: Vec<Arc<dyn ExecutionPlan>>,
580    ) -> Result<Arc<dyn ExecutionPlan>> {
581        self.replace_children(
582            children,
583            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
584        )
585    }
586
587    fn with_new_children_and_same_properties(
588        self: Arc<Self>,
589        children: Vec<Arc<dyn ExecutionPlan>>,
590    ) -> Result<Arc<dyn ExecutionPlan>> {
591        self.replace_children(
592            children,
593            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
594        )
595    }
596
597    fn execute(
598        &self,
599        partition: usize,
600        context: Arc<TaskContext>,
601    ) -> Result<SendableRecordBatchStream> {
602        trace!(
603            "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
604            partition,
605            context.session_id(),
606            context.task_id()
607        );
608        let metrics = FilterExecMetrics::new(&self.metrics, partition);
609        Ok(Box::pin(FilterExecStream {
610            schema: self.schema(),
611            predicate: Arc::clone(&self.predicate),
612            input: self.input.execute(partition, context)?,
613            metrics,
614            projection: self.projection.clone(),
615            batch_coalescer: LimitedBatchCoalescer::new(
616                self.schema(),
617                self.batch_size,
618                self.fetch,
619            ),
620        }))
621    }
622
623    fn metrics(&self) -> Option<MetricsSet> {
624        Some(self.metrics.clone_inner())
625    }
626
627    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
628        vec![ChildStats::At(partition)]
629    }
630
631    /// The output statistics of a filtering operation can be estimated if the
632    /// predicate's selectivity value can be determined for the incoming data.
633    fn statistics_from_inputs(
634        &self,
635        input_stats: &[Arc<Statistics>],
636        _args: &StatisticsArgs,
637    ) -> Result<Arc<Statistics>> {
638        let input_stats = input_stats[0].as_ref().clone();
639        let stats = Self::statistics_helper(
640            &self.input.schema(),
641            input_stats,
642            self.predicate(),
643            self.default_selectivity,
644        )?;
645        Ok(Arc::new(stats.project(self.projection.as_ref())))
646    }
647
648    fn cardinality_effect(&self) -> CardinalityEffect {
649        CardinalityEffect::LowerEqual
650    }
651
652    /// Tries to swap `projection` with its input (`filter`). If possible, performs
653    /// the swap and returns [`FilterExec`] as the top plan. Otherwise, returns `None`.
654    fn try_swapping_with_projection(
655        &self,
656        projection: &ProjectionExec,
657    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
658        // If the projection does not narrow the schema, we should not try to push it down:
659        if projection.expr().len() < projection.input().schema().fields().len() {
660            // Each column in the predicate expression must exist after the projection.
661            if let Some(new_predicate) =
662                update_expr(self.predicate(), projection.expr(), false)?
663            {
664                return FilterExecBuilder::from(self)
665                    .with_input(make_with_child(projection, self.input())?)
666                    .with_predicate(new_predicate)
667                    // The original FilterExec projection referenced columns from its old
668                    // input. After the swap the new input is the ProjectionExec which
669                    // already handles column selection, so clear the projection here.
670                    .apply_projection(None)?
671                    .build()
672                    .map(|e| Some(Arc::new(e) as _));
673            }
674        }
675        try_embed_projection(projection, self)
676    }
677
678    fn gather_filters_for_pushdown(
679        &self,
680        phase: FilterPushdownPhase,
681        parent_filters: Vec<Arc<dyn PhysicalExpr>>,
682        _config: &ConfigOptions,
683    ) -> Result<FilterDescription> {
684        if phase != FilterPushdownPhase::Pre {
685            let child =
686                ChildFilterDescription::from_child(&parent_filters, self.input())?;
687            return Ok(FilterDescription::new().with_child(child));
688        }
689
690        let child = ChildFilterDescription::from_child(&parent_filters, self.input())?
691            .with_self_filters(
692                split_conjunction(&self.predicate)
693                    .into_iter()
694                    .cloned()
695                    .collect(),
696            );
697
698        Ok(FilterDescription::new().with_child(child))
699    }
700
701    fn handle_child_pushdown_result(
702        &self,
703        phase: FilterPushdownPhase,
704        child_pushdown_result: ChildPushdownResult,
705        _config: &ConfigOptions,
706    ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
707        if phase != FilterPushdownPhase::Pre {
708            return Ok(FilterPushdownPropagation::if_all(child_pushdown_result));
709        }
710        // We absorb any parent filters that were not handled by our children
711        let mut unsupported_parent_filters: Vec<Arc<dyn PhysicalExpr>> =
712            child_pushdown_result
713                .parent_filters
714                .iter()
715                .filter_map(|f| {
716                    matches!(f.all(), PushedDown::No).then_some(Arc::clone(&f.filter))
717                })
718                .collect();
719
720        // If this FilterExec has a projection, the unsupported parent filters
721        // are in the output schema (after projection) coordinates. We need to
722        // remap them to the input schema coordinates before combining with self filters.
723        if self.projection.is_some() {
724            let input_schema = self.input().schema();
725            unsupported_parent_filters = unsupported_parent_filters
726                .into_iter()
727                .map(|expr| reassign_expr_columns(expr, &input_schema))
728                .collect::<Result<Vec<_>>>()?;
729        }
730
731        let unsupported_self_filters = child_pushdown_result
732            .self_filters
733            .first()
734            .expect("we have exactly one child")
735            .iter()
736            .filter_map(|f| match f.discriminant {
737                PushedDown::Yes => None,
738                PushedDown::No => Some(&f.predicate),
739            })
740            .cloned();
741
742        let unhandled_filters = unsupported_parent_filters
743            .into_iter()
744            .chain(unsupported_self_filters)
745            .collect_vec();
746
747        // If we have unhandled filters, we need to create a new FilterExec
748        let filter_input = Arc::clone(self.input());
749        let new_predicate = conjunction(unhandled_filters);
750        let updated_node = if new_predicate.eq(&lit(true)) {
751            // FilterExec is no longer needed, but we may need to leave a projection in place.
752            // If this FilterExec had a fetch limit, propagate it to the child.
753            // When the child also has a fetch, use the minimum of both to preserve
754            // the tighter constraint.
755            let filter_input = if let Some(outer_fetch) = self.fetch {
756                let effective_fetch = match filter_input.fetch() {
757                    Some(inner_fetch) => outer_fetch.min(inner_fetch),
758                    None => outer_fetch,
759                };
760                match filter_input.with_fetch(Some(effective_fetch)) {
761                    Some(node) => node,
762                    None => Arc::new(LocalLimitExec::new(filter_input, effective_fetch)),
763                }
764            } else {
765                filter_input
766            };
767            match self.projection().as_ref() {
768                Some(projection_indices) => {
769                    let filter_child_schema = filter_input.schema();
770                    let proj_exprs = projection_indices
771                        .iter()
772                        .map(|p| {
773                            let field = filter_child_schema.field(*p).clone();
774                            ProjectionExpr {
775                                expr: Arc::new(Column::new(field.name(), *p))
776                                    as Arc<dyn PhysicalExpr>,
777                                alias: field.name().to_string(),
778                            }
779                        })
780                        .collect::<Vec<_>>();
781                    Some(Arc::new(ProjectionExec::try_new(proj_exprs, filter_input)?)
782                        as Arc<dyn ExecutionPlan>)
783                }
784                None => {
785                    // No projection needed, just return the input
786                    Some(filter_input)
787                }
788            }
789        } else if new_predicate.eq(&self.predicate) {
790            // The new predicate is the same as our current predicate
791            None
792        } else {
793            // Create a new FilterExec with the new predicate, preserving the projection
794            let new = FilterExec {
795                predicate: Arc::clone(&new_predicate),
796                input: Arc::clone(&filter_input),
797                metrics: self.metrics.clone(),
798                default_selectivity: self.default_selectivity,
799                cache: Arc::new(Self::compute_properties(
800                    &filter_input,
801                    &new_predicate,
802                    self.default_selectivity,
803                    self.projection.as_deref(),
804                )?),
805                projection: self.projection.clone(),
806                batch_size: self.batch_size,
807                fetch: self.fetch,
808            };
809            Some(Arc::new(new) as _)
810        };
811
812        Ok(FilterPushdownPropagation {
813            filters: vec![PushedDown::Yes; child_pushdown_result.parent_filters.len()],
814            updated_node,
815        })
816    }
817
818    fn fetch(&self) -> Option<usize> {
819        self.fetch
820    }
821
822    fn with_fetch(&self, fetch: Option<usize>) -> Option<Arc<dyn ExecutionPlan>> {
823        Some(Arc::new(Self {
824            predicate: Arc::clone(&self.predicate),
825            input: Arc::clone(&self.input),
826            metrics: self.metrics.clone(),
827            default_selectivity: self.default_selectivity,
828            cache: Arc::clone(&self.cache),
829            projection: self.projection.clone(),
830            batch_size: self.batch_size,
831            fetch,
832        }))
833    }
834
835    fn with_preserve_order(
836        &self,
837        preserve_order: bool,
838    ) -> Option<Arc<dyn ExecutionPlan>> {
839        self.input
840            .with_preserve_order(preserve_order)
841            .and_then(|new_input| {
842                replace_children_if_necessary(Arc::new(self.clone()), vec![new_input])
843                    .ok()
844            })
845    }
846
847    #[cfg(feature = "proto")]
848    fn try_to_proto(
849        &self,
850        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
851    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
852        use datafusion_proto_models::protobuf;
853        let input = ctx.encode_child(self.input())?;
854        let expr = ctx.encode_expr(self.predicate())?;
855        // Preserve the exact wire format: `None` (full projection) is serialized
856        // as the identity projection `[0, 1, ..., num_fields - 1]` so that it is
857        // distinguishable from an explicit projection on decode.
858        let projection = if let Some(v) = self.projection() {
859            v.iter().map(|x| *x as u32).collect()
860        } else {
861            (0..self.input().schema().fields().len())
862                .map(|i| i as u32)
863                .collect()
864        };
865        Ok(Some(protobuf::PhysicalPlanNode {
866            physical_plan_type: Some(
867                protobuf::physical_plan_node::PhysicalPlanType::Filter(Box::new(
868                    protobuf::FilterExecNode {
869                        input: Some(Box::new(input)),
870                        expr: Some(expr),
871                        default_filter_selectivity: self.default_selectivity() as u32,
872                        projection,
873                        batch_size: self.batch_size() as u32,
874                        fetch: self.fetch().map(|f| f as u32),
875                    },
876                )),
877            ),
878        }))
879    }
880}
881
882#[cfg(feature = "proto")]
883impl FilterExec {
884    /// Reconstruct a [`FilterExec`] from its protobuf representation.
885    ///
886    /// The exact inverse of [`ExecutionPlan::try_to_proto`]: it takes the whole
887    /// [`PhysicalPlanNode`] so every plan's `try_from_proto` shares one signature.
888    ///
889    /// [`PhysicalPlanNode`]: datafusion_proto_models::protobuf::PhysicalPlanNode
890    /// [`ExecutionPlan::try_to_proto`]: crate::ExecutionPlan::try_to_proto
891    pub fn try_from_proto(
892        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
893        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
894    ) -> Result<Arc<dyn ExecutionPlan>> {
895        use datafusion_proto_models::protobuf;
896        let filter = crate::expect_plan_variant!(
897            node,
898            protobuf::physical_plan_node::PhysicalPlanType::Filter,
899            "FilterExec",
900        );
901        let input =
902            ctx.decode_required_child(filter.input.as_deref(), "FilterExec", "input")?;
903        let predicate = ctx.decode_required_expr(
904            filter.expr.as_ref(),
905            input.schema().as_ref(),
906            "FilterExec",
907            "expr",
908        )?;
909        let filter_selectivity = filter.default_filter_selectivity.try_into();
910
911        // `None` is encoded as the full identity projection. Reconstruct it only
912        // when all input columns are present in order, leaving an empty list as
913        // `Some(vec![])`.
914        let num_fields = input.schema().fields().len();
915        let mut is_full_projection = filter.projection.len() == num_fields;
916        let mut projection_vec: Vec<usize> = Vec::with_capacity(filter.projection.len());
917        for (i, idx) in filter.projection.iter().enumerate() {
918            let idx = *idx as usize;
919            is_full_projection &= idx == i;
920            projection_vec.push(idx);
921        }
922        let projection = if is_full_projection {
923            None
924        } else {
925            Some(projection_vec)
926        };
927        let filter = FilterExecBuilder::new(predicate, input)
928            .apply_projection(projection)?
929            .with_batch_size(filter.batch_size as usize)
930            .with_fetch(filter.fetch.map(|f| f as usize))
931            .build()?;
932        match filter_selectivity {
933            Ok(filter_selectivity) => Ok(Arc::new(
934                filter.with_default_selectivity(filter_selectivity)?,
935            )),
936            Err(_) => Err(datafusion_common::internal_datafusion_err!(
937                "filter_selectivity in PhysicalPlanNode is invalid"
938            )),
939        }
940    }
941}
942
943impl EmbeddedProjection for FilterExec {
944    fn with_projection(&self, projection: Option<Vec<usize>>) -> Result<Self> {
945        FilterExecBuilder::from(self)
946            .apply_projection(projection)?
947            .build()
948    }
949}
950
951/// Collects column equality information from `col = literal` predicates in a
952/// conjunction.
953///
954/// Returns `(eq_columns, is_infeasible)`:
955/// - `eq_columns`: set of column indices constrained to a single literal value.
956/// - `is_infeasible`: `true` when the same column is equated to two different
957///   non-null literals (e.g. `name = 'alice' AND name = 'bob'`), which is
958///   always unsatisfiable.
959///
960/// Only AND conjunctions are traversed; OR is intentionally skipped
961/// since `a = 1 OR a = 2` does not pin NDV to 1.
962fn collect_equality_columns(predicate: &Arc<dyn PhysicalExpr>) -> (HashSet<usize>, bool) {
963    let mut eq_values: HashMap<usize, ScalarValue> = HashMap::new();
964    let mut infeasible = false;
965
966    for expr in split_conjunction(predicate) {
967        let Some(binary) = expr.downcast_ref::<BinaryExpr>() else {
968            continue;
969        };
970        if *binary.op() != Operator::Eq {
971            continue;
972        }
973        let left = binary.left();
974        let right = binary.right();
975        let pair = if let Some(col) = left.downcast_ref::<Column>()
976            && let Some(lit) = right.downcast_ref::<Literal>()
977            && !lit.value().is_null()
978        {
979            Some((col.index(), lit.value().clone()))
980        } else if let Some(col) = right.downcast_ref::<Column>()
981            && let Some(lit) = left.downcast_ref::<Literal>()
982            && !lit.value().is_null()
983        {
984            Some((col.index(), lit.value().clone()))
985        } else {
986            None
987        };
988
989        if let Some((idx, value)) = pair {
990            match eq_values.entry(idx) {
991                Entry::Occupied(prev) => {
992                    if *prev.get() != value {
993                        infeasible = true;
994                        break;
995                    }
996                }
997                Entry::Vacant(slot) => {
998                    slot.insert(value);
999                }
1000            }
1001        }
1002    }
1003
1004    (eq_values.into_keys().collect(), infeasible)
1005}
1006
1007/// Collects columns that cannot be NULL in any surviving row.
1008///
1009/// A filter keeps only rows where the predicate is TRUE, so a column is
1010/// null-rejecting if some top-level AND conjunct evaluates to NULL or FALSE
1011/// whenever that column is NULL. Two such conjuncts are recognized:
1012///
1013/// - a binary operator that returns NULL on NULL input, applied directly to the
1014///   column (e.g. `a = 10`, `a < b`);
1015/// - an `IS NOT NULL` check on the column (e.g. `a IS NOT NULL`).
1016///
1017/// This analysis is conservative; for example, OR clauses are not considered
1018/// null-rejecting, and neither are indirect operands like `a + 1 < 10`.
1019fn collect_null_rejecting_columns(predicate: &Arc<dyn PhysicalExpr>) -> HashSet<usize> {
1020    let mut columns = HashSet::new();
1021
1022    for expr in split_conjunction(predicate) {
1023        // `col IS NOT NULL` keeps only rows where `col` is non-null.
1024        if let Some(is_not_null) = expr.downcast_ref::<IsNotNullExpr>() {
1025            if let Some(col) = is_not_null.arg().downcast_ref::<Column>() {
1026                columns.insert(col.index());
1027            }
1028            continue;
1029        }
1030
1031        // A binary operator that returns NULL on NULL input rejects rows where
1032        // a direct column operand is NULL.
1033        if let Some(binary) = expr.downcast_ref::<BinaryExpr>() {
1034            if !binary.op().returns_null_on_null() {
1035                continue;
1036            }
1037            if let Some(col) = binary.left().downcast_ref::<Column>() {
1038                columns.insert(col.index());
1039            }
1040            if let Some(col) = binary.right().downcast_ref::<Column>() {
1041                columns.insert(col.index());
1042            }
1043        }
1044    }
1045
1046    columns
1047}
1048
1049/// Converts an interval bound to a [`Precision`] value. NULL bounds (which
1050/// represent "unbounded" in the interval type) map to [`Precision::Absent`].
1051fn interval_bound_to_precision(
1052    bound: ScalarValue,
1053    is_exact: bool,
1054) -> Precision<ScalarValue> {
1055    if bound.is_null() {
1056        Precision::Absent
1057    } else if is_exact {
1058        Precision::Exact(bound)
1059    } else {
1060        Precision::Inexact(bound)
1061    }
1062}
1063
1064/// Caps a row-bounded column statistic (a null count or distinct count) at the
1065/// filtered row count, since a column cannot have more nulls or distinct values
1066/// than it has rows. Known counts are demoted to inexact because a
1067/// filter-derived row bound is normally an estimate, the exception being an
1068/// exact zero, which proves the column is empty.
1069fn cap_at_rows(
1070    value: Precision<usize>,
1071    filtered_num_rows: Precision<usize>,
1072) -> Precision<usize> {
1073    match filtered_num_rows {
1074        Precision::Absent => value.to_inexact(),
1075        Precision::Exact(0) => Precision::Exact(0),
1076        rows => value.to_inexact().min(&rows),
1077    }
1078}
1079
1080/// Scales a byte size by the filter selectivity. An exact zero row count means
1081/// the output is exactly empty, so the byte size is an exact zero too.
1082fn scale_byte_size_at_rows(
1083    byte_size: Precision<usize>,
1084    selectivity: f64,
1085    filtered_num_rows: Precision<usize>,
1086) -> Precision<usize> {
1087    if filtered_num_rows == Precision::Exact(0) {
1088        Precision::Exact(0)
1089    } else {
1090        byte_size.with_estimated_selectivity(selectivity)
1091    }
1092}
1093
1094/// Returns the NDV for a column constrained to one non-null value (e.g.
1095/// `column = literal` or a singleton interval), derived from the filtered row
1096/// estimate: zero rows means zero distinct values, a known positive row count
1097/// means exactly one, and an unknown row count means an inexact one (the column
1098/// could still be empty).
1099///
1100/// The caller is responsible for proving the singleton domain.
1101fn distinct_count_for_singleton_domain(
1102    filtered_num_rows: Precision<usize>,
1103) -> Precision<usize> {
1104    match filtered_num_rows {
1105        Precision::Exact(0) | Precision::Inexact(0) => filtered_num_rows,
1106        // The row count is unknown, so the column could still be empty (zero
1107        // distinct values); report an inexact one rather than overstating it.
1108        Precision::Absent => Precision::Inexact(1),
1109        _ => Precision::Exact(1),
1110    }
1111}
1112
1113/// Builds output column statistics from interval-analysis boundaries.
1114///
1115/// The interval bounds become min/max values, singleton intervals become
1116/// singleton NDV, and row-bounded counts are kept consistent with the filtered
1117/// row estimate.
1118fn collect_new_statistics(
1119    schema: &SchemaRef,
1120    input_column_stats: &[ColumnStatistics],
1121    analysis_boundaries: Vec<ExprBoundaries>,
1122    selectivity: f64,
1123    null_rejecting_columns: &HashSet<usize>,
1124    filtered_num_rows: Precision<usize>,
1125) -> Vec<ColumnStatistics> {
1126    analysis_boundaries
1127        .into_iter()
1128        .enumerate()
1129        .map(
1130            |(
1131                idx,
1132                ExprBoundaries {
1133                    interval,
1134                    distinct_count,
1135                    ..
1136                },
1137            )| {
1138                let Some(interval) = interval else {
1139                    // If the interval is `None`, we can say that there are no rows.
1140                    // Use a typed null to preserve the column's data type, so that
1141                    // downstream interval analysis can still intersect intervals
1142                    // of the same type.
1143                    let typed_null = ScalarValue::try_from(schema.field(idx).data_type())
1144                        .unwrap_or(ScalarValue::Null);
1145                    return ColumnStatistics {
1146                        null_count: Precision::Exact(0),
1147                        max_value: Precision::Exact(typed_null.clone()),
1148                        min_value: Precision::Exact(typed_null.clone()),
1149                        sum_value: Precision::Exact(typed_null),
1150                        distinct_count: Precision::Exact(0),
1151                        byte_size: Precision::Exact(0),
1152                    };
1153                };
1154                let (lower, upper) = interval.into_bounds();
1155                let is_single_value =
1156                    !lower.is_null() && !upper.is_null() && lower == upper;
1157                let min_value = interval_bound_to_precision(lower, is_single_value);
1158                let max_value = interval_bound_to_precision(upper, is_single_value);
1159
1160                // Distinct and null counts cannot exceed the number of rows
1161                // that survive the filter. Singleton intervals and
1162                // null-rejecting predicates provide tighter bounds.
1163                let capped_distinct_count = if is_single_value {
1164                    distinct_count_for_singleton_domain(filtered_num_rows)
1165                } else {
1166                    cap_at_rows(distinct_count, filtered_num_rows)
1167                };
1168                let capped_null_count = if null_rejecting_columns.contains(&idx) {
1169                    Precision::Exact(0)
1170                } else {
1171                    cap_at_rows(input_column_stats[idx].null_count, filtered_num_rows)
1172                };
1173                let byte_size = scale_byte_size_at_rows(
1174                    input_column_stats[idx].byte_size,
1175                    selectivity,
1176                    filtered_num_rows,
1177                );
1178                ColumnStatistics {
1179                    null_count: capped_null_count,
1180                    max_value,
1181                    min_value,
1182                    sum_value: Precision::Absent,
1183                    distinct_count: capped_distinct_count,
1184                    byte_size,
1185                }
1186            },
1187        )
1188        .collect()
1189}
1190
1191/// The FilterExec streams wraps the input iterator and applies the predicate expression to
1192/// determine which rows to include in its output batches
1193struct FilterExecStream {
1194    /// Output schema after the projection
1195    schema: SchemaRef,
1196    /// The expression to filter on. This expression must evaluate to a boolean value.
1197    predicate: Arc<dyn PhysicalExpr>,
1198    /// The input partition to filter.
1199    input: SendableRecordBatchStream,
1200    /// Runtime metrics recording
1201    metrics: FilterExecMetrics,
1202    /// The projection indices of the columns in the input schema
1203    projection: Option<ProjectionRef>,
1204    /// Batch coalescer to combine small batches
1205    batch_coalescer: LimitedBatchCoalescer,
1206}
1207
1208/// The metrics for `FilterExec`
1209struct FilterExecMetrics {
1210    /// Common metrics for most operators
1211    baseline_metrics: BaselineMetrics,
1212    /// Selectivity of the filter, calculated as output_rows / input_rows
1213    selectivity: RatioMetrics,
1214    // Remember to update `docs/source/user-guide/metrics.md` when adding new metrics,
1215    // or modifying metrics comments
1216}
1217
1218impl FilterExecMetrics {
1219    pub fn new(metrics: &ExecutionPlanMetricsSet, partition: usize) -> Self {
1220        Self {
1221            baseline_metrics: BaselineMetrics::new(metrics, partition),
1222            selectivity: MetricBuilder::new(metrics)
1223                .with_type(MetricType::Summary)
1224                .ratio_metrics("selectivity", partition),
1225        }
1226    }
1227}
1228
1229pub fn batch_filter(
1230    batch: &RecordBatch,
1231    predicate: &Arc<dyn PhysicalExpr>,
1232) -> Result<RecordBatch> {
1233    filter_and_project(batch, predicate, None)
1234}
1235
1236fn filter_and_project(
1237    batch: &RecordBatch,
1238    predicate: &Arc<dyn PhysicalExpr>,
1239    projection: Option<&Vec<usize>>,
1240) -> Result<RecordBatch> {
1241    predicate
1242        .evaluate(batch)
1243        .and_then(|v| v.into_array(batch.num_rows()))
1244        .and_then(|array| {
1245            Ok(match (as_boolean_array(&array), projection) {
1246                // Apply filter array to record batch
1247                (Ok(filter_array), None) => filter_record_batch(batch, filter_array)?,
1248                (Ok(filter_array), Some(projection)) => {
1249                    let projected_batch = batch.project(projection)?;
1250                    filter_record_batch(&projected_batch, filter_array)?
1251                }
1252                (Err(_), _) => {
1253                    return internal_err!(
1254                        "Cannot create filter_array from non-boolean predicates"
1255                    );
1256                }
1257            })
1258        })
1259}
1260
1261impl Stream for FilterExecStream {
1262    type Item = Result<RecordBatch>;
1263
1264    fn poll_next(
1265        mut self: Pin<&mut Self>,
1266        cx: &mut Context<'_>,
1267    ) -> Poll<Option<Self::Item>> {
1268        let elapsed_compute = self.metrics.baseline_metrics.elapsed_compute().clone();
1269        loop {
1270            // If there is a completed batch ready, return it
1271            if let Some(batch) = self.batch_coalescer.next_completed_batch() {
1272                self.metrics.selectivity.add_part(batch.num_rows());
1273                let poll = Poll::Ready(Some(Ok(batch)));
1274                return self.metrics.baseline_metrics.record_poll(poll);
1275            }
1276
1277            if self.batch_coalescer.is_finished() {
1278                // If input is done and no batches are ready, return None to signal end of stream.
1279                return Poll::Ready(None);
1280            }
1281
1282            // Attempt to pull the next batch from the input stream.
1283            match ready!(self.input.poll_next_unpin(cx)) {
1284                None => {
1285                    self.batch_coalescer.finish()?;
1286                    // Release the input pipeline's resources.
1287                    let input_schema = self.input.schema();
1288                    self.input = Box::pin(EmptyRecordBatchStream::new(input_schema));
1289                    // continue draining the coalescer
1290                }
1291                Some(Ok(batch)) => {
1292                    let timer = elapsed_compute.timer();
1293                    let status = self.predicate.as_ref()
1294                        .evaluate(&batch)
1295                        .and_then(|v| v.into_array(batch.num_rows()))
1296                        .and_then(|array| {
1297                            Ok(match self.projection.as_ref()  {
1298                                Some(projection) => {
1299                                    let projected_batch = batch.project(projection)?;
1300                                    (array, projected_batch)
1301                                },
1302                                None => (array, batch)
1303                            })
1304                        }).and_then(|(array, batch)| {
1305                            match as_boolean_array(&array) {
1306                                Ok(filter_array) => {
1307                                    self.metrics.selectivity.add_total(batch.num_rows());
1308                                    // TODO: support push_batch_with_filter in LimitedBatchCoalescer
1309                                    let batch = filter_record_batch(&batch, filter_array)?;
1310                                    let state = self.batch_coalescer.push_batch(batch)?;
1311                                    Ok(state)
1312                                }
1313                                Err(_) => {
1314                                    internal_err!(
1315                                        "Cannot create filter_array from non-boolean predicates"
1316                                    )
1317                                }
1318                            }
1319                        })?;
1320                    timer.done();
1321
1322                    match status {
1323                        PushBatchStatus::Continue => {
1324                            // Keep pushing more batches
1325                        }
1326                        PushBatchStatus::LimitReached => {
1327                            // limit was reached, so stop early
1328                            self.batch_coalescer.finish()?;
1329                            // Release the input pipeline's resources.
1330                            let input_schema = self.input.schema();
1331                            self.input =
1332                                Box::pin(EmptyRecordBatchStream::new(input_schema));
1333                            // continue draining the coalescer
1334                        }
1335                    }
1336                }
1337
1338                // Error case
1339                other => return Poll::Ready(other),
1340            }
1341        }
1342    }
1343
1344    fn size_hint(&self) -> (usize, Option<usize>) {
1345        // Same number of record batches
1346        self.input.size_hint()
1347    }
1348}
1349impl RecordBatchStream for FilterExecStream {
1350    fn schema(&self) -> SchemaRef {
1351        Arc::clone(&self.schema)
1352    }
1353}
1354
1355/// Return the equals Column-Pairs and Non-equals Column-Pairs
1356#[deprecated(
1357    since = "51.0.0",
1358    note = "This function will be internal in the future"
1359)]
1360pub fn collect_columns_from_predicate(
1361    predicate: &'_ Arc<dyn PhysicalExpr>,
1362) -> EqualAndNonEqual<'_> {
1363    collect_columns_from_predicate_inner(predicate)
1364}
1365
1366fn collect_columns_from_predicate_inner(
1367    predicate: &'_ Arc<dyn PhysicalExpr>,
1368) -> EqualAndNonEqual<'_> {
1369    let mut eq_predicate_columns = Vec::<PhysicalExprPairRef>::new();
1370    let mut ne_predicate_columns = Vec::<PhysicalExprPairRef>::new();
1371
1372    let predicates = split_conjunction(predicate);
1373    predicates.into_iter().for_each(|p| {
1374        if let Some(binary) = p.downcast_ref::<BinaryExpr>() {
1375            // Only extract pairs where at least one side is a Column reference.
1376            // Pairs like `complex_expr = literal` should not create equivalence
1377            // classes — the literal could appear in many unrelated expressions
1378            // (e.g. sort keys), and normalize_expr's deep traversal would
1379            // replace those occurrences with the complex expression, corrupting
1380            // sort orderings. Constant propagation for such pairs is handled
1381            // separately by `extend_constants`.
1382            let has_direct_column_operand =
1383                binary.left().downcast_ref::<Column>().is_some()
1384                    || binary.right().downcast_ref::<Column>().is_some();
1385            if !has_direct_column_operand {
1386                return;
1387            }
1388            match binary.op() {
1389                Operator::Eq => {
1390                    eq_predicate_columns.push((binary.left(), binary.right()))
1391                }
1392                Operator::NotEq => {
1393                    ne_predicate_columns.push((binary.left(), binary.right()))
1394                }
1395                _ => {}
1396            }
1397        }
1398    });
1399
1400    (eq_predicate_columns, ne_predicate_columns)
1401}
1402
1403/// Pair of `Arc<dyn PhysicalExpr>`s
1404pub type PhysicalExprPairRef<'a> = (&'a Arc<dyn PhysicalExpr>, &'a Arc<dyn PhysicalExpr>);
1405
1406/// The equals Column-Pairs and Non-equals Column-Pairs in the Predicates
1407pub type EqualAndNonEqual<'a> =
1408    (Vec<PhysicalExprPairRef<'a>>, Vec<PhysicalExprPairRef<'a>>);
1409
1410#[cfg(test)]
1411mod tests {
1412    use super::*;
1413    use crate::empty::EmptyExec;
1414    use crate::expressions::*;
1415    use crate::statistics::{StatisticsArgs, StatisticsContext};
1416    use crate::test;
1417    use crate::test::exec::StatisticsExec;
1418    use arrow::datatypes::{Field, Schema, UnionFields, UnionMode};
1419
1420    #[tokio::test]
1421    async fn collect_columns_predicates() -> Result<()> {
1422        let schema = test::aggr_test_schema();
1423        let predicate: Arc<dyn PhysicalExpr> = binary(
1424            binary(
1425                binary(col("c2", &schema)?, Operator::GtEq, lit(1u32), &schema)?,
1426                Operator::And,
1427                binary(col("c2", &schema)?, Operator::Eq, lit(4u32), &schema)?,
1428                &schema,
1429            )?,
1430            Operator::And,
1431            binary(
1432                binary(
1433                    col("c2", &schema)?,
1434                    Operator::Eq,
1435                    col("c9", &schema)?,
1436                    &schema,
1437                )?,
1438                Operator::And,
1439                binary(
1440                    col("c1", &schema)?,
1441                    Operator::NotEq,
1442                    col("c13", &schema)?,
1443                    &schema,
1444                )?,
1445                &schema,
1446            )?,
1447            &schema,
1448        )?;
1449
1450        let (equal_pairs, ne_pairs) = collect_columns_from_predicate_inner(&predicate);
1451        assert_eq!(2, equal_pairs.len());
1452        assert!(equal_pairs[0].0.eq(&col("c2", &schema)?));
1453        assert!(equal_pairs[0].1.eq(&lit(4u32)));
1454
1455        assert!(equal_pairs[1].0.eq(&col("c2", &schema)?));
1456        assert!(equal_pairs[1].1.eq(&col("c9", &schema)?));
1457
1458        assert_eq!(1, ne_pairs.len());
1459        assert!(ne_pairs[0].0.eq(&col("c1", &schema)?));
1460        assert!(ne_pairs[0].1.eq(&col("c13", &schema)?));
1461
1462        Ok(())
1463    }
1464
1465    #[tokio::test]
1466    async fn test_filter_statistics_basic_expr() -> Result<()> {
1467        // Table:
1468        //      a: min=1, max=100
1469        let bytes_per_row = 4;
1470        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1471        let input = Arc::new(StatisticsExec::new(
1472            Statistics {
1473                num_rows: Precision::Inexact(100),
1474                total_byte_size: Precision::Inexact(100 * bytes_per_row),
1475                column_statistics: vec![ColumnStatistics {
1476                    min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1477                    max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
1478                    ..Default::default()
1479                }],
1480            },
1481            schema.clone(),
1482        ));
1483
1484        // a <= 25
1485        let predicate: Arc<dyn PhysicalExpr> =
1486            binary(col("a", &schema)?, Operator::LtEq, lit(25i32), &schema)?;
1487
1488        // WHERE a <= 25
1489        let filter: Arc<dyn ExecutionPlan> =
1490            Arc::new(FilterExec::try_new(predicate, input)?);
1491
1492        let statistics =
1493            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
1494        assert_eq!(statistics.num_rows, Precision::Inexact(25));
1495        assert_eq!(
1496            statistics.total_byte_size,
1497            Precision::Inexact(25 * bytes_per_row)
1498        );
1499        assert_eq!(
1500            statistics.column_statistics,
1501            vec![ColumnStatistics {
1502                // `a <= 25` rejects nulls, so the column has no surviving nulls.
1503                null_count: Precision::Exact(0),
1504                min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1505                max_value: Precision::Inexact(ScalarValue::Int32(Some(25))),
1506                ..Default::default()
1507            }]
1508        );
1509
1510        Ok(())
1511    }
1512
1513    #[tokio::test]
1514    async fn test_filter_statistics_column_level_nested() -> Result<()> {
1515        // Table:
1516        //      a: min=1, max=100
1517        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1518        let input = Arc::new(StatisticsExec::new(
1519            Statistics {
1520                num_rows: Precision::Inexact(100),
1521                column_statistics: vec![ColumnStatistics {
1522                    min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1523                    max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
1524                    ..Default::default()
1525                }],
1526                total_byte_size: Precision::Absent,
1527            },
1528            schema.clone(),
1529        ));
1530
1531        // WHERE a <= 25
1532        let sub_filter: Arc<dyn ExecutionPlan> = Arc::new(FilterExec::try_new(
1533            binary(col("a", &schema)?, Operator::LtEq, lit(25i32), &schema)?,
1534            input,
1535        )?);
1536
1537        // Nested filters (two separate physical plans, instead of AND chain in the expr)
1538        // WHERE a >= 10
1539        // WHERE a <= 25
1540        let filter: Arc<dyn ExecutionPlan> = Arc::new(FilterExec::try_new(
1541            binary(col("a", &schema)?, Operator::GtEq, lit(10i32), &schema)?,
1542            sub_filter,
1543        )?);
1544
1545        let statistics =
1546            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
1547        assert_eq!(statistics.num_rows, Precision::Inexact(16));
1548        assert_eq!(
1549            statistics.column_statistics,
1550            vec![ColumnStatistics {
1551                // `a <= 25 AND a >= 10` rejects nulls in `a`.
1552                null_count: Precision::Exact(0),
1553                min_value: Precision::Inexact(ScalarValue::Int32(Some(10))),
1554                max_value: Precision::Inexact(ScalarValue::Int32(Some(25))),
1555                ..Default::default()
1556            }]
1557        );
1558
1559        Ok(())
1560    }
1561
1562    #[tokio::test]
1563    async fn test_filter_statistics_column_level_nested_multiple() -> Result<()> {
1564        // Table:
1565        //      a: min=1, max=100
1566        //      b: min=1, max=50
1567        let schema = Schema::new(vec![
1568            Field::new("a", DataType::Int32, false),
1569            Field::new("b", DataType::Int32, false),
1570        ]);
1571        let input = Arc::new(StatisticsExec::new(
1572            Statistics {
1573                num_rows: Precision::Inexact(100),
1574                column_statistics: vec![
1575                    ColumnStatistics {
1576                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1577                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
1578                        ..Default::default()
1579                    },
1580                    ColumnStatistics {
1581                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1582                        max_value: Precision::Inexact(ScalarValue::Int32(Some(50))),
1583                        ..Default::default()
1584                    },
1585                ],
1586                total_byte_size: Precision::Absent,
1587            },
1588            schema.clone(),
1589        ));
1590
1591        // WHERE a <= 25
1592        let a_lte_25: Arc<dyn ExecutionPlan> = Arc::new(FilterExec::try_new(
1593            binary(col("a", &schema)?, Operator::LtEq, lit(25i32), &schema)?,
1594            input,
1595        )?);
1596
1597        // WHERE b > 45
1598        let b_gt_5: Arc<dyn ExecutionPlan> = Arc::new(FilterExec::try_new(
1599            binary(col("b", &schema)?, Operator::Gt, lit(45i32), &schema)?,
1600            a_lte_25,
1601        )?);
1602
1603        // WHERE a >= 10
1604        let filter: Arc<dyn ExecutionPlan> = Arc::new(FilterExec::try_new(
1605            binary(col("a", &schema)?, Operator::GtEq, lit(10i32), &schema)?,
1606            b_gt_5,
1607        )?);
1608        let statistics =
1609            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
1610        // On a uniform distribution, only fifteen rows will satisfy the
1611        // filter that 'a' proposed (a >= 10 AND a <= 25) (15/100) and only
1612        // 5 rows will satisfy the filter that 'b' proposed (b > 45) (5/50).
1613        //
1614        // Which would result with a selectivity of  '15/100 * 5/50' or 0.015
1615        // and that means about %1.5 of the all rows (rounded up to 2 rows).
1616        assert_eq!(statistics.num_rows, Precision::Inexact(2));
1617        assert_eq!(
1618            statistics.column_statistics,
1619            vec![
1620                ColumnStatistics {
1621                    // `a <= 25 AND a >= 10` rejects nulls in `a`.
1622                    null_count: Precision::Exact(0),
1623                    min_value: Precision::Inexact(ScalarValue::Int32(Some(10))),
1624                    max_value: Precision::Inexact(ScalarValue::Int32(Some(25))),
1625                    ..Default::default()
1626                },
1627                ColumnStatistics {
1628                    // `b > 45` in the upstream filter zeroes b's nulls; the outer
1629                    // filter then caps the (already zero) count, demoting to inexact.
1630                    null_count: Precision::Inexact(0),
1631                    min_value: Precision::Inexact(ScalarValue::Int32(Some(46))),
1632                    max_value: Precision::Inexact(ScalarValue::Int32(Some(50))),
1633                    ..Default::default()
1634                }
1635            ]
1636        );
1637
1638        Ok(())
1639    }
1640
1641    #[tokio::test]
1642    async fn test_filter_statistics_when_input_stats_missing() -> Result<()> {
1643        // Table:
1644        //      a: min=???, max=??? (missing)
1645        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1646        let input = Arc::new(StatisticsExec::new(
1647            Statistics::new_unknown(&schema),
1648            schema.clone(),
1649        ));
1650
1651        // a <= 25
1652        let predicate: Arc<dyn PhysicalExpr> =
1653            binary(col("a", &schema)?, Operator::LtEq, lit(25i32), &schema)?;
1654
1655        // WHERE a <= 25
1656        let filter: Arc<dyn ExecutionPlan> =
1657            Arc::new(FilterExec::try_new(predicate, input)?);
1658
1659        let statistics =
1660            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
1661        assert_eq!(statistics.num_rows, Precision::Absent);
1662
1663        Ok(())
1664    }
1665
1666    #[tokio::test]
1667    async fn test_filter_statistics_multiple_columns() -> Result<()> {
1668        // Table:
1669        //      a: min=1, max=100
1670        //      b: min=1, max=3
1671        //      c: min=1000.0  max=1100.0
1672        let schema = Schema::new(vec![
1673            Field::new("a", DataType::Int32, false),
1674            Field::new("b", DataType::Int32, false),
1675            Field::new("c", DataType::Float32, false),
1676        ]);
1677        let input = Arc::new(StatisticsExec::new(
1678            Statistics {
1679                num_rows: Precision::Inexact(1000),
1680                total_byte_size: Precision::Inexact(4000),
1681                column_statistics: vec![
1682                    ColumnStatistics {
1683                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1684                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
1685                        ..Default::default()
1686                    },
1687                    ColumnStatistics {
1688                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1689                        max_value: Precision::Inexact(ScalarValue::Int32(Some(3))),
1690                        ..Default::default()
1691                    },
1692                    ColumnStatistics {
1693                        min_value: Precision::Inexact(ScalarValue::Float32(Some(1000.0))),
1694                        max_value: Precision::Inexact(ScalarValue::Float32(Some(1100.0))),
1695                        ..Default::default()
1696                    },
1697                ],
1698            },
1699            schema,
1700        ));
1701        // WHERE a<=53 AND (b=3 AND (c<=1075.0 AND a>b))
1702        let predicate = Arc::new(BinaryExpr::new(
1703            Arc::new(BinaryExpr::new(
1704                Arc::new(Column::new("a", 0)),
1705                Operator::LtEq,
1706                Arc::new(Literal::new(ScalarValue::Int32(Some(53)))),
1707            )),
1708            Operator::And,
1709            Arc::new(BinaryExpr::new(
1710                Arc::new(BinaryExpr::new(
1711                    Arc::new(Column::new("b", 1)),
1712                    Operator::Eq,
1713                    Arc::new(Literal::new(ScalarValue::Int32(Some(3)))),
1714                )),
1715                Operator::And,
1716                Arc::new(BinaryExpr::new(
1717                    Arc::new(BinaryExpr::new(
1718                        Arc::new(Column::new("c", 2)),
1719                        Operator::LtEq,
1720                        Arc::new(Literal::new(ScalarValue::Float32(Some(1075.0)))),
1721                    )),
1722                    Operator::And,
1723                    Arc::new(BinaryExpr::new(
1724                        Arc::new(Column::new("a", 0)),
1725                        Operator::Gt,
1726                        Arc::new(Column::new("b", 1)),
1727                    )),
1728                )),
1729            )),
1730        ));
1731        let filter: Arc<dyn ExecutionPlan> =
1732            Arc::new(FilterExec::try_new(predicate, input)?);
1733        let statistics =
1734            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
1735        // 0.5 (from a) * 0.333333... (from b) * 0.798387... (from c) ≈ 0.1330...
1736        // num_rows after ceil => 133.0... => 134
1737        // total_byte_size after ceil => 532.0... => 533
1738        assert_eq!(statistics.num_rows, Precision::Inexact(134));
1739        assert_eq!(statistics.total_byte_size, Precision::Inexact(533));
1740        let exp_col_stats = vec![
1741            ColumnStatistics {
1742                min_value: Precision::Inexact(ScalarValue::Int32(Some(4))),
1743                max_value: Precision::Inexact(ScalarValue::Int32(Some(53))),
1744                ..Default::default()
1745            },
1746            ColumnStatistics {
1747                min_value: Precision::Inexact(ScalarValue::Int32(Some(3))),
1748                max_value: Precision::Inexact(ScalarValue::Int32(Some(3))),
1749                ..Default::default()
1750            },
1751            ColumnStatistics {
1752                min_value: Precision::Inexact(ScalarValue::Float32(Some(1000.0))),
1753                max_value: Precision::Inexact(ScalarValue::Float32(Some(1075.0))),
1754                ..Default::default()
1755            },
1756        ];
1757        let _ = exp_col_stats
1758            .into_iter()
1759            .zip(statistics.column_statistics.clone())
1760            .map(|(expected, actual)| {
1761                if let Some(val) = actual.min_value.get_value() {
1762                    if val.data_type().is_floating() {
1763                        // Windows rounds arithmetic operation results differently for floating point numbers.
1764                        // Therefore, we check if the actual values are in an epsilon range.
1765                        let actual_min = actual.min_value.get_value().unwrap();
1766                        let actual_max = actual.max_value.get_value().unwrap();
1767                        let expected_min = expected.min_value.get_value().unwrap();
1768                        let expected_max = expected.max_value.get_value().unwrap();
1769                        let eps = ScalarValue::Float32(Some(1e-6));
1770
1771                        assert!(actual_min.sub(expected_min).unwrap() < eps);
1772                        assert!(actual_min.sub(expected_min).unwrap() < eps);
1773
1774                        assert!(actual_max.sub(expected_max).unwrap() < eps);
1775                        assert!(actual_max.sub(expected_max).unwrap() < eps);
1776                    } else {
1777                        assert_eq!(actual, expected);
1778                    }
1779                } else {
1780                    assert_eq!(actual, expected);
1781                }
1782            });
1783
1784        Ok(())
1785    }
1786
1787    #[tokio::test]
1788    async fn test_filter_statistics_full_selective() -> Result<()> {
1789        // Table:
1790        //      a: min=1, max=100
1791        //      b: min=1, max=3
1792        let schema = Schema::new(vec![
1793            Field::new("a", DataType::Int32, false),
1794            Field::new("b", DataType::Int32, false),
1795        ]);
1796        let input = Arc::new(StatisticsExec::new(
1797            Statistics {
1798                num_rows: Precision::Inexact(1000),
1799                total_byte_size: Precision::Inexact(4000),
1800                column_statistics: vec![
1801                    ColumnStatistics {
1802                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1803                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
1804                        ..Default::default()
1805                    },
1806                    ColumnStatistics {
1807                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1808                        max_value: Precision::Inexact(ScalarValue::Int32(Some(3))),
1809                        ..Default::default()
1810                    },
1811                ],
1812            },
1813            schema,
1814        ));
1815        // WHERE a<200 AND 1<=b
1816        let predicate = Arc::new(BinaryExpr::new(
1817            Arc::new(BinaryExpr::new(
1818                Arc::new(Column::new("a", 0)),
1819                Operator::Lt,
1820                Arc::new(Literal::new(ScalarValue::Int32(Some(200)))),
1821            )),
1822            Operator::And,
1823            Arc::new(BinaryExpr::new(
1824                Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
1825                Operator::LtEq,
1826                Arc::new(Column::new("b", 1)),
1827            )),
1828        ));
1829        // The filter predicate passes all (non-null) entries, so min/max/NDV
1830        // are unchanged. `a < 200` and `1 <= b` are null-rejecting, though, so
1831        // both columns lose any nulls regardless of selectivity.
1832        let mut expected = StatisticsContext::new()
1833            .compute(input.as_ref(), &StatisticsArgs::new())?
1834            .column_statistics
1835            .clone();
1836        for col in &mut expected {
1837            col.null_count = Precision::Exact(0);
1838        }
1839        let filter: Arc<dyn ExecutionPlan> =
1840            Arc::new(FilterExec::try_new(predicate, input)?);
1841        let statistics =
1842            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
1843
1844        assert_eq!(statistics.num_rows, Precision::Inexact(1000));
1845        assert_eq!(statistics.total_byte_size, Precision::Inexact(4000));
1846        assert_eq!(statistics.column_statistics, expected);
1847
1848        Ok(())
1849    }
1850
1851    #[tokio::test]
1852    async fn test_filter_statistics_zero_selective() -> Result<()> {
1853        // Table:
1854        //      a: min=1, max=100
1855        //      b: min=1, max=3
1856        let schema = Schema::new(vec![
1857            Field::new("a", DataType::Int32, false),
1858            Field::new("b", DataType::Int32, false),
1859        ]);
1860        let input = Arc::new(StatisticsExec::new(
1861            Statistics {
1862                num_rows: Precision::Inexact(1000),
1863                total_byte_size: Precision::Inexact(4000),
1864                column_statistics: vec![
1865                    ColumnStatistics {
1866                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1867                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
1868                        ..Default::default()
1869                    },
1870                    ColumnStatistics {
1871                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1872                        max_value: Precision::Inexact(ScalarValue::Int32(Some(3))),
1873                        ..Default::default()
1874                    },
1875                ],
1876            },
1877            schema,
1878        ));
1879        // WHERE a>200 AND 1<=b
1880        let predicate = Arc::new(BinaryExpr::new(
1881            Arc::new(BinaryExpr::new(
1882                Arc::new(Column::new("a", 0)),
1883                Operator::Gt,
1884                Arc::new(Literal::new(ScalarValue::Int32(Some(200)))),
1885            )),
1886            Operator::And,
1887            Arc::new(BinaryExpr::new(
1888                Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
1889                Operator::LtEq,
1890                Arc::new(Column::new("b", 1)),
1891            )),
1892        ));
1893        let filter: Arc<dyn ExecutionPlan> =
1894            Arc::new(FilterExec::try_new(predicate, input)?);
1895        let statistics =
1896            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
1897
1898        assert_eq!(statistics.num_rows, Precision::Inexact(0));
1899        assert_eq!(statistics.total_byte_size, Precision::Inexact(0));
1900        assert_eq!(
1901            statistics.column_statistics,
1902            vec![
1903                ColumnStatistics {
1904                    min_value: Precision::Exact(ScalarValue::Int32(None)),
1905                    max_value: Precision::Exact(ScalarValue::Int32(None)),
1906                    sum_value: Precision::Exact(ScalarValue::Int32(None)),
1907                    distinct_count: Precision::Exact(0),
1908                    null_count: Precision::Exact(0),
1909                    byte_size: Precision::Exact(0),
1910                },
1911                ColumnStatistics {
1912                    min_value: Precision::Exact(ScalarValue::Int32(None)),
1913                    max_value: Precision::Exact(ScalarValue::Int32(None)),
1914                    sum_value: Precision::Exact(ScalarValue::Int32(None)),
1915                    distinct_count: Precision::Exact(0),
1916                    null_count: Precision::Exact(0),
1917                    byte_size: Precision::Exact(0),
1918                },
1919            ]
1920        );
1921
1922        Ok(())
1923    }
1924
1925    /// Regression test: stacking two FilterExecs where the inner filter
1926    /// proves zero selectivity should not panic with a type mismatch
1927    /// during interval intersection.
1928    ///
1929    /// Previously, when a filter proved no rows could match, the column
1930    /// statistics used untyped `ScalarValue::Null` (data type `Null`).
1931    /// If an outer FilterExec then tried to analyze its own predicate
1932    /// against those statistics, `Interval::intersect` would fail with:
1933    ///   "Only intervals with the same data type are intersectable, lhs:Null, rhs:Int32"
1934    #[tokio::test]
1935    async fn test_nested_filter_with_zero_selectivity_inner() -> Result<()> {
1936        // Inner table: a: [1, 100], b: [1, 3]
1937        let schema = Schema::new(vec![
1938            Field::new("a", DataType::Int32, false),
1939            Field::new("b", DataType::Int32, false),
1940        ]);
1941        let input = Arc::new(StatisticsExec::new(
1942            Statistics {
1943                num_rows: Precision::Inexact(1000),
1944                total_byte_size: Precision::Inexact(4000),
1945                column_statistics: vec![
1946                    ColumnStatistics {
1947                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1948                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
1949                        ..Default::default()
1950                    },
1951                    ColumnStatistics {
1952                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1953                        max_value: Precision::Inexact(ScalarValue::Int32(Some(3))),
1954                        ..Default::default()
1955                    },
1956                ],
1957            },
1958            schema,
1959        ));
1960
1961        // Inner filter: a > 200 (impossible given a max=100 → zero selectivity)
1962        let inner_predicate: Arc<dyn PhysicalExpr> = Arc::new(BinaryExpr::new(
1963            Arc::new(Column::new("a", 0)),
1964            Operator::Gt,
1965            Arc::new(Literal::new(ScalarValue::Int32(Some(200)))),
1966        ));
1967        let inner_filter: Arc<dyn ExecutionPlan> =
1968            Arc::new(FilterExec::try_new(inner_predicate, input)?);
1969
1970        // Outer filter: a = 50
1971        // Before the fix, this would panic because the inner filter's
1972        // zero-selectivity statistics produced Null-typed intervals for
1973        // column `a`, which couldn't intersect with the Int32 literal.
1974        let outer_predicate: Arc<dyn PhysicalExpr> = Arc::new(BinaryExpr::new(
1975            Arc::new(Column::new("a", 0)),
1976            Operator::Eq,
1977            Arc::new(Literal::new(ScalarValue::Int32(Some(50)))),
1978        ));
1979        let outer_filter: Arc<dyn ExecutionPlan> =
1980            Arc::new(FilterExec::try_new(outer_predicate, inner_filter)?);
1981
1982        // Should succeed without error
1983        let statistics = StatisticsContext::new()
1984            .compute(outer_filter.as_ref(), &StatisticsArgs::new())?;
1985        assert_eq!(statistics.num_rows, Precision::Inexact(0));
1986
1987        Ok(())
1988    }
1989
1990    #[tokio::test]
1991    async fn test_filter_statistics_more_inputs() -> Result<()> {
1992        let schema = Schema::new(vec![
1993            Field::new("a", DataType::Int32, false),
1994            Field::new("b", DataType::Int32, false),
1995        ]);
1996        let input = Arc::new(StatisticsExec::new(
1997            Statistics {
1998                num_rows: Precision::Inexact(1000),
1999                total_byte_size: Precision::Inexact(4000),
2000                column_statistics: vec![
2001                    ColumnStatistics {
2002                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
2003                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
2004                        ..Default::default()
2005                    },
2006                    ColumnStatistics {
2007                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
2008                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
2009                        ..Default::default()
2010                    },
2011                ],
2012            },
2013            schema,
2014        ));
2015        // WHERE a<50
2016        let predicate = Arc::new(BinaryExpr::new(
2017            Arc::new(Column::new("a", 0)),
2018            Operator::Lt,
2019            Arc::new(Literal::new(ScalarValue::Int32(Some(50)))),
2020        ));
2021        let filter: Arc<dyn ExecutionPlan> =
2022            Arc::new(FilterExec::try_new(predicate, input)?);
2023        let statistics =
2024            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
2025
2026        assert_eq!(statistics.num_rows, Precision::Inexact(490));
2027        assert_eq!(statistics.total_byte_size, Precision::Inexact(1960));
2028        assert_eq!(
2029            statistics.column_statistics,
2030            vec![
2031                ColumnStatistics {
2032                    // `a < 50` rejects nulls in `a`.
2033                    null_count: Precision::Exact(0),
2034                    min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
2035                    max_value: Precision::Inexact(ScalarValue::Int32(Some(49))),
2036                    ..Default::default()
2037                },
2038                // `b` is not referenced by the predicate, so its stats are
2039                // unchanged (null count stays unknown).
2040                ColumnStatistics {
2041                    min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
2042                    max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
2043                    ..Default::default()
2044                },
2045            ]
2046        );
2047
2048        Ok(())
2049    }
2050
2051    #[tokio::test]
2052    async fn test_empty_input_statistics() -> Result<()> {
2053        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
2054        let input = Arc::new(StatisticsExec::new(
2055            Statistics::new_unknown(&schema),
2056            schema,
2057        ));
2058        // WHERE a <= 10 AND 0 <= a - 5
2059        let predicate = Arc::new(BinaryExpr::new(
2060            Arc::new(BinaryExpr::new(
2061                Arc::new(Column::new("a", 0)),
2062                Operator::LtEq,
2063                Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2064            )),
2065            Operator::And,
2066            Arc::new(BinaryExpr::new(
2067                Arc::new(Literal::new(ScalarValue::Int32(Some(0)))),
2068                Operator::LtEq,
2069                Arc::new(BinaryExpr::new(
2070                    Arc::new(Column::new("a", 0)),
2071                    Operator::Minus,
2072                    Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
2073                )),
2074            )),
2075        ));
2076        let filter: Arc<dyn ExecutionPlan> =
2077            Arc::new(FilterExec::try_new(predicate, input)?);
2078        let filter_statistics =
2079            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
2080
2081        let expected_filter_statistics = Statistics {
2082            num_rows: Precision::Absent,
2083            total_byte_size: Precision::Absent,
2084            column_statistics: vec![ColumnStatistics {
2085                // `a <= 10` rejects nulls, so `a` has no surviving nulls even
2086                // though the input statistics are entirely unknown.
2087                null_count: Precision::Exact(0),
2088                min_value: Precision::Inexact(ScalarValue::Int32(Some(5))),
2089                max_value: Precision::Inexact(ScalarValue::Int32(Some(10))),
2090                sum_value: Precision::Absent,
2091                distinct_count: Precision::Absent,
2092                byte_size: Precision::Absent,
2093            }],
2094        };
2095
2096        assert_eq!(*filter_statistics, expected_filter_statistics);
2097
2098        Ok(())
2099    }
2100
2101    #[tokio::test]
2102    async fn test_statistics_with_constant_column() -> Result<()> {
2103        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
2104        let input = Arc::new(StatisticsExec::new(
2105            Statistics::new_unknown(&schema),
2106            schema,
2107        ));
2108        // WHERE a = 10
2109        let predicate = Arc::new(BinaryExpr::new(
2110            Arc::new(Column::new("a", 0)),
2111            Operator::Eq,
2112            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2113        ));
2114        let filter: Arc<dyn ExecutionPlan> =
2115            Arc::new(FilterExec::try_new(predicate, input)?);
2116        let filter_statistics =
2117            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
2118        // First column is "a", and it is a column with only one value after the filter.
2119        assert!(filter_statistics.column_statistics[0].is_singleton());
2120
2121        Ok(())
2122    }
2123
2124    #[tokio::test]
2125    async fn test_validation_filter_selectivity() -> Result<()> {
2126        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
2127        let input = Arc::new(StatisticsExec::new(
2128            Statistics::new_unknown(&schema),
2129            schema,
2130        ));
2131        // WHERE a = 10
2132        let predicate = Arc::new(BinaryExpr::new(
2133            Arc::new(Column::new("a", 0)),
2134            Operator::Eq,
2135            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2136        ));
2137        let filter = FilterExec::try_new(predicate, input)?;
2138        assert!(filter.with_default_selectivity(120).is_err());
2139        Ok(())
2140    }
2141
2142    #[tokio::test]
2143    async fn test_custom_filter_selectivity() -> Result<()> {
2144        // Need a decimal to trigger inexact selectivity
2145        let schema =
2146            Schema::new(vec![Field::new("a", DataType::Decimal128(2, 3), false)]);
2147        let input = Arc::new(StatisticsExec::new(
2148            Statistics {
2149                num_rows: Precision::Inexact(1000),
2150                total_byte_size: Precision::Inexact(4000),
2151                column_statistics: vec![ColumnStatistics {
2152                    ..Default::default()
2153                }],
2154            },
2155            schema,
2156        ));
2157        // WHERE a = 10
2158        let predicate = Arc::new(BinaryExpr::new(
2159            Arc::new(Column::new("a", 0)),
2160            Operator::Eq,
2161            Arc::new(Literal::new(ScalarValue::Decimal128(Some(10), 10, 10))),
2162        ));
2163        let filter = FilterExec::try_new(predicate, input)?;
2164        let statistics =
2165            StatisticsContext::new().compute(&filter, &StatisticsArgs::new())?;
2166        assert_eq!(statistics.num_rows, Precision::Inexact(200));
2167        assert_eq!(statistics.total_byte_size, Precision::Inexact(800));
2168        let filter = filter.with_default_selectivity(40)?;
2169        let statistics =
2170            StatisticsContext::new().compute(&filter, &StatisticsArgs::new())?;
2171        assert_eq!(statistics.num_rows, Precision::Inexact(400));
2172        assert_eq!(statistics.total_byte_size, Precision::Inexact(1600));
2173        Ok(())
2174    }
2175
2176    #[test]
2177    fn test_equivalence_properties_union_type() -> Result<()> {
2178        let union_type = DataType::Union(
2179            UnionFields::try_new(
2180                vec![0, 1],
2181                vec![
2182                    Field::new("f1", DataType::Int32, true),
2183                    Field::new("f2", DataType::Utf8, true),
2184                ],
2185            )
2186            .unwrap(),
2187            UnionMode::Sparse,
2188        );
2189
2190        let schema = Arc::new(Schema::new(vec![
2191            Field::new("c1", DataType::Int32, true),
2192            Field::new("c2", union_type, true),
2193        ]));
2194
2195        let exec = FilterExec::try_new(
2196            binary(
2197                binary(col("c1", &schema)?, Operator::GtEq, lit(1i32), &schema)?,
2198                Operator::And,
2199                binary(col("c1", &schema)?, Operator::LtEq, lit(4i32), &schema)?,
2200                &schema,
2201            )?,
2202            Arc::new(EmptyExec::new(Arc::clone(&schema))),
2203        )?;
2204
2205        StatisticsContext::new()
2206            .compute(&exec, &StatisticsArgs::new())
2207            .unwrap();
2208
2209        Ok(())
2210    }
2211
2212    #[tokio::test]
2213    async fn test_builder_with_projection() -> Result<()> {
2214        // Create a schema with multiple columns
2215        let schema = Arc::new(Schema::new(vec![
2216            Field::new("a", DataType::Int32, false),
2217            Field::new("b", DataType::Int32, false),
2218            Field::new("c", DataType::Int32, false),
2219        ]));
2220
2221        let input = Arc::new(EmptyExec::new(Arc::clone(&schema)));
2222
2223        // Create a filter predicate: a > 10
2224        let predicate = Arc::new(BinaryExpr::new(
2225            Arc::new(Column::new("a", 0)),
2226            Operator::Gt,
2227            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2228        ));
2229
2230        // Create filter with projection [0, 2] (columns a and c) using builder
2231        let projection = Some(vec![0, 2]);
2232        let filter = FilterExecBuilder::new(predicate, input)
2233            .apply_projection(projection.clone())
2234            .unwrap()
2235            .build()?;
2236
2237        // Verify projection is set correctly
2238        assert_eq!(filter.projection(), &Some([0, 2].into()));
2239
2240        // Verify schema contains only projected columns
2241        let output_schema = filter.schema();
2242        assert_eq!(output_schema.fields().len(), 2);
2243        assert_eq!(output_schema.field(0).name(), "a");
2244        assert_eq!(output_schema.field(1).name(), "c");
2245
2246        Ok(())
2247    }
2248
2249    #[tokio::test]
2250    async fn test_builder_without_projection() -> Result<()> {
2251        let schema = Arc::new(Schema::new(vec![
2252            Field::new("a", DataType::Int32, false),
2253            Field::new("b", DataType::Int32, false),
2254        ]));
2255
2256        let input = Arc::new(EmptyExec::new(Arc::clone(&schema)));
2257
2258        let predicate = Arc::new(BinaryExpr::new(
2259            Arc::new(Column::new("a", 0)),
2260            Operator::Gt,
2261            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
2262        ));
2263
2264        // Create filter without projection using builder
2265        let filter = FilterExecBuilder::new(predicate, input).build()?;
2266
2267        // Verify no projection is set
2268        assert!(filter.projection().is_none());
2269
2270        // Verify schema contains all columns
2271        let output_schema = filter.schema();
2272        assert_eq!(output_schema.fields().len(), 2);
2273
2274        Ok(())
2275    }
2276
2277    #[tokio::test]
2278    async fn test_builder_invalid_projection() -> Result<()> {
2279        let schema = Arc::new(Schema::new(vec![
2280            Field::new("a", DataType::Int32, false),
2281            Field::new("b", DataType::Int32, false),
2282        ]));
2283
2284        let input = Arc::new(EmptyExec::new(Arc::clone(&schema)));
2285
2286        let predicate = Arc::new(BinaryExpr::new(
2287            Arc::new(Column::new("a", 0)),
2288            Operator::Gt,
2289            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
2290        ));
2291
2292        // Try to create filter with invalid projection (index out of bounds) using builder
2293        let result =
2294            FilterExecBuilder::new(predicate, input).apply_projection(Some(vec![0, 5])); // 5 is out of bounds
2295
2296        // Should return an error
2297        assert!(result.is_err());
2298
2299        Ok(())
2300    }
2301
2302    #[tokio::test]
2303    async fn test_builder_vs_with_projection() -> Result<()> {
2304        // This test verifies that the builder with projection produces the same result
2305        // as try_new().with_projection(), but more efficiently (one compute_properties call)
2306        let schema = Schema::new(vec![
2307            Field::new("a", DataType::Int32, false),
2308            Field::new("b", DataType::Int32, false),
2309            Field::new("c", DataType::Int32, false),
2310            Field::new("d", DataType::Int32, false),
2311        ]);
2312
2313        let input = Arc::new(StatisticsExec::new(
2314            Statistics {
2315                num_rows: Precision::Inexact(1000),
2316                total_byte_size: Precision::Inexact(4000),
2317                column_statistics: vec![
2318                    ColumnStatistics {
2319                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
2320                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
2321                        ..Default::default()
2322                    },
2323                    ColumnStatistics {
2324                        ..Default::default()
2325                    },
2326                    ColumnStatistics {
2327                        ..Default::default()
2328                    },
2329                    ColumnStatistics {
2330                        ..Default::default()
2331                    },
2332                ],
2333            },
2334            schema,
2335        ));
2336        let input: Arc<dyn ExecutionPlan> = input;
2337
2338        let predicate: Arc<dyn PhysicalExpr> = Arc::new(BinaryExpr::new(
2339            Arc::new(Column::new("a", 0)),
2340            Operator::Lt,
2341            Arc::new(Literal::new(ScalarValue::Int32(Some(50)))),
2342        ));
2343
2344        let projection = Some(vec![0, 2]);
2345
2346        // Method 1: Builder with projection (one call to compute_properties)
2347        let filter1 = FilterExecBuilder::new(Arc::clone(&predicate), Arc::clone(&input))
2348            .apply_projection(projection.clone())
2349            .unwrap()
2350            .build()?;
2351
2352        // Method 2: Also using builder for comparison (deprecated try_new().with_projection() removed)
2353        let filter2 = FilterExecBuilder::new(predicate, input)
2354            .apply_projection(projection)
2355            .unwrap()
2356            .build()?;
2357
2358        // Both methods should produce equivalent results
2359        assert_eq!(filter1.schema(), filter2.schema());
2360        assert_eq!(filter1.projection(), filter2.projection());
2361
2362        // Verify statistics are the same
2363        let stats1 =
2364            StatisticsContext::new().compute(&filter1, &StatisticsArgs::new())?;
2365        let stats2 =
2366            StatisticsContext::new().compute(&filter2, &StatisticsArgs::new())?;
2367        assert_eq!(stats1.num_rows, stats2.num_rows);
2368        assert_eq!(stats1.total_byte_size, stats2.total_byte_size);
2369
2370        Ok(())
2371    }
2372
2373    #[tokio::test]
2374    async fn test_builder_statistics_with_projection() -> Result<()> {
2375        // Test that statistics are correctly computed when using builder with projection
2376        let schema = Schema::new(vec![
2377            Field::new("a", DataType::Int32, false),
2378            Field::new("b", DataType::Int32, false),
2379            Field::new("c", DataType::Int32, false),
2380        ]);
2381
2382        let input = Arc::new(StatisticsExec::new(
2383            Statistics {
2384                num_rows: Precision::Inexact(1000),
2385                total_byte_size: Precision::Inexact(12000),
2386                column_statistics: vec![
2387                    ColumnStatistics {
2388                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
2389                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
2390                        ..Default::default()
2391                    },
2392                    ColumnStatistics {
2393                        min_value: Precision::Inexact(ScalarValue::Int32(Some(10))),
2394                        max_value: Precision::Inexact(ScalarValue::Int32(Some(200))),
2395                        ..Default::default()
2396                    },
2397                    ColumnStatistics {
2398                        min_value: Precision::Inexact(ScalarValue::Int32(Some(5))),
2399                        max_value: Precision::Inexact(ScalarValue::Int32(Some(50))),
2400                        ..Default::default()
2401                    },
2402                ],
2403            },
2404            schema,
2405        ));
2406
2407        // Filter: a < 50, Project: [0, 2]
2408        let predicate = Arc::new(BinaryExpr::new(
2409            Arc::new(Column::new("a", 0)),
2410            Operator::Lt,
2411            Arc::new(Literal::new(ScalarValue::Int32(Some(50)))),
2412        ));
2413
2414        let filter = FilterExecBuilder::new(predicate, input)
2415            .apply_projection(Some(vec![0, 2]))
2416            .unwrap()
2417            .build()?;
2418
2419        let statistics =
2420            StatisticsContext::new().compute(&filter, &StatisticsArgs::new())?;
2421
2422        // Verify statistics reflect both filtering and projection
2423        assert!(matches!(statistics.num_rows, Precision::Inexact(_)));
2424
2425        // Schema should only have 2 columns after projection
2426        assert_eq!(filter.schema().fields().len(), 2);
2427
2428        Ok(())
2429    }
2430
2431    #[test]
2432    fn test_builder_predicate_validation() -> Result<()> {
2433        // Test that builder validates predicate type correctly
2434        let schema = Arc::new(Schema::new(vec![
2435            Field::new("a", DataType::Int32, false),
2436            Field::new("b", DataType::Int32, false),
2437        ]));
2438
2439        let input = Arc::new(EmptyExec::new(Arc::clone(&schema)));
2440
2441        // Create a predicate that doesn't return boolean (returns Int32)
2442        let invalid_predicate = Arc::new(Column::new("a", 0));
2443
2444        // Should fail because predicate doesn't return boolean
2445        let result = FilterExecBuilder::new(invalid_predicate, input)
2446            .apply_projection(Some(vec![0]))
2447            .unwrap()
2448            .build();
2449
2450        assert!(result.is_err());
2451
2452        Ok(())
2453    }
2454
2455    #[tokio::test]
2456    async fn test_builder_projection_composition() -> Result<()> {
2457        // Test that calling apply_projection multiple times composes projections
2458        // If initial projection is [0, 2, 3] and we call apply_projection([0, 2]),
2459        // the result should be [0, 3] (indices 0 and 2 of [0, 2, 3])
2460        let schema = Arc::new(Schema::new(vec![
2461            Field::new("a", DataType::Int32, false),
2462            Field::new("b", DataType::Int32, false),
2463            Field::new("c", DataType::Int32, false),
2464            Field::new("d", DataType::Int32, false),
2465        ]));
2466
2467        let input = Arc::new(EmptyExec::new(Arc::clone(&schema)));
2468
2469        // Create a filter predicate: a > 10
2470        let predicate = Arc::new(BinaryExpr::new(
2471            Arc::new(Column::new("a", 0)),
2472            Operator::Gt,
2473            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2474        ));
2475
2476        // First projection: [0, 2, 3] -> select columns a, c, d
2477        // Second projection: [0, 2] -> select indices 0 and 2 of [0, 2, 3] -> [0, 3]
2478        // Final result: columns a and d
2479        let filter = FilterExecBuilder::new(predicate, input)
2480            .apply_projection(Some(vec![0, 2, 3]))?
2481            .apply_projection(Some(vec![0, 2]))?
2482            .build()?;
2483
2484        // Verify composed projection is [0, 3]
2485        assert_eq!(filter.projection(), &Some([0, 3].into()));
2486
2487        // Verify schema contains only columns a and d
2488        let output_schema = filter.schema();
2489        assert_eq!(output_schema.fields().len(), 2);
2490        assert_eq!(output_schema.field(0).name(), "a");
2491        assert_eq!(output_schema.field(1).name(), "d");
2492
2493        Ok(())
2494    }
2495
2496    #[tokio::test]
2497    async fn test_builder_projection_composition_none_clears() -> Result<()> {
2498        // Test that passing None clears the projection
2499        let schema = Arc::new(Schema::new(vec![
2500            Field::new("a", DataType::Int32, false),
2501            Field::new("b", DataType::Int32, false),
2502        ]));
2503
2504        let input = Arc::new(EmptyExec::new(Arc::clone(&schema)));
2505
2506        let predicate = Arc::new(BinaryExpr::new(
2507            Arc::new(Column::new("a", 0)),
2508            Operator::Gt,
2509            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2510        ));
2511
2512        // Set a projection then clear it with None
2513        let filter = FilterExecBuilder::new(predicate, input)
2514            .apply_projection(Some(vec![0]))?
2515            .apply_projection(None)?
2516            .build()?;
2517
2518        // Projection should be cleared
2519        assert_eq!(filter.projection(), &None);
2520
2521        // Schema should have all columns
2522        let output_schema = filter.schema();
2523        assert_eq!(output_schema.fields().len(), 2);
2524
2525        Ok(())
2526    }
2527
2528    #[test]
2529    fn test_filter_with_projection_remaps_post_phase_parent_filters() -> Result<()> {
2530        // Test that FilterExec with a projection must remap parent dynamic
2531        // filter column indices from its output schema to the input schema
2532        // before passing them to the child.
2533        let input_schema = Arc::new(Schema::new(vec![
2534            Field::new("a", DataType::Int32, false),
2535            Field::new("b", DataType::Utf8, false),
2536            Field::new("c", DataType::Float64, false),
2537        ]));
2538        let input = Arc::new(EmptyExec::new(Arc::clone(&input_schema)));
2539
2540        // FilterExec: a > 0, projection=[c@2]
2541        let predicate = Arc::new(BinaryExpr::new(
2542            Arc::new(Column::new("a", 0)),
2543            Operator::Gt,
2544            Arc::new(Literal::new(ScalarValue::Int32(Some(0)))),
2545        ));
2546        let filter = FilterExecBuilder::new(predicate, input)
2547            .apply_projection(Some(vec![2]))?
2548            .build()?;
2549
2550        // Output schema should be [c:Float64]
2551        let output_schema = filter.schema();
2552        assert_eq!(output_schema.fields().len(), 1);
2553        assert_eq!(output_schema.field(0).name(), "c");
2554
2555        // Simulate a parent dynamic filter referencing output column c@0
2556        let parent_filter: Arc<dyn PhysicalExpr> = Arc::new(Column::new("c", 0));
2557
2558        let config = ConfigOptions::new();
2559        let desc = filter.gather_filters_for_pushdown(
2560            FilterPushdownPhase::Post,
2561            vec![parent_filter],
2562            &config,
2563        )?;
2564
2565        // The filter pushed to the child must reference c@2 (input schema),
2566        // not c@0 (output schema).
2567        let parent_filters = desc.parent_filters();
2568        assert_eq!(parent_filters.len(), 1); // one child
2569        assert_eq!(parent_filters[0].len(), 1); // one filter
2570        let remapped = &parent_filters[0][0].predicate;
2571        let display = format!("{remapped}");
2572        assert_eq!(
2573            display, "c@2",
2574            "Post-phase parent filter column index must be remapped \
2575             from output schema (c@0) to input schema (c@2)"
2576        );
2577
2578        Ok(())
2579    }
2580
2581    /// Regression test for https://github.com/apache/datafusion/issues/20194
2582    ///
2583    /// `collect_columns_from_predicate_inner` should only extract equality
2584    /// pairs where at least one side is a Column. Pairs like
2585    /// `complex_expr = literal` must not create equivalence classes because
2586    /// `normalize_expr`'s deep traversal would replace the literal inside
2587    /// unrelated expressions (e.g. sort keys) with the complex expression.
2588    #[test]
2589    fn test_collect_columns_skips_non_column_pairs() -> Result<()> {
2590        let schema = test::aggr_test_schema();
2591
2592        // Simulate: nvl(c2, 0) = 0  →  (c2 IS DISTINCT FROM 0) = 0
2593        // Neither side is a Column, so this should NOT be extracted.
2594        let complex_expr: Arc<dyn PhysicalExpr> = binary(
2595            col("c2", &schema)?,
2596            Operator::IsDistinctFrom,
2597            lit(0u32),
2598            &schema,
2599        )?;
2600        let predicate: Arc<dyn PhysicalExpr> =
2601            binary(complex_expr, Operator::Eq, lit(0u32), &schema)?;
2602
2603        let (equal_pairs, _) = collect_columns_from_predicate_inner(&predicate);
2604        assert_eq!(
2605            0,
2606            equal_pairs.len(),
2607            "Should not extract equality pairs where neither side is a Column"
2608        );
2609
2610        // But col = literal should still be extracted
2611        let predicate: Arc<dyn PhysicalExpr> =
2612            binary(col("c2", &schema)?, Operator::Eq, lit(0u32), &schema)?;
2613        let (equal_pairs, _) = collect_columns_from_predicate_inner(&predicate);
2614        assert_eq!(
2615            1,
2616            equal_pairs.len(),
2617            "Should extract equality pairs where one side is a Column"
2618        );
2619
2620        Ok(())
2621    }
2622
2623    /// Columns with Absent min/max statistics should remain Absent after
2624    /// FilterExec.
2625    #[tokio::test]
2626    async fn test_filter_statistics_absent_columns_stay_absent() -> Result<()> {
2627        let schema = Schema::new(vec![
2628            Field::new("a", DataType::Int32, false),
2629            Field::new("b", DataType::Int32, false),
2630        ]);
2631        let input = Arc::new(StatisticsExec::new(
2632            Statistics {
2633                num_rows: Precision::Inexact(1000),
2634                total_byte_size: Precision::Absent,
2635                column_statistics: vec![
2636                    ColumnStatistics::default(),
2637                    ColumnStatistics::default(),
2638                ],
2639            },
2640            schema.clone(),
2641        ));
2642
2643        let predicate = Arc::new(BinaryExpr::new(
2644            Arc::new(Column::new("a", 0)),
2645            Operator::Eq,
2646            Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
2647        ));
2648        let filter: Arc<dyn ExecutionPlan> =
2649            Arc::new(FilterExec::try_new(predicate, input)?);
2650
2651        let statistics =
2652            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
2653        let col_b_stats = &statistics.column_statistics[1];
2654        assert_eq!(col_b_stats.min_value, Precision::Absent);
2655        assert_eq!(col_b_stats.max_value, Precision::Absent);
2656
2657        Ok(())
2658    }
2659
2660    #[tokio::test]
2661    async fn test_filter_statistics_equality_ndv() -> Result<()> {
2662        #[expect(clippy::type_complexity)]
2663        let cases: Vec<(
2664            &str,
2665            Vec<Field>,
2666            Vec<ColumnStatistics>,
2667            Arc<dyn PhysicalExpr>,
2668            Vec<Precision<usize>>,
2669        )> = vec![
2670            (
2671                "utf8 equality",
2672                vec![Field::new("name", DataType::Utf8, false)],
2673                vec![ColumnStatistics {
2674                    distinct_count: Precision::Inexact(50),
2675                    ..Default::default()
2676                }],
2677                Arc::new(BinaryExpr::new(
2678                    Arc::new(Column::new("name", 0)),
2679                    Operator::Eq,
2680                    Arc::new(Literal::new(ScalarValue::Utf8(Some("hello".to_string())))),
2681                )),
2682                vec![Precision::Exact(1)],
2683            ),
2684            (
2685                "utf8view equality",
2686                vec![Field::new("name", DataType::Utf8View, false)],
2687                vec![ColumnStatistics {
2688                    distinct_count: Precision::Inexact(50),
2689                    ..Default::default()
2690                }],
2691                Arc::new(BinaryExpr::new(
2692                    Arc::new(Column::new("name", 0)),
2693                    Operator::Eq,
2694                    Arc::new(Literal::new(ScalarValue::Utf8View(Some(
2695                        "hello".to_string(),
2696                    )))),
2697                )),
2698                vec![Precision::Exact(1)],
2699            ),
2700            (
2701                "largeutf8 equality",
2702                vec![Field::new("name", DataType::LargeUtf8, false)],
2703                vec![ColumnStatistics {
2704                    distinct_count: Precision::Inexact(50),
2705                    ..Default::default()
2706                }],
2707                Arc::new(BinaryExpr::new(
2708                    Arc::new(Column::new("name", 0)),
2709                    Operator::Eq,
2710                    Arc::new(Literal::new(ScalarValue::LargeUtf8(Some(
2711                        "hello".to_string(),
2712                    )))),
2713                )),
2714                vec![Precision::Exact(1)],
2715            ),
2716            (
2717                "utf8 reversed (literal = column)",
2718                vec![Field::new("name", DataType::Utf8, false)],
2719                vec![ColumnStatistics {
2720                    distinct_count: Precision::Inexact(50),
2721                    ..Default::default()
2722                }],
2723                Arc::new(BinaryExpr::new(
2724                    Arc::new(Literal::new(ScalarValue::Utf8(Some("hello".to_string())))),
2725                    Operator::Eq,
2726                    Arc::new(Column::new("name", 0)),
2727                )),
2728                vec![Precision::Exact(1)],
2729            ),
2730            (
2731                "OR is not collapsed to NDV=1, but NDV is capped at filtered rows",
2732                vec![Field::new("name", DataType::Utf8, false)],
2733                vec![ColumnStatistics {
2734                    distinct_count: Precision::Inexact(50),
2735                    ..Default::default()
2736                }],
2737                Arc::new(BinaryExpr::new(
2738                    Arc::new(BinaryExpr::new(
2739                        Arc::new(Column::new("name", 0)),
2740                        Operator::Eq,
2741                        Arc::new(Literal::new(ScalarValue::Utf8(Some("a".to_string())))),
2742                    )),
2743                    Operator::Or,
2744                    Arc::new(BinaryExpr::new(
2745                        Arc::new(Column::new("name", 0)),
2746                        Operator::Eq,
2747                        Arc::new(Literal::new(ScalarValue::Utf8(Some("b".to_string())))),
2748                    )),
2749                )),
2750                // Input NDV is 50, but the 20% default selectivity on 100 rows
2751                // estimates 20 output rows, so NDV is capped at 20.
2752                vec![Precision::Inexact(20)],
2753            ),
2754            (
2755                "AND with mixed types (Utf8 + Int32)",
2756                vec![
2757                    Field::new("name", DataType::Utf8, false),
2758                    Field::new("age", DataType::Int32, false),
2759                ],
2760                vec![
2761                    ColumnStatistics {
2762                        distinct_count: Precision::Inexact(50),
2763                        ..Default::default()
2764                    },
2765                    ColumnStatistics {
2766                        distinct_count: Precision::Inexact(80),
2767                        ..Default::default()
2768                    },
2769                ],
2770                Arc::new(BinaryExpr::new(
2771                    Arc::new(BinaryExpr::new(
2772                        Arc::new(Column::new("name", 0)),
2773                        Operator::Eq,
2774                        Arc::new(Literal::new(ScalarValue::Utf8(Some(
2775                            "hello".to_string(),
2776                        )))),
2777                    )),
2778                    Operator::And,
2779                    Arc::new(BinaryExpr::new(
2780                        Arc::new(Column::new("age", 1)),
2781                        Operator::Eq,
2782                        Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
2783                    )),
2784                )),
2785                vec![Precision::Exact(1), Precision::Exact(1)],
2786            ),
2787            (
2788                "numeric equality with min/max bounds (interval analysis path)",
2789                vec![Field::new("a", DataType::Int32, false)],
2790                vec![ColumnStatistics {
2791                    min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
2792                    max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
2793                    distinct_count: Precision::Inexact(80),
2794                    ..Default::default()
2795                }],
2796                Arc::new(BinaryExpr::new(
2797                    Arc::new(Column::new("a", 0)),
2798                    Operator::Eq,
2799                    Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
2800                )),
2801                vec![Precision::Exact(1)],
2802            ),
2803            (
2804                "timestamp equality",
2805                vec![Field::new(
2806                    "ts",
2807                    DataType::Timestamp(arrow::datatypes::TimeUnit::Nanosecond, None),
2808                    false,
2809                )],
2810                vec![ColumnStatistics {
2811                    distinct_count: Precision::Inexact(500),
2812                    ..Default::default()
2813                }],
2814                Arc::new(BinaryExpr::new(
2815                    Arc::new(Column::new("ts", 0)),
2816                    Operator::Eq,
2817                    Arc::new(Literal::new(ScalarValue::TimestampNanosecond(
2818                        Some(1_609_459_200_000_000_000),
2819                        None,
2820                    ))),
2821                )),
2822                vec![Precision::Exact(1)],
2823            ),
2824            (
2825                "contradictory numeric equality (infeasible)",
2826                vec![Field::new("a", DataType::Int32, false)],
2827                vec![ColumnStatistics {
2828                    distinct_count: Precision::Inexact(50),
2829                    ..Default::default()
2830                }],
2831                Arc::new(BinaryExpr::new(
2832                    Arc::new(BinaryExpr::new(
2833                        Arc::new(Column::new("a", 0)),
2834                        Operator::Eq,
2835                        Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
2836                    )),
2837                    Operator::And,
2838                    Arc::new(BinaryExpr::new(
2839                        Arc::new(Column::new("a", 0)),
2840                        Operator::Eq,
2841                        Arc::new(Literal::new(ScalarValue::Int32(Some(99)))),
2842                    )),
2843                )),
2844                vec![Precision::Exact(0)],
2845            ),
2846            (
2847                "utf8 equality with absent input NDV",
2848                vec![Field::new("name", DataType::Utf8, false)],
2849                vec![ColumnStatistics {
2850                    distinct_count: Precision::Absent,
2851                    ..Default::default()
2852                }],
2853                Arc::new(BinaryExpr::new(
2854                    Arc::new(Column::new("name", 0)),
2855                    Operator::Eq,
2856                    Arc::new(Literal::new(ScalarValue::Utf8(Some("hello".to_string())))),
2857                )),
2858                vec![Precision::Exact(1)],
2859            ),
2860            (
2861                "contradictory utf8 equality (infeasible)",
2862                vec![Field::new("name", DataType::Utf8, false)],
2863                vec![ColumnStatistics {
2864                    distinct_count: Precision::Inexact(100),
2865                    ..Default::default()
2866                }],
2867                Arc::new(BinaryExpr::new(
2868                    Arc::new(BinaryExpr::new(
2869                        Arc::new(Column::new("name", 0)),
2870                        Operator::Eq,
2871                        Arc::new(Literal::new(ScalarValue::Utf8(Some(
2872                            "alice".to_string(),
2873                        )))),
2874                    )),
2875                    Operator::And,
2876                    Arc::new(BinaryExpr::new(
2877                        Arc::new(Column::new("name", 0)),
2878                        Operator::Eq,
2879                        Arc::new(Literal::new(ScalarValue::Utf8(Some(
2880                            "bob".to_string(),
2881                        )))),
2882                    )),
2883                )),
2884                vec![Precision::Exact(0)],
2885            ),
2886            (
2887                "redundant same-value equality combined with another column",
2888                vec![
2889                    Field::new("a", DataType::Int32, false),
2890                    Field::new("b", DataType::Int32, false),
2891                ],
2892                vec![
2893                    ColumnStatistics {
2894                        distinct_count: Precision::Inexact(80),
2895                        ..Default::default()
2896                    },
2897                    ColumnStatistics {
2898                        distinct_count: Precision::Inexact(40),
2899                        ..Default::default()
2900                    },
2901                ],
2902                Arc::new(BinaryExpr::new(
2903                    Arc::new(BinaryExpr::new(
2904                        Arc::new(BinaryExpr::new(
2905                            Arc::new(Column::new("a", 0)),
2906                            Operator::Eq,
2907                            Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
2908                        )),
2909                        Operator::And,
2910                        Arc::new(BinaryExpr::new(
2911                            Arc::new(Column::new("a", 0)),
2912                            Operator::Eq,
2913                            Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
2914                        )),
2915                    )),
2916                    Operator::And,
2917                    Arc::new(BinaryExpr::new(
2918                        Arc::new(Column::new("b", 1)),
2919                        Operator::Eq,
2920                        Arc::new(Literal::new(ScalarValue::Int32(Some(2)))),
2921                    )),
2922                )),
2923                vec![Precision::Exact(1), Precision::Exact(1)],
2924            ),
2925        ];
2926
2927        for (desc, fields, col_stats, predicate, expected_ndvs) in cases {
2928            let schema = Schema::new(fields);
2929            let input = Arc::new(StatisticsExec::new(
2930                Statistics {
2931                    num_rows: Precision::Inexact(100),
2932                    total_byte_size: Precision::Inexact(1000),
2933                    column_statistics: col_stats,
2934                },
2935                schema.clone(),
2936            ));
2937            let filter: Arc<dyn ExecutionPlan> =
2938                Arc::new(FilterExec::try_new(predicate, input)?);
2939            let statistics = StatisticsContext::new()
2940                .compute(filter.as_ref(), &StatisticsArgs::new())?;
2941
2942            for (i, expected) in expected_ndvs.iter().enumerate() {
2943                assert_eq!(
2944                    statistics.column_statistics[i].distinct_count, *expected,
2945                    "case '{desc}': column {i} NDV mismatch"
2946                );
2947            }
2948        }
2949        Ok(())
2950    }
2951
2952    #[tokio::test]
2953    async fn test_filter_statistics_preserves_exactly_empty_input() -> Result<()> {
2954        // A satisfiable predicate over an exactly empty input: the filter cannot
2955        // produce rows, so the whole estimate stays exact. Column `b` is not
2956        // mentioned by the predicate, so its null and distinct counts go through
2957        // the generic row cap.
2958        let schema = Schema::new(vec![
2959            Field::new("a", DataType::Int32, true),
2960            Field::new("b", DataType::Int32, true),
2961        ]);
2962        let input_stats = Statistics {
2963            num_rows: Precision::Exact(0),
2964            total_byte_size: Precision::Exact(0),
2965            column_statistics: vec![
2966                ColumnStatistics {
2967                    null_count: Precision::Exact(0),
2968                    byte_size: Precision::Exact(0),
2969                    ..Default::default()
2970                },
2971                ColumnStatistics {
2972                    null_count: Precision::Exact(3),
2973                    distinct_count: Precision::Exact(7),
2974                    byte_size: Precision::Exact(0),
2975                    ..Default::default()
2976                },
2977            ],
2978        };
2979        let predicate = Arc::new(BinaryExpr::new(
2980            Arc::new(Column::new("a", 0)),
2981            Operator::Gt,
2982            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
2983        ));
2984
2985        let input = Arc::new(StatisticsExec::new(input_stats, schema.clone()));
2986        let filter: Arc<dyn ExecutionPlan> =
2987            Arc::new(FilterExec::try_new(predicate, input)?);
2988        let statistics =
2989            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
2990
2991        assert_eq!(statistics.num_rows, Precision::Exact(0));
2992        assert_eq!(statistics.total_byte_size, Precision::Exact(0));
2993        assert_eq!(
2994            statistics.column_statistics[0].byte_size,
2995            Precision::Exact(0)
2996        );
2997        assert_eq!(
2998            statistics.column_statistics[1].null_count,
2999            Precision::Exact(0)
3000        );
3001        assert_eq!(
3002            statistics.column_statistics[1].distinct_count,
3003            Precision::Exact(0)
3004        );
3005
3006        // A contradictory predicate (`a = 1 AND a = 2`) discards all rows, the
3007        // output is empty independently of the input.
3008        let input = Arc::new(StatisticsExec::new(
3009            Statistics {
3010                num_rows: Precision::Inexact(1000),
3011                total_byte_size: Precision::Inexact(8000),
3012                column_statistics: vec![ColumnStatistics::new_unknown(); 2],
3013            },
3014            schema,
3015        ));
3016        let contradiction = Arc::new(BinaryExpr::new(
3017            Arc::new(BinaryExpr::new(
3018                Arc::new(Column::new("a", 0)),
3019                Operator::Eq,
3020                Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
3021            )),
3022            Operator::And,
3023            Arc::new(BinaryExpr::new(
3024                Arc::new(Column::new("a", 0)),
3025                Operator::Eq,
3026                Arc::new(Literal::new(ScalarValue::Int32(Some(2)))),
3027            )),
3028        ));
3029        let filter: Arc<dyn ExecutionPlan> =
3030            Arc::new(FilterExec::try_new(contradiction, input)?);
3031        let statistics =
3032            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3033
3034        assert_eq!(statistics.num_rows, Precision::Exact(0));
3035        assert_eq!(statistics.total_byte_size, Precision::Exact(0));
3036
3037        Ok(())
3038    }
3039
3040    #[tokio::test]
3041    async fn test_filter_statistics_exact_empty_input_zeroes_byte_size() -> Result<()> {
3042        let cases = [
3043            ("absent", Precision::Absent, Precision::Absent),
3044            ("inexact", Precision::Inexact(8000), Precision::Inexact(400)),
3045        ];
3046
3047        for (desc, input_total_byte_size, input_byte_size) in cases {
3048            let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
3049            let input_stats = Statistics {
3050                num_rows: Precision::Exact(0),
3051                total_byte_size: input_total_byte_size,
3052                column_statistics: vec![ColumnStatistics {
3053                    byte_size: input_byte_size,
3054                    ..Default::default()
3055                }],
3056            };
3057            let predicate = Arc::new(BinaryExpr::new(
3058                Arc::new(Column::new("a", 0)),
3059                Operator::Gt,
3060                Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
3061            ));
3062
3063            let input = Arc::new(StatisticsExec::new(input_stats, schema));
3064            let filter: Arc<dyn ExecutionPlan> =
3065                Arc::new(FilterExec::try_new(predicate, input)?);
3066            let statistics = StatisticsContext::new()
3067                .compute(filter.as_ref(), &StatisticsArgs::new())?;
3068
3069            assert_eq!(
3070                statistics.num_rows,
3071                Precision::Exact(0),
3072                "case '{desc}': num_rows mismatch"
3073            );
3074            assert_eq!(
3075                statistics.total_byte_size,
3076                Precision::Exact(0),
3077                "case '{desc}': total_byte_size mismatch"
3078            );
3079            assert_eq!(
3080                statistics.column_statistics[0].byte_size,
3081                Precision::Exact(0),
3082                "case '{desc}': byte_size mismatch"
3083            );
3084        }
3085
3086        Ok(())
3087    }
3088
3089    #[tokio::test]
3090    async fn test_filter_statistics_empty_input_equality_ndv_zero() -> Result<()> {
3091        let cases: Vec<(&str, Schema, Statistics, Arc<dyn PhysicalExpr>)> = vec![
3092            (
3093                "fallback string equality",
3094                Schema::new(vec![Field::new("name", DataType::Utf8, true)]),
3095                Statistics {
3096                    num_rows: Precision::Exact(0),
3097                    total_byte_size: Precision::Exact(0),
3098                    column_statistics: vec![ColumnStatistics {
3099                        distinct_count: Precision::Exact(0),
3100                        null_count: Precision::Exact(0),
3101                        byte_size: Precision::Exact(0),
3102                        ..Default::default()
3103                    }],
3104                },
3105                Arc::new(BinaryExpr::new(
3106                    Arc::new(Column::new("name", 0)),
3107                    Operator::Eq,
3108                    Arc::new(Literal::new(ScalarValue::Utf8(Some("x".to_string())))),
3109                )),
3110            ),
3111            (
3112                "interval numeric equality",
3113                Schema::new(vec![Field::new("a", DataType::Int32, true)]),
3114                Statistics {
3115                    num_rows: Precision::Exact(0),
3116                    total_byte_size: Precision::Exact(0),
3117                    column_statistics: vec![ColumnStatistics {
3118                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
3119                        max_value: Precision::Inexact(ScalarValue::Int32(Some(10))),
3120                        distinct_count: Precision::Exact(0),
3121                        null_count: Precision::Exact(0),
3122                        byte_size: Precision::Exact(0),
3123                        ..Default::default()
3124                    }],
3125                },
3126                Arc::new(BinaryExpr::new(
3127                    Arc::new(Column::new("a", 0)),
3128                    Operator::Eq,
3129                    Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
3130                )),
3131            ),
3132        ];
3133
3134        for (desc, schema, input_stats, predicate) in cases {
3135            let input = Arc::new(StatisticsExec::new(input_stats, schema));
3136            let filter: Arc<dyn ExecutionPlan> =
3137                Arc::new(FilterExec::try_new(predicate, input)?);
3138            let statistics = StatisticsContext::new()
3139                .compute(filter.as_ref(), &StatisticsArgs::new())?;
3140
3141            assert_eq!(
3142                statistics.num_rows,
3143                Precision::Exact(0),
3144                "case '{desc}': row count mismatch"
3145            );
3146            assert_eq!(
3147                statistics.column_statistics[0].distinct_count,
3148                Precision::Exact(0),
3149                "case '{desc}': NDV should be capped at zero rows"
3150            );
3151        }
3152        Ok(())
3153    }
3154
3155    #[tokio::test]
3156    async fn test_filter_statistics_and_equality_ndv() -> Result<()> {
3157        let schema = Schema::new(vec![
3158            Field::new("a", DataType::Int32, false),
3159            Field::new("b", DataType::Int32, false),
3160            Field::new("c", DataType::Int32, false),
3161        ]);
3162        let input = Arc::new(StatisticsExec::new(
3163            Statistics {
3164                num_rows: Precision::Inexact(100),
3165                total_byte_size: Precision::Inexact(1200),
3166                column_statistics: vec![
3167                    ColumnStatistics {
3168                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
3169                        max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
3170                        null_count: Precision::Inexact(80),
3171                        distinct_count: Precision::Inexact(80),
3172                        ..Default::default()
3173                    },
3174                    ColumnStatistics {
3175                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
3176                        max_value: Precision::Inexact(ScalarValue::Int32(Some(50))),
3177                        distinct_count: Precision::Inexact(40),
3178                        ..Default::default()
3179                    },
3180                    ColumnStatistics {
3181                        min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
3182                        max_value: Precision::Inexact(ScalarValue::Int32(Some(200))),
3183                        null_count: Precision::Inexact(90),
3184                        distinct_count: Precision::Inexact(150),
3185                        ..Default::default()
3186                    },
3187                ],
3188            },
3189            schema.clone(),
3190        ));
3191
3192        // a = 42 AND b > 10 AND c = 7
3193        let predicate = Arc::new(BinaryExpr::new(
3194            Arc::new(BinaryExpr::new(
3195                Arc::new(BinaryExpr::new(
3196                    Arc::new(Column::new("a", 0)),
3197                    Operator::Eq,
3198                    Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3199                )),
3200                Operator::And,
3201                Arc::new(BinaryExpr::new(
3202                    Arc::new(Column::new("b", 1)),
3203                    Operator::Gt,
3204                    Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
3205                )),
3206            )),
3207            Operator::And,
3208            Arc::new(BinaryExpr::new(
3209                Arc::new(Column::new("c", 2)),
3210                Operator::Eq,
3211                Arc::new(Literal::new(ScalarValue::Int32(Some(7)))),
3212            )),
3213        ));
3214        let filter: Arc<dyn ExecutionPlan> =
3215            Arc::new(FilterExec::try_new(predicate, input)?);
3216        let statistics =
3217            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3218        // Equality predicates collapse NDV and reject nulls for their columns.
3219        assert_eq!(
3220            statistics.column_statistics[0].distinct_count,
3221            Precision::Exact(1)
3222        );
3223        assert_eq!(
3224            statistics.column_statistics[0].null_count,
3225            Precision::Exact(0)
3226        );
3227        // b > 10 narrows to [11, 50] but doesn't collapse to a single value.
3228        // The combined selectivity of a=42 (1/80) and c=7 (1/150) on 100 rows
3229        // computes num_rows = 1, so NDV is capped at the row count: min(40, 1) = 1.
3230        assert_eq!(
3231            statistics.column_statistics[1].distinct_count,
3232            Precision::Inexact(1)
3233        );
3234        assert_eq!(
3235            statistics.column_statistics[2].distinct_count,
3236            Precision::Exact(1)
3237        );
3238        assert_eq!(
3239            statistics.column_statistics[2].null_count,
3240            Precision::Exact(0)
3241        );
3242        Ok(())
3243    }
3244
3245    #[tokio::test]
3246    async fn test_filter_statistics_equality_absent_bounds_ndv() -> Result<()> {
3247        // a: ndv=80, no min/max
3248        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
3249        let input = Arc::new(StatisticsExec::new(
3250            Statistics {
3251                num_rows: Precision::Inexact(100),
3252                total_byte_size: Precision::Inexact(400),
3253                column_statistics: vec![ColumnStatistics {
3254                    distinct_count: Precision::Inexact(80),
3255                    ..Default::default()
3256                }],
3257            },
3258            schema.clone(),
3259        ));
3260
3261        // Even without input bounds, interval analysis can derive singleton
3262        // bounds from the equality itself.
3263        let predicate = Arc::new(BinaryExpr::new(
3264            Arc::new(Column::new("a", 0)),
3265            Operator::Eq,
3266            Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3267        ));
3268        let filter: Arc<dyn ExecutionPlan> =
3269            Arc::new(FilterExec::try_new(predicate, input)?);
3270        let statistics =
3271            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3272        assert_eq!(
3273            statistics.column_statistics[0].distinct_count,
3274            Precision::Exact(1)
3275        );
3276        Ok(())
3277    }
3278
3279    #[tokio::test]
3280    async fn test_filter_statistics_equality_int8_ndv() -> Result<()> {
3281        // a: min=-100, max=100, ndv=50
3282        let schema = Schema::new(vec![Field::new("a", DataType::Int8, false)]);
3283        let input = Arc::new(StatisticsExec::new(
3284            Statistics {
3285                num_rows: Precision::Inexact(100),
3286                total_byte_size: Precision::Inexact(100),
3287                column_statistics: vec![ColumnStatistics {
3288                    min_value: Precision::Inexact(ScalarValue::Int8(Some(-100))),
3289                    max_value: Precision::Inexact(ScalarValue::Int8(Some(100))),
3290                    distinct_count: Precision::Inexact(50),
3291                    ..Default::default()
3292                }],
3293            },
3294            schema.clone(),
3295        ));
3296
3297        let predicate = Arc::new(BinaryExpr::new(
3298            Arc::new(Column::new("a", 0)),
3299            Operator::Eq,
3300            Arc::new(Literal::new(ScalarValue::Int8(Some(42)))),
3301        ));
3302        let filter: Arc<dyn ExecutionPlan> =
3303            Arc::new(FilterExec::try_new(predicate, input)?);
3304        let statistics =
3305            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3306        assert_eq!(
3307            statistics.column_statistics[0].distinct_count,
3308            Precision::Exact(1)
3309        );
3310        Ok(())
3311    }
3312
3313    #[tokio::test]
3314    async fn test_filter_statistics_equality_int64_ndv() -> Result<()> {
3315        // a: min=0, max=1_000_000, ndv=100_000
3316        let schema = Schema::new(vec![Field::new("a", DataType::Int64, false)]);
3317        let input = Arc::new(StatisticsExec::new(
3318            Statistics {
3319                num_rows: Precision::Inexact(100_000),
3320                total_byte_size: Precision::Inexact(800_000),
3321                column_statistics: vec![ColumnStatistics {
3322                    min_value: Precision::Inexact(ScalarValue::Int64(Some(0))),
3323                    max_value: Precision::Inexact(ScalarValue::Int64(Some(1_000_000))),
3324                    distinct_count: Precision::Inexact(100_000),
3325                    ..Default::default()
3326                }],
3327            },
3328            schema.clone(),
3329        ));
3330
3331        let predicate = Arc::new(BinaryExpr::new(
3332            Arc::new(Column::new("a", 0)),
3333            Operator::Eq,
3334            Arc::new(Literal::new(ScalarValue::Int64(Some(42)))),
3335        ));
3336        let filter: Arc<dyn ExecutionPlan> =
3337            Arc::new(FilterExec::try_new(predicate, input)?);
3338        let statistics =
3339            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3340        assert_eq!(
3341            statistics.column_statistics[0].distinct_count,
3342            Precision::Exact(1)
3343        );
3344        Ok(())
3345    }
3346
3347    #[tokio::test]
3348    async fn test_filter_statistics_equality_float32_ndv() -> Result<()> {
3349        // a: min=0.0, max=100.0, ndv=50
3350        let schema = Schema::new(vec![Field::new("a", DataType::Float32, false)]);
3351        let input = Arc::new(StatisticsExec::new(
3352            Statistics {
3353                num_rows: Precision::Inexact(100),
3354                total_byte_size: Precision::Inexact(400),
3355                column_statistics: vec![ColumnStatistics {
3356                    min_value: Precision::Inexact(ScalarValue::Float32(Some(0.0))),
3357                    max_value: Precision::Inexact(ScalarValue::Float32(Some(100.0))),
3358                    distinct_count: Precision::Inexact(50),
3359                    ..Default::default()
3360                }],
3361            },
3362            schema.clone(),
3363        ));
3364
3365        let predicate = Arc::new(BinaryExpr::new(
3366            Arc::new(Column::new("a", 0)),
3367            Operator::Eq,
3368            Arc::new(Literal::new(ScalarValue::Float32(Some(42.5)))),
3369        ));
3370        let filter: Arc<dyn ExecutionPlan> =
3371            Arc::new(FilterExec::try_new(predicate, input)?);
3372        let statistics =
3373            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3374        assert_eq!(
3375            statistics.column_statistics[0].distinct_count,
3376            Precision::Exact(1)
3377        );
3378        Ok(())
3379    }
3380
3381    #[tokio::test]
3382    async fn test_filter_statistics_equality_reversed_ndv() -> Result<()> {
3383        // a: min=1, max=100, ndv=80
3384        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
3385        let input = Arc::new(StatisticsExec::new(
3386            Statistics {
3387                num_rows: Precision::Inexact(100),
3388                total_byte_size: Precision::Inexact(400),
3389                column_statistics: vec![ColumnStatistics {
3390                    min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
3391                    max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
3392                    distinct_count: Precision::Inexact(80),
3393                    ..Default::default()
3394                }],
3395            },
3396            schema.clone(),
3397        ));
3398
3399        // 42 = a (literal on the left)
3400        let predicate = Arc::new(BinaryExpr::new(
3401            Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3402            Operator::Eq,
3403            Arc::new(Column::new("a", 0)),
3404        ));
3405        let filter: Arc<dyn ExecutionPlan> =
3406            Arc::new(FilterExec::try_new(predicate, input)?);
3407        let statistics =
3408            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3409        assert_eq!(
3410            statistics.column_statistics[0].distinct_count,
3411            Precision::Exact(1)
3412        );
3413        Ok(())
3414    }
3415
3416    #[tokio::test]
3417    async fn test_filter_statistics_equality_timestamp_ndv() -> Result<()> {
3418        // ts: min=1_000_000_000, max=2_000_000_000, ndv=500
3419        let schema = Schema::new(vec![Field::new(
3420            "ts",
3421            DataType::Timestamp(arrow::datatypes::TimeUnit::Nanosecond, None),
3422            false,
3423        )]);
3424        let input = Arc::new(StatisticsExec::new(
3425            Statistics {
3426                num_rows: Precision::Inexact(1000),
3427                total_byte_size: Precision::Inexact(8000),
3428                column_statistics: vec![ColumnStatistics {
3429                    min_value: Precision::Inexact(ScalarValue::TimestampNanosecond(
3430                        Some(1_000_000_000),
3431                        None,
3432                    )),
3433                    max_value: Precision::Inexact(ScalarValue::TimestampNanosecond(
3434                        Some(2_000_000_000),
3435                        None,
3436                    )),
3437                    distinct_count: Precision::Inexact(500),
3438                    ..Default::default()
3439                }],
3440            },
3441            schema.clone(),
3442        ));
3443
3444        let predicate = Arc::new(BinaryExpr::new(
3445            Arc::new(Column::new("ts", 0)),
3446            Operator::Eq,
3447            Arc::new(Literal::new(ScalarValue::TimestampNanosecond(
3448                Some(1_500_000_000),
3449                None,
3450            ))),
3451        ));
3452        let filter: Arc<dyn ExecutionPlan> =
3453            Arc::new(FilterExec::try_new(predicate, input)?);
3454        let statistics =
3455            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3456        assert_eq!(
3457            statistics.column_statistics[0].distinct_count,
3458            Precision::Exact(1)
3459        );
3460        Ok(())
3461    }
3462
3463    #[test]
3464    fn test_collect_equality_columns() {
3465        use std::collections::HashSet;
3466        // (description, predicate, expected_column_indices, expected_infeasible)
3467        #[expect(clippy::type_complexity)]
3468        let cases: Vec<(&str, Arc<dyn PhysicalExpr>, Vec<usize>, bool)> = vec![
3469            (
3470                "simple col = literal",
3471                Arc::new(BinaryExpr::new(
3472                    Arc::new(Column::new("a", 0)),
3473                    Operator::Eq,
3474                    Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3475                )),
3476                vec![0],
3477                false,
3478            ),
3479            (
3480                "reversed literal = col",
3481                Arc::new(BinaryExpr::new(
3482                    Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3483                    Operator::Eq,
3484                    Arc::new(Column::new("a", 0)),
3485                )),
3486                vec![0],
3487                false,
3488            ),
3489            (
3490                "AND with two equalities",
3491                Arc::new(BinaryExpr::new(
3492                    Arc::new(BinaryExpr::new(
3493                        Arc::new(Column::new("a", 0)),
3494                        Operator::Eq,
3495                        Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3496                    )),
3497                    Operator::And,
3498                    Arc::new(BinaryExpr::new(
3499                        Arc::new(Column::new("b", 1)),
3500                        Operator::Eq,
3501                        Arc::new(Literal::new(ScalarValue::Utf8(Some(
3502                            "hello".to_string(),
3503                        )))),
3504                    )),
3505                )),
3506                vec![0, 1],
3507                false,
3508            ),
3509            (
3510                "OR produces empty set",
3511                Arc::new(BinaryExpr::new(
3512                    Arc::new(BinaryExpr::new(
3513                        Arc::new(Column::new("a", 0)),
3514                        Operator::Eq,
3515                        Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3516                    )),
3517                    Operator::Or,
3518                    Arc::new(BinaryExpr::new(
3519                        Arc::new(Column::new("a", 0)),
3520                        Operator::Eq,
3521                        Arc::new(Literal::new(ScalarValue::Int32(Some(99)))),
3522                    )),
3523                )),
3524                vec![],
3525                false,
3526            ),
3527            (
3528                "greater-than produces empty set",
3529                Arc::new(BinaryExpr::new(
3530                    Arc::new(Column::new("a", 0)),
3531                    Operator::Gt,
3532                    Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3533                )),
3534                vec![],
3535                false,
3536            ),
3537            (
3538                "col = col produces empty set",
3539                Arc::new(BinaryExpr::new(
3540                    Arc::new(Column::new("a", 0)),
3541                    Operator::Eq,
3542                    Arc::new(Column::new("b", 1)),
3543                )),
3544                vec![],
3545                false,
3546            ),
3547            (
3548                "nested AND with three equalities",
3549                Arc::new(BinaryExpr::new(
3550                    Arc::new(BinaryExpr::new(
3551                        Arc::new(BinaryExpr::new(
3552                            Arc::new(Column::new("a", 0)),
3553                            Operator::Eq,
3554                            Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
3555                        )),
3556                        Operator::And,
3557                        Arc::new(BinaryExpr::new(
3558                            Arc::new(Column::new("b", 1)),
3559                            Operator::Eq,
3560                            Arc::new(Literal::new(ScalarValue::Int32(Some(2)))),
3561                        )),
3562                    )),
3563                    Operator::And,
3564                    Arc::new(BinaryExpr::new(
3565                        Arc::new(Column::new("c", 2)),
3566                        Operator::Eq,
3567                        Arc::new(Literal::new(ScalarValue::Int32(Some(3)))),
3568                    )),
3569                )),
3570                vec![0, 1, 2],
3571                false,
3572            ),
3573            (
3574                "AND with mixed equality and non-equality",
3575                Arc::new(BinaryExpr::new(
3576                    Arc::new(BinaryExpr::new(
3577                        Arc::new(Column::new("a", 0)),
3578                        Operator::Eq,
3579                        Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3580                    )),
3581                    Operator::And,
3582                    Arc::new(BinaryExpr::new(
3583                        Arc::new(Column::new("b", 1)),
3584                        Operator::Gt,
3585                        Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
3586                    )),
3587                )),
3588                vec![0],
3589                false,
3590            ),
3591            (
3592                "col = NULL is excluded",
3593                Arc::new(BinaryExpr::new(
3594                    Arc::new(Column::new("a", 0)),
3595                    Operator::Eq,
3596                    Arc::new(Literal::new(ScalarValue::Int32(None))),
3597                )),
3598                vec![],
3599                false,
3600            ),
3601            (
3602                "NULL = col is excluded",
3603                Arc::new(BinaryExpr::new(
3604                    Arc::new(Literal::new(ScalarValue::Utf8(None))),
3605                    Operator::Eq,
3606                    Arc::new(Column::new("a", 0)),
3607                )),
3608                vec![],
3609                false,
3610            ),
3611            (
3612                "contradictory: same col, different literals",
3613                Arc::new(BinaryExpr::new(
3614                    Arc::new(BinaryExpr::new(
3615                        Arc::new(Column::new("a", 0)),
3616                        Operator::Eq,
3617                        Arc::new(Literal::new(ScalarValue::Utf8(Some(
3618                            "alice".to_string(),
3619                        )))),
3620                    )),
3621                    Operator::And,
3622                    Arc::new(BinaryExpr::new(
3623                        Arc::new(Column::new("a", 0)),
3624                        Operator::Eq,
3625                        Arc::new(Literal::new(ScalarValue::Utf8(Some(
3626                            "bob".to_string(),
3627                        )))),
3628                    )),
3629                )),
3630                vec![0],
3631                true,
3632            ),
3633            (
3634                "same col, same literal is not contradictory",
3635                Arc::new(BinaryExpr::new(
3636                    Arc::new(BinaryExpr::new(
3637                        Arc::new(Column::new("a", 0)),
3638                        Operator::Eq,
3639                        Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3640                    )),
3641                    Operator::And,
3642                    Arc::new(BinaryExpr::new(
3643                        Arc::new(Column::new("a", 0)),
3644                        Operator::Eq,
3645                        Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
3646                    )),
3647                )),
3648                vec![0],
3649                false,
3650            ),
3651        ];
3652
3653        for (desc, expr, expected_cols, expected_infeasible) in cases {
3654            let (result, infeasible) = collect_equality_columns(&expr);
3655            let expected: HashSet<usize> = expected_cols.into_iter().collect();
3656            if expected_infeasible {
3657                // When infeasible, the scan is short-circuited, so we only
3658                // assert the infeasibility flag — the partial column set
3659                // contents are an implementation detail.
3660                assert!(infeasible, "case '{desc}': expected infeasible");
3661            } else {
3662                assert_eq!(result, expected, "case '{desc}': columns mismatch");
3663                assert!(!infeasible, "case '{desc}': expected feasible");
3664            }
3665        }
3666    }
3667
3668    /// Regression test: ProjectionExec on top of a FilterExec that already has
3669    /// an explicit projection must not panic when `try_swapping_with_projection`
3670    /// attempts to swap the two nodes.
3671    ///
3672    /// Before the fix, `FilterExecBuilder::from(self)` copied the old projection
3673    /// (e.g. `[0, 1, 2]`) from the FilterExec. After `.with_input` replaced the
3674    /// input with the narrower ProjectionExec (2 columns), `.build()` tried to
3675    /// validate the stale `[0, 1, 2]` projection against the 2-column schema and
3676    /// panicked with "project index 2 out of bounds, max field 2".
3677    #[test]
3678    fn test_filter_with_projection_swap_does_not_panic() -> Result<()> {
3679        use crate::projection::ProjectionExpr;
3680        use datafusion_physical_expr::expressions::col;
3681
3682        // Schema: [ts: Int64, tokens: Int64, svc: Utf8]
3683        let schema = Arc::new(Schema::new(vec![
3684            Field::new("ts", DataType::Int64, false),
3685            Field::new("tokens", DataType::Int64, false),
3686            Field::new("svc", DataType::Utf8, false),
3687        ]));
3688        let input = Arc::new(EmptyExec::new(Arc::clone(&schema)));
3689
3690        // FilterExec: ts > 0, projection=[ts@0, tokens@1, svc@2] (all 3 cols)
3691        let predicate = Arc::new(BinaryExpr::new(
3692            Arc::new(Column::new("ts", 0)),
3693            Operator::Gt,
3694            Arc::new(Literal::new(ScalarValue::Int64(Some(0)))),
3695        ));
3696        let filter = Arc::new(
3697            FilterExecBuilder::new(predicate, input)
3698                .apply_projection(Some(vec![0, 1, 2]))?
3699                .build()?,
3700        );
3701
3702        // ProjectionExec: narrows to [ts, tokens] (drops svc)
3703        let proj_exprs = vec![
3704            ProjectionExpr {
3705                expr: col("ts", &filter.schema())?,
3706                alias: "ts".to_string(),
3707            },
3708            ProjectionExpr {
3709                expr: col("tokens", &filter.schema())?,
3710                alias: "tokens".to_string(),
3711            },
3712        ];
3713        let projection = Arc::new(ProjectionExec::try_new(
3714            proj_exprs,
3715            Arc::clone(&filter) as _,
3716        )?);
3717
3718        // This must not panic
3719        let result = filter.try_swapping_with_projection(&projection)?;
3720        assert!(result.is_some(), "swap should succeed");
3721
3722        let new_plan = result.unwrap();
3723        // Output schema must still be [ts, tokens]
3724        let out_schema = new_plan.schema();
3725        assert_eq!(out_schema.fields().len(), 2);
3726        assert_eq!(out_schema.field(0).name(), "ts");
3727        assert_eq!(out_schema.field(1).name(), "tokens");
3728        Ok(())
3729    }
3730
3731    #[tokio::test]
3732    async fn test_filter_statistics_ndv_capped_at_row_count() -> Result<()> {
3733        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
3734        let input = Arc::new(StatisticsExec::new(
3735            Statistics {
3736                num_rows: Precision::Inexact(100),
3737                total_byte_size: Precision::Inexact(1000),
3738                column_statistics: vec![ColumnStatistics {
3739                    min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
3740                    max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
3741                    null_count: Precision::Inexact(80),
3742                    distinct_count: Precision::Inexact(80),
3743                    byte_size: Precision::Exact(1000),
3744                    ..Default::default()
3745                }],
3746            },
3747            schema.clone(),
3748        ));
3749
3750        // a <= 10 => ~10 rows out of 100
3751        let predicate: Arc<dyn PhysicalExpr> =
3752            binary(col("a", &schema)?, Operator::LtEq, lit(10i32), &schema)?;
3753
3754        let filter: Arc<dyn ExecutionPlan> =
3755            Arc::new(FilterExec::try_new(predicate, input)?);
3756
3757        let statistics =
3758            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3759        // Filter estimates ~10 rows (selectivity = 10/100)
3760        assert_eq!(statistics.num_rows, Precision::Inexact(10));
3761        let ndv = &statistics.column_statistics[0].distinct_count;
3762        assert!(
3763            ndv.get_value().copied() <= Some(10),
3764            "Expected NDV <= 10 (filtered row count), got {ndv:?}"
3765        );
3766        // `a <= 10` rejects nulls, so the 80 input nulls drop to exactly zero.
3767        assert_eq!(
3768            statistics.column_statistics[0].null_count,
3769            Precision::Exact(0)
3770        );
3771        // byte_size follows the same 10% selectivity estimate.
3772        assert_eq!(
3773            statistics.column_statistics[0].byte_size,
3774            Precision::Inexact(100)
3775        );
3776        Ok(())
3777    }
3778
3779    #[tokio::test]
3780    async fn test_filter_statistics_default_selectivity_column_stats() -> Result<()> {
3781        let schema = Schema::new(vec![Field::new("name", DataType::Utf8, true)]);
3782        let input = Arc::new(StatisticsExec::new(
3783            Statistics {
3784                num_rows: Precision::Inexact(100),
3785                total_byte_size: Precision::Inexact(1000),
3786                column_statistics: vec![ColumnStatistics {
3787                    null_count: Precision::Inexact(80),
3788                    distinct_count: Precision::Inexact(60),
3789                    byte_size: Precision::Exact(1000),
3790                    ..Default::default()
3791                }],
3792            },
3793            schema.clone(),
3794        ));
3795
3796        // Utf8 interval analysis is unsupported, so this exercises the default
3797        // selectivity path. The predicate rejects nulls but does not constrain
3798        // the column to one value.
3799        let predicate: Arc<dyn PhysicalExpr> =
3800            binary(col("name", &schema)?, Operator::Gt, lit("m"), &schema)?;
3801        let filter: Arc<dyn ExecutionPlan> =
3802            Arc::new(FilterExec::try_new(predicate, input)?);
3803
3804        let statistics =
3805            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3806        assert_eq!(statistics.num_rows, Precision::Inexact(20));
3807        assert_eq!(
3808            statistics.column_statistics[0].null_count,
3809            Precision::Exact(0)
3810        );
3811        assert_eq!(
3812            statistics.column_statistics[0].byte_size,
3813            Precision::Inexact(200)
3814        );
3815        assert_eq!(
3816            statistics.column_statistics[0].distinct_count,
3817            Precision::Inexact(20)
3818        );
3819        Ok(())
3820    }
3821
3822    #[tokio::test]
3823    async fn test_filter_statistics_or_does_not_reject_nulls() -> Result<()> {
3824        let schema = Schema::new(vec![Field::new("name", DataType::Utf8, true)]);
3825        let input = Arc::new(StatisticsExec::new(
3826            Statistics {
3827                num_rows: Precision::Inexact(100),
3828                total_byte_size: Precision::Inexact(1000),
3829                column_statistics: vec![ColumnStatistics {
3830                    null_count: Precision::Inexact(80),
3831                    distinct_count: Precision::Inexact(60),
3832                    byte_size: Precision::Exact(1000),
3833                    ..Default::default()
3834                }],
3835            },
3836            schema.clone(),
3837        ));
3838
3839        let predicate: Arc<dyn PhysicalExpr> = binary(
3840            binary(col("name", &schema)?, Operator::Gt, lit("m"), &schema)?,
3841            Operator::Or,
3842            is_null(col("name", &schema)?)?,
3843            &schema,
3844        )?;
3845        let filter: Arc<dyn ExecutionPlan> =
3846            Arc::new(FilterExec::try_new(predicate, input)?);
3847
3848        let statistics =
3849            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3850        assert_eq!(statistics.num_rows, Precision::Inexact(20));
3851        assert_eq!(
3852            statistics.column_statistics[0].null_count,
3853            Precision::Inexact(20)
3854        );
3855        assert_eq!(
3856            statistics.column_statistics[0].byte_size,
3857            Precision::Inexact(200)
3858        );
3859        assert_eq!(
3860            statistics.column_statistics[0].distinct_count,
3861            Precision::Inexact(20)
3862        );
3863        Ok(())
3864    }
3865
3866    #[tokio::test]
3867    async fn test_filter_statistics_is_not_null_rejects_nulls() -> Result<()> {
3868        let schema = Schema::new(vec![Field::new("name", DataType::Utf8, true)]);
3869        let input = Arc::new(StatisticsExec::new(
3870            Statistics {
3871                num_rows: Precision::Inexact(100),
3872                total_byte_size: Precision::Inexact(1000),
3873                column_statistics: vec![ColumnStatistics {
3874                    null_count: Precision::Inexact(80),
3875                    distinct_count: Precision::Inexact(60),
3876                    byte_size: Precision::Exact(1000),
3877                    ..Default::default()
3878                }],
3879            },
3880            schema.clone(),
3881        ));
3882
3883        // `name IS NOT NULL` keeps only non-null rows, so the surviving null
3884        // count is exactly zero. Utf8 interval analysis is unsupported, so this
3885        // also exercises the default-selectivity path.
3886        let predicate: Arc<dyn PhysicalExpr> = is_not_null(col("name", &schema)?)?;
3887        let filter: Arc<dyn ExecutionPlan> =
3888            Arc::new(FilterExec::try_new(predicate, input)?);
3889
3890        let statistics =
3891            StatisticsContext::new().compute(filter.as_ref(), &StatisticsArgs::new())?;
3892        assert_eq!(statistics.num_rows, Precision::Inexact(20));
3893        assert_eq!(
3894            statistics.column_statistics[0].null_count,
3895            Precision::Exact(0)
3896        );
3897        assert_eq!(
3898            statistics.column_statistics[0].byte_size,
3899            Precision::Inexact(200)
3900        );
3901        assert_eq!(
3902            statistics.column_statistics[0].distinct_count,
3903            Precision::Inexact(20)
3904        );
3905        Ok(())
3906    }
3907}