Skip to main content

datafusion_physical_plan/sorts/
partial_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//! Partial Sort deals with input data that partially
19//! satisfies the required sort order. Such an input data can be
20//! partitioned into segments where each segment already has the
21//! required information for lexicographic sorting so sorting
22//! can be done without loading the entire dataset.
23//!
24//! Consider a sort plan having an input with ordering `a ASC, b ASC`
25//!
26//! ```text
27//! +---+---+---+
28//! | a | b | d |
29//! +---+---+---+
30//! | 0 | 0 | 3 |
31//! | 0 | 0 | 2 |
32//! | 0 | 1 | 1 |
33//! | 0 | 2 | 0 |
34//! +---+---+---+
35//! ```
36//!
37//! and required ordering for the plan is `a ASC, b ASC, d ASC`.
38//! The first 3 rows(segment) can be sorted as the segment already
39//! has the required information for the sort, but the last row
40//! requires further information as the input can continue with a
41//! batch with a starting row where a and b does not change as below
42//!
43//! ```text
44//! +---+---+---+
45//! | a | b | d |
46//! +---+---+---+
47//! | 0 | 2 | 4 |
48//! +---+---+---+
49//! ```
50//!
51//! The plan concats incoming data with such last rows of previous input
52//! and continues partial sorting of the segments.
53
54use std::fmt::Debug;
55use std::pin::Pin;
56use std::sync::Arc;
57use std::task::{Context, Poll};
58
59use crate::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
60use crate::sorts::sort::sort_batch;
61use crate::statistics::{ChildStats, StatisticsArgs};
62use crate::stream::EmptyRecordBatchStream;
63use crate::{
64    ChildrenPropertiesMode, DisplayAs, DisplayFormatType, Distribution, ExecutionPlan,
65    ExecutionPlanProperties, Partitioning, PlanProperties, ReplaceChildrenOptions,
66    SendableRecordBatchStream, Statistics, validate_child_count,
67};
68
69use arrow::compute::concat_batches;
70use arrow::datatypes::SchemaRef;
71use arrow::record_batch::RecordBatch;
72use datafusion_common::Result;
73use datafusion_common::tree_node::TreeNodeRecursion;
74use datafusion_common::utils::evaluate_partition_ranges;
75use datafusion_execution::{RecordBatchStream, TaskContext};
76use datafusion_physical_expr::{LexOrdering, PhysicalExpr};
77
78use futures::{Stream, StreamExt, ready};
79use log::trace;
80
81/// Sort execution plan for inputs that are already partially sorted.
82///
83/// This operator takes input ordered by a prefix of the required ordering, and
84/// produces output ordered by the required ordering, emitting rows sooner
85/// (streaming) and using less peak memory than [`SortExec`] which must buffer
86/// all rows before producing any output.
87///
88/// [`PartialSortExec`] relies on the property that rows with the same sort
89/// prefix are contiguous, so it can sort one prefix group at a time, emitting
90/// completed groups without reading (and buffering) the entire input.
91///
92/// For example, if the required output is `(a, b, c)`, but the input is only
93/// ordered by `(a, b)`, `PartialSortExec` sorts only within each `(a, b)`
94/// group to produce output ordered by `(a, b, c)`.
95///
96/// ```text
97/// input ordered by a, b              output ordered by a, b, c
98///
99/// +---+---+---+                      +---+---+---+
100/// | a | b | c |                      | a | b | c |
101/// +---+---+---+                      +---+---+---+
102/// | 0 | 0 | 3 |  --  new group  -->  | 0 | 0 | 1 |
103/// | 0 | 0 | 2 |                      | 0 | 0 | 2 |
104/// | 0 | 0 | 1 |                      | 0 | 0 | 3 |
105/// | 0 | 1 | 1 |  --  new group  -->  | 0 | 1 | 1 |
106/// | 0 | 2 | 4 |  --  new group  -->  | 0 | 2 | 0 |
107/// | 0 | 2 | 0 |                      | 0 | 2 | 4 |
108/// | 1 | 0 | 5 |  --  new group  -->  | 1 | 0 | 5 |
109/// +---+---+---+                      +---+---+---+
110/// ```
111///
112/// # Buffering and Emitting Rows
113///
114/// [`PartialSortExec`] buffers rows only until it can *prove* a prefix group
115/// will never be seen again, then sorts and emits buffered rows. A group is
116/// guaranteed to never be seen again once a row with a *different* prefix
117/// value arrives. This relies on the input's existing ordering guarantees.
118///
119/// Using the example from above, rows accumulate in the in-memory buffer in
120/// batches. As long as the `(a, b)` prefix keeps repeating, more rows are
121/// buffered.
122///
123/// ```text
124///            Buffer
125///        +---+---+---+
126///        | a | b | c |
127///        +---+---+---+
128///        | 0 | 0 | 3 |
129///        | 0 | 0 | 2 |
130///        | 0 | 0 | 1 |
131///        +---+---+---+
132/// ```
133///
134/// Once a batch arrives that contains a new `(a, b)` prefix, e.g. `(0, 2)`:
135/// every buffered row for previous prefixes may be emitted:
136///
137/// ```text
138///            Buffer
139///        +---+---+---+
140///        | a | b | c |
141///        +---+---+---+
142///        | 0 | 0 | 3 |
143///        | 0 | 0 | 2 |
144///        | 0 | 0 | 1 |
145///        | 0 | 1 | 1 |  <-- first row of new batch, new prefix
146///        | 0 | 2 | 4 |  <-- new prefix
147///        | 0 | 2 | 0 |
148///        | 1 | 0 | 5 |  <-- last row of new batch, new prefix
149///        +---+---+---+
150/// ```
151///
152/// Once known complete, the buffered rows are sorted by the full `(a, b, c)`
153/// ordering and emitted as a [`RecordBatch`]; Any rows from the most recently
154/// seen prefix remain buffered (as more rows with the same prefix may arrive in
155/// future batches.
156///
157/// ```text
158///          Emitted      <-- fully sorted on (a, b, c)
159///        +---+---+---+
160///        | a | b | c |
161///        +---+---+---+
162///        | 0 | 0 | 1 |   <-- completed group
163///        | 0 | 0 | 2 |
164///        | 0 | 0 | 3 |
165///        | 0 | 2 | 0 |   <-- completed group
166///        | 0 | 2 | 4 |
167///        | 0 | 1 | 1 |   <-- completed group
168///        +---+---+---+
169///
170///            Buffer
171///        +---+---+---+
172///        | a | b | c |
173///        +---+---+---+
174///        | 1 | 0 | 5 |   <-- (possibly) in progress group
175///        +---+---+---+
176/// ```
177///
178/// [`SortExec`]: crate::sorts::sort::SortExec
179#[derive(Debug, Clone)]
180pub struct PartialSortExec {
181    /// Input schema
182    pub(crate) input: Arc<dyn ExecutionPlan>,
183    /// Sort expressions
184    expr: LexOrdering,
185    /// Length of continuous matching columns of input that satisfy
186    /// the required ordering for the sort
187    common_prefix_length: usize,
188    /// Containing all metrics set created during sort
189    metrics_set: ExecutionPlanMetricsSet,
190    /// Preserve partitions of input plan. If false, the input partitions
191    /// will be sorted and merged into a single output partition.
192    preserve_partitioning: bool,
193    /// Fetch highest/lowest n results
194    fetch: Option<usize>,
195    /// Cache holding plan properties like equivalences, output partitioning etc.
196    cache: Arc<PlanProperties>,
197}
198
199impl PartialSortExec {
200    /// Create a new partial sort execution plan
201    pub fn new(
202        expr: LexOrdering,
203        input: Arc<dyn ExecutionPlan>,
204        common_prefix_length: usize,
205    ) -> Self {
206        debug_assert!(common_prefix_length > 0);
207        let preserve_partitioning = false;
208        let cache = Self::compute_properties(&input, expr.clone(), preserve_partitioning)
209            .unwrap();
210        Self {
211            input,
212            expr,
213            common_prefix_length,
214            metrics_set: ExecutionPlanMetricsSet::new(),
215            preserve_partitioning,
216            fetch: None,
217            cache: Arc::new(cache),
218        }
219    }
220
221    /// Whether this `PartialSortExec` preserves partitioning of the children
222    pub fn preserve_partitioning(&self) -> bool {
223        self.preserve_partitioning
224    }
225
226    /// Specify the partitioning behavior of this partial sort exec
227    ///
228    /// If `preserve_partitioning` is true, sorts each partition
229    /// individually, producing one sorted stream for each input partition.
230    ///
231    /// If `preserve_partitioning` is false, sorts and merges all
232    /// input partitions producing a single, sorted partition.
233    pub fn with_preserve_partitioning(mut self, preserve_partitioning: bool) -> Self {
234        self.preserve_partitioning = preserve_partitioning;
235        Arc::make_mut(&mut self.cache).partitioning =
236            Self::output_partitioning_helper(&self.input, self.preserve_partitioning);
237        self
238    }
239
240    /// Modify how many rows to include in the result
241    ///
242    /// If None, then all rows will be returned, in sorted order.
243    /// If Some, then only the top `fetch` rows will be returned.
244    /// This can reduce the memory pressure required by the sort
245    /// operation since rows that are not going to be included
246    /// can be dropped.
247    pub fn with_fetch(mut self, fetch: Option<usize>) -> Self {
248        self.fetch = fetch;
249        self
250    }
251
252    /// Input schema
253    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
254        &self.input
255    }
256
257    /// Sort expressions
258    pub fn expr(&self) -> &LexOrdering {
259        &self.expr
260    }
261
262    /// If `Some(fetch)`, limits output to only the first "fetch" items
263    pub fn fetch(&self) -> Option<usize> {
264        self.fetch
265    }
266
267    /// Common prefix length
268    pub fn common_prefix_length(&self) -> usize {
269        self.common_prefix_length
270    }
271
272    fn output_partitioning_helper(
273        input: &Arc<dyn ExecutionPlan>,
274        preserve_partitioning: bool,
275    ) -> Partitioning {
276        // Get output partitioning:
277        if preserve_partitioning {
278            input.output_partitioning().clone()
279        } else {
280            Partitioning::UnknownPartitioning(1)
281        }
282    }
283
284    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
285    fn compute_properties(
286        input: &Arc<dyn ExecutionPlan>,
287        sort_exprs: LexOrdering,
288        preserve_partitioning: bool,
289    ) -> Result<PlanProperties> {
290        // Calculate equivalence properties; i.e. reset the ordering equivalence
291        // class with the new ordering:
292        let mut eq_properties = input.equivalence_properties().clone();
293        eq_properties.reorder(sort_exprs)?;
294
295        // Get output partitioning:
296        let output_partitioning =
297            Self::output_partitioning_helper(input, preserve_partitioning);
298
299        Ok(PlanProperties::new(
300            eq_properties,
301            output_partitioning,
302            input.pipeline_behavior(),
303            input.boundedness(),
304        ))
305    }
306}
307
308impl DisplayAs for PartialSortExec {
309    fn fmt_as(
310        &self,
311        t: DisplayFormatType,
312        f: &mut std::fmt::Formatter,
313    ) -> std::fmt::Result {
314        match t {
315            DisplayFormatType::Default | DisplayFormatType::Verbose => {
316                let common_prefix_length = self.common_prefix_length;
317                match self.fetch {
318                    Some(fetch) => {
319                        write!(
320                            f,
321                            "PartialSortExec: TopK(fetch={fetch}), expr=[{}], common_prefix_length=[{common_prefix_length}]",
322                            self.expr
323                        )
324                    }
325                    None => write!(
326                        f,
327                        "PartialSortExec: expr=[{}], common_prefix_length=[{common_prefix_length}]",
328                        self.expr
329                    ),
330                }
331            }
332            DisplayFormatType::TreeRender => match self.fetch {
333                Some(fetch) => {
334                    writeln!(f, "{}", self.expr)?;
335                    writeln!(f, "limit={fetch}")
336                }
337                None => {
338                    writeln!(f, "{}", self.expr)
339                }
340            },
341        }
342    }
343}
344
345impl ExecutionPlan for PartialSortExec {
346    fn name(&self) -> &'static str {
347        "PartialSortExec"
348    }
349
350    fn properties(&self) -> &Arc<PlanProperties> {
351        &self.cache
352    }
353
354    fn fetch(&self) -> Option<usize> {
355        self.fetch
356    }
357
358    fn required_input_distribution(&self) -> Vec<Distribution> {
359        self.input_distribution_requirements().into_per_child()
360    }
361
362    fn input_distribution_requirements(&self) -> crate::InputDistributionRequirements {
363        crate::InputDistributionRequirements::new(if self.preserve_partitioning {
364            vec![Distribution::UnspecifiedDistribution]
365        } else {
366            vec![Distribution::SinglePartition]
367        })
368    }
369
370    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
371        vec![false]
372    }
373
374    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
375        vec![&self.input]
376    }
377
378    fn apply_expressions(
379        &self,
380        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
381    ) -> Result<TreeNodeRecursion> {
382        crate::apply_expression_roots(
383            self.expr.iter().map(|sort_expr| &sort_expr.expr),
384            f,
385        )
386    }
387
388    fn replace_children(
389        self: Arc<Self>,
390        mut children: Vec<Arc<dyn ExecutionPlan>>,
391        options: ReplaceChildrenOptions,
392    ) -> Result<Arc<dyn ExecutionPlan>> {
393        validate_child_count!(self, children);
394        match options.children_properties {
395            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
396                input: children.swap_remove(0),
397                metrics_set: ExecutionPlanMetricsSet::new(),
398                ..Self::clone(&*self)
399            })),
400            ChildrenPropertiesMode::Recompute => {
401                let new_partial_sort = PartialSortExec::new(
402                    self.expr.clone(),
403                    Arc::clone(&children[0]),
404                    self.common_prefix_length,
405                )
406                .with_fetch(self.fetch)
407                .with_preserve_partitioning(self.preserve_partitioning);
408
409                Ok(Arc::new(new_partial_sort))
410            }
411        }
412    }
413
414    fn with_new_children(
415        self: Arc<Self>,
416        children: Vec<Arc<dyn ExecutionPlan>>,
417    ) -> Result<Arc<dyn ExecutionPlan>> {
418        self.replace_children(
419            children,
420            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
421        )
422    }
423
424    fn with_new_children_and_same_properties(
425        self: Arc<Self>,
426        children: Vec<Arc<dyn ExecutionPlan>>,
427    ) -> Result<Arc<dyn ExecutionPlan>> {
428        self.replace_children(
429            children,
430            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
431        )
432    }
433
434    fn execute(
435        &self,
436        partition: usize,
437        context: Arc<TaskContext>,
438    ) -> Result<SendableRecordBatchStream> {
439        trace!(
440            "Start PartialSortExec::execute for partition {} of context session_id {} and task_id {:?}",
441            partition,
442            context.session_id(),
443            context.task_id()
444        );
445
446        let input = self.input.execute(partition, Arc::clone(&context))?;
447
448        trace!("End PartialSortExec's input.execute for partition: {partition}");
449
450        // Make sure common prefix length is larger than 0
451        // Otherwise, we should use SortExec.
452        debug_assert!(self.common_prefix_length > 0);
453
454        Ok(Box::pin(PartialSortStream {
455            input,
456            expr: self.expr.clone(),
457            common_prefix_length: self.common_prefix_length,
458            in_mem_batch: RecordBatch::new_empty(Arc::clone(&self.schema())),
459            fetch: self.fetch,
460            is_closed: false,
461            baseline_metrics: BaselineMetrics::new(&self.metrics_set, partition),
462        }))
463    }
464
465    fn metrics(&self) -> Option<MetricsSet> {
466        Some(self.metrics_set.clone_inner())
467    }
468
469    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
470        vec![ChildStats::At(partition)]
471    }
472
473    fn statistics_from_inputs(
474        &self,
475        input_stats: &[Arc<Statistics>],
476        _args: &StatisticsArgs,
477    ) -> Result<Arc<Statistics>> {
478        Ok(Arc::clone(&input_stats[0]))
479    }
480}
481
482struct PartialSortStream {
483    /// The input plan
484    input: SendableRecordBatchStream,
485    /// Sort expressions
486    expr: LexOrdering,
487    /// Length of prefix common to input ordering and required ordering of plan
488    /// should be more than 0 otherwise PartialSort is not applicable
489    common_prefix_length: usize,
490    /// Used as a buffer for part of the input not ready for sort
491    in_mem_batch: RecordBatch,
492    /// Fetch top N results
493    fetch: Option<usize>,
494    /// Whether the stream has finished returning all of its data or not
495    is_closed: bool,
496    /// Execution metrics
497    baseline_metrics: BaselineMetrics,
498}
499
500impl Stream for PartialSortStream {
501    type Item = Result<RecordBatch>;
502
503    fn poll_next(
504        mut self: Pin<&mut Self>,
505        cx: &mut Context<'_>,
506    ) -> Poll<Option<Self::Item>> {
507        let poll = self.poll_next_inner(cx);
508        self.baseline_metrics.record_poll(poll)
509    }
510
511    fn size_hint(&self) -> (usize, Option<usize>) {
512        // we can't predict the size of incoming batches so re-use the size hint from the input
513        self.input.size_hint()
514    }
515}
516
517impl RecordBatchStream for PartialSortStream {
518    fn schema(&self) -> SchemaRef {
519        self.input.schema()
520    }
521}
522
523impl PartialSortStream {
524    fn poll_next_inner(
525        self: &mut Pin<&mut Self>,
526        cx: &mut Context<'_>,
527    ) -> Poll<Option<Result<RecordBatch>>> {
528        if self.is_closed {
529            return Poll::Ready(None);
530        }
531        loop {
532            // Check if we've already reached the fetch limit
533            if self.fetch == Some(0) {
534                self.is_closed = true;
535                // Release the input pipeline's resources.
536                let input_schema = self.input.schema();
537                self.input = Box::pin(EmptyRecordBatchStream::new(input_schema));
538                return Poll::Ready(None);
539            }
540
541            match ready!(self.input.poll_next_unpin(cx)) {
542                Some(Ok(batch)) => {
543                    // Merge new batch into in_mem_batch
544                    self.in_mem_batch = concat_batches(
545                        &self.schema(),
546                        &[self.in_mem_batch.clone(), batch],
547                    )?;
548
549                    // Check if we have a slice point, otherwise keep accumulating in `self.in_mem_batch`.
550                    if let Some(slice_point) = self
551                        .get_slice_point(self.common_prefix_length, &self.in_mem_batch)?
552                    {
553                        let sorted = self.in_mem_batch.slice(0, slice_point);
554                        self.in_mem_batch = self.in_mem_batch.slice(
555                            slice_point,
556                            self.in_mem_batch.num_rows() - slice_point,
557                        );
558                        let sorted_batch = sort_batch(&sorted, &self.expr, self.fetch)?;
559                        if let Some(fetch) = self.fetch.as_mut() {
560                            *fetch -= sorted_batch.num_rows();
561                        }
562
563                        if sorted_batch.num_rows() > 0 {
564                            return Poll::Ready(Some(Ok(sorted_batch)));
565                        }
566                    }
567                }
568                Some(Err(e)) => return Poll::Ready(Some(Err(e))),
569                None => {
570                    self.is_closed = true;
571                    // Release the input pipeline's resources before sorting.
572                    let input_schema = self.input.schema();
573                    self.input = Box::pin(EmptyRecordBatchStream::new(input_schema));
574                    // Once input is consumed, sort the rest of the inserted batches
575                    let remaining_batch = self.sort_in_mem_batch()?;
576                    return if remaining_batch.num_rows() > 0 {
577                        Poll::Ready(Some(Ok(remaining_batch)))
578                    } else {
579                        Poll::Ready(None)
580                    };
581                }
582            };
583        }
584    }
585
586    /// Returns a sorted RecordBatch from in_mem_batches and clears in_mem_batches
587    ///
588    /// If fetch is specified for PartialSortStream `sort_in_mem_batch` will limit
589    /// the last RecordBatch returned and will mark the stream as closed
590    fn sort_in_mem_batch(self: &mut Pin<&mut Self>) -> Result<RecordBatch> {
591        let input_batch = self.in_mem_batch.clone();
592        self.in_mem_batch = RecordBatch::new_empty(self.schema());
593        let result = sort_batch(&input_batch, &self.expr, self.fetch)?;
594        if let Some(remaining_fetch) = self.fetch {
595            // remaining_fetch - result.num_rows() is always be >= 0
596            // because result length of sort_batch with limit cannot be
597            // more than the requested limit
598            self.fetch = Some(remaining_fetch - result.num_rows());
599            if remaining_fetch == result.num_rows() {
600                self.is_closed = true;
601            }
602        }
603        Ok(result)
604    }
605
606    /// Return the end index of the second last partition if the batch
607    /// can be partitioned based on its already sorted columns
608    ///
609    /// Return None if the batch cannot be partitioned, which means the
610    /// batch does not have the information for a safe sort
611    fn get_slice_point(
612        &self,
613        common_prefix_len: usize,
614        batch: &RecordBatch,
615    ) -> Result<Option<usize>> {
616        let common_prefix_sort_keys = (0..common_prefix_len)
617            .map(|idx| self.expr[idx].evaluate_to_sort_column(batch))
618            .collect::<Result<Vec<_>>>()?;
619        let partition_points =
620            evaluate_partition_ranges(batch.num_rows(), &common_prefix_sort_keys)?;
621        // If partition points are [0..100], [100..200], [200..300]
622        // we should return 200, which is the safest and furthest partition boundary
623        // Please note that we shouldn't return 300 (which is number of rows in the batch),
624        // because this boundary may change with new data.
625        if partition_points.len() >= 2 {
626            Ok(Some(partition_points[partition_points.len() - 2].end))
627        } else {
628            Ok(None)
629        }
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use std::collections::HashMap;
636
637    use arrow::array::*;
638    use arrow::compute::SortOptions;
639    use arrow::datatypes::*;
640    use datafusion_common::test_util::batches_to_string;
641    use futures::FutureExt;
642    use insta::allow_duplicates;
643    use insta::assert_snapshot;
644    use itertools::Itertools;
645
646    use crate::collect;
647    use crate::expressions::PhysicalSortExpr;
648    use crate::expressions::col;
649    use crate::sorts::sort::SortExec;
650    use crate::test;
651    use crate::test::TestMemoryExec;
652    use crate::test::assert_is_pending;
653    use crate::test::exec::{BlockingExec, assert_strong_count_converges_to_zero};
654
655    use super::*;
656
657    #[tokio::test]
658    async fn test_partial_sort() -> Result<()> {
659        let task_ctx = Arc::new(TaskContext::default());
660        let source = test::build_table_scan_i32(
661            ("a", &vec![0, 0, 0, 1, 1, 1]),
662            ("b", &vec![1, 1, 2, 2, 3, 3]),
663            ("c", &vec![1, 0, 5, 4, 3, 2]),
664        );
665        let schema = Schema::new(vec![
666            Field::new("a", DataType::Int32, false),
667            Field::new("b", DataType::Int32, false),
668            Field::new("c", DataType::Int32, false),
669        ]);
670        let option_asc = SortOptions {
671            descending: false,
672            nulls_first: false,
673        };
674
675        let partial_sort_exec = Arc::new(PartialSortExec::new(
676            [
677                PhysicalSortExpr {
678                    expr: col("a", &schema)?,
679                    options: option_asc,
680                },
681                PhysicalSortExpr {
682                    expr: col("b", &schema)?,
683                    options: option_asc,
684                },
685                PhysicalSortExpr {
686                    expr: col("c", &schema)?,
687                    options: option_asc,
688                },
689            ]
690            .into(),
691            Arc::clone(&source),
692            2,
693        ));
694
695        let result = collect(partial_sort_exec, Arc::clone(&task_ctx)).await?;
696
697        assert_eq!(2, result.len());
698        allow_duplicates! {
699            assert_snapshot!(batches_to_string(&result), @r"
700            +---+---+---+
701            | a | b | c |
702            +---+---+---+
703            | 0 | 1 | 0 |
704            | 0 | 1 | 1 |
705            | 0 | 2 | 5 |
706            | 1 | 2 | 4 |
707            | 1 | 3 | 2 |
708            | 1 | 3 | 3 |
709            +---+---+---+
710            ");
711        }
712        assert_eq!(
713            task_ctx.runtime_env().memory_pool.reserved(),
714            0,
715            "The sort should have returned all memory used back to the memory manager"
716        );
717
718        Ok(())
719    }
720
721    #[tokio::test]
722    async fn test_partial_sort_with_fetch() -> Result<()> {
723        let task_ctx = Arc::new(TaskContext::default());
724        let source = test::build_table_scan_i32(
725            ("a", &vec![0, 0, 1, 1, 1]),
726            ("b", &vec![1, 2, 2, 3, 3]),
727            ("c", &vec![4, 3, 2, 1, 0]),
728        );
729        let schema = Schema::new(vec![
730            Field::new("a", DataType::Int32, false),
731            Field::new("b", DataType::Int32, false),
732            Field::new("c", DataType::Int32, false),
733        ]);
734        let option_asc = SortOptions {
735            descending: false,
736            nulls_first: false,
737        };
738
739        for common_prefix_length in [1, 2] {
740            let partial_sort_exec = Arc::new(
741                PartialSortExec::new(
742                    [
743                        PhysicalSortExpr {
744                            expr: col("a", &schema)?,
745                            options: option_asc,
746                        },
747                        PhysicalSortExpr {
748                            expr: col("b", &schema)?,
749                            options: option_asc,
750                        },
751                        PhysicalSortExpr {
752                            expr: col("c", &schema)?,
753                            options: option_asc,
754                        },
755                    ]
756                    .into(),
757                    Arc::clone(&source),
758                    common_prefix_length,
759                )
760                .with_fetch(Some(4)),
761            );
762
763            let result = collect(partial_sort_exec, Arc::clone(&task_ctx)).await?;
764
765            assert_eq!(2, result.len());
766            allow_duplicates! {
767                assert_snapshot!(batches_to_string(&result), @r"
768                +---+---+---+
769                | a | b | c |
770                +---+---+---+
771                | 0 | 1 | 4 |
772                | 0 | 2 | 3 |
773                | 1 | 2 | 2 |
774                | 1 | 3 | 0 |
775                +---+---+---+
776                ");
777            }
778            assert_eq!(
779                task_ctx.runtime_env().memory_pool.reserved(),
780                0,
781                "The sort should have returned all memory used back to the memory manager"
782            );
783        }
784
785        Ok(())
786    }
787
788    #[tokio::test]
789    async fn test_partial_sort2() -> Result<()> {
790        let task_ctx = Arc::new(TaskContext::default());
791        let source_tables = [
792            test::build_table_scan_i32(
793                ("a", &vec![0, 0, 0, 0, 1, 1, 1, 1]),
794                ("b", &vec![1, 1, 3, 3, 4, 4, 2, 2]),
795                ("c", &vec![7, 6, 5, 4, 3, 2, 1, 0]),
796            ),
797            test::build_table_scan_i32(
798                ("a", &vec![0, 0, 0, 0, 1, 1, 1, 1]),
799                ("b", &vec![1, 1, 3, 3, 2, 2, 4, 4]),
800                ("c", &vec![7, 6, 5, 4, 1, 0, 3, 2]),
801            ),
802        ];
803        let schema = Schema::new(vec![
804            Field::new("a", DataType::Int32, false),
805            Field::new("b", DataType::Int32, false),
806            Field::new("c", DataType::Int32, false),
807        ]);
808        let option_asc = SortOptions {
809            descending: false,
810            nulls_first: false,
811        };
812        for (common_prefix_length, source) in
813            [(1, &source_tables[0]), (2, &source_tables[1])]
814        {
815            let partial_sort_exec = Arc::new(PartialSortExec::new(
816                [
817                    PhysicalSortExpr {
818                        expr: col("a", &schema)?,
819                        options: option_asc,
820                    },
821                    PhysicalSortExpr {
822                        expr: col("b", &schema)?,
823                        options: option_asc,
824                    },
825                    PhysicalSortExpr {
826                        expr: col("c", &schema)?,
827                        options: option_asc,
828                    },
829                ]
830                .into(),
831                Arc::clone(source),
832                common_prefix_length,
833            ));
834
835            let result = collect(partial_sort_exec, Arc::clone(&task_ctx)).await?;
836            assert_eq!(2, result.len());
837            assert_eq!(
838                task_ctx.runtime_env().memory_pool.reserved(),
839                0,
840                "The sort should have returned all memory used back to the memory manager"
841            );
842            allow_duplicates! {
843                assert_snapshot!(batches_to_string(&result), @r"
844                +---+---+---+
845                | a | b | c |
846                +---+---+---+
847                | 0 | 1 | 6 |
848                | 0 | 1 | 7 |
849                | 0 | 3 | 4 |
850                | 0 | 3 | 5 |
851                | 1 | 2 | 0 |
852                | 1 | 2 | 1 |
853                | 1 | 4 | 2 |
854                | 1 | 4 | 3 |
855                +---+---+---+
856                ");
857            }
858        }
859        Ok(())
860    }
861
862    fn prepare_partitioned_input() -> Arc<dyn ExecutionPlan> {
863        let batch1 = test::build_table_i32(
864            ("a", &vec![1; 100]),
865            ("b", &(0..100).rev().collect()),
866            ("c", &(0..100).rev().collect()),
867        );
868        let batch2 = test::build_table_i32(
869            ("a", &[&vec![1; 25][..], &vec![2; 75][..]].concat()),
870            ("b", &(100..200).rev().collect()),
871            ("c", &(0..100).collect()),
872        );
873        let batch3 = test::build_table_i32(
874            ("a", &[&vec![3; 50][..], &vec![4; 50][..]].concat()),
875            ("b", &(150..250).rev().collect()),
876            ("c", &(0..100).rev().collect()),
877        );
878        let batch4 = test::build_table_i32(
879            ("a", &vec![4; 100]),
880            ("b", &(50..150).rev().collect()),
881            ("c", &(0..100).rev().collect()),
882        );
883        let schema = batch1.schema();
884
885        TestMemoryExec::try_new_exec(
886            &[vec![batch1, batch2, batch3, batch4]],
887            Arc::clone(&schema),
888            None,
889        )
890        .unwrap() as Arc<dyn ExecutionPlan>
891    }
892
893    #[tokio::test]
894    async fn test_partitioned_input_partial_sort() -> Result<()> {
895        let task_ctx = Arc::new(TaskContext::default());
896        let mem_exec = prepare_partitioned_input();
897        let option_asc = SortOptions {
898            descending: false,
899            nulls_first: false,
900        };
901        let option_desc = SortOptions {
902            descending: false,
903            nulls_first: false,
904        };
905        let schema = mem_exec.schema();
906        let partial_sort_exec = PartialSortExec::new(
907            [
908                PhysicalSortExpr {
909                    expr: col("a", &schema)?,
910                    options: option_asc,
911                },
912                PhysicalSortExpr {
913                    expr: col("b", &schema)?,
914                    options: option_desc,
915                },
916                PhysicalSortExpr {
917                    expr: col("c", &schema)?,
918                    options: option_asc,
919                },
920            ]
921            .into(),
922            Arc::clone(&mem_exec),
923            1,
924        );
925        let sort_exec = Arc::new(SortExec::new(
926            partial_sort_exec.expr.clone(),
927            Arc::clone(&partial_sort_exec.input),
928        ));
929        let result = collect(Arc::new(partial_sort_exec), Arc::clone(&task_ctx)).await?;
930        assert_eq!(
931            result.iter().map(|r| r.num_rows()).collect_vec(),
932            [125, 125, 150]
933        );
934
935        assert_eq!(
936            task_ctx.runtime_env().memory_pool.reserved(),
937            0,
938            "The sort should have returned all memory used back to the memory manager"
939        );
940        let partial_sort_result = concat_batches(&schema, &result).unwrap();
941        let sort_result = collect(sort_exec, Arc::clone(&task_ctx)).await?;
942        assert_eq!(sort_result[0], partial_sort_result);
943
944        Ok(())
945    }
946
947    #[tokio::test]
948    async fn test_partitioned_input_partial_sort_with_fetch() -> Result<()> {
949        let task_ctx = Arc::new(TaskContext::default());
950        let mem_exec = prepare_partitioned_input();
951        let schema = mem_exec.schema();
952        let option_asc = SortOptions {
953            descending: false,
954            nulls_first: false,
955        };
956        let option_desc = SortOptions {
957            descending: false,
958            nulls_first: false,
959        };
960        for (fetch_size, expected_batch_num_rows) in [
961            (Some(50), vec![50]),
962            (Some(120), vec![120]),
963            (Some(150), vec![125, 25]),
964            (Some(250), vec![125, 125]),
965        ] {
966            let partial_sort_exec = PartialSortExec::new(
967                [
968                    PhysicalSortExpr {
969                        expr: col("a", &schema)?,
970                        options: option_asc,
971                    },
972                    PhysicalSortExpr {
973                        expr: col("b", &schema)?,
974                        options: option_desc,
975                    },
976                    PhysicalSortExpr {
977                        expr: col("c", &schema)?,
978                        options: option_asc,
979                    },
980                ]
981                .into(),
982                Arc::clone(&mem_exec),
983                1,
984            )
985            .with_fetch(fetch_size);
986
987            let sort_exec = Arc::new(
988                SortExec::new(
989                    partial_sort_exec.expr.clone(),
990                    Arc::clone(&partial_sort_exec.input),
991                )
992                .with_fetch(fetch_size),
993            );
994            let result =
995                collect(Arc::new(partial_sort_exec), Arc::clone(&task_ctx)).await?;
996            assert_eq!(
997                result.iter().map(|r| r.num_rows()).collect_vec(),
998                expected_batch_num_rows
999            );
1000
1001            assert_eq!(
1002                task_ctx.runtime_env().memory_pool.reserved(),
1003                0,
1004                "The sort should have returned all memory used back to the memory manager"
1005            );
1006            let partial_sort_result = concat_batches(&schema, &result)?;
1007            let sort_result = collect(sort_exec, Arc::clone(&task_ctx)).await?;
1008            assert_eq!(sort_result[0], partial_sort_result);
1009        }
1010
1011        Ok(())
1012    }
1013
1014    #[tokio::test]
1015    async fn test_partial_sort_no_empty_batches() -> Result<()> {
1016        let task_ctx = Arc::new(TaskContext::default());
1017        let mem_exec = prepare_partitioned_input();
1018        let schema = mem_exec.schema();
1019        let option_asc = SortOptions {
1020            descending: false,
1021            nulls_first: false,
1022        };
1023        let fetch_size = Some(250);
1024        let partial_sort_exec = PartialSortExec::new(
1025            [
1026                PhysicalSortExpr {
1027                    expr: col("a", &schema)?,
1028                    options: option_asc,
1029                },
1030                PhysicalSortExpr {
1031                    expr: col("c", &schema)?,
1032                    options: option_asc,
1033                },
1034            ]
1035            .into(),
1036            Arc::clone(&mem_exec),
1037            1,
1038        )
1039        .with_fetch(fetch_size);
1040
1041        let result = collect(Arc::new(partial_sort_exec), Arc::clone(&task_ctx)).await?;
1042        for rb in result {
1043            assert!(rb.num_rows() > 0);
1044        }
1045
1046        Ok(())
1047    }
1048
1049    #[tokio::test]
1050    async fn test_sort_metadata() -> Result<()> {
1051        let task_ctx = Arc::new(TaskContext::default());
1052        let field_metadata: HashMap<String, String> =
1053            vec![("foo".to_string(), "bar".to_string())]
1054                .into_iter()
1055                .collect();
1056        let schema_metadata: HashMap<String, String> =
1057            vec![("baz".to_string(), "barf".to_string())]
1058                .into_iter()
1059                .collect();
1060
1061        let mut field = Field::new("field_name", DataType::UInt64, true);
1062        field.set_metadata(field_metadata.clone());
1063        let schema = Schema::new_with_metadata(vec![field], schema_metadata.clone());
1064        let schema = Arc::new(schema);
1065
1066        let data: ArrayRef =
1067            Arc::new(vec![1, 1, 2].into_iter().map(Some).collect::<UInt64Array>());
1068
1069        let batch = RecordBatch::try_new(Arc::clone(&schema), vec![data])?;
1070        let input =
1071            TestMemoryExec::try_new_exec(&[vec![batch]], Arc::clone(&schema), None)?;
1072
1073        let partial_sort_exec = Arc::new(PartialSortExec::new(
1074            [PhysicalSortExpr {
1075                expr: col("field_name", &schema)?,
1076                options: SortOptions::default(),
1077            }]
1078            .into(),
1079            input,
1080            1,
1081        ));
1082
1083        let result: Vec<RecordBatch> = collect(partial_sort_exec, task_ctx).await?;
1084        let expected_batch = vec![
1085            RecordBatch::try_new(
1086                Arc::clone(&schema),
1087                vec![Arc::new(
1088                    vec![1, 1].into_iter().map(Some).collect::<UInt64Array>(),
1089                )],
1090            )?,
1091            RecordBatch::try_new(
1092                Arc::clone(&schema),
1093                vec![Arc::new(
1094                    vec![2].into_iter().map(Some).collect::<UInt64Array>(),
1095                )],
1096            )?,
1097        ];
1098
1099        // Data is correct
1100        assert_eq!(&expected_batch, &result);
1101
1102        // explicitly ensure the metadata is present
1103        assert_eq!(result[0].schema().fields()[0].metadata(), &field_metadata);
1104        assert_eq!(result[0].schema().metadata(), &schema_metadata);
1105
1106        Ok(())
1107    }
1108
1109    #[tokio::test]
1110    async fn test_lex_sort_by_float() -> Result<()> {
1111        let task_ctx = Arc::new(TaskContext::default());
1112        let schema = Arc::new(Schema::new(vec![
1113            Field::new("a", DataType::Float32, true),
1114            Field::new("b", DataType::Float64, true),
1115            Field::new("c", DataType::Float64, true),
1116        ]));
1117        let option_asc = SortOptions {
1118            descending: false,
1119            nulls_first: true,
1120        };
1121        let option_desc = SortOptions {
1122            descending: true,
1123            nulls_first: true,
1124        };
1125
1126        // define data.
1127        let batch = RecordBatch::try_new(
1128            Arc::clone(&schema),
1129            vec![
1130                Arc::new(Float32Array::from(vec![
1131                    Some(1.0_f32),
1132                    Some(1.0_f32),
1133                    Some(1.0_f32),
1134                    Some(2.0_f32),
1135                    Some(2.0_f32),
1136                    Some(3.0_f32),
1137                    Some(3.0_f32),
1138                    Some(3.0_f32),
1139                ])),
1140                Arc::new(Float64Array::from(vec![
1141                    Some(20.0_f64),
1142                    Some(20.0_f64),
1143                    Some(40.0_f64),
1144                    Some(40.0_f64),
1145                    Some(f64::NAN),
1146                    None,
1147                    None,
1148                    Some(f64::NAN),
1149                ])),
1150                Arc::new(Float64Array::from(vec![
1151                    Some(10.0_f64),
1152                    Some(20.0_f64),
1153                    Some(10.0_f64),
1154                    Some(100.0_f64),
1155                    Some(f64::NAN),
1156                    Some(100.0_f64),
1157                    None,
1158                    Some(f64::NAN),
1159                ])),
1160            ],
1161        )?;
1162
1163        let partial_sort_exec = Arc::new(PartialSortExec::new(
1164            [
1165                PhysicalSortExpr {
1166                    expr: col("a", &schema)?,
1167                    options: option_asc,
1168                },
1169                PhysicalSortExpr {
1170                    expr: col("b", &schema)?,
1171                    options: option_asc,
1172                },
1173                PhysicalSortExpr {
1174                    expr: col("c", &schema)?,
1175                    options: option_desc,
1176                },
1177            ]
1178            .into(),
1179            TestMemoryExec::try_new_exec(&[vec![batch]], schema, None)?,
1180            2,
1181        ));
1182
1183        assert_eq!(
1184            DataType::Float32,
1185            *partial_sort_exec.schema().field(0).data_type()
1186        );
1187        assert_eq!(
1188            DataType::Float64,
1189            *partial_sort_exec.schema().field(1).data_type()
1190        );
1191        assert_eq!(
1192            DataType::Float64,
1193            *partial_sort_exec.schema().field(2).data_type()
1194        );
1195
1196        let result: Vec<RecordBatch> = collect(
1197            Arc::clone(&partial_sort_exec) as Arc<dyn ExecutionPlan>,
1198            task_ctx,
1199        )
1200        .await?;
1201        assert_snapshot!(batches_to_string(&result), @r"
1202        +-----+------+-------+
1203        | a   | b    | c     |
1204        +-----+------+-------+
1205        | 1.0 | 20.0 | 20.0  |
1206        | 1.0 | 20.0 | 10.0  |
1207        | 1.0 | 40.0 | 10.0  |
1208        | 2.0 | 40.0 | 100.0 |
1209        | 2.0 | NaN  | NaN   |
1210        | 3.0 |      |       |
1211        | 3.0 |      | 100.0 |
1212        | 3.0 | NaN  | NaN   |
1213        +-----+------+-------+
1214        ");
1215        assert_eq!(result.len(), 2);
1216        let metrics = partial_sort_exec.metrics().unwrap();
1217        assert!(metrics.elapsed_compute().unwrap() > 0);
1218        assert_eq!(metrics.output_rows().unwrap(), 8);
1219
1220        let columns = result[0].columns();
1221
1222        assert_eq!(DataType::Float32, *columns[0].data_type());
1223        assert_eq!(DataType::Float64, *columns[1].data_type());
1224        assert_eq!(DataType::Float64, *columns[2].data_type());
1225
1226        Ok(())
1227    }
1228
1229    #[tokio::test]
1230    async fn test_drop_cancel() -> Result<()> {
1231        let task_ctx = Arc::new(TaskContext::default());
1232        let schema = Arc::new(Schema::new(vec![
1233            Field::new("a", DataType::Float32, true),
1234            Field::new("b", DataType::Float32, true),
1235        ]));
1236
1237        let blocking_exec = Arc::new(BlockingExec::new(Arc::clone(&schema), 1));
1238        let refs = blocking_exec.refs();
1239        let sort_exec = Arc::new(PartialSortExec::new(
1240            [PhysicalSortExpr {
1241                expr: col("a", &schema)?,
1242                options: SortOptions::default(),
1243            }]
1244            .into(),
1245            blocking_exec,
1246            1,
1247        ));
1248
1249        let fut = collect(sort_exec, Arc::clone(&task_ctx));
1250        let mut fut = fut.boxed();
1251
1252        assert_is_pending(&mut fut);
1253        drop(fut);
1254        assert_strong_count_converges_to_zero(refs).await;
1255
1256        assert_eq!(
1257            task_ctx.runtime_env().memory_pool.reserved(),
1258            0,
1259            "The sort should have returned all memory used back to the memory manager"
1260        );
1261
1262        Ok(())
1263    }
1264
1265    #[tokio::test]
1266    async fn test_partial_sort_with_homogeneous_batches() -> Result<()> {
1267        // Test case for the bug where batches with homogeneous sort keys
1268        // (e.g., [1,1,1], [2,2,2]) would not be properly detected as having
1269        // slice points between batches.
1270        let task_ctx = Arc::new(TaskContext::default());
1271
1272        // Create batches where each batch has homogeneous values for sort keys
1273        let batch1 = test::build_table_i32(
1274            ("a", &vec![1; 3]),
1275            ("b", &vec![1; 3]),
1276            ("c", &vec![3, 2, 1]),
1277        );
1278        let batch2 = test::build_table_i32(
1279            ("a", &vec![2; 3]),
1280            ("b", &vec![2; 3]),
1281            ("c", &vec![4, 6, 4]),
1282        );
1283        let batch3 = test::build_table_i32(
1284            ("a", &vec![3; 3]),
1285            ("b", &vec![3; 3]),
1286            ("c", &vec![9, 7, 8]),
1287        );
1288
1289        let schema = batch1.schema();
1290        let mem_exec = TestMemoryExec::try_new_exec(
1291            &[vec![batch1, batch2, batch3]],
1292            Arc::clone(&schema),
1293            None,
1294        )?;
1295
1296        let option_asc = SortOptions {
1297            descending: false,
1298            nulls_first: false,
1299        };
1300
1301        // Partial sort with common prefix of 2 (sorting by a, b, c)
1302        let partial_sort_exec = Arc::new(PartialSortExec::new(
1303            [
1304                PhysicalSortExpr {
1305                    expr: col("a", &schema)?,
1306                    options: option_asc,
1307                },
1308                PhysicalSortExpr {
1309                    expr: col("b", &schema)?,
1310                    options: option_asc,
1311                },
1312                PhysicalSortExpr {
1313                    expr: col("c", &schema)?,
1314                    options: option_asc,
1315                },
1316            ]
1317            .into(),
1318            mem_exec,
1319            2,
1320        ));
1321
1322        let result = collect(partial_sort_exec, Arc::clone(&task_ctx)).await?;
1323
1324        assert_eq!(result.len(), 3,);
1325
1326        allow_duplicates! {
1327            assert_snapshot!(batches_to_string(&result), @r"
1328            +---+---+---+
1329            | a | b | c |
1330            +---+---+---+
1331            | 1 | 1 | 1 |
1332            | 1 | 1 | 2 |
1333            | 1 | 1 | 3 |
1334            | 2 | 2 | 4 |
1335            | 2 | 2 | 4 |
1336            | 2 | 2 | 6 |
1337            | 3 | 3 | 7 |
1338            | 3 | 3 | 8 |
1339            | 3 | 3 | 9 |
1340            +---+---+---+
1341            ");
1342        }
1343
1344        assert_eq!(task_ctx.runtime_env().memory_pool.reserved(), 0,);
1345        Ok(())
1346    }
1347}