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::frag_reuse::lineage::TaggedLineage;
16use crate::system_index::frag_reuse::metadata::is_tagged;
17use crate::system_index::is_system_index;
18use crate::transaction::{
19    DataReplacementGroup, Operation, RewriteGroup, RewrittenIndex, Transaction,
20};
21use lance_core::datatypes::Schema;
22use lance_core::{Error, Result};
23use roaring::RoaringBitmap;
24use std::collections::{HashMap, HashSet};
25use uuid::Uuid;
26
27impl Transaction {
28    pub(super) fn register_pure_rewrite_rows_update_frags_in_indices(
29        indices: &mut [IndexMetadata],
30        pure_update_frag_ids: &[u64],
31        original_fragment_ids: &[u64],
32        fields_for_preserving_frag_bitmap: &[u32],
33        original_overlaid_frags: &HashMap<u32, &Fragment>,
34        schema: &Schema,
35    ) -> Result<()> {
36        if pure_update_frag_ids.is_empty() {
37            return Ok(());
38        }
39
40        let value_updated_field_set = fields_for_preserving_frag_bitmap
41            .iter()
42            .collect::<HashSet<_>>();
43
44        for index in indices.iter_mut() {
45            // Physical row addresses cannot follow moved rows into a new fragment.
46            // Leave that fragment uncovered so the scanner reads it directly.
47            if index.results_are_row_addrs() {
48                continue;
49            }
50            let index_covers_modified_field = index.fields.iter().any(|field_id| {
51                value_updated_field_set.contains(&u32::try_from(*field_id).unwrap())
52            });
53            if index_covers_modified_field {
54                continue;
55            }
56            let Some(fragment_bitmap) = index.fragment_bitmap.as_ref() else {
57                continue;
58            };
59
60            // Check that all the original fragments containing the updated rows are covered by
61            // the index. If not, some updated rows were not indexed, so we cannot index them.
62            let index_covers_all_original_fragments = original_fragment_ids
63                .iter()
64                .all(|&fragment_id| fragment_bitmap.contains(fragment_id as u32));
65            if !index_covers_all_original_fragments {
66                continue;
67            }
68
69            // A rewrite materializes overlays.  If any of those overlays touched the
70            // column being indexed then the rewrite will modify that column.  As a
71            // result, that index will no longer cover the fragment and it does not
72            // count as a pure rewrite and we must exclude it from the index's fragment
73            // bitmap.
74            let mut overlay_stale = RoaringBitmap::new();
75            collect_overlay_stale_frags(
76                index,
77                original_overlaid_frags,
78                &mut overlay_stale,
79                schema,
80            )?;
81            if !overlay_stale.is_empty() {
82                continue;
83            }
84
85            if let Some(fragment_bitmap) = index.fragment_bitmap.as_mut() {
86                for fragment_id in pure_update_frag_ids.iter().map(|f| *f as u32) {
87                    fragment_bitmap.insert(fragment_id);
88                }
89            }
90        }
91        Ok(())
92    }
93
94    /// Withdraw what an in-place column rewrite invalidates from every index
95    /// on a table with a tagged fragment reuse history.
96    ///
97    /// The physical columns `operation` rewrites in place, per live fragment
98    /// id, unexpanded: an update with `fields_modified` rewrites those
99    /// fields in every updated fragment; a merge rewrites, in each fragment
100    /// present in `previous_fragments`, the fields whose backing data file
101    /// changed (`merge_rewritten_fields`); a data replacement rewrites the
102    /// fields its new files carry, read through `schema`. Any other
103    /// operation rewrites nothing. `previous_fragments` is the caller's
104    /// "before" list: the current manifest's for a commit, the read
105    /// version's for a rebase.
106    pub fn rewritten_physical_columns(
107        operation: &Operation,
108        schema: &Schema,
109        previous_fragments: &[Fragment],
110    ) -> Vec<(u64, Vec<u32>)> {
111        match operation {
112            Operation::Update {
113                updated_fragments,
114                fields_modified,
115                ..
116            } if !fields_modified.is_empty() => updated_fragments
117                .iter()
118                .map(|fragment| (fragment.id, fields_modified.clone()))
119                .collect(),
120            Operation::Merge { fragments, .. } => {
121                Self::merge_rewritten_fields(previous_fragments, fragments)
122            }
123            Operation::DataReplacement { replacements } => replacements
124                .iter()
125                .map(|DataReplacementGroup(fragment_id, new_file)| {
126                    let mut fields: Vec<u32> = new_file
127                        .schema(schema)
128                        .field_ids()
129                        .into_iter()
130                        .chain(new_file.fields.iter().copied())
131                        .filter_map(|id| u32::try_from(id).ok())
132                        .collect();
133                    fields.sort_unstable();
134                    fields.dedup();
135                    (*fragment_id, fields)
136                })
137                .collect(),
138            _ => Vec::new(),
139        }
140    }
141
142    /// The one implementation of an in-place column rewrite's effect on
143    /// index coverage, shared by the commit preparation (on the list read
144    /// from the current manifest) and by the conflict resolver rebasing a
145    /// new index over a committed rewrite (on the index being created).
146    ///
147    /// `rewrites` comes from [`Self::rewritten_physical_columns`]. With a
148    /// walkable tagged history (`lineage`), every rewritten field id is
149    /// expanded to its descendants through `schema` (a packed struct is one
150    /// physical column while an index on `s.x` records the child's id) and
151    /// the coverage is withdrawn along the lineage
152    /// ([`Self::withdraw_rewritten_coverage`]). Without a tagged history the
153    /// rewritten fragments are pruned from the covering indices by id, the
154    /// ordinary rule, exactly as before.
155    pub fn withdraw_in_place_rewrites(
156        indices: &mut Vec<IndexMetadata>,
157        rewrites: Vec<(u64, Vec<u32>)>,
158        schema: &Schema,
159        live: &RoaringBitmap,
160        lineage: Option<&TaggedLineage>,
161    ) {
162        if rewrites.is_empty() {
163            return;
164        }
165        match lineage {
166            Some(lineage) => {
167                let rewrites = Self::expand_rewritten_fields(schema, rewrites);
168                Self::withdraw_rewritten_coverage(indices, Some(lineage), live, &rewrites);
169            }
170            None => {
171                for (fragment, fields) in rewrites {
172                    Self::prune_updated_fields_from_indices(
173                        indices,
174                        &[Fragment::new(fragment)],
175                        &fields,
176                    );
177                }
178            }
179        }
180    }
181
182    /// Expand every rewritten field id in `rewrites` to the field and all
183    /// its descendants through `schema`, the form
184    /// `withdraw_rewritten_coverage` compares against index fields.
185    fn expand_rewritten_fields(
186        schema: &Schema,
187        rewrites: Vec<(u64, Vec<u32>)>,
188    ) -> Vec<(u64, Vec<u32>)> {
189        rewrites
190            .into_iter()
191            .map(|(fragment, fields)| (fragment, Self::with_descendants(schema, fields)))
192            .collect()
193    }
194
195    /// `fields` plus every field nested under them, deduplicated.
196    pub fn with_descendants(schema: &Schema, fields: Vec<u32>) -> Vec<u32> {
197        let mut expanded = Vec::with_capacity(fields.len());
198        for id in fields {
199            expanded.push(id);
200            let Ok(field_id) = i32::try_from(id) else {
201                continue;
202            };
203            let mut pending: Vec<&lance_core::datatypes::Field> = schema
204                .field_by_id(field_id)
205                .map(|field| field.children.iter().collect())
206                .unwrap_or_default();
207            while let Some(child) = pending.pop() {
208                if let Ok(child_id) = u32::try_from(child.id) {
209                    expanded.push(child_id);
210                }
211                pending.extend(child.children.iter());
212            }
213        }
214        expanded.sort_unstable();
215        expanded.dedup();
216        expanded
217    }
218
219    /// `rewrites` lists each rewritten live fragment with the fields
220    /// rewritten in it (expanded to their descendants, see
221    /// [`Self::withdraw_in_place_rewrites`]). For every
222    /// non-system segment that indexes one of those fields (keyed or
223    /// carried), with bitmap `B`:
224    /// - a rewritten fragment named in `B` is removed, as on an untagged
225    ///   table;
226    /// - every retired fragment `r` of `B` (not in `live`) whose rows the
227    ///   history moves into the rewritten fragment (`destinations_of`,
228    ///   transitively, so mixed provenance holding intermediates is
229    ///   covered) is removed. The sources of one transition all reach the
230    ///   same destinations, so the transition's sources go whole: the
231    ///   segment derives no coverage of its destinations and those rows are
232    ///   scanned until `optimize_indices` rebuilds them;
233    /// - without a walkable history every retired fragment of `B` goes;
234    /// - a segment without a bitmap is untouched: its coverage is unknown,
235    ///   not empty, and the reader already excludes it.
236    ///
237    /// What a withdrawal leaves is then settled per logical index name:
238    /// - coverage partly left: the segment stays and serves what is left,
239    ///   provided the reader will still translate it. The file keeps the
240    ///   withdrawn rows; the translating load drops them (they resolve
241    ///   outside the derived coverage), but a segment whose remaining
242    ///   bitmap is live-only and disjoint from the history is loaded as it
243    ///   is, and would serve the withdrawn retired rows raw. So when a
244    ///   RETIRED fragment was withdrawn and the remainder would be loaded
245    ///   as it is, the remainder is withdrawn too (the segment empties and
246    ///   is rebuilt). Withdrawing only live fragments leaves the remainder
247    ///   alone: rows of fragments that still exist are masked by the
248    ///   ordinary per-segment ownership filter;
249    /// - coverage emptied while a same-name sibling still has coverage (or
250    ///   an unknown one): the segment's metadata leaves the manifest in this
251    ///   commit; its files are reclaimed by the ordinary index cleanup once
252    ///   no retained version references them;
253    /// - coverage emptied and it is the last segment of its name: it stays
254    ///   as the record of what index to build (queries never use it, since
255    ///   the reader lists nothing for it; maintenance rebuilds it from the
256    ///   live fragments).
257    ///
258    /// Every withdrawal is logged at warn level.
259    pub fn withdraw_rewritten_coverage(
260        indices: &mut Vec<IndexMetadata>,
261        lineage: Option<&TaggedLineage>,
262        live: &RoaringBitmap,
263        rewrites: &[(u64, Vec<u32>)],
264    ) {
265        let mut emptied = Vec::new();
266        for index in indices.iter_mut().filter(|index| !is_system_index(index)) {
267            let indexed: HashSet<u32> = index
268                .fields
269                .iter()
270                .filter_map(|field| u32::try_from(*field).ok())
271                .collect();
272            let Some(bitmap) = index.fragment_bitmap.as_mut() else {
273                continue;
274            };
275            let retired = &*bitmap - live;
276            let mut withdrawn = RoaringBitmap::new();
277            for (fragment, fields) in rewrites {
278                if !fields.iter().any(|field| indexed.contains(field)) {
279                    continue;
280                }
281                let Ok(rewritten) = u32::try_from(*fragment) else {
282                    continue;
283                };
284                if bitmap.contains(rewritten) {
285                    withdrawn.insert(rewritten);
286                }
287                // Every retired source whose rows reach the rewritten
288                // fragment goes; the sources of one transition all reach the
289                // same destinations, so the transition is withdrawn whole.
290                for source in retired.iter() {
291                    match lineage.and_then(|lineage| lineage.destinations_of(source)) {
292                        Some(destinations) if destinations.contains(&rewritten) => {
293                            withdrawn.insert(source);
294                        }
295                        Some(_) => {}
296                        None => {
297                            withdrawn.insert(source);
298                        }
299                    }
300                }
301            }
302            if withdrawn.is_empty() {
303                continue;
304            }
305            *bitmap -= &withdrawn;
306            let withdrew_retired = !(&withdrawn & &retired).is_empty();
307            if withdrew_retired && !bitmap.is_empty() {
308                // Would the reader still translate what is left? Only a
309                // bitmap naming a retired fragment or one the history
310                // mentions is translated (and filtered to its derived
311                // coverage); a live-only remainder the history does not
312                // mention is loaded as it is, which would serve the
313                // withdrawn rows the file still holds. A history that
314                // cannot be walked holds transitions this build cannot
315                // read, and the reader then translates every segment, so
316                // the remainder is safe in that case.
317                let reader_translates = !bitmap.is_subset(live)
318                    || bitmap.iter().any(|fragment| {
319                        lineage
320                            .and_then(|lineage| lineage.mentions(fragment))
321                            .unwrap_or(true)
322                    });
323                if !reader_translates {
324                    log::warn!(
325                        "index {} (segment {}): its remaining coverage {:?} would be loaded \
326                         as it is while its file still holds rows of the withdrawn retired \
327                         fragments; withdrawing it whole so the segment is rebuilt",
328                        index.name,
329                        index.uuid,
330                        bitmap.iter().collect::<Vec<_>>()
331                    );
332                    withdrawn |= &*bitmap;
333                    bitmap.clear();
334                }
335            }
336            if bitmap.is_empty() {
337                emptied.push(index.uuid);
338            }
339            log::warn!(
340                "index {} (segment {}): withdrew {} fragment(s) of coverage after an in-place \
341                 rewrite of an indexed column; the rows they covered are scanned until the index \
342                 is optimized",
343                index.name,
344                index.uuid,
345                withdrawn.len()
346            );
347        }
348        Self::retire_emptied_segments(indices, &emptied);
349    }
350
351    /// Drop the metadata of every segment in `emptied` whose logical index
352    /// still has another segment with coverage (or with unknown coverage).
353    /// The last segment of a name is kept, empty, as the record of what to
354    /// rebuild.
355    fn retire_emptied_segments(indices: &mut Vec<IndexMetadata>, emptied: &[Uuid]) {
356        let removable: Vec<Uuid> = emptied
357            .iter()
358            .copied()
359            .filter(|uuid| {
360                let Some(name) = indices
361                    .iter()
362                    .find(|index| index.uuid == *uuid)
363                    .map(|index| index.name.as_str())
364                else {
365                    return false;
366                };
367                indices.iter().any(|sibling| {
368                    sibling.name == name
369                        && sibling.uuid != *uuid
370                        && sibling
371                            .fragment_bitmap
372                            .as_ref()
373                            .is_none_or(|bitmap| !bitmap.is_empty())
374                })
375            })
376            .collect();
377        if removable.is_empty() {
378            return;
379        }
380        indices.retain(|index| {
381            let retired = removable.contains(&index.uuid);
382            if retired {
383                log::info!(
384                    "index {} (segment {}): every fragment of its coverage was withdrawn and a \
385                     sibling segment still serves the index; the segment leaves the manifest and \
386                     its files are reclaimed by index cleanup",
387                    index.name,
388                    index.uuid
389                );
390            }
391            !retired
392        });
393    }
394
395    /// If an operation modifies one or more fields in a fragment then we need to remove
396    /// that fragment from any indices that cover one of the modified fields.
397    pub fn prune_updated_fields_from_indices(
398        indices: &mut [IndexMetadata],
399        updated_fragments: &[Fragment],
400        fields_modified: &[u32],
401    ) {
402        if fields_modified.is_empty() {
403            return;
404        }
405
406        let fragment_ids = updated_fragments.iter().map(|f| f.id as u32).collect();
407        Self::prune_updated_fragment_ids_from_indices(indices, &fragment_ids, fields_modified);
408    }
409
410    /// [`Self::prune_updated_fields_from_indices`] by fragment id.
411    pub fn prune_updated_fragment_ids_from_indices(
412        indices: &mut [IndexMetadata],
413        fragment_ids: &RoaringBitmap,
414        fields_modified: &[u32],
415    ) {
416        if fields_modified.is_empty() || fragment_ids.is_empty() {
417            return;
418        }
419        // If we modified any fields in the fragments then we need to remove those fragments
420        // from the index if the index covers one of those modified fields.
421        let fields_modified_set = fields_modified.iter().collect::<HashSet<_>>();
422        for index in indices.iter_mut() {
423            if index
424                .fields
425                .iter()
426                .any(|field_id| fields_modified_set.contains(&u32::try_from(*field_id).unwrap()))
427                && let Some(fragment_bitmap) = &mut index.fragment_bitmap
428            {
429                *fragment_bitmap -= fragment_ids;
430            }
431        }
432    }
433
434    /// Map each (non-tombstoned) field id in a fragment to the path of the data
435    /// file that backs it.
436    fn fragment_field_paths(frag: &Fragment) -> HashMap<i32, &str> {
437        let mut map = HashMap::new();
438        for file in &frag.files {
439            for &field_id in file.fields.iter() {
440                if field_id >= 0 {
441                    map.insert(field_id, file.path.as_str());
442                }
443            }
444        }
445        map
446    }
447
448    /// The columns a `Merge` rewrote in place: for each fragment present in
449    /// both lists, the fields still present whose backing data file path
450    /// changed. Brand-new fragments carry nothing stale.
451    pub fn merge_rewritten_fields(
452        prev_fragments: &[Fragment],
453        new_fragments: &[Fragment],
454    ) -> Vec<(u64, Vec<u32>)> {
455        let prev_by_id: HashMap<u64, &Fragment> =
456            prev_fragments.iter().map(|f| (f.id, f)).collect();
457        // Only the first occurrence of a previous id is that fragment's merged
458        // version; later ones are staged fragments still carrying placeholder
459        // id 0, which must not be mistaken for a rewrite of fragment 0.
460        let mut seen_prev_ids = HashSet::new();
461        new_fragments
462            .iter()
463            .filter_map(|new_frag| {
464                let prev = prev_by_id.get(&new_frag.id)?;
465                if !seen_prev_ids.insert(new_frag.id) {
466                    return None;
467                }
468                let prev_paths = Self::fragment_field_paths(prev);
469                let new_paths = Self::fragment_field_paths(new_frag);
470                let mut changed: Vec<u32> = prev_paths
471                    .iter()
472                    .filter(|(field_id, prev_path)| {
473                        new_paths
474                            .get(*field_id)
475                            .is_some_and(|new_path| new_path != *prev_path)
476                    })
477                    .map(|(field_id, _)| *field_id as u32)
478                    .collect();
479                changed.sort_unstable();
480                (!changed.is_empty()).then_some((new_frag.id, changed))
481            })
482            .collect()
483    }
484
485    /// After a `Rewrite` fully compacts a fragment, its data overlays are baked
486    /// into the new fragment's base data. An index built *before* one of those
487    /// overlays (`overlay.committed_version > index.dataset_version`) indexed the
488    /// stale pre-overlay values -- and unlike a live overlay, the compacted
489    /// fragment no longer signals that staleness to the query path. Drop the
490    /// group's fragments from the coverage of any index covering a field such
491    /// an overlay supplied, so those rows fall back to a flat scan.
492    ///
493    /// Both the old and the new ids are dropped: an eager remap has already
494    /// moved the bitmap to the new ids, while a deferred remap leaves the old
495    /// ids for the fragment reuse index to translate at load time.
496    pub(super) fn prune_overlay_stale_fields_from_indices(
497        indices: &mut [IndexMetadata],
498        groups: &[RewriteGroup],
499    ) {
500        for group in groups {
501            // field id -> newest overlay committed_version supplying that field
502            let mut overlaid_field_versions: HashMap<i32, u64> = HashMap::new();
503            for old_frag in &group.old_fragments {
504                for overlay in &old_frag.overlays {
505                    for &field_id in overlay.data_file.fields.iter() {
506                        if field_id < 0 {
507                            // Tombstoned (obsolete) overlay field: supplies nothing.
508                            continue;
509                        }
510                        let entry = overlaid_field_versions.entry(field_id).or_insert(0);
511                        *entry = (*entry).max(overlay.committed_version);
512                    }
513                }
514            }
515            if overlaid_field_versions.is_empty() {
516                continue;
517            }
518
519            let fragment_ids = group
520                .old_fragments
521                .iter()
522                .chain(group.new_fragments.iter())
523                .map(|f| f.id as u32)
524                .collect::<Vec<_>>();
525            for index in indices.iter_mut() {
526                let is_stale = index.fields.iter().any(|field_id| {
527                    overlaid_field_versions
528                        .get(field_id)
529                        .is_some_and(|&overlay_version| overlay_version > index.dataset_version)
530                });
531                if is_stale && let Some(fragment_bitmap) = &mut index.fragment_bitmap {
532                    for id in &fragment_ids {
533                        fragment_bitmap.remove(*id);
534                    }
535                }
536            }
537        }
538    }
539
540    pub(crate) fn retain_relevant_indices(
541        indices: &mut Vec<IndexMetadata>,
542        schema: &Schema,
543        fragments: &[Fragment],
544    ) {
545        let field_ids = schema
546            .fields_pre_order()
547            .map(|f| f.id)
548            .collect::<HashSet<_>>();
549
550        // Remove indices for fields no longer in schema
551        indices.retain(|existing_index| {
552            existing_index
553                .fields
554                .iter()
555                .all(|field_id| field_ids.contains(field_id))
556                || is_system_index(existing_index)
557        });
558
559        let mut indices_by_name: std::collections::HashMap<String, Vec<&IndexMetadata>> =
560            std::collections::HashMap::new();
561
562        for index in indices.iter() {
563            if index.name != FRAG_REUSE_INDEX_NAME {
564                indices_by_name
565                    .entry(index.name.clone())
566                    .or_default()
567                    .push(index);
568            }
569        }
570
571        let mut uuids_to_keep = std::collections::HashSet::new();
572
573        let existing_fragments = fragments
574            .iter()
575            .map(|f| f.id as u32)
576            .collect::<RoaringBitmap>();
577
578        // Under a tagged fragment reuse history a segment's stored bitmap is
579        // its provenance (the retired source fragments it was built from),
580        // not the fragments it serves: its live coverage is derived at query
581        // time by translating through the history, and is empty against the
582        // live fragments by construction once its sources were rewritten.
583        // Measuring such a segment here would drop it. Every segment is kept
584        // instead; superseded segments are pruned by the tagged maintenance
585        // path, which reasons on derived coverage.
586        let tagged = indices.iter().any(is_tagged);
587
588        for (_, same_name_indices) in indices_by_name {
589            if tagged {
590                for index in same_name_indices {
591                    uuids_to_keep.insert(index.uuid);
592                }
593                continue;
594            }
595            // Unknown coverage is not empty coverage: a segment whose bitmap is
596            // missing has never been measured, and dropping it deletes an index
597            // that migration could not open yet.
598            let (unknown_coverage, same_name_indices): (Vec<_>, Vec<_>) = same_name_indices
599                .into_iter()
600                .partition(|index| index.fragment_bitmap.is_none());
601            for index in unknown_coverage {
602                uuids_to_keep.insert(index.uuid);
603            }
604
605            if same_name_indices.len() > 1 {
606                let (empty_indices, non_empty_indices): (Vec<_>, Vec<_>) =
607                    same_name_indices.iter().partition(|index| {
608                        index
609                            .effective_fragment_bitmap(&existing_fragments)
610                            .as_ref()
611                            .is_none_or(|bitmap| bitmap.is_empty())
612                    });
613
614                if non_empty_indices.is_empty() {
615                    // All indices are empty -- keep only the earliest-created definition.
616                    //
617                    // An empty index definition is still correct: the scanner
618                    // falls back to scanning unindexed fragments, and normal
619                    // index maintenance rebuilds coverage once rows accrue.
620                    // Dropping the definition instead would silently lose the
621                    // index whenever an operation replaces every fragment it
622                    // covered (e.g. a full table rewrite), leaving the dataset
623                    // without its declared index.
624                    //
625                    // `dataset_version` is a watermark, not a creation time:
626                    // merges take the minimum of their sources and remaps raise
627                    // it. Order by `created_at` instead, with segments that lack
628                    // one after those that have one, and break ties on the
629                    // lowest `dataset_version`.
630                    let mut sorted_indices = empty_indices;
631                    sorted_indices.sort_by_key(|index: &&IndexMetadata| {
632                        (
633                            index.created_at.is_none(),
634                            index.created_at,
635                            index.dataset_version,
636                        )
637                    });
638
639                    if let Some(oldest) = sorted_indices.first() {
640                        uuids_to_keep.insert(oldest.uuid);
641                    }
642                } else {
643                    for index in non_empty_indices {
644                        uuids_to_keep.insert(index.uuid);
645                    }
646                }
647            } else {
648                // Single index whose column is still in schema: keep it, even
649                // when its coverage is empty (see the all-empty note above).
650                if let Some(index) = same_name_indices.first() {
651                    uuids_to_keep.insert(index.uuid);
652                }
653            }
654        }
655
656        indices.retain(|index| {
657            index.name == FRAG_REUSE_INDEX_NAME || uuids_to_keep.contains(&index.uuid)
658        });
659    }
660
661    /// The rewrite groups whose index bitmaps follow the rewrite (source ids
662    /// swapped for destination ids). Groups covered by the stable-partition
663    /// transitions' sources are excluded: they redistribute rows, so their
664    /// bitmaps keep the retired source ids as provenance and the tagged
665    /// fragment reuse index entry records the row-level translation. A group
666    /// must be entirely reordered or entirely order-preserving.
667    pub(super) fn ordered_rewrite_groups(
668        groups: &[RewriteGroup],
669        reordered_sources: Option<&RoaringBitmap>,
670    ) -> Result<Vec<RewriteGroup>> {
671        let Some(sources) = reordered_sources else {
672            return Ok(groups.to_vec());
673        };
674        let mut ordered = Vec::new();
675        for group in groups {
676            let covered = group
677                .old_fragments
678                .iter()
679                .filter(|frag| sources.contains(frag.id as u32))
680                .count();
681            if covered == 0 {
682                ordered.push(group.clone());
683            } else if covered != group.old_fragments.len() {
684                return Err(Error::invalid_input(
685                    "a rewrite group mixes transition-covered and order-preserving source fragments",
686                ));
687            }
688        }
689        Ok(ordered)
690    }
691
692    pub(super) fn recalculate_fragment_bitmap(
693        old: &RoaringBitmap,
694        groups: &[RewriteGroup],
695    ) -> Result<RoaringBitmap> {
696        let mut new_bitmap = old.clone();
697        for group in groups {
698            let any_in_index = group
699                .old_fragments
700                .iter()
701                .any(|frag| old.contains(frag.id as u32));
702            let all_in_index = group
703                .old_fragments
704                .iter()
705                .all(|frag| old.contains(frag.id as u32));
706            // Any rewrite group may or may not be covered by the index.  However, if any fragment
707            // in a rewrite group was previously covered by the index then all fragments in the rewrite
708            // group must have been previously covered by the index.  plan_compaction takes care of
709            // this for us so this should be safe to assume.
710            if any_in_index {
711                if all_in_index {
712                    for frag_id in group.old_fragments.iter().map(|frag| frag.id as u32) {
713                        new_bitmap.remove(frag_id);
714                    }
715                    new_bitmap.extend(group.new_fragments.iter().map(|frag| frag.id as u32));
716                } else {
717                    return Err(Error::invalid_input(
718                        "The compaction plan included a rewrite group that was a split of indexed and non-indexed data",
719                    ));
720                }
721            }
722        }
723        Ok(new_bitmap)
724    }
725
726    pub(super) fn handle_rewrite_indices(
727        indices: &mut [IndexMetadata],
728        rewritten_indices: &[RewrittenIndex],
729        groups: &[RewriteGroup],
730    ) -> Result<()> {
731        let mut modified_indices = HashSet::new();
732
733        for rewritten_index in rewritten_indices {
734            if !modified_indices.insert(rewritten_index.old_id) {
735                return Err(Error::invalid_input(format!(
736                    "An invalid compaction plan must have been generated because multiple tasks modified the same index: {}",
737                    rewritten_index.old_id
738                )));
739            }
740
741            // Skip indices that no longer exist (may have been removed by concurrent operation)
742            let Some(index) = indices
743                .iter_mut()
744                .find(|idx| idx.uuid == rewritten_index.old_id)
745            else {
746                continue;
747            };
748
749            index.fragment_bitmap = Some(Self::recalculate_fragment_bitmap(
750                index.fragment_bitmap.as_ref().ok_or_else(|| {
751                    Error::invalid_input(format!(
752                        "Cannot rewrite index {} which did not store fragment bitmap",
753                        index.uuid
754                    ))
755                })?,
756                groups,
757            )?);
758            index.uuid = rewritten_index.new_id;
759            // Update file sizes to match the new index files. When not available
760            // (e.g., from older writers), clear the old file sizes to avoid
761            // using stale sizes from the pre-remap index.
762            index.files = rewritten_index.new_index_files.clone();
763        }
764        Ok(())
765    }
766
767    fn duplicate_rewrite_error(version: u64, fragment_id: u64) -> Error {
768        Error::commit_conflict_source(
769            version,
770            format!(
771                "a rewrite operation attempts to replace fragment id={} more than once",
772                fragment_id
773            )
774            .into(),
775        )
776    }
777
778    pub(super) fn handle_rewrite_fragments(
779        final_fragments: &mut Vec<Fragment>,
780        groups: &[RewriteGroup],
781        fragment_id: &mut u64,
782        version: u64,
783        _next_row_id: Option<&u64>,
784    ) -> Result<()> {
785        // Index fragment positions up front so each rewrite group is located in
786        // O(1). Scanning and splicing/retaining the fragment list per group is
787        // O(groups * fragments) and dominates commit time on manifests with
788        // millions of fragments.
789        let positions: HashMap<u64, usize> = final_fragments
790            .iter()
791            .enumerate()
792            .map(|(position, fragment)| (fragment.id, position))
793            .collect();
794
795        // Contiguous groups have their new fragments spliced in at the position
796        // of their first old fragment; non-contiguous groups have their new
797        // fragments appended at the end.
798        let mut removed = vec![false; final_fragments.len()];
799        let mut replacements: HashMap<usize, Vec<Fragment>> = HashMap::new();
800        let mut appended: Vec<Fragment> = Vec::new();
801
802        for group in groups {
803            let start = *positions.get(&group.old_fragments[0].id).ok_or_else(|| {
804                Error::commit_conflict_source(
805                    version,
806                    format!(
807                        "dataset does not contain a fragment a rewrite operation wants to replace: id={}",
808                        group.old_fragments[0].id
809                    )
810                    .into(),
811                )
812            })?;
813
814            let contiguous = start + group.old_fragments.len() <= final_fragments.len()
815                && group
816                    .old_fragments
817                    .iter()
818                    .enumerate()
819                    .all(|(i, old)| final_fragments[start + i].id == old.id);
820
821            let new_fragments = Self::fragments_with_ids(group.new_fragments.clone(), fragment_id)
822                .collect::<Vec<_>>();
823
824            // Version metadata for rewritten fragments is handled by the compaction code
825            // (recalc_versions_for_rewritten_fragments) which preserves version information
826            // from the original fragments. We don't modify it here.
827
828            if contiguous {
829                for (i, old_fragment) in group.old_fragments.iter().enumerate() {
830                    let position = start + i;
831                    if removed[position] {
832                        return Err(Self::duplicate_rewrite_error(version, old_fragment.id));
833                    }
834                    removed[position] = true;
835                }
836                replacements.insert(start, new_fragments);
837            } else {
838                for old_fragment in group.old_fragments.iter() {
839                    let position = positions.get(&old_fragment.id).ok_or_else(|| {
840                        Error::commit_conflict_source(
841                            version,
842                            format!(
843                                "dataset does not contain a fragment a rewrite operation wants to replace: id={}",
844                                old_fragment.id
845                            )
846                            .into(),
847                        )
848                    })?;
849                    if removed[*position] {
850                        return Err(Self::duplicate_rewrite_error(version, old_fragment.id));
851                    }
852                    removed[*position] = true;
853                }
854                appended.extend(new_fragments);
855            }
856        }
857
858        let mut rewritten = Vec::with_capacity(final_fragments.len() + appended.len());
859        for (position, fragment) in std::mem::take(final_fragments).into_iter().enumerate() {
860            if let Some(new_fragments) = replacements.remove(&position) {
861                rewritten.extend(new_fragments);
862            }
863            if !removed[position] {
864                rewritten.push(fragment);
865            }
866        }
867        rewritten.extend(appended);
868        *final_fragments = rewritten;
869        Ok(())
870    }
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876    use crate::transaction::test_support::overlay_with_field;
877    use chrono::{TimeZone, Utc};
878    use uuid::Uuid;
879
880    #[test]
881    fn test_ordered_rewrite_groups_split_and_mixed() {
882        use crate::format::pb::fragment_reuse_index_details as pb_fri;
883        let group = |old_ids: &[u64], new_ids: &[u64]| RewriteGroup {
884            old_fragments: old_ids.iter().map(|&id| Fragment::new(id)).collect(),
885            new_fragments: new_ids.iter().map(|&id| Fragment::new(id)).collect(),
886        };
887        let groups = vec![group(&[0, 1], &[10]), group(&[2], &[11])];
888
889        // No stable partition: every group takes part in bitmap maintenance.
890        assert_eq!(
891            Transaction::ordered_rewrite_groups(&groups, None)
892                .unwrap()
893                .len(),
894            2
895        );
896
897        // Group [0, 1] is reordered: only group [2] remains ordered.
898        let digest = |id: u64| pb_fri::FragmentDigest {
899            id,
900            physical_rows: 4,
901            num_deleted_rows: 0,
902        };
903        let reordered = crate::transaction::reordered_sources(&[pb_fri::Transition {
904            sources: vec![digest(0), digest(1)],
905            destinations: vec![digest(10), digest(10)],
906            mapping: None,
907        }])
908        .unwrap();
909        let ordered = Transaction::ordered_rewrite_groups(&groups, Some(&reordered)).unwrap();
910        assert_eq!(ordered.len(), 1);
911        assert_eq!(ordered[0].old_fragments[0].id, 2);
912
913        // A group straddling reordered and order-preserving sources is
914        // rejected.
915        let mixed = vec![group(&[1, 2], &[10])];
916        assert!(Transaction::ordered_rewrite_groups(&mixed, Some(&reordered)).is_err());
917    }
918
919    #[test]
920    fn test_rewrite_fragments() {
921        let existing_fragments: Vec<Fragment> = (0..10).map(Fragment::new).collect();
922
923        let mut final_fragments = existing_fragments;
924        let rewrite_groups = vec![
925            // Since these are contiguous, they will be put in the same location
926            // as 1 and 2.
927            RewriteGroup {
928                old_fragments: vec![Fragment::new(1), Fragment::new(2)],
929                // These two fragments were previously reserved
930                new_fragments: vec![Fragment::new(15), Fragment::new(16)],
931            },
932            // These are not contiguous, so they will be inserted at the end.
933            RewriteGroup {
934                old_fragments: vec![Fragment::new(5), Fragment::new(8)],
935                // We pretend this id was not reserved.  Does not happen in practice today
936                // but we want to leave the door open.
937                new_fragments: vec![Fragment::new(0)],
938            },
939        ];
940
941        let mut fragment_id = 20;
942        let version = 0;
943
944        Transaction::handle_rewrite_fragments(
945            &mut final_fragments,
946            &rewrite_groups,
947            &mut fragment_id,
948            version,
949            None,
950        )
951        .unwrap();
952
953        assert_eq!(fragment_id, 21);
954
955        let expected_fragments: Vec<Fragment> = vec![
956            Fragment::new(0),
957            Fragment::new(15),
958            Fragment::new(16),
959            Fragment::new(3),
960            Fragment::new(4),
961            Fragment::new(6),
962            Fragment::new(7),
963            Fragment::new(9),
964            Fragment::new(20),
965        ];
966
967        assert_eq!(final_fragments, expected_fragments);
968    }
969
970    #[test]
971    fn test_rewrite_fragments_missing_fragment() {
972        let mut final_fragments: Vec<Fragment> = (0..4).map(Fragment::new).collect();
973        let rewrite_groups = vec![RewriteGroup {
974            old_fragments: vec![Fragment::new(7)],
975            new_fragments: vec![Fragment::new(0)],
976        }];
977
978        let mut fragment_id = 10;
979        let result = Transaction::handle_rewrite_fragments(
980            &mut final_fragments,
981            &rewrite_groups,
982            &mut fragment_id,
983            42,
984            None,
985        );
986
987        let error = result.unwrap_err();
988        assert!(matches!(error, Error::CommitConflict { .. }));
989        assert!(error.to_string().contains("id=7"));
990    }
991
992    #[test]
993    fn test_rewrite_fragments_non_contiguous_missing_fragment() {
994        // The group's first old fragment (1) exists, but it is not adjacent to
995        // the second old fragment (7), which does not exist. The group takes
996        // the non-contiguous path, which must still error instead of silently
997        // dropping fragment 7 from the rewrite.
998        let mut final_fragments: Vec<Fragment> = (0..4).map(Fragment::new).collect();
999        let rewrite_groups = vec![RewriteGroup {
1000            old_fragments: vec![Fragment::new(1), Fragment::new(7)],
1001            new_fragments: vec![Fragment::new(0)],
1002        }];
1003
1004        let mut fragment_id = 10;
1005        let result = Transaction::handle_rewrite_fragments(
1006            &mut final_fragments,
1007            &rewrite_groups,
1008            &mut fragment_id,
1009            42,
1010            None,
1011        );
1012
1013        let error = result.unwrap_err();
1014        assert!(matches!(error, Error::CommitConflict { .. }));
1015        assert!(error.to_string().contains("id=7"));
1016    }
1017
1018    #[test]
1019    fn test_rewrite_fragments_past_the_end() {
1020        // The group's old fragments start near the end of the list and would
1021        // index past it if the contiguity check were not bounds-checked.
1022        let mut final_fragments: Vec<Fragment> = (0..4).map(Fragment::new).collect();
1023        let rewrite_groups = vec![RewriteGroup {
1024            old_fragments: vec![Fragment::new(3), Fragment::new(0)],
1025            new_fragments: vec![Fragment::new(10)],
1026        }];
1027
1028        let mut fragment_id = 11;
1029        Transaction::handle_rewrite_fragments(
1030            &mut final_fragments,
1031            &rewrite_groups,
1032            &mut fragment_id,
1033            42,
1034            None,
1035        )
1036        .unwrap();
1037
1038        // Non-contiguous: fragments 3 and 0 are removed, new fragment 10 is
1039        // appended at the end.
1040        let expected_fragments: Vec<Fragment> =
1041            vec![Fragment::new(1), Fragment::new(2), Fragment::new(10)];
1042        assert_eq!(final_fragments, expected_fragments);
1043    }
1044
1045    #[test]
1046    fn test_rewrite_fragments_duplicate_fragment() {
1047        let mut final_fragments: Vec<Fragment> = (0..4).map(Fragment::new).collect();
1048        let rewrite_groups = vec![
1049            RewriteGroup {
1050                old_fragments: vec![Fragment::new(1), Fragment::new(2)],
1051                new_fragments: vec![Fragment::new(0)],
1052            },
1053            RewriteGroup {
1054                old_fragments: vec![Fragment::new(2), Fragment::new(3)],
1055                new_fragments: vec![Fragment::new(0)],
1056            },
1057        ];
1058
1059        let mut fragment_id = 10;
1060        let result = Transaction::handle_rewrite_fragments(
1061            &mut final_fragments,
1062            &rewrite_groups,
1063            &mut fragment_id,
1064            42,
1065            None,
1066        );
1067
1068        let error = result.unwrap_err();
1069        assert!(matches!(error, Error::CommitConflict { .. }));
1070        assert!(error.to_string().contains("id=2"));
1071    }
1072
1073    #[test]
1074    fn test_retain_indices_removes_missing_fields() {
1075        let schema = create_test_schema(&[1, 2]);
1076        let fragments = vec![Fragment::new(1), Fragment::new(2)];
1077
1078        let mut indices = vec![
1079            create_test_index("idx1", 1, 1, Some(RoaringBitmap::from_iter([1])), false),
1080            create_test_index("idx2", 2, 1, Some(RoaringBitmap::from_iter([1])), false),
1081            create_test_index("idx3", 99, 1, Some(RoaringBitmap::from_iter([1])), false), // Field doesn't exist
1082        ];
1083
1084        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1085
1086        assert_eq!(indices.len(), 2);
1087        assert!(indices.iter().all(|idx| idx.fields[0] != 99));
1088    }
1089
1090    #[test]
1091    fn test_retain_indices_keeps_system_indices() {
1092        use crate::system_index::mem_wal::MEM_WAL_INDEX_NAME;
1093
1094        let schema = create_test_schema(&[1, 2]);
1095        let fragments = vec![Fragment::new(1)];
1096
1097        let mut indices = vec![
1098            legacy_frag_reuse_index(99), // Field doesn't exist but should be kept
1099            create_system_index(MEM_WAL_INDEX_NAME, 99), // Field doesn't exist but should be kept
1100            create_test_index("regular_idx", 99, 1, Some(RoaringBitmap::new()), false), // Should be removed
1101        ];
1102
1103        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1104
1105        assert_eq!(indices.len(), 2);
1106        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
1107        assert!(indices.iter().any(|idx| idx.name == MEM_WAL_INDEX_NAME));
1108    }
1109
1110    #[test]
1111    fn test_retain_indices_keeps_fragment_reuse_index() {
1112        let schema = create_test_schema(&[1]);
1113        let fragments = vec![Fragment::new(1)];
1114
1115        let mut indices = vec![
1116            legacy_frag_reuse_index(1),
1117            create_test_index("other_idx", 1, 1, Some(RoaringBitmap::new()), false),
1118        ];
1119
1120        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1121
1122        // Fragment reuse index should always be kept
1123        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
1124    }
1125
1126    #[test]
1127    fn test_retain_single_empty_scalar_index() {
1128        let schema = create_test_schema(&[1]);
1129        let fragments = vec![Fragment::new(1)];
1130
1131        let mut indices = vec![create_test_index(
1132            "scalar_idx",
1133            1,
1134            1,
1135            Some(RoaringBitmap::new()), // Empty bitmap
1136            false,
1137        )];
1138
1139        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1140
1141        // Single empty scalar index should be kept
1142        assert_eq!(indices.len(), 1);
1143    }
1144
1145    #[test]
1146    fn test_retain_single_empty_vector_index_is_kept() {
1147        let schema = create_test_schema(&[1]);
1148        let fragments = vec![Fragment::new(1)];
1149
1150        let mut indices = vec![create_test_index(
1151            "vector_idx",
1152            1,
1153            1,
1154            Some(RoaringBitmap::new()), // Empty bitmap
1155            true,
1156        )];
1157
1158        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1159
1160        // The empty definition is retained: coverage is empty but the index
1161        // declaration must survive operations that replace every fragment.
1162        assert_eq!(indices.len(), 1);
1163    }
1164
1165    #[test]
1166    fn test_retain_single_nonempty_index() {
1167        let schema = create_test_schema(&[1]);
1168        let fragments = vec![Fragment::new(1)];
1169
1170        let mut scalar_indices = vec![create_test_index(
1171            "scalar_idx",
1172            1,
1173            1,
1174            Some(RoaringBitmap::from_iter([1])),
1175            false,
1176        )];
1177
1178        let mut vector_indices = vec![create_test_index(
1179            "vector_idx",
1180            1,
1181            1,
1182            Some(RoaringBitmap::from_iter([1])),
1183            true,
1184        )];
1185
1186        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
1187        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
1188
1189        // Both should be kept
1190        assert_eq!(scalar_indices.len(), 1);
1191        assert_eq!(vector_indices.len(), 1);
1192    }
1193
1194    #[test]
1195    fn test_retain_single_index_with_none_bitmap() {
1196        let schema = create_test_schema(&[1]);
1197        let fragments = vec![Fragment::new(1)];
1198
1199        let mut scalar_indices = vec![create_test_index("scalar_idx", 1, 1, None, false)];
1200        let mut vector_indices = vec![create_test_index("vector_idx", 1, 1, None, true)];
1201
1202        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
1203        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
1204
1205        // Both kept: a None bitmap is unknown coverage, not empty coverage, and
1206        // an unmeasured segment is retained regardless of index type.
1207        assert_eq!(scalar_indices.len(), 1);
1208        assert_eq!(vector_indices.len(), 1);
1209    }
1210
1211    #[test]
1212    fn test_retain_unknown_coverage_alongside_nonempty_sibling() {
1213        let schema = create_test_schema(&[1]);
1214        let fragments = vec![Fragment::new(1), Fragment::new(2)];
1215
1216        let mut indices = vec![
1217            create_test_index("idx", 1, 1, None, false), // Coverage never measured
1218            create_test_index("idx", 1, 2, Some(RoaringBitmap::from_iter([2])), false),
1219        ];
1220
1221        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1222
1223        // The unmeasured segment must survive its non-empty sibling: its bitmap
1224        // is missing because migration could not open the index, and deleting
1225        // the segment would take the only record of it with it.
1226        assert_eq!(indices.len(), 2);
1227        assert!(indices.iter().any(|idx| idx.fragment_bitmap.is_none()));
1228    }
1229
1230    #[test]
1231    fn test_retain_multiple_empty_scalar_indices_keeps_oldest() {
1232        let schema = create_test_schema(&[1]);
1233        let fragments = vec![Fragment::new(1)];
1234
1235        let mut indices = vec![
1236            create_test_index("idx", 1, 3, Some(RoaringBitmap::new()), false),
1237            create_test_index("idx", 1, 1, Some(RoaringBitmap::new()), false), // Oldest
1238            create_test_index("idx", 1, 2, Some(RoaringBitmap::new()), false),
1239        ];
1240
1241        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1242
1243        // Should keep only the oldest (dataset_version = 1)
1244        assert_eq!(indices.len(), 1);
1245        assert_eq!(indices[0].dataset_version, 1);
1246    }
1247
1248    #[test]
1249    fn test_retain_multiple_empty_vector_indices_keeps_oldest() {
1250        let schema = create_test_schema(&[1]);
1251        let fragments = vec![Fragment::new(1)];
1252
1253        let mut indices = vec![
1254            create_test_index("vec_idx", 1, 1, Some(RoaringBitmap::new()), true),
1255            create_test_index("vec_idx", 1, 2, Some(RoaringBitmap::new()), true),
1256            create_test_index("vec_idx", 1, 3, Some(RoaringBitmap::new()), true),
1257        ];
1258
1259        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1260
1261        // Same as the scalar case: all deltas are empty, so only the oldest
1262        // definition survives.
1263        assert_eq!(indices.len(), 1);
1264        assert_eq!(indices[0].dataset_version, 1);
1265    }
1266
1267    #[test]
1268    fn test_retain_multiple_empty_indices_keeps_earliest_created() {
1269        let schema = create_test_schema(&[1]);
1270        let fragments = vec![Fragment::new(1)];
1271        let at = |secs| Some(Utc.timestamp_opt(secs, 0).unwrap());
1272
1273        // The lowest `dataset_version` is not the earliest-created segment
1274        // (e.g. a merged segment that took the minimum version of its sources).
1275        let mut indices = vec![
1276            create_test_index("idx", 1, 1, Some(RoaringBitmap::new()), false),
1277            create_test_index("idx", 1, 2, Some(RoaringBitmap::new()), false),
1278            create_test_index("idx", 1, 3, Some(RoaringBitmap::new()), false),
1279        ];
1280        indices[0].created_at = at(300);
1281        indices[1].created_at = None;
1282        indices[2].created_at = at(100);
1283
1284        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1285
1286        assert_eq!(indices.len(), 1);
1287        assert_eq!(indices[0].dataset_version, 3);
1288        assert_eq!(indices[0].created_at, at(100));
1289
1290        // Segments without `created_at` sort after those with one, and ties
1291        // go to the lowest `dataset_version`.
1292        let mut indices = vec![
1293            create_test_index("idx", 1, 1, Some(RoaringBitmap::new()), false),
1294            create_test_index("idx", 1, 5, Some(RoaringBitmap::new()), false),
1295            create_test_index("idx", 1, 4, Some(RoaringBitmap::new()), false),
1296        ];
1297        indices[0].created_at = None;
1298        indices[1].created_at = at(200);
1299        indices[2].created_at = at(200);
1300
1301        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1302
1303        assert_eq!(indices.len(), 1);
1304        assert_eq!(indices[0].dataset_version, 4);
1305    }
1306
1307    #[test]
1308    fn test_retain_mixed_empty_nonempty_keeps_nonempty() {
1309        let schema = create_test_schema(&[1]);
1310        let fragments = vec![Fragment::new(1)];
1311
1312        let mut indices = vec![
1313            create_test_index("idx", 1, 1, Some(RoaringBitmap::new()), false), // Empty
1314            create_test_index("idx", 1, 2, Some(RoaringBitmap::from_iter([1])), false), // Non-empty
1315            create_test_index("idx", 1, 3, Some(RoaringBitmap::new()), false), // Empty
1316            create_test_index("idx", 1, 4, Some(RoaringBitmap::from_iter([1])), false), // Non-empty
1317        ];
1318
1319        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1320
1321        // Should keep only non-empty indices
1322        assert_eq!(indices.len(), 2);
1323        assert!(
1324            indices
1325                .iter()
1326                .all(|idx| idx.dataset_version == 2 || idx.dataset_version == 4)
1327        );
1328    }
1329
1330    #[test]
1331    fn test_retain_mixed_empty_nonempty_vector_keeps_nonempty() {
1332        let schema = create_test_schema(&[1]);
1333        let fragments = vec![Fragment::new(1)];
1334
1335        let mut indices = vec![
1336            create_test_index("vec_idx", 1, 1, Some(RoaringBitmap::new()), true), // Empty
1337            create_test_index("vec_idx", 1, 2, Some(RoaringBitmap::from_iter([1])), true), // Non-empty
1338            create_test_index("vec_idx", 1, 3, Some(RoaringBitmap::new()), true),          // Empty
1339        ];
1340
1341        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1342
1343        // Should keep only non-empty index
1344        assert_eq!(indices.len(), 1);
1345        assert_eq!(indices[0].dataset_version, 2);
1346    }
1347
1348    #[test]
1349    fn test_retain_fragment_bitmap_with_nonexistent_fragments() {
1350        let schema = create_test_schema(&[1]);
1351        let fragments = vec![Fragment::new(1), Fragment::new(2)]; // Only fragments 1 and 2 exist
1352
1353        let mut indices = vec![create_test_index(
1354            "idx",
1355            1,
1356            1,
1357            Some(RoaringBitmap::from_iter([1, 2, 3, 4])), // References non-existent fragments 3, 4
1358            false,
1359        )];
1360
1361        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1362
1363        // Should still keep the index (effective bitmap will be intersection with existing)
1364        assert_eq!(indices.len(), 1);
1365        // Original bitmap should be unchanged
1366        assert_eq!(
1367            indices[0].fragment_bitmap.as_ref().unwrap(),
1368            &RoaringBitmap::from_iter([1, 2, 3, 4])
1369        );
1370    }
1371
1372    #[test]
1373    fn test_retain_effective_empty_bitmap_single_index() {
1374        let schema = create_test_schema(&[1]);
1375        let fragments = vec![Fragment::new(5), Fragment::new(6)];
1376
1377        // Bitmap references fragments that don't exist, so effective bitmap is empty
1378        let mut scalar_indices = vec![create_test_index(
1379            "scalar_idx",
1380            1,
1381            1,
1382            Some(RoaringBitmap::from_iter([1, 2, 3])),
1383            false,
1384        )];
1385
1386        let mut vector_indices = vec![create_test_index(
1387            "vector_idx",
1388            1,
1389            1,
1390            Some(RoaringBitmap::from_iter([1, 2, 3])),
1391            true,
1392        )];
1393
1394        Transaction::retain_relevant_indices(&mut scalar_indices, &schema, &fragments);
1395        Transaction::retain_relevant_indices(&mut vector_indices, &schema, &fragments);
1396
1397        // Both kept: a single index whose column is still in schema is
1398        // retained even when its effective coverage is empty.
1399        assert_eq!(scalar_indices.len(), 1);
1400        assert_eq!(vector_indices.len(), 1);
1401    }
1402
1403    #[test]
1404    fn test_retain_different_index_names() {
1405        let schema = create_test_schema(&[1]);
1406        let fragments = vec![Fragment::new(1)];
1407
1408        let mut indices = vec![
1409            create_test_index("idx_a", 1, 1, Some(RoaringBitmap::new()), false),
1410            create_test_index("idx_b", 1, 1, Some(RoaringBitmap::new()), true),
1411            create_test_index("idx_c", 1, 1, Some(RoaringBitmap::from_iter([1])), false),
1412        ];
1413
1414        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1415
1416        // All three kept: empty definitions are retained for scalar and
1417        // vector indexes alike.
1418        assert_eq!(indices.len(), 3);
1419        assert!(indices.iter().any(|idx| idx.name == "idx_a"));
1420        assert!(indices.iter().any(|idx| idx.name == "idx_b"));
1421        assert!(indices.iter().any(|idx| idx.name == "idx_c"));
1422    }
1423
1424    #[test]
1425    fn test_retain_empty_indices_vec() {
1426        let schema = create_test_schema(&[1]);
1427        let fragments = vec![Fragment::new(1)];
1428
1429        let mut indices: Vec<IndexMetadata> = vec![];
1430
1431        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1432
1433        assert_eq!(indices.len(), 0);
1434    }
1435
1436    #[test]
1437    fn test_retain_all_indices_removed() {
1438        let schema = create_test_schema(&[1]);
1439        let fragments = vec![Fragment::new(1)];
1440
1441        let mut indices = vec![
1442            create_test_index("vec1", 1, 1, Some(RoaringBitmap::new()), true),
1443            create_test_index("vec2", 1, 1, Some(RoaringBitmap::new()), true),
1444            create_test_index("idx3", 99, 1, Some(RoaringBitmap::from_iter([1])), false), // Bad field
1445        ];
1446
1447        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1448
1449        // Only the bad-field index is dropped; the empty vector definitions
1450        // are retained.
1451        assert_eq!(indices.len(), 2);
1452        assert!(!indices.iter().any(|idx| idx.name == "idx3"));
1453    }
1454
1455    #[test]
1456    fn test_retain_complex_scenario() {
1457        let schema = create_test_schema(&[1, 2]);
1458        let fragments = vec![Fragment::new(1), Fragment::new(2)];
1459
1460        let mut indices = vec![
1461            // System index - should always be kept
1462            legacy_frag_reuse_index(1),
1463            // Group "idx_a" - all empty scalars, keep oldest
1464            create_test_index("idx_a", 1, 3, Some(RoaringBitmap::new()), false),
1465            create_test_index("idx_a", 1, 1, Some(RoaringBitmap::new()), false), // Oldest
1466            create_test_index("idx_a", 1, 2, Some(RoaringBitmap::new()), false),
1467            // Group "vec_b" - all empty vectors, keep oldest definition
1468            create_test_index("vec_b", 1, 1, Some(RoaringBitmap::new()), true),
1469            create_test_index("vec_b", 1, 2, Some(RoaringBitmap::new()), true),
1470            // Group "idx_c" - mixed empty/non-empty, keep non-empty
1471            create_test_index("idx_c", 2, 1, Some(RoaringBitmap::new()), false),
1472            create_test_index("idx_c", 2, 2, Some(RoaringBitmap::from_iter([1])), false), // Keep
1473            create_test_index("idx_c", 2, 3, Some(RoaringBitmap::from_iter([2])), false), // Keep
1474            // Single non-empty - keep
1475            create_test_index("idx_d", 1, 1, Some(RoaringBitmap::from_iter([1, 2])), false),
1476            // Index with bad field - remove
1477            create_test_index("idx_e", 99, 1, Some(RoaringBitmap::from_iter([1])), false),
1478        ];
1479
1480        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1481
1482        // Expected: frag_reuse, idx_a (oldest), vec_b (oldest), idx_c (2
1483        // non-empty), idx_d = 6 total
1484        assert_eq!(indices.len(), 6);
1485
1486        // Verify system index kept
1487        assert!(indices.iter().any(|idx| idx.name == FRAG_REUSE_INDEX_NAME));
1488
1489        // Verify idx_a kept oldest only
1490        let idx_a_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "idx_a").collect();
1491        assert_eq!(idx_a_indices.len(), 1);
1492        assert_eq!(idx_a_indices[0].dataset_version, 1);
1493
1494        // Verify vec_b kept oldest definition only
1495        let vec_b_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "vec_b").collect();
1496        assert_eq!(vec_b_indices.len(), 1);
1497        assert_eq!(vec_b_indices[0].dataset_version, 1);
1498
1499        // Verify idx_c kept non-empty only
1500        let idx_c_indices: Vec<_> = indices.iter().filter(|idx| idx.name == "idx_c").collect();
1501        assert_eq!(idx_c_indices.len(), 2);
1502        assert!(
1503            idx_c_indices
1504                .iter()
1505                .all(|idx| idx.dataset_version == 2 || idx.dataset_version == 3)
1506        );
1507
1508        // Verify idx_d kept
1509        assert!(indices.iter().any(|idx| idx.name == "idx_d"));
1510
1511        // Verify idx_e removed (bad field)
1512        assert!(!indices.iter().any(|idx| idx.name == "idx_e"));
1513    }
1514
1515    #[test]
1516    fn test_handle_rewrite_indices_skips_missing_index() {
1517        // Create an empty indices list
1518        let mut indices = vec![];
1519
1520        // Create rewritten_indices referring to a non-existent index
1521        let rewritten_indices = vec![RewrittenIndex {
1522            old_id: Uuid::new_v4(),
1523            new_id: Uuid::new_v4(),
1524            new_index_details: prost_types::Any {
1525                type_url: String::new(),
1526                value: vec![],
1527            },
1528            new_index_version: 1,
1529            new_index_files: None,
1530        }];
1531
1532        // Should succeed (skip missing index) instead of error
1533        let result = Transaction::handle_rewrite_indices(&mut indices, &rewritten_indices, &[]);
1534        assert!(result.is_ok());
1535        assert!(indices.is_empty());
1536    }
1537
1538    #[test]
1539    fn test_prune_overlay_stale_fields_from_indices() {
1540        // Fragment 0 carried an overlay on field 1 committed at v5, and was
1541        // fully compacted into new fragment 7.
1542        let mut old_frag = Fragment::new(0);
1543        old_frag.overlays = vec![overlay_with_field(1, 5)];
1544        let groups = vec![RewriteGroup {
1545            old_fragments: vec![old_frag],
1546            new_fragments: vec![Fragment::new(7)],
1547        }];
1548
1549        // Post-remap state: every index already covers the new fragment (7).
1550        let covering = || Some(RoaringBitmap::from_iter([7u32]));
1551        let mut indices = vec![
1552            // Stale: covers the overlaid field 1, built (v2) before the overlay.
1553            create_test_index("stale", 1, 2, covering(), false),
1554            // Not stale: covers field 1 but built at the overlay's version (v5);
1555            // `committed_version > dataset_version` is false at equality.
1556            create_test_index("fresh", 1, 5, covering(), false),
1557            // Unrelated: covers field 2, which the overlay never touched.
1558            create_test_index("unrelated", 2, 2, covering(), false),
1559            // Stale, after a deferred remap: still covers the old fragment (0).
1560            create_test_index(
1561                "deferred",
1562                1,
1563                2,
1564                Some(RoaringBitmap::from_iter([0u32])),
1565                false,
1566            ),
1567        ];
1568
1569        Transaction::prune_overlay_stale_fields_from_indices(&mut indices, &groups);
1570
1571        assert!(
1572            !indices[0].fragment_bitmap.as_ref().unwrap().contains(7),
1573            "stale index must drop the rewritten fragment from its coverage"
1574        );
1575        assert!(
1576            indices[1].fragment_bitmap.as_ref().unwrap().contains(7),
1577            "an index built at/after the overlay is not stale"
1578        );
1579        assert!(
1580            indices[2].fragment_bitmap.as_ref().unwrap().contains(7),
1581            "an index on an un-overlaid field is unaffected"
1582        );
1583        assert!(
1584            indices[3].fragment_bitmap.as_ref().unwrap().is_empty(),
1585            "stale index must drop the old fragment when the remap is deferred"
1586        );
1587    }
1588
1589    // Helper functions for retain_relevant_indices tests
1590    fn create_test_index(
1591        name: &str,
1592        field_id: i32,
1593        dataset_version: u64,
1594        fragment_bitmap: Option<RoaringBitmap>,
1595        is_vector: bool,
1596    ) -> IndexMetadata {
1597        use prost_types::Any;
1598        use std::sync::Arc;
1599
1600        let index_details = if is_vector {
1601            Some(Arc::new(Any {
1602                type_url: "type.googleapis.com/lance.index.VectorIndexDetails".to_string(),
1603                value: vec![],
1604            }))
1605        } else {
1606            Some(Arc::new(Any {
1607                type_url: "type.googleapis.com/lance.index.ScalarIndexDetails".to_string(),
1608                value: vec![],
1609            }))
1610        };
1611
1612        IndexMetadata {
1613            uuid: Uuid::new_v4(),
1614            fields: vec![field_id],
1615            covering_fields: vec![],
1616            name: name.to_string(),
1617            dataset_version,
1618            fragment_bitmap,
1619            index_details,
1620            index_version: 1,
1621            created_at: None,
1622            base_id: None,
1623            files: None,
1624        }
1625    }
1626
1627    /// A version-0 fragment reuse entry: the pre-tagged history whose
1628    /// presence leaves coverage-based retention as it always was.
1629    fn legacy_frag_reuse_index(field_id: i32) -> IndexMetadata {
1630        let mut index = create_system_index(FRAG_REUSE_INDEX_NAME, field_id);
1631        index.index_version = 0;
1632        index
1633    }
1634
1635    /// Under a tagged history a translating segment's stored bitmap is its
1636    /// retired provenance, empty against the live fragments by construction,
1637    /// so retention must keep every segment: only a segment on a field that
1638    /// left the schema is dropped.
1639    #[test]
1640    fn test_retain_keeps_every_segment_under_tagged_history() {
1641        let schema = create_test_schema(&[1, 2]);
1642        let fragments = vec![Fragment::new(10), Fragment::new(11)];
1643        let mut indices = vec![
1644            create_system_index(FRAG_REUSE_INDEX_NAME, 1),
1645            // Two translating segments: provenance {0} and {1} are both dead.
1646            create_test_index("idx_a", 1, 1, Some(RoaringBitmap::from_iter([0])), false),
1647            create_test_index("idx_a", 1, 2, Some(RoaringBitmap::from_iter([1])), false),
1648            // A rebuilt sibling directly covering a live destination.
1649            create_test_index("idx_a", 1, 3, Some(RoaringBitmap::from_iter([10])), false),
1650            // An all-dead group and an all-empty group: every member kept.
1651            create_test_index("vec_b", 1, 1, Some(RoaringBitmap::from_iter([0])), true),
1652            create_test_index("vec_b", 1, 2, Some(RoaringBitmap::from_iter([1])), true),
1653            create_test_index("idx_c", 2, 1, Some(RoaringBitmap::new()), false),
1654            create_test_index("idx_c", 2, 2, Some(RoaringBitmap::new()), false),
1655            // Field left the schema: dropped as always.
1656            create_test_index("idx_e", 99, 1, Some(RoaringBitmap::from_iter([10])), false),
1657        ];
1658        assert!(indices.iter().any(is_tagged));
1659
1660        Transaction::retain_relevant_indices(&mut indices, &schema, &fragments);
1661
1662        assert_eq!(indices.len(), 8);
1663        assert!(!indices.iter().any(|idx| idx.name == "idx_e"));
1664        assert_eq!(indices.iter().filter(|idx| idx.name == "idx_a").count(), 3);
1665        assert_eq!(indices.iter().filter(|idx| idx.name == "vec_b").count(), 2);
1666        assert_eq!(indices.iter().filter(|idx| idx.name == "idx_c").count(), 2);
1667    }
1668
1669    fn create_system_index(name: &str, field_id: i32) -> IndexMetadata {
1670        use prost_types::Any;
1671        use std::sync::Arc;
1672
1673        IndexMetadata {
1674            uuid: Uuid::new_v4(),
1675            fields: vec![field_id],
1676            covering_fields: vec![],
1677            name: name.to_string(),
1678            dataset_version: 1,
1679            fragment_bitmap: Some(RoaringBitmap::from_iter([1, 2])),
1680            index_details: Some(Arc::new(Any {
1681                type_url: "type.googleapis.com/lance.index.SystemIndexDetails".to_string(),
1682                value: vec![],
1683            })),
1684            index_version: 1,
1685            created_at: None,
1686            base_id: None,
1687            files: None,
1688        }
1689    }
1690
1691    fn create_test_schema(field_ids: &[i32]) -> Schema {
1692        use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
1693        use lance_core::datatypes::Schema as LanceSchema;
1694
1695        let fields: Vec<ArrowField> = field_ids
1696            .iter()
1697            .map(|id| ArrowField::new(format!("field_{}", id), DataType::Int32, false))
1698            .collect();
1699
1700        let arrow_schema = ArrowSchema::new(fields);
1701        let mut lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
1702
1703        // Assign field IDs
1704        for (i, field_id) in field_ids.iter().enumerate() {
1705            lance_schema.mut_field_by_id(i as i32).unwrap().id = *field_id;
1706        }
1707
1708        lance_schema
1709    }
1710}
1711
1712#[cfg(test)]
1713mod withdrawal_tests {
1714    use super::*;
1715    use crate::transaction::test_support::{sample_index_metadata, tagged_entry};
1716
1717    fn segment(bitmap: Option<&[u32]>, fields: Vec<i32>) -> IndexMetadata {
1718        let mut segment = sample_index_metadata("idx");
1719        segment.fields = fields;
1720        segment.fragment_bitmap = bitmap.map(|ids| ids.iter().copied().collect());
1721        segment
1722    }
1723
1724    fn lineage(entry: &IndexMetadata) -> TaggedLineage {
1725        TaggedLineage::from_indices_with_ledger(std::slice::from_ref(entry), None)
1726            .unwrap()
1727            .unwrap()
1728    }
1729
1730    /// F1, F2 -> F3 then F3 -> F4 (F4 live), plus an unrelated F6 -> F8.
1731    fn history() -> IndexMetadata {
1732        tagged_entry(&[(&[1, 2], &[3]), (&[3], &[4]), (&[6], &[8])])
1733    }
1734
1735    #[test]
1736    fn withdraws_the_whole_transition_reaching_the_rewritten_fragment() {
1737        let entry = history();
1738        let live: RoaringBitmap = [4u32, 8, 9].into_iter().collect();
1739        // Provenance {1, 2} reaches F4 through F3; F6 (retired, on the
1740        // unrelated F6 -> F8 lineage) is untouched and keeps the segment on
1741        // the translating path, so it survives with {6}.
1742        let mut indices = vec![segment(Some(&[1, 2, 6]), vec![0]), entry.clone()];
1743        Transaction::withdraw_rewritten_coverage(
1744            &mut indices,
1745            Some(&lineage(&entry)),
1746            &live,
1747            &[(4, vec![0])],
1748        );
1749        assert_eq!(
1750            indices[0].fragment_bitmap.as_ref().unwrap(),
1751            &RoaringBitmap::from_iter([6u32])
1752        );
1753        // The entry itself is never touched.
1754        assert_eq!(indices[1].fragment_bitmap, entry.fragment_bitmap);
1755    }
1756
1757    /// The file of a segment whose retired sources were withdrawn still
1758    /// holds their rows. A remainder that is live-only and unknown to the
1759    /// history would be loaded as it is and serve those rows raw, so it is
1760    /// withdrawn whole (the segment empties and is rebuilt).
1761    #[test]
1762    fn live_only_remainder_after_withdrawing_retired_sources_goes_whole() {
1763        let entry = history();
1764        let live: RoaringBitmap = [4u32, 8, 9].into_iter().collect();
1765        let mut indices = vec![segment(Some(&[1, 2, 9]), vec![0]), entry.clone()];
1766        Transaction::withdraw_rewritten_coverage(
1767            &mut indices,
1768            Some(&lineage(&entry)),
1769            &live,
1770            &[(4, vec![0])],
1771        );
1772        assert!(
1773            indices[0].fragment_bitmap.as_ref().unwrap().is_empty(),
1774            "{:?}",
1775            indices[0].fragment_bitmap
1776        );
1777    }
1778
1779    /// A remainder the history mentions (F8 is a destination) is translated
1780    /// and filtered by the reader, so partial survival is safe there.
1781    #[test]
1782    fn remainder_the_history_mentions_survives() {
1783        let entry = history();
1784        let live: RoaringBitmap = [4u32, 8, 9].into_iter().collect();
1785        let mut indices = vec![segment(Some(&[1, 2, 8]), vec![0])];
1786        Transaction::withdraw_rewritten_coverage(
1787            &mut indices,
1788            Some(&lineage(&entry)),
1789            &live,
1790            &[(4, vec![0])],
1791        );
1792        assert_eq!(
1793            indices[0].fragment_bitmap.as_ref().unwrap(),
1794            &RoaringBitmap::from_iter([8u32])
1795        );
1796    }
1797
1798    /// Withdrawing only a live fragment (an index built after the rewrite
1799    /// that produced F4) leaves the remainder alone: rows of fragments that
1800    /// still exist are masked by the ordinary per-segment ownership filter.
1801    #[test]
1802    fn withdrawing_only_a_live_fragment_keeps_the_remainder() {
1803        let entry = history();
1804        let live: RoaringBitmap = [4u32, 8, 9].into_iter().collect();
1805        let mut indices = vec![segment(Some(&[4, 9]), vec![0])];
1806        Transaction::withdraw_rewritten_coverage(
1807            &mut indices,
1808            Some(&lineage(&entry)),
1809            &live,
1810            &[(4, vec![0])],
1811        );
1812        assert_eq!(
1813            indices[0].fragment_bitmap.as_ref().unwrap(),
1814            &RoaringBitmap::from_iter([9u32])
1815        );
1816    }
1817
1818    #[test]
1819    fn mixed_provenance_loses_the_direct_id_and_the_sources() {
1820        let entry = history();
1821        let live: RoaringBitmap = [4u32, 8].into_iter().collect();
1822        // {3} is an intermediate the merge kept beside the live F4.
1823        let mut indices = vec![segment(Some(&[3, 4]), vec![0])];
1824        Transaction::withdraw_rewritten_coverage(
1825            &mut indices,
1826            Some(&lineage(&entry)),
1827            &live,
1828            &[(4, vec![0])],
1829        );
1830        assert!(indices[0].fragment_bitmap.as_ref().unwrap().is_empty());
1831    }
1832
1833    #[test]
1834    fn unrelated_lineage_unindexed_field_and_missing_bitmap_are_untouched() {
1835        let entry = history();
1836        let live: RoaringBitmap = [4u32, 8].into_iter().collect();
1837        let mut indices = vec![
1838            segment(Some(&[6]), vec![0]),
1839            segment(Some(&[1, 2]), vec![1]),
1840            segment(None, vec![0]),
1841        ];
1842        Transaction::withdraw_rewritten_coverage(
1843            &mut indices,
1844            Some(&lineage(&entry)),
1845            &live,
1846            &[(4, vec![0])],
1847        );
1848        assert_eq!(
1849            indices[0].fragment_bitmap.as_ref().unwrap(),
1850            &RoaringBitmap::from_iter([6u32])
1851        );
1852        assert_eq!(
1853            indices[1].fragment_bitmap.as_ref().unwrap(),
1854            &RoaringBitmap::from_iter([1u32, 2])
1855        );
1856        assert!(indices[2].fragment_bitmap.is_none());
1857    }
1858
1859    #[test]
1860    fn without_a_walkable_history_every_retired_fragment_goes() {
1861        let entry = history();
1862        let live: RoaringBitmap = [4u32, 8, 9].into_iter().collect();
1863        let mut indices = vec![segment(Some(&[6, 9]), vec![0])];
1864        Transaction::withdraw_rewritten_coverage(
1865            &mut indices,
1866            Some(&TaggedLineage::new(&entry, None)),
1867            &live,
1868            &[(4, vec![0])],
1869        );
1870        assert_eq!(
1871            indices[0].fragment_bitmap.as_ref().unwrap(),
1872            &RoaringBitmap::from_iter([9u32])
1873        );
1874    }
1875}