Skip to main content

lance_index/scalar/
bitmap.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use lance_core::utils::row_addr_remap::RowAddrRemap;
5use std::{
6    any::Any,
7    cmp::Reverse,
8    collections::{BTreeMap, BinaryHeap, HashMap},
9    fmt::Debug,
10    ops::Bound,
11    sync::Arc,
12};
13
14use arrow::array::BinaryBuilder;
15use arrow_array::{Array, BinaryArray, RecordBatch, UInt64Array, new_null_array};
16use arrow_schema::{DataType, Field, Schema};
17use async_trait::async_trait;
18use bytes::Bytes;
19use datafusion::physical_plan::SendableRecordBatchStream;
20use datafusion_common::ScalarValue;
21use futures::{StreamExt, TryStreamExt, stream};
22use lance_core::deepsize::DeepSizeOf;
23use lance_core::{
24    Error, ROW_ID, Result,
25    cache::{
26        CacheCodec, CacheCodecImpl, CacheEntryReader, CacheEntryWriter, CacheKey, CacheKeySchema,
27        KeyBuilder, LanceCache, WeakLanceCache,
28    },
29    error::LanceOptionExt,
30    utils::tokio::get_num_compute_intensive_cpus,
31};
32use lance_io::object_store::ObjectStore;
33use lance_select::{NullableRowAddrSet, RowAddrTreeMap, RowSetOps};
34use object_store::path::Path;
35use roaring::RoaringBitmap;
36use serde::{Deserialize, Serialize};
37use tracing::{instrument, warn};
38
39use super::{AnyQuery, IndexFile, IndexStore, ScalarIndex};
40use super::{
41    BuiltinIndexType, SargableQuery, ScalarIndexParams, SearchResult, btree::OrderableScalarValue,
42};
43use crate::pbold;
44use crate::{Index, IndexType, metrics::MetricsCollector};
45use crate::{
46    progress::IndexBuildProgress,
47    scalar::{
48        CreatedIndex, RowIdRemapper, UpdateCriteria,
49        expression::SargableQueryParser,
50        registry::{
51            BasicTrainer, ScalarIndexLoad, ScalarIndexPlugin, TrainingCriteria, TrainingOrdering,
52            TrainingRequest, VALUE_COLUMN_NAME, single_flight_open,
53        },
54    },
55};
56use crate::{scalar::IndexReader, scalar::expression::ScalarQueryParser};
57
58pub const BITMAP_LOOKUP_NAME: &str = "bitmap_page_lookup.lance";
59pub const INDEX_STATS_METADATA_KEY: &str = "lance:index_stats";
60const BITMAP_PART_LOOKUP_PREFIX: &str = "part_";
61const BITMAP_PART_LOOKUP_SUFFIX: &str = "_bitmap_page_lookup.lance";
62const EXPLICIT_SHARD_ID_TAG: u64 = 0;
63const IMPLICIT_FRAGMENT_ID_TAG: u64 = 1;
64
65const MAX_BITMAP_ARRAY_LENGTH: usize = i32::MAX as usize - 1024 * 1024; // leave headroom
66
67const MAX_ROWS_PER_CHUNK: usize = 2 * 1024;
68// Smaller than MAX_ROWS_PER_CHUNK to bound the per-cursor in-memory batch
69// footprint during a k-way merge (N cursors × chunk), while still amortising
70// I/O over a reasonable number of rows per read.
71const MERGE_ROWS_PER_CHUNK: usize = 512;
72
73const BITMAP_INDEX_VERSION: u32 = 0;
74
75// We only need to open a file reader if we need to load a bitmap. If all
76// bitmaps are cached we don't open it. If we do open it we should only open it once.
77#[derive(Clone)]
78struct LazyIndexReader {
79    index_reader: Arc<tokio::sync::Mutex<Option<Arc<dyn IndexReader>>>>,
80    store: Arc<dyn IndexStore>,
81}
82
83impl std::fmt::Debug for LazyIndexReader {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.debug_struct("LazyIndexReader")
86            .field("store", &self.store)
87            .finish()
88    }
89}
90
91impl LazyIndexReader {
92    fn new(store: Arc<dyn IndexStore>) -> Self {
93        Self {
94            index_reader: Arc::new(tokio::sync::Mutex::new(None)),
95            store,
96        }
97    }
98
99    async fn get(&self) -> Result<Arc<dyn IndexReader>> {
100        let mut reader = self.index_reader.lock().await;
101        if reader.is_none() {
102            let index_reader = self.store.open_index_file(BITMAP_LOOKUP_NAME).await?;
103            *reader = Some(index_reader);
104        }
105        Ok(reader.as_ref().unwrap().clone())
106    }
107}
108
109/// A scalar index that stores a bitmap for each possible value
110///
111/// This index works best for low-cardinality columns, where the number of unique values is small.
112/// The bitmap stores a list of row ids where the value is present.
113#[derive(Clone, Debug)]
114pub struct BitmapIndex {
115    /// Maps each unique value to its bitmap location in the index file
116    /// The usize value is the row offset in the bitmap_page_lookup.lance file
117    /// for quickly locating the row and reading it out
118    index_map: Arc<BTreeMap<OrderableScalarValue, usize>>,
119
120    null_map: Arc<RowAddrTreeMap>,
121
122    value_type: DataType,
123
124    store: Arc<dyn IndexStore>,
125
126    index_cache: WeakLanceCache,
127
128    frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
129
130    lazy_reader: LazyIndexReader,
131}
132
133#[derive(Debug, Clone)]
134pub struct BitmapKey {
135    row_offset: u64,
136}
137
138impl BitmapKey {
139    fn try_new(row_offset: usize) -> Result<Self> {
140        let row_offset = u64::try_from(row_offset).map_err(|_| {
141            Error::internal(format!(
142                "bitmap row offset {row_offset} does not fit in u64"
143            ))
144        })?;
145        Ok(Self { row_offset })
146    }
147}
148
149impl CacheKey for BitmapKey {
150    type ValueType = RowAddrTreeMap;
151
152    fn key(&self) -> std::borrow::Cow<'_, str> {
153        self.row_offset.to_string().into()
154    }
155
156    fn type_name() -> &'static str {
157        "Bitmap"
158    }
159
160    fn schema() -> CacheKeySchema {
161        CacheKeySchema::new("lance.scalar.bitmap-row-offset-key", 1)
162    }
163
164    fn write_key(&self, builder: &mut KeyBuilder) {
165        builder.write_u64(self.row_offset);
166    }
167
168    fn codec() -> Option<CacheCodec> {
169        Some(CacheCodec::from_impl::<RowAddrTreeMap>())
170    }
171}
172
173/// The serializable state of a [`BitmapIndex`].
174///
175/// `BitmapIndex` holds non-serializable infrastructure (an `IndexStore`, a
176/// cache handle, a lazy reader, a fragment-reuse index). `BitmapIndexState`
177/// captures just the data needed to rebuild it: the value→file-offset map,
178/// the null bitmap, and the value type.
179#[derive(Debug, Clone)]
180pub struct BitmapIndexState {
181    /// Value-to-row-offset lookup, encoded as an Arrow `RecordBatch` so we can
182    /// reuse the existing IPC utilities for zero-copy round trips.
183    ///
184    /// Schema: `keys: <value_type>`, `offsets: UInt64`. Iteration order of
185    /// `index_map` is preserved on serialize and the `BTreeMap` resorts the
186    /// entries on deserialize, so the wire form does not need to be sorted.
187    lookup_batch: RecordBatch,
188    /// Already-remapped null bitmap (remapping is applied during load, so the
189    /// cached state matches the in-memory representation).
190    null_map: Arc<RowAddrTreeMap>,
191    /// Cached separately from the schema for the empty-index case where the
192    /// `lookup_batch` is empty but we still need to remember the column type.
193    value_type: DataType,
194    /// Parsed form of `lookup_batch`. Not serialized — populated eagerly in
195    /// both [`BitmapIndexState::from_index`] and [`CacheCodecImpl::deserialize`].
196    /// Stored as `Arc` so cloning into a new [`BitmapIndex`] is O(1).
197    index_map: Arc<BTreeMap<OrderableScalarValue, usize>>,
198}
199
200impl DeepSizeOf for BitmapIndexState {
201    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
202        self.lookup_batch.get_array_memory_size()
203            + self.null_map.deep_size_of_children(context)
204            + self.index_map.deep_size_of_children(context)
205    }
206}
207
208impl BitmapIndexState {
209    pub(crate) fn from_index(index: &BitmapIndex) -> Result<Self> {
210        Ok(Self {
211            lookup_batch: build_lookup_batch(&index.index_map, &index.value_type)?,
212            null_map: index.null_map.clone(),
213            value_type: index.value_type.clone(),
214            index_map: index.index_map.clone(),
215        })
216    }
217
218    fn from_scalar_index(index: &dyn ScalarIndex) -> Result<Self> {
219        let bitmap = index
220            .as_any()
221            .downcast_ref::<BitmapIndex>()
222            .ok_or_else(|| {
223                Error::internal(
224                    "BitmapIndexState::from_scalar_index called with a non-bitmap index",
225                )
226            })?;
227        Self::from_index(bitmap)
228    }
229
230    pub(crate) fn to_bitmap_index(
231        &self,
232        store: Arc<dyn IndexStore>,
233        index_cache: &LanceCache,
234        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
235    ) -> Result<Arc<BitmapIndex>> {
236        Ok(Arc::new(BitmapIndex::new(
237            self.index_map.clone(),
238            self.null_map.clone(),
239            self.value_type.clone(),
240            store,
241            WeakLanceCache::from(index_cache),
242            frag_reuse_index,
243        )))
244    }
245
246    /// Build a state directly from its parts, for codec tests in sibling
247    /// modules (e.g. the label-list index, which nests a bitmap state).
248    #[cfg(test)]
249    pub(crate) fn new_for_test(
250        index_map: BTreeMap<OrderableScalarValue, usize>,
251        null_map: RowAddrTreeMap,
252        value_type: DataType,
253    ) -> Result<Self> {
254        Ok(Self {
255            lookup_batch: build_lookup_batch(&index_map, &value_type)?,
256            null_map: Arc::new(null_map),
257            value_type,
258            index_map: Arc::new(index_map),
259        })
260    }
261
262    #[cfg(test)]
263    pub(crate) fn lookup_batch(&self) -> &RecordBatch {
264        &self.lookup_batch
265    }
266
267    #[cfg(test)]
268    pub(crate) fn null_map(&self) -> &RowAddrTreeMap {
269        &self.null_map
270    }
271}
272
273fn build_lookup_batch(
274    index_map: &BTreeMap<OrderableScalarValue, usize>,
275    value_type: &DataType,
276) -> Result<RecordBatch> {
277    let keys = if index_map.is_empty() {
278        arrow_array::new_empty_array(value_type)
279    } else {
280        ScalarValue::iter_to_array(index_map.keys().map(|k| k.0.clone()))?
281    };
282    let offsets = Arc::new(UInt64Array::from_iter_values(
283        index_map.values().map(|v| *v as u64),
284    ));
285    let schema = Arc::new(Schema::new(vec![
286        Field::new("keys", value_type.clone(), true),
287        Field::new("offsets", DataType::UInt64, false),
288    ]));
289    Ok(RecordBatch::try_new(schema, vec![keys, offsets])?)
290}
291
292fn parse_lookup_batch(batch: &RecordBatch) -> Result<BTreeMap<OrderableScalarValue, usize>> {
293    let keys = batch.column(0);
294    let offsets = batch
295        .column(1)
296        .as_any()
297        .downcast_ref::<UInt64Array>()
298        .ok_or_else(|| {
299            Error::internal("BitmapIndexState: expected UInt64 offsets column".to_string())
300        })?;
301    let mut index_map = BTreeMap::new();
302    for idx in 0..batch.num_rows() {
303        let value = OrderableScalarValue(ScalarValue::try_from_array(keys, idx)?);
304        index_map.insert(value, offsets.value(idx) as usize);
305    }
306    Ok(index_map)
307}
308
309impl CacheCodecImpl for BitmapIndexState {
310    const TYPE_ID: &'static str = "lance.scalar.BitmapIndexState";
311    const CURRENT_VERSION: u32 = 1;
312
313    /// Wire format:
314    /// ```text
315    /// RAW_BLOB  : null_map (roaring tree map, portable encoding)
316    /// ARROW_IPC : (keys: <value_type>, offsets: UInt64)
317    /// ```
318    /// The value type is recovered from the IPC section schema.
319    fn serialize(&self, w: &mut CacheEntryWriter<'_>) -> Result<()> {
320        let mut null_bytes = Vec::with_capacity(self.null_map.serialized_size());
321        self.null_map.serialize_into(&mut null_bytes)?;
322        w.write_raw(&null_bytes)?;
323        w.write_ipc(&self.lookup_batch)?;
324        Ok(())
325    }
326
327    fn deserialize(r: &mut CacheEntryReader<'_>) -> Result<Self> {
328        let null_bytes = r.read_raw()?;
329        let null_map = Arc::new(RowAddrTreeMap::deserialize_from(null_bytes.as_ref())?);
330        let lookup_batch = r.read_ipc()?;
331        let value_type = lookup_batch.schema().field(0).data_type().clone();
332        let index_map = Arc::new(parse_lookup_batch(&lookup_batch)?);
333        Ok(Self {
334            lookup_batch,
335            null_map,
336            value_type,
337            index_map,
338        })
339    }
340}
341
342/// Cache key for a [`BitmapIndexState`]. The cache is already namespaced
343/// per-index by the caller, so a constant key suffices.
344struct BitmapIndexStateKey;
345
346impl CacheKey for BitmapIndexStateKey {
347    type ValueType = BitmapIndexState;
348
349    fn key(&self) -> std::borrow::Cow<'_, str> {
350        "state".into()
351    }
352
353    fn type_name() -> &'static str {
354        "BitmapIndexState"
355    }
356
357    fn schema() -> CacheKeySchema {
358        CacheKeySchema::new("lance.scalar.bitmap-index-state-key", 1)
359    }
360
361    fn write_key(&self, builder: &mut KeyBuilder) {
362        builder.write_variant(0);
363    }
364
365    fn codec() -> Option<CacheCodec> {
366        Some(CacheCodec::from_impl::<BitmapIndexState>())
367    }
368}
369
370impl BitmapIndex {
371    fn new(
372        index_map: Arc<BTreeMap<OrderableScalarValue, usize>>,
373        null_map: Arc<RowAddrTreeMap>,
374        value_type: DataType,
375        store: Arc<dyn IndexStore>,
376        index_cache: WeakLanceCache,
377        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
378    ) -> Self {
379        let lazy_reader = LazyIndexReader::new(store.clone());
380        Self {
381            index_map,
382            null_map,
383            value_type,
384            store,
385            index_cache,
386            frag_reuse_index,
387            lazy_reader,
388        }
389    }
390
391    pub(crate) async fn load(
392        store: Arc<dyn IndexStore>,
393        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
394        index_cache: &LanceCache,
395    ) -> Result<Arc<Self>> {
396        let page_lookup_file = store.open_index_file(BITMAP_LOOKUP_NAME).await?;
397        let total_rows = page_lookup_file.num_rows();
398
399        if total_rows == 0 {
400            let schema = page_lookup_file.schema();
401            let data_type = schema.fields[0].data_type();
402            return Ok(Arc::new(Self::new(
403                Arc::new(BTreeMap::new()),
404                Arc::new(RowAddrTreeMap::default()),
405                data_type,
406                store,
407                WeakLanceCache::from(index_cache),
408                frag_reuse_index,
409            )));
410        }
411
412        let mut index_map: BTreeMap<OrderableScalarValue, usize> = BTreeMap::new();
413        let mut null_map = Arc::new(RowAddrTreeMap::default());
414        let mut null_location: Option<usize> = None;
415        let value_type = page_lookup_file.schema().fields[0].data_type();
416
417        // Stream keys in bounded batches to avoid loading the entire keys
418        // column into memory at once.
419        let mut keys_stream = page_lookup_file
420            .read_range_stream(0..total_rows, Some(&["keys"]))
421            .await?;
422        let mut row_offset: usize = 0;
423        while let Some(keys_batch) = keys_stream.try_next().await? {
424            let dict_keys = keys_batch.column(0);
425            for idx in 0..keys_batch.num_rows() {
426                let key = OrderableScalarValue(ScalarValue::try_from_array(dict_keys, idx)?);
427                if key.0.is_null() {
428                    null_location = Some(row_offset);
429                } else {
430                    index_map.insert(key, row_offset);
431                }
432                row_offset += 1;
433            }
434        }
435
436        if let Some(null_loc) = null_location {
437            let batch = page_lookup_file
438                .read_range(null_loc..null_loc + 1, Some(&["bitmaps"]))
439                .await?;
440
441            let binary_bitmaps = batch
442                .column(0)
443                .as_any()
444                .downcast_ref::<BinaryArray>()
445                .ok_or_else(|| Error::internal("Invalid bitmap column type".to_string()))?;
446            let bitmap_bytes = binary_bitmaps.value(0);
447            let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
448
449            // Apply fragment remapping if needed
450            if let Some(fri) = &frag_reuse_index {
451                bitmap = fri.remap_row_addrs_tree_map(&bitmap);
452            }
453
454            null_map = Arc::new(bitmap);
455        }
456
457        Ok(Arc::new(Self::new(
458            Arc::new(index_map),
459            null_map,
460            value_type,
461            store,
462            WeakLanceCache::from(index_cache),
463            frag_reuse_index,
464        )))
465    }
466
467    async fn load_bitmap(
468        &self,
469        key: &OrderableScalarValue,
470        metrics: Option<&dyn MetricsCollector>,
471    ) -> Result<Arc<RowAddrTreeMap>> {
472        if key.0.is_null() {
473            return Ok(self.null_map.clone());
474        }
475
476        // A value that isn't in `index_map` never reaches the loader or the
477        // cache, so it should not touch the per-query cache counters either.
478        // Checking here (before the cached-lookup fast path) also avoids
479        // returning an unmapped-value response as a spurious cache hit if a
480        // prior insert somehow ended up under `cache_key`.
481        let row_offset = match self.index_map.get(key) {
482            Some(loc) => *loc,
483            None => return Ok(Arc::new(RowAddrTreeMap::default())),
484        };
485        let cache_key = BitmapKey::try_new(row_offset)?;
486
487        if let Some(cached) = self.index_cache.get_with_key(&cache_key).await {
488            if let Some(metrics) = metrics {
489                metrics.record_index_cache_hit();
490            }
491            return Ok(cached);
492        }
493
494        // Record that we're loading a partition from disk
495        if let Some(metrics) = metrics {
496            metrics.record_index_cache_miss();
497            metrics.record_part_load();
498        }
499
500        let page_lookup_file = self.lazy_reader.get().await?;
501        let batch = page_lookup_file
502            .read_range(row_offset..row_offset + 1, Some(&["bitmaps"]))
503            .await?;
504
505        let binary_bitmaps = batch
506            .column(0)
507            .as_any()
508            .downcast_ref::<BinaryArray>()
509            .ok_or_else(|| Error::internal("Invalid bitmap column type".to_string()))?;
510        let bitmap_bytes = binary_bitmaps.value(0); // First (and only) row
511        let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
512
513        if let Some(fri) = &self.frag_reuse_index {
514            bitmap = fri.remap_row_addrs_tree_map(&bitmap);
515        }
516
517        self.index_cache
518            .insert_with_key(&cache_key, Arc::new(bitmap.clone()))
519            .await;
520
521        Ok(Arc::new(bitmap))
522    }
523
524    pub(crate) fn value_type(&self) -> &DataType {
525        &self.value_type
526    }
527
528    /// Loads the current bitmap index into an in-memory value-to-row-id map.
529    pub(crate) async fn load_bitmap_index_state(
530        &self,
531    ) -> Result<HashMap<ScalarValue, RowAddrTreeMap>> {
532        let mut state = HashMap::new();
533
534        for key in self.index_map.keys() {
535            let bitmap = self.load_bitmap(key, None).await?;
536            state.insert(key.0.clone(), (*bitmap).clone());
537        }
538
539        if !self.null_map.is_empty() {
540            let existing_null = new_null_array(&self.value_type, 1);
541            let existing_null = ScalarValue::try_from_array(existing_null.as_ref(), 0)?;
542            state.insert(existing_null, (*self.null_map).clone());
543        }
544
545        Ok(state)
546    }
547}
548
549impl DeepSizeOf for BitmapIndex {
550    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
551        self.index_map.deep_size_of_children(context) + self.store.deep_size_of_children(context)
552    }
553}
554
555#[derive(Serialize)]
556struct BitmapStatistics {
557    num_bitmaps: usize,
558}
559
560#[derive(Debug, Clone, Default, Serialize, Deserialize)]
561pub struct BitmapParameters {
562    /// Optional shard identifier for distributed bitmap builds spanning
563    /// multiple fragments.
564    pub shard_id: Option<u32>,
565}
566
567struct BitmapTrainingRequest {
568    parameters: BitmapParameters,
569    criteria: TrainingCriteria,
570}
571
572impl BitmapTrainingRequest {
573    fn new(parameters: BitmapParameters) -> Self {
574        Self {
575            parameters,
576            criteria: TrainingCriteria::new(TrainingOrdering::Values).with_row_id(),
577        }
578    }
579}
580
581impl TrainingRequest for BitmapTrainingRequest {
582    fn as_any(&self) -> &dyn std::any::Any {
583        self
584    }
585
586    fn criteria(&self) -> &TrainingCriteria {
587        &self.criteria
588    }
589}
590
591#[async_trait]
592impl Index for BitmapIndex {
593    fn as_any(&self) -> &dyn Any {
594        self
595    }
596
597    fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
598        self
599    }
600
601    async fn prewarm(&self) -> Result<()> {
602        let page_lookup_file = self.lazy_reader.get().await?;
603        let total_rows = page_lookup_file.num_rows();
604
605        if total_rows == 0 {
606            return Ok(());
607        }
608
609        for start_row in (0..total_rows).step_by(MAX_ROWS_PER_CHUNK) {
610            let end_row = (start_row + MAX_ROWS_PER_CHUNK).min(total_rows);
611            let chunk = page_lookup_file
612                .read_range(start_row..end_row, None)
613                .await?;
614
615            if chunk.num_rows() == 0 {
616                continue;
617            }
618
619            let dict_keys = chunk.column(0);
620            let binary_bitmaps = chunk.column(1);
621            let bitmap_binary_array = binary_bitmaps
622                .as_any()
623                .downcast_ref::<BinaryArray>()
624                .unwrap();
625
626            for idx in 0..chunk.num_rows() {
627                let key = OrderableScalarValue(ScalarValue::try_from_array(dict_keys, idx)?);
628
629                if key.0.is_null() {
630                    continue;
631                }
632
633                let bitmap_bytes = bitmap_binary_array.value(idx);
634                let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
635
636                if let Some(frag_reuse_index_ref) = self.frag_reuse_index.as_ref() {
637                    bitmap = frag_reuse_index_ref.remap_row_addrs_tree_map(&bitmap);
638                }
639
640                let row_offset = start_row.checked_add(idx).ok_or_else(|| {
641                    Error::internal(format!(
642                        "bitmap row offset overflow: start_row={start_row}, idx={idx}"
643                    ))
644                })?;
645                let cache_key = BitmapKey::try_new(row_offset)?;
646                self.index_cache
647                    .insert_with_key(&cache_key, Arc::new(bitmap))
648                    .await;
649            }
650        }
651
652        Ok(())
653    }
654
655    fn index_type(&self) -> IndexType {
656        IndexType::Bitmap
657    }
658
659    fn statistics(&self) -> Result<serde_json::Value> {
660        let stats = BitmapStatistics {
661            num_bitmaps: self.index_map.len() + if !self.null_map.is_empty() { 1 } else { 0 },
662        };
663        serde_json::to_value(stats).map_err(|e| {
664            Error::internal(format!(
665                "failed to serialize bitmap index statistics: {}",
666                e
667            ))
668        })
669    }
670
671    async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
672        unimplemented!()
673    }
674}
675
676#[async_trait]
677impl ScalarIndex for BitmapIndex {
678    #[instrument(name = "bitmap_search", level = "debug", skip_all)]
679    async fn search(
680        &self,
681        query: &dyn AnyQuery,
682        metrics: &dyn MetricsCollector,
683    ) -> Result<SearchResult> {
684        let query = query.as_any().downcast_ref::<SargableQuery>().unwrap();
685
686        let (row_ids, null_row_ids) = match query {
687            SargableQuery::Equals(val) => {
688                metrics.record_comparisons(1);
689                if val.is_null() {
690                    // Querying FOR nulls - they are the TRUE result, not NULL result
691                    ((*self.null_map).clone(), None)
692                } else {
693                    let key = OrderableScalarValue(val.clone());
694                    let bitmap = self.load_bitmap(&key, Some(metrics)).await?;
695                    let null_rows = if !self.null_map.is_empty() {
696                        Some((*self.null_map).clone())
697                    } else {
698                        None
699                    };
700                    ((*bitmap).clone(), null_rows)
701                }
702            }
703            SargableQuery::Range(start, end) => {
704                let range_start = match start {
705                    Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
706                    Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
707                    Bound::Unbounded => Bound::Unbounded,
708                };
709
710                let range_end = match end {
711                    Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
712                    Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
713                    Bound::Unbounded => Bound::Unbounded,
714                };
715
716                // Empty range if lower > upper, or if any bound is excluded and lower >= upper.
717                let empty_range = match (&range_start, &range_end) {
718                    (Bound::Included(lower), Bound::Included(upper)) => lower > upper,
719                    (Bound::Included(lower), Bound::Excluded(upper))
720                    | (Bound::Excluded(lower), Bound::Included(upper))
721                    | (Bound::Excluded(lower), Bound::Excluded(upper)) => lower >= upper,
722                    _ => false,
723                };
724
725                let keys: Vec<_> = if empty_range {
726                    Vec::new()
727                } else {
728                    self.index_map
729                        .range((range_start, range_end))
730                        .map(|(k, _v)| k.clone())
731                        .collect()
732                };
733
734                metrics.record_comparisons(keys.len());
735
736                let result = if keys.is_empty() {
737                    RowAddrTreeMap::default()
738                } else {
739                    let bitmaps: Vec<_> = stream::iter(
740                        keys.into_iter()
741                            .map(|key| async move { self.load_bitmap(&key, Some(metrics)).await }),
742                    )
743                    .buffer_unordered(get_num_compute_intensive_cpus())
744                    .try_collect()
745                    .await?;
746
747                    let bitmap_refs: Vec<_> = bitmaps.iter().map(|b| b.as_ref()).collect();
748                    RowAddrTreeMap::union_all(&bitmap_refs)
749                };
750
751                let null_rows = if !self.null_map.is_empty() {
752                    Some((*self.null_map).clone())
753                } else {
754                    None
755                };
756                (result, null_rows)
757            }
758            SargableQuery::IsIn(values) => {
759                metrics.record_comparisons(values.len());
760
761                // Collect keys that exist in the index, tracking if we need nulls
762                let mut has_null = false;
763                let keys: Vec<_> = values
764                    .iter()
765                    .filter_map(|val| {
766                        if val.is_null() {
767                            has_null = true;
768                            None
769                        } else {
770                            let key = OrderableScalarValue(val.clone());
771                            if self.index_map.contains_key(&key) {
772                                Some(key)
773                            } else {
774                                None
775                            }
776                        }
777                    })
778                    .collect();
779
780                // Load bitmaps in parallel
781                let mut bitmaps: Vec<_> = stream::iter(
782                    keys.into_iter()
783                        .map(|key| async move { self.load_bitmap(&key, Some(metrics)).await }),
784                )
785                .buffer_unordered(get_num_compute_intensive_cpus())
786                .try_collect()
787                .await?;
788
789                // Add null bitmap if needed
790                if has_null && !self.null_map.is_empty() {
791                    bitmaps.push(self.null_map.clone());
792                }
793
794                let result = if bitmaps.is_empty() {
795                    RowAddrTreeMap::default()
796                } else {
797                    // Convert Arc<RowAddrTreeMap> to &RowAddrTreeMap for union_all
798                    let bitmap_refs: Vec<_> = bitmaps.iter().map(|b| b.as_ref()).collect();
799                    RowAddrTreeMap::union_all(&bitmap_refs)
800                };
801
802                // If the query explicitly includes null, then nulls are TRUE (not NULL)
803                // Otherwise, nulls remain NULL (unknown)
804                let null_rows = if !has_null && !self.null_map.is_empty() {
805                    Some((*self.null_map).clone())
806                } else {
807                    None
808                };
809                (result, null_rows)
810            }
811            SargableQuery::IsNull() => {
812                metrics.record_comparisons(1);
813                // Querying FOR nulls - they are the TRUE result, not NULL result
814                ((*self.null_map).clone(), None)
815            }
816            SargableQuery::FullTextSearch(_) => {
817                return Err(Error::not_supported_source(
818                    "full text search is not supported for bitmap indexes".into(),
819                ));
820            }
821            SargableQuery::LikePrefix(_) => {
822                return Err(Error::not_supported_source(
823                    "LIKE prefix queries are not supported for bitmap indexes".into(),
824                ));
825            }
826        };
827
828        let selection = NullableRowAddrSet::new(row_ids, null_row_ids.unwrap_or_default());
829        Ok(SearchResult::Exact(selection))
830    }
831
832    fn can_remap(&self) -> bool {
833        true
834    }
835
836    /// Remap the row ids, creating a new remapped version of this index in `dest_store`
837    async fn remap(
838        &self,
839        mapping: &RowAddrRemap,
840        dest_store: &dyn IndexStore,
841    ) -> Result<CreatedIndex> {
842        let state = self.load_bitmap_index_state().await?;
843        let remapped_state = BitmapIndexPlugin::remap_bitmap_state(state, mapping);
844        let file =
845            BitmapIndexPlugin::write_bitmap_index(remapped_state, dest_store, &self.value_type)
846                .await?;
847
848        Ok(CreatedIndex {
849            index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
850                .unwrap(),
851            index_version: BITMAP_INDEX_VERSION,
852            files: vec![file],
853        })
854    }
855
856    /// Add the new data into the index, creating an updated version of the index in `dest_store`
857    async fn update(
858        &self,
859        new_data: SendableRecordBatchStream,
860        dest_store: &dyn IndexStore,
861        old_data_filter: Option<super::OldIndexDataFilter>,
862    ) -> Result<CreatedIndex> {
863        let file = BitmapIndexPlugin::streaming_build_and_write(
864            new_data,
865            Some(self),
866            dest_store,
867            BITMAP_LOOKUP_NAME,
868            old_data_filter.as_ref(),
869        )
870        .await?;
871
872        Ok(CreatedIndex {
873            index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
874                .unwrap(),
875            index_version: BITMAP_INDEX_VERSION,
876            files: vec![file],
877        })
878    }
879
880    fn update_criteria(&self) -> UpdateCriteria {
881        UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::Values).with_row_id())
882    }
883
884    fn derive_index_params(&self) -> Result<ScalarIndexParams> {
885        Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::Bitmap))
886    }
887}
888
889/// Buffers serialized (key, bitmap) pairs and flushes them as record batches
890/// to the index file, respecting the MAX_BITMAP_ARRAY_LENGTH limit.
891struct BitmapBatchWriter {
892    file: Box<dyn super::IndexWriter>,
893    keys: Vec<ScalarValue>,
894    serialized: Vec<Vec<u8>>,
895    bytes: usize,
896    num_bitmaps: usize,
897}
898
899impl BitmapBatchWriter {
900    fn new(file: Box<dyn super::IndexWriter>) -> Self {
901        Self {
902            file,
903            keys: Vec::new(),
904            serialized: Vec::new(),
905            bytes: 0,
906            num_bitmaps: 0,
907        }
908    }
909
910    /// Serialize and buffer a single (key, bitmap) pair, flushing the current
911    /// batch to disk if adding it would exceed MAX_BITMAP_ARRAY_LENGTH.
912    async fn emit(&mut self, key: ScalarValue, bitmap: &RowAddrTreeMap) -> Result<()> {
913        let mut buf = Vec::new();
914        bitmap.serialize_into(&mut buf).unwrap();
915        let size = buf.len();
916
917        if self.bytes + size > MAX_BITMAP_ARRAY_LENGTH {
918            self.flush().await?;
919        }
920
921        self.keys.push(key);
922        self.serialized.push(buf);
923        self.bytes += size;
924        self.num_bitmaps += 1;
925        Ok(())
926    }
927
928    /// Write the current batch to disk.
929    async fn flush(&mut self) -> Result<()> {
930        if self.keys.is_empty() {
931            return Ok(());
932        }
933        let keys_array =
934            ScalarValue::iter_to_array(self.keys.drain(..).collect::<Vec<_>>()).unwrap();
935        let total_size: usize = self.serialized.iter().map(|b| b.len()).sum();
936        let mut binary_builder = BinaryBuilder::with_capacity(self.serialized.len(), total_size);
937        for b in self.serialized.drain(..) {
938            binary_builder.append_value(&b);
939        }
940        let bitmaps_array = Arc::new(binary_builder.finish()) as Arc<dyn Array>;
941        let batch = BitmapIndexPlugin::get_batch_from_arrays(keys_array, bitmaps_array)?;
942        self.file.write_record_batch(batch).await?;
943        self.bytes = 0;
944        Ok(())
945    }
946
947    /// Flush any remaining data, write index statistics, and finalize the file.
948    async fn finish(mut self) -> Result<IndexFile> {
949        self.flush().await?;
950        let stats_json = serde_json::to_string(&BitmapStatistics {
951            num_bitmaps: self.num_bitmaps,
952        })
953        .map_err(|e| Error::internal(format!("failed to serialize bitmap statistics: {e}")))?;
954        let mut metadata = HashMap::new();
955        metadata.insert(INDEX_STATS_METADATA_KEY.to_string(), stats_json);
956        self.file.finish_with_metadata(metadata).await
957    }
958}
959
960fn bitmap_shard_file_name(partition_id: u64) -> String {
961    format!("{BITMAP_PART_LOOKUP_PREFIX}{partition_id}{BITMAP_PART_LOOKUP_SUFFIX}")
962}
963
964fn tagged_bitmap_partition_id(id: u32, tag: u64) -> u64 {
965    ((id as u64) << 32) | tag
966}
967
968fn bitmap_shard_partition_id(fragment_ids: &[u32], shard_id: Option<u32>) -> Result<u64> {
969    if fragment_ids.is_empty() {
970        return Err(Error::invalid_input(
971            "Bitmap shard build requires at least one fragment id".to_string(),
972        ));
973    }
974
975    if let Some(shard_id) = shard_id {
976        return Ok(tagged_bitmap_partition_id(shard_id, EXPLICIT_SHARD_ID_TAG));
977    }
978
979    let [fragment_id] = fragment_ids else {
980        return Err(Error::invalid_input(format!(
981            "Bitmap distributed build over multiple fragments requires an explicit shard_id. \
982             Received {} fragment ids: {:?}. Please assign mutually exclusive shard_id values \
983             to disjoint fragment groups.",
984            fragment_ids.len(),
985            fragment_ids
986        )));
987    };
988
989    Ok(tagged_bitmap_partition_id(
990        *fragment_id,
991        IMPLICIT_FRAGMENT_ID_TAG,
992    ))
993}
994
995fn extract_bitmap_shard_id(filename: &str) -> Result<u64> {
996    let partition_id = filename
997        .strip_prefix(BITMAP_PART_LOOKUP_PREFIX)
998        .and_then(|name| name.strip_suffix(BITMAP_PART_LOOKUP_SUFFIX))
999        .ok_or_else(|| {
1000            Error::internal(format!("Invalid bitmap shard file name format: {filename}"))
1001        })?;
1002    partition_id.parse::<u64>().map_err(|_| {
1003        Error::internal(format!(
1004            "Failed to parse bitmap partition id from file name: {filename}"
1005        ))
1006    })
1007}
1008
1009fn deserialize_bitmap(bitmap_bytes: &[u8], file_name: &str) -> Result<RowAddrTreeMap> {
1010    RowAddrTreeMap::deserialize_from(bitmap_bytes).map_err(|error| {
1011        Error::corrupt_file(
1012            Path::from(file_name),
1013            format!("Failed to deserialize bitmap bytes: {error}"),
1014        )
1015    })
1016}
1017
1018async fn new_bitmap_batch_writer(
1019    index_store: &dyn IndexStore,
1020    file_name: &str,
1021    value_type: &DataType,
1022) -> Result<BitmapBatchWriter> {
1023    let schema = Arc::new(Schema::new(vec![
1024        Field::new("keys", value_type.clone(), true),
1025        Field::new("bitmaps", DataType::Binary, true),
1026    ]));
1027    let index_file = index_store.new_index_file(file_name, schema).await?;
1028    Ok(BitmapBatchWriter::new(index_file))
1029}
1030
1031#[derive(Clone, Debug, Eq, PartialEq)]
1032struct BitmapHeapItem {
1033    key: OrderableScalarValue,
1034    shard_idx: usize,
1035}
1036
1037impl Ord for BitmapHeapItem {
1038    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1039        self.key
1040            .cmp(&other.key)
1041            .then_with(|| self.shard_idx.cmp(&other.shard_idx))
1042    }
1043}
1044
1045impl PartialOrd for BitmapHeapItem {
1046    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1047        Some(self.cmp(other))
1048    }
1049}
1050
1051struct BitmapShardCursor {
1052    file_name: String,
1053    reader: Arc<dyn IndexReader>,
1054    total_rows: usize,
1055    next_row_offset: usize,
1056    batch: Option<RecordBatch>,
1057    batch_row_idx: usize,
1058}
1059
1060impl BitmapShardCursor {
1061    async fn try_new(file_name: String, reader: Arc<dyn IndexReader>) -> Result<Option<Self>> {
1062        let total_rows = reader.num_rows();
1063        if total_rows == 0 {
1064            return Ok(None);
1065        }
1066
1067        let mut cursor = Self {
1068            file_name,
1069            reader,
1070            total_rows,
1071            next_row_offset: 0,
1072            batch: None,
1073            batch_row_idx: 0,
1074        };
1075        if cursor.advance().await? {
1076            Ok(Some(cursor))
1077        } else {
1078            Ok(None)
1079        }
1080    }
1081
1082    fn peek_key(&self) -> Result<OrderableScalarValue> {
1083        let batch = self.batch.as_ref().ok_or_else(|| {
1084            Error::internal(format!(
1085                "Bitmap shard {} has no active batch",
1086                self.file_name
1087            ))
1088        })?;
1089        let key = ScalarValue::try_from_array(batch.column(0), self.batch_row_idx)?;
1090        Ok(OrderableScalarValue(key))
1091    }
1092
1093    fn take_current(&mut self) -> Result<(ScalarValue, RowAddrTreeMap)> {
1094        let batch = self.batch.as_ref().ok_or_else(|| {
1095            Error::internal(format!(
1096                "Bitmap shard {} has no active batch",
1097                self.file_name
1098            ))
1099        })?;
1100        let keys = batch.column(0);
1101        let binary_bitmaps = batch
1102            .column(1)
1103            .as_any()
1104            .downcast_ref::<BinaryArray>()
1105            .ok_or_else(|| {
1106                Error::corrupt_file(
1107                    Path::from(self.file_name.as_str()),
1108                    "Bitmap shard batch has non-binary bitmap column".to_string(),
1109                )
1110            })?;
1111        let key = ScalarValue::try_from_array(keys, self.batch_row_idx)?;
1112        let bitmap = deserialize_bitmap(binary_bitmaps.value(self.batch_row_idx), &self.file_name)?;
1113        self.batch_row_idx += 1;
1114        Ok((key, bitmap))
1115    }
1116
1117    async fn advance(&mut self) -> Result<bool> {
1118        loop {
1119            if let Some(batch) = &self.batch
1120                && self.batch_row_idx < batch.num_rows()
1121            {
1122                return Ok(true);
1123            }
1124
1125            if self.next_row_offset >= self.total_rows {
1126                self.batch = None;
1127                return Ok(false);
1128            }
1129
1130            let end_row = (self.next_row_offset + MERGE_ROWS_PER_CHUNK).min(self.total_rows);
1131            let batch = self
1132                .reader
1133                .read_range(self.next_row_offset..end_row, None)
1134                .await?;
1135            self.next_row_offset = end_row;
1136            self.batch = Some(batch);
1137            self.batch_row_idx = 0;
1138        }
1139    }
1140}
1141
1142async fn advance_cursor_and_push(
1143    cursors: &mut [BitmapShardCursor],
1144    heap: &mut BinaryHeap<Reverse<BitmapHeapItem>>,
1145    shard_idx: usize,
1146) -> Result<()> {
1147    if cursors[shard_idx].advance().await? {
1148        heap.push(Reverse(BitmapHeapItem {
1149            key: cursors[shard_idx].peek_key()?,
1150            shard_idx,
1151        }));
1152    }
1153    Ok(())
1154}
1155
1156async fn drain_same_key_bitmaps(
1157    cursors: &mut [BitmapShardCursor],
1158    heap: &mut BinaryHeap<Reverse<BitmapHeapItem>>,
1159    item: BitmapHeapItem,
1160) -> Result<(ScalarValue, RowAddrTreeMap)> {
1161    let (key, mut merged_bitmap) = cursors[item.shard_idx].take_current()?;
1162    let merged_key = OrderableScalarValue(key);
1163    advance_cursor_and_push(cursors, heap, item.shard_idx).await?;
1164
1165    while let Some(Reverse(next_item)) = heap.peek() {
1166        if next_item.key != merged_key {
1167            break;
1168        }
1169
1170        let shard_idx = next_item.shard_idx;
1171        let _ = heap.pop();
1172        let (_, bitmap) = cursors[shard_idx].take_current()?;
1173        merged_bitmap |= &bitmap;
1174        advance_cursor_and_push(cursors, heap, shard_idx).await?;
1175    }
1176
1177    Ok((merged_key.0, merged_bitmap))
1178}
1179
1180async fn list_bitmap_shard_files(
1181    object_store: &ObjectStore,
1182    index_dir: &Path,
1183    progress: &dyn IndexBuildProgress,
1184) -> Result<Vec<String>> {
1185    let mut shard_files = Vec::new();
1186    let mut list_stream = object_store.list(Some(index_dir.clone()));
1187    while let Some(item) = list_stream.next().await {
1188        match item {
1189            Ok(meta) => {
1190                let file_name = meta.location.filename().unwrap_or_default();
1191                if file_name.starts_with(BITMAP_PART_LOOKUP_PREFIX)
1192                    && file_name.ends_with(BITMAP_PART_LOOKUP_SUFFIX)
1193                {
1194                    shard_files.push(file_name.to_string());
1195                    progress
1196                        .stage_progress("scan_bitmap_shards", shard_files.len() as u64)
1197                        .await?;
1198                }
1199            }
1200            Err(err) => {
1201                return Err(Error::io(format!(
1202                    "Failed to list bitmap shard files in {}: {err}",
1203                    index_dir
1204                )));
1205            }
1206        }
1207    }
1208    let mut shard_files = shard_files
1209        .into_iter()
1210        .map(|file_name| extract_bitmap_shard_id(&file_name).map(|shard_id| (shard_id, file_name)))
1211        .collect::<Result<Vec<_>>>()?;
1212    shard_files.sort_unstable_by_key(|(shard_id, _)| *shard_id);
1213    let shard_files = shard_files
1214        .into_iter()
1215        .map(|(_, file_name)| file_name)
1216        .collect::<Vec<_>>();
1217    if shard_files.is_empty() {
1218        return Err(Error::invalid_input(format!(
1219            "No bitmap shard files found in index directory: {}; \
1220             call build_index for each fragment before calling merge_index_metadata",
1221            index_dir
1222        )));
1223    }
1224    Ok(shard_files)
1225}
1226
1227async fn cleanup_bitmap_shard_files(store: &dyn IndexStore, shard_files: &[String]) {
1228    for file_name in shard_files {
1229        if let Err(error) = store.delete_index_file(file_name).await {
1230            warn!(
1231                "Failed to delete bitmap shard file '{}': {}. \
1232                 This does not affect the merged bitmap index, but the shard file \
1233                 may need manual cleanup.",
1234                file_name, error
1235            );
1236        }
1237    }
1238}
1239
1240#[derive(Debug, Default)]
1241pub struct BitmapIndexPlugin;
1242
1243/// Drop the rows an old posting should no longer expose -- rows whose fragment
1244/// was removed, or (under stable row ids) rows rewritten by an update -- keeping
1245/// only those `filter` still considers valid. A no-op when `filter` is `None`.
1246fn retain_valid(
1247    mut bitmap: RowAddrTreeMap,
1248    filter: Option<&super::OldIndexDataFilter>,
1249) -> RowAddrTreeMap {
1250    if let Some(filter) = filter {
1251        filter.retain_old_rows(&mut bitmap);
1252    }
1253    bitmap
1254}
1255
1256impl BitmapIndexPlugin {
1257    fn get_batch_from_arrays(
1258        keys: Arc<dyn Array>,
1259        binary_bitmaps: Arc<dyn Array>,
1260    ) -> Result<RecordBatch> {
1261        let schema = Arc::new(Schema::new(vec![
1262            Field::new("keys", keys.data_type().clone(), true),
1263            Field::new("bitmaps", binary_bitmaps.data_type().clone(), true),
1264        ]));
1265
1266        let columns = vec![keys, binary_bitmaps];
1267
1268        Ok(RecordBatch::try_new(schema, columns)?)
1269    }
1270
1271    async fn write_bitmap_index(
1272        state: HashMap<ScalarValue, RowAddrTreeMap>,
1273        index_store: &dyn IndexStore,
1274        value_type: &DataType,
1275    ) -> Result<IndexFile> {
1276        Self::write_bitmap_index_with_extras(
1277            state,
1278            index_store,
1279            value_type,
1280            HashMap::new(),
1281            Vec::new(),
1282        )
1283        .await
1284    }
1285
1286    /// Writes a bitmap index and attaches extra metadata and global buffers.
1287    pub(crate) async fn write_bitmap_index_with_extras(
1288        state: HashMap<ScalarValue, RowAddrTreeMap>,
1289        index_store: &dyn IndexStore,
1290        value_type: &DataType,
1291        mut metadata: HashMap<String, String>,
1292        global_buffers: Vec<(String, Bytes)>,
1293    ) -> Result<IndexFile> {
1294        let num_bitmaps = state.len();
1295        let schema = Arc::new(Schema::new(vec![
1296            Field::new("keys", value_type.clone(), true),
1297            Field::new("bitmaps", DataType::Binary, true),
1298        ]));
1299
1300        let mut bitmap_index_file = index_store
1301            .new_index_file(BITMAP_LOOKUP_NAME, schema)
1302            .await?;
1303
1304        for (metadata_key, data) in global_buffers {
1305            let buffer_idx = bitmap_index_file.add_global_buffer(data).await?;
1306            metadata.insert(metadata_key, buffer_idx.to_string());
1307        }
1308
1309        let mut cur_keys = Vec::new();
1310        let mut cur_bitmaps = Vec::new();
1311        let mut cur_bytes = 0;
1312
1313        for (key, bitmap) in state.into_iter() {
1314            let mut bytes = Vec::new();
1315            bitmap.serialize_into(&mut bytes).unwrap();
1316            let bitmap_size = bytes.len();
1317
1318            if cur_bytes + bitmap_size > MAX_BITMAP_ARRAY_LENGTH {
1319                let keys_array = ScalarValue::iter_to_array(cur_keys.clone()).unwrap();
1320                let mut binary_builder = BinaryBuilder::new();
1321                for b in &cur_bitmaps {
1322                    binary_builder.append_value(b);
1323                }
1324                let bitmaps_array = Arc::new(binary_builder.finish()) as Arc<dyn Array>;
1325
1326                let record_batch = Self::get_batch_from_arrays(keys_array, bitmaps_array)?;
1327                bitmap_index_file.write_record_batch(record_batch).await?;
1328
1329                cur_keys.clear();
1330                cur_bitmaps.clear();
1331                cur_bytes = 0;
1332            }
1333
1334            cur_keys.push(key);
1335            cur_bitmaps.push(bytes);
1336            cur_bytes += bitmap_size;
1337        }
1338
1339        // Flush any remaining
1340        if !cur_keys.is_empty() {
1341            let keys_array = ScalarValue::iter_to_array(cur_keys).unwrap();
1342            let mut binary_builder = BinaryBuilder::new();
1343            for b in &cur_bitmaps {
1344                binary_builder.append_value(b);
1345            }
1346            let bitmaps_array = Arc::new(binary_builder.finish()) as Arc<dyn Array>;
1347
1348            let record_batch = Self::get_batch_from_arrays(keys_array, bitmaps_array)?;
1349            bitmap_index_file.write_record_batch(record_batch).await?;
1350        }
1351
1352        // Finish file with metadata that allows lightweight statistics reads
1353        let stats_json = serde_json::to_string(&BitmapStatistics { num_bitmaps })
1354            .map_err(|e| Error::internal(format!("failed to serialize bitmap statistics: {e}")))?;
1355        metadata.insert(INDEX_STATS_METADATA_KEY.to_string(), stats_json);
1356
1357        bitmap_index_file.finish_with_metadata(metadata).await
1358    }
1359
1360    /// Builds bitmap index state from a `(value, row_id)` stream without writing it.
1361    pub(crate) async fn build_bitmap_index_state(
1362        mut data_source: SendableRecordBatchStream,
1363        mut state: HashMap<ScalarValue, RowAddrTreeMap>,
1364    ) -> Result<(HashMap<ScalarValue, RowAddrTreeMap>, DataType)> {
1365        let value_type = data_source.schema().field(0).data_type().clone();
1366        while let Some(batch) = data_source.try_next().await? {
1367            let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
1368            let row_ids = batch.column_by_name(ROW_ID).expect_ok()?;
1369            debug_assert_eq!(row_ids.data_type(), &DataType::UInt64);
1370
1371            let row_id_column = row_ids.as_any().downcast_ref::<UInt64Array>().unwrap();
1372
1373            for i in 0..values.len() {
1374                let row_id = row_id_column.value(i);
1375                let key = ScalarValue::try_from_array(values.as_ref(), i)?;
1376                state.entry(key.clone()).or_default().insert(row_id);
1377            }
1378        }
1379
1380        Ok((state, value_type))
1381    }
1382
1383    pub async fn train_bitmap_index(
1384        data: SendableRecordBatchStream,
1385        index_store: &dyn IndexStore,
1386    ) -> Result<IndexFile> {
1387        Self::streaming_build_and_write(data, None, index_store, BITMAP_LOOKUP_NAME, None).await
1388    }
1389
1390    async fn train_bitmap_shard(
1391        data: SendableRecordBatchStream,
1392        index_store: &dyn IndexStore,
1393        fragment_ids: &[u32],
1394        shard_id: Option<u32>,
1395        progress: Arc<dyn crate::progress::IndexBuildProgress>,
1396    ) -> Result<IndexFile> {
1397        let partition_id = bitmap_shard_partition_id(fragment_ids, shard_id)?;
1398        let file_name = bitmap_shard_file_name(partition_id);
1399        progress
1400            .stage_start("build_bitmap_shard", None, "rows")
1401            .await?;
1402        let file =
1403            Self::streaming_build_and_write(data, None, index_store, &file_name, None).await?;
1404        progress.stage_complete("build_bitmap_shard").await?;
1405        Ok(file)
1406    }
1407
1408    /// Builds and writes a bitmap index in a streaming fashion from value-sorted
1409    /// input. Only one value's bitmap is in memory at a time, reducing peak memory
1410    /// from O(unique_values * avg_bitmap) to O(largest_single_bitmap).
1411    ///
1412    /// If `old_index` is provided, its existing bitmaps are merged with the new
1413    /// data via a sorted merge-join (the old index_map is a BTreeMap, already
1414    /// sorted by value).
1415    async fn streaming_build_and_write(
1416        mut data_source: SendableRecordBatchStream,
1417        old_index: Option<&BitmapIndex>,
1418        index_store: &dyn IndexStore,
1419        output_file_name: &str,
1420        old_data_filter: Option<&super::OldIndexDataFilter>,
1421    ) -> Result<IndexFile> {
1422        let value_type = data_source.schema().field(0).data_type().clone();
1423
1424        let mut writer =
1425            new_bitmap_batch_writer(index_store, output_file_name, &value_type).await?;
1426
1427        // Collect old index keys (already in memory as BTreeMap keys — this is
1428        // just a Vec of references, not a copy of the bitmaps themselves).
1429        let old_keys: Vec<OrderableScalarValue> = old_index
1430            .map(|idx| idx.index_map.keys().cloned().collect())
1431            .unwrap_or_default();
1432        let mut old_pos: usize = 0;
1433
1434        // Current value being accumulated from the new data stream.
1435        let mut current_key: Option<ScalarValue> = None;
1436        let mut current_bitmap = RowAddrTreeMap::default();
1437        // Track whether we emitted a null bitmap (old index stores nulls
1438        // separately in null_map, not in index_map).
1439        let mut emitted_null = false;
1440
1441        while let Some(batch) = data_source.try_next().await? {
1442            let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
1443            let row_ids = batch.column_by_name(ROW_ID).expect_ok()?;
1444            debug_assert_eq!(row_ids.data_type(), &DataType::UInt64);
1445            let row_id_column = row_ids.as_any().downcast_ref::<UInt64Array>().unwrap();
1446
1447            for i in 0..values.len() {
1448                let row_id = row_id_column.value(i);
1449                let key = ScalarValue::try_from_array(values.as_ref(), i)?;
1450
1451                match &current_key {
1452                    Some(cur) if *cur == key => {
1453                        current_bitmap.insert(row_id);
1454                    }
1455                    _ => {
1456                        // Value changed — flush the previous run.
1457                        if let Some(prev_key) = current_key.take() {
1458                            let mut prev_bitmap = std::mem::take(&mut current_bitmap);
1459                            Self::finish_run(
1460                                prev_key,
1461                                &mut prev_bitmap,
1462                                old_index,
1463                                &old_keys,
1464                                &mut old_pos,
1465                                &mut emitted_null,
1466                                &mut writer,
1467                                old_data_filter,
1468                            )
1469                            .await?;
1470                        }
1471                        current_key = Some(key);
1472                        current_bitmap = RowAddrTreeMap::default();
1473                        current_bitmap.insert(row_id);
1474                    }
1475                }
1476            }
1477        }
1478
1479        // Flush the last accumulated run from new data.
1480        if let Some(last_key) = current_key.take() {
1481            let mut last_bitmap = std::mem::take(&mut current_bitmap);
1482            Self::finish_run(
1483                last_key,
1484                &mut last_bitmap,
1485                old_index,
1486                &old_keys,
1487                &mut old_pos,
1488                &mut emitted_null,
1489                &mut writer,
1490                old_data_filter,
1491            )
1492            .await?;
1493        }
1494
1495        // Emit any remaining old-only entries.
1496        if let Some(idx) = old_index {
1497            while old_pos < old_keys.len() {
1498                let old_bitmap = retain_valid(
1499                    idx.load_bitmap(&old_keys[old_pos], None)
1500                        .await?
1501                        .as_ref()
1502                        .clone(),
1503                    old_data_filter,
1504                );
1505                writer
1506                    .emit(old_keys[old_pos].0.clone(), &old_bitmap)
1507                    .await?;
1508                old_pos += 1;
1509            }
1510        }
1511
1512        // Emit old null bitmap if we didn't already merge it with new nulls.
1513        if !emitted_null
1514            && let Some(idx) = old_index
1515            && !idx.null_map.is_empty()
1516        {
1517            let null_key = new_null_array(&value_type, 1);
1518            let null_key = ScalarValue::try_from_array(null_key.as_ref(), 0)?;
1519            let null_bitmap = retain_valid((*idx.null_map).clone(), old_data_filter);
1520            writer.emit(null_key, &null_bitmap).await?;
1521        }
1522
1523        writer.finish().await
1524    }
1525
1526    /// Flush a completed value-run from the new data stream, emitting any
1527    /// old-only entries that sort before it and merging the old bitmap if the
1528    /// key exists in both old and new.
1529    #[allow(clippy::too_many_arguments)]
1530    async fn finish_run(
1531        key: ScalarValue,
1532        bitmap: &mut RowAddrTreeMap,
1533        old_index: Option<&BitmapIndex>,
1534        old_keys: &[OrderableScalarValue],
1535        old_pos: &mut usize,
1536        emitted_null: &mut bool,
1537        writer: &mut BitmapBatchWriter,
1538        old_data_filter: Option<&super::OldIndexDataFilter>,
1539    ) -> Result<()> {
1540        if key.is_null() {
1541            // Null values are stored separately in the old index's null_map.
1542            if let Some(idx) = old_index
1543                && !idx.null_map.is_empty()
1544            {
1545                *bitmap |= &retain_valid((*idx.null_map).clone(), old_data_filter);
1546            }
1547            *emitted_null = true;
1548            writer.emit(key, bitmap).await?;
1549        } else if let Some(idx) = old_index {
1550            let orderable = OrderableScalarValue(key.clone());
1551
1552            // Emit old-only entries that sort before this key.
1553            while *old_pos < old_keys.len() && old_keys[*old_pos] < orderable {
1554                let old_bitmap = retain_valid(
1555                    idx.load_bitmap(&old_keys[*old_pos], None)
1556                        .await?
1557                        .as_ref()
1558                        .clone(),
1559                    old_data_filter,
1560                );
1561                writer
1562                    .emit(old_keys[*old_pos].0.clone(), &old_bitmap)
1563                    .await?;
1564                *old_pos += 1;
1565            }
1566
1567            // If the old index also has this key, merge its bitmap.
1568            if *old_pos < old_keys.len() && old_keys[*old_pos] == orderable {
1569                *bitmap |= &retain_valid(
1570                    idx.load_bitmap(&old_keys[*old_pos], None)
1571                        .await?
1572                        .as_ref()
1573                        .clone(),
1574                    old_data_filter,
1575                );
1576                *old_pos += 1;
1577            }
1578
1579            writer.emit(key, bitmap).await?;
1580        } else {
1581            writer.emit(key, bitmap).await?;
1582        }
1583        Ok(())
1584    }
1585
1586    /// Remaps every bitmap in a materialized bitmap-index state using row-id mappings.
1587    pub(crate) fn remap_bitmap_state(
1588        state: HashMap<ScalarValue, RowAddrTreeMap>,
1589        mapping: &RowAddrRemap,
1590    ) -> HashMap<ScalarValue, RowAddrTreeMap> {
1591        state
1592            .into_iter()
1593            .map(|(key, bitmap)| {
1594                let remapped_bitmap =
1595                    RowAddrTreeMap::from_iter(bitmap.row_addrs().unwrap().filter_map(|addr| {
1596                        let addr_as_u64 = u64::from(addr);
1597                        mapping.get(addr_as_u64).unwrap_or(Some(addr_as_u64))
1598                    }));
1599                (key, remapped_bitmap)
1600            })
1601            .collect()
1602    }
1603
1604    /// Merge per-shard bitmap lookup files into a single bitmap index file.
1605    ///
1606    /// Each shard file is already sorted by key and can contain many distinct keys.
1607    /// This method does not materialize an entire shard in memory. Instead, it keeps
1608    /// one cursor per shard, where each cursor tracks the shard's current row within
1609    /// a small in-memory batch. A min-heap stores the current key for each shard.
1610    ///
1611    /// The merge then proceeds as a streaming K-way merge:
1612    /// - pop the smallest current key across all shards
1613    /// - union the bitmap for that key with any other shards currently positioned on
1614    ///   the same key
1615    /// - advance only those shards that participated in the union and push their next
1616    ///   keys back into the heap
1617    ///
1618    /// This keeps memory usage proportional to the number of shards plus the bitmaps
1619    /// currently being merged, instead of the total number of keys across all shards.
1620    async fn merge_shards(
1621        store: &dyn IndexStore,
1622        shard_files: &[String],
1623        progress: Arc<dyn IndexBuildProgress>,
1624    ) -> Result<IndexFile> {
1625        progress
1626            .stage_start("merge_bitmap_shards", None, "bitmaps")
1627            .await?;
1628
1629        let mut cursors = Vec::with_capacity(shard_files.len());
1630        let mut heap = BinaryHeap::with_capacity(shard_files.len());
1631        let mut value_type: Option<DataType> = None;
1632
1633        for file_name in shard_files {
1634            let reader = store.open_index_file(file_name).await?;
1635            let shard_value_type = reader.schema().fields[0].data_type().clone();
1636            if let Some(existing_type) = &value_type {
1637                if existing_type != &shard_value_type {
1638                    return Err(Error::invalid_input(format!(
1639                        "Bitmap shard {} has value type {:?}, expected {:?}",
1640                        file_name, shard_value_type, existing_type
1641                    )));
1642                }
1643            } else {
1644                value_type = Some(shard_value_type);
1645            }
1646            if let Some(cursor) = BitmapShardCursor::try_new(file_name.clone(), reader).await? {
1647                let key = cursor.peek_key()?;
1648                let shard_idx = cursors.len();
1649                cursors.push(cursor);
1650                heap.push(Reverse(BitmapHeapItem { key, shard_idx }));
1651            }
1652        }
1653
1654        let value_type = value_type.ok_or_else(|| {
1655            Error::invalid_input("Bitmap shard merge requires at least one shard file".to_string())
1656        })?;
1657        let mut writer = new_bitmap_batch_writer(store, BITMAP_LOOKUP_NAME, &value_type).await?;
1658        let mut merged_keys = 0u64;
1659
1660        while let Some(Reverse(item)) = heap.pop() {
1661            let (key, merged_bitmap) =
1662                drain_same_key_bitmaps(&mut cursors, &mut heap, item).await?;
1663            writer.emit(key, &merged_bitmap).await?;
1664            merged_keys += 1;
1665            progress
1666                .stage_progress("merge_bitmap_shards", merged_keys)
1667                .await?;
1668        }
1669
1670        progress.stage_complete("merge_bitmap_shards").await?;
1671        progress
1672            .stage_start("write_bitmap_index", Some(1), "files")
1673            .await?;
1674        let file = writer.finish().await?;
1675        progress.stage_progress("write_bitmap_index", 1).await?;
1676        progress.stage_complete("write_bitmap_index").await?;
1677        Ok(file)
1678    }
1679}
1680
1681pub async fn merge_index_files(
1682    object_store: &ObjectStore,
1683    index_dir: &Path,
1684    store: Arc<dyn IndexStore>,
1685    progress: Arc<dyn IndexBuildProgress>,
1686) -> Result<()> {
1687    progress
1688        .stage_start("scan_bitmap_shards", None, "files")
1689        .await?;
1690    let shard_files = list_bitmap_shard_files(object_store, index_dir, progress.as_ref()).await?;
1691    progress.stage_complete("scan_bitmap_shards").await?;
1692
1693    BitmapIndexPlugin::merge_shards(store.as_ref(), &shard_files, progress).await?;
1694    cleanup_bitmap_shard_files(store.as_ref(), &shard_files).await;
1695    Ok(())
1696}
1697
1698pub async fn merge_bitmap_indices(
1699    source_indices: &[Arc<BitmapIndex>],
1700    dest_store: &dyn IndexStore,
1701    progress: Arc<dyn IndexBuildProgress>,
1702) -> Result<CreatedIndex> {
1703    if source_indices.is_empty() {
1704        return Err(Error::invalid_input(
1705            "Bitmap segment merge requires at least one source segment".to_string(),
1706        ));
1707    }
1708
1709    let value_type = source_indices[0].value_type().clone();
1710    let mut merged_state = HashMap::<ScalarValue, RowAddrTreeMap>::new();
1711
1712    progress
1713        .stage_start(
1714            "merge_bitmap_segments",
1715            Some(source_indices.len() as u64),
1716            "segments",
1717        )
1718        .await?;
1719    for (idx, source_index) in source_indices.iter().enumerate() {
1720        if source_index.value_type() != &value_type {
1721            return Err(Error::invalid_input(format!(
1722                "Bitmap segment has value type {:?}, expected {:?}",
1723                source_index.value_type(),
1724                value_type
1725            )));
1726        }
1727
1728        let state = source_index.load_bitmap_index_state().await?;
1729        for (key, bitmap) in state {
1730            merged_state
1731                .entry(key)
1732                .and_modify(|existing| *existing |= &bitmap)
1733                .or_insert(bitmap);
1734        }
1735        progress
1736            .stage_progress("merge_bitmap_segments", (idx + 1) as u64)
1737            .await?;
1738    }
1739    progress.stage_complete("merge_bitmap_segments").await?;
1740
1741    progress
1742        .stage_start("write_bitmap_index", Some(1), "files")
1743        .await?;
1744    let file = BitmapIndexPlugin::write_bitmap_index(merged_state, dest_store, &value_type).await?;
1745    progress.stage_progress("write_bitmap_index", 1).await?;
1746    progress.stage_complete("write_bitmap_index").await?;
1747
1748    Ok(CreatedIndex {
1749        index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default()).unwrap(),
1750        index_version: BITMAP_INDEX_VERSION,
1751        files: vec![file],
1752    })
1753}
1754
1755#[async_trait]
1756impl BasicTrainer for BitmapIndexPlugin {
1757    fn new_training_request(
1758        &self,
1759        params: &str,
1760        field: &Field,
1761    ) -> Result<Box<dyn TrainingRequest>> {
1762        if field.data_type().is_nested() {
1763            return Err(Error::invalid_input_source(
1764                "A bitmap index can only be created on a non-nested field.".into(),
1765            ));
1766        }
1767        let params = if params.is_empty() {
1768            BitmapParameters::default()
1769        } else {
1770            serde_json::from_str::<BitmapParameters>(params)?
1771        };
1772        Ok(Box::new(BitmapTrainingRequest::new(params)))
1773    }
1774
1775    async fn train_index(
1776        &self,
1777        data: SendableRecordBatchStream,
1778        index_store: &dyn IndexStore,
1779        request: Box<dyn TrainingRequest>,
1780        fragment_ids: Option<Vec<u32>>,
1781        progress: Arc<dyn crate::progress::IndexBuildProgress>,
1782    ) -> Result<CreatedIndex> {
1783        let request = request
1784            .as_any()
1785            .downcast_ref::<BitmapTrainingRequest>()
1786            .ok_or_else(|| {
1787                Error::internal(
1788                    "BitmapIndexPlugin::train_index received a non-bitmap training request"
1789                        .to_string(),
1790                )
1791            })?;
1792        let file = if let Some(fragment_ids) = fragment_ids.as_ref() {
1793            Self::train_bitmap_shard(
1794                data,
1795                index_store,
1796                fragment_ids,
1797                request.parameters.shard_id,
1798                progress,
1799            )
1800            .await?
1801        } else if request.parameters.shard_id.is_some() {
1802            return Err(Error::invalid_input(
1803                "Bitmap shard_id requires fragment_ids and is only supported for distributed shard builds"
1804                    .to_string(),
1805            ));
1806        } else {
1807            Self::train_bitmap_index(data, index_store).await?
1808        };
1809        Ok(CreatedIndex {
1810            index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
1811                .unwrap(),
1812            index_version: BITMAP_INDEX_VERSION,
1813            files: vec![file],
1814        })
1815    }
1816}
1817
1818#[async_trait]
1819impl ScalarIndexPlugin for BitmapIndexPlugin {
1820    fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
1821        Some(self)
1822    }
1823
1824    fn name(&self) -> &str {
1825        "Bitmap"
1826    }
1827
1828    fn provides_exact_answer(&self) -> bool {
1829        true
1830    }
1831
1832    fn version(&self) -> u32 {
1833        BITMAP_INDEX_VERSION
1834    }
1835
1836    fn new_query_parser(
1837        &self,
1838        index_name: String,
1839        _index_details: &prost_types::Any,
1840    ) -> Option<Box<dyn ScalarQueryParser>> {
1841        // Bitmap indexes cannot answer `LikePrefix` queries (see `search`), so the parser
1842        // is configured to skip them and let such predicates fall back to ordinary filtering.
1843        Some(Box::new(
1844            SargableQueryParser::new(index_name, self.name().to_string(), false)
1845                .without_like_prefix(),
1846        ))
1847    }
1848
1849    /// Load an index from storage
1850    async fn load_index(
1851        &self,
1852        index_store: Arc<dyn IndexStore>,
1853        _index_details: &prost_types::Any,
1854        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1855        cache: &LanceCache,
1856    ) -> Result<Arc<dyn ScalarIndex>> {
1857        Ok(BitmapIndex::load(index_store, frag_reuse_index, cache).await? as Arc<dyn ScalarIndex>)
1858    }
1859
1860    async fn get_from_cache(
1861        &self,
1862        index_store: Arc<dyn IndexStore>,
1863        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1864        cache: &LanceCache,
1865    ) -> Result<Option<Arc<dyn ScalarIndex>>> {
1866        let Some(state) = cache.get_with_key(&BitmapIndexStateKey).await else {
1867            return Ok(None);
1868        };
1869        let index = state.to_bitmap_index(index_store, cache, frag_reuse_index)?;
1870        Ok(Some(index as Arc<dyn ScalarIndex>))
1871    }
1872
1873    async fn put_in_cache(&self, cache: &LanceCache, index: Arc<dyn ScalarIndex>) -> Result<()> {
1874        let state = BitmapIndexState::from_scalar_index(index.as_ref())?;
1875        cache
1876            .insert_with_key(&BitmapIndexStateKey, Arc::new(state))
1877            .await;
1878        Ok(())
1879    }
1880
1881    async fn get_or_insert_in_cache(
1882        &self,
1883        index_store: Arc<dyn IndexStore>,
1884        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1885        cache: &LanceCache,
1886        load: ScalarIndexLoad<'_>,
1887    ) -> Result<Arc<dyn ScalarIndex>> {
1888        single_flight_open(
1889            cache,
1890            BitmapIndexStateKey,
1891            load,
1892            BitmapIndexState::from_scalar_index,
1893            move |state| {
1894                Ok(state.to_bitmap_index(index_store, cache, frag_reuse_index)?
1895                    as Arc<dyn ScalarIndex>)
1896            },
1897        )
1898        .await
1899    }
1900
1901    async fn load_statistics(
1902        &self,
1903        index_store: Arc<dyn IndexStore>,
1904        _index_details: &prost_types::Any,
1905    ) -> Result<Option<serde_json::Value>> {
1906        let reader = index_store.open_index_file(BITMAP_LOOKUP_NAME).await?;
1907        if let Some(value) = reader.schema().metadata.get(INDEX_STATS_METADATA_KEY) {
1908            let stats = serde_json::from_str(value).map_err(|e| {
1909                Error::internal(format!("failed to parse bitmap statistics metadata: {e}"))
1910            })?;
1911            Ok(Some(stats))
1912        } else {
1913            Ok(None)
1914        }
1915    }
1916}
1917
1918#[cfg(test)]
1919mod tests {
1920    use super::*;
1921    use crate::metrics::{LocalMetricsCollector, NoOpMetricsCollector};
1922    use crate::scalar::lance_format::LanceIndexStore;
1923    use arrow_array::{RecordBatch, StringArray, UInt64Array, record_batch};
1924    use arrow_schema::{DataType, Field, Schema};
1925
1926    /// Sort a (value, row_id) RecordBatch by the value column so that unit tests
1927    /// match the ordering the production scanner applies via TrainingOrdering::Values.
1928    fn sort_batch_by_value(batch: &RecordBatch) -> RecordBatch {
1929        use arrow::compute::SortOptions;
1930        let values = batch.column(0);
1931        let row_ids = batch.column(1);
1932        let options = SortOptions {
1933            descending: false,
1934            nulls_first: true,
1935        };
1936        let indices = arrow::compute::sort_to_indices(values, Some(options), None).unwrap();
1937        let sorted_values = arrow::compute::take(values.as_ref(), &indices, None).unwrap();
1938        let sorted_row_ids = arrow::compute::take(row_ids.as_ref(), &indices, None).unwrap();
1939        RecordBatch::try_new(batch.schema(), vec![sorted_values, sorted_row_ids]).unwrap()
1940    }
1941    use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
1942    use futures::stream;
1943    use lance_core::utils::{address::RowAddress, tempfile::TempObjDir};
1944    use lance_io::object_store::ObjectStore;
1945    use lance_select::RowSetOps;
1946    use rstest::rstest;
1947
1948    fn assert_state_roundtrips(state: &BitmapIndexState) {
1949        let mut buf = Vec::new();
1950        state
1951            .serialize(&mut CacheEntryWriter::new(&mut buf))
1952            .unwrap();
1953        let data = bytes::Bytes::from(buf);
1954        let mut reader = CacheEntryReader::new(&data, 0, BitmapIndexState::CURRENT_VERSION);
1955        let restored = BitmapIndexState::deserialize(&mut reader).unwrap();
1956        assert_eq!(restored.lookup_batch, state.lookup_batch);
1957        assert_eq!(&*restored.null_map, &*state.null_map);
1958        assert_eq!(restored.value_type, state.value_type);
1959    }
1960
1961    #[test]
1962    fn test_bitmap_index_state_codec_roundtrip() {
1963        // Non-empty state with a few keys and a populated null map.
1964        let mut index_map = BTreeMap::new();
1965        index_map.insert(OrderableScalarValue(ScalarValue::Int32(Some(1))), 0);
1966        index_map.insert(OrderableScalarValue(ScalarValue::Int32(Some(7))), 1);
1967        index_map.insert(OrderableScalarValue(ScalarValue::Int32(Some(42))), 2);
1968        let mut null_map = RowAddrTreeMap::new();
1969        null_map.insert(RowAddress::new_from_parts(0, 3).into());
1970        null_map.insert(RowAddress::new_from_parts(0, 5).into());
1971        let state = BitmapIndexState {
1972            lookup_batch: build_lookup_batch(&index_map, &DataType::Int32).unwrap(),
1973            null_map: Arc::new(null_map),
1974            value_type: DataType::Int32,
1975            index_map: Arc::new(index_map),
1976        };
1977        assert_state_roundtrips(&state);
1978
1979        // Empty state: no keys, empty null map. Schema still carries the type.
1980        let empty_state = BitmapIndexState {
1981            lookup_batch: build_lookup_batch(&BTreeMap::new(), &DataType::Utf8).unwrap(),
1982            null_map: Arc::new(RowAddrTreeMap::new()),
1983            value_type: DataType::Utf8,
1984            index_map: Arc::new(BTreeMap::new()),
1985        };
1986        assert_state_roundtrips(&empty_state);
1987    }
1988
1989    /// The lookup batch must decode zero-copy through the full envelope-bearing
1990    /// [`CacheCodec`] even though the envelope pushes the IPC section to a
1991    /// non-aligned starting offset.
1992    #[test]
1993    fn test_bitmap_index_state_lookup_is_zero_copy() {
1994        const ALIGN: usize = 64;
1995        let mut index_map = BTreeMap::new();
1996        for k in 0..32i32 {
1997            index_map.insert(
1998                OrderableScalarValue(ScalarValue::Int32(Some(k))),
1999                k as usize,
2000            );
2001        }
2002        let state = BitmapIndexState {
2003            lookup_batch: build_lookup_batch(&index_map, &DataType::Int32).unwrap(),
2004            null_map: Arc::new(RowAddrTreeMap::new()),
2005            value_type: DataType::Int32,
2006            index_map: Arc::new(index_map),
2007        };
2008
2009        let codec = CacheCodec::from_impl::<BitmapIndexState>();
2010        let any: Arc<dyn std::any::Any + Send + Sync> = Arc::new(state);
2011        let mut buf = Vec::new();
2012        codec.serialize(&any, &mut buf).unwrap();
2013
2014        // Model a backend reading into a 64-byte-aligned buffer.
2015        let mut v = vec![0u8; buf.len() + ALIGN];
2016        let pad = (ALIGN - (v.as_ptr() as usize % ALIGN)) % ALIGN;
2017        v[pad..pad + buf.len()].copy_from_slice(&buf);
2018        let data = bytes::Bytes::from(v).slice(pad..pad + buf.len());
2019
2020        let restored = codec.deserialize(&data).hit().unwrap();
2021        let restored = restored.downcast::<BitmapIndexState>().unwrap();
2022
2023        let base = data.as_ptr() as usize;
2024        let end = base + data.len();
2025        for col in restored.lookup_batch.columns() {
2026            for buffer in col.to_data().buffers() {
2027                let ptr = buffer.as_ptr() as usize;
2028                assert!(
2029                    ptr >= base && ptr < end,
2030                    "lookup batch buffer was realigned out of the input — misaligned IPC section",
2031                );
2032            }
2033        }
2034    }
2035
2036    #[tokio::test]
2037    async fn test_bitmap_cache_key_uses_row_offset_identity() {
2038        let cache = LanceCache::with_capacity(1024);
2039        let first = BitmapKey::try_new(3).unwrap();
2040        let second = BitmapKey::try_new(4).unwrap();
2041
2042        cache
2043            .insert_with_key(&first, Arc::new(RowAddrTreeMap::default()))
2044            .await;
2045
2046        assert!(cache.get_with_key(&second).await.is_none());
2047    }
2048
2049    #[tokio::test]
2050    async fn test_bitmap_lazy_loading_and_cache() {
2051        // Create a temporary directory for the index
2052        let tmpdir = TempObjDir::default();
2053        let store = Arc::new(LanceIndexStore::new(
2054            Arc::new(ObjectStore::local()),
2055            tmpdir.clone(),
2056            Arc::new(LanceCache::no_cache()),
2057        ));
2058
2059        // Create test data with low cardinality column
2060        let colors = vec![
2061            "red", "blue", "green", "red", "yellow", "blue", "red", "green", "blue", "yellow",
2062            "red", "red", "blue", "green", "yellow",
2063        ];
2064
2065        let row_ids = (0u64..15u64).collect::<Vec<_>>();
2066
2067        let schema = Arc::new(Schema::new(vec![
2068            Field::new("value", DataType::Utf8, false),
2069            Field::new("_rowid", DataType::UInt64, false),
2070        ]));
2071
2072        let batch = RecordBatch::try_new(
2073            schema.clone(),
2074            vec![
2075                Arc::new(StringArray::from(colors.clone())),
2076                Arc::new(UInt64Array::from(row_ids.clone())),
2077            ],
2078        )
2079        .unwrap();
2080
2081        let batch = sort_batch_by_value(&batch);
2082        let stream = stream::once(async move { Ok(batch) });
2083        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2084
2085        // Train and write the bitmap index
2086        BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2087            .await
2088            .unwrap();
2089
2090        // Create a cache with limited capacity
2091        let cache = LanceCache::with_capacity(1024 * 1024); // 1MB cache
2092
2093        // Load the index (should only load metadata, not bitmaps)
2094        let index = BitmapIndex::load(store.clone(), None, &cache)
2095            .await
2096            .unwrap();
2097
2098        assert_eq!(index.index_map.len(), 4); // 4 non-null unique values (red, blue, green, yellow)
2099        assert!(index.null_map.is_empty()); // No nulls in test data
2100
2101        // Test 1: Search for "red"
2102        let query = SargableQuery::Equals(ScalarValue::Utf8(Some("red".to_string())));
2103        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2104
2105        // Verify results
2106        let expected_red_rows = vec![0u64, 3, 6, 10, 11];
2107        if let SearchResult::Exact(row_ids) = result {
2108            let mut actual: Vec<u64> = row_ids
2109                .true_rows()
2110                .row_addrs()
2111                .unwrap()
2112                .map(|id| id.into())
2113                .collect();
2114            actual.sort();
2115            assert_eq!(actual, expected_red_rows);
2116        } else {
2117            panic!("Expected exact search result");
2118        }
2119
2120        // Test 2: Search for "red" again - should hit cache
2121        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2122        if let SearchResult::Exact(row_ids) = result {
2123            let mut actual: Vec<u64> = row_ids
2124                .true_rows()
2125                .row_addrs()
2126                .unwrap()
2127                .map(|id| id.into())
2128                .collect();
2129            actual.sort();
2130            assert_eq!(actual, expected_red_rows);
2131        }
2132
2133        // Test 3: Range query
2134        let query = SargableQuery::Range(
2135            std::ops::Bound::Included(ScalarValue::Utf8(Some("blue".to_string()))),
2136            std::ops::Bound::Included(ScalarValue::Utf8(Some("green".to_string()))),
2137        );
2138        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2139
2140        let expected_range_rows = vec![1u64, 2, 5, 7, 8, 12, 13];
2141        if let SearchResult::Exact(row_ids) = result {
2142            let mut actual: Vec<u64> = row_ids
2143                .true_rows()
2144                .row_addrs()
2145                .unwrap()
2146                .map(|id| id.into())
2147                .collect();
2148            actual.sort();
2149            assert_eq!(actual, expected_range_rows);
2150        }
2151
2152        // Test 3b: Inverted range query should return empty result
2153        let query = SargableQuery::Range(
2154            std::ops::Bound::Included(ScalarValue::Utf8(Some("green".to_string()))),
2155            std::ops::Bound::Included(ScalarValue::Utf8(Some("blue".to_string()))),
2156        );
2157        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2158        if let SearchResult::Exact(row_ids) = result {
2159            assert!(row_ids.true_rows().is_empty());
2160        } else {
2161            panic!("Expected exact search result");
2162        }
2163
2164        // Test 4: IsIn query
2165        let query = SargableQuery::IsIn(vec![
2166            ScalarValue::Utf8(Some("red".to_string())),
2167            ScalarValue::Utf8(Some("yellow".to_string())),
2168        ]);
2169        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2170
2171        let expected_in_rows = vec![0u64, 3, 4, 6, 9, 10, 11, 14];
2172        if let SearchResult::Exact(row_ids) = result {
2173            let mut actual: Vec<u64> = row_ids
2174                .true_rows()
2175                .row_addrs()
2176                .unwrap()
2177                .map(|id| id.into())
2178                .collect();
2179            actual.sort();
2180            assert_eq!(actual, expected_in_rows);
2181        }
2182    }
2183
2184    /// Regression test for the review fix that gates `load_bitmap` on
2185    /// `index_map.contains_key` before recording a miss: a value that is
2186    /// not present in the index must short-circuit before touching the
2187    /// per-query cache counters. Previously an Equals query for a missing
2188    /// value would silently bump `index_cache_misses` and `parts_loaded`
2189    /// on every call even though no bitmap page was actually loaded.
2190    #[tokio::test]
2191    async fn test_bitmap_absent_value_records_no_cache_activity() {
2192        let tmpdir = TempObjDir::default();
2193        let store = Arc::new(LanceIndexStore::new(
2194            Arc::new(ObjectStore::local()),
2195            tmpdir.clone(),
2196            Arc::new(LanceCache::no_cache()),
2197        ));
2198
2199        let colors = vec!["red", "blue", "green", "yellow"];
2200        let row_ids = (0u64..4u64).collect::<Vec<_>>();
2201        let schema = Arc::new(Schema::new(vec![
2202            Field::new("value", DataType::Utf8, false),
2203            Field::new("_rowid", DataType::UInt64, false),
2204        ]));
2205        let batch = RecordBatch::try_new(
2206            schema.clone(),
2207            vec![
2208                Arc::new(StringArray::from(colors)),
2209                Arc::new(UInt64Array::from(row_ids)),
2210            ],
2211        )
2212        .unwrap();
2213        let batch = sort_batch_by_value(&batch);
2214        let stream = stream::once(async move { Ok(batch) });
2215        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2216        BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2217            .await
2218            .unwrap();
2219
2220        // Keep the `LanceCache` alive in test scope so the `WeakLanceCache`
2221        // inside `BitmapIndex` can upgrade during search.
2222        let cache = LanceCache::with_capacity(1024 * 1024);
2223        let index = BitmapIndex::load(store.clone(), None, &cache)
2224            .await
2225            .unwrap();
2226
2227        // Equals on a value that is not in `index_map` must not touch
2228        // the cache counters and must not report a part load.
2229        let metrics = LocalMetricsCollector::default();
2230        let query = SargableQuery::Equals(ScalarValue::Utf8(Some("purple".to_string())));
2231        let result = index.search(&query, &metrics).await.unwrap();
2232        if let SearchResult::Exact(row_ids) = result {
2233            assert!(row_ids.true_rows().is_empty());
2234        } else {
2235            panic!("Expected exact search result");
2236        }
2237        assert_eq!(
2238            metrics.index_cache_hits(),
2239            0,
2240            "absent value must not record any cache hits",
2241        );
2242        assert_eq!(
2243            metrics.index_cache_misses(),
2244            0,
2245            "absent value must not record a cache miss (no loader ran)",
2246        );
2247
2248        // IsIn covering only absent values also stays at 0/0.
2249        let metrics = LocalMetricsCollector::default();
2250        let query = SargableQuery::IsIn(vec![
2251            ScalarValue::Utf8(Some("purple".to_string())),
2252            ScalarValue::Utf8(Some("teal".to_string())),
2253        ]);
2254        let result = index.search(&query, &metrics).await.unwrap();
2255        if let SearchResult::Exact(row_ids) = result {
2256            assert!(row_ids.true_rows().is_empty());
2257        } else {
2258            panic!("Expected exact search result");
2259        }
2260        assert_eq!(metrics.index_cache_hits(), 0);
2261        assert_eq!(metrics.index_cache_misses(), 0);
2262
2263        // Sanity: a present value on the same cold cache still records
2264        // exactly one miss, proving the counters are wired up and the
2265        // absent-value path above is not silently no-op.
2266        let metrics = LocalMetricsCollector::default();
2267        let query = SargableQuery::Equals(ScalarValue::Utf8(Some("red".to_string())));
2268        index.search(&query, &metrics).await.unwrap();
2269        assert_eq!(metrics.index_cache_hits(), 0);
2270        assert_eq!(metrics.index_cache_misses(), 1);
2271    }
2272
2273    // Regression test for the O(N log N) warm-cache rebuild introduced in
2274    // commit 4de5ce67d.  BitmapIndexState now caches the parsed Arc<BTreeMap>
2275    // so that get_from_cache skips parse_lookup_batch on warm hits.
2276    // IS NULL is the worst case: the actual bitmap lookup is O(1) but
2277    // reconstruction of the BTreeMap touched every row in the lookup batch.
2278    #[tokio::test]
2279    async fn test_bitmap_cache_fast_path() {
2280        use arrow_array::Int32Array;
2281
2282        let tmpdir = TempObjDir::default();
2283        let store = Arc::new(LanceIndexStore::new(
2284            Arc::new(ObjectStore::local()),
2285            tmpdir.clone(),
2286            Arc::new(LanceCache::no_cache()),
2287        ));
2288
2289        // High-cardinality: 1 000 unique integers + 5 null rows.
2290        const N: u64 = 1_000;
2291        const NULL_COUNT: u64 = 5;
2292        // nulls first (sorted batch: nulls precede values)
2293        let null_values: Vec<Option<i32>> =
2294            std::iter::repeat_n(None, NULL_COUNT as usize).collect();
2295        let non_null_values: Vec<Option<i32>> = (0..N as i32).map(Some).collect();
2296        let all_values: Vec<Option<i32>> = null_values.into_iter().chain(non_null_values).collect();
2297        let all_row_ids: Vec<u64> = (0..N + NULL_COUNT).collect();
2298
2299        let schema = Arc::new(Schema::new(vec![
2300            Field::new("value", DataType::Int32, true),
2301            Field::new("_rowid", DataType::UInt64, false),
2302        ]));
2303        let batch = RecordBatch::try_new(
2304            schema.clone(),
2305            vec![
2306                Arc::new(Int32Array::from(all_values)),
2307                Arc::new(UInt64Array::from(all_row_ids)),
2308            ],
2309        )
2310        .unwrap();
2311        let stream = stream::once(async move { Ok(batch) });
2312        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2313        BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2314            .await
2315            .unwrap();
2316
2317        let cache = LanceCache::with_capacity(16 * 1024 * 1024);
2318        let index = BitmapIndex::load(store.clone(), None, &cache)
2319            .await
2320            .unwrap();
2321
2322        let plugin = BitmapIndexPlugin;
2323        let index_arc: Arc<dyn ScalarIndex> = index.clone() as Arc<dyn ScalarIndex>;
2324        plugin.put_in_cache(&cache, index_arc).await.unwrap();
2325
2326        // get_from_cache must return Some, and the BitmapIndexState's OnceLock
2327        // must have been populated by put_in_cache so no parse_lookup_batch occurs.
2328        let cached = plugin
2329            .get_from_cache(store.clone(), None, &cache)
2330            .await
2331            .unwrap()
2332            .expect("get_from_cache must return Some after put_in_cache");
2333
2334        // IS NULL: trivial work once the index is in hand.
2335        let query = SargableQuery::IsNull();
2336        match cached.search(&query, &NoOpMetricsCollector).await.unwrap() {
2337            SearchResult::Exact(row_set) => {
2338                let mut null_rows: Vec<u64> = row_set
2339                    .true_rows()
2340                    .row_addrs()
2341                    .unwrap()
2342                    .map(u64::from)
2343                    .collect();
2344                null_rows.sort();
2345                let expected: Vec<u64> = (0..NULL_COUNT).collect();
2346                assert_eq!(null_rows, expected);
2347            }
2348            _ => panic!("Expected Exact result for IS NULL"),
2349        }
2350    }
2351
2352    #[tokio::test]
2353    #[ignore]
2354    async fn test_big_bitmap_index() {
2355        // WARNING: This test allocates a huge state to force overflow over int32 on BinaryArray
2356        // You must run it only on a machine with enough resources (or skip it normally).
2357        use super::{BITMAP_LOOKUP_NAME, BitmapIndex};
2358        use crate::scalar::IndexStore;
2359        use crate::scalar::lance_format::LanceIndexStore;
2360        use arrow_schema::DataType;
2361        use datafusion_common::ScalarValue;
2362        use lance_core::cache::LanceCache;
2363        use lance_io::object_store::ObjectStore;
2364        use lance_select::RowAddrTreeMap;
2365        use std::collections::HashMap;
2366        use std::sync::Arc;
2367
2368        // Adjust these numbers so that:
2369        //     m * (serialized size per bitmap) > 2^31 bytes.
2370        //
2371        // For example, if we assume each bitmap serializes to ~1000 bytes,
2372        // you need m > 2.1e6.
2373        let m: u32 = 2_500_000;
2374        let per_bitmap_size = 1000; // assumed bytes per bitmap
2375
2376        let mut state = HashMap::new();
2377        for i in 0..m {
2378            // Create a bitmap that contains, say, 1000 row IDs.
2379            let bitmap = RowAddrTreeMap::from_iter(0..per_bitmap_size);
2380
2381            let key = ScalarValue::UInt32(Some(i));
2382            state.insert(key, bitmap);
2383        }
2384
2385        // Create a temporary store.
2386        let tmpdir = TempObjDir::default();
2387        let test_store = LanceIndexStore::new(
2388            Arc::new(ObjectStore::local()),
2389            tmpdir.clone(),
2390            Arc::new(LanceCache::no_cache()),
2391        );
2392
2393        // This call should never trigger a "byte array offset overflow" error since now the code supports
2394        // read by chunks
2395        let result =
2396            BitmapIndexPlugin::write_bitmap_index(state, &test_store, &DataType::UInt32).await;
2397
2398        assert!(
2399            result.is_ok(),
2400            "Failed to write bitmap index: {:?}",
2401            result.err()
2402        );
2403
2404        // Verify the index file exists
2405        let index_file = test_store.open_index_file(BITMAP_LOOKUP_NAME).await;
2406        assert!(
2407            index_file.is_ok(),
2408            "Failed to open index file: {:?}",
2409            index_file.err()
2410        );
2411        let index_file = index_file.unwrap();
2412
2413        // Print stats about the index file
2414        tracing::info!(
2415            "Index file contains {} rows in total",
2416            index_file.num_rows()
2417        );
2418
2419        // Load the index using BitmapIndex::load
2420        tracing::info!("Loading index from disk...");
2421        let loaded_index = BitmapIndex::load(Arc::new(test_store), None, &LanceCache::no_cache())
2422            .await
2423            .expect("Failed to load bitmap index");
2424
2425        // Verify the loaded index has the correct number of entries
2426        assert_eq!(
2427            loaded_index.index_map.len(),
2428            m as usize,
2429            "Loaded index has incorrect number of keys (expected {}, got {})",
2430            m,
2431            loaded_index.index_map.len()
2432        );
2433
2434        // Manually verify specific keys without using search()
2435        let test_keys = [0, m / 2, m - 1]; // Beginning, middle, and end
2436        for &key_val in &test_keys {
2437            let key = OrderableScalarValue(ScalarValue::UInt32(Some(key_val)));
2438            // Load the bitmap for this key
2439            let bitmap = loaded_index
2440                .load_bitmap(&key, None)
2441                .await
2442                .unwrap_or_else(|_| panic!("Key {} should exist", key_val));
2443
2444            // Convert RowAddrTreeMap to a vector for easier assertion
2445            let row_addrs: Vec<u64> = bitmap.row_addrs().unwrap().map(u64::from).collect();
2446
2447            // Verify length
2448            assert_eq!(
2449                row_addrs.len(),
2450                per_bitmap_size as usize,
2451                "Bitmap for key {} has wrong size",
2452                key_val
2453            );
2454
2455            // Verify first few and last few elements
2456            for i in 0..5.min(per_bitmap_size) {
2457                assert!(
2458                    row_addrs.contains(&i),
2459                    "Bitmap for key {} should contain row_id {}",
2460                    key_val,
2461                    i
2462                );
2463            }
2464
2465            for i in (per_bitmap_size - 5)..per_bitmap_size {
2466                assert!(
2467                    row_addrs.contains(&i),
2468                    "Bitmap for key {} should contain row_id {}",
2469                    key_val,
2470                    i
2471                );
2472            }
2473
2474            // Verify exact range
2475            let expected_range: Vec<u64> = (0..per_bitmap_size).collect();
2476            assert_eq!(
2477                row_addrs, expected_range,
2478                "Bitmap for key {} doesn't contain expected values",
2479                key_val
2480            );
2481
2482            tracing::info!(
2483                "✓ Verified bitmap for key {}: {} rows as expected",
2484                key_val,
2485                row_addrs.len()
2486            );
2487        }
2488
2489        tracing::info!("Test successful! Index properly contains {} keys", m);
2490    }
2491
2492    #[tokio::test]
2493    async fn test_bitmap_prewarm() {
2494        // Create a temporary directory for the index
2495        let tmpdir = TempObjDir::default();
2496        let store = Arc::new(LanceIndexStore::new(
2497            Arc::new(ObjectStore::local()),
2498            tmpdir.clone(),
2499            Arc::new(LanceCache::no_cache()),
2500        ));
2501
2502        // Create test data with low cardinality
2503        let colors = vec![
2504            "red", "blue", "green", "red", "yellow", "blue", "red", "green", "blue", "yellow",
2505            "red", "red", "blue", "green", "yellow",
2506        ];
2507
2508        let row_ids = (0u64..15u64).collect::<Vec<_>>();
2509
2510        let schema = Arc::new(Schema::new(vec![
2511            Field::new("value", DataType::Utf8, false),
2512            Field::new("_rowid", DataType::UInt64, false),
2513        ]));
2514
2515        let batch = RecordBatch::try_new(
2516            schema.clone(),
2517            vec![
2518                Arc::new(StringArray::from(colors.clone())),
2519                Arc::new(UInt64Array::from(row_ids.clone())),
2520            ],
2521        )
2522        .unwrap();
2523
2524        let batch = sort_batch_by_value(&batch);
2525        let stream = stream::once(async move { Ok(batch) });
2526        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2527
2528        // Train and write the bitmap index
2529        BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2530            .await
2531            .unwrap();
2532
2533        // Create a cache with metrics tracking
2534        let cache = LanceCache::with_capacity(1024 * 1024); // 1MB cache
2535
2536        // Load the index (should only load metadata, not bitmaps)
2537        let index = BitmapIndex::load(store.clone(), None, &cache)
2538            .await
2539            .unwrap();
2540
2541        // Verify no bitmaps are cached yet
2542        let red = OrderableScalarValue(ScalarValue::Utf8(Some("red".to_string())));
2543        let blue = OrderableScalarValue(ScalarValue::Utf8(Some("blue".to_string())));
2544        let cache_key_red = BitmapKey::try_new(*index.index_map.get(&red).unwrap()).unwrap();
2545        let cache_key_blue = BitmapKey::try_new(*index.index_map.get(&blue).unwrap()).unwrap();
2546
2547        assert!(
2548            cache
2549                .get_with_key::<BitmapKey>(&cache_key_red)
2550                .await
2551                .is_none()
2552        );
2553        assert!(
2554            cache
2555                .get_with_key::<BitmapKey>(&cache_key_blue)
2556                .await
2557                .is_none()
2558        );
2559
2560        // Call prewarm
2561        index.prewarm().await.unwrap();
2562
2563        // Verify all bitmaps are now cached
2564        assert!(
2565            cache
2566                .get_with_key::<BitmapKey>(&cache_key_red)
2567                .await
2568                .is_some()
2569        );
2570        assert!(
2571            cache
2572                .get_with_key::<BitmapKey>(&cache_key_blue)
2573                .await
2574                .is_some()
2575        );
2576
2577        // Verify cached bitmaps have correct content
2578        let cached_red = cache
2579            .get_with_key::<BitmapKey>(&cache_key_red)
2580            .await
2581            .unwrap();
2582        let red_rows: Vec<u64> = cached_red.row_addrs().unwrap().map(u64::from).collect();
2583        assert_eq!(red_rows, vec![0, 3, 6, 10, 11]);
2584
2585        // Call prewarm again - should be idempotent
2586        index.prewarm().await.unwrap();
2587
2588        // Verify cache still contains the same items
2589        let cached_red_2 = cache
2590            .get_with_key::<BitmapKey>(&cache_key_red)
2591            .await
2592            .unwrap();
2593        let red_rows_2: Vec<u64> = cached_red_2.row_addrs().unwrap().map(u64::from).collect();
2594        assert_eq!(red_rows_2, vec![0, 3, 6, 10, 11]);
2595    }
2596
2597    // frags 1 and 2 (3 rows each) are compacted into frag 3: the 6 rows are
2598    // rewritten in order to frag 3 offsets 0..6.
2599    fn bitmap_remap_compact() -> RowAddrRemap {
2600        use lance_core::utils::row_addr_remap::GroupInput;
2601        use roaring::RoaringTreemap;
2602        RowAddrRemap::compact([GroupInput {
2603            rewritten_old_row_addrs: RoaringTreemap::from_iter(
2604                (0..3)
2605                    .map(|o| u64::from(RowAddress::new_from_parts(1, o)))
2606                    .chain((0..3).map(|o| u64::from(RowAddress::new_from_parts(2, o)))),
2607            ),
2608            old_frag_ids: vec![1, 2],
2609            new_frags: vec![(3, 6)],
2610        }])
2611        .unwrap()
2612    }
2613
2614    fn bitmap_remap_explicit() -> RowAddrRemap {
2615        // The same mapping, listed out explicitly.
2616        RowAddrRemap::direct(
2617            (0..6u32)
2618                .map(|i| {
2619                    let (f, o) = if i < 3 { (1, i) } else { (2, i - 3) };
2620                    (
2621                        u64::from(RowAddress::new_from_parts(f, o)),
2622                        Some(u64::from(RowAddress::new_from_parts(3, i))),
2623                    )
2624                })
2625                .collect(),
2626        )
2627    }
2628
2629    // remap must behave identically whether the mapping is compact or explicit.
2630    #[rstest]
2631    #[case(bitmap_remap_compact())]
2632    #[case(bitmap_remap_explicit())]
2633    #[tokio::test]
2634    async fn test_remap_bitmap_with_null(#[case] remap: RowAddrRemap) {
2635        use arrow_array::UInt32Array;
2636
2637        // Create a temporary store.
2638        let tmpdir = TempObjDir::default();
2639        let test_store = Arc::new(LanceIndexStore::new(
2640            Arc::new(ObjectStore::local()),
2641            tmpdir.clone(),
2642            Arc::new(LanceCache::no_cache()),
2643        ));
2644
2645        // Create test data that simulates:
2646        // frag 1 - { 0: null, 1: null, 2: 1 }
2647        // frag 2 - { 0: 1, 1: 2, 2: 2 }
2648        // We'll create this data with specific row addresses
2649        let values = vec![
2650            None,       // row 0: null (will be at address (1,0))
2651            None,       // row 1: null (will be at address (1,1))
2652            Some(1u32), // row 2: 1    (will be at address (1,2))
2653            Some(1u32), // row 3: 1    (will be at address (2,0))
2654            Some(2u32), // row 4: 2    (will be at address (2,1))
2655            Some(2u32), // row 5: 2    (will be at address (2,2))
2656        ];
2657
2658        // Create row IDs with specific fragment addresses
2659        let row_ids: Vec<u64> = vec![
2660            RowAddress::new_from_parts(1, 0).into(),
2661            RowAddress::new_from_parts(1, 1).into(),
2662            RowAddress::new_from_parts(1, 2).into(),
2663            RowAddress::new_from_parts(2, 0).into(),
2664            RowAddress::new_from_parts(2, 1).into(),
2665            RowAddress::new_from_parts(2, 2).into(),
2666        ];
2667
2668        let schema = Arc::new(Schema::new(vec![
2669            Field::new("value", DataType::UInt32, true),
2670            Field::new("_rowid", DataType::UInt64, false),
2671        ]));
2672
2673        let batch = RecordBatch::try_new(
2674            schema.clone(),
2675            vec![
2676                Arc::new(UInt32Array::from(values)),
2677                Arc::new(UInt64Array::from(row_ids)),
2678            ],
2679        )
2680        .unwrap();
2681
2682        let stream = stream::once(async move { Ok(batch) });
2683        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2684
2685        // Create the bitmap index
2686        BitmapIndexPlugin::train_bitmap_index(stream, test_store.as_ref())
2687            .await
2688            .unwrap();
2689
2690        // Load the index
2691        let index = BitmapIndex::load(test_store.clone(), None, &LanceCache::no_cache())
2692            .await
2693            .expect("Failed to load bitmap index");
2694
2695        // Verify initial state
2696        assert_eq!(index.index_map.len(), 2); // 2 non-null values (1 and 2)
2697        assert!(!index.null_map.is_empty()); // Should have null values
2698
2699        // Perform remap
2700        index.remap(&remap, test_store.as_ref()).await.unwrap();
2701
2702        // Reload and check
2703        let reloaded_idx = BitmapIndex::load(test_store, None, &LanceCache::no_cache())
2704            .await
2705            .expect("Failed to load remapped bitmap index");
2706
2707        // Verify the null bitmap was remapped correctly
2708        let expected_null_addrs: Vec<u64> = vec![
2709            RowAddress::new_from_parts(3, 0).into(),
2710            RowAddress::new_from_parts(3, 1).into(),
2711        ];
2712        let actual_null_addrs: Vec<u64> = reloaded_idx
2713            .null_map
2714            .row_addrs()
2715            .unwrap()
2716            .map(u64::from)
2717            .collect();
2718        assert_eq!(
2719            actual_null_addrs, expected_null_addrs,
2720            "Null bitmap not remapped correctly"
2721        );
2722
2723        // Search for value 1 and verify remapped addresses
2724        let query = SargableQuery::Equals(ScalarValue::UInt32(Some(1)));
2725        let result = reloaded_idx
2726            .search(&query, &NoOpMetricsCollector)
2727            .await
2728            .unwrap();
2729        if let crate::scalar::SearchResult::Exact(row_ids) = result {
2730            let mut actual: Vec<u64> = row_ids
2731                .true_rows()
2732                .row_addrs()
2733                .unwrap()
2734                .map(u64::from)
2735                .collect();
2736            actual.sort();
2737            let expected: Vec<u64> = vec![
2738                RowAddress::new_from_parts(3, 2).into(),
2739                RowAddress::new_from_parts(3, 3).into(),
2740            ];
2741            assert_eq!(actual, expected, "Value 1 bitmap not remapped correctly");
2742        }
2743
2744        // Search for value 2 and verify remapped addresses
2745        let query = SargableQuery::Equals(ScalarValue::UInt32(Some(2)));
2746        let result = reloaded_idx
2747            .search(&query, &NoOpMetricsCollector)
2748            .await
2749            .unwrap();
2750        if let crate::scalar::SearchResult::Exact(row_ids) = result {
2751            let mut actual: Vec<u64> = row_ids
2752                .true_rows()
2753                .row_addrs()
2754                .unwrap()
2755                .map(u64::from)
2756                .collect();
2757            actual.sort();
2758            let expected: Vec<u64> = vec![
2759                RowAddress::new_from_parts(3, 4).into(),
2760                RowAddress::new_from_parts(3, 5).into(),
2761            ];
2762            assert_eq!(actual, expected, "Value 2 bitmap not remapped correctly");
2763        }
2764
2765        // Search for null values
2766        let query = SargableQuery::IsNull();
2767        let result = reloaded_idx
2768            .search(&query, &NoOpMetricsCollector)
2769            .await
2770            .unwrap();
2771        if let crate::scalar::SearchResult::Exact(row_ids) = result {
2772            let mut actual: Vec<u64> = row_ids
2773                .true_rows()
2774                .row_addrs()
2775                .unwrap()
2776                .map(u64::from)
2777                .collect();
2778            actual.sort();
2779            assert_eq!(
2780                actual, expected_null_addrs,
2781                "Null search results not correct"
2782            );
2783        }
2784    }
2785
2786    #[tokio::test]
2787    async fn test_bitmap_null_handling_in_queries() {
2788        // Test that bitmap index correctly returns null_list for queries
2789        let tmpdir = TempObjDir::default();
2790        let store = Arc::new(LanceIndexStore::new(
2791            Arc::new(ObjectStore::local()),
2792            tmpdir.clone(),
2793            Arc::new(LanceCache::no_cache()),
2794        ));
2795
2796        // Create test data: [0, 5, null]
2797        let batch = record_batch!(
2798            ("value", Int64, [Some(0), Some(5), None]),
2799            ("_rowid", UInt64, [0, 1, 2])
2800        )
2801        .unwrap();
2802        let schema = batch.schema();
2803        let stream = stream::once(async move { Ok(batch) });
2804        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2805
2806        // Train and write the bitmap index
2807        BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2808            .await
2809            .unwrap();
2810
2811        let cache = LanceCache::with_capacity(1024 * 1024);
2812        let index = BitmapIndex::load(store.clone(), None, &cache)
2813            .await
2814            .unwrap();
2815
2816        // Test 1: Search for value 5 - should return allow=[1], null=[2]
2817        let query = SargableQuery::Equals(ScalarValue::Int64(Some(5)));
2818        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2819
2820        match result {
2821            SearchResult::Exact(row_ids) => {
2822                let actual_rows: Vec<u64> = row_ids
2823                    .true_rows()
2824                    .row_addrs()
2825                    .unwrap()
2826                    .map(u64::from)
2827                    .collect();
2828                assert_eq!(actual_rows, vec![1], "Should find row 1 where value == 5");
2829
2830                let null_row_ids = row_ids.null_rows();
2831                // Check that null_row_ids contains row 2
2832                assert!(!null_row_ids.is_empty(), "null_row_ids should be Some");
2833                let null_rows: Vec<u64> =
2834                    null_row_ids.row_addrs().unwrap().map(u64::from).collect();
2835                assert_eq!(null_rows, vec![2], "Should report row 2 as null");
2836            }
2837            _ => panic!("Expected Exact search result"),
2838        }
2839        let entries_after_value_lookup = cache.size().await;
2840
2841        // Test 2: Search for null values - should return allow=[2], null=None
2842        let query = SargableQuery::IsNull();
2843        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2844
2845        match result {
2846            SearchResult::Exact(row_addrs) => {
2847                let actual_rows: Vec<u64> = row_addrs
2848                    .true_rows()
2849                    .row_addrs()
2850                    .unwrap()
2851                    .map(u64::from)
2852                    .collect();
2853                assert_eq!(
2854                    actual_rows,
2855                    vec![2],
2856                    "IsNull should find row 2 where value is null"
2857                );
2858
2859                let null_row_ids = row_addrs.null_rows();
2860                // When querying FOR nulls, null_row_ids should be None (nulls are the TRUE result)
2861                assert!(
2862                    null_row_ids.is_empty(),
2863                    "null_row_ids should be None for IsNull query"
2864                );
2865            }
2866            _ => panic!("Expected Exact search result"),
2867        }
2868        assert_eq!(
2869            cache.size().await,
2870            entries_after_value_lookup,
2871            "null bitmap lookup should bypass the per-value cache"
2872        );
2873
2874        // Test 3: Range query - should return matching rows and null_list
2875        let query = SargableQuery::Range(
2876            std::ops::Bound::Included(ScalarValue::Int64(Some(0))),
2877            std::ops::Bound::Included(ScalarValue::Int64(Some(3))),
2878        );
2879        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2880
2881        match result {
2882            SearchResult::Exact(row_addrs) => {
2883                let actual_rows: Vec<u64> = row_addrs
2884                    .true_rows()
2885                    .row_addrs()
2886                    .unwrap()
2887                    .map(u64::from)
2888                    .collect();
2889                assert_eq!(actual_rows, vec![0], "Should find row 0 where value == 0");
2890
2891                // Should report row 2 as null
2892                let null_row_ids = row_addrs.null_rows();
2893                assert!(!null_row_ids.is_empty(), "null_row_ids should be Some");
2894                let null_rows: Vec<u64> =
2895                    null_row_ids.row_addrs().unwrap().map(u64::from).collect();
2896                assert_eq!(null_rows, vec![2], "Should report row 2 as null");
2897            }
2898            _ => panic!("Expected Exact search result"),
2899        }
2900    }
2901}