use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use rudb_common::{Cancel, Error, LogicalType, Result};
use rudb_pipeline::Lease;
use rudb_vector::{Data, Form, Vector};
use crate::pairs::in_parallel;
use crate::table::{BATCH, Probe, Table, Walk};
pub(crate) const NONE: u32 = u32::MAX;
const PLACES: u64 = 4;
const RANKED: u64 = 256;
const SPLIT: usize = 64 * 1024;
pub(crate) const MISS: usize = usize::MAX;
#[derive(Debug)]
struct Part {
table: Table,
base: usize,
}
#[derive(Debug, Default)]
pub(crate) struct Lookup {
parts: Vec<Part>,
bits: u32,
head: Vec<u32>,
next: Vec<AtomicU32>,
kept: usize,
distinct: usize,
low: Option<i64>,
ranked: Option<Ranked>,
ordered: bool,
}
#[derive(Debug, Default)]
struct Ranked {
words: Vec<u64>,
before: Vec<u32>,
keys: usize,
}
impl Ranked {
fn new(places: usize, rows: impl Iterator<Item = usize>) -> Self {
let mut words = vec![0u64; places.div_ceil(64)];
for place in rows {
words[place / 64] |= 1 << (place % 64);
}
Self::counted(words)
}
fn ascending(
places: usize,
keys: &[i64],
low: i64,
size: usize,
threads: &Lease<'_>,
) -> Result<Self> {
let words: Vec<AtomicU64> = (0..places.div_ceil(64)).map(|_| AtomicU64::new(0)).collect();
let slices = keys.len().div_ceil(size);
let set = |slice: usize| -> Result<()> {
let within = &keys[slice * size..((slice + 1) * size).min(keys.len())];
let (mut at, mut bits) = (usize::MAX, 0u64);
for &key in within {
let place = key.wrapping_sub(low) as usize;
if place / 64 != at {
if bits != 0 {
words[at].fetch_or(bits, Ordering::Relaxed);
}
(at, bits) = (place / 64, 0);
}
bits |= 1 << (place % 64);
}
if bits != 0 {
words[at].fetch_or(bits, Ordering::Relaxed);
}
Ok(())
};
in_parallel(threads, slices, slices, "join index places", set)?;
Ok(Self::counted(words.into_iter().map(AtomicU64::into_inner).collect()))
}
fn counted(words: Vec<u64>) -> Self {
let mut before = Vec::with_capacity(words.len());
let mut count = 0u32;
for word in &words {
before.push(count);
count += word.count_ones();
}
Self { words, before, keys: count as usize }
}
fn slot(&self, place: u64) -> Option<usize> {
let word = usize::try_from(place / 64).ok()?;
let bits = *self.words.get(word)?;
let below = bits & ((1u64 << (place % 64)) - 1);
(bits >> (place % 64) & 1 == 1)
.then(|| self.before[word] as usize + below.count_ones() as usize)
}
fn below(&self, place: usize) -> usize {
self.before.get(place / 64).map_or(self.keys, |&count| count as usize)
}
fn footprint(&self) -> usize {
self.words.capacity() * size_of::<u64>() + self.before.capacity() * size_of::<u32>()
}
}
impl Lookup {
pub(crate) fn build(
keys: &[Vector],
rows: usize,
nulls: &[bool],
threads: &Lease<'_>,
cancel: &Cancel,
) -> Result<Self> {
if rows >= NONE as usize {
return Err(too_many_rows());
}
if rows == 0 || keys.is_empty() {
return Ok(Self::default());
}
let mut keyed = Vec::new();
which_are_keyed(keys, rows, nulls, &mut keyed);
if let Some(direct) = Self::direct(keys, rows, nulls, &keyed, threads, cancel)? {
return Ok(direct);
}
let mut hashes = Vec::new();
crate::table::hash(keys, rows, &mut hashes, crate::table::Across::TwoInputs);
let bits = split_into(rows, threads.degree());
let count = 1usize << bits;
let next: Vec<AtomicU32> = (0..rows).map(|_| AtomicU32::new(NONE)).collect();
let types: Vec<LogicalType> = keys.iter().map(|key| key.logical_type().clone()).collect();
let dealt =
deal(rows, count, threads, |row| keyed[row].then(|| part_of(hashes[row], bits)))?;
let one = |part: usize| -> Result<(Table, Vec<u32>, usize)> {
let mine = dealt.iter().map(|slice| slice[part].as_slice());
fill(mine, &types, keys, &hashes, &next, cancel)
};
let filled = in_parallel(threads, count, threads.degree(), "join table partition", one)?;
let mut parts = Vec::with_capacity(count);
let mut head = Vec::new();
let mut kept = 0;
for (table, mine, held) in filled {
parts.push(Part { base: head.len(), table });
head.extend(mine);
kept += held;
}
let distinct = head.len();
Ok(Self {
parts,
bits,
head,
next,
kept,
distinct,
low: None,
ranked: None,
ordered: false,
})
}
fn direct(
keys: &[Vector],
rows: usize,
nulls: &[bool],
keyed: &[bool],
threads: &Lease<'_>,
cancel: &Cancel,
) -> Result<Option<Self>> {
let ([key], [false]) = (keys, nulls) else { return Ok(None) };
if !integer(key.logical_type()) {
return Ok(None);
}
let mut owned = Vec::new();
let block: &[i64] = match key.data() {
Some(Data::Int64(values)) if key.form() == Form::Flat && values.len() >= rows => {
&values.as_slice()[..rows]
}
_ => {
if !key.signed_block(&mut owned) || owned.len() < rows {
return Ok(None);
}
&owned[..rows]
}
};
let slices = slices_of(rows, threads.degree());
let size = rows.div_ceil(slices);
let range = |slice: usize| -> Result<(i64, i64, bool)> {
let (mut low, mut high) = (i64::MAX, i64::MIN);
let within = slice * size..((slice + 1) * size).min(rows);
let mut ascending = true;
let mut last = None;
for (&value, &keyed) in block[within.clone()].iter().zip(&keyed[within]) {
if keyed {
low = low.min(value);
high = high.max(value);
ascending &= last.is_none_or(|last| last < value);
last = Some(value);
} else {
ascending = false;
}
}
Ok((low, high, ascending))
};
let ranges = in_parallel(threads, slices, slices, "join index range", range)?;
let low = ranges.iter().map(|&(low, _, _)| low).min().unwrap_or(i64::MAX);
let high = ranges.iter().map(|&(_, high, _)| high).max().unwrap_or(i64::MIN);
let held: Vec<_> = ranges.iter().filter(|&&(low, high, _)| low <= high).collect();
let ordered = ranges.iter().all(|&(_, _, ascending)| ascending)
&& held.windows(2).all(|pair| pair[0].1 < pair[1].0);
if low > high {
return Ok(None);
}
let Ok(places) = u64::try_from(i128::from(high) - i128::from(low) + 1) else {
return Ok(None);
};
if places > (rows as u64).saturating_mul(RANKED) || places >= u64::from(NONE) {
return Ok(None);
}
cancel.check()?;
let places = places as usize;
if ordered {
return Self::ordered(block, low, places, size, threads).map(Some);
}
let place_of = |row: usize| block[row].wrapping_sub(low) as usize;
let ranked = (places as u64 > (rows as u64).saturating_mul(PLACES))
.then(|| Ranked::new(places, (0..rows).filter(|&row| keyed[row]).map(place_of)));
let slot_of = |row: usize| match &ranked {
Some(ranked) => ranked.slot(place_of(row) as u64).unwrap_or(0),
None => place_of(row),
};
let first_at = |place: usize| match &ranked {
Some(ranked) if place < places => ranked.below(place),
Some(ranked) => ranked.keys,
None => place.min(places),
};
let next: Vec<AtomicU32> = (0..rows).map(|_| AtomicU32::new(NONE)).collect();
let count = 1usize << split_into(rows, threads.degree());
let run = places.div_ceil(count).next_power_of_two().max(64);
let shift = run.trailing_zeros();
let count = places.div_ceil(run);
let dealt = deal(rows, count, threads, |row| keyed[row].then(|| place_of(row) >> shift))?;
let mut head = vec![0u32; first_at(places)];
let mut shares: Vec<Mutex<&mut [u32]>> = Vec::with_capacity(count);
let mut rest = head.as_mut_slice();
for part in 0..count {
let (mine, after) =
rest.split_at_mut(first_at((part + 1) * run) - first_at(part * run));
shares.push(Mutex::new(mine));
rest = after;
}
let one = |part: usize| -> Result<usize> {
let base = first_at(part * run);
let mut mine = shares[part].lock().map_err(|_| Error::internal("a poisoned share"))?;
mine.fill(0);
let mut distinct = 0;
for slice in dealt.iter().rev() {
for &row in slice[part].iter().rev() {
let place = slot_of(row) - base;
let first = mine[place];
distinct += usize::from(first == 0);
next[row].store(first.wrapping_sub(1), Ordering::Relaxed);
mine[place] = row as u32 + 1;
}
}
Ok(distinct)
};
let distinct = in_parallel(threads, count, threads.degree(), "join index partition", one)?
.into_iter()
.sum();
drop(shares);
let kept = dealt.iter().flatten().map(Vec::len).sum();
Ok(Some(Self {
parts: Vec::new(),
bits: 0,
head,
next,
kept,
distinct,
low: Some(low),
ranked,
ordered: false,
}))
}
fn ordered(
keys: &[i64],
low: i64,
places: usize,
size: usize,
threads: &Lease<'_>,
) -> Result<Self> {
let rows = keys.len();
let ranked = (places != rows)
.then(|| Ranked::ascending(places, keys, low, size, threads))
.transpose()?;
Ok(Self {
parts: Vec::new(),
bits: 0,
head: Vec::new(),
next: Vec::new(),
kept: rows,
distinct: rows,
low: Some(low),
ranked,
ordered: true,
})
}
#[cfg(test)]
pub(crate) fn is_ordered(&self) -> bool {
self.ordered
}
pub(crate) fn is_empty(&self) -> bool {
self.kept == 0
}
pub(crate) fn footprint(&self) -> u64 {
let tables: u64 =
self.parts.iter().map(|part| part.table.footprint() + part.table.owned()).sum();
let chain = self.head.capacity() * size_of::<u32>()
+ self.next.capacity() * size_of::<AtomicU32>()
+ self.ranked.as_ref().map_or(0, Ranked::footprint);
tables + u64::try_from(chain).unwrap_or(u64::MAX)
}
pub(crate) fn slots(
&self,
keys: &[Vector],
rows: usize,
nulls: &[bool],
scratch: &mut Scratch,
into: &mut Vec<usize>,
) {
into.clear();
into.resize(rows, MISS);
if let Some(low) = self.low {
self.places(low, keys, rows, nulls, scratch, into);
return;
}
if rows == 0 || self.parts.is_empty() {
return;
}
crate::table::hash(keys, rows, &mut scratch.hashes, crate::table::Across::TwoInputs);
which_are_keyed(keys, rows, nulls, &mut scratch.keyed);
if self.parts.len() == 1 {
let mut from = 0;
while from < rows {
let upto = (from + BATCH).min(rows);
self.parts[0].table.probe_run(
&scratch.hashes,
keys,
from,
upto,
into,
&mut scratch.walk,
);
from = upto;
}
} else {
self.deal(keys, rows, scratch, into);
}
for (row, &keyed) in scratch.keyed.iter().enumerate().take(rows) {
if !keyed {
into[row] = MISS;
}
}
}
fn places(
&self,
low: i64,
keys: &[Vector],
rows: usize,
nulls: &[bool],
scratch: &mut Scratch,
into: &mut [usize],
) {
let [key] = keys else { return };
which_are_keyed(keys, rows, nulls, &mut scratch.keyed);
let places = self.slot_count() as u64;
let ranked = self.ranked.as_ref();
let hit = |value: i64| {
let place = value.wrapping_sub(low) as u64;
match ranked {
Some(ranked) => ranked.slot(place),
None => (place < places && (self.ordered || self.head[place as usize] != 0))
.then_some(place as usize),
}
};
if key.signed_block(&mut scratch.block) && scratch.block.len() >= rows {
for (row, &value) in scratch.block[..rows].iter().enumerate() {
if scratch.keyed[row] {
into[row] = hit(value).unwrap_or(MISS);
}
}
return;
}
for (row, slot) in into.iter_mut().enumerate().take(rows) {
if scratch.keyed[row] {
let value = key.signed_at(row).and_then(|value| i64::try_from(value).ok());
*slot = value.and_then(hit).unwrap_or(MISS);
}
}
}
fn deal(&self, keys: &[Vector], rows: usize, scratch: &mut Scratch, into: &mut [usize]) {
scratch.by_part.resize_with(self.parts.len(), Vec::new);
for held in &mut scratch.by_part {
held.clear();
}
for row in 0..rows {
if scratch.keyed[row] {
scratch.by_part[part_of(scratch.hashes[row], self.bits)].push(row);
}
}
for (part, mine) in self.parts.iter().zip(&scratch.by_part) {
let mut from = 0;
while from < mine.len() {
let upto = (from + BATCH).min(mine.len());
let batch = &mine[from..upto];
scratch.found.clear();
scratch.found.resize(batch.len(), MISS);
part.table.probe_these(
&scratch.hashes,
keys,
batch,
&mut scratch.found,
&mut scratch.walk,
);
for (&row, &slot) in batch.iter().zip(&scratch.found) {
if slot != MISS {
into[row] = part.base + slot;
}
}
from = upto;
}
}
}
pub(crate) fn slot_count(&self) -> usize {
if self.ordered { self.distinct } else { self.head.len() }
}
pub(crate) fn single(&self) -> bool {
self.distinct == self.kept
}
pub(crate) fn firsts(&self, slots: &[usize], into: &mut Vec<u32>) {
into.clear();
if self.ordered {
into.extend(slots.iter().map(|&slot| u32::try_from(slot).unwrap_or(NONE)));
return;
}
into.extend(
slots
.iter()
.map(|&slot| self.head.get(slot).map_or(NONE, |&first| first.wrapping_sub(1))),
);
}
pub(crate) fn chain_from(&self, first: u32, into: &mut Vec<u32>) {
into.clear();
if self.ordered {
into.extend((first != NONE).then_some(first));
return;
}
let mut at = first;
while at != NONE {
into.push(at);
at = self.next[at as usize].load(Ordering::Relaxed);
}
}
pub(crate) fn matches(&self, slot: usize, into: &mut Vec<u32>) {
into.clear();
if slot == MISS {
return;
}
if self.ordered {
into.push(slot as u32);
return;
}
let mut at = self.head[slot].wrapping_sub(1);
while at != NONE {
into.push(at);
at = self.next[at as usize].load(Ordering::Relaxed);
}
}
}
#[allow(clippy::too_many_arguments)]
fn fill<'r>(
mine: impl Iterator<Item = &'r [usize]>,
types: &[LogicalType],
keys: &[Vector],
hashes: &[u64],
next: &[AtomicU32],
cancel: &Cancel,
) -> Result<(Table, Vec<u32>, usize)> {
let mut table = Table::new(types);
let mut head: Vec<u32> = Vec::new();
let mut tail: Vec<u32> = Vec::new();
let mut found: Vec<usize> = Vec::new();
let mut walk = Walk::default();
let mut kept = 0;
for run in mine {
let mut from = 0;
while from < run.len() {
cancel.check()?;
let upto = (from + BATCH).min(run.len());
let batch = &run[from..upto];
found.clear();
found.resize(batch.len(), MISS);
table.probe_these(hashes, keys, batch, &mut found, &mut walk);
for &place in walk.pending() {
let row = batch[place];
match table.probe(hashes[row], keys, row) {
Probe::Found(slot) => found[place] = 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(0);
tail.push(NONE);
found[place] = slot;
}
}
}
for (&row, &slot) in batch.iter().zip(&found) {
let at = u32::try_from(row).map_err(|_| too_many_rows())?;
if tail[slot] == NONE {
head[slot] = at + 1;
} else {
next[tail[slot] as usize].store(at, Ordering::Relaxed);
}
tail[slot] = at;
kept += 1;
}
from = upto;
}
}
Ok((table, head, kept))
}
fn deal(
rows: usize,
count: usize,
threads: &Lease<'_>,
part_of: impl Fn(usize) -> Option<usize> + Sync,
) -> Result<Vec<Vec<Vec<usize>>>> {
let slices = if count > 1 { slices_of(rows, threads.degree()) } else { 1 };
let size = rows.div_ceil(slices);
let one = |slice: usize| -> Result<Vec<Vec<usize>>> {
let share = size / count + size / count / 8 + 16;
let mut dealt: Vec<Vec<usize>> = (0..count).map(|_| Vec::with_capacity(share)).collect();
for row in slice * size..((slice + 1) * size).min(rows) {
if let Some(part) = part_of(row) {
dealt[part].push(row);
}
}
Ok(dealt)
};
in_parallel(threads, slices, slices, "join side slice", one)
}
fn slices_of(rows: usize, threads: usize) -> usize {
if rows < SPLIT { 1 } else { threads.max(1) }
}
fn split_into(rows: usize, threads: usize) -> u32 {
if rows < SPLIT || threads <= 1 {
return 0;
}
threads.next_power_of_two().trailing_zeros()
}
fn part_of(hash: u64, bits: u32) -> usize {
if bits == 0 {
return 0;
}
(hash >> (64 - bits)) as usize
}
#[derive(Debug, Default)]
pub(crate) struct Scratch {
hashes: Vec<u64>,
keyed: Vec<bool>,
walk: Walk,
by_part: Vec<Vec<usize>>,
found: Vec<usize>,
block: Vec<i64>,
}
fn integer(logical: &LogicalType) -> bool {
matches!(
logical,
LogicalType::TinyInt | LogicalType::SmallInt | LogicalType::Integer | LogicalType::BigInt
)
}
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 => !column.none_null(),
_ => 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 rudb_common::Cancel;
use rudb_pipeline::{Lease, Pool};
use super::{Lookup, MISS, SPLIT, Scratch, column, deal, part_of, split_into};
#[test]
fn rows_are_dealt_to_the_partition_their_hash_names_in_row_order() {
let rows = SPLIT + 4000;
let hashes: Vec<u64> =
(0..rows as u64).map(|row| row.wrapping_mul(0x9E37_79B9_7F4A_7C15)).collect();
let keyed: Vec<bool> = (0..rows).map(|row| row % 5 != 0).collect();
let pool = Pool::new(3);
for threads in [Lease::alone(), pool.lease(3)] {
let dealt = deal(rows, 4, &threads, |row| keyed[row].then(|| part_of(hashes[row], 2)))
.expect("a deal");
for part in 0..4 {
let mine: Vec<usize> =
dealt.iter().flat_map(|slice| slice[part].iter().copied()).collect();
let expected: Vec<usize> = (0..rows)
.filter(|&row| keyed[row] && part_of(hashes[row], 2) == part)
.collect();
assert_eq!(mine, expected);
}
}
}
fn built(values: &[Option<i32>]) -> Lookup {
built_by(values, &[false], &Lease::alone())
}
fn built_by(values: &[Option<i32>], nulls: &[bool], threads: &Lease<'_>) -> Lookup {
Lookup::build(&[column(values)], values.len(), nulls, threads, &Cancel::new())
.expect("a build")
}
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 lookup = built_by(&[Some(1), None, None], &[true], &Lease::alone());
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() {
let nothing = Lookup::build(&[], 0, &[false], &Lease::alone(), &Cancel::new())
.expect("no columns at all");
assert!(nothing.is_empty());
assert!(built(&[None, None]).is_empty(), "every row's key was a rejected null");
assert!(!built(&[Some(1)]).is_empty());
}
#[test]
fn a_partition_is_named_by_the_top_bits_and_a_bucket_by_the_low_ones() {
assert_eq!(part_of(0, 2), 0);
assert_eq!(part_of(u64::MAX, 2), 3);
assert_eq!(part_of(1 << 62, 2), 1);
assert_eq!(part_of(u64::MAX, 0), 0, "one partition holds everything");
assert_eq!(part_of(0xFFFF_FFFF, 2), 0, "the low bits say nothing about which partition");
}
#[test]
fn a_small_side_is_not_split_at_all() {
assert_eq!(split_into(1_000, 8), 0);
assert_eq!(split_into(SPLIT - 1, 8), 0);
assert_eq!(split_into(SPLIT, 1), 0);
assert_eq!(split_into(SPLIT, 8), 3);
assert_eq!(split_into(SPLIT, 6), 3, "rounded up to a power of two");
}
fn answers_in_order(lookup: &Lookup, values: &[Option<i32>], spread: i32) {
let mut scratch = Scratch::default();
let mut slots = Vec::new();
let driving: Vec<Option<i32>> = (-1..9).map(|key| Some(key * spread)).collect();
lookup.slots(&[column(&driving)], driving.len(), &[false], &mut scratch, &mut slots);
let mut chain = Vec::new();
for (&key, &slot) in driving.iter().zip(&slots) {
let wanted: Vec<u32> = (0..values.len())
.filter(|&row| values[row] == key)
.map(|row| u32::try_from(row).expect("a side this long"))
.collect();
if wanted.is_empty() {
assert_eq!(slot, MISS, "key {key:?} is not in the side");
continue;
}
lookup.matches(slot, &mut chain);
assert_eq!(chain, wanted, "key {key:?} came out in the wrong order");
}
}
#[test]
fn a_side_built_in_partitions_answers_the_same_as_one_built_whole() {
let spread = 100_000_007;
let values: Vec<Option<i32>> =
(0..SPLIT as i32 + 1_000).map(|row| Some(row % 7 * spread)).collect();
let pool = Pool::new(4);
let lookup = built_by(&values, &[false], &pool.lease(4));
assert!(lookup.low.is_none(), "keys this far apart take the table");
assert!(lookup.parts.len() > 1, "a side this long is split");
answers_in_order(&lookup, &values, spread);
}
#[test]
fn a_ranked_side_built_in_partitions_answers_in_order() {
let spread = 1_000_003;
let values: Vec<Option<i32>> = (0..SPLIT as i32 + 1_000)
.map(|row| (row % 11 != 5).then_some(row % 13 % 9 * spread))
.filter(|value| *value != Some(4 * spread))
.collect();
let pool = Pool::new(4);
let lookup = built_by(&values, &[false], &pool.lease(4));
assert_eq!(lookup.low, Some(0));
assert!(lookup.ranked.is_some(), "keys this far apart are ranked");
assert_eq!(lookup.head.len(), 8, "one slot a key, not one a place");
answers_in_order(&lookup, &values, spread);
let found = found(&lookup, &[Some(1), Some(spread - 1), Some(-spread), Some(9 * spread)]);
assert!(found.iter().all(Vec::is_empty), "a key between keys or past the ends is a miss");
}
#[test]
fn a_ranked_side_counts_the_keys_below_across_words() {
let keys = [0, 63, 64, 65, 127, 128, 1_000, 1_900];
let values: Vec<Option<i32>> = keys.iter().rev().map(|&key| Some(key + 7)).collect();
let lookup = built(&values);
assert!(lookup.ranked.is_some());
assert!(lookup.single());
for (at, &key) in keys.iter().rev().enumerate() {
let row = u32::try_from(at).expect("a short side");
assert_eq!(found(&lookup, &[Some(key + 7)]), [vec![row]], "key {key}");
let next = found(&lookup, &[Some(key + 8)]).concat();
assert_eq!(next.is_empty(), !keys.contains(&(key + 1)), "key {}", key + 1);
}
assert_eq!(found(&lookup, &[Some(6), Some(1_908)]), [vec![], vec![]]);
}
#[test]
fn a_direct_side_built_in_partitions_answers_in_order() {
let values: Vec<Option<i32>> = (0..SPLIT as i32 + 1_000)
.map(|row| (row % 11 != 5).then_some(row % 13 % 9))
.filter(|value| *value != Some(4))
.collect();
let pool = Pool::new(4);
let lookup = built_by(&values, &[false], &pool.lease(4));
assert_eq!(lookup.low, Some(0), "keys this close index the head");
assert!(lookup.parts.is_empty());
answers_in_order(&lookup, &values, 1);
assert!(!lookup.single(), "each key has many rows");
}
#[test]
fn a_direct_side_of_distinct_keys_is_single_and_misses_past_its_ends() {
let values: Vec<Option<i32>> = (0..100).map(|row| Some(1_000 - row * 2)).collect();
let lookup = built(&values);
assert_eq!(lookup.low, Some(802));
assert!(lookup.single());
let found = found(&lookup, &[Some(1_000), Some(802), Some(801), Some(1_002), Some(999)]);
assert_eq!(found, [vec![0], vec![99], vec![], vec![], vec![]]);
}
#[test]
fn an_ordered_side_answers_each_key_with_its_own_row() {
let values: Vec<Option<i32>> =
(0..SPLIT as i32 + 1_000).map(|row| Some(7 + row * 3)).collect();
let pool = Pool::new(4);
let lookup = built_by(&values, &[false], &pool.lease(4));
assert!(lookup.ordered && lookup.head.is_empty() && lookup.next.is_empty());
assert!(lookup.ranked.is_some(), "keys a third of their span are ranked");
assert!(lookup.single());
assert_eq!(lookup.slot_count(), values.len());
let last = 7 + (values.len() as i32 - 1) * 3;
let asked = [Some(7), Some(8), Some(10), Some(4), Some(last), Some(last + 3), Some(3_007)];
let found = found(&lookup, &asked);
assert_eq!(
found,
[vec![0], vec![], vec![1], vec![], vec![values.len() as u32 - 1], vec![], vec![1_000]]
);
let mut firsts = Vec::new();
lookup.firsts(&[5, MISS], &mut firsts);
assert_eq!(firsts, [5, super::NONE]);
let mut chain = Vec::new();
lookup.chain_from(5, &mut chain);
assert_eq!(chain, [5]);
}
#[test]
fn an_ordered_side_with_no_gaps_is_its_own_places() {
let values: Vec<Option<i32>> = (0..500).map(|row| Some(-20 + row)).collect();
let lookup = built(&values);
assert!(lookup.ordered && lookup.ranked.is_none());
assert_eq!(
found(&lookup, &[Some(-20), Some(479), Some(480), Some(-21)]),
[vec![0], vec![499], vec![], vec![]]
);
}
#[test]
fn a_side_out_of_order_anywhere_keeps_its_chains() {
let rows = SPLIT as i32 + 1_000;
let pool = Pool::new(4);
for broken in [1, rows / 2, rows - 1, (rows as usize).div_ceil(4) as i32] {
let ascending: Vec<Option<i32>> = (0..rows).map(|row| Some(row * 2)).collect();
let mut twice = ascending.clone();
twice[broken as usize] = twice[broken as usize - 1];
let mut lower = ascending.clone();
lower[broken as usize] = Some(-1);
let mut null = ascending.clone();
null[broken as usize] = None;
for values in [twice, lower, null] {
for threads in [Lease::alone(), pool.lease(4)] {
let lookup = built_by(&values, &[false], &threads);
assert!(!lookup.ordered, "broken at {broken}");
answers_in_order(&lookup, &values, 1);
}
}
}
}
}