use rudb_common::{Error, Result, Value};
use rudb_vector::{Data, Vector};
use crate::key::{canonical, mix, same, spread};
use crate::rows;
const EMPTY: u32 = u32::MAX;
const LIMIT: usize = EMPTY as usize;
const FIRST: usize = 64;
const NOTHING: u64 = 0x9e37_79b9_7f4a_7c15;
#[derive(Debug)]
pub(crate) struct Table {
buckets: Vec<u32>,
columns: Vec<Vec<Value>>,
hashes: Vec<u64>,
owned: u64,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Probe {
Found(usize),
Vacant(usize),
}
impl Table {
pub(crate) fn new(columns: usize) -> Self {
Self {
buckets: vec![EMPTY; FIRST],
columns: vec![Vec::new(); columns],
hashes: Vec::new(),
owned: 0,
}
}
pub(crate) fn len(&self) -> usize {
self.hashes.len()
}
pub(crate) fn owned(&self) -> u64 {
self.owned
}
pub(crate) fn footprint(&self) -> u64 {
let buckets = self.buckets.capacity() * size_of::<u32>();
let hashes = self.hashes.capacity() * size_of::<u64>();
let keys: usize =
self.columns.iter().map(|column| column.capacity() * size_of::<Value>()).sum();
u64::try_from(buckets + hashes + keys).unwrap_or(u64::MAX)
}
pub(crate) fn probe(&self, hash: u64, keys: &[Vector], row: usize) -> Probe {
let mask = self.buckets.len() - 1;
let mut at = (hash as usize) & mask;
loop {
let slot = self.buckets[at];
if slot == EMPTY {
return Probe::Vacant(at);
}
let slot = slot as usize;
if self.hashes[slot] == hash && self.holds(slot, keys, row) {
return Probe::Found(slot);
}
at = (at + 1) & mask;
}
}
pub(crate) fn insert(
&mut self,
bucket: usize,
hash: u64,
keys: &[Vector],
row: usize,
) -> Result<usize> {
let slot = self.hashes.len();
if slot >= LIMIT {
return Err(Error::out_of_memory(format!(
"a single group by cannot hold more than {LIMIT} groups"
)));
}
for (at, column) in keys.iter().enumerate() {
let value = column.value_at(row);
self.owned += rows::owned(&value);
self.columns[at].push(value);
}
self.hashes.push(hash);
self.buckets[bucket] = slot as u32;
if self.hashes.len() * 2 >= self.buckets.len() {
self.regrow();
}
Ok(slot)
}
fn holds(&self, slot: usize, keys: &[Vector], row: usize) -> bool {
for (at, column) in keys.iter().enumerate() {
let stored = &self.columns[at][slot];
if let Value::Varchar(text) = stored {
if column.text_at(row) != Some(text.as_str()) {
return false;
}
continue;
}
if !same(stored, &column.value_at(row)) {
return false;
}
}
true
}
fn regrow(&mut self) {
let mut buckets = vec![EMPTY; self.buckets.len() * 2];
let mask = buckets.len() - 1;
for (slot, &hash) in self.hashes.iter().enumerate() {
let mut at = (hash as usize) & mask;
while buckets[at] != EMPTY {
at = (at + 1) & mask;
}
buckets[at] = slot as u32;
}
self.buckets = buckets;
}
pub(crate) fn column(&self, at: usize) -> &[Value] {
&self.columns[at]
}
}
pub(crate) fn hash(keys: &[Vector], rows: usize, hashes: &mut Vec<u64>) {
hashes.clear();
hashes.resize(rows, 0);
for column in keys {
fold(column, rows, hashes);
}
for state in hashes.iter_mut() {
*state = spread(*state);
}
}
fn fold(column: &Vector, rows: usize, hashes: &mut [u64]) {
let validity = column.validity();
macro_rules! run {
($values:expr, $word:expr) => {{
let values = $values.as_slice();
let word = $word;
for (row, state) in hashes.iter_mut().enumerate().take(rows) {
let one = match values.get(row) {
Some(value) if validity.is_valid(row) => word(*value),
_ => NOTHING,
};
*state = mix(*state, one);
}
return;
}};
}
if let Some(data) = column.data() {
match data {
Data::Bool(values) => run!(values, |x: bool| u64::from(x)),
Data::Int8(values) => run!(values, |x: i8| i64::from(x) as u64),
Data::Int16(values) => run!(values, |x: i16| i64::from(x) as u64),
Data::Int32(values) => run!(values, |x: i32| i64::from(x) as u64),
Data::Int64(values) => run!(values, |x: i64| x as u64),
Data::UInt8(values) => run!(values, |x: u8| u64::from(x)),
Data::UInt16(values) => run!(values, |x: u16| u64::from(x)),
Data::UInt32(values) => run!(values, |x: u32| u64::from(x)),
Data::UInt64(values) => run!(values, |x: u64| x),
Data::Float32(values) => run!(values, |x: f32| canonical(f64::from(x))),
Data::Float64(values) => run!(values, canonical),
Data::Varlen(strings) => {
for (row, state) in hashes.iter_mut().enumerate().take(rows) {
let one = match strings.bytes(row) {
Some(bytes) if validity.is_valid(row) => bytes_word(bytes),
_ => NOTHING,
};
*state = mix(*state, one);
}
return;
}
_ => {}
}
}
for (row, state) in hashes.iter_mut().enumerate().take(rows) {
*state = match column.text_at(row) {
Some(text) => mix(*state, bytes_word(text.as_bytes())),
None => fold_value(*state, &column.value_at(row)),
};
}
}
fn fold_value(state: u64, value: &Value) -> u64 {
match value {
Value::Null => mix(state, NOTHING),
Value::Boolean(x) => mix(state, u64::from(*x)),
Value::TinyInt(x) => mix(state, i64::from(*x) as u64),
Value::SmallInt(x) => mix(state, i64::from(*x) as u64),
Value::Integer(x) | Value::Date(x) => mix(state, i64::from(*x) as u64),
Value::BigInt(x) | Value::Time(x) | Value::Timestamp(x) => mix(state, *x as u64),
Value::UTinyInt(x) => mix(state, u64::from(*x)),
Value::USmallInt(x) => mix(state, u64::from(*x)),
Value::UInteger(x) => mix(state, u64::from(*x)),
Value::UBigInt(x) => mix(state, *x),
Value::Float(x) => mix(state, canonical(f64::from(*x))),
Value::Double(x) => mix(state, canonical(*x)),
Value::Varchar(x) => mix(state, bytes_word(x.as_bytes())),
Value::Blob(x) => mix(state, bytes_word(x)),
Value::HugeInt(x) | Value::Decimal { unscaled: x, .. } => {
mix(mix(state, *x as u64), (*x >> 64) as u64)
}
Value::UHugeInt(x) => mix(mix(state, *x as u64), (*x >> 64) as u64),
other => mix(state, bytes_word(other.to_string().as_bytes())),
}
}
fn bytes_word(bytes: &[u8]) -> u64 {
let mut state = 0u64;
let mut words = bytes.chunks_exact(8);
for word in &mut words {
state = mix(state, u64::from_le_bytes(word.try_into().unwrap_or([0; 8])));
}
let rest = words.remainder();
if !rest.is_empty() {
let mut last = [0; 8];
last[..rest.len()].copy_from_slice(rest);
state = mix(state, u64::from_le_bytes(last));
}
mix(state, bytes.len() as u64)
}
#[cfg(test)]
mod tests {
use rudb_common::LogicalType;
use super::*;
fn hashed(column: &Vector) -> Vec<u64> {
let mut hashes = Vec::new();
hash(std::slice::from_ref(column), column.len(), &mut hashes);
hashes
}
fn flat(ty: LogicalType, values: &[Value]) -> Vector {
Vector::from_values(ty, values).expect("a flat vector of these values")
}
#[test]
fn a_dictionary_hashes_the_same_as_the_flat_column_it_stands_for() {
let long = "lovelace, and a string past the sixteen bytes a view holds inline";
let values = [
Value::Varchar("ada".into()),
Value::Varchar(String::new()),
Value::Null,
Value::Varchar(long.into()),
];
let plain = flat(LogicalType::Varchar, &values);
let distinct = flat(LogicalType::Varchar, &values);
let dictionary =
Vector::dictionary(vec![0, 1, 2, 3], distinct).expect("a dictionary of those values");
assert_eq!(hashed(&plain), hashed(&dictionary));
}
#[test]
fn a_constant_and_a_sequence_hash_the_same_as_the_values_they_stand_for() {
let constant = Vector::constant(LogicalType::Integer, Value::Integer(7), 3);
let plain = flat(LogicalType::Integer, &vec![Value::Integer(7); 3]);
assert_eq!(hashed(&constant), hashed(&plain));
let sequence = Vector::sequence(10, 2, 4);
let counted = flat(
LogicalType::BigInt,
&[Value::BigInt(10), Value::BigInt(12), Value::BigInt(14), Value::BigInt(16)],
);
assert_eq!(hashed(&sequence), hashed(&counted));
}
#[test]
fn the_floats_that_group_together_hash_together() {
let left = flat(LogicalType::Double, &[Value::Double(f64::NAN), Value::Double(0.0)]);
let right = flat(LogicalType::Double, &[Value::Double(-f64::NAN), Value::Double(-0.0)]);
assert_eq!(hashed(&left), hashed(&right));
}
#[test]
fn a_null_does_not_hash_as_a_zero() {
let nulls = flat(LogicalType::BigInt, &[Value::Null]);
let zeroes = flat(LogicalType::BigInt, &[Value::BigInt(0)]);
assert_ne!(hashed(&nulls), hashed(&zeroes));
}
#[test]
fn the_same_values_in_a_different_order_hash_apart() {
let ones = flat(LogicalType::Integer, &[Value::Integer(1)]);
let twos = flat(LogicalType::Integer, &[Value::Integer(2)]);
let mut forwards = Vec::new();
let mut backwards = Vec::new();
hash(&[ones.clone(), twos.clone()], 1, &mut forwards);
hash(&[twos, ones], 1, &mut backwards);
assert_ne!(forwards, backwards);
}
#[test]
fn a_key_that_has_been_seen_is_found_and_a_new_one_is_not() {
let names = flat(
LogicalType::Varchar,
&[Value::Varchar("ada".into()), Value::Varchar("ada".into()), Value::Null],
);
let numbers =
flat(LogicalType::Integer, &[Value::Integer(1), Value::Integer(1), Value::Integer(1)]);
let keys = [names, numbers];
let mut hashes = Vec::new();
hash(&keys, 3, &mut hashes);
let mut table = Table::new(2);
let Probe::Vacant(bucket) = table.probe(hashes[0], &keys, 0) else {
panic!("an empty table found a group");
};
let slot = table.insert(bucket, hashes[0], &keys, 0).expect("room for one group");
assert!(matches!(table.probe(hashes[1], &keys, 1), Probe::Found(found) if found == slot));
assert!(matches!(table.probe(hashes[2], &keys, 2), Probe::Vacant(_)));
assert_eq!(table.len(), 1);
}
#[test]
fn every_group_is_still_found_after_the_buckets_have_doubled() {
let values: Vec<Value> = (0..1000).map(Value::BigInt).collect();
let column = flat(LogicalType::BigInt, &values);
let keys = [column];
let mut hashes = Vec::new();
hash(&keys, values.len(), &mut hashes);
let mut table = Table::new(1);
for (row, &one) in hashes.iter().enumerate() {
let Probe::Vacant(bucket) = table.probe(one, &keys, row) else {
panic!("row {row} was found before it was inserted");
};
let slot = table.insert(bucket, one, &keys, row).expect("room");
assert_eq!(slot, row);
}
assert_eq!(table.len(), values.len());
for (row, &one) in hashes.iter().enumerate() {
assert!(
matches!(table.probe(one, &keys, row), Probe::Found(slot) if slot == row),
"row {row} was lost by a rehash"
);
}
assert_eq!(table.column(0).len(), values.len());
assert_eq!(table.column(0)[7], Value::BigInt(7));
}
#[test]
fn what_the_keys_own_is_counted() {
let long = "a string well past the sixteen bytes a view holds inline".to_string();
let column = flat(LogicalType::Varchar, &[Value::Varchar(long.clone())]);
let keys = [column];
let mut hashes = Vec::new();
hash(&keys, 1, &mut hashes);
let mut table = Table::new(1);
assert_eq!(table.owned(), 0);
let Probe::Vacant(bucket) = table.probe(hashes[0], &keys, 0) else {
panic!("an empty table found a group");
};
table.insert(bucket, hashes[0], &keys, 0).expect("room");
assert!(table.owned() >= long.len() as u64, "{} is not the string", table.owned());
}
}