Skip to main content

datafusion_physical_plan/sorts/
sort.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//! Sort that deals with an arbitrary size of the input.
19//! It will do in-memory sorting if it has enough memory budget
20//! but spills to disk if needed.
21
22use std::fmt;
23use std::fmt::{Debug, Formatter};
24use std::sync::Arc;
25
26use parking_lot::RwLock;
27
28use crate::common::spawn_buffered;
29use crate::execution_plan::{
30    Boundedness, CardinalityEffect, EmissionType, has_same_children_properties,
31    replace_children_if_necessary,
32};
33use crate::expressions::PhysicalSortExpr;
34use crate::filter::FilterExec;
35use crate::filter_pushdown::{
36    ChildFilterDescription, ChildPushdownResult, FilterDescription, FilterPushdownPhase,
37    FilterPushdownPropagation, PushedDown,
38};
39use crate::limit::LimitStream;
40use crate::metrics::{
41    BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet, SpillMetrics,
42};
43use crate::projection::{ProjectionExec, make_with_child, update_ordering};
44use crate::sorts::IncrementalSortIterator;
45use crate::sorts::streaming_merge::{SortedSpillFile, StreamingMergeBuilder};
46use crate::spill::get_record_batch_memory_size;
47use crate::spill::in_progress_spill_file::InProgressSpillFile;
48use crate::spill::spill_manager::{GetSlicedSize, SpillManager};
49use crate::statistics::{ChildStats, StatisticsArgs};
50use crate::stream::ReservationStream;
51use crate::stream::{ObservedStream, RecordBatchStreamAdapter};
52use crate::topk::TopK;
53use crate::topk::TopKDynamicFilters;
54use crate::{
55    ChildrenPropertiesMode, DisplayAs, DisplayFormatType, Distribution,
56    EmptyRecordBatchStream, ExecutionPlan, ExecutionPlanProperties, Partitioning,
57    PlanProperties, ReplaceChildrenOptions, SendableRecordBatchStream, Statistics,
58};
59
60use arrow::array::{RecordBatch, RecordBatchOptions};
61use arrow::compute::{concat_batches, lexsort_to_indices, take_arrays};
62use arrow::datatypes::SchemaRef;
63use datafusion_common::config::SpillCompression;
64use datafusion_common::tree_node::TreeNodeRecursion;
65use datafusion_common::{
66    DataFusionError, Result, assert_or_internal_err, internal_datafusion_err,
67    unwrap_or_internal_err,
68};
69use datafusion_execution::TaskContext;
70use datafusion_execution::memory_pool::{MemoryConsumer, MemoryReservation};
71use datafusion_execution::runtime_env::RuntimeEnv;
72use datafusion_physical_expr::LexOrdering;
73use datafusion_physical_expr::PhysicalExpr;
74use datafusion_physical_expr::expressions::{DynamicFilterPhysicalExpr, lit};
75
76use futures::{StreamExt, TryStreamExt};
77use log::{debug, trace};
78
79struct ExternalSorterMetrics {
80    /// metrics
81    baseline: BaselineMetrics,
82
83    spill_metrics: SpillMetrics,
84}
85
86impl ExternalSorterMetrics {
87    fn new(metrics: &ExecutionPlanMetricsSet, partition: usize) -> Self {
88        Self {
89            baseline: BaselineMetrics::new(metrics, partition),
90            spill_metrics: SpillMetrics::new(metrics, partition),
91        }
92    }
93}
94
95/// Sorts an arbitrary sized, unsorted, stream of [`RecordBatch`]es to
96/// a total order. Depending on the input size and memory manager
97/// configuration, writes intermediate results to disk ("spills")
98/// using Arrow IPC format.
99///
100/// # Algorithm
101///
102/// 1. get a non-empty new batch from input
103///
104/// 2. check with the memory manager there is sufficient space to
105///    buffer the batch in memory.
106///
107/// 2.1 if memory is sufficient, buffer batch in memory, go to 1.
108///
109/// 2.2 if no more memory is available, sort all buffered batches and
110///     spill to file.  buffer the next batch in memory, go to 1.
111///
112/// 3. when input is exhausted, merge all in memory batches and spills
113///    to get a total order.
114///
115/// # When data fits in available memory
116///
117/// If there is sufficient memory, data is sorted in memory to produce the output
118///
119/// ```text
120///    ┌─────┐
121///    │  2  │
122///    │  3  │
123///    │  1  │─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
124///    │  4  │
125///    │  2  │                  │
126///    └─────┘                  ▼
127///    ┌─────┐
128///    │  1  │              In memory
129///    │  4  │─ ─ ─ ─ ─ ─▶ sort/merge  ─ ─ ─ ─ ─▶  total sorted output
130///    │  1  │
131///    └─────┘                  ▲
132///      ...                    │
133///
134///    ┌─────┐                  │
135///    │  4  │
136///    │  3  │─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
137///    └─────┘
138///
139/// in_mem_batches
140/// ```
141///
142/// # When data does not fit in available memory
143///
144///  When memory is exhausted, data is first sorted and written to one
145///  or more spill files on disk:
146///
147/// ```text
148///    ┌─────┐                               .─────────────────.
149///    │  2  │                              (                   )
150///    │  3  │                              │`─────────────────'│
151///    │  1  │─ ─ ─ ─ ─ ─ ─                 │  ┌────┐           │
152///    │  4  │             │                │  │ 1  │░          │
153///    │  2  │                              │  │... │░          │
154///    └─────┘             ▼                │  │ 4  │░  ┌ ─ ─   │
155///    ┌─────┐                              │  └────┘░    1  │░ │
156///    │  1  │         In memory            │   ░░░░░░  │    ░░ │
157///    │  4  │─ ─ ▶   sort/merge    ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─▶ ... │░ │
158///    │  1  │     and write to file        │           │    ░░ │
159///    └─────┘                              │             4  │░ │
160///      ...               ▲                │           └░─░─░░ │
161///                        │                │            ░░░░░░ │
162///    ┌─────┐                              │.─────────────────.│
163///    │  4  │             │                (                   )
164///    │  3  │─ ─ ─ ─ ─ ─ ─                  `─────────────────'
165///    └─────┘
166///
167/// in_mem_batches                                  spills
168///                                         (file on disk in Arrow
169///                                               IPC format)
170/// ```
171///
172/// Once the input is completely read, the spill files are read and
173/// merged with any in memory batches to produce a single total sorted
174/// output:
175///
176/// ```text
177///   .─────────────────.
178///  (                   )
179///  │`─────────────────'│
180///  │  ┌────┐           │
181///  │  │ 1  │░          │
182///  │  │... │─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─
183///  │  │ 4  │░ ┌────┐   │           │
184///  │  └────┘░ │ 1  │░  │           ▼
185///  │   ░░░░░░ │    │░  │
186///  │          │... │─ ─│─ ─ ─ ▶ merge  ─ ─ ─▶  total sorted output
187///  │          │    │░  │
188///  │          │ 4  │░  │           ▲
189///  │          └────┘░  │           │
190///  │           ░░░░░░  │
191///  │.─────────────────.│           │
192///  (                   )
193///   `─────────────────'            │
194///         spills
195///                                  │
196///
197///                                  │
198///
199///     ┌─────┐                      │
200///     │  1  │
201///     │  4  │─ ─ ─ ─               │
202///     └─────┘       │
203///       ...                   In memory
204///                   └ ─ ─ ─▶  sort/merge
205///     ┌─────┐
206///     │  4  │                      ▲
207///     │  3  │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
208///     └─────┘
209///
210///  in_mem_batches
211/// ```
212struct ExternalSorter {
213    // ========================================================================
214    // PROPERTIES:
215    // Fields that define the sorter's configuration and remain constant
216    // ========================================================================
217    /// Schema of the output (and the input)
218    schema: SchemaRef,
219    /// Sort expressions
220    expr: LexOrdering,
221    /// The target number of rows for output batches
222    batch_size: usize,
223    /// If the in size of buffered memory batches is below this size,
224    /// the data will be concatenated and sorted in place rather than
225    /// sort/merged.
226    sort_in_place_threshold_bytes: usize,
227
228    // ========================================================================
229    // STATE BUFFERS:
230    // Fields that hold intermediate data during sorting
231    // ========================================================================
232    /// Unsorted input batches stored in the memory buffer
233    in_mem_batches: Vec<RecordBatch>,
234
235    /// During external sorting, in-memory intermediate data will be appended to
236    /// this file incrementally. Once finished, this file will be moved to [`Self::finished_spill_files`].
237    ///
238    /// this is a tuple of:
239    /// 1. `InProgressSpillFile` - the file that is being written to
240    /// 2. `max_record_batch_memory` - the maximum memory usage of a single batch in this spill file.
241    in_progress_spill_file: Option<(InProgressSpillFile, usize)>,
242    /// If data has previously been spilled, the locations of the spill files (in
243    /// Arrow IPC format)
244    /// Within the same spill file, the data might be chunked into multiple batches,
245    /// and ordered by sort keys.
246    finished_spill_files: Vec<SortedSpillFile>,
247
248    // ========================================================================
249    // EXECUTION RESOURCES:
250    // Fields related to managing execution resources and monitoring performance.
251    // ========================================================================
252    /// Runtime metrics
253    metrics: ExternalSorterMetrics,
254    /// A handle to the runtime to get spill files
255    runtime: Arc<RuntimeEnv>,
256    /// Reservation for in_mem_batches
257    reservation: MemoryReservation,
258    spill_manager: SpillManager,
259
260    /// Reservation for the merging of in-memory batches. If the sort
261    /// might spill, `sort_spill_reservation_bytes` will be
262    /// pre-reserved to ensure there is some space for this sort/merge.
263    merge_reservation: MemoryReservation,
264    /// How much memory to reserve for performing in-memory sort/merges
265    /// prior to spilling.
266    sort_spill_reservation_bytes: usize,
267}
268
269impl ExternalSorter {
270    // TODO: make a builder or some other nicer API to avoid the
271    // clippy warning
272    #[expect(clippy::too_many_arguments)]
273    pub fn new(
274        partition_id: usize,
275        schema: SchemaRef,
276        expr: LexOrdering,
277        batch_size: usize,
278        sort_spill_reservation_bytes: usize,
279        sort_in_place_threshold_bytes: usize,
280        // Configured via `datafusion.execution.spill_compression`.
281        spill_compression: SpillCompression,
282        metrics: &ExecutionPlanMetricsSet,
283        runtime: Arc<RuntimeEnv>,
284    ) -> Result<Self> {
285        let metrics = ExternalSorterMetrics::new(metrics, partition_id);
286        let reservation = MemoryConsumer::new(format!("ExternalSorter[{partition_id}]"))
287            .with_can_spill(true)
288            .register(&runtime.memory_pool);
289
290        let merge_reservation =
291            MemoryConsumer::new(format!("ExternalSorterMerge[{partition_id}]"))
292                .register(&runtime.memory_pool);
293
294        let spill_manager = SpillManager::new(
295            Arc::clone(&runtime),
296            metrics.spill_metrics.clone(),
297            Arc::clone(&schema),
298        )
299        .with_compression_type(spill_compression);
300
301        Ok(Self {
302            schema,
303            in_mem_batches: vec![],
304            in_progress_spill_file: None,
305            finished_spill_files: vec![],
306            expr,
307            metrics,
308            reservation,
309            spill_manager,
310            merge_reservation,
311            runtime,
312            batch_size,
313            sort_spill_reservation_bytes,
314            sort_in_place_threshold_bytes,
315        })
316    }
317
318    /// Appends an unsorted [`RecordBatch`] to `in_mem_batches`
319    ///
320    /// Updates memory usage metrics, and possibly triggers spilling to disk
321    async fn insert_batch(&mut self, input: RecordBatch) -> Result<()> {
322        if input.num_rows() == 0 {
323            return Ok(());
324        }
325
326        self.reserve_memory_for_merge()?;
327        self.reserve_memory_for_batch_and_maybe_spill(&input)
328            .await?;
329
330        self.in_mem_batches.push(input);
331        Ok(())
332    }
333
334    fn spilled_before(&self) -> bool {
335        !self.finished_spill_files.is_empty()
336    }
337
338    /// Returns the final sorted output of all batches inserted via
339    /// [`Self::insert_batch`] as a stream of [`RecordBatch`]es.
340    ///
341    /// This process could either be:
342    ///
343    /// 1. An in-memory sort/merge (if the input fit in memory)
344    ///
345    /// 2. A combined streaming merge incorporating both in-memory
346    ///    batches and data from spill files on disk.
347    async fn sort(&mut self) -> Result<SendableRecordBatchStream> {
348        if self.spilled_before() {
349            // Sort `in_mem_batches` and spill it first. If there are many
350            // `in_mem_batches` and the memory limit is almost reached, merging
351            // them with the spilled files at the same time might cause OOM.
352            if !self.in_mem_batches.is_empty() {
353                self.sort_and_spill_in_mem_batches().await?;
354            }
355
356            // Transfer the pre-reserved merge memory to the streaming merge
357            // using `take()` instead of `new_empty()`. This ensures the merge
358            // stream starts with `sort_spill_reservation_bytes` already
359            // allocated, preventing starvation when concurrent sort partitions
360            // compete for pool memory. `take()` moves the bytes atomically
361            // without releasing them back to the pool, so other partitions
362            // cannot race to consume the freed memory.
363            StreamingMergeBuilder::new()
364                .with_sorted_spill_files(std::mem::take(&mut self.finished_spill_files))
365                .with_spill_manager(self.spill_manager.clone())
366                .with_schema(Arc::clone(&self.schema))
367                .with_expressions(&self.expr.clone())
368                .with_metrics(self.metrics.baseline.clone())
369                .with_batch_size(self.batch_size)
370                .with_fetch(None)
371                .with_reservation(self.merge_reservation.take())
372                .build()
373        } else {
374            // Release the memory reserved for merge back to the pool so
375            // there is some left when `in_mem_sort_stream` requests an
376            // allocation. Only needed for the non-spill path; the spill
377            // path transfers the reservation to the merge stream instead.
378            self.merge_reservation.free();
379            self.in_mem_sort_stream(true, true)
380        }
381    }
382
383    /// How much memory is buffered in this `ExternalSorter`?
384    fn used(&self) -> usize {
385        self.reservation.size()
386    }
387
388    /// How much memory is reserved for the merge phase?
389    #[cfg(test)]
390    fn merge_reservation_size(&self) -> usize {
391        self.merge_reservation.size()
392    }
393
394    /// How many bytes have been spilled to disk?
395    fn spilled_bytes(&self) -> usize {
396        self.metrics.spill_metrics.spilled_bytes.value()
397    }
398
399    /// How many rows have been spilled to disk?
400    fn spilled_rows(&self) -> usize {
401        self.metrics.spill_metrics.spilled_rows.value()
402    }
403
404    /// How many spill files have been created?
405    fn spill_count(&self) -> usize {
406        self.metrics.spill_metrics.spill_file_count.value()
407    }
408
409    /// Appending globally sorted batches to the in-progress spill file, and clears
410    /// the `globally_sorted_batches` (also its memory reservation) afterwards.
411    fn consume_and_spill_append(
412        &mut self,
413        globally_sorted_batches: &mut Vec<RecordBatch>,
414    ) -> Result<()> {
415        if globally_sorted_batches.is_empty() {
416            return Ok(());
417        }
418
419        // Lazily initialize the in-progress spill file
420        if self.in_progress_spill_file.is_none() {
421            self.in_progress_spill_file =
422                Some((self.spill_manager.create_in_progress_file("Sorting")?, 0));
423        }
424
425        debug!("Spilling sort data of ExternalSorter to disk whilst inserting");
426
427        let batches_to_spill = std::mem::take(globally_sorted_batches);
428        self.reservation.free();
429
430        let (in_progress_file, max_record_batch_size) =
431            self.in_progress_spill_file.as_mut().ok_or_else(|| {
432                internal_datafusion_err!("In-progress spill file should be initialized")
433            })?;
434
435        for batch in batches_to_spill {
436            let gc_sliced_size = in_progress_file.append_batch(&batch)?;
437
438            *max_record_batch_size = (*max_record_batch_size).max(gc_sliced_size);
439        }
440
441        assert_or_internal_err!(
442            globally_sorted_batches.is_empty(),
443            "This function consumes globally_sorted_batches, so it should be empty after taking."
444        );
445
446        Ok(())
447    }
448
449    /// Finishes the in-progress spill file and moves it to the finished spill files.
450    fn spill_finish(&mut self) -> Result<()> {
451        let (mut in_progress_file, max_record_batch_memory) =
452            self.in_progress_spill_file.take().ok_or_else(|| {
453                internal_datafusion_err!("Should be called after `spill_append`")
454            })?;
455        let spill_file = in_progress_file.finish()?;
456
457        if let Some(spill_file) = spill_file {
458            self.finished_spill_files.push(SortedSpillFile {
459                file: spill_file,
460                max_record_batch_memory,
461            });
462        }
463
464        Ok(())
465    }
466
467    /// Sorts the in-memory batches and merges them into a single sorted run, then writes
468    /// the result to spill files.
469    async fn sort_and_spill_in_mem_batches(&mut self) -> Result<()> {
470        assert_or_internal_err!(
471            !self.in_mem_batches.is_empty(),
472            "in_mem_batches must not be empty when attempting to sort and spill"
473        );
474
475        // Release the memory reserved for merge back to the pool so
476        // there is some left when `in_mem_sort_stream` requests an
477        // allocation. At the end of this function, memory will be
478        // reserved again for the next spill.
479        self.merge_reservation.free();
480
481        let mut sorted_stream = self.in_mem_sort_stream(
482            false,
483            // No coalescing on the spill path: it raises per-run peak memory.
484            false,
485        )?;
486        // After `in_mem_sort_stream()` is constructed, all `in_mem_batches` is taken
487        // to construct a globally sorted stream.
488        assert_or_internal_err!(
489            self.in_mem_batches.is_empty(),
490            "in_mem_batches should be empty after constructing sorted stream"
491        );
492        // 'global' here refers to all buffered batches when the memory limit is
493        // reached. This variable will buffer the sorted batches after
494        // sort-preserving merge and incrementally append to spill files.
495        let mut globally_sorted_batches: Vec<RecordBatch> = vec![];
496
497        while let Some(batch) = sorted_stream.next().await {
498            let batch = batch?;
499            let sorted_size = get_reserved_bytes_for_record_batch(&batch)?;
500            if self.reservation.try_grow(sorted_size).is_err() {
501                // Although the reservation is not enough, the batch is
502                // already in memory, so it's okay to combine it with previously
503                // sorted batches, and spill together.
504                globally_sorted_batches.push(batch);
505                self.consume_and_spill_append(&mut globally_sorted_batches)?; // reservation is freed in spill()
506            } else {
507                globally_sorted_batches.push(batch);
508            }
509        }
510
511        // Drop early to free up memory reserved by the sorted stream, otherwise the
512        // upcoming `self.reserve_memory_for_merge()` may fail due to insufficient memory.
513        drop(sorted_stream);
514
515        self.consume_and_spill_append(&mut globally_sorted_batches)?;
516        self.spill_finish()?;
517
518        // Sanity check after spilling
519        let buffers_cleared_property =
520            self.in_mem_batches.is_empty() && globally_sorted_batches.is_empty();
521        assert_or_internal_err!(
522            buffers_cleared_property,
523            "in_mem_batches and globally_sorted_batches should be cleared before"
524        );
525
526        // Reserve headroom for next sort/merge
527        self.reserve_memory_for_merge()?;
528
529        Ok(())
530    }
531
532    /// Consumes in_mem_batches returning a sorted stream of
533    /// batches. This proceeds in one of two ways:
534    ///
535    /// # Small Datasets
536    ///
537    /// For "smaller" datasets, the data is first concatenated into a
538    /// single batch and then sorted. This is often faster than
539    /// sorting and then merging.
540    ///
541    /// ```text
542    ///        ┌─────┐
543    ///        │  2  │
544    ///        │  3  │
545    ///        │  1  │─ ─ ─ ─ ┐            ┌─────┐
546    ///        │  4  │                     │  2  │
547    ///        │  2  │        │            │  3  │
548    ///        └─────┘                     │  1  │             sorted output
549    ///        ┌─────┐        ▼            │  4  │                stream
550    ///        │  1  │                     │  2  │
551    ///        │  4  │─ ─▶ concat ─ ─ ─ ─ ▶│  1  │─ ─ ▶  sort  ─ ─ ─ ─ ─▶
552    ///        │  1  │                     │  4  │
553    ///        └─────┘        ▲            │  1  │
554    ///          ...          │            │ ... │
555    ///                                    │  4  │
556    ///        ┌─────┐        │            │  3  │
557    ///        │  4  │                     └─────┘
558    ///        │  3  │─ ─ ─ ─ ┘
559    ///        └─────┘
560    ///     in_mem_batches
561    /// ```
562    ///
563    /// # Larger datasets
564    ///
565    /// For larger datasets, the batches are first sorted individually
566    /// and then merged together.
567    ///
568    /// ```text
569    ///      ┌─────┐                ┌─────┐
570    ///      │  2  │                │  1  │
571    ///      │  3  │                │  2  │
572    ///      │  1  │─ ─▶  sort  ─ ─▶│  2  │─ ─ ─ ─ ─ ┐
573    ///      │  4  │                │  3  │
574    ///      │  2  │                │  4  │          │
575    ///      └─────┘                └─────┘               sorted output
576    ///      ┌─────┐                ┌─────┐          ▼       stream
577    ///      │  1  │                │  1  │
578    ///      │  4  │─ ▶  sort  ─ ─ ▶│  1  ├ ─ ─ ▶ merge  ─ ─ ─ ─▶
579    ///      │  1  │                │  4  │
580    ///      └─────┘                └─────┘          ▲
581    ///        ...       ...         ...             │
582    ///
583    ///      ┌─────┐                ┌─────┐          │
584    ///      │  4  │                │  3  │
585    ///      │  3  │─ ▶  sort  ─ ─ ▶│  4  │─ ─ ─ ─ ─ ┘
586    ///      └─────┘                └─────┘
587    ///
588    ///   in_mem_batches
589    /// ```
590    /// `coalesce_runs` merges buffered batches into fewer, larger sorted runs to
591    /// reduce merge fan-in. Disabled on the spill path to keep peak memory low.
592    fn in_mem_sort_stream(
593        &mut self,
594        is_output_stream: bool,
595        coalesce_runs: bool,
596    ) -> Result<SendableRecordBatchStream> {
597        if self.in_mem_batches.is_empty() {
598            let empty_stream =
599                Box::pin(EmptyRecordBatchStream::new(Arc::clone(&self.schema)));
600            return Ok(self.observe_if_output(empty_stream, is_output_stream));
601        }
602
603        // The elapsed compute timer is updated when the value is dropped.
604        // There is no need for an explicit call to drop.
605        let elapsed_compute = self.metrics.baseline.elapsed_compute().clone();
606        let _timer = elapsed_compute.timer();
607
608        // Please pay attention that any operation inside of `in_mem_sort_stream` will
609        // not perform any memory reservation. This is for avoiding the need of handling
610        // reservation failure and spilling in the middle of the sort/merge. The memory
611        // space for batches produced by the resulting stream will be reserved by the
612        // consumer of the stream.
613
614        if self.in_mem_batches.len() == 1 {
615            let batch = self.in_mem_batches.swap_remove(0);
616            let reservation = self.reservation.take();
617            let sorted_stream = self.sort_batch_stream(batch, reservation)?;
618            return Ok(self.observe_if_output(sorted_stream, is_output_stream));
619        }
620
621        // If less than sort_in_place_threshold_bytes, concatenate and sort in place
622        if self.reservation.size() < self.sort_in_place_threshold_bytes {
623            // Concatenate memory batches together and sort
624            let batch = concat_batches(&self.schema, &self.in_mem_batches)?;
625            self.in_mem_batches.clear();
626            self.reservation
627                .try_resize(get_reserved_bytes_for_record_batch(&batch)?)
628                .map_err(Self::err_with_oom_context)?;
629            let reservation = self.reservation.take();
630            let sorted_stream = self.sort_batch_stream(batch, reservation)?;
631            return Ok(self.observe_if_output(sorted_stream, is_output_stream));
632        }
633
634        // For single-column sorts, coalesce the buffered batches into fewer,
635        // larger runs to cut the merge fan-in (where the cheap per-key compare is
636        // dominated by per-stream cursor/merge overhead). Multi-column sorts are
637        // left as one run per batch: the row-format merge of many small runs
638        // beats sorting a few large runs with the lexicographic comparator.
639        let batches = std::mem::take(&mut self.in_mem_batches);
640        let runs = if coalesce_runs && self.expr.len() == 1 {
641            self.coalesce_in_mem_batches_into_runs(batches)?
642        } else {
643            batches
644        };
645
646        let streams = runs
647            .into_iter()
648            .map(|batch| {
649                let reservation = self
650                    .reservation
651                    .split(get_reserved_bytes_for_record_batch(&batch)?);
652                let input = self.sort_batch_stream(batch, reservation)?;
653                Ok(spawn_buffered(input, 1))
654            })
655            .collect::<Result<_>>()?;
656
657        StreamingMergeBuilder::new()
658            .with_streams(streams)
659            .with_schema(Arc::clone(&self.schema))
660            .with_expressions(&self.expr.clone())
661            .with_metrics(if is_output_stream {
662                self.metrics.baseline.clone()
663            } else {
664                self.metrics.baseline.intermediate()
665            })
666            .with_batch_size(self.batch_size)
667            .with_fetch(None)
668            .with_reservation(self.merge_reservation.new_empty())
669            .build()
670    }
671
672    /// Concatenates `batches` into fewer, larger runs, each bounded by
673    /// `sort_in_place_threshold_bytes`, to reduce merge fan-in. `self.reservation`
674    /// is resized to the coalesced footprint so the caller's per-run splits stay
675    /// exact.
676    fn coalesce_in_mem_batches_into_runs(
677        &mut self,
678        batches: Vec<RecordBatch>,
679    ) -> Result<Vec<RecordBatch>> {
680        let target = self.sort_in_place_threshold_bytes.max(1);
681        let mut runs: Vec<RecordBatch> = Vec::new();
682        let mut group: Vec<RecordBatch> = Vec::new();
683        let mut group_bytes = 0usize;
684
685        // Flush a group into a run, skipping the copy for a single-batch group.
686        let flush = |group: &mut Vec<RecordBatch>,
687                     runs: &mut Vec<RecordBatch>,
688                     schema: &SchemaRef|
689         -> Result<()> {
690            match group.len() {
691                0 => {}
692                1 => runs.push(group.pop().unwrap()),
693                _ => {
694                    runs.push(concat_batches(schema, group.iter())?);
695                    group.clear();
696                }
697            }
698            Ok(())
699        };
700
701        for batch in batches {
702            let bytes = get_reserved_bytes_for_record_batch(&batch)?;
703            if !group.is_empty() && group_bytes.saturating_add(bytes) > target {
704                flush(&mut group, &mut runs, &self.schema)?;
705                group_bytes = 0;
706            }
707            group_bytes += bytes;
708            group.push(batch);
709        }
710        flush(&mut group, &mut runs, &self.schema)?;
711
712        // Realign the reservation: concatenation may shift the footprint slightly.
713        let total: usize = runs
714            .iter()
715            .map(get_reserved_bytes_for_record_batch)
716            .sum::<Result<usize>>()?;
717        self.reservation
718            .try_resize(total)
719            .map_err(Self::err_with_oom_context)?;
720
721        Ok(runs)
722    }
723
724    /// Sorts a single `RecordBatch` into a single stream.
725    ///
726    /// This may output multiple batches depending on the size of the
727    /// sorted data and the target batch size.
728    /// For single-batch output cases, `reservation` will be freed immediately after sorting,
729    /// as the batch will be output and is expected to be reserved by the consumer of the stream.
730    /// For multi-batch output cases, `reservation` will be grown to match the actual
731    /// size of sorted output, and as each batch is output, its memory will be freed from the reservation.
732    /// (This leads to the same behaviour, as futures are only evaluated when polled by the consumer.)
733    fn sort_batch_stream(
734        &self,
735        batch: RecordBatch,
736        reservation: MemoryReservation,
737    ) -> Result<SendableRecordBatchStream> {
738        assert_eq!(
739            get_reserved_bytes_for_record_batch(&batch)?,
740            reservation.size()
741        );
742
743        let schema = batch.schema();
744        let expressions = self.expr.clone();
745        let batch_size = self.batch_size;
746
747        let stream = futures::stream::once(async move {
748            let schema = batch.schema();
749
750            // Sort the batch immediately and get all output batches
751            let sorted_batches = sort_batch_chunked(&batch, &expressions, batch_size)?;
752
753            // Resize the reservation to match the actual sorted output size.
754            // Using try_resize avoids a release-then-reacquire cycle, which
755            // matters for MemoryPool implementations where grow/shrink have
756            // non-trivial cost (e.g. JNI calls in Comet).
757            let total_sorted_size: usize = sorted_batches
758                .iter()
759                .map(get_record_batch_memory_size)
760                .sum();
761            reservation
762                .try_resize(total_sorted_size)
763                .map_err(Self::err_with_oom_context)?;
764
765            // Wrap in ReservationStream to hold the reservation
766            Result::<_, DataFusionError>::Ok(Box::pin(ReservationStream::new(
767                Arc::clone(&schema),
768                Box::pin(RecordBatchStreamAdapter::new(
769                    Arc::clone(&schema),
770                    futures::stream::iter(sorted_batches.into_iter().map(Ok)),
771                )),
772                reservation,
773            )) as SendableRecordBatchStream)
774        })
775        .try_flatten();
776
777        Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
778    }
779
780    /// If this sort may spill, pre-allocates
781    /// `sort_spill_reservation_bytes` of memory to guarantee memory
782    /// left for the in memory sort/merge.
783    fn reserve_memory_for_merge(&mut self) -> Result<()> {
784        // Reserve headroom for next merge sort
785        if self.runtime.disk_manager.tmp_files_enabled() {
786            let size = self.sort_spill_reservation_bytes;
787            if self.merge_reservation.size() != size {
788                self.merge_reservation
789                    .try_resize(size)
790                    .map_err(Self::err_with_oom_context)?;
791            }
792        }
793
794        Ok(())
795    }
796
797    /// Reserves memory to be able to accommodate the given batch.
798    /// If memory is scarce, tries to spill current in-memory batches to disk first.
799    async fn reserve_memory_for_batch_and_maybe_spill(
800        &mut self,
801        input: &RecordBatch,
802    ) -> Result<()> {
803        let size = get_reserved_bytes_for_record_batch(input)?;
804
805        match self.reservation.try_grow(size) {
806            Ok(_) => Ok(()),
807            Err(e) => {
808                if self.in_mem_batches.is_empty() {
809                    return Err(Self::err_with_oom_context(e));
810                }
811
812                // Spill and try again.
813                self.sort_and_spill_in_mem_batches().await?;
814                self.reservation
815                    .try_grow(size)
816                    .map_err(Self::err_with_oom_context)
817            }
818        }
819    }
820
821    /// Wraps the error with a context message suggesting settings to tweak.
822    /// This is meant to be used with DataFusionError::ResourcesExhausted only.
823    fn err_with_oom_context(e: DataFusionError) -> DataFusionError {
824        match e {
825            DataFusionError::ResourcesExhausted(_) => e.context(
826                "Not enough memory to continue external sort. \
827                    Consider increasing the memory limit config: 'datafusion.runtime.memory_limit', \
828                    or decreasing the config: 'datafusion.execution.sort_spill_reservation_bytes'."
829            ),
830            // This is not an OOM error, so just return it as is.
831            _ => e,
832        }
833    }
834
835    fn observe_if_output(
836        &self,
837        mut stream: SendableRecordBatchStream,
838        wrap: bool,
839    ) -> SendableRecordBatchStream {
840        if wrap {
841            stream = Box::pin(ObservedStream::new(
842                stream,
843                self.metrics.baseline.clone(),
844                None,
845            ))
846        }
847
848        stream
849    }
850}
851
852/// Estimate how much memory is needed to sort a `RecordBatch`.
853///
854/// This is used to pre-reserve memory for the sort/merge. The sort/merge process involves
855/// creating sorted copies of sorted columns in record batches for speeding up comparison
856/// in sorting and merging. The sorted copies are in either row format or array format.
857/// Please refer to cursor.rs and stream.rs for more details. No matter what format the
858/// sorted copies are, they will use more memory than the original record batch.
859///
860/// This can basically be calculated as the sum of the actual space it takes in
861/// memory (which would be larger for a sliced batch), and the size of the actual data.
862pub(crate) fn get_reserved_bytes_for_record_batch_size(
863    record_batch_size: usize,
864    sliced_size: usize,
865) -> usize {
866    // Even 2x may not be enough for some cases, but it's a good enough estimation as a baseline.
867    // If 2x is not enough, user can set a larger value for `sort_spill_reservation_bytes`
868    // to compensate for the extra memory needed.
869    record_batch_size + sliced_size
870}
871
872/// Estimate how much memory is needed to sort a `RecordBatch`.
873/// This will just call `get_reserved_bytes_for_record_batch_size` with the
874/// memory size of the record batch and its sliced size.
875pub(crate) fn get_reserved_bytes_for_record_batch(batch: &RecordBatch) -> Result<usize> {
876    batch.get_sliced_size().map(|sliced_size| {
877        get_reserved_bytes_for_record_batch_size(
878            get_record_batch_memory_size(batch),
879            sliced_size,
880        )
881    })
882}
883
884impl Debug for ExternalSorter {
885    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
886        f.debug_struct("ExternalSorter")
887            .field("memory_used", &self.used())
888            .field("spilled_bytes", &self.spilled_bytes())
889            .field("spilled_rows", &self.spilled_rows())
890            .field("spill_count", &self.spill_count())
891            .finish()
892    }
893}
894
895pub fn sort_batch(
896    batch: &RecordBatch,
897    expressions: &LexOrdering,
898    fetch: Option<usize>,
899) -> Result<RecordBatch> {
900    let sort_columns = expressions
901        .iter()
902        .map(|expr| expr.evaluate_to_sort_column(batch))
903        .collect::<Result<Vec<_>>>()?;
904
905    let indices = lexsort_to_indices(&sort_columns, fetch)?;
906    let columns = take_arrays(batch.columns(), &indices, None)?;
907
908    let options = RecordBatchOptions::new().with_row_count(Some(indices.len()));
909    Ok(RecordBatch::try_new_with_options(
910        batch.schema(),
911        columns,
912        &options,
913    )?)
914}
915
916/// Sort a batch and return the result as multiple batches of size `batch_size`.
917/// This is useful when you want to avoid creating one large sorted batch in memory,
918/// and instead want to process the sorted data in smaller chunks.
919pub fn sort_batch_chunked(
920    batch: &RecordBatch,
921    expressions: &LexOrdering,
922    batch_size: usize,
923) -> Result<Vec<RecordBatch>> {
924    IncrementalSortIterator::new(batch.clone(), expressions.clone(), batch_size).collect()
925}
926
927/// Sort execution plan.
928///
929/// Support sorting datasets that are larger than the memory allotted
930/// by the memory manager, by spilling to disk.
931#[derive(Debug, Clone)]
932pub struct SortExec {
933    /// Input schema
934    pub(crate) input: Arc<dyn ExecutionPlan>,
935    /// Sort expressions
936    expr: LexOrdering,
937    /// Containing all metrics set created during sort
938    metrics_set: ExecutionPlanMetricsSet,
939    /// Preserve partitions of input plan. If false, the input partitions
940    /// will be sorted and merged into a single output partition.
941    preserve_partitioning: bool,
942    /// Fetch highest/lowest n results
943    fetch: Option<usize>,
944    /// Normalized common sort prefix between the input and the sort expressions (only used with fetch)
945    common_sort_prefix: Vec<PhysicalSortExpr>,
946    /// Cache holding plan properties like equivalences, output partitioning etc.
947    cache: Arc<PlanProperties>,
948    /// Filter matching the state of the sort for dynamic filter pushdown.
949    /// If `fetch` is `Some`, this will also be set and a TopK operator may be used.
950    /// If `fetch` is `None`, this will be `None`.
951    filter: Option<Arc<RwLock<TopKDynamicFilters>>>,
952}
953
954impl SortExec {
955    /// Create a new sort execution plan that produces a single,
956    /// sorted output partition.
957    pub fn new(expr: LexOrdering, input: Arc<dyn ExecutionPlan>) -> Self {
958        let preserve_partitioning = false;
959        let (cache, sort_prefix) =
960            Self::compute_properties(&input, expr.clone(), preserve_partitioning)
961                .unwrap();
962        Self {
963            expr,
964            input,
965            metrics_set: ExecutionPlanMetricsSet::new(),
966            preserve_partitioning,
967            fetch: None,
968            common_sort_prefix: sort_prefix,
969            cache: Arc::new(cache),
970            filter: None,
971        }
972    }
973
974    /// Whether this `SortExec` preserves partitioning of the children
975    pub fn preserve_partitioning(&self) -> bool {
976        self.preserve_partitioning
977    }
978
979    /// Specify the partitioning behavior of this sort exec
980    ///
981    /// If `preserve_partitioning` is true, sorts each partition
982    /// individually, producing one sorted stream for each input partition.
983    ///
984    /// If `preserve_partitioning` is false, sorts and merges all
985    /// input partitions producing a single, sorted partition.
986    pub fn with_preserve_partitioning(mut self, preserve_partitioning: bool) -> Self {
987        self.preserve_partitioning = preserve_partitioning;
988        Arc::make_mut(&mut self.cache).partitioning =
989            Self::output_partitioning_helper(&self.input, self.preserve_partitioning);
990        if self.fetch.is_some() {
991            self.rebuild_filter_for_current_partitioning();
992        }
993        self
994    }
995
996    fn topk_emitter_count(&self) -> usize {
997        self.cache.output_partitioning().partition_count()
998    }
999
1000    /// Build a new shared TopK dynamic filter wrapper for this `SortExec`.
1001    fn create_filter(&self) -> Arc<RwLock<TopKDynamicFilters>> {
1002        let children = self
1003            .expr
1004            .iter()
1005            .map(|sort_expr| Arc::clone(&sort_expr.expr))
1006            .collect::<Vec<_>>();
1007        self.create_filter_with_expr(Arc::new(DynamicFilterPhysicalExpr::new(
1008            children,
1009            lit(true),
1010        )))
1011    }
1012
1013    fn create_filter_with_expr(
1014        &self,
1015        expr: Arc<DynamicFilterPhysicalExpr>,
1016    ) -> Arc<RwLock<TopKDynamicFilters>> {
1017        Arc::new(RwLock::new(
1018            TopKDynamicFilters::new_with_topk_emitter_count(
1019                expr,
1020                self.topk_emitter_count(),
1021            ),
1022        ))
1023    }
1024
1025    /// Rebuild the shared TopK filter wrapper for the current output partitioning.
1026    ///
1027    /// The dynamic filter expression is preserved, but wrapper state such as the
1028    /// shared threshold and remaining emitter count is reset for the new
1029    /// partitioning.
1030    fn rebuild_filter_for_current_partitioning(&mut self) {
1031        let filter_expr = self.filter.as_ref().map(|filter| filter.read().expr());
1032        if let Some(filter_expr) = filter_expr {
1033            self.filter = Some(self.create_filter_with_expr(filter_expr));
1034        }
1035    }
1036
1037    fn cloned(&self) -> Self {
1038        SortExec {
1039            input: Arc::clone(&self.input),
1040            expr: self.expr.clone(),
1041            metrics_set: self.metrics_set.clone(),
1042            preserve_partitioning: self.preserve_partitioning,
1043            common_sort_prefix: self.common_sort_prefix.clone(),
1044            fetch: self.fetch,
1045            cache: Arc::clone(&self.cache),
1046            filter: self.filter.clone(),
1047        }
1048    }
1049
1050    /// Modify how many rows to include in the result
1051    ///
1052    /// If None, then all rows will be returned, in sorted order.
1053    /// If Some, then only the top `fetch` rows will be returned.
1054    /// This can reduce the memory pressure required by the sort
1055    /// operation since rows that are not going to be included
1056    /// can be dropped.
1057    pub fn with_fetch(&self, fetch: Option<usize>) -> Self {
1058        let mut cache = PlanProperties::clone(&self.cache);
1059        // If the SortExec can emit incrementally (that means the sort requirements
1060        // and properties of the input match), the SortExec can generate its result
1061        // without scanning the entire input when a fetch value exists.
1062        let is_pipeline_friendly = matches!(
1063            cache.emission_type,
1064            EmissionType::Incremental | EmissionType::Both
1065        );
1066        if fetch.is_some() && is_pipeline_friendly {
1067            cache = cache.with_boundedness(Boundedness::Bounded);
1068        }
1069        let mut new_sort = self.cloned();
1070        new_sort.fetch = fetch;
1071        new_sort.cache = cache.into();
1072        if fetch.is_some() {
1073            if new_sort.filter.is_some() {
1074                // Keep the dynamic filter expression, but reset wrapper state
1075                // such as the shared threshold and expected emitter count.
1076                new_sort.rebuild_filter_for_current_partitioning();
1077            } else {
1078                new_sort.filter = Some(new_sort.create_filter());
1079            }
1080        } else {
1081            new_sort.filter = None;
1082        }
1083        new_sort
1084    }
1085
1086    /// Input schema
1087    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
1088        &self.input
1089    }
1090
1091    /// Sort expressions
1092    pub fn expr(&self) -> &LexOrdering {
1093        &self.expr
1094    }
1095
1096    /// If `Some(fetch)`, limits output to only the first "fetch" items
1097    pub fn fetch(&self) -> Option<usize> {
1098        self.fetch
1099    }
1100
1101    /// Returns the dynamic filter expression for this sort (TopK), if set.
1102    #[deprecated(
1103        since = "55.0.0",
1104        note = "Use ExecutionPlan::dynamic_expressions_produced instead"
1105    )]
1106    pub fn dynamic_filter_expr(&self) -> Option<Arc<DynamicFilterPhysicalExpr>> {
1107        self.filter.as_ref().map(|f| f.read().expr())
1108    }
1109
1110    /// Replace the dynamic filter expression for this sort.
1111    ///
1112    ///
1113    /// Resets any internal state which may depend on the previous dynamic filter.
1114    ///
1115    /// Validates that the filter's children reference valid columns in
1116    /// the sort's input schema.
1117    pub fn with_dynamic_filter_expr(
1118        mut self,
1119        filter: Arc<DynamicFilterPhysicalExpr>,
1120    ) -> Result<Self> {
1121        let input_schema = self.input.schema();
1122        for child in filter.children() {
1123            child.data_type(&input_schema)?;
1124        }
1125        self.filter = Some(self.create_filter_with_expr(filter));
1126        Ok(self)
1127    }
1128
1129    fn output_partitioning_helper(
1130        input: &Arc<dyn ExecutionPlan>,
1131        preserve_partitioning: bool,
1132    ) -> Partitioning {
1133        // Get output partitioning:
1134        if preserve_partitioning {
1135            input.output_partitioning().clone()
1136        } else {
1137            Partitioning::UnknownPartitioning(1)
1138        }
1139    }
1140
1141    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
1142    /// It also returns the common sort prefix between the input and the sort expressions.
1143    fn compute_properties(
1144        input: &Arc<dyn ExecutionPlan>,
1145        sort_exprs: LexOrdering,
1146        preserve_partitioning: bool,
1147    ) -> Result<(PlanProperties, Vec<PhysicalSortExpr>)> {
1148        let (sort_prefix, sort_satisfied) = input
1149            .equivalence_properties()
1150            .extract_common_sort_prefix(sort_exprs.clone())?;
1151
1152        // The emission type depends on whether the input is already sorted:
1153        // - If already fully sorted, we can emit results in the same way as the input
1154        // - If not sorted, we must wait until all data is processed to emit results (Final)
1155        let emission_type = if sort_satisfied {
1156            input.pipeline_behavior()
1157        } else {
1158            EmissionType::Final
1159        };
1160
1161        // The boundedness depends on whether the input is already sorted:
1162        // - If already sorted, we have the same property as the input
1163        // - If not sorted and input is unbounded, we require infinite memory and generates
1164        //   unbounded data (not practical).
1165        // - If not sorted and input is bounded, then the SortExec is bounded, too.
1166        let boundedness = if sort_satisfied {
1167            input.boundedness()
1168        } else {
1169            match input.boundedness() {
1170                Boundedness::Unbounded { .. } => Boundedness::Unbounded {
1171                    requires_infinite_memory: true,
1172                },
1173                bounded => bounded,
1174            }
1175        };
1176
1177        // Calculate equivalence properties; i.e. reset the ordering equivalence
1178        // class with the new ordering:
1179        let mut eq_properties = input.equivalence_properties().clone();
1180        eq_properties.reorder(sort_exprs)?;
1181
1182        // Get output partitioning:
1183        let output_partitioning =
1184            Self::output_partitioning_helper(input, preserve_partitioning);
1185
1186        Ok((
1187            PlanProperties::new(
1188                eq_properties,
1189                output_partitioning,
1190                emission_type,
1191                boundedness,
1192            ),
1193            sort_prefix,
1194        ))
1195    }
1196}
1197
1198impl DisplayAs for SortExec {
1199    fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result {
1200        match t {
1201            DisplayFormatType::Default | DisplayFormatType::Verbose => {
1202                let preserve_partitioning = self.preserve_partitioning;
1203                match self.fetch {
1204                    Some(fetch) => {
1205                        write!(
1206                            f,
1207                            "SortExec: TopK(fetch={fetch}), expr=[{}], preserve_partitioning=[{preserve_partitioning}]",
1208                            self.expr
1209                        )?;
1210                        if let Some(filter) = &self.filter
1211                            && let Ok(current) = filter.read().expr().current()
1212                            && !current.eq(&lit(true))
1213                        {
1214                            write!(f, ", filter=[{current}]")?;
1215                        }
1216                        if !self.common_sort_prefix.is_empty() {
1217                            write!(f, ", sort_prefix=[")?;
1218                            let mut first = true;
1219                            for sort_expr in &self.common_sort_prefix {
1220                                if first {
1221                                    first = false;
1222                                } else {
1223                                    write!(f, ", ")?;
1224                                }
1225                                write!(f, "{sort_expr}")?;
1226                            }
1227                            write!(f, "]")
1228                        } else {
1229                            Ok(())
1230                        }
1231                    }
1232                    None => write!(
1233                        f,
1234                        "SortExec: expr=[{}], preserve_partitioning=[{preserve_partitioning}]",
1235                        self.expr
1236                    ),
1237                }
1238            }
1239            DisplayFormatType::TreeRender => match self.fetch {
1240                Some(fetch) => {
1241                    writeln!(f, "{}", self.expr)?;
1242                    writeln!(f, "limit={fetch}")
1243                }
1244                None => {
1245                    writeln!(f, "{}", self.expr)
1246                }
1247            },
1248        }
1249    }
1250}
1251
1252impl ExecutionPlan for SortExec {
1253    fn name(&self) -> &'static str {
1254        match self.fetch {
1255            Some(_) => "SortExec(TopK)",
1256            None => "SortExec",
1257        }
1258    }
1259
1260    fn properties(&self) -> &Arc<PlanProperties> {
1261        &self.cache
1262    }
1263
1264    fn required_input_distribution(&self) -> Vec<Distribution> {
1265        self.input_distribution_requirements().into_per_child()
1266    }
1267
1268    fn input_distribution_requirements(&self) -> crate::InputDistributionRequirements {
1269        crate::InputDistributionRequirements::new(if self.preserve_partitioning {
1270            vec![Distribution::UnspecifiedDistribution]
1271        } else {
1272            // global sort
1273            // TODO support range partitioning and OrderedDistribution.
1274            // See https://github.com/apache/datafusion/issues/22395
1275            vec![Distribution::SinglePartition]
1276        })
1277    }
1278
1279    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1280        vec![&self.input]
1281    }
1282
1283    fn apply_expressions(
1284        &self,
1285        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
1286    ) -> Result<TreeNodeRecursion> {
1287        let dynamic_filter = self
1288            .filter
1289            .as_ref()
1290            .map(|filter| filter.read().expr() as Arc<dyn PhysicalExpr>);
1291        crate::apply_expression_roots(
1292            self.expr
1293                .iter()
1294                .map(|sort_expr| &sort_expr.expr)
1295                .chain(dynamic_filter.iter()),
1296            f,
1297        )
1298    }
1299
1300    fn dynamic_expressions_produced(&self) -> Vec<Arc<dyn PhysicalExpr>> {
1301        self.filter
1302            .iter()
1303            .map(|filter| filter.read().expr() as Arc<dyn PhysicalExpr>)
1304            .collect()
1305    }
1306
1307    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
1308        vec![false]
1309    }
1310
1311    fn replace_children(
1312        self: Arc<Self>,
1313        children: Vec<Arc<dyn ExecutionPlan>>,
1314        options: ReplaceChildrenOptions,
1315    ) -> Result<Arc<dyn ExecutionPlan>> {
1316        let mut new_sort = self.cloned();
1317        assert_eq!(children.len(), 1, "SortExec should have exactly one child");
1318        new_sort.input = Arc::clone(&children[0]);
1319
1320        if options.children_properties == ChildrenPropertiesMode::Recompute {
1321            // Recompute the properties based on the new input since they may have changed.
1322            let (cache, sort_prefix) = Self::compute_properties(
1323                &new_sort.input,
1324                new_sort.expr.clone(),
1325                new_sort.preserve_partitioning,
1326            )?;
1327            new_sort.cache = Arc::new(cache);
1328            new_sort.common_sort_prefix = sort_prefix;
1329            if new_sort.fetch.is_some() {
1330                new_sort.rebuild_filter_for_current_partitioning();
1331            }
1332        }
1333
1334        Ok(Arc::new(new_sort))
1335    }
1336
1337    fn with_new_children(
1338        self: Arc<Self>,
1339        children: Vec<Arc<dyn ExecutionPlan>>,
1340    ) -> Result<Arc<dyn ExecutionPlan>> {
1341        match has_same_children_properties(self.as_ref(), &children)? {
1342            true => self.replace_children(
1343                children,
1344                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
1345            ),
1346            false => self.replace_children(
1347                children,
1348                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1349            ),
1350        }
1351    }
1352
1353    fn reset_state(self: Arc<Self>) -> Result<Arc<dyn ExecutionPlan>> {
1354        let children = self.children().into_iter().cloned().collect();
1355        let new_sort = replace_children_if_necessary(self, children)?;
1356        let mut new_sort = new_sort
1357            .downcast_ref::<SortExec>()
1358            .expect("rebuilt SortExec with new children")
1359            .clone();
1360        // Our dynamic filter and execution metrics are the state we need to reset.
1361        new_sort.filter = Some(new_sort.create_filter());
1362        new_sort.metrics_set = ExecutionPlanMetricsSet::new();
1363
1364        Ok(Arc::new(new_sort))
1365    }
1366
1367    fn execute(
1368        &self,
1369        partition: usize,
1370        context: Arc<TaskContext>,
1371    ) -> Result<SendableRecordBatchStream> {
1372        trace!(
1373            "Start SortExec::execute for partition {} of context session_id {} and task_id {:?}",
1374            partition,
1375            context.session_id(),
1376            context.task_id()
1377        );
1378
1379        let mut input = self.input.execute(partition, Arc::clone(&context))?;
1380
1381        let execution_options = &context.session_config().options().execution;
1382
1383        trace!("End SortExec's input.execute for partition: {partition}");
1384
1385        let sort_satisfied = self
1386            .input
1387            .equivalence_properties()
1388            .ordering_satisfy(self.expr.clone())?;
1389
1390        match (sort_satisfied, self.fetch.as_ref()) {
1391            (true, Some(fetch)) => Ok(Box::pin(LimitStream::new(
1392                input,
1393                0,
1394                Some(*fetch),
1395                BaselineMetrics::new(&self.metrics_set, partition),
1396            ))),
1397            (true, None) => Ok(input),
1398            (false, Some(fetch)) => {
1399                let filter = self.filter.clone();
1400                let mut topk = TopK::try_new(
1401                    partition,
1402                    input.schema(),
1403                    self.common_sort_prefix.clone(),
1404                    self.expr.clone(),
1405                    *fetch,
1406                    context.session_config().batch_size(),
1407                    context.runtime_env(),
1408                    &self.metrics_set,
1409                    Arc::clone(&unwrap_or_internal_err!(filter)),
1410                )?;
1411                Ok(Box::pin(RecordBatchStreamAdapter::new(
1412                    self.schema(),
1413                    futures::stream::once(async move {
1414                        while let Some(batch) = input.next().await {
1415                            let batch = batch?;
1416                            topk.insert_batch(batch)?;
1417                            if topk.finished {
1418                                break;
1419                            }
1420                        }
1421                        drop(input);
1422                        topk.emit()
1423                    })
1424                    .try_flatten(),
1425                )))
1426            }
1427            (false, None) => {
1428                let mut sorter = ExternalSorter::new(
1429                    partition,
1430                    input.schema(),
1431                    self.expr.clone(),
1432                    context.session_config().batch_size(),
1433                    execution_options.sort_spill_reservation_bytes,
1434                    execution_options.sort_in_place_threshold_bytes,
1435                    context.session_config().spill_compression(),
1436                    &self.metrics_set,
1437                    context.runtime_env(),
1438                )?;
1439                Ok(Box::pin(RecordBatchStreamAdapter::new(
1440                    self.schema(),
1441                    futures::stream::once(async move {
1442                        while let Some(batch) = input.next().await {
1443                            let batch = batch?;
1444                            sorter.insert_batch(batch).await?;
1445                        }
1446                        drop(input);
1447                        sorter.sort().await
1448                    })
1449                    .try_flatten(),
1450                )))
1451            }
1452        }
1453    }
1454
1455    fn metrics(&self) -> Option<MetricsSet> {
1456        Some(self.metrics_set.clone_inner())
1457    }
1458
1459    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
1460        let child_partition = if self.preserve_partitioning() {
1461            partition
1462        } else {
1463            None
1464        };
1465        vec![ChildStats::At(child_partition)]
1466    }
1467
1468    fn statistics_from_inputs(
1469        &self,
1470        input_stats: &[Arc<Statistics>],
1471        _args: &StatisticsArgs,
1472    ) -> Result<Arc<Statistics>> {
1473        let stats = input_stats[0].as_ref().clone();
1474        Ok(Arc::new(stats.with_fetch(self.fetch, 0, 1)?))
1475    }
1476
1477    fn with_fetch(&self, limit: Option<usize>) -> Option<Arc<dyn ExecutionPlan>> {
1478        Some(Arc::new(SortExec::with_fetch(self, limit)))
1479    }
1480
1481    fn fetch(&self) -> Option<usize> {
1482        self.fetch
1483    }
1484
1485    fn cardinality_effect(&self) -> CardinalityEffect {
1486        if self.fetch.is_none() {
1487            CardinalityEffect::Equal
1488        } else {
1489            CardinalityEffect::LowerEqual
1490        }
1491    }
1492
1493    /// Tries to swap the projection with its input [`SortExec`]. If it can be done,
1494    /// it returns the new swapped version having the [`SortExec`] as the top plan.
1495    /// Otherwise, it returns None.
1496    fn try_swapping_with_projection(
1497        &self,
1498        projection: &ProjectionExec,
1499    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
1500        // If the projection does not narrow the schema, we should not try to push it down.
1501        if projection.expr().len() >= projection.input().schema().fields().len() {
1502            return Ok(None);
1503        }
1504
1505        let Some(updated_exprs) = update_ordering(self.expr.clone(), projection.expr())?
1506        else {
1507            return Ok(None);
1508        };
1509
1510        Ok(Some(Arc::new(
1511            SortExec::new(updated_exprs, make_with_child(projection, self.input())?)
1512                .with_fetch(self.fetch())
1513                .with_preserve_partitioning(self.preserve_partitioning()),
1514        )))
1515    }
1516
1517    fn gather_filters_for_pushdown(
1518        &self,
1519        phase: FilterPushdownPhase,
1520        parent_filters: Vec<Arc<dyn PhysicalExpr>>,
1521        config: &datafusion_common::config::ConfigOptions,
1522    ) -> Result<FilterDescription> {
1523        if phase != FilterPushdownPhase::Post {
1524            if self.fetch.is_some() {
1525                return Ok(FilterDescription::all_unsupported(
1526                    &parent_filters,
1527                    &self.children(),
1528                ));
1529            }
1530            return FilterDescription::from_children(parent_filters, &self.children());
1531        }
1532
1533        // In Post phase: block parent filters when fetch is set,
1534        // but still push the TopK dynamic filter (self-filter).
1535        let mut child = if self.fetch.is_some() {
1536            ChildFilterDescription::all_unsupported(&parent_filters)
1537        } else {
1538            ChildFilterDescription::from_child(&parent_filters, self.input())?
1539        };
1540
1541        if let Some(filter) = &self.filter
1542            && config.optimizer.enable_topk_dynamic_filter_pushdown
1543        {
1544            child = child.with_self_filter(filter.read().expr());
1545        }
1546
1547        Ok(FilterDescription::new().with_child(child))
1548    }
1549
1550    fn handle_child_pushdown_result(
1551        &self,
1552        _phase: FilterPushdownPhase,
1553        child_pushdown_result: ChildPushdownResult,
1554        _config: &datafusion_common::config::ConfigOptions,
1555    ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
1556        // For a plain sort (no fetch) we intercept any unsupported filters
1557        // by inserting a FilterExec below this Sort. Moving the filter below
1558        // Sort is safe because Sort preserves all rows.
1559        //
1560        // Why not fetch (TopK)?
1561        // A sort with fetch limits the number of output rows.  Inserting a
1562        // FilterExec *below* the TopK would change semantics.  A filter *above*
1563        // the TopK is supposed to post-filter its output (e.g. "take the top 10
1564        // rows, then keep only those with a > 5").  Pushing the filter below
1565        // Sort changes the meaning to "filter first, then take top 10", which
1566        // produces a different result.
1567        if self.fetch.is_some() {
1568            return Ok(FilterPushdownPropagation::if_all(child_pushdown_result));
1569        }
1570
1571        // Collect parent filters that were NOT successfully pushed to our child.
1572        let unsupported_filters: Vec<Arc<dyn PhysicalExpr>> = child_pushdown_result
1573            .parent_filters
1574            .iter()
1575            .filter(|&f| matches!(f.all(), PushedDown::No))
1576            .map(|f| Arc::clone(&f.filter))
1577            .collect();
1578
1579        if unsupported_filters.is_empty() {
1580            // All filters were pushed — nothing extra to do.
1581            return Ok(FilterPushdownPropagation::if_all(child_pushdown_result));
1582        }
1583
1584        // Build a single conjunctive predicate from the unsupported filters
1585        // and insert a FilterExec between this SortExec and its child.
1586        let predicate = datafusion_physical_expr::conjunction(unsupported_filters);
1587        let new_child =
1588            Arc::new(FilterExec::try_new(predicate, Arc::clone(self.input()))?)
1589                as Arc<dyn ExecutionPlan>;
1590        let new_sort = Arc::new(
1591            SortExec::new(self.expr.clone(), new_child)
1592                .with_fetch(self.fetch())
1593                .with_preserve_partitioning(self.preserve_partitioning()),
1594        ) as Arc<dyn ExecutionPlan>;
1595
1596        Ok(FilterPushdownPropagation {
1597            filters: vec![PushedDown::Yes; child_pushdown_result.parent_filters.len()],
1598            updated_node: Some(new_sort),
1599        })
1600    }
1601    #[cfg(feature = "proto")]
1602    fn try_to_proto(
1603        &self,
1604        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
1605    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
1606        use datafusion_proto_models::protobuf;
1607        let input = ctx.encode_child(self.input())?;
1608        let expr = self
1609            .expr()
1610            .iter()
1611            .map(|sort_expr| {
1612                let sort_node = Box::new(protobuf::PhysicalSortExprNode {
1613                    expr: Some(Box::new(ctx.encode_expr(&sort_expr.expr)?)),
1614                    asc: !sort_expr.options.descending,
1615                    nulls_first: sort_expr.options.nulls_first,
1616                });
1617                Ok(protobuf::PhysicalExprNode {
1618                    expr_id: None,
1619                    expr_type: Some(protobuf::physical_expr_node::ExprType::Sort(
1620                        sort_node,
1621                    )),
1622                })
1623            })
1624            .collect::<Result<Vec<_>>>()?;
1625        let dynamic_filter = self
1626            .dynamic_expressions_produced()
1627            .into_iter()
1628            .next()
1629            .map(|expr| ctx.encode_expr(&expr))
1630            .transpose()?;
1631        Ok(Some(protobuf::PhysicalPlanNode {
1632            physical_plan_type: Some(
1633                protobuf::physical_plan_node::PhysicalPlanType::Sort(Box::new(
1634                    protobuf::SortExecNode {
1635                        input: Some(Box::new(input)),
1636                        expr,
1637                        fetch: match self.fetch() {
1638                            Some(n) => n as i64,
1639                            None => -1,
1640                        },
1641                        preserve_partitioning: self.preserve_partitioning(),
1642                        dynamic_filter,
1643                    },
1644                )),
1645            ),
1646        }))
1647    }
1648}
1649
1650#[cfg(feature = "proto")]
1651impl SortExec {
1652    pub fn try_from_proto(
1653        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
1654        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
1655    ) -> Result<Arc<dyn ExecutionPlan>> {
1656        use datafusion_proto_models::protobuf;
1657        use protobuf::physical_expr_node::ExprType;
1658        let sort = crate::expect_plan_variant!(
1659            node,
1660            protobuf::physical_plan_node::PhysicalPlanType::Sort,
1661            "SortExec",
1662        );
1663        let input =
1664            ctx.decode_required_child(sort.input.as_deref(), "SortExec", "input")?;
1665        let input_schema = input.schema();
1666        let exprs = sort
1667            .expr
1668            .iter()
1669            .map(|expr| {
1670                let Some(ExprType::Sort(sort_expr)) = expr.expr_type.as_ref() else {
1671                    return datafusion_common::internal_err!(
1672                        "SortExec expr must be a sort expression"
1673                    );
1674                };
1675                let expr_node = sort_expr.expr.as_deref().ok_or_else(|| {
1676                    internal_datafusion_err!(
1677                        "SortExec sort expression is missing its inner expr"
1678                    )
1679                })?;
1680                Ok(PhysicalSortExpr {
1681                    expr: ctx.decode_expr(expr_node, input_schema.as_ref())?,
1682                    options: arrow::compute::SortOptions {
1683                        descending: !sort_expr.asc,
1684                        nulls_first: sort_expr.nulls_first,
1685                    },
1686                })
1687            })
1688            .collect::<Result<Vec<_>>>()?;
1689        let Some(ordering) = LexOrdering::new(exprs) else {
1690            return datafusion_common::internal_err!("SortExec requires an ordering");
1691        };
1692        let fetch = (sort.fetch >= 0).then_some(sort.fetch as usize);
1693        let new_sort = SortExec::new(ordering, input)
1694            .with_fetch(fetch)
1695            .with_preserve_partitioning(sort.preserve_partitioning);
1696
1697        let new_sort = if let Some(df_proto) = &sort.dynamic_filter {
1698            let df_expr =
1699                ctx.decode_expr(df_proto, new_sort.input().schema().as_ref())?;
1700            let df = (df_expr as Arc<dyn std::any::Any + Send + Sync>)
1701                .downcast::<DynamicFilterPhysicalExpr>()
1702                .map_err(|_| {
1703                    internal_datafusion_err!(
1704                        "SortExec dynamic_filter did not decode to a DynamicFilterPhysicalExpr"
1705                    )
1706                })?;
1707            new_sort.with_dynamic_filter_expr(df)?
1708        } else {
1709            new_sort
1710        };
1711
1712        Ok(Arc::new(new_sort))
1713    }
1714}
1715
1716#[cfg(test)]
1717mod tests {
1718    use std::collections::HashMap;
1719    use std::pin::Pin;
1720    use std::task::{Context, Poll};
1721
1722    use super::*;
1723    use crate::coalesce_partitions::CoalescePartitionsExec;
1724    use crate::collect;
1725    use crate::empty::EmptyExec;
1726    use crate::execution_plan::Boundedness;
1727    use crate::expressions::col;
1728    use crate::filter_pushdown::{FilterPushdownPhase, PushedDown};
1729    use crate::test;
1730    use crate::test::TestMemoryExec;
1731    use crate::test::exec::{BlockingExec, assert_strong_count_converges_to_zero};
1732    use crate::test::{assert_is_pending, make_partition};
1733
1734    use arrow::array::*;
1735    use arrow::compute::SortOptions;
1736    use arrow::datatypes::*;
1737    use datafusion_common::ScalarValue;
1738    use datafusion_common::cast::as_primitive_array;
1739    use datafusion_common::config::ConfigOptions;
1740    use datafusion_common::test_util::batches_to_string;
1741    use datafusion_execution::RecordBatchStream;
1742    use datafusion_execution::config::SessionConfig;
1743    use datafusion_execution::memory_pool::{
1744        GreedyMemoryPool, MemoryConsumer, MemoryPool,
1745    };
1746    use datafusion_execution::runtime_env::RuntimeEnvBuilder;
1747    use datafusion_physical_expr::expressions::{Column, Literal};
1748    use datafusion_physical_expr::{DynamicFilterTracking, EquivalenceProperties};
1749
1750    use datafusion_physical_expr_common::metrics::MetricValue;
1751    use futures::{FutureExt, Stream, TryStreamExt};
1752    use insta::assert_snapshot;
1753
1754    #[derive(Debug, Clone)]
1755    pub struct SortedUnboundedExec {
1756        schema: Schema,
1757        batch_size: u64,
1758        cache: Arc<PlanProperties>,
1759    }
1760
1761    impl DisplayAs for SortedUnboundedExec {
1762        fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result {
1763            match t {
1764                DisplayFormatType::Default
1765                | DisplayFormatType::Verbose
1766                | DisplayFormatType::TreeRender => write!(f, "UnboundableExec",).unwrap(),
1767            }
1768            Ok(())
1769        }
1770    }
1771
1772    impl SortedUnboundedExec {
1773        fn compute_properties(schema: SchemaRef) -> PlanProperties {
1774            let mut eq_properties = EquivalenceProperties::new(schema);
1775            eq_properties.add_ordering([PhysicalSortExpr::new_default(Arc::new(
1776                Column::new("c1", 0),
1777            ))]);
1778            PlanProperties::new(
1779                eq_properties,
1780                Partitioning::UnknownPartitioning(1),
1781                EmissionType::Final,
1782                Boundedness::Unbounded {
1783                    requires_infinite_memory: false,
1784                },
1785            )
1786        }
1787    }
1788
1789    impl ExecutionPlan for SortedUnboundedExec {
1790        fn name(&self) -> &'static str {
1791            Self::static_name()
1792        }
1793
1794        fn properties(&self) -> &Arc<PlanProperties> {
1795            &self.cache
1796        }
1797
1798        fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1799            vec![]
1800        }
1801
1802        fn replace_children(
1803            self: Arc<Self>,
1804            _: Vec<Arc<dyn ExecutionPlan>>,
1805            _: ReplaceChildrenOptions,
1806        ) -> Result<Arc<dyn ExecutionPlan>> {
1807            Ok(self)
1808        }
1809
1810        fn apply_expressions(
1811            &self,
1812            _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
1813        ) -> Result<TreeNodeRecursion> {
1814            Ok(TreeNodeRecursion::Continue)
1815        }
1816
1817        fn with_new_children(
1818            self: Arc<Self>,
1819            children: Vec<Arc<dyn ExecutionPlan>>,
1820        ) -> Result<Arc<dyn ExecutionPlan>> {
1821            self.replace_children(
1822                children,
1823                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1824            )
1825        }
1826
1827        fn execute(
1828            &self,
1829            _partition: usize,
1830            _context: Arc<TaskContext>,
1831        ) -> Result<SendableRecordBatchStream> {
1832            Ok(Box::pin(SortedUnboundedStream {
1833                schema: Arc::new(self.schema.clone()),
1834                batch_size: self.batch_size,
1835                offset: 0,
1836            }))
1837        }
1838    }
1839
1840    #[derive(Debug)]
1841    pub struct SortedUnboundedStream {
1842        schema: SchemaRef,
1843        batch_size: u64,
1844        offset: u64,
1845    }
1846
1847    impl Stream for SortedUnboundedStream {
1848        type Item = Result<RecordBatch>;
1849
1850        fn poll_next(
1851            mut self: Pin<&mut Self>,
1852            _cx: &mut Context<'_>,
1853        ) -> Poll<Option<Self::Item>> {
1854            let batch = SortedUnboundedStream::create_record_batch(
1855                Arc::clone(&self.schema),
1856                self.offset,
1857                self.batch_size,
1858            );
1859            self.offset += self.batch_size;
1860            Poll::Ready(Some(Ok(batch)))
1861        }
1862    }
1863
1864    impl RecordBatchStream for SortedUnboundedStream {
1865        fn schema(&self) -> SchemaRef {
1866            Arc::clone(&self.schema)
1867        }
1868    }
1869
1870    impl SortedUnboundedStream {
1871        fn create_record_batch(
1872            schema: SchemaRef,
1873            offset: u64,
1874            batch_size: u64,
1875        ) -> RecordBatch {
1876            let values = (0..batch_size).map(|i| offset + i).collect::<Vec<_>>();
1877            let array = UInt64Array::from(values);
1878            let array_ref: ArrayRef = Arc::new(array);
1879            RecordBatch::try_new(schema, vec![array_ref]).unwrap()
1880        }
1881    }
1882
1883    #[tokio::test]
1884    async fn test_in_mem_sort() -> Result<()> {
1885        let task_ctx = Arc::new(TaskContext::default());
1886        let partitions = 4;
1887        let csv = test::scan_partitioned(partitions);
1888        let schema = csv.schema();
1889
1890        let sort_exec = Arc::new(SortExec::new(
1891            [PhysicalSortExpr {
1892                expr: col("i", &schema)?,
1893                options: SortOptions::default(),
1894            }]
1895            .into(),
1896            Arc::new(CoalescePartitionsExec::new(csv)),
1897        ));
1898
1899        let result = collect(sort_exec, Arc::clone(&task_ctx)).await?;
1900
1901        assert_eq!(result.len(), 1);
1902        assert_eq!(result[0].num_rows(), 400);
1903        assert_eq!(
1904            task_ctx.runtime_env().memory_pool.reserved(),
1905            0,
1906            "The sort should have returned all memory used back to the memory manager"
1907        );
1908
1909        Ok(())
1910    }
1911
1912    /// Single-column run coalescing: many small batches above a tiny in-place
1913    /// threshold (with ample memory, so no spill) must still produce a correct
1914    /// total order, including NULLs.
1915    #[tokio::test]
1916    async fn test_in_mem_sort_coalesced_runs() -> Result<()> {
1917        // Tiny in-place threshold forces the sort-then-merge path and, for a
1918        // single column, the coalescing branch. Ample memory => no spill.
1919        let task_ctx = Arc::new(
1920            TaskContext::default().with_session_config(
1921                SessionConfig::new()
1922                    .with_batch_size(64)
1923                    .with_sort_in_place_threshold_bytes(1024),
1924            ),
1925        );
1926
1927        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1928
1929        // Build many small batches of shuffled values with interspersed NULLs,
1930        // so coalescing produces several multi-row runs that must be merged.
1931        let num_batches = 40;
1932        let rows_per_batch = 50;
1933        let mut all_values: Vec<Option<i32>> = Vec::new();
1934        let mut batches = Vec::with_capacity(num_batches);
1935        for b in 0..num_batches {
1936            let mut col_values: Vec<Option<i32>> = Vec::with_capacity(rows_per_batch);
1937            for r in 0..rows_per_batch {
1938                let idx = (b * rows_per_batch + r) as i64;
1939                // Deterministic scramble to avoid any pre-existing ordering.
1940                let scrambled = ((idx.wrapping_mul(2_654_435_761)) % 1000) as i32;
1941                let v = if idx % 7 == 0 { None } else { Some(scrambled) };
1942                col_values.push(v);
1943                all_values.push(v);
1944            }
1945            let batch = RecordBatch::try_new(
1946                Arc::clone(&schema),
1947                vec![Arc::new(Int32Array::from(col_values))],
1948            )?;
1949            batches.push(batch);
1950        }
1951        let total_rows = num_batches * rows_per_batch;
1952
1953        let options = SortOptions::default();
1954        let sort_exec = Arc::new(SortExec::new(
1955            [PhysicalSortExpr {
1956                expr: col("a", &schema)?,
1957                options,
1958            }]
1959            .into(),
1960            TestMemoryExec::try_new_exec(
1961                std::slice::from_ref(&batches),
1962                Arc::clone(&schema),
1963                None,
1964            )?,
1965        ));
1966
1967        let result = collect(
1968            Arc::clone(&sort_exec) as Arc<dyn ExecutionPlan>,
1969            Arc::clone(&task_ctx),
1970        )
1971        .await?;
1972
1973        // Flatten the sorted output.
1974        let mut got: Vec<Option<i32>> = Vec::with_capacity(total_rows);
1975        for batch in &result {
1976            let arr = as_primitive_array::<Int32Type>(batch.column(0))?;
1977            for i in 0..arr.len() {
1978                got.push(if arr.is_null(i) {
1979                    None
1980                } else {
1981                    Some(arr.value(i))
1982                });
1983            }
1984        }
1985        assert_eq!(got.len(), total_rows, "row count must be preserved");
1986
1987        // Reference: sort the original values with the same semantics
1988        // (ascending, NULLs first per SortOptions::default()).
1989        let mut expected = all_values.clone();
1990        expected.sort_by(|a, b| match (a, b) {
1991            (None, None) => std::cmp::Ordering::Equal,
1992            (None, Some(_)) => std::cmp::Ordering::Less, // nulls_first
1993            (Some(_), None) => std::cmp::Ordering::Greater,
1994            (Some(x), Some(y)) => x.cmp(y),
1995        });
1996
1997        assert_eq!(
1998            got, expected,
1999            "coalesced-run sort output must be totally ordered"
2000        );
2001        assert_eq!(
2002            task_ctx.runtime_env().memory_pool.reserved(),
2003            0,
2004            "The sort should have returned all memory used back to the memory manager"
2005        );
2006
2007        Ok(())
2008    }
2009
2010    #[tokio::test]
2011    async fn test_sort_spill() -> Result<()> {
2012        // trigger spill w/ 100 batches
2013        let session_config = SessionConfig::new();
2014        let sort_spill_reservation_bytes = session_config
2015            .options()
2016            .execution
2017            .sort_spill_reservation_bytes;
2018        let runtime = RuntimeEnvBuilder::new()
2019            .with_memory_limit(sort_spill_reservation_bytes + 12288, 1.0)
2020            .build_arc()?;
2021        let task_ctx = Arc::new(
2022            TaskContext::default()
2023                .with_session_config(session_config)
2024                .with_runtime(runtime),
2025        );
2026
2027        // The input has 100 partitions, each partition has a batch containing 100 rows.
2028        // Each row has a single Int32 column with values 0..100. The total size of the
2029        // input is roughly 40000 bytes.
2030        let partitions = 100;
2031        let input = test::scan_partitioned(partitions);
2032        let schema = input.schema();
2033
2034        let sort_exec = Arc::new(SortExec::new(
2035            [PhysicalSortExpr {
2036                expr: col("i", &schema)?,
2037                options: SortOptions::default(),
2038            }]
2039            .into(),
2040            Arc::new(CoalescePartitionsExec::new(input)),
2041        ));
2042
2043        let result = collect(
2044            Arc::clone(&sort_exec) as Arc<dyn ExecutionPlan>,
2045            Arc::clone(&task_ctx),
2046        )
2047        .await?;
2048
2049        assert_eq!(result.len(), 2);
2050
2051        // Now, validate metrics
2052        let metrics = sort_exec.metrics().unwrap();
2053
2054        assert_eq!(metrics.output_rows().unwrap(), 10000);
2055        assert!(metrics.elapsed_compute().unwrap() > 0);
2056
2057        let spill_count = metrics.spill_count().unwrap();
2058        let spilled_rows = metrics.spilled_rows().unwrap();
2059        let spilled_bytes = metrics.spilled_bytes().unwrap();
2060        // Processing 40000 bytes of data using 12288 bytes of memory requires 3 spills
2061        // unless we do something really clever. It will spill roughly 9000+ rows and 36000
2062        // bytes. We leave a little wiggle room for the actual numbers.
2063        assert!((3..=10).contains(&spill_count));
2064        assert!((9000..=10000).contains(&spilled_rows));
2065        assert!((38000..=44000).contains(&spilled_bytes));
2066
2067        let columns = result[0].columns();
2068
2069        let i = as_primitive_array::<Int32Type>(&columns[0])?;
2070        assert_eq!(i.value(0), 0);
2071        assert_eq!(i.value(i.len() - 1), 81);
2072        assert_eq!(
2073            task_ctx.runtime_env().memory_pool.reserved(),
2074            0,
2075            "The sort should have returned all memory used back to the memory manager"
2076        );
2077
2078        Ok(())
2079    }
2080
2081    #[tokio::test]
2082    async fn test_batch_reservation_error() -> Result<()> {
2083        // Pick a memory limit and sort_spill_reservation that make the first batch reservation fail.
2084        let merge_reservation: usize = 0; // Set to 0 for simplicity
2085
2086        let session_config =
2087            SessionConfig::new().with_sort_spill_reservation_bytes(merge_reservation);
2088
2089        let plan = test::scan_partitioned(1);
2090
2091        // Read the first record batch to determine the actual memory requirement
2092        let expected_batch_reservation = {
2093            let temp_ctx = Arc::new(TaskContext::default());
2094            let mut stream = plan.execute(0, Arc::clone(&temp_ctx))?;
2095            let first_batch = stream.next().await.unwrap()?;
2096            get_reserved_bytes_for_record_batch(&first_batch)?
2097        };
2098
2099        // Set memory limit just short of what we need
2100        let memory_limit: usize = expected_batch_reservation + merge_reservation - 1;
2101
2102        let runtime = RuntimeEnvBuilder::new()
2103            .with_memory_limit(memory_limit, 1.0)
2104            .build_arc()?;
2105        let task_ctx = Arc::new(
2106            TaskContext::default()
2107                .with_session_config(session_config)
2108                .with_runtime(runtime),
2109        );
2110
2111        // Verify that our memory limit is insufficient
2112        {
2113            let mut stream = plan.execute(0, Arc::clone(&task_ctx))?;
2114            let first_batch = stream.next().await.unwrap()?;
2115            let batch_reservation = get_reserved_bytes_for_record_batch(&first_batch)?;
2116
2117            assert_eq!(batch_reservation, expected_batch_reservation);
2118            assert!(memory_limit < (merge_reservation + batch_reservation));
2119        }
2120
2121        let sort_exec = Arc::new(SortExec::new(
2122            [PhysicalSortExpr::new_default(col("i", &plan.schema())?)].into(),
2123            plan,
2124        ));
2125
2126        let result = collect(Arc::clone(&sort_exec) as _, Arc::clone(&task_ctx)).await;
2127
2128        let err = result.unwrap_err();
2129        assert!(
2130            matches!(err, DataFusionError::Context(..)),
2131            "Assertion failed: expected a Context error, but got: {err:?}"
2132        );
2133
2134        // Assert that the context error is wrapping a resources exhausted error.
2135        assert!(
2136            matches!(err.find_root(), DataFusionError::ResourcesExhausted(_)),
2137            "Assertion failed: expected a ResourcesExhausted error, but got: {err:?}"
2138        );
2139
2140        // Verify external sorter error message when resource is exhausted
2141        let config_vector = vec![
2142            "datafusion.runtime.memory_limit",
2143            "datafusion.execution.sort_spill_reservation_bytes",
2144        ];
2145        let error_message = err.message().to_string();
2146        for config in config_vector.into_iter() {
2147            assert!(
2148                error_message.as_str().contains(config),
2149                "Config: '{}' should be contained in error message: {}.",
2150                config,
2151                error_message.as_str()
2152            );
2153        }
2154
2155        Ok(())
2156    }
2157
2158    #[tokio::test]
2159    async fn test_sort_spill_utf8_strings() -> Result<()> {
2160        let session_config = SessionConfig::new()
2161            .with_batch_size(100)
2162            .with_sort_in_place_threshold_bytes(20 * 1024)
2163            .with_sort_spill_reservation_bytes(100 * 1024);
2164        let runtime = RuntimeEnvBuilder::new()
2165            .with_memory_limit(500 * 1024, 1.0)
2166            .build_arc()?;
2167        let task_ctx = Arc::new(
2168            TaskContext::default()
2169                .with_session_config(session_config)
2170                .with_runtime(runtime),
2171        );
2172
2173        // The input has 200 partitions, each partition has a batch containing 100 rows.
2174        // Each row has a single Utf8 column, the Utf8 string values are roughly 42 bytes.
2175        // The total size of the input is roughly 820 KB.
2176        let input = test::scan_partitioned_utf8(200);
2177        let schema = input.schema();
2178
2179        let sort_exec = Arc::new(SortExec::new(
2180            [PhysicalSortExpr {
2181                expr: col("i", &schema)?,
2182                options: SortOptions::default(),
2183            }]
2184            .into(),
2185            Arc::new(CoalescePartitionsExec::new(input)),
2186        ));
2187
2188        let result = collect(Arc::clone(&sort_exec) as _, Arc::clone(&task_ctx)).await?;
2189
2190        let num_rows = result.iter().map(|batch| batch.num_rows()).sum::<usize>();
2191        assert_eq!(num_rows, 20000);
2192
2193        // Now, validate metrics
2194        let metrics = sort_exec.metrics().unwrap();
2195
2196        assert_eq!(metrics.output_rows().unwrap(), 20000);
2197        assert!(metrics.elapsed_compute().unwrap() > 0);
2198
2199        let spill_count = metrics.spill_count().unwrap();
2200        let spilled_rows = metrics.spilled_rows().unwrap();
2201        let spilled_bytes = metrics.spilled_bytes().unwrap();
2202
2203        // This test case is processing 840KB of data using 400KB of memory. Note
2204        // that buffered batches can't be dropped until all sorted batches are
2205        // generated, so we can only buffer `sort_spill_reservation_bytes` of sorted
2206        // batches.
2207        // The number of spills is roughly calculated as:
2208        //  `number_of_batches / (sort_spill_reservation_bytes / batch_size)`
2209
2210        // If this assertion fail with large spill count, make sure the following
2211        // case does not happen:
2212        // During external sorting, one sorted run should be spilled to disk in a
2213        // single file, due to memory limit we might need to append to the file
2214        // multiple times to spill all the data. Make sure we're not writing each
2215        // appending as a separate file.
2216        assert!((4..=8).contains(&spill_count));
2217        assert!((15000..=20000).contains(&spilled_rows));
2218        assert!((900000..=1000000).contains(&spilled_bytes));
2219
2220        // Verify that the result is sorted
2221        let concated_result = concat_batches(&schema, &result)?;
2222        let columns = concated_result.columns();
2223        let string_array = as_string_array(&columns[0]);
2224        for i in 0..string_array.len() - 1 {
2225            assert!(string_array.value(i) <= string_array.value(i + 1));
2226        }
2227
2228        assert_eq!(
2229            task_ctx.runtime_env().memory_pool.reserved(),
2230            0,
2231            "The sort should have returned all memory used back to the memory manager"
2232        );
2233
2234        Ok(())
2235    }
2236
2237    #[tokio::test]
2238    async fn test_sort_fetch_memory_calculation() -> Result<()> {
2239        // This test mirrors down the size from the example above.
2240        let avg_batch_size = 400;
2241        let partitions = 4;
2242
2243        // A tuple of (fetch, expect_spillage)
2244        let test_options = vec![
2245            // Since we don't have a limit (and the memory is less than the total size of
2246            // all the batches we are processing, we expect it to spill.
2247            (None, true),
2248            // When we have a limit however, the buffered size of batches should fit in memory
2249            // since it is much lower than the total size of the input batch.
2250            (Some(1), false),
2251        ];
2252
2253        for (fetch, expect_spillage) in test_options {
2254            let session_config = SessionConfig::new();
2255            let sort_spill_reservation_bytes = session_config
2256                .options()
2257                .execution
2258                .sort_spill_reservation_bytes;
2259
2260            let runtime = RuntimeEnvBuilder::new()
2261                .with_memory_limit(
2262                    sort_spill_reservation_bytes + avg_batch_size * (partitions - 1),
2263                    1.0,
2264                )
2265                .build_arc()?;
2266            let task_ctx = Arc::new(
2267                TaskContext::default()
2268                    .with_runtime(runtime)
2269                    .with_session_config(session_config),
2270            );
2271
2272            let csv = test::scan_partitioned(partitions);
2273            let schema = csv.schema();
2274
2275            let sort_exec = Arc::new(
2276                SortExec::new(
2277                    [PhysicalSortExpr {
2278                        expr: col("i", &schema)?,
2279                        options: SortOptions::default(),
2280                    }]
2281                    .into(),
2282                    Arc::new(CoalescePartitionsExec::new(csv)),
2283                )
2284                .with_fetch(fetch),
2285            );
2286
2287            let result =
2288                collect(Arc::clone(&sort_exec) as _, Arc::clone(&task_ctx)).await?;
2289            assert_eq!(result.len(), 1);
2290
2291            let metrics = sort_exec.metrics().unwrap();
2292            let did_it_spill = metrics.spill_count().unwrap_or(0) > 0;
2293            assert_eq!(did_it_spill, expect_spillage, "with fetch: {fetch:?}");
2294        }
2295        Ok(())
2296    }
2297
2298    #[tokio::test]
2299    async fn test_sort_memory_reduction_per_batch() -> Result<()> {
2300        // This test verifies that memory reservation is reduced for every batch emitted
2301        // during the sort process. This is important to ensure we don't hold onto
2302        // memory longer than necessary.
2303
2304        // Create a large enough batch that will be split into multiple output batches
2305        let batch_size = 50; // Small batch size to force multiple output batches
2306        let num_rows = 1000; // Create enough data for multiple batches
2307
2308        let task_ctx = Arc::new(
2309            TaskContext::default().with_session_config(
2310                SessionConfig::new()
2311                    .with_batch_size(batch_size)
2312                    .with_sort_in_place_threshold_bytes(usize::MAX), // Ensure we don't concat batches
2313            ),
2314        );
2315
2316        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
2317
2318        // Create unsorted data
2319        let mut values: Vec<i32> = (0..num_rows).collect();
2320        values.reverse();
2321
2322        let input_batch = RecordBatch::try_new(
2323            Arc::clone(&schema),
2324            vec![Arc::new(Int32Array::from(values))],
2325        )?;
2326
2327        let batches = vec![input_batch];
2328
2329        let sort_exec = Arc::new(SortExec::new(
2330            [PhysicalSortExpr {
2331                expr: Arc::new(Column::new("a", 0)),
2332                options: SortOptions::default(),
2333            }]
2334            .into(),
2335            TestMemoryExec::try_new_exec(
2336                std::slice::from_ref(&batches),
2337                Arc::clone(&schema),
2338                None,
2339            )?,
2340        ));
2341
2342        let mut stream = sort_exec.execute(0, Arc::clone(&task_ctx))?;
2343
2344        let mut previous_reserved = task_ctx.runtime_env().memory_pool.reserved();
2345        let mut batch_count = 0;
2346
2347        // Collect batches and verify memory is reduced with each batch
2348        while let Some(result) = stream.next().await {
2349            let batch = result?;
2350            batch_count += 1;
2351
2352            // Verify we got a non-empty batch
2353            assert!(batch.num_rows() > 0, "Batch should not be empty");
2354
2355            let current_reserved = task_ctx.runtime_env().memory_pool.reserved();
2356
2357            // After the first batch, memory should be reducing or staying the same
2358            // (it should not increase as we emit batches)
2359            if batch_count > 1 {
2360                assert!(
2361                    current_reserved <= previous_reserved,
2362                    "Memory reservation should decrease or stay same as batches are emitted. \
2363                     Batch {batch_count}: previous={previous_reserved}, current={current_reserved}"
2364                );
2365            }
2366
2367            previous_reserved = current_reserved;
2368        }
2369
2370        assert!(
2371            batch_count > 1,
2372            "Expected multiple batches to be emitted, got {batch_count}"
2373        );
2374
2375        // Verify all memory is returned at the end
2376        assert_eq!(
2377            task_ctx.runtime_env().memory_pool.reserved(),
2378            0,
2379            "All memory should be returned after consuming all batches"
2380        );
2381
2382        Ok(())
2383    }
2384
2385    #[tokio::test]
2386    async fn test_sort_metadata() -> Result<()> {
2387        let task_ctx = Arc::new(TaskContext::default());
2388        let field_metadata: HashMap<String, String> =
2389            vec![("foo".to_string(), "bar".to_string())]
2390                .into_iter()
2391                .collect();
2392        let schema_metadata: HashMap<String, String> =
2393            vec![("baz".to_string(), "barf".to_string())]
2394                .into_iter()
2395                .collect();
2396
2397        let mut field = Field::new("field_name", DataType::UInt64, true);
2398        field.set_metadata(field_metadata.clone());
2399        let schema = Schema::new_with_metadata(vec![field], schema_metadata.clone());
2400        let schema = Arc::new(schema);
2401
2402        let data: ArrayRef =
2403            Arc::new(vec![3, 2, 1].into_iter().map(Some).collect::<UInt64Array>());
2404
2405        let batch = RecordBatch::try_new(Arc::clone(&schema), vec![data])?;
2406        let input =
2407            TestMemoryExec::try_new_exec(&[vec![batch]], Arc::clone(&schema), None)?;
2408
2409        let sort_exec = Arc::new(SortExec::new(
2410            [PhysicalSortExpr {
2411                expr: col("field_name", &schema)?,
2412                options: SortOptions::default(),
2413            }]
2414            .into(),
2415            input,
2416        ));
2417
2418        let result: Vec<RecordBatch> = collect(sort_exec, task_ctx).await?;
2419
2420        let expected_data: ArrayRef =
2421            Arc::new(vec![1, 2, 3].into_iter().map(Some).collect::<UInt64Array>());
2422        let expected_batch =
2423            RecordBatch::try_new(Arc::clone(&schema), vec![expected_data])?;
2424
2425        // Data is correct
2426        assert_eq!(&vec![expected_batch], &result);
2427
2428        // explicitly ensure the metadata is present
2429        assert_eq!(result[0].schema().fields()[0].metadata(), &field_metadata);
2430        assert_eq!(result[0].schema().metadata(), &schema_metadata);
2431
2432        Ok(())
2433    }
2434
2435    #[tokio::test]
2436    async fn test_lex_sort_by_mixed_types() -> Result<()> {
2437        let task_ctx = Arc::new(TaskContext::default());
2438        let schema = Arc::new(Schema::new(vec![
2439            Field::new("a", DataType::Int32, true),
2440            Field::new(
2441                "b",
2442                DataType::List(Arc::new(Field::new_list_field(DataType::Int32, true))),
2443                true,
2444            ),
2445        ]));
2446
2447        // define data.
2448        let batch = RecordBatch::try_new(
2449            Arc::clone(&schema),
2450            vec![
2451                Arc::new(Int32Array::from(vec![Some(2), None, Some(1), Some(2)])),
2452                Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
2453                    Some(vec![Some(3)]),
2454                    Some(vec![Some(1)]),
2455                    Some(vec![Some(6), None]),
2456                    Some(vec![Some(5)]),
2457                ])),
2458            ],
2459        )?;
2460
2461        let sort_exec = Arc::new(SortExec::new(
2462            [
2463                PhysicalSortExpr {
2464                    expr: col("a", &schema)?,
2465                    options: SortOptions {
2466                        descending: false,
2467                        nulls_first: true,
2468                    },
2469                },
2470                PhysicalSortExpr {
2471                    expr: col("b", &schema)?,
2472                    options: SortOptions {
2473                        descending: true,
2474                        nulls_first: false,
2475                    },
2476                },
2477            ]
2478            .into(),
2479            TestMemoryExec::try_new_exec(&[vec![batch]], Arc::clone(&schema), None)?,
2480        ));
2481
2482        assert_eq!(DataType::Int32, *sort_exec.schema().field(0).data_type());
2483        assert_eq!(
2484            DataType::List(Arc::new(Field::new_list_field(DataType::Int32, true))),
2485            *sort_exec.schema().field(1).data_type()
2486        );
2487
2488        let result: Vec<RecordBatch> =
2489            collect(Arc::clone(&sort_exec) as Arc<dyn ExecutionPlan>, task_ctx).await?;
2490        let metrics = sort_exec.metrics().unwrap();
2491        assert!(metrics.elapsed_compute().unwrap() > 0);
2492        assert_eq!(metrics.output_rows().unwrap(), 4);
2493        assert_eq!(result.len(), 1);
2494
2495        let expected = RecordBatch::try_new(
2496            schema,
2497            vec![
2498                Arc::new(Int32Array::from(vec![None, Some(1), Some(2), Some(2)])),
2499                Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
2500                    Some(vec![Some(1)]),
2501                    Some(vec![Some(6), None]),
2502                    Some(vec![Some(5)]),
2503                    Some(vec![Some(3)]),
2504                ])),
2505            ],
2506        )?;
2507
2508        assert_eq!(expected, result[0]);
2509
2510        Ok(())
2511    }
2512
2513    #[tokio::test]
2514    async fn test_lex_sort_by_float() -> Result<()> {
2515        let task_ctx = Arc::new(TaskContext::default());
2516        let schema = Arc::new(Schema::new(vec![
2517            Field::new("a", DataType::Float32, true),
2518            Field::new("b", DataType::Float64, true),
2519        ]));
2520
2521        // define data.
2522        let batch = RecordBatch::try_new(
2523            Arc::clone(&schema),
2524            vec![
2525                Arc::new(Float32Array::from(vec![
2526                    Some(f32::NAN),
2527                    None,
2528                    None,
2529                    Some(f32::NAN),
2530                    Some(1.0_f32),
2531                    Some(1.0_f32),
2532                    Some(2.0_f32),
2533                    Some(3.0_f32),
2534                ])),
2535                Arc::new(Float64Array::from(vec![
2536                    Some(200.0_f64),
2537                    Some(20.0_f64),
2538                    Some(10.0_f64),
2539                    Some(100.0_f64),
2540                    Some(f64::NAN),
2541                    None,
2542                    None,
2543                    Some(f64::NAN),
2544                ])),
2545            ],
2546        )?;
2547
2548        let sort_exec = Arc::new(SortExec::new(
2549            [
2550                PhysicalSortExpr {
2551                    expr: col("a", &schema)?,
2552                    options: SortOptions {
2553                        descending: true,
2554                        nulls_first: true,
2555                    },
2556                },
2557                PhysicalSortExpr {
2558                    expr: col("b", &schema)?,
2559                    options: SortOptions {
2560                        descending: false,
2561                        nulls_first: false,
2562                    },
2563                },
2564            ]
2565            .into(),
2566            TestMemoryExec::try_new_exec(&[vec![batch]], schema, None)?,
2567        ));
2568
2569        assert_eq!(DataType::Float32, *sort_exec.schema().field(0).data_type());
2570        assert_eq!(DataType::Float64, *sort_exec.schema().field(1).data_type());
2571
2572        let result: Vec<RecordBatch> =
2573            collect(Arc::clone(&sort_exec) as Arc<dyn ExecutionPlan>, task_ctx).await?;
2574        let metrics = sort_exec.metrics().unwrap();
2575        assert!(metrics.elapsed_compute().unwrap() > 0);
2576        assert_eq!(metrics.output_rows().unwrap(), 8);
2577        assert_eq!(result.len(), 1);
2578
2579        let columns = result[0].columns();
2580
2581        assert_eq!(DataType::Float32, *columns[0].data_type());
2582        assert_eq!(DataType::Float64, *columns[1].data_type());
2583
2584        let a = as_primitive_array::<Float32Type>(&columns[0])?;
2585        let b = as_primitive_array::<Float64Type>(&columns[1])?;
2586
2587        // convert result to strings to allow comparing to expected result containing NaN
2588        let result: Vec<(Option<String>, Option<String>)> = (0..result[0].num_rows())
2589            .map(|i| {
2590                let aval = if a.is_valid(i) {
2591                    Some(a.value(i).to_string())
2592                } else {
2593                    None
2594                };
2595                let bval = if b.is_valid(i) {
2596                    Some(b.value(i).to_string())
2597                } else {
2598                    None
2599                };
2600                (aval, bval)
2601            })
2602            .collect();
2603
2604        let expected: Vec<(Option<String>, Option<String>)> = vec![
2605            (None, Some("10".to_owned())),
2606            (None, Some("20".to_owned())),
2607            (Some("NaN".to_owned()), Some("100".to_owned())),
2608            (Some("NaN".to_owned()), Some("200".to_owned())),
2609            (Some("3".to_owned()), Some("NaN".to_owned())),
2610            (Some("2".to_owned()), None),
2611            (Some("1".to_owned()), Some("NaN".to_owned())),
2612            (Some("1".to_owned()), None),
2613        ];
2614
2615        assert_eq!(expected, result);
2616
2617        Ok(())
2618    }
2619
2620    #[tokio::test]
2621    async fn test_drop_cancel() -> Result<()> {
2622        let task_ctx = Arc::new(TaskContext::default());
2623        let schema =
2624            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, true)]));
2625
2626        let blocking_exec = Arc::new(BlockingExec::new(Arc::clone(&schema), 1));
2627        let refs = blocking_exec.refs();
2628        let sort_exec = Arc::new(SortExec::new(
2629            [PhysicalSortExpr {
2630                expr: col("a", &schema)?,
2631                options: SortOptions::default(),
2632            }]
2633            .into(),
2634            blocking_exec,
2635        ));
2636
2637        let fut = collect(sort_exec, Arc::clone(&task_ctx));
2638        let mut fut = fut.boxed();
2639
2640        assert_is_pending(&mut fut);
2641        drop(fut);
2642        assert_strong_count_converges_to_zero(refs).await;
2643
2644        assert_eq!(
2645            task_ctx.runtime_env().memory_pool.reserved(),
2646            0,
2647            "The sort should have returned all memory used back to the memory manager"
2648        );
2649
2650        Ok(())
2651    }
2652
2653    #[test]
2654    fn test_empty_sort_batch() {
2655        let schema = Arc::new(Schema::empty());
2656        let options = RecordBatchOptions::new().with_row_count(Some(1));
2657        let batch =
2658            RecordBatch::try_new_with_options(Arc::clone(&schema), vec![], &options)
2659                .unwrap();
2660
2661        let expressions = [PhysicalSortExpr {
2662            expr: Arc::new(Literal::new(ScalarValue::Int64(Some(1)))),
2663            options: SortOptions::default(),
2664        }]
2665        .into();
2666
2667        let result = sort_batch(&batch, &expressions, None).unwrap();
2668        assert_eq!(result.num_rows(), 1);
2669    }
2670
2671    #[tokio::test]
2672    async fn topk_unbounded_source() -> Result<()> {
2673        let task_ctx = Arc::new(TaskContext::default());
2674        let schema = Schema::new(vec![Field::new("c1", DataType::UInt64, false)]);
2675        let source = SortedUnboundedExec {
2676            schema: schema.clone(),
2677            batch_size: 2,
2678            cache: Arc::new(SortedUnboundedExec::compute_properties(Arc::new(
2679                schema.clone(),
2680            ))),
2681        };
2682        let mut plan = SortExec::new(
2683            [PhysicalSortExpr::new_default(Arc::new(Column::new(
2684                "c1", 0,
2685            )))]
2686            .into(),
2687            Arc::new(source),
2688        );
2689        plan = plan.with_fetch(Some(9));
2690
2691        let batches = collect(Arc::new(plan), task_ctx).await?;
2692        assert_snapshot!(batches_to_string(&batches), @r"
2693        +----+
2694        | c1 |
2695        +----+
2696        | 0  |
2697        | 1  |
2698        | 2  |
2699        | 3  |
2700        | 4  |
2701        | 5  |
2702        | 6  |
2703        | 7  |
2704        | 8  |
2705        +----+
2706        ");
2707        Ok(())
2708    }
2709
2710    #[tokio::test]
2711    async fn should_return_stream_with_batches_in_the_requested_size_and_update_metrics()
2712    -> Result<()> {
2713        let batch_size = 100;
2714
2715        let create_task_ctx = |_: &[RecordBatch]| {
2716            TaskContext::default().with_session_config(
2717                SessionConfig::new()
2718                    .with_batch_size(batch_size)
2719                    .with_sort_in_place_threshold_bytes(usize::MAX),
2720            )
2721        };
2722
2723        // Smaller than batch size and require more than a single batch to get the requested batch size
2724        test_sort_output_batch_size_and_base_metrics(10, batch_size / 4, create_task_ctx)
2725            .await?;
2726
2727        // Not evenly divisible by batch size
2728        test_sort_output_batch_size_and_base_metrics(10, batch_size + 7, create_task_ctx)
2729            .await?;
2730
2731        // Evenly divisible by batch size and is larger than 2 output batches
2732        test_sort_output_batch_size_and_base_metrics(10, batch_size * 3, create_task_ctx)
2733            .await?;
2734
2735        Ok(())
2736    }
2737
2738    #[tokio::test]
2739    async fn should_return_stream_with_batches_in_the_requested_size_and_update_metrics_when_sorting_in_place()
2740    -> Result<()> {
2741        let batch_size = 100;
2742
2743        let create_task_ctx = |_: &[RecordBatch]| {
2744            TaskContext::default().with_session_config(
2745                SessionConfig::new()
2746                    .with_batch_size(batch_size)
2747                    .with_sort_in_place_threshold_bytes(usize::MAX - 1),
2748            )
2749        };
2750
2751        // Smaller than batch size and require more than a single batch to get the requested batch size
2752        {
2753            let metrics = test_sort_output_batch_size_and_base_metrics(
2754                10,
2755                batch_size / 4,
2756                create_task_ctx,
2757            )
2758            .await?;
2759
2760            assert_eq!(
2761                metrics.spill_count(),
2762                Some(0),
2763                "Expected no spills when sorting in place"
2764            );
2765        }
2766
2767        // Not evenly divisible by batch size
2768        {
2769            let metrics = test_sort_output_batch_size_and_base_metrics(
2770                10,
2771                batch_size + 7,
2772                create_task_ctx,
2773            )
2774            .await?;
2775
2776            assert_eq!(
2777                metrics.spill_count(),
2778                Some(0),
2779                "Expected no spills when sorting in place"
2780            );
2781        }
2782
2783        // Evenly divisible by batch size and is larger than 2 output batches
2784        {
2785            let metrics = test_sort_output_batch_size_and_base_metrics(
2786                10,
2787                batch_size * 3,
2788                create_task_ctx,
2789            )
2790            .await?;
2791
2792            assert_eq!(
2793                metrics.spill_count(),
2794                Some(0),
2795                "Expected no spills when sorting in place"
2796            );
2797        }
2798
2799        Ok(())
2800    }
2801
2802    #[tokio::test]
2803    async fn should_return_stream_with_batches_in_the_requested_size_and_update_metrics_when_having_a_single_batch()
2804    -> Result<()> {
2805        let batch_size = 100;
2806
2807        let create_task_ctx = |_: &[RecordBatch]| {
2808            TaskContext::default()
2809                .with_session_config(SessionConfig::new().with_batch_size(batch_size))
2810        };
2811
2812        // Smaller than batch size and require more than a single batch to get the requested batch size
2813        {
2814            let metrics = test_sort_output_batch_size_and_base_metrics(
2815                // Single batch
2816                1,
2817                batch_size / 4,
2818                create_task_ctx,
2819            )
2820            .await?;
2821
2822            assert_eq!(
2823                metrics.spill_count(),
2824                Some(0),
2825                "Expected no spills when sorting in place"
2826            );
2827        }
2828
2829        // Not evenly divisible by batch size
2830        {
2831            let metrics = test_sort_output_batch_size_and_base_metrics(
2832                // Single batch
2833                1,
2834                batch_size + 7,
2835                create_task_ctx,
2836            )
2837            .await?;
2838
2839            assert_eq!(
2840                metrics.spill_count(),
2841                Some(0),
2842                "Expected no spills when sorting in place"
2843            );
2844        }
2845
2846        // Evenly divisible by batch size and is larger than 2 output batches
2847        {
2848            let metrics = test_sort_output_batch_size_and_base_metrics(
2849                // Single batch
2850                1,
2851                batch_size * 3,
2852                create_task_ctx,
2853            )
2854            .await?;
2855
2856            assert_eq!(
2857                metrics.spill_count(),
2858                Some(0),
2859                "Expected no spills when sorting in place"
2860            );
2861        }
2862
2863        Ok(())
2864    }
2865
2866    #[tokio::test]
2867    async fn should_return_stream_with_batches_in_the_requested_size_and_update_metrics_when_having_to_spill()
2868    -> Result<()> {
2869        let batch_size = 100;
2870
2871        let create_task_ctx = |generated_batches: &[RecordBatch]| {
2872            let batches_memory = generated_batches
2873                .iter()
2874                .map(|b| b.get_array_memory_size())
2875                .sum::<usize>();
2876
2877            TaskContext::default()
2878                .with_session_config(
2879                    SessionConfig::new()
2880                        .with_batch_size(batch_size)
2881                        // To make sure there is no in place sorting
2882                        .with_sort_in_place_threshold_bytes(1)
2883                        .with_sort_spill_reservation_bytes(1),
2884                )
2885                .with_runtime(
2886                    RuntimeEnvBuilder::default()
2887                        .with_memory_limit(batches_memory, 1.0)
2888                        .build_arc()
2889                        .unwrap(),
2890                )
2891        };
2892
2893        // Smaller than batch size and require more than a single batch to get the requested batch size
2894        {
2895            let metrics = test_sort_output_batch_size_and_base_metrics(
2896                10,
2897                batch_size / 4,
2898                create_task_ctx,
2899            )
2900            .await?;
2901
2902            assert_ne!(metrics.spill_count().unwrap(), 0, "expected to spill");
2903        }
2904
2905        // Not evenly divisible by batch size
2906        {
2907            let metrics = test_sort_output_batch_size_and_base_metrics(
2908                10,
2909                batch_size + 7,
2910                create_task_ctx,
2911            )
2912            .await?;
2913
2914            assert_ne!(metrics.spill_count().unwrap(), 0, "expected to spill");
2915        }
2916
2917        // Evenly divisible by batch size and is larger than 2 batches
2918        {
2919            let metrics = test_sort_output_batch_size_and_base_metrics(
2920                10,
2921                batch_size * 3,
2922                create_task_ctx,
2923            )
2924            .await?;
2925
2926            assert_ne!(metrics.spill_count().unwrap(), 0, "expected to spill");
2927        }
2928
2929        Ok(())
2930    }
2931
2932    async fn test_sort_output_batch_size_and_base_metrics(
2933        number_of_batches: usize,
2934        batch_size_to_generate: usize,
2935        create_task_ctx: impl Fn(&[RecordBatch]) -> TaskContext,
2936    ) -> Result<MetricsSet> {
2937        let batches = (0..number_of_batches)
2938            .map(|_| make_partition(batch_size_to_generate as i32))
2939            .collect::<Vec<_>>();
2940        let task_ctx = create_task_ctx(batches.as_slice());
2941
2942        let output_rows = batches.iter().map(|item| item.num_rows()).sum();
2943
2944        let expected_batch_size = task_ctx.session_config().batch_size();
2945
2946        let schema = batches[0].schema();
2947        let (mut output_batches, metrics) =
2948            run_sort_on_input(task_ctx, "i", batches, schema).await?;
2949
2950        let last_batch = output_batches.pop().unwrap();
2951
2952        for batch in output_batches {
2953            assert_eq!(batch.num_rows(), expected_batch_size);
2954        }
2955
2956        let mut last_expected_batch_size =
2957            (batch_size_to_generate * number_of_batches) % expected_batch_size;
2958        if last_expected_batch_size == 0 {
2959            last_expected_batch_size = expected_batch_size;
2960        }
2961        assert_eq!(last_batch.num_rows(), last_expected_batch_size);
2962
2963        assert_baseline_metrics_for_non_empty_output(
2964            &metrics,
2965            output_rows,
2966            expected_batch_size,
2967        );
2968
2969        Ok(metrics)
2970    }
2971
2972    #[tokio::test]
2973    async fn empty_sort_stream_should_report_end_time() -> Result<()> {
2974        let schema = Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, false)]));
2975        let task_ctx = TaskContext::default();
2976
2977        let (_, metrics) = run_sort_on_input(task_ctx, "i", vec![], schema).await?;
2978
2979        let end_time = metrics
2980            .iter()
2981            .find_map(|item| match item.value() {
2982                MetricValue::EndTimestamp(end) => Some(end),
2983                _ => None,
2984            })
2985            .expect("Must have end time metric since it exists in the baseline");
2986
2987        assert_eq!(
2988            metrics.spill_count().unwrap_or_default(),
2989            0,
2990            "expected to not have spills"
2991        );
2992        assert_ne!(end_time.value(), None);
2993
2994        Ok(())
2995    }
2996
2997    fn assert_baseline_metrics_for_non_empty_output(
2998        metrics: &MetricsSet,
2999        output_rows: usize,
3000        batch_size: usize,
3001    ) {
3002        let end_time = metrics
3003            .iter()
3004            .find_map(|item| match item.value() {
3005                MetricValue::EndTimestamp(end) => Some(end),
3006                _ => None,
3007            })
3008            .expect("Must have end time metric since it exists in the baseline");
3009
3010        assert_ne!(end_time.value(), None);
3011
3012        assert_eq!(metrics.output_rows(), Some(output_rows));
3013
3014        let output_bytes = metrics
3015            .iter()
3016            .find_map(|item| match item.value() {
3017                MetricValue::OutputBytes(total) => Some(total),
3018                _ => None,
3019            })
3020            .expect("Must have output_bytes metric since it exists in the baseline");
3021
3022        assert_ne!(output_bytes.value(), 0_usize);
3023
3024        let output_batches = metrics
3025            .iter()
3026            .find_map(|item| match item.value() {
3027                MetricValue::OutputBatches(total) => Some(total),
3028                _ => None,
3029            })
3030            .expect("Must have output_batches metric since it exists in the baseline");
3031
3032        assert_eq!(output_batches.value(), output_rows.div_ceil(batch_size));
3033    }
3034
3035    async fn run_sort_on_input(
3036        task_ctx: TaskContext,
3037        order_by_col: &str,
3038        batches: Vec<RecordBatch>,
3039        schema: SchemaRef,
3040    ) -> Result<(Vec<RecordBatch>, MetricsSet)> {
3041        let task_ctx = Arc::new(task_ctx);
3042
3043        // let task_ctx = env.
3044        let ordering: LexOrdering = [PhysicalSortExpr {
3045            expr: col(order_by_col, &schema)?,
3046            options: SortOptions {
3047                descending: false,
3048                nulls_first: true,
3049            },
3050        }]
3051        .into();
3052        let sort_exec: Arc<dyn ExecutionPlan> = Arc::new(SortExec::new(
3053            ordering.clone(),
3054            TestMemoryExec::try_new_exec(
3055                std::slice::from_ref(&batches),
3056                Arc::clone(&schema),
3057                None,
3058            )?,
3059        ));
3060
3061        let sorted_batches =
3062            collect(Arc::clone(&sort_exec), Arc::clone(&task_ctx)).await?;
3063
3064        let metrics = sort_exec.metrics().expect("sort have metrics");
3065
3066        // assert output
3067        {
3068            let input_batches_concat = concat_batches(&schema, &batches)?;
3069            let sorted_input_batch = sort_batch(&input_batches_concat, &ordering, None)?;
3070
3071            let sorted_batches_concat = concat_batches(&schema, &sorted_batches)?;
3072
3073            assert_eq!(sorted_input_batch, sorted_batches_concat);
3074        }
3075
3076        Ok((sorted_batches, metrics))
3077    }
3078
3079    #[tokio::test]
3080    async fn test_sort_batch_chunked_basic() -> Result<()> {
3081        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3082
3083        // Create a batch with 1000 rows
3084        let mut values: Vec<i32> = (0..1000).collect();
3085        // Shuffle to make it unsorted
3086        values.reverse();
3087
3088        let batch = RecordBatch::try_new(
3089            Arc::clone(&schema),
3090            vec![Arc::new(Int32Array::from(values))],
3091        )?;
3092
3093        let expressions: LexOrdering =
3094            [PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)))].into();
3095
3096        // Sort with batch_size = 250
3097        let result_batches = sort_batch_chunked(&batch, &expressions, 250)?;
3098
3099        // Verify 4 batches are returned
3100        assert_eq!(result_batches.len(), 4);
3101
3102        // Verify each batch has <= 250 rows
3103        let mut total_rows = 0;
3104        for (i, batch) in result_batches.iter().enumerate() {
3105            assert!(
3106                batch.num_rows() <= 250,
3107                "Batch {} has {} rows, expected <= 250",
3108                i,
3109                batch.num_rows()
3110            );
3111            total_rows += batch.num_rows();
3112        }
3113
3114        // Verify total row count matches input
3115        assert_eq!(total_rows, 1000);
3116
3117        // Verify data is correctly sorted across all chunks
3118        let concatenated = concat_batches(&schema, &result_batches)?;
3119        let array = as_primitive_array::<Int32Type>(concatenated.column(0))?;
3120        for i in 0..array.len() - 1 {
3121            assert!(
3122                array.value(i) <= array.value(i + 1),
3123                "Array not sorted at position {}: {} > {}",
3124                i,
3125                array.value(i),
3126                array.value(i + 1)
3127            );
3128        }
3129        assert_eq!(array.value(0), 0);
3130        assert_eq!(array.value(array.len() - 1), 999);
3131
3132        Ok(())
3133    }
3134
3135    #[tokio::test]
3136    async fn test_sort_batch_chunked_smaller_than_batch_size() -> Result<()> {
3137        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3138
3139        // Create a batch with 50 rows
3140        let values: Vec<i32> = (0..50).rev().collect();
3141        let batch = RecordBatch::try_new(
3142            Arc::clone(&schema),
3143            vec![Arc::new(Int32Array::from(values))],
3144        )?;
3145
3146        let expressions: LexOrdering =
3147            [PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)))].into();
3148
3149        // Sort with batch_size = 100
3150        let result_batches = sort_batch_chunked(&batch, &expressions, 100)?;
3151
3152        // Should return exactly 1 batch
3153        assert_eq!(result_batches.len(), 1);
3154        assert_eq!(result_batches[0].num_rows(), 50);
3155
3156        // Verify it's correctly sorted
3157        let array = as_primitive_array::<Int32Type>(result_batches[0].column(0))?;
3158        for i in 0..array.len() - 1 {
3159            assert!(array.value(i) <= array.value(i + 1));
3160        }
3161        assert_eq!(array.value(0), 0);
3162        assert_eq!(array.value(49), 49);
3163
3164        Ok(())
3165    }
3166
3167    #[tokio::test]
3168    async fn test_sort_batch_chunked_exact_multiple() -> Result<()> {
3169        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3170
3171        // Create a batch with 1000 rows
3172        let values: Vec<i32> = (0..1000).rev().collect();
3173        let batch = RecordBatch::try_new(
3174            Arc::clone(&schema),
3175            vec![Arc::new(Int32Array::from(values))],
3176        )?;
3177
3178        let expressions: LexOrdering =
3179            [PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)))].into();
3180
3181        // Sort with batch_size = 100
3182        let result_batches = sort_batch_chunked(&batch, &expressions, 100)?;
3183
3184        // Should return exactly 10 batches of 100 rows each
3185        assert_eq!(result_batches.len(), 10);
3186        for batch in &result_batches {
3187            assert_eq!(batch.num_rows(), 100);
3188        }
3189
3190        // Verify sorted correctly across all batches
3191        let concatenated = concat_batches(&schema, &result_batches)?;
3192        let array = as_primitive_array::<Int32Type>(concatenated.column(0))?;
3193        for i in 0..array.len() - 1 {
3194            assert!(array.value(i) <= array.value(i + 1));
3195        }
3196
3197        Ok(())
3198    }
3199
3200    #[tokio::test]
3201    async fn test_sort_batch_chunked_empty_batch() -> Result<()> {
3202        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3203
3204        let batch = RecordBatch::new_empty(Arc::clone(&schema));
3205
3206        let expressions: LexOrdering =
3207            [PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)))].into();
3208
3209        let result_batches = sort_batch_chunked(&batch, &expressions, 100)?;
3210
3211        // Empty input produces no output batches (0 chunks)
3212        assert_eq!(result_batches.len(), 0);
3213
3214        Ok(())
3215    }
3216
3217    #[tokio::test]
3218    async fn test_get_reserved_bytes_for_record_batch_with_sliced_batches() -> Result<()>
3219    {
3220        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3221
3222        // Create a larger batch then slice it
3223        let large_array = Int32Array::from((0..1000).collect::<Vec<i32>>());
3224        let sliced_array = large_array.slice(100, 50); // Take 50 elements starting at 100
3225
3226        let sliced_batch =
3227            RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(sliced_array)])?;
3228        let batch =
3229            RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(large_array)])?;
3230
3231        let sliced_reserved = get_reserved_bytes_for_record_batch(&sliced_batch)?;
3232        let reserved = get_reserved_bytes_for_record_batch(&batch)?;
3233
3234        // The reserved memory for the sliced batch should be less than that of the full batch
3235        assert!(reserved > sliced_reserved);
3236
3237        Ok(())
3238    }
3239
3240    #[test]
3241    fn test_with_dynamic_filter() -> Result<()> {
3242        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3243        let child = Arc::new(EmptyExec::new(Arc::clone(&schema)));
3244
3245        let sort = SortExec::new(
3246            LexOrdering::new(vec![PhysicalSortExpr {
3247                expr: Arc::new(Column::new("a", 0)),
3248                options: SortOptions::default(),
3249            }])
3250            .unwrap(),
3251            child,
3252        )
3253        .with_fetch(Some(10));
3254
3255        // SortExec with fetch creates a dynamic filter automatically.
3256        let produced = sort.dynamic_expressions_produced();
3257        assert_eq!(produced.len(), 1);
3258        let original_id = produced[0]
3259            .expression_id()
3260            .expect("DynamicFilterPhysicalExpr always has an expression_id");
3261
3262        // with_dynamic_filter replaces it with a new TopKDynamicFilters.
3263        let new_df = Arc::new(DynamicFilterPhysicalExpr::new(
3264            vec![Arc::new(Column::new("a", 0)) as _],
3265            lit(true),
3266        ));
3267        let new_id = new_df
3268            .expression_id()
3269            .expect("DynamicFilterPhysicalExpr always has an expression_id");
3270        let sort = sort.with_dynamic_filter_expr(Arc::clone(&new_df))?;
3271        let produced = sort.dynamic_expressions_produced();
3272        assert_eq!(produced.len(), 1);
3273        let restored_id = produced[0]
3274            .expression_id()
3275            .expect("DynamicFilterPhysicalExpr always has an expression_id");
3276        assert_eq!(restored_id, new_id);
3277        assert_ne!(restored_id, original_id);
3278        Ok(())
3279    }
3280
3281    async fn emit_sort_partition(
3282        sort: &Arc<SortExec>,
3283        partition: usize,
3284        task_ctx: Arc<TaskContext>,
3285    ) -> Result<()> {
3286        let _batches: Vec<RecordBatch> =
3287            sort.execute(partition, task_ctx)?.try_collect().await?;
3288        Ok(())
3289    }
3290
3291    fn assert_filter_still_waiting(filter: &Arc<DynamicFilterPhysicalExpr>) {
3292        let dynamic_filter_expr: Arc<dyn PhysicalExpr> =
3293            Arc::<DynamicFilterPhysicalExpr>::clone(filter);
3294        assert!(
3295            matches!(
3296                DynamicFilterTracking::classify(&dynamic_filter_expr),
3297                DynamicFilterTracking::Watching(_)
3298            ),
3299            "the shared filter should remain watchable until every partition emits"
3300        );
3301    }
3302
3303    fn dynamic_filter_produced(
3304        plan: &dyn ExecutionPlan,
3305    ) -> Arc<DynamicFilterPhysicalExpr> {
3306        let expr = plan
3307            .dynamic_expressions_produced()
3308            .into_iter()
3309            .next()
3310            .expect("plan should produce a dynamic filter");
3311        (expr as Arc<dyn std::any::Any + Send + Sync>)
3312            .downcast::<DynamicFilterPhysicalExpr>()
3313            .expect("produced expression should be a DynamicFilterPhysicalExpr")
3314    }
3315
3316    #[tokio::test]
3317    async fn test_preserved_topk_filter_waits_for_all_sort_partitions() -> Result<()> {
3318        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3319        let partitions = vec![
3320            vec![RecordBatch::try_new(
3321                Arc::clone(&schema),
3322                vec![Arc::new(Int32Array::from(vec![3, 1, 2]))],
3323            )?],
3324            vec![RecordBatch::try_new(
3325                Arc::clone(&schema),
3326                vec![Arc::new(Int32Array::from(vec![6, 4, 5]))],
3327            )?],
3328        ];
3329        let input = TestMemoryExec::try_new_exec(&partitions, Arc::clone(&schema), None)?;
3330        let sort = SortExec::new(
3331            [PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)))].into(),
3332            input,
3333        )
3334        // `with_fetch` creates the TopK filter; preserving partitioning after
3335        // that must rebuild it with one emitter per output partition.
3336        .with_fetch(Some(2))
3337        .with_preserve_partitioning(true);
3338
3339        let dynamic_filter = dynamic_filter_produced(&sort);
3340        let sort = Arc::new(sort);
3341        let task_ctx = Arc::new(TaskContext::default());
3342
3343        emit_sort_partition(&sort, 0, Arc::clone(&task_ctx)).await?;
3344        assert_filter_still_waiting(&dynamic_filter);
3345
3346        emit_sort_partition(&sort, 1, task_ctx).await?;
3347        tokio::time::timeout(
3348            std::time::Duration::from_secs(1),
3349            dynamic_filter.wait_complete(),
3350        )
3351        .await
3352        .expect("the final preserved SortExec partition should complete the filter");
3353
3354        Ok(())
3355    }
3356
3357    #[tokio::test]
3358    async fn test_with_fetch_rebuilds_existing_topk_filter() -> Result<()> {
3359        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3360        let partitions = vec![
3361            vec![RecordBatch::try_new(
3362                Arc::clone(&schema),
3363                vec![Arc::new(Int32Array::from(vec![3, 1, 2]))],
3364            )?],
3365            vec![RecordBatch::try_new(
3366                Arc::clone(&schema),
3367                vec![Arc::new(Int32Array::from(vec![6, 4, 5]))],
3368            )?],
3369        ];
3370        let input = TestMemoryExec::try_new_exec(&partitions, Arc::clone(&schema), None)?;
3371        let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new(
3372            vec![Arc::new(Column::new("a", 0))],
3373            lit(true),
3374        ));
3375        let dynamic_filter_id = dynamic_filter
3376            .expression_id()
3377            .expect("DynamicFilterPhysicalExpr always has an expression_id");
3378        let sort = SortExec::new(
3379            [PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)))].into(),
3380            input,
3381        )
3382        .with_dynamic_filter_expr(dynamic_filter)?
3383        .with_preserve_partitioning(true)
3384        .with_fetch(Some(2));
3385
3386        let dynamic_filter = dynamic_filter_produced(&sort);
3387        assert_eq!(
3388            dynamic_filter
3389                .expression_id()
3390                .expect("DynamicFilterPhysicalExpr always has an expression_id"),
3391            dynamic_filter_id
3392        );
3393
3394        let sort = Arc::new(sort);
3395        let task_ctx = Arc::new(TaskContext::default());
3396
3397        emit_sort_partition(&sort, 0, Arc::clone(&task_ctx)).await?;
3398        assert_filter_still_waiting(&dynamic_filter);
3399
3400        emit_sort_partition(&sort, 1, task_ctx).await?;
3401        tokio::time::timeout(
3402            std::time::Duration::from_secs(1),
3403            dynamic_filter.wait_complete(),
3404        )
3405        .await
3406        .expect("the final preserved SortExec partition should complete the filter");
3407
3408        Ok(())
3409    }
3410
3411    #[test]
3412    fn test_with_dynamic_filter_rejects_invalid_columns() -> Result<()> {
3413        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3414        let child = Arc::new(EmptyExec::new(Arc::clone(&schema)));
3415
3416        let sort = SortExec::new(
3417            LexOrdering::new(vec![PhysicalSortExpr {
3418                expr: Arc::new(Column::new("a", 0)),
3419                options: SortOptions::default(),
3420            }])
3421            .unwrap(),
3422            child,
3423        )
3424        .with_fetch(Some(10));
3425
3426        // Column index 99 is out of bounds for the input schema.
3427        let df = Arc::new(DynamicFilterPhysicalExpr::new(
3428            vec![Arc::new(Column::new("bad", 99)) as _],
3429            lit(true),
3430        ));
3431        assert!(sort.with_dynamic_filter_expr(df).is_err());
3432        Ok(())
3433    }
3434
3435    /// Verifies that `ExternalSorter::sort()` transfers the pre-reserved
3436    /// merge bytes to the merge stream via `take()`, rather than leaving
3437    /// them in the sorter (via `new_empty()`).
3438    ///
3439    /// 1. Create a sorter with a tight memory pool and insert enough data
3440    ///    to force spilling
3441    /// 2. Verify `merge_reservation` holds the pre-reserved bytes before sort
3442    /// 3. Call `sort()` to get the merge stream
3443    /// 4. Verify `merge_reservation` is now 0 (bytes transferred to merge stream)
3444    /// 5. Simulate contention: a competing consumer grabs all available pool memory
3445    /// 6. Verify the merge stream still works (it uses its pre-reserved bytes
3446    ///    as initial budget, not requesting from pool starting at 0)
3447    ///
3448    /// With `new_empty()` (before fix), step 4 fails: `merge_reservation`
3449    /// still holds the bytes, the merge stream starts with 0 budget, and
3450    /// those bytes become unaccounted-for reserved memory that nobody uses.
3451    #[tokio::test]
3452    async fn test_sort_merge_reservation_transferred_not_freed() -> Result<()> {
3453        let sort_spill_reservation_bytes: usize = 10 * 1024; // 10 KB
3454
3455        // Pool: merge reservation (10KB) + enough room for sort to work.
3456        // The room must accommodate batch data accumulation before spilling.
3457        let sort_working_memory: usize = 40 * 1024; // 40 KB for sort operations
3458        let pool_size = sort_spill_reservation_bytes + sort_working_memory;
3459        let pool: Arc<dyn MemoryPool> = Arc::new(GreedyMemoryPool::new(pool_size));
3460
3461        let runtime = RuntimeEnvBuilder::new()
3462            .with_memory_pool(Arc::clone(&pool))
3463            .build_arc()?;
3464
3465        let metrics_set = ExecutionPlanMetricsSet::new();
3466        let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int32, false)]));
3467
3468        let mut sorter = ExternalSorter::new(
3469            0,
3470            Arc::clone(&schema),
3471            [PhysicalSortExpr::new_default(Arc::new(Column::new("x", 0)))].into(),
3472            128, // batch_size
3473            sort_spill_reservation_bytes,
3474            usize::MAX, // sort_in_place_threshold_bytes (high to avoid concat path)
3475            SpillCompression::Uncompressed,
3476            &metrics_set,
3477            Arc::clone(&runtime),
3478        )?;
3479
3480        // Insert enough data to force spilling.
3481        let num_batches = 200;
3482        for i in 0..num_batches {
3483            let values: Vec<i32> = ((i * 100)..((i + 1) * 100)).rev().collect();
3484            let batch = RecordBatch::try_new(
3485                Arc::clone(&schema),
3486                vec![Arc::new(Int32Array::from(values))],
3487            )?;
3488            sorter.insert_batch(batch).await?;
3489        }
3490
3491        assert!(
3492            sorter.spilled_before(),
3493            "Test requires spilling to exercise the merge path"
3494        );
3495
3496        // Before sort(), merge_reservation holds sort_spill_reservation_bytes.
3497        assert!(
3498            sorter.merge_reservation_size() >= sort_spill_reservation_bytes,
3499            "merge_reservation should hold the pre-reserved bytes before sort()"
3500        );
3501
3502        // Call sort() to get the merge stream. With the fix (take()),
3503        // the pre-reserved merge bytes are transferred to the merge
3504        // stream. Without the fix (free() + new_empty()), the bytes
3505        // are released back to the pool and the merge stream starts
3506        // with 0 bytes.
3507        let merge_stream = sorter.sort().await?;
3508
3509        // THE KEY ASSERTION: after sort(), merge_reservation must be 0.
3510        // This proves take() transferred the bytes to the merge stream,
3511        // rather than them being freed back to the pool where other
3512        // partitions could steal them.
3513        assert_eq!(
3514            sorter.merge_reservation_size(),
3515            0,
3516            "After sort(), merge_reservation should be 0 (bytes transferred \
3517             to merge stream via take()). If non-zero, the bytes are still \
3518             held by the sorter and will be freed on drop, allowing other \
3519             partitions to steal them."
3520        );
3521
3522        // Drop the sorter to free its reservations back to the pool.
3523        drop(sorter);
3524
3525        // Simulate contention: another partition grabs ALL available
3526        // pool memory. If the merge stream didn't receive the
3527        // pre-reserved bytes via take(), it will fail when it tries
3528        // to allocate memory for reading spill files.
3529        let contender = MemoryConsumer::new("CompetingPartition").register(&pool);
3530        let available = pool_size.saturating_sub(pool.reserved());
3531        if available > 0 {
3532            contender.try_grow(available).unwrap();
3533        }
3534
3535        // The merge stream must still produce correct results despite
3536        // the pool being fully consumed by the contender. This only
3537        // works if sort() transferred the pre-reserved bytes to the
3538        // merge stream (via take()) rather than freeing them.
3539        let batches: Vec<RecordBatch> = merge_stream.try_collect().await?;
3540        let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
3541        assert_eq!(
3542            total_rows,
3543            (num_batches * 100) as usize,
3544            "Merge stream should produce all rows even under memory contention"
3545        );
3546
3547        // Verify data is sorted
3548        let merged = concat_batches(&schema, &batches)?;
3549        let col = merged.column(0).as_primitive::<Int32Type>();
3550        for i in 1..col.len() {
3551            assert!(
3552                col.value(i - 1) <= col.value(i),
3553                "Output should be sorted, but found {} > {} at index {}",
3554                col.value(i - 1),
3555                col.value(i),
3556                i
3557            );
3558        }
3559
3560        drop(contender);
3561        Ok(())
3562    }
3563
3564    fn make_sort_exec_with_fetch(fetch: Option<usize>) -> SortExec {
3565        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3566        let input = Arc::new(EmptyExec::new(schema));
3567        SortExec::new(
3568            [PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)))].into(),
3569            input,
3570        )
3571        .with_fetch(fetch)
3572    }
3573
3574    #[test]
3575    fn test_sort_with_fetch_blocks_filter_pushdown() -> Result<()> {
3576        let sort = make_sort_exec_with_fetch(Some(10));
3577        let desc = sort.gather_filters_for_pushdown(
3578            FilterPushdownPhase::Pre,
3579            vec![Arc::new(Column::new("a", 0))],
3580            &ConfigOptions::new(),
3581        )?;
3582        // Sort with fetch (TopK) must not allow filters to be pushed below it.
3583        assert!(matches!(
3584            desc.parent_filters()[0][0].discriminant,
3585            PushedDown::No
3586        ));
3587        Ok(())
3588    }
3589
3590    #[test]
3591    fn test_sort_without_fetch_allows_filter_pushdown() -> Result<()> {
3592        let sort = make_sort_exec_with_fetch(None);
3593        let desc = sort.gather_filters_for_pushdown(
3594            FilterPushdownPhase::Pre,
3595            vec![Arc::new(Column::new("a", 0))],
3596            &ConfigOptions::new(),
3597        )?;
3598        // Plain sort (no fetch) is filter-commutative.
3599        assert!(matches!(
3600            desc.parent_filters()[0][0].discriminant,
3601            PushedDown::Yes
3602        ));
3603        Ok(())
3604    }
3605
3606    #[test]
3607    fn test_sort_with_fetch_allows_topk_self_filter_in_post_phase() -> Result<()> {
3608        let sort = make_sort_exec_with_fetch(Some(10));
3609        assert!(sort.filter.is_some(), "TopK filter should be created");
3610
3611        let mut config = ConfigOptions::new();
3612        config.optimizer.enable_topk_dynamic_filter_pushdown = true;
3613        let desc = sort.gather_filters_for_pushdown(
3614            FilterPushdownPhase::Post,
3615            vec![Arc::new(Column::new("a", 0))],
3616            &config,
3617        )?;
3618        // Parent filters are still blocked in the Post phase.
3619        assert!(matches!(
3620            desc.parent_filters()[0][0].discriminant,
3621            PushedDown::No
3622        ));
3623        // But the TopK self-filter should be pushed down.
3624        assert_eq!(desc.self_filters()[0].len(), 1);
3625        Ok(())
3626    }
3627}