1use std::sync::atomic::{AtomicU8, Ordering};
54use std::sync::{Arc, OnceLock};
55
56use rudb_common::{Error, Result};
57use rudb_vector::Vector;
58
59#[derive(Debug, Default)]
61pub(crate) struct Peel {
62 answers: OnceLock<Answers>,
63}
64
65#[derive(Debug)]
66struct Answers {
67 dictionary: Arc<Vector>,
69 decided: Vec<AtomicU8>,
75}
76
77impl Peel {
78 pub(crate) fn answer<M, D>(
89 &self,
90 column: &Vector,
91 len: usize,
92 map: M,
93 decide: D,
94 ) -> Option<Result<Vec<bool>>>
95 where
96 M: Fn(usize) -> usize,
97 D: Fn(&Vector, usize) -> Result<bool>,
98 {
99 let (codes, dictionary) = column.shared_dictionary_parts()?;
100 let answers = self.answers.get_or_init(|| Answers {
101 dictionary: Arc::clone(dictionary),
102 decided: (0..dictionary.len()).map(|_| AtomicU8::new(0)).collect(),
103 });
104 if !Arc::ptr_eq(&answers.dictionary, dictionary) {
105 return None;
106 }
107 Some(answers.run(dictionary, codes, len, map, decide))
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum Found {
114 At(u32),
117 Absent,
120}
121
122#[derive(Debug, Default)]
124pub struct Lookup {
125 memo: OnceLock<Searched>,
126}
127
128#[derive(Debug)]
129struct Searched {
130 dictionary: Arc<Vector>,
132 found: Found,
133}
134
135impl Lookup {
136 pub fn find(&self, column: &Vector, wanted: &[u8]) -> Option<Result<Found>> {
145 let (_, dictionary) = column.shared_dictionary_parts()?;
146 if let Some(memo) = self.memo.get() {
147 return Arc::ptr_eq(&memo.dictionary, dictionary).then_some(Ok(memo.found));
148 }
149 let ranks = dictionary.ranks()?;
150 let found = match search(dictionary, ranks, wanted) {
151 Ok(found) => found,
152 Err(error) => return Some(Err(error)),
153 };
154 let _ = self.memo.set(Searched { dictionary: Arc::clone(dictionary), found });
157 Some(Ok(found))
158 }
159}
160
161pub(crate) fn search(dictionary: &Vector, ranks: usize, wanted: &[u8]) -> Result<Found> {
169 let mut low = 0;
170 let mut high = ranks;
171 while low < high {
172 let middle = low + (high - low) / 2;
173 match dictionary.compare_rank(middle, wanted)? {
174 std::cmp::Ordering::Less => low = middle + 1,
175 std::cmp::Ordering::Greater => high = middle,
176 std::cmp::Ordering::Equal => {
177 return Ok(Found::At(dictionary.code_at_rank(middle)?));
178 }
179 }
180 }
181 Ok(Found::Absent)
182}
183
184pub(crate) fn below(dictionary: &Vector, ranks: usize, wanted: &[u8]) -> Result<(usize, bool)> {
196 dictionary.below(ranks, wanted)
197}
198
199impl Answers {
200 fn run<M, D>(
201 &self,
202 dictionary: &Vector,
203 codes: &[u32],
204 len: usize,
205 map: M,
206 decide: D,
207 ) -> Result<Vec<bool>>
208 where
209 M: Fn(usize) -> usize,
210 D: Fn(&Vector, usize) -> Result<bool>,
211 {
212 let mut out = Vec::with_capacity(len);
213 for slot in 0..len {
217 let code = *codes
218 .get(map(slot))
219 .ok_or_else(|| Error::internal("a peeled row is past the end of its codes"))?;
220 let state = self.decided.get(code as usize).ok_or_else(|| {
221 Error::internal("a peeled code is past the end of its dictionary")
222 })?;
223 let mut held = state.load(Ordering::Relaxed);
224 if held == 0 {
225 held = u8::from(decide(dictionary, code as usize)?) + 1;
226 state.store(held, Ordering::Relaxed);
227 }
228 out.push(held == 2);
229 }
230 Ok(out)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use std::sync::atomic::AtomicUsize;
237
238 use rudb_common::{LogicalType, Value};
239
240 use super::*;
241
242 fn letters(values: &[&str]) -> Vector {
243 let values: Vec<Value> = values.iter().map(|text| Value::Varchar((*text).into())).collect();
244 Vector::from_values(LogicalType::Varchar, &values).expect("a vector of text")
245 }
246
247 fn holds(dictionary: &Vector, code: usize) -> Result<bool> {
248 Ok(dictionary.try_bytes_at(code)?.is_some_and(|bytes| bytes.starts_with(b"a")))
249 }
250
251 #[test]
252 fn a_peel_decides_once_per_distinct_code_and_reads_the_memo_after_that() {
253 let values = Arc::new(letters(&["apple", "pear", "avocado"]));
254 let column = Vector::stable_dictionary(vec![0, 1, 2, 1, 0, 0], values).expect("in range");
255 let peel = Peel::default();
256 let calls = AtomicUsize::new(0);
257 let answers = peel
258 .answer(
259 &column,
260 6,
261 |slot| slot,
262 |dictionary, code| {
263 calls.fetch_add(1, Ordering::Relaxed);
264 holds(dictionary, code)
265 },
266 )
267 .expect("a shared dictionary is peelable")
268 .expect("the predicate answers");
269 assert_eq!(answers, [true, false, true, false, true, true]);
270 assert_eq!(calls.load(Ordering::Relaxed), 3, "six rows over three distinct values");
271 }
272
273 #[test]
276 fn a_second_chunk_over_the_same_dictionary_asks_the_predicate_nothing() {
277 let values = Arc::new(letters(&["apple", "pear"]));
278 let first = Vector::stable_dictionary(vec![0, 1], Arc::clone(&values)).expect("in range");
279 let second = Vector::stable_dictionary(vec![1, 1, 0], values).expect("in range");
280 let peel = Peel::default();
281 let calls = AtomicUsize::new(0);
282 let count = |column: &Vector, len: usize| {
283 peel.answer(
284 column,
285 len,
286 |slot| slot,
287 |dictionary, code| {
288 calls.fetch_add(1, Ordering::Relaxed);
289 holds(dictionary, code)
290 },
291 )
292 .expect("peelable")
293 .expect("answers")
294 };
295 assert_eq!(count(&first, 2), [true, false]);
296 assert_eq!(calls.load(Ordering::Relaxed), 2);
297 assert_eq!(count(&second, 3), [false, false, true]);
298 assert_eq!(calls.load(Ordering::Relaxed), 2, "the second chunk decided nothing new");
299 }
300
301 #[test]
304 fn a_different_dictionary_is_declined_rather_than_answered_from_the_first_ones_memo() {
305 let peel = Peel::default();
306 let first = Vector::stable_dictionary(vec![0], Arc::new(letters(&["apple"]))).expect("one");
307 let second = Vector::stable_dictionary(vec![0], Arc::new(letters(&["pear"]))).expect("one");
308 assert!(peel.answer(&first, 1, |slot| slot, holds).is_some());
309 assert!(peel.answer(&second, 1, |slot| slot, holds).is_none());
310 }
311
312 #[test]
313 fn a_column_that_is_not_a_shared_dictionary_has_nothing_to_peel() {
314 let flat = letters(&["apple", "pear"]);
315 assert!(Peel::default().answer(&flat, 2, |slot| slot, holds).is_none());
316 }
317
318 #[derive(Debug)]
323 struct Filed {
324 values: Vec<Vec<u8>>,
325 order: Vec<(u64, u32)>,
328 reads: AtomicUsize,
329 }
330
331 fn head(bytes: &[u8]) -> u64 {
334 let mut word = [0; 8];
335 let take = bytes.len().min(8);
336 word[..take].copy_from_slice(&bytes[..take]);
337 u64::from_be_bytes(word)
338 }
339
340 impl Filed {
341 fn new(values: &[&str]) -> Self {
343 let values: Vec<Vec<u8>> = values.iter().map(|text| text.as_bytes().to_vec()).collect();
344 let mut order = (0..values.len() as u32)
345 .map(|code| (head(&values[code as usize]), code))
346 .collect::<Vec<_>>();
347 order.sort_by(|&(_, left), &(_, right)| {
348 values[left as usize].cmp(&values[right as usize])
349 });
350 Self { values, order, reads: AtomicUsize::new(0) }
351 }
352
353 fn at(&self, rank: usize) -> Result<(u64, u32)> {
354 self.order
355 .get(rank)
356 .copied()
357 .ok_or_else(|| Error::internal("a rank past the end of the order"))
358 }
359 }
360
361 impl rudb_vector::TextSource for Filed {
362 fn len(&self) -> usize {
363 self.values.len()
364 }
365
366 fn bytes_at(&self, index: usize) -> Result<Option<&[u8]>> {
367 self.reads.fetch_add(1, Ordering::Relaxed);
368 Ok(self.values.get(index).map(Vec::as_slice))
369 }
370
371 fn footprint(&self) -> usize {
372 self.values.iter().map(Vec::len).sum()
373 }
374
375 fn ranks(&self) -> Option<usize> {
376 Some(self.order.len())
377 }
378
379 fn compare_rank(&self, rank: usize, wanted: &[u8]) -> Result<std::cmp::Ordering> {
380 let (found, code) = self.at(rank)?;
381 let settled = found.cmp(&head(wanted));
382 if settled != std::cmp::Ordering::Equal {
383 return Ok(settled);
384 }
385 Ok(self.bytes_at(code as usize)?.unwrap_or_default().cmp(wanted))
386 }
387
388 fn code_at_rank(&self, rank: usize) -> Result<u32> {
389 Ok(self.at(rank)?.1)
390 }
391 }
392
393 fn filed(count: usize) -> (Vector, Arc<Filed>) {
396 let spellings =
397 (0..count).map(|at| format!("value-{:04}", (at * 7919) % count)).collect::<Vec<_>>();
398 let source =
399 Arc::new(Filed::new(&spellings.iter().map(String::as_str).collect::<Vec<_>>()));
400 let values = Vector::external_text(LogicalType::Varchar, Arc::clone(&source) as Arc<_>)
401 .expect("a filed vector");
402 (values, source)
403 }
404
405 #[test]
406 fn a_literal_is_found_in_a_sorted_dictionary_without_reading_every_value() {
407 let (values, source) = filed(1024);
408 let column = Vector::stable_dictionary(vec![3, 900, 3], Arc::new(values)).expect("codes");
409 let found = Lookup::default()
410 .find(&column, b"value-0700")
411 .expect("a sorted dictionary can be searched")
412 .expect("the search reads");
413 let Found::At(code) = found else { panic!("the dictionary holds it") };
414 assert_eq!(source.values[code as usize], b"value-0700");
415 let reads = source.reads.load(Ordering::Relaxed);
416 assert!(reads <= 11, "a search of 1024 values read {reads} of them");
417 }
418
419 #[test]
423 fn a_search_over_values_that_differ_early_reads_only_the_one_it_finds() {
424 let source = Arc::new(Filed::new(&["cherry", "apple", "date", "banana"]));
425 let values = Vector::external_text(LogicalType::Varchar, Arc::clone(&source) as Arc<_>)
426 .expect("a filed vector");
427 let column = Vector::stable_dictionary(vec![0, 1, 2, 3], Arc::new(values)).expect("codes");
428 assert_eq!(
429 Lookup::default().find(&column, b"cherry").expect("searchable").expect("read"),
430 Found::At(0)
431 );
432 assert_eq!(source.reads.load(Ordering::Relaxed), 1, "only the value it found");
433 assert_eq!(
434 Lookup::default().find(&column, b"fig").expect("searchable").expect("read"),
435 Found::Absent
436 );
437 assert_eq!(source.reads.load(Ordering::Relaxed), 1, "and nothing for the one it did not");
438 }
439
440 #[test]
443 fn values_that_share_their_first_eight_bytes_are_still_told_apart() {
444 let source = Arc::new(Filed::new(&["prefixed-two", "prefixed-one", "prefixed-three"]));
445 let values = Vector::external_text(LogicalType::Varchar, Arc::clone(&source) as Arc<_>)
446 .expect("a filed vector");
447 let column = Vector::stable_dictionary(vec![0, 1, 2], Arc::new(values)).expect("codes");
448 for (wanted, expected) in [
449 (&b"prefixed-one"[..], Found::At(1)),
450 (b"prefixed-two", Found::At(0)),
451 (b"prefixed-three", Found::At(2)),
452 (b"prefixed-four", Found::Absent),
453 ] {
454 assert_eq!(
455 Lookup::default().find(&column, wanted).expect("searchable").expect("read"),
456 expected,
457 "searching for {}",
458 String::from_utf8_lossy(wanted)
459 );
460 }
461 }
462
463 #[test]
464 fn a_literal_the_dictionary_does_not_hold_is_answered_absent() {
465 let (values, _) = filed(64);
466 let column = Vector::stable_dictionary(vec![0], Arc::new(values)).expect("codes");
467 let found = Lookup::default().find(&column, b"nothing").expect("searchable").expect("read");
468 assert_eq!(found, Found::Absent);
469 }
470
471 #[test]
474 fn the_empty_string_is_found_like_any_other_value() {
475 let source = Arc::new(Filed::new(&["pear", "", "apple"]));
476 let values = Vector::external_text(LogicalType::Varchar, source).expect("a filed vector");
477 let column = Vector::stable_dictionary(vec![0, 1, 2], Arc::new(values)).expect("codes");
478 assert_eq!(
479 Lookup::default().find(&column, b"").expect("searchable").expect("read"),
480 Found::At(1)
481 );
482 }
483
484 #[test]
485 fn a_second_chunk_over_the_same_dictionary_searches_nothing() {
486 let (values, source) = filed(256);
487 let values = Arc::new(values);
488 let first = Vector::stable_dictionary(vec![0, 1], Arc::clone(&values)).expect("codes");
489 let second = Vector::stable_dictionary(vec![2], Arc::clone(&values)).expect("codes");
490 let lookup = Lookup::default();
491 let found = lookup.find(&first, b"value-0100").expect("searchable").expect("read");
492 let reads = source.reads.load(Ordering::Relaxed);
493 assert!(reads > 0, "the first chunk did the search");
494 assert_eq!(lookup.find(&second, b"value-0100").expect("searchable").expect("read"), found);
495 assert_eq!(source.reads.load(Ordering::Relaxed), reads, "the second chunk read nothing");
496 }
497
498 #[test]
501 fn a_search_is_not_reused_across_two_dictionaries() {
502 let lookup = Lookup::default();
503 let (first, _) = filed(8);
504 let (second, _) = filed(8);
505 let first = Vector::stable_dictionary(vec![0], Arc::new(first)).expect("codes");
506 let second = Vector::stable_dictionary(vec![0], Arc::new(second)).expect("codes");
507 assert!(lookup.find(&first, b"value-0000").is_some());
508 assert!(lookup.find(&second, b"value-0000").is_none());
509 }
510
511 #[test]
514 fn a_dictionary_with_no_order_is_declined() {
515 let values = Arc::new(letters(&["apple", "pear"]));
516 let column = Vector::stable_dictionary(vec![0, 1], values).expect("codes");
517 assert!(Lookup::default().find(&column, b"apple").is_none());
518 }
519}