vibesql-executor 0.2.0

Query execution engine for vibesql SQL database
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
//! External sort for memory-bounded ORDER BY execution
//!
//! Implements a two-phase external merge sort:
//!
//! 1. **Sort Phase**: Process input in chunks, sort each chunk in memory, and spill sorted runs to
//!    disk when memory is exhausted.
//!
//! 2. **Merge Phase**: K-way merge of sorted runs, streaming results with bounded memory usage.
//!
//! # Algorithm
//!
//! ```text
//! Input rows
//!     │
//!     ▼
//! ┌────────────────────────────────────────┐
//! │        Phase 1: Create Sorted Runs     │
//! │  ┌─────────────┐  ┌─────────────┐      │
//! │  │ In-memory   │  │  Spilled    │      │
//! │  │ buffer      │──▶  run file   │      │
//! │  └─────────────┘  └─────────────┘      │
//! └────────────────────────────────────────┘
//!     │
//!     ▼
//! ┌────────────────────────────────────────┐
//! │       Phase 2: K-way Merge             │
//! │  Run1 ──┐                              │
//! │  Run2 ──┼──▶ Min-heap ──▶ Output       │
//! │  Run3 ──┘                              │
//! └────────────────────────────────────────┘
//! ```
//!
//! # Design Decisions
//!
//! - **Chunk-based processing**: Sorts morsel-sized chunks to maintain cache locality
//! - **Lazy spilling**: Only spills when memory pressure exceeds threshold
//! - **Streaming merge**: Outputs rows incrementally without materializing all results
//! - **Stable sort**: Preserves relative order of equal elements

use std::{
    cmp::Ordering,
    collections::BinaryHeap,
    io::{self, Cursor},
    sync::Arc,
};

use vibesql_ast::OrderDirection;
use vibesql_storage::Row;
use vibesql_types::SqlValue;

use super::{
    row_serialization::{deserialize_row_with_keys, serialize_row_with_keys},
    MemoryController, MemoryReservation, SpillFile,
};

/// Sort key for a row: the evaluated ORDER BY values with their directions
pub type SortKey = Vec<(SqlValue, OrderDirection)>;

/// A row with its evaluated sort keys
pub type RowWithKeys = (Row, SortKey);

/// Configuration for external sort
#[derive(Debug, Clone)]
pub struct ExternalSortConfig {
    /// Maximum rows per in-memory sorted run before considering spill
    pub max_run_size: usize,

    /// Number of runs to merge at once in k-way merge
    pub merge_fanout: usize,
}

impl Default for ExternalSortConfig {
    fn default() -> Self {
        Self {
            max_run_size: 50_000, // ~50K rows per run
            merge_fanout: 16,     // 16-way merge
        }
    }
}

/// A sorted run, either in memory or spilled to disk
enum SortRun {
    /// In-memory sorted run (used when mixing memory/disk runs in merge)
    #[allow(dead_code)]
    InMemory(Vec<RowWithKeys>),

    /// Spilled to disk
    OnDisk { file: SpillFile, row_count: usize },
}

impl SortRun {
    /// Get the number of rows in this run
    fn row_count(&self) -> usize {
        match self {
            SortRun::InMemory(rows) => rows.len(),
            SortRun::OnDisk { row_count, .. } => *row_count,
        }
    }
}

/// External sort operator
///
/// Implements memory-bounded sorting with disk spilling.
pub struct ExternalSort {
    /// Memory reservation for this operator
    reservation: MemoryReservation,

    /// Configuration
    config: ExternalSortConfig,

    /// Current in-memory buffer being accumulated
    buffer: Vec<RowWithKeys>,

    /// Estimated memory used by current buffer
    buffer_memory: usize,

    /// Completed sorted runs
    runs: Vec<SortRun>,

    /// Comparison function for sort keys
    comparator: SortKeyComparator,
}

/// Comparator for sort keys
#[derive(Clone)]
struct SortKeyComparator;

impl SortKeyComparator {
    /// Compare two sort keys
    fn compare(&self, a: &SortKey, b: &SortKey) -> Ordering {
        for ((val_a, dir), (val_b, _)) in a.iter().zip(b.iter()) {
            // Handle NULLs: always sort last regardless of ASC/DESC
            let cmp = match (val_a.is_null(), val_b.is_null()) {
                (true, true) => Ordering::Equal,
                (true, false) => return Ordering::Greater, // NULL always sorts last
                (false, true) => return Ordering::Less,    // non-NULL always sorts first
                (false, false) => {
                    // Compare non-NULL values, respecting direction
                    let base_cmp = compare_sql_values(val_a, val_b);
                    match dir {
                        OrderDirection::Asc => base_cmp,
                        OrderDirection::Desc => base_cmp.reverse(),
                    }
                }
            };

            if cmp != Ordering::Equal {
                return cmp;
            }
        }
        Ordering::Equal
    }

    /// Compare two rows by their sort keys, using rowid as tie-breaker
    ///
    /// When ORDER BY expressions are equal, SQLite uses rowid as a secondary sort key
    /// to maintain deterministic ordering. This function implements the same behavior
    /// (issue #4893).
    fn compare_with_rowid(&self, a: &RowWithKeys, b: &RowWithKeys) -> Ordering {
        let key_cmp = self.compare(&a.1, &b.1);
        if key_cmp != Ordering::Equal {
            return key_cmp;
        }
        // Use rowid as tie-breaker for deterministic ordering
        a.0.row_id.cmp(&b.0.row_id)
    }
}

/// Compare two SqlValue instances
fn compare_sql_values(a: &SqlValue, b: &SqlValue) -> Ordering {
    use SqlValue::*;
    match (a, b) {
        (Integer(x), Integer(y)) => x.cmp(y),
        (Smallint(x), Smallint(y)) => x.cmp(y),
        (Bigint(x), Bigint(y)) => x.cmp(y),
        (Unsigned(x), Unsigned(y)) => x.cmp(y),
        (Float(x), Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
        (Real(x), Real(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
        (Double(x), Double(y)) | (Numeric(x), Numeric(y)) => {
            x.partial_cmp(y).unwrap_or(Ordering::Equal)
        }
        (Character(x), Character(y)) | (Varchar(x), Varchar(y)) => x.cmp(y),
        (Character(x), Varchar(y)) | (Varchar(x), Character(y)) => x.as_str().cmp(y.as_str()),
        (Boolean(x), Boolean(y)) => x.cmp(y),
        (Date(x), Date(y)) => x.cmp(y),
        (Time(x), Time(y)) => x.cmp(y),
        (Timestamp(x), Timestamp(y)) => x.cmp(y),
        (Interval(x), Interval(y)) => x.cmp(y),
        _ => Ordering::Equal, // Type mismatch - treat as equal
    }
}

impl ExternalSort {
    /// Create a new external sort operator
    pub fn new(controller: &Arc<MemoryController>) -> Self {
        Self::with_config(controller, ExternalSortConfig::default())
    }

    /// Create with custom configuration
    pub fn with_config(controller: &Arc<MemoryController>, config: ExternalSortConfig) -> Self {
        Self {
            reservation: controller.create_reservation(),
            config,
            buffer: Vec::new(),
            buffer_memory: 0,
            runs: Vec::new(),
            comparator: SortKeyComparator,
        }
    }

    /// Add a row with its sort keys to the sort operator
    ///
    /// May trigger a spill if memory is exhausted.
    pub fn add_row(&mut self, row: Row, sort_keys: SortKey) -> io::Result<()> {
        // Estimate memory for this row
        let row_memory = row.estimated_size_bytes()
            + sort_keys.iter().map(|(v, _)| v.estimated_size_bytes()).sum::<usize>()
            + std::mem::size_of::<RowWithKeys>();

        // Check if we need to spill
        if !self.reservation.try_grow(row_memory) {
            // Memory exhausted - sort and spill current buffer
            self.spill_buffer()?;

            // Try to reserve again
            if !self.reservation.try_grow(row_memory) {
                // Still can't fit - this single row exceeds available memory
                // This shouldn't happen with reasonable memory limits
                return Err(io::Error::new(
                    io::ErrorKind::OutOfMemory,
                    "single row exceeds available memory budget",
                ));
            }
        }

        self.buffer.push((row, sort_keys));
        self.buffer_memory += row_memory;

        // Check if buffer reached max run size
        if self.buffer.len() >= self.config.max_run_size {
            self.spill_buffer()?;
        }

        Ok(())
    }

    /// Sort and spill the current buffer to disk
    fn spill_buffer(&mut self) -> io::Result<()> {
        if self.buffer.is_empty() {
            return Ok(());
        }

        // Sort the buffer with rowid tie-breaking for SQLite-compatible ordering
        let comparator = self.comparator.clone();
        self.buffer.sort_by(|a, b| comparator.compare_with_rowid(a, b));

        // Create spill file
        let temp_dir = self.reservation.temp_directory();
        let mut spill_file = SpillFile::with_suffix(temp_dir, "sort_run")?;

        // Serialize all rows to the spill file
        let row_count = self.buffer.len();
        let mut spill_buf = Vec::new();

        for (row, keys) in &self.buffer {
            serialize_row_with_keys(row, keys, &mut spill_buf)?;
        }

        spill_file.write_all(&spill_buf)?;
        spill_file.flush()?;

        // Record spill metrics
        self.reservation.record_spill(spill_buf.len());

        // Release memory
        self.reservation.shrink(self.buffer_memory);
        self.buffer.clear();
        self.buffer_memory = 0;

        // Add spilled run
        self.runs.push(SortRun::OnDisk { file: spill_file, row_count });

        Ok(())
    }

    /// Finalize sorting and return an iterator over sorted results
    ///
    /// This completes the sort phase and prepares for the merge phase.
    pub fn finish(mut self) -> io::Result<SortedIterator> {
        // Sort any remaining buffer with rowid tie-breaking for SQLite-compatible ordering
        if !self.buffer.is_empty() {
            let comparator = self.comparator.clone();
            self.buffer.sort_by(|a, b| comparator.compare_with_rowid(a, b));

            // Keep in memory if we have no spilled runs
            if self.runs.is_empty() {
                // All data fits in memory - return directly
                return Ok(SortedIterator::InMemory(InMemoryIterator::new(
                    self.buffer,
                    self.reservation,
                )));
            }

            // We have spilled runs - spill this buffer too for consistent merge
            self.spill_buffer()?;
        }

        // If no runs, return empty iterator
        if self.runs.is_empty() {
            return Ok(SortedIterator::InMemory(InMemoryIterator::new(
                Vec::new(),
                self.reservation,
            )));
        }

        // Create merge iterator over all runs
        Ok(SortedIterator::Merge(MergeIterator::new(self.runs, self.comparator, self.reservation)?))
    }

    /// Get the number of runs created so far
    pub fn run_count(&self) -> usize {
        self.runs.len() + if self.buffer.is_empty() { 0 } else { 1 }
    }

    /// Get total rows added
    pub fn total_rows(&self) -> usize {
        self.runs.iter().map(|r| r.row_count()).sum::<usize>() + self.buffer.len()
    }
}

/// Iterator over sorted results
pub enum SortedIterator {
    /// All data fit in memory
    InMemory(InMemoryIterator),

    /// Merge iterator over spilled runs
    Merge(MergeIterator),
}

impl Iterator for SortedIterator {
    type Item = io::Result<Row>;

    fn next(&mut self) -> Option<Self::Item> {
        match self {
            SortedIterator::InMemory(iter) => iter.next(),
            SortedIterator::Merge(iter) => iter.next(),
        }
    }
}

/// Iterator over in-memory sorted results
pub struct InMemoryIterator {
    rows: std::vec::IntoIter<RowWithKeys>,
    #[allow(dead_code)]
    reservation: MemoryReservation, // Keeps memory reserved until iterator is dropped
}

impl InMemoryIterator {
    fn new(rows: Vec<RowWithKeys>, reservation: MemoryReservation) -> Self {
        Self { rows: rows.into_iter(), reservation }
    }
}

impl Iterator for InMemoryIterator {
    type Item = io::Result<Row>;

    fn next(&mut self) -> Option<Self::Item> {
        self.rows.next().map(|(row, _keys)| Ok(row))
    }
}

/// K-way merge iterator over spilled sorted runs
pub struct MergeIterator {
    /// Min-heap for k-way merge
    heap: BinaryHeap<MergeEntry>,

    /// Run readers (one per spilled run)
    readers: Vec<RunReader>,

    /// Comparator for sort keys
    comparator: SortKeyComparator,

    /// Memory reservation
    #[allow(dead_code)]
    reservation: MemoryReservation,
}

/// Entry in the merge heap
struct MergeEntry {
    /// The row
    row: Row,

    /// Sort key for comparison
    key: SortKey,

    /// Index of the run this came from
    run_index: usize,

    /// Comparator reference for ordering
    comparator: SortKeyComparator,
}

impl PartialEq for MergeEntry {
    fn eq(&self, other: &Self) -> bool {
        // Compare keys first, then use rowid as tie-breaker
        let key_cmp = self.comparator.compare(&self.key, &other.key);
        if key_cmp != Ordering::Equal {
            return false;
        }
        self.row.row_id == other.row.row_id
    }
}

impl Eq for MergeEntry {}

impl PartialOrd for MergeEntry {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for MergeEntry {
    fn cmp(&self, other: &Self) -> Ordering {
        // Reverse for min-heap (BinaryHeap is a max-heap)
        // Compare keys first
        let key_cmp = self.comparator.compare(&other.key, &self.key);
        if key_cmp != Ordering::Equal {
            return key_cmp;
        }
        // Use rowid as tie-breaker for SQLite-compatible ordering (issue #4893)
        // Note: reversed because we're in a max-heap (smaller rowid = higher priority)
        other.row.row_id.cmp(&self.row.row_id)
    }
}

/// Reader for a spilled sorted run
struct RunReader {
    /// Cursor over spill file data (loaded into memory)
    cursor: Cursor<Vec<u8>>,

    /// Remaining rows to read
    remaining: usize,
}

impl RunReader {
    fn new(mut file: SpillFile, row_count: usize) -> io::Result<Self> {
        file.prepare_for_read()?;
        let data = file.read_to_vec()?;
        Ok(Self { cursor: Cursor::new(data), remaining: row_count })
    }

    fn read_next(&mut self) -> io::Result<Option<RowWithKeys>> {
        if self.remaining == 0 {
            return Ok(None);
        }
        self.remaining -= 1;
        let (row, keys) = deserialize_row_with_keys(&mut self.cursor)?;
        Ok(Some((row, keys)))
    }
}

impl MergeIterator {
    fn new(
        runs: Vec<SortRun>,
        comparator: SortKeyComparator,
        reservation: MemoryReservation,
    ) -> io::Result<Self> {
        let mut readers = Vec::with_capacity(runs.len());
        let mut heap = BinaryHeap::new();

        for (run_index, run) in runs.into_iter().enumerate() {
            match run {
                SortRun::InMemory(rows) => {
                    // Shouldn't happen in merge phase, but handle anyway
                    for (row, key) in rows {
                        heap.push(MergeEntry {
                            row,
                            key,
                            run_index,
                            comparator: comparator.clone(),
                        });
                    }
                    readers.push(RunReader { cursor: Cursor::new(Vec::new()), remaining: 0 });
                }
                SortRun::OnDisk { file, row_count } => {
                    let mut reader = RunReader::new(file, row_count)?;

                    // Read first entry from this run
                    if let Some((row, key)) = reader.read_next()? {
                        heap.push(MergeEntry {
                            row,
                            key,
                            run_index,
                            comparator: comparator.clone(),
                        });
                    }

                    readers.push(reader);
                }
            }
        }

        Ok(Self { heap, readers, comparator, reservation })
    }
}

impl Iterator for MergeIterator {
    type Item = io::Result<Row>;

    fn next(&mut self) -> Option<Self::Item> {
        // Pop smallest entry from heap
        let entry = self.heap.pop()?;

        // Read next entry from the same run
        match self.readers[entry.run_index].read_next() {
            Ok(Some((row, key))) => {
                self.heap.push(MergeEntry {
                    row,
                    key,
                    run_index: entry.run_index,
                    comparator: self.comparator.clone(),
                });
            }
            Ok(None) => {
                // This run is exhausted
            }
            Err(e) => {
                return Some(Err(e));
            }
        }

        Some(Ok(entry.row))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_test_controller() -> Arc<MemoryController> {
        Arc::new(MemoryController::with_budget(1024 * 1024)) // 1MB for tests
    }

    #[test]
    fn test_in_memory_sort() {
        let controller = make_test_controller();
        let mut sorter = ExternalSort::new(&controller);

        // Add rows in reverse order
        for i in (0..100).rev() {
            let row = Row::from_vec(vec![SqlValue::Integer(i)]);
            let keys = vec![(SqlValue::Integer(i), OrderDirection::Asc)];
            sorter.add_row(row, keys).unwrap();
        }

        // Finish and collect results
        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();

        assert_eq!(results.len(), 100);
        for (i, row) in results.iter().enumerate() {
            assert_eq!(row.values[0], SqlValue::Integer(i as i64));
        }
    }

    #[test]
    fn test_descending_sort() {
        let controller = make_test_controller();
        let mut sorter = ExternalSort::new(&controller);

        // Add rows in ascending order
        for i in 0..50 {
            let row = Row::from_vec(vec![SqlValue::Integer(i)]);
            let keys = vec![(SqlValue::Integer(i), OrderDirection::Desc)];
            sorter.add_row(row, keys).unwrap();
        }

        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();

        assert_eq!(results.len(), 50);
        // Should be in descending order: 49, 48, ..., 0
        for (i, row) in results.iter().enumerate() {
            assert_eq!(row.values[0], SqlValue::Integer(49 - i as i64));
        }
    }

    #[test]
    fn test_null_handling() {
        let controller = make_test_controller();
        let mut sorter = ExternalSort::new(&controller);

        // Add rows with NULLs
        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Integer(2)]),
                vec![(SqlValue::Integer(2), OrderDirection::Asc)],
            )
            .unwrap();
        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Null]),
                vec![(SqlValue::Null, OrderDirection::Asc)],
            )
            .unwrap();
        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Integer(1)]),
                vec![(SqlValue::Integer(1), OrderDirection::Asc)],
            )
            .unwrap();

        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();

        // ASC: 1, 2, NULL (NULLs last)
        assert_eq!(results[0].values[0], SqlValue::Integer(1));
        assert_eq!(results[1].values[0], SqlValue::Integer(2));
        assert_eq!(results[2].values[0], SqlValue::Null);
    }

    #[test]
    fn test_multi_key_sort() {
        let controller = make_test_controller();
        let mut sorter = ExternalSort::new(&controller);

        // Add rows to test multi-key sorting
        // (a=1, b=2), (a=1, b=1), (a=2, b=1)
        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(2)]),
                vec![
                    (SqlValue::Integer(1), OrderDirection::Asc),
                    (SqlValue::Integer(2), OrderDirection::Asc),
                ],
            )
            .unwrap();
        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(1)]),
                vec![
                    (SqlValue::Integer(1), OrderDirection::Asc),
                    (SqlValue::Integer(1), OrderDirection::Asc),
                ],
            )
            .unwrap();
        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Integer(2), SqlValue::Integer(1)]),
                vec![
                    (SqlValue::Integer(2), OrderDirection::Asc),
                    (SqlValue::Integer(1), OrderDirection::Asc),
                ],
            )
            .unwrap();

        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();

        // Expected order: (1,1), (1,2), (2,1)
        assert_eq!(results[0].values[0], SqlValue::Integer(1));
        assert_eq!(results[0].values[1], SqlValue::Integer(1));
        assert_eq!(results[1].values[0], SqlValue::Integer(1));
        assert_eq!(results[1].values[1], SqlValue::Integer(2));
        assert_eq!(results[2].values[0], SqlValue::Integer(2));
        assert_eq!(results[2].values[1], SqlValue::Integer(1));
    }

    #[test]
    fn test_spill_and_merge() {
        // Use very small memory to force spilling
        let controller = Arc::new(MemoryController::with_budget(4096)); // 4KB
        let config = ExternalSortConfig {
            max_run_size: 10, // Very small runs
            ..Default::default()
        };

        let mut sorter = ExternalSort::with_config(&controller, config);

        // Add enough rows to force multiple spills
        for i in (0..50).rev() {
            let row = Row::from_vec(vec![SqlValue::Integer(i)]);
            let keys = vec![(SqlValue::Integer(i), OrderDirection::Asc)];
            sorter.add_row(row, keys).unwrap();
        }

        // Should have created multiple runs
        assert!(sorter.run_count() > 1, "Expected multiple runs from spilling");

        // Finish and verify sorted order
        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();

        assert_eq!(results.len(), 50);
        for (i, row) in results.iter().enumerate() {
            assert_eq!(row.values[0], SqlValue::Integer(i as i64));
        }
    }

    #[test]
    fn test_empty_sort() {
        let controller = make_test_controller();
        let sorter = ExternalSort::new(&controller);

        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();
        assert!(results.is_empty());
    }

    #[test]
    fn test_string_sort() {
        let controller = make_test_controller();
        let mut sorter = ExternalSort::new(&controller);

        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Varchar("charlie".into())]),
                vec![(SqlValue::Varchar("charlie".into()), OrderDirection::Asc)],
            )
            .unwrap();
        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Varchar("alpha".into())]),
                vec![(SqlValue::Varchar("alpha".into()), OrderDirection::Asc)],
            )
            .unwrap();
        sorter
            .add_row(
                Row::from_vec(vec![SqlValue::Varchar("bravo".into())]),
                vec![(SqlValue::Varchar("bravo".into()), OrderDirection::Asc)],
            )
            .unwrap();

        let results: Vec<Row> = sorter.finish().unwrap().map(|r| r.unwrap()).collect();

        assert_eq!(results[0].values[0], SqlValue::Varchar("alpha".into()));
        assert_eq!(results[1].values[0], SqlValue::Varchar("bravo".into()));
        assert_eq!(results[2].values[0], SqlValue::Varchar("charlie".into()));
    }
}