Skip to main content

datafusion_physical_plan/
projection.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
18//! Defines the projection execution plan. A projection determines which columns or expressions
19//! are returned from a query. The SQL statement `SELECT a, b, a+b FROM t1` is an example
20//! of a projection on table `t1` where the expressions `a`, `b`, and `a+b` are the
21//! projection expressions. `SELECT` without `FROM` will only evaluate expressions.
22
23use super::expressions::Column;
24use super::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
25use super::{
26    DisplayAs, ExecutionPlanProperties, PlanProperties, RecordBatchStream,
27    SendableRecordBatchStream, SortOrderPushdownResult, Statistics,
28};
29use crate::column_rewriter::PhysicalColumnRewriter;
30use crate::execution_plan::{CardinalityEffect, replace_children_if_necessary};
31use crate::filter_pushdown::{
32    ChildFilterDescription, ChildPushdownResult, FilterDescription, FilterPushdownPhase,
33    FilterPushdownPropagation, FilterRemapper, PushedDownPredicate,
34};
35use crate::joins::utils::{ColumnIndex, JoinFilter, JoinOn, JoinOnRef};
36use crate::statistics::{ChildStats, StatisticsArgs};
37use crate::{
38    ChildrenPropertiesMode, DisplayFormatType, ExecutionPlan, PhysicalExpr,
39    ReplaceChildrenOptions, validate_child_count,
40};
41use std::collections::HashMap;
42use std::pin::Pin;
43use std::sync::Arc;
44use std::task::{Context, Poll};
45
46use arrow::datatypes::{Schema, SchemaRef};
47use arrow::record_batch::RecordBatch;
48use datafusion_common::config::ConfigOptions;
49use datafusion_common::tree_node::{
50    Transformed, TransformedResult, TreeNode, TreeNodeRecursion,
51};
52use datafusion_common::{DataFusionError, JoinSide, Result, internal_err, plan_err};
53use datafusion_execution::TaskContext;
54use datafusion_expr::ExpressionPlacement;
55use datafusion_physical_expr::equivalence::ProjectionMapping;
56use datafusion_physical_expr::projection::Projector;
57use datafusion_physical_expr_common::physical_expr::{PhysicalExprRef, fmt_sql};
58use datafusion_physical_expr_common::sort_expr::{
59    LexOrdering, LexRequirement, PhysicalSortExpr,
60};
61// Re-exported from datafusion-physical-expr for backwards compatibility
62// We recommend updating your imports to use datafusion-physical-expr directly
63pub use datafusion_physical_expr::projection::{
64    ProjectionExpr, ProjectionExprs, update_expr,
65};
66
67use futures::stream::{Stream, StreamExt};
68use log::trace;
69
70/// [`ExecutionPlan`] for a projection
71///
72/// Computes a set of scalar value expressions for each input row, producing one
73/// output row for each input row.
74#[derive(Debug, Clone)]
75pub struct ProjectionExec {
76    /// A projector specialized to apply the projection to the input schema from the child node
77    /// and produce [`RecordBatch`]es with the output schema of this node.
78    projector: Projector,
79    /// The input plan
80    input: Arc<dyn ExecutionPlan>,
81    /// Execution metrics
82    metrics: ExecutionPlanMetricsSet,
83    /// Cache holding plan properties like equivalences, output partitioning etc.
84    cache: Arc<PlanProperties>,
85}
86
87impl ProjectionExec {
88    /// Create a projection on an input
89    ///
90    /// # Example:
91    /// Create a `ProjectionExec` to crate `SELECT a, a+b AS sum_ab FROM t1`:
92    ///
93    /// ```
94    /// # use std::sync::Arc;
95    /// # use arrow_schema::{Schema, Field, DataType};
96    /// # use datafusion_expr::Operator;
97    /// # use datafusion_physical_plan::ExecutionPlan;
98    /// # use datafusion_physical_expr::expressions::{col, binary};
99    /// # use datafusion_physical_plan::empty::EmptyExec;
100    /// # use datafusion_physical_plan::projection::{ProjectionExec, ProjectionExpr};
101    /// # fn schema() -> Arc<Schema> {
102    /// #  Arc::new(Schema::new(vec![
103    /// #   Field::new("a", DataType::Int32, false),
104    /// #   Field::new("b", DataType::Int32, false),
105    /// # ]))
106    /// # }
107    /// #
108    /// # fn input() -> Arc<dyn ExecutionPlan> {
109    /// #  Arc::new(EmptyExec::new(schema()))
110    /// # }
111    /// #
112    /// # fn main() {
113    /// let schema = schema();
114    /// // Create PhysicalExprs
115    /// let a = col("a", &schema).unwrap();
116    /// let b = col("b", &schema).unwrap();
117    /// let a_plus_b = binary(Arc::clone(&a), Operator::Plus, b, &schema).unwrap();
118    /// // create ProjectionExec
119    /// let proj = ProjectionExec::try_new(
120    ///     [
121    ///         ProjectionExpr {
122    ///             // expr a produces the column named "a"
123    ///             expr: a,
124    ///             alias: "a".to_string(),
125    ///         },
126    ///         ProjectionExpr {
127    ///             // expr: a + b produces the column named "sum_ab"
128    ///             expr: a_plus_b,
129    ///             alias: "sum_ab".to_string(),
130    ///         },
131    ///     ],
132    ///     input(),
133    /// )
134    /// .unwrap();
135    /// # }
136    /// ```
137    pub fn try_new<I, E>(expr: I, input: Arc<dyn ExecutionPlan>) -> Result<Self>
138    where
139        I: IntoIterator<Item = E>,
140        E: Into<ProjectionExpr>,
141    {
142        let input_schema = input.schema();
143        let expr_arc = expr.into_iter().map(Into::into).collect::<Arc<_>>();
144        let projection = ProjectionExprs::from_expressions(expr_arc);
145        let projector = projection.make_projector(&input_schema)?;
146        Self::try_from_projector(projector, input)
147    }
148
149    /// Create a projection using field and schema metadata from
150    /// `projected_schema`.
151    ///
152    /// Field names, data types, and nullability are still derived from the physical
153    /// projection expressions and the input plan; only field and schema metadata are
154    /// taken from `projected_schema`.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the projection cannot be applied to the input plan, or if
159    /// `projected_schema` has a different number of fields than the projection.
160    pub fn try_new_with_schema_metadata<I, E>(
161        expr: I,
162        input: Arc<dyn ExecutionPlan>,
163        projected_schema: &Schema,
164    ) -> Result<Self>
165    where
166        I: IntoIterator<Item = E>,
167        E: Into<ProjectionExpr>,
168    {
169        let input_schema = input.schema();
170        let expr_arc = expr.into_iter().map(Into::into).collect::<Arc<_>>();
171        let projection = ProjectionExprs::from_expressions(expr_arc);
172        let projector = projection
173            .make_projector_with_schema_metadata(&input_schema, projected_schema)?;
174        Self::try_from_projector(projector, input)
175    }
176
177    fn try_from_projector(
178        projector: Projector,
179        input: Arc<dyn ExecutionPlan>,
180    ) -> Result<Self> {
181        // Construct a map from the input expressions to the output expression of the Projection
182        let projection_mapping =
183            projector.projection().projection_mapping(&input.schema())?;
184        let cache = Self::compute_properties(
185            &input,
186            &projection_mapping,
187            Arc::clone(projector.output_schema()),
188        )?;
189        Ok(Self {
190            projector,
191            input,
192            metrics: ExecutionPlanMetricsSet::new(),
193            cache: Arc::new(cache),
194        })
195    }
196
197    /// The projection expressions stored as tuples of (expression, output column name)
198    pub fn expr(&self) -> &[ProjectionExpr] {
199        self.projector.projection().as_ref()
200    }
201
202    /// The projection expressions as a [`ProjectionExprs`].
203    pub fn projection_expr(&self) -> &ProjectionExprs {
204        self.projector.projection()
205    }
206
207    /// The input plan
208    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
209        &self.input
210    }
211
212    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
213    fn compute_properties(
214        input: &Arc<dyn ExecutionPlan>,
215        projection_mapping: &ProjectionMapping,
216        schema: SchemaRef,
217    ) -> Result<PlanProperties> {
218        // Calculate equivalence properties:
219        let input_eq_properties = input.equivalence_properties();
220        let eq_properties = input_eq_properties.project(projection_mapping, schema);
221        // Calculate output partitioning, which needs to respect aliases:
222        let output_partitioning = input
223            .output_partitioning()
224            .project(projection_mapping, input_eq_properties);
225
226        Ok(PlanProperties::new(
227            eq_properties,
228            output_partitioning,
229            input.pipeline_behavior(),
230            input.boundedness(),
231        ))
232    }
233
234    /// Collect reverse alias mapping from projection expressions.
235    /// The result hash map is a map from aliased Column in parent to original expr.
236    fn collect_reverse_alias(
237        &self,
238    ) -> Result<datafusion_common::HashMap<Column, Arc<dyn PhysicalExpr>>> {
239        let mut alias_map = datafusion_common::HashMap::new();
240        for projection in self.projection_expr().iter() {
241            let (aliased_index, _output_field) = self
242                .projector
243                .output_schema()
244                .column_with_name(&projection.alias)
245                .ok_or_else(|| {
246                    DataFusionError::Internal(format!(
247                        "Expr {} with alias {} not found in output schema",
248                        projection.expr, projection.alias
249                    ))
250                })?;
251            let aliased_col = Column::new(&projection.alias, aliased_index);
252            alias_map.insert(aliased_col, Arc::clone(&projection.expr));
253        }
254        Ok(alias_map)
255    }
256}
257
258impl DisplayAs for ProjectionExec {
259    fn fmt_as(
260        &self,
261        t: DisplayFormatType,
262        f: &mut std::fmt::Formatter,
263    ) -> std::fmt::Result {
264        match t {
265            DisplayFormatType::Default | DisplayFormatType::Verbose => {
266                let expr: Vec<String> = self
267                    .projector
268                    .projection()
269                    .as_ref()
270                    .iter()
271                    .map(|proj_expr| {
272                        let e = proj_expr.expr.to_string();
273                        if e != proj_expr.alias {
274                            format!("{e} as {}", proj_expr.alias)
275                        } else {
276                            e
277                        }
278                    })
279                    .collect();
280
281                write!(f, "ProjectionExec: expr=[{}]", expr.join(", "))
282            }
283            DisplayFormatType::TreeRender => {
284                for (i, proj_expr) in self.expr().iter().enumerate() {
285                    let expr_sql = fmt_sql(proj_expr.expr.as_ref());
286                    if proj_expr.expr.to_string() == proj_expr.alias {
287                        writeln!(f, "expr{i}={expr_sql}")?;
288                    } else {
289                        writeln!(f, "{}={expr_sql}", proj_expr.alias)?;
290                    }
291                }
292
293                Ok(())
294            }
295        }
296    }
297}
298
299impl ExecutionPlan for ProjectionExec {
300    fn name(&self) -> &'static str {
301        "ProjectionExec"
302    }
303
304    /// Return a reference to Any that can be used for downcasting
305    fn properties(&self) -> &Arc<PlanProperties> {
306        &self.cache
307    }
308
309    fn maintains_input_order(&self) -> Vec<bool> {
310        // Tell optimizer this operator doesn't reorder its input
311        vec![true]
312    }
313
314    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
315        let all_simple_exprs =
316            self.projector
317                .projection()
318                .as_ref()
319                .iter()
320                .all(|proj_expr| {
321                    !matches!(
322                        proj_expr.expr.placement(),
323                        ExpressionPlacement::KeepInPlace
324                    )
325                });
326        // If expressions are all either column_expr or Literal (or other cheap expressions),
327        // then all computations in this projection are reorder or rename,
328        // and projection would not benefit from the repartition, benefits_from_input_partitioning will return false.
329        vec![!all_simple_exprs]
330    }
331
332    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
333        vec![&self.input]
334    }
335
336    fn apply_expressions(
337        &self,
338        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
339    ) -> Result<TreeNodeRecursion> {
340        crate::apply_expression_roots(self.projector.projection().as_ref().iter(), f)
341    }
342
343    fn replace_children(
344        self: Arc<Self>,
345        mut children: Vec<Arc<dyn ExecutionPlan>>,
346        options: ReplaceChildrenOptions,
347    ) -> Result<Arc<dyn ExecutionPlan>> {
348        validate_child_count!(self, children);
349        match options.children_properties {
350            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
351                input: children.swap_remove(0),
352                metrics: ExecutionPlanMetricsSet::new(),
353                ..Self::clone(&*self)
354            })),
355            ChildrenPropertiesMode::Recompute => ProjectionExec::try_from_projector(
356                self.projector.clone(),
357                children.swap_remove(0),
358            )
359            .map(|p| Arc::new(p) as _),
360        }
361    }
362
363    fn with_new_children(
364        self: Arc<Self>,
365        children: Vec<Arc<dyn ExecutionPlan>>,
366    ) -> Result<Arc<dyn ExecutionPlan>> {
367        self.replace_children(
368            children,
369            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
370        )
371    }
372
373    fn with_new_children_and_same_properties(
374        self: Arc<Self>,
375        children: Vec<Arc<dyn ExecutionPlan>>,
376    ) -> Result<Arc<dyn ExecutionPlan>> {
377        self.replace_children(
378            children,
379            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
380        )
381    }
382
383    fn execute(
384        &self,
385        partition: usize,
386        context: Arc<TaskContext>,
387    ) -> Result<SendableRecordBatchStream> {
388        trace!(
389            "Start ProjectionExec::execute for partition {} of context session_id {} and task_id {:?}",
390            partition,
391            context.session_id(),
392            context.task_id()
393        );
394
395        let projector = self.projector.with_metrics(&self.metrics, partition);
396        Ok(Box::pin(ProjectionStream::new(
397            projector,
398            self.input.execute(partition, context)?,
399            BaselineMetrics::new(&self.metrics, partition),
400        )?))
401    }
402
403    fn metrics(&self) -> Option<MetricsSet> {
404        Some(self.metrics.clone_inner())
405    }
406
407    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
408        vec![ChildStats::At(partition)]
409    }
410
411    fn statistics_from_inputs(
412        &self,
413        input_stats: &[Arc<Statistics>],
414        _args: &StatisticsArgs,
415    ) -> Result<Arc<Statistics>> {
416        let input_stats = input_stats[0].as_ref().clone();
417        let output_schema = self.schema();
418        Ok(Arc::new(
419            self.projector
420                .projection()
421                .project_statistics(input_stats, &output_schema)?,
422        ))
423    }
424
425    fn supports_limit_pushdown(&self) -> bool {
426        true
427    }
428
429    fn cardinality_effect(&self) -> CardinalityEffect {
430        CardinalityEffect::Equal
431    }
432
433    fn try_swapping_with_projection(
434        &self,
435        projection: &ProjectionExec,
436    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
437        match try_collapse_projection_chain(projection)? {
438            Some(plan) => Ok(Some(plan)),
439            None => Ok(Some(Arc::new(projection.clone()))),
440        }
441    }
442
443    fn gather_filters_for_pushdown(
444        &self,
445        _phase: FilterPushdownPhase,
446        parent_filters: Vec<Arc<dyn PhysicalExpr>>,
447        _config: &ConfigOptions,
448    ) -> Result<FilterDescription> {
449        // expand alias column to original expr in parent filters
450        let invert_alias_map = self.collect_reverse_alias()?;
451        let output_schema = self.schema();
452        let remapper = FilterRemapper::new(output_schema);
453        let mut child_parent_filters = Vec::with_capacity(parent_filters.len());
454
455        for filter in parent_filters {
456            // Check that column exists in child, then reassign column indices to match child schema
457            if let Some(reassigned) = remapper.try_remap(&filter)? {
458                // rewrite filter expression using invert alias map
459                let mut rewriter = PhysicalColumnRewriter::new(&invert_alias_map);
460                let rewritten = reassigned.rewrite(&mut rewriter)?.data;
461                child_parent_filters.push(PushedDownPredicate::supported(rewritten));
462            } else {
463                child_parent_filters.push(PushedDownPredicate::unsupported(filter));
464            }
465        }
466
467        Ok(FilterDescription::new().with_child(ChildFilterDescription {
468            parent_filters: child_parent_filters,
469            self_filters: vec![],
470        }))
471    }
472
473    fn handle_child_pushdown_result(
474        &self,
475        _phase: FilterPushdownPhase,
476        child_pushdown_result: ChildPushdownResult,
477        _config: &ConfigOptions,
478    ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
479        Ok(FilterPushdownPropagation::if_all(child_pushdown_result))
480    }
481
482    fn try_pushdown_sort(
483        &self,
484        order: &[PhysicalSortExpr],
485    ) -> Result<SortOrderPushdownResult<Arc<dyn ExecutionPlan>>> {
486        let child = self.input();
487        let mut child_order = Vec::new();
488
489        // Check and transform sort expressions
490        for sort_expr in order {
491            // Recursively transform the expression
492            let mut can_pushdown = true;
493            let transformed = Arc::clone(&sort_expr.expr).transform(|expr| {
494                if let Some(col) = expr.downcast_ref::<Column>() {
495                    // Check if column index is valid.
496                    // This should always be true but fail gracefully if it's not.
497                    if col.index() >= self.expr().len() {
498                        can_pushdown = false;
499                        return Ok(Transformed::no(expr));
500                    }
501
502                    let proj_expr = &self.expr()[col.index()];
503
504                    // Check if projection expression is a simple column
505                    // We cannot push down order by clauses that depend on
506                    // projected computations as they would have nothing to reference.
507                    if let Some(child_col) = proj_expr.expr.downcast_ref::<Column>() {
508                        // Replace with the child column
509                        Ok(Transformed::yes(Arc::new(child_col.clone()) as _))
510                    } else {
511                        // Projection involves computation, cannot push down
512                        can_pushdown = false;
513                        Ok(Transformed::no(expr))
514                    }
515                } else {
516                    Ok(Transformed::no(expr))
517                }
518            })?;
519
520            if !can_pushdown {
521                return Ok(SortOrderPushdownResult::Unsupported);
522            }
523
524            child_order.push(PhysicalSortExpr {
525                expr: transformed.data,
526                options: sort_expr.options,
527            });
528        }
529
530        // Recursively push down to child node
531        match child.try_pushdown_sort(&child_order)? {
532            SortOrderPushdownResult::Exact { inner } => {
533                let new_exec =
534                    replace_children_if_necessary(Arc::new(self.clone()), vec![inner])?;
535                Ok(SortOrderPushdownResult::Exact { inner: new_exec })
536            }
537            SortOrderPushdownResult::Inexact { inner } => {
538                let new_exec =
539                    replace_children_if_necessary(Arc::new(self.clone()), vec![inner])?;
540                Ok(SortOrderPushdownResult::Inexact { inner: new_exec })
541            }
542            SortOrderPushdownResult::Unsupported => {
543                Ok(SortOrderPushdownResult::Unsupported)
544            }
545        }
546    }
547
548    fn with_preserve_order(
549        &self,
550        preserve_order: bool,
551    ) -> Option<Arc<dyn ExecutionPlan>> {
552        self.input
553            .with_preserve_order(preserve_order)
554            .and_then(|new_input| {
555                replace_children_if_necessary(Arc::new(self.clone()), vec![new_input])
556                    .ok()
557            })
558    }
559
560    #[cfg(feature = "proto")]
561    fn try_to_proto(
562        &self,
563        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
564    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
565        use datafusion_proto_models::protobuf;
566        let input = ctx.encode_child(self.input())?;
567        let expr = ctx.encode_expressions(self.expr().iter().map(|p| &p.expr))?;
568        let expr_name = self.expr().iter().map(|p| p.alias.clone()).collect();
569        Ok(Some(protobuf::PhysicalPlanNode {
570            physical_plan_type: Some(
571                protobuf::physical_plan_node::PhysicalPlanType::Projection(Box::new(
572                    protobuf::ProjectionExecNode {
573                        input: Some(Box::new(input)),
574                        expr,
575                        expr_name,
576                    },
577                )),
578            ),
579        }))
580    }
581}
582
583#[cfg(feature = "proto")]
584impl ProjectionExec {
585    /// Reconstruct a [`ProjectionExec`] from its protobuf representation.
586    ///
587    /// The exact inverse of [`ExecutionPlan::try_to_proto`]: it takes the whole
588    /// [`PhysicalPlanNode`] so every plan's `try_from_proto` shares one
589    /// signature. Child plans and expressions are decoded recursively via the
590    /// [`ExecutionPlanDecodeCtx`].
591    ///
592    /// [`PhysicalPlanNode`]: datafusion_proto_models::protobuf::PhysicalPlanNode
593    /// [`ExecutionPlan::try_to_proto`]: crate::ExecutionPlan::try_to_proto
594    /// [`ExecutionPlanDecodeCtx`]: crate::proto::ExecutionPlanDecodeCtx
595    pub fn try_from_proto(
596        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
597        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
598    ) -> Result<Arc<dyn ExecutionPlan>> {
599        use datafusion_proto_models::protobuf;
600        let projection = crate::expect_plan_variant!(
601            node,
602            protobuf::physical_plan_node::PhysicalPlanType::Projection,
603            "ProjectionExec",
604        );
605        let input = ctx.decode_required_child(
606            projection.input.as_deref(),
607            "ProjectionExec",
608            "input",
609        )?;
610        let input_schema = input.schema();
611        let exprs = projection
612            .expr
613            .iter()
614            .zip(projection.expr_name.iter())
615            .map(|(expr, name)| {
616                Ok(ProjectionExpr {
617                    expr: ctx.decode_expr(expr, input_schema.as_ref())?,
618                    alias: name.to_string(),
619                })
620            })
621            .collect::<Result<Vec<ProjectionExpr>>>()?;
622        Ok(Arc::new(ProjectionExec::try_new(exprs, input)?))
623    }
624}
625
626impl ProjectionStream {
627    /// Create a new projection stream
628    fn new(
629        projector: Projector,
630        input: SendableRecordBatchStream,
631        baseline_metrics: BaselineMetrics,
632    ) -> Result<Self> {
633        Ok(Self {
634            projector,
635            input,
636            baseline_metrics,
637        })
638    }
639
640    fn batch_project(&self, batch: &RecordBatch) -> Result<RecordBatch> {
641        // Records time on drop
642        let _timer = self.baseline_metrics.elapsed_compute().timer();
643        self.projector.project_batch(batch)
644    }
645}
646
647/// Projection iterator
648struct ProjectionStream {
649    projector: Projector,
650    input: SendableRecordBatchStream,
651    baseline_metrics: BaselineMetrics,
652}
653
654impl Stream for ProjectionStream {
655    type Item = Result<RecordBatch>;
656
657    fn poll_next(
658        mut self: Pin<&mut Self>,
659        cx: &mut Context<'_>,
660    ) -> Poll<Option<Self::Item>> {
661        let poll = self.input.poll_next_unpin(cx).map(|x| match x {
662            Some(Ok(batch)) => Some(self.batch_project(&batch)),
663            other => other,
664        });
665
666        self.baseline_metrics.record_poll(poll)
667    }
668
669    fn size_hint(&self) -> (usize, Option<usize>) {
670        // Same number of record batches
671        self.input.size_hint()
672    }
673}
674
675impl RecordBatchStream for ProjectionStream {
676    /// Get the schema
677    fn schema(&self) -> SchemaRef {
678        Arc::clone(self.projector.output_schema())
679    }
680}
681
682/// Trait for execution plans that can embed a projection, avoiding a separate
683/// [`ProjectionExec`] wrapper.
684///
685/// # Empty projections
686///
687/// `Some(vec![])` is a valid projection that produces zero output columns while
688/// preserving the correct row count. Implementors must ensure that runtime batch
689/// construction still returns batches with the right number of rows even when no
690/// columns are selected (e.g. for `SELECT count(1) … JOIN …`).
691pub trait EmbeddedProjection: ExecutionPlan + Sized {
692    fn with_projection(&self, projection: Option<Vec<usize>>) -> Result<Self>;
693}
694
695/// Some projection can't be pushed down left input or right input of hash join because filter or on need may need some columns that won't be used in later.
696/// By embed those projection to hash join, we can reduce the cost of build_batch_from_indices in hash join (build_batch_from_indices need to can compute::take() for each column) and avoid unnecessary output creation.
697pub fn try_embed_projection<Exec: EmbeddedProjection + 'static>(
698    projection: &ProjectionExec,
699    execution_plan: &Exec,
700) -> Result<Option<Arc<dyn ExecutionPlan>>> {
701    // If the projection has no expressions at all (e.g., ProjectionExec: expr=[]),
702    // embed an empty projection into the execution plan so it outputs zero columns.
703    // This avoids allocating throwaway null arrays for build-side columns
704    // when no output columns are actually needed (e.g., count(1) over a right join).
705    if projection.expr().is_empty() {
706        let new_execution_plan = Arc::new(execution_plan.with_projection(Some(vec![]))?);
707        return Ok(Some(new_execution_plan));
708    }
709
710    // Collect all column indices from the given projection expressions.
711    let projection_index = collect_column_indices(projection.expr());
712
713    if projection_index.is_empty() {
714        return Ok(None);
715    };
716
717    let columns_reduced = projection_index.len() < execution_plan.schema().fields().len();
718
719    let new_execution_plan =
720        Arc::new(execution_plan.with_projection(Some(projection_index.to_vec()))?);
721
722    // Build projection expressions for update_expr. Zip the projection_index with the new_execution_plan output schema fields.
723    let embed_project_exprs = projection_index
724        .iter()
725        .zip(new_execution_plan.schema().fields())
726        .map(|(index, field)| ProjectionExpr {
727            expr: Arc::new(Column::new(field.name(), *index)) as Arc<dyn PhysicalExpr>,
728            alias: field.name().to_owned(),
729        })
730        .collect::<Vec<_>>();
731
732    let mut new_projection_exprs = Vec::with_capacity(projection.expr().len());
733
734    for proj_expr in projection.expr() {
735        // update column index for projection expression since the input schema has been changed.
736        let Some(expr) =
737            update_expr(&proj_expr.expr, embed_project_exprs.as_slice(), false)?
738        else {
739            return Ok(None);
740        };
741        new_projection_exprs.push(ProjectionExpr {
742            expr,
743            alias: proj_expr.alias.clone(),
744        });
745    }
746    // Old projection may contain some alias or expression such as `a + 1` and `CAST('true' AS BOOLEAN)`, but our projection_exprs in hash join just contain column, so we need to create the new projection to keep the original projection.
747    let new_projection = Arc::new(ProjectionExec::try_new(
748        new_projection_exprs,
749        Arc::clone(&new_execution_plan) as _,
750    )?);
751    if is_projection_removable(&new_projection) {
752        // Residual is identity — embedding fully absorbed the projection.
753        Ok(Some(new_execution_plan))
754    } else if columns_reduced {
755        // Embedding reduced columns even though a residual is still needed
756        // for renames or expressions — worth keeping.
757        Ok(Some(new_projection))
758    } else {
759        // No columns eliminated and residual still needed — embedding just
760        // adds an unnecessary column reorder inside the operator.
761        Ok(None)
762    }
763}
764
765pub struct JoinData {
766    pub projected_left_child: ProjectionExec,
767    pub projected_right_child: ProjectionExec,
768    pub join_filter: Option<JoinFilter>,
769    pub join_on: JoinOn,
770}
771
772#[deprecated(
773    since = "55.0.0",
774    note = "Use try_pushdown_through_join_with_column_indices instead"
775)]
776pub fn try_pushdown_through_join(
777    projection: &ProjectionExec,
778    join_left: &Arc<dyn ExecutionPlan>,
779    join_right: &Arc<dyn ExecutionPlan>,
780    join_on: JoinOnRef,
781    schema: &SchemaRef,
782    filter: Option<&JoinFilter>,
783) -> Result<Option<JoinData>> {
784    let left_field_count = join_left.schema().fields().len();
785    let column_indices = schema
786        .fields()
787        .iter()
788        .enumerate()
789        .map(|(index, _)| {
790            if index < left_field_count {
791                ColumnIndex {
792                    index,
793                    side: JoinSide::Left,
794                }
795            } else {
796                ColumnIndex {
797                    index: index - left_field_count,
798                    side: JoinSide::Right,
799                }
800            }
801        })
802        .collect::<Vec<_>>();
803
804    try_pushdown_through_join_with_column_indices(
805        projection,
806        join_left,
807        join_right,
808        join_on,
809        schema,
810        filter,
811        &column_indices,
812    )
813}
814
815/// Attempts to move a projection below a join by mapping each join output
816/// column to the child column that produced it.
817///
818/// `schema` is the complete output schema of the join, not either child's
819/// schema. `column_indices` must contain one entry for each field in `schema`.
820/// Each [`JoinSide::Left`] or [`JoinSide::Right`] entry identifies the source
821/// child and uses an index relative to that child's schema.
822///
823/// [`JoinSide::None`] identifies a column produced by the join itself, such as
824/// a mark column. If `projection` references such a column, this function
825/// returns `Ok(None)` because neither child can produce it.
826///
827/// Returns `Ok(None)` when the projection cannot be pushed down safely.
828///
829/// # Errors
830///
831/// Returns an error if `column_indices` does not match `schema` or contains an
832/// index outside the corresponding child schema.
833pub fn try_pushdown_through_join_with_column_indices(
834    projection: &ProjectionExec,
835    join_left: &Arc<dyn ExecutionPlan>,
836    join_right: &Arc<dyn ExecutionPlan>,
837    join_on: JoinOnRef,
838    schema: &SchemaRef,
839    filter: Option<&JoinFilter>,
840    column_indices: &[ColumnIndex],
841) -> Result<Option<JoinData>> {
842    if column_indices.len() != schema.fields().len() {
843        return plan_err!(
844            "Column index mapping has {} entries but join schema has {} fields",
845            column_indices.len(),
846            schema.fields().len()
847        );
848    }
849    // Validate each output-to-child mapping before using it to rewrite the
850    // projection. Synthetic outputs have no child index to validate.
851    for (output_index, column_index) in column_indices.iter().enumerate() {
852        let (side, child_field_count) = match column_index.side {
853            JoinSide::Left => ("left", join_left.schema().fields().len()),
854            JoinSide::Right => ("right", join_right.schema().fields().len()),
855            JoinSide::None => continue,
856        };
857        if column_index.index >= child_field_count {
858            return plan_err!(
859                "Join output column {output_index} maps to {side} child column {}, but the child has {child_field_count} fields",
860                column_index.index
861            );
862        }
863    }
864
865    // Convert projected expressions to columns. We can not proceed if this is not possible.
866    let Some(projection_as_columns) = physical_to_column_exprs(projection.expr()) else {
867        return Ok(None);
868    };
869
870    if projection_as_columns.len() >= schema.fields().len() {
871        return Ok(None);
872    }
873    let mut left_proj: Vec<(Column, String)> = Vec::new();
874    let mut right_proj: Vec<(Column, String)> = Vec::new();
875    let mut seen_right = false;
876    for (col, alias) in &projection_as_columns {
877        let Some(origin) = column_indices.get(col.index()) else {
878            return plan_err!(
879                "Projection column {} is outside the {}-entry column index mapping",
880                col.index(),
881                column_indices.len()
882            );
883        };
884        match origin.side {
885            // Keep the "left block before right block" contiguity the current
886            // pushdown supports; a left column after a right one is "mixed".
887            JoinSide::Left => {
888                if seen_right {
889                    return Ok(None);
890                }
891                left_proj.push((Column::new(col.name(), origin.index), alias.clone()));
892            }
893            JoinSide::Right => {
894                seen_right = true;
895                right_proj.push((Column::new(col.name(), origin.index), alias.clone()));
896            }
897            // Synthetic column (e.g. mark): belongs to neither child.
898            // Phase 2 declines; Phase 3 keeps it at the join output instead.
899            JoinSide::None => return Ok(None),
900        }
901    }
902
903    // Parity: neither side fully dropped.
904    if left_proj.is_empty() || right_proj.is_empty() {
905        return Ok(None);
906    }
907
908    // `left_proj` / `right_proj` carry *child* indices (from `column_indices`),
909    // so the shared `update_join_*` helpers must use a 0 column-index offset for
910    // both sides (the offset bridges child -> join-output index, which is the
911    // identity here).
912    let new_filter = if let Some(filter) = filter {
913        match update_join_filter(&left_proj, &right_proj, filter, 0) {
914            Some(updated) => Some(updated),
915            None => return Ok(None),
916        }
917    } else {
918        None
919    };
920
921    let Some(new_on) = update_join_on(&left_proj, &right_proj, join_on, 0) else {
922        return Ok(None);
923    };
924
925    let (new_left, new_right) =
926        new_join_children_from_groups(&left_proj, &right_proj, join_left, join_right)?;
927
928    Ok(Some(JoinData {
929        projected_left_child: new_left,
930        projected_right_child: new_right,
931        join_filter: new_filter,
932        join_on: new_on,
933    }))
934}
935
936/// This function checks if `plan` is a [`ProjectionExec`], and inspects its
937/// input(s) to test whether it can push `plan` under its input(s). This function
938/// will operate on the entire tree and may ultimately remove `plan` entirely
939/// by leveraging source providers with built-in projection capabilities.
940pub fn remove_unnecessary_projections(
941    plan: Arc<dyn ExecutionPlan>,
942) -> Result<Transformed<Arc<dyn ExecutionPlan>>> {
943    let maybe_modified = if let Some(projection) = plan.downcast_ref::<ProjectionExec>() {
944        // If the projection does not cause any change on the input, we can
945        // safely remove it:
946        if is_projection_removable(projection) {
947            return Ok(Transformed::yes(Arc::clone(projection.input())));
948        }
949        // If it does, check if we can push it under its child(ren):
950        projection
951            .input()
952            .try_swapping_with_projection(projection)?
953    } else {
954        return Ok(Transformed::no(plan));
955    };
956    Ok(maybe_modified.map_or_else(|| Transformed::no(plan), Transformed::yes))
957}
958
959/// Compare the inputs and outputs of the projection. All expressions must be
960/// columns without alias, and projection does not change the order of fields.
961/// For example, if the input schema is `a, b`, `SELECT a, b` is removable,
962/// but `SELECT b, a` and `SELECT a+1, b` and `SELECT a AS c, b` are not.
963fn is_projection_removable(projection: &ProjectionExec) -> bool {
964    let exprs = projection.expr();
965    exprs.iter().enumerate().all(|(idx, proj_expr)| {
966        let Some(col) = proj_expr.expr.downcast_ref::<Column>() else {
967            return false;
968        };
969        col.name() == proj_expr.alias && col.index() == idx
970    }) && exprs.len() == projection.input().schema().fields().len()
971}
972
973/// Given the expression set of a projection, checks if the projection causes
974/// any renaming or constructs a non-`Column` physical expression.
975pub fn all_alias_free_columns(exprs: &[ProjectionExpr]) -> bool {
976    exprs.iter().all(|proj_expr| {
977        proj_expr
978            .expr
979            .downcast_ref::<Column>()
980            .map(|column| column.name() == proj_expr.alias)
981            .unwrap_or(false)
982    })
983}
984
985/// Updates a source provider's projected columns according to the given
986/// projection operator's expressions. To use this function safely, one must
987/// ensure that all expressions are `Column` expressions without aliases.
988pub fn new_projections_for_columns(
989    projection: &[ProjectionExpr],
990    source: &[usize],
991) -> Vec<usize> {
992    projection
993        .iter()
994        .filter_map(|proj_expr| {
995            proj_expr
996                .expr
997                .downcast_ref::<Column>()
998                .map(|expr| source[expr.index()])
999        })
1000        .collect()
1001}
1002
1003/// Creates a new [`ProjectionExec`] instance with the given child plan and
1004/// projected expressions.
1005pub fn make_with_child(
1006    projection: &ProjectionExec,
1007    child: &Arc<dyn ExecutionPlan>,
1008) -> Result<Arc<dyn ExecutionPlan>> {
1009    ProjectionExec::try_new(projection.expr().to_vec(), Arc::clone(child))
1010        .map(|e| Arc::new(e) as _)
1011}
1012
1013/// Returns `true` if all the expressions in the argument are `Column`s.
1014pub fn all_columns(exprs: &[ProjectionExpr]) -> bool {
1015    exprs.iter().all(|proj_expr| proj_expr.expr.is::<Column>())
1016}
1017
1018/// Updates the given lexicographic ordering according to given projected
1019/// expressions using the [`update_expr`] function.
1020pub fn update_ordering(
1021    ordering: LexOrdering,
1022    projected_exprs: &[ProjectionExpr],
1023) -> Result<Option<LexOrdering>> {
1024    let mut updated_exprs = vec![];
1025    for mut sort_expr in ordering.into_iter() {
1026        let Some(updated_expr) = update_expr(&sort_expr.expr, projected_exprs, false)?
1027        else {
1028            return Ok(None);
1029        };
1030        sort_expr.expr = updated_expr;
1031        updated_exprs.push(sort_expr);
1032    }
1033    Ok(LexOrdering::new(updated_exprs))
1034}
1035
1036/// Updates the given lexicographic requirement according to given projected
1037/// expressions using the [`update_expr`] function.
1038pub fn update_ordering_requirement(
1039    reqs: LexRequirement,
1040    projected_exprs: &[ProjectionExpr],
1041) -> Result<Option<LexRequirement>> {
1042    let mut updated_exprs = vec![];
1043    for mut sort_expr in reqs.into_iter() {
1044        let Some(updated_expr) = update_expr(&sort_expr.expr, projected_exprs, false)?
1045        else {
1046            return Ok(None);
1047        };
1048        sort_expr.expr = updated_expr;
1049        updated_exprs.push(sort_expr);
1050    }
1051    Ok(LexRequirement::new(updated_exprs))
1052}
1053
1054/// Downcasts all the expressions in `exprs` to `Column`s. If any of the given
1055/// expressions is not a `Column`, returns `None`.
1056pub fn physical_to_column_exprs(
1057    exprs: &[ProjectionExpr],
1058) -> Option<Vec<(Column, String)>> {
1059    exprs
1060        .iter()
1061        .map(|proj_expr| {
1062            proj_expr
1063                .expr
1064                .downcast_ref::<Column>()
1065                .map(|col| (col.clone(), proj_expr.alias.clone()))
1066        })
1067        .collect()
1068}
1069
1070/// If pushing down the projection over this join's children seems possible,
1071/// this function constructs the new [`ProjectionExec`]s that will come on top
1072/// of the original children of the join.
1073pub fn new_join_children(
1074    projection_as_columns: &[(Column, String)],
1075    far_right_left_col_ind: i32,
1076    far_left_right_col_ind: i32,
1077    left_child: &Arc<dyn ExecutionPlan>,
1078    right_child: &Arc<dyn ExecutionPlan>,
1079) -> Result<(ProjectionExec, ProjectionExec)> {
1080    let new_left = ProjectionExec::try_new(
1081        projection_as_columns[0..=far_right_left_col_ind as _]
1082            .iter()
1083            .map(|(col, alias)| ProjectionExpr {
1084                expr: Arc::new(Column::new(col.name(), col.index())) as _,
1085                alias: alias.clone(),
1086            }),
1087        Arc::clone(left_child),
1088    )?;
1089    let left_size = left_child.schema().fields().len() as i32;
1090    let new_right = ProjectionExec::try_new(
1091        projection_as_columns[far_left_right_col_ind as _..]
1092            .iter()
1093            .map(|(col, alias)| {
1094                ProjectionExpr {
1095                    expr: Arc::new(Column::new(
1096                        col.name(),
1097                        // Align projected expressions coming from the right
1098                        // table with the new right child projection:
1099                        (col.index() as i32 - left_size) as _,
1100                    )) as _,
1101                    alias: alias.clone(),
1102                }
1103            }),
1104        Arc::clone(right_child),
1105    )?;
1106
1107    Ok((new_left, new_right))
1108}
1109
1110/// Build the projected left and right children from side-grouped projection
1111/// columns whose indices are already *child*-relative (e.g. derived from a
1112/// join's `ColumnIndex`). Unlike [`new_join_children`], this does not infer
1113/// child ownership from output position, so it is safe for join schemas whose
1114/// output is not a plain `left ++ right` (used by the schema-aware
1115/// `try_pushdown_through_join_with_column_indices`).
1116fn new_join_children_from_groups(
1117    left_proj: &[(Column, String)],
1118    right_proj: &[(Column, String)],
1119    left_child: &Arc<dyn ExecutionPlan>,
1120    right_child: &Arc<dyn ExecutionPlan>,
1121) -> Result<(ProjectionExec, ProjectionExec)> {
1122    let build = |cols: &[(Column, String)], child: &Arc<dyn ExecutionPlan>| {
1123        ProjectionExec::try_new(
1124            cols.iter().map(|(col, alias)| ProjectionExpr {
1125                expr: Arc::new(Column::new(col.name(), col.index())) as _,
1126                alias: alias.clone(),
1127            }),
1128            Arc::clone(child),
1129        )
1130    };
1131
1132    Ok((
1133        build(left_proj, left_child)?,
1134        build(right_proj, right_child)?,
1135    ))
1136}
1137
1138/// Checks three conditions for pushing a projection down through a join:
1139/// - Projection must narrow the join output schema.
1140/// - Columns coming from left/right tables must be collected at the left/right
1141///   sides of the output table.
1142/// - Left or right table is not lost after the projection.
1143pub fn join_allows_pushdown(
1144    projection_as_columns: &[(Column, String)],
1145    join_schema: &SchemaRef,
1146    far_right_left_col_ind: i32,
1147    far_left_right_col_ind: i32,
1148) -> bool {
1149    // Projection must narrow the join output:
1150    projection_as_columns.len() < join_schema.fields().len()
1151    // Are the columns from different tables mixed?
1152    && (far_right_left_col_ind + 1 == far_left_right_col_ind)
1153    // Left or right table is not lost after the projection.
1154    && far_right_left_col_ind >= 0
1155    && far_left_right_col_ind < projection_as_columns.len() as i32
1156}
1157
1158/// Returns the last index before encountering a column coming from the right table when traveling
1159/// through the projection from left to right, and the last index before encountering a column
1160/// coming from the left table when traveling through the projection from right to left.
1161/// If there is no column in the projection coming from the left side, it returns (-1, ...),
1162/// if there is no column in the projection coming from the right side, it returns (..., projection length).
1163pub fn join_table_borders(
1164    left_table_column_count: usize,
1165    projection_as_columns: &[(Column, String)],
1166) -> (i32, i32) {
1167    let far_right_left_col_ind = projection_as_columns
1168        .iter()
1169        .enumerate()
1170        .take_while(|(_, (projection_column, _))| {
1171            projection_column.index() < left_table_column_count
1172        })
1173        .last()
1174        .map(|(index, _)| index as i32)
1175        .unwrap_or(-1);
1176
1177    let far_left_right_col_ind = projection_as_columns
1178        .iter()
1179        .enumerate()
1180        .rev()
1181        .take_while(|(_, (projection_column, _))| {
1182            projection_column.index() >= left_table_column_count
1183        })
1184        .last()
1185        .map(|(index, _)| index as i32)
1186        .unwrap_or(projection_as_columns.len() as i32);
1187
1188    (far_right_left_col_ind, far_left_right_col_ind)
1189}
1190
1191/// Tries to update the equi-join `Column`'s of a join as if the input of
1192/// the join was replaced by a projection.
1193pub fn update_join_on(
1194    proj_left_exprs: &[(Column, String)],
1195    proj_right_exprs: &[(Column, String)],
1196    hash_join_on: &[(PhysicalExprRef, PhysicalExprRef)],
1197    left_field_size: usize,
1198) -> Option<Vec<(PhysicalExprRef, PhysicalExprRef)>> {
1199    let (left_idx, right_idx): (Vec<_>, Vec<_>) = hash_join_on
1200        .iter()
1201        .map(|(left, right)| (left, right))
1202        .unzip();
1203
1204    let new_left = new_columns_for_join_on(&left_idx, proj_left_exprs, 0)?;
1205    let new_right =
1206        new_columns_for_join_on(&right_idx, proj_right_exprs, left_field_size)?;
1207    Some(new_left.into_iter().zip(new_right).collect())
1208}
1209
1210/// Tries to update the column indices of a [`JoinFilter`] as if the input of
1211/// the join was replaced by a projection.
1212pub fn update_join_filter(
1213    projection_left_exprs: &[(Column, String)],
1214    projection_right_exprs: &[(Column, String)],
1215    join_filter: &JoinFilter,
1216    left_field_size: usize,
1217) -> Option<JoinFilter> {
1218    let mut new_left_indices = new_indices_for_join_filter(
1219        join_filter,
1220        JoinSide::Left,
1221        projection_left_exprs,
1222        0,
1223    )
1224    .into_iter();
1225    let mut new_right_indices = new_indices_for_join_filter(
1226        join_filter,
1227        JoinSide::Right,
1228        projection_right_exprs,
1229        left_field_size,
1230    )
1231    .into_iter();
1232
1233    // Check if all columns match:
1234    (new_right_indices.len() + new_left_indices.len()
1235        == join_filter.column_indices().len())
1236    .then(|| {
1237        JoinFilter::new(
1238            Arc::clone(join_filter.expression()),
1239            join_filter
1240                .column_indices()
1241                .iter()
1242                .map(|col_idx| ColumnIndex {
1243                    index: if col_idx.side == JoinSide::Left {
1244                        new_left_indices.next().unwrap()
1245                    } else {
1246                        new_right_indices.next().unwrap()
1247                    },
1248                    side: col_idx.side,
1249                })
1250                .collect(),
1251            Arc::clone(join_filter.schema()),
1252        )
1253    })
1254}
1255
1256/// Collapse a chain of consecutive [`ProjectionExec`]s into one. Returns
1257/// `None` if nothing could be merged.
1258fn try_collapse_projection_chain(
1259    outer: &ProjectionExec,
1260) -> Result<Option<Arc<dyn ExecutionPlan>>> {
1261    let mut current_exprs: Vec<ProjectionExpr> = outer.expr().to_vec();
1262    let mut current_input: Arc<dyn ExecutionPlan> = Arc::clone(outer.input());
1263    let mut column_ref_map: HashMap<Column, usize> = HashMap::new();
1264    let mut collapsed_any = false;
1265
1266    'outer: while let Some(inner_proj) = current_input.downcast_ref::<ProjectionExec>() {
1267        // Collect the column references usage in the outer projection.
1268        column_ref_map.clear();
1269        for proj_expr in &current_exprs {
1270            proj_expr.expr.apply(|expr| {
1271                if let Some(column) = expr.downcast_ref::<Column>() {
1272                    *column_ref_map.entry(column.clone()).or_default() += 1;
1273                }
1274                Ok(TreeNodeRecursion::Continue)
1275            })?;
1276        }
1277        let inner_exprs = inner_proj.expr();
1278        // Merging these projections is not beneficial, e.g
1279        // If an expression is not trivial (KeepInPlace) and it is referred more than 1, unifies projections will be
1280        // beneficial as caching mechanism for non-trivial computations.
1281        // See discussion in: https://github.com/apache/datafusion/issues/8296
1282        let blocked = column_ref_map.iter().any(|(column, count)| {
1283            *count > 1
1284                && !inner_exprs[column.index()]
1285                    .expr
1286                    .placement()
1287                    .should_push_to_leaves()
1288        });
1289        if blocked {
1290            break;
1291        }
1292
1293        let mut new_phys: Vec<Arc<dyn PhysicalExpr>> =
1294            Vec::with_capacity(current_exprs.len());
1295        for proj_expr in &current_exprs {
1296            // If there is no match in the input projection, we cannot unify these
1297            // projections. This case will arise if the projection expression contains
1298            // a `PhysicalExpr` variant `update_expr` doesn't support.
1299            let Some(expr) = update_expr(&proj_expr.expr, inner_exprs, true)? else {
1300                break 'outer;
1301            };
1302            new_phys.push(expr);
1303        }
1304        for (proj_expr, expr) in current_exprs.iter_mut().zip(new_phys) {
1305            proj_expr.expr = expr;
1306        }
1307        current_input = Arc::clone(inner_proj.input());
1308        collapsed_any = true;
1309    }
1310
1311    if !collapsed_any {
1312        return Ok(None);
1313    }
1314
1315    // To unify 3 or more sequential projections:
1316    let unified: Arc<dyn ExecutionPlan> =
1317        Arc::new(ProjectionExec::try_new(current_exprs, current_input)?);
1318    remove_unnecessary_projections(unified).data().map(Some)
1319}
1320
1321/// Collect all column indices from the given projection expressions.
1322fn collect_column_indices(exprs: &[ProjectionExpr]) -> Vec<usize> {
1323    // Collect column indices in a deterministic order that preserves the
1324    // projection's column ordering. For simple Column expressions, we use
1325    // the column index directly. For complex expressions, we walk the
1326    // expression tree to collect column references in traversal order.
1327    // This allows the embedded projection to match the desired output
1328    // column order, avoiding a residual ProjectionExec.
1329    let mut seen = std::collections::HashSet::new();
1330    let mut indices = Vec::new();
1331    for proj_expr in exprs {
1332        if let Some(col) = proj_expr.expr.downcast_ref::<Column>() {
1333            // Simple column reference: preserve projection order.
1334            if seen.insert(col.index()) {
1335                indices.push(col.index());
1336            }
1337        } else {
1338            // Complex expression: collect all referenced columns in
1339            // expression tree traversal order (deterministic) to preserve
1340            // the natural ordering of column references.
1341            proj_expr
1342                .expr
1343                .apply(|expr| {
1344                    if let Some(col) = expr.downcast_ref::<Column>()
1345                        && seen.insert(col.index())
1346                    {
1347                        indices.push(col.index());
1348                    }
1349                    Ok(TreeNodeRecursion::Continue)
1350                })
1351                .expect("closure always returns OK");
1352        }
1353    }
1354    indices
1355}
1356
1357/// This function determines and returns a vector of indices representing the
1358/// positions of columns in `projection_exprs` that are involved in `join_filter`,
1359/// and correspond to a particular side (`join_side`) of the join operation.
1360///
1361/// Notes: Column indices in the projection expressions are based on the join schema,
1362/// whereas the join filter is based on the join child schema. `column_index_offset`
1363/// represents the offset between them.
1364fn new_indices_for_join_filter(
1365    join_filter: &JoinFilter,
1366    join_side: JoinSide,
1367    projection_exprs: &[(Column, String)],
1368    column_index_offset: usize,
1369) -> Vec<usize> {
1370    join_filter
1371        .column_indices()
1372        .iter()
1373        .filter(|col_idx| col_idx.side == join_side)
1374        .filter_map(|col_idx| {
1375            projection_exprs
1376                .iter()
1377                .position(|(col, _)| col_idx.index + column_index_offset == col.index())
1378        })
1379        .collect()
1380}
1381
1382/// This function generates a new set of columns to be used in a hash join
1383/// operation based on a set of equi-join conditions (`hash_join_on`) and a
1384/// list of projection expressions (`projection_exprs`).
1385///
1386/// Notes: Column indices in the projection expressions are based on the join schema,
1387/// whereas the join on expressions are based on the join child schema. `column_index_offset`
1388/// represents the offset between them.
1389fn new_columns_for_join_on(
1390    hash_join_on: &[&PhysicalExprRef],
1391    projection_exprs: &[(Column, String)],
1392    column_index_offset: usize,
1393) -> Option<Vec<PhysicalExprRef>> {
1394    let new_columns = hash_join_on
1395        .iter()
1396        .filter_map(|on| {
1397            // Rewrite all columns in `on`
1398            Arc::clone(*on)
1399                .transform(|expr| {
1400                    if let Some(column) = expr.downcast_ref::<Column>() {
1401                        // Find the column in the projection expressions
1402                        let new_column = projection_exprs
1403                            .iter()
1404                            .enumerate()
1405                            .find(|(_, (proj_column, _))| {
1406                                column.name() == proj_column.name()
1407                                    && column.index() + column_index_offset
1408                                        == proj_column.index()
1409                            })
1410                            .map(|(index, (_, alias))| Column::new(alias, index));
1411                        if let Some(new_column) = new_column {
1412                            Ok(Transformed::yes(Arc::new(new_column)))
1413                        } else {
1414                            // If the column is not found in the projection expressions,
1415                            // it means that the column is not projected. In this case,
1416                            // we cannot push the projection down.
1417                            internal_err!(
1418                                "Column {:?} not found in projection expressions",
1419                                column
1420                            )
1421                        }
1422                    } else {
1423                        Ok(Transformed::no(expr))
1424                    }
1425                })
1426                .data()
1427                .ok()
1428        })
1429        .collect::<Vec<_>>();
1430    (new_columns.len() == hash_join_on.len()).then_some(new_columns)
1431}
1432
1433#[cfg(test)]
1434mod tests {
1435    use super::*;
1436
1437    use crate::common::collect;
1438    use crate::empty::EmptyExec;
1439
1440    use crate::filter_pushdown::PushedDown;
1441    use crate::statistics::{StatisticsArgs, StatisticsContext};
1442    use crate::test;
1443    use crate::test::exec::StatisticsExec;
1444
1445    use arrow::datatypes::{DataType, Field, Schema};
1446    use datafusion_common::ScalarValue;
1447    use datafusion_common::stats::{ColumnStatistics, Precision, Statistics};
1448
1449    use datafusion_expr::Operator;
1450    use datafusion_physical_expr::expressions::{
1451        BinaryExpr, Column, DynamicFilterPhysicalExpr, Literal, binary, col, lit,
1452    };
1453
1454    #[test]
1455    fn test_try_new_with_schema_metadata_only_replaces_metadata() -> Result<()> {
1456        let input_schema = Arc::new(Schema::new(vec![Field::new(
1457            "input",
1458            DataType::Int32,
1459            false,
1460        )]));
1461        let input: Arc<dyn ExecutionPlan> = Arc::new(EmptyExec::new(input_schema));
1462        let field_metadata =
1463            HashMap::from([("field-key".to_string(), "field-value".to_string())]);
1464        let schema_metadata =
1465            HashMap::from([("schema-key".to_string(), "schema-value".to_string())]);
1466        let metadata_schema = Schema::new_with_metadata(
1467            vec![
1468                Field::new("ignored", DataType::Utf8, true)
1469                    .with_metadata(field_metadata.clone()),
1470            ],
1471            schema_metadata.clone(),
1472        );
1473
1474        let projection = ProjectionExec::try_new_with_schema_metadata(
1475            [ProjectionExpr {
1476                expr: Arc::new(Column::new("input", 0)),
1477                alias: "output".to_string(),
1478            }],
1479            input,
1480            &metadata_schema,
1481        )?;
1482
1483        let expected_schema = Arc::new(Schema::new_with_metadata(
1484            vec![
1485                Field::new("output", DataType::Int32, false)
1486                    .with_metadata(field_metadata),
1487            ],
1488            schema_metadata,
1489        ));
1490        assert_eq!(projection.schema(), expected_schema);
1491        Ok(())
1492    }
1493
1494    #[test]
1495    fn test_collect_column_indices() -> Result<()> {
1496        let expr = Arc::new(BinaryExpr::new(
1497            Arc::new(Column::new("b", 7)),
1498            Operator::Minus,
1499            Arc::new(BinaryExpr::new(
1500                Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
1501                Operator::Plus,
1502                Arc::new(Column::new("a", 1)),
1503            )),
1504        ));
1505        let column_indices = collect_column_indices(&[ProjectionExpr {
1506            expr,
1507            alias: "b-(1+a)".to_string(),
1508        }]);
1509        // Tree traversal order: b@7 is visited before a@1
1510        assert_eq!(column_indices, vec![7, 1]);
1511        Ok(())
1512    }
1513
1514    #[test]
1515    fn test_try_pushdown_through_join_validates_column_indices() -> Result<()> {
1516        let child_schema =
1517            Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, false)]));
1518        let left: Arc<dyn ExecutionPlan> =
1519            Arc::new(EmptyExec::new(Arc::clone(&child_schema)));
1520        let right: Arc<dyn ExecutionPlan> = Arc::new(EmptyExec::new(child_schema));
1521        let join_schema = Arc::new(Schema::new(vec![
1522            Field::new("left_i", DataType::Int32, false),
1523            Field::new("right_i", DataType::Int32, false),
1524        ]));
1525        let join: Arc<dyn ExecutionPlan> =
1526            Arc::new(EmptyExec::new(Arc::clone(&join_schema)));
1527        let projection = ProjectionExec::try_new(
1528            vec![ProjectionExpr {
1529                expr: Arc::new(Column::new("left_i", 0)),
1530                alias: "left_i".to_string(),
1531            }],
1532            join,
1533        )?;
1534
1535        let Err(error) = try_pushdown_through_join_with_column_indices(
1536            &projection,
1537            &left,
1538            &right,
1539            &[],
1540            &join_schema,
1541            None,
1542            &[],
1543        ) else {
1544            panic!("expected a mismatched mapping length to return an error");
1545        };
1546        assert!(
1547            error.to_string().contains(
1548                "Column index mapping has 0 entries but join schema has 2 fields"
1549            )
1550        );
1551
1552        let invalid_child_index = [
1553            ColumnIndex {
1554                index: 1,
1555                side: JoinSide::Left,
1556            },
1557            ColumnIndex {
1558                index: 0,
1559                side: JoinSide::Right,
1560            },
1561        ];
1562        let Err(error) = try_pushdown_through_join_with_column_indices(
1563            &projection,
1564            &left,
1565            &right,
1566            &[],
1567            &join_schema,
1568            None,
1569            &invalid_child_index,
1570        ) else {
1571            panic!("expected an invalid child index to return an error");
1572        };
1573        assert!(error.to_string().contains(
1574            "Join output column 0 maps to left child column 1, but the child has 1 fields"
1575        ));
1576
1577        let wider_join_schema = Arc::new(Schema::new(vec![
1578            Field::new("left_i", DataType::Int32, false),
1579            Field::new("right_i", DataType::Int32, false),
1580            Field::new("extra", DataType::Int32, false),
1581        ]));
1582        let wider_join: Arc<dyn ExecutionPlan> =
1583            Arc::new(EmptyExec::new(wider_join_schema));
1584        let out_of_mapping_projection = ProjectionExec::try_new(
1585            vec![ProjectionExpr {
1586                expr: Arc::new(Column::new("extra", 2)),
1587                alias: "extra".to_string(),
1588            }],
1589            wider_join,
1590        )?;
1591        let valid_child_indices = [
1592            ColumnIndex {
1593                index: 0,
1594                side: JoinSide::Left,
1595            },
1596            ColumnIndex {
1597                index: 0,
1598                side: JoinSide::Right,
1599            },
1600        ];
1601        let Err(error) = try_pushdown_through_join_with_column_indices(
1602            &out_of_mapping_projection,
1603            &left,
1604            &right,
1605            &[],
1606            &join_schema,
1607            None,
1608            &valid_child_indices,
1609        ) else {
1610            panic!("expected an out-of-mapping projection to return an error");
1611        };
1612        assert!(
1613            error.to_string().contains(
1614                "Projection column 2 is outside the 2-entry column index mapping"
1615            )
1616        );
1617
1618        Ok(())
1619    }
1620
1621    #[test]
1622    fn test_join_table_borders() -> Result<()> {
1623        let projections = vec![
1624            (Column::new("b", 1), "b".to_owned()),
1625            (Column::new("c", 2), "c".to_owned()),
1626            (Column::new("e", 4), "e".to_owned()),
1627            (Column::new("d", 3), "d".to_owned()),
1628            (Column::new("c", 2), "c".to_owned()),
1629            (Column::new("f", 5), "f".to_owned()),
1630            (Column::new("h", 7), "h".to_owned()),
1631            (Column::new("g", 6), "g".to_owned()),
1632        ];
1633        let left_table_column_count = 5;
1634        assert_eq!(
1635            join_table_borders(left_table_column_count, &projections),
1636            (4, 5)
1637        );
1638
1639        let left_table_column_count = 8;
1640        assert_eq!(
1641            join_table_borders(left_table_column_count, &projections),
1642            (7, 8)
1643        );
1644
1645        let left_table_column_count = 1;
1646        assert_eq!(
1647            join_table_borders(left_table_column_count, &projections),
1648            (-1, 0)
1649        );
1650
1651        let projections = vec![
1652            (Column::new("a", 0), "a".to_owned()),
1653            (Column::new("b", 1), "b".to_owned()),
1654            (Column::new("d", 3), "d".to_owned()),
1655            (Column::new("g", 6), "g".to_owned()),
1656            (Column::new("e", 4), "e".to_owned()),
1657            (Column::new("f", 5), "f".to_owned()),
1658            (Column::new("e", 4), "e".to_owned()),
1659            (Column::new("h", 7), "h".to_owned()),
1660        ];
1661        let left_table_column_count = 5;
1662        assert_eq!(
1663            join_table_borders(left_table_column_count, &projections),
1664            (2, 7)
1665        );
1666
1667        let left_table_column_count = 7;
1668        assert_eq!(
1669            join_table_borders(left_table_column_count, &projections),
1670            (6, 7)
1671        );
1672
1673        Ok(())
1674    }
1675
1676    #[tokio::test]
1677    async fn project_no_column() -> Result<()> {
1678        let task_ctx = Arc::new(TaskContext::default());
1679
1680        let exec = test::scan_partitioned(1);
1681        let expected = collect(exec.execute(0, Arc::clone(&task_ctx))?).await?;
1682
1683        let projection = ProjectionExec::try_new(vec![] as Vec<ProjectionExpr>, exec)?;
1684        let stream = projection.execute(0, Arc::clone(&task_ctx))?;
1685        let output = collect(stream).await?;
1686        assert_eq!(output.len(), expected.len());
1687
1688        Ok(())
1689    }
1690
1691    #[tokio::test]
1692    async fn project_old_syntax() {
1693        let exec = test::scan_partitioned(1);
1694        let schema = exec.schema();
1695        let expr = col("i", &schema).unwrap();
1696        ProjectionExec::try_new(
1697            vec![
1698                // use From impl of ProjectionExpr to create ProjectionExpr
1699                // to test old syntax
1700                (expr, "c".to_string()),
1701            ],
1702            exec,
1703        )
1704        // expect this to succeed
1705        .unwrap();
1706    }
1707
1708    #[test]
1709    fn test_projection_statistics_uses_input_schema() {
1710        let input_schema = Schema::new(vec![
1711            Field::new("a", DataType::Int32, false),
1712            Field::new("b", DataType::Int32, false),
1713            Field::new("c", DataType::Int32, false),
1714            Field::new("d", DataType::Int32, false),
1715            Field::new("e", DataType::Int32, false),
1716            Field::new("f", DataType::Int32, false),
1717        ]);
1718
1719        let input_statistics = Statistics {
1720            num_rows: Precision::Exact(10),
1721            column_statistics: vec![
1722                ColumnStatistics {
1723                    min_value: Precision::Exact(ScalarValue::Int32(Some(1))),
1724                    max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
1725                    ..Default::default()
1726                },
1727                ColumnStatistics {
1728                    min_value: Precision::Exact(ScalarValue::Int32(Some(5))),
1729                    max_value: Precision::Exact(ScalarValue::Int32(Some(50))),
1730                    ..Default::default()
1731                },
1732                ColumnStatistics {
1733                    min_value: Precision::Exact(ScalarValue::Int32(Some(10))),
1734                    max_value: Precision::Exact(ScalarValue::Int32(Some(40))),
1735                    ..Default::default()
1736                },
1737                ColumnStatistics {
1738                    min_value: Precision::Exact(ScalarValue::Int32(Some(20))),
1739                    max_value: Precision::Exact(ScalarValue::Int32(Some(30))),
1740                    ..Default::default()
1741                },
1742                ColumnStatistics {
1743                    min_value: Precision::Exact(ScalarValue::Int32(Some(21))),
1744                    max_value: Precision::Exact(ScalarValue::Int32(Some(29))),
1745                    ..Default::default()
1746                },
1747                ColumnStatistics {
1748                    min_value: Precision::Exact(ScalarValue::Int32(Some(24))),
1749                    max_value: Precision::Exact(ScalarValue::Int32(Some(26))),
1750                    ..Default::default()
1751                },
1752            ],
1753            ..Default::default()
1754        };
1755
1756        let input = Arc::new(StatisticsExec::new(input_statistics, input_schema));
1757
1758        // Create projection expressions that reference columns from the input schema and the length
1759        // of output schema columns < input schema columns and hence if we use the last few columns
1760        // from the input schema in the expressions here, bounds_check would fail on them if output
1761        // schema is supplied to the partitions_statistics method.
1762        let exprs: Vec<ProjectionExpr> = vec![
1763            ProjectionExpr {
1764                expr: Arc::new(Column::new("c", 2)) as Arc<dyn PhysicalExpr>,
1765                alias: "c_renamed".to_string(),
1766            },
1767            ProjectionExpr {
1768                expr: Arc::new(BinaryExpr::new(
1769                    Arc::new(Column::new("e", 4)),
1770                    Operator::Plus,
1771                    Arc::new(Column::new("f", 5)),
1772                )) as Arc<dyn PhysicalExpr>,
1773                alias: "e_plus_f".to_string(),
1774            },
1775        ];
1776
1777        let projection = ProjectionExec::try_new(exprs, input).unwrap();
1778
1779        let stats = StatisticsContext::new()
1780            .compute(&projection, &StatisticsArgs::new())
1781            .unwrap();
1782
1783        assert_eq!(stats.num_rows, Precision::Exact(10));
1784        assert_eq!(
1785            stats.column_statistics.len(),
1786            2,
1787            "Expected 2 columns in projection statistics"
1788        );
1789        assert!(stats.total_byte_size.is_exact().unwrap_or(false));
1790    }
1791
1792    #[test]
1793    fn test_filter_pushdown_with_alias() -> Result<()> {
1794        let input_schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1795        let input = Arc::new(StatisticsExec::new(
1796            Statistics::new_unknown(&input_schema),
1797            input_schema.clone(),
1798        ));
1799
1800        // project "a" as "b"
1801        let projection = ProjectionExec::try_new(
1802            vec![ProjectionExpr {
1803                expr: Arc::new(Column::new("a", 0)),
1804                alias: "b".to_string(),
1805            }],
1806            input,
1807        )?;
1808
1809        // filter "b > 5"
1810        let filter = Arc::new(BinaryExpr::new(
1811            Arc::new(Column::new("b", 0)),
1812            Operator::Gt,
1813            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
1814        )) as Arc<dyn PhysicalExpr>;
1815
1816        let description = projection.gather_filters_for_pushdown(
1817            FilterPushdownPhase::Post,
1818            vec![filter],
1819            &ConfigOptions::default(),
1820        )?;
1821
1822        // Should be converted to "a > 5"
1823        // "a" is index 0 in input
1824        let expected_filter = Arc::new(BinaryExpr::new(
1825            Arc::new(Column::new("a", 0)),
1826            Operator::Gt,
1827            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
1828        )) as Arc<dyn PhysicalExpr>;
1829
1830        assert_eq!(description.self_filters(), vec![vec![]]);
1831        let pushed_filters = &description.parent_filters()[0];
1832        assert_eq!(
1833            format!("{}", pushed_filters[0].predicate),
1834            format!("{}", expected_filter)
1835        );
1836        // Verify the predicate was actually pushed down
1837        assert!(matches!(pushed_filters[0].discriminant, PushedDown::Yes));
1838
1839        Ok(())
1840    }
1841
1842    #[test]
1843    fn test_filter_pushdown_with_multiple_aliases() -> Result<()> {
1844        let input_schema = Schema::new(vec![
1845            Field::new("a", DataType::Int32, false),
1846            Field::new("b", DataType::Int32, false),
1847        ]);
1848        let input = Arc::new(StatisticsExec::new(
1849            Statistics {
1850                column_statistics: vec![Default::default(); input_schema.fields().len()],
1851                ..Default::default()
1852            },
1853            input_schema.clone(),
1854        ));
1855
1856        // project "a" as "x", "b" as "y"
1857        let projection = ProjectionExec::try_new(
1858            vec![
1859                ProjectionExpr {
1860                    expr: Arc::new(Column::new("a", 0)),
1861                    alias: "x".to_string(),
1862                },
1863                ProjectionExpr {
1864                    expr: Arc::new(Column::new("b", 1)),
1865                    alias: "y".to_string(),
1866                },
1867            ],
1868            input,
1869        )?;
1870
1871        // filter "x > 5"
1872        let filter1 = Arc::new(BinaryExpr::new(
1873            Arc::new(Column::new("x", 0)),
1874            Operator::Gt,
1875            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
1876        )) as Arc<dyn PhysicalExpr>;
1877
1878        // filter "y < 10"
1879        let filter2 = Arc::new(BinaryExpr::new(
1880            Arc::new(Column::new("y", 1)),
1881            Operator::Lt,
1882            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
1883        )) as Arc<dyn PhysicalExpr>;
1884
1885        let description = projection.gather_filters_for_pushdown(
1886            FilterPushdownPhase::Post,
1887            vec![filter1, filter2],
1888            &ConfigOptions::default(),
1889        )?;
1890
1891        // Should be converted to "a > 5" and "b < 10"
1892        let expected_filter1 = Arc::new(BinaryExpr::new(
1893            Arc::new(Column::new("a", 0)),
1894            Operator::Gt,
1895            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
1896        )) as Arc<dyn PhysicalExpr>;
1897
1898        let expected_filter2 = Arc::new(BinaryExpr::new(
1899            Arc::new(Column::new("b", 1)),
1900            Operator::Lt,
1901            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
1902        )) as Arc<dyn PhysicalExpr>;
1903
1904        let pushed_filters = &description.parent_filters()[0];
1905        assert_eq!(pushed_filters.len(), 2);
1906        // Note: The order of filters is preserved
1907        assert_eq!(
1908            format!("{}", pushed_filters[0].predicate),
1909            format!("{}", expected_filter1)
1910        );
1911        assert_eq!(
1912            format!("{}", pushed_filters[1].predicate),
1913            format!("{}", expected_filter2)
1914        );
1915        // Verify the predicates were actually pushed down
1916        assert!(matches!(pushed_filters[0].discriminant, PushedDown::Yes));
1917        assert!(matches!(pushed_filters[1].discriminant, PushedDown::Yes));
1918
1919        Ok(())
1920    }
1921
1922    #[test]
1923    fn test_filter_pushdown_with_swapped_aliases() -> Result<()> {
1924        let input_schema = Schema::new(vec![
1925            Field::new("a", DataType::Int32, false),
1926            Field::new("b", DataType::Int32, false),
1927        ]);
1928        let input = Arc::new(StatisticsExec::new(
1929            Statistics {
1930                column_statistics: vec![Default::default(); input_schema.fields().len()],
1931                ..Default::default()
1932            },
1933            input_schema.clone(),
1934        ));
1935
1936        // project "a" as "b", "b" as "a"
1937        let projection = ProjectionExec::try_new(
1938            vec![
1939                ProjectionExpr {
1940                    expr: Arc::new(Column::new("a", 0)),
1941                    alias: "b".to_string(),
1942                },
1943                ProjectionExpr {
1944                    expr: Arc::new(Column::new("b", 1)),
1945                    alias: "a".to_string(),
1946                },
1947            ],
1948            input,
1949        )?;
1950
1951        // filter "b > 5" (output column 0, which is "a" in input)
1952        let filter1 = Arc::new(BinaryExpr::new(
1953            Arc::new(Column::new("b", 0)),
1954            Operator::Gt,
1955            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
1956        )) as Arc<dyn PhysicalExpr>;
1957
1958        // filter "a < 10" (output column 1, which is "b" in input)
1959        let filter2 = Arc::new(BinaryExpr::new(
1960            Arc::new(Column::new("a", 1)),
1961            Operator::Lt,
1962            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
1963        )) as Arc<dyn PhysicalExpr>;
1964
1965        let description = projection.gather_filters_for_pushdown(
1966            FilterPushdownPhase::Post,
1967            vec![filter1, filter2],
1968            &ConfigOptions::default(),
1969        )?;
1970
1971        let pushed_filters = &description.parent_filters()[0];
1972        assert_eq!(pushed_filters.len(), 2);
1973
1974        // "b" (output index 0) -> "a" (input index 0)
1975        let expected_filter1 = "a@0 > 5";
1976        // "a" (output index 1) -> "b" (input index 1)
1977        let expected_filter2 = "b@1 < 10";
1978
1979        assert_eq!(format!("{}", pushed_filters[0].predicate), expected_filter1);
1980        assert_eq!(format!("{}", pushed_filters[1].predicate), expected_filter2);
1981        // Verify the predicates were actually pushed down
1982        assert!(matches!(pushed_filters[0].discriminant, PushedDown::Yes));
1983        assert!(matches!(pushed_filters[1].discriminant, PushedDown::Yes));
1984
1985        Ok(())
1986    }
1987
1988    #[test]
1989    fn test_filter_pushdown_with_mixed_columns() -> Result<()> {
1990        let input_schema = Schema::new(vec![
1991            Field::new("a", DataType::Int32, false),
1992            Field::new("b", DataType::Int32, false),
1993        ]);
1994        let input = Arc::new(StatisticsExec::new(
1995            Statistics {
1996                column_statistics: vec![Default::default(); input_schema.fields().len()],
1997                ..Default::default()
1998            },
1999            input_schema.clone(),
2000        ));
2001
2002        // project "a" as "x", "b" as "b" (pass through)
2003        let projection = ProjectionExec::try_new(
2004            vec![
2005                ProjectionExpr {
2006                    expr: Arc::new(Column::new("a", 0)),
2007                    alias: "x".to_string(),
2008                },
2009                ProjectionExpr {
2010                    expr: Arc::new(Column::new("b", 1)),
2011                    alias: "b".to_string(),
2012                },
2013            ],
2014            input,
2015        )?;
2016
2017        // filter "x > 5"
2018        let filter1 = Arc::new(BinaryExpr::new(
2019            Arc::new(Column::new("x", 0)),
2020            Operator::Gt,
2021            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
2022        )) as Arc<dyn PhysicalExpr>;
2023
2024        // filter "b < 10" (using output index 1 which corresponds to 'b')
2025        let filter2 = Arc::new(BinaryExpr::new(
2026            Arc::new(Column::new("b", 1)),
2027            Operator::Lt,
2028            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2029        )) as Arc<dyn PhysicalExpr>;
2030
2031        let description = projection.gather_filters_for_pushdown(
2032            FilterPushdownPhase::Post,
2033            vec![filter1, filter2],
2034            &ConfigOptions::default(),
2035        )?;
2036
2037        let pushed_filters = &description.parent_filters()[0];
2038        assert_eq!(pushed_filters.len(), 2);
2039        // "x" -> "a" (index 0)
2040        let expected_filter1 = "a@0 > 5";
2041        // "b" -> "b" (index 1)
2042        let expected_filter2 = "b@1 < 10";
2043
2044        assert_eq!(format!("{}", pushed_filters[0].predicate), expected_filter1);
2045        assert_eq!(format!("{}", pushed_filters[1].predicate), expected_filter2);
2046        // Verify the predicates were actually pushed down
2047        assert!(matches!(pushed_filters[0].discriminant, PushedDown::Yes));
2048        assert!(matches!(pushed_filters[1].discriminant, PushedDown::Yes));
2049
2050        Ok(())
2051    }
2052
2053    #[test]
2054    fn test_filter_pushdown_with_complex_expression() -> Result<()> {
2055        let input_schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
2056        let input = Arc::new(StatisticsExec::new(
2057            Statistics {
2058                column_statistics: vec![Default::default(); input_schema.fields().len()],
2059                ..Default::default()
2060            },
2061            input_schema.clone(),
2062        ));
2063
2064        // project "a + 1" as "z"
2065        let projection = ProjectionExec::try_new(
2066            vec![ProjectionExpr {
2067                expr: Arc::new(BinaryExpr::new(
2068                    Arc::new(Column::new("a", 0)),
2069                    Operator::Plus,
2070                    Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
2071                )),
2072                alias: "z".to_string(),
2073            }],
2074            input,
2075        )?;
2076
2077        // filter "z > 10"
2078        let filter = Arc::new(BinaryExpr::new(
2079            Arc::new(Column::new("z", 0)),
2080            Operator::Gt,
2081            Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2082        )) as Arc<dyn PhysicalExpr>;
2083
2084        let description = projection.gather_filters_for_pushdown(
2085            FilterPushdownPhase::Post,
2086            vec![filter],
2087            &ConfigOptions::default(),
2088        )?;
2089
2090        // expand to `a + 1 > 10`
2091        let pushed_filters = &description.parent_filters()[0];
2092        assert!(matches!(pushed_filters[0].discriminant, PushedDown::Yes));
2093        assert_eq!(format!("{}", pushed_filters[0].predicate), "a@0 + 1 > 10");
2094
2095        Ok(())
2096    }
2097
2098    #[test]
2099    fn test_filter_pushdown_with_unknown_column() -> Result<()> {
2100        let input_schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
2101        let input = Arc::new(StatisticsExec::new(
2102            Statistics {
2103                column_statistics: vec![Default::default(); input_schema.fields().len()],
2104                ..Default::default()
2105            },
2106            input_schema.clone(),
2107        ));
2108
2109        // project "a" as "a"
2110        let projection = ProjectionExec::try_new(
2111            vec![ProjectionExpr {
2112                expr: Arc::new(Column::new("a", 0)),
2113                alias: "a".to_string(),
2114            }],
2115            input,
2116        )?;
2117
2118        // filter "unknown_col > 5" - using a column name that doesn't exist in projection output
2119        // Column constructor: name, index. Index 1 doesn't exist.
2120        let filter = Arc::new(BinaryExpr::new(
2121            Arc::new(Column::new("unknown_col", 1)),
2122            Operator::Gt,
2123            Arc::new(Literal::new(ScalarValue::Int32(Some(5)))),
2124        )) as Arc<dyn PhysicalExpr>;
2125
2126        let description = projection.gather_filters_for_pushdown(
2127            FilterPushdownPhase::Post,
2128            vec![filter],
2129            &ConfigOptions::default(),
2130        )?;
2131
2132        let pushed_filters = &description.parent_filters()[0];
2133        assert!(matches!(pushed_filters[0].discriminant, PushedDown::No));
2134        // The column shouldn't be found in the alias map, so it remains unchanged with its index
2135        assert_eq!(
2136            format!("{}", pushed_filters[0].predicate),
2137            "unknown_col@1 > 5"
2138        );
2139
2140        Ok(())
2141    }
2142
2143    /// Basic test for `DynamicFilterPhysicalExpr` can correctly update its child expression
2144    /// i.e. starting with lit(true) and after update it becomes `a > 5`
2145    /// with projection [b - 1 as a], the pushed down filter should be `b - 1 > 5`
2146    #[test]
2147    fn test_basic_dyn_filter_projection_pushdown_update_child() -> Result<()> {
2148        let input_schema =
2149            Arc::new(Schema::new(vec![Field::new("b", DataType::Int32, false)]));
2150
2151        let input = Arc::new(StatisticsExec::new(
2152            Statistics {
2153                column_statistics: vec![Default::default(); input_schema.fields().len()],
2154                ..Default::default()
2155            },
2156            input_schema.as_ref().clone(),
2157        ));
2158
2159        // project "b" - 1 as "a"
2160        let projection = ProjectionExec::try_new(
2161            vec![ProjectionExpr {
2162                expr: binary(
2163                    Arc::new(Column::new("b", 0)),
2164                    Operator::Minus,
2165                    lit(1),
2166                    &input_schema,
2167                )
2168                .unwrap(),
2169                alias: "a".to_string(),
2170            }],
2171            input,
2172        )?;
2173
2174        // simulate projection's parent create a dynamic filter on "a"
2175        let projected_schema = projection.schema();
2176        let col_a = col("a", &projected_schema)?;
2177        let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new(
2178            vec![Arc::clone(&col_a)],
2179            lit(true),
2180        ));
2181        // Initial state should be lit(true)
2182        let current = dynamic_filter.current()?;
2183        assert_eq!(format!("{current}"), "true");
2184
2185        let dyn_phy_expr: Arc<dyn PhysicalExpr> = Arc::clone(&dynamic_filter) as _;
2186
2187        let description = projection.gather_filters_for_pushdown(
2188            FilterPushdownPhase::Post,
2189            vec![dyn_phy_expr],
2190            &ConfigOptions::default(),
2191        )?;
2192
2193        let pushed_filters = &description.parent_filters()[0][0];
2194
2195        // Check currently pushed_filters is lit(true)
2196        assert_eq!(
2197            format!("{}", pushed_filters.predicate),
2198            "DynamicFilter [ empty ]"
2199        );
2200
2201        // Update to a > 5 (after projection, b is now called a)
2202        let new_expr =
2203            Arc::new(BinaryExpr::new(Arc::clone(&col_a), Operator::Gt, lit(5i32)));
2204        dynamic_filter.update(new_expr)?;
2205
2206        // Now it should be a > 5
2207        let current = dynamic_filter.current()?;
2208        assert_eq!(format!("{current}"), "a@0 > 5");
2209
2210        // Check currently pushed_filters is b - 1 > 5 (because b - 1 is projected as a)
2211        assert_eq!(
2212            format!("{}", pushed_filters.predicate),
2213            "DynamicFilter [ b@0 - 1 > 5 ]"
2214        );
2215
2216        Ok(())
2217    }
2218}