#![allow(dead_code)]
mod hash_table;
mod output;
mod probe;
mod row_extract;
pub use row_extract::{
hash_join_indices_columnar, hash_join_indices_columnar_multi, hash_join_indices_columnar_str,
};
use crate::{errors::ExecutorError, select::columnar::ColumnarBatch};
pub fn columnar_hash_join_inner(
left_batch: &ColumnarBatch,
right_batch: &ColumnarBatch,
left_key_idx: usize,
right_key_idx: usize,
) -> Result<ColumnarBatch, ExecutorError> {
let left_key =
left_batch.column(left_key_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: left_key_idx,
batch_columns: left_batch.column_count(),
})?;
let right_key =
right_batch.column(right_key_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: right_key_idx,
batch_columns: right_batch.column_count(),
})?;
let hash_table = hash_table::ColumnarHashTable::build_from_column(right_key)?;
let join_indices = probe::probe_columnar(&hash_table, left_key, right_key)?;
output::gather_join_result(left_batch, right_batch, &join_indices)
}
pub fn columnar_hash_join_left_outer(
left_batch: &ColumnarBatch,
right_batch: &ColumnarBatch,
left_key_idx: usize,
right_key_idx: usize,
) -> Result<ColumnarBatch, ExecutorError> {
let left_key =
left_batch.column(left_key_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: left_key_idx,
batch_columns: left_batch.column_count(),
})?;
let right_key =
right_batch.column(right_key_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: right_key_idx,
batch_columns: right_batch.column_count(),
})?;
let hash_table = hash_table::ColumnarHashTable::build_from_column(right_key)?;
let join_indices =
probe::probe_columnar_left_outer(&hash_table, left_key, right_key, left_batch.row_count())?;
output::gather_left_outer_result(left_batch, right_batch, &join_indices)
}
pub fn columnar_hash_join_right_outer(
left_batch: &ColumnarBatch,
right_batch: &ColumnarBatch,
left_key_idx: usize,
right_key_idx: usize,
) -> Result<ColumnarBatch, ExecutorError> {
let left_key =
left_batch.column(left_key_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: left_key_idx,
batch_columns: left_batch.column_count(),
})?;
let right_key =
right_batch.column(right_key_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: right_key_idx,
batch_columns: right_batch.column_count(),
})?;
let hash_table = hash_table::ColumnarHashTable::build_from_column(left_key)?;
let join_indices = probe::probe_columnar_right_outer(
&hash_table,
right_key,
left_key,
right_batch.row_count(),
)?;
output::gather_right_outer_result(left_batch, right_batch, &join_indices)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::select::columnar::ColumnArray;
#[test]
fn test_columnar_hash_join() {
let left_columns = vec![
ColumnArray::Int64(Arc::new(vec![1, 2, 3, 4]), None),
ColumnArray::String(
Arc::new(vec!["Alice".into(), "Bob".into(), "Carol".into(), "Dave".into()]),
None,
),
];
let left_batch = ColumnarBatch::from_columns(
left_columns,
Some(vec!["customer_id".into(), "name".into()]),
)
.unwrap();
let right_columns = vec![
ColumnArray::Int64(Arc::new(vec![101, 102, 103, 104, 105]), None),
ColumnArray::Int64(Arc::new(vec![1, 2, 1, 3, 2]), None),
ColumnArray::Float64(Arc::new(vec![100.0, 200.0, 150.0, 300.0, 250.0]), None),
];
let right_batch = ColumnarBatch::from_columns(
right_columns,
Some(vec!["order_id".into(), "customer_id".into(), "amount".into()]),
)
.unwrap();
let result = columnar_hash_join_inner(&left_batch, &right_batch, 0, 1).unwrap();
assert_eq!(result.row_count(), 5);
assert_eq!(result.column_count(), 5);
}
#[test]
fn test_columnar_hash_join_left_outer() {
let left_columns = vec![
ColumnArray::Int64(Arc::new(vec![1, 2, 3, 4]), None),
ColumnArray::String(
Arc::new(vec!["Alice".into(), "Bob".into(), "Carol".into(), "Dave".into()]),
None,
),
];
let left_batch = ColumnarBatch::from_columns(
left_columns,
Some(vec!["customer_id".into(), "name".into()]),
)
.unwrap();
let right_columns = vec![
ColumnArray::Int64(Arc::new(vec![101, 102, 103]), None),
ColumnArray::Int64(Arc::new(vec![1, 2, 1]), None), ];
let right_batch = ColumnarBatch::from_columns(
right_columns,
Some(vec!["order_id".into(), "customer_id".into()]),
)
.unwrap();
let result = columnar_hash_join_left_outer(&left_batch, &right_batch, 0, 1).unwrap();
assert_eq!(result.row_count(), 5);
assert_eq!(result.column_count(), 4);
let rows = result.to_rows().unwrap();
let mut customer_counts = std::collections::HashMap::new();
for row in &rows {
if let Some(vibesql_types::SqlValue::Integer(id)) = row.get(0) {
*customer_counts.entry(*id).or_insert(0) += 1;
}
}
assert_eq!(customer_counts.get(&1), Some(&2)); assert_eq!(customer_counts.get(&2), Some(&1)); assert_eq!(customer_counts.get(&3), Some(&1)); assert_eq!(customer_counts.get(&4), Some(&1)); }
#[test]
fn test_columnar_hash_join_right_outer() {
let left_columns = vec![
ColumnArray::Int64(Arc::new(vec![1, 2]), None),
ColumnArray::String(Arc::new(vec!["Alice".into(), "Bob".into()]), None),
];
let left_batch = ColumnarBatch::from_columns(
left_columns,
Some(vec!["customer_id".into(), "name".into()]),
)
.unwrap();
let right_columns = vec![
ColumnArray::Int64(Arc::new(vec![101, 102, 103, 104]), None),
ColumnArray::Int64(Arc::new(vec![1, 2, 3, 1]), None),
];
let right_batch = ColumnarBatch::from_columns(
right_columns,
Some(vec!["order_id".into(), "customer_id".into()]),
)
.unwrap();
let result = columnar_hash_join_right_outer(&left_batch, &right_batch, 0, 1).unwrap();
assert_eq!(result.row_count(), 4);
assert_eq!(result.column_count(), 4);
let rows = result.to_rows().unwrap();
let mut order_found = std::collections::HashSet::new();
let mut null_customer_count = 0;
for row in &rows {
if let Some(vibesql_types::SqlValue::Integer(order_id)) = row.get(2) {
order_found.insert(*order_id);
}
if let Some(vibesql_types::SqlValue::Null) = row.get(0) {
null_customer_count += 1;
}
}
assert!(order_found.contains(&101));
assert!(order_found.contains(&102));
assert!(order_found.contains(&103));
assert!(order_found.contains(&104));
assert_eq!(null_customer_count, 1);
}
#[test]
fn test_columnar_hash_join_with_nulls() {
let left_columns = vec![
ColumnArray::Int64(
Arc::new(vec![1, 0, 3]), Some(Arc::new(vec![false, true, false])), ),
ColumnArray::String(Arc::new(vec!["Alice".into(), "Bob".into(), "Carol".into()]), None),
];
let left_batch =
ColumnarBatch::from_columns(left_columns, Some(vec!["id".into(), "name".into()]))
.unwrap();
let right_columns = vec![
ColumnArray::Int64(Arc::new(vec![1, 3]), None),
ColumnArray::String(Arc::new(vec!["Order1".into(), "Order3".into()]), None),
];
let right_batch =
ColumnarBatch::from_columns(right_columns, Some(vec!["id".into(), "desc".into()]))
.unwrap();
let result = columnar_hash_join_left_outer(&left_batch, &right_batch, 0, 0).unwrap();
assert_eq!(result.row_count(), 3);
let rows = result.to_rows().unwrap();
let bob_row = rows
.iter()
.find(|r| matches!(r.get(1), Some(vibesql_types::SqlValue::Varchar(s)) if s.as_str() == "Bob"));
assert!(bob_row.is_some());
let bob = bob_row.unwrap();
assert!(matches!(bob.get(2), Some(vibesql_types::SqlValue::Null)));
assert!(matches!(bob.get(3), Some(vibesql_types::SqlValue::Null)));
}
#[test]
fn test_columnar_hash_join_left_outer_preserves_matched_nulls() {
let left_columns = vec![ColumnArray::String(Arc::new(vec!["X".into()]), None)];
let left_batch =
ColumnarBatch::from_columns(left_columns, Some(vec!["x".into()])).unwrap();
let right_columns = vec![
ColumnArray::Int64(
Arc::new(vec![5, 0, 7]), Some(Arc::new(vec![false, true, false])), ),
ColumnArray::String(
Arc::new(vec!["X".into(), "X".into(), "X".into()]),
None,
),
];
let right_batch =
ColumnarBatch::from_columns(right_columns, Some(vec!["c".into(), "d".into()]))
.unwrap();
let result = columnar_hash_join_left_outer(&left_batch, &right_batch, 0, 1).unwrap();
assert_eq!(result.row_count(), 3);
let rows = result.to_rows().unwrap();
let null_c_count = rows
.iter()
.filter(|r| matches!(r.get(1), Some(vibesql_types::SqlValue::Null)))
.count();
assert_eq!(
null_c_count, 1,
"matched-row NULL in c must be preserved; got rows: {:?}",
rows
);
}
}