#[cfg(feature = "parallel")]
use std::sync::{Arc, Mutex};
#[cfg(feature = "parallel")]
use crossbeam_deque::{Injector, Steal, Worker};
#[cfg(feature = "parallel")]
use super::super::combine_rows;
#[cfg(feature = "parallel")]
use super::condition::eval_join_condition_to_bool;
#[cfg(feature = "parallel")]
use crate::select::morsel::{Morsel, MorselConfig};
#[cfg(feature = "parallel")]
use crate::{
errors::ExecutorError, evaluator::CombinedExpressionEvaluator, schema::CombinedSchema,
timeout::TimeoutContext, timeout::CHECK_INTERVAL,
};
#[cfg(feature = "parallel")]
const NESTED_LOOP_DEBUG_ENV: &str = "NESTED_LOOP_DEBUG";
#[cfg(feature = "parallel")]
fn nested_loop_debug_enabled() -> bool {
std::env::var(NESTED_LOOP_DEBUG_ENV).is_ok()
}
#[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")]
#[allow(clippy::too_many_arguments)]
pub fn execute_nested_loop_parallel(
outer_rows: &[vibesql_storage::Row],
inner_rows: &[vibesql_storage::Row],
condition: &Option<vibesql_ast::Expression>,
schema: &CombinedSchema,
database: &vibesql_storage::Database,
timeout_ctx: &TimeoutContext,
) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
let config = MorselConfig::optimal();
let morsels = create_morsels(outer_rows.len(), config.morsel_size);
let morsel_count = morsels.len();
if nested_loop_debug_enabled() {
eprintln!(
"[NESTED_LOOP] Parallel join: {} outer rows, {} inner rows, {} morsels (size={})",
outer_rows.len(),
inner_rows.len(),
morsel_count,
config.morsel_size
);
}
let injector: Injector<Morsel> = Injector::new();
for morsel in morsels {
injector.push(morsel);
}
type MorselResults = Arc<Mutex<Vec<(usize, Vec<vibesql_storage::Row>)>>>;
let results: MorselResults = Arc::new(Mutex::new(Vec::with_capacity(morsel_count)));
let error: Arc<Mutex<Option<ExecutorError>>> = Arc::new(Mutex::new(None));
rayon::scope(|s| {
let num_threads = rayon::current_num_threads();
for _ in 0..num_threads {
let injector_ref = &injector;
let results_ref = results.clone();
let error_ref = error.clone();
s.spawn(move |_| {
let worker: Worker<Morsel> = Worker::new_fifo();
let evaluator = CombinedExpressionEvaluator::with_database(schema, database);
while let Some(m) = steal_morsel(injector_ref, &worker) {
if error_ref.lock().unwrap().is_some() {
break;
}
let start_idx = m.start_idx();
let morsel_rows = m.rows(outer_rows);
let mut local_results = Vec::new();
let mut iterations = 0;
'outer: for outer_row in morsel_rows {
for inner_row in inner_rows {
iterations += 1;
if iterations % CHECK_INTERVAL == 0 {
if let Err(e) = timeout_ctx.check() {
*error_ref.lock().unwrap() = Some(e);
break 'outer;
}
}
let combined_row = combine_rows(outer_row, inner_row);
let matches = match condition {
None => true, Some(cond) => {
evaluator.clear_cse_cache();
match evaluator.eval(cond, &combined_row) {
Ok(value) => {
match eval_join_condition_to_bool(value) {
Ok(b) => b,
Err(e) => {
*error_ref.lock().unwrap() = Some(e);
break 'outer;
}
}
}
Err(e) => {
*error_ref.lock().unwrap() = Some(e);
break 'outer;
}
}
}
};
if matches {
local_results.push(combined_row);
}
}
}
if nested_loop_debug_enabled() {
eprintln!(
"[NESTED_LOOP] Thread processed morsel at {} ({} outer rows -> {} results)",
start_idx,
morsel_rows.len(),
local_results.len()
);
}
results_ref.lock().unwrap().push((start_idx, local_results));
}
});
}
});
if let Some(e) = error.lock().unwrap().take() {
return Err(e);
}
let mut sorted_results = Arc::try_unwrap(results)
.expect("all threads should have completed")
.into_inner()
.expect("mutex not poisoned");
sorted_results.sort_by_key(|(start_idx, _)| *start_idx);
let total_rows: usize = sorted_results.iter().map(|(_, r)| r.len()).sum();
let mut final_results = Vec::with_capacity(total_rows);
for (_, result) in sorted_results {
final_results.extend(result);
}
if nested_loop_debug_enabled() {
eprintln!(
"[NESTED_LOOP] Parallel join complete: {} morsels, {} result rows",
morsel_count,
final_results.len()
);
}
Ok(final_results)
}