Skip to main content

datafusion_physical_plan/
coalesce_batches.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//! [`CoalesceBatchesExec`] combines small batches into larger batches.
19
20use std::pin::Pin;
21use std::sync::Arc;
22use std::task::{Context, Poll};
23
24use super::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
25use super::{DisplayAs, ExecutionPlanProperties, PlanProperties, Statistics};
26use crate::projection::ProjectionExec;
27use crate::statistics::{ChildStats, StatisticsArgs};
28use crate::stream::EmptyRecordBatchStream;
29use crate::{
30    ChildrenPropertiesMode, DisplayFormatType, ExecutionPlan, RecordBatchStream,
31    ReplaceChildrenOptions, SendableRecordBatchStream, validate_child_count,
32};
33
34use arrow::datatypes::SchemaRef;
35use arrow::record_batch::RecordBatch;
36use datafusion_common::Result;
37use datafusion_common::tree_node::TreeNodeRecursion;
38use datafusion_execution::TaskContext;
39use datafusion_physical_expr::PhysicalExpr;
40
41use crate::coalesce::{LimitedBatchCoalescer, PushBatchStatus};
42use crate::execution_plan::{CardinalityEffect, replace_children_if_necessary};
43use crate::filter_pushdown::{
44    ChildPushdownResult, FilterDescription, FilterPushdownPhase,
45    FilterPushdownPropagation,
46};
47use crate::sort_pushdown::SortOrderPushdownResult;
48use datafusion_common::config::ConfigOptions;
49use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
50use futures::ready;
51use futures::stream::{Stream, StreamExt};
52
53/// `CoalesceBatchesExec` combines small batches into larger batches for more
54/// efficient vectorized processing by later operators.
55///
56/// The operator buffers batches until it collects `target_batch_size` rows and
57/// then emits a single concatenated batch. When only a limited number of rows
58/// are necessary (specified by the `fetch` parameter), the operator will stop
59/// buffering and returns the final batch once the number of collected rows
60/// reaches the `fetch` value.
61///
62/// See [`LimitedBatchCoalescer`] for more information
63#[deprecated(
64    since = "52.0.0",
65    note = "We now use BatchCoalescer from arrow-rs instead of a dedicated operator"
66)]
67#[derive(Debug, Clone)]
68pub struct CoalesceBatchesExec {
69    /// The input plan
70    input: Arc<dyn ExecutionPlan>,
71    /// Minimum number of rows for coalescing batches
72    target_batch_size: usize,
73    /// Maximum number of rows to fetch, `None` means fetching all rows
74    fetch: Option<usize>,
75    /// Execution metrics
76    metrics: ExecutionPlanMetricsSet,
77    cache: Arc<PlanProperties>,
78}
79
80#[expect(deprecated)]
81impl CoalesceBatchesExec {
82    /// Create a new CoalesceBatchesExec
83    pub fn new(input: Arc<dyn ExecutionPlan>, target_batch_size: usize) -> Self {
84        let cache = Self::compute_properties(&input);
85        Self {
86            input,
87            target_batch_size,
88            fetch: None,
89            metrics: ExecutionPlanMetricsSet::new(),
90            cache: Arc::new(cache),
91        }
92    }
93
94    /// Update fetch with the argument
95    pub fn with_fetch(mut self, fetch: Option<usize>) -> Self {
96        self.fetch = fetch;
97        self
98    }
99
100    /// The input plan
101    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
102        &self.input
103    }
104
105    /// Minimum number of rows for coalesces batches
106    pub fn target_batch_size(&self) -> usize {
107        self.target_batch_size
108    }
109
110    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
111    fn compute_properties(input: &Arc<dyn ExecutionPlan>) -> PlanProperties {
112        // The coalesce batches operator does not make any changes to the
113        // partitioning of its input.
114        PlanProperties::new(
115            input.equivalence_properties().clone(), // Equivalence Properties
116            input.output_partitioning().clone(),    // Output Partitioning
117            input.pipeline_behavior(),
118            input.boundedness(),
119        )
120    }
121}
122
123#[expect(deprecated)]
124impl DisplayAs for CoalesceBatchesExec {
125    fn fmt_as(
126        &self,
127        t: DisplayFormatType,
128        f: &mut std::fmt::Formatter,
129    ) -> std::fmt::Result {
130        match t {
131            DisplayFormatType::Default | DisplayFormatType::Verbose => {
132                write!(
133                    f,
134                    "CoalesceBatchesExec: target_batch_size={}",
135                    self.target_batch_size,
136                )?;
137                if let Some(fetch) = self.fetch {
138                    write!(f, ", fetch={fetch}")?;
139                };
140
141                Ok(())
142            }
143            DisplayFormatType::TreeRender => {
144                writeln!(f, "target_batch_size={}", self.target_batch_size)?;
145                if let Some(fetch) = self.fetch {
146                    write!(f, "limit={fetch}")?;
147                };
148                Ok(())
149            }
150        }
151    }
152}
153
154#[expect(deprecated)]
155impl ExecutionPlan for CoalesceBatchesExec {
156    fn name(&self) -> &'static str {
157        "CoalesceBatchesExec"
158    }
159
160    /// Return a reference to Any that can be used for downcasting
161    fn properties(&self) -> &Arc<PlanProperties> {
162        &self.cache
163    }
164
165    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
166        vec![&self.input]
167    }
168
169    fn maintains_input_order(&self) -> Vec<bool> {
170        vec![true]
171    }
172
173    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
174        vec![false]
175    }
176
177    fn apply_expressions(
178        &self,
179        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
180    ) -> Result<TreeNodeRecursion> {
181        Ok(TreeNodeRecursion::Continue)
182    }
183
184    fn replace_children(
185        self: Arc<Self>,
186        mut children: Vec<Arc<dyn ExecutionPlan>>,
187        options: ReplaceChildrenOptions,
188    ) -> Result<Arc<dyn ExecutionPlan>> {
189        validate_child_count!(self, children);
190        match options.children_properties {
191            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
192                input: children.swap_remove(0),
193                metrics: ExecutionPlanMetricsSet::new(),
194                ..Self::clone(&*self)
195            })),
196            ChildrenPropertiesMode::Recompute => Ok(Arc::new(
197                CoalesceBatchesExec::new(children.swap_remove(0), self.target_batch_size)
198                    .with_fetch(self.fetch),
199            )),
200        }
201    }
202
203    fn with_new_children(
204        self: Arc<Self>,
205        children: Vec<Arc<dyn ExecutionPlan>>,
206    ) -> Result<Arc<dyn ExecutionPlan>> {
207        self.replace_children(
208            children,
209            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
210        )
211    }
212
213    fn with_new_children_and_same_properties(
214        self: Arc<Self>,
215        children: Vec<Arc<dyn ExecutionPlan>>,
216    ) -> Result<Arc<dyn ExecutionPlan>> {
217        self.replace_children(
218            children,
219            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
220        )
221    }
222
223    fn execute(
224        &self,
225        partition: usize,
226        context: Arc<TaskContext>,
227    ) -> Result<SendableRecordBatchStream> {
228        Ok(Box::pin(CoalesceBatchesStream {
229            input: self.input.execute(partition, context)?,
230            coalescer: LimitedBatchCoalescer::new(
231                self.input.schema(),
232                self.target_batch_size,
233                self.fetch,
234            ),
235            baseline_metrics: BaselineMetrics::new(&self.metrics, partition),
236            completed: false,
237        }))
238    }
239
240    fn metrics(&self) -> Option<MetricsSet> {
241        Some(self.metrics.clone_inner())
242    }
243
244    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
245        vec![ChildStats::At(partition)]
246    }
247
248    fn statistics_from_inputs(
249        &self,
250        input_stats: &[Arc<Statistics>],
251        _args: &StatisticsArgs,
252    ) -> Result<Arc<Statistics>> {
253        let stats = input_stats[0].as_ref().clone();
254        Ok(Arc::new(stats.with_fetch(self.fetch, 0, 1)?))
255    }
256
257    fn with_fetch(&self, limit: Option<usize>) -> Option<Arc<dyn ExecutionPlan>> {
258        Some(Arc::new(CoalesceBatchesExec {
259            input: Arc::clone(&self.input),
260            target_batch_size: self.target_batch_size,
261            fetch: limit,
262            metrics: self.metrics.clone(),
263            cache: Arc::clone(&self.cache),
264        }))
265    }
266
267    fn fetch(&self) -> Option<usize> {
268        self.fetch
269    }
270
271    fn cardinality_effect(&self) -> CardinalityEffect {
272        CardinalityEffect::Equal
273    }
274
275    fn try_swapping_with_projection(
276        &self,
277        projection: &ProjectionExec,
278    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
279        match self.input.try_swapping_with_projection(projection)? {
280            Some(new_input) => Ok(Some(replace_children_if_necessary(
281                Arc::new(self.clone()),
282                vec![new_input],
283            )?)),
284            None => Ok(None),
285        }
286    }
287
288    fn gather_filters_for_pushdown(
289        &self,
290        _phase: FilterPushdownPhase,
291        parent_filters: Vec<Arc<dyn PhysicalExpr>>,
292        _config: &ConfigOptions,
293    ) -> Result<FilterDescription> {
294        FilterDescription::from_children(parent_filters, &self.children())
295    }
296
297    fn handle_child_pushdown_result(
298        &self,
299        _phase: FilterPushdownPhase,
300        child_pushdown_result: ChildPushdownResult,
301        _config: &ConfigOptions,
302    ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
303        Ok(FilterPushdownPropagation::if_all(child_pushdown_result))
304    }
305
306    fn try_pushdown_sort(
307        &self,
308        order: &[PhysicalSortExpr],
309    ) -> Result<SortOrderPushdownResult<Arc<dyn ExecutionPlan>>> {
310        // CoalesceBatchesExec is transparent for sort ordering - it preserves order
311        // Delegate to the child and wrap with a new CoalesceBatchesExec
312        self.input.try_pushdown_sort(order)?.try_map(|new_input| {
313            Ok(Arc::new(
314                CoalesceBatchesExec::new(new_input, self.target_batch_size)
315                    .with_fetch(self.fetch),
316            ) as Arc<dyn ExecutionPlan>)
317        })
318    }
319
320    #[cfg(feature = "proto")]
321    fn try_to_proto(
322        &self,
323        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
324    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
325        use datafusion_proto_models::protobuf;
326        let input = ctx.encode_child(self.input())?;
327        Ok(Some(protobuf::PhysicalPlanNode {
328            physical_plan_type: Some(
329                protobuf::physical_plan_node::PhysicalPlanType::CoalesceBatches(
330                    Box::new(protobuf::CoalesceBatchesExecNode {
331                        input: Some(Box::new(input)),
332                        target_batch_size: self.target_batch_size() as u32,
333                        fetch: self.fetch().map(|n| n as u32),
334                    }),
335                ),
336            ),
337        }))
338    }
339}
340
341#[cfg(feature = "proto")]
342#[expect(deprecated)]
343impl CoalesceBatchesExec {
344    /// Reconstruct a [`CoalesceBatchesExec`] from its protobuf representation.
345    ///
346    /// The exact inverse of [`ExecutionPlan::try_to_proto`]: it takes the whole
347    /// [`PhysicalPlanNode`] so every plan's `try_from_proto` shares one
348    /// signature. The child plan is decoded recursively via the
349    /// [`ExecutionPlanDecodeCtx`].
350    ///
351    /// [`PhysicalPlanNode`]: datafusion_proto_models::protobuf::PhysicalPlanNode
352    /// [`ExecutionPlan::try_to_proto`]: crate::ExecutionPlan::try_to_proto
353    /// [`ExecutionPlanDecodeCtx`]: crate::proto::ExecutionPlanDecodeCtx
354    pub fn try_from_proto(
355        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
356        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
357    ) -> Result<Arc<dyn ExecutionPlan>> {
358        use datafusion_proto_models::protobuf;
359        let coalesce_batches = crate::expect_plan_variant!(
360            node,
361            protobuf::physical_plan_node::PhysicalPlanType::CoalesceBatches,
362            "CoalesceBatchesExec",
363        );
364        let input = ctx.decode_required_child(
365            coalesce_batches.input.as_deref(),
366            "CoalesceBatchesExec",
367            "input",
368        )?;
369        Ok(Arc::new(
370            CoalesceBatchesExec::new(input, coalesce_batches.target_batch_size as usize)
371                .with_fetch(coalesce_batches.fetch.map(|f| f as usize)),
372        ))
373    }
374}
375
376/// Stream for [`CoalesceBatchesExec`]. See [`CoalesceBatchesExec`] for more details.
377struct CoalesceBatchesStream {
378    /// The input plan
379    input: SendableRecordBatchStream,
380    /// Buffer for combining batches
381    coalescer: LimitedBatchCoalescer,
382    /// Execution metrics
383    baseline_metrics: BaselineMetrics,
384    /// is the input stream exhausted or limit reached?
385    completed: bool,
386}
387
388impl Stream for CoalesceBatchesStream {
389    type Item = Result<RecordBatch>;
390
391    fn poll_next(
392        mut self: Pin<&mut Self>,
393        cx: &mut Context<'_>,
394    ) -> Poll<Option<Self::Item>> {
395        let poll = self.poll_next_inner(cx);
396        self.baseline_metrics.record_poll(poll)
397    }
398
399    fn size_hint(&self) -> (usize, Option<usize>) {
400        // we can't predict the size of incoming batches so re-use the size hint from the input
401        self.input.size_hint()
402    }
403}
404
405impl CoalesceBatchesStream {
406    fn poll_next_inner(
407        self: &mut Pin<&mut Self>,
408        cx: &mut Context<'_>,
409    ) -> Poll<Option<Result<RecordBatch>>> {
410        let cloned_time = self.baseline_metrics.elapsed_compute().clone();
411        loop {
412            // If there is any completed batch ready, return it
413            if let Some(batch) = self.coalescer.next_completed_batch() {
414                return Poll::Ready(Some(Ok(batch)));
415            }
416            if self.completed {
417                // If input is done and no batches are ready, return None to signal end of stream.
418                return Poll::Ready(None);
419            }
420            // Attempt to pull the next batch from the input stream.
421            let input_batch = ready!(self.input.poll_next_unpin(cx));
422            // Start timing the operation. The timer records time upon being dropped.
423            let _timer = cloned_time.timer();
424
425            match input_batch {
426                None => {
427                    // Input stream is exhausted, finalize any remaining batches
428                    self.completed = true;
429                    self.input =
430                        Box::pin(EmptyRecordBatchStream::new(self.coalescer.schema()));
431                    self.coalescer.finish()?;
432                }
433                Some(Ok(batch)) => {
434                    match self.coalescer.push_batch(batch)? {
435                        PushBatchStatus::Continue => {
436                            // Keep pushing more batches
437                        }
438                        PushBatchStatus::LimitReached => {
439                            // limit was reached, so stop early
440                            self.completed = true;
441                            self.input = Box::pin(EmptyRecordBatchStream::new(
442                                self.coalescer.schema(),
443                            ));
444                            self.coalescer.finish()?;
445                        }
446                    }
447                }
448                // Error case
449                other => return Poll::Ready(other),
450            }
451        }
452    }
453}
454
455impl RecordBatchStream for CoalesceBatchesStream {
456    fn schema(&self) -> SchemaRef {
457        self.coalescer.schema()
458    }
459}