datafusion_physical_plan/
coalesce_batches.rs1use 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#[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 input: Arc<dyn ExecutionPlan>,
71 target_batch_size: usize,
73 fetch: Option<usize>,
75 metrics: ExecutionPlanMetricsSet,
77 cache: Arc<PlanProperties>,
78}
79
80#[expect(deprecated)]
81impl CoalesceBatchesExec {
82 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 pub fn with_fetch(mut self, fetch: Option<usize>) -> Self {
96 self.fetch = fetch;
97 self
98 }
99
100 pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
102 &self.input
103 }
104
105 pub fn target_batch_size(&self) -> usize {
107 self.target_batch_size
108 }
109
110 fn compute_properties(input: &Arc<dyn ExecutionPlan>) -> PlanProperties {
112 PlanProperties::new(
115 input.equivalence_properties().clone(), input.output_partitioning().clone(), 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 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 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 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
376struct CoalesceBatchesStream {
378 input: SendableRecordBatchStream,
380 coalescer: LimitedBatchCoalescer,
382 baseline_metrics: BaselineMetrics,
384 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 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 let Some(batch) = self.coalescer.next_completed_batch() {
414 return Poll::Ready(Some(Ok(batch)));
415 }
416 if self.completed {
417 return Poll::Ready(None);
419 }
420 let input_batch = ready!(self.input.poll_next_unpin(cx));
422 let _timer = cloned_time.timer();
424
425 match input_batch {
426 None => {
427 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 }
438 PushBatchStatus::LimitReached => {
439 self.completed = true;
441 self.input = Box::pin(EmptyRecordBatchStream::new(
442 self.coalescer.schema(),
443 ));
444 self.coalescer.finish()?;
445 }
446 }
447 }
448 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}