#![allow(clippy::manual_is_multiple_of)]
use std::hash::{Hash, Hasher};
use ahash::{AHashMap, AHasher};
use super::{combine_rows, BloomFilter, FromResult};
use crate::{
errors::ExecutorError,
schema::CombinedSchema,
select::RowIterator,
timeout::{TimeoutContext, CHECK_INTERVAL},
};
const BLOOM_FILTER_MIN_ROWS: usize = 100;
const BLOOM_FILTER_FPR: f64 = 0.01;
#[inline]
fn hash_sql_value(value: &vibesql_types::SqlValue) -> u64 {
let mut hasher = AHasher::default();
value.hash(&mut hasher);
hasher.finish()
}
pub struct HashJoinIterator<L: RowIterator> {
left: L,
right_hash_table: AHashMap<vibesql_types::SqlValue, Vec<vibesql_storage::Row>>,
bloom_filter: Option<BloomFilter>,
schema: CombinedSchema,
left_col_idx: usize,
#[allow(dead_code)]
right_col_idx: usize,
current_left_row: Option<vibesql_storage::Row>,
current_matches: Vec<vibesql_storage::Row>,
match_index: usize,
#[allow(dead_code)]
right_col_count: usize,
timeout_ctx: TimeoutContext,
iteration_count: usize,
#[allow(dead_code)]
bloom_rejections: usize,
}
impl<L: RowIterator> HashJoinIterator<L> {
#[allow(private_interfaces)]
pub fn new(
left: L,
right: FromResult,
left_col_idx: usize,
right_col_idx: usize,
) -> Result<Self, ExecutorError> {
let right_table_name = 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_name)
.ok_or_else(|| ExecutorError::UnsupportedFeature("Complex JOIN".to_string()))?
.1
.clone();
let right_col_count = right_schema.columns.len();
let right_table_display_name = right_table_name.display().to_string();
let combined_schema = CombinedSchema::combine(
left.schema().clone(),
right_table_display_name.clone(),
right_schema,
);
let timeout_ctx = TimeoutContext::new_default();
let right_rows = right.into_rows();
let num_build_rows = right_rows.len();
let mut hash_table: AHashMap<vibesql_types::SqlValue, Vec<vibesql_storage::Row>> =
AHashMap::new();
let bloom_disabled = std::env::var("VIBESQL_DISABLE_BLOOM_FILTER").is_ok();
let mut bloom_filter = if !bloom_disabled && num_build_rows >= BLOOM_FILTER_MIN_ROWS {
Some(BloomFilter::new(num_build_rows, BLOOM_FILTER_FPR))
} else {
None
};
let mut build_iterations = 0;
for row in right_rows {
build_iterations += 1;
if build_iterations % CHECK_INTERVAL == 0 {
timeout_ctx.check()?;
}
let key = row.values[right_col_idx].clone();
if key != vibesql_types::SqlValue::Null {
if let Some(ref mut bf) = bloom_filter {
let hash = hash_sql_value(&key);
bf.insert_hash(hash);
}
hash_table.entry(key).or_default().push(row);
}
}
Ok(Self {
left,
right_hash_table: hash_table,
bloom_filter,
schema: combined_schema,
left_col_idx,
right_col_idx,
current_left_row: None,
current_matches: Vec::new(),
match_index: 0,
right_col_count,
timeout_ctx,
iteration_count: 0,
bloom_rejections: 0,
})
}
pub fn hash_table_size(&self) -> usize {
self.right_hash_table.values().map(|v| v.len()).sum()
}
}
impl<L: RowIterator> Iterator for HashJoinIterator<L> {
type Item = Result<vibesql_storage::Row, ExecutorError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
self.iteration_count += 1;
if self.iteration_count % CHECK_INTERVAL == 0 {
if let Err(e) = self.timeout_ctx.check() {
return Some(Err(e));
}
}
if self.match_index < self.current_matches.len() {
let right_row = &self.current_matches[self.match_index];
self.match_index += 1;
if let Some(ref left_row) = self.current_left_row {
let combined_row = combine_rows(left_row, right_row);
return Some(Ok(combined_row));
}
}
match self.left.next() {
Some(Ok(left_row)) => {
let key = &left_row.values[self.left_col_idx];
if key == &vibesql_types::SqlValue::Null {
continue;
}
if let Some(ref bf) = self.bloom_filter {
let hash = hash_sql_value(key);
if !bf.might_contain_hash(hash) {
self.bloom_rejections += 1;
continue;
}
}
if let Some(matches) = self.right_hash_table.get(key) {
self.current_left_row = Some(left_row);
self.current_matches = matches.clone();
self.match_index = 0;
} else {
continue;
}
}
Some(Err(e)) => {
return Some(Err(e));
}
None => {
return None;
}
}
}
}
}
impl<L: RowIterator> RowIterator for HashJoinIterator<L> {
fn schema(&self) -> &CombinedSchema {
&self.schema
}
}
#[cfg(test)]
mod tests {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_storage::Row;
use vibesql_types::{DataType, SqlValue};
use super::*;
use crate::select::TableScanIterator;
fn create_test_from_result(
table_name: &str,
columns: Vec<(&str, DataType)>,
rows: Vec<Vec<SqlValue>>,
) -> FromResult {
let schema = TableSchema::new(
table_name.to_string(),
columns
.iter()
.map(|(name, dtype)| {
ColumnSchema::new(
name.to_string(),
dtype.clone(),
true, )
})
.collect(),
);
let combined_schema = CombinedSchema::from_table(table_name.to_string(), schema);
let rows = rows.into_iter().map(Row::new).collect();
FromResult::from_rows(combined_schema, rows)
}
#[test]
fn test_hash_join_iterator_simple() {
let left_result = create_test_from_result(
"users",
vec![("id", DataType::Integer), ("name", DataType::Varchar { max_length: Some(50) })],
vec![
vec![SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("Alice"))],
vec![SqlValue::Integer(2), SqlValue::Varchar(arcstr::ArcStr::from("Bob"))],
vec![SqlValue::Integer(3), SqlValue::Varchar(arcstr::ArcStr::from("Charlie"))],
],
);
let left_iter = TableScanIterator::new(left_result.schema.clone(), left_result.into_rows());
let right = create_test_from_result(
"orders",
vec![("user_id", DataType::Integer), ("amount", DataType::Integer)],
vec![
vec![SqlValue::Integer(1), SqlValue::Integer(100)],
vec![SqlValue::Integer(2), SqlValue::Integer(200)],
vec![SqlValue::Integer(1), SqlValue::Integer(150)],
],
);
let join_iter = HashJoinIterator::new(left_iter, right, 0, 0).unwrap();
let results: Result<Vec<_>, _> = join_iter.collect();
let results = results.unwrap();
assert_eq!(results.len(), 3);
for row in &results {
assert_eq!(row.values.len(), 4);
}
let alice_orders: Vec<_> =
results.iter().filter(|r| r.values[0] == SqlValue::Integer(1)).collect();
assert_eq!(alice_orders.len(), 2);
let bob_orders: Vec<_> =
results.iter().filter(|r| r.values[0] == SqlValue::Integer(2)).collect();
assert_eq!(bob_orders.len(), 1);
let charlie_orders: Vec<_> =
results.iter().filter(|r| r.values[0] == SqlValue::Integer(3)).collect();
assert_eq!(charlie_orders.len(), 0);
}
#[test]
fn test_hash_join_iterator_null_values() {
let left_result = create_test_from_result(
"users",
vec![("id", DataType::Integer), ("name", DataType::Varchar { max_length: Some(50) })],
vec![
vec![SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("Alice"))],
vec![SqlValue::Null, SqlValue::Varchar(arcstr::ArcStr::from("Unknown"))],
],
);
let left_iter = TableScanIterator::new(left_result.schema.clone(), left_result.into_rows());
let right = create_test_from_result(
"orders",
vec![("user_id", DataType::Integer), ("amount", DataType::Integer)],
vec![
vec![SqlValue::Integer(1), SqlValue::Integer(100)],
vec![SqlValue::Null, SqlValue::Integer(200)],
],
);
let join_iter = HashJoinIterator::new(left_iter, right, 0, 0).unwrap();
let results: Result<Vec<_>, _> = join_iter.collect();
let results = results.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].values[0], SqlValue::Integer(1)); assert_eq!(results[0].values[1], SqlValue::Varchar(arcstr::ArcStr::from("Alice"))); assert_eq!(results[0].values[2], SqlValue::Integer(1)); assert_eq!(results[0].values[3], SqlValue::Integer(100)); }
#[test]
fn test_hash_join_iterator_no_matches() {
let left_result = create_test_from_result(
"users",
vec![("id", DataType::Integer)],
vec![vec![SqlValue::Integer(1)], vec![SqlValue::Integer(2)]],
);
let left_iter = TableScanIterator::new(left_result.schema.clone(), left_result.into_rows());
let right = create_test_from_result(
"orders",
vec![("user_id", DataType::Integer)],
vec![vec![SqlValue::Integer(3)], vec![SqlValue::Integer(4)]],
);
let join_iter = HashJoinIterator::new(left_iter, right, 0, 0).unwrap();
let results: Result<Vec<_>, _> = join_iter.collect();
let results = results.unwrap();
assert_eq!(results.len(), 0);
}
#[test]
fn test_hash_join_iterator_empty_tables() {
let left_result = create_test_from_result("users", vec![("id", DataType::Integer)], vec![]);
let left_iter = TableScanIterator::new(left_result.schema.clone(), left_result.into_rows());
let right = create_test_from_result("orders", vec![("user_id", DataType::Integer)], vec![]);
let join_iter = HashJoinIterator::new(left_iter, right, 0, 0).unwrap();
let results: Result<Vec<_>, _> = join_iter.collect();
let results = results.unwrap();
assert_eq!(results.len(), 0);
}
#[test]
fn test_hash_join_iterator_duplicate_keys() {
let left_result = create_test_from_result(
"users",
vec![("id", DataType::Integer), ("type", DataType::Varchar { max_length: Some(10) })],
vec![
vec![SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("admin"))],
vec![SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("user"))],
],
);
let left_iter = TableScanIterator::new(left_result.schema.clone(), left_result.into_rows());
let right = create_test_from_result(
"orders",
vec![("user_id", DataType::Integer), ("amount", DataType::Integer)],
vec![
vec![SqlValue::Integer(1), SqlValue::Integer(100)],
vec![SqlValue::Integer(1), SqlValue::Integer(200)],
],
);
let join_iter = HashJoinIterator::new(left_iter, right, 0, 0).unwrap();
let results: Result<Vec<_>, _> = join_iter.collect();
let results = results.unwrap();
assert_eq!(results.len(), 4);
for row in &results {
assert_eq!(row.values[0], SqlValue::Integer(1));
}
}
#[test]
fn test_hash_join_iterator_lazy_evaluation() {
struct CountingIterator {
schema: CombinedSchema,
rows: Vec<Row>,
index: usize,
consumed_count: std::sync::Arc<std::sync::Mutex<usize>>,
}
impl Iterator for CountingIterator {
type Item = Result<Row, ExecutorError>;
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.rows.len() {
let row = self.rows[self.index].clone();
self.index += 1;
*self.consumed_count.lock().unwrap() += 1;
Some(Ok(row))
} else {
None
}
}
}
impl RowIterator for CountingIterator {
fn schema(&self) -> &CombinedSchema {
&self.schema
}
}
let consumed = std::sync::Arc::new(std::sync::Mutex::new(0));
let left_result = create_test_from_result(
"users",
vec![("id", DataType::Integer)],
vec![
vec![SqlValue::Integer(1)],
vec![SqlValue::Integer(2)],
vec![SqlValue::Integer(3)],
vec![SqlValue::Integer(4)],
vec![SqlValue::Integer(5)],
],
);
let counting_iter = CountingIterator {
schema: left_result.schema.clone(),
rows: left_result.into_rows(),
index: 0,
consumed_count: consumed.clone(),
};
let right = create_test_from_result(
"orders",
vec![("user_id", DataType::Integer)],
vec![vec![SqlValue::Integer(1)], vec![SqlValue::Integer(2)]],
);
let join_iter = HashJoinIterator::new(counting_iter, right, 0, 0).unwrap();
let results: Vec<_> = join_iter.take(2).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(results.len(), 2);
let consumed_count = *consumed.lock().unwrap();
assert!(consumed_count <= 3, "Expected at most 3 rows consumed, got {}", consumed_count);
}
}