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