use std::{collections::HashMap, sync::Arc};
use super::{simd_ops, ColumnArray, ColumnarBatch};
use crate::errors::ExecutorError;
#[inline]
fn simd_eq_i64(values: &[i64], target: i64) -> Vec<bool> {
simd_ops::eq_i64(values, target)
}
#[inline]
fn simd_eq_f64(values: &[f64], target: f64) -> Vec<bool> {
simd_ops::eq_f64(values, target)
}
#[derive(Debug)]
enum HashTable {
Int64(HashMap<i64, Vec<usize>>),
Float64(HashMap<OrderedFloat, Vec<usize>>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OrderedFloat(u64);
impl OrderedFloat {
fn new(f: f64) -> Self {
Self(f.to_bits())
}
fn to_f64(self) -> f64 {
f64::from_bits(self.0)
}
}
impl HashTable {
fn build(right: &ColumnarBatch, right_key_col: usize) -> Result<Self, ExecutorError> {
let key_column =
right.column(right_key_col).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: right_key_col,
batch_columns: right.column_count(),
})?;
match key_column {
ColumnArray::Int64(values, nulls) => {
let mut table = HashMap::new();
for (row_idx, &value) in values.iter().enumerate() {
if let Some(null_mask) = nulls {
if null_mask[row_idx] {
continue;
}
}
table.entry(value).or_insert_with(Vec::new).push(row_idx);
}
Ok(HashTable::Int64(table))
}
ColumnArray::Float64(values, nulls) => {
let mut table = HashMap::new();
for (row_idx, &value) in values.iter().enumerate() {
if let Some(null_mask) = nulls {
if null_mask[row_idx] {
continue;
}
}
if value.is_nan() {
continue; }
table.entry(OrderedFloat::new(value)).or_insert_with(Vec::new).push(row_idx);
}
Ok(HashTable::Float64(table))
}
_ => Err(ExecutorError::UnsupportedArrayType {
operation: "columnar hash join".to_string(),
array_type: format!("{:?}", std::mem::discriminant(key_column)),
}),
}
}
fn probe_simd(
&self,
left: &ColumnarBatch,
left_key_col: usize,
) -> Result<Vec<(usize, usize)>, ExecutorError> {
let key_column =
left.column(left_key_col).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: left_key_col,
batch_columns: left.column_count(),
})?;
match (self, key_column) {
(HashTable::Int64(table), ColumnArray::Int64(left_values, left_nulls)) => {
Self::probe_i64_simd(table, left_values, left_nulls.as_ref().map(|n| n.as_slice()))
}
(HashTable::Float64(table), ColumnArray::Float64(left_values, left_nulls)) => {
Self::probe_f64_simd(table, left_values, left_nulls.as_ref().map(|n| n.as_slice()))
}
_ => Err(ExecutorError::ColumnarTypeMismatch {
operation: "hash join probe".to_string(),
left_type: format!("{:?}", std::mem::discriminant(key_column)),
right_type: None,
}),
}
}
fn probe_i64_simd(
table: &HashMap<i64, Vec<usize>>,
left_values: &[i64],
left_nulls: Option<&[bool]>,
) -> Result<Vec<(usize, usize)>, ExecutorError> {
let mut matches = Vec::new();
let table_keys: Vec<i64> = table.keys().copied().collect();
for &key in &table_keys {
let equality_mask = simd_eq_i64(left_values, key);
for (left_row, &is_match) in equality_mask.iter().enumerate() {
if !is_match {
continue;
}
if let Some(null_mask) = left_nulls {
if null_mask[left_row] {
continue;
}
}
if let Some(right_rows) = table.get(&key) {
for &right_row in right_rows {
matches.push((left_row, right_row));
}
}
}
}
Ok(matches)
}
fn probe_f64_simd(
table: &HashMap<OrderedFloat, Vec<usize>>,
left_values: &[f64],
left_nulls: Option<&[bool]>,
) -> Result<Vec<(usize, usize)>, ExecutorError> {
let mut matches = Vec::new();
let table_keys: Vec<f64> = table.keys().map(|k| k.to_f64()).collect();
for &key in &table_keys {
let equality_mask = simd_eq_f64(left_values, key);
for (left_row, &is_match) in equality_mask.iter().enumerate() {
if !is_match {
continue;
}
if let Some(null_mask) = left_nulls {
if null_mask[left_row] {
continue;
}
}
if left_values[left_row].is_nan() {
continue;
}
let ordered_key = OrderedFloat::new(key);
if let Some(right_rows) = table.get(&ordered_key) {
for &right_row in right_rows {
matches.push((left_row, right_row));
}
}
}
}
Ok(matches)
}
}
fn materialize_join_results(
left: &ColumnarBatch,
right: &ColumnarBatch,
matches: Vec<(usize, usize)>,
) -> Result<ColumnarBatch, ExecutorError> {
let output_row_count = matches.len();
let output_col_count = left.column_count() + right.column_count();
let mut output = ColumnarBatch::with_capacity(output_row_count, output_col_count);
for col_idx in 0..left.column_count() {
let left_col =
left.column(col_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: col_idx,
batch_columns: left.column_count(),
})?;
let output_col = gather_column(left_col, &matches, |&(left_row, _)| left_row)?;
output.add_column(output_col)?;
}
for col_idx in 0..right.column_count() {
let right_col =
right.column(col_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: col_idx,
batch_columns: right.column_count(),
})?;
let output_col = gather_column(right_col, &matches, |&(_, right_row)| right_row)?;
output.add_column(output_col)?;
}
Ok(output)
}
fn gather_column<F>(
source: &ColumnArray,
indices: &[(usize, usize)],
index_fn: F,
) -> Result<ColumnArray, ExecutorError>
where
F: Fn(&(usize, usize)) -> usize,
{
match source {
ColumnArray::Int64(values, nulls) => {
let mut output_values = Vec::with_capacity(indices.len());
let mut output_nulls =
if nulls.is_some() { Some(Vec::with_capacity(indices.len())) } else { None };
for pair in indices {
let idx = index_fn(pair);
output_values.push(values[idx]);
if let Some(ref mut out_nulls) = output_nulls {
let is_null = nulls.as_ref().map(|n| n[idx]).unwrap_or(false);
out_nulls.push(is_null);
}
}
Ok(ColumnArray::Int64(Arc::new(output_values), output_nulls.map(Arc::new)))
}
ColumnArray::Float64(values, nulls) => {
let mut output_values = Vec::with_capacity(indices.len());
let mut output_nulls =
if nulls.is_some() { Some(Vec::with_capacity(indices.len())) } else { None };
for pair in indices {
let idx = index_fn(pair);
output_values.push(values[idx]);
if let Some(ref mut out_nulls) = output_nulls {
let is_null = nulls.as_ref().map(|n| n[idx]).unwrap_or(false);
out_nulls.push(is_null);
}
}
Ok(ColumnArray::Float64(Arc::new(output_values), output_nulls.map(Arc::new)))
}
ColumnArray::String(values, nulls) => {
let mut output_values = Vec::with_capacity(indices.len());
let mut output_nulls =
if nulls.is_some() { Some(Vec::with_capacity(indices.len())) } else { None };
for pair in indices {
let idx = index_fn(pair);
output_values.push(values[idx].clone());
if let Some(ref mut out_nulls) = output_nulls {
let is_null = nulls.as_ref().map(|n| n[idx]).unwrap_or(false);
out_nulls.push(is_null);
}
}
Ok(ColumnArray::String(Arc::new(output_values), output_nulls.map(Arc::new)))
}
_ => {
let mut output_values = Vec::with_capacity(indices.len());
for pair in indices {
let idx = index_fn(pair);
let value = source.get_value(idx)?;
output_values.push(value);
}
Ok(ColumnArray::Mixed(Arc::new(output_values)))
}
}
}
pub fn columnar_hash_join_inner(
left: &ColumnarBatch,
right: &ColumnarBatch,
left_key_col: usize,
right_key_col: usize,
) -> Result<ColumnarBatch, ExecutorError> {
let hash_table = HashTable::build(right, right_key_col)?;
let matches = hash_table.probe_simd(left, left_key_col)?;
materialize_join_results(left, right, matches)
}
#[cfg(test)]
mod tests {
use vibesql_types::SqlValue;
use super::*;
fn make_int64_batch(key_values: Vec<i64>, data_values: Vec<i64>) -> ColumnarBatch {
let row_count = key_values.len();
let mut batch = ColumnarBatch::with_capacity(row_count, 2);
batch.add_column(ColumnArray::Int64(Arc::new(key_values), None)).unwrap();
batch.add_column(ColumnArray::Int64(Arc::new(data_values), None)).unwrap();
batch
}
fn make_float64_batch(key_values: Vec<f64>, data_values: Vec<i64>) -> ColumnarBatch {
let row_count = key_values.len();
let mut batch = ColumnarBatch::with_capacity(row_count, 2);
batch.add_column(ColumnArray::Float64(Arc::new(key_values), None)).unwrap();
batch.add_column(ColumnArray::Int64(Arc::new(data_values), None)).unwrap();
batch
}
#[test]
fn test_inner_join_int64_basic() {
let left = make_int64_batch(vec![1, 2, 3], vec![10, 20, 30]);
let right = make_int64_batch(vec![2, 3, 4], vec![200, 300, 400]);
let result = columnar_hash_join_inner(&left, &right, 0, 0).unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.column_count(), 4);
let rows = result.to_rows().unwrap();
let mut found_2 = false;
let mut found_3 = false;
for row in &rows {
match row.get(0) {
Some(&SqlValue::Integer(2)) => {
assert_eq!(row.get(1), Some(&SqlValue::Integer(20)));
assert_eq!(row.get(2), Some(&SqlValue::Integer(2)));
assert_eq!(row.get(3), Some(&SqlValue::Integer(200)));
found_2 = true;
}
Some(&SqlValue::Integer(3)) => {
assert_eq!(row.get(1), Some(&SqlValue::Integer(30)));
assert_eq!(row.get(2), Some(&SqlValue::Integer(3)));
assert_eq!(row.get(3), Some(&SqlValue::Integer(300)));
found_3 = true;
}
_ => panic!("Unexpected join result"),
}
}
assert!(found_2 && found_3, "Missing expected join matches");
}
#[test]
fn test_inner_join_float64_basic() {
let left = make_float64_batch(vec![1.5, 2.5, 3.5], vec![10, 20, 30]);
let right = make_float64_batch(vec![2.5, 3.5, 4.5], vec![200, 300, 400]);
let result = columnar_hash_join_inner(&left, &right, 0, 0).unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.column_count(), 4);
let rows = result.to_rows().unwrap();
let mut found_25 = false;
let mut found_35 = false;
for row in &rows {
match row.get(0) {
Some(&SqlValue::Double(f)) if (f - 2.5).abs() < 1e-10 => {
assert_eq!(row.get(1), Some(&SqlValue::Integer(20)));
found_25 = true;
}
Some(&SqlValue::Double(f)) if (f - 3.5).abs() < 1e-10 => {
assert_eq!(row.get(1), Some(&SqlValue::Integer(30)));
found_35 = true;
}
_ => {}
}
}
assert!(found_25 && found_35);
}
#[test]
fn test_inner_join_with_nulls() {
let mut left = ColumnarBatch::with_capacity(3, 2);
left.add_column(ColumnArray::Int64(
Arc::new(vec![1, 0, 3]), Some(Arc::new(vec![false, true, false])),
))
.unwrap();
left.add_column(ColumnArray::Int64(Arc::new(vec![10, 20, 30]), None)).unwrap();
let mut right = ColumnarBatch::with_capacity(3, 2);
right
.add_column(ColumnArray::Int64(
Arc::new(vec![1, 3, 0]),
Some(Arc::new(vec![false, false, true])),
))
.unwrap();
right.add_column(ColumnArray::Int64(Arc::new(vec![100, 300, 400]), None)).unwrap();
let result = columnar_hash_join_inner(&left, &right, 0, 0).unwrap();
assert_eq!(result.row_count(), 2);
let rows = result.to_rows().unwrap();
for row in &rows {
assert!(!matches!(row.get(0), Some(&SqlValue::Null)));
assert!(!matches!(row.get(2), Some(&SqlValue::Null)));
}
}
#[test]
fn test_inner_join_multi_match() {
let left = make_int64_batch(vec![1, 1, 2], vec![10, 11, 20]);
let right = make_int64_batch(vec![1, 1], vec![100, 101]);
let result = columnar_hash_join_inner(&left, &right, 0, 0).unwrap();
assert_eq!(result.row_count(), 4);
}
#[test]
fn test_inner_join_empty_result() {
let left = make_int64_batch(vec![1, 2], vec![10, 20]);
let right = make_int64_batch(vec![3, 4], vec![300, 400]);
let result = columnar_hash_join_inner(&left, &right, 0, 0).unwrap();
assert_eq!(result.row_count(), 0);
assert_eq!(result.column_count(), 4);
}
#[test]
fn test_inner_join_empty_batches() {
let left = ColumnarBatch::with_capacity(0, 2);
let right = ColumnarBatch::with_capacity(0, 2);
let result = columnar_hash_join_inner(&left, &right, 0, 0);
assert!(result.is_err()); }
}