Skip to main content

sui_inverted_index/bitmap_query/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! DNF bitmap index queries over ordered bucket streams.
5//!
6//! Callers build a `BitmapQuery` as an OR of terms. Each term is an AND of
7//! signed dimension-key literals. Evaluation yields matching bitmap members as
8//! they are produced. Back-pressure from downstream consumers, e.g. a
9//! `.take(page_size)`, propagates back to the backend-provided bucket streams
10//! and avoids materializing matches we won't use.
11//!
12//! Queries are intentionally restricted to anchored DNF: every term must contain
13//! at least one positive literal. Positive literals give the evaluator concrete
14//! bitmap streams to scan and intersect; negative literals only shrink those
15//! candidate streams. Negative-only terms such as `NOT sender = A` are
16//! supported by anchoring them upstream on a universe include — a stored
17//! existence marker in event-space (`EventExtant`), a scan-time-synthesized
18//! dense leaf in tx-space (`TxUniverse`, see [`dense_universe_buckets`]) — so
19//! the evaluator itself stays a set of ordered stream merge-joins with no
20//! complement-specific code path. The full-range scan such a term implies is
21//! inherent to negation and is bounded by the per-request bucket budget.
22//!
23//! Backends provide one ordered `(bucket_id, RoaringBitmap)` stream or iterator
24//! per dimension key. The merge-join machinery here is storage-agnostic:
25//! BigTable, RocksDB, or any other backend can reuse it as long as its bucket
26//! source is sparse, ordered by the requested scan direction, and stores bitmap
27//! positions relative to that bucket.
28
29use 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// Cross-checked against the iterative evaluator in iter.rs tests.
51#[cfg(test)]
52pub(crate) use stream::eval_bitmap_query_bucket_stream;
53
54/// Terminal raised by a single leaf/backend bucket stream. Positionless by
55/// construction — one leaf cannot know the merged multi-term floor. Leaf stops
56/// never reach a List driver: the DNF evaluators consume them and re-raise a
57/// [`ScanStop`] carrying the merged frontier.
58#[derive(Debug, thiserror::Error)]
59pub enum LeafStop {
60    /// This leaf's share of the bucket-scan budget is exhausted.
61    #[error("bitmap scan limit reached")]
62    BudgetExhausted,
63    /// The request's cancellation token fired.
64    #[error("bitmap scan cancelled")]
65    Cancelled,
66    /// A backend/storage fault.
67    #[error(transparent)]
68    Fault(anyhow::Error),
69}
70
71/// Terminal signal of a merged bitmap eval stream, raised on the error channel
72/// so the `try_stream!` pipeline short-circuits (a clean end-of-stream means
73/// "scanned the whole range"). The List handlers map each variant to a wire
74/// outcome with one exhaustive match.
75///
76/// Mental model: leaves raise positionless [`LeafStop`]s; the evaluator turns
77/// them into a `ScanStop` that ALWAYS carries the resume frontier on
78/// `ScanLimit`; in-band `Watermarked::Watermark`s are only mid-scan progress
79/// beacons, never the resume channel.
80#[derive(Debug, thiserror::Error)]
81pub enum ScanStop {
82    /// Budget exhausted: a graceful early stop. The handler ends the stream
83    /// with `QUERY_END_REASON_SCAN_LIMIT`; the frontier is the resume cursor.
84    #[error("bitmap scan limit reached")]
85    ScanLimit {
86        /// Merged floor position every term provably scanned to when the budget
87        /// died — the exact value the stopping round's progress beacon would
88        /// have carried. It lives in the scanned index's member-id domain
89        /// (tx-seq for transaction/checkpoint indexes; encoded event-seq — see
90        /// `event_seq` — for the event index). Consumers apply their existing
91        /// domain conversion and resume arithmetic.
92        scan_frontier: u64,
93    },
94    /// Cancelled → gRPC `Cancelled` status.
95    #[error("bitmap scan cancelled")]
96    Cancelled,
97    /// Backend/storage fault → gRPC `Internal`, error carried unchanged.
98    #[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
114/// Reduce the leaf stops raised in one evaluator round to the driver-facing
115/// terminal. Precedence: Fault > BudgetExhausted > Cancelled (a real fault must
116/// surface as Internal; a budget stop with its frontier beats a bare Cancel —
117/// the resume point costs nothing to deliver). `frontier` is the merged floor
118/// the caller computed for this round; it is bound into `ScanLimit` only.
119/// Panics on empty input — evaluators only collapse when a leaf actually erred.
120pub(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/// Item or progress watermark flowing through a bitmap eval pipeline.
157/// `Watermark(p)` means every Item with position strictly before `p`
158/// in scan direction has been emitted upstream. Downstream stages must
159/// preserve watermark/item ordering — that's what makes the watermark a
160/// safe resume cursor on timeout.
161#[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
183/// A stream of `(bucket_id, RoaringBitmap)` in the requested bucket order.
184/// Bitmap positions are **relative** to the bucket (u32 offsets `[0, BUCKET_SIZE)`)
185/// - edge trimming against the requested range happens at the flatten step.
186pub type BucketItem = Result<(u64, RoaringBitmap), LeafStop>;
187pub type BucketStream = BoxStream<'static, BucketItem>;
188
189/// A bucket stream that interleaves data buckets with progress watermarks.
190/// The flat DNF driver derives each watermark from the slowest leaf's
191/// position, so the output always reflects "every source has scanned past P."
192pub(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/// Physical gap-crossing policy. Logical frontier jumps are unconditional;
201/// this only bounds how many charged dead rows a lagging leaf drains from its
202/// open scan per lag episode before seeking past the gap.
203#[derive(Clone, Copy, Debug)]
204pub struct SkipPolicy {
205    /// `None` never seeks and drains dead rows without bound.
206    pub drain_probe_rows: Option<std::num::NonZeroU32>,
207}
208
209impl SkipPolicy {
210    /// Drain dead rows without seeking; logical frontier jumps remain enabled.
211    pub const DRAIN_ONLY: Self = Self {
212        drain_probe_rows: None,
213    };
214
215    /// What a gap catch-up step that reopens a leaf scan costs, in bucket
216    /// rows: the dead rows this policy drains before it judges a reopen
217    /// cheaper. 0 when it never reopens.
218    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/// How often an evaluator emits the progress watermarks it computes.
231///
232/// A watermark is emitted once `min_buckets` of scan work have been done since
233/// the last emission; until then it is held, replacing any older held one.
234/// The scan's first output is never held. Items and stops pass through
235/// unchanged.
236///
237/// Scan work is counted in bucket rows: the rows charged to the budget
238/// ([`BitmapScanMetrics::buckets_evaluated`]), plus, for every gap catch-up
239/// step that reopened at least one leaf scan, the evaluation's
240/// [`SkipPolicy::drain_probe_rows`]. A reopen is a new backend read, and the
241/// skip policy reopens exactly when it judges that cheaper than draining that
242/// many dead rows, so it is priced at them. Leaves reopened in the same step
243/// count once, because the stream evaluator reopens them concurrently.
244///
245/// An emission is an emitted watermark or an item with a match inside the
246/// scan range; such an item also discards the held watermark. Items are whole
247/// buckets, so when the range starts or ends mid-bucket (e.g. a page resuming
248/// from a cursor) an edge item can hold only matches outside it. That item
249/// sends the client nothing and is not an emission.
250///
251/// The held watermark is discarded at every stop: a [`ScanStop::ScanLimit`]
252/// carries a frontier further along, and after a [`ScanStop::Fault`] or
253/// [`ScanStop::Cancelled`] the consumer is left with the last emitted
254/// watermark. At a clean end it is emitted only with `emit_held_at_end`.
255#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub struct WatermarkPacing {
257    /// Scan work, in bucket rows, between emissions. With 0 nothing is held.
258    pub min_buckets: u64,
259    /// Emit the held watermark at a clean end instead of discarding it.
260    pub emit_held_at_end: bool,
261}
262
263/// Bucket-scan accounting for one evaluation, passed once to `on_metrics`
264/// when the evaluator's stream or iterator is dropped. Nothing is reported
265/// when the stream evaluator rejects a misconfigured budget up front.
266#[derive(Clone, Copy, Debug, Eq, PartialEq)]
267pub struct BitmapScanMetrics {
268    /// Bucket rows charged against the budget, including dead rows drained
269    /// during gap catch-up.
270    pub buckets_evaluated: u64,
271    /// Charged dead bucket rows discarded while catching up lagging leaves.
272    pub buckets_discarded: u64,
273    /// Physical leaf scans abandoned and reopened at a later bucket.
274    pub leaf_seeks: u64,
275}
276
277/// Bucket budget shared by all leaf scans of one evaluation; each leaf charges
278/// a bucket as it pulls it (`stream::budgeted_bucket_stream`,
279/// `iter::BudgetedBucketIter`).
280#[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    /// Charge one bucket. Returns false on underflow.
301    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    /// Charge a leaf's first bucket; always succeeds. Without this
310    /// reservation a sparse term could drain the shared pool before a sibling
311    /// leaf reaches its first bucket, and that leaf could never report a
312    /// position (a cursorless `SCAN_LIMIT`). The pool is still decremented
313    /// when not empty, so `buckets_evaluated` undercounts only first buckets
314    /// taken after it ran out.
315    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    /// Bucket rows charged so far ([`BitmapScanMetrics::buckets_evaluated`]);
324    /// [`WatermarkPacer`] counts these.
325    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    /// Count one gap catch-up step that reopened at least one leaf scan.
339    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    /// Gap catch-up steps that reopened a leaf scan so far;
352    /// [`WatermarkPacer`] counts these.
353    fn reopen_steps(&self) -> u64 {
354        self.reopen_steps.load(Ordering::Relaxed)
355    }
356}
357
358/// Calls `on_metrics` once, on drop, with the final [`BitmapScanMetrics`].
359/// Evaluators keep it inside the stream or iterator they return.
360pub(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
377/// Applies a [`WatermarkPacing`] to one evaluation's bucket-level output,
378/// reading the scan work done from the evaluation's shared budget.
379///
380/// The stream evaluator outputs a round before that round's gap catch-up and
381/// the iterator evaluator after it, so when catch-up drains rows or reopens
382/// leaves the two can pace the same scan differently.
383pub(crate) struct WatermarkPacer {
384    pacing: WatermarkPacing,
385    budget: BitmapScanBudget,
386    /// [`SkipPolicy::reopen_cost_rows`] of the evaluation's skip policy.
387    reopen_cost_rows: u64,
388    /// Scan range; the member-level output drops matches outside it.
389    range: Range<u64>,
390    bucket_size: u64,
391    /// Newest watermark not yet emitted; always newer than every emitted one.
392    held: Option<u64>,
393    /// Scan work done at the last emission; `None` before the first.
394    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    /// The output to emit for one unpaced output: the output itself, or
417    /// nothing while a watermark is held.
418    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    /// Scan work done so far, in bucket rows (see [`WatermarkPacing`]).
447    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    /// Whether `bitmap` (offsets within `bucket`) has a member inside the scan
460    /// range.
461    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    /// The watermark to emit when the unpaced output ends without a stop.
477    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
483/// Synthesized bucket sequence for the dense tx-seq universe: one full bitmap
484/// per bucket touched by `range`, in scan-direction order. Backends return this
485/// for the query-only `IndexDimension::TxUniverse` key instead of reading
486/// storage — the tx-seq namespace is dense, so the universe is computable.
487///
488/// Bitmaps carry all `[0, bucket_size)` relative bits even in edge buckets;
489/// trimming against the requested range happens at the flatten step, same as
490/// for stored buckets.
491pub 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
509/// Synthesized dense-universe bucket iterator.
510pub struct DenseUniverseBuckets {
511    buckets: Range<u64>,
512    direction: ScanDirection,
513    bits: u32,
514}
515
516impl DenseUniverseBuckets {
517    /// Reposition to the first bucket at or past `bucket` in scan direction.
518    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
542/// Storage backend that can scan one bitmap dimension key over a member range.
543///
544/// The returned stream must be sparse and ordered by the requested direction.
545/// Missing bucket rows are interpreted as all-zero bitmaps by the merge-join
546/// operators.
547pub 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
556/// Synchronous bucket iterator that can reposition within its scan bounds.
557pub trait SeekableBucketIterator: Iterator<Item = BucketItem> {
558    /// Reposition so the next bucket is the first at or past `bucket` in scan
559    /// direction. Only the backend scan moves: the evaluator's adapter around
560    /// each leaf keeps the budget and the first-bucket reservation.
561    fn seek_bucket(&mut self, bucket: u64);
562}
563
564/// Storage backend that can scan one bitmap dimension key synchronously.
565///
566/// This is for request-local backends such as RocksDB, where the bucket scan
567/// naturally owns or borrows a synchronous iterator. The iterator evaluator is
568/// fully synchronous so these iterators can stay on the blocking task that owns
569/// them.
570pub 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/// A DNF query over bitmap dimension scans.
582///
583/// A query is a disjunction of terms. It must contain at least one term, and
584/// every term must be anchored by at least one included dimension key.
585#[derive(Clone, Debug)]
586pub struct BitmapQuery {
587    terms: Vec<BitmapTerm>,
588}
589
590/// One conjunction in a DNF bitmap query.
591///
592/// A term is a conjunction of signed literals. It must include at least one
593/// positive literal so the evaluator has a finite candidate stream to refine.
594#[derive(Clone, Debug)]
595pub struct BitmapTerm {
596    literals: Vec<BitmapLiteral>,
597}
598
599/// Validated `[dimension_tag][dimension_value]` lookup key.
600#[derive(Clone, Debug, Eq, PartialEq, Hash)]
601pub struct BitmapKey(Vec<u8>);
602
603/// One signed dimension-key literal in a bitmap term.
604#[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    /// Count of distinct dimension-key leaves the query will scan. Identical
674    /// keys across literals (whether within a term or across terms) collapse
675    /// to one leaf at evaluation time, so the per-request budget floor —
676    /// "every leaf can emit its first watermark" — applies to this
677    /// deduplicated count, not the raw literal total.
678    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
707/// Deduplicated leaf list + per-term references over those leaves, ready for
708/// the evaluator. `keys[i]` is the dimension-key bytes for leaf `i`; each
709/// [`TermSpec`] references leaves by that index.
710pub(crate) struct DedupedQuery {
711    pub(crate) keys: Vec<Vec<u8>>,
712    pub(crate) terms: Vec<TermSpec>,
713}
714
715/// Deduplicate literals across the whole query and translate each term's
716/// includes/excludes into indices over the shared leaf list.
717///
718/// Dedup is keyed on encoded dimension-key bytes, so the same key appearing
719/// in multiple terms (e.g. `(sender=A AND module=X) OR (sender=A AND
720/// type=Y)`) maps to a single backend scan instead of two duplicate scans.
721pub(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
753/// The less-advanced of two frontier positions in scan direction: the min
754/// ascending, the max descending. Used to keep a merged frontier bounded by
755/// the slowest contributor.
756fn 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}
762/// The more-advanced of two positions in scan direction: max ascending, min
763/// descending.
764pub(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
771/// Whether `a` is strictly before `b` in scan direction.
772pub(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
779/// Whether emitting `next` as a watermark advances the frontier past the
780/// previously emitted one. Ascending frontiers strictly increase,
781/// descending strictly decrease; the first watermark always advances.
782fn 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
792/// Clamped member-id edges of `bucket` in scan direction: `(pre, post)` where
793/// `pre` is the leading edge (everything before it is already covered) and
794/// `post` is the trailing edge (everything up to and including the bucket is
795/// covered). Ascending: `(low, high)`; descending: `(high, low)`. Both clamped
796/// to the request range so cursors stay in-bounds when they round-trip into a
797/// follow-up request with a different range.
798pub(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
812/// Pull the snapshotted bitmap for leaf `i` to give to one referencing term
813/// this round. Returns `None` if the leaf isn't on the floor bucket (so the
814/// term short-circuits). When `remaining_refs[i] > 1`, the bitmap is cloned so
815/// other referencing terms still see it; the last reference takes by value to
816/// avoid an unnecessary copy in the common single-reference case.
817pub(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
835/// Per-round refcount: how many satisfiable term-side slots reference each
836/// on-floor leaf. Drives [`take_snapshot_bitmap`]'s take-vs-clone decision so
837/// the last referencing slot reclaims the bitmap by value.
838pub(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
853/// Recompute leaf liveness from current term state. A leaf becomes
854/// `unreferenced` when no satisfiable term still points at it, or when its
855/// head is at EOF (the bucket stream is permanently exhausted; any further
856/// peek would be wasted work, and any include-referencing term will be marked
857/// `unsatisfiable` separately).
858///
859/// `unreferenced` is monotonic — entries only transition false → true — so
860/// this is safe to invoke each round; previously-retired leaves stay retired.
861pub(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
885/// Evaluate one DNF term at a single bucket from the per-leaf bitmaps present
886/// there: intersect the includes (any absent include ⇒ empty term), then
887/// subtract the union of the present excludes (`a AND NOT b`). Returns the
888/// term's matches at the bucket, or `None` if empty. Bitmaps are taken by value
889/// so the caller hands over the consumed leaf rows without cloning.
890pub(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        // A missing include means the intersection is empty at this bucket.
897        let bitmap = include?;
898        acc = Some(match acc {
899            None => bitmap,
900            Some(a) => a & bitmap,
901        });
902    }
903    // Anchored terms always carry at least one include, so `acc` is `Some`.
904    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
911/// One DNF term, as index spans into the flat leaf vector. Shared by the stream
912/// and iterator drivers.
913pub(crate) struct TermSpec {
914    pub(crate) includes: Vec<usize>,
915    pub(crate) excludes: Vec<usize>,
916    /// Set once any include leaf hits EOF: the term's intersection is
917    /// permanently empty (it can never match again). Latched.
918    pub(crate) unsatisfiable: bool,
919}
920
921/// A leaf's head this round, from a non-consuming peek.
922pub(crate) enum LeafHead {
923    Bucket(u64),
924    Eof,
925    Error,
926}
927/// Compute the furthest bucket each active leaf can skip to without changing
928/// query results.
929pub(crate) fn leaf_skip_targets(
930    terms: &[TermSpec],
931    class: &[Option<LeafHead>],
932    unreferenced: &[bool],
933    direction: ScanDirection,
934) -> Vec<Option<u64>> {
935    // Go through each conjunction in the top-level query disjunction.
936    let term_not_before: Vec<Option<u64>> = terms
937        .iter()
938        .map(|term| {
939            if term.unsatisfiable {
940                return None;
941            }
942            // For each conjunction, find its furthest-ahead include leaf.
943            // As far as this conjunction is concerned, every leaf it references
944            // can skip to that bucket. A shared leaf may still be constrained by
945            // another conjunction. Excludes don't set this bound. No exclude row
946            // just means nothing to subtract.
947            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    // Now with term-level knowledge, iterate through the deduped leaves. Find the nearest
958    // safe bound that we can skip ahead to among all terms that reference a leaf.
959    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            // Mirror the real backends: the tx-universe key is synthesized at
1079            // scan time, never read from stored buckets.
1080            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    /// Full `[0, BUCKET_SIZE)` bitmap — what the synthesized universe leaf
1131    /// yields per bucket.
1132    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    /// A `TestBucketSource` that records how many times `scan_bucket_*` is
1139    /// invoked per dimension key. Used to verify the evaluator deduplicates
1140    /// leaves across terms (a key referenced from multiple terms should be
1141    /// scanned exactly once).
1142    #[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    /// A [`TestBucketSource`] whose `stop_key` leaf yields `stop()` after its
1250    /// first `rows_before_stop` bucket rows in scan order, in place of the
1251    /// rest of its rows.
1252    #[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    /// Leaves `a` (member 1) and `b` (member 2) with a row in every bucket of
1307    /// `0..bucket_count`; `b` also holds member 1 in `matching_buckets`, the
1308    /// only buckets [`parallel_leaves_query`] matches. Neither leaf lags, so
1309    /// each bucket charges exactly two rows.
1310    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    /// [`parallel_leaves_source`] where [`parallel_leaves_query`] matches
1323    /// exactly the listed members (relative to the bucket start) of each
1324    /// listed bucket: both leaves hold them in addition to their own member.
1325    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    /// A [`TestBucketSource`] that counts the scan work its scans do: the
1349    /// rows they yield (until the budget runs out this equals
1350    /// [`BitmapScanMetrics::buckets_evaluated`]) and the leaf scans reopened
1351    /// ([`BitmapScanMetrics::leaf_seeks`]). A stream scan opened again for a
1352    /// key it has already opened, or a seek of an iterator scan, is a reopen.
1353    #[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    /// `a AND b` over [`parallel_leaves_source`].
1450    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    /// Pacing that holds no watermark: the evaluation's own output.
1458    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    /// Comparable form of a [`ScanStop`].
1471    #[derive(Clone, Debug, Eq, PartialEq)]
1472    pub(crate) enum ObservedStop {
1473        ScanLimit(u64),
1474        Cancelled,
1475        Fault,
1476    }
1477
1478    /// Comparable form of one member-level evaluator output.
1479    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    /// An item of bucket 5 with a match inside the pacers' scan range.
1510    fn paced_item() -> WatermarkedBucket {
1511        Ok(Watermarked::Item((5, make_bitmap(&[1]))))
1512    }
1513
1514    /// Scan work adds rows charged and, per reopen step, the skip policy's
1515    /// drain probe rows: here 100 of work is due, and a reopen step costs 50.
1516    /// Neither 60 rows nor one reopen step alone is due, but together they
1517    /// are. An item with a match in range is an emission too, so it restarts
1518    /// the count.
1519    #[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        // (rows charged and reopen steps just before the output, the output,
1537        // whether it is emitted)
1538        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            // 100 of work since the last watermark, but 50 since the item.
1545            (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        // Partial edge buckets still yield full bitmaps — trimming happens at
1578        // the flatten step.
1579        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        // Two terms both include `a` and `b`; only `c` is unique to term 1
1608        // and `d` to term 2. The raw literal count is 6, but only 4 unique
1609        // dimension keys are scanned at eval time.
1610        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        // Same key used as include in one term and exclude in another should
1621        // share a single leaf slot, with each term referring to it by the
1622        // same index.
1623        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        // Term 0: include a (slot 0), include b (slot 1).
1630        assert_eq!(specs[0].includes, vec![0, 1]);
1631        assert!(specs[0].excludes.is_empty());
1632        // Term 1: include b (slot 1), exclude a (slot 0) — same slots as term 0.
1633        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}