#[cfg(feature = "parallel")]
use std::sync::Arc;
use ahash::AHashMap;
#[cfg(feature = "parallel")]
use crossbeam_deque::{Injector, Steal, Worker};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[cfg(feature = "parallel")]
use crate::select::morsel::{global_config, Morsel};
#[cfg(feature = "parallel")]
use crate::timeout::TimeoutContext;
pub type GroupedRows = Vec<(Vec<vibesql_types::SqlValue>, Vec<vibesql_storage::Row>)>;
pub fn group_rows<'a>(
rows: &[vibesql_storage::Row],
group_by_exprs: &[vibesql_ast::Expression],
evaluator: &crate::evaluator::CombinedExpressionEvaluator,
executor: &crate::SelectExecutor<'a>,
) -> Result<GroupedRows, crate::errors::ExecutorError> {
#[cfg(feature = "parallel")]
{
use crate::select::parallel::ParallelConfig;
let config = ParallelConfig::global();
if config.should_parallelize_aggregate(rows.len()) {
let components = evaluator.get_parallel_components();
let timeout_ctx = TimeoutContext::from_executor(executor);
return group_rows_parallel(rows, group_by_exprs, components, &timeout_ctx);
}
}
group_rows_sequential(rows, group_by_exprs, evaluator, executor)
}
fn group_rows_sequential<'a>(
rows: &[vibesql_storage::Row],
group_by_exprs: &[vibesql_ast::Expression],
evaluator: &crate::evaluator::CombinedExpressionEvaluator,
executor: &crate::SelectExecutor<'a>,
) -> Result<GroupedRows, crate::errors::ExecutorError> {
let estimated_groups = (rows.len() / 10).max(16);
let mut groups_map: AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>> =
AHashMap::with_capacity(estimated_groups);
let mut rows_processed = 0;
const CHECK_INTERVAL: usize = 1000;
for (idx, row) in rows.iter().enumerate() {
rows_processed += 1;
if rows_processed % CHECK_INTERVAL == 0 {
executor.check_timeout()?;
}
evaluator.clear_cse_cache();
let mut key = Vec::with_capacity(group_by_exprs.len());
for expr in group_by_exprs {
let value = evaluator.eval(expr, row)?;
key.push(value);
}
groups_map.entry(key).or_default().push(idx);
}
let group_count = groups_map.len();
let mut result: GroupedRows = Vec::with_capacity(group_count);
for (key, indices) in groups_map {
let mut group_rows: Vec<vibesql_storage::Row> = Vec::with_capacity(indices.len());
for idx in indices {
group_rows.push(rows[idx].clone());
}
result.push((key, group_rows));
}
result.sort_by(|a, b| a.0.cmp(&b.0));
Ok(result)
}
#[cfg(feature = "parallel")]
#[allow(clippy::type_complexity)] fn group_rows_parallel<'a>(
rows: &[vibesql_storage::Row],
group_by_exprs: &[vibesql_ast::Expression],
components: crate::evaluator::parallel::ParallelComponents<'a>,
timeout_ctx: &TimeoutContext,
) -> Result<GroupedRows, crate::errors::ExecutorError> {
use crate::evaluator::CombinedExpressionEvaluator;
let (schema, database, outer_row, outer_schema, window_mapping, cte_context, enable_cse) =
components;
timeout_ctx.check()?;
let config = global_config();
let morsel_size = config.group_by_size;
if rows.len() < morsel_size {
return group_rows_parallel_simple(rows, group_by_exprs, components, timeout_ctx);
}
let morsels = create_morsels(rows.len(), morsel_size);
let morsel_count = morsels.len();
if morsel_debug_enabled() {
eprintln!(
"[MORSEL] GROUP BY: {} morsels for {} rows (size={})",
morsel_count,
rows.len(),
morsel_size
);
}
let injector: Injector<Morsel> = Injector::new();
for morsel in morsels {
injector.push(morsel);
}
let results: Arc<
std::sync::Mutex<
Vec<
Result<
AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>>,
crate::errors::ExecutorError,
>,
>,
>,
> = Arc::new(std::sync::Mutex::new(Vec::with_capacity(morsel_count)));
rayon::scope(|s| {
let num_threads = rayon::current_num_threads();
for _ in 0..num_threads {
let injector_ref = &injector;
let results_ref = results.clone();
s.spawn(move |_| {
let worker: Worker<Morsel> = Worker::new_fifo();
let evaluator = CombinedExpressionEvaluator::from_parallel_components(
schema,
database,
outer_row,
outer_schema,
window_mapping,
cte_context,
enable_cse,
);
let estimated_groups = (morsel_size / 10).max(16);
let mut local_groups: AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>> =
AHashMap::with_capacity(estimated_groups);
while let Some(m) = steal_morsel(injector_ref, &worker) {
let morsel_start = m.start_idx();
let morsel_rows = &rows[morsel_start..m.end_idx()];
let result: Result<(), crate::errors::ExecutorError> = (|| {
for (local_idx, row) in morsel_rows.iter().enumerate() {
evaluator.clear_cse_cache();
let mut key = Vec::with_capacity(group_by_exprs.len());
for expr in group_by_exprs {
let value = evaluator.eval(expr, row)?;
key.push(value);
}
let absolute_idx = morsel_start + local_idx;
local_groups.entry(key).or_default().push(absolute_idx);
}
Ok(())
})();
if let Err(e) = result {
results_ref.lock().unwrap().push(Err(e));
return;
}
}
if !local_groups.is_empty() {
results_ref.lock().unwrap().push(Ok(local_groups));
}
});
}
});
timeout_ctx.check()?;
let thread_results = Arc::try_unwrap(results)
.expect("all threads should have completed")
.into_inner()
.expect("mutex not poisoned");
if morsel_debug_enabled() {
eprintln!(
"[MORSEL] GROUP BY complete: {} morsels, {} thread results",
morsel_count,
thread_results.len()
);
}
let mut validated_results: Vec<AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>>> =
Vec::with_capacity(thread_results.len());
for result in thread_results {
validated_results.push(result?);
}
let mut iter = validated_results.into_iter();
let mut global_groups: AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>> =
iter.next().unwrap_or_default();
for local_groups in iter {
for (key, mut local_indices) in local_groups {
global_groups.entry(key).or_default().append(&mut local_indices);
}
}
let group_count = global_groups.len();
let mut result: GroupedRows = Vec::with_capacity(group_count);
for (key, indices) in global_groups {
let mut group_rows: Vec<vibesql_storage::Row> = Vec::with_capacity(indices.len());
for idx in indices {
group_rows.push(rows[idx].clone());
}
result.push((key, group_rows));
}
result.sort_by(|a, b| a.0.cmp(&b.0));
Ok(result)
}
#[cfg(feature = "parallel")]
#[allow(clippy::type_complexity)]
fn group_rows_parallel_simple<'a>(
rows: &[vibesql_storage::Row],
group_by_exprs: &[vibesql_ast::Expression],
components: crate::evaluator::parallel::ParallelComponents<'a>,
timeout_ctx: &TimeoutContext,
) -> Result<GroupedRows, crate::errors::ExecutorError> {
use crate::evaluator::CombinedExpressionEvaluator;
let (schema, database, outer_row, outer_schema, window_mapping, cte_context, enable_cse) =
components;
let num_threads = rayon::current_num_threads();
let chunk_size = rows.len().div_ceil(num_threads);
let thread_results: Vec<
Result<
(usize, AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>>),
crate::errors::ExecutorError,
>,
> = rows
.par_chunks(chunk_size.max(1))
.enumerate()
.map(|(chunk_idx, chunk)| {
let chunk_start = chunk_idx * chunk_size;
let evaluator = CombinedExpressionEvaluator::from_parallel_components(
schema,
database,
outer_row,
outer_schema,
window_mapping,
cte_context,
enable_cse,
);
let estimated_groups = (chunk.len() / 10).max(16);
let mut local_groups: AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>> =
AHashMap::with_capacity(estimated_groups);
for (local_idx, row) in chunk.iter().enumerate() {
evaluator.clear_cse_cache();
let mut key = Vec::with_capacity(group_by_exprs.len());
for expr in group_by_exprs {
let value = evaluator.eval(expr, row)?;
key.push(value);
}
let absolute_idx = chunk_start + local_idx;
local_groups.entry(key).or_default().push(absolute_idx);
}
Ok((chunk_start, local_groups))
})
.collect();
timeout_ctx.check()?;
let mut validated_results: Vec<AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>>> =
Vec::with_capacity(thread_results.len());
for result in thread_results {
let (_, local_groups) = result?;
validated_results.push(local_groups);
}
let mut iter = validated_results.into_iter();
let mut global_groups: AHashMap<Vec<vibesql_types::SqlValue>, Vec<usize>> =
iter.next().unwrap_or_default();
for local_groups in iter {
for (key, mut local_indices) in local_groups {
global_groups.entry(key).or_default().append(&mut local_indices);
}
}
let group_count = global_groups.len();
let mut result: GroupedRows = Vec::with_capacity(group_count);
for (key, indices) in global_groups {
let mut group_rows: Vec<vibesql_storage::Row> = Vec::with_capacity(indices.len());
for idx in indices {
group_rows.push(rows[idx].clone());
}
result.push((key, group_rows));
}
result.sort_by(|a, b| a.0.cmp(&b.0));
Ok(result)
}
#[cfg(feature = "parallel")]
fn create_morsels(total_rows: usize, morsel_size: usize) -> Vec<Morsel> {
let mut morsels = Vec::with_capacity(total_rows.div_ceil(morsel_size));
let mut start = 0;
while start < total_rows {
let count = (total_rows - start).min(morsel_size);
morsels.push(Morsel::new(start, count));
start += count;
}
morsels
}
#[cfg(feature = "parallel")]
fn steal_morsel(injector: &Injector<Morsel>, worker: &Worker<Morsel>) -> Option<Morsel> {
worker.pop().or_else(|| {
loop {
match injector.steal() {
Steal::Success(m) => return Some(m),
Steal::Empty => return None,
Steal::Retry => continue,
}
}
})
}
#[cfg(feature = "parallel")]
fn morsel_debug_enabled() -> bool {
std::env::var("MORSEL_DEBUG").is_ok()
}