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            // Unknown coverage is not empty coverage: a segment whose bitmap is
261            // missing has never been measured, and dropping it deletes an index
262            // that migration could not open yet.
263            let (unknown_coverage, same_name_indices): (Vec<_>, Vec<_>) = same_name_indices
264                .into_iter()
265                .partition(|index| index.fragment_bitmap.is_none());
266            for index in unknown_coverage {
267                uuids_to_keep.insert(index.uuid);
268            }
269
270            if same_name_indices.len() > 1 {
271                let (empty_indices, non_empty_indices): (Vec<_>, Vec<_>) =
272                    same_name_indices.iter().partition(|index| {
273                        index
274                            .effective_fragment_bitmap(&existing_fragments)
275                            .as_ref()
276                            .is_none_or(|bitmap| bitmap.is_empty())
277                    });
278
279                if non_empty_indices.is_empty() {
280                    // All indices are empty -- keep only the oldest definition.
281                    //
282                    // An empty index definition is still correct: the scanner
283                    // falls back to scanning unindexed fragments, and normal
284                    // index maintenance rebuilds coverage once rows accrue.
285                    // Dropping the definition instead would silently lose the
286                    // index whenever an operation replaces every fragment it
287                    // covered (e.g. a full table rewrite), leaving the dataset
288                    // without its declared index.
289                    let mut sorted_indices = empty_indices;
290                    sorted_indices.sort_by_key(|index: &&IndexMetadata| index.dataset_version);
291
292                    if let Some(oldest) = sorted_indices.first() {
293                        uuids_to_keep.insert(oldest.uuid);
294                    }
295                } else {
296                    for index in non_empty_indices {
297                        uuids_to_keep.insert(index.uuid);
298                    }
299                }
300            } else {
301                // Single index whose column is still in schema: keep it, even
302                // when its coverage is empty (see the all-empty note above).
303                if let Some(index) = same_name_indices.first() {
304                    uuids_to_keep.insert(index.uuid);
305                }
306            }
307        }
308
309        indices.retain(|index| {
310            index.name == FRAG_REUSE_INDEX_NAME || uuids_to_keep.contains(&index.uuid)
311        });
312    }
313
314    pub(super) fn recalculate_fragment_bitmap(
315        old: &RoaringBitmap,
316        groups: &[RewriteGroup],
317    ) -> Result<RoaringBitmap> {
318        let mut new_bitmap = old.clone();
319        for group in groups {
320            let any_in_index = group
321                .old_fragments
322                .iter()
323                .any(|frag| old.contains(frag.id as u32));
324            let all_in_index = group
325                .old_fragments
326                .iter()
327                .all(|frag| old.contains(frag.id as u32));
328            // Any rewrite group may or may not be covered by the index.  However, if any fragment
329            // in a rewrite group was previously covered by the index then all fragments in the rewrite
330            // group must have been previously covered by the index.  plan_compaction takes care of
331            // this for us so this should be safe to assume.
332            if any_in_index {
333                if all_in_index {
334                    for frag_id in group.old_fragments.iter().map(|frag| frag.id as u32) {
335                        new_bitmap.remove(frag_id);
336                    }
337                    new_bitmap.extend(group.new_fragments.iter().map(|frag| frag.id as u32));
338                } else {
339                    return Err(Error::invalid_input(
340                        "The compaction plan included a rewrite group that was a split of indexed and non-indexed data",
341                    ));
342                }
343            }
344        }
345        Ok(new_bitmap)
346    }
347
348    pub(super) fn handle_rewrite_indices(
349        indices: &mut [IndexMetadata],
350        rewritten_indices: &[RewrittenIndex],
351        groups: &[RewriteGroup],
352    ) -> Result<()> {
353        let mut modified_indices = HashSet::new();
354
355        for rewritten_index in rewritten_indices {
356            if !modified_indices.insert(rewritten_index.old_id) {
357                return Err(Error::invalid_input(format!(
358                    "An invalid compaction plan must have been generated because multiple tasks modified the same index: {}",
359                    rewritten_index.old_id
360                )));
361            }
362
363            // Skip indices that no longer exist (may have been removed by concurrent operation)
364            let Some(index) = indices
365                .iter_mut()
366                .find(|idx| idx.uuid == rewritten_index.old_id)
367            else {
368                continue;
369            };
370
371            index.fragment_bitmap = Some(Self::recalculate_fragment_bitmap(
372                index.fragment_bitmap.as_ref().ok_or_else(|| {
373                    Error::invalid_input(format!(
374                        "Cannot rewrite index {} which did not store fragment bitmap",
375                        index.uuid
376                    ))
377                })?,
378                groups,
379            )?);
380            index.uuid = rewritten_index.new_id;
381            // Update file sizes to match the new index files. When not available
382            // (e.g., from older writers), clear the old file sizes to avoid
383            // using stale sizes from the pre-remap index.
384            index.files = rewritten_index.new_index_files.clone();
385        }
386        Ok(())
387    }
388
389    pub(super) fn handle_rewrite_fragments(
390        final_fragments: &mut Vec<Fragment>,
391        groups: &[RewriteGroup],
392        fragment_id: &mut u64,
393        version: u64,
394        _next_row_id: Option<&u64>,
395    ) -> Result<()> {
396        for group in groups {
397            // If the old fragments are contiguous, find the range
398            let replace_range = {
399                let start = final_fragments
400                    .iter()
401                    .enumerate()
402                    .find(|(_, f)| f.id == group.old_fragments[0].id)
403                    .ok_or_else(|| {
404                        Error::commit_conflict_source(
405                            version,
406                            format!(
407                                "dataset does not contain a fragment a rewrite operation wants to replace: id={}",
408                                group.old_fragments[0].id
409                            )
410                            .into(),
411                        )
412                    })?
413                    .0;
414
415                // Verify old_fragments matches contiguous range
416                let mut i = 1;
417                loop {
418                    if i == group.old_fragments.len() {
419                        break Some(start..start + i);
420                    }
421                    if final_fragments[start + i].id != group.old_fragments[i].id {
422                        break None;
423                    }
424                    i += 1;
425                }
426            };
427
428            let new_fragments = Self::fragments_with_ids(group.new_fragments.clone(), fragment_id)
429                .collect::<Vec<_>>();
430
431            // Version metadata for rewritten fragments is handled by the compaction code
432            // (recalc_versions_for_rewritten_fragments) which preserves version information
433            // from the original fragments. We don't modify it here.
434
435            if let Some(replace_range) = replace_range {
436                // Efficiently path using slice
437                final_fragments.splice(replace_range, new_fragments);
438            } else {
439                // Slower path for non-contiguous ranges
440                for fragment in group.old_fragments.iter() {
441                    final_fragments.retain(|f| f.id != fragment.id);
442                }
443                final_fragments.extend(new_fragments);
444            }
445        }
446        Ok(())
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use crate::transaction::test_support::overlay_with_field;
454    use uuid::Uuid;
455
456    #[test]
457    fn test_rewrite_fragments() {
458        let existing_fragments: Vec<Fragment> = (0..10).map(Fragment::new).collect();
459
460        let mut final_fragments = existing_fragments;
461        let rewrite_groups = vec![
462            // Since these are contiguous, they will be put in the same location
463            // as 1 and 2.
464            RewriteGroup {
465                old_fragments: vec![Fragment::new(1), Fragment::new(2)],
466                // These two fragments were previously reserved
467                new_fragments: vec![Fragment::new(15), Fragment::new(16)],
468            },
469            // These are not contiguous, so they will be inserted at the end.
470            RewriteGroup {
471                old_fragments: vec![Fragment::new(5), Fragment::new(8)],
472                // We pretend this id was not reserved.  Does not happen in practice today
473                // but we want to leave the door open.
474                new_fragments: vec![Fragment::new(0)],
475            },
476        ];
477
478        let mut fragment_id = 20;
479        let version = 0;
480
481        Transaction::handle_rewrite_fragments(
482            &mut final_fragments,
483            &rewrite_groups,
484            &mut fragment_id,
485            version,
486            None,
487        )
488        .unwrap();
489
490        assert_eq!(fragment_id, 21);
491
492        let expected_fragments: Vec<Fragment> = vec![
493            Fragment::new(0),
494            Fragment::new(15),
495            Fragment::new(16),
496            Fragment::new(3),
497            Fragment::new(4),
498            Fragment::new(6),
499            Fragment::new(7),
500            Fragment::new(9),
501            Fragment::new(20),
502        ];
503
504        assert_eq!(final_fragments, expected_fragments);
505    }
506
507    #[test]
508    fn test_retain_indices_removes_missing_fields() {
509        let schema = create_test_schema(&[1, 2]);
510        let fragments = vec![Fragment::new(1), Fragment::new(2)];
511
512        let mut indices = vec![
513            create_test_index("idx1", 1, 1, Some(RoaringBitmap::from_iter([1])), false),
514            create_test_index("idx2", 2, 1, Some(RoaringBitmap::from_iter([1])), false),
515            create_test_index("idx3", 99, 1, Some(RoaringBitmap::from_iter([1])), false), // Field doesn't exist
516        ];
517
518        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
519
520        assert_eq!(indices.len(), 2);
521        assert!(indices.iter().all(|idx| idx.fields[0] != 99));
522    }
523
524    #[test]
525    fn test_retain_indices_keeps_system_indices() {
526        use crate::system_index::mem_wal::MEM_WAL_INDEX_NAME;
527
528        let schema = create_test_schema(&[1, 2]);
529        let fragments = vec![Fragment::new(1)];
530
531        let mut indices = vec![
532            create_system_index(FRAG_REUSE_INDEX_NAME, 99), // Field doesn't exist but should be kept
533            create_system_index(MEM_WAL_INDEX_NAME, 99), // Field doesn't exist but should be kept
534            create_test_index("regular_idx", 99, 1, Some(RoaringBitmap::new()), false), // Should be removed
535        ];
536
537        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
538
539        assert_eq!(indices.len(), 2);
540        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
541        assert!(indices.iter().any(|idx| idx.name == MEM_WAL_INDEX_NAME));
542    }
543
544    #[test]
545    fn test_retain_indices_keeps_fragment_reuse_index() {
546        let schema = create_test_schema(&[1]);
547        let fragments = vec![Fragment::new(1)];
548
549        let mut indices = vec![
550            create_system_index(FRAG_REUSE_INDEX_NAME, 1),
551            create_test_index("other_idx", 1, 1, Some(RoaringBitmap::new()), false),
552        ];
553
554        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
555
556        // Fragment reuse index should always be kept
557        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
558    }
559
560    #[test]
561    fn test_retain_single_empty_scalar_index() {
562        let schema = create_test_schema(&[1]);
563        let fragments = vec![Fragment::new(1)];
564
565        let mut indices = vec![create_test_index(
566            "scalar_idx",
567            1,
568            1,
569            Some(RoaringBitmap::new()), // Empty bitmap
570            false,
571        )];
572
573        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
574
575        // Single empty scalar index should be kept
576        assert_eq!(indices.len(), 1);
577    }
578
579    #[test]
580    fn test_retain_single_empty_vector_index_is_kept() {
581        let schema = create_test_schema(&[1]);
582        let fragments = vec![Fragment::new(1)];
583
584        let mut indices = vec![create_test_index(
585            "vector_idx",
586            1,
587            1,
588            Some(RoaringBitmap::new()), // Empty bitmap
589            true,
590        )];
591
592        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
593
594        // The empty definition is retained: coverage is empty but the index
595        // declaration must survive operations that replace every fragment.
596        assert_eq!(indices.len(), 1);
597    }
598
599    #[test]
600    fn test_retain_single_nonempty_index() {
601        let schema = create_test_schema(&[1]);
602        let fragments = vec![Fragment::new(1)];
603
604        let mut scalar_indices = vec![create_test_index(
605            "scalar_idx",
606            1,
607            1,
608            Some(RoaringBitmap::from_iter([1])),
609            false,
610        )];
611
612        let mut vector_indices = vec![create_test_index(
613            "vector_idx",
614            1,
615            1,
616            Some(RoaringBitmap::from_iter([1])),
617            true,
618        )];
619
620        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
621        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
622
623        // Both should be kept
624        assert_eq!(scalar_indices.len(), 1);
625        assert_eq!(vector_indices.len(), 1);
626    }
627
628    #[test]
629    fn test_retain_single_index_with_none_bitmap() {
630        let schema = create_test_schema(&[1]);
631        let fragments = vec![Fragment::new(1)];
632
633        let mut scalar_indices = vec![create_test_index("scalar_idx", 1, 1, None, false)];
634        let mut vector_indices = vec![create_test_index("vector_idx", 1, 1, None, true)];
635
636        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
637        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
638
639        // Both kept: a None bitmap is unknown coverage, not empty coverage, and
640        // an unmeasured segment is retained regardless of index type.
641        assert_eq!(scalar_indices.len(), 1);
642        assert_eq!(vector_indices.len(), 1);
643    }
644
645    #[test]
646    fn test_retain_unknown_coverage_alongside_nonempty_sibling() {
647        let schema = create_test_schema(&[1]);
648        let fragments = vec![Fragment::new(1), Fragment::new(2)];
649
650        let mut indices = vec![
651            create_test_index("idx", 1, 1, None, false), // Coverage never measured
652            create_test_index("idx", 1, 2, Some(RoaringBitmap::from_iter([2])), false),
653        ];
654
655        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
656
657        // The unmeasured segment must survive its non-empty sibling: its bitmap
658        // is missing because migration could not open the index, and deleting
659        // the segment would take the only record of it with it.
660        assert_eq!(indices.len(), 2);
661        assert!(indices.iter().any(|idx| idx.fragment_bitmap.is_none()));
662    }
663
664    #[test]
665    fn test_retain_multiple_empty_scalar_indices_keeps_oldest() {
666        let schema = create_test_schema(&[1]);
667        let fragments = vec![Fragment::new(1)];
668
669        let mut indices = vec![
670            create_test_index("idx", 1, 3, Some(RoaringBitmap::new()), false),
671            create_test_index("idx", 1, 1, Some(RoaringBitmap::new()), false), // Oldest
672            create_test_index("idx", 1, 2, Some(RoaringBitmap::new()), false),
673        ];
674
675        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
676
677        // Should keep only the oldest (dataset_version = 1)
678        assert_eq!(indices.len(), 1);
679        assert_eq!(indices[0].dataset_version, 1);
680    }
681
682    #[test]
683    fn test_retain_multiple_empty_vector_indices_keeps_oldest() {
684        let schema = create_test_schema(&[1]);
685        let fragments = vec![Fragment::new(1)];
686
687        let mut indices = vec![
688            create_test_index("vec_idx", 1, 1, Some(RoaringBitmap::new()), true),
689            create_test_index("vec_idx", 1, 2, Some(RoaringBitmap::new()), true),
690            create_test_index("vec_idx", 1, 3, Some(RoaringBitmap::new()), true),
691        ];
692
693        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
694
695        // Same as the scalar case: all deltas are empty, so only the oldest
696        // definition survives.
697        assert_eq!(indices.len(), 1);
698        assert_eq!(indices[0].dataset_version, 1);
699    }
700
701    #[test]
702    fn test_retain_mixed_empty_nonempty_keeps_nonempty() {
703        let schema = create_test_schema(&[1]);
704        let fragments = vec![Fragment::new(1)];
705
706        let mut indices = vec![
707            create_test_index("idx", 1, 1, Some(RoaringBitmap::new()), false), // Empty
708            create_test_index("idx", 1, 2, Some(RoaringBitmap::from_iter([1])), false), // Non-empty
709            create_test_index("idx", 1, 3, Some(RoaringBitmap::new()), false), // Empty
710            create_test_index("idx", 1, 4, Some(RoaringBitmap::from_iter([1])), false), // Non-empty
711        ];
712
713        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
714
715        // Should keep only non-empty indices
716        assert_eq!(indices.len(), 2);
717        assert!(
718            indices
719                .iter()
720                .all(|idx| idx.dataset_version == 2 || idx.dataset_version == 4)
721        );
722    }
723
724    #[test]
725    fn test_retain_mixed_empty_nonempty_vector_keeps_nonempty() {
726        let schema = create_test_schema(&[1]);
727        let fragments = vec![Fragment::new(1)];
728
729        let mut indices = vec![
730            create_test_index("vec_idx", 1, 1, Some(RoaringBitmap::new()), true), // Empty
731            create_test_index("vec_idx", 1, 2, Some(RoaringBitmap::from_iter([1])), true), // Non-empty
732            create_test_index("vec_idx", 1, 3, Some(RoaringBitmap::new()), true),          // Empty
733        ];
734
735        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
736
737        // Should keep only non-empty index
738        assert_eq!(indices.len(), 1);
739        assert_eq!(indices[0].dataset_version, 2);
740    }
741
742    #[test]
743    fn test_retain_fragment_bitmap_with_nonexistent_fragments() {
744        let schema = create_test_schema(&[1]);
745        let fragments = vec![Fragment::new(1), Fragment::new(2)]; // Only fragments 1 and 2 exist
746
747        let mut indices = vec![create_test_index(
748            "idx",
749            1,
750            1,
751            Some(RoaringBitmap::from_iter([1, 2, 3, 4])), // References non-existent fragments 3, 4
752            false,
753        )];
754
755        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
756
757        // Should still keep the index (effective bitmap will be intersection with existing)
758        assert_eq!(indices.len(), 1);
759        // Original bitmap should be unchanged
760        assert_eq!(
761            indices[0].fragment_bitmap.as_ref().unwrap(),
762            &RoaringBitmap::from_iter([1, 2, 3, 4])
763        );
764    }
765
766    #[test]
767    fn test_retain_effective_empty_bitmap_single_index() {
768        let schema = create_test_schema(&[1]);
769        let fragments = vec![Fragment::new(5), Fragment::new(6)];
770
771        // Bitmap references fragments that don't exist, so effective bitmap is empty
772        let mut scalar_indices = vec![create_test_index(
773            "scalar_idx",
774            1,
775            1,
776            Some(RoaringBitmap::from_iter([1, 2, 3])),
777            false,
778        )];
779
780        let mut vector_indices = vec![create_test_index(
781            "vector_idx",
782            1,
783            1,
784            Some(RoaringBitmap::from_iter([1, 2, 3])),
785            true,
786        )];
787
788        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
789        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
790
791        // Both kept: a single index whose column is still in schema is
792        // retained even when its effective coverage is empty.
793        assert_eq!(scalar_indices.len(), 1);
794        assert_eq!(vector_indices.len(), 1);
795    }
796
797    #[test]
798    fn test_retain_different_index_names() {
799        let schema = create_test_schema(&[1]);
800        let fragments = vec![Fragment::new(1)];
801
802        let mut indices = vec![
803            create_test_index("idx_a", 1, 1, Some(RoaringBitmap::new()), false),
804            create_test_index("idx_b", 1, 1, Some(RoaringBitmap::new()), true),
805            create_test_index("idx_c", 1, 1, Some(RoaringBitmap::from_iter([1])), false),
806        ];
807
808        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
809
810        // All three kept: empty definitions are retained for scalar and
811        // vector indexes alike.
812        assert_eq!(indices.len(), 3);
813        assert!(indices.iter().any(|idx| idx.name == "idx_a"));
814        assert!(indices.iter().any(|idx| idx.name == "idx_b"));
815        assert!(indices.iter().any(|idx| idx.name == "idx_c"));
816    }
817
818    #[test]
819    fn test_retain_empty_indices_vec() {
820        let schema = create_test_schema(&[1]);
821        let fragments = vec![Fragment::new(1)];
822
823        let mut indices: Vec<IndexMetadata> = vec![];
824
825        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
826
827        assert_eq!(indices.len(), 0);
828    }
829
830    #[test]
831    fn test_retain_all_indices_removed() {
832        let schema = create_test_schema(&[1]);
833        let fragments = vec![Fragment::new(1)];
834
835        let mut indices = vec![
836            create_test_index("vec1", 1, 1, Some(RoaringBitmap::new()), true),
837            create_test_index("vec2", 1, 1, Some(RoaringBitmap::new()), true),
838            create_test_index("idx3", 99, 1, Some(RoaringBitmap::from_iter([1])), false), // Bad field
839        ];
840
841        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
842
843        // Only the bad-field index is dropped; the empty vector definitions
844        // are retained.
845        assert_eq!(indices.len(), 2);
846        assert!(!indices.iter().any(|idx| idx.name == "idx3"));
847    }
848
849    #[test]
850    fn test_retain_complex_scenario() {
851        let schema = create_test_schema(&[1, 2]);
852        let fragments = vec![Fragment::new(1), Fragment::new(2)];
853
854        let mut indices = vec![
855            // System index - should always be kept
856            create_system_index(FRAG_REUSE_INDEX_NAME, 1),
857            // Group "idx_a" - all empty scalars, keep oldest
858            create_test_index("idx_a", 1, 3, Some(RoaringBitmap::new()), false),
859            create_test_index("idx_a", 1, 1, Some(RoaringBitmap::new()), false), // Oldest
860            create_test_index("idx_a", 1, 2, Some(RoaringBitmap::new()), false),
861            // Group "vec_b" - all empty vectors, keep oldest definition
862            create_test_index("vec_b", 1, 1, Some(RoaringBitmap::new()), true),
863            create_test_index("vec_b", 1, 2, Some(RoaringBitmap::new()), true),
864            // Group "idx_c" - mixed empty/non-empty, keep non-empty
865            create_test_index("idx_c", 2, 1, Some(RoaringBitmap::new()), false),
866            create_test_index("idx_c", 2, 2, Some(RoaringBitmap::from_iter([1])), false), // Keep
867            create_test_index("idx_c", 2, 3, Some(RoaringBitmap::from_iter([2])), false), // Keep
868            // Single non-empty - keep
869            create_test_index("idx_d", 1, 1, Some(RoaringBitmap::from_iter([1, 2])), false),
870            // Index with bad field - remove
871            create_test_index("idx_e", 99, 1, Some(RoaringBitmap::from_iter([1])), false),
872        ];
873
874        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
875
876        // Expected: frag_reuse, idx_a (oldest), vec_b (oldest), idx_c (2
877        // non-empty), idx_d = 6 total
878        assert_eq!(indices.len(), 6);
879
880        // Verify system index kept
881        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
882
883        // Verify idx_a kept oldest only
884        let idx_a_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "idx_a").collect();
885        assert_eq!(idx_a_indices.len(), 1);
886        assert_eq!(idx_a_indices[0].dataset_version, 1);
887
888        // Verify vec_b kept oldest definition only
889        let vec_b_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "vec_b").collect();
890        assert_eq!(vec_b_indices.len(), 1);
891        assert_eq!(vec_b_indices[0].dataset_version, 1);
892
893        // Verify idx_c kept non-empty only
894        let idx_c_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "idx_c").collect();
895        assert_eq!(idx_c_indices.len(), 2);
896        assert!(
897            idx_c_indices
898                .iter()
899                .all(|idx| idx.dataset_version == 2 || idx.dataset_version == 3)
900        );
901
902        // Verify idx_d kept
903        assert!(indices.iter().any(|idx| idx.name == "idx_d"));
904
905        // Verify idx_e removed (bad field)
906        assert!(!indices.iter().any(|idx| idx.name == "idx_e"));
907    }
908
909    #[test]
910    fn test_handle_rewrite_indices_skips_missing_index() {
911        // Create an empty indices list
912        let mut indices = vec![];
913
914        // Create rewritten_indices referring to a non-existent index
915        let rewritten_indices = vec![RewrittenIndex {
916            old_id: Uuid::new_v4(),
917            new_id: Uuid::new_v4(),
918            new_index_details: prost_types::Any {
919                type_url: String::new(),
920                value: vec![],
921            },
922            new_index_version: 1,
923            new_index_files: None,
924        }];
925
926        // Should succeed (skip missing index) instead of error
927        let result = Transaction::handle_rewrite_indices(&mut indices, &rewritten_indices, &[]);
928        assert!(result.is_ok());
929        assert!(indices.is_empty());
930    }
931
932    #[test]
933    fn test_prune_overlay_stale_fields_from_indices() {
934        // Fragment 0 carried an overlay on field 1 committed at v5, and was
935        // fully compacted into new fragment 7.
936        let mut old_frag = Fragment::new(0);
937        old_frag.overlays = vec![overlay_with_field(1, 5)];
938        let groups = vec![RewriteGroup {
939            old_fragments: vec![old_frag],
940            new_fragments: vec![Fragment::new(7)],
941        }];
942
943        // Post-remap state: every index already covers the new fragment (7).
944        let covering = || Some(RoaringBitmap::from_iter([7u32]));
945        let mut indices = vec![
946            // Stale: covers the overlaid field 1, built (v2) before the overlay.
947            create_test_index("stale", 1, 2, covering(), false),
948            // Not stale: covers field 1 but built at the overlay's version (v5);
949            // `committed_version > dataset_version` is false at equality.
950            create_test_index("fresh", 1, 5, covering(), false),
951            // Unrelated: covers field 2, which the overlay never touched.
952            create_test_index("unrelated", 2, 2, covering(), false),
953        ];
954
955        Transaction::prune_overlay_stale_fields_from_indices(&mut indices, &groups);
956
957        assert!(
958            !indices[0].fragment_bitmap.as_ref().unwrap().contains(7),
959            "stale index must drop the rewritten fragment from its coverage"
960        );
961        assert!(
962            indices[1].fragment_bitmap.as_ref().unwrap().contains(7),
963            "an index built at/after the overlay is not stale"
964        );
965        assert!(
966            indices[2].fragment_bitmap.as_ref().unwrap().contains(7),
967            "an index on an un-overlaid field is unaffected"
968        );
969    }
970
971    // Helper functions for retain_relevant_indices tests
972    fn create_test_index(
973        name: &str,
974        field_id: i32,
975        dataset_version: u64,
976        fragment_bitmap: Option<RoaringBitmap>,
977        is_vector: bool,
978    ) -> IndexMetadata {
979        use prost_types::Any;
980        use std::sync::Arc;
981
982        let index_details = if is_vector {
983            Some(Arc::new(Any {
984                type_url: "type.googleapis.com/lance.index.VectorIndexDetails".to_string(),
985                value: vec![],
986            }))
987        } else {
988            Some(Arc::new(Any {
989                type_url: "type.googleapis.com/lance.index.ScalarIndexDetails".to_string(),
990                value: vec![],
991            }))
992        };
993
994        IndexMetadata {
995            uuid: Uuid::new_v4(),
996            fields: vec![field_id],
997            covering_fields: vec![],
998            name: name.to_string(),
999            dataset_version,
1000            fragment_bitmap,
1001            index_details,
1002            index_version: 1,
1003            created_at: None,
1004            base_id: None,
1005            files: None,
1006        }
1007    }
1008
1009    fn create_system_index(name: &str, field_id: i32) -> IndexMetadata {
1010        use prost_types::Any;
1011        use std::sync::Arc;
1012
1013        IndexMetadata {
1014            uuid: Uuid::new_v4(),
1015            fields: vec![field_id],
1016            covering_fields: vec![],
1017            name: name.to_string(),
1018            dataset_version: 1,
1019            fragment_bitmap: Some(RoaringBitmap::from_iter([1, 2])),
1020            index_details: Some(Arc::new(Any {
1021                type_url: "type.googleapis.com/lance.index.SystemIndexDetails".to_string(),
1022                value: vec![],
1023            })),
1024            index_version: 1,
1025            created_at: None,
1026            base_id: None,
1027            files: None,
1028        }
1029    }
1030
1031    fn create_test_schema(field_ids: &[i32]) -> Schema {
1032        use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
1033        use lance_core::datatypes::Schema as LanceSchema;
1034
1035        let fields: Vec<ArrowField> = field_ids
1036            .iter()
1037            .map(|id| ArrowField::new(format!("field_{}", id), DataType::Int32, false))
1038            .collect();
1039
1040        let arrow_schema = ArrowSchema::new(fields);
1041        let mut lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
1042
1043        // Assign field IDs
1044        for (i, field_id) in field_ids.iter().enumerate() {
1045            lance_schema.mut_field_by_id(i as i32).unwrap().id = *field_id;
1046        }
1047
1048        lance_schema
1049    }
1050}