Skip to main content

datafusion_physical_plan/
coalesce_partitions.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 merge plan for executing partitions in parallel and then merging the results
19//! into a single partition
20
21use std::sync::Arc;
22
23use super::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
24use super::stream::{ObservedStream, RecordBatchReceiverStream};
25use super::{
26    DisplayAs, ExecutionPlanProperties, PlanProperties, SendableRecordBatchStream,
27    Statistics,
28};
29use crate::execution_plan::{
30    CardinalityEffect, EvaluationType, SchedulingType, replace_children_if_necessary,
31};
32use crate::filter_pushdown::{FilterDescription, FilterPushdownPhase};
33use crate::projection::{ProjectionExec, make_with_child};
34use crate::sort_pushdown::SortOrderPushdownResult;
35use crate::statistics::{ChildStats, StatisticsArgs};
36use crate::{
37    ChildrenPropertiesMode, DisplayFormatType, ExecutionPlan, Partitioning,
38    ReplaceChildrenOptions, validate_child_count,
39};
40use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
41
42use datafusion_common::config::ConfigOptions;
43use datafusion_common::tree_node::TreeNodeRecursion;
44use datafusion_common::{Result, assert_eq_or_internal_err, internal_err};
45use datafusion_execution::TaskContext;
46use datafusion_physical_expr::PhysicalExpr;
47
48/// Merge execution plan executes partitions in parallel and combines them into a single
49/// partition. No guarantees are made about the order of the resulting partition.
50#[derive(Debug, Clone)]
51pub struct CoalescePartitionsExec {
52    /// Input execution plan
53    input: Arc<dyn ExecutionPlan>,
54    /// Execution metrics
55    metrics: ExecutionPlanMetricsSet,
56    cache: Arc<PlanProperties>,
57    /// Optional number of rows to fetch. Stops producing rows after this fetch
58    pub(crate) fetch: Option<usize>,
59}
60
61impl CoalescePartitionsExec {
62    /// Create a new CoalescePartitionsExec
63    pub fn new(input: Arc<dyn ExecutionPlan>) -> Self {
64        let cache = Self::compute_properties(&input);
65        CoalescePartitionsExec {
66            input,
67            metrics: ExecutionPlanMetricsSet::new(),
68            cache: Arc::new(cache),
69            fetch: None,
70        }
71    }
72
73    /// Update fetch with the argument
74    pub fn with_fetch(mut self, fetch: Option<usize>) -> Self {
75        self.fetch = fetch;
76        self
77    }
78
79    /// Input execution plan
80    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
81        &self.input
82    }
83
84    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
85    fn compute_properties(input: &Arc<dyn ExecutionPlan>) -> PlanProperties {
86        let input_partitions = input.output_partitioning().partition_count();
87        let (drive, scheduling) = if input_partitions > 1 {
88            (EvaluationType::Eager, SchedulingType::Cooperative)
89        } else {
90            (
91                input.properties().evaluation_type,
92                input.properties().scheduling_type,
93            )
94        };
95
96        // Coalescing partitions loses existing orderings:
97        let mut eq_properties = input.equivalence_properties().clone();
98        eq_properties.clear_orderings();
99        eq_properties.clear_per_partition_constants();
100        PlanProperties::new(
101            eq_properties,                        // Equivalence Properties
102            Partitioning::UnknownPartitioning(1), // Output Partitioning
103            input.pipeline_behavior(),
104            input.boundedness(),
105        )
106        .with_evaluation_type(drive)
107        .with_scheduling_type(scheduling)
108    }
109}
110
111impl DisplayAs for CoalescePartitionsExec {
112    fn fmt_as(
113        &self,
114        t: DisplayFormatType,
115        f: &mut std::fmt::Formatter,
116    ) -> std::fmt::Result {
117        match t {
118            DisplayFormatType::Default | DisplayFormatType::Verbose => match self.fetch {
119                Some(fetch) => {
120                    write!(f, "CoalescePartitionsExec: fetch={fetch}")
121                }
122                None => write!(f, "CoalescePartitionsExec"),
123            },
124            DisplayFormatType::TreeRender => match self.fetch {
125                Some(fetch) => {
126                    write!(f, "limit: {fetch}")
127                }
128                None => write!(f, ""),
129            },
130        }
131    }
132}
133
134impl ExecutionPlan for CoalescePartitionsExec {
135    fn name(&self) -> &'static str {
136        "CoalescePartitionsExec"
137    }
138
139    /// Return a reference to Any that can be used for downcasting
140    fn properties(&self) -> &Arc<PlanProperties> {
141        &self.cache
142    }
143
144    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
145        vec![&self.input]
146    }
147
148    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
149        vec![false]
150    }
151
152    fn apply_expressions(
153        &self,
154        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
155    ) -> Result<TreeNodeRecursion> {
156        Ok(TreeNodeRecursion::Continue)
157    }
158
159    fn replace_children(
160        self: Arc<Self>,
161        mut children: Vec<Arc<dyn ExecutionPlan>>,
162        options: ReplaceChildrenOptions,
163    ) -> Result<Arc<dyn ExecutionPlan>> {
164        validate_child_count!(self, children);
165        match options.children_properties {
166            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
167                input: children.swap_remove(0),
168                metrics: ExecutionPlanMetricsSet::new(),
169                ..Self::clone(&*self)
170            })),
171            ChildrenPropertiesMode::Recompute => {
172                let mut plan = CoalescePartitionsExec::new(children.swap_remove(0));
173                plan.fetch = self.fetch;
174                Ok(Arc::new(plan))
175            }
176        }
177    }
178
179    fn with_new_children(
180        self: Arc<Self>,
181        children: Vec<Arc<dyn ExecutionPlan>>,
182    ) -> Result<Arc<dyn ExecutionPlan>> {
183        self.replace_children(
184            children,
185            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
186        )
187    }
188
189    fn with_new_children_and_same_properties(
190        self: Arc<Self>,
191        children: Vec<Arc<dyn ExecutionPlan>>,
192    ) -> Result<Arc<dyn ExecutionPlan>> {
193        self.replace_children(
194            children,
195            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
196        )
197    }
198
199    fn execute(
200        &self,
201        partition: usize,
202        context: Arc<TaskContext>,
203    ) -> Result<SendableRecordBatchStream> {
204        // CoalescePartitionsExec produces a single partition
205        assert_eq_or_internal_err!(
206            partition,
207            0,
208            "CoalescePartitionsExec invalid partition {partition}"
209        );
210
211        let input_partitions = self.input.output_partitioning().partition_count();
212        match input_partitions {
213            0 => internal_err!(
214                "CoalescePartitionsExec requires at least one input partition"
215            ),
216            1 => {
217                // single-partition path: execute child directly, but ensure fetch is respected
218                // (wrap with ObservedStream only if fetch is present so we don't add overhead otherwise)
219                let child_stream = self.input.execute(0, context)?;
220                if self.fetch.is_some() {
221                    let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
222                    return Ok(Box::pin(ObservedStream::new(
223                        child_stream,
224                        baseline_metrics,
225                        self.fetch,
226                    )));
227                }
228                Ok(child_stream)
229            }
230            _ => {
231                let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
232                // record the (very) minimal work done so that
233                // elapsed_compute is not reported as 0
234                let elapsed_compute = baseline_metrics.elapsed_compute().clone();
235                let _timer = elapsed_compute.timer();
236
237                // use a stream that allows each sender to put in at
238                // least one result in an attempt to maximize
239                // parallelism.
240                let mut builder =
241                    RecordBatchReceiverStream::builder(self.schema(), input_partitions);
242
243                // spawn independent tasks whose resulting streams (of batches)
244                // are sent to the channel for consumption.
245                for part_i in 0..input_partitions {
246                    builder.run_input(
247                        Arc::clone(&self.input),
248                        part_i,
249                        Arc::clone(&context),
250                    );
251                }
252
253                let stream = builder.build();
254                Ok(Box::pin(ObservedStream::new(
255                    stream,
256                    baseline_metrics,
257                    self.fetch,
258                )))
259            }
260        }
261    }
262
263    fn metrics(&self) -> Option<MetricsSet> {
264        Some(self.metrics.clone_inner())
265    }
266
267    fn child_stats_requests(&self, _partition: Option<usize>) -> Vec<ChildStats> {
268        vec![ChildStats::At(None)]
269    }
270
271    fn statistics_from_inputs(
272        &self,
273        input_stats: &[Arc<Statistics>],
274        _args: &StatisticsArgs,
275    ) -> Result<Arc<Statistics>> {
276        let stats = input_stats[0].as_ref().clone();
277        Ok(Arc::new(stats.with_fetch(self.fetch, 0, 1)?))
278    }
279
280    fn supports_limit_pushdown(&self) -> bool {
281        true
282    }
283
284    fn cardinality_effect(&self) -> CardinalityEffect {
285        CardinalityEffect::Equal
286    }
287
288    /// Tries to swap `projection` with its input, which is known to be a
289    /// [`CoalescePartitionsExec`]. If possible, performs the swap and returns
290    /// [`CoalescePartitionsExec`] as the top plan. Otherwise, returns `None`.
291    fn try_swapping_with_projection(
292        &self,
293        projection: &ProjectionExec,
294    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
295        // If the projection does not narrow the schema, we should not try to push it down:
296        if projection.expr().len() >= projection.input().schema().fields().len() {
297            return Ok(None);
298        }
299        // CoalescePartitionsExec always has a single child, so zero indexing is safe.
300        make_with_child(projection, projection.input().children()[0]).map(|e| {
301            if self.fetch.is_some() {
302                let mut plan = CoalescePartitionsExec::new(e);
303                plan.fetch = self.fetch;
304                Some(Arc::new(plan) as _)
305            } else {
306                Some(Arc::new(CoalescePartitionsExec::new(e)) as _)
307            }
308        })
309    }
310
311    fn fetch(&self) -> Option<usize> {
312        self.fetch
313    }
314
315    fn with_fetch(&self, limit: Option<usize>) -> Option<Arc<dyn ExecutionPlan>> {
316        Some(Arc::new(CoalescePartitionsExec {
317            input: Arc::clone(&self.input),
318            fetch: limit,
319            metrics: self.metrics.clone(),
320            cache: Arc::clone(&self.cache),
321        }))
322    }
323
324    fn with_preserve_order(
325        &self,
326        preserve_order: bool,
327    ) -> Option<Arc<dyn ExecutionPlan>> {
328        self.input
329            .with_preserve_order(preserve_order)
330            .and_then(|new_input| {
331                replace_children_if_necessary(Arc::new(self.clone()), vec![new_input])
332                    .ok()
333            })
334    }
335
336    fn gather_filters_for_pushdown(
337        &self,
338        _phase: FilterPushdownPhase,
339        parent_filters: Vec<Arc<dyn PhysicalExpr>>,
340        _config: &ConfigOptions,
341    ) -> Result<FilterDescription> {
342        FilterDescription::from_children(parent_filters, &self.children())
343    }
344
345    fn try_pushdown_sort(
346        &self,
347        order: &[PhysicalSortExpr],
348    ) -> Result<SortOrderPushdownResult<Arc<dyn ExecutionPlan>>> {
349        // CoalescePartitionsExec merges multiple partitions into one, which loses
350        // global ordering. However, we can still push the sort requirement down
351        // to optimize individual partitions - the Sort operator above will handle
352        // the global ordering.
353        //
354        // Note: The result will always be at most Inexact (never Exact) when there
355        // are multiple partitions, because merging destroys global ordering.
356        let result = self.input.try_pushdown_sort(order)?;
357
358        // If we have multiple partitions, we can't return Exact even if the
359        // underlying source claims Exact - merging destroys global ordering
360        let has_multiple_partitions =
361            self.input.output_partitioning().partition_count() > 1;
362
363        result
364            .try_map(|new_input| {
365                Ok(
366                    Arc::new(
367                        CoalescePartitionsExec::new(new_input).with_fetch(self.fetch),
368                    ) as Arc<dyn ExecutionPlan>,
369                )
370            })
371            .map(|r| {
372                if has_multiple_partitions {
373                    // Downgrade Exact to Inexact when merging multiple partitions
374                    r.into_inexact()
375                } else {
376                    r
377                }
378            })
379    }
380
381    #[cfg(feature = "proto")]
382    fn try_to_proto(
383        &self,
384        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
385    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
386        use datafusion_proto_models::protobuf;
387        let input = ctx.encode_child(self.input())?;
388        Ok(Some(protobuf::PhysicalPlanNode {
389            physical_plan_type: Some(
390                protobuf::physical_plan_node::PhysicalPlanType::Merge(Box::new(
391                    protobuf::CoalescePartitionsExecNode {
392                        input: Some(Box::new(input)),
393                        fetch: self.fetch().map(|f| f as u32),
394                    },
395                )),
396            ),
397        }))
398    }
399}
400
401#[cfg(feature = "proto")]
402impl CoalescePartitionsExec {
403    /// Reconstruct a [`CoalescePartitionsExec`] from its protobuf representation.
404    ///
405    /// The exact inverse of [`ExecutionPlan::try_to_proto`]. Note the protobuf
406    /// variant is named `Merge` (node [`CoalescePartitionsExecNode`]).
407    ///
408    /// [`CoalescePartitionsExecNode`]: datafusion_proto_models::protobuf::CoalescePartitionsExecNode
409    /// [`ExecutionPlan::try_to_proto`]: crate::ExecutionPlan::try_to_proto
410    pub fn try_from_proto(
411        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
412        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
413    ) -> Result<Arc<dyn ExecutionPlan>> {
414        use datafusion_proto_models::protobuf;
415        let merge = crate::expect_plan_variant!(
416            node,
417            protobuf::physical_plan_node::PhysicalPlanType::Merge,
418            "CoalescePartitionsExec",
419        );
420        let input = ctx.decode_required_child(
421            merge.input.as_deref(),
422            "CoalescePartitionsExec",
423            "input",
424        )?;
425        Ok(Arc::new(
426            CoalescePartitionsExec::new(input)
427                .with_fetch(merge.fetch.map(|f| f as usize)),
428        ))
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::test::exec::{
436        BarrierExec, BlockingExec, PanicExec, assert_strong_count_converges_to_zero,
437    };
438    use crate::test::{self, assert_is_pending};
439    use crate::{collect, common};
440
441    use std::time::Duration;
442
443    use arrow::array::RecordBatch;
444    use arrow::datatypes::{DataType, Field, Schema};
445
446    use futures::FutureExt;
447
448    #[tokio::test]
449    async fn merge() -> Result<()> {
450        let task_ctx = Arc::new(TaskContext::default());
451
452        let num_partitions = 4;
453        let csv = test::scan_partitioned(num_partitions);
454
455        // input should have 4 partitions
456        assert_eq!(csv.output_partitioning().partition_count(), num_partitions);
457
458        let merge = CoalescePartitionsExec::new(csv);
459
460        // output of CoalescePartitionsExec should have a single partition
461        assert_eq!(
462            merge.properties().output_partitioning().partition_count(),
463            1
464        );
465
466        // the result should contain 4 batches (one per input partition)
467        let iter = merge.execute(0, task_ctx)?;
468        let batches = common::collect(iter).await?;
469        assert_eq!(batches.len(), num_partitions);
470
471        // there should be a total of 400 rows (100 per each partition)
472        let row_count: usize = batches.iter().map(|batch| batch.num_rows()).sum();
473        assert_eq!(row_count, 400);
474
475        Ok(())
476    }
477
478    #[tokio::test]
479    async fn drops_input_plan_after_input_streams_start() -> Result<()> {
480        let task_ctx = Arc::new(TaskContext::default());
481        let schema =
482            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, true)]));
483        let input_partitions = 2;
484        let batch = RecordBatch::new_empty(Arc::clone(&schema));
485        let input = Arc::new(
486            BarrierExec::new(vec![vec![batch]; input_partitions], schema)
487                .without_start_barrier()
488                .with_finish_barrier()
489                .with_log(false),
490        );
491        let refs = Arc::downgrade(&input);
492
493        let input_plan: Arc<BarrierExec> = Arc::clone(&input);
494        let coalesce = CoalescePartitionsExec::new(input_plan);
495        let stream = coalesce.execute(0, task_ctx)?;
496        drop(coalesce);
497
498        tokio::time::timeout(Duration::from_secs(5), async {
499            // Why not `wait_finish` here: that releases the barrier which lets the input tasks
500            // finish, which drops the input Arcs and hides the bug.
501            while !input.is_finish_barrier_reached() {
502                tokio::task::yield_now().await;
503            }
504        })
505        .await
506        .expect("input streams should reach pending");
507
508        drop(input);
509
510        assert_strong_count_converges_to_zero(refs).await;
511
512        drop(stream);
513
514        Ok(())
515    }
516
517    #[tokio::test]
518    async fn test_drop_cancel() -> Result<()> {
519        let task_ctx = Arc::new(TaskContext::default());
520        let schema =
521            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, true)]));
522
523        let blocking_exec = Arc::new(BlockingExec::new(Arc::clone(&schema), 2));
524        let refs = blocking_exec.refs();
525        let coalesce_partitions_exec =
526            Arc::new(CoalescePartitionsExec::new(blocking_exec));
527
528        let fut = collect(coalesce_partitions_exec, task_ctx);
529        let mut fut = fut.boxed();
530
531        assert_is_pending(&mut fut);
532        drop(fut);
533        assert_strong_count_converges_to_zero(refs).await;
534
535        Ok(())
536    }
537
538    #[tokio::test]
539    #[should_panic(expected = "PanickingStream did panic")]
540    async fn test_panic() {
541        let task_ctx = Arc::new(TaskContext::default());
542        let schema =
543            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, true)]));
544
545        let panicking_exec = Arc::new(PanicExec::new(Arc::clone(&schema), 2));
546        let coalesce_partitions_exec =
547            Arc::new(CoalescePartitionsExec::new(panicking_exec));
548
549        collect(coalesce_partitions_exec, task_ctx).await.unwrap();
550    }
551
552    #[tokio::test]
553    async fn test_single_partition_with_fetch() -> Result<()> {
554        let task_ctx = Arc::new(TaskContext::default());
555
556        // Use existing scan_partitioned with 1 partition (returns 100 rows per partition)
557        let input = test::scan_partitioned(1);
558
559        // Test with fetch=3
560        let coalesce = CoalescePartitionsExec::new(input).with_fetch(Some(3));
561
562        let stream = coalesce.execute(0, task_ctx)?;
563        let batches = common::collect(stream).await?;
564
565        let row_count: usize = batches.iter().map(|batch| batch.num_rows()).sum();
566        assert_eq!(row_count, 3, "Should only return 3 rows due to fetch=3");
567
568        Ok(())
569    }
570
571    #[tokio::test]
572    async fn test_multi_partition_with_fetch_one() -> Result<()> {
573        let task_ctx = Arc::new(TaskContext::default());
574
575        // Create 4 partitions, each with 100 rows
576        // This simulates the real-world scenario where each partition has data
577        let input = test::scan_partitioned(4);
578
579        // Test with fetch=1 (the original bug: was returning multiple rows instead of 1)
580        let coalesce = CoalescePartitionsExec::new(input).with_fetch(Some(1));
581
582        let stream = coalesce.execute(0, task_ctx)?;
583        let batches = common::collect(stream).await?;
584
585        let row_count: usize = batches.iter().map(|batch| batch.num_rows()).sum();
586        assert_eq!(
587            row_count, 1,
588            "Should only return 1 row due to fetch=1, not one per partition"
589        );
590
591        Ok(())
592    }
593
594    #[tokio::test]
595    async fn test_single_partition_without_fetch() -> Result<()> {
596        let task_ctx = Arc::new(TaskContext::default());
597
598        // Use scan_partitioned with 1 partition
599        let input = test::scan_partitioned(1);
600
601        // Test without fetch (should return all rows)
602        let coalesce = CoalescePartitionsExec::new(input);
603
604        let stream = coalesce.execute(0, task_ctx)?;
605        let batches = common::collect(stream).await?;
606
607        let row_count: usize = batches.iter().map(|batch| batch.num_rows()).sum();
608        assert_eq!(
609            row_count, 100,
610            "Should return all 100 rows when fetch is None"
611        );
612
613        Ok(())
614    }
615
616    #[tokio::test]
617    async fn test_single_partition_fetch_larger_than_batch() -> Result<()> {
618        let task_ctx = Arc::new(TaskContext::default());
619
620        // Use scan_partitioned with 1 partition (returns 100 rows)
621        let input = test::scan_partitioned(1);
622
623        // Test with fetch larger than available rows
624        let coalesce = CoalescePartitionsExec::new(input).with_fetch(Some(200));
625
626        let stream = coalesce.execute(0, task_ctx)?;
627        let batches = common::collect(stream).await?;
628
629        let row_count: usize = batches.iter().map(|batch| batch.num_rows()).sum();
630        assert_eq!(
631            row_count, 100,
632            "Should return all available rows (100) when fetch (200) is larger"
633        );
634
635        Ok(())
636    }
637
638    #[tokio::test]
639    async fn test_multi_partition_fetch_exact_match() -> Result<()> {
640        let task_ctx = Arc::new(TaskContext::default());
641
642        // Create 4 partitions, each with 100 rows
643        let num_partitions = 4;
644        let csv = test::scan_partitioned(num_partitions);
645
646        // Test with fetch=400 (exactly all rows)
647        let coalesce = CoalescePartitionsExec::new(csv).with_fetch(Some(400));
648
649        let stream = coalesce.execute(0, task_ctx)?;
650        let batches = common::collect(stream).await?;
651
652        let row_count: usize = batches.iter().map(|batch| batch.num_rows()).sum();
653        assert_eq!(row_count, 400, "Should return exactly 400 rows");
654
655        Ok(())
656    }
657}