1use std::collections::HashMap;
30use std::collections::HashSet;
31use std::ops::Range;
32
33use anyhow::Result;
34use anyhow::bail;
35use futures::stream::BoxStream;
36use roaring::RoaringBitmap;
37
38use crate::dimensions::IndexDimension;
39
40mod iter;
41mod stream;
42
43pub use iter::eval_bitmap_query_bucket_iter;
44pub use stream::BitmapScanMetrics;
45pub use stream::eval_bitmap_query_stream;
46pub use stream::flatten_watermarked_buckets;
47
48#[cfg(test)]
50pub(crate) use stream::BitmapScanBudget;
51#[cfg(test)]
52pub(crate) use stream::eval_bitmap_query_bucket_stream;
53
54#[derive(Debug, thiserror::Error)]
59pub enum LeafStop {
60 #[error("bitmap scan limit reached")]
62 BudgetExhausted,
63 #[error("bitmap scan cancelled")]
65 Cancelled,
66 #[error(transparent)]
68 Fault(anyhow::Error),
69}
70
71#[derive(Debug, thiserror::Error)]
81pub enum ScanStop {
82 #[error("bitmap scan limit reached")]
85 ScanLimit {
86 scan_frontier: u64,
93 },
94 #[error("bitmap scan cancelled")]
96 Cancelled,
97 #[error(transparent)]
99 Fault(anyhow::Error),
100}
101
102impl From<anyhow::Error> for LeafStop {
103 fn from(err: anyhow::Error) -> Self {
104 LeafStop::Fault(err)
105 }
106}
107
108impl From<anyhow::Error> for ScanStop {
109 fn from(err: anyhow::Error) -> Self {
110 ScanStop::Fault(err)
111 }
112}
113
114pub(crate) fn collapse(stops: Vec<LeafStop>, scan_frontier: u64) -> ScanStop {
121 assert!(!stops.is_empty(), "collapse on empty Vec");
122 let mut cancelled = false;
123 let mut budget = false;
124 let mut faults: Vec<anyhow::Error> = Vec::new();
125 for s in stops {
126 match s {
127 LeafStop::Cancelled => cancelled = true,
128 LeafStop::BudgetExhausted => budget = true,
129 LeafStop::Fault(err) => faults.push(err),
130 }
131 }
132 match faults.len() {
133 0 => {
134 if budget {
135 ScanStop::ScanLimit { scan_frontier }
136 } else {
137 debug_assert!(cancelled, "collapse saw only non-erroring leaves");
138 ScanStop::Cancelled
139 }
140 }
141 1 => ScanStop::Fault(faults.pop().expect("len == 1")),
142 n => {
143 let combined = faults
144 .iter()
145 .enumerate()
146 .map(|(i, err)| format!(" [{i}] {err}"))
147 .collect::<Vec<_>>()
148 .join("\n");
149 ScanStop::Fault(anyhow::anyhow!(
150 "{n} concurrent bitmap scan faults:\n{combined}"
151 ))
152 }
153 }
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum Watermarked<T, P = u64> {
163 Item(T),
164 Watermark(P),
165}
166
167impl<T, P> Watermarked<T, P> {
168 pub fn map_item<U>(self, f: impl FnOnce(T) -> U) -> Watermarked<U, P> {
169 match self {
170 Watermarked::Item(t) => Watermarked::Item(f(t)),
171 Watermarked::Watermark(p) => Watermarked::Watermark(p),
172 }
173 }
174
175 pub fn map_watermark<Q>(self, f: impl FnOnce(P) -> Q) -> Watermarked<T, Q> {
176 match self {
177 Watermarked::Item(t) => Watermarked::Item(t),
178 Watermarked::Watermark(p) => Watermarked::Watermark(f(p)),
179 }
180 }
181}
182
183pub type BucketItem = Result<(u64, RoaringBitmap), LeafStop>;
187pub type BucketStream = BoxStream<'static, BucketItem>;
188
189pub(crate) type WatermarkedBucket = Result<Watermarked<(u64, RoaringBitmap)>, ScanStop>;
193pub type WatermarkedBucketStream = BoxStream<'static, WatermarkedBucket>;
194
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub enum ScanDirection {
197 Ascending,
198 Descending,
199}
200#[derive(Clone, Copy, Debug)]
204pub struct SkipPolicy {
205 pub drain_probe_rows: Option<std::num::NonZeroU32>,
207}
208
209impl SkipPolicy {
210 pub const DRAIN_ONLY: Self = Self {
212 drain_probe_rows: None,
213 };
214}
215
216impl ScanDirection {
217 pub fn is_ascending(self) -> bool {
218 matches!(self, Self::Ascending)
219 }
220}
221
222pub fn dense_universe_buckets(
231 range: Range<u64>,
232 bucket_size: u64,
233 direction: ScanDirection,
234) -> DenseUniverseBuckets {
235 let bits = u32::try_from(bucket_size).expect("bucket size fits in u32");
236 let buckets = if range.is_empty() {
237 0..0
238 } else {
239 (range.start / bucket_size)..(range.end - 1) / bucket_size + 1
240 };
241 DenseUniverseBuckets {
242 buckets,
243 direction,
244 bits,
245 }
246}
247
248pub struct DenseUniverseBuckets {
250 buckets: Range<u64>,
251 direction: ScanDirection,
252 bits: u32,
253}
254
255impl DenseUniverseBuckets {
256 pub fn seek_bucket(&mut self, bucket: u64) {
258 match self.direction {
259 ScanDirection::Ascending => self.buckets.start = self.buckets.start.max(bucket),
260 ScanDirection::Descending => {
261 self.buckets.end = self.buckets.end.min(bucket.saturating_add(1))
262 }
263 }
264 }
265}
266
267impl Iterator for DenseUniverseBuckets {
268 type Item = (u64, RoaringBitmap);
269
270 fn next(&mut self) -> Option<Self::Item> {
271 let bucket = match self.direction {
272 ScanDirection::Ascending => self.buckets.next(),
273 ScanDirection::Descending => self.buckets.next_back(),
274 }?;
275 let mut bitmap = RoaringBitmap::new();
276 bitmap.insert_range(0..self.bits);
277 Some((bucket, bitmap))
278 }
279}
280
281pub trait BitmapBucketSource: Clone + Send + 'static {
287 fn scan_bucket_stream(
288 &self,
289 dimension_key: Vec<u8>,
290 range: Range<u64>,
291 direction: ScanDirection,
292 ) -> BucketStream;
293}
294
295pub trait SeekableBucketIterator: Iterator<Item = BucketItem> {
297 fn seek_bucket(&mut self, bucket: u64);
301}
302
303pub trait BitmapBucketIteratorSource<'a>: Clone + 'a {
310 type Iter: SeekableBucketIterator + 'a;
311
312 fn scan_bucket_iter(
313 &self,
314 dimension_key: Vec<u8>,
315 range: Range<u64>,
316 direction: ScanDirection,
317 ) -> Self::Iter;
318}
319
320#[derive(Clone, Debug)]
325pub struct BitmapQuery {
326 terms: Vec<BitmapTerm>,
327}
328
329#[derive(Clone, Debug)]
334pub struct BitmapTerm {
335 literals: Vec<BitmapLiteral>,
336}
337
338#[derive(Clone, Debug, Eq, PartialEq, Hash)]
340pub struct BitmapKey(Vec<u8>);
341
342#[derive(Clone, Debug)]
344pub enum BitmapLiteral {
345 Include(BitmapKey),
346 Exclude(BitmapKey),
347}
348
349impl BitmapKey {
350 pub fn new(bytes: Vec<u8>) -> Result<Self> {
351 if bytes.is_empty() {
352 bail!("bitmap dimension key must not be empty");
353 }
354 if bytes.len() == 1 {
355 bail!("bitmap dimension value must not be empty");
356 }
357 if IndexDimension::from_tag_byte(bytes[0]).is_none() {
358 bail!("unknown bitmap dimension tag {}", bytes[0]);
359 }
360 Ok(Self(bytes))
361 }
362
363 pub fn into_inner(self) -> Vec<u8> {
364 self.0
365 }
366
367 pub fn as_bytes(&self) -> &[u8] {
368 &self.0
369 }
370}
371
372impl TryFrom<Vec<u8>> for BitmapKey {
373 type Error = anyhow::Error;
374
375 fn try_from(value: Vec<u8>) -> Result<Self> {
376 Self::new(value)
377 }
378}
379
380impl BitmapLiteral {
381 pub fn include(dimension_key: Vec<u8>) -> Result<Self> {
382 Ok(Self::Include(BitmapKey::new(dimension_key)?))
383 }
384
385 pub fn exclude(dimension_key: Vec<u8>) -> Result<Self> {
386 Ok(Self::Exclude(BitmapKey::new(dimension_key)?))
387 }
388
389 pub fn key_bytes(&self) -> &[u8] {
390 match self {
391 BitmapLiteral::Include(k) | BitmapLiteral::Exclude(k) => k.as_bytes(),
392 }
393 }
394}
395
396impl BitmapQuery {
397 pub fn new(terms: Vec<BitmapTerm>) -> Result<Self> {
398 if terms.is_empty() {
399 bail!("bitmap query must contain at least one term");
400 }
401 Ok(Self { terms })
402 }
403
404 pub fn scan(dimension_key: Vec<u8>) -> Result<Self> {
405 Ok(Self {
406 terms: vec![BitmapTerm::new(vec![BitmapLiteral::include(
407 dimension_key,
408 )?])?],
409 })
410 }
411
412 pub fn unique_leaf_count(&self) -> usize {
418 self.terms
419 .iter()
420 .flat_map(|t| t.literals.iter().map(|l| l.key_bytes()))
421 .collect::<HashSet<_>>()
422 .len()
423 }
424
425 pub fn terms(&self) -> &[BitmapTerm] {
426 &self.terms
427 }
428}
429
430impl BitmapTerm {
431 pub fn new(literals: Vec<BitmapLiteral>) -> Result<Self> {
432 if !literals
433 .iter()
434 .any(|literal| matches!(literal, BitmapLiteral::Include(_)))
435 {
436 bail!("bitmap query term must contain at least one include literal");
437 }
438 Ok(Self { literals })
439 }
440
441 pub fn literals(&self) -> &[BitmapLiteral] {
442 &self.literals
443 }
444}
445
446pub(crate) struct DedupedQuery {
450 pub(crate) keys: Vec<Vec<u8>>,
451 pub(crate) terms: Vec<TermSpec>,
452}
453
454pub(crate) fn build_term_specs(terms: Vec<BitmapTerm>) -> DedupedQuery {
461 let mut key_to_idx: HashMap<Vec<u8>, usize> = HashMap::new();
462 let mut keys: Vec<Vec<u8>> = Vec::new();
463 let mut specs: Vec<TermSpec> = Vec::with_capacity(terms.len());
464 for term in terms {
465 let mut include_idx = Vec::with_capacity(term.literals.len());
466 let mut exclude_idx = Vec::with_capacity(term.literals.len());
467 for literal in term.literals {
468 let (push_target, key) = match literal {
469 BitmapLiteral::Include(k) => (&mut include_idx, k.into_inner()),
470 BitmapLiteral::Exclude(k) => (&mut exclude_idx, k.into_inner()),
471 };
472 let idx = match key_to_idx.get(&key) {
473 Some(&i) => i,
474 None => {
475 let i = keys.len();
476 key_to_idx.insert(key.clone(), i);
477 keys.push(key);
478 i
479 }
480 };
481 push_target.push(idx);
482 }
483 specs.push(TermSpec {
484 includes: include_idx,
485 excludes: exclude_idx,
486 unsatisfiable: false,
487 });
488 }
489 DedupedQuery { keys, terms: specs }
490}
491
492fn bound_in_direction(a: u64, b: u64, direction: ScanDirection) -> u64 {
496 match direction {
497 ScanDirection::Ascending => a.min(b),
498 ScanDirection::Descending => a.max(b),
499 }
500}
501pub(crate) fn advance_in_direction(a: u64, b: u64, direction: ScanDirection) -> u64 {
504 match direction {
505 ScanDirection::Ascending => a.max(b),
506 ScanDirection::Descending => a.min(b),
507 }
508}
509
510pub(crate) fn strictly_before(a: u64, b: u64, direction: ScanDirection) -> bool {
512 match direction {
513 ScanDirection::Ascending => a < b,
514 ScanDirection::Descending => a > b,
515 }
516}
517
518fn frontier_advanced(prev: Option<u64>, next: u64, direction: ScanDirection) -> bool {
522 match prev {
523 None => true,
524 Some(prev) => match direction {
525 ScanDirection::Ascending => next > prev,
526 ScanDirection::Descending => next < prev,
527 },
528 }
529}
530
531pub(crate) fn bucket_edges(
538 bucket: u64,
539 bucket_size: u64,
540 range: &Range<u64>,
541 direction: ScanDirection,
542) -> (u64, u64) {
543 let start = bucket.saturating_mul(bucket_size);
544 let end = start.saturating_add(bucket_size);
545 match direction {
546 ScanDirection::Ascending => (start.max(range.start), end.min(range.end)),
547 ScanDirection::Descending => (end.min(range.end), start.max(range.start)),
548 }
549}
550
551pub(crate) fn take_snapshot_bitmap(
557 snapshot: &mut [Option<RoaringBitmap>],
558 remaining_refs: &mut [usize],
559 on_floor: &[bool],
560 i: usize,
561) -> Option<RoaringBitmap> {
562 if !on_floor[i] {
563 return None;
564 }
565 if remaining_refs[i] > 1 {
566 remaining_refs[i] -= 1;
567 snapshot[i].clone()
568 } else {
569 remaining_refs[i] = remaining_refs[i].saturating_sub(1);
570 snapshot[i].take()
571 }
572}
573
574pub(crate) fn count_on_floor_refs(terms: &[TermSpec], on_floor: &[bool]) -> Vec<usize> {
578 let mut refs = vec![0usize; on_floor.len()];
579 for term in terms {
580 if term.unsatisfiable {
581 continue;
582 }
583 for &i in term.includes.iter().chain(term.excludes.iter()) {
584 if on_floor[i] {
585 refs[i] += 1;
586 }
587 }
588 }
589 refs
590}
591
592pub(crate) fn recompute_unreferenced(
601 terms: &[TermSpec],
602 class: &[Option<LeafHead>],
603 unreferenced: &mut [bool],
604) {
605 let leaf_count = unreferenced.len();
606 let mut referenced = vec![false; leaf_count];
607 for term in terms {
608 if term.unsatisfiable {
609 continue;
610 }
611 for &i in term.includes.iter().chain(term.excludes.iter()) {
612 if !unreferenced[i] && !matches!(class[i], Some(LeafHead::Eof)) {
613 referenced[i] = true;
614 }
615 }
616 }
617 for i in 0..leaf_count {
618 if !referenced[i] {
619 unreferenced[i] = true;
620 }
621 }
622}
623
624pub(crate) fn eval_term_at_bucket(
630 includes: Vec<Option<RoaringBitmap>>,
631 excludes: Vec<Option<RoaringBitmap>>,
632) -> Option<RoaringBitmap> {
633 let mut acc: Option<RoaringBitmap> = None;
634 for include in includes {
635 let bitmap = include?;
637 acc = Some(match acc {
638 None => bitmap,
639 Some(a) => a & bitmap,
640 });
641 }
642 let mut acc = acc?;
644 for exclude in excludes.into_iter().flatten() {
645 acc -= exclude;
646 }
647 (!acc.is_empty()).then_some(acc)
648}
649
650pub(crate) struct TermSpec {
653 pub(crate) includes: Vec<usize>,
654 pub(crate) excludes: Vec<usize>,
655 pub(crate) unsatisfiable: bool,
658}
659
660pub(crate) enum LeafHead {
662 Bucket(u64),
663 Eof,
664 Error,
665}
666pub(crate) fn leaf_skip_targets(
669 terms: &[TermSpec],
670 class: &[Option<LeafHead>],
671 unreferenced: &[bool],
672 direction: ScanDirection,
673) -> Vec<Option<u64>> {
674 let term_not_before: Vec<Option<u64>> = terms
676 .iter()
677 .map(|term| {
678 if term.unsatisfiable {
679 return None;
680 }
681 term.includes
687 .iter()
688 .filter_map(|&i| match class.get(i).and_then(Option::as_ref) {
689 Some(LeafHead::Bucket(bucket)) => Some(*bucket),
690 Some(LeafHead::Error | LeafHead::Eof) | None => None,
691 })
692 .reduce(|a, b| advance_in_direction(a, b, direction))
693 })
694 .collect();
695
696 class
699 .iter()
700 .enumerate()
701 .map(|(i, head)| {
702 if unreferenced.get(i).copied().unwrap_or(true) {
703 return None;
704 }
705 let Some(LeafHead::Bucket(head)) = head else {
706 return None;
707 };
708
709 let mut target = None;
710 for (term_index, term) in terms.iter().enumerate() {
711 if term.unsatisfiable
712 || !term
713 .includes
714 .iter()
715 .chain(&term.excludes)
716 .any(|&leaf| leaf == i)
717 {
718 continue;
719 }
720
721 let not_before = term_not_before[term_index]?;
722 target = Some(match target {
723 None => not_before,
724 Some(current) => bound_in_direction(current, not_before, direction),
725 });
726 }
727 target.filter(|&target| strictly_before(*head, target, direction))
728 })
729 .collect()
730}
731
732#[cfg(test)]
733pub(crate) mod test_utils {
734 use std::collections::BTreeMap;
735 use std::collections::HashMap;
736 use std::sync::Arc;
737 use std::sync::Mutex;
738
739 use futures::StreamExt;
740 use futures::stream;
741
742 use super::*;
743
744 pub(crate) const BUCKET_SIZE: u64 = 100_000;
745 pub(crate) type TestBuckets = BTreeMap<Vec<u8>, Vec<(u64, Vec<u32>)>>;
746 type SeekRecorder = (Vec<u8>, Arc<Mutex<HashMap<Vec<u8>, usize>>>);
747 pub(crate) struct VecBucketIter {
748 items: std::vec::IntoIter<BucketItem>,
749 direction: ScanDirection,
750 recorder: Option<SeekRecorder>,
751 }
752
753 impl Iterator for VecBucketIter {
754 type Item = BucketItem;
755
756 fn next(&mut self) -> Option<Self::Item> {
757 self.items.next()
758 }
759 }
760
761 impl SeekableBucketIterator for VecBucketIter {
762 fn seek_bucket(&mut self, target: u64) {
763 if let Some((key, seek_counts)) = &self.recorder {
764 *seek_counts.lock().unwrap().entry(key.clone()).or_insert(0) += 1;
765 }
766 while matches!(
767 self.items.as_slice().first(),
768 Some(Ok((bucket, _))) if strictly_before(*bucket, target, self.direction)
769 ) {
770 self.items.next();
771 }
772 }
773 }
774
775 #[derive(Clone)]
776 pub(crate) struct TestBucketSource {
777 pub(crate) buckets: Arc<TestBuckets>,
778 }
779
780 impl BitmapBucketSource for TestBucketSource {
781 fn scan_bucket_stream(
782 &self,
783 dimension_key: Vec<u8>,
784 range: Range<u64>,
785 direction: ScanDirection,
786 ) -> BucketStream {
787 stream::iter(self.bucket_items(&dimension_key, range, direction)).boxed()
788 }
789 }
790
791 impl<'a> BitmapBucketIteratorSource<'a> for TestBucketSource {
792 type Iter = VecBucketIter;
793
794 fn scan_bucket_iter(
795 &self,
796 dimension_key: Vec<u8>,
797 range: Range<u64>,
798 direction: ScanDirection,
799 ) -> Self::Iter {
800 VecBucketIter {
801 items: self
802 .bucket_items(&dimension_key, range, direction)
803 .into_iter(),
804 direction,
805 recorder: None,
806 }
807 }
808 }
809
810 impl TestBucketSource {
811 pub(crate) fn bucket_items(
812 &self,
813 dimension_key: &[u8],
814 range: Range<u64>,
815 direction: ScanDirection,
816 ) -> Vec<BucketItem> {
817 if dimension_key == universe_key() {
820 return dense_universe_buckets(range, BUCKET_SIZE, direction)
821 .map(Ok)
822 .collect();
823 }
824 let mut buckets = self.buckets.get(dimension_key).cloned().unwrap_or_default();
825 if range.is_empty() {
826 buckets.clear();
827 } else {
828 let first_bucket = range.start / BUCKET_SIZE;
829 let last_bucket = (range.end - 1) / BUCKET_SIZE;
830 buckets.retain(|(bucket, _)| first_bucket <= *bucket && *bucket <= last_bucket);
831 }
832 if matches!(direction, ScanDirection::Descending) {
833 buckets.reverse();
834 }
835 buckets
836 .into_iter()
837 .map(|(bucket_id, bits)| Ok((bucket_id, make_bitmap(&bits))))
838 .collect()
839 }
840 }
841
842 pub(crate) fn make_bitmap(bits: &[u32]) -> RoaringBitmap {
843 let mut bm = RoaringBitmap::new();
844 for &b in bits {
845 bm.insert(b);
846 }
847 bm
848 }
849
850 pub(crate) fn test_key(value: &[u8]) -> Vec<u8> {
851 crate::dimensions::encode_dimension_key(crate::dimensions::IndexDimension::Sender, value)
852 }
853
854 pub(crate) fn universe_key() -> Vec<u8> {
855 crate::dimensions::encode_dimension_key(
856 crate::dimensions::IndexDimension::TxUniverse,
857 crate::dimensions::TX_UNIVERSE_VALUE,
858 )
859 }
860
861 pub(crate) fn include(value: &[u8]) -> BitmapLiteral {
862 BitmapLiteral::include(test_key(value)).unwrap()
863 }
864
865 pub(crate) fn include_universe() -> BitmapLiteral {
866 BitmapLiteral::include(universe_key()).unwrap()
867 }
868
869 pub(crate) fn full_bucket() -> RoaringBitmap {
872 let mut bm = RoaringBitmap::new();
873 bm.insert_range(0..BUCKET_SIZE as u32);
874 bm
875 }
876
877 #[derive(Clone)]
882 pub(crate) struct CountingBucketSource {
883 pub(crate) buckets: Arc<TestBuckets>,
884 scan_counts: Arc<Mutex<HashMap<Vec<u8>, usize>>>,
885 seek_counts: Arc<Mutex<HashMap<Vec<u8>, usize>>>,
886 }
887
888 impl CountingBucketSource {
889 pub(crate) fn new(buckets: TestBuckets) -> Self {
890 Self {
891 buckets: Arc::new(buckets),
892 scan_counts: Arc::new(Mutex::new(HashMap::new())),
893 seek_counts: Arc::new(Mutex::new(HashMap::new())),
894 }
895 }
896
897 pub(crate) fn scan_count(&self, key: &[u8]) -> usize {
898 self.scan_counts
899 .lock()
900 .unwrap()
901 .get(key)
902 .copied()
903 .unwrap_or(0)
904 }
905 pub(crate) fn seek_count(&self, key: &[u8]) -> usize {
906 self.seek_counts
907 .lock()
908 .unwrap()
909 .get(key)
910 .copied()
911 .unwrap_or(0)
912 }
913
914 fn record(&self, key: &[u8]) {
915 *self
916 .scan_counts
917 .lock()
918 .unwrap()
919 .entry(key.to_vec())
920 .or_insert(0) += 1;
921 }
922
923 fn bucket_items(
924 &self,
925 dimension_key: &[u8],
926 range: Range<u64>,
927 direction: ScanDirection,
928 ) -> Vec<BucketItem> {
929 if dimension_key == universe_key() {
930 return dense_universe_buckets(range, BUCKET_SIZE, direction)
931 .map(Ok)
932 .collect();
933 }
934 let mut buckets = self.buckets.get(dimension_key).cloned().unwrap_or_default();
935 if range.is_empty() {
936 buckets.clear();
937 } else {
938 let first_bucket = range.start / BUCKET_SIZE;
939 let last_bucket = (range.end - 1) / BUCKET_SIZE;
940 buckets.retain(|(bucket, _)| first_bucket <= *bucket && *bucket <= last_bucket);
941 }
942 if matches!(direction, ScanDirection::Descending) {
943 buckets.reverse();
944 }
945 buckets
946 .into_iter()
947 .map(|(bucket_id, bits)| Ok((bucket_id, make_bitmap(&bits))))
948 .collect()
949 }
950 }
951
952 impl BitmapBucketSource for CountingBucketSource {
953 fn scan_bucket_stream(
954 &self,
955 dimension_key: Vec<u8>,
956 range: Range<u64>,
957 direction: ScanDirection,
958 ) -> BucketStream {
959 self.record(&dimension_key);
960 stream::iter(self.bucket_items(&dimension_key, range, direction)).boxed()
961 }
962 }
963
964 impl<'a> BitmapBucketIteratorSource<'a> for CountingBucketSource {
965 type Iter = VecBucketIter;
966
967 fn scan_bucket_iter(
968 &self,
969 dimension_key: Vec<u8>,
970 range: Range<u64>,
971 direction: ScanDirection,
972 ) -> Self::Iter {
973 self.record(&dimension_key);
974 VecBucketIter {
975 items: self
976 .bucket_items(&dimension_key, range, direction)
977 .into_iter(),
978 direction,
979 recorder: Some((dimension_key, self.seek_counts.clone())),
980 }
981 }
982 }
983
984 pub(crate) fn exclude(value: &[u8]) -> BitmapLiteral {
985 BitmapLiteral::exclude(test_key(value)).unwrap()
986 }
987}
988
989#[cfg(test)]
990mod tests {
991 use super::test_utils::exclude;
992 use super::test_utils::include;
993 use super::*;
994
995 #[test]
996 fn bitmap_query_validation_rejects_empty_shapes() {
997 assert!(BitmapQuery::new(Vec::new()).is_err());
998 assert!(BitmapLiteral::include(Vec::new()).is_err());
999 assert!(
1000 BitmapLiteral::include(vec![crate::dimensions::IndexDimension::Sender.tag_byte()])
1001 .is_err()
1002 );
1003 assert!(BitmapLiteral::include(vec![0xff, 0x00]).is_err());
1004 assert!(BitmapTerm::new(vec![exclude(b"neg")]).is_err());
1005 }
1006
1007 #[test]
1008 fn dense_universe_buckets_covers_range_in_direction_order() {
1009 use super::test_utils::BUCKET_SIZE;
1010
1011 let asc: Vec<u64> =
1014 dense_universe_buckets(150_000..350_001, BUCKET_SIZE, ScanDirection::Ascending)
1015 .map(|(bucket, bitmap)| {
1016 assert_eq!(bitmap.len(), BUCKET_SIZE);
1017 bucket
1018 })
1019 .collect();
1020 assert_eq!(asc, vec![1, 2, 3]);
1021
1022 let desc: Vec<u64> =
1023 dense_universe_buckets(150_000..350_001, BUCKET_SIZE, ScanDirection::Descending)
1024 .map(|(bucket, _)| bucket)
1025 .collect();
1026 assert_eq!(desc, vec![3, 2, 1]);
1027
1028 let single: Vec<u64> = dense_universe_buckets(5..6, BUCKET_SIZE, ScanDirection::Ascending)
1029 .map(|(bucket, _)| bucket)
1030 .collect();
1031 assert_eq!(single, vec![0]);
1032
1033 assert_eq!(
1034 dense_universe_buckets(7..7, BUCKET_SIZE, ScanDirection::Ascending).count(),
1035 0
1036 );
1037 }
1038
1039 #[test]
1040 fn unique_leaf_count_counts_distinct_keys_across_terms() {
1041 let query = BitmapQuery::new(vec![
1045 BitmapTerm::new(vec![include(b"a"), include(b"b"), include(b"c")]).unwrap(),
1046 BitmapTerm::new(vec![include(b"a"), include(b"b"), include(b"d")]).unwrap(),
1047 ])
1048 .unwrap();
1049 assert_eq!(query.unique_leaf_count(), 4);
1050 }
1051
1052 #[test]
1053 fn build_term_specs_collapses_duplicate_keys_to_one_leaf() {
1054 let terms = vec![
1058 BitmapTerm::new(vec![include(b"a"), include(b"b")]).unwrap(),
1059 BitmapTerm::new(vec![include(b"b"), exclude(b"a")]).unwrap(),
1060 ];
1061 let DedupedQuery { keys, terms: specs } = build_term_specs(terms);
1062 assert_eq!(keys.len(), 2, "only `a` and `b` are unique");
1063 assert_eq!(specs[0].includes, vec![0, 1]);
1065 assert!(specs[0].excludes.is_empty());
1066 assert_eq!(specs[1].includes, vec![1]);
1068 assert_eq!(specs[1].excludes, vec![0]);
1069 }
1070 #[test]
1071 fn leaf_skip_targets_advance_lagging_include_in_both_directions() {
1072 let term = TermSpec {
1073 includes: vec![0, 1],
1074 excludes: vec![],
1075 unsatisfiable: false,
1076 };
1077 for (direction, heads, expected) in [
1078 (ScanDirection::Ascending, [2, 10], vec![Some(10), None]),
1079 (ScanDirection::Descending, [10, 2], vec![Some(2), None]),
1080 ] {
1081 let class = heads.map(|bucket| Some(LeafHead::Bucket(bucket)));
1082 assert_eq!(
1083 leaf_skip_targets(std::slice::from_ref(&term), &class, &[false; 2], direction),
1084 expected
1085 );
1086 }
1087 }
1088
1089 #[test]
1090 fn leaf_skip_targets_use_least_candidate_across_shared_terms() {
1091 let terms = [
1092 TermSpec {
1093 includes: vec![0],
1094 excludes: vec![1],
1095 unsatisfiable: false,
1096 },
1097 TermSpec {
1098 includes: vec![1],
1099 excludes: vec![],
1100 unsatisfiable: false,
1101 },
1102 ];
1103 for (direction, heads) in [
1104 (ScanDirection::Ascending, [50, 10]),
1105 (ScanDirection::Descending, [10, 50]),
1106 ] {
1107 let class = heads.map(|bucket| Some(LeafHead::Bucket(bucket)));
1108 assert_eq!(
1109 leaf_skip_targets(&terms, &class, &[false; 2], direction)[1],
1110 None
1111 );
1112 }
1113 }
1114
1115 #[test]
1116 fn leaf_skip_targets_drag_exclude_to_include_candidate() {
1117 let term = TermSpec {
1118 includes: vec![0],
1119 excludes: vec![1],
1120 unsatisfiable: false,
1121 };
1122 for (direction, heads, expected) in [
1123 (ScanDirection::Ascending, [50, 10], Some(50)),
1124 (ScanDirection::Descending, [10, 50], Some(10)),
1125 ] {
1126 let class = heads.map(|bucket| Some(LeafHead::Bucket(bucket)));
1127 assert_eq!(
1128 leaf_skip_targets(std::slice::from_ref(&term), &class, &[false; 2], direction)[1],
1129 expected
1130 );
1131 }
1132 }
1133
1134 #[test]
1135 fn leaf_skip_targets_drop_error_heads_and_poison_unknown_candidates() {
1136 for (direction, a_head, c_head) in [
1137 (ScanDirection::Ascending, 0, 9),
1138 (ScanDirection::Descending, 9, 0),
1139 ] {
1140 let terms = [
1141 TermSpec {
1142 includes: vec![0, 1],
1143 excludes: vec![],
1144 unsatisfiable: false,
1145 },
1146 TermSpec {
1147 includes: vec![0, 2],
1148 excludes: vec![],
1149 unsatisfiable: false,
1150 },
1151 ];
1152 let class = [
1153 Some(LeafHead::Bucket(a_head)),
1154 Some(LeafHead::Error),
1155 Some(LeafHead::Bucket(c_head)),
1156 ];
1157 assert_eq!(
1158 leaf_skip_targets(&terms, &class, &[false; 3], direction)[0],
1159 None
1160 );
1161
1162 let poisoned_term = TermSpec {
1163 includes: vec![1],
1164 excludes: vec![0],
1165 unsatisfiable: false,
1166 };
1167 assert_eq!(
1168 leaf_skip_targets(&[poisoned_term], &class, &[false; 3], direction)[0],
1169 None
1170 );
1171 }
1172 }
1173}