1use lance_core::utils::row_addr_remap::{GroupInput, RowAddrRemap};
84use std::borrow::Cow;
85use std::collections::{HashMap, HashSet};
86use std::io::Cursor;
87use std::ops::{AddAssign, Range};
88use std::sync::Arc;
89
90use super::fragment::FileFragment;
91use super::index::{DatasetIndexRemapperOptions, load_indices_for_remapping};
92use super::rowids::RowVersionKind;
93use super::rowids::{
94 RowLineage, RowLineagePlan, RowLineageSpill, inline_row_lineage_max_bytes,
95 load_row_id_sequences, load_row_version_sequence, place_row_lineage, plan_row_lineage_spill,
96};
97use super::transaction::{
98 Operation, RewriteGroup, RewrittenIndex, Transaction, TransactionBuilder,
99};
100use super::utils::make_rowid_capture_stream;
101use super::versions;
102use super::{
103 WriteMode, WriteParams, cleanup_data_fragments,
104 write::write_fragments_internal_with_file_row_counts,
105};
106use crate::Dataset;
107use crate::Result;
108use crate::dataset::utils::CapturedRowIds;
109use crate::index::{DatasetIndexInternalExt, index_is_usable, load_all_indices};
110use crate::io::commit::{commit_transaction, default_commit_retry_timeout, migrate_fragments};
111use arrow::array::AsArray;
112use arrow::datatypes::{UInt8Type, UInt32Type, UInt64Type};
113use arrow_array::builder::{LargeBinaryBuilder, PrimitiveBuilder, StringBuilder};
114use arrow_array::{
115 Array, ArrayRef, GenericListArray, OffsetSizeTrait, RecordBatch, StructArray, UInt32Array,
116};
117use arrow_buffer::{OffsetBuffer, ScalarBuffer};
118use arrow_schema::{
119 DataType as ArrowDataType, Field as ArrowField, Schema as ArrowSchema, SchemaRef,
120};
121use datafusion::physical_plan::SendableRecordBatchStream;
122use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
123use futures::future::BoxFuture;
124use futures::{FutureExt, StreamExt, TryStreamExt};
125use lance_arrow::{list::ListArrayExt, r#struct::StructArrayExt};
126use lance_core::Error;
127use lance_core::datatypes::{
128 BLOB_V2_LOGICAL_FIELDS, BLOB_V2_LOGICAL_TYPE, BlobHandling, BlobKind, BlobV2Layout,
129 Field as LanceField,
130};
131use lance_core::utils::tokio::get_num_compute_intensive_cpus;
132use lance_core::utils::tracing::{DATASET_COMPACTING_EVENT, TRACE_DATASET_EVENTS};
133use lance_file::version::{ConcreteFileVersion, LanceFileVersion};
134use lance_index::frag_reuse::{FRAG_REUSE_INDEX_NAME, FragReuseGroup};
135use lance_index::is_system_index;
136use lance_index::metrics::NoOpMetricsCollector;
137use lance_table::format::overlay::TOMBSTONE_FIELD_ID;
138use lance_table::format::{
139 Fragment, IndexMetadata, ROW_CREATED_AT_VERSION_FIELD_ID, ROW_ID_FIELD_ID,
140 ROW_LAST_UPDATED_AT_VERSION_FIELD_ID, RowDatasetVersionSequence,
141};
142use lance_table::rowids::RowIdSequence;
143use roaring::{RoaringBitmap, RoaringTreemap};
144use serde::{Deserialize, Serialize};
145use tracing::{info, warn};
146
147pub(super) mod binary_copy;
148pub mod remapping;
149
150use crate::index::frag_reuse::build_new_frag_reuse_index;
151use crate::io::deletion::read_dataset_deletion_file;
152pub use remapping::{IgnoreRemap, IndexRemapper, IndexRemapperOptions, RemappedIndex};
153
154#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
156pub enum CompactionMode {
157 Reencode,
159 TryBinaryCopy,
161 ForceBinaryCopy,
163}
164
165impl TryFrom<&str> for CompactionMode {
166 type Error = Error;
167
168 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
169 match value.to_lowercase().as_str() {
170 "reencode" => Ok(Self::Reencode),
171 "try_binary_copy" => Ok(Self::TryBinaryCopy),
172 "force_binary_copy" => Ok(Self::ForceBinaryCopy),
173 _ => Err(Error::invalid_input(format!(
174 "Invalid compaction mode \"{}\". Valid values: \"reencode\", \"try_binary_copy\", \"force_binary_copy\"",
175 value
176 ))),
177 }
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
184pub enum IndexRemapMode {
185 Compact,
190 #[default]
196 Direct,
197}
198
199impl TryFrom<&str> for IndexRemapMode {
200 type Error = Error;
201
202 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
203 match value.to_lowercase().as_str() {
204 "compact" => Ok(Self::Compact),
205 "direct" => Ok(Self::Direct),
206 _ => Err(Error::invalid_input(format!(
207 "Invalid index remap mode \"{}\". Valid values: \"compact\", \"direct\"",
208 value
209 ))),
210 }
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216pub struct CompactionOptions {
217 pub target_rows_per_fragment: usize,
223 pub max_rows_per_group: usize,
228 pub max_bytes_per_file: Option<usize>,
235 pub materialize_deletions: bool,
238 pub materialize_deletions_threshold: f32,
243 pub num_threads: Option<usize>,
247 pub batch_size: Option<usize>,
251 pub io_buffer_size: Option<u64>,
258 pub defer_index_remap: bool,
262 #[serde(default)]
265 pub index_remap_mode: IndexRemapMode,
266 pub compaction_mode: Option<CompactionMode>,
271 #[deprecated(note = "Use `compaction_mode` instead")]
273 pub enable_binary_copy: bool,
274 #[deprecated(note = "Use `compaction_mode` instead")]
276 pub enable_binary_copy_force: bool,
277 pub binary_copy_read_batch_bytes: Option<usize>,
281 pub max_source_fragments: Option<usize>,
287 pub max_source_rows: Option<usize>,
293 pub max_source_bytes: Option<u64>,
302 #[serde(default)]
309 pub excluded_fragment_ids: Vec<u32>,
310 pub max_overlays_per_fragment: Option<usize>,
319 pub data_storage_version: Option<LanceFileVersion>,
334 #[serde(skip)]
340 pub transaction_properties: Option<Arc<HashMap<String, String>>>,
341}
342
343#[allow(deprecated)]
344impl Default for CompactionOptions {
345 fn default() -> Self {
346 Self {
347 target_rows_per_fragment: 1024 * 1024,
349 max_rows_per_group: 1024,
350 materialize_deletions: true,
351 materialize_deletions_threshold: 0.1,
352 num_threads: None,
353 max_bytes_per_file: None,
354 batch_size: None,
355 io_buffer_size: None,
356 defer_index_remap: false,
357 index_remap_mode: IndexRemapMode::Direct,
358 compaction_mode: None,
359 enable_binary_copy: false,
360 enable_binary_copy_force: false,
361 binary_copy_read_batch_bytes: Some(16 * 1024 * 1024),
362 max_source_fragments: None,
363 max_source_rows: None,
364 max_source_bytes: None,
365 excluded_fragment_ids: Vec::new(),
366 max_overlays_per_fragment: Some(10),
367 data_storage_version: None,
368 transaction_properties: None,
369 }
370 }
371}
372
373pub const COMPACTION_CONFIG_PREFIX: &str = "lance.compaction.";
375
376#[allow(deprecated)]
377impl CompactionOptions {
378 pub fn from_dataset_config(config: &HashMap<String, String>) -> Result<Self> {
399 let mut opts = Self::default();
400 opts.apply_dataset_config(config)?;
401 Ok(opts)
402 }
403
404 pub fn apply_dataset_config(&mut self, config: &HashMap<String, String>) -> Result<()> {
409 for (key, value) in config {
410 let Some(field) = key.strip_prefix(COMPACTION_CONFIG_PREFIX) else {
411 continue;
412 };
413 match field {
414 "target_rows_per_fragment" => {
415 self.target_rows_per_fragment = value.parse().map_err(|_| {
416 Error::invalid_input(format!(
417 "Invalid value for {}: '{}' (expected a non-negative integer)",
418 key, value
419 ))
420 })?;
421 }
422 "max_rows_per_group" => {
423 self.max_rows_per_group = value.parse().map_err(|_| {
424 Error::invalid_input(format!(
425 "Invalid value for {}: '{}' (expected a non-negative integer)",
426 key, value
427 ))
428 })?;
429 }
430 "max_bytes_per_file" => {
431 self.max_bytes_per_file = Some(value.parse().map_err(|_| {
432 Error::invalid_input(format!(
433 "Invalid value for {}: '{}' (expected a non-negative integer)",
434 key, value
435 ))
436 })?);
437 }
438 "materialize_deletions" => {
439 self.materialize_deletions = match value.to_lowercase().as_str() {
440 "true" => true,
441 "false" => false,
442 _ => {
443 return Err(Error::invalid_input(format!(
444 "Invalid value for {}: '{}' (expected 'true' or 'false')",
445 key, value
446 )));
447 }
448 };
449 }
450 "materialize_deletions_threshold" => {
451 self.materialize_deletions_threshold = value.parse().map_err(|_| {
452 Error::invalid_input(format!(
453 "Invalid value for {}: '{}' (expected a float between 0.0 and 1.0)",
454 key, value
455 ))
456 })?;
457 }
458 "defer_index_remap" => {
459 self.defer_index_remap = match value.to_lowercase().as_str() {
460 "true" => true,
461 "false" => false,
462 _ => {
463 return Err(Error::invalid_input(format!(
464 "Invalid value for {}: '{}' (expected 'true' or 'false')",
465 key, value
466 )));
467 }
468 };
469 }
470 "index_remap_mode" => {
471 self.index_remap_mode = IndexRemapMode::try_from(value.as_str())?;
472 }
473 "batch_size" => {
474 self.batch_size = Some(value.parse().map_err(|_| {
475 Error::invalid_input(format!(
476 "Invalid value for {}: '{}' (expected a non-negative integer)",
477 key, value
478 ))
479 })?);
480 }
481 "io_buffer_size" => {
482 self.io_buffer_size = Some(value.parse().map_err(|_| {
483 Error::invalid_input(format!(
484 "Invalid value for {}: '{}' (expected a non-negative integer)",
485 key, value
486 ))
487 })?);
488 }
489 "compaction_mode" => {
490 self.compaction_mode = Some(CompactionMode::try_from(value.as_str())?);
491 }
492 "data_storage_version" => {
493 self.data_storage_version = Some(value.parse().map_err(|error| {
494 Error::invalid_input(format!(
495 "Invalid value for {}: '{}': {}",
496 key, value, error
497 ))
498 })?);
499 }
500 "binary_copy_read_batch_bytes" => {
501 self.binary_copy_read_batch_bytes = Some(value.parse().map_err(|_| {
502 Error::invalid_input(format!(
503 "Invalid value for {}: '{}' (expected a non-negative integer)",
504 key, value
505 ))
506 })?);
507 }
508 "max_source_fragments" => {
509 self.max_source_fragments = Some(value.parse().map_err(|_| {
510 Error::invalid_input(format!(
511 "Invalid value for {}: '{}' (expected a non-negative integer)",
512 key, value
513 ))
514 })?);
515 }
516 "max_source_rows" => {
517 self.max_source_rows = Some(value.parse().map_err(|_| {
518 Error::invalid_input(format!(
519 "Invalid value for {}: '{}' (expected a non-negative integer)",
520 key, value
521 ))
522 })?);
523 }
524 "max_source_bytes" => {
525 self.max_source_bytes = Some(value.parse().map_err(|_| {
526 Error::invalid_input(format!(
527 "Invalid value for {}: '{}' (expected a non-negative integer)",
528 key, value
529 ))
530 })?);
531 }
532 "max_overlays_per_fragment" => {
533 self.max_overlays_per_fragment = match value.to_ascii_lowercase().as_str() {
536 "none" => None,
537 _ => Some(value.parse().map_err(|_| {
538 Error::invalid_input(format!(
539 "Invalid value for {}: '{}' (expected a non-negative integer or 'none')",
540 key, value
541 ))
542 })?),
543 };
544 }
545 _ => {
546 warn!("Ignoring unknown compaction config key: {}", key);
547 }
548 }
549 }
550 Ok(())
551 }
552
553 pub fn validate(&mut self) -> Result<()> {
554 if self.materialize_deletions && self.materialize_deletions_threshold >= 1.0 {
556 self.materialize_deletions = false;
557 }
558
559 for (name, value) in [
560 (
561 "max_source_fragments",
562 self.max_source_fragments.map(|v| v as u64),
563 ),
564 ("max_source_rows", self.max_source_rows.map(|v| v as u64)),
565 ("max_source_bytes", self.max_source_bytes),
566 ] {
567 if value == Some(0) {
568 return Err(Error::invalid_input(format!(
569 "CompactionOptions::{} must be greater than 0 (use None for no limit)",
570 name
571 )));
572 }
573 }
574 Ok(())
575 }
576
577 pub fn compaction_mode(&self) -> CompactionMode {
581 if let Some(mode) = self.compaction_mode {
582 return mode;
583 }
584 match (self.enable_binary_copy, self.enable_binary_copy_force) {
586 (true, true) => CompactionMode::ForceBinaryCopy,
587 (true, false) => CompactionMode::TryBinaryCopy,
588 _ => CompactionMode::Reencode,
589 }
590 }
591
592 pub fn transaction_properties(mut self, properties: HashMap<String, String>) -> Self {
594 self.transaction_properties = Some(Arc::new(properties));
595 self
596 }
597
598 fn write_version(&self, dataset: &Dataset) -> ConcreteFileVersion {
599 self.data_storage_version
600 .map(LanceFileVersion::resolve)
601 .unwrap_or_else(|| dataset.manifest.data_storage_format.lance_file_format())
602 }
603}
604
605async fn can_use_binary_copy(
618 dataset: &Dataset,
619 options: &CompactionOptions,
620 fragments: &[Fragment],
621) -> bool {
622 let version = options.write_version(dataset);
623 versions::can_use_binary_copy(version, dataset, options, fragments)
624 .await
625 .unwrap_or_else(|err| {
626 log::warn!("Binary copy disabled due to error: {}", err);
627 false
628 })
629}
630
631fn first_binary_copy_version_mismatch(
632 fragments: &[Fragment],
633 target: ConcreteFileVersion,
634) -> Result<Option<(String, ConcreteFileVersion)>> {
635 for fragment in fragments {
636 for data_file in fragment.referenced_lance_files() {
637 let actual = data_file.file_version()?;
638 if actual != target {
639 return Ok(Some((data_file.path.clone(), actual)));
640 }
641 }
642 }
643 Ok(None)
644}
645
646pub(super) async fn can_use_binary_copy_current(
647 dataset: &Dataset,
648 options: &CompactionOptions,
649 fragments: &[Fragment],
650) -> Result<bool> {
651 use lance_file::reader::FileReader as LFReader;
652 use lance_io::scheduler::{ScanScheduler, SchedulerConfig};
653
654 if matches!(options.compaction_mode(), CompactionMode::Reencode) {
655 log::debug!("Binary copy disabled: compaction mode is Reencode");
656 return Ok(false);
657 }
658
659 let has_blob_columns = dataset
660 .schema()
661 .fields_pre_order()
662 .any(|field| field.is_blob());
663 if has_blob_columns {
664 log::debug!("Binary copy disabled: dataset contains blob columns");
665 return Ok(false);
666 }
667
668 if fragments.is_empty() {
669 log::debug!("Binary copy disabled: no fragments to compact");
670 return Ok(false);
671 }
672
673 if fragments[0].files.is_empty() {
674 log::debug!(
675 "Binary copy disabled: fragment {} has no data files",
676 fragments[0].id
677 );
678 return Ok(false);
679 }
680 let ref_fields = &fragments[0].files[0].fields;
681 let ref_cols = &fragments[0].files[0].column_indices;
682 let version = dataset.manifest.data_storage_format.lance_file_format();
683 let (schema_fields, schema_column_indices) =
684 lance_file::versions::data_file_columns(version, dataset.schema());
685 if ref_fields.as_ref() != schema_fields.as_slice()
686 || ref_cols.as_ref() != schema_column_indices.as_slice()
687 {
688 log::debug!(
689 "Binary copy disabled: data files do not use the dataset schema's physical column order"
690 );
691 return Ok(false);
692 }
693 for fragment in fragments {
694 if !fragment.overlays.is_empty() {
696 log::debug!(
697 "Binary copy disabled: fragment {} has data overlays",
698 fragment.id
699 );
700 return Ok(false);
701 }
702 if fragment.deletion_file.is_some() {
703 log::debug!(
704 "Binary copy disabled: fragment {} has a deletion file",
705 fragment.id
706 );
707 return Ok(false);
708 }
709
710 for data_file in &fragment.files {
711 if data_file.fields != *ref_fields || data_file.column_indices != *ref_cols {
712 return Ok(false);
713 }
714
715 let object_store = match data_file.base_id {
717 Some(base_id) => dataset.object_store(Some(base_id)).await?,
718 None => dataset.object_store.clone(),
719 };
720 let full_path = dataset
721 .data_file_dir(data_file)?
722 .clone()
723 .join(data_file.path.as_str());
724 let scan_scheduler = ScanScheduler::new(
725 object_store.clone(),
726 SchedulerConfig::max_bandwidth(&object_store),
727 );
728 let file_scheduler = scan_scheduler
729 .open_file_with_priority(&full_path, 0, &data_file.file_size_bytes)
730 .await?;
731 let file_meta = LFReader::read_all_metadata(&file_scheduler).await?;
732 if file_meta.file_buffers.len() > 1 {
738 log::debug!(
739 "Binary copy disabled: data file has extra global buffers (len={})",
740 file_meta.file_buffers.len()
741 );
742 return Ok(false);
743 }
744 }
745 }
746
747 Ok(true)
748}
749
750#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
752pub struct CompactionMetrics {
753 pub fragments_removed: usize,
755 pub fragments_added: usize,
757 pub files_removed: usize,
759 pub files_added: usize,
762}
763
764impl AddAssign for CompactionMetrics {
765 fn add_assign(&mut self, rhs: Self) {
766 self.fragments_removed += rhs.fragments_removed;
767 self.fragments_added += rhs.fragments_added;
768 self.files_removed += rhs.files_removed;
769 self.files_added += rhs.files_added;
770 }
771}
772
773#[async_trait::async_trait]
778pub trait CompactionPlanner: Send + Sync {
779 async fn plan(&self, dataset: &Dataset) -> Result<CompactionPlan>;
788}
789
790#[derive(Debug, Clone, Default)]
799pub struct DefaultCompactionPlanner {
800 options: CompactionOptions,
801 excluded_fragment_ids: RoaringBitmap,
802}
803
804impl DefaultCompactionPlanner {
805 pub fn new(mut options: CompactionOptions) -> Result<Self> {
806 options.validate()?;
807 let excluded_fragment_ids = options.excluded_fragment_ids.iter().copied().collect();
808 Ok(Self {
809 options,
810 excluded_fragment_ids,
811 })
812 }
813}
814
815#[async_trait::async_trait]
816impl CompactionPlanner for DefaultCompactionPlanner {
817 async fn plan(&self, dataset: &Dataset) -> Result<CompactionPlan> {
818 let write_version = self.options.write_version(dataset);
819 versions::validate_write_version(
820 dataset.manifest.data_storage_format.lance_file_format(),
821 write_version,
822 )?;
823 if self.options.defer_index_remap && dataset.manifest.uses_stable_row_ids() {
824 return Err(Error::invalid_input(
825 "defer_index_remap=true is not supported on datasets with stable row IDs: \
826 stable row IDs do not require index remapping during compaction, so there \
827 is nothing to defer."
828 .to_string(),
829 ));
830 }
831
832 let fragments = dataset.get_fragments();
835
836 debug_assert!(
837 fragments.windows(2).all(|w| w[0].id() < w[1].id()),
838 "fragments in manifest are not sorted"
839 );
840 let mut excluded_fragment_ids = self.excluded_fragment_ids.clone();
847 if !dataset.manifest.uses_stable_row_ids() && !self.options.defer_index_remap {
848 let unremappable = unremappable_index_coverage(dataset)
849 .await?
850 .into_iter()
851 .fold(RoaringBitmap::new(), |mut covered, (_, fragments)| {
852 covered |= fragments;
853 covered
854 });
855 if !unremappable.is_empty() {
856 log::info!(
859 "holding {} fragment(s) back from compaction: they are covered by an index \
860 this build cannot read, and so cannot be remapped here",
861 unremappable.len(),
862 );
863 }
864 excluded_fragment_ids |= unremappable;
865 }
866 let excluded_fragment_ids = &excluded_fragment_ids;
867
868 let mut fragment_metrics = futures::stream::iter(fragments)
869 .map(|fragment| async move {
870 if u32::try_from(fragment.id())
871 .is_ok_and(|fragment_id| excluded_fragment_ids.contains(fragment_id))
872 {
873 Ok(None)
874 } else {
875 collect_metrics(&fragment)
876 .await
877 .map(|metrics| Some((fragment.metadata, metrics)))
878 }
879 })
880 .buffered(dataset.object_store.as_ref().io_parallelism());
881
882 let index_fragmaps = load_index_fragmaps(dataset).await?;
883 let indices_containing_frag = |frag_id: u32| {
884 index_fragmaps
885 .iter()
886 .enumerate()
887 .filter(|(_, bitmap)| bitmap.contains(frag_id))
888 .map(|(pos, _)| pos)
889 .collect::<Vec<_>>()
890 };
891
892 let schema_field_ids = dataset
893 .schema()
894 .fields_pre_order()
895 .map(|field| field.id)
896 .collect::<HashSet<_>>();
897 let is_binary_copy_only = matches!(
901 self.options.compaction_mode(),
902 CompactionMode::ForceBinaryCopy
903 );
904
905 let mut candidate_bins: Vec<CandidateBin> = Vec::new();
906 let mut current_bin: Option<CandidateBin> = None;
907 let mut i = 0;
908
909 while let Some(res) = fragment_metrics.next().await {
910 let Some((fragment, metrics)) = res? else {
911 if let Some(bin) = current_bin.take() {
915 candidate_bins.push(bin);
916 }
917 i += 1;
918 continue;
919 };
920
921 let over_overlay_limit = self
922 .options
923 .max_overlays_per_fragment
924 .is_some_and(|max| fragment.overlays.len() > max);
925
926 let candidacy = if over_overlay_limit {
927 Some(CompactionCandidacy::CompactItself)
930 } else if !is_binary_copy_only
931 && pins_dead_user_columns_for_lineage(&fragment, &schema_field_ids)
932 {
933 Some(CompactionCandidacy::CompactItself)
936 } else if self.options.materialize_deletions
937 && metrics.deletion_percentage() > self.options.materialize_deletions_threshold
938 {
939 Some(CompactionCandidacy::CompactItself)
940 } else if metrics.physical_rows < self.options.target_rows_per_fragment {
941 Some(CompactionCandidacy::CompactWithNeighbors)
944 } else {
945 None
947 };
948
949 let indices = indices_containing_frag(fragment.id as u32);
950
951 match (candidacy, &mut current_bin) {
952 (None, None) => {} (Some(candidacy), None) => {
954 current_bin = Some(CandidateBin {
956 fragments: vec![fragment],
957 pos_range: i..(i + 1),
958 candidacy: vec![candidacy],
959 row_counts: vec![metrics.num_rows()],
960 indices,
961 });
962 }
963 (Some(candidacy), Some(bin)) => {
964 if bin.indices == indices {
967 bin.fragments.push(fragment);
969 bin.pos_range.end += 1;
970 bin.candidacy.push(candidacy);
971 bin.row_counts.push(metrics.num_rows());
972 } else {
973 candidate_bins.push(current_bin.take().unwrap());
975 current_bin = Some(CandidateBin {
976 fragments: vec![fragment],
977 pos_range: i..(i + 1),
978 candidacy: vec![candidacy],
979 row_counts: vec![metrics.num_rows()],
980 indices,
981 });
982 }
983 }
984 (None, Some(_)) => {
985 candidate_bins.push(current_bin.take().unwrap());
987 }
988 }
989
990 i += 1;
991 }
992
993 if let Some(bin) = current_bin {
995 candidate_bins.push(bin);
996 }
997
998 let all_tasks: Vec<(TaskData, usize)> = candidate_bins
999 .into_iter()
1000 .filter(|bin| !bin.is_noop())
1001 .flat_map(|bin| bin.split_for_size(self.options.target_rows_per_fragment))
1002 .map(|bin| {
1003 let live_rows = bin.row_counts.iter().sum();
1004 (
1005 TaskData {
1006 fragments: bin.fragments,
1007 },
1008 live_rows,
1009 )
1010 })
1011 .collect();
1012
1013 let tasks = limit_tasks_to_source_budget(&self.options, dataset.schema(), all_tasks)?;
1014
1015 let mut options = self.options.clone();
1016 options.data_storage_version = Some(write_version.to_selector());
1017 let mut compaction_plan = CompactionPlan::new(dataset.manifest.version, options);
1018 compaction_plan.extend_tasks(tasks);
1019
1020 Ok(compaction_plan)
1021 }
1022}
1023
1024pub async fn compact_files(
1035 dataset: &mut Dataset,
1036 options: CompactionOptions,
1037 remap_options: Option<Arc<dyn IndexRemapperOptions>>, ) -> Result<CompactionMetrics> {
1039 info!(target: TRACE_DATASET_EVENTS, event=DATASET_COMPACTING_EVENT, uri = &dataset.uri);
1040 let planner = DefaultCompactionPlanner::new(options)?;
1041 compact_files_with_planner(dataset, remap_options, &planner).await
1042}
1043
1044pub async fn compact_files_with_planner(
1045 dataset: &mut Dataset,
1046 remap_options: Option<Arc<dyn IndexRemapperOptions>>, planner: &dyn CompactionPlanner,
1048) -> Result<CompactionMetrics> {
1049 let compaction_plan: CompactionPlan = planner.plan(dataset).await?;
1050
1051 if !compaction_plan.options.defer_index_remap
1057 && dataset.manifest.writer_feature_flags
1058 & lance_table::feature_flags::FLAG_FRAGMENT_REUSE_INDEX
1059 != 0
1060 && crate::index::load_all_indices(dataset)
1061 .await?
1062 .iter()
1063 .any(lance_table::system_index::frag_reuse::metadata::is_tagged)
1064 {
1065 return Err(Error::not_supported(
1066 "Compacting a table with a tagged fragment reuse history requires \
1067 defer_index_remap; set defer_index_remap, or upgrade to a writer that \
1068 remaps tagged histories eagerly",
1069 ));
1070 }
1071
1072 if compaction_plan.tasks().is_empty() {
1074 return Ok(CompactionMetrics::default());
1075 }
1076
1077 let dataset_ref = &dataset.clone();
1078
1079 let result_stream = futures::stream::iter(compaction_plan.tasks)
1080 .map(|task| rewrite_files(Cow::Borrowed(dataset_ref), task, &compaction_plan.options))
1081 .buffer_unordered(
1082 compaction_plan
1083 .options
1084 .num_threads
1085 .unwrap_or_else(get_num_compute_intensive_cpus),
1086 );
1087
1088 let completed_tasks: Vec<RewriteResult> = result_stream.try_collect().await?;
1089 let remap_options = remap_options.unwrap_or(Arc::new(DatasetIndexRemapperOptions::default()));
1090 let metrics = commit_compaction(
1091 dataset,
1092 completed_tasks,
1093 remap_options,
1094 &compaction_plan.options,
1095 )
1096 .await?;
1097
1098 Ok(metrics)
1099}
1100
1101#[derive(Debug)]
1103struct FragmentMetrics {
1104 pub physical_rows: usize,
1106 pub num_deletions: usize,
1108}
1109
1110impl FragmentMetrics {
1111 fn deletion_percentage(&self) -> f32 {
1113 if self.physical_rows > 0 {
1114 self.num_deletions as f32 / self.physical_rows as f32
1115 } else {
1116 0.0
1117 }
1118 }
1119
1120 fn num_rows(&self) -> usize {
1122 self.physical_rows - self.num_deletions
1123 }
1124}
1125
1126async fn collect_metrics(fragment: &FileFragment) -> Result<FragmentMetrics> {
1127 let physical_rows = fragment.physical_rows();
1128 let num_deletions = fragment.count_deletions();
1129 let (physical_rows, num_deletions) =
1130 futures::future::try_join(physical_rows, num_deletions).await?;
1131 Ok(FragmentMetrics {
1132 physical_rows,
1133 num_deletions,
1134 })
1135}
1136
1137fn pins_dead_user_columns_for_lineage(
1150 fragment: &Fragment,
1151 schema_field_ids: &HashSet<i32>,
1152) -> bool {
1153 fragment.files.iter().any(|file| {
1154 let mut holds_dead_user_column = false;
1155 let mut holds_lineage = false;
1156 for field_id in file.fields.iter() {
1157 if schema_field_ids.contains(field_id) {
1158 return false;
1159 }
1160 match *field_id {
1161 ROW_ID_FIELD_ID
1162 | ROW_CREATED_AT_VERSION_FIELD_ID
1163 | ROW_LAST_UPDATED_AT_VERSION_FIELD_ID => holds_lineage = true,
1164 TOMBSTONE_FIELD_ID => holds_dead_user_column = true,
1165 other => holds_dead_user_column |= other >= 0,
1168 }
1169 }
1170 holds_dead_user_column && holds_lineage
1171 })
1172}
1173
1174fn limit_tasks_to_source_budget(
1185 options: &CompactionOptions,
1186 schema: &lance_core::datatypes::Schema,
1187 all_tasks: Vec<(TaskData, usize)>,
1188) -> Result<Vec<TaskData>> {
1189 if options.max_source_fragments.is_none()
1190 && options.max_source_rows.is_none()
1191 && options.max_source_bytes.is_none()
1192 {
1193 return Ok(all_tasks.into_iter().map(|(task, _)| task).collect());
1194 }
1195
1196 let schema_field_ids: HashSet<i32> = if options.max_source_bytes.is_some() {
1200 schema.field_ids().into_iter().collect()
1201 } else {
1202 HashSet::new()
1203 };
1204
1205 let num_candidate_tasks = all_tasks.len();
1206 let mut total_fragments = 0_usize;
1207 let mut total_rows = 0_usize;
1208 let mut total_bytes = 0_u64;
1209 let mut tasks = Vec::with_capacity(all_tasks.len());
1210 for (task, live_rows) in all_tasks {
1211 total_fragments += task.fragments.len();
1212 total_rows = total_rows.saturating_add(live_rows);
1213 if options.max_source_bytes.is_some() {
1214 total_bytes = total_bytes.saturating_add(task_source_bytes(&task, &schema_field_ids)?);
1215 }
1216
1217 let over_budget = options
1218 .max_source_fragments
1219 .is_some_and(|max| total_fragments > max)
1220 || options.max_source_rows.is_some_and(|max| total_rows > max)
1221 || options
1222 .max_source_bytes
1223 .is_some_and(|max| total_bytes > max);
1224 if over_budget {
1225 break;
1226 }
1227
1228 tasks.push(task);
1229 }
1230
1231 if tasks.is_empty() && num_candidate_tasks > 0 {
1232 warn!(
1233 "Compaction plan is empty: the first of {} candidate tasks already exceeds a source \
1234 budget (max_source_fragments={:?}, max_source_rows={:?}, max_source_bytes={:?}); \
1235 compaction cannot make progress until the budget is raised",
1236 num_candidate_tasks,
1237 options.max_source_fragments,
1238 options.max_source_rows,
1239 options.max_source_bytes
1240 );
1241 }
1242
1243 Ok(tasks)
1244}
1245
1246fn task_source_bytes(task: &TaskData, schema_field_ids: &HashSet<i32>) -> Result<u64> {
1255 let mut total_bytes = 0_u64;
1256 for fragment in &task.fragments {
1257 let overlay_files = fragment.overlays.iter().map(|overlay| &overlay.data_file);
1258 for data_file in fragment.files.iter().chain(overlay_files) {
1259 if !data_file
1260 .fields
1261 .iter()
1262 .any(|field_id| schema_field_ids.contains(field_id))
1263 {
1264 continue;
1265 }
1266 let size = data_file.file_size_bytes.get().ok_or_else(|| {
1267 Error::invalid_input(format!(
1268 "max_source_bytes is set but file '{}' of fragment {} has no size recorded \
1269 in the manifest; unset max_source_bytes to compact this dataset",
1270 data_file.path, fragment.id
1271 ))
1272 })?;
1273 total_bytes = total_bytes.saturating_add(size.get());
1274 }
1275 }
1276 Ok(total_bytes)
1277}
1278
1279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1283pub struct CompactionPlan {
1284 pub tasks: Vec<TaskData>,
1285 pub read_version: u64,
1286 pub options: CompactionOptions,
1287}
1288
1289impl CompactionPlan {
1290 pub fn compaction_tasks(&self) -> impl Iterator<Item = CompactionTask> + '_ {
1292 let read_version = self.read_version;
1293 let options = self.options.clone();
1294 self.tasks.iter().map(move |task| CompactionTask {
1295 task: task.clone(),
1296 read_version,
1297 options: options.clone(),
1298 })
1299 }
1300
1301 pub fn num_tasks(&self) -> usize {
1303 self.tasks.len()
1304 }
1305
1306 pub fn read_version(&self) -> u64 {
1308 self.read_version
1309 }
1310
1311 pub fn options(&self) -> &CompactionOptions {
1313 &self.options
1314 }
1315}
1316
1317enum RowClass {
1323 Null,
1324 External,
1325 DataBlob,
1326}
1327
1328struct BlobV2Descriptor<'a> {
1330 kind_col: &'a arrow::array::UInt8Array,
1331 position_col: &'a arrow::array::UInt64Array,
1332 size_col: &'a arrow::array::UInt64Array,
1333 blob_uri_col: &'a arrow::array::StringArray,
1334 blob_id_col: &'a arrow::array::UInt32Array,
1335}
1336
1337impl<'a> BlobV2Descriptor<'a> {
1338 fn try_from_struct(struct_arr: &'a StructArray, column_name: &str) -> Result<Self> {
1340 if BlobV2Layout::classify(struct_arr.fields()) != Some(BlobV2Layout::Descriptor) {
1341 let actual = BlobV2Layout::classify(struct_arr.fields())
1342 .map(|layout| layout.to_string())
1343 .unwrap_or_else(|| format!("unrecognized ({:?})", struct_arr.fields()));
1344 return Err(Error::invalid_input(format!(
1345 "Blob v2 column '{column_name}' has {actual} layout; expected descriptor layout before conversion to logical"
1346 )));
1347 }
1348 let kind_col = struct_arr
1349 .column_by_name("kind")
1350 .ok_or_else(|| {
1351 Error::internal(format!(
1352 "Blob v2 descriptor for column '{}' missing `kind` field",
1353 column_name
1354 ))
1355 })?
1356 .as_primitive::<UInt8Type>();
1357 let position_col = struct_arr
1358 .column_by_name("position")
1359 .ok_or_else(|| {
1360 Error::internal(format!(
1361 "Blob v2 descriptor for column '{}' missing `position` field",
1362 column_name
1363 ))
1364 })?
1365 .as_primitive::<UInt64Type>();
1366 let size_col = struct_arr
1367 .column_by_name("size")
1368 .ok_or_else(|| {
1369 Error::internal(format!(
1370 "Blob v2 descriptor for column '{}' missing `size` field",
1371 column_name
1372 ))
1373 })?
1374 .as_primitive::<UInt64Type>();
1375 let blob_uri_col = struct_arr
1376 .column_by_name("blob_uri")
1377 .ok_or_else(|| {
1378 Error::internal(format!(
1379 "Blob v2 descriptor for column '{}' missing `blob_uri` field",
1380 column_name
1381 ))
1382 })?
1383 .as_string::<i32>();
1384 let blob_id_col = struct_arr
1385 .column_by_name("blob_id")
1386 .ok_or_else(|| {
1387 Error::internal(format!(
1388 "Blob v2 descriptor for column '{}' missing `blob_id` field",
1389 column_name
1390 ))
1391 })?
1392 .as_primitive::<UInt32Type>();
1393 Ok(Self {
1394 kind_col,
1395 position_col,
1396 size_col,
1397 blob_uri_col,
1398 blob_id_col,
1399 })
1400 }
1401}
1402
1403struct RowClassification {
1405 row_classes: Vec<RowClass>,
1406 data_blob_indices: Vec<usize>,
1407}
1408
1409fn classify_rows(
1411 struct_arr: &StructArray,
1412 descriptor: &BlobV2Descriptor<'_>,
1413 column_name: &str,
1414) -> Result<RowClassification> {
1415 let num_rows = struct_arr.len();
1416 let mut row_classes = Vec::with_capacity(num_rows);
1417 let mut data_blob_indices = Vec::with_capacity(num_rows);
1418
1419 for i in 0..num_rows {
1420 if struct_arr.is_null(i) || descriptor.kind_col.is_null(i) {
1421 row_classes.push(RowClass::Null);
1422 } else {
1423 let kind = BlobKind::try_from(descriptor.kind_col.value(i)).map_err(|e| {
1424 Error::internal(format!(
1425 "Blob v2 column '{}' has invalid kind at row {}: {e}",
1426 column_name, i
1427 ))
1428 })?;
1429 if kind == BlobKind::External {
1430 row_classes.push(RowClass::External);
1431 } else {
1432 row_classes.push(RowClass::DataBlob);
1433 data_blob_indices.push(i);
1434 }
1435 }
1436 }
1437
1438 Ok(RowClassification {
1439 row_classes,
1440 data_blob_indices,
1441 })
1442}
1443
1444async fn descriptor_to_logical_blob_array(
1449 dataset: &Arc<Dataset>,
1450 blob_field_id: u32,
1451 struct_arr: &StructArray,
1452 descriptor: &BlobV2Descriptor<'_>,
1453 classification: &RowClassification,
1454 row_addrs: &[u64],
1455 field_name: &str,
1456) -> Result<StructArray> {
1457 if struct_arr.len() != row_addrs.len() {
1458 return Err(Error::internal(format!(
1459 "Blob v2 field '{}' row count {} did not match row address count {}",
1460 field_name,
1461 struct_arr.len(),
1462 row_addrs.len()
1463 )));
1464 }
1465 let blob_files = if classification.data_blob_indices.is_empty() {
1466 Vec::new()
1467 } else {
1468 let indices = classification
1469 .data_blob_indices
1470 .iter()
1471 .map(|index| {
1472 u32::try_from(*index).map_err(|_| {
1473 Error::internal(format!(
1474 "Blob v2 row index {} in field '{}' does not fit in u32",
1475 index, field_name
1476 ))
1477 })
1478 })
1479 .collect::<Result<Vec<_>>>()?;
1480 let indices = UInt32Array::from(indices);
1481 let descriptions = arrow_select::take::take(struct_arr, &indices, None)?;
1482 let descriptions = descriptions.as_struct();
1483 let data_row_addrs = classification
1484 .data_blob_indices
1485 .iter()
1486 .map(|index| row_addrs[*index])
1487 .collect::<Vec<_>>();
1488 super::blob::collect_blob_v2_descriptor_files(
1489 dataset,
1490 blob_field_id,
1491 descriptions,
1492 &data_row_addrs,
1493 )
1494 .await?
1495 };
1496
1497 let data_capacity =
1498 classification
1499 .data_blob_indices
1500 .iter()
1501 .try_fold(0usize, |data_capacity, row_idx| {
1502 if descriptor.size_col.is_null(*row_idx) {
1503 return Err(Error::internal(format!(
1504 "Non-null blob row {} in field '{}' is missing its size",
1505 row_idx, field_name
1506 )));
1507 }
1508 let size = usize::try_from(descriptor.size_col.value(*row_idx)).map_err(|_| {
1509 Error::internal(format!(
1510 "Blob size {} at row {} in field '{}' does not fit in usize",
1511 descriptor.size_col.value(*row_idx),
1512 row_idx,
1513 field_name
1514 ))
1515 })?;
1516 data_capacity.checked_add(size).ok_or_else(|| {
1517 Error::internal(format!(
1518 "Total blob size in field '{}' exceeds usize",
1519 field_name
1520 ))
1521 })
1522 })?;
1523
1524 let num_rows = struct_arr.len();
1525 let mut data_builder = LargeBinaryBuilder::with_capacity(num_rows, data_capacity);
1526 let mut uri_builder = StringBuilder::with_capacity(num_rows, 0);
1527 let mut out_position_builder = PrimitiveBuilder::<UInt64Type>::with_capacity(num_rows);
1528 let mut out_size_builder = PrimitiveBuilder::<UInt64Type>::with_capacity(num_rows);
1529
1530 let mut blob_file_idx = 0;
1531 #[allow(clippy::needless_range_loop)]
1532 for i in 0..num_rows {
1533 match classification.row_classes[i] {
1534 RowClass::Null => {
1535 data_builder.append_null();
1536 uri_builder.append_null();
1537 out_position_builder.append_null();
1538 out_size_builder.append_null();
1539 }
1540 RowClass::External => {
1541 data_builder.append_null();
1542 let base_id = descriptor.blob_id_col.value(i);
1543 let uri_val = descriptor.blob_uri_col.value(i);
1544 if base_id == 0 {
1545 uri_builder.append_value(uri_val);
1546 } else {
1547 let base = dataset.manifest().base_paths.get(&base_id).ok_or_else(|| {
1548 Error::internal(format!(
1549 "External blob in column '{}' references unknown base_id {}",
1550 field_name, base_id
1551 ))
1552 })?;
1553 let absolute_uri = format!("{}/{}", base.path.trim_end_matches('/'), uri_val);
1554 uri_builder.append_value(&absolute_uri);
1555 }
1556 let position =
1557 (!descriptor.position_col.is_null(i)).then(|| descriptor.position_col.value(i));
1558 let size = (!descriptor.size_col.is_null(i)).then(|| descriptor.size_col.value(i));
1559 if position == Some(0) && size == Some(0) {
1560 out_position_builder.append_null();
1563 out_size_builder.append_null();
1564 } else {
1565 out_position_builder.append_option(position);
1566 out_size_builder.append_option(size);
1567 }
1568 }
1569 RowClass::DataBlob => {
1570 let blob_file = blob_files
1571 .get(blob_file_idx)
1572 .and_then(Option::as_ref)
1573 .ok_or_else(|| {
1574 Error::internal(format!(
1575 "Non-null blob row {} in field '{}' resolved to null",
1576 i, field_name
1577 ))
1578 })?;
1579 let data = blob_file.read().await?;
1580 blob_file_idx += 1;
1581 data_builder.append_value(data.as_ref());
1582 uri_builder.append_null();
1583 out_position_builder.append_null();
1584 out_size_builder.append_null();
1585 }
1586 }
1587 }
1588
1589 Ok(StructArray::try_new(
1590 BLOB_V2_LOGICAL_FIELDS.clone(),
1591 vec![
1592 Arc::new(data_builder.finish()),
1593 Arc::new(uri_builder.finish()),
1594 Arc::new(out_position_builder.finish()),
1595 Arc::new(out_size_builder.finish()),
1596 ],
1597 struct_arr.nulls().cloned(),
1598 )?)
1599}
1600
1601fn transformed_arrow_field(field: &LanceField, data_type: ArrowDataType) -> Arc<ArrowField> {
1602 let arrow_field = ArrowField::from(field);
1603 arrow_field_with_data_type(&arrow_field, data_type)
1604}
1605
1606fn arrow_field_with_data_type(field: &ArrowField, data_type: ArrowDataType) -> Arc<ArrowField> {
1607 Arc::new(
1608 ArrowField::new(field.name(), data_type, field.is_nullable())
1609 .with_metadata(field.metadata().clone()),
1610 )
1611}
1612
1613pub(crate) fn field_contains_blob_v2(field: &LanceField) -> bool {
1614 field.is_blob_v2() || field.children.iter().any(field_contains_blob_v2)
1615}
1616
1617enum BlobV2FieldRewritePlan {
1618 Passthrough {
1619 output_field: Arc<ArrowField>,
1620 },
1621 Blob {
1622 field_id: u32,
1623 field_name: String,
1624 output_field: Arc<ArrowField>,
1625 },
1626 Struct {
1627 field_name: String,
1628 output_field: Arc<ArrowField>,
1629 children: Vec<Self>,
1630 },
1631 List {
1632 field_name: String,
1633 output_field: Arc<ArrowField>,
1634 child: Box<Self>,
1635 },
1636 LargeList {
1637 field_name: String,
1638 output_field: Arc<ArrowField>,
1639 child: Box<Self>,
1640 },
1641}
1642
1643impl BlobV2FieldRewritePlan {
1644 fn passthrough(field: &ArrowField) -> Self {
1645 Self::Passthrough {
1646 output_field: Arc::new(field.clone()),
1647 }
1648 }
1649
1650 fn try_new(field: &LanceField, input_field: &ArrowField) -> Result<Self> {
1651 if !field_contains_blob_v2(field) {
1652 return Ok(Self::passthrough(input_field));
1653 }
1654
1655 if field.is_blob_v2() {
1656 let field_id = u32::try_from(field.id).map_err(|_| {
1657 Error::internal(format!(
1658 "Blob v2 field id {} for '{}' does not fit in u32",
1659 field.id, field.name
1660 ))
1661 })?;
1662 let ArrowDataType::Struct(input_children) = input_field.data_type() else {
1663 return Err(Error::invalid_input(format!(
1664 "Blob v2 field '{}' has non-struct input type {:?}",
1665 field.name,
1666 input_field.data_type()
1667 )));
1668 };
1669 let output_field = match BlobV2Layout::classify(input_children) {
1670 Some(BlobV2Layout::Logical) => Arc::new(input_field.clone()),
1671 Some(BlobV2Layout::Descriptor) => {
1672 transformed_arrow_field(field, BLOB_V2_LOGICAL_TYPE.clone())
1673 }
1674 Some(actual) => {
1675 return Err(Error::invalid_input(format!(
1676 "Blob v2 field '{}' has {actual} input layout; expected logical or descriptor layout during rewrite",
1677 field.name
1678 )));
1679 }
1680 None => {
1681 return Err(Error::invalid_input(format!(
1682 "Blob v2 field '{}' has unrecognized input layout {:?}; expected logical or descriptor layout during rewrite",
1683 field.name, input_children
1684 )));
1685 }
1686 };
1687 return Ok(Self::Blob {
1688 field_id,
1689 field_name: field.name.clone(),
1690 output_field,
1691 });
1692 }
1693
1694 match (field.data_type(), input_field.data_type()) {
1695 (ArrowDataType::Struct(_), ArrowDataType::Struct(input_children)) => {
1696 if field.children.len() != input_children.len() {
1697 return Err(Error::internal(format!(
1698 "Struct field '{}' expected {} children in blob rewrite plan, got {}",
1699 field.name,
1700 field.children.len(),
1701 input_children.len()
1702 )));
1703 }
1704 let children = field
1705 .children
1706 .iter()
1707 .zip(input_children.iter())
1708 .map(|(child, input_child)| Self::try_new(child, input_child))
1709 .collect::<Result<Vec<_>>>()?;
1710 let output_children = children
1711 .iter()
1712 .map(|child| child.output_field().clone())
1713 .collect::<Vec<_>>();
1714 Ok(Self::Struct {
1715 field_name: field.name.clone(),
1716 output_field: arrow_field_with_data_type(
1717 input_field,
1718 ArrowDataType::Struct(output_children.into()),
1719 ),
1720 children,
1721 })
1722 }
1723 (ArrowDataType::List(_), ArrowDataType::List(input_child)) => {
1724 let child = field.children.first().ok_or_else(|| {
1725 Error::internal(format!(
1726 "List field '{}' is missing its child in blob rewrite plan",
1727 field.name
1728 ))
1729 })?;
1730 let child = Box::new(Self::try_new(child, input_child)?);
1731 Ok(Self::List {
1732 field_name: field.name.clone(),
1733 output_field: arrow_field_with_data_type(
1734 input_field,
1735 ArrowDataType::List(child.output_field().clone()),
1736 ),
1737 child,
1738 })
1739 }
1740 (ArrowDataType::LargeList(_), ArrowDataType::LargeList(input_child)) => {
1741 let child = field.children.first().ok_or_else(|| {
1742 Error::internal(format!(
1743 "Large list field '{}' is missing its child in blob rewrite plan",
1744 field.name
1745 ))
1746 })?;
1747 let child = Box::new(Self::try_new(child, input_child)?);
1748 Ok(Self::LargeList {
1749 field_name: field.name.clone(),
1750 output_field: arrow_field_with_data_type(
1751 input_field,
1752 ArrowDataType::LargeList(child.output_field().clone()),
1753 ),
1754 child,
1755 })
1756 }
1757 (logical_type, input_type) => Err(Error::invalid_input(format!(
1758 "Field '{}' contains blob v2 descendants but logical type {:?} and input type {:?} do not form a supported rewrite container",
1759 field.name, logical_type, input_type
1760 ))),
1761 }
1762 }
1763
1764 fn output_field(&self) -> &Arc<ArrowField> {
1765 match self {
1766 Self::Passthrough { output_field }
1767 | Self::Blob { output_field, .. }
1768 | Self::Struct { output_field, .. }
1769 | Self::List { output_field, .. }
1770 | Self::LargeList { output_field, .. } => output_field,
1771 }
1772 }
1773
1774 fn transform<'a>(
1775 &'a self,
1776 dataset: &'a Arc<Dataset>,
1777 array: ArrayRef,
1778 row_addrs: Arc<[u64]>,
1779 ) -> BoxFuture<'a, Result<ArrayRef>> {
1780 async move {
1781 match self {
1782 Self::Passthrough { .. } => Ok(array),
1783 Self::Blob {
1784 field_id,
1785 field_name,
1786 ..
1787 } => {
1788 let struct_arr = array.as_struct();
1789 match BlobV2Layout::classify(struct_arr.fields()) {
1790 Some(BlobV2Layout::Logical) => Ok(array),
1791 Some(BlobV2Layout::Descriptor) => {
1792 let descriptor =
1793 BlobV2Descriptor::try_from_struct(struct_arr, field_name)?;
1794 let classification =
1795 classify_rows(struct_arr, &descriptor, field_name)?;
1796 let logical = descriptor_to_logical_blob_array(
1797 dataset,
1798 *field_id,
1799 struct_arr,
1800 &descriptor,
1801 &classification,
1802 row_addrs.as_ref(),
1803 field_name,
1804 )
1805 .await?;
1806 Ok(Arc::new(logical) as ArrayRef)
1807 }
1808 Some(actual) => Err(Error::invalid_input(format!(
1809 "Blob v2 field '{}' has {actual} layout; expected logical or descriptor layout during rewrite",
1810 field_name
1811 ))),
1812 None => Err(Error::invalid_input(format!(
1813 "Blob v2 field '{}' has unrecognized layout {:?}; expected logical or descriptor layout during rewrite",
1814 field_name,
1815 struct_arr.fields()
1816 ))),
1817 }
1818 }
1819 Self::Struct {
1820 field_name,
1821 output_field,
1822 children,
1823 } => {
1824 let struct_arr = array.as_struct().normalize_slicing()?;
1825 let parent_nulls = struct_arr.nulls().cloned();
1826 let struct_arr = struct_arr.pushdown_nulls()?;
1827 if children.len() != struct_arr.num_columns() {
1828 return Err(Error::internal(format!(
1829 "Struct field '{}' expected {} children during blob rewrite, got {}",
1830 field_name,
1831 children.len(),
1832 struct_arr.num_columns()
1833 )));
1834 }
1835 let mut child_arrays = Vec::with_capacity(children.len());
1836 for (child, child_array) in children.iter().zip(struct_arr.columns()) {
1837 child_arrays.push(
1838 child
1839 .transform(dataset, child_array.clone(), row_addrs.clone())
1840 .await?,
1841 );
1842 }
1843 let ArrowDataType::Struct(output_fields) = output_field.data_type() else {
1844 return Err(Error::internal(format!(
1845 "Struct field '{}' rewrite plan has non-struct output type {:?}",
1846 field_name,
1847 output_field.data_type()
1848 )));
1849 };
1850 Ok(Arc::new(StructArray::try_new(
1851 output_fields.clone(),
1852 child_arrays,
1853 parent_nulls,
1854 )?) as ArrayRef)
1855 }
1856 Self::List {
1857 field_name, child, ..
1858 } => {
1859 transform_blob_v2_list_array::<i32>(
1860 dataset,
1861 field_name,
1862 child,
1863 array.as_list::<i32>(),
1864 row_addrs,
1865 )
1866 .await
1867 }
1868 Self::LargeList {
1869 field_name, child, ..
1870 } => {
1871 transform_blob_v2_list_array::<i64>(
1872 dataset,
1873 field_name,
1874 child,
1875 array.as_list::<i64>(),
1876 row_addrs,
1877 )
1878 .await
1879 }
1880 }
1881 }
1882 .boxed()
1883 }
1884}
1885
1886async fn transform_blob_v2_list_array<O: OffsetSizeTrait>(
1887 dataset: &Arc<Dataset>,
1888 field_name: &str,
1889 child: &BlobV2FieldRewritePlan,
1890 list_array: &GenericListArray<O>,
1891 row_addrs: Arc<[u64]>,
1892) -> Result<ArrayRef> {
1893 let list_array = if list_array.null_count() > 0 {
1894 list_array.filter_garbage_nulls()
1895 } else {
1896 list_array.clone()
1897 };
1898 let offsets = list_array.value_offsets();
1899 let values_start = offsets[0].as_usize();
1900 let values_end = offsets[list_array.len()].as_usize();
1901 if values_end < values_start {
1902 return Err(Error::internal(format!(
1903 "List field '{}' has invalid offsets during blob rewrite",
1904 field_name
1905 )));
1906 }
1907
1908 let values_len = values_end - values_start;
1909 let mut normalized_offsets = Vec::with_capacity(list_array.len() + 1);
1910 normalized_offsets.push(O::usize_as(0));
1911 let mut child_row_addrs = Vec::with_capacity(values_len);
1912 for row_idx in 0..list_array.len() {
1913 let start = offsets[row_idx].as_usize();
1914 let end = offsets[row_idx + 1].as_usize();
1915 if end < start {
1916 return Err(Error::internal(format!(
1917 "List field '{}' has decreasing offsets during blob rewrite",
1918 field_name
1919 )));
1920 }
1921 let row_addr = row_addrs.get(row_idx).copied().ok_or_else(|| {
1922 Error::internal(format!(
1923 "List field '{}' row address count {} did not match row count {}",
1924 field_name,
1925 row_addrs.len(),
1926 list_array.len()
1927 ))
1928 })?;
1929 child_row_addrs.extend(std::iter::repeat_n(row_addr, end - start));
1930 normalized_offsets.push(O::usize_as(end - values_start));
1931 }
1932
1933 let values = list_array.values().slice(values_start, values_len);
1934 let values = child
1935 .transform(dataset, values, Arc::<[u64]>::from(child_row_addrs))
1936 .await?;
1937 let list_array = GenericListArray::<O>::try_new(
1938 child.output_field().clone(),
1939 OffsetBuffer::new(ScalarBuffer::from(normalized_offsets)),
1940 values,
1941 list_array.nulls().cloned(),
1942 )?;
1943 Ok(Arc::new(list_array))
1944}
1945
1946pub(crate) struct BlobV2BatchRewritePlan {
1947 row_addr_idx: usize,
1948 columns: Vec<(usize, BlobV2FieldRewritePlan)>,
1949 output_schema: SchemaRef,
1950}
1951
1952impl BlobV2BatchRewritePlan {
1953 pub(crate) fn try_new(
1954 schema: &lance_core::datatypes::Schema,
1955 input_schema: &ArrowSchema,
1956 keep_row_addr: bool,
1957 ) -> Result<Self> {
1958 let row_addr_idx = input_schema
1959 .column_with_name(lance_core::ROW_ADDR)
1960 .ok_or_else(|| {
1961 Error::internal(format!(
1962 "_rowaddr column missing from batch for blob v2 rewrite, columns: {:?}",
1963 input_schema
1964 .fields()
1965 .iter()
1966 .map(|f| f.name())
1967 .collect::<Vec<_>>()
1968 ))
1969 })?
1970 .0;
1971 let mut columns = Vec::with_capacity(input_schema.fields().len());
1972 let mut output_fields = Vec::with_capacity(input_schema.fields().len());
1973 for (column_idx, input_field) in input_schema.fields().iter().enumerate() {
1974 if input_field.name() == lance_core::ROW_ADDR && !keep_row_addr {
1975 continue;
1976 }
1977 let field_plan = if let Some(field) = schema.field(input_field.name()) {
1978 BlobV2FieldRewritePlan::try_new(field, input_field)?
1979 } else {
1980 BlobV2FieldRewritePlan::passthrough(input_field)
1981 };
1982 output_fields.push(field_plan.output_field().as_ref().clone());
1983 columns.push((column_idx, field_plan));
1984 }
1985
1986 Ok(Self {
1987 row_addr_idx,
1988 columns,
1989 output_schema: Arc::new(ArrowSchema::new_with_metadata(
1990 output_fields,
1991 input_schema.metadata().clone(),
1992 )),
1993 })
1994 }
1995
1996 pub(crate) fn output_schema(&self) -> &SchemaRef {
1997 &self.output_schema
1998 }
1999
2000 pub(crate) async fn transform_batch(
2001 &self,
2002 dataset: &Arc<Dataset>,
2003 batch: RecordBatch,
2004 ) -> Result<RecordBatch> {
2005 let row_addrs: Arc<[u64]> = batch
2006 .column(self.row_addr_idx)
2007 .as_primitive::<UInt64Type>()
2008 .values()
2009 .iter()
2010 .copied()
2011 .collect::<Vec<_>>()
2012 .into();
2013 let mut output_columns = Vec::with_capacity(self.columns.len());
2014 for (column_idx, field_plan) in &self.columns {
2015 output_columns.push(
2016 field_plan
2017 .transform(
2018 dataset,
2019 batch.column(*column_idx).clone(),
2020 row_addrs.clone(),
2021 )
2022 .await?,
2023 );
2024 }
2025 Ok(RecordBatch::try_new(
2026 self.output_schema.clone(),
2027 output_columns,
2028 )?)
2029 }
2030}
2031
2032pub(crate) async fn transform_blob_v2_batch(
2033 dataset: &Arc<Dataset>,
2034 schema: &lance_core::datatypes::Schema,
2035 batch: RecordBatch,
2036 keep_row_addr: bool,
2037) -> Result<RecordBatch> {
2038 let plan = BlobV2BatchRewritePlan::try_new(schema, batch.schema().as_ref(), keep_row_addr)?;
2039 plan.transform_batch(dataset, batch).await
2040}
2041
2042async fn prepare_reader(
2064 dataset: &Dataset,
2065 fragments: &[Fragment],
2066 batch_size: Option<usize>,
2067 io_buffer_size: Option<u64>,
2068 with_frags: bool,
2069 capture_row_ids: bool,
2070) -> Result<(
2071 SendableRecordBatchStream,
2072 Option<std::sync::mpsc::Receiver<CapturedRowIds>>,
2073 bool,
2074)> {
2075 let mut scanner = dataset.scan();
2076 let has_legacy_blob_columns = dataset
2077 .schema()
2078 .fields_pre_order()
2079 .any(|field| field.is_blob() && !field.is_blob_v2());
2080 if has_legacy_blob_columns {
2081 scanner.blob_handling(BlobHandling::AllBinary);
2082 }
2083 let has_blob_v2_columns = dataset
2084 .schema()
2085 .fields_pre_order()
2086 .any(|field| field.is_blob_v2());
2087 if has_blob_v2_columns {
2088 scanner.with_row_address();
2089 }
2090 if let Some(bs) = batch_size {
2091 scanner.batch_size(bs);
2092 }
2093 if let Some(io_buffer_size) = io_buffer_size {
2094 scanner.io_buffer_size(io_buffer_size);
2095 }
2096 if with_frags {
2097 scanner
2098 .with_fragments(fragments.to_vec())
2099 .scan_in_order(true);
2100 }
2101 if capture_row_ids {
2102 scanner.with_row_id();
2103 let data = SendableRecordBatchStream::from(scanner.try_into_stream().await?);
2104 let (data_no_row_ids, rx) =
2105 make_rowid_capture_stream(data, dataset.manifest.uses_stable_row_ids())?;
2106 Ok((data_no_row_ids, Some(rx), has_blob_v2_columns))
2107 } else {
2108 Ok((
2109 SendableRecordBatchStream::from(scanner.try_into_stream().await?),
2110 None,
2111 has_blob_v2_columns,
2112 ))
2113 }
2114}
2115
2116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2120pub struct TaskData {
2121 pub fragments: Vec<Fragment>,
2123}
2124
2125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2128pub struct CompactionTask {
2129 pub task: TaskData,
2130 pub read_version: u64,
2131 pub options: CompactionOptions,
2132}
2133
2134impl CompactionTask {
2135 pub async fn execute(&self, dataset: &Dataset) -> Result<RewriteResult> {
2144 let dataset = if dataset.manifest.version == self.read_version {
2145 Cow::Borrowed(dataset)
2146 } else {
2147 Cow::Owned(dataset.checkout_version(self.read_version).await?)
2148 };
2149 rewrite_files(dataset, self.task.clone(), &self.options).await
2150 }
2151}
2152
2153impl CompactionPlan {
2154 fn new(read_version: u64, options: CompactionOptions) -> Self {
2155 Self {
2156 tasks: Vec::new(),
2157 read_version,
2158 options,
2159 }
2160 }
2161
2162 fn extend_tasks(&mut self, tasks: impl IntoIterator<Item = TaskData>) {
2163 self.tasks.extend(tasks);
2164 }
2165
2166 fn tasks(&self) -> &[TaskData] {
2167 &self.tasks
2168 }
2169}
2170
2171#[derive(Debug, Clone)]
2172enum CompactionCandidacy {
2173 CompactWithNeighbors,
2175 CompactItself,
2177}
2178
2179struct CandidateBin {
2181 pub fragments: Vec<Fragment>,
2182 pub pos_range: Range<usize>,
2183 pub candidacy: Vec<CompactionCandidacy>,
2184 pub row_counts: Vec<usize>,
2185 pub indices: Vec<usize>,
2186}
2187
2188impl CandidateBin {
2189 fn is_noop(&self) -> bool {
2191 if self.fragments.is_empty() {
2192 return true;
2193 }
2194 if self.fragments.len() == 1 {
2196 matches!(self.candidacy[0], CompactionCandidacy::CompactWithNeighbors)
2197 } else {
2198 false
2199 }
2200 }
2201
2202 fn split_for_size(self, min_num_rows: usize) -> Vec<Self> {
2204 let total_rows = self.row_counts.iter().sum::<usize>();
2205 let mut remaining_rows = total_rows;
2206 let mut current_rows = 0;
2207 let mut current_len = 0;
2208 let mut split_lengths = Vec::new();
2209
2210 for row_count in &self.row_counts {
2211 current_rows += *row_count;
2212 current_len += 1;
2213 remaining_rows -= *row_count;
2214
2215 if current_rows >= min_num_rows && remaining_rows > 0 && remaining_rows >= min_num_rows
2218 {
2219 split_lengths.push(current_len);
2220 current_rows = 0;
2221 current_len = 0;
2222 }
2223 }
2224
2225 if split_lengths.is_empty() {
2226 return vec![self];
2227 }
2228
2229 let mut bins = Vec::with_capacity(split_lengths.len() + 1);
2230 let mut fragments = self.fragments.into_iter();
2231 let mut candidacy = self.candidacy.into_iter();
2232 let mut row_counts = self.row_counts.into_iter();
2233 let mut pos_start = self.pos_range.start;
2234
2235 for bin_len in split_lengths {
2236 bins.push(Self {
2237 fragments: fragments.by_ref().take(bin_len).collect(),
2238 pos_range: pos_start..(pos_start + bin_len),
2239 candidacy: candidacy.by_ref().take(bin_len).collect(),
2240 row_counts: row_counts.by_ref().take(bin_len).collect(),
2241 indices: Vec::new(),
2243 });
2244 pos_start += bin_len;
2245 }
2246
2247 bins.push(Self {
2248 fragments: fragments.collect(),
2249 pos_range: pos_start..self.pos_range.end,
2250 candidacy: candidacy.collect(),
2251 row_counts: row_counts.collect(),
2252 indices: self.indices,
2253 });
2254
2255 bins
2256 }
2257}
2258
2259async fn load_index_fragmaps(dataset: &Dataset) -> Result<Vec<RoaringBitmap>> {
2260 let indices = load_all_indices(dataset).await?;
2270 let mut index_fragmaps = Vec::with_capacity(indices.len());
2271 for index in indices.iter().filter(|idx| !is_system_index(idx)) {
2276 index_fragmaps.push(index_fragment_coverage(dataset, index).await?);
2277 }
2278 Ok(index_fragmaps)
2279}
2280
2281async fn index_fragment_coverage(
2285 dataset: &Dataset,
2286 index: &IndexMetadata,
2287) -> Result<RoaringBitmap> {
2288 if let Some(fragment_bitmap) = index.fragment_bitmap.as_ref() {
2289 return Ok(fragment_bitmap.clone());
2290 }
2291 let dataset_at_index = dataset.checkout_version(index.dataset_version).await?;
2292 let frags = 0..dataset_at_index
2295 .manifest
2296 .max_fragment_id
2297 .map_or(0, |m| m + 1);
2298 let mut coverage = RoaringBitmap::from_sorted_iter(frags).unwrap();
2299 if let Some(frag_reuse_index) = dataset.open_frag_reuse_index(&NoOpMetricsCollector).await? {
2308 frag_reuse_index.remap_fragment_bitmap(&mut coverage)?;
2309 }
2310 Ok(coverage)
2311}
2312
2313async fn unremappable_index_coverage(dataset: &Dataset) -> Result<Vec<(String, RoaringBitmap)>> {
2326 let mut coverage = Vec::new();
2327 for index in load_all_indices(dataset).await?.iter() {
2328 if index_is_usable(index) {
2329 continue;
2330 }
2331 coverage.push((
2332 index.name.clone(),
2333 index_fragment_coverage(dataset, index).await?,
2334 ));
2335 }
2336 Ok(coverage)
2337}
2338
2339async fn reject_unremappable_rewrite(
2348 dataset: &Dataset,
2349 completed_tasks: &[RewriteResult],
2350) -> Result<()> {
2351 let rewritten = completed_tasks
2352 .iter()
2353 .flat_map(|task| task.original_fragments.iter())
2354 .filter_map(|fragment| u32::try_from(fragment.id).ok())
2355 .collect::<RoaringBitmap>();
2356
2357 for (name, covered) in unremappable_index_coverage(dataset).await? {
2358 let blocked = covered & &rewritten;
2359 if !blocked.is_empty() {
2360 return Err(Error::invalid_input(format!(
2361 "compaction would rewrite fragment(s) {:?}, which index {:?} covers. This build \
2362 has no reader for that index, so it cannot be remapped onto the rewritten \
2363 fragments and the commit would leave it addressing rows that no longer exist. \
2364 Plan with DefaultCompactionPlanner, which holds those fragments back, set \
2365 defer_index_remap, or compact from a build that can read the index.",
2366 blocked.iter().collect::<Vec<_>>(),
2367 name,
2368 )));
2369 }
2370 }
2371 Ok(())
2372}
2373
2374pub async fn plan_compaction(
2375 dataset: &Dataset,
2376 options: &CompactionOptions,
2377) -> Result<CompactionPlan> {
2378 let planner = DefaultCompactionPlanner::new(options.clone())?;
2379 planner.plan(dataset).await
2380}
2381
2382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2386pub struct RewriteResult {
2387 pub metrics: CompactionMetrics,
2388 pub new_fragments: Vec<Fragment>,
2389 pub read_version: u64,
2391 pub original_fragments: Vec<Fragment>,
2393 pub row_addrs: Option<Vec<u8>>,
2402}
2403
2404async fn reserve_fragment_ids(
2405 dataset: &Dataset,
2406 fragments: impl ExactSizeIterator<Item = &mut Fragment>,
2407) -> Result<()> {
2408 let transaction = Transaction::new(
2409 dataset.manifest.version,
2410 Operation::ReserveFragments {
2411 num_fragments: fragments.len() as u32,
2412 },
2413 None,
2414 );
2415
2416 let (manifest, _) = commit_transaction(
2417 dataset,
2418 dataset.object_store.as_ref(),
2419 dataset.commit_handler.as_ref(),
2420 &transaction,
2421 &Default::default(),
2422 &Default::default(),
2423 default_commit_retry_timeout(),
2424 dataset.manifest_location.naming_scheme,
2425 None,
2426 )
2427 .await?;
2428
2429 let new_max_exclusive = manifest.max_fragment_id.unwrap_or(0) + 1;
2431 let reserved_ids = (new_max_exclusive - fragments.len() as u32)..(new_max_exclusive);
2432
2433 for (fragment, new_id) in fragments.zip(reserved_ids) {
2434 fragment.id = new_id as u64;
2435 }
2436
2437 Ok(())
2438}
2439
2440async fn rewrite_files(
2444 dataset: Cow<'_, Dataset>,
2445 task: TaskData,
2446 options: &CompactionOptions,
2447) -> Result<RewriteResult> {
2448 let mut metrics = CompactionMetrics::default();
2449 let write_version = options.write_version(dataset.as_ref());
2450 versions::validate_write_version(
2451 dataset.manifest.data_storage_format.lance_file_format(),
2452 write_version,
2453 )?;
2454
2455 if task.fragments.is_empty() {
2456 return Ok(RewriteResult {
2457 metrics,
2458 new_fragments: Vec::new(),
2459 read_version: dataset.manifest.version,
2460 original_fragments: task.fragments,
2461 row_addrs: None,
2462 });
2463 }
2464
2465 let previous_writer_version = &dataset.manifest.writer_version;
2466 let recompute_stats = previous_writer_version.is_none();
2471
2472 let fragments = migrate_fragments(dataset.as_ref(), &task.fragments, recompute_stats).await?;
2476 let num_rows = fragments
2477 .iter()
2478 .map(|f| f.physical_rows.unwrap() as u64)
2479 .sum::<u64>();
2480 let capture_row_addrs = !dataset.manifest.uses_stable_row_ids()
2483 && (options.defer_index_remap
2484 || load_indices_for_remapping(dataset.as_ref())
2485 .await?
2486 .is_some());
2487 let mut new_fragments: Vec<Fragment>;
2488 let task_id = uuid::Uuid::new_v4();
2489 log::info!(
2490 "Compaction task {}: Begin compacting {} rows across {} fragments",
2491 task_id,
2492 num_rows,
2493 fragments.len()
2494 );
2495 let mode = options.compaction_mode();
2496 let can_binary_copy = can_use_binary_copy(dataset.as_ref(), options, &fragments).await;
2497 if !can_binary_copy && matches!(mode, CompactionMode::ForceBinaryCopy) {
2498 if let Some((path, actual)) = first_binary_copy_version_mismatch(&fragments, write_version)?
2499 {
2500 return Err(Error::not_supported_source(
2501 format!(
2502 "compaction task {task_id}: binary copy target is {write_version}, but data file '{path}' uses {actual}"
2503 )
2504 .into(),
2505 ));
2506 }
2507 return Err(Error::not_supported_source(
2508 format!("compaction task {}: binary copy is not supported", task_id).into(),
2509 ));
2510 }
2511 let mut row_ids_rx: Option<std::sync::mpsc::Receiver<CapturedRowIds>> = None;
2512 let mut reader: Option<SendableRecordBatchStream> = None;
2513
2514 if !can_binary_copy {
2515 let (prepared_reader, rx_initial, has_blob_v2_columns) = prepare_reader(
2516 dataset.as_ref(),
2517 &fragments,
2518 options.batch_size,
2519 options.io_buffer_size,
2520 true,
2521 capture_row_addrs,
2522 )
2523 .await?;
2524 row_ids_rx = rx_initial;
2525
2526 let mut rows_read = 0;
2527 let schema = prepared_reader.schema();
2528 let reader_with_progress = prepared_reader.inspect_ok(move |batch| {
2529 rows_read += batch.num_rows();
2530 log::info!(
2531 "Compaction task {}: Read progress {}/{}",
2532 task_id,
2533 rows_read,
2534 num_rows,
2535 );
2536 });
2537
2538 if has_blob_v2_columns {
2539 let dataset_arc = Arc::new(dataset.as_ref().clone());
2540 let rewrite_plan = Arc::new(BlobV2BatchRewritePlan::try_new(
2541 dataset.schema(),
2542 schema.as_ref(),
2543 false,
2544 )?);
2545 let transformed_schema = rewrite_plan.output_schema.clone();
2546 let transformed = reader_with_progress.then(move |batch_result| {
2547 let dataset = dataset_arc.clone();
2548 let rewrite_plan = rewrite_plan.clone();
2549 async move {
2550 let batch = batch_result?;
2551 rewrite_plan
2552 .transform_batch(&dataset, batch)
2553 .await
2554 .map_err(|e| datafusion::error::DataFusionError::External(Box::new(e)))
2555 }
2556 });
2557 reader = Some(Box::pin(RecordBatchStreamAdapter::new(
2558 transformed_schema,
2559 transformed,
2560 )));
2561 } else {
2562 reader = Some(Box::pin(RecordBatchStreamAdapter::new(
2563 schema,
2564 reader_with_progress,
2565 )));
2566 }
2567 }
2568
2569 let surviving_rows = fragments.iter().try_fold(0_u64, |total, fragment| {
2570 let fragment_rows = fragment.num_rows().ok_or_else(|| {
2571 Error::internal(format!(
2572 "Fragment {} is missing row count metadata after migration",
2573 fragment.id
2574 ))
2575 })?;
2576 total.checked_add(fragment_rows as u64).ok_or_else(|| {
2577 Error::internal("Compaction task surviving row count overflowed u64".to_string())
2578 })
2579 })?;
2580
2581 let target_rows_per_fragment = options.target_rows_per_fragment as u64;
2586 let output_fragment_count = surviving_rows
2587 .checked_div(target_rows_per_fragment)
2588 .unwrap_or(1)
2589 .max(1);
2590 let output_fragment_count_usize = usize::try_from(output_fragment_count).map_err(|_| {
2591 Error::internal(format!(
2592 "Compaction output fragment count {output_fragment_count} does not fit in usize"
2593 ))
2594 })?;
2595 let base_rows_per_file = surviving_rows / output_fragment_count;
2596 let larger_file_count =
2597 usize::try_from(surviving_rows % output_fragment_count).map_err(|_| {
2598 Error::internal("Compaction larger output fragment count does not fit in usize")
2599 })?;
2600 let file_row_counts = if surviving_rows == 0 {
2601 Vec::new()
2602 } else {
2603 (0..output_fragment_count_usize)
2604 .map(|file_index| {
2605 let file_rows = base_rows_per_file + u64::from(file_index < larger_file_count);
2606 usize::try_from(file_rows).map_err(|_| {
2607 Error::internal(format!(
2608 "Compaction output row count {file_rows} does not fit in usize"
2609 ))
2610 })
2611 })
2612 .collect::<Result<Vec<_>>>()?
2613 };
2614 let max_rows_per_file = file_row_counts.first().copied().unwrap_or(1);
2615 let mut params = WriteParams {
2616 max_rows_per_file,
2617 max_rows_per_group: options.max_rows_per_group,
2618 mode: WriteMode::Append,
2619 allow_external_blob_outside_bases: true,
2623 data_storage_version: Some(write_version.to_selector()),
2624 ..Default::default()
2625 };
2626 if let Some(max_bytes_per_file) = options.max_bytes_per_file {
2627 params.max_bytes_per_file = max_bytes_per_file;
2628 }
2629
2630 if dataset.manifest.uses_stable_row_ids() {
2631 params.enable_stable_row_ids = true;
2632 }
2633
2634 let lineage_plan = if can_binary_copy {
2635 new_fragments = versions::rewrite_files_binary_copy(
2636 write_version,
2637 dataset.as_ref(),
2638 &fragments,
2639 ¶ms,
2640 options.binary_copy_read_batch_bytes,
2641 )
2642 .await?;
2643
2644 if new_fragments.is_empty() && matches!(mode, CompactionMode::ForceBinaryCopy) {
2645 return Err(Error::not_supported_source(
2646 format!("compaction task {}: binary copy is not supported", task_id).into(),
2647 ));
2648 }
2649
2650 if capture_row_addrs {
2651 let (tx, rx) = std::sync::mpsc::channel();
2652 let mut addrs = RoaringTreemap::new();
2653 for frag in &fragments {
2654 let frag_id = frag.id as u32;
2655 let count = u64::try_from(frag.physical_rows.unwrap_or(0)).map_err(|_| {
2656 Error::internal(format!(
2657 "Fragment {} has too many physical rows to represent as row addresses",
2658 frag.id
2659 ))
2660 })?;
2661 let start = u64::from(lance_core::utils::address::RowAddress::first_row(frag_id));
2662 addrs.insert_range(start..start + count);
2663 }
2664 let captured = CapturedRowIds::AddressStyle(addrs);
2665 let _ = tx.send(captured);
2666 row_ids_rx = Some(rx);
2667 }
2668 None
2669 } else {
2670 let mut stream = reader.expect("reader must be prepared for non-binary-copy path");
2671 let mut write_schema = dataset.schema().clone();
2672 let spill_budget = if dataset.manifest.uses_stable_row_ids() {
2681 inline_row_lineage_max_bytes(dataset.as_ref())?
2682 } else {
2683 None
2684 };
2685 let lineage_plan = match spill_budget {
2686 Some(limit) => {
2687 let planned_rows = file_row_counts
2688 .iter()
2689 .map(|rows| *rows as u64)
2690 .collect::<Vec<_>>();
2691 let mut lineages =
2692 compute_row_lineage(dataset.as_ref(), &fragments, &planned_rows).await?;
2693 let plan = plan_row_lineage_spill(limit, &lineages);
2694 if let RowLineagePlan::InFile(spill) = plan
2695 && spill.any()
2696 {
2697 let fields = spill.schema_fields()?;
2698 let columns = spill.take_columns(&mut lineages);
2699 stream = append_row_lineage_columns(stream, &fields, columns);
2700 write_schema.fields.extend(fields);
2702 }
2703 Some(PlannedRowLineage {
2704 lineages,
2705 planned_rows,
2706 plan,
2707 })
2708 }
2709 None => None,
2710 };
2711 let (frags, _) = write_fragments_internal_with_file_row_counts(
2712 write_version,
2713 Some(dataset.as_ref()),
2714 dataset.object_store.clone(),
2715 &dataset.base,
2716 write_schema,
2717 stream,
2718 params,
2719 None,
2720 Some(file_row_counts),
2721 )
2722 .await?;
2723 new_fragments = frags;
2724 lineage_plan
2725 };
2726
2727 log::info!("Compaction task {}: file written", task_id);
2728
2729 let row_addrs_result: Result<Option<Vec<u8>>> = async {
2732 if let Some(row_ids_rx) = row_ids_rx {
2733 let captured_ids = row_ids_rx
2734 .try_recv()
2735 .map_err(|err| Error::internal(format!("Failed to receive row ids: {}", err)))?;
2736 let mut row_addrs = captured_ids.row_addrs(None)?.into_owned();
2737 row_addrs.optimize();
2741 let mut serialized = Vec::with_capacity(row_addrs.serialized_size());
2742 row_addrs.serialize_into(&mut serialized)?;
2743 Ok(Some(serialized))
2744 } else {
2745 if dataset.manifest.uses_stable_row_ids() {
2746 log::info!("Compaction task {}: rechunking stable row ids", task_id);
2747 match lineage_plan {
2748 Some(planned) => {
2749 place_planned_row_lineage(dataset.as_ref(), &mut new_fragments, planned)
2750 .await?
2751 }
2752 None => {
2753 rechunk_row_lineage(dataset.as_ref(), &mut new_fragments, &fragments)
2754 .await?
2755 }
2756 }
2757 }
2758 Ok(None)
2759 }
2760 }
2761 .await;
2762
2763 let row_addrs = match row_addrs_result {
2764 Ok(v) => v,
2765 Err(e) => {
2766 cleanup_data_fragments(&dataset.object_store, &dataset.base, None, &new_fragments)
2767 .await;
2768 return Err(e);
2769 }
2770 };
2771
2772 metrics.files_removed = task
2773 .fragments
2774 .iter()
2775 .map(|f| f.files.len() + f.deletion_file.is_some() as usize)
2776 .sum();
2777 metrics.fragments_removed = task.fragments.len();
2778 metrics.fragments_added = new_fragments.len();
2779 metrics.files_added = new_fragments
2780 .iter()
2781 .map(|f| f.files.len() + f.deletion_file.is_some() as usize)
2782 .sum();
2783
2784 log::info!("Compaction task {}: completed", task_id);
2785
2786 Ok(RewriteResult {
2787 metrics,
2788 new_fragments,
2789 read_version: dataset.manifest.version,
2790 original_fragments: fragments,
2791 row_addrs,
2792 })
2793}
2794
2795async fn compute_row_lineage(
2799 dataset: &Dataset,
2800 old_fragments: &[Fragment],
2801 chunk_sizes: &[u64],
2802) -> Result<Vec<RowLineage>> {
2803 let mut old_sequences = load_row_id_sequences(dataset, old_fragments)
2804 .try_collect::<Vec<_>>()
2805 .await?;
2806 old_sequences.sort_by_key(|(frag_id, _)| {
2808 old_fragments
2809 .iter()
2810 .position(|frag| frag.id as u32 == *frag_id)
2811 .expect("Fragment not found")
2812 });
2813
2814 let mut old_created_at_sequences = Vec::with_capacity(old_fragments.len());
2817 let mut old_last_updated_sequences = Vec::with_capacity(old_fragments.len());
2818 for (frag, (_, row_ids)) in old_fragments.iter().zip(old_sequences.iter()) {
2819 let created_at =
2820 match load_row_version_sequence(dataset, frag, RowVersionKind::CreatedAt).await? {
2821 Some(sequence) => sequence.as_ref().clone(),
2822 None => RowDatasetVersionSequence::from_uniform_row_count(row_ids.len(), 1),
2823 };
2824 let last_updated_at =
2825 match load_row_version_sequence(dataset, frag, RowVersionKind::LastUpdatedAt).await? {
2826 Some(sequence) => sequence.as_ref().clone(),
2827 None => created_at.clone(),
2828 };
2829 old_created_at_sequences.push(created_at);
2830 old_last_updated_sequences.push(last_updated_at);
2831 }
2832
2833 for (index, frag) in old_fragments.iter().enumerate() {
2835 if let Some(deletion_file) = &frag.deletion_file {
2836 let deletions = read_dataset_deletion_file(dataset, frag.id, deletion_file).await?;
2837
2838 let mut new_seq = old_sequences[index].1.as_ref().clone();
2839 new_seq.mask(deletions.to_sorted_iter())?;
2840 old_sequences[index].1 = Arc::new(new_seq);
2841 old_created_at_sequences[index].mask(deletions.to_sorted_iter())?;
2842 old_last_updated_sequences[index].mask(deletions.to_sorted_iter())?;
2843 }
2844 }
2845
2846 debug_assert_eq!(
2847 { old_sequences.iter().map(|(_, seq)| seq.len()).sum::<u64>() },
2848 { chunk_sizes.iter().sum::<u64>() },
2849 "{:?}",
2850 old_sequences
2851 );
2852
2853 let new_row_ids = lance_table::rowids::rechunk_sequences(
2854 old_sequences
2855 .into_iter()
2856 .map(|(_, seq)| seq.as_ref().clone()),
2857 chunk_sizes.iter().copied(),
2858 false,
2859 )?;
2860 let new_created_at = lance_table::rowids::version::rechunk_version_sequences(
2861 old_created_at_sequences,
2862 chunk_sizes.iter().copied(),
2863 false,
2864 )?;
2865 let new_last_updated_at = lance_table::rowids::version::rechunk_version_sequences(
2866 old_last_updated_sequences,
2867 chunk_sizes.iter().copied(),
2868 false,
2869 )?;
2870
2871 Ok(new_row_ids
2872 .into_iter()
2873 .zip(new_created_at)
2874 .zip(new_last_updated_at)
2875 .map(|((row_ids, created_at), last_updated_at)| RowLineage {
2876 row_ids,
2877 created_at,
2878 last_updated_at,
2879 })
2880 .collect())
2881}
2882
2883async fn rechunk_row_lineage(
2889 dataset: &Dataset,
2890 new_fragments: &mut [Fragment],
2891 old_fragments: &[Fragment],
2892) -> Result<()> {
2893 let chunk_sizes: Vec<u64> = new_fragments
2894 .iter()
2895 .map(|frag| frag.physical_rows.unwrap() as u64)
2896 .collect();
2897 let lineages = compute_row_lineage(dataset, old_fragments, &chunk_sizes).await?;
2898 for (fragment, lineage) in new_fragments.iter_mut().zip(lineages) {
2899 place_row_lineage(dataset, &lineage).await?.apply(fragment);
2900 }
2901 Ok(())
2902}
2903
2904struct PlannedRowLineage {
2907 lineages: Vec<RowLineage>,
2911 planned_rows: Vec<u64>,
2914 plan: RowLineagePlan,
2915}
2916
2917async fn place_planned_row_lineage(
2920 dataset: &Dataset,
2921 new_fragments: &mut [Fragment],
2922 planned: PlannedRowLineage,
2923) -> Result<()> {
2924 let PlannedRowLineage {
2925 lineages,
2926 planned_rows,
2927 plan,
2928 } = planned;
2929 match plan {
2930 RowLineagePlan::InFile(spill) => {
2931 place_row_lineage_in_files(new_fragments, lineages, &planned_rows, spill)
2932 }
2933 RowLineagePlan::PerFragment => {
2934 let lineages = fit_row_lineage_to_written(
2935 new_fragments,
2936 lineages,
2937 &planned_rows,
2938 RowLineageSpill::default(),
2939 )?;
2940 for (fragment, lineage) in new_fragments.iter_mut().zip(lineages) {
2941 place_row_lineage(dataset, &lineage).await?.apply(fragment);
2942 }
2943 Ok(())
2944 }
2945 }
2946}
2947
2948fn place_row_lineage_in_files(
2952 new_fragments: &mut [Fragment],
2953 lineages: Vec<RowLineage>,
2954 planned_rows: &[u64],
2955 spill: RowLineageSpill,
2956) -> Result<()> {
2957 let lineages = fit_row_lineage_to_written(new_fragments, lineages, planned_rows, spill)?;
2958 for (fragment, lineage) in new_fragments.iter_mut().zip(&lineages) {
2959 if spill.any() {
2960 let [file] = fragment.files.as_slice() else {
2961 return Err(Error::internal(format!(
2962 "compaction wrote {} data files for fragment {}; the row lineage columns \
2963 are in exactly one",
2964 fragment.files.len(),
2965 fragment.id
2966 )));
2967 };
2968 let missing = spill
2969 .field_ids()
2970 .find(|field_id| !file.fields.contains(field_id));
2971 if let Some(field_id) = missing {
2972 return Err(Error::internal(format!(
2973 "compaction wrote fragment {}'s data file {} without row lineage field {}",
2974 fragment.id, file.path, field_id
2975 )));
2976 }
2977 }
2978 spill.place_in_file(lineage).apply(fragment);
2979 }
2980 Ok(())
2981}
2982
2983fn fit_row_lineage_to_written(
2999 new_fragments: &[Fragment],
3000 lineages: Vec<RowLineage>,
3001 planned_rows: &[u64],
3002 in_file: RowLineageSpill,
3003) -> Result<Vec<RowLineage>> {
3004 let written_rows = new_fragments
3005 .iter()
3006 .map(|fragment| match fragment.physical_rows {
3007 Some(rows) => Ok(rows as u64),
3008 None => Err(Error::internal(format!(
3009 "compaction wrote fragment {} without a physical row count",
3010 fragment.id
3011 ))),
3012 })
3013 .collect::<Result<Vec<_>>>()?;
3014 let planned_total = planned_rows.iter().sum::<u64>();
3015 let written_total = written_rows.iter().sum::<u64>();
3016 if written_total != planned_total {
3017 return Err(Error::internal(format!(
3018 "compaction wrote {written_total} rows, in fragments of {written_rows:?} rows, \
3019 but planned row lineage for {planned_total} rows, in fragments of \
3020 {planned_rows:?} rows"
3021 )));
3022 }
3023
3024 let lineages = if written_rows.as_slice() == planned_rows {
3025 lineages
3026 } else {
3027 let cut_error = |error: Error| {
3028 Error::internal(format!(
3029 "compaction could not cut the row lineage planned for fragments of \
3030 {planned_rows:?} rows at the written fragments of {written_rows:?} rows: \
3031 {error}"
3032 ))
3033 };
3034 let mut row_ids = Vec::with_capacity(lineages.len());
3035 let mut created_at = Vec::with_capacity(lineages.len());
3036 let mut last_updated_at = Vec::with_capacity(lineages.len());
3037 for lineage in lineages {
3038 row_ids.push(lineage.row_ids);
3039 created_at.push(lineage.created_at);
3040 last_updated_at.push(lineage.last_updated_at);
3041 }
3042 let fragment_count = written_rows.len();
3043 let written = || written_rows.iter().copied();
3044 let row_ids = if in_file.row_ids {
3045 vec![RowIdSequence::new(); fragment_count]
3046 } else {
3047 lance_table::rowids::rechunk_sequences(row_ids, written(), false).map_err(cut_error)?
3048 };
3049 let created_at = if in_file.created_at {
3050 vec![RowDatasetVersionSequence::new(); fragment_count]
3051 } else {
3052 lance_table::rowids::version::rechunk_version_sequences(created_at, written(), false)
3053 .map_err(cut_error)?
3054 };
3055 let last_updated_at = if in_file.last_updated_at {
3056 vec![RowDatasetVersionSequence::new(); fragment_count]
3057 } else {
3058 lance_table::rowids::version::rechunk_version_sequences(
3059 last_updated_at,
3060 written(),
3061 false,
3062 )
3063 .map_err(cut_error)?
3064 };
3065 row_ids
3066 .into_iter()
3067 .zip(created_at)
3068 .zip(last_updated_at)
3069 .map(|((row_ids, created_at), last_updated_at)| RowLineage {
3070 row_ids,
3071 created_at,
3072 last_updated_at,
3073 })
3074 .collect::<Vec<_>>()
3075 };
3076
3077 if lineages.len() != new_fragments.len() {
3078 return Err(Error::internal(format!(
3079 "compaction has row lineage for {} fragments but wrote {}",
3080 lineages.len(),
3081 new_fragments.len()
3082 )));
3083 }
3084 for ((fragment, &rows), lineage) in new_fragments.iter().zip(&written_rows).zip(&lineages) {
3085 let inline_lengths = [
3086 (in_file.row_ids, "row ids", lineage.row_ids.len()),
3087 (
3088 in_file.created_at,
3089 "created-at versions",
3090 lineage.created_at.len(),
3091 ),
3092 (
3093 in_file.last_updated_at,
3094 "last-updated-at versions",
3095 lineage.last_updated_at.len(),
3096 ),
3097 ];
3098 let mismatch = inline_lengths
3099 .into_iter()
3100 .find(|&(is_in_file, _, len)| !is_in_file && len != rows);
3101 if let Some((_, sequence, len)) = mismatch {
3102 return Err(Error::internal(format!(
3103 "compaction has {len} inline {sequence} for fragment {} of {rows} physical rows",
3104 fragment.id
3105 )));
3106 }
3107 }
3108 Ok(lineages)
3109}
3110
3111fn append_row_lineage_columns(
3115 stream: SendableRecordBatchStream,
3116 fields: &[LanceField],
3117 columns: Vec<Vec<u64>>,
3118) -> SendableRecordBatchStream {
3119 let mut arrow_fields = stream.schema().fields().to_vec();
3120 arrow_fields.extend(fields.iter().map(|field| Arc::new(ArrowField::from(field))));
3121 let schema = Arc::new(ArrowSchema::new(arrow_fields));
3122 let total_rows = columns.first().map_or(0, |values| values.len());
3123 let batch_schema = schema.clone();
3124 let mut offset = 0usize;
3125 let batches = stream.map(move |batch| {
3126 let batch = batch?;
3127 let rows = batch.num_rows();
3128 if offset + rows > total_rows {
3129 return Err(datafusion::error::DataFusionError::External(Box::new(
3130 Error::internal(format!(
3131 "compaction read more rows than its row lineage covers ({total_rows})"
3132 )),
3133 )));
3134 }
3135 let mut arrays = batch.columns().to_vec();
3136 for values in &columns {
3140 let batch_values = values[offset..offset + rows].to_vec();
3141 arrays.push(Arc::new(arrow_array::UInt64Array::from(batch_values)) as ArrayRef);
3142 }
3143 offset += rows;
3144 RecordBatch::try_new(batch_schema.clone(), arrays)
3145 .map_err(datafusion::error::DataFusionError::from)
3146 });
3147 Box::pin(RecordBatchStreamAdapter::new(schema, batches))
3148}
3149
3150pub async fn commit_compaction(
3157 dataset: &mut Dataset,
3158 completed_tasks: Vec<RewriteResult>,
3159 remap_options: Arc<dyn IndexRemapperOptions>,
3160 options: &CompactionOptions,
3161) -> Result<CompactionMetrics> {
3162 if completed_tasks.is_empty() {
3163 return Ok(CompactionMetrics::default());
3164 }
3165
3166 let tagged_fri = load_all_indices(dataset)
3172 .await?
3173 .iter()
3174 .any(lance_table::system_index::frag_reuse::metadata::is_tagged);
3175 if tagged_fri && !options.defer_index_remap {
3176 return Err(Error::not_supported(
3177 "Compacting a table with a tagged fragment reuse history requires \
3178 defer_index_remap; set defer_index_remap, or upgrade to a writer that \
3179 remaps tagged histories eagerly",
3180 ));
3181 }
3182
3183 if !dataset.manifest.uses_stable_row_ids() && !options.defer_index_remap {
3188 reject_unremappable_rewrite(dataset, &completed_tasks).await?;
3189 }
3190
3191 let has_address_style = completed_tasks.iter().any(|t| t.row_addrs.is_some());
3192 let needs_remapping =
3194 !dataset.manifest.uses_stable_row_ids() && !options.defer_index_remap && has_address_style;
3195
3196 let index_remapper = if needs_remapping {
3198 remap_options.create_remapper(dataset).await?
3199 } else {
3200 None
3201 };
3202
3203 let tasks_read_version = completed_tasks
3217 .iter()
3218 .map(|t| t.read_version)
3219 .min()
3220 .unwrap_or(dataset.manifest.version);
3221
3222 let mut completed_tasks = completed_tasks;
3223
3224 let all_new_fragments: Vec<Fragment> = completed_tasks
3229 .iter()
3230 .flat_map(|t| t.new_fragments.iter().cloned())
3231 .collect();
3232
3233 if has_address_style {
3235 let frags: Vec<&mut Fragment> = completed_tasks
3236 .iter_mut()
3237 .filter(|t| t.row_addrs.is_some())
3238 .flat_map(|t| t.new_fragments.iter_mut())
3239 .collect();
3240 if let Err(e) = reserve_fragment_ids(dataset, frags.into_iter()).await {
3241 cleanup_compaction_files_after_reservation_failure(dataset, &all_new_fragments).await;
3242 return Err(e);
3243 }
3244 }
3245
3246 let mut rewrite_groups = Vec::with_capacity(completed_tasks.len());
3247 let mut metrics = CompactionMetrics::default();
3248
3249 let mut remap_group_inputs: Vec<GroupInput> = Vec::new();
3250 let mut direct_row_id_map: HashMap<u64, Option<u64>> = HashMap::default();
3251 let mut frag_reuse_groups: Vec<FragReuseGroup> = Vec::new();
3252 let mut new_fragment_bitmap: RoaringBitmap = RoaringBitmap::new();
3253
3254 let indexed_frags: RoaringBitmap = if options.defer_index_remap {
3268 let mut covered = RoaringBitmap::new();
3269 for bm in load_index_fragmaps(dataset).await? {
3270 covered |= bm;
3271 }
3272 if let Some(bm) = load_all_indices(dataset)
3275 .await?
3276 .iter()
3277 .find(|idx| idx.name == FRAG_REUSE_INDEX_NAME)
3278 .and_then(|fri| fri.fragment_bitmap.clone())
3279 {
3280 covered |= bm;
3281 }
3282 covered
3283 } else {
3284 RoaringBitmap::new()
3285 };
3286 let mut any_group_indexed = false;
3287
3288 for task in completed_tasks {
3289 metrics += task.metrics;
3290 let old_fragments = if index_remapper.is_none() && options.defer_index_remap {
3295 normalize_source_fragments(dataset, &task.original_fragments).await?
3296 } else {
3297 task.original_fragments.clone()
3298 };
3299 let rewrite_group = RewriteGroup {
3300 old_fragments: old_fragments.clone(),
3301 new_fragments: task.new_fragments.clone(),
3302 };
3303
3304 if index_remapper.is_some() {
3305 if let Some(row_addrs_bytes) = task.row_addrs {
3306 let row_addrs =
3307 RoaringTreemap::deserialize_from(&mut Cursor::new(&row_addrs_bytes))?;
3308 match options.index_remap_mode {
3309 IndexRemapMode::Direct => {
3310 let transposed = remapping::transpose_row_addrs(
3311 row_addrs,
3312 &task.original_fragments,
3313 &task.new_fragments,
3314 );
3315 direct_row_id_map.extend(transposed);
3316 }
3317 IndexRemapMode::Compact => {
3318 let new_frags = task
3319 .new_fragments
3320 .iter()
3321 .map(|f| {
3322 let physical_rows = f.physical_rows.ok_or_else(|| {
3323 Error::invalid_input(format!(
3324 "compacted fragment {} is missing physical_rows",
3325 f.id
3326 ))
3327 })?;
3328 let fragment_id = u32::try_from(f.id).map_err(|_| {
3329 Error::invalid_input(format!(
3330 "compacted fragment id {} is outside the row-address range",
3331 f.id
3332 ))
3333 })?;
3334 let physical_rows = u32::try_from(physical_rows).map_err(|_| {
3335 Error::invalid_input(format!(
3336 "compacted fragment {} has physical_rows={} outside the row-address range",
3337 f.id, physical_rows
3338 ))
3339 })?;
3340 Ok((fragment_id, physical_rows))
3341 })
3342 .collect::<Result<Vec<_>>>()?;
3343
3344 remap_group_inputs.push(GroupInput {
3345 rewritten_old_row_addrs: row_addrs,
3346 old_frag_ids: task
3347 .original_fragments
3348 .iter()
3349 .map(|f| {
3350 u32::try_from(f.id).map_err(|_| {
3351 Error::invalid_input(format!(
3352 "compacted source fragment id {} is outside the row-address range",
3353 f.id
3354 ))
3355 })
3356 })
3357 .collect::<Result<Vec<_>>>()?,
3358 new_frags,
3359 });
3360 }
3361 }
3362 }
3363 } else if options.defer_index_remap {
3364 if old_fragments
3368 .iter()
3369 .any(|f| u32::try_from(f.id).is_ok_and(|id| indexed_frags.contains(id)))
3370 {
3371 any_group_indexed = true;
3372 }
3373 let changed_row_addrs = task.row_addrs.ok_or_else(|| {
3374 Error::internal(
3375 "defer_index_remap requires row_addrs but none were provided".to_string(),
3376 )
3377 })?;
3378 let digest =
3383 |frag: &Fragment| -> Result<lance_table::system_index::frag_reuse::FragDigest> {
3384 Ok(lance_table::system_index::frag_reuse::FragDigest {
3385 id: frag.id,
3386 physical_rows: frag.physical_rows.ok_or_else(|| {
3387 Error::invalid_input(format!(
3388 "fragment {} has no physical row count for the fragment reuse record",
3389 frag.id
3390 ))
3391 })?,
3392 num_deleted_rows: frag
3393 .deletion_file
3394 .as_ref()
3395 .map(|deletion| {
3396 deletion.num_deleted_rows.ok_or_else(|| {
3397 Error::invalid_input(format!(
3398 "fragment {} has a deletion file without a materialized row count",
3399 frag.id
3400 ))
3401 })
3402 })
3403 .transpose()?
3404 .unwrap_or(0),
3405 })
3406 };
3407 frag_reuse_groups.push(FragReuseGroup {
3408 changed_row_addrs,
3409 old_frags: old_fragments.iter().map(digest).collect::<Result<_>>()?,
3410 new_frags: task
3411 .new_fragments
3412 .iter()
3413 .map(digest)
3414 .collect::<Result<_>>()?,
3415 });
3416 for frag in task.new_fragments.iter() {
3417 new_fragment_bitmap.insert(u32::try_from(frag.id).map_err(|_| {
3418 Error::invalid_input(format!(
3419 "compacted fragment id {} is outside the row-address range",
3420 frag.id
3421 ))
3422 })?);
3423 }
3424 }
3425 rewrite_groups.push(rewrite_group);
3426 }
3427
3428 let rewritten_indices = if let Some(index_remapper) = index_remapper {
3429 let affected_ids = rewrite_groups
3430 .iter()
3431 .flat_map(|group| group.old_fragments.iter().map(|frag| frag.id))
3432 .collect::<Vec<_>>();
3433
3434 let remap = match options.index_remap_mode {
3435 IndexRemapMode::Direct => RowAddrRemap::direct(direct_row_id_map),
3436 IndexRemapMode::Compact => RowAddrRemap::compact(remap_group_inputs)?,
3437 };
3438 let remapped_indices = index_remapper.remap_indices(remap, &affected_ids).await?;
3439 remapped_indices
3440 .into_iter()
3441 .map(|rewritten| RewrittenIndex {
3442 old_id: rewritten.old_id,
3443 new_id: rewritten.new_id,
3444 new_index_details: rewritten.index_details,
3445 new_index_version: rewritten.index_version,
3446 new_index_files: rewritten.files,
3447 })
3448 .collect()
3449 } else if !options.defer_index_remap && !has_address_style {
3450 let new_fragments = rewrite_groups
3454 .iter_mut()
3455 .flat_map(|group| group.new_fragments.iter_mut())
3456 .collect::<Vec<_>>();
3457 if let Err(e) = reserve_fragment_ids(dataset, new_fragments.into_iter()).await {
3458 cleanup_compaction_files_after_reservation_failure(dataset, &all_new_fragments).await;
3459 return Err(e);
3460 }
3461 Vec::new()
3462 } else {
3463 Vec::new()
3464 };
3465
3466 let frag_reuse_index = if options.defer_index_remap && any_group_indexed {
3489 let stored = load_all_indices(dataset).await?;
3501 let tagged_at_commit = lance_table::system_index::frag_reuse::metadata::uses_tagged_fri(
3502 &dataset.manifest,
3503 stored.iter().find(|idx| idx.name == FRAG_REUSE_INDEX_NAME),
3504 );
3505 if tagged_at_commit {
3506 if let Some(overlaid) = rewrite_groups
3514 .iter()
3515 .flat_map(|group| group.old_fragments.iter())
3516 .find(|frag| !frag.overlays.is_empty())
3517 {
3518 return Err(Error::not_supported(format!(
3519 "source fragment {} carries data overlay files; deferred compaction on a \
3520 tagged fragment reuse table would materialize the overlaid values while \
3521 indices keep translated coverage over the old addresses. Compact the \
3522 overlays away or rebuild the covering indices eagerly first",
3523 overlaid.id
3524 )));
3525 }
3526 use lance_table::format::pb::fragment_reuse_index_details as pb_fri;
3527 let transitions = frag_reuse_groups
3528 .into_iter()
3529 .map(|group| pb_fri::Transition {
3530 sources: group
3531 .old_frags
3532 .iter()
3533 .map(pb_fri::FragmentDigest::from)
3534 .collect(),
3535 destinations: group
3536 .new_frags
3537 .iter()
3538 .map(pb_fri::FragmentDigest::from)
3539 .collect(),
3540 mapping: Some(pb_fri::transition::Mapping::OrderedCompaction(
3541 pb_fri::OrderedCompaction {
3542 changed_row_addrs: group.changed_row_addrs,
3543 },
3544 )),
3545 })
3546 .collect();
3547 let read_snapshot = if dataset.manifest.version == tasks_read_version {
3552 Cow::Borrowed(&*dataset)
3553 } else {
3554 Cow::Owned(dataset.checkout_version(tasks_read_version).await?)
3557 };
3558 Some(
3559 crate::index::frag_reuse::frag_reuse_entry_appending(&read_snapshot, transitions)
3560 .await?,
3561 )
3562 } else {
3563 Some(build_new_frag_reuse_index(dataset, frag_reuse_groups, new_fragment_bitmap).await?)
3564 }
3565 } else {
3566 if options.defer_index_remap {
3567 log::debug!(
3568 "skipping fragment reuse record: no rewritten fragments are covered by an index or the reuse lineage"
3569 );
3570 }
3571 None
3572 };
3573
3574 let transaction = TransactionBuilder::new(
3575 tasks_read_version,
3583 Operation::Rewrite {
3584 groups: rewrite_groups,
3585 rewritten_indices,
3586 frag_reuse_index,
3587 },
3588 )
3589 .transaction_properties(options.transaction_properties.clone())
3590 .build();
3591
3592 if let Err(e) = dataset
3593 .apply_commit(transaction, &Default::default(), &Default::default())
3594 .await
3595 {
3596 log::warn!(
3600 "Compaction commit failed; leaving {} rewritten fragment(s) in place for GC: {}",
3601 all_new_fragments.len(),
3602 e
3603 );
3604 return Err(e);
3605 }
3606
3607 Ok(metrics)
3608}
3609
3610async fn normalize_source_fragments(
3618 dataset: &Dataset,
3619 fragments: &[Fragment],
3620) -> Result<Vec<Fragment>> {
3621 let mut normalized = Vec::with_capacity(fragments.len());
3622 for fragment in fragments {
3623 let mut fragment = fragment.clone();
3624 if fragment.physical_rows.is_none() {
3625 let file_fragment = FileFragment::new(Arc::new(dataset.clone()), fragment.clone());
3626 fragment.physical_rows = Some(file_fragment.physical_rows().await?);
3627 }
3628 if fragment
3629 .deletion_file
3630 .as_ref()
3631 .is_some_and(|deletion| deletion.num_deleted_rows.is_none())
3632 {
3633 let deletion_file = fragment.deletion_file.as_ref().unwrap();
3634 let count = read_dataset_deletion_file(dataset, fragment.id, deletion_file)
3635 .await?
3636 .len();
3637 fragment.deletion_file.as_mut().unwrap().num_deleted_rows = Some(count);
3638 }
3639 normalized.push(fragment);
3640 }
3641 Ok(normalized)
3642}
3643
3644async fn cleanup_compaction_files_after_reservation_failure(
3648 dataset: &Dataset,
3649 all_new_fragments: &[Fragment],
3650) {
3651 cleanup_data_fragments(
3652 &dataset.object_store,
3653 &dataset.base,
3654 None,
3655 all_new_fragments,
3656 )
3657 .await;
3658}
3659
3660#[cfg(test)]
3661mod tests {
3662
3663 mod binary_copy;
3664 use self::remapping::RemappedIndex;
3665 use super::*;
3666 use crate::dataset::WriteDestination;
3667 use crate::dataset::index::frag_reuse::cleanup_frag_reuse_index;
3668 use crate::dataset::optimize::remapping::{transpose_row_addrs, transpose_row_ids_from_digest};
3669 use crate::dataset::rowids::read_spilled_row_ids;
3670 use crate::dataset::rowids::{
3671 INLINE_ROW_LINEAGE_MAX_BYTES_CONFIG_KEY, SPILL_ROW_LINEAGE_CONFIG_KEY,
3672 };
3673 use crate::dataset::scanner::ColumnOrdering;
3674 use crate::index::DatasetIndexExt;
3675 use crate::index::frag_reuse::{load_frag_reuse_index_details, open_frag_reuse_index};
3676 use crate::index::vector::{StageParams, VectorIndexParams};
3677 use crate::utils::test::{DatagenExt, FragmentCount, FragmentRowCount};
3678 use arrow_array::types::{Float32Type, Float64Type, Int32Type, Int64Type};
3679 use arrow_array::{
3680 ArrayRef, Float32Array, Int32Array, Int64Array, LargeBinaryArray, LargeStringArray,
3681 PrimitiveArray, RecordBatch, RecordBatchIterator,
3682 };
3683 use arrow_schema::{DataType, Field, Schema};
3684 use arrow_select::concat::concat_batches;
3685 use async_trait::async_trait;
3686 use lance_arrow::BLOB_META_KEY;
3687 use lance_core::Error;
3688 use lance_core::ROW_ID;
3689 use lance_core::ROW_ID_FIELD_ID;
3690 use lance_core::utils::address::RowAddress;
3691 use lance_core::utils::tempfile::TempStrDir;
3692 use lance_datagen::Dimension;
3693 use lance_file::version::LanceFileVersion;
3694 use lance_index::frag_reuse::CompactFragReuseIndexHandle;
3695 use lance_index::frag_reuse::FRAG_REUSE_INDEX_NAME;
3696 use lance_index::scalar::{
3697 BuiltinIndexType, FullTextSearchQuery, InvertedIndexParams, ScalarIndexParams,
3698 };
3699 use lance_index::vector::ivf::IvfBuildParams;
3700 use lance_index::vector::pq::PQBuildParams;
3701 use lance_index::{Index, IndexType};
3702 use lance_linalg::distance::{DistanceType, MetricType};
3703 use lance_table::format::RowIdMeta;
3704 use lance_table::io::manifest::read_manifest_indexes;
3705 use lance_table::rowids::read_row_ids;
3706 use lance_testing::datagen::{BatchGenerator, IncrementingInt32, RandomVector};
3707 use rstest::rstest;
3708 use std::collections::HashSet;
3709 use std::io::Cursor;
3710 use std::sync::Arc;
3711 use uuid::Uuid;
3712
3713 #[test]
3714 fn test_candidate_bin() {
3715 let empty_bin = CandidateBin {
3716 fragments: vec![],
3717 pos_range: 0..0,
3718 candidacy: vec![],
3719 row_counts: vec![],
3720 indices: vec![],
3721 };
3722 assert!(empty_bin.is_noop());
3723
3724 let fragment = Fragment {
3725 id: 0,
3726 files: vec![],
3727 overlays: vec![],
3728 deletion_file: None,
3729 row_id_meta: None,
3730 physical_rows: Some(0),
3731 last_updated_at_version_meta: None,
3732 created_at_version_meta: None,
3733 };
3734 let single_bin = CandidateBin {
3735 fragments: vec![fragment.clone()],
3736 pos_range: 0..1,
3737 candidacy: vec![CompactionCandidacy::CompactWithNeighbors],
3738 row_counts: vec![100],
3739 indices: vec![],
3740 };
3741 assert!(single_bin.is_noop());
3742
3743 let single_bin = CandidateBin {
3744 fragments: vec![fragment.clone()],
3745 pos_range: 0..1,
3746 candidacy: vec![CompactionCandidacy::CompactItself],
3747 row_counts: vec![100],
3748 indices: vec![],
3749 };
3750 assert!(!single_bin.is_noop());
3752
3753 let big_bin = CandidateBin {
3754 fragments: std::iter::repeat_n(fragment, 8).collect(),
3755 pos_range: 0..8,
3756 candidacy: std::iter::repeat_n(CompactionCandidacy::CompactItself, 8).collect(),
3757 row_counts: vec![100, 400, 200, 200, 400, 300, 300, 100],
3758 indices: vec![],
3759 };
3762 assert!(!big_bin.is_noop());
3763 let split = big_bin.split_for_size(500);
3764 assert_eq!(split.len(), 3);
3765 assert_eq!(split[0].pos_range, 0..2);
3766 assert_eq!(split[1].pos_range, 2..5);
3767 assert_eq!(split[2].pos_range, 5..8);
3768
3769 let zero_min_split_bin = CandidateBin {
3770 fragments: std::iter::repeat_n(
3771 Fragment {
3772 id: 0,
3773 files: vec![],
3774 overlays: vec![],
3775 deletion_file: None,
3776 row_id_meta: None,
3777 physical_rows: Some(0),
3778 last_updated_at_version_meta: None,
3779 created_at_version_meta: None,
3780 },
3781 3,
3782 )
3783 .collect(),
3784 pos_range: 0..3,
3785 candidacy: std::iter::repeat_n(CompactionCandidacy::CompactItself, 3).collect(),
3786 row_counts: vec![100, 200, 300],
3787 indices: vec![],
3788 };
3789 let split = zero_min_split_bin.split_for_size(0);
3790 assert_eq!(split.len(), 3);
3791 assert!(split.iter().all(|bin| !bin.fragments.is_empty()));
3792 assert_eq!(split[0].pos_range, 0..1);
3793 assert_eq!(split[1].pos_range, 1..2);
3794 assert_eq!(split[2].pos_range, 2..3);
3795 }
3796
3797 fn sample_data() -> RecordBatch {
3798 let schema = Schema::new(vec![Field::new("a", DataType::Int64, false)]);
3799
3800 RecordBatch::try_new(
3801 Arc::new(schema),
3802 vec![Arc::new(Int64Array::from_iter_values(0..10_000))],
3803 )
3804 .unwrap()
3805 }
3806
3807 async fn create_scalar_index(dataset: &mut Dataset, col: &str, replace: bool) {
3809 dataset
3810 .create_index(
3811 &[col],
3812 IndexType::Scalar,
3813 Some("scalar".into()),
3814 &ScalarIndexParams::default(),
3815 replace,
3816 )
3817 .await
3818 .unwrap();
3819 }
3820
3821 #[derive(Debug, Default, Clone, PartialEq)]
3822 struct MockIndexRemapperExpectation {
3823 expected: HashMap<u64, Option<u64>>,
3824 answer: Vec<RemappedIndex>,
3825 }
3826
3827 #[derive(Debug, Default, Clone, PartialEq)]
3828 struct MockIndexRemapper {
3829 expectations: Vec<MockIndexRemapperExpectation>,
3830 }
3831
3832 impl MockIndexRemapper {
3833 fn stringify_map(map: &HashMap<u64, Option<u64>>) -> String {
3834 let mut sorted_keys = map.keys().collect::<Vec<_>>();
3835 sorted_keys.sort();
3836 let mut first_keys = sorted_keys
3837 .into_iter()
3838 .take(10)
3839 .map(|key| {
3840 format!(
3841 "{}:{:?}",
3842 RowAddress::from(*key),
3843 map[key].map(RowAddress::from)
3844 )
3845 })
3846 .collect::<Vec<_>>()
3847 .join(",");
3848 if map.len() > 10 {
3849 first_keys.push_str(", ...");
3850 }
3851 let mut result_str = format!("(len={})", map.len());
3852 result_str.push_str(&first_keys);
3853 result_str
3854 }
3855
3856 fn in_any_order(expectations: &[Self]) -> Self {
3857 let expectations = expectations
3858 .iter()
3859 .flat_map(|item| item.expectations.clone())
3860 .collect::<Vec<_>>();
3861 Self { expectations }
3862 }
3863 }
3864
3865 #[async_trait]
3866 impl IndexRemapper for MockIndexRemapper {
3867 async fn remap_indices(
3868 &self,
3869 index_map: RowAddrRemap,
3870 _: &[u64],
3871 ) -> Result<Vec<RemappedIndex>> {
3872 for expectation in &self.expectations {
3873 let matches = match &index_map {
3874 RowAddrRemap::Direct(map) => map == &expectation.expected,
3875 RowAddrRemap::Compact(_) => {
3876 let expected_frags: RoaringBitmap = expectation
3877 .expected
3878 .keys()
3879 .map(|addr| (addr >> 32) as u32)
3880 .collect();
3881 index_map.affected_fragments() == expected_frags
3882 && expectation
3883 .expected
3884 .iter()
3885 .all(|(k, v)| index_map.get(*k) == Some(*v))
3886 }
3887 };
3888 if matches {
3889 return Ok(expectation.answer.clone());
3890 }
3891 }
3892 panic!(
3893 "Unexpected index map; expected one of:\n {}",
3894 self.expectations
3895 .iter()
3896 .map(|expectation| Self::stringify_map(&expectation.expected))
3897 .collect::<Vec<_>>()
3898 .join("\n ")
3899 );
3900 }
3901 }
3902
3903 #[async_trait]
3904 impl IndexRemapperOptions for MockIndexRemapper {
3905 async fn create_remapper(&self, _: &Dataset) -> Result<Option<Box<dyn IndexRemapper>>> {
3906 Ok(Some(Box::new(self.clone())))
3907 }
3908 }
3909
3910 #[rstest]
3911 #[tokio::test]
3912 async fn test_compact_empty(
3913 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
3914 data_storage_version: LanceFileVersion,
3915 ) {
3916 let test_dir = TempStrDir::default();
3917 let test_uri = &test_dir;
3918
3919 let schema = Schema::new(vec![Field::new("a", DataType::Int64, false)]);
3921
3922 let reader = RecordBatchIterator::new(vec![].into_iter().map(Ok), Arc::new(schema));
3923 let mut dataset = Dataset::write(
3924 reader,
3925 test_uri,
3926 Some(WriteParams {
3927 data_storage_version: Some(data_storage_version),
3928 ..Default::default()
3929 }),
3930 )
3931 .await
3932 .unwrap();
3933
3934 let plan = plan_compaction(&dataset, &CompactionOptions::default())
3935 .await
3936 .unwrap();
3937 assert_eq!(plan.tasks().len(), 0);
3938
3939 let metrics = compact_files(&mut dataset, CompactionOptions::default(), None)
3940 .await
3941 .unwrap();
3942
3943 assert_eq!(metrics, CompactionMetrics::default());
3944 assert_eq!(dataset.manifest.version, 1);
3945 }
3946
3947 #[tokio::test]
3948 async fn test_compact_v2_0_file_missing_evolved_struct_child() {
3949 let old_child = Arc::new(Field::new("old", DataType::Int32, true));
3950 let schema = Arc::new(Schema::new(vec![
3951 Field::new(
3952 "values",
3953 DataType::Struct(vec![old_child.clone()].into()),
3954 true,
3955 ),
3956 Field::new("id", DataType::Int32, false),
3957 ]));
3958 let batch = RecordBatch::try_new(
3959 schema.clone(),
3960 vec![
3961 Arc::new(StructArray::from(vec![(
3962 old_child,
3963 Arc::new(Int32Array::from(vec![10, 20, 30, 40])) as ArrayRef,
3964 )])),
3965 Arc::new(Int32Array::from(vec![0, 1, 2, 3])),
3966 ],
3967 )
3968 .unwrap();
3969 let mut dataset = Dataset::write(
3970 RecordBatchIterator::new([Ok(batch)], schema),
3971 "memory://",
3972 Some(WriteParams {
3973 data_storage_version: Some(LanceFileVersion::V2_0),
3974 max_rows_per_file: 1,
3975 ..Default::default()
3976 }),
3977 )
3978 .await
3979 .unwrap();
3980
3981 let mut evolved_schema = dataset.schema().clone();
3985 let parent_id = evolved_schema.fields[0].id;
3986 let mut later_child = LanceField::new_arrow("later", DataType::Int32, true).unwrap();
3987 later_child.id = evolved_schema.max_field_id().unwrap() + 1;
3988 later_child.parent_id = parent_id;
3989 evolved_schema.fields[0].children = vec![later_child];
3990 let read_version = dataset.version().version;
3991 dataset = Dataset::commit(
3992 WriteDestination::Dataset(Arc::new(dataset)),
3993 Operation::Project {
3994 schema: evolved_schema,
3995 preserves_nullability: true,
3996 },
3997 Some(read_version),
3998 None,
3999 None,
4000 Arc::new(Default::default()),
4001 false,
4002 )
4003 .await
4004 .unwrap();
4005
4006 let metrics = compact_files(&mut dataset, CompactionOptions::default(), None)
4007 .await
4008 .unwrap();
4009 assert_eq!(metrics.fragments_removed, 4);
4010 assert_eq!(metrics.fragments_added, 1);
4011
4012 let compacted = dataset.scan().try_into_batch().await.unwrap();
4013 assert_eq!(compacted.num_rows(), 4);
4014 assert_eq!(
4015 compacted["id"].as_ref(),
4016 &Int32Array::from(vec![0, 1, 2, 3])
4017 );
4018 let values = compacted["values"].as_struct();
4019 assert_eq!(values.null_count(), 0);
4020 assert_eq!(values.column_by_name("later").unwrap().null_count(), 4);
4021 }
4022
4023 #[rstest]
4024 #[case::default(None, LanceFileVersion::V2_0)]
4025 #[case::stable(Some(LanceFileVersion::Stable), LanceFileVersion::V2_2)]
4026 #[case::next(Some(LanceFileVersion::Next), LanceFileVersion::V2_3)]
4027 #[tokio::test]
4028 async fn plan_compaction_freezes_storage_version_selector(
4029 #[case] selector: Option<LanceFileVersion>,
4030 #[case] exact: LanceFileVersion,
4031 ) {
4032 let data = arrow_array::record_batch!(("id", Int32, [1, 2, 3, 4, 5, 6])).unwrap();
4033 let mut dataset = Dataset::write(
4034 RecordBatchIterator::new([Ok(data.clone())], data.schema()),
4035 "memory://",
4036 Some(WriteParams {
4037 max_rows_per_file: 2,
4038 data_storage_version: Some(LanceFileVersion::V2_0),
4039 ..Default::default()
4040 }),
4041 )
4042 .await
4043 .unwrap();
4044 let options = CompactionOptions {
4045 data_storage_version: selector,
4046 excluded_fragment_ids: vec![2],
4047 ..Default::default()
4048 };
4049
4050 let plan = plan_compaction(&dataset, &options).await.unwrap();
4051
4052 assert_eq!(plan.options.data_storage_version, Some(exact));
4053 let serialized = serde_json::to_value(&plan).unwrap();
4054 assert_eq!(
4055 serialized["options"]["data_storage_version"],
4056 exact.to_string()
4057 );
4058 assert_eq!(
4059 serde_json::from_value::<CompactionPlan>(serialized).unwrap(),
4060 plan
4061 );
4062 assert_eq!(plan.num_tasks(), 1);
4063 let retained = dataset.manifest.fragments[2].clone();
4064 let task = plan.compaction_tasks().next().unwrap();
4065 let task: CompactionTask =
4066 serde_json::from_slice(&serde_json::to_vec(&task).unwrap()).unwrap();
4067 let result = task.execute(&dataset).await.unwrap();
4068 assert!(
4069 result
4070 .new_fragments
4071 .iter()
4072 .flat_map(Fragment::referenced_lance_files)
4073 .all(|file| file.file_version().unwrap() == exact.resolve())
4074 );
4075 let result: RewriteResult =
4076 serde_json::from_slice(&serde_json::to_vec(&result).unwrap()).unwrap();
4077 commit_compaction(
4078 &mut dataset,
4079 vec![result],
4080 Arc::new(DatasetIndexRemapperOptions::default()),
4081 &options,
4082 )
4083 .await
4084 .unwrap();
4085 assert!(
4086 dataset
4087 .manifest
4088 .fragments
4089 .iter()
4090 .any(|fragment| fragment == &retained)
4091 );
4092 assert_eq!(
4093 dataset.manifest.data_storage_format.lance_file_format(),
4094 ConcreteFileVersion::V2_0
4095 );
4096 let actual = dataset
4097 .scan()
4098 .order_by(Some(vec![ColumnOrdering::asc_nulls_first("id".into())]))
4099 .unwrap()
4100 .try_into_batch()
4101 .await
4102 .unwrap();
4103 assert_eq!(actual, data);
4104 }
4105
4106 #[rstest]
4107 #[case(LanceFileVersion::Legacy, LanceFileVersion::V2_0)]
4108 #[case(LanceFileVersion::V2_0, LanceFileVersion::Legacy)]
4109 #[tokio::test]
4110 async fn compaction_rejects_cross_family_target(
4111 #[case] source: LanceFileVersion,
4112 #[case] target: LanceFileVersion,
4113 ) {
4114 let batch = arrow_array::record_batch!(("id", Int32, [1, 2])).unwrap();
4115 let dataset = Dataset::write(
4116 RecordBatchIterator::new([Ok(batch.clone())], batch.schema()),
4117 "memory://",
4118 Some(WriteParams {
4119 data_storage_version: Some(source),
4120 max_rows_per_file: 1,
4121 ..Default::default()
4122 }),
4123 )
4124 .await
4125 .unwrap();
4126 let options = CompactionOptions {
4127 data_storage_version: Some(target),
4128 ..Default::default()
4129 };
4130 let error = plan_compaction(&dataset, &options).await.unwrap_err();
4131 assert!(matches!(error, Error::InvalidInput { .. }));
4132 assert!(
4133 error
4134 .to_string()
4135 .contains("V1 and V2 storage versions cannot be mixed")
4136 );
4137 let mut task = plan_compaction(&dataset, &CompactionOptions::default())
4139 .await
4140 .unwrap()
4141 .compaction_tasks()
4142 .next()
4143 .unwrap();
4144 task.options = options;
4145 let error = task.execute(&dataset).await.unwrap_err();
4146 assert!(matches!(error, Error::InvalidInput { .. }));
4147 assert!(
4148 error
4149 .to_string()
4150 .contains("V1 and V2 storage versions cannot be mixed")
4151 );
4152 }
4153
4154 #[rstest]
4155 #[case("0.1", Some(LanceFileVersion::Legacy))]
4156 #[case("2.0", Some(LanceFileVersion::V2_0))]
4157 #[case("2.1", Some(LanceFileVersion::V2_1))]
4158 #[case("2.2", Some(LanceFileVersion::V2_2))]
4159 #[case("2.3", Some(LanceFileVersion::V2_3))]
4160 #[case("stable", Some(LanceFileVersion::Stable))]
4161 #[case("next", Some(LanceFileVersion::Next))]
4162 #[case("invalid", None)]
4163 fn compaction_storage_version_config(
4164 #[case] value: &str,
4165 #[case] expected: Option<LanceFileVersion>,
4166 ) {
4167 let config = HashMap::from([(
4168 "lance.compaction.data_storage_version".to_string(),
4169 value.to_string(),
4170 )]);
4171 let result = CompactionOptions::from_dataset_config(&config);
4172 if let Some(expected) = expected {
4173 let options = result.unwrap();
4174 assert_eq!(options.data_storage_version, Some(expected));
4175 let json = serde_json::to_value(&options).unwrap();
4176 assert_eq!(json["data_storage_version"], value);
4177 assert_eq!(
4178 serde_json::from_value::<CompactionOptions>(json).unwrap(),
4179 options
4180 );
4181 } else {
4182 let error = result.unwrap_err();
4183 assert!(matches!(error, Error::InvalidInput { .. }));
4184 assert!(
4185 error
4186 .to_string()
4187 .contains("lance.compaction.data_storage_version")
4188 );
4189 assert!(error.to_string().contains(value));
4190 let error =
4191 serde_json::from_value::<LanceFileVersion>(serde_json::json!(value)).unwrap_err();
4192 assert!(error.to_string().contains(value));
4193 }
4194 }
4195
4196 #[rstest]
4197 #[tokio::test]
4198 async fn test_compact_all_good(
4199 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
4200 data_storage_version: LanceFileVersion,
4201 ) {
4202 let test_dir = TempStrDir::default();
4204 let test_uri = &test_dir;
4205
4206 let data = sample_data();
4207 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
4208 let write_params = WriteParams {
4210 max_rows_per_file: 10_000,
4211 data_storage_version: Some(data_storage_version),
4212 ..Default::default()
4213 };
4214 let dataset = Dataset::write(reader, test_uri, Some(write_params))
4215 .await
4216 .unwrap();
4217
4218 let plan = plan_compaction(&dataset, &CompactionOptions::default())
4220 .await
4221 .unwrap();
4222 assert_eq!(plan.tasks().len(), 0);
4223
4224 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
4226 let write_params = WriteParams {
4227 max_rows_per_file: 3_000,
4228 max_rows_per_group: 1_000,
4229 data_storage_version: Some(data_storage_version),
4230 mode: WriteMode::Overwrite,
4231 ..Default::default()
4232 };
4233 let dataset = Dataset::write(reader, test_uri, Some(write_params))
4234 .await
4235 .unwrap();
4236
4237 let options = CompactionOptions {
4238 target_rows_per_fragment: 3_000,
4239 ..Default::default()
4240 };
4241 let plan = plan_compaction(&dataset, &options).await.unwrap();
4242 assert_eq!(plan.tasks().len(), 0);
4243 }
4244
4245 fn list_data_files(uri: &str) -> std::collections::BTreeSet<String> {
4246 std::fs::read_dir(std::path::Path::new(uri).join("data"))
4247 .map(|rd| {
4248 rd.filter_map(|e| e.ok())
4249 .map(|e| e.file_name().to_string_lossy().into_owned())
4250 .collect()
4251 })
4252 .unwrap_or_default()
4253 }
4254
4255 async fn execute_compaction_plan(
4256 dataset: &Dataset,
4257 options: &CompactionOptions,
4258 ) -> Vec<RewriteResult> {
4259 let plan = plan_compaction(dataset, options).await.unwrap();
4260 assert!(!plan.tasks.is_empty());
4261 let snapshot = dataset.clone();
4262 futures::stream::iter(plan.tasks)
4263 .map(|task| rewrite_files(Cow::Borrowed(&snapshot), task, options))
4264 .buffer_unordered(1)
4265 .try_collect()
4266 .await
4267 .unwrap()
4268 }
4269
4270 #[tokio::test]
4277 async fn distributed_deferred_compaction_merges_with_a_disjoint_concurrent_rewrite() {
4278 use crate::index::DatasetIndexExt;
4279 use arrow_array::cast::AsArray;
4280 let test_dir = TempStrDir::default();
4281 let uri = test_dir.as_str();
4282 let fresh = |uri: &str| {
4283 crate::dataset::builder::DatasetBuilder::from_uri(uri)
4284 .with_session(Arc::new(crate::session::Session::default()))
4285 .load()
4286 };
4287 let mut dataset = lance_datagen::gen_batch()
4288 .col("i", lance_datagen::array::step::<Int32Type>())
4289 .into_dataset(uri, FragmentCount::from(4), FragmentRowCount::from(8))
4290 .await
4291 .unwrap();
4292 dataset
4293 .create_index(
4294 &["i"],
4295 IndexType::Scalar,
4296 Some("i_idx".into()),
4297 &lance_index::scalar::ScalarIndexParams::default(),
4298 false,
4299 )
4300 .await
4301 .unwrap();
4302 let indices: Vec<IndexMetadata> = crate::index::load_all_indices(&dataset)
4304 .await
4305 .unwrap()
4306 .as_ref()
4307 .clone();
4308 {
4309 let manifest = Arc::make_mut(&mut dataset.manifest);
4310 manifest.reader_feature_flags |= lance_table::feature_flags::FLAG_FRAGMENT_REUSE_INDEX;
4311 manifest.writer_feature_flags |= lance_table::feature_flags::FLAG_FRAGMENT_REUSE_INDEX;
4312 }
4313 crate::index::frag_reuse_reader::tests::persist_fixture(&mut dataset, indices).await;
4314
4315 let ours = CompactionOptions {
4317 target_rows_per_fragment: 16,
4318 defer_index_remap: true,
4319 excluded_fragment_ids: vec![2, 3],
4320 ..Default::default()
4321 };
4322 let planned = fresh(uri).await.unwrap();
4323 let read_version = planned.manifest.version;
4324 let completed = execute_compaction_plan(&planned, &ours).await;
4325 assert_eq!(completed.len(), 1);
4326 assert_eq!(completed[0].read_version, read_version);
4327
4328 let mut other = fresh(uri).await.unwrap();
4330 let theirs = CompactionOptions {
4331 target_rows_per_fragment: 16,
4332 defer_index_remap: true,
4333 excluded_fragment_ids: vec![0, 1],
4334 ..Default::default()
4335 };
4336 compact_files(&mut other, theirs, None).await.unwrap();
4337 assert!(other.manifest.version > read_version);
4339
4340 let mut driver = fresh(uri).await.unwrap();
4342 commit_compaction(
4343 &mut driver,
4344 completed,
4345 Arc::new(DatasetIndexRemapperOptions::default()),
4346 &ours,
4347 )
4348 .await
4349 .unwrap();
4350
4351 let committed = fresh(uri).await.unwrap();
4352 assert_eq!(committed.fragments().len(), 2);
4353 let stored = crate::index::load_all_indices(&committed).await.unwrap();
4354 let entry = stored
4355 .iter()
4356 .find(|idx| idx.name == lance_index::frag_reuse::FRAG_REUSE_INDEX_NAME)
4357 .expect("both compactions recorded their transitions");
4358 let ledger = crate::index::frag_reuse::decode_frag_reuse_ledger(&committed, entry)
4359 .await
4360 .unwrap();
4361 assert_eq!(ledger.transitions().len(), 2);
4362 let batch = committed.scan().try_into_batch().await.unwrap();
4363 let mut values: Vec<i32> = batch["i"]
4364 .as_primitive::<Int32Type>()
4365 .iter()
4366 .map(|v| v.unwrap())
4367 .collect();
4368 values.sort_unstable();
4369 assert_eq!(values, (0..32).collect::<Vec<_>>());
4370 for predicate in ["i = 3", "i = 20", "i > 10"] {
4371 let indexed = committed
4372 .scan()
4373 .filter(predicate)
4374 .unwrap()
4375 .use_scalar_index(true)
4376 .try_into_batch()
4377 .await
4378 .unwrap();
4379 let scanned = committed
4380 .scan()
4381 .filter(predicate)
4382 .unwrap()
4383 .use_scalar_index(false)
4384 .try_into_batch()
4385 .await
4386 .unwrap();
4387 assert_eq!(indexed, scanned, "{predicate}");
4388 }
4389 }
4390
4391 #[tokio::test]
4396 async fn test_compaction_retry_after_ambiguous_success_preserves_live_files() {
4397 use crate::utils::test::{AmbiguousCommitHandler, AmbiguousFailure};
4398
4399 let test_dir = TempStrDir::default();
4400 let test_uri = test_dir.as_str();
4401 let data = sample_data();
4402 let num_rows = data.num_rows();
4403 let handler = Arc::new(AmbiguousCommitHandler::default());
4404
4405 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
4406 let write_params = WriteParams {
4407 max_rows_per_file: 3_000,
4408 enable_stable_row_ids: true,
4409 commit_handler: Some(handler.clone()),
4410 ..Default::default()
4411 };
4412 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
4413 .await
4414 .unwrap();
4415 let files_before = list_data_files(test_uri);
4416 let options = CompactionOptions::default();
4417 let completed = execute_compaction_plan(&dataset, &options).await;
4418 let serialized = serde_json::to_vec(&completed).unwrap();
4419 let retry_results: Vec<RewriteResult> = serde_json::from_slice(&serialized).unwrap();
4420 let rewritten_files = list_data_files(test_uri)
4421 .difference(&files_before)
4422 .cloned()
4423 .collect::<Vec<_>>();
4424 assert!(!rewritten_files.is_empty());
4425
4426 handler.fail_next_rewrite(AmbiguousFailure::LandAndError);
4427 handler
4428 .fail_resolve
4429 .store(true, std::sync::atomic::Ordering::SeqCst);
4430 let err = commit_compaction(
4431 &mut dataset,
4432 completed,
4433 Arc::new(DatasetIndexRemapperOptions::default()),
4434 &options,
4435 )
4436 .await
4437 .expect_err("unknown commit status must surface as an error");
4438 assert!(
4439 err.is_commit_status_unknown(),
4440 "expected CommitStatusUnknown, got: {:?}",
4441 err
4442 );
4443
4444 handler
4445 .fail_resolve
4446 .store(false, std::sync::atomic::Ordering::SeqCst);
4447
4448 let retry_error = commit_compaction(
4451 &mut dataset,
4452 retry_results,
4453 Arc::new(DatasetIndexRemapperOptions::default()),
4454 &options,
4455 )
4456 .await
4457 .expect_err("the replayed rewrite must conflict with its landed predecessor");
4458 assert!(
4459 matches!(retry_error, Error::RetryableCommitConflict { .. }),
4460 "expected RetryableCommitConflict, got: {retry_error:?}"
4461 );
4462 let files_after_retry = list_data_files(test_uri);
4463 assert!(
4464 rewritten_files
4465 .iter()
4466 .all(|file| files_after_retry.contains(file)),
4467 "a replayed RewriteResult must not delete files referenced by the landed rewrite"
4468 );
4469
4470 let ds = Dataset::open(test_uri).await.unwrap();
4471 assert_eq!(ds.count_rows(None).await.unwrap(), num_rows);
4472 let scanned = ds.scan().try_into_batch().await.unwrap();
4473 assert_eq!(scanned.num_rows(), num_rows);
4474 }
4475
4476 #[tokio::test]
4483 async fn test_deferred_compaction_recognizes_ambiguous_commit_as_own() {
4484 use crate::utils::test::{AmbiguousCommitHandler, AmbiguousFailure};
4485
4486 let handler = Arc::new(AmbiguousCommitHandler::default());
4487 let mut data_gen =
4488 BatchGenerator::new().col(Box::new(IncrementingInt32::new().named("i".to_owned())));
4489 let mut dataset = Dataset::write(
4490 data_gen.batch(600),
4491 "memory://test/deferred_ambiguous",
4492 Some(WriteParams {
4493 max_rows_per_file: 100, commit_handler: Some(handler.clone()),
4495 ..Default::default()
4496 }),
4497 )
4498 .await
4499 .unwrap();
4500 create_scalar_index(&mut dataset, "i", false).await;
4503 let fragments_before = dataset.get_fragments().len();
4504
4505 let options = CompactionOptions {
4506 target_rows_per_fragment: 100_000,
4507 defer_index_remap: true,
4508 ..Default::default()
4509 };
4510 let completed = execute_compaction_plan(&dataset, &options).await;
4511
4512 handler.fail_next_rewrite(AmbiguousFailure::LandAndConflict);
4514 commit_compaction(
4515 &mut dataset,
4516 completed,
4517 Arc::new(DatasetIndexRemapperOptions::default()),
4518 &options,
4519 )
4520 .await
4521 .expect("verification must recognize the landed compaction as our own commit");
4522
4523 assert!(
4524 dataset.get_fragments().len() < fragments_before,
4525 "the landed compaction must be visible"
4526 );
4527 let fri = dataset
4528 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
4529 .await
4530 .unwrap()
4531 .expect("the deferred compaction installs the frag reuse entry");
4532 assert_eq!(fri.index_version, 0);
4533 assert_eq!(dataset.count_rows(None).await.unwrap(), 600);
4534
4535 let transaction_file = dataset
4538 .manifest
4539 .transaction_file
4540 .as_deref()
4541 .expect("the landed manifest records its transaction file");
4542 let path = dataset
4543 .base
4544 .clone()
4545 .join(crate::dataset::TRANSACTIONS_DIR)
4546 .join(transaction_file);
4547 assert!(dataset.object_store.exists(&path).await.unwrap());
4548 }
4549
4550 #[tokio::test]
4553 async fn test_compaction_cleans_up_files_when_fragment_reservation_fails() {
4554 use crate::utils::test::{AmbiguousCommitHandler, AmbiguousFailure};
4555
4556 let test_dir = TempStrDir::default();
4557 let test_uri = test_dir.as_str();
4558 let data = sample_data();
4559 let num_rows = data.num_rows();
4560 let handler = Arc::new(AmbiguousCommitHandler::default());
4561
4562 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
4563 let write_params = WriteParams {
4564 max_rows_per_file: 3_000,
4565 enable_stable_row_ids: true,
4566 commit_handler: Some(handler.clone()),
4567 ..Default::default()
4568 };
4569 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
4570 .await
4571 .unwrap();
4572 let files_before = list_data_files(test_uri);
4573 let options = CompactionOptions::default();
4574 let completed = execute_compaction_plan(&dataset, &options).await;
4575 assert!(list_data_files(test_uri).len() > files_before.len());
4576
4577 handler.fail_next_reserve(AmbiguousFailure::FailOutright);
4578 let err = commit_compaction(
4579 &mut dataset,
4580 completed,
4581 Arc::new(DatasetIndexRemapperOptions::default()),
4582 &options,
4583 )
4584 .await
4585 .expect_err("fragment reservation that did not land must fail");
4586 assert!(
4587 !err.is_commit_status_unknown(),
4588 "a verified-absent reservation is a definite failure, got: {:?}",
4589 err
4590 );
4591
4592 let files_after = list_data_files(test_uri);
4593 let leftover: Vec<_> = files_after.difference(&files_before).collect();
4594 assert!(
4595 leftover.is_empty(),
4596 "rewritten files must be cleaned up after reservation fails; leftover: {:?}",
4597 leftover
4598 );
4599 let ds = Dataset::open(test_uri).await.unwrap();
4600 assert_eq!(ds.count_rows(None).await.unwrap(), num_rows);
4601 }
4602
4603 #[tokio::test]
4604 async fn test_compact_blob_columns() {
4605 let test_dir = TempStrDir::default();
4606 let schema = Arc::new(Schema::new(vec![
4607 Field::new("id", DataType::Int32, false),
4608 Field::new("blob", DataType::LargeBinary, false)
4609 .with_metadata([(BLOB_META_KEY.to_string(), "true".to_string())].into()),
4610 ]));
4611 let expected_payload: Vec<Vec<u8>> =
4612 vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9, 10], vec![11]];
4613 let id_column: ArrayRef = Arc::new(Int32Array::from_iter_values(
4614 0..expected_payload.len() as i32,
4615 ));
4616 let blob_array: ArrayRef = Arc::new(LargeBinaryArray::from_iter(
4617 expected_payload.iter().map(|value| Some(value.as_slice())),
4618 ));
4619 let batch = RecordBatch::try_new(schema.clone(), vec![id_column, blob_array]).unwrap();
4620 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
4621
4622 let mut dataset = Dataset::write(
4623 reader,
4624 &test_dir,
4625 Some(WriteParams {
4626 data_storage_version: Some(LanceFileVersion::V2_1),
4627 max_rows_per_file: 1,
4628 ..Default::default()
4629 }),
4630 )
4631 .await
4632 .unwrap();
4633 dataset.validate().await.unwrap();
4634 assert!(dataset.get_fragments().len() > 1);
4635
4636 compact_files(&mut dataset, CompactionOptions::default(), None)
4637 .await
4638 .unwrap();
4639 dataset.validate().await.unwrap();
4640 assert_eq!(dataset.get_fragments().len(), 1);
4641
4642 let dataset = Arc::new(dataset);
4643 let row_indices: Vec<u64> = (0..expected_payload.len() as u64).collect();
4644 let blobs = dataset
4645 .take_blobs_by_indices(&row_indices, "blob")
4646 .await
4647 .unwrap();
4648 assert_eq!(blobs.len(), expected_payload.len());
4649 for (blob, expected) in blobs.iter().zip(expected_payload.iter()) {
4650 let bytes = blob.as_ref().unwrap().read().await.unwrap();
4651 assert_eq!(bytes.as_ref(), expected.as_slice());
4652 }
4653 }
4654
4655 fn row_addrs(frag_idx: u32, offsets: Range<u32>) -> Range<u64> {
4656 let start = RowAddress::new_from_parts(frag_idx, offsets.start);
4657 let end = RowAddress::new_from_parts(frag_idx, offsets.end);
4658 start.into()..end.into()
4659 }
4660
4661 fn expect_remap(
4664 ranges: &[Vec<(Range<u64>, bool)>],
4665 starting_new_frag_idx: u32,
4666 ) -> MockIndexRemapper {
4667 let mut expected_remap: HashMap<u64, Option<u64>> = HashMap::default();
4668 expected_remap.reserve(ranges.iter().map(|r| r.len()).sum());
4669 for (new_frag_offset, new_frag_ranges) in ranges.iter().enumerate() {
4670 let new_frag_idx = starting_new_frag_idx + new_frag_offset as u32;
4671 let mut row_offset = 0;
4672 for (old_id_range, is_found) in new_frag_ranges.iter() {
4673 for old_id in old_id_range.clone() {
4674 if *is_found {
4675 let new_id = RowAddress::new_from_parts(new_frag_idx, row_offset);
4676 expected_remap.insert(old_id, Some(new_id.into()));
4677 row_offset += 1;
4678 } else {
4679 expected_remap.insert(old_id, None);
4680 }
4681 }
4682 }
4683 }
4684 MockIndexRemapper {
4685 expectations: vec![MockIndexRemapperExpectation {
4686 expected: expected_remap,
4687 answer: vec![],
4688 }],
4689 }
4690 }
4691
4692 #[rstest]
4693 #[tokio::test]
4694 async fn test_compact_many(
4695 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
4696 data_storage_version: LanceFileVersion,
4697 ) {
4698 let test_dir = TempStrDir::default();
4699 let test_uri = &test_dir;
4700
4701 let data = sample_data();
4702
4703 let reader = RecordBatchIterator::new(vec![Ok(data.slice(0, 1200))], data.schema());
4705 let write_params = WriteParams {
4706 max_rows_per_file: 400,
4707 data_storage_version: Some(data_storage_version),
4708 ..Default::default()
4709 };
4710 Dataset::write(reader, test_uri, Some(write_params))
4711 .await
4712 .unwrap();
4713
4714 let reader = RecordBatchIterator::new(vec![Ok(data.slice(1200, 2000))], data.schema());
4716 let write_params = WriteParams {
4717 max_rows_per_file: 1000,
4718 data_storage_version: Some(data_storage_version),
4719 mode: WriteMode::Append,
4720 ..Default::default()
4721 };
4722 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
4723 .await
4724 .unwrap();
4725
4726 dataset.delete("a = 1300").await.unwrap();
4728
4729 dataset.delete("a >= 2400 AND a < 2600").await.unwrap();
4731
4732 let reader = RecordBatchIterator::new(vec![Ok(data.slice(3200, 600))], data.schema());
4734 let write_params = WriteParams {
4735 max_rows_per_file: 300,
4736 data_storage_version: Some(data_storage_version),
4737 mode: WriteMode::Append,
4738 ..Default::default()
4739 };
4740 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
4741 .await
4742 .unwrap();
4743
4744 let first_new_frag_idx = 7;
4745 let remap_a = expect_remap(
4748 &[
4749 vec![
4750 (row_addrs(0, 0..400), true),
4752 (row_addrs(1, 0..400), true),
4753 (row_addrs(2, 0..400), true),
4754 ],
4755 vec![
4758 (row_addrs(4, 0..200), true),
4759 (row_addrs(4, 200..400), false),
4760 (row_addrs(4, 400..1000), true),
4761 (row_addrs(5, 0..300), true),
4762 (row_addrs(6, 0..300), true),
4763 ],
4764 ],
4765 first_new_frag_idx,
4766 );
4767 let remap_b = expect_remap(
4768 &[
4769 vec![
4771 (row_addrs(4, 0..200), true),
4772 (row_addrs(4, 200..400), false),
4773 (row_addrs(4, 400..1000), true),
4774 (row_addrs(5, 0..300), true),
4775 (row_addrs(6, 0..300), true),
4776 ],
4777 vec![
4779 (row_addrs(0, 0..400), true),
4780 (row_addrs(1, 0..400), true),
4781 (row_addrs(2, 0..400), true),
4782 ],
4783 ],
4784 first_new_frag_idx,
4785 );
4786
4787 let options = CompactionOptions {
4789 target_rows_per_fragment: 1000,
4790 ..Default::default()
4791 };
4792 let plan = plan_compaction(&dataset, &options).await.unwrap();
4793 assert_eq!(plan.tasks().len(), 2);
4794 assert_eq!(plan.tasks()[0].fragments.len(), 3);
4795 assert_eq!(plan.tasks()[1].fragments.len(), 3);
4796
4797 assert_eq!(
4798 plan.tasks()[0]
4799 .fragments
4800 .iter()
4801 .map(|f| f.id)
4802 .collect::<Vec<_>>(),
4803 vec![0, 1, 2]
4804 );
4805 assert_eq!(
4806 plan.tasks()[1]
4807 .fragments
4808 .iter()
4809 .map(|f| f.id)
4810 .collect::<Vec<_>>(),
4811 vec![4, 5, 6]
4812 );
4813
4814 let mock_remapper = MockIndexRemapper::in_any_order(&[remap_a, remap_b]);
4815
4816 let metrics = compact_files(&mut dataset, options, Some(Arc::new(mock_remapper)))
4818 .await
4819 .unwrap();
4820
4821 assert_eq!(metrics.fragments_removed, 6);
4823 assert_eq!(metrics.fragments_added, 2);
4824 assert_eq!(metrics.files_removed, 7); assert_eq!(metrics.files_added, 2);
4826
4827 let fragment_ids = dataset
4828 .get_fragments()
4829 .iter()
4830 .map(|f| f.id())
4831 .collect::<Vec<_>>();
4832 assert_eq!(fragment_ids, vec![3, 7, 8]);
4833 }
4834
4835 #[rstest]
4836 #[tokio::test]
4837 async fn test_compaction_does_not_strand_small_remainders(
4838 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
4839 data_storage_version: LanceFileVersion,
4840 ) {
4841 let test_dir = TempStrDir::default();
4842 let data = sample_data().slice(0, 2_000);
4843 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
4844 let mut dataset = Dataset::write(
4845 reader,
4846 &test_dir,
4847 Some(WriteParams {
4848 max_rows_per_file: 200,
4849 data_storage_version: Some(data_storage_version),
4850 ..Default::default()
4851 }),
4852 )
4853 .await
4854 .unwrap();
4855
4856 let options = CompactionOptions {
4857 target_rows_per_fragment: 500,
4858 ..Default::default()
4859 };
4860 let metrics = compact_files(&mut dataset, options.clone(), None)
4861 .await
4862 .unwrap();
4863
4864 assert_eq!(metrics.fragments_removed, 10);
4865 assert_eq!(metrics.fragments_added, 3);
4866 let mut fragment_sizes = dataset
4867 .get_fragments()
4868 .iter()
4869 .map(|fragment| fragment.metadata.physical_rows.unwrap())
4870 .collect::<Vec<_>>();
4871 fragment_sizes.sort_unstable();
4872 assert_eq!(fragment_sizes, vec![600, 600, 800]);
4873
4874 let second_metrics = compact_files(&mut dataset, options, None).await.unwrap();
4875 assert_eq!(second_metrics, CompactionMetrics::default());
4876 }
4877
4878 #[rstest]
4879 #[case::legacy(LanceFileVersion::Legacy)]
4880 #[case::stable(LanceFileVersion::Stable)]
4881 #[tokio::test]
4882 async fn test_compaction_rebalances_oversized_task(
4883 #[case] data_storage_version: LanceFileVersion,
4884 ) {
4885 let test_dir = TempStrDir::default();
4886 let data = sample_data().slice(0, 5_100);
4887 let reader = RecordBatchIterator::new(vec![Ok(data.slice(0, 5_000))], data.schema());
4888 let mut dataset = Dataset::write(
4889 reader,
4890 &test_dir,
4891 Some(WriteParams {
4892 max_rows_per_file: 5_000,
4893 data_storage_version: Some(data_storage_version),
4894 ..Default::default()
4895 }),
4896 )
4897 .await
4898 .unwrap();
4899 let reader = RecordBatchIterator::new(vec![Ok(data.slice(5_000, 100))], data.schema());
4900 dataset.append(reader, None).await.unwrap();
4901
4902 dataset.delete("a < 1000").await.unwrap();
4903
4904 let options = CompactionOptions {
4905 target_rows_per_fragment: 1_000,
4906 ..Default::default()
4907 };
4908 let plan = plan_compaction(&dataset, &options).await.unwrap();
4909 assert_eq!(plan.tasks.len(), 1);
4910 assert_eq!(plan.tasks[0].fragments.len(), 2);
4911
4912 let metrics = compact_files(&mut dataset, options.clone(), None)
4913 .await
4914 .unwrap();
4915 assert_eq!(metrics.fragments_removed, 2);
4916 assert_eq!(metrics.fragments_added, 4);
4917 assert_eq!(
4918 dataset
4919 .get_fragments()
4920 .iter()
4921 .map(|fragment| fragment.metadata.physical_rows.unwrap())
4922 .collect::<Vec<_>>(),
4923 vec![1_025; 4]
4924 );
4925
4926 let second_metrics = compact_files(&mut dataset, options, None).await.unwrap();
4927 assert_eq!(second_metrics, CompactionMetrics::default());
4928 }
4929
4930 #[tokio::test]
4931 async fn test_compaction_balances_non_divisible_stable_task() {
4932 let test_dir = TempStrDir::default();
4933 let data = sample_data().slice(0, 121);
4934 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
4935 let mut dataset = Dataset::write(
4936 reader,
4937 &test_dir,
4938 Some(WriteParams {
4939 max_rows_per_file: 121,
4940 data_storage_version: Some(LanceFileVersion::Stable),
4941 ..Default::default()
4942 }),
4943 )
4944 .await
4945 .unwrap();
4946 dataset.delete("a < 20").await.unwrap();
4947
4948 let options = CompactionOptions {
4949 target_rows_per_fragment: 10,
4950 ..Default::default()
4951 };
4952 let plan = plan_compaction(&dataset, &options).await.unwrap();
4953 assert_eq!(plan.tasks.len(), 1);
4954 assert_eq!(plan.tasks[0].fragments.len(), 1);
4955
4956 let metrics = compact_files(&mut dataset, options.clone(), None)
4957 .await
4958 .unwrap();
4959 assert_eq!(metrics.fragments_removed, 1);
4960 assert_eq!(metrics.fragments_added, 10);
4961 assert_eq!(
4962 dataset
4963 .get_fragments()
4964 .iter()
4965 .map(|fragment| fragment.metadata.physical_rows.unwrap())
4966 .collect::<Vec<_>>(),
4967 [vec![11], vec![10; 9]].concat()
4968 );
4969
4970 let second_metrics = compact_files(&mut dataset, options, None).await.unwrap();
4971 assert_eq!(second_metrics, CompactionMetrics::default());
4972 }
4973
4974 #[rstest]
4980 #[case::extra_file(
4981 &[10, 10],
4982 &[9, 6, 5],
4983 RowLineageSpill { row_ids: true, ..Default::default() },
4984 None
4985 )]
4986 #[case::shifted(
4987 &[101, 100, 100],
4988 &[100, 101, 100],
4989 RowLineageSpill { row_ids: true, ..Default::default() },
4990 None
4991 )]
4992 #[case::shifted_all_inline(
4993 &[101, 100, 100],
4994 &[100, 101, 100],
4995 RowLineageSpill::default(),
4996 None
4997 )]
4998 #[case::mismatched_totals(
4999 &[10, 10],
5000 &[10, 9],
5001 RowLineageSpill { row_ids: true, ..Default::default() },
5002 Some("wrote 19 rows")
5003 )]
5004 fn place_row_lineage_in_files_follows_written_sizes(
5005 #[case] planned_rows: &[u64],
5006 #[case] written_rows: &[usize],
5007 #[case] spill: RowLineageSpill,
5008 #[case] expected_error: Option<&str>,
5009 ) {
5010 let total_rows = planned_rows.iter().sum::<u64>();
5011 let versions = (1..=total_rows).collect::<Vec<_>>();
5014 let mut lineages = Vec::with_capacity(planned_rows.len());
5015 let mut start = 0_u64;
5016 for rows in planned_rows {
5017 let end = start + rows;
5018 let created_at =
5019 RowDatasetVersionSequence::from_versions(&versions[start as usize..end as usize]);
5020 lineages.push(RowLineage {
5021 row_ids: RowIdSequence::from(start..end),
5022 last_updated_at: created_at.clone(),
5023 created_at,
5024 });
5025 start = end;
5026 }
5027 let columns = spill.take_columns(&mut lineages);
5028 if spill.row_ids {
5029 assert_eq!(columns, vec![(0..total_rows).collect::<Vec<_>>()]);
5030 } else {
5031 assert!(columns.is_empty(), "{columns:?}");
5032 }
5033 let fields = std::iter::once(0)
5035 .chain(spill.field_ids())
5036 .collect::<Vec<_>>();
5037 let column_indices = (0..fields.len() as i32).collect::<Vec<_>>();
5038 let mut fragments = written_rows
5039 .iter()
5040 .enumerate()
5041 .map(|(id, rows)| {
5042 Fragment::new(id as u64)
5043 .with_file(
5044 format!("{id}.lance"),
5045 fields.clone(),
5046 column_indices.clone(),
5047 ConcreteFileVersion::V2_0,
5048 None,
5049 )
5050 .with_physical_rows(*rows)
5051 })
5052 .collect::<Vec<_>>();
5053
5054 let placed = place_row_lineage_in_files(&mut fragments, lineages, planned_rows, spill);
5055 if let Some(expected) = expected_error {
5056 let error = placed.unwrap_err();
5057 let Error::Internal { message, .. } = &error else {
5058 panic!("expected an internal error, got {error}");
5059 };
5060 assert!(message.contains(expected), "{message}");
5061 return;
5062 }
5063 placed.unwrap();
5064 let mut placed_row_ids = Vec::with_capacity(versions.len());
5065 let mut placed_created_at = Vec::with_capacity(versions.len());
5066 let mut placed_last_updated_at = Vec::with_capacity(versions.len());
5067 for fragment in &fragments {
5068 let physical_rows = fragment.physical_rows.unwrap() as u64;
5069 match &fragment.row_id_meta {
5070 Some(RowIdMeta::Column) if spill.row_ids => {}
5071 Some(RowIdMeta::Inline(bytes)) if !spill.row_ids => {
5072 let row_ids = read_row_ids(bytes).unwrap();
5073 assert_eq!(row_ids.len(), physical_rows);
5074 placed_row_ids.extend(row_ids.iter());
5075 }
5076 other => panic!(
5077 "fragment {} has row id meta {other:?} under {spill:?}",
5078 fragment.id
5079 ),
5080 }
5081 for (meta, collected) in [
5082 (&fragment.created_at_version_meta, &mut placed_created_at),
5083 (
5084 &fragment.last_updated_at_version_meta,
5085 &mut placed_last_updated_at,
5086 ),
5087 ] {
5088 let sequence = meta.as_ref().unwrap().load_sequence().unwrap();
5089 assert_eq!(sequence.len(), physical_rows);
5090 collected.extend(sequence.versions());
5091 }
5092 }
5093 if !spill.row_ids {
5094 assert_eq!(placed_row_ids, (0..total_rows).collect::<Vec<_>>());
5095 }
5096 assert_eq!(placed_created_at, versions);
5097 assert_eq!(placed_last_updated_at, versions);
5098 }
5099
5100 #[tokio::test]
5105 async fn per_fragment_plan_spills_only_the_fragments_over_budget() {
5106 let mut dataset = lance_datagen::gen_batch()
5107 .col("i", lance_datagen::array::step::<Int32Type>())
5108 .into_ram_dataset_with_params(
5109 FragmentCount::from(1),
5110 FragmentRowCount::from(10),
5111 Some(WriteParams {
5112 enable_stable_row_ids: true,
5113 max_rows_per_file: 10,
5114 ..Default::default()
5115 }),
5116 )
5117 .await
5118 .unwrap();
5119 dataset
5120 .update_config([
5121 (SPILL_ROW_LINEAGE_CONFIG_KEY, "true"),
5122 (INLINE_ROW_LINEAGE_MAX_BYTES_CONFIG_KEY, "100"),
5123 ])
5124 .await
5125 .unwrap();
5126 let scattered = (0..500_u64)
5130 .map(|i| 500 + (i * 7919) % 500)
5131 .collect::<Vec<_>>();
5132 let row_ids = [
5133 RowIdSequence::from(0..500),
5134 RowIdSequence::from(scattered.as_slice()),
5135 ];
5136 let lineages = row_ids
5137 .into_iter()
5138 .map(|row_ids| {
5139 let created_at =
5140 RowDatasetVersionSequence::from_uniform_row_count(row_ids.len(), 1);
5141 RowLineage {
5142 row_ids,
5143 last_updated_at: created_at.clone(),
5144 created_at,
5145 }
5146 })
5147 .collect::<Vec<_>>();
5148 let plan = plan_row_lineage_spill(100, &lineages);
5149 assert_eq!(plan, RowLineagePlan::PerFragment);
5150 let mut fragments = [250, 500, 250]
5154 .into_iter()
5155 .enumerate()
5156 .map(|(id, rows)| {
5157 Fragment::new(id as u64)
5158 .with_file(
5159 format!("{id}.lance"),
5160 vec![0],
5161 vec![0],
5162 ConcreteFileVersion::V2_0,
5163 None,
5164 )
5165 .with_physical_rows(rows)
5166 })
5167 .collect::<Vec<_>>();
5168
5169 place_planned_row_lineage(
5170 &dataset,
5171 &mut fragments,
5172 PlannedRowLineage {
5173 lineages,
5174 planned_rows: vec![500, 500],
5175 plan,
5176 },
5177 )
5178 .await
5179 .unwrap();
5180
5181 assert!(
5182 matches!(fragments[0].row_id_meta, Some(RowIdMeta::Inline(_))),
5183 "a fragment whose row ids fit must keep them inline, got {:?}",
5184 fragments[0].row_id_meta
5185 );
5186 assert_eq!(fragments[0].files.len(), 1);
5187 for fragment in &fragments[1..] {
5188 assert!(
5189 matches!(fragment.row_id_meta, Some(RowIdMeta::Column)),
5190 "fragment {}'s row ids are over the budget, got {:?}",
5191 fragment.id,
5192 fragment.row_id_meta
5193 );
5194 assert_eq!(fragment.files.len(), 2);
5195 assert_eq!(fragment.files[1].fields.as_ref(), [ROW_ID_FIELD_ID]);
5196 }
5197 let mut placed_row_ids = Vec::with_capacity(1000);
5198 for fragment in &fragments {
5199 let row_ids = match &fragment.row_id_meta {
5200 Some(RowIdMeta::Inline(bytes)) => read_row_ids(bytes).unwrap(),
5201 _ => read_spilled_row_ids(&dataset, fragment).await.unwrap(),
5202 };
5203 placed_row_ids.extend(row_ids.iter());
5204 }
5205 assert_eq!(
5206 placed_row_ids,
5207 (0..500)
5208 .chain(scattered.iter().copied())
5209 .collect::<Vec<_>>()
5210 );
5211 }
5212
5213 #[rstest]
5219 #[case::in_file_carrier(vec![vec![0, 1, -3, -4, -5]], false)]
5220 #[case::lineage_only_file(vec![vec![0, 1], vec![-3, -4, -5]], false)]
5221 #[case::tombstoned_carrier(vec![vec![-2, -2, -3, -4, -5], vec![0, 1]], true)]
5222 #[case::carrier_of_dropped_columns(vec![vec![5, 6, -3, -4, -5], vec![0, 1]], true)]
5223 #[case::dead_file_without_lineage(vec![vec![-2, 6], vec![0, 1]], false)]
5224 fn compaction_reclaims_lineage_carriers_of_dead_user_columns(
5225 #[case] files: Vec<Vec<i32>>,
5226 #[case] expected: bool,
5227 ) {
5228 let mut fragment = Fragment::new(0);
5229 for (index, fields) in files.into_iter().enumerate() {
5230 let column_indices = (0..fields.len() as i32).collect();
5231 fragment.add_file(
5232 format!("{index}.lance"),
5233 fields,
5234 column_indices,
5235 ConcreteFileVersion::V2_2,
5236 None,
5237 );
5238 }
5239 let schema_field_ids = HashSet::from([0, 1]);
5240 assert_eq!(
5241 pins_dead_user_columns_for_lineage(&fragment, &schema_field_ids),
5242 expected
5243 );
5244 }
5245
5246 #[tokio::test]
5250 async fn append_row_lineage_columns_gives_each_batch_its_own_buffer() {
5251 let batch = arrow_array::record_batch!(("i", Int32, [0, 1, 2, 3])).unwrap();
5252 let schema = batch.schema();
5253 let batches = vec![batch; 3]
5254 .into_iter()
5255 .map(Ok::<_, datafusion::error::DataFusionError>);
5256 let input: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
5257 schema,
5258 futures::stream::iter(batches),
5259 ));
5260 let spill = RowLineageSpill {
5261 row_ids: true,
5262 ..Default::default()
5263 };
5264 let fields = spill.schema_fields().unwrap();
5265
5266 let output = append_row_lineage_columns(input, &fields, vec![(0..12).collect()])
5267 .try_collect::<Vec<_>>()
5268 .await
5269 .unwrap();
5270
5271 assert_eq!(output.len(), 3);
5272 for (index, batch) in output.iter().enumerate() {
5273 let start = index as u64 * 4;
5274 let row_ids = batch[ROW_ID].as_primitive::<UInt64Type>();
5275 assert_eq!(
5276 row_ids.values().to_vec(),
5277 (start..start + 4).collect::<Vec<_>>()
5278 );
5279 assert!(
5280 row_ids.get_buffer_memory_size() <= batch.num_rows() * 8,
5281 "batch {index}'s row ids hold {} bytes for {} rows",
5282 row_ids.get_buffer_memory_size(),
5283 batch.num_rows()
5284 );
5285 }
5286 }
5287
5288 #[rstest]
5289 #[tokio::test]
5290 async fn test_compact_data_files(
5291 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5292 data_storage_version: LanceFileVersion,
5293 ) {
5294 let test_dir = TempStrDir::default();
5295 let test_uri = &test_dir;
5296
5297 let data = sample_data();
5298
5299 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
5301 let write_params = WriteParams {
5302 max_rows_per_file: 5_000,
5303 max_rows_per_group: 1_000,
5304 data_storage_version: Some(data_storage_version),
5305 ..Default::default()
5306 };
5307 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
5308 .await
5309 .unwrap();
5310
5311 let schema = Schema::new(vec![
5313 Field::new("a", DataType::Int64, false),
5314 Field::new("x", DataType::Float32, false),
5315 ]);
5316
5317 let data = RecordBatch::try_new(
5318 Arc::new(schema),
5319 vec![
5320 Arc::new(Int64Array::from_iter_values(0..10_000)),
5321 Arc::new(Float32Array::from_iter_values(
5322 (0..10_000).map(|x| x as f32 * std::f32::consts::PI),
5323 )),
5324 ],
5325 )
5326 .unwrap();
5327 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
5328
5329 dataset.merge(reader, "a", "a").await.unwrap();
5330
5331 let expected_remap = expect_remap(
5332 &[vec![
5333 (row_addrs(0, 0..5000), true),
5335 (row_addrs(1, 0..5000), true),
5336 ]],
5337 2,
5338 );
5339
5340 let plan = plan_compaction(
5341 &dataset,
5342 &CompactionOptions {
5343 ..Default::default()
5344 },
5345 )
5346 .await
5347 .unwrap();
5348 assert_eq!(plan.tasks().len(), 1);
5349 assert_eq!(plan.tasks()[0].fragments.len(), 2);
5350
5351 let metrics = compact_files(&mut dataset, plan.options, Some(Arc::new(expected_remap)))
5352 .await
5353 .unwrap();
5354
5355 assert_eq!(metrics.files_removed, 4); assert_eq!(metrics.files_added, 1); assert_eq!(metrics.fragments_removed, 2);
5358 assert_eq!(metrics.fragments_added, 1);
5359
5360 let scanner = dataset.scan();
5362 let batches = scanner
5363 .try_into_stream()
5364 .await
5365 .unwrap()
5366 .try_collect::<Vec<_>>()
5367 .await
5368 .unwrap();
5369 let scanned_data = concat_batches(&batches[0].schema(), &batches).unwrap();
5370
5371 assert_eq!(scanned_data, data);
5372 }
5373
5374 #[rstest]
5375 #[tokio::test]
5376 async fn test_compact_with_io_buffer_size(
5377 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5378 data_storage_version: LanceFileVersion,
5379 ) {
5380 let test_dir = TempStrDir::default();
5383 let test_uri = &test_dir;
5384
5385 let data = sample_data();
5386
5387 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], data.schema());
5389 let write_params = WriteParams {
5390 max_rows_per_file: 5_000,
5391 max_rows_per_group: 1_000,
5392 data_storage_version: Some(data_storage_version),
5393 ..Default::default()
5394 };
5395 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
5396 .await
5397 .unwrap();
5398 assert_eq!(dataset.get_fragments().len(), 2);
5399
5400 let options = CompactionOptions {
5401 io_buffer_size: Some(256 * 1024 * 1024),
5403 ..Default::default()
5404 };
5405 let plan = plan_compaction(&dataset, &options).await.unwrap();
5406 assert_eq!(plan.tasks().len(), 1);
5407
5408 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
5409 assert_eq!(metrics.fragments_removed, 2);
5410 assert_eq!(metrics.fragments_added, 1);
5411
5412 let scanner = dataset.scan();
5414 let batches = scanner
5415 .try_into_stream()
5416 .await
5417 .unwrap()
5418 .try_collect::<Vec<_>>()
5419 .await
5420 .unwrap();
5421 let scanned_data = concat_batches(&batches[0].schema(), &batches).unwrap();
5422 assert_eq!(scanned_data.num_rows(), data.num_rows());
5423 }
5424
5425 #[rstest]
5426 #[tokio::test]
5427 async fn test_compact_deletions(
5428 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5429 data_storage_version: LanceFileVersion,
5430 ) {
5431 let test_dir = TempStrDir::default();
5435 let test_uri = &test_dir;
5436
5437 let data = sample_data();
5438
5439 let reader = RecordBatchIterator::new(vec![Ok(data.slice(0, 1000))], data.schema());
5441 let write_params = WriteParams {
5442 max_rows_per_file: 1000,
5443 data_storage_version: Some(data_storage_version),
5444 ..Default::default()
5445 };
5446 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
5447 .await
5448 .unwrap();
5449
5450 dataset.delete("a <= 500").await.unwrap();
5451
5452 let mut options = CompactionOptions {
5454 materialize_deletions_threshold: 0.8,
5455 ..Default::default()
5456 };
5457 let plan = plan_compaction(&dataset, &options).await.unwrap();
5458 assert_eq!(plan.tasks().len(), 0);
5459
5460 options.materialize_deletions_threshold = 0.1;
5462 options.materialize_deletions = false;
5463 let plan = plan_compaction(&dataset, &options).await.unwrap();
5464 assert_eq!(plan.tasks().len(), 0);
5465
5466 options.materialize_deletions = true;
5468 let plan = plan_compaction(&dataset, &options).await.unwrap();
5469 assert_eq!(plan.tasks().len(), 1);
5470
5471 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
5472 assert_eq!(metrics.fragments_removed, 1);
5473 assert_eq!(metrics.files_removed, 2);
5474 assert_eq!(metrics.fragments_added, 1);
5475
5476 let fragments = dataset.get_fragments();
5477 assert_eq!(fragments.len(), 1);
5478 assert!(fragments[0].metadata.deletion_file.is_none());
5479 }
5480
5481 #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
5482 struct IgnoreRemap {}
5483
5484 #[async_trait]
5485 impl IndexRemapper for IgnoreRemap {
5486 async fn remap_indices(&self, _: RowAddrRemap, _: &[u64]) -> Result<Vec<RemappedIndex>> {
5487 Ok(Vec::new())
5488 }
5489 }
5490
5491 #[async_trait]
5492 impl IndexRemapperOptions for IgnoreRemap {
5493 async fn create_remapper(&self, _: &Dataset) -> Result<Option<Box<dyn IndexRemapper>>> {
5494 Ok(None)
5495 }
5496 }
5497
5498 #[rstest]
5499 #[case::without_index(false)]
5500 #[case::with_index(true)]
5501 #[tokio::test]
5502 async fn test_row_addrs_only_used_with_remappable_index(#[case] has_index: bool) {
5503 let data = sample_data();
5504 let reader = RecordBatchIterator::new(vec![Ok(data.slice(0, 9_000))], data.schema());
5505 let mut dataset = Dataset::write(
5506 reader,
5507 "memory://",
5508 Some(WriteParams {
5509 max_rows_per_file: 3_000,
5510 data_storage_version: Some(LanceFileVersion::Legacy),
5511 ..Default::default()
5512 }),
5513 )
5514 .await
5515 .unwrap();
5516
5517 if has_index {
5518 create_scalar_index(&mut dataset, "a", false).await;
5519 }
5520
5521 let options = CompactionOptions {
5522 target_rows_per_fragment: 9_000,
5523 ..Default::default()
5524 };
5525 let plan = plan_compaction(&dataset, &options).await.unwrap();
5526 assert_eq!(plan.tasks().len(), 1);
5527
5528 let mut result = rewrite_files(Cow::Borrowed(&dataset), plan.tasks()[0].clone(), &options)
5529 .await
5530 .unwrap();
5531 assert_eq!(result.row_addrs.is_some(), has_index);
5532
5533 if has_index {
5534 let row_addrs_bytes = result
5535 .row_addrs
5536 .as_ref()
5537 .expect("indexed compaction should capture row addresses");
5538 let row_addrs =
5539 RoaringTreemap::deserialize_from(&mut Cursor::new(row_addrs_bytes)).unwrap();
5540 assert_eq!(row_addrs.len(), 9_000);
5541 assert!(
5547 row_addrs_bytes.len() < row_addrs.len() as usize,
5548 "serialized row addrs ({} bytes for {} addresses) should be \
5549 run-optimized before persisting",
5550 row_addrs_bytes.len(),
5551 row_addrs.len()
5552 );
5553 } else {
5554 result.row_addrs = Some(b"not a roaring treemap".to_vec());
5558 commit_compaction(
5559 &mut dataset,
5560 vec![result],
5561 Arc::new(DatasetIndexRemapperOptions::default()),
5562 &options,
5563 )
5564 .await
5565 .unwrap();
5566 assert_eq!(dataset.get_fragments().len(), 1);
5567 }
5568 }
5569
5570 #[rstest::rstest]
5571 #[tokio::test]
5572 async fn test_compact_distributed(
5573 #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5574 data_storage_version: LanceFileVersion,
5575 #[values(false, true)] use_stable_row_id: bool,
5576 ) {
5577 let test_dir = TempStrDir::default();
5581 let test_uri = &test_dir;
5582
5583 let data = sample_data();
5584
5585 let reader = RecordBatchIterator::new(vec![Ok(data.slice(0, 9000))], data.schema());
5587 let write_params = WriteParams {
5588 max_rows_per_file: 1000,
5589 data_storage_version: Some(data_storage_version),
5590 enable_stable_row_ids: use_stable_row_id,
5591 ..Default::default()
5592 };
5593 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
5594 .await
5595 .unwrap();
5596
5597 let options = CompactionOptions {
5599 target_rows_per_fragment: 3_000,
5600 ..Default::default()
5601 };
5602 let plan = plan_compaction(&dataset, &options).await.unwrap();
5603 assert_eq!(plan.tasks().len(), 3);
5604
5605 let dataset_ref = &dataset;
5606 let mut results = futures::stream::iter(plan.compaction_tasks())
5607 .then(|task| async move { task.execute(dataset_ref).await.unwrap() })
5608 .collect::<Vec<_>>()
5609 .await;
5610
5611 assert_eq!(results.len(), 3);
5612
5613 assert_eq!(
5614 results[0]
5615 .original_fragments
5616 .iter()
5617 .map(|f| f.id)
5618 .collect::<Vec<_>>(),
5619 vec![0, 1, 2]
5620 );
5621 assert_eq!(results[0].metrics.files_removed, 3);
5622 assert_eq!(results[0].metrics.files_added, 1);
5623
5624 commit_compaction(
5626 &mut dataset,
5627 vec![results.pop().unwrap()],
5628 Arc::new(IgnoreRemap::default()),
5629 &options,
5630 )
5631 .await
5632 .unwrap();
5633
5634 assert_eq!(dataset.manifest.version, 3);
5637
5638 commit_compaction(
5640 &mut dataset,
5641 results,
5642 Arc::new(IgnoreRemap::default()),
5643 &options,
5644 )
5645 .await
5646 .unwrap();
5647 assert_eq!(dataset.manifest.version, 5);
5650
5651 assert_eq!(dataset.manifest.uses_stable_row_ids(), use_stable_row_id,);
5652 }
5653
5654 #[tokio::test]
5655 async fn test_stable_row_indices() {
5656 let mut data_gen = BatchGenerator::new()
5658 .col(Box::new(
5659 RandomVector::new().vec_width(16).named("vec".to_owned()),
5660 ))
5661 .col(Box::new(IncrementingInt32::new().named("i".to_owned())));
5662 let mut dataset = Dataset::write(
5663 data_gen.batch(500),
5664 "memory://test/table",
5665 Some(WriteParams {
5666 enable_stable_row_ids: true,
5667 max_rows_per_file: 100, ..Default::default()
5669 }),
5670 )
5671 .await
5672 .unwrap();
5673
5674 dataset.delete("i < 110").await.unwrap();
5678
5679 dataset
5680 .create_index(
5681 &["i"],
5682 IndexType::Scalar,
5683 Some("scalar".into()),
5684 &ScalarIndexParams::default(),
5685 false,
5686 )
5687 .await
5688 .unwrap();
5689 let params = VectorIndexParams::ivf_pq(1, 8, 1, MetricType::L2, 50);
5690 dataset
5691 .create_index(
5692 &["vec"],
5693 IndexType::Vector,
5694 Some("vector".into()),
5695 ¶ms,
5696 false,
5697 )
5698 .await
5699 .unwrap();
5700
5701 async fn index_set(dataset: &Dataset) -> HashSet<Uuid> {
5702 dataset
5703 .load_indices()
5704 .await
5705 .unwrap()
5706 .iter()
5707 .map(|index| index.uuid)
5708 .collect()
5709 }
5710 let indices = index_set(&dataset).await;
5711
5712 async fn vector_query(dataset: &Dataset) -> RecordBatch {
5713 let mut scanner = dataset.scan();
5714
5715 let query = Float32Array::from(vec![0.0f32; 16]);
5716 scanner
5717 .nearest("vec", &query, 10)
5718 .unwrap()
5719 .project(&["i"])
5720 .unwrap();
5721
5722 scanner.try_into_batch().await.unwrap()
5723 }
5724
5725 async fn scalar_query(dataset: &Dataset) -> RecordBatch {
5726 let mut scanner = dataset.scan();
5727
5728 scanner.filter("i = 100").unwrap().project(&["i"]).unwrap();
5729
5730 scanner.try_into_batch().await.unwrap()
5731 }
5732
5733 let before_vec_result = vector_query(&dataset).await;
5734 let before_scalar_result = scalar_query(&dataset).await;
5735
5736 let options = CompactionOptions {
5737 target_rows_per_fragment: 180,
5738 ..Default::default()
5739 };
5740 let _metrics = compact_files(&mut dataset, options, None).await.unwrap();
5741
5742 let current_indices = index_set(&dataset).await;
5745 assert_eq!(indices, current_indices);
5746
5747 let after_vec_result = vector_query(&dataset).await;
5748 assert_eq!(before_vec_result, after_vec_result);
5749
5750 let after_scalar_result = scalar_query(&dataset).await;
5751 assert_eq!(before_scalar_result, after_scalar_result);
5752 }
5753
5754 #[rstest]
5762 #[case::zone_map(BuiltinIndexType::ZoneMap, IndexType::ZoneMap, "i", "i > 0", 199)]
5763 #[case::bloom_filter(BuiltinIndexType::BloomFilter, IndexType::BloomFilter, "i", "i = 0", 1)]
5764 #[case::fm(
5765 BuiltinIndexType::Fm,
5766 IndexType::Fm,
5767 "text",
5768 "contains(text, 'needle')",
5769 100
5770 )]
5771 #[tokio::test]
5772 async fn test_addr_domain_index_after_compaction_with_stable_row_ids(
5773 #[case] builtin: BuiltinIndexType,
5774 #[case] index_type: IndexType,
5775 #[case] indexed_column: &str,
5776 #[case] query: &str,
5777 #[case] expected_rows: usize,
5778 ) {
5779 let mut dataset = lance_datagen::gen_batch()
5780 .col("i", lance_datagen::array::step::<Int32Type>())
5781 .col(
5782 "text",
5783 lance_datagen::array::cycle_utf8_literals(&["needle", "haystack"]),
5784 )
5785 .into_ram_dataset_with_params(
5786 FragmentCount::from(2),
5787 FragmentRowCount::from(100),
5788 Some(WriteParams {
5789 enable_stable_row_ids: true,
5790 max_rows_per_file: 100,
5791 ..Default::default()
5792 }),
5793 )
5794 .await
5795 .unwrap();
5796
5797 dataset
5798 .create_index(
5799 &[indexed_column],
5800 index_type,
5801 Some("addr_idx".to_string()),
5802 &ScalarIndexParams::for_builtin(builtin),
5803 false,
5804 )
5805 .await
5806 .unwrap();
5807
5808 compact_files(&mut dataset, CompactionOptions::default(), None)
5809 .await
5810 .unwrap();
5811
5812 let live_fragments: RoaringBitmap =
5815 dataset.fragments().iter().map(|f| f.id as u32).collect();
5816 let index = dataset
5817 .load_indices()
5818 .await
5819 .unwrap()
5820 .iter()
5821 .find(|index| index.name == "addr_idx")
5822 .expect("index must survive compaction")
5823 .clone();
5824 assert!(
5825 index
5826 .effective_fragment_bitmap(&live_fragments)
5827 .is_none_or(|covered| covered.is_empty()),
5828 "compaction must not point an address-domain index at the fragments it wrote"
5829 );
5830
5831 let mut scanner = dataset.scan();
5834 scanner.filter(query).unwrap();
5835 let matched = scanner.try_into_batch().await.unwrap();
5836 assert_eq!(matched.num_rows(), expected_rows);
5837 }
5838
5839 #[tokio::test]
5845 async fn test_defer_index_remap_large_external_file() {
5846 let test_dir = TempStrDir::default();
5847 let test_uri = &test_dir;
5848
5849 let num_fragments = 150usize;
5852 let rows_per_fragment = 1000usize;
5853 let total_rows = num_fragments * rows_per_fragment;
5854
5855 let schema = Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, false)]));
5856
5857 let mut dataset = Dataset::write(
5858 RecordBatchIterator::new(
5859 vec![Ok(RecordBatch::try_new(
5860 schema.clone(),
5861 vec![Arc::new(Int32Array::from_iter_values(0..total_rows as i32)) as ArrayRef],
5862 )
5863 .unwrap())],
5864 schema.clone(),
5865 ),
5866 test_uri,
5867 Some(WriteParams {
5868 max_rows_per_file: rows_per_fragment,
5869 ..Default::default()
5870 }),
5871 )
5872 .await
5873 .unwrap();
5874
5875 assert_eq!(dataset.get_fragments().len(), num_fragments);
5876
5877 create_scalar_index(&mut dataset, "i", false).await;
5880
5881 dataset.delete("i % 1000 = 0").await.unwrap();
5883
5884 compact_files(
5885 &mut dataset,
5886 CompactionOptions {
5887 defer_index_remap: true,
5888 ..Default::default()
5889 },
5890 None,
5891 )
5892 .await
5893 .unwrap();
5894
5895 let frag_reuse_meta = dataset
5898 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
5899 .await
5900 .unwrap()
5901 .expect("fragment reuse index must exist after compaction");
5902
5903 load_frag_reuse_index_details(&dataset, &frag_reuse_meta)
5904 .await
5905 .expect("loading large frag reuse index details must not fail");
5906 }
5907
5908 #[tokio::test]
5914 async fn test_shallow_clone_loads_external_frag_reuse_details() {
5915 let source_dir = TempStrDir::default();
5918 let clone_dir = TempStrDir::default();
5919 let clone_uri = format!("{}/clone", clone_dir);
5920
5921 use crate::index::frag_reuse::build_frag_reuse_index_metadata;
5922 use lance_index::frag_reuse::{FragDigest, FragReuseIndexDetails, FragReuseVersion};
5923
5924 let schema = Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, false)]));
5925 let mut dataset = Dataset::write(
5926 RecordBatchIterator::new(
5927 vec![Ok(RecordBatch::try_new(
5928 schema.clone(),
5929 vec![Arc::new(Int32Array::from_iter_values(0..8)) as ArrayRef],
5930 )
5931 .unwrap())],
5932 schema.clone(),
5933 ),
5934 &source_dir,
5935 Some(WriteParams {
5936 max_rows_per_file: 4,
5937 ..Default::default()
5938 }),
5939 )
5940 .await
5941 .unwrap();
5942
5943 let digest = |id: u64| FragDigest {
5948 id,
5949 physical_rows: 4,
5950 num_deleted_rows: 0,
5951 };
5952 let mut versions = Vec::new();
5953 for i in 0..3500u64 {
5954 let old_id = 1_000 + i;
5955 let mut addrs = RoaringTreemap::new();
5956 for offset in 0..4u64 {
5957 addrs.insert((old_id << 32) + offset);
5958 }
5959 let mut serialized = Vec::new();
5960 addrs.serialize_into(&mut serialized).unwrap();
5961 versions.push(FragReuseVersion {
5962 dataset_version: i + 1,
5963 groups: vec![FragReuseGroup {
5964 changed_row_addrs: serialized,
5965 old_frags: vec![digest(old_id)],
5966 new_frags: vec![digest(100_000 + i)],
5967 }],
5968 });
5969 }
5970 let details = FragReuseIndexDetails { versions };
5971 let bitmap: RoaringBitmap = (0..3500u32).map(|i| 100_000 + i).collect();
5972 let entry = build_frag_reuse_index_metadata(&dataset, None, details, bitmap)
5973 .await
5974 .unwrap();
5975 dataset
5976 .apply_commit(
5977 Transaction::new(
5978 dataset.manifest.version,
5979 Operation::CreateIndex {
5980 new_indices: vec![entry],
5981 removed_indices: vec![],
5982 },
5983 None,
5984 ),
5985 &Default::default(),
5986 &Default::default(),
5987 )
5988 .await
5989 .unwrap();
5990
5991 let source_meta = dataset
5992 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
5993 .await
5994 .unwrap()
5995 .expect("fragment reuse index must exist");
5996 let proto = source_meta
5998 .index_details
5999 .as_ref()
6000 .unwrap()
6001 .to_msg::<lance_table::format::pb::FragmentReuseIndexDetails>()
6002 .unwrap();
6003 assert!(matches!(
6004 proto.content,
6005 Some(lance_table::format::pb::fragment_reuse_index_details::Content::External(_))
6006 ));
6007 let source_details = load_frag_reuse_index_details(&dataset, &source_meta)
6008 .await
6009 .unwrap();
6010 assert_eq!(source_details.versions.len(), 3500);
6011
6012 let version = dataset.manifest.version;
6013 let clone = dataset
6014 .shallow_clone(clone_uri.as_str(), version, None)
6015 .await
6016 .unwrap();
6017 let clone_meta = clone
6018 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6019 .await
6020 .unwrap()
6021 .expect("the clone must carry the fragment reuse index");
6022 assert!(clone_meta.base_id.is_some());
6023
6024 let clone_details = load_frag_reuse_index_details(&clone, &clone_meta)
6027 .await
6028 .expect("the clone must load external FRI details through its base");
6029 assert_eq!(clone_details, source_details);
6030
6031 let index = clone
6033 .open_frag_reuse_index(&NoOpMetricsCollector)
6034 .await
6035 .unwrap()
6036 .expect("the clone must open the fragment reuse index");
6037 assert_eq!(index.details.versions, source_details.versions);
6038 }
6039
6040 #[tokio::test]
6041 async fn test_defer_index_remap_rejected_with_stable_row_ids() {
6042 let test_dir = TempStrDir::default();
6043 let test_uri = &test_dir;
6044
6045 let data = sample_data();
6046 let reader = RecordBatchIterator::new(vec![Ok(data.slice(0, 9000))], data.schema());
6047 let mut dataset = Dataset::write(
6048 reader,
6049 test_uri,
6050 Some(WriteParams {
6051 max_rows_per_file: 1000, enable_stable_row_ids: true,
6053 ..Default::default()
6054 }),
6055 )
6056 .await
6057 .unwrap();
6058 assert!(dataset.manifest.uses_stable_row_ids());
6059
6060 let options = CompactionOptions {
6061 target_rows_per_fragment: 3_000,
6062 defer_index_remap: true,
6063 ..Default::default()
6064 };
6065
6066 let plan_err = plan_compaction(&dataset, &options).await.unwrap_err();
6068 assert!(matches!(plan_err, Error::InvalidInput { .. }));
6069 let msg = plan_err.to_string();
6070 assert!(msg.contains("defer_index_remap"));
6071 assert!(msg.contains("stable row IDs"));
6072
6073 let version_before = dataset.manifest.version;
6076 let compact_err = compact_files(&mut dataset, options, None)
6077 .await
6078 .unwrap_err();
6079 assert!(matches!(compact_err, Error::InvalidInput { .. }));
6080 assert_eq!(dataset.manifest.version, version_before);
6081 }
6082
6083 #[tokio::test]
6084 async fn test_defer_index_remap() {
6085 let mut data_gen = BatchGenerator::new()
6086 .col(Box::new(
6087 RandomVector::new().vec_width(128).named("vec".to_owned()),
6088 ))
6089 .col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6090
6091 let mut dataset = Dataset::write(
6092 data_gen.batch(6_000),
6093 "memory://test/table",
6094 Some(WriteParams {
6095 max_rows_per_file: 1_000, ..Default::default()
6097 }),
6098 )
6099 .await
6100 .unwrap();
6101
6102 let mut data_gen2 = BatchGenerator::new()
6104 .col(Box::new(
6105 RandomVector::new().vec_width(128).named("vec".to_owned()),
6106 ))
6107 .col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6108
6109 let mut dataset2 = Dataset::write(
6110 data_gen2.batch(6_000),
6111 "memory://test/table",
6112 Some(WriteParams {
6113 max_rows_per_file: 1_000, ..Default::default()
6115 }),
6116 )
6117 .await
6118 .unwrap();
6119
6120 dataset.delete("i < 500").await.unwrap();
6122 dataset2.delete("i < 500").await.unwrap();
6123
6124 create_scalar_index(&mut dataset, "i", false).await;
6127 create_scalar_index(&mut dataset2, "i", false).await;
6128
6129 let initial_indices = dataset.load_indices().await.unwrap();
6131 assert_eq!(initial_indices.len(), 1);
6132 assert_eq!(initial_indices[0].name, "scalar");
6133
6134 let original_scalar_uuid = initial_indices[0].uuid;
6136
6137 let options = CompactionOptions {
6139 target_rows_per_fragment: 2_000,
6140 defer_index_remap: true,
6141 ..Default::default()
6142 };
6143 let options2 = CompactionOptions {
6144 target_rows_per_fragment: 2_000,
6145 defer_index_remap: false,
6146 ..Default::default()
6147 };
6148
6149 let plan = plan_compaction(&dataset, &options).await.unwrap();
6150 let plan2 = plan_compaction(&dataset2, &options2).await.unwrap();
6151
6152 let mut expected_all_old_frag_ids = Vec::new();
6153 let mut expected_all_new_frag_ids = Vec::new();
6154 let mut expected_all_new_frag_bitmap = RoaringBitmap::new();
6155 let mut expected_all_row_id_map = HashMap::new();
6156 let mut deferred_results = Vec::new();
6157 let mut immediate_results = Vec::new();
6158
6159 for (task, task2) in plan.tasks().iter().zip(plan2.tasks()) {
6160 let deferred_result = rewrite_files(Cow::Borrowed(&dataset), task.clone(), &options)
6161 .await
6162 .unwrap();
6163 let immediate_result =
6164 rewrite_files(Cow::Borrowed(&dataset2), task2.clone(), &options2)
6165 .await
6166 .unwrap();
6167
6168 assert!(deferred_result.row_addrs.is_some());
6170 assert!(!deferred_result.row_addrs.as_ref().unwrap().is_empty());
6171 assert!(!deferred_result.row_addrs.as_ref().unwrap().is_empty());
6172 assert!(!deferred_result.original_fragments.is_empty());
6173 assert!(!deferred_result.new_fragments.is_empty());
6174
6175 assert!(immediate_result.row_addrs.is_some());
6176 assert!(!immediate_result.original_fragments.is_empty());
6177 assert!(!immediate_result.new_fragments.is_empty());
6178
6179 assert_eq!(deferred_result.row_addrs, immediate_result.row_addrs);
6181
6182 deferred_results.push(deferred_result);
6183 immediate_results.push(immediate_result);
6184 }
6185
6186 {
6188 let frags: Vec<&mut Fragment> = immediate_results
6189 .iter_mut()
6190 .flat_map(|r| r.new_fragments.iter_mut())
6191 .collect();
6192 reserve_fragment_ids(&dataset2, frags.into_iter())
6193 .await
6194 .unwrap();
6195 }
6196
6197 for immediate_result in &immediate_results {
6199 let row_addrs_bytes = immediate_result.row_addrs.as_ref().unwrap();
6200 let row_addrs =
6201 RoaringTreemap::deserialize_from(&mut Cursor::new(row_addrs_bytes)).unwrap();
6202 let transposed = transpose_row_addrs(
6203 row_addrs,
6204 &immediate_result.original_fragments,
6205 &immediate_result.new_fragments,
6206 );
6207 expected_all_row_id_map.extend(transposed);
6208 immediate_result.new_fragments.iter().for_each(|frag| {
6209 expected_all_new_frag_bitmap.insert(frag.id as u32);
6210 });
6211 expected_all_new_frag_ids.extend(
6212 immediate_result
6213 .new_fragments
6214 .iter()
6215 .map(|s| s.id)
6216 .collect::<Vec<_>>(),
6217 );
6218 expected_all_old_frag_ids.extend(
6219 immediate_result
6220 .original_fragments
6221 .iter()
6222 .map(|s| s.id)
6223 .collect::<Vec<_>>(),
6224 );
6225 }
6226
6227 let first_metrics = commit_compaction(
6229 &mut dataset,
6230 deferred_results.clone(),
6231 Arc::new(DatasetIndexRemapperOptions::default()),
6232 &options,
6233 )
6234 .await
6235 .unwrap();
6236
6237 assert!(first_metrics.fragments_removed > 0);
6239 assert!(first_metrics.fragments_added > 0);
6240
6241 let Some(frag_reuse_index_meta) = dataset
6243 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6244 .await
6245 .unwrap()
6246 else {
6247 panic!("Fragment reuse index must be available");
6248 };
6249
6250 assert_eq!(
6251 frag_reuse_index_meta.fragment_bitmap.clone().unwrap(),
6252 expected_all_new_frag_bitmap
6253 );
6254 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
6255 .await
6256 .unwrap();
6257 let frag_reuse_index =
6258 open_frag_reuse_index(frag_reuse_index_meta.uuid, frag_reuse_details.as_ref())
6259 .await
6260 .unwrap();
6261 let stats = CompactFragReuseIndexHandle(Arc::new(frag_reuse_index.clone()))
6262 .statistics()
6263 .unwrap();
6264 assert_eq!(
6265 serde_json::to_string(&stats).unwrap(),
6266 dataset
6267 .index_statistics(FRAG_REUSE_INDEX_NAME)
6268 .await
6269 .unwrap()
6270 );
6271
6272 let compaction_version = &frag_reuse_index.details.versions[0];
6274 assert_eq!(frag_reuse_index.details.versions.len(), 1);
6275 assert_eq!(
6276 compaction_version.dataset_version,
6277 frag_reuse_index_meta.dataset_version
6278 );
6279
6280 let mut compacted_all_old_frag_digests = Vec::new();
6282 let mut compacted_all_new_frag_digests = Vec::new();
6283 let mut transposed_map = HashMap::new();
6284 for group in compaction_version.groups.iter() {
6285 let changed_row_addr_bytes = &group.changed_row_addrs;
6286 let mut cursor = Cursor::new(&changed_row_addr_bytes);
6287 let changed_row_addrs = RoaringTreemap::deserialize_from(&mut cursor).unwrap();
6288 compacted_all_old_frag_digests.extend(group.old_frags.clone());
6289 compacted_all_new_frag_digests.extend(group.new_frags.clone());
6290
6291 let group_transposed_map = transpose_row_ids_from_digest(
6292 changed_row_addrs,
6293 &group.old_frags,
6294 &group.new_frags,
6295 );
6296 transposed_map.extend(group_transposed_map);
6297 }
6298 assert_eq!(transposed_map, expected_all_row_id_map);
6299 assert_eq!(
6300 compacted_all_old_frag_digests
6301 .iter()
6302 .map(|f| f.id)
6303 .collect::<Vec<_>>(),
6304 expected_all_old_frag_ids
6305 );
6306 assert_eq!(
6307 compacted_all_new_frag_digests
6308 .iter()
6309 .map(|f| f.id)
6310 .collect::<Vec<_>>(),
6311 expected_all_new_frag_ids
6312 );
6313
6314 let Some(current_scalar_index) = dataset.load_index_by_name("scalar").await.unwrap() else {
6316 panic!("scalar index must be available");
6317 };
6318 assert_eq!(current_scalar_index.uuid, original_scalar_uuid);
6319 }
6320
6321 #[tokio::test]
6322 async fn test_defer_index_remap_skips_fri_when_no_indexed_data() {
6323 let mut data_gen =
6326 BatchGenerator::new().col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6327
6328 let mut dataset = Dataset::write(
6329 data_gen.batch(600),
6330 "memory://test/noindex",
6331 Some(WriteParams {
6332 max_rows_per_file: 100, ..Default::default()
6334 }),
6335 )
6336 .await
6337 .unwrap();
6338
6339 assert!(dataset.load_indices().await.unwrap().is_empty());
6341 let fragments_before = dataset.get_fragments().len();
6342 assert!(fragments_before > 1, "need multiple fragments to compact");
6343
6344 let options = CompactionOptions {
6345 target_rows_per_fragment: 100_000,
6346 defer_index_remap: true,
6347 ..Default::default()
6348 };
6349 compact_files(&mut dataset, options, None).await.unwrap();
6350
6351 assert!(
6353 dataset.get_fragments().len() < fragments_before,
6354 "compaction should have merged fragments"
6355 );
6356 assert!(
6358 dataset
6359 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6360 .await
6361 .unwrap()
6362 .is_none(),
6363 "deferred compaction with no indexed data must not create an FRI"
6364 );
6365 }
6366
6367 #[tokio::test]
6368 async fn test_defer_index_remap_multiple_compactions() {
6369 let mut data_gen = BatchGenerator::new()
6370 .col(Box::new(
6371 RandomVector::new().vec_width(128).named("vec".to_owned()),
6372 ))
6373 .col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6374
6375 let mut dataset = Dataset::write(
6376 data_gen.batch(6_000),
6377 "memory://test/table",
6378 Some(WriteParams {
6379 max_rows_per_file: 1_000, ..Default::default()
6381 }),
6382 )
6383 .await
6384 .unwrap();
6385
6386 create_scalar_index(&mut dataset, "i", false).await;
6389
6390 let options = CompactionOptions {
6391 target_rows_per_fragment: 2_000,
6392 defer_index_remap: true,
6393 ..Default::default()
6394 };
6395
6396 let mut compact_read_versions = Vec::new();
6397 for i in 0..10 {
6398 dataset
6399 .delete(&format!("i < {}", 500 * (i + 1)))
6400 .await
6401 .unwrap();
6402 let read_version = dataset.manifest.version;
6403 compact_files(&mut dataset, options.clone(), None)
6404 .await
6405 .unwrap();
6406
6407 if dataset.manifest.version > read_version {
6409 compact_read_versions.push(read_version);
6410 }
6411
6412 let Some(frag_reuse_index_meta) = dataset
6414 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6415 .await
6416 .unwrap()
6417 else {
6418 panic!("Fragment reuse index must be available");
6419 };
6420 let frag_reuse_details =
6421 load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
6422 .await
6423 .unwrap();
6424 let frag_reuse_index =
6425 open_frag_reuse_index(frag_reuse_index_meta.uuid, frag_reuse_details.as_ref())
6426 .await
6427 .unwrap();
6428
6429 assert_eq!(
6431 frag_reuse_index
6432 .details
6433 .versions
6434 .iter()
6435 .map(|v| v.dataset_version)
6436 .collect::<Vec<_>>(),
6437 compact_read_versions
6438 );
6439 }
6440 }
6441
6442 #[tokio::test]
6443 async fn test_defer_index_remap_mixed_records_all_groups() {
6444 let mut data_gen =
6447 BatchGenerator::new().col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6448 let mut dataset = Dataset::write(
6449 data_gen.batch(300),
6450 "memory://test/mixed",
6451 Some(WriteParams {
6452 max_rows_per_file: 100, ..Default::default()
6454 }),
6455 )
6456 .await
6457 .unwrap();
6458
6459 create_scalar_index(&mut dataset, "i", false).await;
6461 Dataset::write(
6462 data_gen.batch(300),
6463 WriteDestination::Dataset(Arc::new(dataset.clone())),
6464 Some(WriteParams {
6465 max_rows_per_file: 100, mode: WriteMode::Append,
6467 ..Default::default()
6468 }),
6469 )
6470 .await
6471 .unwrap();
6472 dataset.checkout_latest().await.unwrap();
6473
6474 let indexed: HashSet<u32> = dataset
6476 .load_index_by_name("scalar")
6477 .await
6478 .unwrap()
6479 .unwrap()
6480 .fragment_bitmap
6481 .unwrap()
6482 .iter()
6483 .collect();
6484 let unindexed_frags: Vec<u64> = dataset
6485 .fragments()
6486 .iter()
6487 .map(|f| f.id)
6488 .filter(|id| !indexed.contains(&(*id as u32)))
6489 .collect();
6490 assert!(
6491 !unindexed_frags.is_empty(),
6492 "expected some unindexed fragments"
6493 );
6494
6495 compact_files(
6496 &mut dataset,
6497 CompactionOptions {
6498 target_rows_per_fragment: 100_000,
6499 defer_index_remap: true,
6500 ..Default::default()
6501 },
6502 None,
6503 )
6504 .await
6505 .unwrap();
6506
6507 let fri_meta = dataset
6511 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6512 .await
6513 .unwrap()
6514 .expect("mixed compaction must write an FRI");
6515 let details = load_frag_reuse_index_details(&dataset, &fri_meta)
6516 .await
6517 .unwrap();
6518 let recorded_old: HashSet<u64> = details
6519 .versions
6520 .iter()
6521 .flat_map(|v| v.old_frag_ids())
6522 .collect();
6523 for f in &unindexed_frags {
6524 assert!(
6525 recorded_old.contains(f),
6526 "unindexed fragment {f} must be recorded in the FRI (all-or-nothing)"
6527 );
6528 }
6529 }
6530
6531 #[tokio::test]
6532 async fn test_deferred_compaction_not_split_by_frag_reuse_index() {
6533 let data = sample_data();
6537 let test_dir = TempStrDir::default();
6538 let test_uri = &test_dir;
6539 let options = CompactionOptions {
6540 defer_index_remap: true,
6541 ..Default::default()
6542 };
6543
6544 let reader = RecordBatchIterator::new(vec![Ok(data.slice(0, 400))], data.schema());
6547 let mut dataset = Dataset::write(
6548 reader,
6549 test_uri,
6550 Some(WriteParams {
6551 max_rows_per_file: 200,
6552 ..Default::default()
6553 }),
6554 )
6555 .await
6556 .unwrap();
6557
6558 create_scalar_index(&mut dataset, "a", false).await;
6562 compact_files(&mut dataset, options.clone(), None)
6563 .await
6564 .unwrap();
6565 assert_eq!(dataset.get_fragments().len(), 1);
6566 assert!(
6567 dataset
6568 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6569 .await
6570 .unwrap()
6571 .is_some()
6572 );
6573
6574 let reader = RecordBatchIterator::new(vec![Ok(data.slice(400, 400))], data.schema());
6576 let mut dataset = Dataset::write(
6577 reader,
6578 test_uri,
6579 Some(WriteParams {
6580 max_rows_per_file: 200,
6581 mode: WriteMode::Append,
6582 ..Default::default()
6583 }),
6584 )
6585 .await
6586 .unwrap();
6587 assert_eq!(dataset.get_fragments().len(), 3);
6588
6589 create_scalar_index(&mut dataset, "a", true).await;
6593
6594 compact_files(&mut dataset, options, None).await.unwrap();
6595 assert_eq!(
6596 dataset.get_fragments().len(),
6597 1,
6598 "FRI (a system index) must not split the compaction bin; all fragments coalesce"
6599 );
6600 }
6601
6602 #[tokio::test]
6603 async fn test_remap_index_after_compaction() {
6604 let mut data_gen = BatchGenerator::new()
6605 .col(Box::new(
6606 RandomVector::new().vec_width(128).named("vec".to_owned()),
6607 ))
6608 .col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6609
6610 let mut dataset = Dataset::write(
6611 data_gen.batch(6_000),
6612 "memory://test/table",
6613 Some(WriteParams {
6614 max_rows_per_file: 1_000, ..Default::default()
6616 }),
6617 )
6618 .await
6619 .unwrap();
6620
6621 let index_name = Some("scalar".into());
6623 dataset
6624 .create_index(
6625 &["i"],
6626 IndexType::Scalar,
6627 index_name.clone(),
6628 &ScalarIndexParams::default(),
6629 false,
6630 )
6631 .await
6632 .unwrap();
6633
6634 let options = CompactionOptions {
6635 target_rows_per_fragment: 2_000,
6636 defer_index_remap: true,
6637 ..Default::default()
6638 };
6639
6640 let Some(scalar_index) = dataset.load_index_by_name("scalar").await.unwrap() else {
6642 panic!("scalar index must be available");
6643 };
6644
6645 let result = remapping::remap_column_index(&mut dataset, &["i"], index_name.clone()).await;
6646 assert!(matches!(result, Err(Error::NotSupported { .. })));
6647
6648 let plan = plan_compaction(&dataset, &options).await.unwrap();
6649
6650 for task in plan.tasks().iter() {
6653 let rewrite_result = rewrite_files(Cow::Borrowed(&dataset), task.clone(), &options)
6654 .await
6655 .unwrap();
6656
6657 commit_compaction(
6658 &mut dataset,
6659 Vec::from([rewrite_result]),
6660 Arc::new(DatasetIndexRemapperOptions::default()),
6661 &options,
6662 )
6663 .await
6664 .unwrap();
6665 }
6666
6667 let Some(frag_reuse_index_meta) = dataset
6669 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6670 .await
6671 .unwrap()
6672 else {
6673 panic!("Fragment reuse index must be available");
6674 };
6675 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
6676 .await
6677 .unwrap();
6678 let frag_reuse_index =
6679 open_frag_reuse_index(frag_reuse_index_meta.uuid, frag_reuse_details.as_ref())
6680 .await
6681 .unwrap();
6682
6683 assert_eq!(frag_reuse_index.details.versions.len(), plan.tasks().len());
6684
6685 let mut all_fragment_bitmap = RoaringBitmap::new();
6687 dataset.fragments().iter().for_each(|f| {
6688 all_fragment_bitmap.insert(f.id as u32);
6689 });
6690 let Some(scalar_index_before_remap) = dataset.load_index_by_name("scalar").await.unwrap()
6691 else {
6692 panic!("scalar index must be available");
6693 };
6694 assert_eq!(
6695 scalar_index_before_remap.fragment_bitmap.unwrap(),
6696 all_fragment_bitmap
6697 );
6698
6699 remapping::remap_column_index(&mut dataset, &["i"], index_name.clone())
6701 .await
6702 .unwrap();
6703
6704 let indices = read_manifest_indexes(
6706 &dataset.object_store,
6707 &dataset.manifest_location,
6708 &dataset.manifest,
6709 )
6710 .await
6711 .unwrap();
6712 let Some(remapped_scalar_index) = indices.into_iter().find(|idx| idx.name == "scalar")
6713 else {
6714 panic!("scalar index must be available");
6715 };
6716 assert_ne!(remapped_scalar_index.uuid, scalar_index.uuid);
6717 assert_eq!(
6718 remapped_scalar_index.fragment_bitmap.unwrap(),
6719 all_fragment_bitmap
6720 );
6721 }
6722
6723 #[tokio::test]
6724 async fn test_concurrent_compaction_reindex_compaction_commit_first() {
6725 let mut data_gen = BatchGenerator::new()
6726 .col(Box::new(
6727 RandomVector::new().vec_width(128).named("vec".to_owned()),
6728 ))
6729 .col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6730
6731 let mut dataset = Dataset::write(
6732 data_gen.batch(6_000),
6733 "memory://test/table",
6734 Some(WriteParams {
6735 max_rows_per_file: 1_000, ..Default::default()
6737 }),
6738 )
6739 .await
6740 .unwrap();
6741
6742 let index_name = Some("scalar".into());
6744 dataset
6745 .create_index(
6746 &["i"],
6747 IndexType::Scalar,
6748 index_name.clone(),
6749 &ScalarIndexParams::default(),
6750 false,
6751 )
6752 .await
6753 .unwrap();
6754
6755 Dataset::write(
6757 data_gen.batch(6_000),
6758 WriteDestination::Dataset(Arc::new(dataset.clone())),
6759 Some(WriteParams {
6760 max_rows_per_file: 1_000, mode: WriteMode::Append,
6762 ..Default::default()
6763 }),
6764 )
6765 .await
6766 .unwrap();
6767
6768 dataset.checkout_latest().await.unwrap();
6769 let mut dataset_clone = dataset.clone();
6770
6771 compact_files(
6773 &mut dataset,
6774 CompactionOptions {
6775 target_rows_per_fragment: 2_000,
6776 defer_index_remap: true,
6777 ..Default::default()
6778 },
6779 None,
6780 )
6781 .await
6782 .unwrap();
6783
6784 dataset_clone
6786 .create_index(
6787 &["i"],
6788 IndexType::Scalar,
6789 index_name.clone(),
6790 &ScalarIndexParams::default(),
6791 true,
6792 )
6793 .await
6794 .unwrap();
6795
6796 dataset.checkout_latest().await.unwrap();
6798
6799 let Some(scalar_index) = dataset.load_index_by_name("scalar").await.unwrap() else {
6800 panic!("scalar index must be available");
6801 };
6802 let index_frags = scalar_index
6803 .fragment_bitmap
6804 .unwrap()
6805 .iter()
6806 .collect::<HashSet<_>>();
6807 assert_eq!(
6808 index_frags,
6809 dataset
6810 .fragments()
6811 .iter()
6812 .map(|f| f.id as u32)
6813 .collect::<HashSet<_>>()
6814 )
6815 }
6816
6817 #[tokio::test]
6818 async fn test_concurrent_compaction_reindex_reindex_commit_first() {
6819 let mut data_gen = BatchGenerator::new()
6820 .col(Box::new(
6821 RandomVector::new().vec_width(128).named("vec".to_owned()),
6822 ))
6823 .col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6824
6825 let mut dataset = Dataset::write(
6826 data_gen.batch(6_000),
6827 "memory://test/table",
6828 Some(WriteParams {
6829 max_rows_per_file: 1_000, ..Default::default()
6831 }),
6832 )
6833 .await
6834 .unwrap();
6835
6836 let index_name = Some("scalar".into());
6838 dataset
6839 .create_index(
6840 &["i"],
6841 IndexType::Scalar,
6842 index_name.clone(),
6843 &ScalarIndexParams::default(),
6844 false,
6845 )
6846 .await
6847 .unwrap();
6848
6849 Dataset::write(
6851 data_gen.batch(6_000),
6852 WriteDestination::Dataset(Arc::new(dataset.clone())),
6853 Some(WriteParams {
6854 max_rows_per_file: 1_000, mode: WriteMode::Append,
6856 ..Default::default()
6857 }),
6858 )
6859 .await
6860 .unwrap();
6861
6862 dataset.checkout_latest().await.unwrap();
6863 let mut dataset_clone = dataset.clone();
6864
6865 dataset
6867 .create_index(
6868 &["i"],
6869 IndexType::Scalar,
6870 index_name.clone(),
6871 &ScalarIndexParams::default(),
6872 true,
6873 )
6874 .await
6875 .unwrap();
6876
6877 compact_files(
6879 &mut dataset_clone,
6880 CompactionOptions {
6881 target_rows_per_fragment: 2_000,
6882 defer_index_remap: true,
6883 ..Default::default()
6884 },
6885 None,
6886 )
6887 .await
6888 .unwrap();
6889
6890 dataset.checkout_latest().await.unwrap();
6892 let Some(scalar_index) = dataset.load_index_by_name("scalar").await.unwrap() else {
6893 panic!("scalar index must be available");
6894 };
6895 let index_frags = scalar_index
6896 .fragment_bitmap
6897 .unwrap()
6898 .iter()
6899 .collect::<HashSet<_>>();
6900 assert_eq!(
6901 index_frags,
6902 dataset
6903 .fragments()
6904 .iter()
6905 .map(|f| f.id as u32)
6906 .collect::<HashSet<_>>()
6907 )
6908 }
6909
6910 #[tokio::test]
6911 async fn test_concurrent_cleanup_and_compaction_rebase_cleanup() {
6912 let mut dataset = lance_datagen::gen_batch()
6913 .col(
6914 "vec",
6915 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
6916 )
6917 .col("i", lance_datagen::array::step::<Int32Type>())
6918 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
6919 .await
6920 .unwrap();
6921
6922 create_scalar_index(&mut dataset, "i", false).await;
6924
6925 let options = CompactionOptions {
6926 target_rows_per_fragment: 2_000,
6927 defer_index_remap: true,
6928 ..Default::default()
6929 };
6930
6931 let plan = plan_compaction(&dataset, &options).await.unwrap();
6932 let tasks = plan.tasks();
6933
6934 let rewrite_result = rewrite_files(Cow::Borrowed(&dataset), tasks[0].clone(), &options)
6936 .await
6937 .unwrap();
6938
6939 commit_compaction(
6940 &mut dataset,
6941 Vec::from([rewrite_result]),
6942 Arc::new(DatasetIndexRemapperOptions::default()),
6943 &options,
6944 )
6945 .await
6946 .unwrap();
6947
6948 let mut dataset_clone = dataset.clone();
6949
6950 let Some(frag_reuse_index_meta) = dataset
6952 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6953 .await
6954 .unwrap()
6955 else {
6956 panic!("Fragment reuse index must be available");
6957 };
6958
6959 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
6960 .await
6961 .unwrap();
6962 assert_eq!(frag_reuse_details.versions.len(), 1);
6963
6964 let rewrite_result2 = rewrite_files(Cow::Borrowed(&dataset), tasks[1].clone(), &options)
6966 .await
6967 .unwrap();
6968 let rewritten_frags2 = rewrite_result2
6969 .original_fragments
6970 .iter()
6971 .map(|f| f.id)
6972 .collect::<Vec<_>>();
6973 commit_compaction(
6974 &mut dataset,
6975 Vec::from([rewrite_result2]),
6976 Arc::new(DatasetIndexRemapperOptions::default()),
6977 &options,
6978 )
6979 .await
6980 .unwrap();
6981
6982 let frag_reuse_index_meta2 = dataset
6984 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
6985 .await
6986 .unwrap()
6987 .unwrap();
6988 let frag_reuse_details2 = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta2)
6989 .await
6990 .unwrap();
6991 let new_frags2 = frag_reuse_details2.versions.last().unwrap().new_frag_ids();
6992
6993 let rewrite_result3 = rewrite_files(Cow::Borrowed(&dataset), tasks[2].clone(), &options)
6994 .await
6995 .unwrap();
6996 let rewritten_frags3 = rewrite_result3
6997 .original_fragments
6998 .iter()
6999 .map(|f| f.id)
7000 .collect::<Vec<_>>();
7001 commit_compaction(
7002 &mut dataset,
7003 Vec::from([rewrite_result3]),
7004 Arc::new(DatasetIndexRemapperOptions::default()),
7005 &options,
7006 )
7007 .await
7008 .unwrap();
7009
7010 let frag_reuse_index_meta3 = dataset
7012 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
7013 .await
7014 .unwrap()
7015 .unwrap();
7016 let frag_reuse_details3 = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta3)
7017 .await
7018 .unwrap();
7019 let new_frags3 = frag_reuse_details3.versions.last().unwrap().new_frag_ids();
7020
7021 remapping::remap_column_index(&mut dataset_clone, &["i"], Some("scalar".into()))
7026 .await
7027 .unwrap();
7028 cleanup_frag_reuse_index(&mut dataset_clone).await.unwrap();
7029
7030 dataset.checkout_latest().await.unwrap();
7032 let Some(frag_reuse_index_meta) = dataset
7033 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
7034 .await
7035 .unwrap()
7036 else {
7037 panic!("Fragment reuse index must be available");
7038 };
7039 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
7040 .await
7041 .unwrap();
7042 assert_eq!(frag_reuse_details.versions.len(), 2);
7043 assert_eq!(
7044 frag_reuse_details.versions[0].old_frag_ids(),
7045 rewritten_frags2
7046 );
7047 assert_eq!(frag_reuse_details.versions[0].new_frag_ids(), new_frags2);
7048 assert_eq!(
7049 frag_reuse_details.versions[1].old_frag_ids(),
7050 rewritten_frags3
7051 );
7052 assert_eq!(frag_reuse_details.versions[1].new_frag_ids(), new_frags3);
7053 }
7054
7055 #[tokio::test]
7056 async fn test_concurrent_cleanup_and_compaction_rebase_compaction() {
7057 let mut dataset = lance_datagen::gen_batch()
7058 .col(
7059 "vec",
7060 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
7061 )
7062 .col("i", lance_datagen::array::step::<Int32Type>())
7063 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
7064 .await
7065 .unwrap();
7066
7067 create_scalar_index(&mut dataset, "i", false).await;
7069
7070 let options = CompactionOptions {
7071 target_rows_per_fragment: 2_000,
7072 defer_index_remap: true,
7073 ..Default::default()
7074 };
7075
7076 let plan = plan_compaction(&dataset, &options).await.unwrap();
7077 let tasks = plan.tasks();
7078
7079 let rewrite_result = rewrite_files(Cow::Borrowed(&dataset), tasks[0].clone(), &options)
7081 .await
7082 .unwrap();
7083
7084 commit_compaction(
7085 &mut dataset,
7086 Vec::from([rewrite_result]),
7087 Arc::new(DatasetIndexRemapperOptions::default()),
7088 &options,
7089 )
7090 .await
7091 .unwrap();
7092
7093 let mut dataset_clone = dataset.clone();
7094
7095 let Some(frag_reuse_index_meta) = dataset
7097 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
7098 .await
7099 .unwrap()
7100 else {
7101 panic!("Fragment reuse index must be available");
7102 };
7103 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
7104 .await
7105 .unwrap();
7106 assert_eq!(frag_reuse_details.versions.len(), 1);
7107
7108 remapping::remap_column_index(&mut dataset, &["i"], Some("scalar".into()))
7112 .await
7113 .unwrap();
7114 cleanup_frag_reuse_index(&mut dataset).await.unwrap();
7115
7116 dataset.checkout_latest().await.unwrap();
7118 let Some(frag_reuse_index_meta) = dataset
7119 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
7120 .await
7121 .unwrap()
7122 else {
7123 panic!("Fragment reuse index must be available");
7124 };
7125 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
7126 .await
7127 .unwrap();
7128 assert_eq!(frag_reuse_details.versions.len(), 0);
7129
7130 let rewrite_result2 =
7133 rewrite_files(Cow::Borrowed(&dataset_clone), tasks[1].clone(), &options)
7134 .await
7135 .unwrap();
7136 let rewritten_frags2 = rewrite_result2
7137 .original_fragments
7138 .iter()
7139 .map(|f| f.id)
7140 .collect::<Vec<_>>();
7141 commit_compaction(
7142 &mut dataset_clone,
7143 Vec::from([rewrite_result2]),
7144 Arc::new(DatasetIndexRemapperOptions::default()),
7145 &options,
7146 )
7147 .await
7148 .unwrap();
7149
7150 dataset.checkout_latest().await.unwrap();
7152 let Some(frag_reuse_index_meta) = dataset
7153 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
7154 .await
7155 .unwrap()
7156 else {
7157 panic!("Fragment reuse index must be available");
7158 };
7159 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
7160 .await
7161 .unwrap();
7162 assert_eq!(frag_reuse_details.versions.len(), 1);
7163 assert_eq!(
7164 frag_reuse_details.versions[0].old_frag_ids(),
7165 rewritten_frags2
7166 );
7167 let new_frags2 = frag_reuse_details.versions[0].new_frag_ids();
7169 assert!(new_frags2.iter().all(|id| *id != 0));
7170 }
7171
7172 #[tokio::test]
7173 async fn test_concurrent_compactions_with_defer_index_remap() {
7174 let mut dataset = lance_datagen::gen_batch()
7175 .col(
7176 "vec",
7177 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
7178 )
7179 .col("i", lance_datagen::array::step::<Int32Type>())
7180 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
7181 .await
7182 .unwrap();
7183
7184 create_scalar_index(&mut dataset, "i", false).await;
7186
7187 let options = CompactionOptions {
7188 target_rows_per_fragment: 2_000,
7189 defer_index_remap: true,
7190 ..Default::default()
7191 };
7192
7193 let plan = plan_compaction(&dataset, &options).await.unwrap();
7194 let tasks = plan.tasks();
7195
7196 let mut dataset_clone = dataset.clone();
7197
7198 let rewrite_result = rewrite_files(Cow::Borrowed(&dataset), tasks[0].clone(), &options)
7200 .await
7201 .unwrap();
7202
7203 commit_compaction(
7204 &mut dataset,
7205 Vec::from([rewrite_result]),
7206 Arc::new(DatasetIndexRemapperOptions::default()),
7207 &options,
7208 )
7209 .await
7210 .unwrap();
7211
7212 let Some(frag_reuse_index_meta) = dataset
7214 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
7215 .await
7216 .unwrap()
7217 else {
7218 panic!("Fragment reuse index must be available");
7219 };
7220 let frag_reuse_details = load_frag_reuse_index_details(&dataset, &frag_reuse_index_meta)
7221 .await
7222 .unwrap();
7223 assert_eq!(frag_reuse_details.versions.len(), 1);
7224
7225 let rewrite_result2 =
7227 rewrite_files(Cow::Borrowed(&dataset_clone), tasks[1].clone(), &options)
7228 .await
7229 .unwrap();
7230 let result = commit_compaction(
7231 &mut dataset_clone,
7232 Vec::from([rewrite_result2]),
7233 Arc::new(DatasetIndexRemapperOptions::default()),
7234 &options,
7235 )
7236 .await;
7237 assert!(matches!(result, Err(Error::RetryableCommitConflict { .. })));
7238 }
7239
7240 #[tokio::test]
7241 async fn test_read_bitmap_index_with_defer_index_remap() {
7242 let mut dataset = lance_datagen::gen_batch()
7244 .col(
7245 "vec",
7246 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
7247 )
7248 .col(
7249 "category",
7250 lance_datagen::array::cycle::<Int32Type>(vec![1, 2, 3]),
7251 )
7252 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
7253 .await
7254 .unwrap();
7255
7256 let count1 = dataset
7258 .count_rows(Some("category = 1".to_owned()))
7259 .await
7260 .unwrap();
7261 let count2 = dataset
7262 .count_rows(Some("category = 2".to_owned()))
7263 .await
7264 .unwrap();
7265 let count3 = dataset
7266 .count_rows(Some("category = 3".to_owned()))
7267 .await
7268 .unwrap();
7269
7270 let index_name = Some("category_idx".into());
7272 dataset
7273 .create_index(
7274 &["category"],
7275 IndexType::Bitmap,
7276 index_name.clone(),
7277 &ScalarIndexParams::default(),
7278 false,
7279 )
7280 .await
7281 .unwrap();
7282 let indices = dataset.load_indices().await.unwrap();
7283 let original_index = indices
7284 .iter()
7285 .find(|idx| idx.name == "category_idx")
7286 .unwrap();
7287
7288 let options = CompactionOptions {
7290 target_rows_per_fragment: 2_000,
7291 defer_index_remap: true,
7292 ..Default::default()
7293 };
7294
7295 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
7296 assert!(metrics.fragments_removed > 0);
7297 assert!(metrics.fragments_added > 0);
7298
7299 let Some(current_index) = dataset.load_index_by_name("category_idx").await.unwrap() else {
7301 panic!("category index must be available");
7302 };
7303 assert_eq!(current_index.uuid, original_index.uuid);
7304
7305 assert_eq!(
7307 dataset
7308 .count_rows(Some("category = 1".to_owned()))
7309 .await
7310 .unwrap(),
7311 count1
7312 );
7313 assert_eq!(
7314 dataset
7315 .count_rows(Some("category = 2".to_owned()))
7316 .await
7317 .unwrap(),
7318 count2
7319 );
7320 assert_eq!(
7321 dataset
7322 .count_rows(Some("category = 3".to_owned()))
7323 .await
7324 .unwrap(),
7325 count3
7326 );
7327
7328 let mut scanner = dataset.scan();
7330 scanner.filter("category = 1").unwrap();
7331 scanner.project::<String>(&[]).unwrap().with_row_id();
7332 let plan = scanner.explain_plan(false).await.unwrap();
7333 assert!(
7334 plan.contains("ScalarIndexQuery: query=[category = 1]@category_idx(Bitmap)"),
7335 "Expected index query in plan: {}",
7336 plan
7337 );
7338 }
7339
7340 #[tokio::test]
7341 async fn test_read_bloom_filter_index_with_defer_index_remap() {
7342 let mut dataset = lance_datagen::gen_batch()
7343 .col("id", lance_datagen::array::step::<Int32Type>())
7344 .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(3))
7345 .await
7346 .unwrap();
7347
7348 dataset
7349 .create_index(
7350 &["id"],
7351 IndexType::BloomFilter,
7352 Some("id_idx".into()),
7353 &ScalarIndexParams::for_builtin(BuiltinIndexType::BloomFilter),
7354 false,
7355 )
7356 .await
7357 .unwrap();
7358
7359 let metrics = compact_files(
7360 &mut dataset,
7361 CompactionOptions {
7362 target_rows_per_fragment: 512,
7363 defer_index_remap: true,
7364 ..Default::default()
7365 },
7366 None,
7367 )
7368 .await
7369 .unwrap();
7370 assert!(metrics.fragments_removed > 0);
7371 assert!(metrics.fragments_added > 0);
7372
7373 assert_eq!(
7374 dataset.count_rows(Some("id = 2".to_owned())).await.unwrap(),
7375 1
7376 );
7377
7378 let mut scanner = dataset.scan();
7379 scanner.filter("id = 2").unwrap();
7380 let plan = scanner.explain_plan(false).await.unwrap();
7381 assert!(
7382 plan.contains("ScalarIndexQuery: query=[id = 2]@id_idx(BloomFilter)"),
7383 "Expected BloomFilter index query in plan: {plan}"
7384 );
7385 }
7386
7387 #[tokio::test]
7388 async fn test_read_zonemap_index_with_defer_index_remap() {
7389 let batch = arrow_array::record_batch!(
7390 ("id", Int32, (0..12).collect::<Vec<_>>()),
7391 (
7392 "value",
7393 Int64,
7394 [
7395 Some(0),
7396 None,
7397 Some(20),
7398 Some(30),
7399 Some(40),
7400 None,
7401 Some(60),
7402 Some(70),
7403 Some(80),
7404 None,
7405 Some(100),
7406 Some(110)
7407 ]
7408 )
7409 )
7410 .unwrap();
7411 let reader = RecordBatchIterator::new([Ok(batch.clone())], batch.schema());
7412 let mut dataset = Dataset::write(
7413 reader,
7414 "memory://",
7415 Some(WriteParams {
7416 max_rows_per_file: 4,
7417 max_rows_per_group: 4,
7418 enable_stable_row_ids: false,
7419 ..Default::default()
7420 }),
7421 )
7422 .await
7423 .unwrap();
7424 assert_eq!(dataset.get_fragments().len(), 3);
7425
7426 dataset
7427 .create_index(
7428 &["value"],
7429 IndexType::ZoneMap,
7430 Some("value_idx".into()),
7431 &ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap),
7432 false,
7433 )
7434 .await
7435 .unwrap();
7436
7437 let metrics = compact_files(
7438 &mut dataset,
7439 CompactionOptions {
7440 target_rows_per_fragment: 512,
7441 defer_index_remap: true,
7442 ..Default::default()
7443 },
7444 None,
7445 )
7446 .await
7447 .unwrap();
7448 assert_eq!(metrics.fragments_removed, 3);
7449 assert_eq!(metrics.fragments_added, 1);
7450
7451 async fn scan_ids(dataset: &Dataset, filter: &str, use_scalar_index: bool) -> Vec<i32> {
7452 let mut scanner = dataset.scan();
7453 scanner.filter(filter).unwrap();
7454 scanner.project(&["id"]).unwrap();
7455 scanner.use_scalar_index(use_scalar_index);
7456 scanner.try_into_batch().await.unwrap()["id"]
7457 .as_primitive::<Int32Type>()
7458 .values()
7459 .to_vec()
7460 }
7461
7462 for (filter, expected) in [
7463 ("value IS NULL", vec![1, 5, 9]),
7464 ("value = 20", vec![2]),
7465 ("value > 90", vec![10, 11]),
7466 ] {
7467 assert_eq!(scan_ids(&dataset, filter, false).await, expected);
7468 assert_eq!(scan_ids(&dataset, filter, true).await, expected);
7469 }
7470
7471 let merged = dataset
7472 .merge_existing_index_segments(dataset.load_indices_by_name("value_idx").await.unwrap())
7473 .await
7474 .unwrap();
7475 dataset
7476 .commit_existing_index_segments("value_idx", "value", vec![merged])
7477 .await
7478 .unwrap();
7479
7480 for (filter, expected) in [
7481 ("value IS NULL", vec![1, 5, 9]),
7482 ("value = 20", vec![2]),
7483 ("value > 90", vec![10, 11]),
7484 ] {
7485 assert_eq!(scan_ids(&dataset, filter, false).await, expected);
7486 assert_eq!(scan_ids(&dataset, filter, true).await, expected);
7487 }
7488
7489 let mut scanner = dataset.scan();
7490 scanner.filter("value IS NULL").unwrap();
7491 let plan = scanner.explain_plan(false).await.unwrap();
7492 assert!(
7493 plan.contains("ScalarIndexQuery: query=[value IS NULL]@value_idx(ZoneMap)"),
7494 "Expected ZoneMap index query in plan: {plan}"
7495 );
7496 }
7497
7498 #[tokio::test]
7499 async fn test_read_btree_index_with_defer_index_remap() {
7500 let mut dataset = lance_datagen::gen_batch()
7502 .col(
7503 "vec",
7504 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
7505 )
7506 .col("id", lance_datagen::array::step::<Int32Type>())
7507 .into_ram_dataset(FragmentCount::from(110), FragmentRowCount::from(1000))
7508 .await
7509 .unwrap();
7510
7511 let count_low = dataset
7513 .count_rows(Some("id < 1000".to_owned()))
7514 .await
7515 .unwrap();
7516 let count_mid = dataset
7517 .count_rows(Some("id >= 2000 and id < 3000".to_owned()))
7518 .await
7519 .unwrap();
7520 let count_high = dataset
7521 .count_rows(Some("id >= 5000".to_owned()))
7522 .await
7523 .unwrap();
7524
7525 let index_name = Some("id_idx".into());
7527 dataset
7528 .create_index(
7529 &["id"],
7530 IndexType::BTree,
7531 index_name.clone(),
7532 &ScalarIndexParams::default(),
7533 false,
7534 )
7535 .await
7536 .unwrap();
7537 let indices = dataset.load_indices().await.unwrap();
7538 let original_index = indices.iter().find(|idx| idx.name == "id_idx").unwrap();
7539
7540 let options = CompactionOptions {
7542 target_rows_per_fragment: 50_000,
7543 defer_index_remap: true,
7544 ..Default::default()
7545 };
7546
7547 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
7548 assert!(metrics.fragments_removed > 0);
7549 assert!(metrics.fragments_added > 0);
7550
7551 let Some(current_index) = dataset.load_index_by_name("id_idx").await.unwrap() else {
7553 panic!("id index must be available");
7554 };
7555 assert_eq!(current_index.uuid, original_index.uuid);
7556
7557 assert_eq!(
7559 dataset
7560 .count_rows(Some("id < 1000".to_owned()))
7561 .await
7562 .unwrap(),
7563 count_low
7564 );
7565 assert_eq!(
7566 dataset
7567 .count_rows(Some("id >= 2000 and id < 3000".to_owned()))
7568 .await
7569 .unwrap(),
7570 count_mid
7571 );
7572 assert_eq!(
7573 dataset
7574 .count_rows(Some("id >= 5000".to_owned()))
7575 .await
7576 .unwrap(),
7577 count_high
7578 );
7579
7580 let mut scanner = dataset.scan();
7582 scanner.filter("id >= 2000 and id < 3000").unwrap();
7583 scanner.project::<String>(&[]).unwrap().with_row_id();
7584 let plan = scanner.explain_plan(false).await.unwrap();
7585 assert!(
7586 plan.contains("ScalarIndexQuery: query=[id >= 2000 && id < 3000]@id_idx(BTree)"),
7587 "Expected scalar index query in plan: {}",
7588 plan
7589 );
7590 }
7591
7592 #[rstest]
7593 #[case(IndexRemapMode::Compact)]
7594 #[case(IndexRemapMode::Direct)]
7595 #[tokio::test]
7596 async fn test_btree_index_remap_after_compaction(#[case] index_remap_mode: IndexRemapMode) {
7597 let mut dataset = lance_datagen::gen_batch()
7598 .col(
7599 "vec",
7600 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(32)),
7601 )
7602 .col("id", lance_datagen::array::step::<Int32Type>())
7603 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
7604 .await
7605 .unwrap();
7606
7607 dataset.delete("id % 10 == 0").await.unwrap();
7610
7611 dataset
7612 .create_index(
7613 &["id"],
7614 IndexType::BTree,
7615 Some("id_idx".into()),
7616 &ScalarIndexParams::default(),
7617 false,
7618 )
7619 .await
7620 .unwrap();
7621
7622 let count_low = dataset
7623 .count_rows(Some("id < 1000".to_owned()))
7624 .await
7625 .unwrap();
7626 let count_mid = dataset
7627 .count_rows(Some("id >= 2000 and id < 3000".to_owned()))
7628 .await
7629 .unwrap();
7630 let count_high = dataset
7631 .count_rows(Some("id >= 5000".to_owned()))
7632 .await
7633 .unwrap();
7634
7635 let options = CompactionOptions {
7636 target_rows_per_fragment: 50_000,
7637 index_remap_mode,
7638 ..Default::default()
7639 };
7640 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
7641 assert!(metrics.fragments_removed > 0);
7642 assert!(metrics.fragments_added > 0);
7643
7644 let mut scanner = dataset.scan();
7646 scanner.filter("id >= 2000 and id < 3000").unwrap();
7647 scanner.project::<String>(&[]).unwrap().with_row_id();
7648 let plan = scanner.explain_plan(false).await.unwrap();
7649 assert!(
7650 plan.contains("ScalarIndexQuery: query=[id >= 2000 && id < 3000]@id_idx(BTree)"),
7651 "Expected scalar index query in plan: {}",
7652 plan
7653 );
7654
7655 assert_eq!(
7658 dataset
7659 .count_rows(Some("id < 1000".to_owned()))
7660 .await
7661 .unwrap(),
7662 count_low
7663 );
7664 assert_eq!(
7665 dataset
7666 .count_rows(Some("id >= 2000 and id < 3000".to_owned()))
7667 .await
7668 .unwrap(),
7669 count_mid
7670 );
7671 assert_eq!(
7672 dataset
7673 .count_rows(Some("id >= 5000".to_owned()))
7674 .await
7675 .unwrap(),
7676 count_high
7677 );
7678 }
7679
7680 #[rstest]
7681 #[case(IndexRemapMode::Compact)]
7682 #[case(IndexRemapMode::Direct)]
7683 #[tokio::test]
7684 async fn test_ivf_pq_index_remap_after_compaction(#[case] index_remap_mode: IndexRemapMode) {
7685 use arrow_array::cast::AsArray;
7686 use lance_index::vector::pq::PQBuildParams;
7687
7688 const DIM: u32 = 32;
7689 let mut dataset = lance_datagen::gen_batch()
7690 .col("id", lance_datagen::array::step::<Int32Type>())
7691 .col(
7692 "vec",
7693 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(DIM)),
7694 )
7695 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
7696 .await
7697 .unwrap();
7698
7699 let params = VectorIndexParams::with_ivf_pq_params(
7700 DistanceType::L2,
7701 small_ivf(),
7702 PQBuildParams {
7703 max_iters: 2,
7704 num_sub_vectors: 2,
7705 ..Default::default()
7706 },
7707 );
7708 dataset
7709 .create_index(
7710 &["vec"],
7711 IndexType::Vector,
7712 Some("vec_idx".into()),
7713 ¶ms,
7714 false,
7715 )
7716 .await
7717 .unwrap();
7718 let original_uuid = dataset
7719 .load_index_by_name("vec_idx")
7720 .await
7721 .unwrap()
7722 .unwrap()
7723 .uuid;
7724
7725 dataset.delete("id % 10 == 0").await.unwrap();
7728
7729 let mut survivors: Vec<(i32, Vec<f32>)> = Vec::new();
7732 {
7733 let mut scanner = dataset.scan();
7734 scanner.project(&["id", "vec"]).unwrap();
7735 let batches = scanner
7736 .try_into_stream()
7737 .await
7738 .unwrap()
7739 .try_collect::<Vec<_>>()
7740 .await
7741 .unwrap();
7742 for batch in &batches {
7743 let ids = batch["id"].as_primitive::<Int32Type>();
7744 let vecs = batch["vec"].as_fixed_size_list();
7745 for i in 0..batch.num_rows() {
7746 let v = vecs.value(i);
7747 survivors.push((
7748 ids.value(i),
7749 v.as_primitive::<Float32Type>().values().to_vec(),
7750 ));
7751 }
7752 }
7753 }
7754 let surviving_ids: std::collections::HashSet<i32> =
7755 survivors.iter().map(|(id, _)| *id).collect();
7756 let step = (survivors.len() / 16).max(1);
7757 let queries: Vec<Vec<f32>> = survivors
7758 .iter()
7759 .step_by(step)
7760 .map(|(_, v)| v.clone())
7761 .collect();
7762 let k = 10;
7763 let mut baseline: Vec<Vec<i32>> = Vec::new();
7764 for q in &queries {
7765 baseline.push(vector_knn_ids(&dataset, q, k).await);
7766 }
7767
7768 let metrics = compact_files(
7771 &mut dataset,
7772 CompactionOptions {
7773 target_rows_per_fragment: 50_000,
7774 index_remap_mode,
7775 ..Default::default()
7776 },
7777 None,
7778 )
7779 .await
7780 .unwrap();
7781 assert!(metrics.fragments_removed > 0);
7782 assert!(metrics.fragments_added > 0);
7783
7784 assert_ne!(
7786 dataset
7787 .load_index_by_name("vec_idx")
7788 .await
7789 .unwrap()
7790 .unwrap()
7791 .uuid,
7792 original_uuid,
7793 "vector index must be physically remapped inline"
7794 );
7795
7796 for (i, q) in queries.iter().enumerate() {
7800 let after = vector_knn_ids(&dataset, q, k).await;
7801 for id in &after {
7802 assert!(
7803 surviving_ids.contains(id),
7804 "KNN returned id {id} that is not a surviving row (query #{i}, mode {index_remap_mode:?})"
7805 );
7806 }
7807 let overlap = after.iter().filter(|id| baseline[i].contains(id)).count();
7808 assert!(
7809 overlap >= 8,
7810 "KNN top-{k} diverged after compaction: overlap {overlap} < 8 (query #{i}, mode {index_remap_mode:?})"
7811 );
7812 }
7813 }
7814
7815 #[rstest]
7816 #[case(IndexRemapMode::Compact)]
7817 #[case(IndexRemapMode::Direct)]
7818 #[tokio::test]
7819 async fn test_inverted_index_remap_after_compaction(#[case] index_remap_mode: IndexRemapMode) {
7820 use arrow_array::cast::AsArray;
7821
7822 let mut dataset = lance_datagen::gen_batch()
7823 .col("id", lance_datagen::array::step::<Int32Type>())
7824 .col("doc", lance_datagen::array::random_sentence(1, 100, false))
7825 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
7826 .await
7827 .unwrap();
7828
7829 dataset
7830 .create_index(
7831 &["doc"],
7832 IndexType::Inverted,
7833 Some("doc_idx".into()),
7834 &InvertedIndexParams::default(),
7835 false,
7836 )
7837 .await
7838 .unwrap();
7839 let original_uuid = dataset
7840 .load_index_by_name("doc_idx")
7841 .await
7842 .unwrap()
7843 .unwrap()
7844 .uuid;
7845
7846 let words: Vec<String> = {
7848 let mut scanner = dataset.scan();
7849 scanner
7850 .project(&["doc"])
7851 .unwrap()
7852 .limit(Some(1), None)
7853 .unwrap();
7854 let batches = scanner
7855 .try_into_stream()
7856 .await
7857 .unwrap()
7858 .try_collect::<Vec<_>>()
7859 .await
7860 .unwrap();
7861 let mut words: Vec<String> = batches[0]["doc"]
7862 .as_string::<i32>()
7863 .value(0)
7864 .split_whitespace()
7865 .map(|s| s.to_string())
7866 .collect();
7867 words.sort();
7868 words.dedup();
7869 words.truncate(3);
7870 words
7871 };
7872 assert!(!words.is_empty(), "sampled document must contain words");
7873
7874 dataset.delete("id % 10 == 0").await.unwrap();
7877
7878 let mut before = Vec::new();
7881 for word in &words {
7882 let mut scanner = dataset.scan();
7883 scanner
7884 .full_text_search(FullTextSearchQuery::new(word.clone()))
7885 .unwrap();
7886 scanner.project::<String>(&[]).unwrap().with_row_id();
7887 before.push(scanner.count_rows().await.unwrap());
7888 }
7889
7890 let options = CompactionOptions {
7893 target_rows_per_fragment: 50_000,
7894 index_remap_mode,
7895 ..Default::default()
7896 };
7897 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
7898 assert!(metrics.fragments_removed > 0);
7899 assert!(metrics.fragments_added > 0);
7900
7901 assert_ne!(
7903 dataset
7904 .load_index_by_name("doc_idx")
7905 .await
7906 .unwrap()
7907 .unwrap()
7908 .uuid,
7909 original_uuid,
7910 "inverted index must be physically remapped inline (mode {index_remap_mode:?})"
7911 );
7912
7913 let mut scanner = dataset.scan();
7915 scanner
7916 .full_text_search(FullTextSearchQuery::new(words[0].clone()))
7917 .unwrap();
7918 scanner.project::<String>(&[]).unwrap().with_row_id();
7919 let plan = scanner.explain_plan(true).await.unwrap();
7920 assert!(
7921 plan.contains("MatchQuery"),
7922 "Expected inverted index scan in plan: {}",
7923 plan
7924 );
7925
7926 for (word, expected) in words.iter().zip(before) {
7929 let mut scanner = dataset.scan();
7930 scanner
7931 .full_text_search(FullTextSearchQuery::new(word.clone()))
7932 .unwrap();
7933 scanner.project::<String>(&[]).unwrap().with_row_id();
7934 assert_eq!(
7935 scanner.count_rows().await.unwrap(),
7936 expected,
7937 "full-text count for {word:?} changed after compaction (mode {index_remap_mode:?})"
7938 );
7939 }
7940 }
7941
7942 #[tokio::test]
7943 async fn test_read_inverted_index_with_defer_index_remap() {
7944 let mut words_gen = lance_datagen::array::random_sentence(1, 100, true);
7946 let doc_col = words_gen
7947 .generate_default(lance_datagen::RowCount::from(6000))
7948 .unwrap();
7949
7950 let batch = RecordBatch::try_new(
7951 Schema::new(vec![Field::new("doc", DataType::LargeUtf8, false)]).into(),
7952 vec![doc_col.clone()],
7953 )
7954 .unwrap();
7955 let schema_ref = batch.schema();
7956 let stream = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema_ref);
7957 let mut dataset = Dataset::write(
7958 stream,
7959 "memory://test/table",
7960 Some(WriteParams {
7961 max_rows_per_file: 1_000, ..Default::default()
7963 }),
7964 )
7965 .await
7966 .unwrap();
7967
7968 let large_string_array = doc_col.as_any().downcast_ref::<LargeStringArray>().unwrap();
7971 let sample_words: Vec<String> = large_string_array
7972 .value(0)
7973 .split_whitespace()
7974 .take(10)
7975 .map(|s| s.to_string())
7976 .collect();
7977 let test_word1 = &sample_words[0];
7978 let test_word2 = &sample_words[1];
7979 let test_word3 = &sample_words[2];
7980
7981 let index_name = Some("doc_idx".into());
7983 dataset
7984 .create_index(
7985 &["doc"],
7986 IndexType::Inverted,
7987 index_name.clone(),
7988 &InvertedIndexParams::default(),
7989 false,
7990 )
7991 .await
7992 .unwrap();
7993 let indices = dataset.load_indices().await.unwrap();
7994 let original_index = indices.iter().find(|idx| idx.name == "doc_idx").unwrap();
7995
7996 let options = CompactionOptions {
7998 target_rows_per_fragment: 2_000,
7999 defer_index_remap: true,
8000 ..Default::default()
8001 };
8002
8003 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
8004 assert!(metrics.fragments_removed > 0);
8005 assert!(metrics.fragments_added > 0);
8006
8007 let Some(current_index) = dataset.load_index_by_name("doc_idx").await.unwrap() else {
8009 panic!("doc index must be available");
8010 };
8011 assert_eq!(current_index.uuid, original_index.uuid);
8012
8013 let mut scanner = dataset.scan();
8015 scanner
8016 .full_text_search(FullTextSearchQuery::new(test_word1.clone()))
8017 .unwrap();
8018 scanner.project::<String>(&[]).unwrap().with_row_id();
8019 let count1 = scanner.count_rows().await.unwrap();
8020 scanner = dataset.scan();
8021 scanner
8022 .full_text_search(FullTextSearchQuery::new(test_word2.clone()))
8023 .unwrap();
8024 scanner.project::<String>(&[]).unwrap().with_row_id();
8025 let count2 = scanner.count_rows().await.unwrap();
8026 scanner = dataset.scan();
8027 scanner
8028 .full_text_search(FullTextSearchQuery::new(test_word3.clone()))
8029 .unwrap();
8030 scanner.project::<String>(&[]).unwrap().with_row_id();
8031 let count3 = scanner.count_rows().await.unwrap();
8032
8033 let mut scanner = dataset.scan();
8035 scanner
8036 .full_text_search(FullTextSearchQuery::new(test_word1.clone()))
8037 .unwrap();
8038 scanner.project::<String>(&[]).unwrap().with_row_id();
8039 let plan = scanner.explain_plan(true).await.unwrap();
8040 assert!(
8041 plan.contains("MatchQuery"),
8042 "Expected inverted index scan in plan: {}",
8043 plan
8044 );
8045 assert!(
8046 !plan.contains("LanceScan"),
8047 "Expected no fragment scan in plan: {}",
8048 plan
8049 );
8050
8051 dataset
8053 .create_index(
8054 &["doc"],
8055 IndexType::Inverted,
8056 index_name.clone(),
8057 &InvertedIndexParams::default(),
8058 true,
8059 )
8060 .await
8061 .unwrap();
8062
8063 let mut scanner = dataset.scan();
8065 scanner
8066 .full_text_search(FullTextSearchQuery::new(test_word1.clone()))
8067 .unwrap();
8068 scanner.project::<String>(&[]).unwrap().with_row_id();
8069 assert_eq!(scanner.count_rows().await.unwrap(), count1);
8070 scanner = dataset.scan();
8071 scanner
8072 .full_text_search(FullTextSearchQuery::new(test_word2.clone()))
8073 .unwrap();
8074 scanner.project::<String>(&[]).unwrap().with_row_id();
8075 assert_eq!(scanner.count_rows().await.unwrap(), count2);
8076 scanner = dataset.scan();
8077 scanner
8078 .full_text_search(FullTextSearchQuery::new(test_word3.clone()))
8079 .unwrap();
8080 scanner.project::<String>(&[]).unwrap().with_row_id();
8081 assert_eq!(scanner.count_rows().await.unwrap(), count3);
8082 }
8083
8084 #[tokio::test]
8092 async fn test_read_inverted_index_with_defer_index_remap_and_deletions() {
8093 const ROWS: i32 = 1200;
8097 const DELETED: i32 = 400;
8098
8099 let ids = Int32Array::from_iter_values(0..ROWS);
8102 let docs = LargeStringArray::from_iter_values((0..ROWS).map(|_| "lance apple orange"));
8103 let batch = RecordBatch::try_new(
8104 Schema::new(vec![
8105 Field::new("id", DataType::Int32, false),
8106 Field::new("doc", DataType::LargeUtf8, false),
8107 ])
8108 .into(),
8109 vec![Arc::new(ids) as ArrayRef, Arc::new(docs) as ArrayRef],
8110 )
8111 .unwrap();
8112 let schema_ref = batch.schema();
8113 let stream = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema_ref);
8114 let mut dataset = Dataset::write(
8115 stream,
8116 "memory://test/table",
8117 Some(WriteParams {
8118 max_rows_per_file: 200, ..Default::default()
8120 }),
8121 )
8122 .await
8123 .unwrap();
8124
8125 dataset
8126 .create_index(
8127 &["doc"],
8128 IndexType::Inverted,
8129 Some("doc_idx".into()),
8130 &InvertedIndexParams::default(),
8131 false,
8132 )
8133 .await
8134 .unwrap();
8135
8136 dataset.delete(&format!("id < {DELETED}")).await.unwrap();
8139 compact_files(
8140 &mut dataset,
8141 CompactionOptions {
8142 target_rows_per_fragment: 2_000,
8143 defer_index_remap: true,
8144 ..Default::default()
8145 },
8146 None,
8147 )
8148 .await
8149 .unwrap();
8150 assert!(
8151 dataset
8152 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
8153 .await
8154 .unwrap()
8155 .is_some(),
8156 "deferred compaction must leave a fragment-reuse index"
8157 );
8158
8159 async fn search_ids(dataset: &Dataset) -> Vec<i32> {
8162 let mut scanner = dataset.scan();
8163 scanner
8164 .full_text_search(FullTextSearchQuery::new("lance".to_owned()))
8165 .unwrap();
8166 scanner.project::<&str>(&["id"]).unwrap();
8167 let batches = scanner
8168 .try_into_stream()
8169 .await
8170 .unwrap()
8171 .try_collect::<Vec<_>>()
8172 .await
8173 .unwrap();
8174 let mut ids: Vec<i32> = batches
8175 .iter()
8176 .flat_map(|b| {
8177 b.column_by_name("id")
8178 .unwrap()
8179 .as_any()
8180 .downcast_ref::<Int32Array>()
8181 .unwrap()
8182 .values()
8183 .to_vec()
8184 })
8185 .collect();
8186 ids.sort_unstable();
8187 ids
8188 }
8189
8190 let expected = (DELETED..ROWS).collect::<Vec<_>>();
8191
8192 let during = search_ids(&dataset).await;
8194 assert_eq!(
8195 during, expected,
8196 "FRI-window FTS must return exactly the surviving rows (no resurrection, no loss, no stale rows)"
8197 );
8198
8199 remapping::remap_column_index(&mut dataset, &["doc"], Some("doc_idx".into()))
8201 .await
8202 .unwrap();
8203 cleanup_frag_reuse_index(&mut dataset).await.unwrap();
8204 let after = search_ids(&dataset).await;
8205 assert_eq!(
8206 after, expected,
8207 "FTS must stay correct after physical remap + fragment-reuse trim"
8208 );
8209 }
8210
8211 #[tokio::test]
8212 async fn test_read_ngram_index_with_defer_index_remap() {
8213 let mut words_gen = lance_datagen::array::random_sentence(1, 100, true);
8215 let doc_col = words_gen
8216 .generate_default(lance_datagen::RowCount::from(6000))
8217 .unwrap();
8218
8219 let batch = RecordBatch::try_new(
8220 Schema::new(vec![Field::new("doc", DataType::LargeUtf8, false)]).into(),
8221 vec![doc_col.clone()],
8222 )
8223 .unwrap();
8224 let schema_ref = batch.schema();
8225 let stream = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema_ref);
8226 let mut dataset = Dataset::write(
8227 stream,
8228 "memory://test/table",
8229 Some(WriteParams {
8230 max_rows_per_file: 1_000, ..Default::default()
8232 }),
8233 )
8234 .await
8235 .unwrap();
8236
8237 let large_string_array = doc_col.as_any().downcast_ref::<LargeStringArray>().unwrap();
8240 let sample_words: Vec<String> = large_string_array
8241 .value(0)
8242 .split_whitespace()
8243 .take(10)
8244 .map(|s| s.to_string())
8245 .collect();
8246 let test_word1 = &sample_words[0];
8247 let test_word2 = &sample_words[1];
8248 let test_word3 = &sample_words[2];
8249
8250 let index_name = Some("doc_idx".into());
8252 dataset
8253 .create_index(
8254 &["doc"],
8255 IndexType::NGram,
8256 index_name.clone(),
8257 &ScalarIndexParams::default(),
8258 false,
8259 )
8260 .await
8261 .unwrap();
8262 let indices = dataset.load_indices().await.unwrap();
8263 let original_index = indices.iter().find(|idx| idx.name == "doc_idx").unwrap();
8264
8265 let count1 = dataset
8267 .count_rows(Some(format!("contains(doc, '{}')", test_word1)))
8268 .await
8269 .unwrap();
8270 let count2 = dataset
8271 .count_rows(Some(format!("contains(doc, '{}')", test_word2)))
8272 .await
8273 .unwrap();
8274 let count3 = dataset
8275 .count_rows(Some(format!("contains(doc, '{}')", test_word3)))
8276 .await
8277 .unwrap();
8278
8279 let options = CompactionOptions {
8281 target_rows_per_fragment: 2_000,
8282 defer_index_remap: true,
8283 ..Default::default()
8284 };
8285
8286 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
8287 assert!(metrics.fragments_removed > 0);
8288 assert!(metrics.fragments_added > 0);
8289
8290 let Some(current_index) = dataset.load_index_by_name("doc_idx").await.unwrap() else {
8292 panic!("doc index must be available");
8293 };
8294 assert_eq!(current_index.uuid, original_index.uuid);
8295
8296 assert_eq!(
8298 dataset
8299 .count_rows(Some(format!("contains(doc, '{}')", test_word1)))
8300 .await
8301 .unwrap(),
8302 count1
8303 );
8304 assert_eq!(
8305 dataset
8306 .count_rows(Some(format!("contains(doc, '{}')", test_word2)))
8307 .await
8308 .unwrap(),
8309 count2
8310 );
8311 assert_eq!(
8312 dataset
8313 .count_rows(Some(format!("contains(doc, '{}')", test_word3)))
8314 .await
8315 .unwrap(),
8316 count3
8317 );
8318
8319 let mut scanner = dataset.scan();
8321 scanner
8322 .filter(&format!("contains(doc, '{}')", test_word1))
8323 .unwrap();
8324 scanner.project::<String>(&[]).unwrap().with_row_id();
8325 let plan = scanner.explain_plan(false).await.unwrap();
8326 assert!(
8327 plan.contains("ScalarIndexQuery: query=[contains(doc, Utf8"),
8328 "Expected scalar index query in plan: {}",
8329 plan
8330 );
8331 }
8332
8333 #[tokio::test]
8334 async fn test_read_label_list_index_with_defer_index_remap() {
8335 let mut dataset = lance_datagen::gen_batch()
8337 .col(
8338 "vec",
8339 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
8340 )
8341 .col(
8342 "labels",
8343 lance_datagen::array::rand_list_any(
8344 lance_datagen::array::cycle::<Int64Type>(vec![1, 2, 3, 4, 5]),
8345 false,
8346 ),
8347 )
8348 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
8349 .await
8350 .unwrap();
8351
8352 let count1 = dataset
8354 .count_rows(Some("array_has_any(labels, [1])".to_owned()))
8355 .await
8356 .unwrap();
8357 let count2 = dataset
8358 .count_rows(Some("array_has_any(labels, [5])".to_owned()))
8359 .await
8360 .unwrap();
8361 let count3 = dataset
8362 .count_rows(Some("array_has_any(labels, [10])".to_owned()))
8363 .await
8364 .unwrap();
8365
8366 let index_name = Some("labels_idx".into());
8368 dataset
8369 .create_index(
8370 &["labels"],
8371 IndexType::LabelList,
8372 index_name.clone(),
8373 &ScalarIndexParams::default(),
8374 false,
8375 )
8376 .await
8377 .unwrap();
8378 let indices = dataset.load_indices().await.unwrap();
8379 let original_index = indices.iter().find(|idx| idx.name == "labels_idx").unwrap();
8380
8381 let options = CompactionOptions {
8383 target_rows_per_fragment: 2000,
8384 defer_index_remap: true,
8385 ..Default::default()
8386 };
8387 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
8388 assert!(metrics.fragments_removed > 0);
8389 assert!(metrics.fragments_added > 0);
8390
8391 let indices = dataset.load_indices().await.unwrap();
8393 let current_index = indices.iter().find(|idx| idx.name == "labels_idx").unwrap();
8394 assert_eq!(current_index.uuid, original_index.uuid);
8395
8396 assert_eq!(
8398 dataset
8399 .count_rows(Some("array_has_any(labels, [1])".to_owned()))
8400 .await
8401 .unwrap(),
8402 count1
8403 );
8404 assert_eq!(
8405 dataset
8406 .count_rows(Some("array_has_any(labels, [5])".to_owned()))
8407 .await
8408 .unwrap(),
8409 count2
8410 );
8411 assert_eq!(
8412 dataset
8413 .count_rows(Some("array_has_any(labels, [10])".to_owned()))
8414 .await
8415 .unwrap(),
8416 count3
8417 );
8418
8419 let mut scanner = dataset.scan();
8421 scanner.filter("array_has_any(labels, [1])").unwrap();
8422 scanner.project::<String>(&[]).unwrap().with_row_id();
8423 let plan = scanner.explain_plan(false).await.unwrap();
8424 assert!(
8425 plan.contains(
8426 "ScalarIndexQuery: query=[array_has_any(labels, List([1]))]@labels_idx(LabelList)",
8427 ),
8428 "Expected scalar index query in plan: {}",
8429 plan
8430 );
8431 }
8432
8433 #[tokio::test]
8434 async fn test_read_ivf_pq_index_v3_with_defer_index_remap() {
8435 let mut dataset = lance_datagen::gen_batch()
8437 .col(
8438 "vec",
8439 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
8440 )
8441 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
8442 .await
8443 .unwrap();
8444
8445 let query_vec1: PrimitiveArray<Float32Type> =
8447 PrimitiveArray::from_iter_values(std::iter::repeat_n(0.0, 128));
8448 let query_vec2: PrimitiveArray<Float32Type> =
8449 PrimitiveArray::from_iter_values(std::iter::repeat_n(1.1, 128));
8450 let query_vec3: PrimitiveArray<Float32Type> =
8451 PrimitiveArray::from_iter_values(std::iter::repeat_n(2.2, 128));
8452
8453 let mut scanner = dataset.scan();
8455 scanner.nearest("vec", &query_vec1, 10).unwrap();
8456 scanner.project::<String>(&[]).unwrap().with_row_id();
8457 let results1 = scanner
8458 .try_into_stream()
8459 .await
8460 .unwrap()
8461 .try_collect::<Vec<_>>()
8462 .await
8463 .unwrap();
8464 let count1 = results1.len();
8465
8466 scanner = dataset.scan();
8467 scanner.nearest("vec", &query_vec2, 10).unwrap();
8468 scanner.project::<String>(&[]).unwrap().with_row_id();
8469 let results2 = scanner
8470 .try_into_stream()
8471 .await
8472 .unwrap()
8473 .try_collect::<Vec<_>>()
8474 .await
8475 .unwrap();
8476 let count2 = results2.len();
8477
8478 scanner = dataset.scan();
8479 scanner.nearest("vec", &query_vec3, 10).unwrap();
8480 scanner.project::<String>(&[]).unwrap().with_row_id();
8481 let results3 = scanner
8482 .try_into_stream()
8483 .await
8484 .unwrap()
8485 .try_collect::<Vec<_>>()
8486 .await
8487 .unwrap();
8488 let count3 = results3.len();
8489
8490 let index_name = Some("vec_idx".into());
8492 dataset
8493 .create_index(
8494 &["vec"],
8495 IndexType::Vector,
8496 index_name.clone(),
8497 &VectorIndexParams {
8498 metric_type: DistanceType::L2,
8499 stages: vec![
8500 StageParams::Ivf(IvfBuildParams {
8501 max_iters: 2,
8502 num_partitions: Some(2),
8503 sample_rate: 2,
8504 ..Default::default()
8505 }),
8506 StageParams::PQ(PQBuildParams {
8507 max_iters: 2,
8508 num_sub_vectors: 2,
8509 ..Default::default()
8510 }),
8511 ],
8512 version: crate::index::vector::IndexFileVersion::V3,
8513 skip_transpose: false,
8514 runtime_hints: Default::default(),
8515 },
8516 false,
8517 )
8518 .await
8519 .unwrap();
8520 let indices = dataset.load_indices().await.unwrap();
8521 let original_index = indices.iter().find(|idx| idx.name == "vec_idx").unwrap();
8522
8523 let options = CompactionOptions {
8525 target_rows_per_fragment: 2_000,
8526 defer_index_remap: true,
8527 ..Default::default()
8528 };
8529
8530 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
8531 assert!(metrics.fragments_removed > 0);
8532 assert!(metrics.fragments_added > 0);
8533
8534 let Some(current_index) = dataset.load_index_by_name("vec_idx").await.unwrap() else {
8536 panic!("vec index must be available");
8537 };
8538 assert_eq!(current_index.uuid, original_index.uuid);
8539
8540 let mut scanner = dataset.scan();
8542 scanner.nearest("vec", &query_vec1, 10).unwrap();
8543 scanner.project::<String>(&[]).unwrap().with_row_id();
8544 let new_results1 = scanner
8545 .try_into_stream()
8546 .await
8547 .unwrap()
8548 .try_collect::<Vec<_>>()
8549 .await
8550 .unwrap();
8551 assert_eq!(new_results1.len(), count1);
8552
8553 scanner = dataset.scan();
8554 scanner.nearest("vec", &query_vec2, 10).unwrap();
8555 scanner.project::<String>(&[]).unwrap().with_row_id();
8556 let new_results2 = scanner
8557 .try_into_stream()
8558 .await
8559 .unwrap()
8560 .try_collect::<Vec<_>>()
8561 .await
8562 .unwrap();
8563 assert_eq!(new_results2.len(), count2);
8564
8565 scanner = dataset.scan();
8566 scanner.nearest("vec", &query_vec3, 10).unwrap();
8567 scanner.project::<String>(&[]).unwrap().with_row_id();
8568 let new_results3 = scanner
8569 .try_into_stream()
8570 .await
8571 .unwrap()
8572 .try_collect::<Vec<_>>()
8573 .await
8574 .unwrap();
8575 assert_eq!(new_results3.len(), count3);
8576
8577 let mut scanner = dataset.scan();
8579 scanner.nearest("vec", &query_vec1, 10).unwrap();
8580 scanner.project::<String>(&[]).unwrap().with_row_id();
8581 let plan = scanner.explain_plan(false).await.unwrap();
8582 assert!(
8583 plan.contains("ANNSubIndex"),
8584 "Expected vector index scan in plan: {}",
8585 plan
8586 );
8587 assert!(
8588 !plan.contains("LanceScan"),
8589 "Expected no fragment scan in plan: {}",
8590 plan
8591 );
8592 }
8593
8594 #[tokio::test]
8595 async fn test_read_ivf_rq_index_v3_with_defer_index_remap() {
8596 use arrow_array::cast::AsArray;
8597 use lance_index::vector::bq::RQBuildParams;
8598
8599 let mut dataset = lance_datagen::gen_batch()
8600 .col(
8601 "vec",
8602 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(128)),
8603 )
8604 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
8605 .await
8606 .unwrap();
8607
8608 let stored: Vec<Vec<f32>> = {
8609 let mut scanner = dataset.scan();
8610 scanner.project(&["vec"]).unwrap();
8611 let batches = scanner
8612 .try_into_stream()
8613 .await
8614 .unwrap()
8615 .try_collect::<Vec<_>>()
8616 .await
8617 .unwrap();
8618 let mut out = Vec::new();
8619 for batch in &batches {
8620 let vecs = batch["vec"].as_fixed_size_list();
8621 for i in 0..batch.num_rows() {
8622 let values = vecs.value(i);
8623 let values = values.as_primitive::<Float32Type>();
8624 out.push(values.values().to_vec());
8625 }
8626 }
8627 out
8628 };
8629
8630 let index_name = Some("vec_idx".into());
8631 dataset
8632 .create_index(
8633 &["vec"],
8634 IndexType::Vector,
8635 index_name.clone(),
8636 &VectorIndexParams {
8637 metric_type: DistanceType::L2,
8638 stages: vec![
8639 StageParams::Ivf(IvfBuildParams {
8640 max_iters: 2,
8641 num_partitions: Some(2),
8642 sample_rate: 2,
8643 ..Default::default()
8644 }),
8645 StageParams::RQ(RQBuildParams::new(1)),
8646 ],
8647 version: crate::index::vector::IndexFileVersion::V3,
8648 skip_transpose: false,
8649 runtime_hints: Default::default(),
8650 },
8651 false,
8652 )
8653 .await
8654 .unwrap();
8655 let indices = dataset.load_indices().await.unwrap();
8656 let original_index = indices.iter().find(|idx| idx.name == "vec_idx").unwrap();
8657
8658 let options = CompactionOptions {
8659 target_rows_per_fragment: 2_000,
8660 defer_index_remap: true,
8661 ..Default::default()
8662 };
8663 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
8664 assert!(metrics.fragments_removed > 0);
8665 assert!(metrics.fragments_added > 0);
8666
8667 let Some(current_index) = dataset.load_index_by_name("vec_idx").await.unwrap() else {
8668 panic!("vec index must be available");
8669 };
8670 assert_eq!(current_index.uuid, original_index.uuid);
8671
8672 let frag_reuse_present = dataset
8673 .load_indices()
8674 .await
8675 .unwrap()
8676 .iter()
8677 .any(|idx| idx.name == FRAG_REUSE_INDEX_NAME);
8678 assert!(
8679 frag_reuse_present,
8680 "defer_index_remap must record a {} index",
8681 FRAG_REUSE_INDEX_NAME
8682 );
8683
8684 let sample_step = (stored.len() / 8).max(1);
8685 let mut checked = 0;
8686 for query in stored.iter().step_by(sample_step) {
8687 let query_vec = PrimitiveArray::<Float32Type>::from_iter_values(query.iter().copied());
8688 let mut scanner = dataset.scan();
8689 scanner.nearest("vec", &query_vec, 5).unwrap();
8690 scanner.project(&["vec"]).unwrap().with_row_id();
8691 let batches = scanner
8692 .try_into_stream()
8693 .await
8694 .unwrap()
8695 .try_collect::<Vec<_>>()
8696 .await
8697 .unwrap();
8698 assert!(!batches.is_empty(), "query returned no batches");
8699 let top = &batches[0];
8700 assert!(top.num_rows() > 0, "query returned empty top batch");
8701 let top_vec = top["vec"].as_fixed_size_list().value(0);
8702 let top_vec = top_vec.as_primitive::<Float32Type>();
8703 assert_eq!(
8704 top_vec.values(),
8705 query.as_slice(),
8706 "top-1 self-recall returned a different vector than the query"
8707 );
8708 checked += 1;
8709 }
8710 assert!(checked > 0, "expected to check at least one stored vector");
8711 }
8712
8713 async fn vector_knn_ids(dataset: &Dataset, query: &[f32], k: usize) -> Vec<i32> {
8727 use arrow_array::cast::AsArray;
8728 use arrow_array::types::{Float32Type, Int32Type};
8729 let qa = PrimitiveArray::<Float32Type>::from_iter_values(query.iter().copied());
8730 let mut scanner = dataset.scan();
8731 scanner.nearest("vec", &qa, k).unwrap();
8732 scanner.project(&["id"]).unwrap();
8733 let batches = scanner
8734 .try_into_stream()
8735 .await
8736 .unwrap()
8737 .try_collect::<Vec<_>>()
8738 .await
8739 .unwrap();
8740 let mut ids = Vec::new();
8741 for b in &batches {
8742 ids.extend(b["id"].as_primitive::<Int32Type>().values().iter().copied());
8743 }
8744 ids
8745 }
8746
8747 async fn check_vector_defer_compaction(
8748 params: VectorIndexParams,
8749 delete_predicate: Option<&str>,
8750 k: usize,
8751 min_overlap: usize,
8752 ) {
8753 use arrow_array::cast::AsArray;
8754 use arrow_array::types::{Float32Type, Int32Type};
8755 use lance_datagen::Dimension;
8756
8757 const DIM: u32 = 32;
8758 let mut dataset = lance_datagen::gen_batch()
8759 .col("id", lance_datagen::array::step::<Int32Type>())
8760 .col(
8761 "vec",
8762 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(DIM)),
8763 )
8764 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
8765 .await
8766 .unwrap();
8767
8768 dataset
8769 .create_index(
8770 &["vec"],
8771 IndexType::Vector,
8772 Some("vec_idx".into()),
8773 ¶ms,
8774 false,
8775 )
8776 .await
8777 .unwrap();
8778 let original_uuid = dataset
8779 .load_index_by_name("vec_idx")
8780 .await
8781 .unwrap()
8782 .unwrap()
8783 .uuid;
8784
8785 if let Some(pred) = delete_predicate {
8786 dataset.delete(pred).await.unwrap();
8787 }
8788
8789 let mut survivors: Vec<(i32, Vec<f32>)> = Vec::new();
8791 {
8792 let mut scanner = dataset.scan();
8793 scanner.project(&["id", "vec"]).unwrap();
8794 let batches = scanner
8795 .try_into_stream()
8796 .await
8797 .unwrap()
8798 .try_collect::<Vec<_>>()
8799 .await
8800 .unwrap();
8801 for batch in &batches {
8802 let ids = batch["id"].as_primitive::<Int32Type>();
8803 let vecs = batch["vec"].as_fixed_size_list();
8804 for i in 0..batch.num_rows() {
8805 let v = vecs.value(i);
8806 let v = v.as_primitive::<Float32Type>().values().to_vec();
8807 survivors.push((ids.value(i), v));
8808 }
8809 }
8810 }
8811 assert!(!survivors.is_empty());
8812 let surviving_ids: std::collections::HashSet<i32> =
8813 survivors.iter().map(|(id, _)| *id).collect();
8814
8815 let step = (survivors.len() / 16).max(1);
8817 let queries: Vec<(i32, Vec<f32>)> = survivors.iter().step_by(step).cloned().collect();
8818 let mut baseline: Vec<Vec<i32>> = Vec::new();
8819 for (_, q) in &queries {
8820 baseline.push(vector_knn_ids(&dataset, q, k).await);
8821 }
8822
8823 let metrics = compact_files(
8825 &mut dataset,
8826 CompactionOptions {
8827 target_rows_per_fragment: 2_000,
8828 defer_index_remap: true,
8829 ..Default::default()
8830 },
8831 None,
8832 )
8833 .await
8834 .unwrap();
8835 assert!(metrics.fragments_removed > 0);
8836 assert!(
8837 dataset
8838 .load_indices()
8839 .await
8840 .unwrap()
8841 .iter()
8842 .any(|idx| idx.name == FRAG_REUSE_INDEX_NAME),
8843 "deferred compaction must record a frag-reuse index"
8844 );
8845 assert_eq!(
8846 dataset
8847 .load_index_by_name("vec_idx")
8848 .await
8849 .unwrap()
8850 .unwrap()
8851 .uuid,
8852 original_uuid,
8853 "index must not be physically remapped yet (FRI window)"
8854 );
8855
8856 for (i, (_, q)) in queries.iter().enumerate() {
8858 let after = vector_knn_ids(&dataset, q, k).await;
8859 for id in &after {
8860 assert!(
8861 surviving_ids.contains(id),
8862 "KNN returned id {id} that is not a surviving row (query #{i})"
8863 );
8864 }
8865 let overlap = after.iter().filter(|id| baseline[i].contains(id)).count();
8866 assert!(
8867 overlap >= min_overlap,
8868 "KNN top-{k} diverged after deferred compaction: overlap {overlap} < {min_overlap} (query #{i})"
8869 );
8870 }
8871 }
8872
8873 fn small_ivf() -> lance_index::vector::ivf::IvfBuildParams {
8874 lance_index::vector::ivf::IvfBuildParams {
8875 max_iters: 2,
8876 num_partitions: Some(2),
8877 sample_rate: 2,
8878 ..Default::default()
8879 }
8880 }
8881
8882 #[tokio::test]
8883 async fn test_ivf_flat_defer_compaction_with_deletions() {
8884 let params = VectorIndexParams::with_ivf_flat_params(DistanceType::L2, small_ivf());
8885 Box::pin(check_vector_defer_compaction(
8888 params,
8889 Some("id < 1500"),
8890 10,
8891 10,
8892 ))
8893 .await;
8894 }
8895
8896 #[tokio::test]
8897 async fn test_ivf_hnsw_sq_defer_compaction_merge_only() {
8898 use lance_index::vector::{hnsw::builder::HnswBuildParams, sq::builder::SQBuildParams};
8899 let params = VectorIndexParams::with_ivf_hnsw_sq_params(
8900 DistanceType::L2,
8901 small_ivf(),
8902 HnswBuildParams::default(),
8903 SQBuildParams::default(),
8904 );
8905 Box::pin(check_vector_defer_compaction(params, None, 10, 9)).await;
8908 }
8909
8910 #[tokio::test]
8917 async fn test_ivf_pq_defer_compaction_with_deletions() {
8918 use lance_index::vector::pq::PQBuildParams;
8919 let params = VectorIndexParams::with_ivf_pq_params(
8920 DistanceType::L2,
8921 small_ivf(),
8922 PQBuildParams {
8923 max_iters: 2,
8924 num_sub_vectors: 2,
8925 ..Default::default()
8926 },
8927 );
8928 Box::pin(check_vector_defer_compaction(
8930 params,
8931 Some("id < 1500"),
8932 10,
8933 8,
8934 ))
8935 .await;
8936 }
8937
8938 #[tokio::test]
8939 async fn test_ivf_sq_defer_compaction_with_deletions() {
8940 use lance_index::vector::sq::builder::SQBuildParams;
8941 let params = VectorIndexParams::with_ivf_sq_params(
8942 DistanceType::L2,
8943 small_ivf(),
8944 SQBuildParams::default(),
8945 );
8946 Box::pin(check_vector_defer_compaction(
8948 params,
8949 Some("id < 1500"),
8950 10,
8951 8,
8952 ))
8953 .await;
8954 }
8955
8956 #[tokio::test]
8957 async fn test_ivf_rq_defer_compaction_with_deletions() {
8958 use lance_index::vector::bq::RQBuildParams;
8959 let params = VectorIndexParams::with_ivf_rq_params(
8960 DistanceType::L2,
8961 small_ivf(),
8962 RQBuildParams::new(1),
8963 );
8964 Box::pin(check_vector_defer_compaction(
8966 params,
8967 Some("id < 1500"),
8968 10,
8969 8,
8970 ))
8971 .await;
8972 }
8973
8974 async fn check_vector_remap_and_trim(
8980 params: VectorIndexParams,
8981 k: usize,
8982 window_overlap: usize,
8983 post_remap_overlap: Option<usize>,
8984 ) {
8985 use arrow_array::cast::AsArray;
8986 use arrow_array::types::{Float32Type, Int32Type};
8987 use lance_datagen::Dimension;
8988
8989 const DIM: u32 = 8;
8990 let mut dataset = lance_datagen::gen_batch()
8991 .col("id", lance_datagen::array::step::<Int32Type>())
8992 .col(
8993 "vec",
8994 lance_datagen::array::rand_vec::<Float32Type>(Dimension::from(DIM)),
8995 )
8996 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(64))
8997 .await
8998 .unwrap();
8999 dataset
9000 .create_index(
9001 &["vec"],
9002 IndexType::Vector,
9003 Some("vec_idx".into()),
9004 ¶ms,
9005 false,
9006 )
9007 .await
9008 .unwrap();
9009 let original_uuid = dataset
9010 .load_index_by_name("vec_idx")
9011 .await
9012 .unwrap()
9013 .unwrap()
9014 .uuid;
9015
9016 let mut rows: Vec<Vec<f32>> = Vec::new();
9018 {
9019 let mut scanner = dataset.scan();
9020 scanner.project(&["vec"]).unwrap();
9021 let batches = scanner
9022 .try_into_stream()
9023 .await
9024 .unwrap()
9025 .try_collect::<Vec<_>>()
9026 .await
9027 .unwrap();
9028 for batch in &batches {
9029 let vecs = batch["vec"].as_fixed_size_list();
9030 for i in 0..batch.num_rows() {
9031 let v = vecs.value(i);
9032 rows.push(v.as_primitive::<Float32Type>().values().to_vec());
9033 }
9034 }
9035 }
9036 let step = (rows.len() / 4).max(1);
9037 let queries: Vec<Vec<f32>> = rows.iter().step_by(step).cloned().collect();
9038 let mut baseline: Vec<Vec<i32>> = Vec::new();
9039 for q in &queries {
9040 baseline.push(vector_knn_ids(&dataset, q, k).await);
9041 }
9042
9043 let metrics = compact_files(
9045 &mut dataset,
9046 CompactionOptions {
9047 target_rows_per_fragment: 128,
9048 defer_index_remap: true,
9049 ..Default::default()
9050 },
9051 None,
9052 )
9053 .await
9054 .unwrap();
9055 assert!(metrics.fragments_removed > 0);
9056 assert_eq!(
9057 dataset
9058 .load_index_by_name("vec_idx")
9059 .await
9060 .unwrap()
9061 .unwrap()
9062 .uuid,
9063 original_uuid,
9064 "index must not be physically remapped yet (FRI window)"
9065 );
9066 for (i, q) in queries.iter().enumerate() {
9067 let window = vector_knn_ids(&dataset, q, k).await;
9068 let overlap = window.iter().filter(|id| baseline[i].contains(id)).count();
9069 assert!(
9070 overlap >= window_overlap,
9071 "FRI-window KNN diverged: overlap {overlap} < {window_overlap} (query #{i})"
9072 );
9073 }
9074
9075 remapping::remap_column_index(&mut dataset, &["vec"], Some("vec_idx".into()))
9077 .await
9078 .unwrap();
9079 cleanup_frag_reuse_index(&mut dataset).await.unwrap();
9080
9081 let remapped_uuid = dataset
9082 .load_index_by_name("vec_idx")
9083 .await
9084 .unwrap()
9085 .unwrap()
9086 .uuid;
9087 assert_ne!(
9088 remapped_uuid, original_uuid,
9089 "index should have been physically remapped"
9090 );
9091 if let Some(meta) = dataset
9092 .load_index_by_name(FRAG_REUSE_INDEX_NAME)
9093 .await
9094 .unwrap()
9095 {
9096 let versions = load_frag_reuse_index_details(&dataset, &meta)
9097 .await
9098 .unwrap()
9099 .versions
9100 .len();
9101 assert_eq!(versions, 0, "frag-reuse index must trim to zero versions");
9102 }
9103
9104 for (i, q) in queries.iter().enumerate() {
9105 let after = vector_knn_ids(&dataset, q, k).await;
9106 assert!(
9108 !after.is_empty(),
9109 "post-remap KNN returned no rows (query #{i})"
9110 );
9111 if let Some(min_overlap) = post_remap_overlap {
9114 let overlap = after.iter().filter(|id| baseline[i].contains(id)).count();
9115 assert!(
9116 overlap >= min_overlap,
9117 "post-remap KNN diverged: overlap {overlap} < {min_overlap} (query #{i})"
9118 );
9119 }
9120 }
9121 }
9122
9123 #[tokio::test]
9124 async fn test_ivf_flat_remap_and_trim() {
9125 let params = VectorIndexParams::with_ivf_flat_params(DistanceType::L2, small_ivf());
9126 Box::pin(check_vector_remap_and_trim(params, 10, 8, Some(8))).await;
9128 }
9129
9130 #[tokio::test]
9137 async fn test_ivf_pq_remap_and_trim() {
9138 use lance_index::vector::pq::PQBuildParams;
9139 let params = VectorIndexParams::with_ivf_pq_params(
9140 DistanceType::L2,
9141 small_ivf(),
9142 PQBuildParams {
9143 max_iters: 2,
9144 num_sub_vectors: 2,
9145 ..Default::default()
9146 },
9147 );
9148 Box::pin(check_vector_remap_and_trim(params, 10, 8, Some(8))).await;
9150 }
9151
9152 #[tokio::test]
9153 async fn test_ivf_sq_remap_and_trim() {
9154 use lance_index::vector::sq::builder::SQBuildParams;
9155 let params = VectorIndexParams::with_ivf_sq_params(
9156 DistanceType::L2,
9157 small_ivf(),
9158 SQBuildParams::default(),
9159 );
9160 Box::pin(check_vector_remap_and_trim(params, 10, 8, Some(8))).await;
9162 }
9163
9164 #[tokio::test]
9165 async fn test_ivf_rq_remap_and_trim() {
9166 use lance_index::vector::bq::RQBuildParams;
9167 let params = VectorIndexParams::with_ivf_rq_params(
9168 DistanceType::L2,
9169 small_ivf(),
9170 RQBuildParams::new(1),
9171 );
9172 Box::pin(check_vector_remap_and_trim(params, 10, 8, Some(8))).await;
9174 }
9175
9176 #[tokio::test]
9177 async fn test_ivf_hnsw_sq_remap_and_trim() {
9178 use lance_index::vector::{hnsw::builder::HnswBuildParams, sq::builder::SQBuildParams};
9179 let params = VectorIndexParams::with_ivf_hnsw_sq_params(
9180 DistanceType::L2,
9181 small_ivf(),
9182 HnswBuildParams::default(),
9183 SQBuildParams::default(),
9184 );
9185 Box::pin(check_vector_remap_and_trim(params, 10, 7, None)).await;
9188 }
9189
9190 #[tokio::test]
9191 async fn test_ivf_hnsw_pq_remap_and_trim() {
9192 use lance_index::vector::{hnsw::builder::HnswBuildParams, pq::PQBuildParams};
9193 let params = VectorIndexParams::with_ivf_hnsw_pq_params(
9194 DistanceType::L2,
9195 small_ivf(),
9196 HnswBuildParams::default()
9197 .max_level(2)
9198 .num_edges(4)
9199 .ef_construction(16),
9200 PQBuildParams {
9201 max_iters: 2,
9202 num_sub_vectors: 2,
9203 num_bits: 4,
9204 sample_rate: 2,
9205 ..Default::default()
9206 },
9207 );
9208 Box::pin(check_vector_remap_and_trim(params, 10, 7, None)).await;
9210 }
9211
9212 #[tokio::test]
9222 async fn test_bitmap_index_defer_compaction_with_deletions() {
9223 use arrow_array::cast::AsArray;
9224 use arrow_array::types::Int32Type;
9225 let mut dataset = lance_datagen::gen_batch()
9226 .col("id", lance_datagen::array::step::<Int32Type>())
9227 .col(
9228 "category",
9229 lance_datagen::array::cycle::<Int32Type>(vec![1, 2, 3]),
9230 )
9231 .into_ram_dataset(FragmentCount::from(6), FragmentRowCount::from(1000))
9232 .await
9233 .unwrap();
9234 dataset
9235 .create_index(
9236 &["category"],
9237 IndexType::Bitmap,
9238 Some("category_idx".into()),
9239 &ScalarIndexParams::default(),
9240 false,
9241 )
9242 .await
9243 .unwrap();
9244 dataset.delete("id < 1500").await.unwrap();
9245 let metrics = compact_files(
9246 &mut dataset,
9247 CompactionOptions {
9248 target_rows_per_fragment: 2_000,
9249 defer_index_remap: true,
9250 ..Default::default()
9251 },
9252 None,
9253 )
9254 .await
9255 .unwrap();
9256 assert!(metrics.fragments_removed > 0);
9257 assert!(
9258 dataset
9259 .load_indices()
9260 .await
9261 .unwrap()
9262 .iter()
9263 .any(|idx| idx.name == FRAG_REUSE_INDEX_NAME),
9264 "deferred compaction must record a frag-reuse index"
9265 );
9266
9267 let mut scanner = dataset.scan();
9268 scanner.filter("category = 3").unwrap();
9269 scanner.project(&["id"]).unwrap();
9270 let batches = scanner
9271 .try_into_stream()
9272 .await
9273 .unwrap()
9274 .try_collect::<Vec<_>>()
9275 .await
9276 .unwrap();
9277 let mut returned = 0;
9278 for b in &batches {
9279 for id in b["id"].as_primitive::<Int32Type>().values() {
9280 assert!(
9281 *id >= 1500,
9282 "bitmap returned deleted id {id} in the FRI window"
9283 );
9284 returned += 1;
9285 }
9286 }
9287 assert!(returned > 0, "expected surviving category=3 rows");
9288 }
9289
9290 #[tokio::test]
9297 async fn test_default_compaction_planner() {
9298 let test_dir = TempStrDir::default();
9299 let test_uri = &test_dir;
9300
9301 let data = sample_data();
9302 let schema = data.schema();
9303
9304 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], schema.clone());
9306 let write_params = WriteParams {
9307 max_rows_per_file: 2000,
9308 ..Default::default()
9309 };
9310 let dataset = Dataset::write(reader, test_uri, Some(write_params))
9311 .await
9312 .unwrap();
9313
9314 assert_eq!(dataset.get_fragments().len(), 5);
9315
9316 let options = CompactionOptions {
9318 target_rows_per_fragment: 5000,
9319 materialize_deletions_threshold: 2.0,
9320 ..Default::default()
9321 };
9322
9323 let planner = DefaultCompactionPlanner::new(options).unwrap();
9324 let plan = planner.plan(&dataset).await.unwrap();
9325
9326 assert!(!plan.tasks.is_empty());
9328 assert_eq!(plan.read_version, dataset.manifest.version);
9329 assert!(!plan.options.materialize_deletions);
9331 }
9332
9333 #[test]
9334 fn test_from_dataset_config() {
9335 let config = HashMap::from([
9336 (
9337 "lance.compaction.target_rows_per_fragment".to_string(),
9338 "500000".to_string(),
9339 ),
9340 (
9341 "lance.compaction.max_rows_per_group".to_string(),
9342 "2048".to_string(),
9343 ),
9344 (
9345 "lance.compaction.max_bytes_per_file".to_string(),
9346 "1000000".to_string(),
9347 ),
9348 (
9349 "lance.compaction.materialize_deletions".to_string(),
9350 "false".to_string(),
9351 ),
9352 (
9353 "lance.compaction.materialize_deletions_threshold".to_string(),
9354 "0.25".to_string(),
9355 ),
9356 (
9357 "lance.compaction.defer_index_remap".to_string(),
9358 "true".to_string(),
9359 ),
9360 (
9361 "lance.compaction.batch_size".to_string(),
9362 "4096".to_string(),
9363 ),
9364 (
9365 "lance.compaction.io_buffer_size".to_string(),
9366 "1073741824".to_string(),
9367 ),
9368 (
9369 "lance.compaction.compaction_mode".to_string(),
9370 "try_binary_copy".to_string(),
9371 ),
9372 (
9373 "lance.compaction.binary_copy_read_batch_bytes".to_string(),
9374 "8388608".to_string(),
9375 ),
9376 (
9377 "lance.compaction.index_remap_mode".to_string(),
9378 "compact".to_string(),
9379 ),
9380 (
9381 "lance.compaction.max_source_fragments".to_string(),
9382 "20".to_string(),
9383 ),
9384 (
9385 "lance.compaction.max_source_rows".to_string(),
9386 "1000000".to_string(),
9387 ),
9388 (
9389 "lance.compaction.max_source_bytes".to_string(),
9390 "1073741824".to_string(),
9391 ),
9392 ]);
9393
9394 let opts = CompactionOptions::from_dataset_config(&config).unwrap();
9395 assert_eq!(opts.target_rows_per_fragment, 500_000);
9396 assert_eq!(opts.max_rows_per_group, 2048);
9397 assert_eq!(opts.max_bytes_per_file, Some(1_000_000));
9398 assert!(!opts.materialize_deletions);
9399 assert!((opts.materialize_deletions_threshold - 0.25).abs() < f32::EPSILON);
9400 assert!(opts.defer_index_remap);
9401 assert_eq!(opts.batch_size, Some(4096));
9402 assert_eq!(opts.io_buffer_size, Some(1_073_741_824));
9403 assert_eq!(opts.compaction_mode, Some(CompactionMode::TryBinaryCopy));
9404 assert_eq!(opts.binary_copy_read_batch_bytes, Some(8_388_608));
9405 assert_eq!(opts.index_remap_mode, IndexRemapMode::Compact);
9407 assert_eq!(opts.max_source_fragments, Some(20));
9408 assert_eq!(opts.max_source_rows, Some(1_000_000));
9409 assert_eq!(opts.max_source_bytes, Some(1_073_741_824));
9410 }
9411
9412 #[test]
9413 fn test_from_dataset_config_empty() {
9414 let config = HashMap::new();
9415 let opts = CompactionOptions::from_dataset_config(&config).unwrap();
9416 let defaults = CompactionOptions::default();
9417 assert_eq!(
9418 opts.target_rows_per_fragment,
9419 defaults.target_rows_per_fragment
9420 );
9421 assert_eq!(opts.max_rows_per_group, defaults.max_rows_per_group);
9422 assert_eq!(opts.max_bytes_per_file, defaults.max_bytes_per_file);
9423 assert_eq!(opts.materialize_deletions, defaults.materialize_deletions);
9424 assert_eq!(
9425 opts.materialize_deletions_threshold,
9426 defaults.materialize_deletions_threshold
9427 );
9428 assert_eq!(opts.defer_index_remap, defaults.defer_index_remap);
9429 assert_eq!(opts.index_remap_mode, defaults.index_remap_mode);
9430 assert_eq!(opts.index_remap_mode, IndexRemapMode::Direct);
9431 assert_eq!(opts.batch_size, defaults.batch_size);
9432 assert_eq!(opts.compaction_mode, defaults.compaction_mode);
9433 assert_eq!(
9434 opts.binary_copy_read_batch_bytes,
9435 defaults.binary_copy_read_batch_bytes
9436 );
9437 }
9438
9439 #[test]
9440 fn test_from_dataset_config_partial() {
9441 let config = HashMap::from([(
9442 "lance.compaction.target_rows_per_fragment".to_string(),
9443 "500000".to_string(),
9444 )]);
9445
9446 let opts = CompactionOptions::from_dataset_config(&config).unwrap();
9447 assert_eq!(opts.target_rows_per_fragment, 500_000);
9448 let defaults = CompactionOptions::default();
9450 assert_eq!(opts.max_rows_per_group, defaults.max_rows_per_group);
9451 assert_eq!(opts.max_bytes_per_file, defaults.max_bytes_per_file);
9452 assert_eq!(opts.materialize_deletions, defaults.materialize_deletions);
9453 assert_eq!(opts.defer_index_remap, defaults.defer_index_remap);
9454 assert_eq!(opts.batch_size, defaults.batch_size);
9455 assert_eq!(opts.compaction_mode, defaults.compaction_mode);
9456 assert_eq!(
9457 opts.binary_copy_read_batch_bytes,
9458 defaults.binary_copy_read_batch_bytes
9459 );
9460 }
9461
9462 #[test]
9463 fn test_from_dataset_config_ignores_other_keys() {
9464 let config = HashMap::from([
9465 (
9466 "lance.compaction.target_rows_per_fragment".to_string(),
9467 "500000".to_string(),
9468 ),
9469 (
9470 "lance.auto_cleanup.interval".to_string(),
9471 "3600".to_string(),
9472 ),
9473 ("some.other.key".to_string(), "value".to_string()),
9474 ]);
9475
9476 let opts = CompactionOptions::from_dataset_config(&config).unwrap();
9477 assert_eq!(opts.target_rows_per_fragment, 500_000);
9478 }
9479
9480 #[test]
9481 fn test_from_dataset_config_invalid_value() {
9482 let config = HashMap::from([(
9483 "lance.compaction.target_rows_per_fragment".to_string(),
9484 "not_a_number".to_string(),
9485 )]);
9486
9487 let result = CompactionOptions::from_dataset_config(&config);
9488 let err_msg = result.unwrap_err().to_string();
9489 assert!(err_msg.contains("target_rows_per_fragment"));
9490 assert!(err_msg.contains("not_a_number"));
9491 }
9492
9493 #[test]
9494 fn test_from_dataset_config_invalid_bool() {
9495 let config = HashMap::from([(
9496 "lance.compaction.materialize_deletions".to_string(),
9497 "yes".to_string(),
9498 )]);
9499
9500 let result = CompactionOptions::from_dataset_config(&config);
9501 let err_msg = result.unwrap_err().to_string();
9502 assert!(err_msg.contains("materialize_deletions"));
9503 assert!(err_msg.contains("yes"));
9504 }
9505
9506 #[test]
9507 fn test_from_dataset_config_unknown_compaction_key() {
9508 let config = HashMap::from([(
9510 "lance.compaction.unknown_key".to_string(),
9511 "value".to_string(),
9512 )]);
9513
9514 let opts = CompactionOptions::from_dataset_config(&config).unwrap();
9515 let defaults = CompactionOptions::default();
9517 assert_eq!(
9518 opts.target_rows_per_fragment,
9519 defaults.target_rows_per_fragment
9520 );
9521 }
9522
9523 #[test]
9524 fn test_from_dataset_config_invalid_compaction_mode() {
9525 let config = HashMap::from([(
9526 "lance.compaction.compaction_mode".to_string(),
9527 "invalid_mode".to_string(),
9528 )]);
9529
9530 let result = CompactionOptions::from_dataset_config(&config);
9531 let err_msg = result.unwrap_err().to_string();
9532 assert!(err_msg.contains("invalid_mode"));
9533 }
9534
9535 #[test]
9536 fn test_from_dataset_config_max_overlays_per_fragment() {
9537 let key = "lance.compaction.max_overlays_per_fragment".to_string();
9538
9539 let config = HashMap::from([(key.clone(), "3".to_string())]);
9541 let opts = CompactionOptions::from_dataset_config(&config).unwrap();
9542 assert_eq!(opts.max_overlays_per_fragment, Some(3));
9543
9544 let config = HashMap::from([(key.clone(), "None".to_string())]);
9546 let opts = CompactionOptions::from_dataset_config(&config).unwrap();
9547 assert_eq!(opts.max_overlays_per_fragment, None);
9548
9549 let config = HashMap::from([(key, "not_a_number".to_string())]);
9551 let err_msg = CompactionOptions::from_dataset_config(&config)
9552 .unwrap_err()
9553 .to_string();
9554 assert!(err_msg.contains("max_overlays_per_fragment"));
9555 assert!(err_msg.contains("not_a_number"));
9556 }
9557
9558 #[test]
9559 fn test_apply_dataset_config_overrides() {
9560 let config = HashMap::from([(
9561 "lance.compaction.target_rows_per_fragment".to_string(),
9562 "500000".to_string(),
9563 )]);
9564
9565 let mut opts = CompactionOptions {
9566 max_rows_per_group: 4096,
9567 ..Default::default()
9568 };
9569 opts.apply_dataset_config(&config).unwrap();
9570
9571 assert_eq!(opts.target_rows_per_fragment, 500_000);
9573 assert_eq!(opts.max_rows_per_group, 4096);
9575 }
9576
9577 #[test]
9578 fn test_apply_dataset_config_overwrites_matching_field() {
9579 let config = HashMap::from([(
9580 "lance.compaction.max_rows_per_group".to_string(),
9581 "2048".to_string(),
9582 )]);
9583
9584 let mut opts = CompactionOptions {
9585 max_rows_per_group: 4096,
9586 ..Default::default()
9587 };
9588 opts.apply_dataset_config(&config).unwrap();
9589
9590 assert_eq!(opts.max_rows_per_group, 2048);
9592 }
9593
9594 #[tokio::test]
9595 async fn test_max_source_fragments() {
9596 let test_dir = TempStrDir::default();
9597 let dataset = dataset_with_ten_small_fragments(&test_dir).await;
9598
9599 let opts_no_limit = CompactionOptions {
9602 target_rows_per_fragment: 250,
9603 ..Default::default()
9604 };
9605 let plan_all = plan_compaction(&dataset, &opts_no_limit).await.unwrap();
9606 let total_source_frags: usize = plan_all.tasks().iter().map(|t| t.fragments.len()).sum();
9607 assert_eq!(total_source_frags, 10);
9608 assert!(
9609 plan_all.num_tasks() > 2,
9610 "need multiple tasks to test bounding, got {}",
9611 plan_all.num_tasks()
9612 );
9613
9614 let opts_bounded = CompactionOptions {
9617 target_rows_per_fragment: 250,
9618 max_source_fragments: Some(4),
9619 ..Default::default()
9620 };
9621 let plan_bounded = plan_compaction(&dataset, &opts_bounded).await.unwrap();
9622 let bounded_source_frags: usize =
9623 plan_bounded.tasks().iter().map(|t| t.fragments.len()).sum();
9624 assert!(
9625 bounded_source_frags <= 4,
9626 "expected at most 4 source fragments, got {bounded_source_frags}"
9627 );
9628 assert!(
9629 bounded_source_frags > 0,
9630 "expected at least 1 source fragment in bounded plan"
9631 );
9632 assert!(
9633 plan_bounded.num_tasks() < plan_all.num_tasks(),
9634 "bounded plan ({}) should have fewer tasks than unbounded ({})",
9635 plan_bounded.num_tasks(),
9636 plan_all.num_tasks()
9637 );
9638
9639 let mut dataset = dataset;
9641 compact_files(&mut dataset, opts_bounded, None)
9642 .await
9643 .unwrap();
9644 let after_first = dataset.get_fragments().len();
9645 assert!(
9646 after_first < 10,
9647 "expected fewer than 10 fragments after first compaction, got {after_first}"
9648 );
9649 assert!(
9650 after_first > 1,
9651 "expected partial compaction (not fully compacted), got {after_first}"
9652 );
9653
9654 let opts_bounded = CompactionOptions {
9656 target_rows_per_fragment: 250,
9657 max_source_fragments: Some(4),
9658 ..Default::default()
9659 };
9660 compact_files(&mut dataset, opts_bounded, None)
9661 .await
9662 .unwrap();
9663 let after_second = dataset.get_fragments().len();
9664 assert!(
9665 after_second <= after_first,
9666 "expected progress: {after_second} should be <= {after_first}"
9667 );
9668 }
9669
9670 async fn dataset_with_ten_small_fragments(test_uri: &str) -> Dataset {
9675 let data = sample_data();
9676 let schema = data.schema();
9677 let write_params = WriteParams {
9678 max_rows_per_file: 100,
9679 ..Default::default()
9680 };
9681 Dataset::write(
9682 RecordBatchIterator::new(vec![Ok(data.slice(0, 100))], schema.clone()),
9683 test_uri,
9684 Some(write_params.clone()),
9685 )
9686 .await
9687 .unwrap();
9688 for i in 1..10 {
9689 let mut append_params = write_params.clone();
9690 append_params.mode = WriteMode::Append;
9691 Dataset::write(
9692 RecordBatchIterator::new(vec![Ok(data.slice(i * 100, 100))], schema.clone()),
9693 test_uri,
9694 Some(append_params),
9695 )
9696 .await
9697 .unwrap();
9698 }
9699 let mut dataset = Dataset::open(test_uri).await.unwrap();
9700 assert_eq!(dataset.get_fragments().len(), 10);
9701 dataset.delete("a < 50").await.unwrap();
9703 let dataset = commit_overlay(
9706 dataset,
9707 0,
9708 &[0],
9709 OverlayCoverage::dense(bitmap([60])),
9710 vec![Arc::new(Int64Array::from(vec![60_000_i64]))],
9711 )
9712 .await;
9713 assert_eq!(
9714 dataset.get_fragment(0).unwrap().metadata().overlays.len(),
9715 1
9716 );
9717 dataset
9718 }
9719
9720 #[tokio::test]
9721 async fn test_excluded_fragments_are_planning_boundaries() {
9722 let test_dir = TempStrDir::default();
9723 let mut dataset = dataset_with_ten_small_fragments(&test_dir).await;
9724 let excluded_fragment_id = 4;
9725 let options = CompactionOptions {
9726 target_rows_per_fragment: 250,
9727 excluded_fragment_ids: vec![excluded_fragment_id, excluded_fragment_id, u32::MAX],
9728 ..Default::default()
9729 };
9730
9731 let plan = plan_compaction(&dataset, &options).await.unwrap();
9732 let planned_fragment_ids = plan
9733 .tasks()
9734 .iter()
9735 .map(|task| {
9736 task.fragments
9737 .iter()
9738 .map(|fragment| fragment.id as u32)
9739 .collect::<Vec<_>>()
9740 })
9741 .collect::<Vec<_>>();
9742
9743 assert_eq!(
9744 planned_fragment_ids,
9745 vec![vec![0, 1, 2, 3], vec![5, 6, 7, 8, 9]]
9746 );
9747 assert!(
9748 planned_fragment_ids
9749 .iter()
9750 .flatten()
9751 .all(|fragment_id| *fragment_id != excluded_fragment_id)
9752 );
9753
9754 let metrics = compact_files(&mut dataset, options, None).await.unwrap();
9755 assert_eq!(metrics.fragments_removed, 9);
9756 let remaining_fragment_ids = dataset
9757 .get_fragments()
9758 .iter()
9759 .map(|fragment| fragment.id() as u32)
9760 .collect::<Vec<_>>();
9761 assert!(remaining_fragment_ids.contains(&excluded_fragment_id));
9762 }
9763
9764 #[tokio::test]
9765 async fn test_max_source_rows() {
9766 let test_dir = TempStrDir::default();
9767 let dataset = dataset_with_ten_small_fragments(&test_dir).await;
9768
9769 let opts = CompactionOptions {
9774 target_rows_per_fragment: 250,
9775 max_source_rows: Some(270),
9776 ..Default::default()
9777 };
9778 let plan = plan_compaction(&dataset, &opts).await.unwrap();
9779 assert_eq!(plan.num_tasks(), 1);
9780 let physical_rows: usize = plan
9781 .tasks()
9782 .iter()
9783 .flat_map(|t| &t.fragments)
9784 .map(|f| f.physical_rows.unwrap())
9785 .sum();
9786 assert_eq!(physical_rows, 300);
9787 }
9788
9789 #[tokio::test]
9790 async fn test_max_source_bytes() {
9791 let test_dir = TempStrDir::default();
9792 let dataset = dataset_with_ten_small_fragments(&test_dir).await;
9793
9794 let first_task_base_bytes: u64 = dataset.get_fragments()[..3]
9795 .iter()
9796 .flat_map(|f| f.metadata.files.iter())
9797 .map(|df| df.file_size_bytes.get().unwrap().get())
9798 .sum();
9799 let overlay_bytes = dataset.get_fragment(0).unwrap().metadata().overlays[0]
9800 .data_file
9801 .file_size_bytes
9802 .get()
9803 .unwrap()
9804 .get();
9805 assert!(first_task_base_bytes > 0 && overlay_bytes > 0);
9806
9807 let opts = CompactionOptions {
9813 target_rows_per_fragment: 250,
9814 max_source_bytes: Some(first_task_base_bytes),
9815 ..Default::default()
9816 };
9817 let plan = plan_compaction(&dataset, &opts).await.unwrap();
9818 assert_eq!(plan.num_tasks(), 0);
9819
9820 let budget = first_task_base_bytes + overlay_bytes;
9823 let opts = CompactionOptions {
9824 target_rows_per_fragment: 250,
9825 max_source_bytes: Some(budget),
9826 ..Default::default()
9827 };
9828 let plan = plan_compaction(&dataset, &opts).await.unwrap();
9829 assert_eq!(plan.num_tasks(), 1);
9830 let source_bytes: u64 = plan
9831 .tasks()
9832 .iter()
9833 .flat_map(|t| &t.fragments)
9834 .flat_map(|f| {
9835 f.files
9836 .iter()
9837 .chain(f.overlays.iter().map(|o| &o.data_file))
9838 })
9839 .map(|df| df.file_size_bytes.get().unwrap().get())
9840 .sum();
9841 assert_eq!(source_bytes, budget);
9842 }
9843
9844 #[tokio::test]
9845 async fn test_compaction_uses_manifest_config() {
9846 let test_dir = TempStrDir::default();
9847 let test_uri = &test_dir;
9848
9849 let data = sample_data();
9850 let schema = data.schema();
9851
9852 let reader = RecordBatchIterator::new(vec![Ok(data.clone())], schema.clone());
9854 let write_params = WriteParams {
9855 max_rows_per_file: 2000,
9856 ..Default::default()
9857 };
9858 let mut dataset = Dataset::write(reader, test_uri, Some(write_params))
9859 .await
9860 .unwrap();
9861
9862 assert_eq!(dataset.get_fragments().len(), 5);
9863
9864 dataset
9866 .update_config([
9867 ("lance.compaction.target_rows_per_fragment", "5000"),
9868 ("lance.compaction.materialize_deletions_threshold", "2.0"),
9869 ])
9870 .await
9871 .unwrap();
9872
9873 let opts = CompactionOptions::from_dataset_config(&dataset.manifest.config).unwrap();
9875 assert_eq!(opts.target_rows_per_fragment, 5000);
9876 assert!((opts.materialize_deletions_threshold - 2.0).abs() < f32::EPSILON);
9877
9878 let plan = plan_compaction(&dataset, &opts).await.unwrap();
9880 assert!(!plan.tasks.is_empty());
9881 assert_eq!(plan.options.target_rows_per_fragment, 5000);
9882 assert!(!plan.options.materialize_deletions);
9884 }
9885
9886 #[tokio::test]
9894 async fn test_rewrite_fri_vs_create_index_conflict() {
9895 use crate::index::DatasetIndexExt;
9896 use crate::index::vector::VectorIndexParams;
9897 use futures::TryStreamExt;
9898 use lance_datagen::{BatchCount, Dimension, RowCount, array, gen_batch};
9899 use lance_index::IndexType;
9900 use lance_linalg::distance::MetricType;
9901
9902 async fn append_fragment(uri: &str, rows: u64) -> Dataset {
9903 let reader = gen_batch()
9904 .col("vec", array::rand_vec::<Float32Type>(Dimension::from(16)))
9905 .into_reader_rows(RowCount::from(rows), BatchCount::from(1));
9906 let params = WriteParams {
9907 max_rows_per_file: rows as usize,
9908 mode: WriteMode::Append,
9909 ..Default::default()
9910 };
9911 Dataset::write(reader, uri, Some(params)).await.unwrap()
9912 }
9913
9914 let tmpdir = TempStrDir::default();
9915 let uri = format!("file://{}", tmpdir.as_str());
9916
9917 let reader = gen_batch()
9919 .col("vec", array::rand_vec::<Float32Type>(Dimension::from(16)))
9920 .into_reader_rows(RowCount::from(256), BatchCount::from(1));
9921 let mut dataset = Dataset::write(
9922 reader,
9923 &uri,
9924 Some(WriteParams {
9925 max_rows_per_file: 256,
9926 mode: WriteMode::Overwrite,
9927 ..Default::default()
9928 }),
9929 )
9930 .await
9931 .unwrap();
9932 let index_params = VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 50);
9933 dataset
9934 .create_index(&["vec"], IndexType::Vector, None, &index_params, true)
9935 .await
9936 .unwrap();
9937
9938 dataset = append_fragment(&uri, 64).await;
9941 let mut stale = dataset.clone();
9942 dataset = append_fragment(&uri, 64).await;
9943
9944 let options = CompactionOptions {
9946 defer_index_remap: true,
9947 ..Default::default()
9948 };
9949 let plan = plan_compaction(&dataset, &options).await.unwrap();
9950 assert!(!plan.tasks.is_empty());
9951 let snapshot = dataset.clone();
9952 let completed: Vec<RewriteResult> = futures::stream::iter(plan.tasks.into_iter())
9953 .map(|task| rewrite_files(Cow::Borrowed(&snapshot), task, &options))
9954 .buffer_unordered(1)
9955 .try_collect()
9956 .await
9957 .unwrap();
9958
9959 stale
9964 .optimize_indices(&lance_index::optimize::OptimizeOptions::append())
9965 .await
9966 .unwrap();
9967
9968 let err = commit_compaction(
9973 &mut dataset,
9974 completed,
9975 Arc::new(DatasetIndexRemapperOptions::default()),
9976 &options,
9977 )
9978 .await
9979 .expect_err("commit should fail with retryable conflict");
9980 assert!(
9981 matches!(err, Error::RetryableCommitConflict { .. }),
9982 "unexpected error: {err}"
9983 );
9984 }
9985
9986 #[tokio::test]
10004 async fn test_distributed_compact_concurrent_delete_no_resurrection() {
10005 let test_dir = TempStrDir::default();
10006 let test_uri = &test_dir;
10007
10008 let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
10010 let data = RecordBatch::try_new(
10011 schema.clone(),
10012 vec![Arc::new(Int64Array::from_iter_values(0..4_000))],
10013 )
10014 .unwrap();
10015 let mut dataset_plan = Dataset::write(
10016 RecordBatchIterator::new(vec![Ok(data)], schema.clone()),
10017 test_uri,
10018 Some(WriteParams {
10019 max_rows_per_file: 1_000,
10020 ..Default::default()
10021 }),
10022 )
10023 .await
10024 .unwrap();
10025
10026 assert_eq!(dataset_plan.manifest.version, 1);
10027 assert_eq!(dataset_plan.get_fragments().len(), 4);
10028
10029 let options = CompactionOptions {
10031 target_rows_per_fragment: 10_000,
10032 ..Default::default()
10033 };
10034 let plan = plan_compaction(&dataset_plan, &options).await.unwrap();
10035 assert_eq!(plan.tasks().len(), 1, "expected one compaction task");
10036
10037 let dataset_for_tasks = dataset_plan.clone();
10041 let results: Vec<RewriteResult> = futures::stream::iter(plan.compaction_tasks())
10042 .then(|task| {
10043 let ds = dataset_for_tasks.clone();
10044 async move {
10045 task.execute(&ds).await.unwrap()
10047 }
10048 })
10049 .collect()
10050 .await;
10051 assert_eq!(results.len(), 1);
10052 assert_eq!(
10053 results[0].read_version, 1,
10054 "tasks must carry read_version=1"
10055 );
10056
10057 dataset_plan.delete("a < 1000").await.unwrap();
10060 assert_eq!(dataset_plan.manifest.version, 2);
10061
10062 let mut dataset_commit = Dataset::open(test_uri).await.unwrap();
10065 assert_eq!(
10066 dataset_commit.manifest.version, 2,
10067 "fresh dataset must be at the post-delete version"
10068 );
10069
10070 let commit_result = commit_compaction(
10072 &mut dataset_commit,
10073 results,
10074 Arc::new(IgnoreRemap::default()),
10075 &options,
10076 )
10077 .await;
10078
10079 assert!(
10083 commit_result.is_err(),
10084 "commit_compaction must fail with a conflict error when a concurrent \
10085 DELETE touched the same fragments; got Ok instead — deleted rows were \
10086 silently resurrected"
10087 );
10088 let err_msg = commit_result.unwrap_err().to_string();
10089 assert!(
10090 err_msg.contains("retryable")
10091 || err_msg.contains("conflict")
10092 || err_msg.contains("preempted"),
10093 "expected a retryable conflict error, got: {err_msg}"
10094 );
10095
10096 let latest = Dataset::open(test_uri).await.unwrap();
10098 let row_count = latest
10099 .count_rows(Some("a < 1000".to_string()))
10100 .await
10101 .unwrap();
10102 assert_eq!(
10103 row_count, 0,
10104 "rows deleted before compaction must not be resurrected; found {row_count}"
10105 );
10106 }
10107
10108 fn count_all_files_in(dir: &std::path::Path) -> std::io::Result<usize> {
10109 if !dir.exists() {
10110 return Ok(0);
10111 }
10112 let mut count = 0;
10113 for entry in std::fs::read_dir(dir)? {
10114 let path = entry?.path();
10115 if path.is_dir() {
10116 count += count_all_files_in(&path)?;
10117 } else if path.is_file() {
10118 if path
10120 .file_name()
10121 .and_then(|name| name.to_str())
10122 .is_some_and(|file_name| !file_name.starts_with('.'))
10123 {
10124 count += 1;
10125 }
10126 }
10127 }
10128 Ok(count)
10129 }
10130
10131 fn count_data_files_in(base_dir: &str) -> usize {
10132 let data_dir = std::path::Path::new(base_dir).join("data");
10133 count_all_files_in(&data_dir).unwrap_or(0)
10134 }
10135
10136 #[tokio::test]
10141 async fn test_commit_compaction_leaves_data_for_gc_on_commit_failure() {
10142 use crate::dataset::builder::DatasetBuilder;
10143 use crate::utils::test::FailingProxyStore;
10144 use lance_io::object_store::ObjectStoreParams;
10145
10146 let test_dir = TempStrDir::default();
10147 let test_uri = test_dir.as_str();
10148 let path_prefix = if test_uri.starts_with('/') { "" } else { "/" };
10151 let routed_uri = format!("file-object-store://{path_prefix}{test_uri}");
10152
10153 let data = sample_data();
10154 let reader = RecordBatchIterator::new(vec![Ok(data.slice(0, 200))], data.schema());
10155 Dataset::write(
10156 reader,
10157 &routed_uri,
10158 Some(WriteParams {
10159 max_rows_per_file: 100,
10160 enable_stable_row_ids: true,
10161 ..Default::default()
10162 }),
10163 )
10164 .await
10165 .unwrap();
10166
10167 let baseline_files = count_data_files_in(test_uri);
10168
10169 let failing = Arc::new(FailingProxyStore::new());
10170 failing.fail_after_n("put", "_transactions", 1, "injected commit failure");
10176 failing.fail_after_n(
10177 "put_multipart",
10178 "_transactions",
10179 1,
10180 "injected commit failure",
10181 );
10182
10183 let mut dataset = DatasetBuilder::from_uri(&routed_uri)
10184 .with_read_params(crate::dataset::ReadParams {
10185 store_options: Some(ObjectStoreParams {
10186 object_store_wrapper: Some(failing.clone()),
10187 ..Default::default()
10188 }),
10189 ..Default::default()
10190 })
10191 .load()
10192 .await
10193 .unwrap();
10194
10195 let options = CompactionOptions {
10196 target_rows_per_fragment: 1000,
10197 ..Default::default()
10198 };
10199 let result = compact_files(&mut dataset, options, None).await;
10200 assert!(
10201 result.is_err(),
10202 "Compaction should fail when transaction commit fails"
10203 );
10204
10205 assert!(
10206 count_data_files_in(test_uri) > baseline_files,
10207 "Compaction data files should be retained for GC after the Rewrite commit fails"
10208 );
10209 }
10210
10211 #[tokio::test]
10212 async fn test_commit_compaction_leaves_blob_v2_sidecars_for_gc_on_commit_failure() {
10213 use crate::BlobArrayBuilder;
10214 use crate::dataset::builder::DatasetBuilder;
10215 use crate::utils::test::FailingProxyStore;
10216 use lance_io::object_store::ObjectStoreParams;
10217
10218 let test_dir = TempStrDir::default();
10219 let test_uri = test_dir.as_str();
10220 let path_prefix = if test_uri.starts_with('/') { "" } else { "/" };
10221 let routed_uri = format!("file-object-store://{path_prefix}{test_uri}");
10222
10223 let id_array = Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef;
10224 let packed_data = vec![0u8; 100 * 1024];
10226 let dedicated_data = vec![1u8; 5 * 1024 * 1024];
10227 let mut blob_builder = BlobArrayBuilder::new(2);
10228 blob_builder.push_bytes(&packed_data).unwrap();
10229 blob_builder.push_bytes(&dedicated_data).unwrap();
10230 let blob_array: ArrayRef = blob_builder.finish().unwrap();
10231
10232 let schema = Arc::new(Schema::new(vec![
10233 Field::new("id", DataType::Int32, false),
10234 crate::blob_field("blob", true),
10235 ]));
10236 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
10237 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
10238
10239 Dataset::write(
10240 reader,
10241 &routed_uri,
10242 Some(WriteParams {
10243 max_rows_per_file: 1, enable_stable_row_ids: true,
10245 data_storage_version: Some(lance_file::version::LanceFileVersion::V2_2),
10246 ..Default::default()
10247 }),
10248 )
10249 .await
10250 .unwrap();
10251
10252 let baseline_files = count_data_files_in(test_uri);
10253
10254 let failing = Arc::new(FailingProxyStore::new());
10255 failing.fail_after_n("put", "_transactions", 1, "injected commit failure");
10256 failing.fail_after_n(
10257 "put_multipart",
10258 "_transactions",
10259 1,
10260 "injected commit failure",
10261 );
10262
10263 let mut dataset = DatasetBuilder::from_uri(&routed_uri)
10264 .with_read_params(crate::dataset::ReadParams {
10265 store_options: Some(ObjectStoreParams {
10266 object_store_wrapper: Some(failing.clone()),
10267 ..Default::default()
10268 }),
10269 ..Default::default()
10270 })
10271 .load()
10272 .await
10273 .unwrap();
10274
10275 let options = CompactionOptions {
10276 target_rows_per_fragment: 1000,
10277 ..Default::default()
10278 };
10279 let result = compact_files(&mut dataset, options, None).await;
10280 assert!(
10281 result.is_err(),
10282 "Compaction should fail when transaction commit fails"
10283 );
10284
10285 assert!(
10286 count_data_files_in(test_uri) > baseline_files,
10287 "Blob v2 sidecars should be retained for GC after the Rewrite commit fails"
10288 );
10289 }
10290
10291 async fn read_blob_bytes_by_index(
10292 dataset: &Arc<Dataset>,
10293 column: &str,
10294 ) -> Vec<(i32, Option<Vec<u8>>)> {
10295 let mut scanner = dataset.scan();
10296 scanner.with_row_id();
10297 let batch = scanner
10298 .project(&["id", column])
10299 .unwrap()
10300 .try_into_batch()
10301 .await
10302 .unwrap();
10303 let ids = batch
10304 .column_by_name("id")
10305 .unwrap()
10306 .as_primitive::<Int32Type>();
10307 let row_ids = batch
10308 .column_by_name(ROW_ID)
10309 .unwrap()
10310 .as_primitive::<UInt64Type>();
10311
10312 let mut result = Vec::with_capacity(batch.num_rows());
10313 for i in 0..batch.num_rows() {
10314 let row_id = row_ids.value(i);
10315 let id = ids.value(i);
10316 let blobs = dataset.take_blobs(&[row_id], column).await.unwrap();
10317 match blobs.into_iter().next().flatten() {
10318 Some(blob) => {
10319 let data = blob.read().await.unwrap();
10320 result.push((id, Some(data.to_vec())));
10321 }
10322 None => result.push((id, None)),
10323 }
10324 }
10325 result
10326 }
10327
10328 fn mixed_blob_values() -> Vec<(i32, Option<Vec<u8>>)> {
10329 vec![
10330 (0, Some(vec![b'0'; 80])),
10331 (1, None),
10332 (2, Some(Vec::new())),
10333 (3, Some(vec![b'3'; 80])),
10334 (4, Some(vec![b'4'; 80])),
10335 (5, Some(vec![b'5'; 80])),
10336 ]
10337 }
10338
10339 async fn assert_compaction_preserves_blob_values(
10340 mut dataset: Dataset,
10341 expected: &[(i32, Option<Vec<u8>>)],
10342 ) {
10343 assert_eq!(dataset.get_fragments().len(), 3);
10344
10345 let mut before = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
10346 before.sort_by_key(|(id, _)| *id);
10347 assert_eq!(before, expected);
10348
10349 compact_files(
10350 &mut dataset,
10351 CompactionOptions {
10352 target_rows_per_fragment: 1024 * 1024,
10353 ..Default::default()
10354 },
10355 None,
10356 )
10357 .await
10358 .unwrap();
10359
10360 assert_eq!(dataset.get_fragments().len(), 1);
10361
10362 let mut after = read_blob_bytes_by_index(&Arc::new(dataset), "blob").await;
10363 after.sort_by_key(|(id, _)| *id);
10364 assert_eq!(after, expected);
10365 }
10366
10367 #[test]
10368 fn test_blob_v2_rewrite_plan_skips_non_blob_list_subtree() {
10369 let items_field = Field::new(
10370 "items",
10371 DataType::List(Arc::new(Field::new("item", DataType::Int32, true))),
10372 true,
10373 );
10374 let info_field = Field::new(
10375 "info",
10376 DataType::Struct(
10377 vec![
10378 Arc::new(items_field),
10379 Arc::new(crate::blob_field("blob", true)),
10380 ]
10381 .into(),
10382 ),
10383 true,
10384 );
10385 let logical_arrow_schema = Schema::new(vec![info_field]);
10386 let logical_schema =
10387 lance_core::datatypes::Schema::try_from(&logical_arrow_schema).unwrap();
10388 let mut input_schema = logical_schema.clone();
10389 input_schema.fields[0].unload_blobs_recursive();
10390 let input_field = Field::from(&input_schema.fields[0]);
10391
10392 let plan =
10393 BlobV2FieldRewritePlan::try_new(&logical_schema.fields[0], &input_field).unwrap();
10394 let BlobV2FieldRewritePlan::Struct { children, .. } = &plan else {
10395 panic!("nested blob field should produce a struct rewrite plan");
10396 };
10397 assert!(matches!(
10398 children[0],
10399 BlobV2FieldRewritePlan::Passthrough { .. }
10400 ));
10401 assert!(matches!(children[1], BlobV2FieldRewritePlan::Blob { .. }));
10402
10403 let DataType::Struct(output_children) = plan.output_field().data_type() else {
10404 panic!("nested blob rewrite plan should declare a struct output");
10405 };
10406 let DataType::Struct(input_children) = input_field.data_type() else {
10407 panic!("nested blob input should be a struct");
10408 };
10409 assert_eq!(output_children[0], input_children[0]);
10410 let DataType::Struct(blob_children) = output_children[1].data_type() else {
10411 panic!("blob rewrite plan should declare a struct blob output");
10412 };
10413 assert_eq!(
10414 BlobV2Layout::classify(blob_children),
10415 Some(BlobV2Layout::Logical)
10416 );
10417 }
10418
10419 #[tokio::test]
10420 async fn test_compact_blob_v1_preserves_null_empty_and_payload_order() {
10421 let test_dir = TempStrDir::default();
10422 let expected = mixed_blob_values();
10423 let schema = Arc::new(Schema::new(vec![
10424 Field::new("id", DataType::Int32, false),
10425 Field::new("blob", DataType::LargeBinary, true)
10426 .with_metadata([(BLOB_META_KEY.to_string(), "true".to_string())].into()),
10427 ]));
10428 let batch = RecordBatch::try_new(
10429 schema.clone(),
10430 vec![
10431 Arc::new(Int32Array::from_iter_values(0..expected.len() as i32)),
10432 Arc::new(LargeBinaryArray::from_iter(
10433 expected.iter().map(|(_, value)| value.as_deref()),
10434 )),
10435 ],
10436 )
10437 .unwrap();
10438 let dataset = Dataset::write(
10439 RecordBatchIterator::new(vec![Ok(batch)], schema),
10440 &test_dir,
10441 Some(WriteParams {
10442 data_storage_version: Some(LanceFileVersion::V2_0),
10443 max_rows_per_file: 2,
10444 ..Default::default()
10445 }),
10446 )
10447 .await
10448 .unwrap();
10449
10450 assert_compaction_preserves_blob_values(dataset, &expected).await;
10451 }
10452
10453 #[tokio::test]
10454 async fn test_compact_blob_v2_preserves_null_empty_and_payload_order() {
10455 use crate::BlobArrayBuilder;
10456
10457 let test_dir = TempStrDir::default();
10458 let expected = mixed_blob_values();
10459 let mut blob_builder = BlobArrayBuilder::new(expected.len());
10460 for (_, value) in &expected {
10461 match value {
10462 Some(value) => blob_builder.push_bytes(value).unwrap(),
10463 None => blob_builder.push_null().unwrap(),
10464 }
10465 }
10466 let schema = Arc::new(Schema::new(vec![
10467 Field::new("id", DataType::Int32, false),
10468 crate::blob_field("blob", true),
10469 ]));
10470 let batch = RecordBatch::try_new(
10471 schema.clone(),
10472 vec![
10473 Arc::new(Int32Array::from_iter_values(0..expected.len() as i32)),
10474 blob_builder.finish().unwrap(),
10475 ],
10476 )
10477 .unwrap();
10478 let dataset = Dataset::write(
10479 RecordBatchIterator::new(vec![Ok(batch)], schema),
10480 &test_dir,
10481 Some(WriteParams {
10482 data_storage_version: Some(LanceFileVersion::V2_2),
10483 max_rows_per_file: 2,
10484 ..Default::default()
10485 }),
10486 )
10487 .await
10488 .unwrap();
10489
10490 assert_compaction_preserves_blob_values(dataset, &expected).await;
10491 }
10492
10493 #[tokio::test]
10494 async fn test_compact_blob_v2_preserves_external_references() {
10495 use crate::BlobArrayBuilder;
10496 use lance_core::utils::tempfile::TempDir;
10497 use lance_table::format::BasePath;
10498
10499 let test_dir = TempDir::default();
10500 let external_dir = TempDir::default();
10501 let external_path = external_dir.std_path().join("external.bin");
10502 std::fs::write(&external_path, b"external-data").unwrap();
10503 let external_uri = format!("file://{}", external_path.display());
10504 let base_uri = format!("file://{}", external_dir.std_path().display());
10505
10506 let mut blob_builder = BlobArrayBuilder::new(2);
10507 blob_builder.push_uri(external_uri.clone()).unwrap();
10508 blob_builder.push_bytes(b"inline-data").unwrap();
10509 let blob_array: ArrayRef = blob_builder.finish().unwrap();
10510
10511 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1]));
10512 let schema = Arc::new(Schema::new(vec![
10513 Field::new("id", DataType::Int32, false),
10514 crate::blob_field("blob", true),
10515 ]));
10516
10517 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
10518 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
10519
10520 let mut dataset = Dataset::write(
10521 reader,
10522 &test_dir.path_str(),
10523 Some(WriteParams {
10524 data_storage_version: Some(LanceFileVersion::V2_2),
10525 max_rows_per_file: 1,
10526 initial_bases: Some(vec![BasePath {
10527 id: 1,
10528 name: Some("external".to_string()),
10529 path: base_uri,
10530 is_dataset_root: false,
10531 }]),
10532 ..Default::default()
10533 }),
10534 )
10535 .await
10536 .unwrap();
10537
10538 assert_eq!(dataset.get_fragments().len(), 2);
10539
10540 for frag in dataset.get_fragments() {
10541 let rows = frag.physical_rows().await.unwrap();
10542 assert!(rows > 0, "fragment {} should have rows", frag.id());
10543 }
10544
10545 let options = CompactionOptions {
10546 target_rows_per_fragment: 1024 * 1024,
10547 ..Default::default()
10548 };
10549 let plan = plan_compaction(&dataset, &options).await.unwrap();
10550 assert!(
10551 !plan.tasks().is_empty(),
10552 "compaction plan should have tasks, got {} tasks",
10553 plan.tasks().len()
10554 );
10555
10556 compact_files(&mut dataset, options, None).await.unwrap();
10557
10558 assert_eq!(dataset.get_fragments().len(), 1);
10559
10560 let scan_result = dataset
10561 .scan()
10562 .project(&["id", "blob"])
10563 .unwrap()
10564 .try_into_batch()
10565 .await
10566 .unwrap();
10567 assert_eq!(scan_result.num_rows(), 2);
10568
10569 let ids = scan_result
10570 .column_by_name("id")
10571 .unwrap()
10572 .as_primitive::<Int32Type>();
10573 let mut id_values: Vec<i32> = ids.iter().map(|v| v.unwrap()).collect();
10574 id_values.sort();
10575 assert_eq!(id_values, vec![0, 1]);
10576
10577 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
10578 blob_values.sort_by_key(|(id, _)| *id);
10579 assert_eq!(
10580 blob_values,
10581 vec![
10582 (0, Some(b"external-data".to_vec())),
10583 (1, Some(b"inline-data".to_vec()))
10584 ]
10585 );
10586 }
10587
10588 #[tokio::test]
10589 async fn test_compact_blob_v2_packed_and_dedicated() {
10590 use crate::BlobArrayBuilder;
10591 use lance_arrow::BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY;
10592 use lance_core::utils::tempfile::TempDir;
10593
10594 let test_dir = TempDir::default();
10595
10596 let inline_data = b"small-inline-blob".as_slice();
10597 let packed_data: Vec<u8> = (0..64 * 1024 + 1024).map(|i| (i % 256) as u8).collect();
10598 let dedicated_data: Vec<u8> = (0..4 * 1024 * 1024 + 512)
10599 .map(|i| ((i + 97) % 256) as u8)
10600 .collect();
10601
10602 let mut blob_builder = BlobArrayBuilder::new(3);
10603 blob_builder.push_bytes(inline_data).unwrap();
10604 blob_builder.push_bytes(&packed_data).unwrap();
10605 blob_builder.push_bytes(&dedicated_data).unwrap();
10606 let blob_array: ArrayRef = blob_builder.finish().unwrap();
10607
10608 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1, 2]));
10609 let mut blob_field = crate::blob_field("blob", true);
10610 {
10611 let metadata = blob_field.metadata().clone();
10612 let mut new_metadata = metadata;
10613 new_metadata.insert(
10614 BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY.to_string(),
10615 (4 * 1024 * 1024).to_string(),
10616 );
10617 blob_field = blob_field.with_metadata(new_metadata);
10618 }
10619 let schema = Arc::new(Schema::new(vec![
10620 Field::new("id", DataType::Int32, false),
10621 blob_field,
10622 ]));
10623
10624 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
10625 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
10626
10627 let mut dataset = Dataset::write(
10628 reader,
10629 &test_dir.path_str(),
10630 Some(WriteParams {
10631 data_storage_version: Some(LanceFileVersion::V2_2),
10632 max_rows_per_file: 1,
10633 ..Default::default()
10634 }),
10635 )
10636 .await
10637 .unwrap();
10638
10639 assert_eq!(dataset.get_fragments().len(), 3);
10640
10641 compact_files(
10642 &mut dataset,
10643 CompactionOptions {
10644 target_rows_per_fragment: 1024 * 1024,
10645 ..Default::default()
10646 },
10647 None,
10648 )
10649 .await
10650 .unwrap();
10651
10652 assert_eq!(dataset.get_fragments().len(), 1);
10653
10654 let scan_result = dataset
10655 .scan()
10656 .project(&["id", "blob"])
10657 .unwrap()
10658 .try_into_batch()
10659 .await
10660 .unwrap();
10661 assert_eq!(scan_result.num_rows(), 3);
10662
10663 let ids = scan_result
10664 .column_by_name("id")
10665 .unwrap()
10666 .as_primitive::<Int32Type>();
10667 let id_values: Vec<i32> = ids.iter().map(|v| v.unwrap()).collect();
10668 assert_eq!(id_values, vec![0, 1, 2]);
10669
10670 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
10671 blob_values.sort_by_key(|(id, _)| *id);
10672 assert_eq!(
10673 blob_values,
10674 vec![
10675 (0, Some(inline_data.to_vec())),
10676 (1, Some(packed_data)),
10677 (2, Some(dedicated_data))
10678 ]
10679 );
10680 }
10681
10682 #[tokio::test]
10683 async fn test_compact_blob_v2_with_null_rows() {
10684 use crate::BlobArrayBuilder;
10685 use lance_core::utils::tempfile::TempDir;
10686
10687 let test_dir = TempDir::default();
10688
10689 let mut blob_builder = BlobArrayBuilder::new(4);
10690 blob_builder.push_bytes(b"inline-0").unwrap();
10691 blob_builder.push_null().unwrap();
10692 blob_builder.push_bytes(b"inline-2").unwrap();
10693 blob_builder.push_null().unwrap();
10694 let blob_array: ArrayRef = blob_builder.finish().unwrap();
10695
10696 let id_array: ArrayRef =
10697 Arc::new(Int32Array::from(vec![Some(0), Some(1), Some(2), Some(3)]));
10698 let schema = Arc::new(Schema::new(vec![
10699 Field::new("id", DataType::Int32, false),
10700 crate::blob_field("blob", true),
10701 ]));
10702
10703 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
10704 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
10705
10706 let mut dataset = Dataset::write(
10707 reader,
10708 &test_dir.path_str(),
10709 Some(WriteParams {
10710 data_storage_version: Some(LanceFileVersion::V2_2),
10711 max_rows_per_file: 2,
10712 ..Default::default()
10713 }),
10714 )
10715 .await
10716 .unwrap();
10717
10718 assert_eq!(dataset.get_fragments().len(), 2);
10719
10720 compact_files(
10721 &mut dataset,
10722 CompactionOptions {
10723 target_rows_per_fragment: 1024 * 1024,
10724 ..Default::default()
10725 },
10726 None,
10727 )
10728 .await
10729 .unwrap();
10730
10731 assert_eq!(dataset.get_fragments().len(), 1);
10732
10733 let scan_result = dataset
10734 .scan()
10735 .project(&["id", "blob"])
10736 .unwrap()
10737 .try_into_batch()
10738 .await
10739 .unwrap();
10740 assert_eq!(scan_result.num_rows(), 4);
10741
10742 let ids = scan_result
10743 .column_by_name("id")
10744 .unwrap()
10745 .as_primitive::<Int32Type>();
10746 let id_values: Vec<i32> = ids.iter().map(|v| v.unwrap()).collect();
10747 assert_eq!(id_values, vec![0, 1, 2, 3]);
10748
10749 let blob_col = scan_result.column_by_name("blob").unwrap();
10750 assert!(
10751 matches!(blob_col.data_type(), DataType::Struct(_)),
10752 "blob column should be a struct after compaction"
10753 );
10754
10755 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
10756 blob_values.sort_by_key(|(id, _)| *id);
10757 assert_eq!(
10758 blob_values,
10759 vec![
10760 (0, Some(b"inline-0".to_vec())),
10761 (1, None),
10762 (2, Some(b"inline-2".to_vec())),
10763 (3, None)
10764 ]
10765 );
10766 }
10767
10768 #[tokio::test]
10769 async fn test_compact_blob_v2_deleted_rows_not_resurrected() {
10770 use crate::BlobArrayBuilder;
10771 use lance_core::utils::tempfile::TempDir;
10772
10773 let test_dir = TempDir::default();
10774
10775 let mut blob_builder = BlobArrayBuilder::new(4);
10776 blob_builder.push_bytes(b"blob-0").unwrap();
10777 blob_builder.push_bytes(b"blob-1").unwrap();
10778 blob_builder.push_bytes(b"blob-2").unwrap();
10779 blob_builder.push_bytes(b"blob-3").unwrap();
10780 let blob_array: ArrayRef = blob_builder.finish().unwrap();
10781
10782 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1, 2, 3]));
10783 let schema = Arc::new(Schema::new(vec![
10784 Field::new("id", DataType::Int32, false),
10785 crate::blob_field("blob", true),
10786 ]));
10787
10788 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
10789 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
10790
10791 let mut dataset = Dataset::write(
10792 reader,
10793 &test_dir.path_str(),
10794 Some(WriteParams {
10795 data_storage_version: Some(LanceFileVersion::V2_2),
10796 max_rows_per_file: 2,
10797 ..Default::default()
10798 }),
10799 )
10800 .await
10801 .unwrap();
10802
10803 assert_eq!(dataset.get_fragments().len(), 2);
10804
10805 dataset.delete("id = 1").await.unwrap();
10806 dataset.delete("id = 2").await.unwrap();
10807
10808 compact_files(
10809 &mut dataset,
10810 CompactionOptions {
10811 target_rows_per_fragment: 1024 * 1024,
10812 materialize_deletions_threshold: 0.0,
10813 ..Default::default()
10814 },
10815 None,
10816 )
10817 .await
10818 .unwrap();
10819
10820 let scan_result = dataset
10821 .scan()
10822 .project(&["id", "blob"])
10823 .unwrap()
10824 .try_into_batch()
10825 .await
10826 .unwrap();
10827 assert_eq!(scan_result.num_rows(), 2);
10828
10829 let ids = scan_result
10830 .column_by_name("id")
10831 .unwrap()
10832 .as_primitive::<Int32Type>();
10833 let mut id_values: Vec<i32> = ids.iter().map(|v| v.unwrap()).collect();
10834 id_values.sort();
10835 assert_eq!(id_values, vec![0, 3]);
10836
10837 let blob_col = scan_result.column_by_name("blob").unwrap();
10838 let struct_arr = blob_col.as_any().downcast_ref::<StructArray>().unwrap();
10839 let kind_col = struct_arr
10840 .column_by_name("kind")
10841 .unwrap()
10842 .as_primitive::<UInt8Type>();
10843
10844 for i in 0..kind_col.len() {
10845 assert!(
10846 !kind_col.is_null(i),
10847 "row {} should have a non-null kind after compaction of deleted rows",
10848 i
10849 );
10850 }
10851
10852 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
10853 blob_values.sort_by_key(|(id, _)| *id);
10854 assert_eq!(
10855 blob_values,
10856 vec![(0, Some(b"blob-0".to_vec())), (3, Some(b"blob-3".to_vec()))]
10857 );
10858 }
10859
10860 #[tokio::test]
10861 async fn test_compact_blob_v2_external_and_data_blob_mixed() {
10862 use crate::BlobArrayBuilder;
10863 use lance_arrow::BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY;
10864 use lance_core::utils::tempfile::TempDir;
10865 use lance_table::format::BasePath;
10866
10867 let test_dir = TempDir::default();
10868 let external_dir = TempDir::default();
10869 let external_path = external_dir.std_path().join("external.bin");
10870 std::fs::write(&external_path, b"external-payload").unwrap();
10871 let external_uri = format!("file://{}", external_path.display());
10872 let base_uri = format!("file://{}", external_dir.std_path().display());
10873
10874 let packed_data: Vec<u8> = (0..64 * 1024 + 512).map(|i| (i % 256) as u8).collect();
10875
10876 let mut blob_builder = BlobArrayBuilder::new(4);
10877 blob_builder.push_uri(external_uri.clone()).unwrap();
10878 blob_builder.push_bytes(&packed_data).unwrap();
10879 blob_builder.push_bytes(b"inline-small").unwrap();
10880 blob_builder.push_uri(external_uri.clone()).unwrap();
10881 let blob_array: ArrayRef = blob_builder.finish().unwrap();
10882
10883 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1, 2, 3]));
10884 let mut blob_field = crate::blob_field("blob", true);
10885 {
10886 let mut new_metadata = blob_field.metadata().clone();
10887 new_metadata.insert(
10888 BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY.to_string(),
10889 (4 * 1024 * 1024).to_string(),
10890 );
10891 blob_field = blob_field.with_metadata(new_metadata);
10892 }
10893 let schema = Arc::new(Schema::new(vec![
10894 Field::new("id", DataType::Int32, false),
10895 blob_field,
10896 ]));
10897
10898 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
10899 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
10900
10901 let mut dataset = Dataset::write(
10902 reader,
10903 &test_dir.path_str(),
10904 Some(WriteParams {
10905 data_storage_version: Some(LanceFileVersion::V2_2),
10906 max_rows_per_file: 2,
10907 initial_bases: Some(vec![BasePath {
10908 id: 1,
10909 name: Some("external".to_string()),
10910 path: base_uri,
10911 is_dataset_root: false,
10912 }]),
10913 ..Default::default()
10914 }),
10915 )
10916 .await
10917 .unwrap();
10918
10919 assert_eq!(dataset.get_fragments().len(), 2);
10920
10921 compact_files(
10922 &mut dataset,
10923 CompactionOptions {
10924 target_rows_per_fragment: 1024 * 1024,
10925 ..Default::default()
10926 },
10927 None,
10928 )
10929 .await
10930 .unwrap();
10931
10932 assert_eq!(dataset.get_fragments().len(), 1);
10933
10934 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
10935 blob_values.sort_by_key(|(id, _)| *id);
10936 assert_eq!(
10937 blob_values,
10938 vec![
10939 (0, Some(b"external-payload".to_vec())),
10940 (1, Some(packed_data)),
10941 (2, Some(b"inline-small".to_vec())),
10942 (3, Some(b"external-payload".to_vec()))
10943 ]
10944 );
10945 }
10946
10947 #[tokio::test]
10948 async fn test_compact_blob_v2_multiple_blob_columns() {
10949 use crate::BlobArrayBuilder;
10950 use lance_core::utils::tempfile::TempDir;
10951
10952 let test_dir = TempDir::default();
10953
10954 let mut image_builder = BlobArrayBuilder::new(3);
10955 image_builder.push_bytes(b"image-0").unwrap();
10956 image_builder.push_bytes(b"image-1").unwrap();
10957 image_builder.push_bytes(b"image-2").unwrap();
10958 let image_array: ArrayRef = image_builder.finish().unwrap();
10959
10960 let mut thumb_builder = BlobArrayBuilder::new(3);
10961 thumb_builder.push_bytes(b"thumb-0").unwrap();
10962 thumb_builder.push_null().unwrap();
10963 thumb_builder.push_bytes(b"thumb-2").unwrap();
10964 let thumb_array: ArrayRef = thumb_builder.finish().unwrap();
10965
10966 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1, 2]));
10967 let schema = Arc::new(Schema::new(vec![
10968 Field::new("id", DataType::Int32, false),
10969 crate::blob_field("image", true),
10970 crate::blob_field("thumbnail", true),
10971 ]));
10972
10973 let batch =
10974 RecordBatch::try_new(schema.clone(), vec![id_array, image_array, thumb_array]).unwrap();
10975 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
10976
10977 let mut dataset = Dataset::write(
10978 reader,
10979 &test_dir.path_str(),
10980 Some(WriteParams {
10981 data_storage_version: Some(LanceFileVersion::V2_2),
10982 max_rows_per_file: 1,
10983 ..Default::default()
10984 }),
10985 )
10986 .await
10987 .unwrap();
10988
10989 assert_eq!(dataset.get_fragments().len(), 3);
10990
10991 compact_files(
10992 &mut dataset,
10993 CompactionOptions {
10994 target_rows_per_fragment: 1024 * 1024,
10995 ..Default::default()
10996 },
10997 None,
10998 )
10999 .await
11000 .unwrap();
11001
11002 assert_eq!(dataset.get_fragments().len(), 1);
11003
11004 let mut image_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "image").await;
11005 image_values.sort_by_key(|(id, _)| *id);
11006 assert_eq!(
11007 image_values,
11008 vec![
11009 (0, Some(b"image-0".to_vec())),
11010 (1, Some(b"image-1".to_vec())),
11011 (2, Some(b"image-2".to_vec()))
11012 ]
11013 );
11014
11015 let mut thumb_values =
11016 read_blob_bytes_by_index(&Arc::new(dataset.clone()), "thumbnail").await;
11017 thumb_values.sort_by_key(|(id, _)| *id);
11018 assert_eq!(
11019 thumb_values,
11020 vec![
11021 (0, Some(b"thumb-0".to_vec())),
11022 (1, None),
11023 (2, Some(b"thumb-2".to_vec()))
11024 ]
11025 );
11026 }
11027
11028 #[tokio::test]
11029 async fn test_compact_blob_v2_external_and_null_mixed() {
11030 use crate::BlobArrayBuilder;
11031 use lance_core::utils::tempfile::TempDir;
11032 use lance_table::format::BasePath;
11033
11034 let test_dir = TempDir::default();
11035 let external_dir = TempDir::default();
11036 let external_path = external_dir.std_path().join("mixed-external.bin");
11037 std::fs::write(&external_path, b"external-mixed-data").unwrap();
11038 let external_uri = format!("file://{}", external_path.display());
11039 let base_uri = format!("file://{}", external_dir.std_path().display());
11040
11041 let mut blob_builder = BlobArrayBuilder::new(4);
11042 blob_builder.push_uri(external_uri.clone()).unwrap();
11043 blob_builder.push_null().unwrap();
11044 blob_builder.push_uri(external_uri.clone()).unwrap();
11045 blob_builder.push_null().unwrap();
11046 let blob_array: ArrayRef = blob_builder.finish().unwrap();
11047
11048 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1, 2, 3]));
11049 let schema = Arc::new(Schema::new(vec![
11050 Field::new("id", DataType::Int32, false),
11051 crate::blob_field("blob", true),
11052 ]));
11053
11054 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
11055 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
11056
11057 let mut dataset = Dataset::write(
11058 reader,
11059 &test_dir.path_str(),
11060 Some(WriteParams {
11061 data_storage_version: Some(LanceFileVersion::V2_2),
11062 max_rows_per_file: 2,
11063 initial_bases: Some(vec![BasePath {
11064 id: 1,
11065 name: Some("external".to_string()),
11066 path: base_uri,
11067 is_dataset_root: false,
11068 }]),
11069 ..Default::default()
11070 }),
11071 )
11072 .await
11073 .unwrap();
11074
11075 assert_eq!(dataset.get_fragments().len(), 2);
11076
11077 compact_files(
11078 &mut dataset,
11079 CompactionOptions {
11080 target_rows_per_fragment: 1024 * 1024,
11081 ..Default::default()
11082 },
11083 None,
11084 )
11085 .await
11086 .unwrap();
11087
11088 assert_eq!(dataset.get_fragments().len(), 1);
11089
11090 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
11091 blob_values.sort_by_key(|(id, _)| *id);
11092 assert_eq!(
11093 blob_values,
11094 vec![
11095 (0, Some(b"external-mixed-data".to_vec())),
11096 (1, None),
11097 (2, Some(b"external-mixed-data".to_vec())),
11098 (3, None)
11099 ]
11100 );
11101 }
11102
11103 #[tokio::test]
11104 async fn test_compact_blob_v2_all_null_and_all_external_fragments() {
11105 use crate::BlobArrayBuilder;
11106 use lance_core::utils::tempfile::TempDir;
11107 use lance_table::format::BasePath;
11108
11109 let test_dir = TempDir::default();
11110 let external_dir = TempDir::default();
11111 let external_path = external_dir.std_path().join("all-ext.bin");
11112 std::fs::write(&external_path, b"all-external-data").unwrap();
11113 let external_uri = format!("file://{}", external_path.display());
11114 let base_uri = format!("file://{}", external_dir.std_path().display());
11115
11116 let mut null_builder = BlobArrayBuilder::new(2);
11117 null_builder.push_null().unwrap();
11118 null_builder.push_null().unwrap();
11119 let null_array: ArrayRef = null_builder.finish().unwrap();
11120
11121 let mut ext_builder = BlobArrayBuilder::new(2);
11122 ext_builder.push_uri(external_uri.clone()).unwrap();
11123 ext_builder.push_uri(external_uri.clone()).unwrap();
11124 let ext_array: ArrayRef = ext_builder.finish().unwrap();
11125
11126 let id_null_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1]));
11127 let null_schema = Arc::new(Schema::new(vec![
11128 Field::new("id", DataType::Int32, false),
11129 crate::blob_field("blob", true),
11130 ]));
11131 let null_batch =
11132 RecordBatch::try_new(null_schema.clone(), vec![id_null_array, null_array]).unwrap();
11133
11134 let id_ext_array: ArrayRef = Arc::new(Int32Array::from(vec![2, 3]));
11135 let ext_schema = Arc::new(Schema::new(vec![
11136 Field::new("id", DataType::Int32, false),
11137 crate::blob_field("blob", true),
11138 ]));
11139 let ext_batch =
11140 RecordBatch::try_new(ext_schema.clone(), vec![id_ext_array, ext_array]).unwrap();
11141
11142 let mut dataset = Dataset::write(
11143 RecordBatchIterator::new(
11144 vec![null_batch, ext_batch].into_iter().map(Ok),
11145 null_schema.clone(),
11146 ),
11147 &test_dir.path_str(),
11148 Some(WriteParams {
11149 data_storage_version: Some(LanceFileVersion::V2_2),
11150 max_rows_per_file: 2,
11151 initial_bases: Some(vec![BasePath {
11152 id: 1,
11153 name: Some("external".to_string()),
11154 path: base_uri,
11155 is_dataset_root: false,
11156 }]),
11157 ..Default::default()
11158 }),
11159 )
11160 .await
11161 .unwrap();
11162
11163 assert_eq!(dataset.get_fragments().len(), 2);
11164
11165 compact_files(
11166 &mut dataset,
11167 CompactionOptions {
11168 target_rows_per_fragment: 1024 * 1024,
11169 ..Default::default()
11170 },
11171 None,
11172 )
11173 .await
11174 .unwrap();
11175
11176 assert_eq!(dataset.get_fragments().len(), 1);
11177
11178 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
11179 blob_values.sort_by_key(|(id, _)| *id);
11180 assert_eq!(
11181 blob_values,
11182 vec![
11183 (0, None),
11184 (1, None),
11185 (2, Some(b"all-external-data".to_vec())),
11186 (3, Some(b"all-external-data".to_vec()))
11187 ]
11188 );
11189 }
11190
11191 #[tokio::test]
11192 async fn test_compact_blob_v2_external_with_multiple_base_ids() {
11193 use crate::BlobArrayBuilder;
11194 use lance_core::utils::tempfile::TempDir;
11195 use lance_table::format::BasePath;
11196
11197 let test_dir = TempDir::default();
11198 let base_a_dir = TempDir::default();
11199 let base_b_dir = TempDir::default();
11200
11201 let path_a = base_a_dir.std_path().join("data-a.bin");
11202 std::fs::write(&path_a, b"from-base-a").unwrap();
11203 let uri_a = format!("file://{}", path_a.display());
11204 let base_uri_a = format!("file://{}", base_a_dir.std_path().display());
11205
11206 let path_b = base_b_dir.std_path().join("data-b.bin");
11207 std::fs::write(&path_b, b"from-base-b").unwrap();
11208 let uri_b = format!("file://{}", path_b.display());
11209 let base_uri_b = format!("file://{}", base_b_dir.std_path().display());
11210
11211 let mut blob_builder = BlobArrayBuilder::new(4);
11212 blob_builder.push_uri(uri_a.clone()).unwrap();
11213 blob_builder.push_uri(uri_b).unwrap();
11214 blob_builder.push_bytes(b"inline-data").unwrap();
11215 blob_builder.push_uri(uri_a).unwrap();
11216 let blob_array: ArrayRef = blob_builder.finish().unwrap();
11217
11218 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1, 2, 3]));
11219 let schema = Arc::new(Schema::new(vec![
11220 Field::new("id", DataType::Int32, false),
11221 crate::blob_field("blob", true),
11222 ]));
11223
11224 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
11225 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
11226
11227 let mut dataset = Dataset::write(
11228 reader,
11229 &test_dir.path_str(),
11230 Some(WriteParams {
11231 data_storage_version: Some(LanceFileVersion::V2_2),
11232 max_rows_per_file: 2,
11233 initial_bases: Some(vec![
11234 BasePath {
11235 id: 1,
11236 name: Some("base_a".to_string()),
11237 path: base_uri_a,
11238 is_dataset_root: false,
11239 },
11240 BasePath {
11241 id: 2,
11242 name: Some("base_b".to_string()),
11243 path: base_uri_b,
11244 is_dataset_root: false,
11245 },
11246 ]),
11247 ..Default::default()
11248 }),
11249 )
11250 .await
11251 .unwrap();
11252
11253 assert_eq!(dataset.get_fragments().len(), 2);
11254
11255 compact_files(
11256 &mut dataset,
11257 CompactionOptions {
11258 target_rows_per_fragment: 1024 * 1024,
11259 ..Default::default()
11260 },
11261 None,
11262 )
11263 .await
11264 .unwrap();
11265
11266 assert_eq!(dataset.get_fragments().len(), 1);
11267
11268 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
11269 blob_values.sort_by_key(|(id, _)| *id);
11270 assert_eq!(
11271 blob_values,
11272 vec![
11273 (0, Some(b"from-base-a".to_vec())),
11274 (1, Some(b"from-base-b".to_vec())),
11275 (2, Some(b"inline-data".to_vec())),
11276 (3, Some(b"from-base-a".to_vec()))
11277 ]
11278 );
11279 }
11280
11281 #[tokio::test]
11282 async fn test_compact_blob_v2_large_blobs() {
11283 use crate::BlobArrayBuilder;
11284 use lance_core::utils::tempfile::TempDir;
11285
11286 let test_dir = TempDir::default();
11287
11288 let large_blob_a: Vec<u8> = (0..512 * 1024).map(|i| (i % 256) as u8).collect();
11289 let large_blob_b: Vec<u8> = (0..256 * 1024).map(|i| ((i + 42) % 256) as u8).collect();
11290
11291 let mut blob_builder = BlobArrayBuilder::new(3);
11292 blob_builder.push_bytes(&large_blob_a).unwrap();
11293 blob_builder.push_bytes(&large_blob_b).unwrap();
11294 blob_builder.push_bytes(b"small-blob").unwrap();
11295 let blob_array: ArrayRef = blob_builder.finish().unwrap();
11296
11297 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1, 2]));
11298 let schema = Arc::new(Schema::new(vec![
11299 Field::new("id", DataType::Int32, false),
11300 crate::blob_field("blob", true),
11301 ]));
11302
11303 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
11304 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
11305
11306 let mut dataset = Dataset::write(
11307 reader,
11308 &test_dir.path_str(),
11309 Some(WriteParams {
11310 data_storage_version: Some(LanceFileVersion::V2_2),
11311 max_rows_per_file: 1,
11312 ..Default::default()
11313 }),
11314 )
11315 .await
11316 .unwrap();
11317
11318 assert_eq!(dataset.get_fragments().len(), 3);
11319
11320 compact_files(
11321 &mut dataset,
11322 CompactionOptions {
11323 target_rows_per_fragment: 1024 * 1024,
11324 ..Default::default()
11325 },
11326 None,
11327 )
11328 .await
11329 .unwrap();
11330
11331 assert_eq!(dataset.get_fragments().len(), 1);
11332
11333 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
11334 blob_values.sort_by_key(|(id, _)| *id);
11335 assert_eq!(
11336 blob_values,
11337 vec![
11338 (0, Some(large_blob_a)),
11339 (1, Some(large_blob_b)),
11340 (2, Some(b"small-blob".to_vec()))
11341 ]
11342 );
11343 }
11344
11345 #[tokio::test]
11346 async fn test_compact_blob_v2_blob_kind_reclassification() {
11347 use crate::BlobArrayBuilder;
11348 use lance_arrow::BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY;
11349 use lance_core::utils::tempfile::TempDir;
11350
11351 let test_dir = TempDir::default();
11352
11353 let medium_data: Vec<u8> = (0..32 * 1024).map(|i| (i % 256) as u8).collect();
11354
11355 let mut blob_builder = BlobArrayBuilder::new(2);
11356 blob_builder.push_bytes(&medium_data).unwrap();
11357 blob_builder.push_bytes(&medium_data).unwrap();
11358 let blob_array: ArrayRef = blob_builder.finish().unwrap();
11359
11360 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1]));
11361 let mut blob_field = crate::blob_field("blob", true);
11362 {
11363 let mut new_metadata = blob_field.metadata().clone();
11364 new_metadata.insert(
11365 BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY.to_string(),
11366 (16 * 1024).to_string(),
11367 );
11368 blob_field = blob_field.with_metadata(new_metadata);
11369 }
11370 let schema = Arc::new(Schema::new(vec![
11371 Field::new("id", DataType::Int32, false),
11372 blob_field,
11373 ]));
11374
11375 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
11376 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
11377
11378 let mut dataset = Dataset::write(
11379 reader,
11380 &test_dir.path_str(),
11381 Some(WriteParams {
11382 data_storage_version: Some(LanceFileVersion::V2_2),
11383 max_rows_per_file: 1,
11384 ..Default::default()
11385 }),
11386 )
11387 .await
11388 .unwrap();
11389
11390 assert_eq!(dataset.get_fragments().len(), 2);
11391
11392 compact_files(
11393 &mut dataset,
11394 CompactionOptions {
11395 target_rows_per_fragment: 1024 * 1024,
11396 ..Default::default()
11397 },
11398 None,
11399 )
11400 .await
11401 .unwrap();
11402
11403 assert_eq!(dataset.get_fragments().len(), 1);
11404
11405 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
11406 blob_values.sort_by_key(|(id, _)| *id);
11407 assert_eq!(
11408 blob_values,
11409 vec![
11410 (0, Some(medium_data.clone())),
11411 (1, Some(medium_data.clone()))
11412 ]
11413 );
11414 }
11415
11416 #[tokio::test]
11417 async fn test_compact_blob_v2_multi_batch() {
11418 use crate::BlobArrayBuilder;
11419 use lance_core::utils::tempfile::TempDir;
11420
11421 let test_dir = TempDir::default();
11422
11423 let mut blob_builder = BlobArrayBuilder::new(6);
11424 blob_builder.push_bytes(b"batch-0-row-0").unwrap();
11425 blob_builder.push_bytes(b"batch-0-row-1").unwrap();
11426 blob_builder.push_bytes(b"batch-1-row-0").unwrap();
11427 blob_builder.push_null().unwrap();
11428 blob_builder.push_bytes(b"batch-1-row-2").unwrap();
11429 blob_builder.push_bytes(b"batch-1-row-3").unwrap();
11430 let blob_array: ArrayRef = blob_builder.finish().unwrap();
11431
11432 let id_array: ArrayRef = Arc::new(Int32Array::from(vec![0, 1, 2, 3, 4, 5]));
11433 let schema = Arc::new(Schema::new(vec![
11434 Field::new("id", DataType::Int32, false),
11435 crate::blob_field("blob", true),
11436 ]));
11437
11438 let batch = RecordBatch::try_new(schema.clone(), vec![id_array, blob_array]).unwrap();
11439 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
11440
11441 let mut dataset = Dataset::write(
11442 reader,
11443 &test_dir.path_str(),
11444 Some(WriteParams {
11445 data_storage_version: Some(LanceFileVersion::V2_2),
11446 max_rows_per_file: 2,
11447 ..Default::default()
11448 }),
11449 )
11450 .await
11451 .unwrap();
11452
11453 assert_eq!(dataset.get_fragments().len(), 3);
11454
11455 compact_files(
11456 &mut dataset,
11457 CompactionOptions {
11458 target_rows_per_fragment: 1024 * 1024,
11459 batch_size: Some(2),
11460 ..Default::default()
11461 },
11462 None,
11463 )
11464 .await
11465 .unwrap();
11466
11467 assert_eq!(dataset.get_fragments().len(), 1);
11468
11469 let mut blob_values = read_blob_bytes_by_index(&Arc::new(dataset.clone()), "blob").await;
11470 blob_values.sort_by_key(|(id, _)| *id);
11471 assert_eq!(
11472 blob_values,
11473 vec![
11474 (0, Some(b"batch-0-row-0".to_vec())),
11475 (1, Some(b"batch-0-row-1".to_vec())),
11476 (2, Some(b"batch-1-row-0".to_vec())),
11477 (3, None),
11478 (4, Some(b"batch-1-row-2".to_vec())),
11479 (5, Some(b"batch-1-row-3".to_vec()))
11480 ]
11481 );
11482 }
11483 use arrow_array::record_batch;
11489 use lance_file::writer::FileWriterOptions;
11490 use lance_io::utils::CachedFileSize;
11491 use lance_table::format::DataFile;
11492 use lance_table::format::overlay::{DataOverlayFile, OverlayCoverage};
11493 use std::collections::BTreeMap;
11494
11495 use crate::dataset::DATA_DIR;
11496 use crate::dataset::transaction::DataOverlayGroup;
11497
11498 async fn create_base_dataset(uri: &str) -> Dataset {
11501 let batch = record_batch!(
11502 ("id", Int32, (0..12).collect::<Vec<_>>()),
11503 ("val", Int32, (0..12).map(|v| v * 10).collect::<Vec<_>>())
11504 )
11505 .unwrap();
11506 let schema = batch.schema();
11507 let write_params = WriteParams {
11508 max_rows_per_file: 6,
11509 max_rows_per_group: 6,
11510 data_storage_version: Some(LanceFileVersion::Stable),
11511 ..Default::default()
11512 };
11513 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
11514 Dataset::write(reader, uri, Some(write_params))
11515 .await
11516 .unwrap()
11517 }
11518
11519 fn i32_array(values: impl IntoIterator<Item = Option<i32>>) -> ArrayRef {
11520 Arc::new(Int32Array::from_iter(values))
11521 }
11522
11523 fn bitmap(offsets: impl IntoIterator<Item = u32>) -> RoaringBitmap {
11524 RoaringBitmap::from_iter(offsets)
11525 }
11526
11527 async fn commit_overlay(
11530 dataset: Dataset,
11531 fragment_id: u64,
11532 fields: &[i32],
11533 coverage: OverlayCoverage,
11534 columns: Vec<ArrayRef>,
11535 ) -> Dataset {
11536 let read_version = dataset.version().version;
11537 let overlay_schema = dataset.schema().project_by_ids(fields, true);
11538 let filename = format!("{}.lance", Uuid::new_v4());
11539 let path = dataset.base.clone().join(DATA_DIR).join(filename.as_str());
11540 let obj_writer = dataset.object_store.create(&path).await.unwrap();
11541 let file_version = LanceFileVersion::Stable.resolve();
11542 let mut writer = lance_file::versions::create_writer(
11543 file_version,
11544 obj_writer,
11545 overlay_schema,
11546 FileWriterOptions::default(),
11547 )
11548 .unwrap();
11549 for (column_index, array) in columns.into_iter().enumerate() {
11550 writer.write_column(column_index, array).await.unwrap();
11551 }
11552 let summary = writer.finish().await.unwrap();
11553
11554 let mut data_file = DataFile::new_unstarted(filename, file_version);
11555 data_file.fields = writer
11556 .field_id_to_column_indices()
11557 .iter()
11558 .map(|(f, _)| *f as i32)
11559 .collect::<Vec<_>>()
11560 .into();
11561 data_file.column_indices = writer
11562 .field_id_to_column_indices()
11563 .iter()
11564 .map(|(_, c)| *c as i32)
11565 .collect::<Vec<_>>()
11566 .into();
11567 data_file.file_size_bytes = CachedFileSize::new(summary.size_bytes);
11568
11569 Dataset::commit(
11570 WriteDestination::Dataset(Arc::new(dataset)),
11571 Operation::DataOverlay {
11572 groups: vec![DataOverlayGroup {
11573 fragment_id,
11574 overlays: vec![DataOverlayFile {
11575 data_file,
11576 coverage,
11577 committed_version: 0,
11578 }],
11579 }],
11580 },
11581 Some(read_version),
11582 None,
11583 None,
11584 Arc::new(Default::default()),
11585 false,
11586 )
11587 .await
11588 .unwrap()
11589 }
11590
11591 async fn commit_n_overlays(mut dataset: Dataset, n: u32) -> Dataset {
11595 for i in 0..n {
11596 dataset = commit_overlay(
11597 dataset,
11598 0,
11599 &[1],
11600 OverlayCoverage::dense(bitmap([i])),
11601 vec![i32_array([Some(1000 + i as i32)])],
11602 )
11603 .await;
11604 }
11605 dataset
11606 }
11607
11608 fn overlay_only_options(max_overlays_per_fragment: usize) -> CompactionOptions {
11612 CompactionOptions {
11613 max_overlays_per_fragment: Some(max_overlays_per_fragment),
11614 target_rows_per_fragment: 6,
11615 ..Default::default()
11616 }
11617 }
11618
11619 async fn id_val_map(dataset: &Dataset) -> BTreeMap<i32, Option<i32>> {
11621 let mut scanner = dataset.scan();
11622 scanner.project(&["id", "val"]).unwrap();
11623 let batch = scanner.try_into_batch().await.unwrap();
11624 let mut out = BTreeMap::new();
11625 let ids = batch
11626 .column(0)
11627 .as_any()
11628 .downcast_ref::<Int32Array>()
11629 .unwrap();
11630 let vals = batch
11631 .column(1)
11632 .as_any()
11633 .downcast_ref::<Int32Array>()
11634 .unwrap();
11635 for i in 0..batch.num_rows() {
11636 let v = if vals.is_null(i) {
11637 None
11638 } else {
11639 Some(vals.value(i))
11640 };
11641 out.insert(ids.value(i), v);
11642 }
11643 out
11644 }
11645
11646 #[rstest]
11647 #[case::default_target(LanceFileVersion::V2_2, None)]
11648 #[case::explicit_target(LanceFileVersion::V2_2, Some(LanceFileVersion::V2_2))]
11649 #[case::non_default_target(LanceFileVersion::V2_0, Some(LanceFileVersion::V2_2))]
11650 #[tokio::test]
11651 async fn test_compaction_target_preserves_overlays(
11652 #[case] default_version: LanceFileVersion,
11653 #[case] target: Option<LanceFileVersion>,
11654 #[values(
11655 CompactionMode::Reencode,
11656 CompactionMode::TryBinaryCopy,
11657 CompactionMode::ForceBinaryCopy
11658 )]
11659 mode: CompactionMode,
11660 ) {
11661 let batch = record_batch!(
11662 ("id", Int32, [0, 1, 2, 3]),
11663 ("val", Int32, [10, 20, 30, 40])
11664 )
11665 .unwrap();
11666 let mut dataset = Dataset::write(
11667 RecordBatchIterator::new([Ok(RecordBatch::new_empty(batch.schema()))], batch.schema()),
11668 "memory://",
11669 Some(WriteParams {
11670 data_storage_version: Some(default_version),
11671 ..Default::default()
11672 }),
11673 )
11674 .await
11675 .unwrap();
11676 dataset
11677 .append(
11678 RecordBatchIterator::new([Ok(batch.clone())], batch.schema()),
11679 Some(WriteParams {
11680 data_storage_version: Some(LanceFileVersion::V2_2),
11681 max_rows_per_file: 2,
11682 ..Default::default()
11683 }),
11684 )
11685 .await
11686 .unwrap();
11687 let mut dataset = commit_overlay(
11689 dataset,
11690 1,
11691 &[1],
11692 OverlayCoverage::dense(bitmap([0, 1])),
11693 vec![i32_array([Some(999), None])],
11694 )
11695 .await;
11696 let expected = BTreeMap::from([(0, Some(10)), (1, Some(20)), (2, Some(999)), (3, None)]);
11697 assert_eq!(id_val_map(&dataset).await, expected);
11698 let manifest = dataset.manifest.clone();
11699 let options = CompactionOptions {
11700 target_rows_per_fragment: 4,
11701 data_storage_version: target,
11702 compaction_mode: Some(mode),
11703 ..Default::default()
11704 };
11705 let result = compact_files(&mut dataset, options, None).await;
11706 if mode == CompactionMode::ForceBinaryCopy {
11707 let error = result.unwrap_err();
11708 assert!(matches!(error, Error::NotSupported { .. }));
11709 assert!(error.to_string().contains("binary copy is not supported"));
11710 assert_eq!(dataset.manifest, manifest);
11711 } else {
11712 let metrics = result.unwrap();
11713 assert_eq!(metrics.fragments_removed, 2);
11714 assert_eq!(metrics.fragments_added, 1);
11715 assert!(dataset.manifest.fragments[0].overlays.is_empty());
11716 assert_eq!(
11717 dataset.manifest.fragments[0].files[0]
11718 .file_version()
11719 .unwrap(),
11720 ConcreteFileVersion::V2_2
11721 );
11722 }
11723 assert_eq!(id_val_map(&dataset).await, expected);
11724 assert_eq!(
11725 dataset.manifest.data_storage_format.lance_file_format(),
11726 default_version.resolve()
11727 );
11728 dataset.validate().await.unwrap();
11729 }
11730
11731 #[tokio::test]
11732 async fn test_max_overlays_triggers_full_compaction() {
11733 let dataset = create_base_dataset("memory://").await;
11735 let mut dataset = commit_n_overlays(dataset, 3).await;
11736 assert_eq!(
11737 dataset.get_fragment(0).unwrap().metadata().overlays.len(),
11738 3
11739 );
11740
11741 let metrics = compact_files(&mut dataset, overlay_only_options(2), None)
11743 .await
11744 .unwrap();
11745 assert_eq!(metrics.fragments_removed, 1);
11746 assert_eq!(metrics.fragments_added, 1);
11747
11748 let fragments = dataset.get_fragments();
11749 assert_eq!(fragments.len(), 2);
11750 let compacted = fragments
11753 .iter()
11754 .find(|f| f.id() != 1)
11755 .expect("a new fragment id was assigned");
11756 assert!(compacted.metadata().overlays.is_empty());
11757 assert_eq!(compacted.metadata().files.len(), 1);
11758
11759 let values = id_val_map(&dataset).await;
11761 let expected: BTreeMap<i32, Option<i32>> = (0..12)
11762 .map(|id| {
11763 let v = if id < 3 { 1000 + id } else { id * 10 };
11764 (id, Some(v))
11765 })
11766 .collect();
11767 assert_eq!(values, expected);
11768 }
11769
11770 #[tokio::test]
11771 async fn test_below_threshold_is_a_noop() {
11772 let dataset = create_base_dataset("memory://").await;
11773 let mut dataset = commit_n_overlays(dataset, 2).await;
11774
11775 let metrics = compact_files(&mut dataset, overlay_only_options(2), None)
11777 .await
11778 .unwrap();
11779 assert_eq!(metrics.fragments_removed, 0);
11780 assert_eq!(metrics.fragments_added, 0);
11781 assert_eq!(
11782 dataset.get_fragment(0).unwrap().metadata().overlays.len(),
11783 2
11784 );
11785 }
11786
11787 #[tokio::test]
11788 async fn test_overlay_compaction_materializes_deletions() {
11789 let dataset = create_base_dataset("memory://").await;
11790 let mut dataset = commit_n_overlays(dataset, 3).await;
11791 dataset.delete("id = 2").await.unwrap();
11793 assert!(
11794 dataset
11795 .get_fragment(0)
11796 .unwrap()
11797 .metadata()
11798 .deletion_file
11799 .is_some()
11800 );
11801
11802 compact_files(&mut dataset, overlay_only_options(2), None)
11803 .await
11804 .unwrap();
11805
11806 for fragment in dataset.get_fragments() {
11808 assert!(fragment.metadata().deletion_file.is_none());
11809 assert!(fragment.metadata().overlays.is_empty());
11810 }
11811 let values = id_val_map(&dataset).await;
11812 assert!(!values.contains_key(&2));
11813 assert_eq!(values.get(&0), Some(&Some(1000)));
11815 assert_eq!(values.get(&1), Some(&Some(1001)));
11816 }
11817
11818 #[tokio::test]
11819 async fn test_overlay_compaction_reconciles_stale_index() {
11820 let mut dataset = create_base_dataset("memory://").await;
11821 dataset
11823 .create_index(
11824 &["val"],
11825 IndexType::Scalar,
11826 None,
11827 &ScalarIndexParams::default(),
11828 true,
11829 )
11830 .await
11831 .unwrap();
11832
11833 let mut dataset = commit_n_overlays(dataset, 3).await;
11836
11837 let val_index_before = dataset
11838 .load_indices()
11839 .await
11840 .unwrap()
11841 .iter()
11842 .find(|i| i.fields == vec![1])
11843 .expect("val index present")
11844 .clone();
11845 assert!(
11846 val_index_before
11847 .fragment_bitmap
11848 .as_ref()
11849 .unwrap()
11850 .contains(0)
11851 );
11852
11853 compact_files(&mut dataset, overlay_only_options(2), None)
11854 .await
11855 .unwrap();
11856
11857 let indices = dataset.load_indices().await.unwrap();
11860 let val_index = indices
11861 .iter()
11862 .find(|i| i.fields == vec![1])
11863 .expect("val index present");
11864 let compacted_id = dataset
11865 .get_fragments()
11866 .iter()
11867 .map(|f| f.id() as u32)
11868 .find(|id| *id != 1)
11869 .unwrap();
11870 assert!(
11871 !val_index
11872 .fragment_bitmap
11873 .as_ref()
11874 .unwrap()
11875 .contains(compacted_id),
11876 "stale index must drop the compacted fragment from its coverage"
11877 );
11878
11879 let mut scanner = dataset.scan();
11882 scanner
11883 .filter("val = 1000")
11884 .unwrap()
11885 .project(&["id"])
11886 .unwrap();
11887 let batch = scanner.try_into_batch().await.unwrap();
11888 let ids = batch
11889 .column(0)
11890 .as_any()
11891 .downcast_ref::<Int32Array>()
11892 .unwrap();
11893 assert_eq!(ids.len(), 1);
11894 assert_eq!(ids.value(0), 0);
11895
11896 let mut scanner = dataset.scan();
11897 scanner.filter("val = 0").unwrap().project(&["id"]).unwrap();
11898 let batch = scanner.try_into_batch().await.unwrap();
11899 assert_eq!(batch.num_rows(), 0, "stale value 0 must no longer match");
11900 }
11901
11902 #[tokio::test]
11908 async fn test_normalize_source_fragments_keeps_fully_deleted() {
11909 use lance_table::format::{DeletionFile, DeletionFileType};
11910 let dataset = lance_datagen::gen_batch()
11911 .col("i", lance_datagen::array::step::<Int32Type>())
11912 .into_ram_dataset(FragmentCount::from(1), FragmentRowCount::from(4))
11913 .await
11914 .unwrap();
11915 let mut fragment = Fragment::new(7);
11916 fragment.physical_rows = Some(4);
11917 fragment.deletion_file = Some(DeletionFile {
11918 read_version: 1,
11919 id: 1,
11920 file_type: DeletionFileType::Array,
11921 num_deleted_rows: Some(4),
11922 base_id: None,
11923 });
11924 let normalized = normalize_source_fragments(&dataset, &[fragment])
11925 .await
11926 .unwrap();
11927 assert_eq!(normalized.len(), 1);
11928 assert_eq!(normalized[0].id, 7);
11929 assert_eq!(normalized[0].physical_rows, Some(4));
11930 assert_eq!(
11931 normalized[0]
11932 .deletion_file
11933 .as_ref()
11934 .unwrap()
11935 .num_deleted_rows,
11936 Some(4)
11937 );
11938 }
11939}