use rudb_common::{Error, Result, Value, interval_micros};
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;
pub(crate) const BATCH: usize = 64;
const HOT: usize = 8 * 1024;
const NOTHING: u64 = 0x9e37_79b9_7f4a_7c15;
#[derive(Debug)]
pub(crate) struct Table {
buckets: Vec<u32>,
columns: Vec<Column>,
hashes: Vec<u64>,
owned: u64,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Probe {
Found(usize),
Vacant(usize),
}
impl Table {
pub(crate) fn new(types: &[rudb_common::LogicalType]) -> Self {
Self {
buckets: vec![EMPTY; FIRST],
columns: types.iter().map(Column::new).collect(),
hashes: Vec::new(),
owned: 0,
}
}
pub(crate) fn len(&self) -> usize {
self.hashes.len()
}
pub(crate) fn hash_of(&self, slot: usize) -> u64 {
self.hashes[slot]
}
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::footprint).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 probe_run(
&self,
hashes: &[u64],
keys: &[Vector],
from: usize,
upto: usize,
slots: &mut [usize],
walk: &mut Walk,
) {
let mask = self.buckets.len() - 1;
walk.pending.clear();
if self.buckets.len() <= HOT {
for row in from..upto {
match self.probe(hashes[row], keys, row) {
Probe::Found(slot) => slots[row] = slot,
Probe::Vacant(_) => walk.pending.push(row),
}
}
return;
}
walk.here.clear();
walk.here.extend((from..upto).map(|row| Step { row, at: (hashes[row] as usize) & mask }));
while !walk.here.is_empty() {
walk.seen.clear();
walk.seen.extend(walk.here.iter().map(|step| self.buckets[step.at]));
walk.same.clear();
walk.same.extend(walk.here.iter().zip(&walk.seen).map(|(step, &slot)| {
slot != EMPTY && self.hashes[slot as usize] == hashes[step.row]
}));
for (at, column) in keys.iter().enumerate() {
self.columns[at].holds_run(&walk.here, &walk.seen, column, &mut walk.same);
}
walk.next.clear();
for ((step, &slot), &same) in walk.here.iter().zip(&walk.seen).zip(&walk.same) {
if slot == EMPTY {
walk.pending.push(step.row);
} else if same {
slots[step.row] = slot as usize;
} else {
walk.next.push(Step { row: step.row, at: (step.at + 1) & mask });
}
}
std::mem::swap(&mut walk.here, &mut walk.next);
}
walk.pending.sort_unstable();
}
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() {
self.owned += self.columns[at].push_from(column, row)?;
}
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() {
if !self.columns[at].holds(slot, column, 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,
ty: &rudb_common::LogicalType,
range: std::ops::Range<usize>,
) -> Result<Vector> {
self.columns[at].vector(ty, range)
}
}
#[derive(Debug, Clone, Copy)]
struct Step {
row: usize,
at: usize,
}
#[derive(Debug, Default)]
pub(crate) struct Walk {
here: Vec<Step>,
next: Vec<Step>,
seen: Vec<u32>,
same: Vec<bool>,
pending: Vec<usize>,
}
impl Walk {
pub(crate) fn pending(&self) -> &[usize] {
&self.pending
}
}
#[derive(Debug)]
struct Column {
valid: Vec<bool>,
data: StoredData,
}
#[derive(Debug)]
enum StoredData {
Integer(Vec<i32>),
BigInt(Vec<i64>),
Varchar(StringColumn),
Other(Vec<Stored>),
}
impl Column {
fn new(ty: &rudb_common::LogicalType) -> Self {
let data = match ty {
rudb_common::LogicalType::Integer => StoredData::Integer(Vec::new()),
rudb_common::LogicalType::BigInt => StoredData::BigInt(Vec::new()),
rudb_common::LogicalType::Varchar => StoredData::Varchar(StringColumn::default()),
_ => StoredData::Other(Vec::new()),
};
Self { valid: Vec::new(), data }
}
fn push(&mut self, value: Value) -> Result<()> {
let present = !matches!(value, Value::Null);
match (&mut self.data, value) {
(StoredData::Integer(values), Value::Integer(value)) => values.push(value),
(StoredData::Integer(values), Value::Null) => values.push(0),
(StoredData::BigInt(values), Value::BigInt(value)) => values.push(value),
(StoredData::BigInt(values), Value::Null) => values.push(0),
(StoredData::Varchar(values), Value::Varchar(value)) => {
values.push(value.as_bytes())?
}
(StoredData::Varchar(values), Value::Null) => values.push(&[])?,
(StoredData::Other(values), value) => values.push(Stored::from(value)),
(_, value) => {
return Err(Error::internal(format!(
"a group key column was given a value of the wrong type: {value:?}"
)));
}
}
self.valid.push(present);
Ok(())
}
fn push_from(&mut self, column: &Vector, row: usize) -> Result<u64> {
if column.is_null_at(row) {
return self.push(Value::Null).map(|()| 0);
}
let taken = match &mut self.data {
StoredData::Integer(values) => {
match column.signed_at(row).and_then(|value| i32::try_from(value).ok()) {
Some(value) => {
values.push(value);
true
}
None => false,
}
}
StoredData::BigInt(values) => {
match column.signed_at(row).and_then(|value| i64::try_from(value).ok()) {
Some(value) => {
values.push(value);
true
}
None => false,
}
}
StoredData::Varchar(values) => match column.bytes_at(row) {
Some(bytes) => {
values.push(bytes)?;
true
}
None => false,
},
StoredData::Other(_) => false,
};
if taken {
self.valid.push(true);
return Ok(0);
}
let value = column.value_at(row);
let owned = if self.stores_payload() { 0 } else { rows::owned(&value) };
self.push(value)?;
Ok(owned)
}
fn footprint(&self) -> usize {
let values = match &self.data {
StoredData::Integer(values) => values.capacity() * size_of::<i32>(),
StoredData::BigInt(values) => values.capacity() * size_of::<i64>(),
StoredData::Varchar(values) => values.footprint(),
StoredData::Other(values) => values.capacity() * size_of::<Stored>(),
};
values + self.valid.capacity().div_ceil(8)
}
fn holds(&self, slot: usize, column: &Vector, row: usize) -> bool {
if !self.valid[slot] {
return column.is_null_at(row);
}
match &self.data {
StoredData::Integer(values) => match column.signed_at(row) {
Some(value) => value == i128::from(values[slot]),
None => same(&Value::Integer(values[slot]), &column.value_at(row)),
},
StoredData::BigInt(values) => match column.signed_at(row) {
Some(value) => value == i128::from(values[slot]),
None => same(&Value::BigInt(values[slot]), &column.value_at(row)),
},
StoredData::Varchar(values) => column.bytes_at(row).map_or_else(
|| same(&Value::Varchar(values.string(slot)), &column.value_at(row)),
|value| value == values.get(slot),
),
StoredData::Other(values) => same(&values[slot].value(), &column.value_at(row)),
}
}
fn holds_run(&self, here: &[Step], seen: &[u32], column: &Vector, same: &mut [bool]) {
let validity = column.validity();
macro_rules! run {
($stored:expr, $values:expr) => {{
let stored = $stored;
let values = $values.as_slice();
for ((step, &slot), flag) in here.iter().zip(seen).zip(same.iter_mut()) {
if !*flag {
continue;
}
let slot = slot as usize;
*flag = match values.get(step.row) {
_ if !self.valid[slot] => !validity.is_valid(step.row),
Some(value) => validity.is_valid(step.row) && *value == stored[slot],
None => self.holds(slot, column, step.row),
};
}
return;
}};
}
if let Some(data) = column.data() {
match (&self.data, data) {
(StoredData::Integer(stored), Data::Int32(values)) => run!(stored, values),
(StoredData::BigInt(stored), Data::Int64(values)) => run!(stored, values),
(StoredData::Varchar(stored), Data::Varlen(strings)) => {
for ((step, &slot), flag) in here.iter().zip(seen).zip(same.iter_mut()) {
if !*flag {
continue;
}
let slot = slot as usize;
*flag = match strings.bytes(step.row) {
_ if !self.valid[slot] => !validity.is_valid(step.row),
Some(bytes) => validity.is_valid(step.row) && bytes == stored.get(slot),
None => self.holds(slot, column, step.row),
};
}
return;
}
_ => {}
}
}
for ((step, &slot), flag) in here.iter().zip(seen).zip(same.iter_mut()) {
if *flag {
*flag = self.holds(slot as usize, column, step.row);
}
}
}
fn vector(
&self,
ty: &rudb_common::LogicalType,
range: std::ops::Range<usize>,
) -> Result<Vector> {
let (start, len) = (range.start, range.len());
let data = match &self.data {
StoredData::Integer(values) => Data::Int32(values[range.clone()].to_vec().into()),
StoredData::BigInt(values) => Data::Int64(values[range.clone()].to_vec().into()),
StoredData::Varchar(_) | StoredData::Other(_) => {
return Vector::from_values(ty.clone(), &self.values(range));
}
};
let valid = &self.valid;
let validity = rudb_vector::Validity::from_iter(len, |index| valid[start + index]);
Ok(Vector::flat(ty.clone(), data)?.with_validity(validity))
}
fn values(&self, range: std::ops::Range<usize>) -> Vec<Value> {
range
.map(|slot| {
if !self.valid[slot] {
return Value::Null;
}
match &self.data {
StoredData::Integer(values) => Value::Integer(values[slot]),
StoredData::BigInt(values) => Value::BigInt(values[slot]),
StoredData::Varchar(values) => Value::Varchar(values.string(slot)),
StoredData::Other(values) => values[slot].value(),
}
})
.collect()
}
fn stores_payload(&self) -> bool {
matches!(self.data, StoredData::Varchar(_))
}
}
#[derive(Debug, Default)]
struct StringColumn {
bytes: Vec<u8>,
ends: Vec<u32>,
}
impl StringColumn {
fn push(&mut self, value: &[u8]) -> Result<()> {
let length = self.bytes.len().checked_add(value.len()).ok_or_else(|| {
Error::out_of_memory("an aggregate partition's string keys are too large")
})?;
let end = u32::try_from(length).map_err(|_| {
Error::out_of_memory("one aggregate partition holds more than 4 GiB of string keys")
})?;
self.bytes.extend_from_slice(value);
self.ends.push(end);
Ok(())
}
fn get(&self, slot: usize) -> &[u8] {
let start = slot.checked_sub(1).map_or(0, |before| self.ends[before]) as usize;
&self.bytes[start..self.ends[slot] as usize]
}
fn string(&self, slot: usize) -> String {
String::from_utf8(self.get(slot).to_vec()).expect("a VARCHAR group key is valid UTF-8")
}
fn footprint(&self) -> usize {
self.bytes.capacity() + self.ends.capacity() * size_of::<u32>()
}
}
#[derive(Debug, Clone)]
enum Stored {
Null,
Boolean(bool),
TinyInt(i8),
SmallInt(i16),
Integer(i32),
BigInt(i64),
HugeInt(i128),
UTinyInt(u8),
USmallInt(u16),
UInteger(u32),
UBigInt(u64),
UHugeInt(u128),
Float(f32),
Double(f64),
Decimal { unscaled: i128, width: u8, scale: u8 },
Varchar(String),
Blob(Vec<u8>),
Date(i32),
Time(i64),
Timestamp(i64),
Interval { months: i32, days: i32, micros: i64 },
Other(Box<Value>),
}
impl From<Value> for Stored {
fn from(value: Value) -> Self {
match value {
Value::Null => Self::Null,
Value::Boolean(v) => Self::Boolean(v),
Value::TinyInt(v) => Self::TinyInt(v),
Value::SmallInt(v) => Self::SmallInt(v),
Value::Integer(v) => Self::Integer(v),
Value::BigInt(v) => Self::BigInt(v),
Value::HugeInt(v) => Self::HugeInt(v),
Value::UTinyInt(v) => Self::UTinyInt(v),
Value::USmallInt(v) => Self::USmallInt(v),
Value::UInteger(v) => Self::UInteger(v),
Value::UBigInt(v) => Self::UBigInt(v),
Value::UHugeInt(v) => Self::UHugeInt(v),
Value::Float(v) => Self::Float(v),
Value::Double(v) => Self::Double(v),
Value::Decimal { unscaled, width, scale } => Self::Decimal { unscaled, width, scale },
Value::Varchar(v) => Self::Varchar(v),
Value::Blob(v) => Self::Blob(v),
Value::Date(v) => Self::Date(v),
Value::Time(v) => Self::Time(v),
Value::Timestamp(v) => Self::Timestamp(v),
Value::Interval { months, days, micros } => Self::Interval { months, days, micros },
other => Self::Other(Box::new(other)),
}
}
}
impl Stored {
fn value(&self) -> Value {
match self {
Self::Null => Value::Null,
Self::Boolean(v) => Value::Boolean(*v),
Self::TinyInt(v) => Value::TinyInt(*v),
Self::SmallInt(v) => Value::SmallInt(*v),
Self::Integer(v) => Value::Integer(*v),
Self::BigInt(v) => Value::BigInt(*v),
Self::HugeInt(v) => Value::HugeInt(*v),
Self::UTinyInt(v) => Value::UTinyInt(*v),
Self::USmallInt(v) => Value::USmallInt(*v),
Self::UInteger(v) => Value::UInteger(*v),
Self::UBigInt(v) => Value::UBigInt(*v),
Self::UHugeInt(v) => Value::UHugeInt(*v),
Self::Float(v) => Value::Float(*v),
Self::Double(v) => Value::Double(*v),
Self::Decimal { unscaled, width, scale } => {
Value::Decimal { unscaled: *unscaled, width: *width, scale: *scale }
}
Self::Varchar(v) => Value::Varchar(v.clone()),
Self::Blob(v) => Value::Blob(v.clone()),
Self::Date(v) => Value::Date(*v),
Self::Time(v) => Value::Time(*v),
Self::Timestamp(v) => Value::Timestamp(*v),
Self::Interval { months, days, micros } => {
Value::Interval { months: *months, days: *days, micros: *micros }
}
Self::Other(v) => (**v).clone(),
}
}
}
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 = if column.logical_type() == &rudb_common::LogicalType::Varchar {
match column.bytes_at(row) {
Some(bytes) => mix(*state, bytes_word(bytes)),
None => mix(*state, NOTHING),
}
} else {
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),
Value::Interval { months, days, micros } => {
let length = interval_micros(*months, *days, *micros);
mix(mix(state, length as u64), (length >> 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::*;
#[test]
fn a_stored_group_key_is_narrower_than_a_general_recursive_value() {
assert!(size_of::<Stored>() < size_of::<Value>());
}
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));
}
fn one_at_a_time(keys: &[Vector], rows: usize, types: &[LogicalType]) -> (Table, Vec<usize>) {
let mut table = Table::new(types);
let mut hashes = Vec::new();
hash(keys, rows, &mut hashes);
let mut slots = Vec::new();
for (row, &hash) in hashes.iter().enumerate() {
slots.push(match table.probe(hash, keys, row) {
Probe::Found(slot) => slot,
Probe::Vacant(bucket) => {
table.insert(bucket, hash, keys, row).expect("room for this group")
}
});
}
(table, slots)
}
fn a_batch_at_a_time(
keys: &[Vector],
rows: usize,
types: &[LogicalType],
) -> (Table, Vec<usize>) {
let mut table = Table::new(types);
let mut hashes = Vec::new();
hash(keys, rows, &mut hashes);
let mut slots = vec![usize::MAX; rows];
let mut walk = Walk::default();
let mut from = 0;
while from < rows {
let upto = (from + BATCH).min(rows);
table.probe_run(&hashes, keys, from, upto, &mut slots, &mut walk);
from = upto;
for &row in walk.pending() {
slots[row] = match table.probe(hashes[row], keys, row) {
Probe::Found(slot) => slot,
Probe::Vacant(bucket) => {
table.insert(bucket, hashes[row], keys, row).expect("room for this group")
}
};
}
}
(table, slots)
}
#[test]
fn a_batch_at_a_time_finds_the_groups_one_at_a_time_found_in_the_order_it_found_them() {
let values: Vec<Value> = (0..40_000)
.map(|row: i64| match row % 97 {
0 => Value::Null,
_ => Value::BigInt((row * 7919) % 12_007),
})
.collect();
let keys = [flat(LogicalType::BigInt, &values)];
let types = [LogicalType::BigInt];
let (was, before) = one_at_a_time(&keys, values.len(), &types);
let (now, after) = a_batch_at_a_time(&keys, values.len(), &types);
assert_eq!(before, after);
assert_eq!(was.len(), now.len());
assert!(now.buckets.len() > HOT, "the test has to reach the batched path");
}
fn dictionary_of(ty: LogicalType, seen: &[Value], codes: Vec<u32>, values: &[Value]) -> Vector {
let valid =
rudb_vector::Validity::from_iter(values.len(), |row| values[row] != Value::Null);
Vector::dictionary(codes, flat(ty, seen))
.expect("a dictionary of those values")
.with_validity(valid)
}
#[test]
fn the_batched_key_compare_agrees_with_the_one_at_a_time_one_on_every_form() {
let rows = 30_000;
let number = |row: i64| (row * 7919) % 5003;
let word = |row: i64| (row * 104_729) % 4001;
let numbers: Vec<Value> = (0..rows)
.map(|row| match row % 61 {
0 => Value::Null,
_ => Value::Integer(number(row) as i32),
})
.collect();
let words: Vec<Value> = (0..rows)
.map(|row| match row % 37 {
0 => Value::Null,
_ => Value::Varchar(format!("row {}", word(row))),
})
.collect();
let types = [LogicalType::Integer, LogicalType::Varchar];
let flatly = [flat(LogicalType::Integer, &numbers), flat(LogicalType::Varchar, &words)];
let (was, before) = one_at_a_time(&flatly, numbers.len(), &types);
let (now, after) = a_batch_at_a_time(&flatly, numbers.len(), &types);
assert_eq!(before, after);
assert_eq!(was.len(), now.len());
assert!(now.buckets.len() > HOT, "the test has to reach the batched path");
let digits: Vec<Value> = (0..5003).map(Value::Integer).collect();
let phrases: Vec<Value> = (0..4001).map(|at| Value::Varchar(format!("row {at}"))).collect();
let indirect = [
dictionary_of(
LogicalType::Integer,
&digits,
(0..rows).map(|row| number(row) as u32).collect(),
&numbers,
),
dictionary_of(
LogicalType::Varchar,
&phrases,
(0..rows).map(|row| word(row) as u32).collect(),
&words,
),
];
let (_, through) = a_batch_at_a_time(&indirect, numbers.len(), &types);
assert_eq!(before, through);
}
#[test]
fn rows_of_one_new_group_in_one_batch_get_one_slot() {
let values = vec![Value::Integer(4); BATCH * 3];
let keys = [flat(LogicalType::Integer, &values)];
let types = [LogicalType::Integer];
let (table, slots) = a_batch_at_a_time(&keys, values.len(), &types);
assert_eq!(table.len(), 1);
assert!(slots.iter().all(|&slot| slot == 0));
}
#[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(&[LogicalType::Varchar, LogicalType::Integer]);
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 two_null_rows_are_one_group_when_they_arrive_behind_a_dictionary() {
let values = flat(LogicalType::Integer, &[Value::Null, Value::Integer(1)]);
let keys = [Vector::dictionary(vec![0, 0, 1], values).expect("a dictionary of those rows")];
let (table, slots) = one_at_a_time(&keys, 3, &[LogicalType::Integer]);
assert_eq!(slots, [0, 0, 1], "the two nulls did not find each other");
assert_eq!(table.len(), 2);
}
#[test]
fn a_null_group_does_not_take_a_row_that_has_a_value_behind_a_dictionary() {
let values = flat(LogicalType::Varchar, &[Value::Null, Value::Varchar("ada".into())]);
let keys = [Vector::dictionary(vec![0, 1, 0], values).expect("a dictionary of those rows")];
let (table, slots) = one_at_a_time(&keys, 3, &[LogicalType::Varchar]);
assert_eq!(slots, [0, 1, 0]);
assert_eq!(table.len(), 2);
}
#[test]
fn a_group_is_found_again_when_its_string_key_arrives_as_a_constant() {
let long = "lovelace, and a string past the sixteen bytes a view holds inline";
for text in ["ada", "", long] {
let names = Vector::constant(LogicalType::Varchar, Value::Varchar(text.into()), 2);
let keys = [names];
let mut hashes = Vec::new();
hash(&keys, 2, &mut hashes);
let mut table = Table::new(&[LogicalType::Varchar]);
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),
"{text:?} did not find itself"
);
assert_eq!(table.len(), 1);
}
}
#[test]
fn two_constants_of_different_strings_are_still_two_groups() {
let ada = Vector::constant(LogicalType::Varchar, Value::Varchar("ada".into()), 1);
let grace = Vector::constant(LogicalType::Varchar, Value::Varchar("grace".into()), 1);
let mut first = Vec::new();
let mut second = Vec::new();
hash(std::slice::from_ref(&ada), 1, &mut first);
hash(std::slice::from_ref(&grace), 1, &mut second);
let mut table = Table::new(&[LogicalType::Varchar]);
let keys = [ada];
let Probe::Vacant(bucket) = table.probe(first[0], &keys, 0) else {
panic!("an empty table found a group");
};
table.insert(bucket, first[0], &keys, 0).expect("room for one group");
assert!(matches!(table.probe(second[0], &[grace], 0), Probe::Vacant(_)));
}
#[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(&[LogicalType::BigInt]);
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"
);
}
let column = table.column(0, &LogicalType::BigInt, 0..values.len()).expect("a bigint key");
assert_eq!(column.len(), values.len());
assert_eq!(column.value_at(7), Value::BigInt(7));
}
#[test]
fn a_packed_integer_column_groups_the_same_as_the_flat_one_it_stands_for() {
let values: Vec<Value> = (0..256).map(|row| Value::BigInt(row % 7)).collect();
let plain = flat(LogicalType::BigInt, &values);
let packed = plain.bit_packed().expect("a column of seven small values packs");
assert!(packed.signed_at(0).is_none(), "a packed row is not an integer a read can reach");
let mut grouped = Vec::new();
for column in [&plain, &packed] {
let keys = std::slice::from_ref(column);
let hashes = hashed(column);
let mut table = Table::new(&[LogicalType::BigInt]);
let mut slots = Vec::new();
for (row, &one) in hashes.iter().enumerate() {
slots.push(match table.probe(one, keys, row) {
Probe::Found(slot) => slot,
Probe::Vacant(bucket) => table.insert(bucket, one, keys, row).expect("room"),
});
}
assert_eq!(table.len(), 7, "seven distinct keys whichever form they arrived in");
grouped.push(slots);
}
assert_eq!(grouped[0], grouped[1]);
}
#[test]
fn a_key_column_comes_back_as_a_vector_with_its_nulls_where_they_were() {
let values = [Value::BigInt(5), Value::Null, Value::BigInt(9)];
let keys = [flat(LogicalType::BigInt, &values)];
let hashes = hashed(&keys[0]);
let mut table = Table::new(&[LogicalType::BigInt]);
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");
};
table.insert(bucket, one, &keys, row).expect("room");
}
let whole = table.column(0, &LogicalType::BigInt, 0..3).expect("a bigint key");
assert_eq!(whole.value_at(0), Value::BigInt(5));
assert_eq!(whole.value_at(1), Value::Null);
assert_eq!(whole.value_at(2), Value::BigInt(9));
let tail = table.column(0, &LogicalType::BigInt, 1..3).expect("a bigint key");
assert_eq!(tail.len(), 2);
assert_eq!(tail.value_at(0), Value::Null);
assert_eq!(tail.value_at(1), Value::BigInt(9));
}
#[test]
fn common_numeric_keys_keep_their_physical_width() {
let values: Vec<Value> = (0..1000).map(Value::BigInt).collect();
let keys = [flat(LogicalType::BigInt, &values)];
let mut hashes = Vec::new();
hash(&keys, values.len(), &mut hashes);
let mut table = Table::new(&[LogicalType::BigInt]);
for (row, &hash) in hashes.iter().enumerate() {
let Probe::Vacant(bucket) = table.probe(hash, &keys, row) else {
panic!("a unique key was already present");
};
table.insert(bucket, hash, &keys, row).expect("room for the group");
}
let key_bytes = table.columns[0].footprint();
assert!(
key_bytes < values.len() * 9,
"{key_bytes} bytes stored a thousand eight-byte keys and their validity"
);
}
#[test]
fn string_key_bytes_are_counted_in_the_column() {
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(&[LogicalType::Varchar]);
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.footprint() >= long.len() as u64,
"{} bytes do not include the string",
table.footprint()
);
}
}