1use std::collections::HashMap;
30use std::collections::HashSet;
31use std::ops::Range;
32use std::sync::Arc;
33use std::sync::atomic::AtomicU64;
34use std::sync::atomic::Ordering;
35
36use anyhow::Result;
37use anyhow::bail;
38use futures::stream::BoxStream;
39use roaring::RoaringBitmap;
40
41use crate::dimensions::IndexDimension;
42
43mod iter;
44mod stream;
45
46pub use iter::eval_bitmap_query_bucket_iter;
47pub use stream::eval_bitmap_query_stream;
48pub use stream::flatten_watermarked_buckets;
49
50#[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 pub(crate) fn reopen_cost_rows(self) -> u64 {
219 self.drain_probe_rows
220 .map_or(0, |probe| u64::from(probe.get()))
221 }
222}
223
224impl ScanDirection {
225 pub fn is_ascending(self) -> bool {
226 matches!(self, Self::Ascending)
227 }
228}
229
230#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub struct WatermarkPacing {
257 pub min_buckets: u64,
259 pub emit_held_at_end: bool,
261}
262
263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
267pub struct BitmapScanMetrics {
268 pub buckets_evaluated: u64,
271 pub buckets_discarded: u64,
273 pub leaf_seeks: u64,
275}
276
277#[derive(Clone)]
281pub(crate) struct BitmapScanBudget {
282 initial: u64,
283 remaining: Arc<AtomicU64>,
284 discarded: Arc<AtomicU64>,
285 seeks: Arc<AtomicU64>,
286 reopen_steps: Arc<AtomicU64>,
287}
288
289impl BitmapScanBudget {
290 pub(crate) fn new(initial: u64) -> Self {
291 Self {
292 initial,
293 remaining: Arc::new(AtomicU64::new(initial)),
294 discarded: Arc::new(AtomicU64::new(0)),
295 seeks: Arc::new(AtomicU64::new(0)),
296 reopen_steps: Arc::new(AtomicU64::new(0)),
297 }
298 }
299
300 fn try_take(&self) -> bool {
302 self.remaining
303 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |b| {
304 if b == 0 { None } else { Some(b - 1) }
305 })
306 .is_ok()
307 }
308
309 fn take_first(&self) {
316 let _ = self
317 .remaining
318 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |b| {
319 Some(b.saturating_sub(1))
320 });
321 }
322
323 fn buckets_evaluated(&self) -> u64 {
326 self.initial
327 .saturating_sub(self.remaining.load(Ordering::SeqCst))
328 }
329
330 fn note_discarded(&self) {
331 self.discarded.fetch_add(1, Ordering::Relaxed);
332 }
333
334 fn note_seek(&self) {
335 self.seeks.fetch_add(1, Ordering::Relaxed);
336 }
337
338 fn note_reopen_step(&self) {
340 self.reopen_steps.fetch_add(1, Ordering::Relaxed);
341 }
342
343 fn discarded(&self) -> u64 {
344 self.discarded.load(Ordering::Relaxed)
345 }
346
347 fn seeks(&self) -> u64 {
348 self.seeks.load(Ordering::Relaxed)
349 }
350
351 fn reopen_steps(&self) -> u64 {
354 self.reopen_steps.load(Ordering::Relaxed)
355 }
356}
357
358pub(crate) struct ObserveOnDrop<F: FnOnce(BitmapScanMetrics)> {
361 budget: BitmapScanBudget,
362 callback: Option<F>,
363}
364
365impl<F: FnOnce(BitmapScanMetrics)> Drop for ObserveOnDrop<F> {
366 fn drop(&mut self) {
367 if let Some(cb) = self.callback.take() {
368 cb(BitmapScanMetrics {
369 buckets_evaluated: self.budget.buckets_evaluated(),
370 buckets_discarded: self.budget.discarded(),
371 leaf_seeks: self.budget.seeks(),
372 });
373 }
374 }
375}
376
377pub(crate) struct WatermarkPacer {
384 pacing: WatermarkPacing,
385 budget: BitmapScanBudget,
386 reopen_cost_rows: u64,
388 range: Range<u64>,
390 bucket_size: u64,
391 held: Option<u64>,
393 work_at_last_emission: Option<u64>,
395}
396
397impl WatermarkPacer {
398 pub(crate) fn new(
399 pacing: WatermarkPacing,
400 policy: SkipPolicy,
401 budget: BitmapScanBudget,
402 range: Range<u64>,
403 bucket_size: u64,
404 ) -> Self {
405 Self {
406 pacing,
407 budget,
408 reopen_cost_rows: policy.reopen_cost_rows(),
409 range,
410 bucket_size,
411 held: None,
412 work_at_last_emission: None,
413 }
414 }
415
416 pub(crate) fn pace(&mut self, output: WatermarkedBucket) -> Option<WatermarkedBucket> {
419 match output {
420 Ok(Watermarked::Item((bucket, bitmap))) => {
421 if self.has_member_in_range(bucket, &bitmap) {
422 self.record_emission(self.scan_work());
423 }
424 Some(Ok(Watermarked::Item((bucket, bitmap))))
425 }
426 Ok(Watermarked::Watermark(position)) => {
427 let work = self.scan_work();
428 let due = self
429 .work_at_last_emission
430 .is_none_or(|last| work.saturating_sub(last) >= self.pacing.min_buckets);
431 if due {
432 self.record_emission(work);
433 Some(Ok(Watermarked::Watermark(position)))
434 } else {
435 self.held = Some(position);
436 None
437 }
438 }
439 Err(stop) => {
440 self.held = None;
441 Some(Err(stop))
442 }
443 }
444 }
445
446 fn scan_work(&self) -> u64 {
448 self.budget.buckets_evaluated().saturating_add(
449 self.reopen_cost_rows
450 .saturating_mul(self.budget.reopen_steps()),
451 )
452 }
453
454 fn record_emission(&mut self, work: u64) {
455 self.held = None;
456 self.work_at_last_emission = Some(work);
457 }
458
459 fn has_member_in_range(&self, bucket: u64, bitmap: &RoaringBitmap) -> bool {
462 let bucket_start = bucket.saturating_mul(self.bucket_size);
463 let low = self
464 .range
465 .start
466 .saturating_sub(bucket_start)
467 .min(self.bucket_size);
468 let high = self
469 .range
470 .end
471 .saturating_sub(bucket_start)
472 .min(self.bucket_size);
473 low < high && bitmap.range(low as u32..high as u32).next().is_some()
474 }
475
476 pub(crate) fn finish(&mut self) -> Option<u64> {
478 let held = self.held.take();
479 held.filter(|_| self.pacing.emit_held_at_end)
480 }
481}
482
483pub fn dense_universe_buckets(
492 range: Range<u64>,
493 bucket_size: u64,
494 direction: ScanDirection,
495) -> DenseUniverseBuckets {
496 let bits = u32::try_from(bucket_size).expect("bucket size fits in u32");
497 let buckets = if range.is_empty() {
498 0..0
499 } else {
500 (range.start / bucket_size)..(range.end - 1) / bucket_size + 1
501 };
502 DenseUniverseBuckets {
503 buckets,
504 direction,
505 bits,
506 }
507}
508
509pub struct DenseUniverseBuckets {
511 buckets: Range<u64>,
512 direction: ScanDirection,
513 bits: u32,
514}
515
516impl DenseUniverseBuckets {
517 pub fn seek_bucket(&mut self, bucket: u64) {
519 match self.direction {
520 ScanDirection::Ascending => self.buckets.start = self.buckets.start.max(bucket),
521 ScanDirection::Descending => {
522 self.buckets.end = self.buckets.end.min(bucket.saturating_add(1))
523 }
524 }
525 }
526}
527
528impl Iterator for DenseUniverseBuckets {
529 type Item = (u64, RoaringBitmap);
530
531 fn next(&mut self) -> Option<Self::Item> {
532 let bucket = match self.direction {
533 ScanDirection::Ascending => self.buckets.next(),
534 ScanDirection::Descending => self.buckets.next_back(),
535 }?;
536 let mut bitmap = RoaringBitmap::new();
537 bitmap.insert_range(0..self.bits);
538 Some((bucket, bitmap))
539 }
540}
541
542pub trait BitmapBucketSource: Clone + Send + 'static {
548 fn scan_bucket_stream(
549 &self,
550 dimension_key: Vec<u8>,
551 range: Range<u64>,
552 direction: ScanDirection,
553 ) -> BucketStream;
554}
555
556pub trait SeekableBucketIterator: Iterator<Item = BucketItem> {
558 fn seek_bucket(&mut self, bucket: u64);
562}
563
564pub trait BitmapBucketIteratorSource<'a>: Clone + 'a {
571 type Iter: SeekableBucketIterator + 'a;
572
573 fn scan_bucket_iter(
574 &self,
575 dimension_key: Vec<u8>,
576 range: Range<u64>,
577 direction: ScanDirection,
578 ) -> Self::Iter;
579}
580
581#[derive(Clone, Debug)]
586pub struct BitmapQuery {
587 terms: Vec<BitmapTerm>,
588}
589
590#[derive(Clone, Debug)]
595pub struct BitmapTerm {
596 literals: Vec<BitmapLiteral>,
597}
598
599#[derive(Clone, Debug, Eq, PartialEq, Hash)]
601pub struct BitmapKey(Vec<u8>);
602
603#[derive(Clone, Debug)]
605pub enum BitmapLiteral {
606 Include(BitmapKey),
607 Exclude(BitmapKey),
608}
609
610impl BitmapKey {
611 pub fn new(bytes: Vec<u8>) -> Result<Self> {
612 if bytes.is_empty() {
613 bail!("bitmap dimension key must not be empty");
614 }
615 if bytes.len() == 1 {
616 bail!("bitmap dimension value must not be empty");
617 }
618 if IndexDimension::from_tag_byte(bytes[0]).is_none() {
619 bail!("unknown bitmap dimension tag {}", bytes[0]);
620 }
621 Ok(Self(bytes))
622 }
623
624 pub fn into_inner(self) -> Vec<u8> {
625 self.0
626 }
627
628 pub fn as_bytes(&self) -> &[u8] {
629 &self.0
630 }
631}
632
633impl TryFrom<Vec<u8>> for BitmapKey {
634 type Error = anyhow::Error;
635
636 fn try_from(value: Vec<u8>) -> Result<Self> {
637 Self::new(value)
638 }
639}
640
641impl BitmapLiteral {
642 pub fn include(dimension_key: Vec<u8>) -> Result<Self> {
643 Ok(Self::Include(BitmapKey::new(dimension_key)?))
644 }
645
646 pub fn exclude(dimension_key: Vec<u8>) -> Result<Self> {
647 Ok(Self::Exclude(BitmapKey::new(dimension_key)?))
648 }
649
650 pub fn key_bytes(&self) -> &[u8] {
651 match self {
652 BitmapLiteral::Include(k) | BitmapLiteral::Exclude(k) => k.as_bytes(),
653 }
654 }
655}
656
657impl BitmapQuery {
658 pub fn new(terms: Vec<BitmapTerm>) -> Result<Self> {
659 if terms.is_empty() {
660 bail!("bitmap query must contain at least one term");
661 }
662 Ok(Self { terms })
663 }
664
665 pub fn scan(dimension_key: Vec<u8>) -> Result<Self> {
666 Ok(Self {
667 terms: vec![BitmapTerm::new(vec![BitmapLiteral::include(
668 dimension_key,
669 )?])?],
670 })
671 }
672
673 pub fn unique_leaf_count(&self) -> usize {
679 self.terms
680 .iter()
681 .flat_map(|t| t.literals.iter().map(|l| l.key_bytes()))
682 .collect::<HashSet<_>>()
683 .len()
684 }
685
686 pub fn terms(&self) -> &[BitmapTerm] {
687 &self.terms
688 }
689}
690
691impl BitmapTerm {
692 pub fn new(literals: Vec<BitmapLiteral>) -> Result<Self> {
693 if !literals
694 .iter()
695 .any(|literal| matches!(literal, BitmapLiteral::Include(_)))
696 {
697 bail!("bitmap query term must contain at least one include literal");
698 }
699 Ok(Self { literals })
700 }
701
702 pub fn literals(&self) -> &[BitmapLiteral] {
703 &self.literals
704 }
705}
706
707pub(crate) struct DedupedQuery {
711 pub(crate) keys: Vec<Vec<u8>>,
712 pub(crate) terms: Vec<TermSpec>,
713}
714
715pub(crate) fn build_term_specs(terms: Vec<BitmapTerm>) -> DedupedQuery {
722 let mut key_to_idx: HashMap<Vec<u8>, usize> = HashMap::new();
723 let mut keys: Vec<Vec<u8>> = Vec::new();
724 let mut specs: Vec<TermSpec> = Vec::with_capacity(terms.len());
725 for term in terms {
726 let mut include_idx = Vec::with_capacity(term.literals.len());
727 let mut exclude_idx = Vec::with_capacity(term.literals.len());
728 for literal in term.literals {
729 let (push_target, key) = match literal {
730 BitmapLiteral::Include(k) => (&mut include_idx, k.into_inner()),
731 BitmapLiteral::Exclude(k) => (&mut exclude_idx, k.into_inner()),
732 };
733 let idx = match key_to_idx.get(&key) {
734 Some(&i) => i,
735 None => {
736 let i = keys.len();
737 key_to_idx.insert(key.clone(), i);
738 keys.push(key);
739 i
740 }
741 };
742 push_target.push(idx);
743 }
744 specs.push(TermSpec {
745 includes: include_idx,
746 excludes: exclude_idx,
747 unsatisfiable: false,
748 });
749 }
750 DedupedQuery { keys, terms: specs }
751}
752
753fn bound_in_direction(a: u64, b: u64, direction: ScanDirection) -> u64 {
757 match direction {
758 ScanDirection::Ascending => a.min(b),
759 ScanDirection::Descending => a.max(b),
760 }
761}
762pub(crate) fn advance_in_direction(a: u64, b: u64, direction: ScanDirection) -> u64 {
765 match direction {
766 ScanDirection::Ascending => a.max(b),
767 ScanDirection::Descending => a.min(b),
768 }
769}
770
771pub(crate) fn strictly_before(a: u64, b: u64, direction: ScanDirection) -> bool {
773 match direction {
774 ScanDirection::Ascending => a < b,
775 ScanDirection::Descending => a > b,
776 }
777}
778
779fn frontier_advanced(prev: Option<u64>, next: u64, direction: ScanDirection) -> bool {
783 match prev {
784 None => true,
785 Some(prev) => match direction {
786 ScanDirection::Ascending => next > prev,
787 ScanDirection::Descending => next < prev,
788 },
789 }
790}
791
792pub(crate) fn bucket_edges(
799 bucket: u64,
800 bucket_size: u64,
801 range: &Range<u64>,
802 direction: ScanDirection,
803) -> (u64, u64) {
804 let start = bucket.saturating_mul(bucket_size);
805 let end = start.saturating_add(bucket_size);
806 match direction {
807 ScanDirection::Ascending => (start.max(range.start), end.min(range.end)),
808 ScanDirection::Descending => (end.min(range.end), start.max(range.start)),
809 }
810}
811
812pub(crate) fn take_snapshot_bitmap(
818 snapshot: &mut [Option<RoaringBitmap>],
819 remaining_refs: &mut [usize],
820 on_floor: &[bool],
821 i: usize,
822) -> Option<RoaringBitmap> {
823 if !on_floor[i] {
824 return None;
825 }
826 if remaining_refs[i] > 1 {
827 remaining_refs[i] -= 1;
828 snapshot[i].clone()
829 } else {
830 remaining_refs[i] = remaining_refs[i].saturating_sub(1);
831 snapshot[i].take()
832 }
833}
834
835pub(crate) fn count_on_floor_refs(terms: &[TermSpec], on_floor: &[bool]) -> Vec<usize> {
839 let mut refs = vec![0usize; on_floor.len()];
840 for term in terms {
841 if term.unsatisfiable {
842 continue;
843 }
844 for &i in term.includes.iter().chain(term.excludes.iter()) {
845 if on_floor[i] {
846 refs[i] += 1;
847 }
848 }
849 }
850 refs
851}
852
853pub(crate) fn recompute_unreferenced(
862 terms: &[TermSpec],
863 class: &[Option<LeafHead>],
864 unreferenced: &mut [bool],
865) {
866 let leaf_count = unreferenced.len();
867 let mut referenced = vec![false; leaf_count];
868 for term in terms {
869 if term.unsatisfiable {
870 continue;
871 }
872 for &i in term.includes.iter().chain(term.excludes.iter()) {
873 if !unreferenced[i] && !matches!(class[i], Some(LeafHead::Eof)) {
874 referenced[i] = true;
875 }
876 }
877 }
878 for i in 0..leaf_count {
879 if !referenced[i] {
880 unreferenced[i] = true;
881 }
882 }
883}
884
885pub(crate) fn eval_term_at_bucket(
891 includes: Vec<Option<RoaringBitmap>>,
892 excludes: Vec<Option<RoaringBitmap>>,
893) -> Option<RoaringBitmap> {
894 let mut acc: Option<RoaringBitmap> = None;
895 for include in includes {
896 let bitmap = include?;
898 acc = Some(match acc {
899 None => bitmap,
900 Some(a) => a & bitmap,
901 });
902 }
903 let mut acc = acc?;
905 for exclude in excludes.into_iter().flatten() {
906 acc -= exclude;
907 }
908 (!acc.is_empty()).then_some(acc)
909}
910
911pub(crate) struct TermSpec {
914 pub(crate) includes: Vec<usize>,
915 pub(crate) excludes: Vec<usize>,
916 pub(crate) unsatisfiable: bool,
919}
920
921pub(crate) enum LeafHead {
923 Bucket(u64),
924 Eof,
925 Error,
926}
927pub(crate) fn leaf_skip_targets(
930 terms: &[TermSpec],
931 class: &[Option<LeafHead>],
932 unreferenced: &[bool],
933 direction: ScanDirection,
934) -> Vec<Option<u64>> {
935 let term_not_before: Vec<Option<u64>> = terms
937 .iter()
938 .map(|term| {
939 if term.unsatisfiable {
940 return None;
941 }
942 term.includes
948 .iter()
949 .filter_map(|&i| match class.get(i).and_then(Option::as_ref) {
950 Some(LeafHead::Bucket(bucket)) => Some(*bucket),
951 Some(LeafHead::Error | LeafHead::Eof) | None => None,
952 })
953 .reduce(|a, b| advance_in_direction(a, b, direction))
954 })
955 .collect();
956
957 class
960 .iter()
961 .enumerate()
962 .map(|(i, head)| {
963 if unreferenced.get(i).copied().unwrap_or(true) {
964 return None;
965 }
966 let Some(LeafHead::Bucket(head)) = head else {
967 return None;
968 };
969
970 let mut target = None;
971 for (term_index, term) in terms.iter().enumerate() {
972 if term.unsatisfiable
973 || !term
974 .includes
975 .iter()
976 .chain(&term.excludes)
977 .any(|&leaf| leaf == i)
978 {
979 continue;
980 }
981
982 let not_before = term_not_before[term_index]?;
983 target = Some(match target {
984 None => not_before,
985 Some(current) => bound_in_direction(current, not_before, direction),
986 });
987 }
988 target.filter(|&target| strictly_before(*head, target, direction))
989 })
990 .collect()
991}
992
993#[cfg(test)]
994pub(crate) mod test_utils {
995 use std::collections::BTreeMap;
996 use std::collections::HashMap;
997 use std::sync::Arc;
998 use std::sync::Mutex;
999
1000 use futures::StreamExt;
1001 use futures::stream;
1002
1003 use super::*;
1004
1005 pub(crate) const BUCKET_SIZE: u64 = 100_000;
1006 pub(crate) type TestBuckets = BTreeMap<Vec<u8>, Vec<(u64, Vec<u32>)>>;
1007 type SeekRecorder = (Vec<u8>, Arc<Mutex<HashMap<Vec<u8>, usize>>>);
1008 pub(crate) struct VecBucketIter {
1009 items: std::vec::IntoIter<BucketItem>,
1010 direction: ScanDirection,
1011 recorder: Option<SeekRecorder>,
1012 }
1013
1014 impl Iterator for VecBucketIter {
1015 type Item = BucketItem;
1016
1017 fn next(&mut self) -> Option<Self::Item> {
1018 self.items.next()
1019 }
1020 }
1021
1022 impl SeekableBucketIterator for VecBucketIter {
1023 fn seek_bucket(&mut self, target: u64) {
1024 if let Some((key, seek_counts)) = &self.recorder {
1025 *seek_counts.lock().unwrap().entry(key.clone()).or_insert(0) += 1;
1026 }
1027 while matches!(
1028 self.items.as_slice().first(),
1029 Some(Ok((bucket, _))) if strictly_before(*bucket, target, self.direction)
1030 ) {
1031 self.items.next();
1032 }
1033 }
1034 }
1035
1036 #[derive(Clone)]
1037 pub(crate) struct TestBucketSource {
1038 pub(crate) buckets: Arc<TestBuckets>,
1039 }
1040
1041 impl BitmapBucketSource for TestBucketSource {
1042 fn scan_bucket_stream(
1043 &self,
1044 dimension_key: Vec<u8>,
1045 range: Range<u64>,
1046 direction: ScanDirection,
1047 ) -> BucketStream {
1048 stream::iter(self.bucket_items(&dimension_key, range, direction)).boxed()
1049 }
1050 }
1051
1052 impl<'a> BitmapBucketIteratorSource<'a> for TestBucketSource {
1053 type Iter = VecBucketIter;
1054
1055 fn scan_bucket_iter(
1056 &self,
1057 dimension_key: Vec<u8>,
1058 range: Range<u64>,
1059 direction: ScanDirection,
1060 ) -> Self::Iter {
1061 VecBucketIter {
1062 items: self
1063 .bucket_items(&dimension_key, range, direction)
1064 .into_iter(),
1065 direction,
1066 recorder: None,
1067 }
1068 }
1069 }
1070
1071 impl TestBucketSource {
1072 pub(crate) fn bucket_items(
1073 &self,
1074 dimension_key: &[u8],
1075 range: Range<u64>,
1076 direction: ScanDirection,
1077 ) -> Vec<BucketItem> {
1078 if dimension_key == universe_key() {
1081 return dense_universe_buckets(range, BUCKET_SIZE, direction)
1082 .map(Ok)
1083 .collect();
1084 }
1085 let mut buckets = self.buckets.get(dimension_key).cloned().unwrap_or_default();
1086 if range.is_empty() {
1087 buckets.clear();
1088 } else {
1089 let first_bucket = range.start / BUCKET_SIZE;
1090 let last_bucket = (range.end - 1) / BUCKET_SIZE;
1091 buckets.retain(|(bucket, _)| first_bucket <= *bucket && *bucket <= last_bucket);
1092 }
1093 if matches!(direction, ScanDirection::Descending) {
1094 buckets.reverse();
1095 }
1096 buckets
1097 .into_iter()
1098 .map(|(bucket_id, bits)| Ok((bucket_id, make_bitmap(&bits))))
1099 .collect()
1100 }
1101 }
1102
1103 pub(crate) fn make_bitmap(bits: &[u32]) -> RoaringBitmap {
1104 let mut bm = RoaringBitmap::new();
1105 for &b in bits {
1106 bm.insert(b);
1107 }
1108 bm
1109 }
1110
1111 pub(crate) fn test_key(value: &[u8]) -> Vec<u8> {
1112 crate::dimensions::encode_dimension_key(crate::dimensions::IndexDimension::Sender, value)
1113 }
1114
1115 pub(crate) fn universe_key() -> Vec<u8> {
1116 crate::dimensions::encode_dimension_key(
1117 crate::dimensions::IndexDimension::TxUniverse,
1118 crate::dimensions::TX_UNIVERSE_VALUE,
1119 )
1120 }
1121
1122 pub(crate) fn include(value: &[u8]) -> BitmapLiteral {
1123 BitmapLiteral::include(test_key(value)).unwrap()
1124 }
1125
1126 pub(crate) fn include_universe() -> BitmapLiteral {
1127 BitmapLiteral::include(universe_key()).unwrap()
1128 }
1129
1130 pub(crate) fn full_bucket() -> RoaringBitmap {
1133 let mut bm = RoaringBitmap::new();
1134 bm.insert_range(0..BUCKET_SIZE as u32);
1135 bm
1136 }
1137
1138 #[derive(Clone)]
1143 pub(crate) struct CountingBucketSource {
1144 pub(crate) buckets: Arc<TestBuckets>,
1145 scan_counts: Arc<Mutex<HashMap<Vec<u8>, usize>>>,
1146 seek_counts: Arc<Mutex<HashMap<Vec<u8>, usize>>>,
1147 }
1148
1149 impl CountingBucketSource {
1150 pub(crate) fn new(buckets: TestBuckets) -> Self {
1151 Self {
1152 buckets: Arc::new(buckets),
1153 scan_counts: Arc::new(Mutex::new(HashMap::new())),
1154 seek_counts: Arc::new(Mutex::new(HashMap::new())),
1155 }
1156 }
1157
1158 pub(crate) fn scan_count(&self, key: &[u8]) -> usize {
1159 self.scan_counts
1160 .lock()
1161 .unwrap()
1162 .get(key)
1163 .copied()
1164 .unwrap_or(0)
1165 }
1166 pub(crate) fn seek_count(&self, key: &[u8]) -> usize {
1167 self.seek_counts
1168 .lock()
1169 .unwrap()
1170 .get(key)
1171 .copied()
1172 .unwrap_or(0)
1173 }
1174
1175 fn record(&self, key: &[u8]) {
1176 *self
1177 .scan_counts
1178 .lock()
1179 .unwrap()
1180 .entry(key.to_vec())
1181 .or_insert(0) += 1;
1182 }
1183
1184 fn bucket_items(
1185 &self,
1186 dimension_key: &[u8],
1187 range: Range<u64>,
1188 direction: ScanDirection,
1189 ) -> Vec<BucketItem> {
1190 if dimension_key == universe_key() {
1191 return dense_universe_buckets(range, BUCKET_SIZE, direction)
1192 .map(Ok)
1193 .collect();
1194 }
1195 let mut buckets = self.buckets.get(dimension_key).cloned().unwrap_or_default();
1196 if range.is_empty() {
1197 buckets.clear();
1198 } else {
1199 let first_bucket = range.start / BUCKET_SIZE;
1200 let last_bucket = (range.end - 1) / BUCKET_SIZE;
1201 buckets.retain(|(bucket, _)| first_bucket <= *bucket && *bucket <= last_bucket);
1202 }
1203 if matches!(direction, ScanDirection::Descending) {
1204 buckets.reverse();
1205 }
1206 buckets
1207 .into_iter()
1208 .map(|(bucket_id, bits)| Ok((bucket_id, make_bitmap(&bits))))
1209 .collect()
1210 }
1211 }
1212
1213 impl BitmapBucketSource for CountingBucketSource {
1214 fn scan_bucket_stream(
1215 &self,
1216 dimension_key: Vec<u8>,
1217 range: Range<u64>,
1218 direction: ScanDirection,
1219 ) -> BucketStream {
1220 self.record(&dimension_key);
1221 stream::iter(self.bucket_items(&dimension_key, range, direction)).boxed()
1222 }
1223 }
1224
1225 impl<'a> BitmapBucketIteratorSource<'a> for CountingBucketSource {
1226 type Iter = VecBucketIter;
1227
1228 fn scan_bucket_iter(
1229 &self,
1230 dimension_key: Vec<u8>,
1231 range: Range<u64>,
1232 direction: ScanDirection,
1233 ) -> Self::Iter {
1234 self.record(&dimension_key);
1235 VecBucketIter {
1236 items: self
1237 .bucket_items(&dimension_key, range, direction)
1238 .into_iter(),
1239 direction,
1240 recorder: Some((dimension_key, self.seek_counts.clone())),
1241 }
1242 }
1243 }
1244
1245 pub(crate) fn exclude(value: &[u8]) -> BitmapLiteral {
1246 BitmapLiteral::exclude(test_key(value)).unwrap()
1247 }
1248
1249 #[derive(Clone)]
1253 pub(crate) struct StoppingBucketSource {
1254 pub(crate) inner: TestBucketSource,
1255 pub(crate) stop_key: Vec<u8>,
1256 pub(crate) rows_before_stop: usize,
1257 pub(crate) stop: fn() -> LeafStop,
1258 }
1259
1260 impl StoppingBucketSource {
1261 fn bucket_items(
1262 &self,
1263 dimension_key: &[u8],
1264 range: Range<u64>,
1265 direction: ScanDirection,
1266 ) -> Vec<BucketItem> {
1267 let mut items = self.inner.bucket_items(dimension_key, range, direction);
1268 if dimension_key == self.stop_key {
1269 items.truncate(self.rows_before_stop);
1270 items.push(Err((self.stop)()));
1271 }
1272 items
1273 }
1274 }
1275
1276 impl BitmapBucketSource for StoppingBucketSource {
1277 fn scan_bucket_stream(
1278 &self,
1279 dimension_key: Vec<u8>,
1280 range: Range<u64>,
1281 direction: ScanDirection,
1282 ) -> BucketStream {
1283 stream::iter(self.bucket_items(&dimension_key, range, direction)).boxed()
1284 }
1285 }
1286
1287 impl<'a> BitmapBucketIteratorSource<'a> for StoppingBucketSource {
1288 type Iter = VecBucketIter;
1289
1290 fn scan_bucket_iter(
1291 &self,
1292 dimension_key: Vec<u8>,
1293 range: Range<u64>,
1294 direction: ScanDirection,
1295 ) -> Self::Iter {
1296 VecBucketIter {
1297 items: self
1298 .bucket_items(&dimension_key, range, direction)
1299 .into_iter(),
1300 direction,
1301 recorder: None,
1302 }
1303 }
1304 }
1305
1306 pub(crate) fn parallel_leaves_source(
1311 bucket_count: u64,
1312 matching_buckets: &[u64],
1313 ) -> TestBucketSource {
1314 const MATCHED_MEMBERS: &[u32] = &[1];
1315 let matches: Vec<(u64, &[u32])> = matching_buckets
1316 .iter()
1317 .map(|&bucket| (bucket, MATCHED_MEMBERS))
1318 .collect();
1319 parallel_leaves_source_with_matches(bucket_count, &matches)
1320 }
1321
1322 pub(crate) fn parallel_leaves_source_with_matches(
1326 bucket_count: u64,
1327 matches: &[(u64, &[u32])],
1328 ) -> TestBucketSource {
1329 let leaf_rows = |own_member: u32| -> Vec<(u64, Vec<u32>)> {
1330 (0..bucket_count)
1331 .map(|bucket| {
1332 let mut members = vec![own_member];
1333 for (_, matched) in matches.iter().filter(|(b, _)| *b == bucket) {
1334 members.extend_from_slice(matched);
1335 }
1336 (bucket, members)
1337 })
1338 .collect()
1339 };
1340 TestBucketSource {
1341 buckets: Arc::new(BTreeMap::from([
1342 (test_key(b"a"), leaf_rows(1)),
1343 (test_key(b"b"), leaf_rows(2)),
1344 ])),
1345 }
1346 }
1347
1348 #[derive(Clone)]
1354 pub(crate) struct ScanWorkCountingSource {
1355 inner: TestBucketSource,
1356 rows_yielded: Arc<AtomicU64>,
1357 reopens: Arc<AtomicU64>,
1358 opened_keys: Arc<Mutex<HashSet<Vec<u8>>>>,
1359 }
1360
1361 impl ScanWorkCountingSource {
1362 pub(crate) fn new(inner: TestBucketSource) -> Self {
1363 Self {
1364 inner,
1365 rows_yielded: Arc::new(AtomicU64::new(0)),
1366 reopens: Arc::new(AtomicU64::new(0)),
1367 opened_keys: Arc::new(Mutex::new(HashSet::new())),
1368 }
1369 }
1370
1371 pub(crate) fn rows_yielded(&self) -> u64 {
1372 self.rows_yielded.load(Ordering::SeqCst)
1373 }
1374
1375 pub(crate) fn reopens(&self) -> u64 {
1376 self.reopens.load(Ordering::SeqCst)
1377 }
1378 }
1379
1380 impl BitmapBucketSource for ScanWorkCountingSource {
1381 fn scan_bucket_stream(
1382 &self,
1383 dimension_key: Vec<u8>,
1384 range: Range<u64>,
1385 direction: ScanDirection,
1386 ) -> BucketStream {
1387 let first_open = self
1388 .opened_keys
1389 .lock()
1390 .unwrap()
1391 .insert(dimension_key.clone());
1392 if !first_open {
1393 self.reopens.fetch_add(1, Ordering::SeqCst);
1394 }
1395 let rows_yielded = self.rows_yielded.clone();
1396 self.inner
1397 .scan_bucket_stream(dimension_key, range, direction)
1398 .inspect(move |row| {
1399 if row.is_ok() {
1400 rows_yielded.fetch_add(1, Ordering::SeqCst);
1401 }
1402 })
1403 .boxed()
1404 }
1405 }
1406
1407 impl<'a> BitmapBucketIteratorSource<'a> for ScanWorkCountingSource {
1408 type Iter = ScanWorkCountingIter;
1409
1410 fn scan_bucket_iter(
1411 &self,
1412 dimension_key: Vec<u8>,
1413 range: Range<u64>,
1414 direction: ScanDirection,
1415 ) -> Self::Iter {
1416 ScanWorkCountingIter {
1417 inner: self.inner.scan_bucket_iter(dimension_key, range, direction),
1418 rows_yielded: self.rows_yielded.clone(),
1419 reopens: self.reopens.clone(),
1420 }
1421 }
1422 }
1423
1424 pub(crate) struct ScanWorkCountingIter {
1425 inner: VecBucketIter,
1426 rows_yielded: Arc<AtomicU64>,
1427 reopens: Arc<AtomicU64>,
1428 }
1429
1430 impl Iterator for ScanWorkCountingIter {
1431 type Item = BucketItem;
1432
1433 fn next(&mut self) -> Option<BucketItem> {
1434 let row = self.inner.next();
1435 if matches!(row, Some(Ok(_))) {
1436 self.rows_yielded.fetch_add(1, Ordering::SeqCst);
1437 }
1438 row
1439 }
1440 }
1441
1442 impl SeekableBucketIterator for ScanWorkCountingIter {
1443 fn seek_bucket(&mut self, target: u64) {
1444 self.reopens.fetch_add(1, Ordering::SeqCst);
1445 self.inner.seek_bucket(target);
1446 }
1447 }
1448
1449 pub(crate) fn parallel_leaves_query() -> BitmapQuery {
1451 BitmapQuery::new(vec![
1452 BitmapTerm::new(vec![include(b"a"), include(b"b")]).unwrap(),
1453 ])
1454 .unwrap()
1455 }
1456
1457 pub(crate) const EVERY_WATERMARK: WatermarkPacing = WatermarkPacing {
1459 min_buckets: 0,
1460 emit_held_at_end: false,
1461 };
1462
1463 pub(crate) fn charged_buckets(min_buckets: u64, emit_held_at_end: bool) -> WatermarkPacing {
1464 WatermarkPacing {
1465 min_buckets,
1466 emit_held_at_end,
1467 }
1468 }
1469
1470 #[derive(Clone, Debug, Eq, PartialEq)]
1472 pub(crate) enum ObservedStop {
1473 ScanLimit(u64),
1474 Cancelled,
1475 Fault,
1476 }
1477
1478 pub(crate) type ObservedMemberOutput = Result<Watermarked<u64>, ObservedStop>;
1480
1481 pub(crate) fn observe_member_outputs(
1482 outputs: Vec<Result<Watermarked<u64>, ScanStop>>,
1483 ) -> Vec<ObservedMemberOutput> {
1484 outputs
1485 .into_iter()
1486 .map(|output| {
1487 output.map_err(|stop| match stop {
1488 ScanStop::ScanLimit { scan_frontier } => ObservedStop::ScanLimit(scan_frontier),
1489 ScanStop::Cancelled => ObservedStop::Cancelled,
1490 ScanStop::Fault(_) => ObservedStop::Fault,
1491 })
1492 })
1493 .collect()
1494 }
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499 use super::test_utils::BUCKET_SIZE;
1500 use super::test_utils::exclude;
1501 use super::test_utils::include;
1502 use super::test_utils::make_bitmap;
1503 use super::*;
1504
1505 fn paced_watermark(position: u64) -> WatermarkedBucket {
1506 Ok(Watermarked::Watermark(position))
1507 }
1508
1509 fn paced_item() -> WatermarkedBucket {
1511 Ok(Watermarked::Item((5, make_bitmap(&[1]))))
1512 }
1513
1514 #[test]
1520 fn scan_work_adds_charged_rows_and_reopen_steps() {
1521 let budget = BitmapScanBudget::new(1_000);
1522 let pacing = WatermarkPacing {
1523 min_buckets: 100,
1524 emit_held_at_end: false,
1525 };
1526 let policy = SkipPolicy {
1527 drain_probe_rows: std::num::NonZeroU32::new(50),
1528 };
1529 let mut pacer = WatermarkPacer::new(
1530 pacing,
1531 policy,
1532 budget.clone(),
1533 0..(10 * BUCKET_SIZE),
1534 BUCKET_SIZE,
1535 );
1536 let steps = [
1539 (0, 0, paced_watermark(1), true),
1540 (60, 0, paced_watermark(2), false),
1541 (0, 1, paced_watermark(3), true),
1542 (0, 1, paced_watermark(4), false),
1543 (0, 0, paced_item(), true),
1544 (0, 1, paced_watermark(6), false),
1546 (49, 0, paced_watermark(7), false),
1547 (1, 0, paced_watermark(8), true),
1548 ];
1549 for (step, (rows, reopen_steps, output, emitted)) in steps.into_iter().enumerate() {
1550 for _ in 0..rows {
1551 assert!(budget.try_take());
1552 }
1553 for _ in 0..reopen_steps {
1554 budget.note_reopen_step();
1555 }
1556 let paced = pacer.pace(output);
1557 assert_eq!(paced.is_some(), emitted, "step {step}: {paced:?}");
1558 }
1559 }
1560
1561 #[test]
1562 fn bitmap_query_validation_rejects_empty_shapes() {
1563 assert!(BitmapQuery::new(Vec::new()).is_err());
1564 assert!(BitmapLiteral::include(Vec::new()).is_err());
1565 assert!(
1566 BitmapLiteral::include(vec![crate::dimensions::IndexDimension::Sender.tag_byte()])
1567 .is_err()
1568 );
1569 assert!(BitmapLiteral::include(vec![0xff, 0x00]).is_err());
1570 assert!(BitmapTerm::new(vec![exclude(b"neg")]).is_err());
1571 }
1572
1573 #[test]
1574 fn dense_universe_buckets_covers_range_in_direction_order() {
1575 use super::test_utils::BUCKET_SIZE;
1576
1577 let asc: Vec<u64> =
1580 dense_universe_buckets(150_000..350_001, BUCKET_SIZE, ScanDirection::Ascending)
1581 .map(|(bucket, bitmap)| {
1582 assert_eq!(bitmap.len(), BUCKET_SIZE);
1583 bucket
1584 })
1585 .collect();
1586 assert_eq!(asc, vec![1, 2, 3]);
1587
1588 let desc: Vec<u64> =
1589 dense_universe_buckets(150_000..350_001, BUCKET_SIZE, ScanDirection::Descending)
1590 .map(|(bucket, _)| bucket)
1591 .collect();
1592 assert_eq!(desc, vec![3, 2, 1]);
1593
1594 let single: Vec<u64> = dense_universe_buckets(5..6, BUCKET_SIZE, ScanDirection::Ascending)
1595 .map(|(bucket, _)| bucket)
1596 .collect();
1597 assert_eq!(single, vec![0]);
1598
1599 assert_eq!(
1600 dense_universe_buckets(7..7, BUCKET_SIZE, ScanDirection::Ascending).count(),
1601 0
1602 );
1603 }
1604
1605 #[test]
1606 fn unique_leaf_count_counts_distinct_keys_across_terms() {
1607 let query = BitmapQuery::new(vec![
1611 BitmapTerm::new(vec![include(b"a"), include(b"b"), include(b"c")]).unwrap(),
1612 BitmapTerm::new(vec![include(b"a"), include(b"b"), include(b"d")]).unwrap(),
1613 ])
1614 .unwrap();
1615 assert_eq!(query.unique_leaf_count(), 4);
1616 }
1617
1618 #[test]
1619 fn build_term_specs_collapses_duplicate_keys_to_one_leaf() {
1620 let terms = vec![
1624 BitmapTerm::new(vec![include(b"a"), include(b"b")]).unwrap(),
1625 BitmapTerm::new(vec![include(b"b"), exclude(b"a")]).unwrap(),
1626 ];
1627 let DedupedQuery { keys, terms: specs } = build_term_specs(terms);
1628 assert_eq!(keys.len(), 2, "only `a` and `b` are unique");
1629 assert_eq!(specs[0].includes, vec![0, 1]);
1631 assert!(specs[0].excludes.is_empty());
1632 assert_eq!(specs[1].includes, vec![1]);
1634 assert_eq!(specs[1].excludes, vec![0]);
1635 }
1636 #[test]
1637 fn leaf_skip_targets_advance_lagging_include_in_both_directions() {
1638 let term = TermSpec {
1639 includes: vec![0, 1],
1640 excludes: vec![],
1641 unsatisfiable: false,
1642 };
1643 for (direction, heads, expected) in [
1644 (ScanDirection::Ascending, [2, 10], vec![Some(10), None]),
1645 (ScanDirection::Descending, [10, 2], vec![Some(2), None]),
1646 ] {
1647 let class = heads.map(|bucket| Some(LeafHead::Bucket(bucket)));
1648 assert_eq!(
1649 leaf_skip_targets(std::slice::from_ref(&term), &class, &[false; 2], direction),
1650 expected
1651 );
1652 }
1653 }
1654
1655 #[test]
1656 fn leaf_skip_targets_use_least_candidate_across_shared_terms() {
1657 let terms = [
1658 TermSpec {
1659 includes: vec![0],
1660 excludes: vec![1],
1661 unsatisfiable: false,
1662 },
1663 TermSpec {
1664 includes: vec![1],
1665 excludes: vec![],
1666 unsatisfiable: false,
1667 },
1668 ];
1669 for (direction, heads) in [
1670 (ScanDirection::Ascending, [50, 10]),
1671 (ScanDirection::Descending, [10, 50]),
1672 ] {
1673 let class = heads.map(|bucket| Some(LeafHead::Bucket(bucket)));
1674 assert_eq!(
1675 leaf_skip_targets(&terms, &class, &[false; 2], direction)[1],
1676 None
1677 );
1678 }
1679 }
1680
1681 #[test]
1682 fn leaf_skip_targets_drag_exclude_to_include_candidate() {
1683 let term = TermSpec {
1684 includes: vec![0],
1685 excludes: vec![1],
1686 unsatisfiable: false,
1687 };
1688 for (direction, heads, expected) in [
1689 (ScanDirection::Ascending, [50, 10], Some(50)),
1690 (ScanDirection::Descending, [10, 50], Some(10)),
1691 ] {
1692 let class = heads.map(|bucket| Some(LeafHead::Bucket(bucket)));
1693 assert_eq!(
1694 leaf_skip_targets(std::slice::from_ref(&term), &class, &[false; 2], direction)[1],
1695 expected
1696 );
1697 }
1698 }
1699
1700 #[test]
1701 fn leaf_skip_targets_drop_error_heads_and_poison_unknown_candidates() {
1702 for (direction, a_head, c_head) in [
1703 (ScanDirection::Ascending, 0, 9),
1704 (ScanDirection::Descending, 9, 0),
1705 ] {
1706 let terms = [
1707 TermSpec {
1708 includes: vec![0, 1],
1709 excludes: vec![],
1710 unsatisfiable: false,
1711 },
1712 TermSpec {
1713 includes: vec![0, 2],
1714 excludes: vec![],
1715 unsatisfiable: false,
1716 },
1717 ];
1718 let class = [
1719 Some(LeafHead::Bucket(a_head)),
1720 Some(LeafHead::Error),
1721 Some(LeafHead::Bucket(c_head)),
1722 ];
1723 assert_eq!(
1724 leaf_skip_targets(&terms, &class, &[false; 3], direction)[0],
1725 None
1726 );
1727
1728 let poisoned_term = TermSpec {
1729 includes: vec![1],
1730 excludes: vec![0],
1731 unsatisfiable: false,
1732 };
1733 assert_eq!(
1734 leaf_skip_targets(&[poisoned_term], &class, &[false; 3], direction)[0],
1735 None
1736 );
1737 }
1738 }
1739}