#![allow(clippy::doc_lazy_continuation)]
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use super::{
batch_combine_rows_with_table_names,
build::{build_hash_table_composite_parallel, build_hash_table_parallel, CompositeKey},
columnar::{
hash_join_indices_columnar, hash_join_indices_columnar_multi,
hash_join_indices_columnar_str,
},
FromResult,
};
#[cfg(feature = "parallel")]
use crate::select::parallel::ParallelConfig;
use crate::{errors::ExecutorError, schema::CombinedSchema};
fn is_potential_cross_type_match(key: &vibesql_types::SqlValue) -> bool {
use vibesql_types::SqlValue;
match key {
SqlValue::Varchar(s) | SqlValue::Character(s) => s.trim().parse::<f64>().is_ok(),
SqlValue::Integer(_)
| SqlValue::Bigint(_)
| SqlValue::Smallint(_)
| SqlValue::Real(_)
| SqlValue::Double(_)
| SqlValue::Float(_)
| SqlValue::Numeric(_) => true,
_ => false,
}
}
pub(in crate::select::join) fn hash_join_inner(
left: FromResult,
right: FromResult,
left_col_idx: usize,
right_col_idx: usize,
) -> Result<FromResult, ExecutorError> {
let left_table_names = left.schema.table_names();
let right_table_names = right.schema.table_names();
let combined_schema = CombinedSchema::merge(left.schema.clone(), right.schema.clone());
let left_slice = left.as_slice();
let right_slice = right.as_slice();
let (
build_rows,
probe_rows,
build_col_idx,
probe_col_idx,
left_is_build,
build_table_names,
probe_table_names,
) = if left_slice.len() <= right_slice.len() {
(
left_slice,
right_slice,
left_col_idx,
right_col_idx,
true,
&left_table_names,
&right_table_names,
)
} else {
(
right_slice,
left_slice,
right_col_idx,
left_col_idx,
false,
&right_table_names,
&left_table_names,
)
};
let join_pairs: Vec<(usize, usize)> = if let Some(pairs) =
hash_join_indices_columnar(build_rows, probe_rows, build_col_idx, probe_col_idx)
{
pairs
} else if let Some(pairs) =
hash_join_indices_columnar_str(build_rows, probe_rows, build_col_idx, probe_col_idx)
{
pairs
} else {
let hash_table = build_hash_table_parallel(build_rows, build_col_idx);
#[cfg(feature = "parallel")]
{
use crate::select::morsel::{morsel_parallel_probe_sqlvalue, MorselConfig};
let config = ParallelConfig::global();
if config.should_parallelize_join(probe_rows.len()) {
let morsel_config = MorselConfig::optimal();
let pairs = morsel_parallel_probe_sqlvalue(
probe_rows,
probe_col_idx,
&hash_table,
&morsel_config,
);
return Ok(FromResult::from_rows(
combined_schema,
batch_combine_rows_with_table_names(
build_rows,
probe_rows,
&pairs,
left_is_build,
build_table_names,
probe_table_names,
),
));
}
}
let estimated_capacity = probe_rows.len().saturating_mul(2).min(100_000);
let mut pairs: Vec<(usize, usize)> = Vec::with_capacity(estimated_capacity);
for (probe_idx, probe_row) in probe_rows.iter().enumerate() {
let key = &probe_row.values[probe_col_idx];
if key == &vibesql_types::SqlValue::Null {
continue;
}
if let Some(build_indices) = hash_table.get(key) {
for &build_idx in build_indices {
pairs.push((build_idx, probe_idx));
}
} else if is_potential_cross_type_match(key) {
for (build_key, build_indices) in hash_table.iter() {
if crate::evaluator::values_are_equal(key, build_key) {
for &build_idx in build_indices {
pairs.push((build_idx, probe_idx));
}
break; }
}
}
}
pairs
};
let result_rows = batch_combine_rows_with_table_names(
build_rows,
probe_rows,
&join_pairs,
left_is_build,
build_table_names,
probe_table_names,
);
Ok(FromResult::from_rows(combined_schema, result_rows))
}
pub(in crate::select::join) fn hash_join_inner_multi(
mut left: FromResult,
mut right: FromResult,
left_col_indices: &[usize],
right_col_indices: &[usize],
) -> Result<FromResult, ExecutorError> {
let left_table_names = left.schema.table_names();
let right_table_names = right.schema.table_names();
let combined_schema = CombinedSchema::merge(left.schema.clone(), right.schema.clone());
let (
build_rows,
probe_rows,
build_col_indices,
probe_col_indices,
left_is_build,
build_table_names,
probe_table_names,
) = if left.rows().len() <= right.rows().len() {
(
left.rows(),
right.rows(),
left_col_indices,
right_col_indices,
true,
&left_table_names,
&right_table_names,
)
} else {
(
right.rows(),
left.rows(),
right_col_indices,
left_col_indices,
false,
&right_table_names,
&left_table_names,
)
};
let join_pairs: Vec<(usize, usize)> = if let Some(pairs) = hash_join_indices_columnar_multi(
build_rows,
probe_rows,
build_col_indices,
probe_col_indices,
) {
pairs
} else {
let hash_table = build_hash_table_composite_parallel(build_rows, build_col_indices);
#[cfg(feature = "parallel")]
{
let config = ParallelConfig::global();
if config.should_parallelize_join(probe_rows.len()) {
let pairs: Vec<(usize, usize)> = probe_rows
.par_iter()
.enumerate()
.flat_map(|(probe_idx, probe_row)| {
let probe_key = CompositeKey::from_row(probe_row, probe_col_indices);
if probe_key.has_null() {
return Vec::new();
}
hash_table
.get(&probe_key)
.map(|build_indices| {
build_indices
.iter()
.map(|&build_idx| (build_idx, probe_idx))
.collect::<Vec<_>>()
})
.unwrap_or_default()
})
.collect();
return Ok(FromResult::from_rows(
combined_schema,
batch_combine_rows_with_table_names(
build_rows,
probe_rows,
&pairs,
left_is_build,
build_table_names,
probe_table_names,
),
));
}
}
let estimated_capacity = probe_rows.len().saturating_mul(2).min(100_000);
let mut pairs: Vec<(usize, usize)> = Vec::with_capacity(estimated_capacity);
for (probe_idx, probe_row) in probe_rows.iter().enumerate() {
let probe_key = CompositeKey::from_row(probe_row, probe_col_indices);
if probe_key.has_null() {
continue;
}
if let Some(build_indices) = hash_table.get(&probe_key) {
for &build_idx in build_indices {
pairs.push((build_idx, probe_idx));
}
} else if probe_key.0.iter().any(|v| is_potential_cross_type_match(v)) {
for (build_key, build_indices) in hash_table.iter() {
let all_match = probe_key
.0
.iter()
.zip(build_key.0.iter())
.all(|(p, b)| crate::evaluator::values_are_equal(p, b));
if all_match {
for &build_idx in build_indices {
pairs.push((build_idx, probe_idx));
}
break; }
}
}
}
pairs
};
let result_rows = batch_combine_rows_with_table_names(
build_rows,
probe_rows,
&join_pairs,
left_is_build,
build_table_names,
probe_table_names,
);
Ok(FromResult::from_rows(combined_schema, result_rows))
}
pub(in crate::select::join) fn hash_join_inner_arithmetic(
left: FromResult,
right: FromResult,
left_col_idx: usize,
right_col_idx: usize,
offset: i64,
) -> Result<FromResult, ExecutorError> {
use ahash::AHashMap;
use vibesql_types::SqlValue;
let left_table_names = left.schema.table_names();
let right_table_names = right.schema.table_names();
let right_table_id = right
.schema
.table_schemas
.keys()
.next()
.ok_or_else(|| ExecutorError::UnsupportedFeature("Complex JOIN".to_string()))?
.clone();
let right_schema = right
.schema
.table_schemas
.get(&right_table_id)
.ok_or_else(|| ExecutorError::UnsupportedFeature("Complex JOIN".to_string()))?
.1
.clone();
let right_table_display_name = right_table_id.display().to_string();
let combined_schema = CombinedSchema::combine(
left.schema.clone(),
right_table_display_name.clone(),
right_schema,
);
let left_slice = left.as_slice();
let right_slice = right.as_slice();
let mut hash_table: AHashMap<i64, Vec<usize>> = AHashMap::with_capacity(right_slice.len());
for (idx, row) in right_slice.iter().enumerate() {
let value = &row.values[right_col_idx];
if let SqlValue::Integer(n) = value {
let key = n + offset;
hash_table.entry(key).or_default().push(idx);
}
}
#[cfg(feature = "parallel")]
{
let config = ParallelConfig::global();
if config.should_parallelize_join(left_slice.len()) {
let pairs: Vec<(usize, usize)> = left_slice
.par_iter()
.enumerate()
.flat_map(|(left_idx, left_row)| {
let value = &left_row.values[left_col_idx];
if let SqlValue::Integer(n) = value {
hash_table
.get(n)
.map(|right_indices| {
right_indices
.iter()
.map(|&right_idx| (left_idx, right_idx))
.collect::<Vec<_>>()
})
.unwrap_or_default()
} else {
Vec::new()
}
})
.collect();
let swapped_pairs: Vec<(usize, usize)> =
pairs.into_iter().map(|(left, right)| (right, left)).collect();
let result_rows = batch_combine_rows_with_table_names(
right_slice,
left_slice,
&swapped_pairs,
false,
&right_table_names,
&left_table_names,
);
return Ok(FromResult::from_rows(combined_schema, result_rows));
}
}
let estimated_capacity = left_slice.len().saturating_mul(2).min(100_000);
let mut pairs: Vec<(usize, usize)> = Vec::with_capacity(estimated_capacity);
for (left_idx, left_row) in left_slice.iter().enumerate() {
let value = &left_row.values[left_col_idx];
if let SqlValue::Integer(n) = value {
if let Some(right_indices) = hash_table.get(n) {
for &right_idx in right_indices {
pairs.push((left_idx, right_idx));
}
}
}
}
let swapped_pairs: Vec<(usize, usize)> =
pairs.into_iter().map(|(left, right)| (right, left)).collect();
let result_rows = batch_combine_rows_with_table_names(
right_slice,
left_slice,
&swapped_pairs,
false,
&right_table_names,
&left_table_names,
);
Ok(FromResult::from_rows(combined_schema, result_rows))
}