Skip to main content

vibesql_executor/memory/
external_sort.rs

1//! External sort for memory-bounded ORDER BY execution
2//!
3//! Implements a two-phase external merge sort:
4//!
5//! 1. **Sort Phase**: Process input in chunks, sort each chunk in memory, and spill sorted runs to
6//!    disk when memory is exhausted.
7//!
8//! 2. **Merge Phase**: K-way merge of sorted runs, streaming results with bounded memory usage.
9//!
10//! # Algorithm
11//!
12//! ```text
13//! Input rows
14//!     │
15//!     ▼
16//! ┌────────────────────────────────────────┐
17//! │        Phase 1: Create Sorted Runs     │
18//! │  ┌─────────────┐  ┌─────────────┐      │
19//! │  │ In-memory   │  │  Spilled    │      │
20//! │  │ buffer      │──▶  run file   │      │
21//! │  └─────────────┘  └─────────────┘      │
22//! └────────────────────────────────────────┘
23//!     │
24//!     ▼
25//! ┌────────────────────────────────────────┐
26//! │       Phase 2: K-way Merge             │
27//! │  Run1 ──┐                              │
28//! │  Run2 ──┼──▶ Min-heap ──▶ Output       │
29//! │  Run3 ──┘                              │
30//! └────────────────────────────────────────┘
31//! ```
32//!
33//! # Design Decisions
34//!
35//! - **Chunk-based processing**: Sorts morsel-sized chunks to maintain cache locality
36//! - **Lazy spilling**: Only spills when memory pressure exceeds threshold
37//! - **Streaming merge**: Outputs rows incrementally without materializing all results
38//! - **Stable sort**: Preserves relative order of equal elements
39
40use std::{
41    cmp::Ordering,
42    collections::BinaryHeap,
43    io::{self, Cursor},
44    sync::Arc,
45};
46
47use vibesql_ast::OrderDirection;
48use vibesql_storage::Row;
49use vibesql_types::SqlValue;
50
51use super::{
52    row_serialization::{deserialize_row_with_keys, serialize_row_with_keys},
53    MemoryController, MemoryReservation, SpillFile,
54};
55
56/// Sort key for a row: the evaluated ORDER BY values with their directions
57pub type SortKey = Vec<(SqlValue, OrderDirection)>;
58
59/// A row with its evaluated sort keys
60pub type RowWithKeys = (Row, SortKey);
61
62/// Configuration for external sort
63#[derive(Debug, Clone)]
64pub struct ExternalSortConfig {
65    /// Maximum rows per in-memory sorted run before considering spill
66    pub max_run_size: usize,
67
68    /// Number of runs to merge at once in k-way merge
69    pub merge_fanout: usize,
70}
71
72impl Default for ExternalSortConfig {
73    fn default() -> Self {
74        Self {
75            max_run_size: 50_000, // ~50K rows per run
76            merge_fanout: 16,     // 16-way merge
77        }
78    }
79}
80
81/// A sorted run, either in memory or spilled to disk
82enum SortRun {
83    /// In-memory sorted run (used when mixing memory/disk runs in merge)
84    #[allow(dead_code)]
85    InMemory(Vec<RowWithKeys>),
86
87    /// Spilled to disk
88    OnDisk { file: SpillFile, row_count: usize },
89}
90
91impl SortRun {
92    /// Get the number of rows in this run
93    fn row_count(&self) -> usize {
94        match self {
95            SortRun::InMemory(rows) => rows.len(),
96            SortRun::OnDisk { row_count, .. } => *row_count,
97        }
98    }
99}
100
101/// External sort operator
102///
103/// Implements memory-bounded sorting with disk spilling.
104pub struct ExternalSort {
105    /// Memory reservation for this operator
106    reservation: MemoryReservation,
107
108    /// Configuration
109    config: ExternalSortConfig,
110
111    /// Current in-memory buffer being accumulated
112    buffer: Vec<RowWithKeys>,
113
114    /// Estimated memory used by current buffer
115    buffer_memory: usize,
116
117    /// Completed sorted runs
118    runs: Vec<SortRun>,
119
120    /// Comparison function for sort keys
121    comparator: SortKeyComparator,
122}
123
124/// Comparator for sort keys
125#[derive(Clone)]
126struct SortKeyComparator;
127
128impl SortKeyComparator {
129    /// Compare two sort keys
130    fn compare(&self, a: &SortKey, b: &SortKey) -> Ordering {
131        for ((val_a, dir), (val_b, _)) in a.iter().zip(b.iter()) {
132            // Handle NULLs: always sort last regardless of ASC/DESC
133            let cmp = match (val_a.is_null(), val_b.is_null()) {
134                (true, true) => Ordering::Equal,
135                (true, false) => return Ordering::Greater, // NULL always sorts last
136                (false, true) => return Ordering::Less,    // non-NULL always sorts first
137                (false, false) => {
138                    // Compare non-NULL values, respecting direction
139                    let base_cmp = compare_sql_values(val_a, val_b);
140                    match dir {
141                        OrderDirection::Asc => base_cmp,
142                        OrderDirection::Desc => base_cmp.reverse(),
143                    }
144                }
145            };
146
147            if cmp != Ordering::Equal {
148                return cmp;
149            }
150        }
151        Ordering::Equal
152    }
153
154    /// Compare two rows by their sort keys, using rowid as tie-breaker
155    ///
156    /// When ORDER BY expressions are equal, SQLite uses rowid as a secondary sort key
157    /// to maintain deterministic ordering. This function implements the same behavior
158    /// (issue #4893).
159    fn compare_with_rowid(&self, a: &RowWithKeys, b: &RowWithKeys) -> Ordering {
160        let key_cmp = self.compare(&a.1, &b.1);
161        if key_cmp != Ordering::Equal {
162            return key_cmp;
163        }
164        // Use rowid as tie-breaker for deterministic ordering
165        a.0.row_id.cmp(&b.0.row_id)
166    }
167}
168
169/// Compare two SqlValue instances
170fn compare_sql_values(a: &SqlValue, b: &SqlValue) -> Ordering {
171    use SqlValue::*;
172    match (a, b) {
173        (Integer(x), Integer(y)) => x.cmp(y),
174        (Smallint(x), Smallint(y)) => x.cmp(y),
175        (Bigint(x), Bigint(y)) => x.cmp(y),
176        (Unsigned(x), Unsigned(y)) => x.cmp(y),
177        (Float(x), Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
178        (Real(x), Real(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
179        (Double(x), Double(y)) | (Numeric(x), Numeric(y)) => {
180            x.partial_cmp(y).unwrap_or(Ordering::Equal)
181        }
182        (Character(x), Character(y)) | (Varchar(x), Varchar(y)) => x.cmp(y),
183        (Character(x), Varchar(y)) | (Varchar(x), Character(y)) => x.as_str().cmp(y.as_str()),
184        (Boolean(x), Boolean(y)) => x.cmp(y),
185        (Date(x), Date(y)) => x.cmp(y),
186        (Time(x), Time(y)) => x.cmp(y),
187        (Timestamp(x), Timestamp(y)) => x.cmp(y),
188        (Interval(x), Interval(y)) => x.cmp(y),
189        _ => Ordering::Equal, // Type mismatch - treat as equal
190    }
191}
192
193impl ExternalSort {
194    /// Create a new external sort operator
195    pub fn new(controller: &Arc<MemoryController>) -> Self {
196        Self::with_config(controller, ExternalSortConfig::default())
197    }
198
199    /// Create with custom configuration
200    pub fn with_config(controller: &Arc<MemoryController>, config: ExternalSortConfig) -> Self {
201        Self {
202            reservation: controller.create_reservation(),
203            config,
204            buffer: Vec::new(),
205            buffer_memory: 0,
206            runs: Vec::new(),
207            comparator: SortKeyComparator,
208        }
209    }
210
211    /// Add a row with its sort keys to the sort operator
212    ///
213    /// May trigger a spill if memory is exhausted.
214    pub fn add_row(&mut self, row: Row, sort_keys: SortKey) -> io::Result<()> {
215        // Estimate memory for this row
216        let row_memory = row.estimated_size_bytes()
217            + sort_keys.iter().map(|(v, _)| v.estimated_size_bytes()).sum::<usize>()
218            + std::mem::size_of::<RowWithKeys>();
219
220        // Check if we need to spill
221        if !self.reservation.try_grow(row_memory) {
222            // Memory exhausted - sort and spill current buffer
223            self.spill_buffer()?;
224
225            // Try to reserve again
226            if !self.reservation.try_grow(row_memory) {
227                // Still can't fit - this single row exceeds available memory
228                // This shouldn't happen with reasonable memory limits
229                return Err(io::Error::new(
230                    io::ErrorKind::OutOfMemory,
231                    "single row exceeds available memory budget",
232                ));
233            }
234        }
235
236        self.buffer.push((row, sort_keys));
237        self.buffer_memory += row_memory;
238
239        // Check if buffer reached max run size
240        if self.buffer.len() >= self.config.max_run_size {
241            self.spill_buffer()?;
242        }
243
244        Ok(())
245    }
246
247    /// Sort and spill the current buffer to disk
248    fn spill_buffer(&mut self) -> io::Result<()> {
249        if self.buffer.is_empty() {
250            return Ok(());
251        }
252
253        // Sort the buffer with rowid tie-breaking for SQLite-compatible ordering
254        let comparator = self.comparator.clone();
255        self.buffer.sort_by(|a, b| comparator.compare_with_rowid(a, b));
256
257        // Create spill file
258        let temp_dir = self.reservation.temp_directory();
259        let mut spill_file = SpillFile::with_suffix(temp_dir, "sort_run")?;
260
261        // Serialize all rows to the spill file
262        let row_count = self.buffer.len();
263        let mut spill_buf = Vec::new();
264
265        for (row, keys) in &self.buffer {
266            serialize_row_with_keys(row, keys, &mut spill_buf)?;
267        }
268
269        spill_file.write_all(&spill_buf)?;
270        spill_file.flush()?;
271
272        // Record spill metrics
273        self.reservation.record_spill(spill_buf.len());
274
275        // Release memory
276        self.reservation.shrink(self.buffer_memory);
277        self.buffer.clear();
278        self.buffer_memory = 0;
279
280        // Add spilled run
281        self.runs.push(SortRun::OnDisk { file: spill_file, row_count });
282
283        Ok(())
284    }
285
286    /// Finalize sorting and return an iterator over sorted results
287    ///
288    /// This completes the sort phase and prepares for the merge phase.
289    pub fn finish(mut self) -> io::Result<SortedIterator> {
290        // Sort any remaining buffer with rowid tie-breaking for SQLite-compatible ordering
291        if !self.buffer.is_empty() {
292            let comparator = self.comparator.clone();
293            self.buffer.sort_by(|a, b| comparator.compare_with_rowid(a, b));
294
295            // Keep in memory if we have no spilled runs
296            if self.runs.is_empty() {
297                // All data fits in memory - return directly
298                return Ok(SortedIterator::InMemory(InMemoryIterator::new(
299                    self.buffer,
300                    self.reservation,
301                )));
302            }
303
304            // We have spilled runs - spill this buffer too for consistent merge
305            self.spill_buffer()?;
306        }
307
308        // If no runs, return empty iterator
309        if self.runs.is_empty() {
310            return Ok(SortedIterator::InMemory(InMemoryIterator::new(
311                Vec::new(),
312                self.reservation,
313            )));
314        }
315
316        // Create merge iterator over all runs
317        Ok(SortedIterator::Merge(MergeIterator::new(self.runs, self.comparator, self.reservation)?))
318    }
319
320    /// Get the number of runs created so far
321    pub fn run_count(&self) -> usize {
322        self.runs.len() + if self.buffer.is_empty() { 0 } else { 1 }
323    }
324
325    /// Get total rows added
326    pub fn total_rows(&self) -> usize {
327        self.runs.iter().map(|r| r.row_count()).sum::<usize>() + self.buffer.len()
328    }
329}
330
331/// Iterator over sorted results
332pub enum SortedIterator {
333    /// All data fit in memory
334    InMemory(InMemoryIterator),
335
336    /// Merge iterator over spilled runs
337    Merge(MergeIterator),
338}
339
340impl Iterator for SortedIterator {
341    type Item = io::Result<Row>;
342
343    fn next(&mut self) -> Option<Self::Item> {
344        match self {
345            SortedIterator::InMemory(iter) => iter.next(),
346            SortedIterator::Merge(iter) => iter.next(),
347        }
348    }
349}
350
351/// Iterator over in-memory sorted results
352pub struct InMemoryIterator {
353    rows: std::vec::IntoIter<RowWithKeys>,
354    #[allow(dead_code)]
355    reservation: MemoryReservation, // Keeps memory reserved until iterator is dropped
356}
357
358impl InMemoryIterator {
359    fn new(rows: Vec<RowWithKeys>, reservation: MemoryReservation) -> Self {
360        Self { rows: rows.into_iter(), reservation }
361    }
362}
363
364impl Iterator for InMemoryIterator {
365    type Item = io::Result<Row>;
366
367    fn next(&mut self) -> Option<Self::Item> {
368        self.rows.next().map(|(row, _keys)| Ok(row))
369    }
370}
371
372/// K-way merge iterator over spilled sorted runs
373pub struct MergeIterator {
374    /// Min-heap for k-way merge
375    heap: BinaryHeap<MergeEntry>,
376
377    /// Run readers (one per spilled run)
378    readers: Vec<RunReader>,
379
380    /// Comparator for sort keys
381    comparator: SortKeyComparator,
382
383    /// Memory reservation
384    #[allow(dead_code)]
385    reservation: MemoryReservation,
386}
387
388/// Entry in the merge heap
389struct MergeEntry {
390    /// The row
391    row: Row,
392
393    /// Sort key for comparison
394    key: SortKey,
395
396    /// Index of the run this came from
397    run_index: usize,
398
399    /// Comparator reference for ordering
400    comparator: SortKeyComparator,
401}
402
403impl PartialEq for MergeEntry {
404    fn eq(&self, other: &Self) -> bool {
405        // Compare keys first, then use rowid as tie-breaker
406        let key_cmp = self.comparator.compare(&self.key, &other.key);
407        if key_cmp != Ordering::Equal {
408            return false;
409        }
410        self.row.row_id == other.row.row_id
411    }
412}
413
414impl Eq for MergeEntry {}
415
416impl PartialOrd for MergeEntry {
417    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
418        Some(self.cmp(other))
419    }
420}
421
422impl Ord for MergeEntry {
423    fn cmp(&self, other: &Self) -> Ordering {
424        // Reverse for min-heap (BinaryHeap is a max-heap)
425        // Compare keys first
426        let key_cmp = self.comparator.compare(&other.key, &self.key);
427        if key_cmp != Ordering::Equal {
428            return key_cmp;
429        }
430        // Use rowid as tie-breaker for SQLite-compatible ordering (issue #4893)
431        // Note: reversed because we're in a max-heap (smaller rowid = higher priority)
432        other.row.row_id.cmp(&self.row.row_id)
433    }
434}
435
436/// Reader for a spilled sorted run
437struct RunReader {
438    /// Cursor over spill file data (loaded into memory)
439    cursor: Cursor<Vec<u8>>,
440
441    /// Remaining rows to read
442    remaining: usize,
443}
444
445impl RunReader {
446    fn new(mut file: SpillFile, row_count: usize) -> io::Result<Self> {
447        file.prepare_for_read()?;
448        let data = file.read_to_vec()?;
449        Ok(Self { cursor: Cursor::new(data), remaining: row_count })
450    }
451
452    fn read_next(&mut self) -> io::Result<Option<RowWithKeys>> {
453        if self.remaining == 0 {
454            return Ok(None);
455        }
456        self.remaining -= 1;
457        let (row, keys) = deserialize_row_with_keys(&mut self.cursor)?;
458        Ok(Some((row, keys)))
459    }
460}
461
462impl MergeIterator {
463    fn new(
464        runs: Vec<SortRun>,
465        comparator: SortKeyComparator,
466        reservation: MemoryReservation,
467    ) -> io::Result<Self> {
468        let mut readers = Vec::with_capacity(runs.len());
469        let mut heap = BinaryHeap::new();
470
471        for (run_index, run) in runs.into_iter().enumerate() {
472            match run {
473                SortRun::InMemory(rows) => {
474                    // Shouldn't happen in merge phase, but handle anyway
475                    for (row, key) in rows {
476                        heap.push(MergeEntry {
477                            row,
478                            key,
479                            run_index,
480                            comparator: comparator.clone(),
481                        });
482                    }
483                    readers.push(RunReader { cursor: Cursor::new(Vec::new()), remaining: 0 });
484                }
485                SortRun::OnDisk { file, row_count } => {
486                    let mut reader = RunReader::new(file, row_count)?;
487
488                    // Read first entry from this run
489                    if let Some((row, key)) = reader.read_next()? {
490                        heap.push(MergeEntry {
491                            row,
492                            key,
493                            run_index,
494                            comparator: comparator.clone(),
495                        });
496                    }
497
498                    readers.push(reader);
499                }
500            }
501        }
502
503        Ok(Self { heap, readers, comparator, reservation })
504    }
505}
506
507impl Iterator for MergeIterator {
508    type Item = io::Result<Row>;
509
510    fn next(&mut self) -> Option<Self::Item> {
511        // Pop smallest entry from heap
512        let entry = self.heap.pop()?;
513
514        // Read next entry from the same run
515        match self.readers[entry.run_index].read_next() {
516            Ok(Some((row, key))) => {
517                self.heap.push(MergeEntry {
518                    row,
519                    key,
520                    run_index: entry.run_index,
521                    comparator: self.comparator.clone(),
522                });
523            }
524            Ok(None) => {
525                // This run is exhausted
526            }
527            Err(e) => {
528                return Some(Err(e));
529            }
530        }
531
532        Some(Ok(entry.row))
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    fn make_test_controller() -> Arc<MemoryController> {
541        Arc::new(MemoryController::with_budget(1024 * 1024)) // 1MB for tests
542    }
543
544    #[test]
545    fn test_in_memory_sort() {
546        let controller = make_test_controller();
547        let mut sorter = ExternalSort::new(&controller);
548
549        // Add rows in reverse order
550        for i in (0..100).rev() {
551            let row = Row::from_vec(vec![SqlValue::Integer(i)]);
552            let keys = vec![(SqlValue::Integer(i), OrderDirection::Asc)];
553            sorter.add_row(row, keys).unwrap();
554        }
555
556        // Finish and collect results
557        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();
558
559        assert_eq!(results.len(), 100);
560        for (i, row) in results.iter().enumerate() {
561            assert_eq!(row.values[0], SqlValue::Integer(i as i64));
562        }
563    }
564
565    #[test]
566    fn test_descending_sort() {
567        let controller = make_test_controller();
568        let mut sorter = ExternalSort::new(&controller);
569
570        // Add rows in ascending order
571        for i in 0..50 {
572            let row = Row::from_vec(vec![SqlValue::Integer(i)]);
573            let keys = vec![(SqlValue::Integer(i), OrderDirection::Desc)];
574            sorter.add_row(row, keys).unwrap();
575        }
576
577        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();
578
579        assert_eq!(results.len(), 50);
580        // Should be in descending order: 49, 48, ..., 0
581        for (i, row) in results.iter().enumerate() {
582            assert_eq!(row.values[0], SqlValue::Integer(49 - i as i64));
583        }
584    }
585
586    #[test]
587    fn test_null_handling() {
588        let controller = make_test_controller();
589        let mut sorter = ExternalSort::new(&controller);
590
591        // Add rows with NULLs
592        sorter
593            .add_row(
594                Row::from_vec(vec![SqlValue::Integer(2)]),
595                vec![(SqlValue::Integer(2), OrderDirection::Asc)],
596            )
597            .unwrap();
598        sorter
599            .add_row(
600                Row::from_vec(vec![SqlValue::Null]),
601                vec![(SqlValue::Null, OrderDirection::Asc)],
602            )
603            .unwrap();
604        sorter
605            .add_row(
606                Row::from_vec(vec![SqlValue::Integer(1)]),
607                vec![(SqlValue::Integer(1), OrderDirection::Asc)],
608            )
609            .unwrap();
610
611        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();
612
613        // ASC: 1, 2, NULL (NULLs last)
614        assert_eq!(results[0].values[0], SqlValue::Integer(1));
615        assert_eq!(results[1].values[0], SqlValue::Integer(2));
616        assert_eq!(results[2].values[0], SqlValue::Null);
617    }
618
619    #[test]
620    fn test_multi_key_sort() {
621        let controller = make_test_controller();
622        let mut sorter = ExternalSort::new(&controller);
623
624        // Add rows to test multi-key sorting
625        // (a=1, b=2), (a=1, b=1), (a=2, b=1)
626        sorter
627            .add_row(
628                Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(2)]),
629                vec![
630                    (SqlValue::Integer(1), OrderDirection::Asc),
631                    (SqlValue::Integer(2), OrderDirection::Asc),
632                ],
633            )
634            .unwrap();
635        sorter
636            .add_row(
637                Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(1)]),
638                vec![
639                    (SqlValue::Integer(1), OrderDirection::Asc),
640                    (SqlValue::Integer(1), OrderDirection::Asc),
641                ],
642            )
643            .unwrap();
644        sorter
645            .add_row(
646                Row::from_vec(vec![SqlValue::Integer(2), SqlValue::Integer(1)]),
647                vec![
648                    (SqlValue::Integer(2), OrderDirection::Asc),
649                    (SqlValue::Integer(1), OrderDirection::Asc),
650                ],
651            )
652            .unwrap();
653
654        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();
655
656        // Expected order: (1,1), (1,2), (2,1)
657        assert_eq!(results[0].values[0], SqlValue::Integer(1));
658        assert_eq!(results[0].values[1], SqlValue::Integer(1));
659        assert_eq!(results[1].values[0], SqlValue::Integer(1));
660        assert_eq!(results[1].values[1], SqlValue::Integer(2));
661        assert_eq!(results[2].values[0], SqlValue::Integer(2));
662        assert_eq!(results[2].values[1], SqlValue::Integer(1));
663    }
664
665    #[test]
666    fn test_spill_and_merge() {
667        // Use very small memory to force spilling
668        let controller = Arc::new(MemoryController::with_budget(4096)); // 4KB
669        let config = ExternalSortConfig {
670            max_run_size: 10, // Very small runs
671            ..Default::default()
672        };
673
674        let mut sorter = ExternalSort::with_config(&controller, config);
675
676        // Add enough rows to force multiple spills
677        for i in (0..50).rev() {
678            let row = Row::from_vec(vec![SqlValue::Integer(i)]);
679            let keys = vec![(SqlValue::Integer(i), OrderDirection::Asc)];
680            sorter.add_row(row, keys).unwrap();
681        }
682
683        // Should have created multiple runs
684        assert!(sorter.run_count() > 1, "Expected multiple runs from spilling");
685
686        // Finish and verify sorted order
687        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();
688
689        assert_eq!(results.len(), 50);
690        for (i, row) in results.iter().enumerate() {
691            assert_eq!(row.values[0], SqlValue::Integer(i as i64));
692        }
693    }
694
695    #[test]
696    fn test_empty_sort() {
697        let controller = make_test_controller();
698        let sorter = ExternalSort::new(&controller);
699
700        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();
701        assert!(results.is_empty());
702    }
703
704    #[test]
705    fn test_string_sort() {
706        let controller = make_test_controller();
707        let mut sorter = ExternalSort::new(&controller);
708
709        sorter
710            .add_row(
711                Row::from_vec(vec![SqlValue::Varchar("charlie".into())]),
712                vec![(SqlValue::Varchar("charlie".into()), OrderDirection::Asc)],
713            )
714            .unwrap();
715        sorter
716            .add_row(
717                Row::from_vec(vec![SqlValue::Varchar("alpha".into())]),
718                vec![(SqlValue::Varchar("alpha".into()), OrderDirection::Asc)],
719            )
720            .unwrap();
721        sorter
722            .add_row(
723                Row::from_vec(vec![SqlValue::Varchar("bravo".into())]),
724                vec![(SqlValue::Varchar("bravo".into()), OrderDirection::Asc)],
725            )
726            .unwrap();
727
728        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();
729
730        assert_eq!(results[0].values[0], SqlValue::Varchar("alpha".into()));
731        assert_eq!(results[1].values[0], SqlValue::Varchar("bravo".into()));
732        assert_eq!(results[2].values[0], SqlValue::Varchar("charlie".into()));
733    }
734}