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,
};
pub type SortKey = Vec<(SqlValue, OrderDirection)>;
pub type RowWithKeys = (Row, SortKey);
#[derive(Debug, Clone)]
pub struct ExternalSortConfig {
pub max_run_size: usize,
pub merge_fanout: usize,
}
impl Default for ExternalSortConfig {
fn default() -> Self {
Self {
max_run_size: 50_000, merge_fanout: 16, }
}
}
enum SortRun {
#[allow(dead_code)]
InMemory(Vec<RowWithKeys>),
OnDisk { file: SpillFile, row_count: usize },
}
impl SortRun {
fn row_count(&self) -> usize {
match self {
SortRun::InMemory(rows) => rows.len(),
SortRun::OnDisk { row_count, .. } => *row_count,
}
}
}
pub struct ExternalSort {
reservation: MemoryReservation,
config: ExternalSortConfig,
buffer: Vec<RowWithKeys>,
buffer_memory: usize,
runs: Vec<SortRun>,
comparator: SortKeyComparator,
}
#[derive(Clone)]
struct SortKeyComparator;
impl SortKeyComparator {
fn compare(&self, a: &SortKey, b: &SortKey) -> Ordering {
for ((val_a, dir), (val_b, _)) in a.iter().zip(b.iter()) {
let cmp = match (val_a.is_null(), val_b.is_null()) {
(true, true) => Ordering::Equal,
(true, false) => return Ordering::Greater, (false, true) => return Ordering::Less, (false, false) => {
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
}
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;
}
a.0.row_id.cmp(&b.0.row_id)
}
}
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, }
}
impl ExternalSort {
pub fn new(controller: &Arc<MemoryController>) -> Self {
Self::with_config(controller, ExternalSortConfig::default())
}
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,
}
}
pub fn add_row(&mut self, row: Row, sort_keys: SortKey) -> io::Result<()> {
let row_memory = row.estimated_size_bytes()
+ sort_keys.iter().map(|(v, _)| v.estimated_size_bytes()).sum::<usize>()
+ std::mem::size_of::<RowWithKeys>();
if !self.reservation.try_grow(row_memory) {
self.spill_buffer()?;
if !self.reservation.try_grow(row_memory) {
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;
if self.buffer.len() >= self.config.max_run_size {
self.spill_buffer()?;
}
Ok(())
}
fn spill_buffer(&mut self) -> io::Result<()> {
if self.buffer.is_empty() {
return Ok(());
}
let comparator = self.comparator.clone();
self.buffer.sort_by(|a, b| comparator.compare_with_rowid(a, b));
let temp_dir = self.reservation.temp_directory();
let mut spill_file = SpillFile::with_suffix(temp_dir, "sort_run")?;
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()?;
self.reservation.record_spill(spill_buf.len());
self.reservation.shrink(self.buffer_memory);
self.buffer.clear();
self.buffer_memory = 0;
self.runs.push(SortRun::OnDisk { file: spill_file, row_count });
Ok(())
}
pub fn finish(mut self) -> io::Result<SortedIterator> {
if !self.buffer.is_empty() {
let comparator = self.comparator.clone();
self.buffer.sort_by(|a, b| comparator.compare_with_rowid(a, b));
if self.runs.is_empty() {
return Ok(SortedIterator::InMemory(InMemoryIterator::new(
self.buffer,
self.reservation,
)));
}
self.spill_buffer()?;
}
if self.runs.is_empty() {
return Ok(SortedIterator::InMemory(InMemoryIterator::new(
Vec::new(),
self.reservation,
)));
}
Ok(SortedIterator::Merge(MergeIterator::new(self.runs, self.comparator, self.reservation)?))
}
pub fn run_count(&self) -> usize {
self.runs.len() + if self.buffer.is_empty() { 0 } else { 1 }
}
pub fn total_rows(&self) -> usize {
self.runs.iter().map(|r| r.row_count()).sum::<usize>() + self.buffer.len()
}
}
pub enum SortedIterator {
InMemory(InMemoryIterator),
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(),
}
}
}
pub struct InMemoryIterator {
rows: std::vec::IntoIter<RowWithKeys>,
#[allow(dead_code)]
reservation: MemoryReservation, }
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))
}
}
pub struct MergeIterator {
heap: BinaryHeap<MergeEntry>,
readers: Vec<RunReader>,
comparator: SortKeyComparator,
#[allow(dead_code)]
reservation: MemoryReservation,
}
struct MergeEntry {
row: Row,
key: SortKey,
run_index: usize,
comparator: SortKeyComparator,
}
impl PartialEq for MergeEntry {
fn eq(&self, other: &Self) -> bool {
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 {
let key_cmp = self.comparator.compare(&other.key, &self.key);
if key_cmp != Ordering::Equal {
return key_cmp;
}
other.row.row_id.cmp(&self.row.row_id)
}
}
struct RunReader {
cursor: Cursor<Vec<u8>>,
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) => {
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)?;
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> {
let entry = self.heap.pop()?;
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) => {
}
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)) }
#[test]
fn test_in_memory_sort() {
let controller = make_test_controller();
let mut sorter = ExternalSort::new(&controller);
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();
}
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);
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);
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);
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();
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);
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();
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() {
let controller = Arc::new(MemoryController::with_budget(4096)); let config = ExternalSortConfig {
max_run_size: 10, ..Default::default()
};
let mut sorter = ExternalSort::with_config(&controller, config);
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();
}
assert!(sorter.run_count() > 1, "Expected multiple runs from spilling");
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()));
}
}