use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, OnceLock};
use rudb_common::{Error, Result};
use rudb_vector::Vector;
#[derive(Debug, Default)]
pub(crate) struct Peel {
answers: OnceLock<Answers>,
}
#[derive(Debug)]
struct Answers {
dictionary: Arc<Vector>,
decided: Vec<AtomicU8>,
}
impl Peel {
pub(crate) fn answer<M, D>(
&self,
column: &Vector,
len: usize,
map: M,
decide: D,
) -> Option<Result<Vec<bool>>>
where
M: Fn(usize) -> usize,
D: Fn(&Vector, usize) -> Result<bool>,
{
let (codes, dictionary) = column.shared_dictionary_parts()?;
let answers = self.answers.get_or_init(|| Answers {
dictionary: Arc::clone(dictionary),
decided: (0..dictionary.len()).map(|_| AtomicU8::new(0)).collect(),
});
if !Arc::ptr_eq(&answers.dictionary, dictionary) {
return None;
}
Some(answers.run(dictionary, codes, len, map, decide))
}
}
impl Answers {
fn run<M, D>(
&self,
dictionary: &Vector,
codes: &[u32],
len: usize,
map: M,
decide: D,
) -> Result<Vec<bool>>
where
M: Fn(usize) -> usize,
D: Fn(&Vector, usize) -> Result<bool>,
{
let mut out = Vec::with_capacity(len);
for slot in 0..len {
let code = *codes
.get(map(slot))
.ok_or_else(|| Error::internal("a peeled row is past the end of its codes"))?;
let state = self.decided.get(code as usize).ok_or_else(|| {
Error::internal("a peeled code is past the end of its dictionary")
})?;
let mut held = state.load(Ordering::Relaxed);
if held == 0 {
held = u8::from(decide(dictionary, code as usize)?) + 1;
state.store(held, Ordering::Relaxed);
}
out.push(held == 2);
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicUsize;
use rudb_common::{LogicalType, Value};
use super::*;
fn letters(values: &[&str]) -> Vector {
let values: Vec<Value> = values.iter().map(|text| Value::Varchar((*text).into())).collect();
Vector::from_values(LogicalType::Varchar, &values).expect("a vector of text")
}
fn holds(dictionary: &Vector, code: usize) -> Result<bool> {
Ok(dictionary.try_bytes_at(code)?.is_some_and(|bytes| bytes.starts_with(b"a")))
}
#[test]
fn a_peel_decides_once_per_distinct_code_and_reads_the_memo_after_that() {
let values = Arc::new(letters(&["apple", "pear", "avocado"]));
let column = Vector::stable_dictionary(vec![0, 1, 2, 1, 0, 0], values).expect("in range");
let peel = Peel::default();
let calls = AtomicUsize::new(0);
let answers = peel
.answer(
&column,
6,
|slot| slot,
|dictionary, code| {
calls.fetch_add(1, Ordering::Relaxed);
holds(dictionary, code)
},
)
.expect("a shared dictionary is peelable")
.expect("the predicate answers");
assert_eq!(answers, [true, false, true, false, true, true]);
assert_eq!(calls.load(Ordering::Relaxed), 3, "six rows over three distinct values");
}
#[test]
fn a_second_chunk_over_the_same_dictionary_asks_the_predicate_nothing() {
let values = Arc::new(letters(&["apple", "pear"]));
let first = Vector::stable_dictionary(vec![0, 1], Arc::clone(&values)).expect("in range");
let second = Vector::stable_dictionary(vec![1, 1, 0], values).expect("in range");
let peel = Peel::default();
let calls = AtomicUsize::new(0);
let count = |column: &Vector, len: usize| {
peel.answer(
column,
len,
|slot| slot,
|dictionary, code| {
calls.fetch_add(1, Ordering::Relaxed);
holds(dictionary, code)
},
)
.expect("peelable")
.expect("answers")
};
assert_eq!(count(&first, 2), [true, false]);
assert_eq!(calls.load(Ordering::Relaxed), 2);
assert_eq!(count(&second, 3), [false, false, true]);
assert_eq!(calls.load(Ordering::Relaxed), 2, "the second chunk decided nothing new");
}
#[test]
fn a_different_dictionary_is_declined_rather_than_answered_from_the_first_ones_memo() {
let peel = Peel::default();
let first = Vector::stable_dictionary(vec![0], Arc::new(letters(&["apple"]))).expect("one");
let second = Vector::stable_dictionary(vec![0], Arc::new(letters(&["pear"]))).expect("one");
assert!(peel.answer(&first, 1, |slot| slot, holds).is_some());
assert!(peel.answer(&second, 1, |slot| slot, holds).is_none());
}
#[test]
fn a_column_that_is_not_a_shared_dictionary_has_nothing_to_peel() {
let flat = letters(&["apple", "pear"]);
assert!(Peel::default().answer(&flat, 2, |slot| slot, holds).is_none());
}
}