Skip to main content

datafusion_physical_plan/repartition/
mod.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//! This file implements the [`RepartitionExec`] operator, which maps N input
19//! partitions to M output partitions based on a partitioning scheme, optionally
20//! maintaining the order of the input rows in the output.
21
22use std::cmp::Ordering;
23use std::fmt::{Debug, Display, Formatter};
24use std::pin::Pin;
25use std::sync::Arc;
26use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
27use std::task::{Context, Poll};
28use std::vec;
29
30use super::common::SharedMemoryReservation;
31use super::metrics::{self, ExecutionPlanMetricsSet, MetricBuilder, MetricsSet};
32use super::{
33    DisplayAs, ExecutionPlanProperties, RecordBatchStream, SendableRecordBatchStream,
34};
35use crate::coalesce::LimitedBatchCoalescer;
36use crate::execution_plan::{CardinalityEffect, EvaluationType, SchedulingType};
37use crate::hash_utils::create_hashes;
38use crate::metrics::{BaselineMetrics, SpillMetrics};
39use crate::projection::{ProjectionExec, all_columns, make_with_child, update_expr};
40use crate::sorts::streaming_merge::StreamingMergeBuilder;
41use crate::spill::spill_manager::SpillManager;
42use crate::spill::spill_pool::{self, SpillPoolSink, SpillPoolWriter};
43use crate::statistics::{ChildStats, StatisticsArgs};
44use crate::stream::{EmptyRecordBatchStream, RecordBatchStreamAdapter};
45use crate::{
46    ChildrenPropertiesMode, DisplayFormatType, ExecutionPlan, Partitioning,
47    PlanProperties, ReplaceChildrenOptions, Statistics, validate_child_count,
48};
49
50use arrow::array::{Array, PrimitiveArray, RecordBatch, RecordBatchOptions, UInt64Array};
51use arrow::compute::take_arrays;
52use arrow::datatypes::{DataType, Schema, SchemaRef, UInt32Type};
53use arrow_schema::SortOptions;
54use datafusion_common::config::ConfigOptions;
55use datafusion_common::stats::Precision;
56use datafusion_common::tree_node::TreeNodeRecursion;
57use datafusion_common::utils::{compare_rows, extract_row_at_idx_to_buf, transpose};
58use datafusion_common::{
59    ColumnStatistics, DataFusionError, HashMap, ScalarValue, SplitPoint,
60    assert_or_internal_err, internal_datafusion_err, internal_err,
61    validate_range_split_points,
62};
63use datafusion_common::{Result, not_impl_err};
64use datafusion_common_runtime::SpawnedTask;
65use datafusion_execution::TaskContext;
66use datafusion_execution::memory_pool::MemoryConsumer;
67use datafusion_expr::ColumnarValue;
68use datafusion_physical_expr::{EquivalenceProperties, PhysicalExpr, RangePartitioning};
69use datafusion_physical_expr_common::physical_expr::PhysicalExprRef;
70use datafusion_physical_expr_common::sort_expr::LexOrdering;
71#[cfg(feature = "proto")]
72use datafusion_physical_expr_common::sort_expr::{
73    sort_exprs_try_from_proto, sort_exprs_try_to_proto,
74};
75#[cfg(feature = "proto")]
76use datafusion_proto_models::protobuf;
77
78use crate::filter_pushdown::{
79    ChildPushdownResult, FilterDescription, FilterPushdownPhase,
80    FilterPushdownPropagation,
81};
82use crate::joins::SeededRandomState;
83use crate::sort_pushdown::SortOrderPushdownResult;
84use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
85use datafusion_physical_expr_common::utils::evaluate_expressions_to_arrays;
86use futures::stream::Stream;
87use futures::{FutureExt, StreamExt, TryStreamExt};
88use log::trace;
89use parking_lot::Mutex;
90
91mod distributor_channels;
92use crate::repartition::distributor_channels::SendError;
93use distributor_channels::{
94    DistributionReceiver, DistributionSender, channels, partition_aware_channels,
95};
96
97/// A batch in the repartition queue - either in memory or spilled to disk.
98///
99/// This enum represents the two states a batch can be in during repartitioning.
100/// The decision to spill is made based on memory availability when sending a batch
101/// to an output partition.
102///
103/// # Batch Flow with Spilling
104///
105/// ```text
106///                      Input Stream   ◀──────┐
107///                           │                │
108///                           ▼                │
109///                    Partition Logic         │
110///                           │           `batch_size` not
111///                           ▼            reached yet
112///                    Coalesce Batch          │
113///           ┌───────────────┴────────────────┘
114///           ▼
115/// `batch_size` reached
116///           │
117///           └───────────────┐
118///                           ▼
119///                        try_grow()
120///           ┌───────────────┴────────────────┐
121///           ▼                                ▼
122/// try_grow() succeeds              try_grow() fails
123/// (Memory Available)               (Memory Pressure)
124///           │                                │
125///           ▼                                ▼
126/// RepartitionBatch::Memory         spill_writer.push_batch()
127/// (batch held in memory)           (batch written to disk)
128///           │                                │
129///           │                                ▼
130///           │                      RepartitionBatch::Spilled
131///           │                      (marker - no batch data)
132///           └──────────────┬─────────────────┘
133///                          │
134///                          ▼
135///                   Send to channel
136///                          │
137///                          ▼
138///                 Output Stream (poll)
139///                          │
140///           ┌──────────────┴────────────────┐
141///           ▼                               ▼
142/// RepartitionBatch::Memory      RepartitionBatch::Spilled
143/// Return batch immediately       Poll spill_stream (blocks)
144///           └─────────────┬─────────────────┘
145///                         │
146///                         ▼
147///                    Return batch
148///               (FIFO order preserved)
149/// ```
150///
151/// See [`RepartitionExec`] for overall architecture and [`StreamState`] for
152/// the state machine that handles reading these batches.
153#[derive(Debug)]
154enum RepartitionBatch {
155    /// Batch held in memory (counts against memory reservation)
156    Memory(RecordBatch),
157    /// Marker indicating a batch was spilled to the partition's SpillPool.
158    /// The actual batch can be retrieved by reading from the SpillPoolStream.
159    /// This variant contains no data itself - it's just a signal to the reader
160    /// to fetch the next batch from the spill stream.
161    Spilled,
162}
163
164type MaybeBatch = Option<Result<RepartitionBatch>>;
165type InputPartitionsToCurrentPartitionSender = Vec<DistributionSender<MaybeBatch>>;
166type InputPartitionsToCurrentPartitionReceiver = Vec<DistributionReceiver<MaybeBatch>>;
167
168/// Output channel with its associated memory reservation and spill writer.
169///
170/// `coalescer` is `None` for preserve-order mode, where downstream
171/// [`StreamingMergeBuilder`] performs the batching; otherwise it's a
172/// [`SharedCoalescer`] cloned from the per-partition one held by
173/// [`PartitionChannels`].
174struct OutputChannel {
175    sender: DistributionSender<MaybeBatch>,
176    reservation: SharedMemoryReservation,
177    spill_writer: SpillPoolSink,
178    shared_coalescer: Option<SharedCoalescer>,
179}
180
181/// The set of spill-pool writers for a single output partition, before they are handed to the
182/// per-input tasks. The variant encodes the repartition mode so the wrong writer topology cannot
183/// be constructed for a given mode.
184enum PartitionSpillWriters {
185    /// `preserve_order`: one single-producer FIFO writer per input partition. Each is `take`n
186    /// exactly once (moved into the matching input task), so the pool always has one writer.
187    PerInput(Vec<Option<SpillPoolSink>>),
188    /// Non-preserve-order: one shared writer, cloned into every input task.
189    Shared(SpillPoolWriter),
190}
191
192impl PartitionSpillWriters {
193    /// Hand out the writer for input partition `input`.
194    ///
195    /// In `PerInput` mode this moves the dedicated writer out (it must only be requested once per
196    /// input); in `Shared` mode it clones the shared writer.
197    fn take_for_input(&mut self, input: usize) -> Result<SpillPoolSink> {
198        match self {
199            PartitionSpillWriters::PerInput(writers) => {
200                writers[input].take().ok_or_else(|| {
201                    internal_datafusion_err!(
202                        "spill writer for input partition requested more than once"
203                    )
204                })
205            }
206            PartitionSpillWriters::Shared(writer) => Ok(writer.new_sink()),
207        }
208    }
209}
210
211impl OutputChannel {
212    fn coalesce(&mut self, batch: RecordBatch) -> Result<Vec<RecordBatch>> {
213        match &self.shared_coalescer {
214            Some(shared) => Ok(shared.push_and_drain(batch)?),
215            None => Ok(vec![batch]),
216        }
217    }
218
219    /// Send a single batch through the channel for `partition`, applying
220    /// the memory reservation / spill-writer fallback. Removes the channel
221    /// from `self.inner` if the receiver has hung up.
222    ///
223    /// Used after [`OutputChannel::coalesce`] for performance purposes.
224    async fn send(&mut self, batch: RecordBatch) -> Result<(), SendError<MaybeBatch>> {
225        let size = batch.get_array_memory_size();
226
227        // Decide the payload outside of any await: never hold a MutexGuard
228        // across an await point.
229        let (payload, is_memory_batch) = {
230            match self.reservation.try_grow(size) {
231                Ok(_) => (Ok(RepartitionBatch::Memory(batch)), true),
232                Err(_) => match self.spill_writer.push_batch(&batch) {
233                    Ok(()) => (Ok(RepartitionBatch::Spilled), false),
234                    Err(err) => (Err(err), false),
235                },
236            }
237        };
238
239        let result = self.sender.send(Some(payload)).await;
240        if result.is_err() && is_memory_batch {
241            self.reservation.shrink(size);
242        }
243        result
244    }
245
246    async fn finalize(mut self) -> Result<()> {
247        let Some(shared) = self.shared_coalescer.take() else {
248            return Ok(());
249        };
250        for batch in shared.finalize()? {
251            // If this errored, it means that nobody is listening on the other side, which is fine
252            // and can happen in certain cases, like when a LIMIT drops the stream that listens.
253            let _ = self.send(batch).await;
254        }
255        Ok(())
256    }
257}
258
259/// A producer-side coalescer shared across all input tasks targeting a
260/// single output partition.
261///
262/// Bundles the [`LimitedBatchCoalescer`] (behind a [`Mutex`]) with the
263/// active-sender counter that tracks how many input tasks may still push
264/// into it. The last task to call [`Self::finalize`] is the one that
265/// finalizes the coalescer and ships the residual batch.
266///
267/// Cheap to [`Clone`]: both fields are [`Arc`]s.
268#[derive(Clone)]
269struct SharedCoalescer {
270    inner: Arc<Mutex<LimitedBatchCoalescer>>,
271    active_senders: Arc<AtomicUsize>,
272}
273
274impl SharedCoalescer {
275    fn new(schema: SchemaRef, target_batch_size: usize, num_senders: usize) -> Self {
276        Self {
277            inner: Arc::new(Mutex::new(LimitedBatchCoalescer::new(
278                schema,
279                target_batch_size,
280                None,
281            ))),
282            active_senders: Arc::new(AtomicUsize::new(num_senders)),
283        }
284    }
285
286    /// Push `batch` into the coalescer and drain any newly completed
287    /// batches. The mutex is held only briefly.
288    fn push_and_drain(&self, batch: RecordBatch) -> Result<Vec<RecordBatch>> {
289        let mut acc = Vec::new();
290        let mut c = self.inner.lock();
291        c.push_batch(batch)?;
292        while let Some(b) = c.next_completed_batch() {
293            acc.push(b);
294        }
295        Ok(acc)
296    }
297
298    /// Decrement the active-senders counter. If this caller was the last
299    /// sender, finalize the coalescer and return its residual batches; if
300    /// other senders are still active, return `Ok(None)`.
301    fn finalize(&self) -> Result<Vec<RecordBatch>> {
302        let was_last = self.active_senders.fetch_sub(1, AtomicOrdering::AcqRel) == 1;
303        if !was_last {
304            return Ok(vec![]);
305        }
306        let mut acc = Vec::new();
307        let mut c = self.inner.lock();
308        c.finish()?;
309        while let Some(b) = c.next_completed_batch() {
310            acc.push(b);
311        }
312        Ok(acc)
313    }
314}
315
316/// Channels and resources for a single output partition.
317///
318/// Each output partition has channels to receive data from all input partitions.
319/// To handle memory pressure, each (input, output) pair gets its own
320/// [`SpillPool`](crate::spill::spill_pool) channel via [`spill_pool::channel`].
321///
322/// # Structure
323///
324/// For an output partition receiving from N input partitions:
325/// - `tx`: N senders (one per input) for sending batches to this output
326/// - `rx`: N receivers (one per input) for receiving batches at this output
327/// - `spill_writers`: N spill writers (one per input) for writing spilled data
328/// - `spill_readers`: N spill readers (one per input) for reading spilled data
329///
330/// This 1:1 mapping between input partitions and spill channels ensures that
331/// batches from each input are processed in FIFO order, even when some batches
332/// are spilled to disk and others remain in memory.
333///
334/// See [`RepartitionExec`] for the overall N×M architecture.
335///
336/// [`spill_pool::channel`]: crate::spill::spill_pool::spsc_channel
337struct PartitionChannels {
338    /// Senders for each input partition to send data to this output partition
339    tx: InputPartitionsToCurrentPartitionSender,
340    /// Receivers for each input partition sending data to this output partition
341    rx: InputPartitionsToCurrentPartitionReceiver,
342    /// Memory reservation for this output partition
343    reservation: SharedMemoryReservation,
344    /// Shared coalescer used by all input tasks targeting this output
345    /// partition. `None` in preserve-order mode (downstream
346    /// `StreamingMergeBuilder` handles batching).
347    shared_coalescer: Option<SharedCoalescer>,
348    /// Spill writers for writing spilled data, before they are handed to the per-input tasks.
349    /// The variant is chosen by the repartition mode (see [`PartitionSpillWriters`]): a dedicated
350    /// single-producer FIFO writer per input in preserve-order mode, or one shared writer in
351    /// non-preserve-order mode.
352    spill_writers: PartitionSpillWriters,
353    /// Spill readers for reading spilled data - one per input partition (FIFO semantics).
354    /// Each (input, output) pair gets its own reader to maintain proper ordering.
355    spill_readers: Vec<SendableRecordBatchStream>,
356}
357
358struct ConsumingInputStreamsState {
359    /// Channels for sending batches from input partitions to output partitions.
360    /// Key is the partition number.
361    channels: HashMap<usize, PartitionChannels>,
362
363    /// Helper that ensures that background jobs are killed once they are no longer needed.
364    abort_helper: Arc<Vec<SpawnedTask<()>>>,
365}
366
367impl Debug for ConsumingInputStreamsState {
368    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
369        f.debug_struct("ConsumingInputStreamsState")
370            .field("num_channels", &self.channels.len())
371            .field("abort_helper", &self.abort_helper)
372            .finish()
373    }
374}
375
376/// Inner state of [`RepartitionExec`].
377#[derive(Default)]
378enum RepartitionExecState {
379    /// Not initialized yet. This is the default state stored in the RepartitionExec node
380    /// upon instantiation.
381    #[default]
382    NotInitialized,
383    /// Input streams are initialized, but they are still not being consumed. The node
384    /// transitions to this state when the arrow's RecordBatch stream is created in
385    /// RepartitionExec::execute(), but before any message is polled.
386    InputStreamsInitialized(Vec<(SendableRecordBatchStream, RepartitionMetrics)>),
387    /// The input streams are being consumed. The node transitions to this state when
388    /// the first message in the arrow's RecordBatch stream is consumed.
389    ConsumingInputStreams(ConsumingInputStreamsState),
390}
391
392impl Debug for RepartitionExecState {
393    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
394        match self {
395            RepartitionExecState::NotInitialized => write!(f, "NotInitialized"),
396            RepartitionExecState::InputStreamsInitialized(v) => {
397                write!(f, "InputStreamsInitialized({:?})", v.len())
398            }
399            RepartitionExecState::ConsumingInputStreams(v) => {
400                write!(f, "ConsumingInputStreams({v:?})")
401            }
402        }
403    }
404}
405
406impl RepartitionExecState {
407    fn ensure_input_streams_initialized(
408        &mut self,
409        input: &Arc<dyn ExecutionPlan>,
410        metrics: &ExecutionPlanMetricsSet,
411        output_partitions: usize,
412        ctx: &Arc<TaskContext>,
413    ) -> Result<()> {
414        if !matches!(self, RepartitionExecState::NotInitialized) {
415            return Ok(());
416        }
417
418        let num_input_partitions = input.output_partitioning().partition_count();
419        let mut streams_and_metrics = Vec::with_capacity(num_input_partitions);
420
421        for i in 0..num_input_partitions {
422            let metrics = RepartitionMetrics::new(i, output_partitions, metrics);
423
424            let timer = metrics.fetch_time.timer();
425            let stream = input.execute(i, Arc::clone(ctx))?;
426            timer.done();
427
428            streams_and_metrics.push((stream, metrics));
429        }
430        *self = RepartitionExecState::InputStreamsInitialized(streams_and_metrics);
431        Ok(())
432    }
433
434    #[expect(clippy::too_many_arguments)]
435    fn consume_input_streams(
436        &mut self,
437        input: &Arc<dyn ExecutionPlan>,
438        metrics: &ExecutionPlanMetricsSet,
439        partitioning: &Partitioning,
440        preserve_order: bool,
441        name: &str,
442        context: &Arc<TaskContext>,
443        spill_manager: SpillManager,
444    ) -> Result<&mut ConsumingInputStreamsState> {
445        let streams_and_metrics = match self {
446            RepartitionExecState::NotInitialized => {
447                self.ensure_input_streams_initialized(
448                    input,
449                    metrics,
450                    partitioning.partition_count(),
451                    context,
452                )?;
453                let RepartitionExecState::InputStreamsInitialized(value) = self else {
454                    // This cannot happen, as ensure_input_streams_initialized() was just called,
455                    // but the compiler does not know.
456                    return internal_err!(
457                        "Programming error: RepartitionExecState must be in the InputStreamsInitialized state after calling RepartitionExecState::ensure_input_streams_initialized"
458                    );
459                };
460                value
461            }
462            RepartitionExecState::ConsumingInputStreams(value) => return Ok(value),
463            RepartitionExecState::InputStreamsInitialized(value) => value,
464        };
465
466        let num_input_partitions = streams_and_metrics.len();
467        let num_output_partitions = partitioning.partition_count();
468        let coalesce_batches = !preserve_order && !input.boundedness().is_unbounded();
469
470        let spill_manager = Arc::new(spill_manager);
471
472        let (txs, rxs) = if preserve_order {
473            // Create partition-aware channels with one channel per (input, output) pair
474            // This provides backpressure while maintaining proper ordering
475            let (txs_all, rxs_all) =
476                partition_aware_channels(num_input_partitions, num_output_partitions);
477            // Take transpose of senders and receivers. `state.channels` keeps track of entries per output partition
478            let txs = transpose(txs_all);
479            let rxs = transpose(rxs_all);
480            (txs, rxs)
481        } else {
482            // Create one channel per *output* partition with backpressure
483            let (txs, rxs) = channels(num_output_partitions);
484            // Clone sender for each input partitions
485            let txs = txs
486                .into_iter()
487                .map(|item| vec![item; num_input_partitions])
488                .collect::<Vec<_>>();
489            let rxs = rxs.into_iter().map(|item| vec![item]).collect::<Vec<_>>();
490            (txs, rxs)
491        };
492
493        let mut channels = HashMap::with_capacity(txs.len());
494        for (partition, (tx, rx)) in txs.into_iter().zip(rxs).enumerate() {
495            let reservation = Arc::new(
496                MemoryConsumer::new(format!("{name}[{partition}]"))
497                    .with_can_spill(true)
498                    .register(context.memory_pool()),
499            );
500
501            // Create spill channels based on mode:
502            // - preserve_order: one spill channel per (input, output) pair for proper FIFO ordering
503            // - non-preserve-order: one shared spill channel per output partition since all inputs
504            //   share the same receiver
505            let max_file_size = context
506                .session_config()
507                .options()
508                .execution
509                .max_spill_file_size_bytes
510                .get();
511
512            let (spill_writers, spill_readers) = if preserve_order {
513                // preserve_order: one dedicated single-producer FIFO pool per input partition.
514                // Each writer is moved into exactly one input task (never cloned), so the ordering
515                // the downstream merge relies on is preserved across the spill boundary.
516                let mut writers = Vec::with_capacity(num_input_partitions);
517                let mut readers = Vec::with_capacity(num_input_partitions);
518                for _ in 0..num_input_partitions {
519                    let (writer, reader) = spill_pool::spsc_channel(
520                        max_file_size,
521                        Arc::clone(&spill_manager),
522                    );
523                    writers.push(Some(writer));
524                    readers.push(reader);
525                }
526                (PartitionSpillWriters::PerInput(writers), readers)
527            } else {
528                // non-preserve-order: one shared multi-producer pool per output partition, since
529                // all inputs share the same receiver and the output is an unordered multiset.
530                let (writer, reader) =
531                    spill_pool::mpsc_channel(max_file_size, Arc::clone(&spill_manager));
532                (PartitionSpillWriters::Shared(writer), vec![reader])
533            };
534
535            // Coalesce on the producer side, before the channel's gate, so
536            // the consumer never sees the per-input-task small batches.
537            // Skip in preserve-order mode, where `StreamingMergeBuilder`
538            // handles batching, and for unbounded inputs, where a residual
539            // batch could otherwise be withheld indefinitely.
540            let shared_coalescer = coalesce_batches.then(|| {
541                SharedCoalescer::new(
542                    input.schema(),
543                    context.session_config().batch_size(),
544                    num_input_partitions,
545                )
546            });
547
548            channels.insert(
549                partition,
550                PartitionChannels {
551                    tx,
552                    rx,
553                    reservation,
554                    spill_readers,
555                    spill_writers,
556                    shared_coalescer,
557                },
558            );
559        }
560
561        // launch one async task per *input* partition
562        let mut spawned_tasks = Vec::with_capacity(num_input_partitions);
563        for (i, (stream, metrics)) in
564            std::mem::take(streams_and_metrics).into_iter().enumerate()
565        {
566            let txs: HashMap<_, _> = channels
567                .iter_mut()
568                .map(|(partition, channels)| {
569                    // Hand this input task its spill writer: in preserve_order mode this moves
570                    // the input's dedicated FIFO writer out; otherwise it clones the shared
571                    // writer. See [`PartitionSpillWriters::take_for_input`].
572                    Ok((
573                        *partition,
574                        OutputChannel {
575                            sender: channels.tx[i].clone(),
576                            reservation: Arc::clone(&channels.reservation),
577                            spill_writer: channels.spill_writers.take_for_input(i)?,
578                            shared_coalescer: channels.shared_coalescer.clone(),
579                        },
580                    ))
581                })
582                .collect::<Result<HashMap<_, _>>>()?;
583
584            // Extract senders for wait_for_task before moving txs
585            let senders: HashMap<_, _> = txs
586                .iter()
587                .map(|(partition, channel)| (*partition, channel.sender.clone()))
588                .collect();
589
590            let input_task = SpawnedTask::spawn(RepartitionExec::pull_from_input(
591                stream,
592                txs,
593                partitioning.clone(),
594                metrics,
595                // preserve_order depends on partition index to start from 0
596                if preserve_order { 0 } else { i },
597                num_input_partitions,
598            ));
599
600            // In a separate task, wait for each input to be done
601            // (and pass along any errors, including panic!s)
602            let wait_for_task =
603                SpawnedTask::spawn(RepartitionExec::wait_for_task(input_task, senders));
604            spawned_tasks.push(wait_for_task);
605        }
606        *self = Self::ConsumingInputStreams(ConsumingInputStreamsState {
607            channels,
608            abort_helper: Arc::new(spawned_tasks),
609        });
610        match self {
611            RepartitionExecState::ConsumingInputStreams(value) => Ok(value),
612            _ => unreachable!(),
613        }
614    }
615}
616
617/// A utility that can be used to partition batches based on [`Partitioning`]
618pub struct BatchPartitioner {
619    state: BatchPartitionerState,
620    timer: metrics::Time,
621}
622
623enum BatchPartitionerState {
624    Hash {
625        exprs: Vec<Arc<dyn PhysicalExpr>>,
626        partition_reducer: StrengthReducedU64,
627        hash_buffer: Vec<u64>,
628        indices: Vec<Vec<u32>>,
629    },
630    RoundRobin {
631        num_partitions: usize,
632        next_idx: usize,
633    },
634    Range {
635        /// Ordered partitioning key.
636        ordering: LexOrdering,
637        /// Sort options from the `LexOrdering`
638        sort_options: Vec<SortOptions>,
639        /// Boundaries between adjacent partitions.
640        split_points: Vec<SplitPoint>,
641        /// Row indices grouped by output partition
642        indices: Vec<Vec<u32>>,
643        /// Buffer of `ScalarValue` used to represent the values for a row - based on the `LexOrdering` ordering - to compare against split points
644        partition_buffer: Vec<ScalarValue>,
645    },
646}
647
648/// Fixed RandomState used for hash repartitioning to ensure consistent behavior across
649/// executions and runs.
650pub const REPARTITION_RANDOM_STATE: SeededRandomState = SeededRandomState::with_seed(0);
651
652/// Physical expression that returns the Range partition for each input row.
653///
654/// This uses the same routing function as [`BatchPartitioner`], so dynamic
655/// filtering and repartitioning agree for every [`ScalarValue`] comparison.
656#[derive(Debug, Hash, PartialEq, Eq)]
657pub struct RangeExpr {
658    on_columns: Vec<PhysicalExprRef>,
659    split_points: Vec<SplitPoint>,
660    sort_options: Vec<SortOptions>,
661}
662
663impl RangeExpr {
664    /// Creates a Range expression for `on_columns` using the supplied routing
665    /// metadata.
666    pub fn try_new(
667        on_columns: Vec<PhysicalExprRef>,
668        range_partitioning: &RangePartitioning,
669    ) -> Result<Self> {
670        let sort_options = range_partitioning
671            .ordering()
672            .iter()
673            .map(|expr| expr.options)
674            .collect();
675        Self::try_new_parts(
676            on_columns,
677            range_partitioning.split_points().to_vec(),
678            sort_options,
679        )
680    }
681
682    fn try_new_parts(
683        on_columns: Vec<PhysicalExprRef>,
684        split_points: Vec<SplitPoint>,
685        sort_options: Vec<SortOptions>,
686    ) -> Result<Self> {
687        assert_or_internal_err!(!on_columns.is_empty(), "RangeExpr requires a key");
688        assert_or_internal_err!(
689            on_columns.len() == sort_options.len(),
690            "RangeExpr key count must match sort options"
691        );
692        validate_range_split_points(&split_points, &sort_options)?;
693        Ok(Self {
694            on_columns,
695            split_points,
696            sort_options,
697        })
698    }
699
700    /// Get the columns used to compute Range partition IDs.
701    pub fn on_columns(&self) -> &[PhysicalExprRef] {
702        &self.on_columns
703    }
704
705    /// Returns the Range split points used for routing.
706    pub fn split_points(&self) -> &[SplitPoint] {
707        &self.split_points
708    }
709
710    /// Returns the per-key sort options used for routing.
711    pub fn sort_options(&self) -> &[SortOptions] {
712        &self.sort_options
713    }
714}
715
716impl Display for RangeExpr {
717    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
718        write!(f, "range_partition")
719    }
720}
721
722impl PhysicalExpr for RangeExpr {
723    fn children(&self) -> Vec<&PhysicalExprRef> {
724        self.on_columns.iter().collect()
725    }
726
727    fn with_new_children(
728        self: Arc<Self>,
729        children: Vec<PhysicalExprRef>,
730    ) -> Result<PhysicalExprRef> {
731        assert_or_internal_err!(
732            children.len() == self.on_columns.len(),
733            "RangeExpr expected {} children, got {}",
734            self.on_columns.len(),
735            children.len()
736        );
737        Ok(Arc::new(Self::try_new_parts(
738            children,
739            self.split_points.clone(),
740            self.sort_options.clone(),
741        )?))
742    }
743
744    fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
745        Ok(DataType::UInt64)
746    }
747
748    fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
749        Ok(false)
750    }
751
752    fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
753        let arrays = evaluate_expressions_to_arrays(self.on_columns.iter(), batch)?;
754        let mut row_key_buffer = Vec::with_capacity(arrays.len());
755        let mut partition_ids = Vec::with_capacity(batch.num_rows());
756        for row_idx in 0..batch.num_rows() {
757            extract_row_at_idx_to_buf(&arrays, row_idx, &mut row_key_buffer)?;
758            partition_ids.push(range_partition_id(
759                &row_key_buffer,
760                &self.split_points,
761                &self.sort_options,
762            )? as u64);
763        }
764        Ok(ColumnarValue::Array(Arc::new(UInt64Array::from(
765            partition_ids,
766        ))))
767    }
768
769    fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
770        write!(f, "range_partition")
771    }
772
773    #[cfg(feature = "proto")]
774    fn try_to_proto(
775        &self,
776        ctx: &datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx<'_>,
777    ) -> Result<Option<protobuf::PhysicalExprNode>> {
778        // Encode the raw ordered children: rebuilding a `LexOrdering` would
779        // deduplicate equivalent children after dynamic-filter remapping.
780        let sort_exprs = self
781            .on_columns
782            .iter()
783            .zip(&self.sort_options)
784            .map(|(expr, options)| PhysicalSortExpr::new(Arc::clone(expr), *options))
785            .collect::<Vec<_>>();
786        let sort_expr = sort_exprs_try_to_proto(&sort_exprs, ctx)?;
787        let split_point = self
788            .split_points
789            .iter()
790            .map(|split_point| {
791                let value = split_point
792                    .values()
793                    .iter()
794                    .map(|value| value.try_into().map_err(Into::into))
795                    .collect::<Result<Vec<_>>>()?;
796                Ok(protobuf::PhysicalRangeSplitPoint { value })
797            })
798            .collect::<Result<Vec<_>>>()?;
799        Ok(Some(protobuf::PhysicalExprNode {
800            expr_id: None,
801            expr_type: Some(protobuf::physical_expr_node::ExprType::RangeExpr(
802                protobuf::PhysicalRangeExprNode {
803                    sort_expr,
804                    split_point,
805                },
806            )),
807        }))
808    }
809}
810
811#[cfg(feature = "proto")]
812impl RangeExpr {
813    /// Reconstructs a [`RangeExpr`] from its protobuf representation.
814    pub fn try_from_proto(
815        node: &protobuf::PhysicalExprNode,
816        ctx: &datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx<'_>,
817    ) -> Result<PhysicalExprRef> {
818        // Decode the raw ordered children for the same reason as `try_to_proto`.
819        let range_expr = match &node.expr_type {
820            Some(protobuf::physical_expr_node::ExprType::RangeExpr(expr)) => expr,
821            _ => return internal_err!("PhysicalExprNode is not a RangeExpr"),
822        };
823        let sort_exprs = sort_exprs_try_from_proto(&range_expr.sort_expr, ctx)?;
824        let (on_columns, sort_options) = sort_exprs
825            .into_iter()
826            .map(|sort_expr| (sort_expr.expr, sort_expr.options))
827            .unzip();
828        let split_points = range_expr
829            .split_point
830            .iter()
831            .map(|split_point| {
832                let values = split_point
833                    .value
834                    .iter()
835                    .map(|value| ScalarValue::try_from(value).map_err(Into::into))
836                    .collect::<Result<Vec<_>>>()?;
837                Ok(SplitPoint::new(values))
838            })
839            .collect::<Result<Vec<_>>>()?;
840        Ok(Arc::new(Self::try_new_parts(
841            on_columns,
842            split_points,
843            sort_options,
844        )?))
845    }
846}
847
848fn range_partition_id(
849    row_key: &[ScalarValue],
850    split_points: &[SplitPoint],
851    sort_options: &[SortOptions],
852) -> Result<usize> {
853    let mut low = 0;
854    let mut high = split_points.len();
855    while low < high {
856        let mid = low + (high - low) / 2;
857        match compare_rows(row_key, split_points[mid].values(), sort_options)? {
858            Ordering::Less => high = mid,
859            Ordering::Equal | Ordering::Greater => low = mid + 1,
860        }
861    }
862    Ok(low)
863}
864
865/// Computes `value % divisor` without division in the hot loop when `divisor`
866/// is fixed for many values.
867///
868/// Hash repartitioning computes a remainder for every row. Integer division is
869/// relatively expensive, so this precomputes the strength-reduced form of the
870/// divisor: powers of two use a bit mask, and other divisors use a reciprocal
871/// multiply to recover the quotient and therefore the remainder. This is the
872/// same invariant-divisor optimization compilers use for `%` by a constant.
873#[derive(Debug, Clone, Copy)]
874enum StrengthReducedU64 {
875    PowerOfTwo { mask: u64 },
876    Reciprocal { divisor: u64, reciprocal: u128 },
877}
878
879impl StrengthReducedU64 {
880    fn new(divisor: u64) -> Self {
881        debug_assert!(divisor > 0);
882
883        if divisor.is_power_of_two() {
884            Self::PowerOfTwo { mask: divisor - 1 }
885        } else {
886            Self::Reciprocal {
887                divisor,
888                // ceil(2^128 / divisor), computed without representing 2^128
889                reciprocal: u128::MAX / u128::from(divisor) + 1,
890            }
891        }
892    }
893
894    fn partition_indices(self, hash_buffer: &[u64], indices: &mut [Vec<u32>]) {
895        match self {
896            Self::PowerOfTwo { mask } => {
897                for (index, hash) in hash_buffer.iter().enumerate() {
898                    indices[(*hash & mask) as usize].push(index as u32);
899                }
900            }
901            Self::Reciprocal {
902                divisor,
903                reciprocal,
904            } => {
905                for (index, hash) in hash_buffer.iter().enumerate() {
906                    let quotient = Self::quotient(*hash, reciprocal);
907                    let partition = *hash - quotient * divisor;
908                    indices[partition as usize].push(index as u32);
909                }
910            }
911        }
912    }
913
914    #[cfg(test)]
915    fn remainder(self, value: u64) -> u64 {
916        match self {
917            Self::PowerOfTwo { mask } => value & mask,
918            Self::Reciprocal {
919                divisor,
920                reciprocal,
921            } => value - Self::quotient(value, reciprocal) * divisor,
922        }
923    }
924
925    #[inline]
926    fn quotient(value: u64, reciprocal: u128) -> u64 {
927        let reciprocal_low = reciprocal as u64;
928        let reciprocal_high = (reciprocal >> 64) as u64;
929        let low_product = u128::from(value) * u128::from(reciprocal_low);
930        let high_product = u128::from(value) * u128::from(reciprocal_high);
931        let carry = ((high_product & u128::from(u64::MAX)) + (low_product >> 64)) >> 64;
932
933        ((high_product >> 64) + carry) as u64
934    }
935}
936
937impl BatchPartitioner {
938    /// Create a new [`BatchPartitioner`] for hash-based repartitioning.
939    ///
940    /// # Parameters
941    /// - `exprs`: Expressions used to compute the hash for each input row.
942    /// - `num_partitions`: Total number of output partitions.
943    /// - `timer`: Metric used to record time spent during repartitioning.
944    ///
945    /// The partition count is fixed for the lifetime of the partitioner, so this
946    /// precomputes a strength-reduced reducer for `hash % num_partitions`.
947    ///
948    /// # Errors
949    /// Returns an error if `num_partitions` is zero.
950    pub fn new_hash_partitioner(
951        exprs: Vec<Arc<dyn PhysicalExpr>>,
952        num_partitions: usize,
953        timer: metrics::Time,
954    ) -> Result<Self> {
955        if num_partitions == 0 {
956            return internal_err!("Hash repartition requires at least one partition");
957        }
958
959        Ok(Self {
960            state: BatchPartitionerState::Hash {
961                exprs,
962                partition_reducer: StrengthReducedU64::new(num_partitions as u64),
963                hash_buffer: vec![],
964                indices: vec![vec![]; num_partitions],
965            },
966            timer,
967        })
968    }
969
970    /// Create a new [`BatchPartitioner`] for round-robin repartitioning.
971    ///
972    /// # Parameters
973    /// - `num_partitions`: Total number of output partitions.
974    /// - `timer`: Metric used to record time spent during repartitioning.
975    /// - `input_partition`: Index of the current input partition.
976    /// - `num_input_partitions`: Total number of input partitions.
977    ///
978    /// # Notes
979    /// The starting output partition is derived from the input partition
980    /// to avoid skew when multiple input partitions are used.
981    pub fn new_round_robin_partitioner(
982        num_partitions: usize,
983        timer: metrics::Time,
984        input_partition: usize,
985        num_input_partitions: usize,
986    ) -> Self {
987        Self {
988            state: BatchPartitionerState::RoundRobin {
989                num_partitions,
990                next_idx: (input_partition * num_partitions) / num_input_partitions,
991            },
992            timer,
993        }
994    }
995
996    /// Create a new [`BatchPartitioner`] for range-based repartitioning.
997    ///
998    /// # Parameters
999    /// - `range_partitioning`: `RangePartitioning` struct used for ordering, split points, and number of partitions
1000    /// - `timer`: Metric used to record time spent during repartitioning.
1001    pub fn new_range_partitioner(
1002        range_partitioning: &RangePartitioning,
1003        timer: metrics::Time,
1004    ) -> Self {
1005        let ordering = range_partitioning.ordering().clone();
1006        let split_points = range_partitioning.split_points().to_vec();
1007        let num_partitions = range_partitioning.partition_count();
1008        let sort_options: Vec<SortOptions> = ordering.iter().map(|e| e.options).collect();
1009
1010        Self {
1011            state: BatchPartitionerState::Range {
1012                partition_buffer: Vec::with_capacity(ordering.len()),
1013                ordering,
1014                sort_options,
1015                split_points,
1016                indices: vec![vec![]; num_partitions],
1017            },
1018            timer,
1019        }
1020    }
1021
1022    /// Create a new [`BatchPartitioner`] based on the provided [`Partitioning`] scheme.
1023    ///
1024    /// This is a convenience constructor that delegates to the specialized
1025    /// hash, round-robin, or range constructors depending on the partitioning variant.
1026    ///
1027    /// # Parameters
1028    /// - `partitioning`: Partitioning scheme to apply (hash, round-robin, or range).
1029    /// - `timer`: Metric used to record time spent during repartitioning.
1030    /// - `input_partition`: Index of the current input partition.
1031    /// - `num_input_partitions`: Total number of input partitions.
1032    ///
1033    /// # Errors
1034    /// Returns an error if the provided partitioning scheme is not supported,
1035    /// or if hash partitioning is requested with zero output partitions.
1036    pub fn try_new(
1037        partitioning: Partitioning,
1038        timer: metrics::Time,
1039        input_partition: usize,
1040        num_input_partitions: usize,
1041    ) -> Result<Self> {
1042        match partitioning {
1043            Partitioning::Hash(exprs, num_partitions) => {
1044                Self::new_hash_partitioner(exprs, num_partitions, timer)
1045            }
1046            Partitioning::RoundRobinBatch(num_partitions) => {
1047                Ok(Self::new_round_robin_partitioner(
1048                    num_partitions,
1049                    timer,
1050                    input_partition,
1051                    num_input_partitions,
1052                ))
1053            }
1054            Partitioning::Range(range_repartitioning) => {
1055                Ok(Self::new_range_partitioner(&range_repartitioning, timer))
1056            }
1057            other => {
1058                not_impl_err!("Unsupported repartitioning scheme {other:?}")
1059            }
1060        }
1061    }
1062
1063    /// Partition the provided [`RecordBatch`] into one or more partitioned [`RecordBatch`]
1064    /// based on the [`Partitioning`] specified on construction
1065    ///
1066    /// `f` will be called for each partitioned [`RecordBatch`] with the corresponding
1067    /// partition index. Any error returned by `f` will be immediately returned by this
1068    /// function without attempting to publish further [`RecordBatch`]
1069    ///
1070    /// The time spent repartitioning, not including time spent in `f` will be recorded
1071    /// to the [`metrics::Time`] provided on construction
1072    pub fn partition<F>(&mut self, batch: RecordBatch, mut f: F) -> Result<()>
1073    where
1074        F: FnMut(usize, RecordBatch) -> Result<()>,
1075    {
1076        self.partition_iter(batch)?.try_for_each(|res| match res {
1077            Ok((partition, batch)) => f(partition, batch),
1078            Err(e) => Err(e),
1079        })
1080    }
1081
1082    /// Returns an iterator of `(partition_index, RecordBatch)` pairs for the given batch.
1083    ///
1084    /// This is useful for async consumers that want to separate CPU-bound partitioning
1085    /// from I/O. For example, you can iterate results on the async side and send them
1086    /// through a channel, while performing file I/O on a blocking task:
1087    ///
1088    /// ```ignore
1089    /// for result in partitioner.partition_iter(batch)? {
1090    ///     let (partition, batch) = result?;
1091    ///     tx.send((partition, batch)).await?;
1092    /// }
1093    /// ```
1094    ///
1095    /// The sync [`partition`](Self::partition) method is implemented on top of this.
1096    pub fn partition_iter(
1097        &mut self,
1098        batch: RecordBatch,
1099    ) -> Result<impl Iterator<Item = Result<(usize, RecordBatch)>> + Send + '_> {
1100        let it: Box<dyn Iterator<Item = Result<(usize, RecordBatch)>> + Send> =
1101            match &mut self.state {
1102                BatchPartitionerState::RoundRobin {
1103                    num_partitions,
1104                    next_idx,
1105                } => {
1106                    let idx = *next_idx;
1107                    *next_idx = (*next_idx + 1) % *num_partitions;
1108                    Box::new(std::iter::once(Ok((idx, batch))))
1109                }
1110                BatchPartitionerState::Hash {
1111                    exprs,
1112                    partition_reducer,
1113                    hash_buffer,
1114                    indices,
1115                } => {
1116                    // Tracking time required for distributing indexes across output partitions
1117                    let timer = self.timer.timer();
1118
1119                    let arrays =
1120                        evaluate_expressions_to_arrays(exprs.as_slice(), &batch)?;
1121
1122                    hash_buffer.clear();
1123                    hash_buffer.resize(batch.num_rows(), 0);
1124
1125                    create_hashes(
1126                        &arrays,
1127                        REPARTITION_RANDOM_STATE.random_state(),
1128                        hash_buffer,
1129                    )?;
1130
1131                    indices.iter_mut().for_each(|v| v.clear());
1132
1133                    partition_reducer.partition_indices(hash_buffer, indices);
1134
1135                    // Finished building index-arrays for output partitions
1136                    timer.done();
1137
1138                    let partitioned_batches =
1139                        Self::partition_grouped_take(&batch, indices, &self.timer)?;
1140
1141                    Box::new(partitioned_batches.into_iter())
1142                }
1143                BatchPartitionerState::Range {
1144                    ordering,
1145                    sort_options,
1146                    split_points,
1147                    indices,
1148                    partition_buffer,
1149                } => {
1150                    // Tracking time required for distributing indexes across output partitions
1151                    let timer = self.timer.timer();
1152                    if split_points.is_empty() {
1153                        timer.done();
1154                        Box::new(std::iter::once(Ok((0, batch))))
1155                    } else {
1156                        let arrays = evaluate_expressions_to_arrays(
1157                            ordering.iter().map(|e| &e.expr),
1158                            &batch,
1159                        )?;
1160
1161                        indices.iter_mut().for_each(|v| v.clear());
1162
1163                        Self::partition_range_indices(
1164                            &arrays,
1165                            split_points,
1166                            sort_options,
1167                            partition_buffer,
1168                            indices,
1169                        )?;
1170
1171                        // Finished building index-arrays for output partitions
1172                        timer.done();
1173
1174                        let partitioned_batches =
1175                            Self::partition_grouped_take(&batch, indices, &self.timer)?;
1176
1177                        Box::new(partitioned_batches.into_iter())
1178                    }
1179                }
1180            };
1181
1182        Ok(it)
1183    }
1184
1185    /// Groups input row indices by range partition. This populates `indices[p]` with the
1186    /// row indices from `arrays` that belong in output partition `p` according to `split_points` and `sort_options`.
1187    fn partition_range_indices(
1188        arrays: &[Arc<dyn Array>],
1189        split_points: &[SplitPoint],
1190        sort_options: &[SortOptions],
1191        row_key_buffer: &mut Vec<ScalarValue>,
1192        indices: &mut [Vec<u32>],
1193    ) -> Result<()> {
1194        let num_rows = arrays.first().map(|a| a.len()).unwrap_or(0);
1195        for row_idx in 0..num_rows {
1196            // Note that `extract_row_at_idx_to_buf` clears the `row_key_buffer` on each invocation, creating a new row key for comparison for each row
1197            extract_row_at_idx_to_buf(arrays, row_idx, row_key_buffer)?;
1198
1199            let partition =
1200                range_partition_id(row_key_buffer, split_points, sort_options)?;
1201            indices[partition].push(row_idx as u32)
1202        }
1203
1204        Ok(())
1205    }
1206
1207    // return the number of output partitions
1208    fn num_partitions(&self) -> usize {
1209        match &self.state {
1210            BatchPartitionerState::RoundRobin { num_partitions, .. } => *num_partitions,
1211            BatchPartitionerState::Hash { indices, .. }
1212            | BatchPartitionerState::Range { indices, .. } => indices.len(),
1213        }
1214    }
1215
1216    /// Build repartitioned hash/range output batches using one `take` per input batch.
1217    ///
1218    /// The routers first fills one index vector per output partition. This method
1219    /// concatenates those index vectors, performs one grouped `take_arrays`, and
1220    /// then returns each output partition as a slice of the reordered batch.
1221    ///
1222    /// For example, given partition indices:
1223    ///
1224    /// ```text
1225    /// partition 0: [2, 5]
1226    /// partition 1: []
1227    /// partition 2: [0, 3, 4]
1228    /// ```
1229    ///
1230    /// this method takes rows in `[2, 5, 0, 3, 4]` order once, then returns
1231    /// `partition 0 = slice(0, 2)` and `partition 2 = slice(2, 3)`.
1232    fn partition_grouped_take(
1233        batch: &RecordBatch,
1234        indices: &mut [Vec<u32>],
1235        timer: &metrics::Time,
1236    ) -> Result<Vec<Result<(usize, RecordBatch)>>> {
1237        let mut partition_ranges = Vec::with_capacity(indices.len());
1238        let mut reordered_indices = Vec::with_capacity(batch.num_rows());
1239
1240        for (partition, p_indices) in indices.iter_mut().enumerate() {
1241            if p_indices.is_empty() {
1242                continue;
1243            }
1244
1245            let start = reordered_indices.len();
1246            reordered_indices.extend_from_slice(p_indices);
1247            partition_ranges.push((partition, start, p_indices.len()));
1248            p_indices.clear();
1249        }
1250
1251        if reordered_indices.is_empty() {
1252            return Ok(vec![]);
1253        }
1254
1255        let batches = {
1256            let _timer = timer.timer();
1257            let indices_array: PrimitiveArray<UInt32Type> = reordered_indices.into();
1258            let columns = take_arrays(batch.columns(), &indices_array, None)?;
1259
1260            let mut options = RecordBatchOptions::new();
1261            options = options.with_row_count(Some(indices_array.len()));
1262            let reordered_batch =
1263                RecordBatch::try_new_with_options(batch.schema(), columns, &options)?;
1264
1265            partition_ranges
1266                .into_iter()
1267                .map(|(partition, start, len)| {
1268                    Ok((partition, reordered_batch.slice(start, len)))
1269                })
1270                .collect()
1271        };
1272
1273        Ok(batches)
1274    }
1275}
1276
1277/// Maps `N` input partitions to `M` output partitions based on a
1278/// [`Partitioning`] scheme.
1279///
1280/// # Background
1281///
1282/// DataFusion, like most other commercial systems, with the
1283/// notable exception of DuckDB, uses the "Exchange Operator" based
1284/// approach to parallelism which works well in practice given
1285/// sufficient care in implementation.
1286///
1287/// DataFusion's planner picks the target number of partitions and
1288/// then [`RepartitionExec`] redistributes [`RecordBatch`]es to that number
1289/// of output partitions.
1290///
1291/// For example, given `target_partitions=3` (trying to use 3 cores)
1292/// but scanning an input with 2 partitions, `RepartitionExec` can be
1293/// used to get 3 even streams of `RecordBatch`es
1294///
1295///
1296/// ```text
1297///        ▲                  ▲                  ▲
1298///        │                  │                  │
1299///        │                  │                  │
1300///        │                  │                  │
1301/// ┌───────────────┐  ┌───────────────┐  ┌───────────────┐
1302/// │    GroupBy    │  │    GroupBy    │  │    GroupBy    │
1303/// │   (Partial)   │  │   (Partial)   │  │   (Partial)   │
1304/// └───────────────┘  └───────────────┘  └───────────────┘
1305///        ▲                  ▲                  ▲
1306///        └──────────────────┼──────────────────┘
1307///                           │
1308///              ┌─────────────────────────┐
1309///              │     RepartitionExec     │
1310///              │   (hash/round robin)    │
1311///              └─────────────────────────┘
1312///                         ▲   ▲
1313///             ┌───────────┘   └───────────┐
1314///             │                           │
1315///             │                           │
1316///        .─────────.                 .─────────.
1317///     ,─'           '─.           ,─'           '─.
1318///    ;      Input      :         ;      Input      :
1319///    :   Partition 0   ;         :   Partition 1   ;
1320///     ╲               ╱           ╲               ╱
1321///      '─.         ,─'             '─.         ,─'
1322///         `───────'                   `───────'
1323/// ```
1324///
1325/// # Error Handling
1326///
1327/// If any of the input partitions return an error, the error is propagated to
1328/// all output partitions and inputs are not polled again.
1329///
1330/// # Output Ordering
1331///
1332/// If more than one stream is being repartitioned, the output will be some
1333/// arbitrary interleaving (and thus unordered) unless
1334/// [`Self::with_preserve_order`] specifies otherwise.
1335///
1336/// # Batch coalescing
1337///
1338/// Repartitioning one [`RecordBatch`] implies creating multiple smaller batches, potentially
1339/// as many as the number of output partitions. [`RepartitionExec`] makes sure that the returned
1340/// batches adhere to the configured `datafusion.execution.batch_size` for efficient operations,
1341/// and for that, it will automatically coalesce batches right after repartitioning for bounded
1342/// inputs. Coalescing is skipped for unbounded inputs so partial batches are emitted promptly.
1343///
1344/// For this, one shared [`LimitedBatchCoalescer`] per output partition is used:
1345///
1346/// ```text
1347///                         ┌───┐                           ┌───┐
1348///                      ┌─▶│   │────────▶.───────────.     │   │     ┌──────────────────┐
1349///                      │  └───┘ ┌───┐  ( Coalescer 0 )──▶ ├───┤ ───▶│     Output 0     │
1350///                      │┌──────▶│   │──▶`───────────'     │   │     └──────────────────┘
1351///                      ││       └───┘                     └───┘
1352/// ┌──────────────────┐ ││                                           ┌──────────────────┐
1353/// │BatchPartitioner 0│─┘│                                           │     Output 1     │
1354/// └──────────────────┘  │                                           └──────────────────┘
1355///                       │
1356/// ┌──────────────────┐  │                ...                        ┌──────────────────┐
1357/// │BatchPartitioner 1│──┘                                           │     Output 2     │
1358/// └──────────────────┘                                              └──────────────────┘
1359///
1360///                                                                   ┌──────────────────┐
1361///                                                                   │     Output 3     │
1362///                                                                   └──────────────────┘
1363/// ```
1364///
1365/// # Spilling Architecture
1366///
1367/// RepartitionExec uses [`SpillPool`](crate::spill::spill_pool) channels to handle
1368/// memory pressure during repartitioning. Each (input partition, output partition)
1369/// pair gets its own SpillPool channel for FIFO ordering.
1370///
1371/// ```text
1372/// Input Partitions (N)          Output Partitions (M)
1373/// ────────────────────          ─────────────────────
1374///
1375///    Input 0 ──┐                      ┌──▶ Output 0
1376///              │  ┌──────────────┐    │
1377///              ├─▶│ SpillPool    │────┤
1378///              │  │ [In0→Out0]   │    │
1379///    Input 1 ──┤  └──────────────┘    ├──▶ Output 1
1380///              │                       │
1381///              │  ┌──────────────┐    │
1382///              ├─▶│ SpillPool    │────┤
1383///              │  │ [In1→Out0]   │    │
1384///    Input 2 ──┤  └──────────────┘    ├──▶ Output 2
1385///              │                      │
1386///              │       ... (N×M SpillPools total)
1387///              │                      │
1388///              │  ┌──────────────┐    │
1389///              └─▶│ SpillPool    │────┘
1390///                 │ [InN→OutM]   │
1391///                 └──────────────┘
1392///
1393/// Each SpillPool maintains FIFO order for its (input, output) pair.
1394/// See `RepartitionBatch` for details on the memory/spill decision logic.
1395/// ```
1396///
1397/// # Footnote
1398///
1399/// The "Exchange Operator" was first described in the 1989 paper
1400/// [Encapsulation of parallelism in the Volcano query processing
1401/// system Paper](https://dl.acm.org/doi/pdf/10.1145/93605.98720)
1402/// which uses the term "Exchange" for the concept of repartitioning
1403/// data across threads.
1404///
1405/// For more background, please also see the [Optimizing Repartitions in DataFusion] blog.
1406///
1407/// [Optimizing Repartitions in DataFusion]: https://datafusion.apache.org/blog/2025/12/15/avoid-consecutive-repartitions
1408#[derive(Debug, Clone)]
1409pub struct RepartitionExec {
1410    /// Input execution plan
1411    input: Arc<dyn ExecutionPlan>,
1412    /// Inner state that is initialized when the parent calls .execute() on this node
1413    /// and consumed as soon as the parent starts consuming this node.
1414    state: Arc<Mutex<RepartitionExecState>>,
1415    /// Execution metrics
1416    metrics: ExecutionPlanMetricsSet,
1417    /// Boolean flag to decide whether to preserve ordering. If true means
1418    /// `SortPreservingRepartitionExec`, false means `RepartitionExec`.
1419    preserve_order: bool,
1420    /// Cache holding plan properties like equivalences, output partitioning etc.
1421    cache: Arc<PlanProperties>,
1422}
1423
1424#[derive(Debug, Clone)]
1425struct RepartitionMetrics {
1426    /// Time in nanos to execute child operator and fetch batches
1427    fetch_time: metrics::Time,
1428    /// Repartitioning elapsed time in nanos
1429    repartition_time: metrics::Time,
1430    /// Time in nanos for sending resulting batches to channels.
1431    ///
1432    /// One metric per output partition.
1433    send_time: Vec<metrics::Time>,
1434}
1435
1436impl RepartitionMetrics {
1437    pub fn new(
1438        input_partition: usize,
1439        num_output_partitions: usize,
1440        metrics: &ExecutionPlanMetricsSet,
1441    ) -> Self {
1442        // Time in nanos to execute child operator and fetch batches
1443        let fetch_time =
1444            MetricBuilder::new(metrics).subset_time("fetch_time", input_partition);
1445
1446        // Time in nanos to perform repartitioning
1447        let repartition_time =
1448            MetricBuilder::new(metrics).subset_time("repartition_time", input_partition);
1449
1450        // Time in nanos for sending resulting batches to channels
1451        let send_time = (0..num_output_partitions)
1452            .map(|output_partition| {
1453                let label =
1454                    metrics::Label::new("outputPartition", output_partition.to_string());
1455                MetricBuilder::new(metrics)
1456                    .with_label(label)
1457                    .subset_time("send_time", input_partition)
1458            })
1459            .collect();
1460
1461        Self {
1462            fetch_time,
1463            repartition_time,
1464            send_time,
1465        }
1466    }
1467}
1468
1469impl RepartitionExec {
1470    /// Input execution plan
1471    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
1472        &self.input
1473    }
1474
1475    /// Partitioning scheme to use
1476    pub fn partitioning(&self) -> &Partitioning {
1477        &self.cache.partitioning
1478    }
1479
1480    /// Get preserve_order flag of the RepartitionExec
1481    /// `true` means `SortPreservingRepartitionExec`, `false` means `RepartitionExec`
1482    pub fn preserve_order(&self) -> bool {
1483        self.preserve_order
1484    }
1485
1486    /// Get name used to display this Exec
1487    pub fn name(&self) -> &str {
1488        "RepartitionExec"
1489    }
1490}
1491
1492impl DisplayAs for RepartitionExec {
1493    fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result {
1494        let input_partition_count = self.input.output_partitioning().partition_count();
1495        match t {
1496            DisplayFormatType::Default | DisplayFormatType::Verbose => {
1497                write!(
1498                    f,
1499                    "{}: partitioning={}, input_partitions={}",
1500                    self.name(),
1501                    self.partitioning(),
1502                    input_partition_count,
1503                )?;
1504
1505                if self.preserve_order {
1506                    write!(f, ", preserve_order=true")?;
1507                } else if input_partition_count <= 1
1508                    && self.input.output_ordering().is_some()
1509                {
1510                    // Make it explicit that repartition maintains sortedness for a single input partition even
1511                    // when `preserve_sort order` is false
1512                    write!(f, ", maintains_sort_order=true")?;
1513                }
1514
1515                if let Some(sort_exprs) = self.sort_exprs() {
1516                    write!(f, ", sort_exprs={}", sort_exprs.clone())?;
1517                }
1518                Ok(())
1519            }
1520            DisplayFormatType::TreeRender => {
1521                writeln!(f, "partitioning_scheme={}", self.partitioning(),)?;
1522                let output_partition_count = self.partitioning().partition_count();
1523                let input_to_output_partition_str =
1524                    format!("{input_partition_count} -> {output_partition_count}");
1525                writeln!(
1526                    f,
1527                    "partition_count(in->out)={input_to_output_partition_str}"
1528                )?;
1529
1530                if self.preserve_order {
1531                    writeln!(f, "preserve_order={}", self.preserve_order)?;
1532                }
1533                Ok(())
1534            }
1535        }
1536    }
1537}
1538
1539impl ExecutionPlan for RepartitionExec {
1540    fn name(&self) -> &'static str {
1541        "RepartitionExec"
1542    }
1543
1544    /// Return a reference to Any that can be used for downcasting
1545    fn properties(&self) -> &Arc<PlanProperties> {
1546        &self.cache
1547    }
1548
1549    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1550        vec![&self.input]
1551    }
1552
1553    fn apply_expressions(
1554        &self,
1555        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
1556    ) -> Result<TreeNodeRecursion> {
1557        match self.partitioning() {
1558            Partitioning::Hash(exprs, _) => crate::apply_expression_roots(exprs, f),
1559            Partitioning::Range(range) => crate::apply_expression_roots(
1560                range.ordering().iter().map(|sort_expr| &sort_expr.expr),
1561                f,
1562            ),
1563            _ => Ok(TreeNodeRecursion::Continue),
1564        }
1565    }
1566
1567    fn replace_children(
1568        self: Arc<Self>,
1569        mut children: Vec<Arc<dyn ExecutionPlan>>,
1570        options: ReplaceChildrenOptions,
1571    ) -> Result<Arc<dyn ExecutionPlan>> {
1572        validate_child_count!(self, children);
1573        match options.children_properties {
1574            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
1575                input: children.swap_remove(0),
1576                metrics: ExecutionPlanMetricsSet::new(),
1577                state: Default::default(),
1578                ..Self::clone(&*self)
1579            })),
1580            ChildrenPropertiesMode::Recompute => {
1581                let mut repartition = RepartitionExec::try_new(
1582                    children.swap_remove(0),
1583                    self.partitioning().clone(),
1584                )?;
1585                if self.preserve_order {
1586                    repartition = repartition.with_preserve_order();
1587                }
1588                Ok(Arc::new(repartition))
1589            }
1590        }
1591    }
1592
1593    fn with_new_children(
1594        self: Arc<Self>,
1595        children: Vec<Arc<dyn ExecutionPlan>>,
1596    ) -> Result<Arc<dyn ExecutionPlan>> {
1597        self.replace_children(
1598            children,
1599            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1600        )
1601    }
1602
1603    fn with_new_children_and_same_properties(
1604        self: Arc<Self>,
1605        children: Vec<Arc<dyn ExecutionPlan>>,
1606    ) -> Result<Arc<dyn ExecutionPlan>> {
1607        self.replace_children(
1608            children,
1609            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
1610        )
1611    }
1612
1613    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
1614        vec![matches!(self.partitioning(), Partitioning::Hash(_, _))]
1615    }
1616
1617    fn maintains_input_order(&self) -> Vec<bool> {
1618        Self::maintains_input_order_helper(self.input(), self.preserve_order)
1619    }
1620
1621    fn execute(
1622        &self,
1623        partition: usize,
1624        context: Arc<TaskContext>,
1625    ) -> Result<SendableRecordBatchStream> {
1626        trace!(
1627            "Start {}::execute for partition: {}",
1628            self.name(),
1629            partition
1630        );
1631
1632        let spill_metrics = SpillMetrics::new(&self.metrics, partition);
1633
1634        let input = Arc::clone(&self.input);
1635        let partitioning = self.partitioning().clone();
1636        let metrics = self.metrics.clone();
1637        let preserve_order = self.sort_exprs().is_some();
1638        let name = self.name().to_owned();
1639        let schema = self.schema();
1640        let schema_captured = Arc::clone(&schema);
1641
1642        let spill_manager = SpillManager::new(
1643            Arc::clone(&context.runtime_env()),
1644            spill_metrics,
1645            input.schema(),
1646        );
1647
1648        // Get existing ordering to use for merging
1649        let sort_exprs = self.sort_exprs().cloned();
1650
1651        let state = Arc::clone(&self.state);
1652        if let Some(mut state) = state.try_lock() {
1653            state.ensure_input_streams_initialized(
1654                &input,
1655                &metrics,
1656                partitioning.partition_count(),
1657                &context,
1658            )?;
1659        }
1660
1661        let num_input_partitions = input.output_partitioning().partition_count();
1662
1663        let stream = futures::stream::once(async move {
1664            // lock scope
1665            let (rx, reservation, spill_readers, abort_helper) = {
1666                // lock mutexes
1667                let mut state = state.lock();
1668                let state = state.consume_input_streams(
1669                    &input,
1670                    &metrics,
1671                    &partitioning,
1672                    preserve_order,
1673                    &name,
1674                    &context,
1675                    spill_manager.clone(),
1676                )?;
1677
1678                // now return stream for the specified *output* partition which will
1679                // read from the channel
1680                let PartitionChannels {
1681                    rx,
1682                    reservation,
1683                    spill_readers,
1684                    ..
1685                } = state
1686                    .channels
1687                    .remove(&partition)
1688                    .expect("partition not used yet");
1689
1690                (
1691                    rx,
1692                    reservation,
1693                    spill_readers,
1694                    Arc::clone(&state.abort_helper),
1695                )
1696            };
1697
1698            trace!(
1699                "Before returning stream in {name}::execute for partition: {partition}"
1700            );
1701
1702            if preserve_order {
1703                // Store streams from all the input partitions:
1704                // Each input partition gets its own spill reader to maintain proper FIFO ordering
1705                //
1706                // Pass None for metrics here — these intermediate streams feed into
1707                // StreamingMerge which is the actual output. Only the merge's
1708                // BaselineMetrics should contribute to the operator's reported
1709                // output_rows. Without this, every row would be counted twice
1710                // (once by PerPartitionStream, once by StreamingMerge).
1711                let input_streams = rx
1712                    .into_iter()
1713                    .zip(spill_readers)
1714                    .map(|(receiver, spill_stream)| {
1715                        // In preserve_order mode, each receiver corresponds to exactly one input partition
1716                        Box::pin(PerPartitionStream::new(
1717                            Arc::clone(&schema_captured),
1718                            receiver,
1719                            Arc::clone(&abort_helper),
1720                            Arc::clone(&reservation),
1721                            spill_stream,
1722                            1, // Each receiver handles one input partition
1723                            None,
1724                        )) as SendableRecordBatchStream
1725                    })
1726                    .collect::<Vec<_>>();
1727                // Note that receiver size (`rx.len()`) and `num_input_partitions` are same.
1728
1729                // Merge streams (while preserving ordering) coming from
1730                // input partitions to this partition:
1731                let fetch = None;
1732                let merge_reservation =
1733                    MemoryConsumer::new(format!("{name}[Merge {partition}]"))
1734                        .register(context.memory_pool());
1735                StreamingMergeBuilder::new()
1736                    .with_streams(input_streams)
1737                    .with_schema(schema_captured)
1738                    .with_expressions(&sort_exprs.unwrap())
1739                    .with_metrics(BaselineMetrics::new(&metrics, partition))
1740                    .with_batch_size(context.session_config().batch_size())
1741                    .with_fetch(fetch)
1742                    .with_reservation(merge_reservation)
1743                    .with_spill_manager(spill_manager)
1744                    .build()
1745            } else {
1746                // Non-preserve-order case: single input stream, so use the first spill reader
1747                let spill_stream = spill_readers
1748                    .into_iter()
1749                    .next()
1750                    .expect("at least one spill reader should exist");
1751
1752                Ok(Box::pin(PerPartitionStream::new(
1753                    schema_captured,
1754                    rx.into_iter()
1755                        .next()
1756                        .expect("at least one receiver should exist"),
1757                    abort_helper,
1758                    reservation,
1759                    spill_stream,
1760                    num_input_partitions,
1761                    Some(BaselineMetrics::new(&metrics, partition)),
1762                )) as SendableRecordBatchStream)
1763            }
1764        })
1765        .try_flatten();
1766        let stream = RecordBatchStreamAdapter::new(schema, stream);
1767        Ok(Box::pin(stream))
1768    }
1769
1770    fn metrics(&self) -> Option<MetricsSet> {
1771        Some(self.metrics.clone_inner())
1772    }
1773
1774    fn child_stats_requests(&self, _partition: Option<usize>) -> Vec<ChildStats> {
1775        vec![ChildStats::At(None)]
1776    }
1777
1778    fn statistics_from_inputs(
1779        &self,
1780        input_stats: &[Arc<Statistics>],
1781        args: &StatisticsArgs,
1782    ) -> Result<Arc<Statistics>> {
1783        if args.partition().is_some() {
1784            let partition_count = self.partitioning().partition_count();
1785            // `StatisticsContext::compute` validates the partition index against
1786            // this same count before calling, so it is non-zero here; guard
1787            // defensively against a direct call so the division below cannot
1788            // divide by zero
1789            assert_or_internal_err!(
1790                partition_count > 0,
1791                "RepartitionExec statistics requested for a partition but the partition count is 0"
1792            );
1793
1794            let mut stats = input_stats[0].as_ref().clone();
1795
1796            // Distribute statistics across partitions
1797            stats.num_rows = stats
1798                .num_rows
1799                .get_value()
1800                .map(|rows| Precision::Inexact(rows / partition_count))
1801                .unwrap_or(Precision::Absent);
1802            stats.total_byte_size = stats
1803                .total_byte_size
1804                .get_value()
1805                .map(|bytes| Precision::Inexact(bytes / partition_count))
1806                .unwrap_or(Precision::Absent);
1807
1808            // Make all column stats unknown
1809            stats.column_statistics = stats
1810                .column_statistics
1811                .iter()
1812                .map(|_| ColumnStatistics::new_unknown())
1813                .collect();
1814
1815            Ok(Arc::new(stats))
1816        } else {
1817            Ok(Arc::clone(&input_stats[0]))
1818        }
1819    }
1820
1821    fn cardinality_effect(&self) -> CardinalityEffect {
1822        CardinalityEffect::Equal
1823    }
1824
1825    fn try_swapping_with_projection(
1826        &self,
1827        projection: &ProjectionExec,
1828    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
1829        // If the projection does not narrow the schema, we should not try to push it down.
1830        if projection.expr().len() >= projection.input().schema().fields().len() {
1831            return Ok(None);
1832        }
1833
1834        // If pushdown is not beneficial or applicable, break it.
1835        if projection.benefits_from_input_partitioning()[0]
1836            || !all_columns(projection.expr())
1837        {
1838            return Ok(None);
1839        }
1840
1841        let new_projection = make_with_child(projection, self.input())?;
1842
1843        let new_partitioning = match self.partitioning() {
1844            Partitioning::Hash(partitions, size) => {
1845                let mut new_partitions = vec![];
1846                for partition in partitions {
1847                    let Some(new_partition) =
1848                        update_expr(partition, projection.expr(), false)?
1849                    else {
1850                        return Ok(None);
1851                    };
1852                    new_partitions.push(new_partition);
1853                }
1854                Partitioning::Hash(new_partitions, *size)
1855            }
1856            Partitioning::Range(range_partitioning) => {
1857                // Rewrite range key expressions through the projection.
1858                let mut sort_exprs =
1859                    Vec::with_capacity(range_partitioning.ordering().len());
1860                for sort_expr in range_partitioning.ordering() {
1861                    let Some(new_expr) =
1862                        update_expr(&sort_expr.expr, projection.expr(), false)?
1863                    else {
1864                        return Ok(None);
1865                    };
1866                    sort_exprs.push(PhysicalSortExpr::new(new_expr, sort_expr.options));
1867                }
1868
1869                let Some(ordering) = LexOrdering::new(sort_exprs) else {
1870                    return internal_err!(
1871                        "failed to create LexOrdering for range partitioning"
1872                    );
1873                };
1874
1875                Partitioning::Range(RangePartitioning::try_new(
1876                    ordering,
1877                    range_partitioning.split_points().to_vec(),
1878                )?)
1879            }
1880            others => others.clone(),
1881        };
1882
1883        Ok(Some(Arc::new(RepartitionExec::try_new(
1884            new_projection,
1885            new_partitioning,
1886        )?)))
1887    }
1888
1889    fn gather_filters_for_pushdown(
1890        &self,
1891        _phase: FilterPushdownPhase,
1892        parent_filters: Vec<Arc<dyn PhysicalExpr>>,
1893        _config: &ConfigOptions,
1894    ) -> Result<FilterDescription> {
1895        FilterDescription::from_children(parent_filters, &self.children())
1896    }
1897
1898    fn handle_child_pushdown_result(
1899        &self,
1900        _phase: FilterPushdownPhase,
1901        child_pushdown_result: ChildPushdownResult,
1902        _config: &ConfigOptions,
1903    ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
1904        Ok(FilterPushdownPropagation::if_all(child_pushdown_result))
1905    }
1906
1907    fn try_pushdown_sort(
1908        &self,
1909        order: &[PhysicalSortExpr],
1910    ) -> Result<SortOrderPushdownResult<Arc<dyn ExecutionPlan>>> {
1911        // RepartitionExec only maintains input order if preserve_order is set
1912        // or if there's only one partition
1913        if !self.maintains_input_order()[0] {
1914            return Ok(SortOrderPushdownResult::Unsupported);
1915        }
1916
1917        // Delegate to the child and wrap with a new RepartitionExec
1918        self.input.try_pushdown_sort(order)?.try_map(|new_input| {
1919            let mut new_repartition =
1920                RepartitionExec::try_new(new_input, self.partitioning().clone())?;
1921            if self.preserve_order {
1922                new_repartition = new_repartition.with_preserve_order();
1923            }
1924            Ok(Arc::new(new_repartition) as Arc<dyn ExecutionPlan>)
1925        })
1926    }
1927
1928    fn repartitioned(
1929        &self,
1930        target_partitions: usize,
1931        _config: &ConfigOptions,
1932    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
1933        use Partitioning::*;
1934        let mut new_properties = PlanProperties::clone(&self.cache);
1935        new_properties.partitioning = match new_properties.partitioning {
1936            RoundRobinBatch(_) => RoundRobinBatch(target_partitions),
1937            Hash(hash, _) => Hash(hash, target_partitions),
1938            Range(_) => {
1939                // Number of partitions is constrained by the split points and cannot be changed
1940                return Ok(None);
1941            }
1942            UnknownPartitioning(_) => UnknownPartitioning(target_partitions),
1943        };
1944        Ok(Some(Arc::new(Self {
1945            input: Arc::clone(&self.input),
1946            state: Arc::clone(&self.state),
1947            metrics: self.metrics.clone(),
1948            preserve_order: self.preserve_order,
1949            cache: new_properties.into(),
1950        })))
1951    }
1952
1953    #[cfg(feature = "proto")]
1954    fn try_to_proto(
1955        &self,
1956        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
1957    ) -> Result<Option<protobuf::PhysicalPlanNode>> {
1958        let input = ctx.encode_child(self.input())?;
1959
1960        let partitioning = self.partitioning().try_to_proto(&ctx.expr_ctx())?;
1961
1962        Ok(Some(protobuf::PhysicalPlanNode {
1963            physical_plan_type: Some(
1964                protobuf::physical_plan_node::PhysicalPlanType::Repartition(Box::new(
1965                    protobuf::RepartitionExecNode {
1966                        input: Some(Box::new(input)),
1967                        partitioning: Some(partitioning),
1968                        preserve_order: self.preserve_order(),
1969                    },
1970                )),
1971            ),
1972        }))
1973    }
1974}
1975
1976#[cfg(feature = "proto")]
1977impl RepartitionExec {
1978    /// Reconstruct a [`RepartitionExec`] from its protobuf representation.
1979    pub fn try_from_proto(
1980        node: &protobuf::PhysicalPlanNode,
1981        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
1982    ) -> Result<Arc<dyn ExecutionPlan>> {
1983        let repart = crate::expect_plan_variant!(
1984            node,
1985            protobuf::physical_plan_node::PhysicalPlanType::Repartition,
1986            "RepartitionExec",
1987        );
1988        let input = ctx.decode_required_child(
1989            repart.input.as_deref(),
1990            "RepartitionExec",
1991            "input",
1992        )?;
1993        let input_schema = input.schema();
1994
1995        let partitioning = repart
1996            .partitioning
1997            .as_ref()
1998            .map(|partitioning| {
1999                Partitioning::try_from_proto(
2000                    partitioning,
2001                    &ctx.expr_ctx(input_schema.as_ref()),
2002                )
2003            })
2004            .transpose()?
2005            .flatten()
2006            .ok_or_else(|| {
2007                datafusion_common::internal_datafusion_err!(
2008                    "RepartitionExec is missing required field 'partitioning'"
2009                )
2010            })?;
2011
2012        let mut repart_exec = RepartitionExec::try_new(input, partitioning)?;
2013        if repart.preserve_order {
2014            repart_exec = repart_exec.with_preserve_order();
2015        }
2016        Ok(Arc::new(repart_exec))
2017    }
2018}
2019
2020impl RepartitionExec {
2021    /// Create a new RepartitionExec, that produces output `partitioning`, and
2022    /// does not preserve the order of the input (see [`Self::with_preserve_order`]
2023    /// for more details)
2024    pub fn try_new(
2025        input: Arc<dyn ExecutionPlan>,
2026        partitioning: Partitioning,
2027    ) -> Result<Self> {
2028        let preserve_order = false;
2029        let cache = Self::compute_properties(&input, partitioning, preserve_order);
2030        Ok(RepartitionExec {
2031            input,
2032            state: Default::default(),
2033            metrics: ExecutionPlanMetricsSet::new(),
2034            preserve_order,
2035            cache: Arc::new(cache),
2036        })
2037    }
2038
2039    fn maintains_input_order_helper(
2040        input: &Arc<dyn ExecutionPlan>,
2041        preserve_order: bool,
2042    ) -> Vec<bool> {
2043        // We preserve ordering when repartition is order preserving variant or input partitioning is 1
2044        vec![preserve_order || input.output_partitioning().partition_count() <= 1]
2045    }
2046
2047    fn eq_properties_helper(
2048        input: &Arc<dyn ExecutionPlan>,
2049        preserve_order: bool,
2050    ) -> EquivalenceProperties {
2051        // Equivalence Properties
2052        let mut eq_properties = input.equivalence_properties().clone();
2053        // If the ordering is lost, reset the ordering equivalence class:
2054        if !Self::maintains_input_order_helper(input, preserve_order)[0] {
2055            eq_properties.clear_orderings();
2056        }
2057        // When there are more than one input partitions, they will be fused at the output.
2058        // Therefore, remove per partition constants.
2059        if input.output_partitioning().partition_count() > 1 {
2060            eq_properties.clear_per_partition_constants();
2061        }
2062        eq_properties
2063    }
2064
2065    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
2066    fn compute_properties(
2067        input: &Arc<dyn ExecutionPlan>,
2068        partitioning: Partitioning,
2069        preserve_order: bool,
2070    ) -> PlanProperties {
2071        PlanProperties::new(
2072            Self::eq_properties_helper(input, preserve_order),
2073            partitioning,
2074            input.pipeline_behavior(),
2075            input.boundedness(),
2076        )
2077        .with_scheduling_type(SchedulingType::Cooperative)
2078        .with_evaluation_type(EvaluationType::Eager)
2079    }
2080
2081    /// Specify if this repartitioning operation should preserve the order of
2082    /// rows from its input when producing output. Preserving order is more
2083    /// expensive at runtime, so should only be set if the output of this
2084    /// operator can take advantage of it.
2085    ///
2086    /// If the input is not ordered, or has only one partition, this is a no op,
2087    /// and the node remains a `RepartitionExec`.
2088    pub fn with_preserve_order(mut self) -> Self {
2089        self.preserve_order =
2090                // If the input isn't ordered, there is no ordering to preserve
2091                self.input.output_ordering().is_some() &&
2092                // if there is only one input partition, merging is not required
2093                // to maintain order
2094                self.input.output_partitioning().partition_count() > 1;
2095        let eq_properties = Self::eq_properties_helper(&self.input, self.preserve_order);
2096        Arc::make_mut(&mut self.cache).set_eq_properties(eq_properties);
2097        self
2098    }
2099
2100    /// Return the sort expressions that are used to merge
2101    fn sort_exprs(&self) -> Option<&LexOrdering> {
2102        if self.preserve_order {
2103            self.input.output_ordering()
2104        } else {
2105            None
2106        }
2107    }
2108
2109    /// Pulls data from the specified input plan, feeding it to the
2110    /// output partitions based on the desired partitioning
2111    ///
2112    /// `output_channels` holds the output sending channels for each output partition
2113    async fn pull_from_input(
2114        mut stream: SendableRecordBatchStream,
2115        mut output_channels: HashMap<usize, OutputChannel>,
2116        partitioning: Partitioning,
2117        metrics: RepartitionMetrics,
2118        input_partition: usize,
2119        num_input_partitions: usize,
2120    ) -> Result<()> {
2121        let mut partitioner = BatchPartitioner::try_new(
2122            partitioning,
2123            metrics.repartition_time.clone(),
2124            input_partition,
2125            num_input_partitions,
2126        )?;
2127
2128        // While there are still outputs to send to, keep pulling inputs
2129        let mut batches_until_yield = partitioner.num_partitions();
2130        while !output_channels.is_empty() {
2131            // fetch the next batch
2132            let timer = metrics.fetch_time.timer();
2133            let result = stream.next().await;
2134            timer.done();
2135
2136            // Input is done
2137            let batch = match result {
2138                Some(result) => result?,
2139                None => break,
2140            };
2141
2142            // Handle empty batch
2143            if batch.num_rows() == 0 {
2144                continue;
2145            }
2146
2147            for res in partitioner.partition_iter(batch)? {
2148                let (partition, batch) = res?;
2149
2150                let timer = metrics.send_time[partition].timer();
2151                // if there is still a receiver, send to it
2152                if let Some(output_channel) = output_channels.get_mut(&partition) {
2153                    for batch in output_channel.coalesce(batch)? {
2154                        if output_channel.send(batch).await.is_err() {
2155                            // If the other end has hung up, it was an early shutdown (e.g. LIMIT)
2156                            // so ignore this channel from now on.
2157                            output_channels.remove(&partition);
2158                            break;
2159                        }
2160                    }
2161                }
2162                timer.done();
2163            }
2164
2165            // If the input stream is endless, we may spin forever and
2166            // never yield back to tokio.  See
2167            // https://github.com/apache/datafusion/issues/5278.
2168            //
2169            // However, yielding on every batch causes a bottleneck
2170            // when running with multiple cores. See
2171            // https://github.com/apache/datafusion/issues/6290
2172            //
2173            // Thus, heuristically yield after producing num_partition
2174            // batches
2175            //
2176            // In round robin this is ideal as each input will get a
2177            // new batch. In hash partitioning it may yield too often
2178            // on uneven distributions even if some partition can not
2179            // make progress, but parallelism is going to be limited
2180            // in that case anyways
2181            if batches_until_yield == 0 {
2182                tokio::task::yield_now().await;
2183                batches_until_yield = partitioner.num_partitions();
2184            } else {
2185                batches_until_yield -= 1;
2186            }
2187        }
2188
2189        // End of input for this task. For each output partition we still
2190        // have a channel to, decrement the active-senders counter; whoever
2191        // sees the count drop to zero is the last input task and must
2192        // finalize the shared coalescer and ship its residual.
2193        for (_, output_channel) in output_channels.drain() {
2194            output_channel.finalize().await?;
2195        }
2196
2197        // Spill writers will auto-finalize when dropped
2198        // No need for explicit flush
2199        Ok(())
2200    }
2201
2202    /// Waits for `input_task` which is consuming one of the inputs to
2203    /// complete. Upon each successful completion, sends a `None` to
2204    /// each of the output tx channels to signal one of the inputs is
2205    /// complete. Upon error, propagates the errors to all output tx
2206    /// channels.
2207    async fn wait_for_task(
2208        input_task: SpawnedTask<Result<()>>,
2209        txs: HashMap<usize, DistributionSender<MaybeBatch>>,
2210    ) {
2211        // wait for completion, and propagate error
2212        // note we ignore errors on send (.ok) as that means the receiver has already shutdown.
2213
2214        match input_task.join().await {
2215            // Error in joining task
2216            Err(e) => {
2217                let e = Arc::new(e);
2218
2219                for (_, tx) in txs {
2220                    let err = Err(DataFusionError::Context(
2221                        "Join Error".to_string(),
2222                        Box::new(DataFusionError::External(Box::new(Arc::clone(&e)))),
2223                    ));
2224                    tx.send(Some(err)).await.ok();
2225                }
2226            }
2227            // Error from running input task
2228            Ok(Err(e)) => {
2229                // send the same Arc'd error to all output partitions
2230                let e = Arc::new(e);
2231
2232                for (_, tx) in txs {
2233                    // wrap it because need to send error to all output partitions
2234                    let err = Err(DataFusionError::from(&e));
2235                    tx.send(Some(err)).await.ok();
2236                }
2237            }
2238            // Input task completed successfully
2239            Ok(Ok(())) => {
2240                // notify each output partition that this input partition has no more data
2241                for (_partition, tx) in txs {
2242                    tx.send(None).await.ok();
2243                }
2244            }
2245        }
2246    }
2247}
2248
2249/// State for tracking whether we're reading from memory channel or spill stream.
2250///
2251/// This state machine ensures proper ordering when batches are mixed between memory
2252/// and spilled storage. When a [`RepartitionBatch::Spilled`] marker is received,
2253/// the stream must block on the spill stream until the corresponding batch arrives.
2254///
2255/// # State Machine
2256///
2257/// ```text
2258///                        ┌─────────────────┐
2259///                   ┌───▶│  ReadingMemory  │◀───┐
2260///                   │    └────────┬────────┘    │
2261///                   │             │             │
2262///                   │     Poll channel          │
2263///                   │             │             │
2264///                   │  ┌──────────┼─────────────┐
2265///                   │  │          │             │
2266///                   │  ▼          ▼             │
2267///                   │ Memory   Spilled          │
2268///       Got batch   │ batch    marker           │
2269///       from spill  │  │          │             │
2270///                   │  │          ▼             │
2271///                   │  │  ┌──────────────────┐  │
2272///                   │  │  │ ReadingSpilled   │  │
2273///                   │  │  └────────┬─────────┘  │
2274///                   │  │           │            │
2275///                   │  │   Poll spill_stream    │
2276///                   │  │           │            │
2277///                   │  │           ▼            │
2278///                   │  │      Get batch         │
2279///                   │  │           │            │
2280///                   └──┴───────────┴────────────┘
2281///                                  │
2282///                                  ▼
2283///                           Return batch
2284///                     (Order preserved within
2285///                      (input, output) pair)
2286/// ```
2287///
2288/// The transition to `ReadingSpilled` blocks further channel polling to maintain
2289/// FIFO ordering - we cannot read the next item from the channel until the spill
2290/// stream provides the current batch.
2291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2292enum StreamState {
2293    /// Reading from the memory channel (normal operation)
2294    ReadingMemory,
2295    /// Waiting for a spilled batch from the spill stream.
2296    /// Must not poll channel until spilled batch is received to preserve ordering.
2297    ReadingSpilled,
2298}
2299
2300/// This struct converts a receiver to a stream.
2301/// Receiver receives data on an SPSC channel.
2302struct PerPartitionStream {
2303    /// Schema wrapped by Arc
2304    schema: SchemaRef,
2305
2306    /// channel containing the repartitioned batches
2307    receiver: DistributionReceiver<MaybeBatch>,
2308
2309    /// Handle to ensure background tasks are killed when no longer needed.
2310    _drop_helper: Arc<Vec<SpawnedTask<()>>>,
2311
2312    /// Memory reservation.
2313    reservation: SharedMemoryReservation,
2314
2315    /// Infinite stream for reading from the spill pool
2316    spill_stream: SendableRecordBatchStream,
2317
2318    /// Internal state indicating if we are reading from memory or spill stream
2319    state: StreamState,
2320
2321    /// Number of input partitions that have not yet finished.
2322    /// In non-preserve-order mode, multiple input partitions send to the same channel,
2323    /// each sending None when complete. We must wait for all of them.
2324    remaining_partitions: usize,
2325
2326    /// Execution metrics (None in preserve-order mode where StreamingMerge owns the metrics)
2327    baseline_metrics: Option<BaselineMetrics>,
2328}
2329
2330impl PerPartitionStream {
2331    fn new(
2332        schema: SchemaRef,
2333        receiver: DistributionReceiver<MaybeBatch>,
2334        drop_helper: Arc<Vec<SpawnedTask<()>>>,
2335        reservation: SharedMemoryReservation,
2336        spill_stream: SendableRecordBatchStream,
2337        num_input_partitions: usize,
2338        baseline_metrics: Option<BaselineMetrics>,
2339    ) -> Self {
2340        Self {
2341            schema,
2342            receiver,
2343            _drop_helper: drop_helper,
2344            reservation,
2345            spill_stream,
2346            state: StreamState::ReadingMemory,
2347            remaining_partitions: num_input_partitions,
2348            baseline_metrics,
2349        }
2350    }
2351
2352    fn poll_next_inner(
2353        self: &mut Pin<&mut Self>,
2354        cx: &mut Context<'_>,
2355    ) -> Poll<Option<Result<RecordBatch>>> {
2356        use futures::StreamExt;
2357        let elapsed = self
2358            .baseline_metrics
2359            .as_ref()
2360            .map(|m| m.elapsed_compute().clone());
2361        let _timer = elapsed.as_ref().map(|t| t.timer());
2362
2363        loop {
2364            match self.state {
2365                StreamState::ReadingMemory => {
2366                    // Poll the memory channel for next message
2367                    let value = match self.receiver.recv().poll_unpin(cx) {
2368                        Poll::Ready(v) => v,
2369                        Poll::Pending => {
2370                            // Nothing from channel, wait
2371                            return Poll::Pending;
2372                        }
2373                    };
2374
2375                    match value {
2376                        Some(Some(v)) => match v {
2377                            Ok(RepartitionBatch::Memory(batch)) => {
2378                                // Release memory and return batch
2379                                self.reservation.shrink(batch.get_array_memory_size());
2380                                return Poll::Ready(Some(Ok(batch)));
2381                            }
2382                            Ok(RepartitionBatch::Spilled) => {
2383                                // Batch was spilled, transition to reading from spill stream
2384                                // We must block on spill stream until we get the batch
2385                                // to preserve ordering
2386                                self.state = StreamState::ReadingSpilled;
2387                                continue;
2388                            }
2389                            Err(e) => {
2390                                return Poll::Ready(Some(Err(e)));
2391                            }
2392                        },
2393                        Some(None) => {
2394                            // One input partition finished
2395                            self.remaining_partitions -= 1;
2396                            if self.remaining_partitions == 0 {
2397                                // All input partitions finished
2398                                return Poll::Ready(None);
2399                            }
2400                            // Continue to poll for more data from other partitions
2401                            continue;
2402                        }
2403                        None => {
2404                            // Channel closed unexpectedly
2405                            return Poll::Ready(None);
2406                        }
2407                    }
2408                }
2409                StreamState::ReadingSpilled => {
2410                    // Poll spill stream for the spilled batch
2411                    match self.spill_stream.poll_next_unpin(cx) {
2412                        Poll::Ready(Some(Ok(batch))) => {
2413                            self.state = StreamState::ReadingMemory;
2414                            return Poll::Ready(Some(Ok(batch)));
2415                        }
2416                        Poll::Ready(Some(Err(e))) => {
2417                            return Poll::Ready(Some(Err(e)));
2418                        }
2419                        Poll::Ready(None) => {
2420                            // Spill stream ended — release its resources before
2421                            // we go back to draining the memory channel.
2422                            let spill_schema = self.spill_stream.schema();
2423                            self.spill_stream =
2424                                Box::pin(EmptyRecordBatchStream::new(spill_schema));
2425                            self.state = StreamState::ReadingMemory;
2426                        }
2427                        Poll::Pending => {
2428                            // Spilled batch not ready yet, must wait
2429                            // This preserves ordering by blocking until spill data arrives
2430                            return Poll::Pending;
2431                        }
2432                    }
2433                }
2434            }
2435        }
2436    }
2437}
2438
2439impl Stream for PerPartitionStream {
2440    type Item = Result<RecordBatch>;
2441
2442    fn poll_next(
2443        mut self: Pin<&mut Self>,
2444        cx: &mut Context<'_>,
2445    ) -> Poll<Option<Self::Item>> {
2446        let poll = self.poll_next_inner(cx);
2447        if let Some(metrics) = &self.baseline_metrics {
2448            metrics.record_poll(poll)
2449        } else {
2450            poll
2451        }
2452    }
2453}
2454
2455impl RecordBatchStream for PerPartitionStream {
2456    /// Get the schema
2457    fn schema(&self) -> SchemaRef {
2458        Arc::clone(&self.schema)
2459    }
2460}
2461
2462#[cfg(test)]
2463mod tests {
2464    use std::collections::HashSet;
2465
2466    use super::*;
2467    use crate::empty::EmptyExec;
2468    use crate::projection::ProjectionExpr;
2469    use crate::streaming::{PartitionStream, StreamingTableExec};
2470    use crate::test::TestMemoryExec;
2471    use crate::{
2472        test::{
2473            assert_is_pending,
2474            exec::{
2475                BarrierExec, BlockingExec, ErrorExec, MockExec,
2476                assert_strong_count_converges_to_zero,
2477            },
2478        },
2479        {collect, expressions::col},
2480    };
2481
2482    use arrow::array::{ArrayRef, StringArray, UInt32Array};
2483    use arrow::datatypes::{DataType, Field, Schema};
2484    use datafusion_common::ScalarValue;
2485    use datafusion_common::cast::{as_string_array, as_uint32_array};
2486    use datafusion_common::exec_err;
2487    use datafusion_common::test_util::batches_to_sort_string;
2488    use datafusion_common_runtime::JoinSet;
2489    use datafusion_execution::config::SessionConfig;
2490    use datafusion_execution::runtime_env::RuntimeEnvBuilder;
2491    use datafusion_physical_expr::{PhysicalSortExpr, RangePartitioning, SplitPoint};
2492    use insta::assert_snapshot;
2493
2494    #[derive(Debug)]
2495    struct UnboundedTestPartition {
2496        schema: SchemaRef,
2497        batch: RecordBatch,
2498    }
2499
2500    impl PartitionStream for UnboundedTestPartition {
2501        fn schema(&self) -> &SchemaRef {
2502            &self.schema
2503        }
2504
2505        fn execute(&self, _ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
2506            let stream = futures::stream::iter([Ok(self.batch.clone())])
2507                .chain(futures::stream::pending());
2508            Box::pin(RecordBatchStreamAdapter::new(
2509                Arc::clone(&self.schema),
2510                stream,
2511            ))
2512        }
2513    }
2514
2515    #[test]
2516    fn range_expr_preserves_duplicate_remapped_children() -> Result<()> {
2517        let schema = Arc::new(Schema::new(vec![
2518            Field::new("a", DataType::UInt32, false),
2519            Field::new("b", DataType::UInt32, false),
2520        ]));
2521        let sort_options = [SortOptions::new(false, false), SortOptions::new(true, true)];
2522        let split_points = vec![SplitPoint::new(vec![
2523            ScalarValue::UInt32(Some(10)),
2524            ScalarValue::UInt32(Some(20)),
2525        ])];
2526        let range_partitioning = RangePartitioning::try_new(
2527            [
2528                PhysicalSortExpr::new(col("a", &schema)?, sort_options[0]),
2529                PhysicalSortExpr::new(col("b", &schema)?, sort_options[1]),
2530            ]
2531            .into(),
2532            split_points.clone(),
2533        )?;
2534        let expr = Arc::new(RangeExpr::try_new(
2535            vec![col("a", &schema)?, col("b", &schema)?],
2536            &range_partitioning,
2537        )?);
2538        let remapped = col("a", &schema)?;
2539        let rewritten =
2540            expr.with_new_children(vec![Arc::clone(&remapped), Arc::clone(&remapped)])?;
2541
2542        let rewritten = rewritten
2543            .downcast_ref::<RangeExpr>()
2544            .expect("rewritten expression should remain a RangeExpr");
2545        assert_eq!(rewritten.on_columns().len(), 2);
2546        assert!(Arc::ptr_eq(
2547            &rewritten.on_columns()[0],
2548            &rewritten.on_columns()[1]
2549        ));
2550        assert_eq!(rewritten.sort_options(), sort_options);
2551        assert_eq!(rewritten.split_points(), split_points);
2552
2553        Ok(())
2554    }
2555
2556    #[test]
2557    fn strength_reduced_u64_remainder_matches_modulo() {
2558        let divisors = [
2559            1,
2560            2,
2561            3,
2562            4,
2563            5,
2564            7,
2565            8,
2566            10,
2567            16,
2568            31,
2569            32,
2570            63,
2571            64,
2572            65,
2573            97,
2574            u64::from(u32::MAX),
2575            u64::from(u32::MAX) + 1,
2576            1_u64 << 32,
2577            (1_u64 << 63) - 1,
2578            1_u64 << 63,
2579            u64::MAX - 1,
2580            u64::MAX,
2581        ];
2582        let values = [
2583            0,
2584            1,
2585            2,
2586            3,
2587            4,
2588            5,
2589            31,
2590            32,
2591            33,
2592            63,
2593            64,
2594            65,
2595            u64::from(u32::MAX) - 1,
2596            u64::from(u32::MAX),
2597            u64::from(u32::MAX) + 1,
2598            (1_u64 << 32) - 1,
2599            1_u64 << 32,
2600            (1_u64 << 32) + 1,
2601            (1_u64 << 63) - 1,
2602            1_u64 << 63,
2603            (1_u64 << 63) + 1,
2604            u64::MAX - 1,
2605            u64::MAX,
2606        ];
2607
2608        for divisor in divisors {
2609            let reducer = StrengthReducedU64::new(divisor);
2610            for value in values {
2611                assert_eq!(
2612                    reducer.remainder(value),
2613                    value % divisor,
2614                    "value={value} divisor={divisor}"
2615                );
2616            }
2617
2618            let mut value = 0x1234_5678_9abc_def0 ^ divisor;
2619            for _ in 0..10_000 {
2620                value = value
2621                    .wrapping_mul(6_364_136_223_846_793_005)
2622                    .wrapping_add(1_442_695_040_888_963_407);
2623                assert_eq!(
2624                    reducer.remainder(value),
2625                    value % divisor,
2626                    "value={value} divisor={divisor}"
2627                );
2628            }
2629        }
2630    }
2631
2632    #[test]
2633    fn hash_partitioner_requires_nonzero_partitions() {
2634        let metrics = ExecutionPlanMetricsSet::new();
2635        let timer = MetricBuilder::new(&metrics).subset_time("test", 0);
2636
2637        let err = BatchPartitioner::new_hash_partitioner(vec![], 0, timer)
2638            .err()
2639            .expect("zero hash partitions should fail")
2640            .to_string();
2641
2642        assert!(
2643            err.contains("Hash repartition requires at least one partition"),
2644            "actual: {err}"
2645        );
2646    }
2647
2648    #[tokio::test]
2649    async fn one_to_many_round_robin() -> Result<()> {
2650        // define input partitions
2651        let schema = test_schema(false);
2652        let partition = create_vec_batches(50);
2653        let partitions = vec![partition];
2654
2655        // repartition from 1 input to 4 output
2656        let output_partitions =
2657            repartition(&schema, partitions, Partitioning::RoundRobinBatch(4)).await?;
2658
2659        assert_eq!(4, output_partitions.len());
2660        for partition in &output_partitions {
2661            assert_eq!(1, partition.len());
2662        }
2663        assert_eq!(13 * 8, output_partitions[0][0].num_rows());
2664        assert_eq!(13 * 8, output_partitions[1][0].num_rows());
2665        assert_eq!(12 * 8, output_partitions[2][0].num_rows());
2666        assert_eq!(12 * 8, output_partitions[3][0].num_rows());
2667
2668        Ok(())
2669    }
2670
2671    #[tokio::test]
2672    async fn many_to_one_round_robin() -> Result<()> {
2673        // define input partitions
2674        let schema = test_schema(false);
2675        let partition = create_vec_batches(50);
2676        let partitions = vec![partition.clone(), partition.clone(), partition.clone()];
2677
2678        // repartition from 3 input to 1 output
2679        let output_partitions =
2680            repartition(&schema, partitions, Partitioning::RoundRobinBatch(1)).await?;
2681
2682        assert_eq!(1, output_partitions.len());
2683        assert_eq!(150 * 8, output_partitions[0][0].num_rows());
2684
2685        Ok(())
2686    }
2687
2688    #[tokio::test]
2689    async fn many_to_many_round_robin() -> Result<()> {
2690        // define input partitions
2691        let schema = test_schema(false);
2692        let partition = create_vec_batches(50);
2693        let partitions = vec![partition.clone(), partition.clone(), partition.clone()];
2694
2695        // repartition from 3 input to 5 output
2696        let output_partitions =
2697            repartition(&schema, partitions, Partitioning::RoundRobinBatch(5)).await?;
2698
2699        let total_rows_per_partition = 8 * 50 * 3 / 5;
2700        assert_eq!(5, output_partitions.len());
2701        for partition in output_partitions {
2702            assert_eq!(1, partition.len());
2703            assert_eq!(total_rows_per_partition, partition[0].num_rows());
2704        }
2705
2706        Ok(())
2707    }
2708
2709    #[tokio::test]
2710    async fn many_to_many_hash_partition() -> Result<()> {
2711        // define input partitions
2712        let schema = test_schema(false);
2713        let partition = create_vec_batches(50);
2714        let partitions = vec![partition.clone(), partition.clone(), partition.clone()];
2715
2716        let output_partitions = repartition(
2717            &schema,
2718            partitions,
2719            Partitioning::Hash(vec![col("c0", &schema)?], 8),
2720        )
2721        .await?;
2722
2723        let total_rows: usize = output_partitions
2724            .iter()
2725            .map(|x| x.iter().map(|x| x.num_rows()).sum::<usize>())
2726            .sum();
2727
2728        assert_eq!(8, output_partitions.len());
2729        assert_eq!(total_rows, 8 * 50 * 3);
2730
2731        Ok(())
2732    }
2733
2734    #[tokio::test]
2735    async fn many_to_many_range_partition() -> Result<()> {
2736        let schema = test_schema(false);
2737        let partition = create_vec_batches(50);
2738        let partitions = vec![partition.clone(), partition.clone(), partition.clone()];
2739
2740        // create_batch values are [1, 2, 3, 4, 5, 6, 7, 8]; split at 3 and 6 yields
2741        // 2, 3, and 3 rows per batch respectively
2742        let partitioning =
2743            u32_range_partitioning(&schema, SortOptions::default(), vec![3, 6])?;
2744
2745        let output_partitions = repartition(&schema, partitions, partitioning).await?;
2746
2747        assert_eq!(3, output_partitions.len());
2748        assert_eq!(300, partition_row_count(&output_partitions[0]));
2749        assert_eq!(450, partition_row_count(&output_partitions[1]));
2750        assert_eq!(450, partition_row_count(&output_partitions[2]));
2751        assert_eq!(
2752            collect_partition_u32_values(&output_partitions[0])
2753                .into_iter()
2754                .flatten()
2755                .collect::<HashSet<_>>(),
2756            HashSet::from([1, 2])
2757        );
2758        assert_eq!(
2759            collect_partition_u32_values(&output_partitions[1])
2760                .into_iter()
2761                .flatten()
2762                .collect::<HashSet<_>>(),
2763            HashSet::from([3, 4, 5])
2764        );
2765        assert_eq!(
2766            collect_partition_u32_values(&output_partitions[2])
2767                .into_iter()
2768                .flatten()
2769                .collect::<HashSet<_>>(),
2770            HashSet::from([6, 7, 8])
2771        );
2772
2773        Ok(())
2774    }
2775
2776    #[tokio::test]
2777    async fn range_repartition_routes_compound_keys() -> Result<()> {
2778        let schema = Arc::new(Schema::new(vec![
2779            Field::new("a", DataType::UInt32, false),
2780            Field::new("b", DataType::UInt32, false),
2781        ]));
2782        let batch = RecordBatch::try_new(
2783            Arc::clone(&schema),
2784            vec![
2785                Arc::new(UInt32Array::from(vec![5, 10, 10, 10, 10, 15])),
2786                Arc::new(UInt32Array::from(vec![1, 1, 3, 5, 7, 0])),
2787            ],
2788        )?;
2789        let partitioning = Partitioning::Range(RangePartitioning::try_new(
2790            [
2791                PhysicalSortExpr::new(col("a", &schema)?, SortOptions::default()),
2792                PhysicalSortExpr::new(col("b", &schema)?, SortOptions::default()),
2793            ]
2794            .into(),
2795            vec![
2796                SplitPoint::new(vec![
2797                    ScalarValue::UInt32(Some(10)),
2798                    ScalarValue::UInt32(Some(1)),
2799                ]),
2800                SplitPoint::new(vec![
2801                    ScalarValue::UInt32(Some(10)),
2802                    ScalarValue::UInt32(Some(5)),
2803                ]),
2804            ],
2805        )?);
2806
2807        let output_partitions =
2808            repartition(&schema, vec![vec![batch]], partitioning).await?;
2809
2810        assert_eq!(3, output_partitions.len());
2811        assert_eq!(
2812            vec![(5, 1)],
2813            collect_partition_u32_pairs(&output_partitions[0])
2814        );
2815        assert_eq!(
2816            vec![(10, 1), (10, 3)],
2817            collect_partition_u32_pairs(&output_partitions[1])
2818        );
2819        assert_eq!(
2820            vec![(10, 5), (10, 7), (15, 0)],
2821            collect_partition_u32_pairs(&output_partitions[2])
2822        );
2823
2824        Ok(())
2825    }
2826
2827    #[tokio::test]
2828    async fn range_repartition_routes_nulls_asc_nulls_last() -> Result<()> {
2829        let schema = test_schema(true);
2830        let batch = RecordBatch::try_new(
2831            Arc::clone(&schema),
2832            vec![Arc::new(UInt32Array::from(vec![
2833                None,
2834                Some(5),
2835                Some(10),
2836                Some(15),
2837            ]))],
2838        )?;
2839        let partitioning =
2840            u32_range_partitioning(&schema, SortOptions::new(false, false), vec![10])?;
2841
2842        let output_partitions =
2843            repartition(&schema, vec![vec![batch]], partitioning).await?;
2844
2845        assert_eq!(2, output_partitions.len());
2846        assert_eq!(
2847            vec![Some(5)],
2848            collect_partition_u32_values(&output_partitions[0])
2849        );
2850        assert_eq!(
2851            vec![None, Some(10), Some(15)],
2852            collect_partition_u32_values(&output_partitions[1])
2853        );
2854
2855        Ok(())
2856    }
2857
2858    #[tokio::test]
2859    async fn range_repartition_routes_nulls_asc_nulls_first() -> Result<()> {
2860        let schema = test_schema(true);
2861        let batch = RecordBatch::try_new(
2862            Arc::clone(&schema),
2863            vec![Arc::new(UInt32Array::from(vec![
2864                None,
2865                Some(5),
2866                Some(10),
2867                Some(15),
2868            ]))],
2869        )?;
2870        let partitioning =
2871            u32_range_partitioning(&schema, SortOptions::new(false, true), vec![10])?;
2872
2873        let output_partitions =
2874            repartition(&schema, vec![vec![batch]], partitioning).await?;
2875
2876        assert_eq!(2, output_partitions.len());
2877        assert_eq!(
2878            vec![None, Some(5)],
2879            collect_partition_u32_values(&output_partitions[0])
2880        );
2881        assert_eq!(
2882            vec![Some(10), Some(15)],
2883            collect_partition_u32_values(&output_partitions[1])
2884        );
2885
2886        Ok(())
2887    }
2888
2889    #[tokio::test]
2890    async fn range_repartition_routes_rows_asc() -> Result<()> {
2891        let schema = test_schema(false);
2892        let batch = RecordBatch::try_new(
2893            Arc::clone(&schema),
2894            vec![Arc::new(UInt32Array::from(vec![5, 10, 15, 25]))],
2895        )?;
2896        let partitioning =
2897            u32_range_partitioning(&schema, SortOptions::default(), vec![10, 20])?;
2898
2899        let output_partitions =
2900            repartition(&schema, vec![vec![batch]], partitioning).await?;
2901
2902        assert_eq!(3, output_partitions.len());
2903        assert_eq!(
2904            vec![Some(5)],
2905            collect_partition_u32_values(&output_partitions[0])
2906        );
2907        assert_eq!(
2908            vec![Some(10), Some(15)],
2909            collect_partition_u32_values(&output_partitions[1])
2910        );
2911        assert_eq!(
2912            vec![Some(25)],
2913            collect_partition_u32_values(&output_partitions[2])
2914        );
2915
2916        Ok(())
2917    }
2918
2919    #[tokio::test]
2920    async fn range_repartition_routes_rows_desc() -> Result<()> {
2921        let schema = test_schema(false);
2922        let batch = RecordBatch::try_new(
2923            Arc::clone(&schema),
2924            vec![Arc::new(UInt32Array::from(vec![5, 10, 15, 20, 25]))],
2925        )?;
2926        let partitioning =
2927            u32_range_partitioning(&schema, SortOptions::new(true, false), vec![20, 10])?;
2928
2929        let output_partitions =
2930            repartition(&schema, vec![vec![batch]], partitioning).await?;
2931
2932        assert_eq!(3, output_partitions.len());
2933        assert_eq!(
2934            vec![Some(25)],
2935            collect_partition_u32_values(&output_partitions[0])
2936        );
2937        assert_eq!(
2938            vec![Some(15), Some(20)],
2939            collect_partition_u32_values(&output_partitions[1])
2940        );
2941        assert_eq!(
2942            vec![Some(5), Some(10)],
2943            collect_partition_u32_values(&output_partitions[2])
2944        );
2945
2946        Ok(())
2947    }
2948
2949    #[tokio::test]
2950    async fn range_repartition_routes_string_rows() -> Result<()> {
2951        let task_ctx = Arc::new(TaskContext::default());
2952        let batch = RecordBatch::try_from_iter(vec![(
2953            "my_awesome_field",
2954            Arc::new(StringArray::from(vec!["bar", "baz", "foo", "qux"])) as ArrayRef,
2955        )])?;
2956
2957        let schema = batch.schema();
2958        let expr = col("my_awesome_field", &schema)?;
2959        let input = MockExec::new(vec![Ok(batch)], Arc::clone(&schema));
2960        let partitioning = Partitioning::Range(RangePartitioning::try_new(
2961            [PhysicalSortExpr::new_default(expr)].into(),
2962            vec![SplitPoint::new(vec![ScalarValue::Utf8(Some(
2963                "foo".to_string(),
2964            ))])],
2965        )?);
2966        let exec = RepartitionExec::try_new(Arc::new(input), partitioning)?;
2967
2968        let mut partition_0 = Vec::new();
2969        let mut stream = exec.execute(0, Arc::clone(&task_ctx))?;
2970        while let Some(result) = stream.next().await {
2971            partition_0.push(result?);
2972        }
2973
2974        let mut partition_1 = Vec::new();
2975        let mut stream = exec.execute(1, task_ctx)?;
2976        while let Some(result) = stream.next().await {
2977            partition_1.push(result?);
2978        }
2979
2980        assert_eq!(
2981            vec!["bar", "baz"],
2982            collect_partition_string_values(&partition_0)
2983        );
2984        assert_eq!(
2985            vec!["foo", "qux"],
2986            collect_partition_string_values(&partition_1)
2987        );
2988
2989        Ok(())
2990    }
2991
2992    #[test]
2993    fn range_repartition_swaps_with_projection_rewrites_key_index() -> Result<()> {
2994        // Three columns so the projection both narrows the schema (required for
2995        // swap) and moves the range key from @0 to @1.
2996        let schema = Arc::new(Schema::new(vec![
2997            Field::new("id", DataType::UInt32, false),
2998            Field::new("region", DataType::Utf8, false),
2999            Field::new("payload", DataType::UInt32, false),
3000        ]));
3001        let repartition = Arc::new(RepartitionExec::try_new(
3002            Arc::new(EmptyExec::new(Arc::clone(&schema))),
3003            range_partitioning_on_columns(&schema, &["id"], vec![vec![10]])?,
3004        )?);
3005
3006        let projection =
3007            projection_on_columns(&(Arc::clone(&repartition) as _), &["payload", "id"])?;
3008
3009        let swapped = repartition
3010            .try_swapping_with_projection(&projection)?
3011            .expect("swap should succeed when projection keeps the range key");
3012        let swapped_repartition = swapped
3013            .downcast_ref::<RepartitionExec>()
3014            .expect("top node should be RepartitionExec");
3015
3016        assert!(swapped_repartition.input().is::<ProjectionExec>());
3017        let range = expect_range_partitioning(swapped_repartition.partitioning());
3018        assert_eq!(range.ordering()[0].to_string(), "id@1 ASC");
3019        assert_eq!(
3020            range.split_points(),
3021            &[SplitPoint::new(vec![ScalarValue::UInt32(Some(10))])]
3022        );
3023
3024        Ok(())
3025    }
3026
3027    #[test]
3028    fn range_repartition_does_not_swap_when_projection_drops_key() -> Result<()> {
3029        // Drop a simple range key.
3030        let schema = Arc::new(Schema::new(vec![
3031            Field::new("id", DataType::UInt32, false),
3032            Field::new("payload", DataType::UInt32, false),
3033        ]));
3034        let repartition = Arc::new(RepartitionExec::try_new(
3035            Arc::new(EmptyExec::new(Arc::clone(&schema))),
3036            range_partitioning_on_columns(&schema, &["id"], vec![vec![10]])?,
3037        )?);
3038        let projection =
3039            projection_on_columns(&(Arc::clone(&repartition) as _), &["payload"])?;
3040        assert!(
3041            repartition
3042                .try_swapping_with_projection(&projection)?
3043                .is_none()
3044        );
3045
3046        // Drop part of a compound range key.
3047        let schema = Arc::new(Schema::new(vec![
3048            Field::new("a", DataType::UInt32, false),
3049            Field::new("b", DataType::UInt32, false),
3050            Field::new("c", DataType::UInt32, false),
3051        ]));
3052        let repartition = Arc::new(RepartitionExec::try_new(
3053            Arc::new(EmptyExec::new(Arc::clone(&schema))),
3054            range_partitioning_on_columns(&schema, &["a", "b"], vec![vec![10, 1]])?,
3055        )?);
3056        let projection =
3057            projection_on_columns(&(Arc::clone(&repartition) as _), &["a", "c"])?;
3058        assert!(
3059            repartition
3060                .try_swapping_with_projection(&projection)?
3061                .is_none()
3062        );
3063
3064        Ok(())
3065    }
3066
3067    #[test]
3068    fn range_repartition_try_pushdown_sort_when_maintains_order() -> Result<()> {
3069        let schema =
3070            Arc::new(Schema::new(vec![Field::new("id", DataType::UInt32, false)]));
3071        let ordering = LexOrdering::new([PhysicalSortExpr::new(
3072            col("id", &schema)?,
3073            SortOptions::default(),
3074        )])
3075        .expect("ordering must not be empty");
3076
3077        // Multi-partition source with preserve_order: Range maintains input order.
3078        let source = Arc::new(ExactSortPushdownExec::new(
3079            Arc::clone(&schema),
3080            2,
3081            ordering.clone(),
3082        ));
3083        let repartition = Arc::new(
3084            RepartitionExec::try_new(
3085                source,
3086                range_partitioning_on_columns(&schema, &["id"], vec![vec![10]])?,
3087            )?
3088            .with_preserve_order(),
3089        );
3090        assert!(repartition.maintains_input_order()[0]);
3091
3092        match repartition.try_pushdown_sort(ordering.as_ref())? {
3093            SortOrderPushdownResult::Exact { inner } => {
3094                let pushed = inner
3095                    .downcast_ref::<RepartitionExec>()
3096                    .expect("pushdown should keep RepartitionExec");
3097
3098                assert!(pushed.preserve_order());
3099                assert!(pushed.maintains_input_order()[0]);
3100
3101                let range = expect_range_partitioning(pushed.partitioning());
3102                assert_eq!(range.ordering()[0].to_string(), "id@0 ASC");
3103                assert_eq!(
3104                    inner.properties().output_ordering().map(|o| o.to_string()),
3105                    Some(ordering.to_string()),
3106                    "pushed repartition output ordering should match the requested sort"
3107                );
3108            }
3109            other => panic!("expected Exact sort pushdown, got {other:?}"),
3110        }
3111
3112        Ok(())
3113    }
3114
3115    #[test]
3116    fn range_repartition_try_pushdown_sort_unsupported_without_order_maintenance()
3117    -> Result<()> {
3118        let schema =
3119            Arc::new(Schema::new(vec![Field::new("id", DataType::UInt32, false)]));
3120        let ordering = LexOrdering::new([PhysicalSortExpr::new(
3121            col("id", &schema)?,
3122            SortOptions::default(),
3123        )])
3124        .expect("ordering must not be empty");
3125
3126        // Multi-partition source without preserve_order: Range does not maintain order.
3127        let source = Arc::new(ExactSortPushdownExec::new(
3128            Arc::clone(&schema),
3129            2,
3130            ordering.clone(),
3131        ));
3132        let repartition = Arc::new(RepartitionExec::try_new(
3133            source,
3134            range_partitioning_on_columns(&schema, &["id"], vec![vec![10]])?,
3135        )?);
3136        assert!(!repartition.maintains_input_order()[0]);
3137
3138        assert!(matches!(
3139            repartition.try_pushdown_sort(ordering.as_ref())?,
3140            SortOrderPushdownResult::Unsupported
3141        ));
3142
3143        Ok(())
3144    }
3145
3146    fn range_partitioning_on_columns(
3147        schema: &SchemaRef,
3148        key_columns: &[&str],
3149        split_points: Vec<Vec<u32>>,
3150    ) -> Result<Partitioning> {
3151        let Some(ordering) = LexOrdering::new(
3152            key_columns
3153                .iter()
3154                .map(|name| {
3155                    Ok(PhysicalSortExpr::new(
3156                        col(name, schema)?,
3157                        SortOptions::default(),
3158                    ))
3159                })
3160                .collect::<Result<Vec<_>>>()?,
3161        ) else {
3162            return exec_err!("range ordering must not be empty");
3163        };
3164        Ok(Partitioning::Range(RangePartitioning::try_new(
3165            ordering,
3166            split_points
3167                .into_iter()
3168                .map(|values| {
3169                    SplitPoint::new(
3170                        values
3171                            .into_iter()
3172                            .map(|value| ScalarValue::UInt32(Some(value)))
3173                            .collect(),
3174                    )
3175                })
3176                .collect(),
3177        )?))
3178    }
3179
3180    fn projection_on_columns(
3181        input: &Arc<dyn ExecutionPlan>,
3182        names: &[&str],
3183    ) -> Result<ProjectionExec> {
3184        let exprs = names
3185            .iter()
3186            .map(|name| {
3187                Ok(ProjectionExpr {
3188                    expr: col(name, &input.schema())?,
3189                    alias: (*name).to_string(),
3190                })
3191            })
3192            .collect::<Result<Vec<_>>>()?;
3193        ProjectionExec::try_new(exprs, Arc::clone(input))
3194    }
3195
3196    fn expect_range_partitioning(partitioning: &Partitioning) -> &RangePartitioning {
3197        match partitioning {
3198            Partitioning::Range(range) => range,
3199            other => panic!("expected Range partitioning, got {other:?}"),
3200        }
3201    }
3202
3203    /// Test source that claims Exact support for any sort pushdown request.
3204    #[derive(Debug, Clone)]
3205    struct ExactSortPushdownExec {
3206        cache: Arc<PlanProperties>,
3207    }
3208
3209    impl ExactSortPushdownExec {
3210        fn new(schema: SchemaRef, num_partitions: usize, ordering: LexOrdering) -> Self {
3211            use crate::execution_plan::{Boundedness, EmissionType};
3212            Self {
3213                cache: Arc::new(PlanProperties::new(
3214                    EquivalenceProperties::new_with_orderings(schema, [ordering]),
3215                    Partitioning::UnknownPartitioning(num_partitions),
3216                    EmissionType::Incremental,
3217                    Boundedness::Bounded,
3218                )),
3219            }
3220        }
3221    }
3222
3223    impl DisplayAs for ExactSortPushdownExec {
3224        fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result {
3225            write!(f, "ExactSortPushdownExec")
3226        }
3227    }
3228
3229    impl ExecutionPlan for ExactSortPushdownExec {
3230        fn name(&self) -> &str {
3231            "ExactSortPushdownExec"
3232        }
3233
3234        fn properties(&self) -> &Arc<PlanProperties> {
3235            &self.cache
3236        }
3237
3238        fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
3239            vec![]
3240        }
3241
3242        fn apply_expressions(
3243            &self,
3244            _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
3245        ) -> Result<TreeNodeRecursion> {
3246            Ok(TreeNodeRecursion::Continue)
3247        }
3248
3249        fn replace_children(
3250            self: Arc<Self>,
3251            _: Vec<Arc<dyn ExecutionPlan>>,
3252            _: ReplaceChildrenOptions,
3253        ) -> Result<Arc<dyn ExecutionPlan>> {
3254            Ok(self)
3255        }
3256
3257        fn with_new_children(
3258            self: Arc<Self>,
3259            children: Vec<Arc<dyn ExecutionPlan>>,
3260        ) -> Result<Arc<dyn ExecutionPlan>> {
3261            self.replace_children(
3262                children,
3263                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
3264            )
3265        }
3266
3267        fn execute(
3268            &self,
3269            _partition: usize,
3270            _context: Arc<TaskContext>,
3271        ) -> Result<SendableRecordBatchStream> {
3272            Ok(Box::pin(EmptyRecordBatchStream::new(self.schema())))
3273        }
3274
3275        fn try_pushdown_sort(
3276            &self,
3277            _order: &[PhysicalSortExpr],
3278        ) -> Result<SortOrderPushdownResult<Arc<dyn ExecutionPlan>>> {
3279            Ok(SortOrderPushdownResult::Exact {
3280                inner: Arc::new(self.clone()),
3281            })
3282        }
3283    }
3284
3285    #[tokio::test]
3286    async fn test_repartition_with_coalescing() -> Result<()> {
3287        let schema = test_schema(false);
3288        // create 50 batches, each having 8 rows
3289        let partition = create_vec_batches(50);
3290        let partitions = vec![partition.clone(), partition.clone()];
3291        let partitioning = Partitioning::RoundRobinBatch(1);
3292
3293        let session_config = SessionConfig::new().with_batch_size(200);
3294        let task_ctx = TaskContext::default().with_session_config(session_config);
3295        let task_ctx = Arc::new(task_ctx);
3296
3297        // create physical plan
3298        let exec = TestMemoryExec::try_new_exec(&partitions, Arc::clone(&schema), None)?;
3299        let exec = RepartitionExec::try_new(exec, partitioning)?;
3300
3301        for i in 0..exec.partitioning().partition_count() {
3302            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
3303            while let Some(result) = stream.next().await {
3304                let batch = result?;
3305                assert_eq!(200, batch.num_rows());
3306            }
3307        }
3308        Ok(())
3309    }
3310
3311    #[tokio::test]
3312    async fn unbounded_input_emits_before_batch_size() -> Result<()> {
3313        let schema = test_schema(false);
3314        let batch = create_batch();
3315        let source = Arc::new(StreamingTableExec::try_new(
3316            Arc::clone(&schema),
3317            vec![Arc::new(UnboundedTestPartition {
3318                schema: Arc::clone(&schema),
3319                batch: batch.clone(),
3320            })],
3321            None,
3322            vec![],
3323            true,
3324            None,
3325        )?);
3326        let exec = RepartitionExec::try_new(source, Partitioning::RoundRobinBatch(1))?;
3327        let session_config = SessionConfig::new().with_batch_size(batch.num_rows() * 2);
3328        let task_ctx =
3329            Arc::new(TaskContext::default().with_session_config(session_config));
3330
3331        let mut stream = exec.execute(0, task_ctx)?;
3332        let output =
3333            tokio::time::timeout(std::time::Duration::from_secs(5), stream.next())
3334                .await
3335                .expect("unbounded repartition withheld a partial batch")
3336                .expect("unbounded input ended unexpectedly")?;
3337
3338        assert_eq!(batch, output);
3339        Ok(())
3340    }
3341
3342    fn test_schema(nullable: bool) -> Arc<Schema> {
3343        Arc::new(Schema::new(vec![Field::new(
3344            "c0",
3345            DataType::UInt32,
3346            nullable,
3347        )]))
3348    }
3349
3350    fn u32_range_partitioning(
3351        schema: &SchemaRef,
3352        sort_options: SortOptions,
3353        split_values: Vec<u32>,
3354    ) -> Result<Partitioning> {
3355        let expr = col("c0", schema)?;
3356        Ok(Partitioning::Range(RangePartitioning::try_new(
3357            [PhysicalSortExpr::new(expr, sort_options)].into(),
3358            split_values
3359                .into_iter()
3360                .map(|value| SplitPoint::new(vec![ScalarValue::UInt32(Some(value))]))
3361                .collect(),
3362        )?))
3363    }
3364
3365    fn partition_row_count(batches: &[RecordBatch]) -> usize {
3366        batches.iter().map(|batch| batch.num_rows()).sum()
3367    }
3368
3369    fn collect_partition_u32_values(batches: &[RecordBatch]) -> Vec<Option<u32>> {
3370        batches
3371            .iter()
3372            .flat_map(|batch| {
3373                let array =
3374                    as_uint32_array(batch.column(0)).expect("expected UInt32 column");
3375                (0..array.len())
3376                    .map(|idx| {
3377                        if array.is_null(idx) {
3378                            None
3379                        } else {
3380                            Some(array.value(idx))
3381                        }
3382                    })
3383                    .collect::<Vec<_>>()
3384            })
3385            .collect()
3386    }
3387
3388    fn collect_partition_u32_pairs(batches: &[RecordBatch]) -> Vec<(u32, u32)> {
3389        batches
3390            .iter()
3391            .flat_map(|batch| {
3392                let a = as_uint32_array(batch.column(0)).expect("expected UInt32 column");
3393                let b = as_uint32_array(batch.column(1)).expect("expected UInt32 column");
3394                (0..a.len())
3395                    .map(|idx| (a.value(idx), b.value(idx)))
3396                    .collect::<Vec<_>>()
3397            })
3398            .collect()
3399    }
3400
3401    fn collect_partition_string_values(batches: &[RecordBatch]) -> Vec<&str> {
3402        batches
3403            .iter()
3404            .flat_map(|batch| {
3405                let array =
3406                    as_string_array(batch.column(0)).expect("expected Utf8 column");
3407                (0..array.len())
3408                    .map(|idx| array.value(idx))
3409                    .collect::<Vec<_>>()
3410            })
3411            .collect()
3412    }
3413
3414    async fn repartition(
3415        schema: &SchemaRef,
3416        input_partitions: Vec<Vec<RecordBatch>>,
3417        partitioning: Partitioning,
3418    ) -> Result<Vec<Vec<RecordBatch>>> {
3419        let task_ctx = Arc::new(TaskContext::default());
3420        // create physical plan
3421        let exec =
3422            TestMemoryExec::try_new_exec(&input_partitions, Arc::clone(schema), None)?;
3423        let exec = RepartitionExec::try_new(exec, partitioning)?;
3424
3425        // execute and collect results
3426        let mut output_partitions = vec![];
3427        for i in 0..exec.partitioning().partition_count() {
3428            // execute this *output* partition and collect all batches
3429            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
3430            let mut batches = vec![];
3431            while let Some(result) = stream.next().await {
3432                batches.push(result?);
3433            }
3434            output_partitions.push(batches);
3435        }
3436        Ok(output_partitions)
3437    }
3438
3439    #[tokio::test]
3440    async fn many_to_many_round_robin_within_tokio_task() -> Result<()> {
3441        let handle: SpawnedTask<Result<Vec<Vec<RecordBatch>>>> =
3442            SpawnedTask::spawn(async move {
3443                // define input partitions
3444                let schema = test_schema(false);
3445                let partition = create_vec_batches(50);
3446                let partitions =
3447                    vec![partition.clone(), partition.clone(), partition.clone()];
3448
3449                // repartition from 3 input to 5 output
3450                repartition(&schema, partitions, Partitioning::RoundRobinBatch(5)).await
3451            });
3452
3453        let output_partitions = handle.join().await.unwrap().unwrap();
3454
3455        let total_rows_per_partition = 8 * 50 * 3 / 5;
3456        assert_eq!(5, output_partitions.len());
3457        for partition in output_partitions {
3458            assert_eq!(1, partition.len());
3459            assert_eq!(total_rows_per_partition, partition[0].num_rows());
3460        }
3461
3462        Ok(())
3463    }
3464
3465    #[tokio::test]
3466    async fn unsupported_partitioning() {
3467        let task_ctx = Arc::new(TaskContext::default());
3468        // have to send at least one batch through to provoke error
3469        let batch = RecordBatch::try_from_iter(vec![(
3470            "my_awesome_field",
3471            Arc::new(StringArray::from(vec!["foo", "bar"])) as ArrayRef,
3472        )])
3473        .unwrap();
3474
3475        let schema = batch.schema();
3476        let input = MockExec::new(vec![Ok(batch)], schema);
3477        // This generates an error (partitioning type not supported)
3478        // but only after the plan is executed. The error should be
3479        // returned and no results produced
3480        let partitioning = Partitioning::UnknownPartitioning(1);
3481        let exec = RepartitionExec::try_new(Arc::new(input), partitioning).unwrap();
3482        let output_stream = exec.execute(0, task_ctx).unwrap();
3483
3484        // Expect that an error is returned
3485        let result_string = crate::common::collect(output_stream)
3486            .await
3487            .unwrap_err()
3488            .to_string();
3489        assert!(
3490            result_string
3491                .contains("Unsupported repartitioning scheme UnknownPartitioning(1)"),
3492            "actual: {result_string}"
3493        );
3494    }
3495
3496    #[tokio::test]
3497    async fn error_for_input_exec() {
3498        // This generates an error on a call to execute. The error
3499        // should be returned and no results produced.
3500
3501        let task_ctx = Arc::new(TaskContext::default());
3502        let input = ErrorExec::new();
3503        let partitioning = Partitioning::RoundRobinBatch(1);
3504        let exec = RepartitionExec::try_new(Arc::new(input), partitioning).unwrap();
3505
3506        // Expect that an error is returned
3507        let result_string = exec.execute(0, task_ctx).err().unwrap().to_string();
3508
3509        assert!(
3510            result_string.contains("ErrorExec, unsurprisingly, errored in partition 0"),
3511            "actual: {result_string}"
3512        );
3513    }
3514
3515    #[tokio::test]
3516    async fn repartition_with_error_in_stream() {
3517        let task_ctx = Arc::new(TaskContext::default());
3518        let batch = RecordBatch::try_from_iter(vec![(
3519            "my_awesome_field",
3520            Arc::new(StringArray::from(vec!["foo", "bar"])) as ArrayRef,
3521        )])
3522        .unwrap();
3523
3524        // input stream returns one good batch and then one error. The
3525        // error should be returned.
3526        let err = exec_err!("bad data error");
3527
3528        let schema = batch.schema();
3529        let input = MockExec::new(vec![Ok(batch), err], schema);
3530        let partitioning = Partitioning::RoundRobinBatch(1);
3531        let exec = RepartitionExec::try_new(Arc::new(input), partitioning).unwrap();
3532
3533        // Note: this should pass (the stream can be created) but the
3534        // error when the input is executed should get passed back
3535        let output_stream = exec.execute(0, task_ctx).unwrap();
3536
3537        // Expect that an error is returned
3538        let result_string = crate::common::collect(output_stream)
3539            .await
3540            .unwrap_err()
3541            .to_string();
3542        assert!(
3543            result_string.contains("bad data error"),
3544            "actual: {result_string}"
3545        );
3546    }
3547
3548    #[tokio::test]
3549    async fn repartition_with_delayed_stream() {
3550        let task_ctx = Arc::new(TaskContext::default());
3551        let batch1 = RecordBatch::try_from_iter(vec![(
3552            "my_awesome_field",
3553            Arc::new(StringArray::from(vec!["foo", "bar"])) as ArrayRef,
3554        )])
3555        .unwrap();
3556
3557        let batch2 = RecordBatch::try_from_iter(vec![(
3558            "my_awesome_field",
3559            Arc::new(StringArray::from(vec!["frob", "baz"])) as ArrayRef,
3560        )])
3561        .unwrap();
3562
3563        // The mock exec doesn't return immediately (instead it
3564        // requires the input to wait at least once)
3565        let schema = batch1.schema();
3566        let expected_batches = vec![batch1.clone(), batch2.clone()];
3567        let input = MockExec::new(vec![Ok(batch1), Ok(batch2)], schema);
3568        let partitioning = Partitioning::RoundRobinBatch(1);
3569
3570        let exec = RepartitionExec::try_new(Arc::new(input), partitioning).unwrap();
3571
3572        assert_snapshot!(batches_to_sort_string(&expected_batches), @r"
3573        +------------------+
3574        | my_awesome_field |
3575        +------------------+
3576        | bar              |
3577        | baz              |
3578        | foo              |
3579        | frob             |
3580        +------------------+
3581        ");
3582
3583        let output_stream = exec.execute(0, task_ctx).unwrap();
3584        let batches = crate::common::collect(output_stream).await.unwrap();
3585
3586        assert_snapshot!(batches_to_sort_string(&batches), @r"
3587        +------------------+
3588        | my_awesome_field |
3589        +------------------+
3590        | bar              |
3591        | baz              |
3592        | foo              |
3593        | frob             |
3594        +------------------+
3595        ");
3596    }
3597
3598    #[tokio::test]
3599    async fn robin_repartition_with_dropping_output_stream() {
3600        let task_ctx = Arc::new(TaskContext::default());
3601        let partitioning = Partitioning::RoundRobinBatch(2);
3602        // The barrier exec waits to be pinged
3603        // requires the input to wait at least once)
3604        let input = Arc::new(make_barrier_exec());
3605
3606        // partition into two output streams
3607        let exec = RepartitionExec::try_new(
3608            Arc::clone(&input) as Arc<dyn ExecutionPlan>,
3609            partitioning,
3610        )
3611        .unwrap();
3612
3613        let output_stream0 = exec.execute(0, Arc::clone(&task_ctx)).unwrap();
3614        let output_stream1 = exec.execute(1, Arc::clone(&task_ctx)).unwrap();
3615
3616        // now, purposely drop output stream 0
3617        // *before* any outputs are produced
3618        drop(output_stream0);
3619
3620        // Now, start sending input
3621        let mut background_task = JoinSet::new();
3622        background_task.spawn(async move {
3623            input.wait().await;
3624        });
3625
3626        // output stream 1 should *not* error and have one of the input batches
3627        let batches = crate::common::collect(output_stream1).await.unwrap();
3628
3629        assert_snapshot!(batches_to_sort_string(&batches), @r"
3630        +------------------+
3631        | my_awesome_field |
3632        +------------------+
3633        | baz              |
3634        | frob             |
3635        | gar              |
3636        | goo              |
3637        +------------------+
3638        ");
3639    }
3640
3641    #[tokio::test]
3642    // As the hash results might be different on different platforms or
3643    // with different compilers, we will compare the same execution with
3644    // and without dropping the output stream.
3645    async fn hash_repartition_with_dropping_output_stream() {
3646        let task_ctx = Arc::new(TaskContext::default());
3647        let partitioning = Partitioning::Hash(
3648            vec![Arc::new(crate::expressions::Column::new(
3649                "my_awesome_field",
3650                0,
3651            ))],
3652            2,
3653        );
3654
3655        // We first collect the results without dropping the output stream.
3656        let input = Arc::new(make_barrier_exec());
3657        let exec = RepartitionExec::try_new(
3658            Arc::clone(&input) as Arc<dyn ExecutionPlan>,
3659            partitioning.clone(),
3660        )
3661        .unwrap();
3662        let output_stream1 = exec.execute(1, Arc::clone(&task_ctx)).unwrap();
3663        let mut background_task = JoinSet::new();
3664        background_task.spawn(async move {
3665            input.wait().await;
3666        });
3667        let batches_without_drop = crate::common::collect(output_stream1).await.unwrap();
3668
3669        // run some checks on the result
3670        let items_vec = str_batches_to_vec(&batches_without_drop);
3671        let items_set: HashSet<&str> = items_vec.iter().copied().collect();
3672        assert_eq!(items_vec.len(), items_set.len());
3673        let source_str_set: HashSet<&str> =
3674            ["foo", "bar", "frob", "baz", "goo", "gar", "grob", "gaz"]
3675                .iter()
3676                .copied()
3677                .collect();
3678        assert_eq!(items_set.difference(&source_str_set).count(), 0);
3679
3680        // Now do the same but dropping the stream before waiting for the barrier
3681        let input = Arc::new(make_barrier_exec());
3682        let exec = RepartitionExec::try_new(
3683            Arc::clone(&input) as Arc<dyn ExecutionPlan>,
3684            partitioning,
3685        )
3686        .unwrap();
3687        let output_stream0 = exec.execute(0, Arc::clone(&task_ctx)).unwrap();
3688        let output_stream1 = exec.execute(1, Arc::clone(&task_ctx)).unwrap();
3689        // now, purposely drop output stream 0
3690        // *before* any outputs are produced
3691        drop(output_stream0);
3692        let mut background_task = JoinSet::new();
3693        background_task.spawn(async move {
3694            input.wait().await;
3695        });
3696        let batches_with_drop = crate::common::collect(output_stream1).await.unwrap();
3697
3698        let items_vec_with_drop = str_batches_to_vec(&batches_with_drop);
3699        let items_set_with_drop: HashSet<&str> =
3700            items_vec_with_drop.iter().copied().collect();
3701        assert_eq!(
3702            items_set_with_drop.symmetric_difference(&items_set).count(),
3703            0
3704        );
3705    }
3706
3707    fn str_batches_to_vec(batches: &[RecordBatch]) -> Vec<&str> {
3708        batches
3709            .iter()
3710            .flat_map(|batch| {
3711                assert_eq!(batch.columns().len(), 1);
3712                let string_array = as_string_array(batch.column(0))
3713                    .expect("Unexpected type for repartitioned batch");
3714
3715                string_array
3716                    .iter()
3717                    .map(|v| v.expect("Unexpected null"))
3718                    .collect::<Vec<_>>()
3719            })
3720            .collect::<Vec<_>>()
3721    }
3722
3723    /// Create a BarrierExec that returns two partitions of two batches each
3724    fn make_barrier_exec() -> BarrierExec {
3725        let batch1 = RecordBatch::try_from_iter(vec![(
3726            "my_awesome_field",
3727            Arc::new(StringArray::from(vec!["foo", "bar"])) as ArrayRef,
3728        )])
3729        .unwrap();
3730
3731        let batch2 = RecordBatch::try_from_iter(vec![(
3732            "my_awesome_field",
3733            Arc::new(StringArray::from(vec!["frob", "baz"])) as ArrayRef,
3734        )])
3735        .unwrap();
3736
3737        let batch3 = RecordBatch::try_from_iter(vec![(
3738            "my_awesome_field",
3739            Arc::new(StringArray::from(vec!["goo", "gar"])) as ArrayRef,
3740        )])
3741        .unwrap();
3742
3743        let batch4 = RecordBatch::try_from_iter(vec![(
3744            "my_awesome_field",
3745            Arc::new(StringArray::from(vec!["grob", "gaz"])) as ArrayRef,
3746        )])
3747        .unwrap();
3748
3749        // The barrier exec waits to be pinged
3750        // requires the input to wait at least once)
3751        let schema = batch1.schema();
3752        BarrierExec::new(vec![vec![batch1, batch2], vec![batch3, batch4]], schema)
3753    }
3754
3755    #[tokio::test]
3756    async fn test_drop_cancel() -> Result<()> {
3757        let task_ctx = Arc::new(TaskContext::default());
3758        let schema =
3759            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, true)]));
3760
3761        let blocking_exec = Arc::new(BlockingExec::new(Arc::clone(&schema), 2));
3762        let refs = blocking_exec.refs();
3763        let repartition_exec = Arc::new(RepartitionExec::try_new(
3764            blocking_exec,
3765            Partitioning::UnknownPartitioning(1),
3766        )?);
3767
3768        let fut = collect(repartition_exec, task_ctx);
3769        let mut fut = fut.boxed();
3770
3771        assert_is_pending(&mut fut);
3772        drop(fut);
3773        assert_strong_count_converges_to_zero(refs).await;
3774
3775        Ok(())
3776    }
3777
3778    #[tokio::test]
3779    async fn hash_repartition_avoid_empty_batch() -> Result<()> {
3780        let task_ctx = Arc::new(TaskContext::default());
3781        let batch = RecordBatch::try_from_iter(vec![(
3782            "a",
3783            Arc::new(StringArray::from(vec!["foo"])) as ArrayRef,
3784        )])
3785        .unwrap();
3786        let partitioning = Partitioning::Hash(
3787            vec![Arc::new(crate::expressions::Column::new("a", 0))],
3788            2,
3789        );
3790        let schema = batch.schema();
3791        let input = MockExec::new(vec![Ok(batch)], schema);
3792        let exec = RepartitionExec::try_new(Arc::new(input), partitioning).unwrap();
3793        let output_stream0 = exec.execute(0, Arc::clone(&task_ctx)).unwrap();
3794        let batch0 = crate::common::collect(output_stream0).await.unwrap();
3795        let output_stream1 = exec.execute(1, Arc::clone(&task_ctx)).unwrap();
3796        let batch1 = crate::common::collect(output_stream1).await.unwrap();
3797        assert!(batch0.is_empty() || batch1.is_empty());
3798        Ok(())
3799    }
3800
3801    #[tokio::test]
3802    async fn repartition_with_spilling() -> Result<()> {
3803        // Test that repartition successfully spills to disk when memory is constrained
3804        let schema = test_schema(false);
3805        let partition = create_vec_batches(50);
3806        let input_partitions = vec![partition];
3807        let partitioning = Partitioning::RoundRobinBatch(4);
3808
3809        // Set up context with very tight memory limit to force spilling
3810        let runtime = RuntimeEnvBuilder::default()
3811            .with_memory_limit(1, 1.0)
3812            .build_arc()?;
3813
3814        let task_ctx = TaskContext::default().with_runtime(runtime);
3815        let task_ctx = Arc::new(task_ctx);
3816
3817        // create physical plan
3818        let exec =
3819            TestMemoryExec::try_new_exec(&input_partitions, Arc::clone(&schema), None)?;
3820        let exec = RepartitionExec::try_new(exec, partitioning)?;
3821
3822        // Collect all partitions - should succeed by spilling to disk
3823        let mut total_rows = 0;
3824        for i in 0..exec.partitioning().partition_count() {
3825            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
3826            while let Some(result) = stream.next().await {
3827                let batch = result?;
3828                total_rows += batch.num_rows();
3829            }
3830        }
3831
3832        // Verify we got all the data (50 batches * 8 rows each)
3833        assert_eq!(total_rows, 50 * 8);
3834
3835        // Verify spilling metrics to confirm spilling actually happened
3836        let metrics = exec.metrics().unwrap();
3837        assert!(
3838            metrics.spill_count().unwrap() > 0,
3839            "Expected spill_count > 0, but got {:?}",
3840            metrics.spill_count()
3841        );
3842        println!("Spilled {} times", metrics.spill_count().unwrap());
3843        assert!(
3844            metrics.spilled_bytes().unwrap() > 0,
3845            "Expected spilled_bytes > 0, but got {:?}",
3846            metrics.spilled_bytes()
3847        );
3848        println!(
3849            "Spilled {} bytes in {} spills",
3850            metrics.spilled_bytes().unwrap(),
3851            metrics.spill_count().unwrap()
3852        );
3853        assert!(
3854            metrics.spilled_rows().unwrap() > 0,
3855            "Expected spilled_rows > 0, but got {:?}",
3856            metrics.spilled_rows()
3857        );
3858        println!("Spilled {} rows", metrics.spilled_rows().unwrap());
3859
3860        Ok(())
3861    }
3862
3863    #[tokio::test]
3864    async fn repartition_with_partial_spilling() -> Result<()> {
3865        // Test that repartition can handle partial spilling (some batches in memory, some spilled)
3866        let schema = test_schema(false);
3867        let partition = create_vec_batches(50);
3868        let input_partitions = vec![partition];
3869        let partitioning = Partitioning::RoundRobinBatch(4);
3870
3871        // With `batch_size = 1024` and a single UInt32 column, each
3872        // coalesced residual is ~4 KiB. An 8 KiB pool fits one and forces
3873        // the rest to spill.
3874        let runtime = RuntimeEnvBuilder::default()
3875            .with_memory_limit(8 * 1024, 1.0)
3876            .build_arc()?;
3877
3878        let session_config = SessionConfig::new().with_batch_size(1024);
3879        let task_ctx = TaskContext::default()
3880            .with_runtime(runtime)
3881            .with_session_config(session_config);
3882        let task_ctx = Arc::new(task_ctx);
3883
3884        // create physical plan
3885        let exec =
3886            TestMemoryExec::try_new_exec(&input_partitions, Arc::clone(&schema), None)?;
3887        let exec = RepartitionExec::try_new(exec, partitioning)?;
3888
3889        // Collect all partitions - should succeed with partial spilling
3890        let mut total_rows = 0;
3891        for i in 0..exec.partitioning().partition_count() {
3892            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
3893            while let Some(result) = stream.next().await {
3894                let batch = result?;
3895                total_rows += batch.num_rows();
3896            }
3897        }
3898
3899        // Verify we got all the data (50 batches * 8 rows each)
3900        assert_eq!(total_rows, 50 * 8);
3901
3902        // Verify partial spilling metrics
3903        let metrics = exec.metrics().unwrap();
3904        let spill_count = metrics.spill_count().unwrap();
3905        let spilled_rows = metrics.spilled_rows().unwrap();
3906        let spilled_bytes = metrics.spilled_bytes().unwrap();
3907
3908        assert!(
3909            spill_count > 0,
3910            "Expected some spilling to occur, but got spill_count={spill_count}"
3911        );
3912        assert!(
3913            spilled_rows > 0 && spilled_rows < total_rows,
3914            "Expected partial spilling (0 < spilled_rows < {total_rows}), but got spilled_rows={spilled_rows}"
3915        );
3916        assert!(
3917            spilled_bytes > 0,
3918            "Expected some bytes to be spilled, but got spilled_bytes={spilled_bytes}"
3919        );
3920
3921        println!(
3922            "Partial spilling: spilled {} out of {} rows ({:.1}%) in {} spills, {} bytes",
3923            spilled_rows,
3924            total_rows,
3925            (spilled_rows as f64 / total_rows as f64) * 100.0,
3926            spill_count,
3927            spilled_bytes
3928        );
3929
3930        Ok(())
3931    }
3932
3933    #[tokio::test]
3934    async fn repartition_without_spilling() -> Result<()> {
3935        // Test that repartition does not spill when there's ample memory
3936        let schema = test_schema(false);
3937        let partition = create_vec_batches(50);
3938        let input_partitions = vec![partition];
3939        let partitioning = Partitioning::RoundRobinBatch(4);
3940
3941        // Set up context with generous memory limit - no spilling should occur
3942        let runtime = RuntimeEnvBuilder::default()
3943            .with_memory_limit(10 * 1024 * 1024, 1.0) // 10MB
3944            .build_arc()?;
3945
3946        let task_ctx = TaskContext::default().with_runtime(runtime);
3947        let task_ctx = Arc::new(task_ctx);
3948
3949        // create physical plan
3950        let exec =
3951            TestMemoryExec::try_new_exec(&input_partitions, Arc::clone(&schema), None)?;
3952        let exec = RepartitionExec::try_new(exec, partitioning)?;
3953
3954        // Collect all partitions - should succeed without spilling
3955        let mut total_rows = 0;
3956        for i in 0..exec.partitioning().partition_count() {
3957            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
3958            while let Some(result) = stream.next().await {
3959                let batch = result?;
3960                total_rows += batch.num_rows();
3961            }
3962        }
3963
3964        // Verify we got all the data (50 batches * 8 rows each)
3965        assert_eq!(total_rows, 50 * 8);
3966
3967        // Verify no spilling occurred
3968        let metrics = exec.metrics().unwrap();
3969        assert_eq!(
3970            metrics.spill_count(),
3971            Some(0),
3972            "Expected no spilling, but got spill_count={:?}",
3973            metrics.spill_count()
3974        );
3975        assert_eq!(
3976            metrics.spilled_bytes(),
3977            Some(0),
3978            "Expected no bytes spilled, but got spilled_bytes={:?}",
3979            metrics.spilled_bytes()
3980        );
3981        assert_eq!(
3982            metrics.spilled_rows(),
3983            Some(0),
3984            "Expected no rows spilled, but got spilled_rows={:?}",
3985            metrics.spilled_rows()
3986        );
3987
3988        println!("No spilling occurred - all data processed in memory");
3989
3990        Ok(())
3991    }
3992
3993    #[tokio::test]
3994    async fn oom() -> Result<()> {
3995        use datafusion_execution::disk_manager::{DiskManagerBuilder, DiskManagerMode};
3996
3997        // Test that repartition fails with OOM when disk manager is disabled
3998        let schema = test_schema(false);
3999        let partition = create_vec_batches(50);
4000        let input_partitions = vec![partition];
4001        let partitioning = Partitioning::RoundRobinBatch(4);
4002
4003        // Setup context with memory limit but NO disk manager (explicitly disabled)
4004        let runtime = RuntimeEnvBuilder::default()
4005            .with_memory_limit(1, 1.0)
4006            .with_disk_manager_builder(
4007                DiskManagerBuilder::default().with_mode(DiskManagerMode::Disabled),
4008            )
4009            .build_arc()?;
4010
4011        let task_ctx = TaskContext::default().with_runtime(runtime);
4012        let task_ctx = Arc::new(task_ctx);
4013
4014        // create physical plan
4015        let exec =
4016            TestMemoryExec::try_new_exec(&input_partitions, Arc::clone(&schema), None)?;
4017        let exec = RepartitionExec::try_new(exec, partitioning)?;
4018
4019        // Attempt to execute - should fail with ResourcesExhausted error
4020        for i in 0..exec.partitioning().partition_count() {
4021            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
4022            let err = stream.next().await.unwrap().unwrap_err();
4023            let err = err.find_root();
4024            assert!(
4025                matches!(err, DataFusionError::ResourcesExhausted(_)),
4026                "Wrong error type: {err}",
4027            );
4028        }
4029
4030        Ok(())
4031    }
4032
4033    /// Create vector batches
4034    fn create_vec_batches(n: usize) -> Vec<RecordBatch> {
4035        let batch = create_batch();
4036        std::iter::repeat_n(batch, n).collect()
4037    }
4038
4039    /// Create batch
4040    fn create_batch() -> RecordBatch {
4041        let schema = test_schema(false);
4042        RecordBatch::try_new(
4043            schema,
4044            vec![Arc::new(UInt32Array::from(vec![1, 2, 3, 4, 5, 6, 7, 8]))],
4045        )
4046        .unwrap()
4047    }
4048
4049    /// Create batches with sequential values for ordering tests
4050    fn create_ordered_batches(num_batches: usize) -> Vec<RecordBatch> {
4051        let schema = test_schema(false);
4052        (0..num_batches)
4053            .map(|i| {
4054                let start = (i * 8) as u32;
4055                RecordBatch::try_new(
4056                    Arc::clone(&schema),
4057                    vec![Arc::new(UInt32Array::from(
4058                        (start..start + 8).collect::<Vec<_>>(),
4059                    ))],
4060                )
4061                .unwrap()
4062            })
4063            .collect()
4064    }
4065
4066    #[tokio::test]
4067    async fn test_repartition_ordering_with_spilling() -> Result<()> {
4068        // Test that repartition preserves ordering when spilling occurs
4069        // This tests the state machine fix where we must block on spill_stream
4070        // when a Spilled marker is received, rather than continuing to poll the channel
4071
4072        let schema = test_schema(false);
4073        // Create batches with sequential values: batch 0 has [0,1,2,3,4,5,6,7],
4074        // batch 1 has [8,9,10,11,12,13,14,15], etc.
4075        let partition = create_ordered_batches(20);
4076        let input_partitions = vec![partition];
4077
4078        // Use RoundRobinBatch to ensure predictable ordering
4079        let partitioning = Partitioning::RoundRobinBatch(2);
4080
4081        // Set up context with very tight memory limit to force spilling
4082        let runtime = RuntimeEnvBuilder::default()
4083            .with_memory_limit(1, 1.0)
4084            .build_arc()?;
4085
4086        let task_ctx = TaskContext::default().with_runtime(runtime);
4087        let task_ctx = Arc::new(task_ctx);
4088
4089        // create physical plan
4090        let exec =
4091            TestMemoryExec::try_new_exec(&input_partitions, Arc::clone(&schema), None)?;
4092        let exec = RepartitionExec::try_new(exec, partitioning)?;
4093
4094        // Collect all output partitions
4095        let mut all_batches = Vec::new();
4096        for i in 0..exec.partitioning().partition_count() {
4097            let mut partition_batches = Vec::new();
4098            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
4099            while let Some(result) = stream.next().await {
4100                let batch = result?;
4101                partition_batches.push(batch);
4102            }
4103            all_batches.push(partition_batches);
4104        }
4105
4106        // Verify spilling occurred
4107        let metrics = exec.metrics().unwrap();
4108        assert!(
4109            metrics.spill_count().unwrap() > 0,
4110            "Expected spilling to occur, but spill_count = 0"
4111        );
4112
4113        // Verify ordering is preserved within each partition
4114        // With RoundRobinBatch, even batches go to partition 0, odd batches to partition 1
4115        for (partition_idx, batches) in all_batches.iter().enumerate() {
4116            let mut last_value = None;
4117            for batch in batches {
4118                let array = batch
4119                    .column(0)
4120                    .as_any()
4121                    .downcast_ref::<UInt32Array>()
4122                    .unwrap();
4123
4124                for i in 0..array.len() {
4125                    let value = array.value(i);
4126                    if let Some(last) = last_value {
4127                        assert!(
4128                            value > last,
4129                            "Ordering violated in partition {partition_idx}: {value} is not greater than {last}"
4130                        );
4131                    }
4132                    last_value = Some(value);
4133                }
4134            }
4135        }
4136
4137        Ok(())
4138    }
4139}
4140
4141#[cfg(test)]
4142mod test {
4143    use super::*;
4144    use crate::test::TestMemoryExec;
4145    use crate::union::UnionExec;
4146    use arrow::array::{UInt32Array, record_batch};
4147    use arrow::compute::SortOptions;
4148    use arrow::datatypes::{DataType, Field, Schema};
4149    use datafusion_common::assert_batches_eq;
4150    use datafusion_common::config::ConfigNonZeroUsize;
4151
4152    use datafusion_physical_expr::expressions::col;
4153
4154    /// Asserts that the plan is as expected
4155    ///
4156    /// `$EXPECTED_PLAN_LINES`: input plan
4157    /// `$PLAN`: the plan to optimized
4158    macro_rules! assert_plan {
4159        ($PLAN: expr,  @ $EXPECTED: expr) => {
4160            let formatted = crate::displayable($PLAN).indent(true).to_string();
4161
4162            insta::assert_snapshot!(
4163                formatted,
4164                @$EXPECTED
4165            );
4166        };
4167    }
4168
4169    #[tokio::test]
4170    async fn test_preserve_order() -> Result<()> {
4171        let schema = test_schema();
4172        let sort_exprs = sort_exprs(&schema);
4173        let source1 = sorted_memory_exec(&schema, sort_exprs.clone());
4174        let source2 = sorted_memory_exec(&schema, sort_exprs);
4175        // output has multiple partitions, and is sorted
4176        let union = UnionExec::try_new(vec![source1, source2])?;
4177        let exec = RepartitionExec::try_new(union, Partitioning::RoundRobinBatch(10))?
4178            .with_preserve_order();
4179
4180        // Repartition should preserve order
4181        assert_plan!(&exec, @r"
4182        RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=2, preserve_order=true, sort_exprs=c0@0 ASC
4183          UnionExec
4184            DataSourceExec: partitions=1, partition_sizes=[0], output_ordering=c0@0 ASC
4185            DataSourceExec: partitions=1, partition_sizes=[0], output_ordering=c0@0 ASC
4186        ");
4187        Ok(())
4188    }
4189
4190    #[tokio::test]
4191    async fn test_preserve_order_one_partition() -> Result<()> {
4192        let schema = test_schema();
4193        let sort_exprs = sort_exprs(&schema);
4194        let source = sorted_memory_exec(&schema, sort_exprs);
4195        // output is sorted, but has only a single partition, so no need to sort
4196        let exec = RepartitionExec::try_new(source, Partitioning::RoundRobinBatch(10))?
4197            .with_preserve_order();
4198
4199        // Repartition should not preserve order
4200        assert_plan!(&exec, @r"
4201        RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=1, maintains_sort_order=true
4202          DataSourceExec: partitions=1, partition_sizes=[0], output_ordering=c0@0 ASC
4203        ");
4204
4205        Ok(())
4206    }
4207
4208    #[tokio::test]
4209    async fn test_preserve_order_input_not_sorted() -> Result<()> {
4210        let schema = test_schema();
4211        let source1 = memory_exec(&schema);
4212        let source2 = memory_exec(&schema);
4213        // output has multiple partitions, but is not sorted
4214        let union = UnionExec::try_new(vec![source1, source2])?;
4215        let exec = RepartitionExec::try_new(union, Partitioning::RoundRobinBatch(10))?
4216            .with_preserve_order();
4217
4218        // Repartition should not preserve order, as there is no order to preserve
4219        assert_plan!(&exec, @r"
4220        RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=2
4221          UnionExec
4222            DataSourceExec: partitions=1, partition_sizes=[0]
4223            DataSourceExec: partitions=1, partition_sizes=[0]
4224        ");
4225        Ok(())
4226    }
4227
4228    #[tokio::test]
4229    async fn test_preserve_order_with_spilling() -> Result<()> {
4230        use datafusion_execution::runtime_env::RuntimeEnvBuilder;
4231
4232        // Create sorted input data across multiple partitions
4233        // Partition1: [1,3], [5,7], [9,11]
4234        // Partition2: [2,4], [6,8], [10,12]
4235        let batch1 = record_batch!(("c0", UInt32, [1, 3])).unwrap();
4236        let batch2 = record_batch!(("c0", UInt32, [2, 4])).unwrap();
4237        let batch3 = record_batch!(("c0", UInt32, [5, 7])).unwrap();
4238        let batch4 = record_batch!(("c0", UInt32, [6, 8])).unwrap();
4239        let batch5 = record_batch!(("c0", UInt32, [9, 11])).unwrap();
4240        let batch6 = record_batch!(("c0", UInt32, [10, 12])).unwrap();
4241        let schema = batch1.schema();
4242        let sort_exprs = LexOrdering::new([PhysicalSortExpr {
4243            expr: col("c0", &schema).unwrap(),
4244            options: SortOptions::default().asc(),
4245        }])
4246        .unwrap();
4247        let partition1 = vec![batch1.clone(), batch3.clone(), batch5.clone()];
4248        let partition2 = vec![batch2.clone(), batch4.clone(), batch6.clone()];
4249        let input_partitions = vec![partition1, partition2];
4250
4251        // Set up context with tight memory limit to force spilling
4252        // Sorting needs some non-spillable memory, so 608 bytes should force spilling while still allowing the query to complete
4253        let runtime = RuntimeEnvBuilder::default()
4254            .with_memory_limit(608, 1.0)
4255            .build_arc()?;
4256
4257        let task_ctx = TaskContext::default().with_runtime(runtime);
4258        let task_ctx = Arc::new(task_ctx);
4259
4260        // Create physical plan with order preservation
4261        let exec = TestMemoryExec::try_new(&input_partitions, Arc::clone(&schema), None)?
4262            .try_with_sort_information(vec![sort_exprs.clone(), sort_exprs])?;
4263        let exec = Arc::new(exec);
4264        let exec = Arc::new(TestMemoryExec::update_cache(&exec));
4265        // Repartition into 3 partitions with order preservation
4266        // We expect 1 batch per output partition after repartitioning
4267        let exec = RepartitionExec::try_new(exec, Partitioning::RoundRobinBatch(3))?
4268            .with_preserve_order();
4269
4270        let mut batches = vec![];
4271
4272        // Collect all partitions - should succeed by spilling to disk
4273        for i in 0..exec.partitioning().partition_count() {
4274            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
4275            while let Some(result) = stream.next().await {
4276                let batch = result?;
4277                batches.push(batch);
4278            }
4279        }
4280
4281        #[rustfmt::skip]
4282        let expected = [
4283            [
4284                "+----+",
4285                "| c0 |",
4286                "+----+",
4287                "| 1  |",
4288                "| 2  |",
4289                "| 3  |",
4290                "| 4  |",
4291                "+----+",
4292            ],
4293            [
4294                "+----+",
4295                "| c0 |",
4296                "+----+",
4297                "| 5  |",
4298                "| 6  |",
4299                "| 7  |",
4300                "| 8  |",
4301                "+----+",
4302            ],
4303            [
4304                "+----+",
4305                "| c0 |",
4306                "+----+",
4307                "| 9  |",
4308                "| 10 |",
4309                "| 11 |",
4310                "| 12 |",
4311                "+----+",
4312            ],
4313        ];
4314
4315        for (batch, expected) in batches.iter().zip(expected.iter()) {
4316            assert_batches_eq!(expected, std::slice::from_ref(batch));
4317        }
4318
4319        // We should have spilled
4320        let metrics = exec.metrics().unwrap();
4321        assert!(
4322            metrics.spill_count().unwrap() > 0,
4323            "Expected spilling to occur for order-preserving repartition at this \
4324             memory limit. If this fails, the memory limit may need adjustment."
4325        );
4326        Ok(())
4327    }
4328
4329    /// Regression test for order preservation across spill *file rotation*.
4330    ///
4331    /// A `preserve_order` repartition relies on each per-(input, output) spill pool delivering
4332    /// batches in strict FIFO order (see [`spill_pool::spsc_channel`] / [`SpillPoolSink`]). This uses
4333    /// the same memory profile as [`Self::test_preserve_order_with_spilling`] — which is tuned to
4334    /// force spilling while still completing — but additionally sets `max_spill_file_size_bytes`
4335    /// to 1 so every spilled batch lands in its own file. That exercises the FIFO-across-rotation
4336    /// path: if ordering were lost across rotated files (e.g. by feeding an ordered pool with a
4337    /// shared multi-producer writer), the downstream `StreamingMerge` would emit out-of-order rows
4338    /// and the sortedness assertion below would fail.
4339    #[tokio::test]
4340    async fn test_preserve_order_with_spill_file_rotation() -> Result<()> {
4341        use datafusion_execution::config::SessionConfig;
4342        use datafusion_execution::runtime_env::RuntimeEnvBuilder;
4343
4344        // Same sorted input as `test_preserve_order_with_spilling`:
4345        // Partition1: [1,3], [5,7], [9,11]; Partition2: [2,4], [6,8], [10,12]
4346        let batch1 = record_batch!(("c0", UInt32, [1, 3])).unwrap();
4347        let batch2 = record_batch!(("c0", UInt32, [2, 4])).unwrap();
4348        let batch3 = record_batch!(("c0", UInt32, [5, 7])).unwrap();
4349        let batch4 = record_batch!(("c0", UInt32, [6, 8])).unwrap();
4350        let batch5 = record_batch!(("c0", UInt32, [9, 11])).unwrap();
4351        let batch6 = record_batch!(("c0", UInt32, [10, 12])).unwrap();
4352        let schema = batch1.schema();
4353        let sort_exprs = LexOrdering::new([PhysicalSortExpr {
4354            expr: col("c0", &schema).unwrap(),
4355            options: SortOptions::default().asc(),
4356        }])
4357        .unwrap();
4358        let partition1 = vec![batch1, batch3, batch5];
4359        let partition2 = vec![batch2, batch4, batch6];
4360        let input_partitions = vec![partition1, partition2];
4361
4362        // Force a new spill file per spilled batch to exercise FIFO across rotation.
4363        let mut session_config = SessionConfig::new();
4364        session_config
4365            .options_mut()
4366            .execution
4367            .max_spill_file_size_bytes = ConfigNonZeroUsize::try_new(1).unwrap();
4368        // Same tight limit as `test_preserve_order_with_spilling`: forces spilling while leaving
4369        // the merge enough non-spillable headroom to complete.
4370        let runtime = RuntimeEnvBuilder::default()
4371            .with_memory_limit(608, 1.0)
4372            .build_arc()?;
4373        let task_ctx = Arc::new(
4374            TaskContext::default()
4375                .with_session_config(session_config)
4376                .with_runtime(runtime),
4377        );
4378
4379        let exec = TestMemoryExec::try_new(&input_partitions, Arc::clone(&schema), None)?
4380            .try_with_sort_information(vec![sort_exprs.clone(), sort_exprs])?;
4381        let exec = Arc::new(TestMemoryExec::update_cache(&Arc::new(exec)));
4382        let exec = RepartitionExec::try_new(exec, Partitioning::RoundRobinBatch(3))?
4383            .with_preserve_order();
4384
4385        // Each output partition merges sorted substreams, so its rows must be non-decreasing.
4386        for i in 0..exec.partitioning().partition_count() {
4387            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
4388            let mut last: Option<u32> = None;
4389            while let Some(result) = stream.next().await {
4390                let batch = result?;
4391                let col = batch
4392                    .column(0)
4393                    .as_any()
4394                    .downcast_ref::<UInt32Array>()
4395                    .unwrap();
4396                for r in 0..col.len() {
4397                    let v = col.value(r);
4398                    if let Some(prev) = last {
4399                        assert!(
4400                            prev <= v,
4401                            "output partition {i} not sorted: {prev} came before {v}"
4402                        );
4403                    }
4404                    last = Some(v);
4405                }
4406            }
4407        }
4408
4409        let metrics = exec.metrics().unwrap();
4410        assert!(
4411            metrics.spill_count().unwrap() > 0,
4412            "Expected spilling to occur for order-preserving repartition at this \
4413             memory limit. If this fails, the memory limit may need adjustment."
4414        );
4415        Ok(())
4416    }
4417
4418    #[tokio::test]
4419    async fn test_hash_partitioning_with_spilling() -> Result<()> {
4420        use datafusion_execution::runtime_env::RuntimeEnvBuilder;
4421
4422        // Create input data similar to the round-robin test
4423        let batch1 = record_batch!(("c0", UInt32, [1, 3])).unwrap();
4424        let batch2 = record_batch!(("c0", UInt32, [2, 4])).unwrap();
4425        let batch3 = record_batch!(("c0", UInt32, [5, 7])).unwrap();
4426        let batch4 = record_batch!(("c0", UInt32, [6, 8])).unwrap();
4427        let schema = batch1.schema();
4428
4429        let partition1 = vec![batch1.clone(), batch3.clone()];
4430        let partition2 = vec![batch2.clone(), batch4.clone()];
4431        let input_partitions = vec![partition1, partition2];
4432
4433        // Set up context with memory limit to test hash partitioning with spilling infrastructure
4434        let runtime = RuntimeEnvBuilder::default()
4435            .with_memory_limit(1, 1.0)
4436            .build_arc()?;
4437
4438        let task_ctx = TaskContext::default().with_runtime(runtime);
4439        let task_ctx = Arc::new(task_ctx);
4440
4441        // Create physical plan with hash partitioning
4442        let exec = TestMemoryExec::try_new(&input_partitions, Arc::clone(&schema), None)?;
4443        let exec = Arc::new(exec);
4444        let exec = Arc::new(TestMemoryExec::update_cache(&exec));
4445        // Hash partition into 2 partitions by column c0
4446        let hash_expr = col("c0", &schema)?;
4447        let exec =
4448            RepartitionExec::try_new(exec, Partitioning::Hash(vec![hash_expr], 2))?;
4449
4450        // Collect all partitions concurrently using JoinSet - this prevents deadlock
4451        // where the distribution channel gate closes when all output channels are full
4452        let mut join_set = tokio::task::JoinSet::new();
4453        for i in 0..exec.partitioning().partition_count() {
4454            let stream = exec.execute(i, Arc::clone(&task_ctx))?;
4455            join_set.spawn(async move {
4456                let mut count = 0;
4457                futures::pin_mut!(stream);
4458                while let Some(result) = stream.next().await {
4459                    let batch = result?;
4460                    count += batch.num_rows();
4461                }
4462                Ok::<usize, DataFusionError>(count)
4463            });
4464        }
4465
4466        // Wait for all partitions and sum the rows
4467        let mut total_rows = 0;
4468        while let Some(result) = join_set.join_next().await {
4469            total_rows += result.unwrap()?;
4470        }
4471
4472        // Verify we got all rows back
4473        let all_batches = [batch1, batch2, batch3, batch4];
4474        let expected_rows: usize = all_batches.iter().map(|b| b.num_rows()).sum();
4475        assert_eq!(total_rows, expected_rows);
4476
4477        // Verify metrics are available
4478        let metrics = exec.metrics().unwrap();
4479        // Just verify the metrics can be retrieved (spilling may or may not occur)
4480        let spill_count = metrics.spill_count().unwrap_or(0);
4481        assert!(spill_count > 0);
4482        let spilled_bytes = metrics.spilled_bytes().unwrap_or(0);
4483        assert!(spilled_bytes > 0);
4484        let spilled_rows = metrics.spilled_rows().unwrap_or(0);
4485        assert!(spilled_rows > 0);
4486
4487        Ok(())
4488    }
4489
4490    #[tokio::test]
4491    async fn test_repartition() -> Result<()> {
4492        let schema = test_schema();
4493        let sort_exprs = sort_exprs(&schema);
4494        let source = sorted_memory_exec(&schema, sort_exprs);
4495        // output is sorted, but has only a single partition, so no need to sort
4496        let exec = RepartitionExec::try_new(source, Partitioning::RoundRobinBatch(10))?
4497            .repartitioned(20, &Default::default())?
4498            .unwrap();
4499
4500        // Repartition should not preserve order
4501        assert_plan!(exec.as_ref(), @r"
4502        RepartitionExec: partitioning=RoundRobinBatch(20), input_partitions=1, maintains_sort_order=true
4503          DataSourceExec: partitions=1, partition_sizes=[0], output_ordering=c0@0 ASC
4504        ");
4505        Ok(())
4506    }
4507
4508    #[test]
4509    fn test_range_repartitioned_returns_none() -> Result<()> {
4510        let schema = test_schema();
4511        let source = memory_exec(&schema);
4512        let partitioning = Partitioning::Range(RangePartitioning::try_new(
4513            [PhysicalSortExpr::new(
4514                col("c0", &schema)?,
4515                SortOptions::default(),
4516            )]
4517            .into(),
4518            vec![
4519                SplitPoint::new(vec![ScalarValue::UInt32(Some(10))]),
4520                SplitPoint::new(vec![ScalarValue::UInt32(Some(20))]),
4521            ],
4522        )?);
4523        let exec = RepartitionExec::try_new(source, partitioning)?;
4524
4525        let mut expressions = vec![];
4526        exec.apply_expressions(&mut |expr| {
4527            expressions.push(expr.to_string());
4528            Ok(TreeNodeRecursion::Continue)
4529        })?;
4530        assert_eq!(expressions, ["c0@0"]);
4531
4532        // Range partition count is fixed by split points, so repartitioned()
4533        // cannot change it to an arbitrary target.
4534        let result = exec.repartitioned(10, &Default::default())?;
4535        assert!(
4536            result.is_none(),
4537            "range repartitioning should not support changing partition count"
4538        );
4539        Ok(())
4540    }
4541
4542    fn test_schema() -> Arc<Schema> {
4543        Arc::new(Schema::new(vec![Field::new("c0", DataType::UInt32, false)]))
4544    }
4545
4546    fn sort_exprs(schema: &Schema) -> LexOrdering {
4547        [PhysicalSortExpr {
4548            expr: col("c0", schema).unwrap(),
4549            options: SortOptions::default(),
4550        }]
4551        .into()
4552    }
4553
4554    fn memory_exec(schema: &SchemaRef) -> Arc<dyn ExecutionPlan> {
4555        TestMemoryExec::try_new_exec(&[vec![]], Arc::clone(schema), None).unwrap()
4556    }
4557
4558    fn sorted_memory_exec(
4559        schema: &SchemaRef,
4560        sort_exprs: LexOrdering,
4561    ) -> Arc<dyn ExecutionPlan> {
4562        let exec = TestMemoryExec::try_new(&[vec![]], Arc::clone(schema), None)
4563            .unwrap()
4564            .try_with_sort_information(vec![sort_exprs])
4565            .unwrap();
4566        let exec = Arc::new(exec);
4567        Arc::new(TestMemoryExec::update_cache(&exec))
4568    }
4569
4570    /// preserve_order repartition should not double-count
4571    /// output rows.
4572    #[tokio::test]
4573    async fn test_preserve_order_output_rows_not_double_counted() -> Result<()> {
4574        use datafusion_execution::TaskContext;
4575
4576        // Two sorted input partitions, 2 rows each (4 total)
4577        let batch1 = record_batch!(("c0", UInt32, [1, 3])).unwrap();
4578        let batch2 = record_batch!(("c0", UInt32, [2, 4])).unwrap();
4579        let schema = batch1.schema();
4580        let sort_exprs = sort_exprs(&schema);
4581
4582        let input_partitions = vec![vec![batch1], vec![batch2]];
4583        let exec = TestMemoryExec::try_new(&input_partitions, Arc::clone(&schema), None)?
4584            .try_with_sort_information(vec![sort_exprs.clone(), sort_exprs])?;
4585        let exec = Arc::new(exec);
4586        let exec = Arc::new(TestMemoryExec::update_cache(&exec));
4587
4588        let exec = RepartitionExec::try_new(exec, Partitioning::RoundRobinBatch(3))?
4589            .with_preserve_order();
4590
4591        let task_ctx = Arc::new(TaskContext::default());
4592        let mut total_rows = 0;
4593        for i in 0..exec.partitioning().partition_count() {
4594            let mut stream = exec.execute(i, Arc::clone(&task_ctx))?;
4595            while let Some(result) = stream.next().await {
4596                total_rows += result?.num_rows();
4597            }
4598        }
4599
4600        assert_eq!(total_rows, 4, "actual rows collected should be 4");
4601
4602        let metrics = exec.metrics().unwrap();
4603        let reported_output_rows = metrics.output_rows().unwrap();
4604        assert_eq!(
4605            reported_output_rows, total_rows,
4606            "metrics output_rows ({reported_output_rows}) should match \
4607             actual rows collected ({total_rows}), not double-count"
4608        );
4609
4610        Ok(())
4611    }
4612}