Skip to main content

datafusion_physical_plan/
union.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// Some of these functions reference the Postgres documentation
19// or implementation to ensure compatibility and are subject to
20// the Postgres license.
21
22//! The Union operator combines multiple inputs with the same schema
23
24use std::borrow::Borrow;
25use std::pin::Pin;
26use std::sync::Arc;
27use std::task::{Context, Poll};
28
29use super::{
30    DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, Partitioning,
31    PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
32    metrics::{ExecutionPlanMetricsSet, MetricsSet},
33};
34use crate::execution_plan::{
35    CardinalityEffect, InvariantLevel, boundedness_from_children,
36    check_default_invariants, emission_type_from_children,
37};
38use crate::filter::FilterExec;
39use crate::filter_pushdown::{
40    ChildPushdownResult, FilterDescription, FilterPushdownPhase,
41    FilterPushdownPropagation, PushedDown,
42};
43use crate::metrics::BaselineMetrics;
44use crate::projection::{ProjectionExec, ProjectionExpr, make_with_child};
45use crate::statistics::{ChildStats, StatisticsArgs};
46use crate::stream::ObservedStream;
47use crate::{ChildrenPropertiesMode, ReplaceChildrenOptions, validate_child_count};
48
49use arrow::datatypes::{Field, Schema, SchemaRef};
50use arrow::record_batch::RecordBatch;
51use datafusion_common::config::ConfigOptions;
52use datafusion_common::stats::NdvFallback;
53use datafusion_common::tree_node::TreeNodeRecursion;
54use datafusion_common::{
55    Result, assert_or_internal_err, exec_err, internal_datafusion_err, plan_err,
56};
57use datafusion_execution::TaskContext;
58use datafusion_physical_expr::expressions::{CastExpr, Column};
59use datafusion_physical_expr::{
60    EquivalenceProperties, PhysicalExpr, calculate_union, conjunction,
61};
62
63use futures::Stream;
64use itertools::Itertools;
65use log::{debug, trace, warn};
66use tokio::macros::support::thread_rng_n;
67
68/// Coerces `input`'s output schema to exactly `schema` via a `ProjectionExec`
69/// that re-stamps each column with the union's merged field (same
70/// `DataType`, but the union's merged nullability/name/metadata), or returns
71/// `input` unchanged if its schema already matches. [`UnionExec::try_new`]
72/// and [`InterleaveExec::try_new`] call this on every child, so the coercion
73/// is visible in the plan tree (e.g. in `EXPLAIN`) instead of happening
74/// invisibly inside the union operator's own `execute()`.
75///
76/// A column whose `DataType` doesn't already match the union's is a genuine
77/// data type mismatch (as opposed to a nullability/name/metadata-only one),
78/// and is rejected eagerly here rather than silently cast or deferred to a
79/// runtime failure -- this only ever changes a column's declared schema,
80/// never its values.
81///
82/// Casting a column to its own `DataType` (only the `Field`'s nullability,
83/// name, or metadata changes) is a zero-copy relabeling: the cast kernel's
84/// same-type fast path (`cast_array_by_name`) just clones the `Arc<dyn
85/// Array>`, so this carries no runtime overhead over the schema it replaces.
86///
87/// See <https://github.com/apache/datafusion/issues/15394>.
88fn coerce_schema(
89    input: Arc<dyn ExecutionPlan>,
90    schema: &SchemaRef,
91) -> Result<Arc<dyn ExecutionPlan>> {
92    let input_schema = input.schema();
93    if &input_schema == schema {
94        return Ok(input);
95    }
96
97    let exprs = input_schema
98        .fields()
99        .iter()
100        .zip(schema.fields())
101        .enumerate()
102        .map(|(i, (input_field, target_field))| {
103            if input_field.data_type() != target_field.data_type() {
104                return plan_err!(
105                    "UnionExec/InterleaveExec requires all inputs to have the same \
106                     data type per column; column {i} has type {} in one input, but \
107                     the union schema expects {}",
108                    input_field.data_type(),
109                    target_field.data_type()
110                );
111            }
112            let column: Arc<dyn PhysicalExpr> =
113                Arc::new(Column::new(input_field.name(), i));
114            let expr = if input_field == target_field {
115                column
116            } else {
117                Arc::new(CastExpr::new_with_target_field(
118                    column,
119                    Arc::clone(target_field),
120                    None,
121                )) as Arc<dyn PhysicalExpr>
122            };
123            Ok(ProjectionExpr {
124                expr,
125                alias: target_field.name().clone(),
126            })
127        })
128        .collect::<Result<Vec<_>>>()?;
129
130    Ok(Arc::new(ProjectionExec::try_new(exprs, input)?))
131}
132
133/// `UnionExec`: `UNION ALL` execution plan.
134///
135/// `UnionExec` combines multiple inputs with the same schema by
136/// concatenating the partitions.  It does not mix or copy data within
137/// or across partitions. Thus if the input partitions are sorted, the
138/// output partitions of the union are also sorted.
139///
140/// For example, given a `UnionExec` of two inputs, with `N`
141/// partitions, and `M` partitions, there will be `N+M` output
142/// partitions. The first `N` output partitions are from Input 1
143/// partitions, and then next `M` output partitions are from Input 2.
144///
145/// ```text
146///                        ▲       ▲           ▲         ▲
147///                        │       │           │         │
148///      Output            │  ...  │           │         │
149///    Partitions          │0      │N-1        │ N       │N+M-1
150/// (passes through   ┌────┴───────┴───────────┴─────────┴───┐
151///  the N+M input    │              UnionExec               │
152///   partitions)     │                                      │
153///                   └──────────────────────────────────────┘
154///                                      ▲
155///                                      │
156///                                      │
157///       Input           ┌────────┬─────┴────┬──────────┐
158///     Partitions        │ ...    │          │     ...  │
159///                    0  │        │ N-1      │ 0        │  M-1
160///                  ┌────┴────────┴───┐  ┌───┴──────────┴───┐
161///                  │                 │  │                  │
162///                  │                 │  │                  │
163///                  │                 │  │                  │
164///                  │                 │  │                  │
165///                  │                 │  │                  │
166///                  │                 │  │                  │
167///                  │Input 1          │  │Input 2           │
168///                  └─────────────────┘  └──────────────────┘
169/// ```
170#[derive(Debug, Clone)]
171pub struct UnionExec {
172    /// Input execution plan
173    inputs: Vec<Arc<dyn ExecutionPlan>>,
174    /// Execution metrics
175    metrics: ExecutionPlanMetricsSet,
176    /// Cache holding plan properties like equivalences, output partitioning etc.
177    cache: Arc<PlanProperties>,
178}
179
180impl UnionExec {
181    /// Try to create a new UnionExec.
182    ///
183    /// # Errors
184    /// Returns an error if:
185    /// - `inputs` is empty
186    ///
187    /// # Optimization
188    /// If there is only one input, returns that input directly rather than wrapping it in a UnionExec
189    pub fn try_new(
190        inputs: Vec<Arc<dyn ExecutionPlan>>,
191    ) -> Result<Arc<dyn ExecutionPlan>> {
192        match inputs.len() {
193            0 => exec_err!("UnionExec requires at least one input"),
194            1 => Ok(inputs.into_iter().next().unwrap()),
195            _ => {
196                let schema = union_schema(&inputs)?;
197                // The schema of the inputs and the union schema is consistent when:
198                // - They have the same number of fields, and
199                // - Their fields have same types at the same indices.
200                let inputs = inputs
201                    .into_iter()
202                    .map(|input| coerce_schema(input, &schema))
203                    .collect::<Result<Vec<_>>>()?;
204                let cache = Self::compute_properties(&inputs, schema)?;
205                Ok(Arc::new(UnionExec {
206                    inputs,
207                    metrics: ExecutionPlanMetricsSet::new(),
208                    cache: Arc::new(cache),
209                }))
210            }
211        }
212    }
213
214    /// Get inputs of the execution plan
215    pub fn inputs(&self) -> &Vec<Arc<dyn ExecutionPlan>> {
216        &self.inputs
217    }
218
219    /// Maps a global output partition index to the `(input index, local
220    /// partition index)` of the input that owns it, or `None` if out of range.
221    fn owning_input(&self, partition: usize) -> Option<(usize, usize)> {
222        let mut remaining = partition;
223        for (i, input) in self.inputs.iter().enumerate() {
224            let count = input.output_partitioning().partition_count();
225            if remaining < count {
226                return Some((i, remaining));
227            }
228            remaining -= count;
229        }
230        None
231    }
232
233    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
234    fn compute_properties(
235        inputs: &[Arc<dyn ExecutionPlan>],
236        schema: SchemaRef,
237    ) -> Result<PlanProperties> {
238        // Calculate equivalence properties:
239        let children_eqps = inputs
240            .iter()
241            .map(|child| child.equivalence_properties().clone())
242            .collect::<Vec<_>>();
243        let eq_properties = calculate_union(children_eqps, schema)?;
244
245        // Calculate output partitioning; i.e. sum output partitions of the inputs.
246        let num_partitions = inputs
247            .iter()
248            .map(|plan| plan.output_partitioning().partition_count())
249            .sum();
250        let output_partitioning = Partitioning::UnknownPartitioning(num_partitions);
251        Ok(PlanProperties::new(
252            eq_properties,
253            output_partitioning,
254            emission_type_from_children(inputs),
255            boundedness_from_children(inputs),
256        ))
257    }
258}
259
260impl DisplayAs for UnionExec {
261    fn fmt_as(
262        &self,
263        t: DisplayFormatType,
264        f: &mut std::fmt::Formatter,
265    ) -> std::fmt::Result {
266        match t {
267            DisplayFormatType::Default | DisplayFormatType::Verbose => {
268                write!(f, "UnionExec")
269            }
270            DisplayFormatType::TreeRender => Ok(()),
271        }
272    }
273}
274
275impl ExecutionPlan for UnionExec {
276    fn name(&self) -> &'static str {
277        "UnionExec"
278    }
279
280    /// Return a reference to Any that can be used for downcasting
281    fn properties(&self) -> &Arc<PlanProperties> {
282        &self.cache
283    }
284
285    fn check_invariants(&self, check: InvariantLevel) -> Result<()> {
286        check_default_invariants(self, check)?;
287
288        (self.inputs().len() >= 2).then_some(()).ok_or_else(|| {
289            internal_datafusion_err!("UnionExec should have at least 2 children")
290        })
291    }
292
293    fn maintains_input_order(&self) -> Vec<bool> {
294        // If the Union has an output ordering, it maintains at least one
295        // child's ordering (i.e. the meet).
296        // For instance, assume that the first child is SortExpr('a','b','c'),
297        // the second child is SortExpr('a','b') and the third child is
298        // SortExpr('a','b'). The output ordering would be SortExpr('a','b'),
299        // which is the "meet" of all input orderings. In this example, this
300        // function will return vec![false, true, true], indicating that we
301        // preserve the orderings for the 2nd and the 3rd children.
302        if let Some(output_ordering) = self.properties().output_ordering() {
303            self.inputs()
304                .iter()
305                .map(|child| {
306                    if let Some(child_ordering) = child.output_ordering() {
307                        output_ordering.len() == child_ordering.len()
308                    } else {
309                        false
310                    }
311                })
312                .collect()
313        } else {
314            vec![false; self.inputs().len()]
315        }
316    }
317
318    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
319        vec![false; self.children().len()]
320    }
321
322    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
323        self.inputs.iter().collect()
324    }
325
326    fn apply_expressions(
327        &self,
328        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
329    ) -> Result<TreeNodeRecursion> {
330        Ok(TreeNodeRecursion::Continue)
331    }
332
333    fn replace_children(
334        self: Arc<Self>,
335        children: Vec<Arc<dyn ExecutionPlan>>,
336        options: ReplaceChildrenOptions,
337    ) -> Result<Arc<dyn ExecutionPlan>> {
338        validate_child_count!(self, children);
339        match options.children_properties {
340            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
341                inputs: children,
342                metrics: ExecutionPlanMetricsSet::new(),
343                ..Self::clone(&*self)
344            })),
345            ChildrenPropertiesMode::Recompute => UnionExec::try_new(children),
346        }
347    }
348
349    fn with_new_children(
350        self: Arc<Self>,
351        children: Vec<Arc<dyn ExecutionPlan>>,
352    ) -> Result<Arc<dyn ExecutionPlan>> {
353        self.replace_children(
354            children,
355            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
356        )
357    }
358
359    fn with_new_children_and_same_properties(
360        self: Arc<Self>,
361        children: Vec<Arc<dyn ExecutionPlan>>,
362    ) -> Result<Arc<dyn ExecutionPlan>> {
363        self.replace_children(
364            children,
365            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
366        )
367    }
368
369    fn execute(
370        &self,
371        mut partition: usize,
372        context: Arc<TaskContext>,
373    ) -> Result<SendableRecordBatchStream> {
374        trace!(
375            "Start UnionExec::execute for partition {} of context session_id {} and task_id {:?}",
376            partition,
377            context.session_id(),
378            context.task_id()
379        );
380        let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
381        // record the tiny amount of work done in this function so
382        // elapsed_compute is reported as non zero
383        let elapsed_compute = baseline_metrics.elapsed_compute().clone();
384        let _timer = elapsed_compute.timer(); // record on drop
385
386        // find partition to execute
387        for input in self.inputs.iter() {
388            // Calculate whether partition belongs to the current partition
389            if partition < input.output_partitioning().partition_count() {
390                let stream = input.execute(partition, context)?;
391                debug!("Found a Union partition to execute");
392                return Ok(Box::pin(ObservedStream::new(
393                    stream,
394                    baseline_metrics,
395                    None,
396                )));
397            } else {
398                partition -= input.output_partitioning().partition_count();
399            }
400        }
401
402        warn!("Error in Union: Partition {partition} not found");
403
404        exec_err!("Partition {partition} not found in Union")
405    }
406
407    fn metrics(&self) -> Option<MetricsSet> {
408        Some(self.metrics.clone_inner())
409    }
410
411    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
412        if let Some(partition_idx) = partition {
413            // For a specific partition, compute stats only for the input that
414            // owns it; the other inputs are not needed and are skipped.
415            let targeted = self.owning_input(partition_idx);
416            self.inputs
417                .iter()
418                .enumerate()
419                .map(|(i, _)| match targeted {
420                    Some((target_i, target_partition)) if i == target_i => {
421                        ChildStats::At(Some(target_partition))
422                    }
423                    _ => ChildStats::Skip,
424                })
425                .collect()
426        } else {
427            vec![ChildStats::At(None); self.inputs.len()]
428        }
429    }
430
431    fn statistics_from_inputs(
432        &self,
433        input_stats: &[Arc<Statistics>],
434        args: &StatisticsArgs,
435    ) -> Result<Arc<Statistics>> {
436        if let Some(partition_idx) = args.partition() {
437            // For a specific partition, find which input it belongs to
438            if let Some((target_i, _)) = self.owning_input(partition_idx) {
439                // This partition belongs to this input - return its stats
440                return Ok(Arc::clone(&input_stats[target_i]));
441            }
442            // If we get here, the partition index is out of bounds
443            Ok(Arc::new(Statistics::new_unknown(&self.schema())))
444        } else {
445            let stats_refs = input_stats.iter().map(|s| s.as_ref()).collect::<Vec<_>>();
446
447            Ok(Arc::new(Statistics::try_merge_iter_with_ndv_fallback(
448                stats_refs,
449                self.schema().as_ref(),
450                NdvFallback::Sum,
451            )?))
452        }
453    }
454
455    fn cardinality_effect(&self) -> CardinalityEffect {
456        // Union combines rows from multiple inputs, so output rows are not tied
457        // to any single input and can only be constrained as greater-or-equal.
458        CardinalityEffect::GreaterEqual
459    }
460
461    fn supports_limit_pushdown(&self) -> bool {
462        true
463    }
464
465    /// Tries to push `projection` down through `union`. If possible, performs the
466    /// pushdown and returns a new [`UnionExec`] as the top plan which has projections
467    /// as its children. Otherwise, returns `None`.
468    fn try_swapping_with_projection(
469        &self,
470        projection: &ProjectionExec,
471    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
472        // If the projection doesn't narrow the schema, we shouldn't try to push it down.
473        if projection.expr().len() >= projection.input().schema().fields().len() {
474            return Ok(None);
475        }
476
477        let new_children = self
478            .children()
479            .into_iter()
480            .map(|child| make_with_child(projection, child))
481            .collect::<Result<Vec<_>>>()?;
482
483        Ok(Some(UnionExec::try_new(new_children.clone())?))
484    }
485
486    fn gather_filters_for_pushdown(
487        &self,
488        _phase: FilterPushdownPhase,
489        parent_filters: Vec<Arc<dyn PhysicalExpr>>,
490        _config: &ConfigOptions,
491    ) -> Result<FilterDescription> {
492        FilterDescription::from_children(parent_filters, &self.children())
493    }
494
495    fn handle_child_pushdown_result(
496        &self,
497        phase: FilterPushdownPhase,
498        child_pushdown_result: ChildPushdownResult,
499        _config: &ConfigOptions,
500    ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
501        // Pre phase: handle heterogeneous pushdown by wrapping individual
502        // children with FilterExec and reporting all filters as handled.
503        // Post phase: use default behavior to let the filter creator decide how to handle
504        // filters that weren't fully pushed down.
505        if phase != FilterPushdownPhase::Pre {
506            return Ok(FilterPushdownPropagation::if_all(child_pushdown_result));
507        }
508
509        // UnionExec needs specialized filter pushdown handling when children have
510        // heterogeneous pushdown support. Without this, when some children support
511        // pushdown and others don't, the default behavior would leave FilterExec
512        // above UnionExec, re-applying filters to outputs of all children—including
513        // those that already applied the filters via pushdown. This specialized
514        // implementation adds FilterExec only to children that don't support
515        // pushdown, avoiding redundant filtering and improving performance.
516        //
517        // Example: Given Child1 (no pushdown support) and Child2 (has pushdown support)
518        //   Default behavior:          This implementation:
519        //   FilterExec                 UnionExec
520        //     UnionExec                  FilterExec
521        //       Child1                     Child1
522        //       Child2(filter)           Child2(filter)
523
524        // Collect unsupported filters for each child
525        let mut unsupported_filters_per_child = vec![Vec::new(); self.inputs.len()];
526        for parent_filter_result in child_pushdown_result.parent_filters.iter() {
527            for (child_idx, &child_result) in
528                parent_filter_result.child_results.iter().enumerate()
529            {
530                if matches!(child_result, PushedDown::No) {
531                    unsupported_filters_per_child[child_idx]
532                        .push(Arc::clone(&parent_filter_result.filter));
533                }
534            }
535        }
536
537        // Wrap children that have unsupported filters with FilterExec
538        let mut new_children = self.inputs.clone();
539        for (child_idx, unsupported_filters) in
540            unsupported_filters_per_child.iter().enumerate()
541        {
542            if !unsupported_filters.is_empty() {
543                let combined_filter = conjunction(unsupported_filters.clone());
544                new_children[child_idx] = Arc::new(FilterExec::try_new(
545                    combined_filter,
546                    Arc::clone(&self.inputs[child_idx]),
547                )?);
548            }
549        }
550
551        // Check if any children were modified
552        let children_modified = new_children
553            .iter()
554            .zip(self.inputs.iter())
555            .any(|(new, old)| !Arc::ptr_eq(new, old));
556
557        let all_filters_pushed =
558            vec![PushedDown::Yes; child_pushdown_result.parent_filters.len()];
559        let propagation = if children_modified {
560            let updated_node = UnionExec::try_new(new_children)?;
561            FilterPushdownPropagation::with_parent_pushdown_result(all_filters_pushed)
562                .with_updated_node(updated_node)
563        } else {
564            FilterPushdownPropagation::with_parent_pushdown_result(all_filters_pushed)
565        };
566
567        // Report all parent filters as supported since we've ensured they're applied
568        // on all children (either pushed down or via FilterExec)
569        Ok(propagation)
570    }
571    #[cfg(feature = "proto")]
572    fn try_to_proto(
573        &self,
574        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
575    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
576        use datafusion_proto_models::protobuf;
577        let inputs = ctx.encode_children(self.inputs())?;
578        Ok(Some(protobuf::PhysicalPlanNode {
579            physical_plan_type: Some(
580                protobuf::physical_plan_node::PhysicalPlanType::Union(
581                    protobuf::UnionExecNode { inputs },
582                ),
583            ),
584        }))
585    }
586}
587
588#[cfg(feature = "proto")]
589impl UnionExec {
590    pub fn try_from_proto(
591        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
592        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
593    ) -> Result<Arc<dyn ExecutionPlan>> {
594        use datafusion_proto_models::protobuf;
595        let union = crate::expect_plan_variant!(
596            node,
597            protobuf::physical_plan_node::PhysicalPlanType::Union,
598            "UnionExec",
599        );
600        let inputs = union
601            .inputs
602            .iter()
603            .map(|input| ctx.decode_child(input))
604            .collect::<Result<Vec<_>>>()?;
605        UnionExec::try_new(inputs)
606    }
607}
608
609/// Combines multiple input streams by interleaving them.
610///
611/// All inputs must share an identical [`Partitioning::Hash`] or [`Partitioning::Range`] so that
612/// partition `k` covers the same data across every input. Each output partition is the
613/// interleaving of the same-indexed partition from all inputs:
614/// `output[k] = input[0][k] + input[1][k] + ... + input[n-1][k]`
615///
616/// # Data Flow
617/// ```text
618/// +---------+
619/// |         |---+
620/// | Input 1 |   |
621/// |         |-------------+
622/// +---------+   |         |
623///               |         |         +---------+
624///               +------------------>|         |
625///                 +---------------->| Combine |-->
626///                 | +-------------->|         |
627///                 | |     |         +---------+
628/// +---------+     | |     |
629/// |         |-----+ |     |
630/// | Input 2 |       |     |
631/// |         |---------------+
632/// +---------+       |     | |
633///                   |     | |       +---------+
634///                   |     +-------->|         |
635///                   |       +------>| Combine |-->
636///                   |         +---->|         |
637///                   |         |     +---------+
638/// +---------+       |         |
639/// |         |-------+         |
640/// | Input 3 |                 |
641/// |         |-----------------+
642/// +---------+
643/// ```
644#[derive(Debug, Clone)]
645pub struct InterleaveExec {
646    /// Input execution plan
647    inputs: Vec<Arc<dyn ExecutionPlan>>,
648    /// Execution metrics
649    metrics: ExecutionPlanMetricsSet,
650    /// Cache holding plan properties like equivalences, output partitioning etc.
651    cache: Arc<PlanProperties>,
652}
653
654impl InterleaveExec {
655    /// Create a new InterleaveExec
656    pub fn try_new(inputs: Vec<Arc<dyn ExecutionPlan>>) -> Result<Self> {
657        assert_or_internal_err!(
658            can_interleave(inputs.iter()),
659            "Not all InterleaveExec children have a consistent hash or range partitioning"
660        );
661        let schema = union_schema(&inputs)?;
662        let inputs = inputs
663            .into_iter()
664            .map(|input| coerce_schema(input, &schema))
665            .collect::<Result<Vec<_>>>()?;
666        let cache = Self::compute_properties(&inputs, schema)?;
667        Ok(InterleaveExec {
668            inputs,
669            metrics: ExecutionPlanMetricsSet::new(),
670            cache: Arc::new(cache),
671        })
672    }
673
674    /// Get inputs of the execution plan
675    pub fn inputs(&self) -> &Vec<Arc<dyn ExecutionPlan>> {
676        &self.inputs
677    }
678
679    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
680    fn compute_properties(
681        inputs: &[Arc<dyn ExecutionPlan>],
682        schema: SchemaRef,
683    ) -> Result<PlanProperties> {
684        let eq_properties = EquivalenceProperties::new(schema);
685        // Get output partitioning:
686        let output_partitioning = inputs[0].output_partitioning().clone();
687        Ok(PlanProperties::new(
688            eq_properties,
689            output_partitioning,
690            emission_type_from_children(inputs),
691            boundedness_from_children(inputs),
692        ))
693    }
694}
695
696impl DisplayAs for InterleaveExec {
697    fn fmt_as(
698        &self,
699        t: DisplayFormatType,
700        f: &mut std::fmt::Formatter,
701    ) -> std::fmt::Result {
702        match t {
703            DisplayFormatType::Default | DisplayFormatType::Verbose => {
704                write!(f, "InterleaveExec")
705            }
706            DisplayFormatType::TreeRender => Ok(()),
707        }
708    }
709}
710
711impl ExecutionPlan for InterleaveExec {
712    fn name(&self) -> &'static str {
713        "InterleaveExec"
714    }
715
716    /// Return a reference to Any that can be used for downcasting
717    fn properties(&self) -> &Arc<PlanProperties> {
718        &self.cache
719    }
720
721    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
722        self.inputs.iter().collect()
723    }
724
725    fn maintains_input_order(&self) -> Vec<bool> {
726        vec![false; self.inputs().len()]
727    }
728
729    fn apply_expressions(
730        &self,
731        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
732    ) -> Result<TreeNodeRecursion> {
733        Ok(TreeNodeRecursion::Continue)
734    }
735
736    fn replace_children(
737        self: Arc<Self>,
738        children: Vec<Arc<dyn ExecutionPlan>>,
739        options: ReplaceChildrenOptions,
740    ) -> Result<Arc<dyn ExecutionPlan>> {
741        validate_child_count!(self, children);
742        match options.children_properties {
743            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
744                inputs: children,
745                metrics: ExecutionPlanMetricsSet::new(),
746                ..Self::clone(&*self)
747            })),
748            ChildrenPropertiesMode::Recompute => {
749                // New children are no longer interleavable, which might be a bug of optimization rewrite.
750                assert_or_internal_err!(
751                    can_interleave(children.iter()),
752                    "Can not create InterleaveExec: new children can not be interleaved"
753                );
754                Ok(Arc::new(InterleaveExec::try_new(children)?))
755            }
756        }
757    }
758
759    fn with_new_children(
760        self: Arc<Self>,
761        children: Vec<Arc<dyn ExecutionPlan>>,
762    ) -> Result<Arc<dyn ExecutionPlan>> {
763        self.replace_children(
764            children,
765            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
766        )
767    }
768
769    fn with_new_children_and_same_properties(
770        self: Arc<Self>,
771        children: Vec<Arc<dyn ExecutionPlan>>,
772    ) -> Result<Arc<dyn ExecutionPlan>> {
773        self.replace_children(
774            children,
775            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
776        )
777    }
778
779    fn execute(
780        &self,
781        partition: usize,
782        context: Arc<TaskContext>,
783    ) -> Result<SendableRecordBatchStream> {
784        trace!(
785            "Start InterleaveExec::execute for partition {} of context session_id {} and task_id {:?}",
786            partition,
787            context.session_id(),
788            context.task_id()
789        );
790        let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
791        // record the tiny amount of work done in this function so
792        // elapsed_compute is reported as non zero
793        let elapsed_compute = baseline_metrics.elapsed_compute().clone();
794        let _timer = elapsed_compute.timer(); // record on drop
795
796        let mut input_stream_vec = vec![];
797        for input in self.inputs.iter() {
798            if partition < input.output_partitioning().partition_count() {
799                let stream = input.execute(partition, Arc::clone(&context))?;
800                input_stream_vec.push(stream);
801            } else {
802                // Do not find a partition to execute
803                break;
804            }
805        }
806        if input_stream_vec.len() == self.inputs.len() {
807            let stream = Box::pin(CombinedRecordBatchStream::new(
808                self.schema(),
809                input_stream_vec,
810            ));
811            return Ok(Box::pin(ObservedStream::new(
812                stream,
813                baseline_metrics,
814                None,
815            )));
816        }
817
818        warn!("Error in InterleaveExec: Partition {partition} not found");
819
820        exec_err!("Partition {partition} not found in InterleaveExec")
821    }
822
823    fn metrics(&self) -> Option<MetricsSet> {
824        Some(self.metrics.clone_inner())
825    }
826
827    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
828        vec![ChildStats::At(partition); self.inputs.len()]
829    }
830
831    fn statistics_from_inputs(
832        &self,
833        input_stats: &[Arc<Statistics>],
834        _args: &StatisticsArgs,
835    ) -> Result<Arc<Statistics>> {
836        let stats = input_stats
837            .iter()
838            .map(|s| s.as_ref().clone())
839            .collect::<Vec<_>>();
840
841        Ok(Arc::new(Statistics::try_merge_iter_with_ndv_fallback(
842            stats.iter(),
843            self.schema().as_ref(),
844            NdvFallback::Sum,
845        )?))
846    }
847
848    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
849        vec![false; self.children().len()]
850    }
851    #[cfg(feature = "proto")]
852    fn try_to_proto(
853        &self,
854        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
855    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
856        use datafusion_proto_models::protobuf;
857        let inputs = ctx.encode_children(self.inputs())?;
858        Ok(Some(protobuf::PhysicalPlanNode {
859            physical_plan_type: Some(
860                protobuf::physical_plan_node::PhysicalPlanType::Interleave(
861                    protobuf::InterleaveExecNode { inputs },
862                ),
863            ),
864        }))
865    }
866}
867
868#[cfg(feature = "proto")]
869impl InterleaveExec {
870    pub fn try_from_proto(
871        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
872        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
873    ) -> Result<Arc<dyn ExecutionPlan>> {
874        use datafusion_proto_models::protobuf;
875        let interleave = crate::expect_plan_variant!(
876            node,
877            protobuf::physical_plan_node::PhysicalPlanType::Interleave,
878            "InterleaveExec",
879        );
880        let inputs = interleave
881            .inputs
882            .iter()
883            .map(|input| ctx.decode_child(input))
884            .collect::<Result<Vec<_>>>()?;
885        Ok(Arc::new(InterleaveExec::try_new(inputs)?))
886    }
887}
888
889/// Returns true if all inputs have the same [`Partitioning::Hash`] or [`Partitioning::Range`]
890/// spec, making them safe to interleave. Two inputs are interleave-compatible when partition
891/// `k` covers the identical key range or hash bucket across every input.
892///
893/// Note: compatibility is checked sequentially against the first input, so
894/// `InputDistributionRequirements::co_partitioned` is not needed here.
895///
896/// It might be too strict here in the case that the input partition specs are compatible but not exactly the same.
897/// For example one input partition has the partition spec Hash('a','b','c') and
898/// other has the partition spec Hash('a'), It is safe to derive the out partition with the spec Hash('a','b','c').
899pub fn can_interleave<T: Borrow<Arc<dyn ExecutionPlan>>>(
900    mut inputs: impl Iterator<Item = T>,
901) -> bool {
902    let Some(first) = inputs.next() else {
903        return false;
904    };
905
906    let reference = first.borrow().output_partitioning();
907    matches!(reference, Partitioning::Hash(_, _) | Partitioning::Range(_))
908        && inputs
909            .map(|plan| plan.borrow().output_partitioning().clone())
910            .all(|partition| partition == *reference)
911}
912
913fn union_schema(inputs: &[Arc<dyn ExecutionPlan>]) -> Result<SchemaRef> {
914    if inputs.is_empty() {
915        return exec_err!("Cannot create union schema from empty inputs");
916    }
917
918    let first_schema = inputs[0].schema();
919    let first_field_count = first_schema.fields().len();
920
921    // validate that all inputs have the same number of fields
922    for (idx, input) in inputs.iter().enumerate().skip(1) {
923        let field_count = input.schema().fields().len();
924        if field_count != first_field_count {
925            return exec_err!(
926                "UnionExec/InterleaveExec requires all inputs to have the same number of fields. \
927                 Input 0 has {first_field_count} fields, but input {idx} has {field_count} fields"
928            );
929        }
930    }
931
932    let fields = (0..first_field_count)
933        .map(|i| {
934            // We take the name from the left side of the union to match how names are coerced during logical planning,
935            // which also uses the left side names.
936            let base_field = first_schema.field(i).clone();
937
938            // Coerce metadata and nullability across all inputs
939
940            inputs
941                .iter()
942                .enumerate()
943                .map(|(input_idx, input)| {
944                    let field = input.schema().field(i).clone();
945                    let mut metadata = field.metadata().clone();
946
947                    let other_metadatas = inputs
948                        .iter()
949                        .enumerate()
950                        .filter(|(other_idx, _)| *other_idx != input_idx)
951                        .flat_map(|(_, other_input)| {
952                            other_input.schema().field(i).metadata().clone().into_iter()
953                        });
954
955                    metadata.extend(other_metadatas);
956                    field.with_metadata(metadata)
957                })
958                .find_or_first(Field::is_nullable)
959                // We can unwrap this because if inputs was empty, this would've already panic'ed when we
960                // indexed into inputs[0].
961                .unwrap()
962                .with_name(base_field.name())
963        })
964        .collect::<Vec<_>>();
965
966    let all_metadata_merged = inputs
967        .iter()
968        .flat_map(|i| i.schema().metadata().clone().into_iter())
969        .collect();
970
971    Ok(Arc::new(Schema::new_with_metadata(
972        fields,
973        all_metadata_merged,
974    )))
975}
976
977/// CombinedRecordBatchStream can be used to combine a Vec of SendableRecordBatchStreams into one
978struct CombinedRecordBatchStream {
979    /// Schema wrapped by Arc
980    schema: SchemaRef,
981    /// Stream entries
982    entries: Vec<SendableRecordBatchStream>,
983}
984
985impl CombinedRecordBatchStream {
986    /// Create an CombinedRecordBatchStream
987    pub fn new(schema: SchemaRef, entries: Vec<SendableRecordBatchStream>) -> Self {
988        Self { schema, entries }
989    }
990}
991
992impl RecordBatchStream for CombinedRecordBatchStream {
993    fn schema(&self) -> SchemaRef {
994        Arc::clone(&self.schema)
995    }
996}
997
998impl Stream for CombinedRecordBatchStream {
999    type Item = Result<RecordBatch>;
1000
1001    fn poll_next(
1002        mut self: Pin<&mut Self>,
1003        cx: &mut Context<'_>,
1004    ) -> Poll<Option<Self::Item>> {
1005        use Poll::*;
1006
1007        let start = thread_rng_n(self.entries.len() as u32) as usize;
1008        let mut idx = start;
1009
1010        for _ in 0..self.entries.len() {
1011            let stream = self.entries.get_mut(idx).unwrap();
1012
1013            match Pin::new(stream).poll_next(cx) {
1014                Ready(Some(val)) => return Ready(Some(val)),
1015                Ready(None) => {
1016                    // Remove the entry
1017                    self.entries.swap_remove(idx);
1018
1019                    // Check if this was the last entry, if so the cursor needs
1020                    // to wrap
1021                    if idx == self.entries.len() {
1022                        idx = 0;
1023                    } else if idx < start && start <= self.entries.len() {
1024                        // The stream being swapped into the current index has
1025                        // already been polled, so skip it.
1026                        idx = idx.wrapping_add(1) % self.entries.len();
1027                    }
1028                }
1029                Pending => {
1030                    idx = idx.wrapping_add(1) % self.entries.len();
1031                }
1032            }
1033        }
1034
1035        // If the map is empty, then the stream is complete.
1036        if self.entries.is_empty() {
1037            Ready(None)
1038        } else {
1039            Pending
1040        }
1041    }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use super::*;
1047    use crate::collect;
1048    use crate::repartition::RepartitionExec;
1049    use crate::statistics::{StatisticsArgs, StatisticsContext};
1050    use crate::test::exec::StatisticsExec;
1051    use crate::test::{self, TestMemoryExec};
1052
1053    use arrow::compute::SortOptions;
1054    use arrow::datatypes::DataType;
1055    use datafusion_common::SplitPoint;
1056    use datafusion_common::stats::Precision;
1057    use datafusion_common::{ColumnStatistics, ScalarValue};
1058    use datafusion_physical_expr::RangePartitioning;
1059    use datafusion_physical_expr::equivalence::convert_to_orderings;
1060    use datafusion_physical_expr::expressions::col;
1061    use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
1062
1063    // Generate a schema which consists of 7 columns (a, b, c, d, e, f, g)
1064    fn create_test_schema() -> Result<SchemaRef> {
1065        let a = Field::new("a", DataType::Int32, true);
1066        let b = Field::new("b", DataType::Int32, true);
1067        let c = Field::new("c", DataType::Int32, true);
1068        let d = Field::new("d", DataType::Int32, true);
1069        let e = Field::new("e", DataType::Int32, true);
1070        let f = Field::new("f", DataType::Int32, true);
1071        let g = Field::new("g", DataType::Int32, true);
1072        let schema = Arc::new(Schema::new(vec![a, b, c, d, e, f, g]));
1073
1074        Ok(schema)
1075    }
1076
1077    fn create_test_schema2() -> Result<SchemaRef> {
1078        let a = Field::new("a", DataType::Int32, true);
1079        let b = Field::new("b", DataType::Int32, true);
1080        let c = Field::new("c", DataType::Int32, true);
1081        let d = Field::new("d", DataType::Int32, true);
1082        let e = Field::new("e", DataType::Int32, true);
1083        let f = Field::new("f", DataType::Int32, true);
1084        let schema = Arc::new(Schema::new(vec![a, b, c, d, e, f]));
1085
1086        Ok(schema)
1087    }
1088
1089    #[tokio::test]
1090    async fn test_union_partitions() -> Result<()> {
1091        let task_ctx = Arc::new(TaskContext::default());
1092
1093        // Create inputs with different partitioning
1094        let csv = test::scan_partitioned(4);
1095        let csv2 = test::scan_partitioned(5);
1096
1097        let union_exec: Arc<dyn ExecutionPlan> = UnionExec::try_new(vec![csv, csv2])?;
1098
1099        // Should have 9 partitions and 9 output batches
1100        assert_eq!(
1101            union_exec
1102                .properties()
1103                .output_partitioning()
1104                .partition_count(),
1105            9
1106        );
1107
1108        let result: Vec<RecordBatch> = collect(union_exec, task_ctx).await?;
1109        assert_eq!(result.len(), 9);
1110
1111        Ok(())
1112    }
1113
1114    #[tokio::test]
1115    async fn test_interleave_conforms_batch_schema() -> Result<()> {
1116        // Two inputs agree on the column's type but disagree on nullability;
1117        // InterleaveExec's declared schema ORs nullability across inputs, so
1118        // every yielded batch must be re-stamped with that schema. See
1119        // <https://github.com/apache/datafusion/issues/15394>.
1120        let task_ctx = Arc::new(TaskContext::default());
1121
1122        let schema_not_null =
1123            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1124        let batch_not_null = RecordBatch::try_new(
1125            Arc::clone(&schema_not_null),
1126            vec![Arc::new(arrow::array::Int32Array::from(vec![1, 2]))],
1127        )?;
1128
1129        let schema_nullable =
1130            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1131        let batch_nullable = RecordBatch::try_new(
1132            Arc::clone(&schema_nullable),
1133            vec![Arc::new(arrow::array::Int32Array::from(vec![3, 4]))],
1134        )?;
1135
1136        let hash_expr = vec![col("a", schema_not_null.as_ref())?];
1137        let left: Arc<dyn ExecutionPlan> = Arc::new(RepartitionExec::try_new(
1138            TestMemoryExec::try_new_exec(&[vec![batch_not_null]], schema_not_null, None)?,
1139            Partitioning::Hash(hash_expr.clone(), 1),
1140        )?);
1141        let right: Arc<dyn ExecutionPlan> = Arc::new(RepartitionExec::try_new(
1142            TestMemoryExec::try_new_exec(&[vec![batch_nullable]], schema_nullable, None)?,
1143            Partitioning::Hash(hash_expr, 1),
1144        )?);
1145
1146        let interleave: Arc<dyn ExecutionPlan> =
1147            Arc::new(InterleaveExec::try_new(vec![left, right])?);
1148        let interleave_schema = interleave.schema();
1149        assert!(interleave_schema.field(0).is_nullable());
1150
1151        let batches = collect(interleave, task_ctx).await?;
1152        assert!(!batches.is_empty());
1153        for batch in &batches {
1154            assert_eq!(batch.schema(), interleave_schema);
1155        }
1156
1157        Ok(())
1158    }
1159
1160    fn stats_merge_inputs() -> (SchemaRef, Statistics, Statistics, Statistics) {
1161        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::UInt32, true)]));
1162
1163        let left = Statistics::default()
1164            .with_num_rows(Precision::Exact(5))
1165            .with_total_byte_size(Precision::Exact(23))
1166            .add_column_statistics(
1167                ColumnStatistics::new_unknown()
1168                    .with_distinct_count(Precision::Exact(5))
1169                    .with_min_value(Precision::Exact(ScalarValue::UInt32(Some(1))))
1170                    .with_max_value(Precision::Exact(ScalarValue::UInt32(Some(21))))
1171                    .with_sum_value(Precision::Exact(ScalarValue::UInt32(Some(42))))
1172                    .with_null_count(Precision::Exact(0))
1173                    .with_byte_size(Precision::Exact(40)),
1174            );
1175
1176        let right = Statistics::default()
1177            .with_num_rows(Precision::Exact(7))
1178            .with_total_byte_size(Precision::Exact(29))
1179            .add_column_statistics(
1180                ColumnStatistics::new_unknown()
1181                    .with_distinct_count(Precision::Exact(3))
1182                    .with_min_value(Precision::Exact(ScalarValue::UInt32(Some(22))))
1183                    .with_max_value(Precision::Exact(ScalarValue::UInt32(Some(34))))
1184                    .with_sum_value(Precision::Exact(ScalarValue::UInt32(Some(8))))
1185                    .with_null_count(Precision::Exact(1))
1186                    .with_byte_size(Precision::Exact(60)),
1187            );
1188
1189        let expected = Statistics::default()
1190            .with_num_rows(Precision::Exact(12))
1191            .with_total_byte_size(Precision::Exact(52))
1192            .add_column_statistics(
1193                ColumnStatistics::new_unknown()
1194                    .with_distinct_count(Precision::Inexact(8))
1195                    .with_min_value(Precision::Exact(ScalarValue::UInt32(Some(1))))
1196                    .with_max_value(Precision::Exact(ScalarValue::UInt32(Some(34))))
1197                    .with_sum_value(Precision::Exact(ScalarValue::UInt64(Some(50))))
1198                    .with_null_count(Precision::Exact(1))
1199                    .with_byte_size(Precision::Exact(100)),
1200            );
1201
1202        (schema, left, right, expected)
1203    }
1204
1205    fn stats_merge_multicolumn_inputs() -> (SchemaRef, Statistics, Statistics, Statistics)
1206    {
1207        let schema = Arc::new(Schema::new(vec![
1208            Field::new("a", DataType::Int64, true),
1209            Field::new("b", DataType::Utf8, true),
1210            Field::new("c", DataType::Float32, true),
1211        ]));
1212
1213        let left = Statistics::default()
1214            .with_num_rows(Precision::Exact(5))
1215            .with_total_byte_size(Precision::Exact(23))
1216            .add_column_statistics(
1217                ColumnStatistics::new_unknown()
1218                    .with_distinct_count(Precision::Exact(5))
1219                    .with_min_value(Precision::Exact(ScalarValue::Int64(Some(-4))))
1220                    .with_max_value(Precision::Exact(ScalarValue::Int64(Some(21))))
1221                    .with_sum_value(Precision::Exact(ScalarValue::Int64(Some(42))))
1222                    .with_null_count(Precision::Exact(0)),
1223            )
1224            .add_column_statistics(
1225                ColumnStatistics::new_unknown()
1226                    .with_distinct_count(Precision::Exact(2))
1227                    .with_min_value(Precision::Exact(ScalarValue::from("a")))
1228                    .with_max_value(Precision::Exact(ScalarValue::from("x")))
1229                    .with_null_count(Precision::Exact(3)),
1230            )
1231            .add_column_statistics(
1232                ColumnStatistics::new_unknown()
1233                    .with_max_value(Precision::Exact(ScalarValue::Float32(Some(1.1))))
1234                    .with_min_value(Precision::Exact(ScalarValue::Float32(Some(0.1))))
1235                    .with_sum_value(Precision::Exact(ScalarValue::Float32(Some(42.0)))),
1236            );
1237
1238        let right = Statistics::default()
1239            .with_num_rows(Precision::Exact(7))
1240            .with_total_byte_size(Precision::Exact(29))
1241            .add_column_statistics(
1242                ColumnStatistics::new_unknown()
1243                    .with_distinct_count(Precision::Exact(3))
1244                    .with_min_value(Precision::Exact(ScalarValue::Int64(Some(1))))
1245                    .with_max_value(Precision::Exact(ScalarValue::Int64(Some(34))))
1246                    .with_sum_value(Precision::Exact(ScalarValue::Int64(Some(42))))
1247                    .with_null_count(Precision::Exact(1)),
1248            )
1249            .add_column_statistics(
1250                ColumnStatistics::new_unknown()
1251                    .with_distinct_count(Precision::Exact(3))
1252                    .with_min_value(Precision::Exact(ScalarValue::from("b")))
1253                    .with_max_value(Precision::Exact(ScalarValue::from("z"))),
1254            )
1255            .add_column_statistics(ColumnStatistics::new_unknown());
1256
1257        let expected = Statistics::default()
1258            .with_num_rows(Precision::Exact(12))
1259            .with_total_byte_size(Precision::Exact(52))
1260            .add_column_statistics(
1261                ColumnStatistics::new_unknown()
1262                    .with_distinct_count(Precision::Inexact(6))
1263                    .with_min_value(Precision::Exact(ScalarValue::Int64(Some(-4))))
1264                    .with_max_value(Precision::Exact(ScalarValue::Int64(Some(34))))
1265                    .with_sum_value(Precision::Exact(ScalarValue::Int64(Some(84))))
1266                    .with_null_count(Precision::Exact(1)),
1267            )
1268            .add_column_statistics(
1269                ColumnStatistics::new_unknown()
1270                    .with_distinct_count(Precision::Inexact(5))
1271                    .with_min_value(Precision::Exact(ScalarValue::from("a")))
1272                    .with_max_value(Precision::Exact(ScalarValue::from("z"))),
1273            )
1274            .add_column_statistics(ColumnStatistics::new_unknown());
1275
1276        (schema, left, right, expected)
1277    }
1278
1279    #[test]
1280    fn test_union_partition_statistics_uses_shared_statistics_merge() -> Result<()> {
1281        let (schema, left, right, expected) = stats_merge_inputs();
1282
1283        let left: Arc<dyn ExecutionPlan> =
1284            Arc::new(StatisticsExec::new(left, schema.as_ref().clone()));
1285        let right: Arc<dyn ExecutionPlan> =
1286            Arc::new(StatisticsExec::new(right, schema.as_ref().clone()));
1287
1288        let union = UnionExec::try_new(vec![left, right])?;
1289        let stats =
1290            StatisticsContext::new().compute(union.as_ref(), &StatisticsArgs::new())?;
1291
1292        assert_eq!(stats.as_ref(), &expected);
1293        Ok(())
1294    }
1295
1296    #[test]
1297    fn test_union_partition_statistics_uses_shared_statistics_merge_multicolumn()
1298    -> Result<()> {
1299        let (schema, left, right, expected) = stats_merge_multicolumn_inputs();
1300
1301        let left: Arc<dyn ExecutionPlan> =
1302            Arc::new(StatisticsExec::new(left, schema.as_ref().clone()));
1303        let right: Arc<dyn ExecutionPlan> =
1304            Arc::new(StatisticsExec::new(right, schema.as_ref().clone()));
1305
1306        let union = UnionExec::try_new(vec![left, right])?;
1307        let stats =
1308            StatisticsContext::new().compute(union.as_ref(), &StatisticsArgs::new())?;
1309
1310        assert_eq!(stats.as_ref(), &expected);
1311        Ok(())
1312    }
1313
1314    #[test]
1315    fn test_union_partition_statistics_with_mismatched_nullability() -> Result<()> {
1316        // Regression test for the `ProjectionExec` wrapper `UnionExec::try_new`
1317        // inserts above the non-nullable leg here (via `coerce_schema`):
1318        // exact column statistics (min/max/null/distinct/sum/byte_size) must
1319        // still make it through the wrapper's same-type `CastExpr`, not get
1320        // poisoned into `Absent` the way a generic (type-changing) cast's
1321        // statistics would be.
1322        let (_, left, right, expected) = stats_merge_inputs();
1323
1324        // `total_byte_size` differs from the plain-merge fixture (52): the
1325        // wrapper is a `ProjectionExec`, whose `statistics_from_inputs`
1326        // recomputes `total_byte_size` from the (unchanged) schema's row
1327        // width times row count, rather than trusting the wrapped leg's own
1328        // self-reported total -- still `Exact`, just derived differently.
1329        // left: 5 rows * 4 bytes (UInt32) = 20 (was 23); right is untouched
1330        // (already nullable, so `coerce_schema` doesn't wrap it): 20 + 29 = 49.
1331        let expected = expected.with_total_byte_size(Precision::Exact(49));
1332
1333        let non_nullable_schema =
1334            Schema::new(vec![Field::new("a", DataType::UInt32, false)]);
1335        let nullable_schema = Schema::new(vec![Field::new("a", DataType::UInt32, true)]);
1336
1337        let left: Arc<dyn ExecutionPlan> =
1338            Arc::new(StatisticsExec::new(left, non_nullable_schema));
1339        let right: Arc<dyn ExecutionPlan> =
1340            Arc::new(StatisticsExec::new(right, nullable_schema));
1341
1342        let union = UnionExec::try_new(vec![left, right])?;
1343        let stats =
1344            StatisticsContext::new().compute(union.as_ref(), &StatisticsArgs::new())?;
1345
1346        assert_eq!(stats.as_ref(), &expected);
1347        Ok(())
1348    }
1349
1350    #[tokio::test]
1351    async fn test_coerce_schema_no_op_when_already_matching() -> Result<()> {
1352        let schema_not_null =
1353            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1354        let input: Arc<dyn ExecutionPlan> =
1355            TestMemoryExec::try_new_exec(&[vec![]], Arc::clone(&schema_not_null), None)?;
1356
1357        let coerced = coerce_schema(Arc::clone(&input), &schema_not_null)?;
1358        assert!(Arc::ptr_eq(&coerced, &input));
1359
1360        Ok(())
1361    }
1362
1363    #[tokio::test]
1364    async fn test_coerce_schema_casts_only_nullability() -> Result<()> {
1365        // Mismatched nullability: the input gets wrapped in a `ProjectionExec`
1366        // whose `CastExpr` re-stamps the column with the target's `Field`
1367        // (same `DataType`, so this is a zero-copy relabeling, not a real cast).
1368        let schema_not_null =
1369            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1370        let batch_not_null = RecordBatch::try_new(
1371            Arc::clone(&schema_not_null),
1372            vec![Arc::new(arrow::array::Int32Array::from(vec![1, 2]))],
1373        )?;
1374        let input: Arc<dyn ExecutionPlan> = TestMemoryExec::try_new_exec(
1375            &[vec![batch_not_null]],
1376            Arc::clone(&schema_not_null),
1377            None,
1378        )?;
1379
1380        let nullable_schema =
1381            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1382        let coerced = coerce_schema(Arc::clone(&input), &nullable_schema)?;
1383        assert_eq!(&coerced.schema(), &nullable_schema);
1384        let plan_str = crate::displayable(coerced.as_ref())
1385            .indent(true)
1386            .to_string();
1387        assert!(
1388            plan_str.contains("CAST"),
1389            "expected a CAST in the coerced plan:\n{plan_str}"
1390        );
1391
1392        let task_ctx = Arc::new(TaskContext::default());
1393        let batches = collect(coerced, task_ctx).await?;
1394        assert_eq!(batches.len(), 1);
1395        assert_eq!(batches[0].schema(), nullable_schema);
1396
1397        Ok(())
1398    }
1399
1400    #[test]
1401    fn test_coerce_schema_rejects_genuine_type_mismatch() -> Result<()> {
1402        let schema_int =
1403            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1404        let input: Arc<dyn ExecutionPlan> =
1405            TestMemoryExec::try_new_exec(&[vec![]], Arc::clone(&schema_int), None)?;
1406
1407        let schema_utf8 =
1408            Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, false)]));
1409        let err = coerce_schema(input, &schema_utf8).unwrap_err();
1410        assert!(err.to_string().contains("same data type per column"));
1411
1412        Ok(())
1413    }
1414
1415    #[test]
1416    fn test_interleave_partition_statistics_uses_shared_statistics_merge() -> Result<()> {
1417        let (schema, left, right, expected) = stats_merge_inputs();
1418        let hash_expr = vec![col("a", schema.as_ref())?];
1419
1420        let left: Arc<dyn ExecutionPlan> = Arc::new(RepartitionExec::try_new(
1421            Arc::new(StatisticsExec::new(left, schema.as_ref().clone())),
1422            Partitioning::Hash(hash_expr.clone(), 2),
1423        )?);
1424        let right: Arc<dyn ExecutionPlan> = Arc::new(RepartitionExec::try_new(
1425            Arc::new(StatisticsExec::new(right, schema.as_ref().clone())),
1426            Partitioning::Hash(hash_expr, 2),
1427        )?);
1428
1429        let interleave = InterleaveExec::try_new(vec![left, right])?;
1430        let stats =
1431            StatisticsContext::new().compute(&interleave, &StatisticsArgs::new())?;
1432
1433        assert_eq!(stats.as_ref(), &expected);
1434        Ok(())
1435    }
1436
1437    #[test]
1438    fn test_interleave_partition_statistics_for_partition_uses_shared_statistics_merge()
1439    -> Result<()> {
1440        let (schema, left, right, _) = stats_merge_inputs();
1441        let hash_expr = vec![col("a", schema.as_ref())?];
1442
1443        let left: Arc<dyn ExecutionPlan> = Arc::new(RepartitionExec::try_new(
1444            Arc::new(StatisticsExec::new(left, schema.as_ref().clone())),
1445            Partitioning::Hash(hash_expr.clone(), 2),
1446        )?);
1447        let right: Arc<dyn ExecutionPlan> = Arc::new(RepartitionExec::try_new(
1448            Arc::new(StatisticsExec::new(right, schema.as_ref().clone())),
1449            Partitioning::Hash(hash_expr, 2),
1450        )?);
1451
1452        let interleave = InterleaveExec::try_new(vec![left, right])?;
1453        let stats = StatisticsContext::new()
1454            .compute(&interleave, &StatisticsArgs::new().with_partition(Some(0)))?;
1455
1456        let expected = Statistics::default()
1457            .with_num_rows(Precision::Inexact(5))
1458            .with_total_byte_size(Precision::Inexact(25))
1459            .add_column_statistics(ColumnStatistics::new_unknown());
1460
1461        assert_eq!(stats.as_ref(), &expected);
1462        Ok(())
1463    }
1464
1465    #[tokio::test]
1466    async fn test_union_equivalence_properties() -> Result<()> {
1467        let schema = create_test_schema()?;
1468        let col_a = &col("a", &schema)?;
1469        let col_b = &col("b", &schema)?;
1470        let col_c = &col("c", &schema)?;
1471        let col_d = &col("d", &schema)?;
1472        let col_e = &col("e", &schema)?;
1473        let col_f = &col("f", &schema)?;
1474        let options = SortOptions::default();
1475        let test_cases = [
1476            //-----------TEST CASE 1----------//
1477            (
1478                // First child orderings
1479                vec![
1480                    // [a ASC, b ASC, f ASC]
1481                    vec![(col_a, options), (col_b, options), (col_f, options)],
1482                ],
1483                // Second child orderings
1484                vec![
1485                    // [a ASC, b ASC, c ASC]
1486                    vec![(col_a, options), (col_b, options), (col_c, options)],
1487                    // [a ASC, b ASC, f ASC]
1488                    vec![(col_a, options), (col_b, options), (col_f, options)],
1489                ],
1490                // Union output orderings
1491                vec![
1492                    // [a ASC, b ASC, f ASC]
1493                    vec![(col_a, options), (col_b, options), (col_f, options)],
1494                ],
1495            ),
1496            //-----------TEST CASE 2----------//
1497            (
1498                // First child orderings
1499                vec![
1500                    // [a ASC, b ASC, f ASC]
1501                    vec![(col_a, options), (col_b, options), (col_f, options)],
1502                    // d ASC
1503                    vec![(col_d, options)],
1504                ],
1505                // Second child orderings
1506                vec![
1507                    // [a ASC, b ASC, c ASC]
1508                    vec![(col_a, options), (col_b, options), (col_c, options)],
1509                    // [e ASC]
1510                    vec![(col_e, options)],
1511                ],
1512                // Union output orderings
1513                vec![
1514                    // [a ASC, b ASC]
1515                    vec![(col_a, options), (col_b, options)],
1516                ],
1517            ),
1518        ];
1519
1520        for (
1521            test_idx,
1522            (first_child_orderings, second_child_orderings, union_orderings),
1523        ) in test_cases.iter().enumerate()
1524        {
1525            let first_orderings = convert_to_orderings(first_child_orderings);
1526            let second_orderings = convert_to_orderings(second_child_orderings);
1527            let union_expected_orderings = convert_to_orderings(union_orderings);
1528            let child1_exec = TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?
1529                .try_with_sort_information(first_orderings)?;
1530            let child1 = Arc::new(child1_exec);
1531            let child1 = Arc::new(TestMemoryExec::update_cache(&child1));
1532            let child2_exec = TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?
1533                .try_with_sort_information(second_orderings)?;
1534            let child2 = Arc::new(child2_exec);
1535            let child2 = Arc::new(TestMemoryExec::update_cache(&child2));
1536
1537            let mut union_expected_eq = EquivalenceProperties::new(Arc::clone(&schema));
1538            union_expected_eq.add_orderings(union_expected_orderings);
1539
1540            let union: Arc<dyn ExecutionPlan> = UnionExec::try_new(vec![child1, child2])?;
1541            let union_eq_properties = union.properties().equivalence_properties();
1542            let err_msg = format!(
1543                "Error in test id: {:?}, test case: {:?}",
1544                test_idx, test_cases[test_idx]
1545            );
1546            assert_eq_properties_same(union_eq_properties, &union_expected_eq, err_msg);
1547        }
1548        Ok(())
1549    }
1550
1551    fn assert_eq_properties_same(
1552        lhs: &EquivalenceProperties,
1553        rhs: &EquivalenceProperties,
1554        err_msg: String,
1555    ) {
1556        // Check whether orderings are same.
1557        let lhs_orderings = lhs.oeq_class();
1558        let rhs_orderings = rhs.oeq_class();
1559        assert_eq!(lhs_orderings.len(), rhs_orderings.len(), "{err_msg}");
1560        for rhs_ordering in rhs_orderings.iter() {
1561            assert!(lhs_orderings.contains(rhs_ordering), "{}", err_msg);
1562        }
1563    }
1564
1565    #[test]
1566    fn test_union_empty_inputs() {
1567        // Test that UnionExec::try_new fails with empty inputs
1568        let result = UnionExec::try_new(vec![]);
1569        assert!(
1570            result
1571                .unwrap_err()
1572                .to_string()
1573                .contains("UnionExec requires at least one input")
1574        );
1575    }
1576
1577    #[test]
1578    fn test_union_schema_empty_inputs() {
1579        // Test that union_schema fails with empty inputs
1580        let result = union_schema(&[]);
1581        assert!(
1582            result
1583                .unwrap_err()
1584                .to_string()
1585                .contains("Cannot create union schema from empty inputs")
1586        );
1587    }
1588
1589    #[test]
1590    fn test_union_single_input() -> Result<()> {
1591        // Test that UnionExec::try_new returns the single input directly
1592        let schema = create_test_schema()?;
1593        let memory_exec: Arc<dyn ExecutionPlan> =
1594            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?);
1595        let memory_exec_clone = Arc::clone(&memory_exec);
1596        let result = UnionExec::try_new(vec![memory_exec])?;
1597
1598        // Check that the result is the same as the input (no UnionExec wrapper)
1599        assert_eq!(result.schema(), schema);
1600        // Verify it's the same execution plan
1601        assert!(Arc::ptr_eq(&result, &memory_exec_clone));
1602
1603        Ok(())
1604    }
1605
1606    #[test]
1607    fn test_union_schema_multiple_inputs() -> Result<()> {
1608        // Test that existing functionality with multiple inputs still works
1609        let schema = create_test_schema()?;
1610        let memory_exec1 =
1611            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?);
1612        let memory_exec2 =
1613            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?);
1614
1615        let union_plan = UnionExec::try_new(vec![memory_exec1, memory_exec2])?;
1616
1617        // Downcast to verify it's a UnionExec
1618        let union = union_plan
1619            .downcast_ref::<UnionExec>()
1620            .expect("Expected UnionExec");
1621
1622        // Check that schema is correct
1623        assert_eq!(union.schema(), schema);
1624        // Check that we have 2 inputs
1625        assert_eq!(union.inputs().len(), 2);
1626
1627        Ok(())
1628    }
1629
1630    #[test]
1631    fn test_union_schema_mismatch() {
1632        // Test that UnionExec properly rejects inputs with different field counts
1633        let schema = create_test_schema().unwrap();
1634        let schema2 = create_test_schema2().unwrap();
1635        let memory_exec1 =
1636            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema), None).unwrap());
1637        let memory_exec2 =
1638            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema2), None).unwrap());
1639
1640        let result = UnionExec::try_new(vec![memory_exec1, memory_exec2]);
1641        assert!(result.is_err());
1642        assert!(
1643            result.unwrap_err().to_string().contains(
1644                "UnionExec/InterleaveExec requires all inputs to have the same number of fields"
1645            )
1646        );
1647    }
1648
1649    fn make_hash_exec(
1650        schema: &SchemaRef,
1651        hash_cols: Vec<&str>,
1652        buckets: usize,
1653    ) -> Result<Arc<dyn ExecutionPlan>> {
1654        let exprs = hash_cols
1655            .iter()
1656            .map(|c| col(c, schema))
1657            .collect::<Result<Vec<_>>>()?;
1658        let base = Arc::new(TestMemoryExec::try_new(&[], Arc::clone(schema), None)?);
1659        Ok(Arc::new(RepartitionExec::try_new(
1660            base,
1661            Partitioning::Hash(exprs, buckets),
1662        )?))
1663    }
1664
1665    fn make_range_exec(
1666        schema: &SchemaRef,
1667        split_values: Vec<i32>,
1668        sort_options: SortOptions,
1669    ) -> Result<Arc<dyn ExecutionPlan>> {
1670        let sort_expr =
1671            PhysicalSortExpr::new(col(schema.field(0).name(), schema)?, sort_options);
1672        let ordering = LexOrdering::new(vec![sort_expr]).unwrap();
1673        let split_points = split_values
1674            .into_iter()
1675            .map(|v| SplitPoint::new(vec![ScalarValue::Int32(Some(v))]))
1676            .collect();
1677        let base = Arc::new(TestMemoryExec::try_new(&[], Arc::clone(schema), None)?);
1678        Ok(Arc::new(RepartitionExec::try_new(
1679            base,
1680            Partitioning::Range(RangePartitioning::try_new(ordering, split_points)?),
1681        )?))
1682    }
1683
1684    #[test]
1685    fn test_can_interleave_matrix() -> Result<()> {
1686        let name_column = "name";
1687        let age_column = "age";
1688        let schema = Arc::new(Schema::new(vec![
1689            Field::new(name_column, DataType::Int32, true),
1690            Field::new(age_column, DataType::Int32, true),
1691        ]));
1692
1693        let ascending = SortOptions {
1694            descending: false,
1695            nulls_first: false,
1696        };
1697        struct Case {
1698            inputs: Vec<Arc<dyn ExecutionPlan>>,
1699            expected: bool,
1700            label: &'static str,
1701        }
1702
1703        let cases = vec![
1704            // compatible
1705            Case {
1706                label: "matching hash on single column",
1707                expected: true,
1708                inputs: vec![
1709                    make_hash_exec(&schema, vec![name_column], 3)?,
1710                    make_hash_exec(&schema, vec![name_column], 3)?,
1711                ],
1712            },
1713            Case {
1714                label: "matching hash on multiple columns",
1715                expected: true,
1716                inputs: vec![
1717                    make_hash_exec(&schema, vec![name_column, age_column], 3)?,
1718                    make_hash_exec(&schema, vec![name_column, age_column], 3)?,
1719                ],
1720            },
1721            Case {
1722                label: "matching range same splits and order",
1723                expected: true,
1724                inputs: vec![
1725                    make_range_exec(&schema, vec![10, 20], ascending)?,
1726                    make_range_exec(&schema, vec![10, 20], ascending)?,
1727                ],
1728            },
1729            // incompatible
1730            Case {
1731                label: "subset range partition",
1732                expected: false,
1733                inputs: vec![
1734                    make_range_exec(&schema, vec![10, 20], ascending)?,
1735                    make_range_exec(&schema, vec![10, 15], ascending)?,
1736                ],
1737            },
1738            Case {
1739                label: "range different split points",
1740                expected: false,
1741                inputs: vec![
1742                    make_range_exec(&schema, vec![10, 20], ascending)?,
1743                    make_range_exec(&schema, vec![10, 30], ascending)?,
1744                ],
1745            },
1746            Case {
1747                label: "mixed range and hash",
1748                expected: false,
1749                inputs: vec![
1750                    make_range_exec(&schema, vec![10, 20], ascending)?,
1751                    make_hash_exec(&schema, vec![name_column], 3)?,
1752                ],
1753            },
1754        ];
1755
1756        for case in cases {
1757            assert_eq!(
1758                can_interleave(case.inputs.iter()),
1759                case.expected,
1760                "{}",
1761                case.label
1762            );
1763        }
1764        Ok(())
1765    }
1766
1767    #[test]
1768    fn test_union_cardinality_effect() -> Result<()> {
1769        let schema = create_test_schema()?;
1770        let input1: Arc<dyn ExecutionPlan> =
1771            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?);
1772        let input2: Arc<dyn ExecutionPlan> =
1773            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?);
1774
1775        let union = UnionExec::try_new(vec![input1, input2])?;
1776        let union = union
1777            .downcast_ref::<UnionExec>()
1778            .expect("expected UnionExec for multiple inputs");
1779
1780        assert!(matches!(
1781            union.cardinality_effect(),
1782            CardinalityEffect::GreaterEqual
1783        ));
1784        Ok(())
1785    }
1786}