use rudb_common::{Error, LogicalType, Result};
use rudb_vector::{Form, Vector};
use crate::table::{Across, BATCH, Probe, Table, Walk};
const NONE: u32 = u32::MAX;
pub(crate) const MISS: usize = usize::MAX;
#[derive(Debug, Default)]
pub(crate) struct Lookup {
table: Option<Table>,
head: Vec<u32>,
tail: Vec<u32>,
next: Vec<u32>,
kept: usize,
hashes: Vec<u64>,
slots: Vec<usize>,
keyed: Vec<bool>,
walk: Walk,
}
impl Lookup {
pub(crate) fn new(rows: usize) -> Result<Self> {
if rows >= NONE as usize {
return Err(Error::out_of_memory(format!(
"a hash join cannot gather more than {} rows on one side",
NONE - 1
)));
}
Ok(Self { next: vec![NONE; rows], ..Self::default() })
}
pub(crate) fn is_empty(&self) -> bool {
self.kept == 0
}
pub(crate) fn footprint(&self) -> u64 {
let table = self.table.as_ref().map_or(0, |table| table.footprint() + table.owned());
let chain =
(self.head.capacity() + self.tail.capacity() + self.next.capacity()) * size_of::<u32>();
let batch = self.hashes.capacity() * size_of::<u64>()
+ self.slots.capacity() * size_of::<usize>()
+ self.keyed.capacity();
table + u64::try_from(chain + batch).unwrap_or(u64::MAX)
}
pub(crate) fn add(
&mut self,
keys: &[Vector],
rows: usize,
base: usize,
nulls: &[bool],
) -> Result<()> {
if rows == 0 {
return Ok(());
}
let Self { table, head, tail, next, kept, hashes, slots, keyed, walk } = self;
let table = table.get_or_insert_with(|| {
let types: Vec<LogicalType> =
keys.iter().map(|key| key.logical_type().clone()).collect();
Table::new(&types)
});
crate::table::hash(keys, rows, hashes, Across::TwoInputs);
which_are_keyed(keys, rows, nulls, keyed);
slots.clear();
slots.resize(rows, MISS);
let mut from = 0;
while from < rows {
let upto = (from + BATCH).min(rows);
table.probe_run(hashes, keys, from, upto, slots, walk);
for &row in walk.pending() {
if !keyed[row] {
continue;
}
match table.probe(hashes[row], keys, row) {
Probe::Found(slot) => slots[row] = slot,
Probe::Vacant(bucket) => {
let slot = table.insert(bucket, hashes[row], keys, row)?;
debug_assert_eq!(
slot,
head.len(),
"a slot is the number of keys before it"
);
head.push(NONE);
tail.push(NONE);
slots[row] = slot;
}
}
}
for row in from..upto {
let slot = slots[row];
if slot == MISS || !keyed[row] {
continue;
}
let at = u32::try_from(base + row).map_err(|_| too_many_rows())?;
if tail[slot] == NONE {
head[slot] = at;
} else {
next[tail[slot] as usize] = at;
}
tail[slot] = at;
*kept += 1;
}
from = upto;
}
Ok(())
}
pub(crate) fn seal(&mut self) {
self.tail = Vec::new();
self.hashes = Vec::new();
self.slots = Vec::new();
self.keyed = Vec::new();
self.walk = Walk::default();
}
pub(crate) fn slots(
&self,
keys: &[Vector],
rows: usize,
nulls: &[bool],
scratch: &mut Scratch,
into: &mut Vec<usize>,
) {
into.clear();
into.resize(rows, MISS);
let Some(table) = self.table.as_ref() else { return };
if rows == 0 {
return;
}
crate::table::hash(keys, rows, &mut scratch.hashes, Across::TwoInputs);
which_are_keyed(keys, rows, nulls, &mut scratch.keyed);
let mut from = 0;
while from < rows {
let upto = (from + BATCH).min(rows);
table.probe_run(&scratch.hashes, keys, from, upto, into, &mut scratch.walk);
from = upto;
}
for (row, &keyed) in scratch.keyed.iter().enumerate().take(rows) {
if !keyed {
into[row] = MISS;
}
}
}
pub(crate) fn matches(&self, slot: usize, into: &mut Vec<u32>) {
into.clear();
if slot == MISS {
return;
}
let mut at = self.head[slot];
while at != NONE {
into.push(at);
at = self.next[at as usize];
}
}
}
#[derive(Debug, Default)]
pub(crate) struct Scratch {
hashes: Vec<u64>,
keyed: Vec<bool>,
walk: Walk,
}
fn which_are_keyed(keys: &[Vector], rows: usize, nulls: &[bool], keyed: &mut Vec<bool>) {
keyed.clear();
keyed.resize(rows, true);
for (column, &stored) in keys.iter().zip(nulls) {
if stored || !has_nulls(column, rows) {
continue;
}
for (row, flag) in keyed.iter_mut().enumerate().take(rows) {
*flag = *flag && !column.is_null_at(row);
}
}
}
pub(crate) fn has_nulls(column: &Vector, rows: usize) -> bool {
match column.form() {
Form::Dictionary | Form::Rle => true,
_ => column.validity().has_nulls(rows),
}
}
fn too_many_rows() -> Error {
Error::out_of_memory(format!(
"a hash join cannot gather more than {} rows on one side",
NONE - 1
))
}
#[cfg(test)]
fn column(values: &[Option<i32>]) -> Vector {
use rudb_common::Value;
let values: Vec<Value> =
values.iter().map(|value| value.map_or(Value::Null, Value::Integer)).collect();
Vector::from_values(LogicalType::Integer, &values).expect("a column of integers")
}
#[cfg(test)]
mod tests {
use super::{Lookup, MISS, Scratch, column};
fn built(values: &[Option<i32>]) -> Lookup {
let mut lookup = Lookup::new(values.len()).expect("a lookup");
lookup.add(&[column(values)], values.len(), 0, &[false]).expect("a build");
lookup.seal();
lookup
}
fn found(lookup: &Lookup, values: &[Option<i32>]) -> Vec<Vec<u32>> {
let mut scratch = Scratch::default();
let mut slots = Vec::new();
lookup.slots(&[column(values)], values.len(), &[false], &mut scratch, &mut slots);
let mut chain = Vec::new();
slots
.iter()
.map(|&slot| {
lookup.matches(slot, &mut chain);
chain.clone()
})
.collect()
}
#[test]
fn a_key_with_no_rows_is_a_miss_and_a_key_with_one_is_that_row() {
let lookup = built(&[Some(10), Some(20)]);
assert_eq!(
found(&lookup, &[Some(20), Some(30), Some(10)]),
vec![vec![1], Vec::new(), vec![0]]
);
}
#[test]
fn a_keys_rows_come_out_in_the_order_the_gathered_side_holds_them() {
let lookup = built(&[Some(7), Some(9), Some(7), Some(7), Some(9)]);
assert_eq!(found(&lookup, &[Some(7), Some(9)]), vec![vec![0, 2, 3], vec![1, 4]]);
}
#[test]
fn a_key_first_seen_twice_inside_one_batch_is_one_key() {
let lookup = built(&[Some(4), Some(4)]);
assert_eq!(found(&lookup, &[Some(4)]), vec![vec![0, 1]]);
}
#[test]
fn a_chain_that_spans_several_batches_stays_in_order() {
let values: Vec<Option<i32>> = (0..500).map(|row| Some(row % 3)).collect();
let lookup = built(&values);
let mut chain = Vec::new();
let mut scratch = Scratch::default();
let mut slots = Vec::new();
lookup.slots(&[column(&[Some(1)])], 1, &[false], &mut scratch, &mut slots);
lookup.matches(slots[0], &mut chain);
let wanted: Vec<u32> = (0..500).filter(|row| row % 3 == 1).collect();
assert_eq!(chain, wanted);
}
#[test]
fn a_rejected_null_is_neither_stored_nor_found() {
let lookup = built(&[Some(1), None, Some(2)]);
let mut scratch = Scratch::default();
let mut slots = Vec::new();
let driving = [Some(1), None];
lookup.slots(&[column(&driving)], 2, &[false], &mut scratch, &mut slots);
assert_ne!(slots[0], MISS, "a driving row with a key finds it");
assert_eq!(slots[1], MISS, "a driving row whose key is null finds nothing");
}
#[test]
fn a_null_a_join_calls_a_value_is_stored_and_found() {
let values = [Some(1), None, None];
let mut lookup = Lookup::new(values.len()).expect("a lookup");
lookup.add(&[column(&values)], values.len(), 0, &[true]).expect("a build");
lookup.seal();
let mut scratch = Scratch::default();
let mut slots = Vec::new();
let driving = [None];
lookup.slots(&[column(&driving)], 1, &[true], &mut scratch, &mut slots);
let mut chain = Vec::new();
lookup.matches(slots[0], &mut chain);
assert_eq!(chain, vec![1, 2]);
}
#[test]
fn a_gathered_side_with_nothing_keyed_in_it_is_empty() {
assert!(Lookup::new(0).expect("a lookup").is_empty());
assert!(built(&[None, None]).is_empty(), "every row's key was a rejected null");
assert!(!built(&[Some(1)]).is_empty());
}
#[test]
fn sealing_gives_back_what_only_the_build_needed() {
let mut lookup = Lookup::new(4).expect("a lookup");
lookup
.add(&[column(&[Some(1), Some(2), Some(1), Some(3)])], 4, 0, &[false])
.expect("built");
let before = lookup.footprint();
lookup.seal();
assert!(lookup.footprint() < before, "{} is not less than {before}", lookup.footprint());
assert_eq!(found(&lookup, &[Some(1)]), vec![vec![0, 2]], "and it still answers");
}
}