Skip to main content

lance_table/transaction/
index_maintenance.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Keeping index metadata honest about what the new fragment list contains.
5//!
6//! An index entry claims coverage of a set of fragments and fields. Any operation
7//! that rewrites data can invalidate part of that claim, so a commit has to either
8//! narrow the entry's fragment bitmap, drop the fields it no longer describes, or
9//! drop the index. Getting this wrong does not fail the commit -- it silently
10//! returns stale rows from the index -- so each rule here is paired with a test.
11
12use crate::format::overlay::staleness::collect_overlay_stale_frags;
13use crate::format::{Fragment, IndexMetadata};
14use crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME;
15use crate::system_index::is_system_index;
16use crate::transaction::{RewriteGroup, RewrittenIndex, Transaction};
17use lance_core::datatypes::Schema;
18use lance_core::{Error, Result};
19use roaring::RoaringBitmap;
20use std::collections::{HashMap, HashSet};
21
22impl Transaction {
23    pub(super) fn register_pure_rewrite_rows_update_frags_in_indices(
24        indices: &mut [IndexMetadata],
25        pure_update_frag_ids: &[u64],
26        original_fragment_ids: &[u64],
27        fields_for_preserving_frag_bitmap: &[u32],
28        original_overlaid_frags: &HashMap<u32, &Fragment>,
29        schema: &Schema,
30    ) -> Result<()> {
31        if pure_update_frag_ids.is_empty() {
32            return Ok(());
33        }
34
35        let value_updated_field_set = fields_for_preserving_frag_bitmap
36            .iter()
37            .collect::<HashSet<_>>();
38
39        for index in indices.iter_mut() {
40            // Physical row addresses cannot follow moved rows into a new fragment.
41            // Leave that fragment uncovered so the scanner reads it directly.
42            if index.results_are_row_addrs() {
43                continue;
44            }
45            let index_covers_modified_field = index.fields.iter().any(|field_id| {
46                value_updated_field_set.contains(&u32::try_from(*field_id).unwrap())
47            });
48            if index_covers_modified_field {
49                continue;
50            }
51            let Some(fragment_bitmap) = index.fragment_bitmap.as_ref() else {
52                continue;
53            };
54
55            // Check that all the original fragments containing the updated rows are covered by
56            // the index. If not, some updated rows were not indexed, so we cannot index them.
57            let index_covers_all_original_fragments = original_fragment_ids
58                .iter()
59                .all(|&fragment_id| fragment_bitmap.contains(fragment_id as u32));
60            if !index_covers_all_original_fragments {
61                continue;
62            }
63
64            // A rewrite materializes overlays.  If any of those overlays touched the
65            // column being indexed then the rewrite will modify that column.  As a
66            // result, that index will no longer cover the fragment and it does not
67            // count as a pure rewrite and we must exclude it from the index's fragment
68            // bitmap.
69            let mut overlay_stale = RoaringBitmap::new();
70            collect_overlay_stale_frags(
71                index,
72                original_overlaid_frags,
73                &mut overlay_stale,
74                schema,
75            )?;
76            if !overlay_stale.is_empty() {
77                continue;
78            }
79
80            if let Some(fragment_bitmap) = index.fragment_bitmap.as_mut() {
81                for fragment_id in pure_update_frag_ids.iter().map(|f| *f as u32) {
82                    fragment_bitmap.insert(fragment_id);
83                }
84            }
85        }
86        Ok(())
87    }
88
89    /// If an operation modifies one or more fields in a fragment then we need to remove
90    /// that fragment from any indices that cover one of the modified fields.
91    pub fn prune_updated_fields_from_indices(
92        indices: &mut [IndexMetadata],
93        updated_fragments: &[Fragment],
94        fields_modified: &[u32],
95    ) {
96        if fields_modified.is_empty() {
97            return;
98        }
99
100        // If we modified any fields in the fragments then we need to remove those fragments
101        // from the index if the index covers one of those modified fields.
102        let fields_modified_set = fields_modified.iter().collect::<HashSet<_>>();
103        for index in indices.iter_mut() {
104            if index
105                .fields
106                .iter()
107                .any(|field_id| fields_modified_set.contains(&u32::try_from(*field_id).unwrap()))
108                && let Some(fragment_bitmap) = &mut index.fragment_bitmap
109            {
110                for fragment_id in updated_fragments.iter().map(|f| f.id as u32) {
111                    fragment_bitmap.remove(fragment_id);
112                }
113            }
114        }
115    }
116
117    /// Map each (non-tombstoned) field id in a fragment to the path of the data
118    /// file that backs it.
119    fn fragment_field_paths(frag: &Fragment) -> HashMap<i32, &str> {
120        let mut map = HashMap::new();
121        for file in &frag.files {
122            for &field_id in file.fields.iter() {
123                if field_id >= 0 {
124                    map.insert(field_id, file.path.as_str());
125                }
126            }
127        }
128        map
129    }
130
131    /// A `Merge` can rewrite a column's data *in place* -- the field stays in the
132    /// schema but its backing data file changes (the overlay fragment carries a new
133    /// file for the field and tombstones its old field id). `retain_relevant_indices`
134    /// only drops indices for *removed* fields, so without this the index keeps
135    /// covering the rewritten fragments with stale entries. Remove each such fragment
136    /// from any index covering a field whose backing data file changed.
137    pub(super) fn prune_merge_rewritten_fields_from_indices(
138        indices: &mut [IndexMetadata],
139        prev_fragments: &[Fragment],
140        new_fragments: &[Fragment],
141    ) {
142        let prev_by_id: HashMap<u64, &Fragment> =
143            prev_fragments.iter().map(|f| (f.id, f)).collect();
144        for new_frag in new_fragments {
145            let Some(prev) = prev_by_id.get(&new_frag.id) else {
146                continue; // brand-new fragment: nothing stale to prune
147            };
148            let prev_paths = Self::fragment_field_paths(prev);
149            let new_paths = Self::fragment_field_paths(new_frag);
150            // Fields still present whose backing file path changed == rewritten data.
151            let changed: Vec<u32> = prev_paths
152                .iter()
153                .filter(|(field_id, prev_path)| {
154                    new_paths
155                        .get(*field_id)
156                        .is_some_and(|new_path| new_path != *prev_path)
157                })
158                .map(|(field_id, _)| *field_id as u32)
159                .collect();
160            if changed.is_empty() {
161                continue;
162            }
163            Self::prune_updated_fields_from_indices(
164                indices,
165                std::slice::from_ref(new_frag),
166                &changed,
167            );
168        }
169    }
170
171    /// After a `Rewrite` fully compacts a fragment, its data overlays are baked
172    /// into the new fragment's base data. An index built *before* one of those
173    /// overlays (`overlay.committed_version > index.dataset_version`) indexed the
174    /// stale pre-overlay values -- and unlike a live overlay, the compacted
175    /// fragment no longer signals that staleness to the query path. Drop each
176    /// rewritten (new) fragment from the coverage of any index covering a field
177    /// such an overlay supplied, so those rows fall back to a flat scan.
178    pub(super) fn prune_overlay_stale_fields_from_indices(
179        indices: &mut [IndexMetadata],
180        groups: &[RewriteGroup],
181    ) {
182        for group in groups {
183            // field id -> newest overlay committed_version supplying that field
184            let mut overlaid_field_versions: HashMap<i32, u64> = HashMap::new();
185            for old_frag in &group.old_fragments {
186                for overlay in &old_frag.overlays {
187                    for &field_id in overlay.data_file.fields.iter() {
188                        if field_id < 0 {
189                            // Tombstoned (obsolete) overlay field: supplies nothing.
190                            continue;
191                        }
192                        let entry = overlaid_field_versions.entry(field_id).or_insert(0);
193                        *entry = (*entry).max(overlay.committed_version);
194                    }
195                }
196            }
197            if overlaid_field_versions.is_empty() {
198                continue;
199            }
200
201            let new_fragment_ids = group
202                .new_fragments
203                .iter()
204                .map(|f| f.id as u32)
205                .collect::<Vec<_>>();
206            for index in indices.iter_mut() {
207                let is_stale = index.fields.iter().any(|field_id| {
208                    overlaid_field_versions
209                        .get(field_id)
210                        .is_some_and(|&overlay_version| overlay_version > index.dataset_version)
211                });
212                if is_stale && let Some(fragment_bitmap) = &mut index.fragment_bitmap {
213                    for new_id in &new_fragment_ids {
214                        fragment_bitmap.remove(*new_id);
215                    }
216                }
217            }
218        }
219    }
220
221    pub(crate) fn retain_relevant_indices(
222        indices: &mut Vec<IndexMetadata>,
223        schema: &Schema,
224        fragments: &[Fragment],
225    ) {
226        let field_ids = schema
227            .fields_pre_order()
228            .map(|f| f.id)
229            .collect::<HashSet<_>>();
230
231        // Remove indices for fields no longer in schema
232        indices.retain(|existing_index| {
233            existing_index
234                .fields
235                .iter()
236                .all(|field_id| field_ids.contains(field_id))
237                || is_system_index(existing_index)
238        });
239
240        let mut indices_by_name: std::collections::HashMap<String, Vec<&IndexMetadata>> =
241            std::collections::HashMap::new();
242
243        for index in indices.iter() {
244            if index.name != FRAG_REUSE_INDEX_NAME {
245                indices_by_name
246                    .entry(index.name.clone())
247                    .or_default()
248                    .push(index);
249            }
250        }
251
252        let mut uuids_to_keep = std::collections::HashSet::new();
253
254        let existing_fragments = fragments
255            .iter()
256            .map(|f| f.id as u32)
257            .collect::<RoaringBitmap>();
258
259        for (_, same_name_indices) in indices_by_name {
260            if same_name_indices.len() > 1 {
261                let (empty_indices, non_empty_indices): (Vec<_>, Vec<_>) =
262                    same_name_indices.iter().partition(|index| {
263                        index
264                            .effective_fragment_bitmap(&existing_fragments)
265                            .as_ref()
266                            .is_none_or(|bitmap| bitmap.is_empty())
267                    });
268
269                if non_empty_indices.is_empty() {
270                    // All indices are empty -- keep only the oldest definition.
271                    //
272                    // An empty index definition is still correct: the scanner
273                    // falls back to scanning unindexed fragments, and normal
274                    // index maintenance rebuilds coverage once rows accrue.
275                    // Dropping the definition instead would silently lose the
276                    // index whenever an operation replaces every fragment it
277                    // covered (e.g. a full table rewrite), leaving the dataset
278                    // without its declared index.
279                    let mut sorted_indices = empty_indices;
280                    sorted_indices.sort_by_key(|index: &&IndexMetadata| index.dataset_version);
281
282                    if let Some(oldest) = sorted_indices.first() {
283                        uuids_to_keep.insert(oldest.uuid);
284                    }
285                } else {
286                    for index in non_empty_indices {
287                        uuids_to_keep.insert(index.uuid);
288                    }
289                }
290            } else {
291                // Single index whose column is still in schema: keep it, even
292                // when its coverage is empty (see the all-empty note above).
293                if let Some(index) = same_name_indices.first() {
294                    uuids_to_keep.insert(index.uuid);
295                }
296            }
297        }
298
299        indices.retain(|index| {
300            index.name == FRAG_REUSE_INDEX_NAME || uuids_to_keep.contains(&index.uuid)
301        });
302    }
303
304    pub(super) fn recalculate_fragment_bitmap(
305        old: &RoaringBitmap,
306        groups: &[RewriteGroup],
307    ) -> Result<RoaringBitmap> {
308        let mut new_bitmap = old.clone();
309        for group in groups {
310            let any_in_index = group
311                .old_fragments
312                .iter()
313                .any(|frag| old.contains(frag.id as u32));
314            let all_in_index = group
315                .old_fragments
316                .iter()
317                .all(|frag| old.contains(frag.id as u32));
318            // Any rewrite group may or may not be covered by the index.  However, if any fragment
319            // in a rewrite group was previously covered by the index then all fragments in the rewrite
320            // group must have been previously covered by the index.  plan_compaction takes care of
321            // this for us so this should be safe to assume.
322            if any_in_index {
323                if all_in_index {
324                    for frag_id in group.old_fragments.iter().map(|frag| frag.id as u32) {
325                        new_bitmap.remove(frag_id);
326                    }
327                    new_bitmap.extend(group.new_fragments.iter().map(|frag| frag.id as u32));
328                } else {
329                    return Err(Error::invalid_input(
330                        "The compaction plan included a rewrite group that was a split of indexed and non-indexed data",
331                    ));
332                }
333            }
334        }
335        Ok(new_bitmap)
336    }
337
338    pub(super) fn handle_rewrite_indices(
339        indices: &mut [IndexMetadata],
340        rewritten_indices: &[RewrittenIndex],
341        groups: &[RewriteGroup],
342    ) -> Result<()> {
343        let mut modified_indices = HashSet::new();
344
345        for rewritten_index in rewritten_indices {
346            if !modified_indices.insert(rewritten_index.old_id) {
347                return Err(Error::invalid_input(format!(
348                    "An invalid compaction plan must have been generated because multiple tasks modified the same index: {}",
349                    rewritten_index.old_id
350                )));
351            }
352
353            // Skip indices that no longer exist (may have been removed by concurrent operation)
354            let Some(index) = indices
355                .iter_mut()
356                .find(|idx| idx.uuid == rewritten_index.old_id)
357            else {
358                continue;
359            };
360
361            index.fragment_bitmap = Some(Self::recalculate_fragment_bitmap(
362                index.fragment_bitmap.as_ref().ok_or_else(|| {
363                    Error::invalid_input(format!(
364                        "Cannot rewrite index {} which did not store fragment bitmap",
365                        index.uuid
366                    ))
367                })?,
368                groups,
369            )?);
370            index.uuid = rewritten_index.new_id;
371            // Update file sizes to match the new index files. When not available
372            // (e.g., from older writers), clear the old file sizes to avoid
373            // using stale sizes from the pre-remap index.
374            index.files = rewritten_index.new_index_files.clone();
375        }
376        Ok(())
377    }
378
379    pub(super) fn handle_rewrite_fragments(
380        final_fragments: &mut Vec<Fragment>,
381        groups: &[RewriteGroup],
382        fragment_id: &mut u64,
383        version: u64,
384        _next_row_id: Option<&u64>,
385    ) -> Result<()> {
386        for group in groups {
387            // If the old fragments are contiguous, find the range
388            let replace_range = {
389                let start = final_fragments
390                    .iter()
391                    .enumerate()
392                    .find(|(_, f)| f.id == group.old_fragments[0].id)
393                    .ok_or_else(|| {
394                        Error::commit_conflict_source(
395                            version,
396                            format!(
397                                "dataset does not contain a fragment a rewrite operation wants to replace: id={}",
398                                group.old_fragments[0].id
399                            )
400                            .into(),
401                        )
402                    })?
403                    .0;
404
405                // Verify old_fragments matches contiguous range
406                let mut i = 1;
407                loop {
408                    if i == group.old_fragments.len() {
409                        break Some(start..start + i);
410                    }
411                    if final_fragments[start + i].id != group.old_fragments[i].id {
412                        break None;
413                    }
414                    i += 1;
415                }
416            };
417
418            let new_fragments = Self::fragments_with_ids(group.new_fragments.clone(), fragment_id)
419                .collect::<Vec<_>>();
420
421            // Version metadata for rewritten fragments is handled by the compaction code
422            // (recalc_versions_for_rewritten_fragments) which preserves version information
423            // from the original fragments. We don't modify it here.
424
425            if let Some(replace_range) = replace_range {
426                // Efficiently path using slice
427                final_fragments.splice(replace_range, new_fragments);
428            } else {
429                // Slower path for non-contiguous ranges
430                for fragment in group.old_fragments.iter() {
431                    final_fragments.retain(|f| f.id != fragment.id);
432                }
433                final_fragments.extend(new_fragments);
434            }
435        }
436        Ok(())
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use crate::transaction::test_support::overlay_with_field;
444    use uuid::Uuid;
445
446    #[test]
447    fn test_rewrite_fragments() {
448        let existing_fragments: Vec<Fragment> = (0..10).map(Fragment::new).collect();
449
450        let mut final_fragments = existing_fragments;
451        let rewrite_groups = vec![
452            // Since these are contiguous, they will be put in the same location
453            // as 1 and 2.
454            RewriteGroup {
455                old_fragments: vec![Fragment::new(1), Fragment::new(2)],
456                // These two fragments were previously reserved
457                new_fragments: vec![Fragment::new(15), Fragment::new(16)],
458            },
459            // These are not contiguous, so they will be inserted at the end.
460            RewriteGroup {
461                old_fragments: vec![Fragment::new(5), Fragment::new(8)],
462                // We pretend this id was not reserved.  Does not happen in practice today
463                // but we want to leave the door open.
464                new_fragments: vec![Fragment::new(0)],
465            },
466        ];
467
468        let mut fragment_id = 20;
469        let version = 0;
470
471        Transaction::handle_rewrite_fragments(
472            &mut final_fragments,
473            &rewrite_groups,
474            &mut fragment_id,
475            version,
476            None,
477        )
478        .unwrap();
479
480        assert_eq!(fragment_id, 21);
481
482        let expected_fragments: Vec<Fragment> = vec![
483            Fragment::new(0),
484            Fragment::new(15),
485            Fragment::new(16),
486            Fragment::new(3),
487            Fragment::new(4),
488            Fragment::new(6),
489            Fragment::new(7),
490            Fragment::new(9),
491            Fragment::new(20),
492        ];
493
494        assert_eq!(final_fragments, expected_fragments);
495    }
496
497    #[test]
498    fn test_retain_indices_removes_missing_fields() {
499        let schema = create_test_schema(&[1, 2]);
500        let fragments = vec![Fragment::new(1), Fragment::new(2)];
501
502        let mut indices = vec![
503            create_test_index("idx1", 1, 1, Some(RoaringBitmap::from_iter([1])), false),
504            create_test_index("idx2", 2, 1, Some(RoaringBitmap::from_iter([1])), false),
505            create_test_index("idx3", 99, 1, Some(RoaringBitmap::from_iter([1])), false), // Field doesn't exist
506        ];
507
508        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
509
510        assert_eq!(indices.len(), 2);
511        assert!(indices.iter().all(|idx| idx.fields[0] != 99));
512    }
513
514    #[test]
515    fn test_retain_indices_keeps_system_indices() {
516        use crate::system_index::mem_wal::MEM_WAL_INDEX_NAME;
517
518        let schema = create_test_schema(&[1, 2]);
519        let fragments = vec![Fragment::new(1)];
520
521        let mut indices = vec![
522            create_system_index(FRAG_REUSE_INDEX_NAME, 99), // Field doesn't exist but should be kept
523            create_system_index(MEM_WAL_INDEX_NAME, 99), // Field doesn't exist but should be kept
524            create_test_index("regular_idx", 99, 1, Some(RoaringBitmap::new()), false), // Should be removed
525        ];
526
527        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
528
529        assert_eq!(indices.len(), 2);
530        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
531        assert!(indices.iter().any(|idx| idx.name == MEM_WAL_INDEX_NAME));
532    }
533
534    #[test]
535    fn test_retain_indices_keeps_fragment_reuse_index() {
536        let schema = create_test_schema(&[1]);
537        let fragments = vec![Fragment::new(1)];
538
539        let mut indices = vec![
540            create_system_index(FRAG_REUSE_INDEX_NAME, 1),
541            create_test_index("other_idx", 1, 1, Some(RoaringBitmap::new()), false),
542        ];
543
544        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
545
546        // Fragment reuse index should always be kept
547        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
548    }
549
550    #[test]
551    fn test_retain_single_empty_scalar_index() {
552        let schema = create_test_schema(&[1]);
553        let fragments = vec![Fragment::new(1)];
554
555        let mut indices = vec![create_test_index(
556            "scalar_idx",
557            1,
558            1,
559            Some(RoaringBitmap::new()), // Empty bitmap
560            false,
561        )];
562
563        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
564
565        // Single empty scalar index should be kept
566        assert_eq!(indices.len(), 1);
567    }
568
569    #[test]
570    fn test_retain_single_empty_vector_index_is_kept() {
571        let schema = create_test_schema(&[1]);
572        let fragments = vec![Fragment::new(1)];
573
574        let mut indices = vec![create_test_index(
575            "vector_idx",
576            1,
577            1,
578            Some(RoaringBitmap::new()), // Empty bitmap
579            true,
580        )];
581
582        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
583
584        // The empty definition is retained: coverage is empty but the index
585        // declaration must survive operations that replace every fragment.
586        assert_eq!(indices.len(), 1);
587    }
588
589    #[test]
590    fn test_retain_single_nonempty_index() {
591        let schema = create_test_schema(&[1]);
592        let fragments = vec![Fragment::new(1)];
593
594        let mut scalar_indices = vec![create_test_index(
595            "scalar_idx",
596            1,
597            1,
598            Some(RoaringBitmap::from_iter([1])),
599            false,
600        )];
601
602        let mut vector_indices = vec![create_test_index(
603            "vector_idx",
604            1,
605            1,
606            Some(RoaringBitmap::from_iter([1])),
607            true,
608        )];
609
610        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
611        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
612
613        // Both should be kept
614        assert_eq!(scalar_indices.len(), 1);
615        assert_eq!(vector_indices.len(), 1);
616    }
617
618    #[test]
619    fn test_retain_single_index_with_none_bitmap() {
620        let schema = create_test_schema(&[1]);
621        let fragments = vec![Fragment::new(1)];
622
623        let mut scalar_indices = vec![create_test_index("scalar_idx", 1, 1, None, false)];
624        let mut vector_indices = vec![create_test_index("vector_idx", 1, 1, None, true)];
625
626        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
627        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
628
629        // Both kept: a None bitmap counts as empty coverage, and empty
630        // definitions are retained regardless of index type.
631        assert_eq!(scalar_indices.len(), 1);
632        assert_eq!(vector_indices.len(), 1);
633    }
634
635    #[test]
636    fn test_retain_multiple_empty_scalar_indices_keeps_oldest() {
637        let schema = create_test_schema(&[1]);
638        let fragments = vec![Fragment::new(1)];
639
640        let mut indices = vec![
641            create_test_index("idx", 1, 3, Some(RoaringBitmap::new()), false),
642            create_test_index("idx", 1, 1, Some(RoaringBitmap::new()), false), // Oldest
643            create_test_index("idx", 1, 2, Some(RoaringBitmap::new()), false),
644        ];
645
646        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
647
648        // Should keep only the oldest (dataset_version = 1)
649        assert_eq!(indices.len(), 1);
650        assert_eq!(indices[0].dataset_version, 1);
651    }
652
653    #[test]
654    fn test_retain_multiple_empty_vector_indices_keeps_oldest() {
655        let schema = create_test_schema(&[1]);
656        let fragments = vec![Fragment::new(1)];
657
658        let mut indices = vec![
659            create_test_index("vec_idx", 1, 1, Some(RoaringBitmap::new()), true),
660            create_test_index("vec_idx", 1, 2, Some(RoaringBitmap::new()), true),
661            create_test_index("vec_idx", 1, 3, Some(RoaringBitmap::new()), true),
662        ];
663
664        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
665
666        // Same as the scalar case: all deltas are empty, so only the oldest
667        // definition survives.
668        assert_eq!(indices.len(), 1);
669        assert_eq!(indices[0].dataset_version, 1);
670    }
671
672    #[test]
673    fn test_retain_mixed_empty_nonempty_keeps_nonempty() {
674        let schema = create_test_schema(&[1]);
675        let fragments = vec![Fragment::new(1)];
676
677        let mut indices = vec![
678            create_test_index("idx", 1, 1, Some(RoaringBitmap::new()), false), // Empty
679            create_test_index("idx", 1, 2, Some(RoaringBitmap::from_iter([1])), false), // Non-empty
680            create_test_index("idx", 1, 3, Some(RoaringBitmap::new()), false), // Empty
681            create_test_index("idx", 1, 4, Some(RoaringBitmap::from_iter([1])), false), // Non-empty
682        ];
683
684        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
685
686        // Should keep only non-empty indices
687        assert_eq!(indices.len(), 2);
688        assert!(
689            indices
690                .iter()
691                .all(|idx| idx.dataset_version == 2 || idx.dataset_version == 4)
692        );
693    }
694
695    #[test]
696    fn test_retain_mixed_empty_nonempty_vector_keeps_nonempty() {
697        let schema = create_test_schema(&[1]);
698        let fragments = vec![Fragment::new(1)];
699
700        let mut indices = vec![
701            create_test_index("vec_idx", 1, 1, Some(RoaringBitmap::new()), true), // Empty
702            create_test_index("vec_idx", 1, 2, Some(RoaringBitmap::from_iter([1])), true), // Non-empty
703            create_test_index("vec_idx", 1, 3, Some(RoaringBitmap::new()), true),          // Empty
704        ];
705
706        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
707
708        // Should keep only non-empty index
709        assert_eq!(indices.len(), 1);
710        assert_eq!(indices[0].dataset_version, 2);
711    }
712
713    #[test]
714    fn test_retain_fragment_bitmap_with_nonexistent_fragments() {
715        let schema = create_test_schema(&[1]);
716        let fragments = vec![Fragment::new(1), Fragment::new(2)]; // Only fragments 1 and 2 exist
717
718        let mut indices = vec![create_test_index(
719            "idx",
720            1,
721            1,
722            Some(RoaringBitmap::from_iter([1, 2, 3, 4])), // References non-existent fragments 3, 4
723            false,
724        )];
725
726        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
727
728        // Should still keep the index (effective bitmap will be intersection with existing)
729        assert_eq!(indices.len(), 1);
730        // Original bitmap should be unchanged
731        assert_eq!(
732            indices[0].fragment_bitmap.as_ref().unwrap(),
733            &RoaringBitmap::from_iter([1, 2, 3, 4])
734        );
735    }
736
737    #[test]
738    fn test_retain_effective_empty_bitmap_single_index() {
739        let schema = create_test_schema(&[1]);
740        let fragments = vec![Fragment::new(5), Fragment::new(6)];
741
742        // Bitmap references fragments that don't exist, so effective bitmap is empty
743        let mut scalar_indices = vec![create_test_index(
744            "scalar_idx",
745            1,
746            1,
747            Some(RoaringBitmap::from_iter([1, 2, 3])),
748            false,
749        )];
750
751        let mut vector_indices = vec![create_test_index(
752            "vector_idx",
753            1,
754            1,
755            Some(RoaringBitmap::from_iter([1, 2, 3])),
756            true,
757        )];
758
759        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
760        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
761
762        // Both kept: a single index whose column is still in schema is
763        // retained even when its effective coverage is empty.
764        assert_eq!(scalar_indices.len(), 1);
765        assert_eq!(vector_indices.len(), 1);
766    }
767
768    #[test]
769    fn test_retain_different_index_names() {
770        let schema = create_test_schema(&[1]);
771        let fragments = vec![Fragment::new(1)];
772
773        let mut indices = vec![
774            create_test_index("idx_a", 1, 1, Some(RoaringBitmap::new()), false),
775            create_test_index("idx_b", 1, 1, Some(RoaringBitmap::new()), true),
776            create_test_index("idx_c", 1, 1, Some(RoaringBitmap::from_iter([1])), false),
777        ];
778
779        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
780
781        // All three kept: empty definitions are retained for scalar and
782        // vector indexes alike.
783        assert_eq!(indices.len(), 3);
784        assert!(indices.iter().any(|idx| idx.name == "idx_a"));
785        assert!(indices.iter().any(|idx| idx.name == "idx_b"));
786        assert!(indices.iter().any(|idx| idx.name == "idx_c"));
787    }
788
789    #[test]
790    fn test_retain_empty_indices_vec() {
791        let schema = create_test_schema(&[1]);
792        let fragments = vec![Fragment::new(1)];
793
794        let mut indices: Vec<IndexMetadata> = vec![];
795
796        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
797
798        assert_eq!(indices.len(), 0);
799    }
800
801    #[test]
802    fn test_retain_all_indices_removed() {
803        let schema = create_test_schema(&[1]);
804        let fragments = vec![Fragment::new(1)];
805
806        let mut indices = vec![
807            create_test_index("vec1", 1, 1, Some(RoaringBitmap::new()), true),
808            create_test_index("vec2", 1, 1, Some(RoaringBitmap::new()), true),
809            create_test_index("idx3", 99, 1, Some(RoaringBitmap::from_iter([1])), false), // Bad field
810        ];
811
812        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
813
814        // Only the bad-field index is dropped; the empty vector definitions
815        // are retained.
816        assert_eq!(indices.len(), 2);
817        assert!(!indices.iter().any(|idx| idx.name == "idx3"));
818    }
819
820    #[test]
821    fn test_retain_complex_scenario() {
822        let schema = create_test_schema(&[1, 2]);
823        let fragments = vec![Fragment::new(1), Fragment::new(2)];
824
825        let mut indices = vec![
826            // System index - should always be kept
827            create_system_index(FRAG_REUSE_INDEX_NAME, 1),
828            // Group "idx_a" - all empty scalars, keep oldest
829            create_test_index("idx_a", 1, 3, Some(RoaringBitmap::new()), false),
830            create_test_index("idx_a", 1, 1, Some(RoaringBitmap::new()), false), // Oldest
831            create_test_index("idx_a", 1, 2, Some(RoaringBitmap::new()), false),
832            // Group "vec_b" - all empty vectors, keep oldest definition
833            create_test_index("vec_b", 1, 1, Some(RoaringBitmap::new()), true),
834            create_test_index("vec_b", 1, 2, Some(RoaringBitmap::new()), true),
835            // Group "idx_c" - mixed empty/non-empty, keep non-empty
836            create_test_index("idx_c", 2, 1, Some(RoaringBitmap::new()), false),
837            create_test_index("idx_c", 2, 2, Some(RoaringBitmap::from_iter([1])), false), // Keep
838            create_test_index("idx_c", 2, 3, Some(RoaringBitmap::from_iter([2])), false), // Keep
839            // Single non-empty - keep
840            create_test_index("idx_d", 1, 1, Some(RoaringBitmap::from_iter([1, 2])), false),
841            // Index with bad field - remove
842            create_test_index("idx_e", 99, 1, Some(RoaringBitmap::from_iter([1])), false),
843        ];
844
845        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
846
847        // Expected: frag_reuse, idx_a (oldest), vec_b (oldest), idx_c (2
848        // non-empty), idx_d = 6 total
849        assert_eq!(indices.len(), 6);
850
851        // Verify system index kept
852        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
853
854        // Verify idx_a kept oldest only
855        let idx_a_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "idx_a").collect();
856        assert_eq!(idx_a_indices.len(), 1);
857        assert_eq!(idx_a_indices[0].dataset_version, 1);
858
859        // Verify vec_b kept oldest definition only
860        let vec_b_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "vec_b").collect();
861        assert_eq!(vec_b_indices.len(), 1);
862        assert_eq!(vec_b_indices[0].dataset_version, 1);
863
864        // Verify idx_c kept non-empty only
865        let idx_c_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "idx_c").collect();
866        assert_eq!(idx_c_indices.len(), 2);
867        assert!(
868            idx_c_indices
869                .iter()
870                .all(|idx| idx.dataset_version == 2 || idx.dataset_version == 3)
871        );
872
873        // Verify idx_d kept
874        assert!(indices.iter().any(|idx| idx.name == "idx_d"));
875
876        // Verify idx_e removed (bad field)
877        assert!(!indices.iter().any(|idx| idx.name == "idx_e"));
878    }
879
880    #[test]
881    fn test_handle_rewrite_indices_skips_missing_index() {
882        // Create an empty indices list
883        let mut indices = vec![];
884
885        // Create rewritten_indices referring to a non-existent index
886        let rewritten_indices = vec![RewrittenIndex {
887            old_id: Uuid::new_v4(),
888            new_id: Uuid::new_v4(),
889            new_index_details: prost_types::Any {
890                type_url: String::new(),
891                value: vec![],
892            },
893            new_index_version: 1,
894            new_index_files: None,
895        }];
896
897        // Should succeed (skip missing index) instead of error
898        let result = Transaction::handle_rewrite_indices(&mut indices, &rewritten_indices, &[]);
899        assert!(result.is_ok());
900        assert!(indices.is_empty());
901    }
902
903    #[test]
904    fn test_prune_overlay_stale_fields_from_indices() {
905        // Fragment 0 carried an overlay on field 1 committed at v5, and was
906        // fully compacted into new fragment 7.
907        let mut old_frag = Fragment::new(0);
908        old_frag.overlays = vec![overlay_with_field(1, 5)];
909        let groups = vec![RewriteGroup {
910            old_fragments: vec![old_frag],
911            new_fragments: vec![Fragment::new(7)],
912        }];
913
914        // Post-remap state: every index already covers the new fragment (7).
915        let covering = || Some(RoaringBitmap::from_iter([7u32]));
916        let mut indices = vec![
917            // Stale: covers the overlaid field 1, built (v2) before the overlay.
918            create_test_index("stale", 1, 2, covering(), false),
919            // Not stale: covers field 1 but built at the overlay's version (v5);
920            // `committed_version > dataset_version` is false at equality.
921            create_test_index("fresh", 1, 5, covering(), false),
922            // Unrelated: covers field 2, which the overlay never touched.
923            create_test_index("unrelated", 2, 2, covering(), false),
924        ];
925
926        Transaction::prune_overlay_stale_fields_from_indices(&mut indices, &groups);
927
928        assert!(
929            !indices[0].fragment_bitmap.as_ref().unwrap().contains(7),
930            "stale index must drop the rewritten fragment from its coverage"
931        );
932        assert!(
933            indices[1].fragment_bitmap.as_ref().unwrap().contains(7),
934            "an index built at/after the overlay is not stale"
935        );
936        assert!(
937            indices[2].fragment_bitmap.as_ref().unwrap().contains(7),
938            "an index on an un-overlaid field is unaffected"
939        );
940    }
941
942    // Helper functions for retain_relevant_indices tests
943    fn create_test_index(
944        name: &str,
945        field_id: i32,
946        dataset_version: u64,
947        fragment_bitmap: Option<RoaringBitmap>,
948        is_vector: bool,
949    ) -> IndexMetadata {
950        use prost_types::Any;
951        use std::sync::Arc;
952
953        let index_details = if is_vector {
954            Some(Arc::new(Any {
955                type_url: "type.googleapis.com/lance.index.VectorIndexDetails".to_string(),
956                value: vec![],
957            }))
958        } else {
959            Some(Arc::new(Any {
960                type_url: "type.googleapis.com/lance.index.ScalarIndexDetails".to_string(),
961                value: vec![],
962            }))
963        };
964
965        IndexMetadata {
966            uuid: Uuid::new_v4(),
967            fields: vec![field_id],
968            covering_fields: vec![],
969            name: name.to_string(),
970            dataset_version,
971            fragment_bitmap,
972            index_details,
973            index_version: 1,
974            created_at: None,
975            base_id: None,
976            files: None,
977        }
978    }
979
980    fn create_system_index(name: &str, field_id: i32) -> IndexMetadata {
981        use prost_types::Any;
982        use std::sync::Arc;
983
984        IndexMetadata {
985            uuid: Uuid::new_v4(),
986            fields: vec![field_id],
987            covering_fields: vec![],
988            name: name.to_string(),
989            dataset_version: 1,
990            fragment_bitmap: Some(RoaringBitmap::from_iter([1, 2])),
991            index_details: Some(Arc::new(Any {
992                type_url: "type.googleapis.com/lance.index.SystemIndexDetails".to_string(),
993                value: vec![],
994            })),
995            index_version: 1,
996            created_at: None,
997            base_id: None,
998            files: None,
999        }
1000    }
1001
1002    fn create_test_schema(field_ids: &[i32]) -> Schema {
1003        use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
1004        use lance_core::datatypes::Schema as LanceSchema;
1005
1006        let fields: Vec<ArrowField> = field_ids
1007            .iter()
1008            .map(|id| ArrowField::new(format!("field_{}", id), DataType::Int32, false))
1009            .collect();
1010
1011        let arrow_schema = ArrowSchema::new(fields);
1012        let mut lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
1013
1014        // Assign field IDs
1015        for (i, field_id) in field_ids.iter().enumerate() {
1016            lance_schema.mut_field_by_id(i as i32).unwrap().id = *field_id;
1017        }
1018
1019        lance_schema
1020    }
1021}