#[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::select::parallel::ParallelConfig;
use crate::{
errors::ExecutorError,
timeout::{TimeoutContext, CHECK_INTERVAL},
};
#[cfg(feature = "parallel")]
const PARALLEL_MERGE_THRESHOLD: usize = 4;
#[cfg(feature = "parallel")]
const MORSEL_BUILD_DEBUG_ENV: &str = "MORSEL_BUILD_DEBUG";
#[cfg(feature = "parallel")]
fn morsel_build_debug_enabled() -> bool {
std::env::var(MORSEL_BUILD_DEBUG_ENV).is_ok()
}
#[cfg(feature = "parallel")]
fn parallel_merge_composite(
partial_tables: Vec<AHashMap<CompositeKey, Vec<usize>>>,
) -> AHashMap<CompositeKey, Vec<usize>> {
if partial_tables.is_empty() {
return AHashMap::new();
}
if partial_tables.len() < PARALLEL_MERGE_THRESHOLD {
return partial_tables.into_iter().fold(AHashMap::new(), |mut acc, partial| {
for (key, mut indices) in partial {
acc.entry(key).or_default().append(&mut indices);
}
acc
});
}
partial_tables.into_par_iter().reduce(AHashMap::new, |mut acc, mut partial| {
if partial.len() > acc.len() {
for (key, mut indices) in acc {
partial.entry(key).or_default().append(&mut indices);
}
partial
} else {
for (key, mut indices) in partial {
acc.entry(key).or_default().append(&mut indices);
}
acc
}
})
}
#[cfg(feature = "parallel")]
fn parallel_merge_single_key(
partial_tables: Vec<AHashMap<vibesql_types::SqlValue, Vec<usize>>>,
) -> AHashMap<vibesql_types::SqlValue, Vec<usize>> {
if partial_tables.is_empty() {
return AHashMap::new();
}
if partial_tables.len() < PARALLEL_MERGE_THRESHOLD {
return partial_tables.into_iter().fold(AHashMap::new(), |mut acc, partial| {
for (key, mut indices) in partial {
acc.entry(key).or_default().append(&mut indices);
}
acc
});
}
partial_tables.into_par_iter().reduce(AHashMap::new, |mut acc, mut partial| {
if partial.len() > acc.len() {
for (key, mut indices) in acc {
partial.entry(key).or_default().append(&mut indices);
}
partial
} else {
for (key, mut indices) in partial {
acc.entry(key).or_default().append(&mut indices);
}
acc
}
})
}
#[cfg(feature = "parallel")]
fn parallel_merge_existence(
partial_tables: Vec<AHashMap<vibesql_types::SqlValue, ()>>,
) -> AHashMap<vibesql_types::SqlValue, ()> {
if partial_tables.is_empty() {
return AHashMap::new();
}
if partial_tables.len() < PARALLEL_MERGE_THRESHOLD {
return partial_tables.into_iter().fold(AHashMap::new(), |mut acc, partial| {
for (key, _) in partial {
acc.insert(key, ());
}
acc
});
}
partial_tables.into_par_iter().reduce(AHashMap::new, |mut acc, mut partial| {
if partial.len() > acc.len() {
for (key, _) in acc {
partial.insert(key, ());
}
partial
} else {
for (key, _) in partial {
acc.insert(key, ());
}
acc
}
})
}
#[cfg(feature = "parallel")]
fn create_build_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,
}
})
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CompositeKey(pub Vec<vibesql_types::SqlValue>);
impl CompositeKey {
#[inline]
pub fn from_row(row: &vibesql_storage::Row, col_indices: &[usize]) -> Self {
let values: Vec<vibesql_types::SqlValue> =
col_indices.iter().map(|&idx| row.values[idx].clone()).collect();
CompositeKey(values)
}
#[inline]
pub fn has_null(&self) -> bool {
self.0.iter().any(|v| v == &vibesql_types::SqlValue::Null)
}
}
pub(crate) fn build_hash_table_composite_sequential(
build_rows: &[vibesql_storage::Row],
build_col_indices: &[usize],
) -> AHashMap<CompositeKey, Vec<usize>> {
let mut hash_table: AHashMap<CompositeKey, Vec<usize>> =
AHashMap::with_capacity(build_rows.len());
for (idx, row) in build_rows.iter().enumerate() {
let key = CompositeKey::from_row(row, build_col_indices);
if !key.has_null() {
hash_table.entry(key).or_default().push(idx);
}
}
hash_table
}
#[cfg(feature = "parallel")]
#[allow(clippy::type_complexity)]
pub(crate) fn build_hash_table_composite_parallel(
build_rows: &[vibesql_storage::Row],
build_col_indices: &[usize],
) -> AHashMap<CompositeKey, Vec<usize>> {
let parallel_config = ParallelConfig::global();
let morsel_config = global_config();
let morsel_size = morsel_config.join_build_size;
if !parallel_config.should_parallelize_join(build_rows.len()) {
return build_hash_table_composite_sequential(build_rows, build_col_indices);
}
if build_rows.len() < morsel_size {
return build_hash_table_composite_sequential(build_rows, build_col_indices);
}
let morsels = create_build_morsels(build_rows.len(), morsel_size);
let morsel_count = morsels.len();
if morsel_build_debug_enabled() {
eprintln!(
"[MORSEL_BUILD] Composite: {} morsels for {} rows (size={})",
morsel_count,
build_rows.len(),
morsel_size
);
}
let injector: Injector<Morsel> = Injector::new();
for morsel in morsels {
injector.push(morsel);
}
let results: Arc<std::sync::Mutex<Vec<AHashMap<CompositeKey, Vec<usize>>>>> =
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 mut local_table: AHashMap<CompositeKey, Vec<usize>> = AHashMap::new();
while let Some(m) = steal_morsel(injector_ref, &worker) {
let base_idx = m.start_idx();
let morsel_rows = &build_rows[m.start_idx()..m.end_idx()];
for (i, row) in morsel_rows.iter().enumerate() {
let key = CompositeKey::from_row(row, build_col_indices);
if !key.has_null() {
local_table.entry(key).or_default().push(base_idx + i);
}
}
}
if !local_table.is_empty() {
results_ref.lock().unwrap().push(local_table);
}
});
}
});
let partial_tables = Arc::try_unwrap(results)
.expect("all threads should have completed")
.into_inner()
.expect("mutex not poisoned");
if morsel_build_debug_enabled() {
eprintln!(
"[MORSEL_BUILD] Composite complete: {} partial tables to merge",
partial_tables.len()
);
}
parallel_merge_composite(partial_tables)
}
#[cfg(not(feature = "parallel"))]
pub(crate) fn build_hash_table_composite_parallel(
build_rows: &[vibesql_storage::Row],
build_col_indices: &[usize],
) -> AHashMap<CompositeKey, Vec<usize>> {
build_hash_table_composite_sequential(build_rows, build_col_indices)
}
#[allow(dead_code)]
pub(super) fn build_hash_table_sequential(
build_rows: &[vibesql_storage::Row],
build_col_idx: usize,
) -> AHashMap<vibesql_types::SqlValue, Vec<usize>> {
let mut hash_table: AHashMap<vibesql_types::SqlValue, Vec<usize>> = AHashMap::new();
for (idx, row) in build_rows.iter().enumerate() {
let key = row.values[build_col_idx].clone();
if key != vibesql_types::SqlValue::Null {
hash_table.entry(key).or_default().push(idx);
}
}
hash_table
}
#[allow(dead_code)]
#[allow(clippy::type_complexity)]
pub(crate) fn build_hash_table_parallel(
build_rows: &[vibesql_storage::Row],
build_col_idx: usize,
) -> AHashMap<vibesql_types::SqlValue, Vec<usize>> {
#[cfg(feature = "parallel")]
{
let parallel_config = ParallelConfig::global();
let morsel_config = global_config();
let morsel_size = morsel_config.join_build_size;
if !parallel_config.should_parallelize_join(build_rows.len()) {
return build_hash_table_sequential(build_rows, build_col_idx);
}
if build_rows.len() < morsel_size {
return build_hash_table_sequential(build_rows, build_col_idx);
}
let morsels = create_build_morsels(build_rows.len(), morsel_size);
let morsel_count = morsels.len();
if morsel_build_debug_enabled() {
eprintln!(
"[MORSEL_BUILD] Single-key: {} morsels for {} rows (size={})",
morsel_count,
build_rows.len(),
morsel_size
);
}
let injector: Injector<Morsel> = Injector::new();
for morsel in morsels {
injector.push(morsel);
}
let results: Arc<std::sync::Mutex<Vec<AHashMap<vibesql_types::SqlValue, Vec<usize>>>>> =
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 mut local_table: AHashMap<vibesql_types::SqlValue, Vec<usize>> =
AHashMap::new();
while let Some(m) = steal_morsel(injector_ref, &worker) {
let base_idx = m.start_idx();
let morsel_rows = &build_rows[m.start_idx()..m.end_idx()];
for (i, row) in morsel_rows.iter().enumerate() {
let key = row.values[build_col_idx].clone();
if key != vibesql_types::SqlValue::Null {
local_table.entry(key).or_default().push(base_idx + i);
}
}
}
if !local_table.is_empty() {
results_ref.lock().unwrap().push(local_table);
}
});
}
});
let partial_tables = Arc::try_unwrap(results)
.expect("all threads should have completed")
.into_inner()
.expect("mutex not poisoned");
if morsel_build_debug_enabled() {
eprintln!(
"[MORSEL_BUILD] Single-key complete: {} partial tables to merge",
partial_tables.len()
);
}
parallel_merge_single_key(partial_tables)
}
#[cfg(not(feature = "parallel"))]
{
build_hash_table_sequential(build_rows, build_col_idx)
}
}
pub(crate) fn build_existence_hash_table_sequential(
build_rows: &[vibesql_storage::Row],
build_col_idx: usize,
timeout_ctx: &TimeoutContext,
) -> Result<AHashMap<vibesql_types::SqlValue, ()>, ExecutorError> {
let mut hash_table: AHashMap<vibesql_types::SqlValue, ()> = AHashMap::new();
for (idx, row) in build_rows.iter().enumerate() {
if idx % CHECK_INTERVAL == 0 {
timeout_ctx.check()?;
}
let key = row.values[build_col_idx].clone();
if key != vibesql_types::SqlValue::Null {
hash_table.insert(key, ());
}
}
Ok(hash_table)
}
pub(crate) fn build_existence_hash_table_parallel(
build_rows: &[vibesql_storage::Row],
build_col_idx: usize,
timeout_ctx: &TimeoutContext,
) -> Result<AHashMap<vibesql_types::SqlValue, ()>, ExecutorError> {
#[cfg(feature = "parallel")]
{
let config = ParallelConfig::global();
if !config.should_parallelize_join(build_rows.len()) {
return build_existence_hash_table_sequential(build_rows, build_col_idx, timeout_ctx);
}
timeout_ctx.check()?;
let chunk_size = (build_rows.len() / config.num_threads).max(1000);
let partial_tables: Vec<AHashMap<_, ()>> = build_rows
.par_chunks(chunk_size)
.map(|chunk| {
let mut local_table: AHashMap<vibesql_types::SqlValue, ()> = AHashMap::new();
for row in chunk.iter() {
let key = row.values[build_col_idx].clone();
if key != vibesql_types::SqlValue::Null {
local_table.insert(key, ());
}
}
local_table
})
.collect();
timeout_ctx.check()?;
Ok(parallel_merge_existence(partial_tables))
}
#[cfg(not(feature = "parallel"))]
{
build_existence_hash_table_sequential(build_rows, build_col_idx, timeout_ctx)
}
}