1use std::sync::Arc;
5
6use crate::Dataset;
7use crate::dataset::transaction::{Operation, Transaction};
8use crate::index::DatasetIndexInternalExt;
9use crate::index::frag_reuse::{build_frag_reuse_index_metadata, load_frag_reuse_index_details};
10use lance_core::{Error, Result};
11use lance_index::frag_reuse::{
12 CompactFragReuseIndex, FRAG_REUSE_INDEX_NAME, FragReuseIndexDetails, FragReuseVersion,
13};
14use lance_index::is_system_index;
15use lance_index::metrics::NoOpMetricsCollector;
16use lance_table::format::IndexMetadata;
17use lance_table::io::manifest::read_manifest_indexes;
18use log::warn;
19use roaring::RoaringBitmap;
20
21impl Dataset {
22 pub async fn frag_reuse_index(&self) -> Result<Option<Arc<CompactFragReuseIndex>>> {
72 self.open_frag_reuse_index(&NoOpMetricsCollector).await
73 }
74}
75
76pub async fn cleanup_frag_reuse_index(dataset: &mut Dataset) -> lance_core::Result<()> {
109 let indices = read_manifest_indexes(
111 &dataset.object_store,
112 &dataset.manifest_location,
113 &dataset.manifest,
114 )
115 .await?;
116 let Some(frag_reuse_index_meta) = indices.iter().find(|idx| idx.name == FRAG_REUSE_INDEX_NAME)
117 else {
118 return Ok(());
119 };
120
121 let frag_reuse_details = load_frag_reuse_index_details(dataset, frag_reuse_index_meta).await?;
122
123 let chain_frag_bitmap = reuse_chain_frag_bitmap(&frag_reuse_details.versions);
124
125 let mut retained_versions = Vec::new();
126 let mut fragment_bitmaps = RoaringBitmap::new();
127 for version in frag_reuse_details.versions.iter() {
128 let check_results = indices
129 .iter()
130 .map(|idx| is_index_remap_caught_up(version, idx, &chain_frag_bitmap))
131 .collect::<Vec<_>>();
132
133 if check_results
134 .iter()
135 .any(|r| matches!(r, Err(Error::InvalidInput { .. })))
136 {
137 continue;
139 }
140
141 if !check_results.into_iter().all(|r| r.unwrap()) {
142 fragment_bitmaps.extend(version.new_frag_bitmap());
143 retained_versions.push(version.clone());
144 }
145 }
146
147 if retained_versions.len() == frag_reuse_details.versions.len() {
149 return Ok(());
150 }
151
152 let frag_reuse_index_details = FragReuseIndexDetails {
153 versions: retained_versions,
154 };
155
156 let new_index_meta = build_frag_reuse_index_metadata(
157 dataset,
158 Some(frag_reuse_index_meta),
159 frag_reuse_index_details,
160 fragment_bitmaps,
161 )
162 .await?;
163
164 let transaction = Transaction::new(
165 dataset.manifest.version,
166 Operation::CreateIndex {
167 new_indices: vec![new_index_meta],
168 removed_indices: vec![frag_reuse_index_meta.clone()],
169 },
170 None,
171 );
172
173 dataset
174 .apply_commit(transaction, &Default::default(), &Default::default())
175 .await?;
176
177 Ok(())
178}
179
180fn reuse_chain_frag_bitmap(versions: &[FragReuseVersion]) -> RoaringBitmap {
184 let mut bitmap = RoaringBitmap::new();
185 for version in versions {
186 bitmap.extend(version.old_frag_ids().iter().map(|&id| id as u32));
187 bitmap.extend(version.new_frag_ids().iter().map(|&id| id as u32));
188 }
189 bitmap
190}
191
192fn is_index_remap_caught_up(
193 frag_reuse_version: &FragReuseVersion,
194 index_meta: &IndexMetadata,
195 chain_frag_bitmap: &RoaringBitmap,
196) -> lance_core::Result<bool> {
197 if is_system_index(index_meta) {
198 return Ok(true);
199 }
200
201 if let Some(index_frag_bitmap) = &index_meta.fragment_bitmap
207 && index_frag_bitmap.is_disjoint(chain_frag_bitmap)
208 {
209 return Ok(true);
210 }
211
212 if index_meta.dataset_version < frag_reuse_version.dataset_version {
213 return Ok(false);
214 }
215
216 match index_meta.fragment_bitmap.clone() {
217 Some(index_frag_bitmap) => {
218 for group in frag_reuse_version.groups.iter() {
219 let mut old_frag_in_index = 0;
220 for old_frag in group.old_frags.iter() {
221 if index_frag_bitmap.contains(old_frag.id as u32) {
222 old_frag_in_index += 1;
223 }
224 }
225
226 if old_frag_in_index > 0 {
227 if old_frag_in_index != group.old_frags.len() {
228 return Err(Error::invalid_input(format!(
233 "The compaction plan included a rewrite group that was a split of indexed and non-indexed data: {:?}",
234 group.old_frags
235 )));
236 }
237 return Ok(false);
238 }
239 }
240 Ok(true)
241 }
242 None => {
243 warn!(
244 "Index {} ({}) missing fragment bitmap, cannot determine if it is caught up with the fragment reuse version, consider retraining the index",
245 index_meta.name, index_meta.uuid
246 );
247 Ok(true)
248 }
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::dataset::optimize::{CompactionOptions, compact_files, remapping};
256 use crate::index::DatasetIndexExt;
257 use crate::utils::test::{DatagenExt, FragmentCount, FragmentRowCount};
258 use all_asserts::{assert_false, assert_true};
259 use arrow_array::cast::AsArray;
260 use arrow_array::types::{Float32Type, Int32Type};
261 use lance_core::ROW_ADDR;
262 use lance_core::utils::address::RowAddress;
263 use lance_datagen::Dimension;
264 use lance_index::IndexType;
265 use lance_index::scalar::ScalarIndexParams;
266 use std::collections::HashMap;
267
268 fn frag_digest(id: u64) -> lance_index::frag_reuse::FragDigest {
269 lance_index::frag_reuse::FragDigest {
270 id,
271 physical_rows: 100,
272 num_deleted_rows: 0,
273 }
274 }
275
276 fn reuse_version(dataset_version: u64, old: &[u64], new: &[u64]) -> FragReuseVersion {
277 FragReuseVersion {
278 dataset_version,
279 groups: vec![lance_index::frag_reuse::FragReuseGroup {
280 changed_row_addrs: Vec::new(),
281 old_frags: old.iter().copied().map(frag_digest).collect(),
282 new_frags: new.iter().copied().map(frag_digest).collect(),
283 }],
284 }
285 }
286
287 fn index_covering(dataset_version: u64, covered: &[u32]) -> IndexMetadata {
288 IndexMetadata {
289 uuid: uuid::Uuid::new_v4(),
290 fields: vec![0],
291 covering_fields: vec![],
292 name: "test_idx".into(),
293 dataset_version,
294 fragment_bitmap: Some(RoaringBitmap::from_iter(covered.iter().copied())),
295 index_details: None,
296 index_version: 0,
297 created_at: None,
298 base_id: None,
299 files: None,
300 }
301 }
302
303 #[test]
307 fn test_caught_up_uses_fragment_coverage_not_only_version() {
308 let version = reuse_version(10, &[4, 5], &[6]);
310 let chain = reuse_chain_frag_bitmap(std::slice::from_ref(&version));
311
312 assert_true!(
315 is_index_remap_caught_up(&version, &index_covering(5, &[1, 2, 3]), &chain).unwrap()
316 );
317
318 assert_false!(
320 is_index_remap_caught_up(&version, &index_covering(5, &[1, 4, 5]), &chain).unwrap()
321 );
322
323 assert_false!(
326 is_index_remap_caught_up(&version, &index_covering(5, &[1, 6]), &chain).unwrap()
327 );
328
329 assert_true!(
331 is_index_remap_caught_up(&version, &index_covering(11, &[1, 6]), &chain).unwrap()
332 );
333 }
334
335 #[test]
339 fn test_caught_up_uses_whole_reuse_chain() {
340 let v1 = reuse_version(10, &[4, 5], &[6]); let v2 = reuse_version(11, &[6], &[7]); let chain = reuse_chain_frag_bitmap(&[v1.clone(), v2]);
343
344 assert_false!(is_index_remap_caught_up(&v1, &index_covering(5, &[1, 7]), &chain).unwrap());
347 }
348
349 #[test]
354 fn test_caught_up_handles_fragment_removal() {
355 let version = reuse_version(20, &[7], &[]);
357 let chain = reuse_chain_frag_bitmap(std::slice::from_ref(&version));
358
359 assert_true!(is_index_remap_caught_up(&version, &index_covering(5, &[]), &chain).unwrap());
361
362 assert_false!(
364 is_index_remap_caught_up(&version, &index_covering(5, &[7]), &chain).unwrap()
365 );
366 }
367
368 #[tokio::test]
369 async fn test_cleanup_frag_reuse_index() {
370 let mut dataset = lance_datagen::gen_batch()
371 .col(
372 "vec",
373 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
374 )
375 .col("i", lance_datagen::array::step::<Int32Type>())
376 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
377 .await
378 .unwrap();
379
380 let index_name = Some("scalar".into());
382 dataset
383 .create_index(
384 &["i"],
385 IndexType::Scalar,
386 index_name.clone(),
387 &ScalarIndexParams::default(),
388 false,
389 )
390 .await
391 .unwrap();
392
393 compact_files(
395 &mut dataset,
396 CompactionOptions {
397 target_rows_per_fragment: 2_000,
398 defer_index_remap: true,
399 ..Default::default()
400 },
401 None,
402 )
403 .await
404 .unwrap();
405 let Some(frag_reuse_index_meta) = dataset
406 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
407 .await
408 .unwrap()
409 else {
410 panic!("Fragment reuse index must be available");
411 };
412 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
413 .await
414 .unwrap();
415 assert_eq!(frag_reuse_details.versions.len(), 1);
416 let indices = dataset.load_indices().await.unwrap();
417 let scalar_index = indices.iter().find(|idx| idx.name == "scalar").unwrap();
418 assert_false!(
420 is_index_remap_caught_up(
421 &frag_reuse_details.versions[0],
422 scalar_index,
423 &reuse_chain_frag_bitmap(&frag_reuse_details.versions),
424 )
425 .unwrap()
426 );
427
428 remapping::remap_column_index(&mut dataset, &["i"], index_name.clone())
430 .await
431 .unwrap();
432 let indices = dataset.load_indices().await.unwrap();
433 let scalar_index = indices.iter().find(|idx| idx.name == "scalar").unwrap();
434 assert_true!(
435 is_index_remap_caught_up(
436 &frag_reuse_details.versions[0],
437 scalar_index,
438 &reuse_chain_frag_bitmap(&frag_reuse_details.versions),
439 )
440 .unwrap()
441 );
442
443 let mut dataset_clone = dataset.clone();
445 cleanup_frag_reuse_index(&mut dataset).await.unwrap();
446 let Some(frag_reuse_index_meta) = dataset
447 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
448 .await
449 .unwrap()
450 else {
451 panic!("Fragment reuse index must be available");
452 };
453 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
454 .await
455 .unwrap();
456 assert_eq!(frag_reuse_details.versions.len(), 0);
457
458 assert!(matches!(
460 cleanup_frag_reuse_index(&mut dataset_clone).await,
461 Err(Error::RetryableCommitConflict { .. })
462 ));
463 }
464
465 #[tokio::test]
477 async fn test_cleanup_frag_reuse_index_multiple_indices() {
478 let mut dataset = lance_datagen::gen_batch()
479 .col("i", lance_datagen::array::step::<Int32Type>())
480 .col("j", lance_datagen::array::step::<Int32Type>())
481 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
482 .await
483 .unwrap();
484
485 for col in ["i", "j"] {
486 dataset
487 .create_index(
488 &[col],
489 IndexType::Scalar,
490 Some(format!("{col}_idx")),
491 &ScalarIndexParams::default(),
492 false,
493 )
494 .await
495 .unwrap();
496 }
497
498 compact_files(
499 &mut dataset,
500 CompactionOptions {
501 target_rows_per_fragment: 2_000,
502 defer_index_remap: true,
503 ..Default::default()
504 },
505 None,
506 )
507 .await
508 .unwrap();
509
510 let frag_reuse_index_meta = dataset
511 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
512 .await
513 .unwrap()
514 .expect("Fragment reuse index must be available");
515 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
516 .await
517 .unwrap();
518 assert_eq!(frag_reuse_details.versions.len(), 1);
519
520 for col in ["i", "j"] {
521 remapping::remap_column_index(&mut dataset, &[col], Some(format!("{col}_idx")))
522 .await
523 .unwrap();
524 }
525
526 let indices = dataset.load_indices().await.unwrap();
528 for col in ["i", "j"] {
529 let index = indices
530 .iter()
531 .find(|idx| idx.name == format!("{col}_idx"))
532 .unwrap();
533 assert!(
534 is_index_remap_caught_up(
535 &frag_reuse_details.versions[0],
536 index,
537 &reuse_chain_frag_bitmap(&frag_reuse_details.versions),
538 )
539 .unwrap(),
540 "index {col}_idx was not caught up after remap"
541 );
542 }
543
544 cleanup_frag_reuse_index(&mut dataset).await.unwrap();
546 let frag_reuse_index_meta = dataset
547 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
548 .await
549 .unwrap()
550 .expect("Fragment reuse index must be available");
551 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
552 .await
553 .unwrap();
554 assert_eq!(frag_reuse_details.versions.len(), 0);
555
556 use futures::TryStreamExt;
562 for col in ["i", "j"] {
563 let rows: usize = dataset
564 .scan()
565 .filter(&format!("{col} >= 2000 AND {col} < 3000"))
566 .unwrap()
567 .try_into_stream()
568 .await
569 .unwrap()
570 .try_collect::<Vec<_>>()
571 .await
572 .unwrap()
573 .iter()
574 .map(|b| b.num_rows())
575 .sum();
576 assert_eq!(
577 rows, 1000,
578 "index {col}_idx must resolve to live rows after remap+trim"
579 );
580 }
581 }
582
583 #[tokio::test]
587 async fn test_remap_index_batches_multiple_reuse_versions() {
588 let mut dataset = lance_datagen::gen_batch()
589 .col("i", lance_datagen::array::step::<Int32Type>())
590 .into_ram_dataset(FragmentCount::from(8), FragmentRowCount::from(1000))
591 .await
592 .unwrap();
593 dataset
594 .create_index(
595 &["i"],
596 IndexType::Scalar,
597 Some("i_idx".into()),
598 &ScalarIndexParams::default(),
599 false,
600 )
601 .await
602 .unwrap();
603
604 let options = CompactionOptions {
607 target_rows_per_fragment: 4_000,
608 defer_index_remap: true,
609 ..Default::default()
610 };
611 for round in 0..4 {
612 dataset
613 .delete(&format!("i < {}", 1_000 * (round + 1)))
614 .await
615 .unwrap();
616 compact_files(&mut dataset, options.clone(), None)
617 .await
618 .unwrap();
619 }
620
621 let frag_reuse_index_meta = dataset
622 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
623 .await
624 .unwrap()
625 .expect("Fragment reuse index must be available");
626 let num_versions = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
627 .await
628 .unwrap()
629 .versions
630 .len();
631 assert!(
632 num_versions >= 2,
633 "test needs multiple reuse versions to exercise batching, got {num_versions}"
634 );
635
636 let version_before = dataset.manifest.version;
638 remapping::remap_column_index(&mut dataset, &["i"], Some("i_idx".into()))
639 .await
640 .unwrap();
641 let commits = dataset.manifest.version - version_before;
642 assert_eq!(
643 commits, 1,
644 "batched remap must commit once, not once per reuse version ({num_versions})"
645 );
646
647 cleanup_frag_reuse_index(&mut dataset).await.unwrap();
649 let frag_reuse_index_meta = dataset
650 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
651 .await
652 .unwrap()
653 .expect("Fragment reuse index must be available");
654 assert_eq!(
655 load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
656 .await
657 .unwrap()
658 .versions
659 .len(),
660 0
661 );
662 }
663
664 async fn row_addrs_by_i(dataset: &Dataset) -> HashMap<i32, u64> {
665 let batch = dataset
666 .scan()
667 .project(&["i"])
668 .unwrap()
669 .with_row_address()
670 .try_into_batch()
671 .await
672 .unwrap();
673 let ids = batch["i"].as_primitive::<Int32Type>();
674 let addrs = batch[ROW_ADDR].as_primitive::<arrow_array::types::UInt64Type>();
675 ids.values()
676 .iter()
677 .copied()
678 .zip(addrs.values().iter().copied())
679 .collect()
680 }
681
682 #[tokio::test]
683 async fn test_frag_reuse_index_accessor() {
684 let mut dataset = lance_datagen::gen_batch()
685 .col("i", lance_datagen::array::step::<Int32Type>())
686 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
687 .await
688 .unwrap();
689
690 assert!(dataset.frag_reuse_index().await.unwrap().is_none());
691
692 dataset.delete("i < 200").await.unwrap();
694 compact_files(
695 &mut dataset,
696 CompactionOptions {
697 target_rows_per_fragment: 1_000,
698 ..Default::default()
699 },
700 None,
701 )
702 .await
703 .unwrap();
704 assert!(dataset.frag_reuse_index().await.unwrap().is_none());
705 let num_fragments = dataset.fragments().len();
706 assert_eq!(num_fragments, 6);
707
708 dataset
709 .create_index(
710 &["i"],
711 IndexType::Scalar,
712 Some("i_idx".into()),
713 &ScalarIndexParams::default(),
714 false,
715 )
716 .await
717 .unwrap();
718
719 dataset.delete("i >= 1000 AND i < 1250").await.unwrap();
721 dataset.delete("i >= 2000 AND i < 2250").await.unwrap();
722 let before = row_addrs_by_i(&dataset).await;
723 let pre_compaction_version = dataset.version().version;
724 let rewritten_frags = [
725 RowAddress::from(before[&1250]).fragment_id(),
726 RowAddress::from(before[&2250]).fragment_id(),
727 ];
728 let untouched_addr = before[&5000];
729 let deleted_addr = u64::from(RowAddress::new_from_parts(rewritten_frags[0], 0));
731
732 compact_files(
733 &mut dataset,
734 CompactionOptions {
735 target_rows_per_fragment: 1_000,
736 defer_index_remap: true,
737 ..Default::default()
738 },
739 None,
740 )
741 .await
742 .unwrap();
743 let after = row_addrs_by_i(&dataset).await;
744 for frag in rewritten_frags {
745 assert!(
746 dataset.fragments().iter().all(|f| f.id != u64::from(frag)),
747 "fragment {frag} should have been rewritten"
748 );
749 }
750
751 let frag_reuse_index = dataset
752 .frag_reuse_index()
753 .await
754 .unwrap()
755 .expect("deferred compaction must record an FRI");
756 let frag_reuse_index_meta = dataset
757 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
758 .await
759 .unwrap()
760 .expect("FRI must be in the manifest");
761 assert_eq!(frag_reuse_index.uuid, frag_reuse_index_meta.uuid);
762 assert_eq!(frag_reuse_index.details.versions.len(), 1);
763 assert_false!(frag_reuse_index.is_empty());
764
765 let again = dataset.frag_reuse_index().await.unwrap().unwrap();
767 assert!(Arc::ptr_eq(&frag_reuse_index, &again));
768
769 let remap = frag_reuse_index.row_addr_remap();
770 for i in [1250, 1999, 2250, 2999] {
771 assert_eq!(
772 remap.get(before[&i]),
773 Some(Some(after[&i])),
774 "row i={i} should have moved"
775 );
776 assert_eq!(frag_reuse_index.remap_row_id(before[&i]), Some(after[&i]));
777 }
778 assert_eq!(remap.get(deleted_addr), Some(None));
779 assert_eq!(frag_reuse_index.remap_row_id(deleted_addr), None);
780 assert_eq!(remap.get(untouched_addr), None);
781 assert_eq!(after[&5000], untouched_addr);
782 assert_eq!(
783 frag_reuse_index.remap_row_id(untouched_addr),
784 Some(untouched_addr)
785 );
786
787 let pre_compaction = dataset
788 .checkout_version(pre_compaction_version)
789 .await
790 .unwrap();
791 assert!(pre_compaction.frag_reuse_index().await.unwrap().is_none());
792
793 remapping::remap_column_index(&mut dataset, &["i"], Some("i_idx".into()))
794 .await
795 .unwrap();
796 cleanup_frag_reuse_index(&mut dataset).await.unwrap();
797 let trimmed = dataset
798 .frag_reuse_index()
799 .await
800 .unwrap()
801 .expect("trimmed FRI keeps an (empty) manifest entry");
802 assert_true!(trimmed.is_empty());
803 assert_eq!(trimmed.details.versions.len(), 0);
804 assert_ne!(trimmed.uuid, frag_reuse_index.uuid);
805 assert_eq!(trimmed.row_addr_remap().get(before[&1250]), None);
806 assert_eq!(
807 trimmed.remap_row_id(before[&1250]),
808 Some(before[&1250]),
809 "trimmed history passes a moved address through unchanged"
810 );
811 }
812
813 #[tokio::test]
816 async fn test_frag_reuse_index_mapped_destination_can_be_removed() {
817 let mut dataset = lance_datagen::gen_batch()
818 .col("i", lance_datagen::array::step::<Int32Type>())
819 .into_ram_dataset(FragmentCount::from(4), FragmentRowCount::from(1000))
820 .await
821 .unwrap();
822 dataset
823 .create_index(
824 &["i"],
825 IndexType::Scalar,
826 Some("i_idx".into()),
827 &ScalarIndexParams::default(),
828 false,
829 )
830 .await
831 .unwrap();
832
833 dataset.delete("i < 250").await.unwrap();
835 dataset.delete("i >= 1000 AND i < 1250").await.unwrap();
836 let before = row_addrs_by_i(&dataset).await;
837 compact_files(
838 &mut dataset,
839 CompactionOptions {
840 target_rows_per_fragment: 1_000,
841 defer_index_remap: true,
842 ..Default::default()
843 },
844 None,
845 )
846 .await
847 .unwrap();
848 let after = row_addrs_by_i(&dataset).await;
849 let moved_rows = [250, 999, 1250, 1999];
850 let destination_frags: Vec<u32> = moved_rows
851 .iter()
852 .map(|i| RowAddress::from(after[i]).fragment_id())
853 .collect();
854 for i in moved_rows {
855 assert_ne!(before[&i], after[&i], "row i={i} should have moved");
856 }
857
858 dataset.delete("i < 2000").await.unwrap();
859 let remaining_frag_ids: Vec<u64> = dataset.fragments().iter().map(|f| f.id).collect();
860 for frag in &destination_frags {
861 assert!(
862 !remaining_frag_ids.contains(&u64::from(*frag)),
863 "destination fragment {frag} should be removed; remaining: {remaining_frag_ids:?}"
864 );
865 }
866
867 let frag_reuse_index = dataset.frag_reuse_index().await.unwrap().unwrap();
868 assert_eq!(frag_reuse_index.details.versions.len(), 1);
869 for i in moved_rows {
870 assert_eq!(
871 frag_reuse_index.row_addr_remap().get(before[&i]),
872 Some(Some(after[&i])),
873 "row i={i} still maps into a removed fragment"
874 );
875 assert_eq!(frag_reuse_index.remap_row_id(before[&i]), Some(after[&i]));
876 }
877 }
878}