1use crate::feature_flags::{
14 FLAG_COVERED_INDEX_METADATA, FLAG_FRAGMENT_REUSE_INDEX, FLAG_STABLE_ROW_IDS,
15 apply_feature_flags, ensure_can_read_manifest, ensure_can_write_manifest,
16 inherit_sticky_feature_flags,
17};
18use crate::format::overlay::{OverlayCoverage, TOMBSTONE_FIELD_ID};
19use crate::format::{
20 DataFile, DataStorageFormat, Fragment, IndexMetadata, Manifest, ManifestBuildConfig, RowIdMeta,
21 overlay::DataOverlayFile,
22};
23use crate::io::{
24 commit::CommitHandler,
25 manifest::{read_manifest, read_manifest_indexes},
26};
27use crate::rowids::version::build_version_meta;
28use crate::rowids::{read_row_ids, write_row_ids};
29use crate::system_index::frag_reuse::metadata::{is_tagged, validate_flags};
30use crate::system_index::frag_reuse::{FRAG_REUSE_INDEX_NAME, is_frag_reuse_index_entry};
31use crate::system_index::is_system_index;
32use crate::system_index::mem_wal::{
33 CompactedSsTable, IndexCatchupProgress, MEM_WAL_INDEX_NAME, load_mem_wal_index_details,
34 new_mem_wal_index_meta, update_mem_wal_index_compacted_sstables,
35};
36use crate::transaction::UpdateMode::{RewriteColumns, RewriteRows};
37use crate::transaction::prepare::{FragReuseUpdate, PreparedIndices};
38use crate::transaction::row_version::resolve_update_version_metadata;
39use crate::transaction::update_map::apply_update_map;
40use crate::transaction::validate::merge_fragment_physically_rewritten;
41use crate::transaction::{
42 CoverageIdentity, DataReplacementGroup, LogicalIndexSegments, Operation, ReadVersionState,
43 RewriteGroup, Transaction, UpdatedFragmentOffsets,
44};
45use lance_core::datatypes::{
46 LANCE_UNENFORCED_CLUSTERING_KEY_POSITION, LANCE_UNENFORCED_PRIMARY_KEY,
47 LANCE_UNENFORCED_PRIMARY_KEY_POSITION,
48};
49use lance_core::utils::parse::str_is_truthy;
50use lance_core::{Error, Result};
51use lance_file::version::ConcreteFileVersion;
52use lance_io::object_store::ObjectStore;
53use object_store::path::Path;
54use roaring::RoaringBitmap;
55use std::collections::{BTreeMap, HashMap, HashSet};
56use std::sync::Arc;
57use uuid::Uuid;
58
59pub const RANGE_SEGMENTS_CONFIG_KEY: &str = "lance.row_ids.range_segments";
74
75fn range_segments_enabled(manifest: &Manifest) -> bool {
76 manifest
77 .config
78 .get(RANGE_SEGMENTS_CONFIG_KEY)
79 .is_some_and(|value| str_is_truthy(value))
80}
81
82fn apply_range_segments(
90 manifest: &mut Manifest,
91 current_manifest: Option<&Manifest>,
92) -> Result<()> {
93 if !range_segments_enabled(manifest) {
94 return Ok(());
95 }
96 let unchanged: HashMap<u64, *const u8> = current_manifest
97 .filter(|current| range_segments_enabled(current))
98 .map(|current| {
99 current
100 .fragments
101 .iter()
102 .filter_map(|fragment| match &fragment.row_id_meta {
103 Some(RowIdMeta::Inline(data)) => Some((fragment.id, data.as_ptr())),
104 _ => None,
105 })
106 .collect()
107 })
108 .unwrap_or_default();
109 let fragments = Arc::make_mut(&mut manifest.fragments);
110 for fragment in fragments.iter_mut() {
111 let Some(RowIdMeta::Inline(data)) = &fragment.row_id_meta else {
112 continue;
113 };
114 if unchanged.get(&fragment.id) == Some(&data.as_ptr()) {
115 continue;
116 }
117 let mut sequence = read_row_ids(&data[..])?;
118 if sequence.use_range_segments() {
119 fragment.row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&sequence).into()));
120 }
121 }
122 Ok(())
123}
124
125impl Transaction {
126 pub(super) fn fragments_with_ids<'a, T>(
127 new_fragments: T,
128 fragment_id: &'a mut u64,
129 ) -> impl Iterator<Item = Fragment> + 'a
130 where
131 T: IntoIterator<Item = Fragment> + 'a,
132 {
133 new_fragments.into_iter().map(move |mut f| {
134 if f.id == 0 {
135 f.id = *fragment_id;
136 *fragment_id += 1;
137 }
138 f
139 })
140 }
141
142 fn data_storage_format_from_files(
143 fragments: &[Fragment],
144 user_requested: Option<ConcreteFileVersion>,
145 ) -> Result<DataStorageFormat> {
146 let version = match user_requested {
149 Some(ConcreteFileVersion::V1) => {
150 if let Some(actual) = Fragment::try_infer_version(fragments)?
152 && actual != ConcreteFileVersion::V1
153 {
154 return Err(Error::invalid_input(format!(
155 "User requested data storage version ({}) does not match version in data files ({actual})",
156 ConcreteFileVersion::V1
157 )));
158 }
159 Some(ConcreteFileVersion::V1)
160 }
161 Some(version) => Some(version),
162 None => Fragment::try_infer_version(fragments)?,
163 };
164 Ok(version.map(DataStorageFormat::new).unwrap_or_default())
165 }
166
167 pub async fn restore_old_manifest(
168 object_store: &ObjectStore,
169 commit_handler: &dyn CommitHandler,
170 base_path: &Path,
171 version: u64,
172 config: &ManifestBuildConfig,
173 tx_path: &str,
174 current_manifest: &Manifest,
175 ) -> Result<(Manifest, Vec<IndexMetadata>)> {
176 let location = commit_handler
177 .resolve_version_location(base_path, version, &object_store.inner)
178 .await?;
179 let mut manifest = read_manifest(object_store, &location.path, location.size).await?;
180 ensure_can_read_manifest(&manifest)?;
184 ensure_can_write_manifest(&manifest)?;
185 manifest.set_timestamp(config.timestamp_nanos);
186 manifest.transaction_file = Some(tx_path.to_string());
187 let indices = read_manifest_indexes(object_store, &location, &manifest).await?;
188 validate_flags(&manifest, &indices)?;
189 manifest.max_fragment_id = manifest
190 .max_fragment_id
191 .max(current_manifest.max_fragment_id);
192 manifest.next_row_id = manifest.next_row_id.max(current_manifest.next_row_id);
194 if current_manifest.uses_stable_row_ids() && !manifest.uses_stable_row_ids() {
197 return Err(Error::invalid_input(format!(
198 "Cannot restore version {version}: stable row ids were enabled \
199 after it, and turning them back off would let row addresses \
200 collide with ids this table has already used"
201 )));
202 }
203 inherit_sticky_feature_flags(&mut manifest, current_manifest)?;
204 Ok((manifest, indices))
205 }
206
207 pub fn logical_index_segments(indices: &[IndexMetadata]) -> LogicalIndexSegments {
213 let mut by_name: LogicalIndexSegments = BTreeMap::new();
214 for idx in indices.iter().filter(|idx| !is_system_index(idx)) {
215 by_name
216 .entry(idx.name.clone())
217 .or_default()
218 .push(CoverageIdentity {
219 uuid: idx.uuid,
220 fragment_bitmap: idx.fragment_bitmap.clone(),
221 });
222 }
223 for segments in by_name.values_mut() {
224 segments.sort_unstable_by_key(|segment| segment.uuid);
225 }
226 by_name
227 }
228
229 pub fn apply_mem_wal_index_coverage(
253 final_indices: &mut [IndexMetadata],
254 segments_before: &LogicalIndexSegments,
255 read_version_state: Option<ReadVersionState<'_>>,
256 new_version: u64,
257 ) -> Result<()> {
258 let Some(pos) = final_indices
259 .iter()
260 .position(|idx| idx.name == MEM_WAL_INDEX_NAME)
261 else {
262 return Ok(());
265 };
266
267 let mut details = load_mem_wal_index_details(final_indices[pos].clone())?;
268
269 if details.compacted_sstables.is_empty() && details.index_catchup.is_empty() {
272 return Ok(());
273 }
274
275 let segments_after = Self::logical_index_segments(final_indices);
276 let catchup_before = std::mem::take(&mut details.index_catchup);
277
278 let read_details = read_version_state
284 .map(|state| {
285 state
286 .indices
287 .iter()
288 .find(|idx| idx.name == MEM_WAL_INDEX_NAME)
289 .cloned()
290 .map(load_mem_wal_index_details)
291 .transpose()
292 })
293 .transpose()?
294 .flatten();
295 let shards: Vec<(Uuid, u64, u64)> = details
296 .compacted_sstables
297 .iter()
298 .map(|committed| {
299 let at_read = read_details
300 .as_ref()
301 .and_then(|read| {
302 read.compacted_sstables
303 .iter()
304 .find(|s| s.shard_id == committed.shard_id)
305 })
306 .map_or(0, |s| s.generation);
307 (
308 committed.shard_id,
309 committed.generation,
310 at_read.min(committed.generation),
311 )
312 })
313 .collect();
314
315 let read_fragments: Option<RoaringBitmap> = read_version_state.map(|state| {
318 state
319 .manifest
320 .fragments
321 .iter()
322 .map(|fragment| fragment.id as u32)
323 .collect()
324 });
325
326 let covers_read_version = |segments: &[CoverageIdentity]| -> bool {
327 let Some(required) = read_fragments.as_ref() else {
328 return false;
329 };
330 if required.is_empty() {
331 return false;
340 }
341 let mut covered = RoaringBitmap::new();
342 for segment in segments {
343 match segment.fragment_bitmap.as_ref() {
344 Some(bitmap) => covered |= bitmap,
345 None => return false,
347 }
348 }
349 required.is_subset(&covered)
350 };
351
352 let mut rebuilt: Vec<IndexCatchupProgress> = Vec::new();
353 for (name, after) in segments_after.iter() {
354 let unchanged = segments_before.get(name) == Some(after);
359 let carried = unchanged
360 .then(|| catchup_before.iter().find(|e| e.index_name == *name))
361 .flatten();
362 let proven = covers_read_version(after);
363
364 if carried.is_none() && !proven {
365 continue;
368 }
369
370 let generations = shards
371 .iter()
372 .map(|&(shard_id, committed, creditable)| {
373 let prior = carried
374 .and_then(|entry| entry.caught_up_generation_for_shard(&shard_id))
375 .unwrap_or(0);
376 let credited = if proven { creditable } else { 0 };
377 CompactedSsTable::new(shard_id, prior.max(credited).min(committed))
384 })
385 .collect::<Vec<_>>();
386 if generations.iter().all(|g| g.generation == 0) {
387 continue;
388 }
389 rebuilt.push(IndexCatchupProgress::new(name.clone(), generations));
390 }
391 rebuilt.sort_by(|a, b| a.index_name.cmp(&b.index_name));
392
393 let mut before_sorted = catchup_before;
394 before_sorted.sort_by(|a, b| a.index_name.cmp(&b.index_name));
395 if rebuilt == before_sorted {
396 return Ok(());
397 }
398
399 let dropped: Vec<&str> = before_sorted
400 .iter()
401 .map(|e| e.index_name.as_str())
402 .filter(|name| !rebuilt.iter().any(|kept| kept.index_name == *name))
403 .collect();
404 if !dropped.is_empty() {
405 log::info!(
407 "MemWAL index catch-up invalidated at version {new_version} for {dropped:?}: \
408 these indices changed and no longer cover the version this commit read"
409 );
410 }
411
412 details.index_catchup = rebuilt;
413 final_indices[pos] = new_mem_wal_index_meta(new_version, details)?;
414 Ok(())
415 }
416
417 pub fn withdraw_coverage_invalidated_after_build(
425 indices: &mut [IndexMetadata],
426 changed: &[String],
427 new_version: u64,
428 ) -> Result<()> {
429 if changed.is_empty() {
430 return Ok(());
431 }
432 let Some(pos) = indices
433 .iter()
434 .position(|idx| idx.name == MEM_WAL_INDEX_NAME)
435 else {
436 return Ok(());
437 };
438 let mut details = load_mem_wal_index_details(indices[pos].clone())?;
439 let before = details.index_catchup.len();
440 details
441 .index_catchup
442 .retain(|entry| !changed.contains(&entry.index_name));
443 if details.index_catchup.len() == before {
444 return Ok(());
445 }
446 log::info!(
447 "MemWAL index catch-up withdrawn at version {new_version} for {changed:?}: \
448 these indices were recalculated after their coverage was derived"
449 );
450 indices[pos] = new_mem_wal_index_meta(new_version, details)?;
451 Ok(())
452 }
453
454 pub fn prepare_indices(
475 &self,
476 current_manifest: Option<&Manifest>,
477 current_indices: Vec<IndexMetadata>,
478 config: &ManifestBuildConfig,
479 frag_reuse_ledger: Option<Arc<crate::system_index::frag_reuse::ledger::FragReuseLedger>>,
480 frag_reuse: FragReuseUpdate,
481 ) -> Result<PreparedIndices> {
482 crate::system_index::frag_reuse::gate::classify(
483 &self.operation,
484 current_manifest,
485 ¤t_indices,
486 &frag_reuse,
487 config.migration_next_row_id,
488 )?;
489 let mut prepared = current_indices.clone();
490 if let Some(manifest) = current_manifest {
491 let rewrites = Self::rewritten_physical_columns(
492 &self.operation,
493 &manifest.schema,
494 &manifest.fragments,
495 );
496 if !rewrites.is_empty() {
497 let lineage =
498 crate::system_index::frag_reuse::lineage::TaggedLineage::from_indices_with_ledger(
499 ¤t_indices,
500 frag_reuse_ledger,
501 )?;
502 let live: RoaringBitmap = manifest
503 .fragments
504 .iter()
505 .filter_map(|fragment| u32::try_from(fragment.id).ok())
506 .collect();
507 Self::withdraw_in_place_rewrites(
508 &mut prepared,
509 rewrites,
510 &manifest.schema,
511 &live,
512 lineage.as_ref(),
513 );
514 }
515 }
516 Ok(PreparedIndices::new(
517 current_manifest.map(|manifest| manifest.version),
518 current_indices,
519 prepared,
520 frag_reuse,
521 ))
522 }
523
524 pub fn build_manifest(
528 &self,
529 current_manifest: Option<&Manifest>,
530 current_indices: Vec<IndexMetadata>,
531 transaction_file_path: &str,
532 config: &ManifestBuildConfig,
533 ) -> Result<(Manifest, Vec<IndexMetadata>)> {
534 self.build_manifest_with_read_version(
535 current_manifest,
536 current_indices,
537 transaction_file_path,
538 config,
539 None,
540 )
541 }
542
543 pub fn build_manifest_with_read_version(
557 &self,
558 current_manifest: Option<&Manifest>,
559 current_indices: Vec<IndexMetadata>,
560 transaction_file_path: &str,
561 config: &ManifestBuildConfig,
562 read_version_state: Option<ReadVersionState<'_>>,
563 ) -> Result<(Manifest, Vec<IndexMetadata>)> {
564 let prepared = self.prepare_indices(
565 current_manifest,
566 current_indices,
567 config,
568 None,
569 FragReuseUpdate::None,
570 )?;
571 self.build_manifest_prepared(
572 current_manifest,
573 prepared,
574 transaction_file_path,
575 config,
576 read_version_state,
577 )
578 }
579
580 pub fn build_manifest_prepared(
586 &self,
587 current_manifest: Option<&Manifest>,
588 prepared: PreparedIndices,
589 transaction_file_path: &str,
590 config: &ManifestBuildConfig,
591 read_version_state: Option<ReadVersionState<'_>>,
592 ) -> Result<(Manifest, Vec<IndexMetadata>)> {
593 if prepared.manifest_version() != current_manifest.map(|manifest| manifest.version) {
594 return Err(Error::invalid_input(format!(
595 "the index list was prepared against manifest version {:?} but the manifest \
596 being built on is at version {:?}; every commit attempt prepares against the \
597 manifest it builds on",
598 prepared.manifest_version(),
599 current_manifest.map(|manifest| manifest.version)
600 )));
601 }
602 crate::system_index::frag_reuse::gate::classify(
605 &self.operation,
606 current_manifest,
607 prepared.original(),
608 prepared.frag_reuse_update(),
609 config.migration_next_row_id,
610 )?;
611 let (original_indices, prepared_indices, frag_reuse) = prepared.into_parts();
612 if config.use_stable_row_ids
613 && config.migration_next_row_id.is_none()
614 && current_manifest
615 .map(|m| !m.uses_stable_row_ids())
616 .unwrap_or_default()
617 {
618 return Err(Error::not_supported_source(
619 "This dataset was not created with the stable row ids feature. Please run `migrate_to_stable_row_ids` before attempting to use stable row ids".into(),
620 ));
621 }
622
623 let blocking_indices: Vec<&str> = original_indices
631 .iter()
632 .filter(|idx| idx.name != FRAG_REUSE_INDEX_NAME)
633 .map(|idx| idx.name.as_str())
634 .collect();
635 if config.migration_next_row_id.is_some() && !blocking_indices.is_empty() {
636 return Err(Error::invalid_input(format!(
637 "Cannot migrate to stable row IDs while indexes exist on the dataset. \
638 Drop the following indexes first, then re-run the migration, and \
639 recreate them afterwards: {}",
640 blocking_indices.join(", ")
641 )));
642 }
643 let mut reference_paths = match current_manifest {
644 Some(m) => m.base_paths.clone(),
645 None => HashMap::new(),
646 };
647
648 if let Operation::Overwrite {
649 initial_bases: Some(initial_bases),
650 ..
651 } = &self.operation
652 {
653 if current_manifest.is_none() {
654 for base_path in initial_bases.iter() {
658 if reference_paths.contains_key(&base_path.id) {
659 return Err(Error::invalid_input(format!(
660 "Duplicate base path ID {} detected. Base path IDs must be unique.",
661 base_path.id
662 )));
663 }
664 reference_paths.insert(base_path.id, base_path.clone());
665 }
666 } else {
667 return Err(Error::invalid_input(
670 "OVERWRITE mode cannot register new bases. This should have been caught by validation.",
671 ));
672 }
673 }
674
675 let mut schema = match self.operation {
677 Operation::Overwrite { ref schema, .. } => schema.clone(),
678 Operation::Merge { ref schema, .. } => schema.clone(),
679 Operation::Project { ref schema, .. } => schema.clone(),
680 _ => {
681 if let Some(current_manifest) = current_manifest {
682 current_manifest.schema.clone()
683 } else {
684 return Err(Error::internal(
685 "Cannot create a new dataset without a schema".to_string(),
686 ));
687 }
688 }
689 };
690
691 let mut fragment_id = current_manifest
696 .and_then(|m| m.max_fragment_id())
697 .map(|id| id + 1)
698 .unwrap_or(0);
699 let mut final_fragments = Vec::new();
700 let mut final_indices = prepared_indices;
701
702 if config.migration_next_row_id.is_some() {
706 final_indices.retain(|idx| idx.name != FRAG_REUSE_INDEX_NAME);
707 }
708
709 let mem_wal_segments_before = original_indices
715 .iter()
716 .any(|idx| idx.name == MEM_WAL_INDEX_NAME)
717 .then(|| Self::logical_index_segments(&original_indices));
718 drop(original_indices);
719
720 let mut next_row_id = {
721 match (current_manifest, config.use_stable_row_ids) {
724 (Some(manifest), _) if manifest.reader_feature_flags & FLAG_STABLE_ROW_IDS != 0 => {
725 Some(manifest.next_row_id)
726 }
727 (None, true) => Some(0),
728 (_, false) => None,
729 (Some(_), true) => {
730 if let Some(migration_nri) = config.migration_next_row_id {
732 Some(migration_nri)
733 } else {
734 return Err(Error::not_supported_source(
735 "This dataset was not created with the stable row ids feature. Please run `migrate_to_stable_row_ids` before attempting to use stable row ids".into(),
736 ));
737 }
738 }
739 }
740 };
741
742 let maybe_existing_fragments =
743 current_manifest
744 .map(|m| m.fragments.as_ref())
745 .ok_or_else(|| {
746 Error::internal(format!(
747 "No current manifest was provided while building manifest for operation {}",
748 self.operation.name()
749 ))
750 });
751
752 let new_version = current_manifest.map_or(1, |m| m.version + 1);
753
754 match &self.operation {
755 Operation::Clone { .. } => {
756 return Err(Error::internal(
757 "Clone operation should not enter build_manifest.".to_string(),
758 ));
759 }
760 Operation::Append { fragments } => {
761 final_fragments.extend(maybe_existing_fragments?.clone());
762 let mut new_fragments =
763 Self::fragments_with_ids(fragments.clone(), &mut fragment_id)
764 .collect::<Vec<_>>();
765 if let Some(next_row_id) = &mut next_row_id {
766 Self::assign_row_ids(next_row_id, new_fragments.as_mut_slice())?;
767 for fragment in new_fragments.iter_mut() {
769 let version_meta = build_version_meta(fragment, new_version);
770 fragment.last_updated_at_version_meta = version_meta.clone();
771 fragment.created_at_version_meta = version_meta;
772 }
773 }
774 final_fragments.extend(new_fragments);
775 }
776 Operation::Delete {
777 updated_fragments,
778 deleted_fragment_ids,
779 ..
780 } => {
781 let deleted_ids: HashSet<u64> = deleted_fragment_ids.iter().copied().collect();
784 let updated_by_id: HashMap<u64, &Fragment> =
785 updated_fragments.iter().map(|f| (f.id, f)).collect();
786 final_fragments.extend(maybe_existing_fragments?.clone());
787 final_fragments.retain(|f| !deleted_ids.contains(&f.id));
788 final_fragments.iter_mut().for_each(|f| {
789 if let Some(updated) = updated_by_id.get(&f.id) {
790 f.deletion_file = updated.deletion_file.clone();
794 }
795 });
796 Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments)
797 }
798 Operation::Update {
799 removed_fragment_ids,
800 updated_fragments,
801 new_fragments,
802 fields_modified,
803 compacted_sstables,
804 fields_for_preserving_frag_bitmap,
805 update_mode,
806 updated_fragment_offsets,
807 ..
808 } => {
809 let existing_fragments = maybe_existing_fragments?;
811
812 let removed_ids: HashSet<u64> = removed_fragment_ids.iter().copied().collect();
815 let mut updated_by_id: HashMap<u64, &Fragment> =
816 HashMap::with_capacity(updated_fragments.len());
817 for fragment in updated_fragments {
818 updated_by_id.entry(fragment.id).or_insert(fragment);
819 }
820 let live_field_ids = schema
821 .fields_pre_order()
822 .map(|field| field.id)
823 .collect::<HashSet<_>>();
824 let updated_frags: Vec<Fragment> = existing_fragments
825 .iter()
826 .filter_map(|f| {
827 if removed_ids.contains(&f.id) {
828 return None;
829 }
830 if let Some(&updated) = updated_by_id.get(&f.id) {
831 let mut updated = updated.clone();
832 updated.overlays = f.overlays.clone();
838 let spilled = updated.spilled_row_lineage_field_ids();
843 updated.files.retain(|file| {
844 file.fields.iter().any(|field_id| {
845 live_field_ids.contains(field_id) || spilled.contains(field_id)
846 })
847 });
848 if matches!(update_mode, Some(RewriteColumns)) {
849 crate::format::overlay::tombstone_overlay_fields(
850 &mut updated.overlays,
851 fields_modified,
852 );
853 }
854 Some(updated)
855 } else {
856 Some(f.clone())
857 }
858 })
859 .collect();
860
861 if next_row_id.is_some() {
866 }
868
869 final_fragments.extend(updated_frags);
870
871 if next_row_id.is_some()
872 && matches!(update_mode, Some(RewriteColumns))
873 && let Some(UpdatedFragmentOffsets(off_map)) = updated_fragment_offsets
874 && !off_map.is_empty()
875 {
876 let prev_version = current_manifest.map(|m| m.version).unwrap_or(0);
877 for fragment in final_fragments.iter_mut() {
878 let Some(bitmap) = off_map.get(&fragment.id) else {
879 continue;
880 };
881 if !updated_by_id.contains_key(&fragment.id) {
886 continue;
887 }
888 if bitmap.is_empty() {
889 continue;
890 }
891 if fragment.last_updated_at_version_meta.is_none() {
895 continue;
896 }
897 let max_allowed = existing_fragments
898 .iter()
899 .find(|f| f.id == fragment.id)
900 .and_then(|f| f.physical_rows)
901 .unwrap_or(1 << 24);
902 if bitmap.len() as usize > max_allowed {
903 return Err(Error::invalid_input(format!(
904 "updatedFragmentOffsets cardinality {} exceeds fragment {} limit {}",
905 bitmap.len(),
906 fragment.id,
907 max_allowed
908 )));
909 }
910 if let Some(max_off) = bitmap.max()
911 && max_off as usize >= max_allowed
912 {
913 return Err(Error::invalid_input(format!(
914 "updatedFragmentOffsets max offset {} exceeds fragment {} limit {}",
915 max_off, fragment.id, max_allowed
916 )));
917 }
918 let offsets: Vec<usize> = bitmap.iter().map(|o| o as usize).collect();
919 crate::rowids::version::refresh_row_latest_update_meta_for_partial_frag_rewrite_cols(
920 fragment,
921 &offsets,
922 new_version,
923 prev_version,
924 &config.spilled_row_lineage,
925 )?;
926 }
927 }
928
929 let mut new_fragments =
933 Self::fragments_with_ids(new_fragments.clone(), &mut fragment_id)
934 .collect::<Vec<_>>();
935 for fragment in &mut new_fragments {
937 let spilled = fragment.spilled_row_lineage_field_ids();
938 fragment.files.retain(|file| {
939 file.fields.iter().any(|field_id| {
940 live_field_ids.contains(field_id) || spilled.contains(field_id)
941 })
942 });
943 }
944
945 if let Some(next_row_id) = &mut next_row_id {
948 Self::assign_row_ids(next_row_id, new_fragments.as_mut_slice())?;
949 }
950
951 if next_row_id.is_some() {
952 resolve_update_version_metadata(
953 existing_fragments,
954 new_fragments.as_mut_slice(),
955 new_version,
956 &config.spilled_row_lineage,
957 )?;
958 }
959
960 if config.use_stable_row_ids
961 && update_mode.is_some()
962 && *update_mode == Some(RewriteRows)
963 {
964 let pure_updated_frag_ids =
965 Self::collect_pure_rewrite_row_update_frags_ids(&new_fragments)?;
966
967 let original_fragment_ids: Vec<u64> = removed_fragment_ids
969 .iter()
970 .chain(updated_fragments.iter().map(|f| &f.id))
971 .copied()
972 .collect();
973
974 let original_overlaid_frags: HashMap<u32, &Fragment> = existing_fragments
979 .iter()
980 .filter(|f| {
981 (removed_ids.contains(&f.id) || updated_by_id.contains_key(&f.id))
982 && !f.overlays.is_empty()
983 })
984 .map(|f| (f.id as u32, f))
985 .collect();
986
987 Self::register_pure_rewrite_rows_update_frags_in_indices(
988 &mut final_indices,
989 &pure_updated_frag_ids,
990 &original_fragment_ids,
991 fields_for_preserving_frag_bitmap,
992 &original_overlaid_frags,
993 &schema,
994 )?;
995 }
996
997 if let Some(next_row_id) = &mut next_row_id {
998 Self::assign_row_ids(next_row_id, new_fragments.as_mut_slice())?;
999 }
1003 let mut target_ids: HashSet<u64> = HashSet::new();
1005 target_ids.extend(new_fragments.iter().map(|f| f.id));
1006 final_fragments.extend(new_fragments);
1007 Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments);
1008
1009 if !compacted_sstables.is_empty() {
1010 update_mem_wal_index_compacted_sstables(
1011 &mut final_indices,
1012 new_version,
1013 compacted_sstables.clone(),
1014 )?;
1015 }
1016 }
1017 Operation::Overwrite { fragments, .. } => {
1018 let mut new_fragments = fragments.clone();
1024 for fragment in new_fragments.iter_mut() {
1025 fragment.id = fragment_id;
1026 fragment_id += 1;
1027 }
1028 if let Some(next_row_id) = &mut next_row_id {
1029 Self::assign_row_ids(next_row_id, new_fragments.as_mut_slice())?;
1030 for fragment in new_fragments.iter_mut() {
1032 let version_meta = build_version_meta(fragment, new_version);
1033 fragment.last_updated_at_version_meta = version_meta.clone();
1034 fragment.created_at_version_meta = version_meta;
1035 }
1036 }
1037 final_fragments.extend(new_fragments);
1038 final_indices = Vec::new();
1039 }
1040 Operation::Rewrite {
1041 groups,
1042 rewritten_indices,
1043 frag_reuse_index,
1044 } => {
1045 final_fragments.extend(maybe_existing_fragments?.clone());
1046 let current_version = current_manifest.map(|m| m.version).unwrap_or_default();
1047 Self::handle_rewrite_fragments(
1048 &mut final_fragments,
1049 groups,
1050 &mut fragment_id,
1051 current_version,
1052 next_row_id.as_ref(),
1053 )?;
1054
1055 let assembly = match &frag_reuse {
1063 FragReuseUpdate::Rewrite(assembly) => Some(assembly.as_ref()),
1064 _ => None,
1065 };
1066 let ordered_groups = Self::ordered_rewrite_groups(
1067 groups,
1068 assembly.map(|assembly| &assembly.reordered_sources),
1069 )?;
1070
1071 if next_row_id.is_some() {
1072 debug_assert!(rewritten_indices.is_empty());
1074 for index in final_indices.iter_mut() {
1075 if is_frag_reuse_index_entry(index) {
1078 continue;
1079 }
1080 let results_are_row_addrs = index.results_are_row_addrs();
1081 if let Some(fragment_bitmap) = &mut index.fragment_bitmap {
1082 *fragment_bitmap = if results_are_row_addrs {
1083 Self::drop_rewritten_fragments(fragment_bitmap, &ordered_groups)
1092 } else {
1093 Self::recalculate_fragment_bitmap(fragment_bitmap, &ordered_groups)?
1094 };
1095 }
1096 }
1097 } else {
1098 Self::handle_rewrite_indices(
1099 &mut final_indices,
1100 rewritten_indices,
1101 &ordered_groups,
1102 )?;
1103 }
1104
1105 Self::prune_overlay_stale_fields_from_indices(&mut final_indices, groups);
1110
1111 match (frag_reuse_index, assembly) {
1112 (_, Some(assembly)) => {
1117 let existing_version = final_indices
1130 .iter()
1131 .find(|idx| idx.name == FRAG_REUSE_INDEX_NAME)
1132 .map(|idx| idx.dataset_version);
1133 if existing_version != assembly.base_entry_version {
1134 return Err(Error::invalid_input(format!(
1135 "the {} index entry in this manifest is at version {:?} but the \
1136 rewrite's entry was assembled against version {:?}; rebuild it \
1137 through the lance commit path, which assembles the entry against \
1138 the manifest it commits to",
1139 FRAG_REUSE_INDEX_NAME,
1140 existing_version,
1141 assembly.base_entry_version,
1142 )));
1143 }
1144 final_indices.retain(|idx| idx.name != assembly.entry.name);
1145 final_indices.push(assembly.entry.clone());
1146 }
1147 (Some(entry), None) => {
1151 final_indices.retain(|idx| idx.name != entry.name);
1152 final_indices.push(entry.clone());
1153 }
1154 (None, None) => {}
1155 }
1156 }
1157 Operation::CreateIndex {
1158 new_indices,
1159 removed_indices,
1160 ..
1161 } => {
1162 final_fragments.extend(maybe_existing_fragments?.clone());
1163 let removed_uuids = removed_indices
1164 .iter()
1165 .map(|old_index| old_index.uuid)
1166 .collect::<HashSet<_>>();
1167 let new_uuids = new_indices
1168 .iter()
1169 .map(|new_index| new_index.uuid)
1170 .collect::<HashSet<_>>();
1171 final_indices.retain(|existing_index| {
1172 !removed_uuids.contains(&existing_index.uuid)
1173 && !new_uuids.contains(&existing_index.uuid)
1174 });
1175 for new_index in new_indices {
1176 new_index.validate_covering_fields()?;
1177 }
1178 final_indices.extend(new_indices.clone());
1179 }
1180 Operation::ReserveFragments { .. } | Operation::UpdateConfig { .. } => {
1181 final_fragments.extend(maybe_existing_fragments?.clone());
1182 }
1183 Operation::Merge { fragments, .. } => {
1184 let existing_fragments = maybe_existing_fragments?;
1185 let mut merged_fragments = fragments.clone();
1186 if next_row_id.is_some() {
1187 let prev_by_id: HashMap<u64, &Fragment> =
1188 existing_fragments.iter().map(|f| (f.id, f)).collect();
1189 for fragment in merged_fragments.iter_mut() {
1190 match prev_by_id.get(&fragment.id) {
1191 Some(prev) => {
1192 if merge_fragment_physically_rewritten(prev, fragment) {
1193 crate::rowids::version::refresh_row_latest_update_meta_for_full_frag_rewrite_cols(
1194 fragment,
1195 new_version,
1196 )?;
1197 }
1198 }
1199 None => {
1200 crate::rowids::version::refresh_row_latest_update_meta_for_full_frag_rewrite_cols(
1204 fragment,
1205 new_version,
1206 )?;
1207 fragment.created_at_version_meta =
1208 fragment.last_updated_at_version_meta.clone();
1209 }
1210 }
1211 }
1212 }
1213 final_fragments.extend(merged_fragments);
1214
1215 Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments)
1222 }
1223 Operation::Project { .. } => {
1224 final_fragments.extend(maybe_existing_fragments?.clone());
1225
1226 let remaining_field_ids = schema
1231 .fields_pre_order()
1232 .map(|f| f.id)
1233 .collect::<HashSet<_>>();
1234 for fragment in final_fragments.iter_mut() {
1235 let spilled = fragment.spilled_row_lineage_field_ids();
1236 fragment.files.retain(|file| {
1237 file.fields.iter().any(|field_id| {
1238 remaining_field_ids.contains(field_id) || spilled.contains(field_id)
1239 })
1240 });
1241 }
1242
1243 Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments)
1246 }
1247 Operation::Restore { .. } => {
1248 unreachable!()
1249 }
1250 Operation::DataReplacement { replacements } => {
1251 log::warn!(
1252 "Building manifest with DataReplacement operation. This operation is not stable yet, please use with caution."
1253 );
1254
1255 let (old_fragment_ids, new_datafiles): (Vec<&u64>, Vec<&DataFile>) = replacements
1256 .iter()
1257 .map(|DataReplacementGroup(fragment_id, new_file)| (fragment_id, new_file))
1258 .unzip();
1259
1260 if new_datafiles
1264 .iter()
1265 .map(|f| f.fields.clone())
1266 .collect::<HashSet<_>>()
1267 .len()
1268 > 1
1269 {
1270 let field_info = new_datafiles
1271 .iter()
1272 .enumerate()
1273 .map(|(id, f)| (id, f.fields.clone()))
1274 .fold("".to_string(), |acc, (id, fields)| {
1275 format!("{}File {}: {:?}\n", acc, id, fields)
1276 });
1277
1278 return Err(Error::invalid_input(format!(
1279 "All new data files must have the same fields, but found different fields:\n{field_info}"
1280 )));
1281 }
1282
1283 let existing_fragments = maybe_existing_fragments?;
1284
1285 let replaced_fields: Vec<u32> = new_datafiles
1287 .first()
1288 .map(|f| {
1289 f.schema(&schema)
1290 .field_ids()
1291 .iter()
1292 .chain(f.fields.iter())
1293 .filter(|&&id| id >= 0)
1294 .map(|&id| id as u32)
1295 .collect()
1296 })
1297 .unwrap_or_default();
1298
1299 for (frag_id, new_file) in old_fragment_ids.iter().zip(new_datafiles) {
1302 let frag = existing_fragments
1303 .iter()
1304 .find(|f| f.id == **frag_id)
1305 .ok_or_else(|| {
1306 Error::invalid_input(
1307 "Fragment being replaced not found in existing fragments",
1308 )
1309 })?;
1310 let mut new_frag = frag.clone();
1311
1312 let replacement_ids = new_file
1316 .schema(&schema)
1317 .field_ids()
1318 .into_iter()
1319 .chain(new_file.fields.iter().copied())
1320 .collect::<HashSet<_>>();
1321
1322 let mut columns_covered = HashSet::new();
1325 let mut replaced_in_place = false;
1328 for file in &mut new_frag.files {
1329 if file.fields == new_file.fields
1330 && file.file_major_version == new_file.file_major_version
1331 && file.file_minor_version == new_file.file_minor_version
1332 {
1333 file.path = new_file.path.clone();
1335 file.file_size_bytes = new_file.file_size_bytes.clone();
1336 file.base_id = new_file.base_id;
1337 replaced_in_place = true;
1338 }
1339 columns_covered.extend(file.schema(&schema).field_ids());
1340 columns_covered.extend(file.fields.iter().copied());
1341 }
1342 new_file.file_version()?;
1345
1346 if columns_covered.is_disjoint(&replacement_ids) {
1351 new_frag.files.push(new_file.clone());
1352 } else if !replaced_in_place
1353 && replacement_ids.is_subset(&columns_covered)
1354 && new_frag.files.iter().all(|file| {
1355 file.file_version()
1356 .is_ok_and(|version| version != ConcreteFileVersion::V1)
1357 || file
1358 .fields
1359 .iter()
1360 .all(|field| !replacement_ids.contains(field))
1361 })
1362 {
1363 for file in &mut new_frag.files {
1375 if file.file_version()? == ConcreteFileVersion::V1 {
1377 continue;
1378 }
1379 file.fields = file
1380 .fields
1381 .iter()
1382 .map(|field| {
1383 if replacement_ids.contains(field) {
1384 TOMBSTONE_FIELD_ID
1385 } else {
1386 *field
1387 }
1388 })
1389 .collect::<Vec<_>>()
1390 .into();
1391 }
1392 let live_ids = schema
1403 .fields_pre_order()
1404 .map(|field| field.id)
1405 .collect::<HashSet<i32>>();
1406 let spilled = new_frag.spilled_row_lineage_field_ids();
1407 new_frag.files.retain(|file| {
1408 file.fields
1409 .iter()
1410 .any(|f| live_ids.contains(f) || spilled.contains(f))
1411 });
1412 new_frag.files.push(new_file.clone());
1413 }
1414
1415 if &new_frag == frag {
1417 return Err(Error::invalid_input(
1418 "Expected to modify the fragment but no changes were made. This means the new data files does not align with any exiting datafiles. Please check if the schema of the new data files matches the schema of the old data files including the file major and minor versions",
1419 ));
1420 }
1421
1422 let (mut superseded, newer): (Vec<_>, Vec<_>) = new_frag
1429 .overlays
1430 .drain(..)
1431 .partition(|overlay| overlay.committed_version <= self.read_version);
1432 crate::format::overlay::tombstone_overlay_fields(
1433 &mut superseded,
1434 &replaced_fields,
1435 );
1436 superseded.extend(newer);
1437 new_frag.overlays = superseded;
1438
1439 final_fragments.push(new_frag);
1440 }
1441
1442 let fragments_changed = old_fragment_ids
1443 .iter()
1444 .cloned()
1445 .cloned()
1446 .collect::<HashSet<_>>();
1447
1448 let unmodified_fragments = existing_fragments
1450 .iter()
1451 .filter(|f| !fragments_changed.contains(&f.id))
1452 .cloned()
1453 .collect::<Vec<_>>();
1454
1455 final_fragments.extend(unmodified_fragments);
1456
1457 if next_row_id.is_some() {
1461 let new_version = current_manifest.map_or(1, |m| m.version + 1);
1462 for fragment in final_fragments
1463 .iter_mut()
1464 .filter(|f| fragments_changed.contains(&f.id))
1465 {
1466 crate::rowids::version::refresh_row_latest_update_meta_for_full_frag_rewrite_cols(
1467 fragment,
1468 new_version,
1469 )?;
1470 }
1471 }
1472
1473 }
1476 Operation::DataOverlay { groups } => {
1477 let new_version = current_manifest.map_or(1, |m| m.version + 1);
1481
1482 let existing_fragments = maybe_existing_fragments?;
1483 let mut overlays_by_fragment: HashMap<u64, Vec<&DataOverlayFile>> = HashMap::new();
1487 for group in groups {
1488 overlays_by_fragment
1489 .entry(group.fragment_id)
1490 .or_default()
1491 .extend(group.overlays.iter());
1492 }
1493
1494 let existing_fragment_ids: HashSet<u64> =
1498 existing_fragments.iter().map(|f| f.id).collect();
1499 for fragment_id in overlays_by_fragment.keys() {
1500 if !existing_fragment_ids.contains(fragment_id) {
1501 return Err(Error::invalid_input(format!(
1502 "DataOverlay targets fragment {fragment_id}, which does not exist"
1503 )));
1504 }
1505 }
1506
1507 for fragment in existing_fragments {
1508 let mut fragment = fragment.clone();
1509 if let Some(new_overlays) = overlays_by_fragment.get(&fragment.id) {
1510 if next_row_id.is_some() {
1511 let mut covered_offsets = RoaringBitmap::new();
1512 for overlay in new_overlays {
1513 match &overlay.coverage {
1514 OverlayCoverage::Shared(bitmap) => {
1515 covered_offsets |= bitmap.as_ref();
1516 }
1517 OverlayCoverage::PerField(bitmaps) => {
1518 for bitmap in bitmaps {
1519 covered_offsets |= bitmap.as_ref();
1520 }
1521 }
1522 }
1523 }
1524 if !covered_offsets.is_empty() {
1525 let covered_offsets: Vec<usize> = covered_offsets
1526 .iter()
1527 .map(|offset| offset as usize)
1528 .collect();
1529 crate::rowids::version::refresh_row_latest_update_meta_for_partial_frag_rewrite_cols(
1533 &mut fragment,
1534 &covered_offsets,
1535 new_version,
1536 1,
1537 &config.spilled_row_lineage,
1538 )?;
1539 }
1540 }
1541 fragment
1544 .overlays
1545 .extend(new_overlays.iter().map(|&overlay| {
1546 let mut overlay = overlay.clone();
1547 overlay.committed_version = new_version;
1548 overlay
1549 }));
1550 }
1551 final_fragments.push(fragment);
1552 }
1553 }
1554 Operation::UpdateMemWalState {
1555 compacted_sstables, ..
1556 } => {
1557 final_fragments.extend(maybe_existing_fragments?.clone());
1559 update_mem_wal_index_compacted_sstables(
1560 &mut final_indices,
1561 new_version,
1562 compacted_sstables.clone(),
1563 )?;
1564 }
1565 Operation::UpdateBases { .. } => {
1566 final_fragments.extend(maybe_existing_fragments?.clone());
1569 }
1570 };
1571
1572 final_fragments.sort_by_key(|frag| frag.id);
1574
1575 Self::remove_tombstoned_data_files(&mut final_fragments);
1577
1578 for fragment in &final_fragments {
1582 if !fragment.overlays.is_empty() {
1583 crate::format::overlay::verify_overlays_newest_last(&fragment.overlays)?;
1584 }
1585 }
1586
1587 for fragment in &final_fragments {
1595 fragment.validate_row_lineage_carriers()?;
1596 }
1597
1598 let user_requested_version = match (&config.storage_format, config.use_legacy_format) {
1599 (Some(storage_format), _) => Some(storage_format.lance_file_format()),
1600 (None, Some(true)) => Some(ConcreteFileVersion::V1),
1601 (None, Some(false)) => Some(ConcreteFileVersion::V2_0),
1602 (None, None) => None,
1603 };
1604
1605 if let Some(segments_before) = mem_wal_segments_before.as_ref() {
1609 Self::apply_mem_wal_index_coverage(
1610 &mut final_indices,
1611 segments_before,
1612 read_version_state,
1613 new_version,
1614 )?;
1615 }
1616
1617 let legacy_blob_ids = schema
1622 .fields_pre_order()
1623 .filter(|field| field.is_blob() && !field.is_blob_v2())
1624 .map(|field| field.id)
1625 .collect::<HashSet<_>>();
1626 if !legacy_blob_ids.is_empty() {
1627 let mut blob_v2_fields = std::collections::BTreeSet::new();
1628 for file in final_fragments
1629 .iter()
1630 .flat_map(|fragment| fragment.referenced_lance_files())
1631 {
1632 if matches!(
1633 file.file_version()?,
1634 ConcreteFileVersion::V2_2 | ConcreteFileVersion::V2_3
1635 ) {
1636 blob_v2_fields.extend(
1637 file.fields
1638 .iter()
1639 .copied()
1640 .filter(|id| legacy_blob_ids.contains(id)),
1641 );
1642 }
1643 }
1644 let mut next_field_id = current_manifest
1645 .map(|manifest| manifest.max_field_id())
1646 .unwrap_or(-1)
1647 .max(schema.max_field_id().unwrap_or(-1))
1648 + 1;
1649 for id in blob_v2_fields {
1650 if let Some(field) = schema.mut_field_by_id(id) {
1651 field.promote_blob_v2()?;
1652 field.set_id(field.parent_id, &mut next_field_id);
1653 }
1654 }
1655 }
1656
1657 let mut manifest = if let Some(current_manifest) = current_manifest {
1658 let mut prev_manifest =
1661 Manifest::new_from_previous(current_manifest, schema, Arc::new(final_fragments));
1662
1663 if let (Some(user_requested_version), Operation::Overwrite { .. }) =
1664 (user_requested_version, &self.operation)
1665 {
1666 prev_manifest.data_storage_format = DataStorageFormat::new(user_requested_version);
1670 }
1671
1672 prev_manifest
1673 } else {
1674 let data_storage_format =
1675 Self::data_storage_format_from_files(&final_fragments, user_requested_version)?;
1676 Manifest::new(
1677 schema,
1678 Arc::new(final_fragments),
1679 data_storage_format,
1680 reference_paths,
1681 )
1682 };
1683
1684 manifest.tag.clone_from(&self.tag);
1685
1686 if config.auto_set_feature_flags {
1687 let inherited = current_manifest
1691 .map(|m| m.uses_stable_row_ids())
1692 .unwrap_or(false);
1693 let use_stable_row_ids = config.use_stable_row_ids || inherited;
1694 apply_feature_flags(
1695 &mut manifest,
1696 use_stable_row_ids,
1697 config.disable_transaction_file,
1698 )?;
1699 }
1700 if final_indices
1715 .iter()
1716 .any(|index| !index.covering_fields.is_empty())
1717 {
1718 manifest.reader_feature_flags |= FLAG_COVERED_INDEX_METADATA;
1719 manifest.writer_feature_flags |= FLAG_COVERED_INDEX_METADATA;
1720 }
1721
1722 if let Some(current_manifest) = current_manifest {
1723 inherit_sticky_feature_flags(&mut manifest, current_manifest)?;
1724 }
1725
1726 manifest.set_timestamp(config.timestamp_nanos);
1727
1728 manifest.update_max_fragment_id();
1729
1730 match &self.operation {
1731 Operation::Overwrite {
1732 config_upsert_values: Some(tm),
1733 ..
1734 } => {
1735 manifest.config_mut().extend(tm.clone());
1736 }
1737 Operation::UpdateConfig {
1738 config_updates,
1739 table_metadata_updates,
1740 schema_metadata_updates,
1741 field_metadata_updates,
1742 } => {
1743 if let Some(config_updates) = config_updates {
1744 let mut config = manifest.config.clone();
1745 apply_update_map(&mut config, config_updates);
1746 manifest.config = config;
1747 }
1748 if let Some(table_metadata_updates) = table_metadata_updates {
1749 let mut table_metadata = manifest.table_metadata.clone();
1750 apply_update_map(&mut table_metadata, table_metadata_updates);
1751 manifest.table_metadata = table_metadata;
1752 }
1753 if let Some(schema_metadata_updates) = schema_metadata_updates {
1754 let mut schema_metadata = manifest.schema.metadata.clone();
1755 apply_update_map(&mut schema_metadata, schema_metadata_updates);
1756 manifest.schema.metadata = schema_metadata;
1757 }
1758 let primary_key_before: Vec<i32> = manifest
1767 .schema
1768 .unenforced_primary_key()
1769 .iter()
1770 .map(|field| field.id)
1771 .collect();
1772 let writes_primary_key = field_metadata_updates.values().any(|update| {
1773 update.update_entries.iter().any(|entry| {
1774 entry.key == LANCE_UNENFORCED_PRIMARY_KEY
1775 || entry.key == LANCE_UNENFORCED_PRIMARY_KEY_POSITION
1776 })
1777 });
1778 let clustering_key_before: Vec<i32> = manifest
1779 .schema
1780 .unenforced_clustering_key()
1781 .iter()
1782 .map(|field| field.id)
1783 .collect();
1784 let writes_clustering_key = field_metadata_updates.values().any(|update| {
1785 update
1786 .update_entries
1787 .iter()
1788 .any(|entry| entry.key == LANCE_UNENFORCED_CLUSTERING_KEY_POSITION)
1789 });
1790 for (field_id, field_metadata_update) in field_metadata_updates {
1791 if let Some(field) = manifest.schema.field_by_id_mut(*field_id) {
1792 apply_update_map(&mut field.metadata, field_metadata_update);
1793 field.unenforced_primary_key_position = field
1795 .metadata
1796 .get(LANCE_UNENFORCED_PRIMARY_KEY_POSITION)
1797 .and_then(|s| s.parse::<u32>().ok())
1798 .or_else(|| {
1799 field
1800 .metadata
1801 .get(LANCE_UNENFORCED_PRIMARY_KEY)
1802 .filter(|s| str_is_truthy(s))
1803 .map(|_| 0)
1804 });
1805 field.unenforced_clustering_key_position = field
1808 .metadata
1809 .get(LANCE_UNENFORCED_CLUSTERING_KEY_POSITION)
1810 .and_then(|s| s.parse::<u32>().ok());
1811 } else {
1812 return Err(Error::invalid_input_source(
1813 format!("Field with id {} does not exist", field_id).into(),
1814 ));
1815 }
1816 }
1817 let primary_key_after: Vec<i32> = manifest
1818 .schema
1819 .unenforced_primary_key()
1820 .iter()
1821 .map(|field| field.id)
1822 .collect();
1823 if !primary_key_before.is_empty() {
1824 if writes_primary_key || primary_key_after != primary_key_before {
1827 return Err(Error::invalid_input(
1828 "the unenforced primary key is a reserved key and cannot be changed once set",
1829 ));
1830 }
1831 } else if writes_primary_key && primary_key_after.is_empty() {
1832 return Err(Error::invalid_input(
1836 "the unenforced primary key is a reserved key and cannot be set to an invalid value",
1837 ));
1838 }
1839 if writes_primary_key {
1840 manifest.schema.verify_primary_key()?;
1843 }
1844 let clustering_key_after: Vec<i32> = manifest
1845 .schema
1846 .unenforced_clustering_key()
1847 .iter()
1848 .map(|field| field.id)
1849 .collect();
1850 if !clustering_key_before.is_empty() {
1851 if writes_clustering_key || clustering_key_after != clustering_key_before {
1854 return Err(Error::invalid_input(
1855 "the unenforced clustering key is a reserved key and cannot be changed once set",
1856 ));
1857 }
1858 } else if writes_clustering_key && clustering_key_after.is_empty() {
1859 return Err(Error::invalid_input(
1863 "the unenforced clustering key is a reserved key and cannot be set to an invalid value",
1864 ));
1865 }
1866 }
1867 _ => {}
1868 }
1869
1870 apply_range_segments(&mut manifest, current_manifest)?;
1871
1872 if let Operation::UpdateBases { new_bases } = &self.operation {
1874 for new_base in new_bases {
1876 if let Some(existing_base) = manifest
1878 .base_paths
1879 .values()
1880 .find(|bp| bp.name == new_base.name || bp.path == new_base.path)
1881 {
1882 return Err(Error::invalid_input(format!(
1883 "Conflict detected: Base path with name '{:?}' or path '{}' already exists. Existing: name='{:?}', path='{}'",
1884 new_base.name, new_base.path, existing_base.name, existing_base.path
1885 )));
1886 }
1887
1888 let mut base_to_add = new_base.clone();
1890 if base_to_add.id == 0 {
1891 let next_id = manifest
1892 .base_paths
1893 .keys()
1894 .max()
1895 .map(|&id| id + 1)
1896 .unwrap_or(1);
1897 base_to_add.id = next_id;
1898 }
1899
1900 manifest.base_paths.insert(base_to_add.id, base_to_add);
1901 }
1902 }
1903
1904 if let Operation::ReserveFragments { num_fragments } = self.operation {
1905 manifest.max_fragment_id = Some(manifest.max_fragment_id.unwrap_or(0) + num_fragments);
1906 }
1907
1908 manifest.transaction_file = Some(transaction_file_path.to_string());
1909
1910 if let Some(next_row_id) = next_row_id {
1911 manifest.next_row_id = next_row_id;
1912 }
1913
1914 if final_indices.iter().any(is_tagged) {
1915 manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
1916 manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
1917 }
1918 Ok((manifest, final_indices))
1919 }
1920
1921 fn remove_tombstoned_data_files(fragments: &mut [Fragment]) {
1924 for fragment in fragments {
1925 fragment.files.retain(|file| {
1926 file.fields.iter().any(|&field_id| field_id != -2)
1928 });
1929 }
1930 }
1931 fn drop_rewritten_fragments(old: &RoaringBitmap, groups: &[RewriteGroup]) -> RoaringBitmap {
1934 let mut new_bitmap = old.clone();
1935 for group in groups {
1936 for old_fragment in &group.old_fragments {
1937 new_bitmap.remove(old_fragment.id as u32);
1938 }
1939 }
1940 new_bitmap
1941 }
1942}
1943
1944#[cfg(test)]
1945mod tests {
1946 use super::*;
1947 use crate::format::overlay::OverlayCoverage;
1948 use crate::format::pb;
1949 use crate::format::{
1950 DeletionFile, DeletionFileType, ROW_CREATED_AT_VERSION_FIELD_ID, ROW_ID_FIELD_ID,
1951 ROW_LAST_UPDATED_AT_VERSION_FIELD_ID, RowDatasetVersionMeta, RowDatasetVersionSequence,
1952 RowIdMeta,
1953 };
1954 use crate::rowids::{RowIdSequence, write_row_ids};
1955 use crate::transaction::test_support::{
1956 default_build_config, last_updated_at_versions, make_stable_row_id_manifest,
1957 overlay_with_field, sample_index_metadata, sample_manifest, tagged_entry,
1958 };
1959 use crate::transaction::{DataOverlayGroup, UpdateMode, validate_operation};
1960 use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
1961 use lance_core::datatypes::Schema as LanceSchema;
1962 use lance_file::version::{ConcreteFileVersion, LanceFileVersion};
1963 use lance_io::utils::CachedFileSize;
1964 use std::collections::HashMap;
1965 use std::sync::Arc;
1966 use uuid::Uuid;
1967
1968 fn tagged_sample_manifest() -> (Manifest, IndexMetadata) {
1971 let mut manifest = sample_manifest();
1972 manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
1973 manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
1974 (manifest, tagged_entry(&[(&[5], &[0])]))
1975 }
1976
1977 fn row_mutation(kind: &str) -> Operation {
1978 match kind {
1979 "delete" => Operation::Delete {
1980 updated_fragments: vec![],
1981 deleted_fragment_ids: vec![0],
1982 predicate: "true".into(),
1983 },
1984 "update" => crate::transaction::test_support::update_txn(vec![]).operation,
1985 "config" => Operation::UpdateConfig {
1986 config_updates: None,
1987 table_metadata_updates: None,
1988 schema_metadata_updates: None,
1989 field_metadata_updates: HashMap::new(),
1990 },
1991 _ => unreachable!(),
1992 }
1993 }
1994
1995 #[rstest::rstest]
2000 #[case::delete("delete")]
2001 #[case::update("update")]
2002 #[case::config("config")]
2003 #[case::project("project")]
2004 #[case::merge("merge")]
2005 fn tagged_history_allows_row_mutations(#[case] kind: &str) {
2006 let (manifest, fri) = tagged_sample_manifest();
2007 let operation = match kind {
2008 "project" => Operation::Project {
2009 schema: manifest.schema.clone(),
2010 preserves_nullability: true,
2011 },
2012 "merge" => Operation::Merge {
2013 fragments: manifest.fragments.as_ref().clone(),
2014 schema: manifest.schema.clone(),
2015 preserves_nullability: true,
2016 },
2017 _ => row_mutation(kind),
2018 };
2019 let transaction = Transaction::new(manifest.version, operation, None);
2020 let (_, indices) = transaction
2021 .build_manifest(
2022 Some(&manifest),
2023 vec![fri.clone()],
2024 "txn",
2025 &default_build_config(),
2026 )
2027 .unwrap();
2028 let carried = indices
2029 .iter()
2030 .find(|idx| idx.name == crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME)
2031 .expect("the tagged entry is carried through");
2032 assert_eq!(carried.uuid, fri.uuid);
2033 assert_eq!(carried.index_version, fri.index_version);
2034 }
2035
2036 #[test]
2039 fn tagged_history_overwrite_discards_the_entry() {
2040 let (manifest, fri) = tagged_sample_manifest();
2041 let transaction = Transaction::new(
2042 manifest.version,
2043 Operation::Overwrite {
2044 fragments: vec![Fragment::new(0)],
2045 schema: manifest.schema.clone(),
2046 config_upsert_values: None,
2047 initial_bases: None,
2048 },
2049 None,
2050 );
2051 let (new_manifest, indices) = transaction
2052 .build_manifest(Some(&manifest), vec![fri], "txn", &default_build_config())
2053 .unwrap();
2054 assert!(indices.is_empty(), "{indices:?}");
2055 assert_ne!(
2056 new_manifest.reader_feature_flags & FLAG_FRAGMENT_REUSE_INDEX,
2057 0
2058 );
2059 }
2060
2061 #[test]
2064 fn tagged_history_refuses_stable_row_id_migration() {
2065 let (manifest, fri) = tagged_sample_manifest();
2066 let transaction = Transaction::new(
2067 manifest.version,
2068 Operation::Merge {
2069 fragments: manifest.fragments.as_ref().clone(),
2070 schema: manifest.schema.clone(),
2071 preserves_nullability: true,
2072 },
2073 None,
2074 );
2075 let config = ManifestBuildConfig {
2076 migration_next_row_id: Some(100),
2077 ..default_build_config()
2078 };
2079 let error = transaction
2080 .build_manifest(Some(&manifest), vec![fri], "txn", &config)
2081 .unwrap_err();
2082 assert!(matches!(error, Error::NotSupported { .. }), "{error}");
2083 assert!(
2084 error.to_string().contains("stable row id migration"),
2085 "{error}"
2086 );
2087 }
2088
2089 fn manifest_with_file(path: &str) -> Manifest {
2091 let mut manifest = sample_manifest();
2092 manifest.fragments = Arc::new(vec![Fragment::new(0).with_file(
2093 path,
2094 vec![0],
2095 vec![0],
2096 ConcreteFileVersion::V2_0,
2097 None,
2098 )]);
2099 manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2100 manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2101 manifest
2102 }
2103
2104 fn translating_segment_and_entry() -> (IndexMetadata, IndexMetadata) {
2108 let mut segment = sample_index_metadata("id_idx");
2109 segment.fragment_bitmap = Some([5u32].into_iter().collect());
2110 (segment, tagged_entry(&[(&[5], &[0])]))
2111 }
2112
2113 fn user_bitmap(indices: &[IndexMetadata]) -> RoaringBitmap {
2114 indices
2115 .iter()
2116 .find(|idx| idx.name == "id_idx")
2117 .unwrap()
2118 .fragment_bitmap
2119 .clone()
2120 .unwrap()
2121 }
2122
2123 fn in_place_rewrite(kind: &str, manifest: &Manifest, fields: Vec<u32>) -> Operation {
2125 match kind {
2126 "update" => {
2127 let mut update = crate::transaction::test_support::update_txn(vec![]);
2128 let Operation::Update {
2129 updated_fragments,
2130 fields_modified,
2131 ..
2132 } = &mut update.operation
2133 else {
2134 unreachable!()
2135 };
2136 *updated_fragments = vec![Fragment::new(0)];
2137 *fields_modified = fields;
2138 update.operation
2139 }
2140 "merge" => Operation::Merge {
2141 fragments: vec![Fragment::new(0).with_file(
2142 "b.lance",
2143 fields.iter().map(|f| *f as i32).collect(),
2144 vec![0],
2145 ConcreteFileVersion::V2_0,
2146 None,
2147 )],
2148 schema: manifest.schema.clone(),
2149 preserves_nullability: true,
2150 },
2151 "data_replacement" => Operation::DataReplacement {
2152 replacements: vec![DataReplacementGroup(
2153 0,
2154 DataFile::new(
2155 "b.lance",
2156 fields.iter().map(|f| *f as i32).collect(),
2157 vec![0],
2158 ConcreteFileVersion::V2_0,
2159 None,
2160 None,
2161 ),
2162 )],
2163 },
2164 _ => unreachable!(),
2165 }
2166 }
2167
2168 #[rstest::rstest]
2174 #[case::update("update")]
2175 #[case::merge("merge")]
2176 #[case::data_replacement("data_replacement")]
2177 fn tagged_history_withdraws_in_place_rewrite_covered_only_by_translation(#[case] kind: &str) {
2178 let manifest = manifest_with_file("a.lance");
2179 let (segment, fri) = translating_segment_and_entry();
2180 let transaction = Transaction::new(
2181 manifest.version,
2182 in_place_rewrite(kind, &manifest, vec![0]),
2183 None,
2184 );
2185 let (_, indices) = transaction
2186 .build_manifest(
2187 Some(&manifest),
2188 vec![segment, fri.clone()],
2189 "txn",
2190 &default_build_config(),
2191 )
2192 .unwrap();
2193 assert!(
2194 indices.iter().any(|idx| idx.uuid == fri.uuid),
2195 "entry carried"
2196 );
2197 assert!(
2198 user_bitmap(&indices).is_empty(),
2199 "{:?}",
2200 user_bitmap(&indices)
2201 );
2202 }
2203
2204 #[rstest::rstest]
2209 #[case::update("update")]
2210 #[case::merge("merge")]
2211 #[case::data_replacement("data_replacement")]
2212 fn tagged_history_withdraws_mixed_provenance_whole(#[case] kind: &str) {
2213 let manifest = manifest_with_file("a.lance");
2214 let (mut segment, fri) = translating_segment_and_entry();
2215 segment.fragment_bitmap = Some([0u32, 5].into_iter().collect());
2216 let transaction = Transaction::new(
2217 manifest.version,
2218 in_place_rewrite(kind, &manifest, vec![0]),
2219 None,
2220 );
2221 let (_, indices) = transaction
2222 .build_manifest(
2223 Some(&manifest),
2224 vec![segment, fri],
2225 "txn",
2226 &default_build_config(),
2227 )
2228 .unwrap();
2229 assert!(
2230 user_bitmap(&indices).is_empty(),
2231 "{:?}",
2232 user_bitmap(&indices)
2233 );
2234 }
2235
2236 #[test]
2243 fn tagged_history_without_a_walkable_lineage_withdraws_every_retired_source() {
2244 use crate::system_index::frag_reuse::lineage::TaggedLineage;
2245 let entry = tagged_entry(&[(&[5], &[0]), (&[6], &[8])]);
2246 let mut segment = sample_index_metadata("id_idx");
2247 segment.fragment_bitmap = Some([0u32, 5, 6, 7].into_iter().collect());
2248 let live: RoaringBitmap = [0u32, 7, 8].into_iter().collect();
2249
2250 let unwalkable = TaggedLineage::new(&entry, None);
2251 assert!(!unwalkable.has_ledger());
2252 let mut indices = vec![segment.clone()];
2253 Transaction::withdraw_rewritten_coverage(
2254 &mut indices,
2255 Some(&unwalkable),
2256 &live,
2257 &[(0, vec![0])],
2258 );
2259 assert_eq!(
2260 user_bitmap(&indices),
2261 [7u32].into_iter().collect::<RoaringBitmap>(),
2262 "the rewritten fragment and every retired fragment go"
2263 );
2264
2265 let walkable = TaggedLineage::from_indices_with_ledger(&[entry], None)
2266 .unwrap()
2267 .unwrap();
2268 let mut indices = vec![segment];
2269 Transaction::withdraw_rewritten_coverage(
2270 &mut indices,
2271 Some(&walkable),
2272 &live,
2273 &[(0, vec![0])],
2274 );
2275 assert_eq!(
2276 user_bitmap(&indices),
2277 [6u32, 7].into_iter().collect::<RoaringBitmap>(),
2278 "only the transition reaching fragment 0 is withdrawn"
2279 );
2280 }
2281
2282 #[test]
2288 fn tagged_history_retires_an_emptied_segment_only_beside_a_serving_sibling() {
2289 let mut manifest = manifest_with_file("a.lance");
2290 manifest.fragments = Arc::new(vec![
2291 Fragment::new(0).with_file(
2292 "a.lance",
2293 vec![0],
2294 vec![0],
2295 ConcreteFileVersion::V2_0,
2296 None,
2297 ),
2298 Fragment::new(1).with_file(
2299 "c.lance",
2300 vec![0],
2301 vec![0],
2302 ConcreteFileVersion::V2_0,
2303 None,
2304 ),
2305 ]);
2306 let (translating, fri) = translating_segment_and_entry();
2307 let mut sibling = sample_index_metadata("id_idx");
2308 sibling.uuid = Uuid::new_v4();
2309 sibling.fragment_bitmap = Some([1u32].into_iter().collect());
2310 let mut unknown = sample_index_metadata("other_idx");
2311 unknown.uuid = Uuid::new_v4();
2312 unknown.fragment_bitmap = None;
2313 let transaction = Transaction::new(
2314 manifest.version,
2315 in_place_rewrite("update", &manifest, vec![0]),
2316 None,
2317 );
2318
2319 let (_, indices) = transaction
2321 .build_manifest(
2322 Some(&manifest),
2323 vec![
2324 translating.clone(),
2325 sibling.clone(),
2326 unknown.clone(),
2327 fri.clone(),
2328 ],
2329 "txn",
2330 &default_build_config(),
2331 )
2332 .unwrap();
2333 let names: Vec<(String, Uuid)> = indices
2334 .iter()
2335 .map(|idx| (idx.name.clone(), idx.uuid))
2336 .collect();
2337 assert!(
2338 !names.contains(&("id_idx".to_string(), translating.uuid)),
2339 "{names:?}"
2340 );
2341 assert_eq!(
2342 indices
2343 .iter()
2344 .find(|idx| idx.uuid == sibling.uuid)
2345 .unwrap()
2346 .fragment_bitmap,
2347 sibling.fragment_bitmap,
2348 "the sibling is untouched"
2349 );
2350 assert_eq!(
2351 indices
2352 .iter()
2353 .find(|idx| idx.uuid == unknown.uuid)
2354 .unwrap()
2355 .fragment_bitmap,
2356 None,
2357 "unknown coverage is neither withdrawn nor emptied"
2358 );
2359
2360 let (_, indices) = transaction
2362 .build_manifest(
2363 Some(&manifest),
2364 vec![translating.clone(), fri],
2365 "txn",
2366 &default_build_config(),
2367 )
2368 .unwrap();
2369 let kept = indices
2370 .iter()
2371 .find(|idx| idx.uuid == translating.uuid)
2372 .expect("the last segment stays");
2373 assert!(kept.fragment_bitmap.as_ref().unwrap().is_empty());
2374 }
2375
2376 #[test]
2380 fn tagged_history_leaves_unrelated_lineage_alone() {
2381 let manifest = manifest_with_file("a.lance");
2382 let fri = tagged_entry(&[(&[5], &[0]), (&[6], &[8])]);
2383 let mut segment = sample_index_metadata("id_idx");
2384 segment.fragment_bitmap = Some([6u32].into_iter().collect());
2385 let transaction = Transaction::new(
2386 manifest.version,
2387 in_place_rewrite("update", &manifest, vec![0]),
2388 None,
2389 );
2390 let (_, indices) = transaction
2391 .build_manifest(
2392 Some(&manifest),
2393 vec![segment, fri],
2394 "txn",
2395 &default_build_config(),
2396 )
2397 .unwrap();
2398 assert_eq!(user_bitmap(&indices), RoaringBitmap::from_iter([6u32]));
2399 }
2400
2401 #[test]
2404 fn tagged_history_without_readable_history_is_an_error() {
2405 let manifest = manifest_with_file("a.lance");
2406 let (segment, mut fri) = translating_segment_and_entry();
2407 fri.index_details = None;
2408 let transaction = Transaction::new(
2409 manifest.version,
2410 in_place_rewrite("update", &manifest, vec![0]),
2411 None,
2412 );
2413 let error = transaction
2414 .build_manifest(
2415 Some(&manifest),
2416 vec![segment, fri],
2417 "txn",
2418 &default_build_config(),
2419 )
2420 .unwrap_err();
2421 assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
2422 assert!(error.to_string().contains("history"), "{error}");
2423 }
2424
2425 #[rstest::rstest]
2430 #[case::update_direct("update", "direct")]
2431 #[case::merge_direct("merge", "direct")]
2432 #[case::data_replacement_direct("data_replacement", "direct")]
2433 #[case::update_unindexed_field("update", "unindexed_field")]
2434 #[case::merge_unindexed_field("merge", "unindexed_field")]
2435 #[case::update_off_lineage("update", "off_lineage")]
2436 fn tagged_history_prunes_or_keeps_what_the_history_says(
2437 #[case] kind: &str,
2438 #[case] shape: &str,
2439 ) {
2440 let manifest = manifest_with_file("a.lance");
2441 let (mut segment, mut fri) = translating_segment_and_entry();
2442 let mut fields = vec![0];
2443 match shape {
2444 "direct" => segment.fragment_bitmap = Some([0u32].into_iter().collect()),
2445 "unindexed_field" => fields = vec![1],
2446 "off_lineage" => fri = tagged_entry(&[(&[5], &[7])]),
2448 _ => unreachable!(),
2449 }
2450 let transaction = Transaction::new(
2451 manifest.version,
2452 in_place_rewrite(kind, &manifest, fields),
2453 None,
2454 );
2455 let (_, indices) = transaction
2456 .build_manifest(
2457 Some(&manifest),
2458 vec![segment.clone(), fri.clone()],
2459 "txn",
2460 &default_build_config(),
2461 )
2462 .unwrap();
2463 let carried = indices
2464 .iter()
2465 .find(|idx| idx.name == crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME)
2466 .expect("the tagged entry is carried through");
2467 assert_eq!(carried.uuid, fri.uuid);
2468 let user = indices.iter().find(|idx| idx.name == "id_idx").unwrap();
2469 let expected: RoaringBitmap = if shape == "direct" {
2470 RoaringBitmap::new()
2472 } else {
2473 segment.fragment_bitmap.unwrap()
2474 };
2475 assert_eq!(user.fragment_bitmap.as_ref().unwrap(), &expected, "{shape}");
2476 }
2477
2478 #[test]
2481 fn tagged_history_allows_user_index_create_index() {
2482 let (manifest, fri) = tagged_sample_manifest();
2483 let transaction = Transaction::new(
2484 manifest.version,
2485 Operation::CreateIndex {
2486 new_indices: vec![sample_index_metadata("id_idx")],
2487 removed_indices: vec![],
2488 },
2489 None,
2490 );
2491 let (_, indices) = transaction
2492 .build_manifest(
2493 Some(&manifest),
2494 vec![fri.clone()],
2495 "txn",
2496 &default_build_config(),
2497 )
2498 .unwrap();
2499 assert!(indices.iter().any(|idx| idx.name == "id_idx"));
2500 let carried = indices.iter().find(|idx| idx.uuid == fri.uuid).unwrap();
2501 assert_eq!(carried.index_details, fri.index_details);
2502 assert_eq!(carried.fragment_bitmap, fri.fragment_bitmap);
2503 }
2504
2505 #[rstest::rstest]
2506 #[case::memwal("memwal")]
2507 #[case::bare_rewrite("bare_rewrite")]
2508 #[case::rewrite_with_v0_entry("rewrite_with_v0_entry")]
2509 fn tagged_history_rejects_unsupported_transactions(#[case] kind: &str) {
2510 let (manifest, fri) = tagged_sample_manifest();
2511 let operation = match kind {
2512 "memwal" => Operation::UpdateMemWalState {
2513 compacted_sstables: vec![],
2514 },
2515 "bare_rewrite" => Operation::Rewrite {
2521 groups: vec![RewriteGroup {
2522 old_fragments: vec![Fragment::new(0)],
2523 new_fragments: vec![Fragment::new(10)],
2524 }],
2525 rewritten_indices: vec![],
2526 frag_reuse_index: None,
2527 },
2528 "rewrite_with_v0_entry" => {
2529 let mut v0_entry = fri.clone();
2530 v0_entry.index_version = 0;
2531 Operation::Rewrite {
2532 groups: vec![],
2533 rewritten_indices: vec![],
2534 frag_reuse_index: Some(v0_entry),
2535 }
2536 }
2537 _ => unreachable!(),
2538 };
2539 let transaction = Transaction::new(manifest.version, operation, None);
2540 let error = transaction
2541 .build_manifest(Some(&manifest), vec![fri], "txn", &default_build_config())
2542 .unwrap_err();
2543 assert!(matches!(error, Error::NotSupported { .. }), "{error}");
2544 assert!(
2545 error.to_string().contains("Tagged FRI history maintenance"),
2546 "{error}"
2547 );
2548 }
2549
2550 #[rstest::rstest]
2551 #[case::untagged_table(false)]
2552 #[case::tagged_table(true)]
2553 fn tagged_snapshot_replace_rejected(#[case] table_is_tagged: bool) {
2554 let mut manifest = sample_manifest();
2559 let mut current_indices = vec![];
2560 if table_is_tagged {
2561 manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2562 manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2563 let mut current =
2564 sample_index_metadata(crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME);
2565 current.fields.clear();
2566 current_indices.push(current);
2567 }
2568 let mut entry =
2569 sample_index_metadata(crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME);
2570 entry.fields.clear();
2571 let transaction = Transaction::new(
2572 manifest.version,
2573 Operation::Rewrite {
2574 groups: vec![],
2575 rewritten_indices: vec![],
2576 frag_reuse_index: Some(entry),
2577 },
2578 None,
2579 );
2580 let error = transaction
2581 .build_manifest(
2582 Some(&manifest),
2583 current_indices,
2584 "txn",
2585 &default_build_config(),
2586 )
2587 .unwrap_err();
2588 assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
2589 assert!(error.to_string().contains("must be assembled"), "{error}");
2590 }
2591
2592 #[test]
2593 fn v0_snapshot_replace_rejected_on_flag_carrying_manifest_without_entry() {
2594 let mut manifest = sample_manifest();
2599 manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2600 manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2601 let mut entry =
2602 sample_index_metadata(crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME);
2603 entry.fields.clear();
2604 entry.index_version = 0;
2605 let transaction = Transaction::new(
2606 manifest.version,
2607 Operation::Rewrite {
2608 groups: vec![],
2609 rewritten_indices: vec![],
2610 frag_reuse_index: Some(entry),
2611 },
2612 None,
2613 );
2614 let error = transaction
2615 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2616 .unwrap_err();
2617 assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
2618 assert!(error.to_string().contains("sticky feature flag"), "{error}");
2619 }
2620
2621 #[test]
2622 fn tagged_history_allows_rewrite_of_uncovered_fragments() {
2623 let mut manifest = sample_manifest_with_fragments(0..6);
2627 manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2628 manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2629 let mut fri = sample_index_metadata(crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME);
2630 fri.fields.clear();
2631 assert_eq!(
2632 fri.fragment_bitmap.as_ref().unwrap(),
2633 &roaring::RoaringBitmap::from_iter([0u32])
2634 );
2635 let transaction = Transaction::new(
2636 manifest.version,
2637 Operation::Rewrite {
2638 groups: vec![RewriteGroup {
2639 old_fragments: vec![Fragment::new(5)],
2640 new_fragments: vec![Fragment::new(10)],
2641 }],
2642 rewritten_indices: vec![],
2643 frag_reuse_index: None,
2644 },
2645 None,
2646 );
2647 let (_, final_indices) = transaction
2648 .build_manifest(
2649 Some(&manifest),
2650 vec![fri.clone()],
2651 "txn",
2652 &default_build_config(),
2653 )
2654 .unwrap();
2655 assert!(final_indices.iter().any(|idx| idx.uuid == fri.uuid));
2657 }
2658
2659 #[test]
2660 fn out_of_range_transition_source_id_rejected() {
2661 let error = crate::transaction::reordered_sources(&[
2665 crate::format::pb::fragment_reuse_index_details::Transition {
2666 sources: vec![
2667 crate::format::pb::fragment_reuse_index_details::FragmentDigest {
2668 id: u64::from(u32::MAX) + 1,
2669 physical_rows: 4,
2670 num_deleted_rows: 0,
2671 },
2672 ],
2673 destinations: vec![],
2674 mapping: None,
2675 },
2676 ])
2677 .unwrap_err();
2678 assert!(error.to_string().contains("row-address range"), "{error}");
2679 }
2680
2681 #[test]
2682 fn tagged_history_allows_rewrite_appending_tagged_entry() {
2683 let mut manifest = sample_manifest();
2684 manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2685 manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2686 let mut current =
2687 sample_index_metadata(crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME);
2688 current.fields.clear();
2689 current.dataset_version = 7;
2690 let mut appended =
2691 sample_index_metadata(crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME);
2692 appended.fields.clear();
2693
2694 let transaction = Transaction::new(
2695 manifest.version,
2696 Operation::Rewrite {
2697 groups: vec![],
2698 rewritten_indices: vec![],
2699 frag_reuse_index: Some(appended.clone()),
2700 },
2701 None,
2702 );
2703 let config = default_build_config();
2704 let prepared = transaction
2705 .prepare_indices(
2706 Some(&manifest),
2707 vec![current.clone()],
2708 &config,
2709 None,
2710 FragReuseUpdate::Rewrite(Arc::new(crate::transaction::TaggedRewriteAssembly {
2711 entry: appended.clone(),
2712 base_entry_version: Some(7),
2713 reordered_sources: RoaringBitmap::new(),
2714 })),
2715 )
2716 .unwrap();
2717 let (new_manifest, final_indices) = transaction
2718 .build_manifest_prepared(Some(&manifest), prepared, "txn", &config, None)
2719 .unwrap();
2720 let entries: Vec<_> = final_indices
2722 .iter()
2723 .filter(|idx| idx.name == crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME)
2724 .collect();
2725 assert_eq!(entries.len(), 1);
2726 assert_eq!(entries[0].uuid, appended.uuid);
2727 assert_ne!(
2728 new_manifest.reader_feature_flags & FLAG_FRAGMENT_REUSE_INDEX,
2729 0
2730 );
2731 }
2732
2733 #[test]
2734 fn frag_reuse_rewrite_guards_base_entry_version() {
2735 let mut manifest = sample_manifest();
2736 manifest.reader_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2737 manifest.writer_feature_flags |= FLAG_FRAGMENT_REUSE_INDEX;
2738 let mut current =
2739 sample_index_metadata(crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME);
2740 current.fields.clear();
2741 current.dataset_version = 7;
2742 let mut appended =
2743 sample_index_metadata(crate::system_index::frag_reuse::FRAG_REUSE_INDEX_NAME);
2744 appended.fields.clear();
2745
2746 let transaction = Transaction::new(
2749 manifest.version,
2750 Operation::Rewrite {
2751 groups: vec![],
2752 rewritten_indices: vec![],
2753 frag_reuse_index: Some(appended.clone()),
2754 },
2755 None,
2756 );
2757 let config = default_build_config();
2758 let prepared = transaction
2759 .prepare_indices(
2760 Some(&manifest),
2761 vec![current],
2762 &config,
2763 None,
2764 FragReuseUpdate::Rewrite(Arc::new(crate::transaction::TaggedRewriteAssembly {
2765 entry: appended.clone(),
2766 base_entry_version: None,
2767 reordered_sources: RoaringBitmap::new(),
2768 })),
2769 )
2770 .unwrap();
2771 let error = transaction
2772 .build_manifest_prepared(Some(&manifest), prepared, "txn", &config, None)
2773 .unwrap_err();
2774 assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
2775 assert!(error.to_string().contains("rebuild"), "{error}");
2776 }
2777
2778 #[rstest::rstest]
2781 #[case::update("update")]
2782 #[case::merge("merge")]
2783 #[case::data_replacement("data_replacement")]
2784 fn prepare_indices_keeps_original_and_withdraws_prepared(#[case] kind: &str) {
2785 let manifest = manifest_with_file("a.lance");
2786 let (segment, fri) = translating_segment_and_entry();
2787 let fri = {
2789 let mut merged = fri;
2790 merged.index_details = tagged_entry(&[(&[5], &[0]), (&[6], &[8])]).index_details;
2791 merged.fragment_bitmap = Some([0u32, 5, 6, 8].into_iter().collect());
2792 merged
2793 };
2794 let mut unrelated = sample_index_metadata("other_idx");
2795 unrelated.fragment_bitmap = Some([6u32].into_iter().collect());
2796 let transaction = Transaction::new(
2797 manifest.version,
2798 in_place_rewrite(kind, &manifest, vec![0]),
2799 None,
2800 );
2801 let config = default_build_config();
2802 let prepared = transaction
2803 .prepare_indices(
2804 Some(&manifest),
2805 vec![segment.clone(), unrelated.clone(), fri.clone()],
2806 &config,
2807 None,
2808 FragReuseUpdate::None,
2809 )
2810 .unwrap();
2811 assert_eq!(prepared.manifest_version(), Some(manifest.version));
2812 assert_eq!(
2813 prepared.original(),
2814 &[segment.clone(), unrelated.clone(), fri],
2815 "the original list is what was read"
2816 );
2817 let prepared_user = prepared
2818 .prepared()
2819 .iter()
2820 .find(|idx| idx.uuid == segment.uuid)
2821 .unwrap();
2822 assert!(prepared_user.fragment_bitmap.as_ref().unwrap().is_empty());
2823 let prepared_unrelated = prepared
2824 .prepared()
2825 .iter()
2826 .find(|idx| idx.uuid == unrelated.uuid)
2827 .unwrap();
2828 assert_eq!(
2829 prepared_unrelated.fragment_bitmap,
2830 unrelated.fragment_bitmap
2831 );
2832 let (_, indices) = transaction
2833 .build_manifest_prepared(Some(&manifest), prepared.clone(), "txn", &config, None)
2834 .unwrap();
2835 assert_eq!(indices, prepared.prepared());
2836 }
2837
2838 #[rstest::rstest]
2840 #[case::update("update")]
2841 #[case::merge("merge")]
2842 #[case::data_replacement("data_replacement")]
2843 fn prepare_indices_prunes_by_id_on_an_untagged_table(#[case] kind: &str) {
2844 let mut manifest = manifest_with_file("a.lance");
2845 manifest.reader_feature_flags &= !FLAG_FRAGMENT_REUSE_INDEX;
2846 manifest.writer_feature_flags &= !FLAG_FRAGMENT_REUSE_INDEX;
2847 let mut segment = sample_index_metadata("id_idx");
2848 segment.fragment_bitmap = Some([0u32, 3].into_iter().collect());
2849 let transaction = Transaction::new(
2850 manifest.version,
2851 in_place_rewrite(kind, &manifest, vec![0]),
2852 None,
2853 );
2854 let prepared = transaction
2855 .prepare_indices(
2856 Some(&manifest),
2857 vec![segment.clone()],
2858 &default_build_config(),
2859 None,
2860 FragReuseUpdate::None,
2861 )
2862 .unwrap();
2863 assert_eq!(prepared.original(), &[segment]);
2864 assert_eq!(
2865 prepared.prepared()[0].fragment_bitmap,
2866 Some(RoaringBitmap::from_iter([3u32]))
2867 );
2868 }
2869
2870 #[test]
2873 fn prepare_indices_restore_and_non_rewrites_are_identity() {
2874 let manifest = manifest_with_file("a.lance");
2875 let (segment, fri) = translating_segment_and_entry();
2876 for operation in [
2877 Operation::Restore { version: 1 },
2878 Operation::Append { fragments: vec![] },
2879 Operation::Delete {
2880 updated_fragments: vec![],
2881 deleted_fragment_ids: vec![0],
2882 predicate: "true".into(),
2883 },
2884 in_place_rewrite("update", &manifest, vec![]),
2885 ] {
2886 let transaction = Transaction::new(manifest.version, operation, None);
2887 let prepared = transaction
2888 .prepare_indices(
2889 Some(&manifest),
2890 vec![segment.clone(), fri.clone()],
2891 &default_build_config(),
2892 None,
2893 FragReuseUpdate::None,
2894 )
2895 .unwrap();
2896 assert_eq!(prepared.prepared(), prepared.original());
2897 }
2898 }
2899
2900 #[test]
2903 fn prepare_indices_external_history_needs_the_supplied_ledger() {
2904 let manifest = manifest_with_file("a.lance");
2905 let (segment, fri) = translating_segment_and_entry();
2906 let ledger = crate::system_index::frag_reuse::ledger::FragReuseLedger::decode_inline(
2907 fri.index_version,
2908 fri.index_details.as_ref().unwrap(),
2909 )
2910 .unwrap()
2911 .unwrap();
2912 let mut external = fri;
2913 external.index_details = None;
2914 let transaction = Transaction::new(
2915 manifest.version,
2916 in_place_rewrite("update", &manifest, vec![0]),
2917 None,
2918 );
2919 let error = transaction
2920 .prepare_indices(
2921 Some(&manifest),
2922 vec![segment.clone(), external.clone()],
2923 &default_build_config(),
2924 None,
2925 FragReuseUpdate::None,
2926 )
2927 .unwrap_err();
2928 assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
2929 let prepared = transaction
2930 .prepare_indices(
2931 Some(&manifest),
2932 vec![segment, external],
2933 &default_build_config(),
2934 Some(Arc::new(ledger)),
2935 FragReuseUpdate::None,
2936 )
2937 .unwrap();
2938 assert!(user_bitmap(prepared.prepared()).is_empty());
2939 }
2940
2941 #[test]
2944 fn prepare_indices_corrupt_inline_details_is_an_error() {
2945 let manifest = manifest_with_file("a.lance");
2946 let (segment, mut fri) = translating_segment_and_entry();
2947 let mut details = fri.index_details.as_ref().unwrap().as_ref().clone();
2948 details.value = vec![0xff, 0xff, 0xff, 0xff, 0xff];
2949 fri.index_details = Some(Arc::new(details));
2950 let transaction = Transaction::new(
2951 manifest.version,
2952 in_place_rewrite("update", &manifest, vec![0]),
2953 None,
2954 );
2955 let error = transaction
2956 .prepare_indices(
2957 Some(&manifest),
2958 vec![segment.clone(), fri.clone()],
2959 &default_build_config(),
2960 None,
2961 FragReuseUpdate::None,
2962 )
2963 .unwrap_err();
2964 assert!(
2965 matches!(error, Error::CorruptFile { .. }),
2966 "corrupt history must surface as corruption: {error:?}"
2967 );
2968 let error = transaction
2969 .build_manifest(
2970 Some(&manifest),
2971 vec![segment, fri],
2972 "txn",
2973 &default_build_config(),
2974 )
2975 .unwrap_err();
2976 assert!(matches!(error, Error::CorruptFile { .. }), "{error:?}");
2977 }
2978
2979 #[test]
2982 fn build_manifest_prepared_refuses_a_result_prepared_against_another_manifest() {
2983 let manifest = manifest_with_file("a.lance");
2984 let (segment, fri) = translating_segment_and_entry();
2985 let transaction = Transaction::new(
2986 manifest.version,
2987 in_place_rewrite("update", &manifest, vec![0]),
2988 None,
2989 );
2990 let prepared = transaction
2991 .prepare_indices(
2992 Some(&manifest),
2993 vec![segment, fri],
2994 &default_build_config(),
2995 None,
2996 FragReuseUpdate::None,
2997 )
2998 .unwrap();
2999 let mut later = manifest;
3000 later.version += 1;
3001 let error = transaction
3002 .build_manifest_prepared(Some(&later), prepared, "txn", &default_build_config(), None)
3003 .unwrap_err();
3004 assert!(matches!(error, Error::InvalidInput { .. }), "{error}");
3005 assert!(error.to_string().contains("prepared against"), "{error}");
3006 }
3007
3008 #[test]
3012 fn build_manifest_prepared_still_runs_the_gate() {
3013 let manifest = manifest_with_file("a.lance");
3014 let (_, fri) = translating_segment_and_entry();
3015 let mut appended = fri.clone();
3016 appended.uuid = uuid::Uuid::new_v4();
3017 let transaction = Transaction::new(
3018 manifest.version,
3019 Operation::Rewrite {
3020 groups: vec![],
3021 rewritten_indices: vec![],
3022 frag_reuse_index: Some(appended),
3023 },
3024 None,
3025 );
3026 let error = transaction
3027 .prepare_indices(
3028 Some(&manifest),
3029 vec![fri.clone()],
3030 &default_build_config(),
3031 None,
3032 FragReuseUpdate::None,
3033 )
3034 .unwrap_err();
3035 assert!(
3036 error.to_string().contains("assembled by the commit path"),
3037 "{error}"
3038 );
3039 let error = transaction
3040 .build_manifest(Some(&manifest), vec![fri], "txn", &default_build_config())
3041 .unwrap_err();
3042 assert!(
3043 error.to_string().contains("assembled by the commit path"),
3044 "{error}"
3045 );
3046 }
3047
3048 fn sample_manifest_with_fragments(ids: std::ops::Range<u64>) -> Manifest {
3049 let schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
3050 Manifest::new(
3051 LanceSchema::try_from(&schema).unwrap(),
3052 Arc::new(ids.map(Fragment::new).collect()),
3053 DataStorageFormat::new(ConcreteFileVersion::V2_0),
3054 HashMap::new(),
3055 )
3056 }
3057
3058 #[test]
3059 fn test_create_index_build_manifest_keeps_unremoved_same_name_indices() {
3060 let manifest = sample_manifest();
3061 let first_index = sample_index_metadata("vector_idx");
3062 let second_index = sample_index_metadata("vector_idx");
3063 let third_index = sample_index_metadata("vector_idx");
3064
3065 let transaction = Transaction::new(
3066 manifest.version,
3067 Operation::CreateIndex {
3068 new_indices: vec![third_index.clone()],
3069 removed_indices: vec![second_index.clone()],
3070 },
3071 None,
3072 );
3073
3074 let (_, final_indices) = transaction
3075 .build_manifest(
3076 Some(&manifest),
3077 vec![first_index.clone(), second_index.clone()],
3078 "txn",
3079 &default_build_config(),
3080 )
3081 .unwrap();
3082
3083 assert_eq!(final_indices.len(), 2);
3084 assert!(final_indices.iter().any(|idx| idx.uuid == first_index.uuid));
3085 assert!(final_indices.iter().any(|idx| idx.uuid == third_index.uuid));
3086 assert!(
3087 !final_indices
3088 .iter()
3089 .any(|idx| idx.uuid == second_index.uuid)
3090 );
3091 }
3092
3093 #[test]
3094 fn test_create_index_build_manifest_deduplicates_relisted_indices_by_uuid() {
3095 let manifest = sample_manifest();
3096 let first_index = sample_index_metadata("vector_idx");
3097 let second_index = sample_index_metadata("vector_idx");
3098 let third_index = sample_index_metadata("vector_idx");
3099
3100 let transaction = Transaction::new(
3101 manifest.version,
3102 Operation::CreateIndex {
3103 new_indices: vec![first_index.clone(), third_index.clone()],
3104 removed_indices: vec![second_index.clone()],
3105 },
3106 None,
3107 );
3108
3109 let (_, final_indices) = transaction
3110 .build_manifest(
3111 Some(&manifest),
3112 vec![first_index.clone(), second_index.clone()],
3113 "txn",
3114 &default_build_config(),
3115 )
3116 .unwrap();
3117
3118 assert_eq!(final_indices.len(), 2);
3119 assert_eq!(
3120 final_indices
3121 .iter()
3122 .filter(|idx| idx.uuid == first_index.uuid)
3123 .count(),
3124 1
3125 );
3126 assert!(final_indices.iter().any(|idx| idx.uuid == third_index.uuid));
3127 assert!(
3128 !final_indices
3129 .iter()
3130 .any(|idx| idx.uuid == second_index.uuid)
3131 );
3132 }
3133
3134 #[test]
3135 fn test_update_build_manifest_replaces_and_removes_fragments() {
3136 let manifest = sample_manifest_with_fragments(0..5);
3137
3138 let mut updated2 = Fragment::new(2);
3139 updated2.physical_rows = Some(42);
3140 let mut updated4 = Fragment::new(4);
3141 updated4.physical_rows = Some(43);
3142
3143 let transaction = Transaction::new(
3144 manifest.version,
3145 Operation::Update {
3146 removed_fragment_ids: vec![1],
3147 updated_fragments: vec![updated2, updated4, Fragment::new(99)],
3150 new_fragments: vec![],
3151 fields_modified: vec![],
3152 compacted_sstables: vec![],
3153 fields_for_preserving_frag_bitmap: vec![],
3154 update_mode: None,
3155 inserted_rows_filter: None,
3156 updated_fragment_offsets: None,
3157 },
3158 None,
3159 );
3160
3161 let (new_manifest, _) = transaction
3162 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
3163 .unwrap();
3164
3165 let ids: Vec<u64> = new_manifest.fragments.iter().map(|f| f.id).collect();
3166 assert_eq!(ids, vec![0, 2, 3, 4]);
3167 let rows: Vec<Option<usize>> = new_manifest
3168 .fragments
3169 .iter()
3170 .map(|f| f.physical_rows)
3171 .collect();
3172 assert_eq!(rows, vec![None, Some(42), None, Some(43)]);
3173 }
3174
3175 #[rstest::rstest]
3176 #[case::unspecified(None)]
3177 #[case::rewrite_rows(Some(UpdateMode::RewriteRows))]
3178 #[case::rewrite_columns(Some(UpdateMode::RewriteColumns))]
3179 fn test_update_build_manifest_does_not_restore_projected_files(
3180 #[case] update_mode: Option<UpdateMode>,
3181 #[values(false, true)] with_spilled_lineage: bool,
3182 ) {
3183 let mut manifest = sample_manifest_with_fragments(0..3);
3184 if with_spilled_lineage {
3185 manifest.reader_feature_flags = FLAG_STABLE_ROW_IDS;
3186 manifest.writer_feature_flags = FLAG_STABLE_ROW_IDS;
3187 manifest.next_row_id = 126;
3188 for fragment in Arc::make_mut(&mut manifest.fragments) {
3189 let start = fragment.id * 42;
3190 fragment.physical_rows = Some(42);
3191 fragment.row_id_meta = Some(RowIdMeta::Inline(
3192 write_row_ids(&RowIdSequence::from(start..start + 42)).into(),
3193 ));
3194 }
3195 }
3196 let projected_file = DataFile::new(
3197 "projected.lance",
3198 vec![0],
3199 vec![0],
3200 ConcreteFileVersion::V2_0,
3201 None,
3202 None,
3203 );
3204 Arc::make_mut(&mut manifest.fragments)[1].files = vec![projected_file.clone()];
3205
3206 let mut updated = manifest.fragments[1].clone();
3208 updated.files.push(DataFile::new(
3209 "dropped.lance",
3210 vec![1],
3211 vec![0],
3212 ConcreteFileVersion::V2_0,
3213 None,
3214 None,
3215 ));
3216 updated.physical_rows = Some(42);
3217 let inserted_projected_file = DataFile::new(
3218 "inserted-projected.lance",
3219 vec![0],
3220 vec![0],
3221 ConcreteFileVersion::V2_0,
3222 None,
3223 None,
3224 );
3225 let mut inserted = Fragment::new(0);
3226 inserted.physical_rows = Some(42);
3227 inserted.files = vec![
3228 inserted_projected_file.clone(),
3229 DataFile::new(
3230 "inserted-dropped.lance",
3231 vec![1],
3232 vec![0],
3233 ConcreteFileVersion::V2_0,
3234 None,
3235 None,
3236 ),
3237 ];
3238
3239 let mut expected_updated_files = vec![projected_file];
3240 let mut expected_inserted_files = vec![inserted_projected_file];
3241 if with_spilled_lineage {
3242 for (fragment, expected_files) in [
3245 (&mut updated, &mut expected_updated_files),
3246 (&mut inserted, &mut expected_inserted_files),
3247 ] {
3248 fragment.row_id_meta = Some(RowIdMeta::Column);
3249 fragment.created_at_version_meta = Some(RowDatasetVersionMeta::Column);
3250 fragment.last_updated_at_version_meta = Some(RowDatasetVersionMeta::Column);
3251 for field_id in [
3252 ROW_ID_FIELD_ID,
3253 ROW_CREATED_AT_VERSION_FIELD_ID,
3254 ROW_LAST_UPDATED_AT_VERSION_FIELD_ID,
3255 ] {
3256 let file = DataFile::new(
3257 format!("lineage-{field_id}.lance"),
3258 vec![field_id],
3259 vec![0],
3260 ConcreteFileVersion::V2_0,
3261 None,
3262 None,
3263 );
3264 fragment.files.push(file.clone());
3265 expected_files.push(file);
3266 }
3267 }
3268 }
3269
3270 let transaction = Transaction::new(
3271 manifest.version,
3272 Operation::Update {
3273 removed_fragment_ids: vec![],
3274 updated_fragments: vec![updated],
3275 new_fragments: vec![inserted],
3276 fields_modified: vec![1],
3277 compacted_sstables: vec![],
3278 fields_for_preserving_frag_bitmap: vec![],
3279 update_mode,
3280 inserted_rows_filter: None,
3281 updated_fragment_offsets: None,
3282 },
3283 None,
3284 );
3285
3286 let (new_manifest, _) = transaction
3287 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
3288 .unwrap();
3289
3290 let fragment = &new_manifest.fragments[1];
3291 assert_eq!(fragment.files, expected_updated_files);
3292 assert_eq!(fragment.physical_rows, Some(42));
3293 assert_eq!(new_manifest.fragments[3].files, expected_inserted_files);
3294 assert_eq!(new_manifest.max_field_id(), 0);
3295 }
3296
3297 #[test]
3298 fn test_delete_build_manifest_applies_deletion_to_current_fragment() {
3299 let mut manifest = sample_manifest_with_fragments(0..5);
3300 manifest.version = 2;
3301 let projected_file = DataFile::new_legacy_from_fields("projected.lance", vec![0], None);
3302 let current_fragment = &mut Arc::make_mut(&mut manifest.fragments)[2];
3303 current_fragment.files = vec![projected_file.clone()];
3304 current_fragment.physical_rows = Some(42);
3305 current_fragment.overlays = vec![overlay_with_field(0, 2)];
3306 current_fragment.last_updated_at_version_meta = Some(
3307 RowDatasetVersionMeta::from_sequence(
3308 &RowDatasetVersionSequence::from_uniform_row_count(42, 2),
3309 )
3310 .unwrap(),
3311 );
3312
3313 let mut updated2 = Fragment::new(2);
3315 updated2.physical_rows = Some(42);
3316 updated2.files = vec![
3317 projected_file.clone(),
3318 DataFile::new_legacy_from_fields("dropped.lance", vec![1], None),
3319 ];
3320 let deletion_file = DeletionFile {
3321 read_version: 1,
3322 id: 10,
3323 file_type: DeletionFileType::Array,
3324 num_deleted_rows: Some(1),
3325 base_id: None,
3326 };
3327 updated2.deletion_file = Some(deletion_file.clone());
3328
3329 let transaction = Transaction::new(
3330 1,
3331 Operation::Delete {
3332 updated_fragments: vec![updated2],
3333 deleted_fragment_ids: vec![1, 3],
3334 predicate: "id > 0".to_string(),
3335 },
3336 None,
3337 );
3338
3339 let (new_manifest, _) = transaction
3340 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
3341 .unwrap();
3342
3343 let ids: Vec<u64> = new_manifest.fragments.iter().map(|f| f.id).collect();
3344 assert_eq!(ids, vec![0, 2, 4]);
3345 let fragment2 = new_manifest
3346 .fragments
3347 .iter()
3348 .find(|fragment| fragment.id == 2)
3349 .unwrap();
3350 assert_eq!(fragment2.files, vec![projected_file]);
3351 assert_eq!(fragment2.deletion_file, Some(deletion_file));
3352 assert_eq!(fragment2.overlays.len(), 1);
3353 assert_eq!(fragment2.overlays[0].committed_version, 2);
3354 assert_eq!(fragment2.physical_rows, Some(42));
3355 assert_eq!(new_manifest.max_field_id(), 0);
3356 assert_eq!(last_updated_at_versions(&new_manifest, 2), vec![2; 42]);
3357 }
3358
3359 #[test]
3360 fn test_remove_tombstoned_data_files() {
3361 let mut fragment = Fragment::new(1);
3363
3364 fragment.files.push(DataFile {
3366 path: "normal.lance".to_string(),
3367 fields: Arc::from([1, 2, 3]),
3368 column_indices: Arc::from([]),
3369 file_major_version: 2,
3370 file_minor_version: 0,
3371 file_size_bytes: CachedFileSize::new(1000),
3372 base_id: None,
3373 });
3374
3375 fragment.files.push(DataFile {
3377 path: "all_tombstoned.lance".to_string(),
3378 fields: Arc::from([-2, -2, -2]),
3379 column_indices: Arc::from([]),
3380 file_major_version: 2,
3381 file_minor_version: 0,
3382 file_size_bytes: CachedFileSize::new(500),
3383 base_id: None,
3384 });
3385
3386 fragment.files.push(DataFile {
3388 path: "mixed.lance".to_string(),
3389 fields: Arc::from([4, -2, 5]),
3390 column_indices: Arc::from([]),
3391 file_major_version: 2,
3392 file_minor_version: 0,
3393 file_size_bytes: CachedFileSize::new(750),
3394 base_id: None,
3395 });
3396
3397 fragment.files.push(DataFile {
3399 path: "another_tombstoned.lance".to_string(),
3400 fields: Arc::from([-2_i32]),
3401 column_indices: Arc::from([]),
3402 file_major_version: 2,
3403 file_minor_version: 0,
3404 file_size_bytes: CachedFileSize::new(250),
3405 base_id: None,
3406 });
3407
3408 let mut fragments = vec![fragment];
3409
3410 Transaction::remove_tombstoned_data_files(&mut fragments);
3412
3413 assert_eq!(fragments[0].files.len(), 2);
3415 assert_eq!(fragments[0].files[0].path, "normal.lance");
3416 assert_eq!(fragments[0].files[1].path, "mixed.lance");
3417 }
3418
3419 #[test]
3423 fn test_partial_rewrite_skips_fragment_with_no_version_meta() {
3424 let row_ids = RowIdSequence::from([10u64, 11, 12, 13, 14].as_slice());
3425 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
3426
3427 let data_file = DataFile::new(
3428 "data.lance",
3429 vec![0],
3430 vec![0],
3431 LanceFileVersion::Stable.resolve(),
3432 None,
3433 None,
3434 );
3435
3436 let fragment = Fragment {
3437 id: 1,
3438 files: vec![data_file],
3439 overlays: vec![],
3440 deletion_file: None,
3441 row_id_meta,
3442 physical_rows: Some(5),
3443 last_updated_at_version_meta: None,
3444 created_at_version_meta: None,
3445 };
3446
3447 let manifest = make_stable_row_id_manifest(vec![fragment.clone()]);
3448
3449 let off_map = HashMap::from([(1u64, RoaringBitmap::from_iter([1u32, 3]))]);
3451 let tx = Transaction::new(
3452 manifest.version,
3453 Operation::Update {
3454 removed_fragment_ids: vec![],
3455 updated_fragments: vec![fragment],
3456 new_fragments: vec![],
3457 fields_modified: vec![],
3458 compacted_sstables: vec![],
3459 fields_for_preserving_frag_bitmap: vec![],
3460 update_mode: Some(UpdateMode::RewriteColumns),
3461 inserted_rows_filter: None,
3462 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
3463 },
3464 None,
3465 );
3466
3467 let (out, _) = tx
3468 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
3469 .unwrap();
3470
3471 assert!(
3472 out.fragments[0].last_updated_at_version_meta.is_none(),
3473 "fragment with no prior version metadata must not have fabricated prev_version stamped on unmatched rows"
3474 );
3475 }
3476
3477 #[test]
3478 fn test_bitmap_cardinality_exceeds_physical_rows() {
3479 let row_ids = RowIdSequence::from([10u64, 11, 12, 13, 14].as_slice());
3480 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
3481
3482 let data_file = DataFile::new(
3483 "data.lance",
3484 vec![0],
3485 vec![0],
3486 LanceFileVersion::Stable.resolve(),
3487 None,
3488 None,
3489 );
3490
3491 let version_seq = RowDatasetVersionSequence::from_uniform_row_count(5, 1);
3492 let version_meta = RowDatasetVersionMeta::from_sequence(&version_seq).unwrap();
3493
3494 let fragment = Fragment {
3495 id: 1,
3496 files: vec![data_file],
3497 overlays: vec![],
3498 deletion_file: None,
3499 row_id_meta,
3500 physical_rows: Some(5),
3501 last_updated_at_version_meta: Some(version_meta.clone()),
3502 created_at_version_meta: Some(version_meta),
3503 };
3504
3505 let manifest = make_stable_row_id_manifest(vec![fragment.clone()]);
3506
3507 let off_map = HashMap::from([(1u64, RoaringBitmap::from_iter(0u32..10))]);
3509 let tx = Transaction::new(
3510 manifest.version,
3511 Operation::Update {
3512 removed_fragment_ids: vec![],
3513 updated_fragments: vec![fragment],
3514 new_fragments: vec![],
3515 fields_modified: vec![],
3516 compacted_sstables: vec![],
3517 fields_for_preserving_frag_bitmap: vec![],
3518 update_mode: Some(UpdateMode::RewriteColumns),
3519 inserted_rows_filter: None,
3520 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
3521 },
3522 None,
3523 );
3524
3525 let result = tx.build_manifest(Some(&manifest), vec![], "txn", &default_build_config());
3526 assert!(result.is_err());
3527 let msg = result.unwrap_err().to_string();
3528 assert!(
3529 msg.contains("cardinality"),
3530 "expected cardinality error, got: {msg}"
3531 );
3532 }
3533
3534 #[test]
3535 fn test_bitmap_max_offset_exceeds_physical_rows() {
3536 let row_ids = RowIdSequence::from([10u64, 11, 12, 13, 14].as_slice());
3537 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
3538
3539 let data_file = DataFile::new(
3540 "data.lance",
3541 vec![0],
3542 vec![0],
3543 LanceFileVersion::Stable.resolve(),
3544 None,
3545 None,
3546 );
3547
3548 let version_seq = RowDatasetVersionSequence::from_uniform_row_count(5, 1);
3549 let version_meta = RowDatasetVersionMeta::from_sequence(&version_seq).unwrap();
3550
3551 let fragment = Fragment {
3552 id: 1,
3553 files: vec![data_file],
3554 overlays: vec![],
3555 deletion_file: None,
3556 row_id_meta,
3557 physical_rows: Some(5),
3558 last_updated_at_version_meta: Some(version_meta.clone()),
3559 created_at_version_meta: Some(version_meta),
3560 };
3561
3562 let manifest = make_stable_row_id_manifest(vec![fragment.clone()]);
3563
3564 let off_map = HashMap::from([(1u64, RoaringBitmap::from_iter([0u32, 100]))]);
3566 let tx = Transaction::new(
3567 manifest.version,
3568 Operation::Update {
3569 removed_fragment_ids: vec![],
3570 updated_fragments: vec![fragment],
3571 new_fragments: vec![],
3572 fields_modified: vec![],
3573 compacted_sstables: vec![],
3574 fields_for_preserving_frag_bitmap: vec![],
3575 update_mode: Some(UpdateMode::RewriteColumns),
3576 inserted_rows_filter: None,
3577 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
3578 },
3579 None,
3580 );
3581
3582 let result = tx.build_manifest(Some(&manifest), vec![], "txn", &default_build_config());
3583 assert!(result.is_err());
3584 let msg = result.unwrap_err().to_string();
3585 assert!(
3586 msg.contains("max offset"),
3587 "expected max offset error, got: {msg}"
3588 );
3589 }
3590
3591 #[test]
3592 fn test_bitmap_at_exact_physical_rows_boundary_succeeds() {
3593 let row_ids = RowIdSequence::from([10u64, 11, 12, 13, 14].as_slice());
3594 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
3595
3596 let data_file = DataFile::new(
3597 "data.lance",
3598 vec![0],
3599 vec![0],
3600 LanceFileVersion::Stable.resolve(),
3601 None,
3602 None,
3603 );
3604
3605 let version_seq = RowDatasetVersionSequence::from_uniform_row_count(5, 1);
3606 let version_meta = RowDatasetVersionMeta::from_sequence(&version_seq).unwrap();
3607
3608 let fragment = Fragment {
3609 id: 1,
3610 files: vec![data_file],
3611 overlays: vec![],
3612 deletion_file: None,
3613 row_id_meta,
3614 physical_rows: Some(5),
3615 last_updated_at_version_meta: Some(version_meta.clone()),
3616 created_at_version_meta: Some(version_meta),
3617 };
3618
3619 let manifest = make_stable_row_id_manifest(vec![fragment.clone()]);
3620
3621 let off_map = HashMap::from([(1u64, RoaringBitmap::from_iter(0u32..5))]);
3623 let tx = Transaction::new(
3624 manifest.version,
3625 Operation::Update {
3626 removed_fragment_ids: vec![],
3627 updated_fragments: vec![fragment],
3628 new_fragments: vec![],
3629 fields_modified: vec![],
3630 compacted_sstables: vec![],
3631 fields_for_preserving_frag_bitmap: vec![],
3632 update_mode: Some(UpdateMode::RewriteColumns),
3633 inserted_rows_filter: None,
3634 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
3635 },
3636 None,
3637 );
3638
3639 tx.build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
3640 .expect("bitmap at exact physical_rows boundary should succeed");
3641 }
3642
3643 #[test]
3644 fn test_updated_fragment_offsets_key_not_in_updated_fragments_is_rejected() {
3645 let make_fragment = |id: u64| {
3649 let row_ids = RowIdSequence::from([id * 10].as_slice());
3650 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
3651 Fragment {
3652 id,
3653 files: vec![DataFile::new(
3654 format!("{id}.lance"),
3655 vec![0],
3656 vec![0],
3657 LanceFileVersion::Stable.resolve(),
3658 None,
3659 None,
3660 )],
3661 overlays: vec![],
3662 deletion_file: None,
3663 row_id_meta,
3664 physical_rows: Some(5),
3665 last_updated_at_version_meta: None,
3666 created_at_version_meta: None,
3667 }
3668 };
3669
3670 let frag_a = make_fragment(1);
3671 let frag_b = make_fragment(2);
3672 let manifest = make_stable_row_id_manifest(vec![frag_a.clone(), frag_b.clone()]);
3673
3674 let off_map = HashMap::from([(frag_b.id, RoaringBitmap::from_iter([0u32, 1, 2]))]);
3676 let operation = Operation::Update {
3677 removed_fragment_ids: vec![],
3678 updated_fragments: vec![frag_a],
3679 new_fragments: vec![],
3680 fields_modified: vec![],
3681 compacted_sstables: vec![],
3682 fields_for_preserving_frag_bitmap: vec![],
3683 update_mode: Some(UpdateMode::RewriteColumns),
3684 inserted_rows_filter: None,
3685 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
3686 };
3687
3688 let err = validate_operation(Some(&manifest), &operation).unwrap_err();
3689 assert!(
3690 err.to_string().contains("not in updated_fragments"),
3691 "expected key-presence error, got: {err}"
3692 );
3693 }
3694
3695 #[test]
3696 fn test_proto_round_trip_field_10() {
3697 let off_map = HashMap::from([
3698 (1u64, RoaringBitmap::from_iter([1u32, 3, 5])),
3699 (2u64, RoaringBitmap::from_iter([0u32, 2, 4, 6])),
3700 ]);
3701 let tx = Transaction::new(
3702 1,
3703 Operation::Update {
3704 removed_fragment_ids: vec![],
3705 updated_fragments: vec![],
3706 new_fragments: vec![],
3707 fields_modified: vec![],
3708 compacted_sstables: vec![],
3709 fields_for_preserving_frag_bitmap: vec![],
3710 update_mode: Some(UpdateMode::RewriteColumns),
3711 inserted_rows_filter: None,
3712 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map.clone())),
3713 },
3714 None,
3715 );
3716
3717 let pb_tx: pb::Transaction = pb::Transaction::from(&tx);
3718
3719 if let Some(pb::transaction::Operation::Update(ref update)) = pb_tx.operation {
3721 assert!(
3722 update.updated_fragment_offsets.is_empty(),
3723 "field 9 should be empty"
3724 );
3725 assert_eq!(update.updated_fragment_offset_bitmaps.len(), 2);
3726 } else {
3727 panic!("expected Update operation");
3728 }
3729
3730 let tx2 = Transaction::try_from(pb_tx).unwrap();
3731 if let Operation::Update {
3732 updated_fragment_offsets: Some(UpdatedFragmentOffsets(m)),
3733 ..
3734 } = &tx2.operation
3735 {
3736 assert_eq!(m.len(), 2);
3737 assert_eq!(*m.get(&1).unwrap(), off_map[&1]);
3738 assert_eq!(*m.get(&2).unwrap(), off_map[&2]);
3739 } else {
3740 panic!("expected Update with offsets");
3741 }
3742 }
3743
3744 #[test]
3745 fn test_proto_legacy_field_9_read() {
3746 let pb_tx = pb::Transaction {
3748 read_version: 1,
3749 uuid: "test".to_string(),
3750 tag: String::new(),
3751 transaction_properties: HashMap::new(),
3752 operation: Some(pb::transaction::Operation::Update(
3753 pb::transaction::Update {
3754 removed_fragment_ids: vec![],
3755 updated_fragments: vec![],
3756 new_fragments: vec![],
3757 fields_modified: vec![],
3758 compacted_sstables: vec![],
3759 fields_for_preserving_frag_bitmap: vec![],
3760 update_mode: 1,
3761 inserted_rows: None,
3762 updated_fragment_offsets: HashMap::from([(
3763 1u64,
3764 pb::transaction::UInt32List {
3765 values: vec![1, 3, 5],
3766 },
3767 )]),
3768 updated_fragment_offset_bitmaps: HashMap::new(),
3769 },
3770 )),
3771 };
3772
3773 let tx = Transaction::try_from(pb_tx).unwrap();
3774 if let Operation::Update {
3775 updated_fragment_offsets: Some(UpdatedFragmentOffsets(m)),
3776 ..
3777 } = &tx.operation
3778 {
3779 assert_eq!(m.len(), 1);
3780 let bitmap = m.get(&1).unwrap();
3781 let offsets: Vec<u32> = bitmap.iter().collect();
3782 assert_eq!(offsets, vec![1, 3, 5]);
3783 } else {
3784 panic!("expected Update with offsets from legacy field 9");
3785 }
3786 }
3787
3788 #[test]
3789 fn test_proto_field_10_takes_precedence_over_field_9() {
3790 let mut bitmap_bytes = Vec::new();
3792 RoaringBitmap::from_iter([10u32, 20, 30])
3793 .serialize_into(&mut bitmap_bytes)
3794 .unwrap();
3795
3796 let pb_tx = pb::Transaction {
3797 read_version: 1,
3798 uuid: "test".to_string(),
3799 tag: String::new(),
3800 transaction_properties: HashMap::new(),
3801 operation: Some(pb::transaction::Operation::Update(
3802 pb::transaction::Update {
3803 removed_fragment_ids: vec![],
3804 updated_fragments: vec![],
3805 new_fragments: vec![],
3806 fields_modified: vec![],
3807 compacted_sstables: vec![],
3808 fields_for_preserving_frag_bitmap: vec![],
3809 update_mode: 1,
3810 inserted_rows: None,
3811 updated_fragment_offsets: HashMap::from([(
3813 1u64,
3814 pb::transaction::UInt32List {
3815 values: vec![99, 100],
3816 },
3817 )]),
3818 updated_fragment_offset_bitmaps: HashMap::from([(1u64, bitmap_bytes)]),
3819 },
3820 )),
3821 };
3822
3823 let tx = Transaction::try_from(pb_tx).unwrap();
3824 if let Operation::Update {
3825 updated_fragment_offsets: Some(UpdatedFragmentOffsets(m)),
3826 ..
3827 } = &tx.operation
3828 {
3829 let offsets: Vec<u32> = m.get(&1).unwrap().iter().collect();
3830 assert_eq!(offsets, vec![10, 20, 30], "field 10 should take precedence");
3831 } else {
3832 panic!("expected Update with offsets from field 10");
3833 }
3834 }
3835
3836 #[test]
3837 fn merge_build_manifest_refreshes_last_updated_when_data_files_change_stable_row_ids() {
3838 use crate::feature_flags::FLAG_STABLE_ROW_IDS;
3839 use lance_file::version::LanceFileVersion;
3840
3841 let mk_file = |path: &str| {
3842 DataFile::new(
3843 path,
3844 vec![0],
3845 vec![0],
3846 LanceFileVersion::Stable.resolve(),
3847 None,
3848 None,
3849 )
3850 };
3851
3852 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
3853 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
3854
3855 let row_ids = RowIdSequence::from([100u64, 101, 102, 103, 104].as_slice());
3856 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
3857
3858 let prev_fragment = Fragment {
3859 id: 0,
3860 files: vec![mk_file("before.lance")],
3861 overlays: vec![],
3862 deletion_file: None,
3863 row_id_meta,
3864 physical_rows: Some(5),
3865 last_updated_at_version_meta: None,
3866 created_at_version_meta: None,
3867 };
3868
3869 let mut manifest = Manifest::new(
3870 lance_schema.clone(),
3871 Arc::new(vec![prev_fragment.clone()]),
3872 DataStorageFormat::new(ConcreteFileVersion::V2_0),
3873 HashMap::new(),
3874 );
3875 manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
3876 manifest.next_row_id = 100;
3877
3878 let merged_fragment = Fragment {
3879 files: vec![mk_file("after.lance")],
3880 ..prev_fragment
3881 };
3882
3883 let tx = Transaction::new(
3884 manifest.version,
3885 Operation::Merge {
3886 fragments: vec![merged_fragment],
3887 schema: lance_schema,
3888 preserves_nullability: true,
3889 },
3890 None,
3891 );
3892
3893 let (out, _) = tx
3894 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
3895 .unwrap();
3896
3897 assert_eq!(out.version, 2);
3898 let frag = &out.fragments[0];
3899 let seq = frag
3900 .last_updated_at_version_meta
3901 .as_ref()
3902 .unwrap()
3903 .load_sequence()
3904 .unwrap();
3905 assert_eq!(seq.version_at(0).unwrap(), 2);
3906 assert_eq!(seq.version_at(4).unwrap(), 2);
3907 }
3908
3909 #[test]
3910 fn merge_build_manifest_skips_refresh_when_carry_forward_stable_row_ids() {
3911 use crate::feature_flags::FLAG_STABLE_ROW_IDS;
3912 use crate::rowids::version::{RowDatasetVersionMeta, RowDatasetVersionSequence};
3913 use lance_file::version::LanceFileVersion;
3914
3915 let data_file = DataFile::new(
3916 "same.lance",
3917 vec![0],
3918 vec![0],
3919 LanceFileVersion::Stable.resolve(),
3920 None,
3921 None,
3922 );
3923
3924 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
3925 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
3926
3927 let row_ids = RowIdSequence::from([200u64, 201, 202, 203, 204].as_slice());
3928 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
3929
3930 let uniform_v1 = RowDatasetVersionSequence::from_uniform_row_count(5, 1);
3931 let meta_v1 = RowDatasetVersionMeta::from_sequence(&uniform_v1).unwrap();
3932
3933 let prev_fragment = Fragment {
3934 id: 0,
3935 files: vec![data_file.clone()],
3936 overlays: vec![],
3937 deletion_file: None,
3938 row_id_meta: row_id_meta.clone(),
3939 physical_rows: Some(5),
3940 last_updated_at_version_meta: Some(meta_v1.clone()),
3941 created_at_version_meta: None,
3942 };
3943
3944 let mut manifest = Manifest::new(
3945 lance_schema.clone(),
3946 Arc::new(vec![prev_fragment]),
3947 DataStorageFormat::new(ConcreteFileVersion::V2_0),
3948 HashMap::new(),
3949 );
3950 manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
3951 manifest.next_row_id = 100;
3952
3953 let merged_fragment = Fragment {
3954 id: 0,
3955 files: vec![data_file],
3956 overlays: vec![],
3957 deletion_file: None,
3958 row_id_meta,
3959 physical_rows: Some(5),
3960 last_updated_at_version_meta: Some(meta_v1),
3961 created_at_version_meta: None,
3962 };
3963
3964 let tx = Transaction::new(
3965 manifest.version,
3966 Operation::Merge {
3967 fragments: vec![merged_fragment],
3968 schema: lance_schema,
3969 preserves_nullability: true,
3970 },
3971 None,
3972 );
3973
3974 let (out, _) = tx
3975 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
3976 .unwrap();
3977
3978 let seq = out.fragments[0]
3979 .last_updated_at_version_meta
3980 .as_ref()
3981 .unwrap()
3982 .load_sequence()
3983 .unwrap();
3984 assert_eq!(seq.version_at(0).unwrap(), 1);
3985 assert_eq!(seq.version_at(4).unwrap(), 1);
3986 }
3987
3988 #[test]
3989 fn merge_build_manifest_no_last_updated_refresh_without_stable_row_ids() {
3990 use crate::feature_flags::FLAG_STABLE_ROW_IDS;
3991 use lance_file::version::LanceFileVersion;
3992
3993 let mk_file = |path: &str| {
3994 DataFile::new(
3995 path,
3996 vec![0],
3997 vec![0],
3998 LanceFileVersion::Stable.resolve(),
3999 None,
4000 None,
4001 )
4002 };
4003
4004 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
4005 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
4006
4007 let prev_fragment = Fragment {
4008 id: 0,
4009 files: vec![mk_file("before.lance")],
4010 overlays: vec![],
4011 deletion_file: None,
4012 row_id_meta: None,
4013 physical_rows: Some(5),
4014 last_updated_at_version_meta: None,
4015 created_at_version_meta: None,
4016 };
4017
4018 let manifest = Manifest::new(
4019 lance_schema.clone(),
4020 Arc::new(vec![prev_fragment.clone()]),
4021 DataStorageFormat::new(ConcreteFileVersion::V2_0),
4022 HashMap::new(),
4023 );
4024 assert_eq!(
4025 manifest.reader_feature_flags & FLAG_STABLE_ROW_IDS,
4026 0,
4027 "manifest must not use stable row IDs for this guard test"
4028 );
4029
4030 let merged_fragment = Fragment {
4031 files: vec![mk_file("after.lance")],
4032 ..prev_fragment
4033 };
4034
4035 let tx = Transaction::new(
4036 manifest.version,
4037 Operation::Merge {
4038 fragments: vec![merged_fragment],
4039 schema: lance_schema,
4040 preserves_nullability: true,
4041 },
4042 None,
4043 );
4044
4045 let (out, _) = tx
4046 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4047 .unwrap();
4048
4049 assert!(
4050 out.fragments[0].last_updated_at_version_meta.is_none(),
4051 "without stable row IDs, Merge must not populate per-row last_updated metadata"
4052 );
4053 }
4054
4055 #[test]
4056 fn merge_build_manifest_sets_both_version_meta_for_new_fragment_id_stable_row_ids() {
4057 use crate::feature_flags::FLAG_STABLE_ROW_IDS;
4058 use lance_file::version::LanceFileVersion;
4059
4060 let mk_file = |path: &str| {
4061 DataFile::new(
4062 path,
4063 vec![0],
4064 vec![0],
4065 LanceFileVersion::Stable.resolve(),
4066 None,
4067 None,
4068 )
4069 };
4070
4071 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
4072 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
4073
4074 let row_ids_0 = RowIdSequence::from([10u64, 11, 12].as_slice());
4076 let existing_fragment = Fragment {
4077 id: 0,
4078 files: vec![mk_file("existing.lance")],
4079 overlays: vec![],
4080 deletion_file: None,
4081 row_id_meta: Some(RowIdMeta::Inline(write_row_ids(&row_ids_0).into())),
4082 physical_rows: Some(3),
4083 last_updated_at_version_meta: None,
4084 created_at_version_meta: None,
4085 };
4086
4087 let mut manifest = Manifest::new(
4088 lance_schema.clone(),
4089 Arc::new(vec![existing_fragment.clone()]),
4090 DataStorageFormat::new(ConcreteFileVersion::V2_0),
4091 HashMap::new(),
4092 );
4093 manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
4094 manifest.next_row_id = 100;
4095 manifest.version = 1;
4096
4097 let row_ids_1 = RowIdSequence::from([20u64, 21, 22, 23].as_slice());
4099 let new_fragment = Fragment {
4100 id: 1,
4101 files: vec![mk_file("new.lance")],
4102 overlays: vec![],
4103 deletion_file: None,
4104 row_id_meta: Some(RowIdMeta::Inline(write_row_ids(&row_ids_1).into())),
4105 physical_rows: Some(4),
4106 last_updated_at_version_meta: None,
4107 created_at_version_meta: None,
4108 };
4109
4110 let tx = Transaction::new(
4111 manifest.version,
4112 Operation::Merge {
4113 fragments: vec![existing_fragment, new_fragment],
4114 schema: lance_schema,
4115 preserves_nullability: true,
4116 },
4117 None,
4118 );
4119
4120 let (out, _) = tx
4121 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4122 .unwrap();
4123
4124 assert_eq!(out.version, 2);
4125
4126 let new_frag = out.fragments.iter().find(|f| f.id == 1).unwrap();
4127
4128 let last_updated_seq = new_frag
4130 .last_updated_at_version_meta
4131 .as_ref()
4132 .expect("new fragment must have last_updated_at_version_meta")
4133 .load_sequence()
4134 .unwrap();
4135 assert_eq!(last_updated_seq.version_at(0).unwrap(), 2);
4136 assert_eq!(last_updated_seq.version_at(3).unwrap(), 2);
4137
4138 let created_seq = new_frag
4140 .created_at_version_meta
4141 .as_ref()
4142 .expect("new fragment must have created_at_version_meta")
4143 .load_sequence()
4144 .unwrap();
4145 assert_eq!(created_seq.version_at(0).unwrap(), 2);
4146 assert_eq!(created_seq.version_at(3).unwrap(), 2);
4147 }
4148
4149 #[test]
4150 fn test_data_overlay_build_manifest_multi_fragment() {
4151 let mut frag0 = Fragment::new(0);
4157 frag0.overlays = vec![overlay_with_field(5, 3)]; let frag1 = Fragment::new(1);
4159 let mut frag2 = Fragment::new(2);
4160 frag2.overlays = vec![overlay_with_field(9, 3)]; let schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
4162 let mut manifest = Manifest::new(
4163 LanceSchema::try_from(&schema).unwrap(),
4164 Arc::new(vec![frag0, frag1, frag2]),
4165 crate::format::DataStorageFormat::new(ConcreteFileVersion::V2_0),
4166 HashMap::new(),
4167 );
4168 manifest.version = 3;
4172
4173 let txn = Transaction::new(
4174 manifest.version,
4175 Operation::DataOverlay {
4176 groups: vec![
4177 DataOverlayGroup {
4178 fragment_id: 0,
4179 overlays: vec![overlay_with_field(1, 0)],
4180 },
4181 DataOverlayGroup {
4182 fragment_id: 1,
4183 overlays: vec![overlay_with_field(2, 0)],
4184 },
4185 ],
4186 },
4187 None,
4188 );
4189
4190 let (result, _) = txn
4191 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4192 .unwrap();
4193
4194 let frag = |id: u64| {
4195 result
4196 .fragments
4197 .iter()
4198 .find(|f| f.id == id)
4199 .unwrap_or_else(|| panic!("fragment {id} missing from result"))
4200 };
4201 assert_eq!(frag(0).overlays.len(), 2);
4204 assert_eq!(frag(0).overlays[0].committed_version, 3);
4205 assert_eq!(frag(0).overlays[1].committed_version, result.version);
4206 assert_eq!(frag(1).overlays.len(), 1);
4208 assert_eq!(frag(1).overlays[0].committed_version, result.version);
4209 assert_eq!(frag(2).overlays.len(), 1);
4211 assert_eq!(frag(2).overlays[0].committed_version, 3);
4212 assert!(result.version > manifest.version);
4213 }
4214
4215 #[test]
4216 fn test_data_overlay_build_manifest_updates_row_lineage() {
4217 let row_ids = RowIdSequence::from([10u64, 11, 12, 13].as_slice());
4218 let version_meta = RowDatasetVersionMeta::from_sequence(
4219 &RowDatasetVersionSequence::from_uniform_row_count(4, 1),
4220 )
4221 .unwrap();
4222 let fragment = Fragment {
4223 id: 1,
4224 files: vec![],
4225 overlays: vec![],
4226 deletion_file: None,
4227 row_id_meta: Some(RowIdMeta::Inline(write_row_ids(&row_ids).into())),
4228 physical_rows: Some(4),
4229 last_updated_at_version_meta: Some(version_meta.clone()),
4230 created_at_version_meta: Some(version_meta),
4231 };
4232 let untracked_row_ids = RowIdSequence::from([20u64, 21].as_slice());
4235 let untracked_fragment = Fragment {
4236 id: 2,
4237 files: vec![],
4238 overlays: vec![],
4239 deletion_file: None,
4240 row_id_meta: Some(RowIdMeta::Inline(write_row_ids(&untracked_row_ids).into())),
4241 physical_rows: Some(2),
4242 last_updated_at_version_meta: None,
4243 created_at_version_meta: None,
4244 };
4245 let manifest = make_stable_row_id_manifest(vec![fragment, untracked_fragment]);
4246 let txn = Transaction::new(
4247 manifest.version,
4248 Operation::DataOverlay {
4249 groups: vec![
4250 DataOverlayGroup {
4251 fragment_id: 1,
4252 overlays: vec![
4253 DataOverlayFile {
4254 data_file: DataFile::new_legacy_from_fields(
4255 "dense.lance",
4256 vec![1],
4257 None,
4258 ),
4259 coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
4260 committed_version: 0,
4261 },
4262 DataOverlayFile {
4263 data_file: DataFile::new_legacy_from_fields(
4264 "sparse.lance",
4265 vec![1, 2],
4266 None,
4267 ),
4268 coverage: OverlayCoverage::sparse(vec![
4269 RoaringBitmap::from_iter([2u32]),
4270 RoaringBitmap::from_iter([3u32]),
4271 ]),
4272 committed_version: 0,
4273 },
4274 ],
4275 },
4276 DataOverlayGroup {
4277 fragment_id: 2,
4278 overlays: vec![DataOverlayFile {
4279 data_file: DataFile::new_legacy_from_fields(
4280 "migrated.lance",
4281 vec![1],
4282 None,
4283 ),
4284 coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([1u32])),
4285 committed_version: 0,
4286 }],
4287 },
4288 ],
4289 },
4290 None,
4291 );
4292
4293 let (result, _) = txn
4294 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4295 .unwrap();
4296
4297 assert_eq!(last_updated_at_versions(&result, 1), vec![5, 1, 5, 5]);
4298 assert_eq!(last_updated_at_versions(&result, 2), vec![1, 5]);
4299 assert!(
4300 result.fragments[0]
4301 .overlays
4302 .iter()
4303 .all(|overlay| overlay.committed_version == result.version)
4304 );
4305 }
4306
4307 #[test]
4308 fn test_data_replacement_tombstones_overlaid_fields() {
4309 let mut fragment = Fragment::new(0);
4315 fragment.files = vec![
4316 DataFile::new_legacy_from_fields("f3.lance", vec![3], None),
4317 DataFile::new_legacy_from_fields("f5.lance", vec![5], None),
4318 ];
4319 fragment.overlays = vec![
4320 DataOverlayFile {
4321 data_file: DataFile::new_legacy_from_fields("o35.lance", vec![3, 5], None),
4322 coverage: OverlayCoverage::sparse(vec![
4323 roaring::RoaringBitmap::from_iter([0u32]),
4324 roaring::RoaringBitmap::from_iter([0u32]),
4325 ]),
4326 committed_version: 1,
4327 },
4328 DataOverlayFile {
4329 data_file: DataFile::new_legacy_from_fields("o5.lance", vec![5], None),
4330 coverage: OverlayCoverage::dense(roaring::RoaringBitmap::from_iter([0u32])),
4331 committed_version: 1,
4332 },
4333 ];
4334
4335 let schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
4336 let manifest = Manifest::new(
4337 LanceSchema::try_from(&schema).unwrap(),
4338 Arc::new(vec![fragment]),
4339 crate::format::DataStorageFormat::new(ConcreteFileVersion::V2_0),
4340 HashMap::new(),
4341 );
4342
4343 let txn = Transaction::new(
4344 manifest.version,
4345 Operation::DataReplacement {
4346 replacements: vec![DataReplacementGroup(
4347 0,
4348 DataFile::new_legacy_from_fields("f5-new.lance", vec![5], None),
4349 )],
4350 },
4351 None,
4352 );
4353
4354 let (result, _) = txn
4355 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4356 .unwrap();
4357
4358 let frag = &result.fragments[0];
4359 assert!(frag.files.iter().any(|f| f.path == "f5-new.lance"));
4361 assert_eq!(frag.overlays.len(), 1);
4364 assert_eq!(frag.overlays[0].data_file.fields.as_ref(), &[3, -2]);
4365 }
4366
4367 fn replace_fields(
4371 fragment: Fragment,
4372 fields: Vec<i32>,
4373 manifest_version: u64,
4374 read_version: u64,
4375 ) -> Result<Fragment> {
4376 let schema = ArrowSchema::new(vec![
4377 ArrowField::new("x", DataType::Int32, true),
4378 ArrowField::new("a", DataType::Int32, true),
4379 ArrowField::new("v", DataType::Int32, true),
4380 ArrowField::new("y", DataType::Int32, true),
4381 ]);
4382 let mut lance_schema = LanceSchema::try_from(&schema).unwrap();
4383 lance_schema.fields[0].id = 3;
4384 lance_schema.fields[1].id = 4;
4385 lance_schema.fields[2].id = 5;
4386 lance_schema.fields[3].id = 6;
4387 let mut manifest = Manifest::new(
4388 lance_schema,
4389 Arc::new(vec![fragment]),
4390 crate::format::DataStorageFormat::new(ConcreteFileVersion::V2_0),
4391 HashMap::new(),
4392 );
4393 manifest.version = manifest_version;
4394
4395 let column_indices = (0..fields.len() as i32).collect();
4396 let txn = Transaction::new(
4397 read_version,
4398 Operation::DataReplacement {
4399 replacements: vec![DataReplacementGroup(
4400 0,
4401 DataFile::new(
4402 "v-new.lance",
4403 fields,
4404 column_indices,
4405 ConcreteFileVersion::V2_0,
4406 None,
4407 None,
4408 ),
4409 )],
4410 },
4411 None,
4412 );
4413 txn.build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4414 .map(|(manifest, _)| manifest.fragments[0].clone())
4415 }
4416
4417 fn replace_field_5(
4420 fragment: Fragment,
4421 manifest_version: u64,
4422 read_version: u64,
4423 ) -> Result<Fragment> {
4424 replace_fields(fragment, vec![5], manifest_version, read_version)
4425 }
4426
4427 #[test]
4428 fn test_data_replacement_rejects_subset_of_legacy_file() {
4429 let mut fragment = Fragment::new(0);
4434 fragment.files = vec![DataFile::new_legacy_from_fields(
4435 "wide.lance",
4436 vec![4, 5],
4437 None,
4438 )];
4439
4440 let result = replace_field_5(fragment, 1, 1);
4441 assert!(
4442 result.is_err(),
4443 "legacy subset replacement must be rejected, got: {:?}",
4444 result.map(|fragment| fragment.files)
4445 );
4446 }
4447
4448 #[test]
4449 fn test_data_replacement_tombstones_fields_spanning_files() {
4450 let mut fragment = Fragment::new(0);
4454 fragment.files = vec![
4455 DataFile::new(
4456 "ab.lance",
4457 vec![3, 4],
4458 vec![0, 1],
4459 ConcreteFileVersion::V2_0,
4460 None,
4461 None,
4462 ),
4463 DataFile::new(
4464 "cd.lance",
4465 vec![5, 6],
4466 vec![0, 1],
4467 ConcreteFileVersion::V2_0,
4468 None,
4469 None,
4470 ),
4471 ];
4472
4473 let fragment = replace_fields(fragment, vec![4, 5], 1, 1).unwrap();
4474 let file = |path| {
4475 fragment
4476 .files
4477 .iter()
4478 .find(|file| file.path == path)
4479 .unwrap_or_else(|| panic!("{path} survives on its live field"))
4480 };
4481 assert_eq!(file("ab.lance").fields.as_ref(), &[3, TOMBSTONE_FIELD_ID]);
4482 assert_eq!(file("cd.lance").fields.as_ref(), &[TOMBSTONE_FIELD_ID, 6]);
4483 assert!(fragment.files.iter().any(|file| file.path == "v-new.lance"));
4484 }
4485
4486 #[test]
4487 fn test_data_replacement_rejects_fields_spanning_a_legacy_file() {
4488 let mut fragment = Fragment::new(0);
4492 fragment.files = vec![
4493 DataFile::new(
4494 "ab.lance",
4495 vec![3, 4],
4496 vec![0, 1],
4497 ConcreteFileVersion::V2_0,
4498 None,
4499 None,
4500 ),
4501 DataFile::new_legacy_from_fields("cd.lance", vec![5, 6], None),
4502 ];
4503
4504 let result = replace_fields(fragment, vec![4, 5], 1, 1);
4505 assert!(
4506 result.is_err(),
4507 "spanning a legacy file must be rejected, got: {:?}",
4508 result.map(|fragment| fragment.files)
4509 );
4510 }
4511
4512 #[test]
4513 fn test_data_replacement_retombstones_wider_file() {
4514 let mut fragment = Fragment::new(0);
4518 fragment.files = vec![DataFile::new(
4519 "wide.lance",
4520 vec![4, TOMBSTONE_FIELD_ID, 5],
4521 vec![0, 1, 2],
4522 ConcreteFileVersion::V2_0,
4523 None,
4524 None,
4525 )];
4526
4527 let fragment = replace_fields(fragment, vec![5], 1, 1).unwrap();
4528 let wide = fragment
4529 .files
4530 .iter()
4531 .find(|file| file.path == "wide.lance")
4532 .expect("wider file survives on its live field");
4533 assert_eq!(
4534 wide.fields.as_ref(),
4535 &[4, TOMBSTONE_FIELD_ID, TOMBSTONE_FIELD_ID]
4536 );
4537 assert!(fragment.files.iter().any(|file| file.path == "v-new.lance"));
4538 }
4539
4540 #[test]
4541 fn test_data_replacement_preserves_overlay_newer_than_snapshot() {
4542 let mut fragment = Fragment::new(0);
4547 fragment.files = vec![DataFile::new(
4549 "wide.lance",
4550 vec![4, 5],
4551 vec![0, 1],
4552 ConcreteFileVersion::V2_0,
4553 None,
4554 None,
4555 )];
4556 fragment.overlays = vec![DataOverlayFile {
4557 data_file: DataFile::new(
4558 "newer.lance",
4559 vec![5],
4560 vec![0],
4561 ConcreteFileVersion::V2_0,
4562 None,
4563 None,
4564 ),
4565 coverage: OverlayCoverage::dense(roaring::RoaringBitmap::from_iter([0u32])),
4566 committed_version: 7,
4567 }];
4568
4569 let fragment = replace_field_5(fragment, 7, 6).unwrap();
4571 assert!(fragment.files.iter().any(|f| f.path == "v-new.lance"));
4572 assert_eq!(
4573 fragment.overlays.len(),
4574 1,
4575 "overlay committed after the snapshot must survive"
4576 );
4577 assert_eq!(fragment.overlays[0].data_file.fields.as_ref(), &[5]);
4578 }
4579
4580 #[test]
4584 fn project_keeps_file_carrying_spilled_row_lineage() {
4585 let mut fragment = Fragment::new(0);
4586 fragment.physical_rows = Some(4);
4587 fragment.row_id_meta = Some(RowIdMeta::Column);
4588 fragment.created_at_version_meta = Some(RowDatasetVersionMeta::Column);
4589 fragment.files = vec![
4590 DataFile::new(
4591 "d.lance",
4592 vec![0, 1],
4593 vec![0, 1],
4594 ConcreteFileVersion::V2_0,
4595 None,
4596 None,
4597 ),
4598 DataFile::new(
4599 "l.lance",
4600 vec![ROW_ID_FIELD_ID, ROW_CREATED_AT_VERSION_FIELD_ID],
4601 vec![0, 1],
4602 ConcreteFileVersion::V2_0,
4603 None,
4604 None,
4605 ),
4606 ];
4607 let schema = ArrowSchema::new(vec![
4608 ArrowField::new("a", DataType::Int32, false),
4609 ArrowField::new("b", DataType::Int32, false),
4610 ]);
4611 let mut manifest = Manifest::new(
4612 LanceSchema::try_from(&schema).unwrap(),
4613 Arc::new(vec![fragment]),
4614 DataStorageFormat::new(ConcreteFileVersion::V2_0),
4615 HashMap::new(),
4616 );
4617 manifest.reader_feature_flags = FLAG_STABLE_ROW_IDS;
4618 manifest.writer_feature_flags = FLAG_STABLE_ROW_IDS;
4619
4620 let transaction = Transaction::new(
4622 manifest.version,
4623 Operation::Project {
4624 schema: manifest.schema.project_by_ids(&[0], true),
4625 preserves_nullability: true,
4626 },
4627 None,
4628 );
4629 let (result, _) = transaction
4630 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4631 .unwrap();
4632
4633 let fragment = &result.fragments[0];
4634 let paths = fragment
4635 .files
4636 .iter()
4637 .map(|file| file.path.as_str())
4638 .collect::<Vec<_>>();
4639 assert_eq!(paths, ["d.lance", "l.lance"]);
4640 for field_id in [ROW_ID_FIELD_ID, ROW_CREATED_AT_VERSION_FIELD_ID] {
4641 assert_eq!(
4642 fragment
4643 .row_lineage_file(field_id)
4644 .unwrap()
4645 .map(|file| file.path.as_str()),
4646 Some("l.lance"),
4647 "field {field_id}"
4648 );
4649 }
4650 }
4651
4652 #[rstest::rstest]
4659 #[case::lineage_only_file(
4660 vec![("v.lance", vec![3, 4, 5, 6]), ("l.lance", vec![ROW_ID_FIELD_ID])],
4661 vec![3, 4],
4662 vec![
4663 ("v.lance", vec![TOMBSTONE_FIELD_ID, TOMBSTONE_FIELD_ID, 5, 6]),
4664 ("l.lance", vec![ROW_ID_FIELD_ID]),
4665 ("v-new.lance", vec![3, 4]),
4666 ],
4667 "l.lance"
4668 )]
4669 #[case::in_file_carrier(
4670 vec![("v.lance", vec![3, 4, 5, 6, ROW_ID_FIELD_ID])],
4671 vec![3, 4, 5, 6],
4672 vec![
4673 (
4674 "v.lance",
4675 vec![
4676 TOMBSTONE_FIELD_ID,
4677 TOMBSTONE_FIELD_ID,
4678 TOMBSTONE_FIELD_ID,
4679 TOMBSTONE_FIELD_ID,
4680 ROW_ID_FIELD_ID,
4681 ],
4682 ),
4683 ("v-new.lance", vec![3, 4, 5, 6]),
4684 ],
4685 "v.lance"
4686 )]
4687 fn data_replacement_keeps_lineage_carrier(
4688 #[case] files: Vec<(&str, Vec<i32>)>,
4689 #[case] replaced_fields: Vec<i32>,
4690 #[case] expected_files: Vec<(&str, Vec<i32>)>,
4691 #[case] carrier: &str,
4692 ) {
4693 let mut fragment = Fragment::new(0);
4694 fragment.row_id_meta = Some(RowIdMeta::Column);
4695 fragment.files = files
4696 .into_iter()
4697 .map(|(path, fields)| {
4698 let column_indices = (0..fields.len() as i32).collect();
4699 DataFile::new(
4700 path,
4701 fields,
4702 column_indices,
4703 ConcreteFileVersion::V2_0,
4704 None,
4705 None,
4706 )
4707 })
4708 .collect();
4709
4710 let fragment = replace_fields(fragment, replaced_fields, 1, 1).unwrap();
4711 let files = fragment
4712 .files
4713 .iter()
4714 .map(|file| (file.path.as_str(), file.fields.to_vec()))
4715 .collect::<Vec<_>>();
4716 assert_eq!(files, expected_files);
4717 assert_eq!(
4718 fragment
4719 .row_lineage_file(ROW_ID_FIELD_ID)
4720 .unwrap()
4721 .map(|file| file.path.as_str()),
4722 Some(carrier)
4723 );
4724 }
4725
4726 #[rstest::rstest]
4731 #[case::no_carrier(vec![0], ConcreteFileVersion::V2_0)]
4732 #[case::v1_carrier(vec![0, ROW_ID_FIELD_ID], ConcreteFileVersion::V1)]
4733 fn build_manifest_rejects_spilled_arm_without_carrier(
4734 #[case] fields: Vec<i32>,
4735 #[case] version: ConcreteFileVersion,
4736 ) {
4737 let manifest = sample_manifest();
4738 let mut fragment = Fragment::new(0);
4739 fragment.physical_rows = Some(4);
4740 fragment.row_id_meta = Some(RowIdMeta::Column);
4741 let data_file = DataFile::new("d.lance", fields, vec![], version, None, None);
4743 fragment.files = vec![data_file];
4744 let transaction = Transaction::new(
4745 manifest.version,
4746 Operation::Merge {
4747 fragments: vec![fragment],
4748 schema: manifest.schema.clone(),
4749 preserves_nullability: true,
4750 },
4751 None,
4752 );
4753
4754 let err = transaction
4755 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4756 .unwrap_err();
4757 assert!(matches!(err, Error::Internal { .. }), "{err:?}");
4758 let message = err.to_string();
4759 assert!(
4760 message.contains("fragment 0") && message.contains(&ROW_ID_FIELD_ID.to_string()),
4761 "{message}"
4762 );
4763 }
4764
4765 #[test]
4766 fn test_data_overlay_build_manifest_merges_duplicate_groups() {
4767 let manifest = sample_manifest();
4770 let txn = Transaction::new(
4771 manifest.version,
4772 Operation::DataOverlay {
4773 groups: vec![
4774 DataOverlayGroup {
4775 fragment_id: 0,
4776 overlays: vec![overlay_with_field(1, 0)],
4777 },
4778 DataOverlayGroup {
4779 fragment_id: 0,
4780 overlays: vec![overlay_with_field(2, 0)],
4781 },
4782 ],
4783 },
4784 None,
4785 );
4786
4787 let (result, _) = txn
4788 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4789 .unwrap();
4790
4791 let overlays = &result.fragments[0].overlays;
4792 assert_eq!(overlays.len(), 2);
4793 assert_eq!(overlays[0].data_file.fields.as_ref(), [1i32].as_slice());
4794 assert_eq!(overlays[1].data_file.fields.as_ref(), [2i32].as_slice());
4795 }
4796
4797 #[test]
4798 fn test_data_overlay_build_manifest_rejects_unknown_fragment() {
4799 let manifest = sample_manifest();
4800 let txn = Transaction::new(
4801 manifest.version,
4802 Operation::DataOverlay {
4803 groups: vec![DataOverlayGroup {
4804 fragment_id: 99,
4805 overlays: vec![overlay_with_field(1, 0)],
4806 }],
4807 },
4808 None,
4809 );
4810 let err = txn
4811 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
4812 .unwrap_err();
4813 assert!(err.to_string().contains("does not exist"), "{err}");
4814 }
4815
4816 #[test]
4817 fn test_nullability_assertion_defaults_conservative() {
4818 for encoded in [false, true] {
4822 let txn = Transaction::try_from(pb::Transaction {
4823 read_version: 1,
4824 uuid: "test".to_string(),
4825 operation: Some(pb::transaction::Operation::Project(
4826 pb::transaction::Project {
4827 schema: vec![],
4828 preserves_nullability: encoded,
4829 },
4830 )),
4831 ..Default::default()
4832 })
4833 .unwrap();
4834 assert!(
4835 matches!(txn.operation, Operation::Project { preserves_nullability, .. } if preserves_nullability == encoded),
4836 "encoded={encoded:?}"
4837 );
4838
4839 let txn = Transaction::try_from(pb::Transaction {
4840 read_version: 1,
4841 uuid: "test".to_string(),
4842 operation: Some(pb::transaction::Operation::Merge(pb::transaction::Merge {
4843 fragments: vec![],
4844 schema: vec![],
4845 schema_metadata: Default::default(),
4846 preserves_nullability: encoded,
4847 })),
4848 ..Default::default()
4849 })
4850 .unwrap();
4851 assert!(
4852 matches!(txn.operation, Operation::Merge { preserves_nullability, .. } if preserves_nullability == encoded),
4853 "encoded={encoded:?}"
4854 );
4855 }
4856 }
4857
4858 mod mem_wal_index_coverage {
4859 use super::*;
4860 use crate::system_index::mem_wal::{
4861 CompactedSsTable, IndexCatchupProgress, MEM_WAL_INDEX_NAME, MemWalIndexDetails,
4862 };
4863
4864 fn user_index(name: &str, uuid: Uuid, frags: &[u32]) -> IndexMetadata {
4865 IndexMetadata {
4866 uuid,
4867 name: name.to_string(),
4868 fields: vec![0],
4869 covering_fields: vec![],
4870 dataset_version: 1,
4871 fragment_bitmap: Some(RoaringBitmap::from_iter(frags.iter().copied())),
4872 index_details: None,
4873 index_version: 0,
4874 created_at: None,
4875 base_id: None,
4876 files: None,
4877 }
4878 }
4879
4880 fn mem_wal_index(details: MemWalIndexDetails) -> IndexMetadata {
4881 crate::system_index::mem_wal::new_mem_wal_index_meta(1, details).unwrap()
4882 }
4883
4884 fn coverage_for(indices: &[IndexMetadata], name: &str) -> Option<Vec<CompactedSsTable>> {
4885 let meta = indices
4886 .iter()
4887 .find(|idx| idx.name == MEM_WAL_INDEX_NAME)
4888 .expect("mem wal index present");
4889 load_mem_wal_index_details(meta.clone())
4890 .unwrap()
4891 .index_catchup
4892 .into_iter()
4893 .find(|entry| entry.index_name == name)
4894 .map(|entry| entry.caught_up_generations)
4895 }
4896
4897 fn compacted(shard: Uuid, generation: u64) -> Vec<CompactedSsTable> {
4898 vec![CompactedSsTable::new(shard, generation)]
4899 }
4900
4901 fn manifest_with(frags: &[u32]) -> Manifest {
4904 let fragments: Vec<Fragment> =
4905 frags.iter().map(|id| Fragment::new(*id as u64)).collect();
4906 Manifest::new(
4907 LanceSchema::default(),
4908 Arc::new(fragments),
4909 DataStorageFormat::default(),
4910 Default::default(),
4911 )
4912 }
4913
4914 fn apply(
4916 after: &mut [IndexMetadata],
4917 before: &[IndexMetadata],
4918 read_frags: &[u32],
4919 read_indices: &[IndexMetadata],
4920 ) -> Result<()> {
4921 let manifest = manifest_with(read_frags);
4922 let segments_before = Transaction::logical_index_segments(before);
4923 Transaction::apply_mem_wal_index_coverage(
4924 after,
4925 &segments_before,
4926 Some(ReadVersionState {
4927 manifest: &manifest,
4928 indices: read_indices,
4929 }),
4930 2,
4931 )
4932 }
4933
4934 fn table(idx_frags: &[u32], uuid: Uuid, details: MemWalIndexDetails) -> Vec<IndexMetadata> {
4935 vec![user_index("idx", uuid, idx_frags), mem_wal_index(details)]
4936 }
4937
4938 fn progress(shard: Uuid, generation: u64) -> MemWalIndexDetails {
4939 MemWalIndexDetails {
4940 compacted_sstables: compacted(shard, generation),
4941 ..Default::default()
4942 }
4943 }
4944
4945 fn progress_with_catchup(shard: Uuid, generation: u64, caught: u64) -> MemWalIndexDetails {
4946 MemWalIndexDetails {
4947 compacted_sstables: compacted(shard, generation),
4948 index_catchup: vec![IndexCatchupProgress::new(
4949 "idx".to_string(),
4950 compacted(shard, caught),
4951 )],
4952 ..Default::default()
4953 }
4954 }
4955
4956 #[test]
4962 fn a_withdrawal_in_preparation_withdraws_the_catch_up_record() {
4963 let shard = Uuid::new_v4();
4964 let uuid = Uuid::new_v4();
4965 let read = table(&[0, 1], uuid, progress_with_catchup(shard, 5, 5));
4966 let mut manifest = crate::transaction::test_support::sample_manifest();
4968 manifest.fragments = Arc::new(vec![Fragment::new(0), Fragment::new(1)]);
4969 let mut update = crate::transaction::test_support::update_txn(vec![]);
4970 let Operation::Update {
4971 updated_fragments,
4972 fields_modified,
4973 ..
4974 } = &mut update.operation
4975 else {
4976 unreachable!()
4977 };
4978 *updated_fragments = vec![Fragment::new(0)];
4979 *fields_modified = vec![0];
4980 let transaction = Transaction::new(manifest.version, update.operation, None);
4981 let (_, after) = transaction
4982 .build_manifest_with_read_version(
4983 Some(&manifest),
4984 read.clone(),
4985 "txn",
4986 &default_build_config(),
4987 Some(ReadVersionState {
4988 manifest: &manifest,
4989 indices: &read,
4990 }),
4991 )
4992 .unwrap();
4993 let idx = after.iter().find(|idx| idx.name == "idx").unwrap();
4994 assert_eq!(
4995 idx.fragment_bitmap,
4996 Some(RoaringBitmap::from_iter([1u32])),
4997 "the rewritten fragment left the coverage in preparation"
4998 );
4999 assert_eq!(
5000 coverage_for(&after, "idx"),
5001 None,
5002 "a record derived over the original coverage is not carried"
5003 );
5004 }
5005
5006 #[test]
5009 fn an_index_covering_the_read_version_is_credited() {
5010 let shard = Uuid::new_v4();
5011 let read = table(&[0, 1], Uuid::new_v4(), progress(shard, 5));
5012 let mut after = table(&[0, 1], Uuid::new_v4(), progress(shard, 5));
5013 apply(&mut after, &read, &[0, 1], &read).unwrap();
5014 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 5)));
5015 }
5016
5017 #[test]
5020 fn an_index_short_of_the_read_version_is_not_credited() {
5021 let shard = Uuid::new_v4();
5022 let read = table(&[0], Uuid::new_v4(), progress(shard, 5));
5023 let mut after = table(&[0], Uuid::new_v4(), progress(shard, 5));
5024 apply(&mut after, &read, &[0, 1], &read).unwrap();
5025 assert_eq!(coverage_for(&after, "idx"), None);
5026 }
5027
5028 #[test]
5036 fn a_bitmap_pruned_in_place_does_not_keep_its_position() {
5037 let shard = Uuid::new_v4();
5038 let uuid = Uuid::new_v4();
5039 let before = table(&[0, 1], uuid, progress_with_catchup(shard, 5, 5));
5040 let mut after = table(&[0], uuid, progress_with_catchup(shard, 5, 5));
5042 apply(&mut after, &before, &[0, 1], &before).unwrap();
5043 assert_eq!(
5044 coverage_for(&after, "idx"),
5045 None,
5046 "a shrunken index kept a position it no longer earns"
5047 );
5048 }
5049
5050 #[test]
5055 fn an_unchanged_index_is_not_raised_beyond_what_it_proves() {
5056 let shard = Uuid::new_v4();
5057 let uuid = Uuid::new_v4();
5058 let before = table(&[0], uuid, progress_with_catchup(shard, 5, 2));
5060 let mut after = before.clone();
5061 apply(&mut after, &before, &[0, 1], &before).unwrap();
5063 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 2)));
5064 }
5065
5066 #[test]
5070 fn a_carried_position_cannot_exceed_the_committed_progress() {
5071 let shard = Uuid::new_v4();
5072 let uuid = Uuid::new_v4();
5073 let before = table(&[0], uuid, progress_with_catchup(shard, 3, 9));
5074 let mut after = before.clone();
5075 apply(&mut after, &before, &[0], &before).unwrap();
5076 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 3)));
5077 }
5078
5079 #[test]
5082 fn an_unchanged_index_is_never_lowered() {
5083 let shard = Uuid::new_v4();
5084 let uuid = Uuid::new_v4();
5085 let before = table(&[0], uuid, progress_with_catchup(shard, 9, 9));
5086 let mut after = before.clone();
5087 apply(&mut after, &before, &[0, 1], &before).unwrap();
5088 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 9)));
5089 }
5090
5091 #[test]
5095 fn credit_is_capped_by_the_committed_progress() {
5096 let shard = Uuid::new_v4();
5097 let read = table(&[0], Uuid::new_v4(), progress(shard, 9));
5098 let mut after = table(&[0], Uuid::new_v4(), progress(shard, 3));
5099 apply(&mut after, &read, &[0], &read).unwrap();
5100 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 3)));
5101 }
5102
5103 #[test]
5108 fn credit_never_reaches_past_the_read_version() {
5109 let shard = Uuid::new_v4();
5110 let read = table(&[0], Uuid::new_v4(), progress(shard, 2));
5112 let mut after = table(&[0], Uuid::new_v4(), progress(shard, 5));
5113 apply(&mut after, &read, &[0], &read).unwrap();
5114 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 2)));
5115 }
5116
5117 #[test]
5121 fn an_index_with_an_unknown_segment_is_not_credited() {
5122 let shard = Uuid::new_v4();
5123 let mut unknown = user_index("idx", Uuid::new_v4(), &[]);
5124 unknown.fragment_bitmap = None;
5125 let read = vec![
5126 user_index("idx", Uuid::new_v4(), &[0, 1]),
5127 unknown,
5128 mem_wal_index(progress(shard, 5)),
5129 ];
5130 let mut after = read.clone();
5131 apply(&mut after, &read, &[0, 1], &read).unwrap();
5132 assert_eq!(coverage_for(&after, "idx"), None);
5133 }
5134
5135 #[test]
5137 fn a_dropped_index_loses_its_entry() {
5138 let shard = Uuid::new_v4();
5139 let before = table(&[0], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
5140 let mut after = vec![mem_wal_index(progress_with_catchup(shard, 5, 5))];
5141 apply(&mut after, &before, &[0], &before).unwrap();
5142 assert_eq!(coverage_for(&after, "idx"), None);
5143 }
5144
5145 #[test]
5150 fn a_new_index_covering_the_read_version_is_credited() {
5151 let shard = Uuid::new_v4();
5152 let before = vec![mem_wal_index(progress(shard, 5))];
5153 let mut after = table(&[0], Uuid::new_v4(), progress(shard, 5));
5154 apply(&mut after, &before, &[0], &before).unwrap();
5156 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 5)));
5157 }
5158
5159 #[test]
5163 fn a_table_with_no_catchup_entry_earns_one() {
5164 let shard = Uuid::new_v4();
5165 let before = table(&[0], Uuid::new_v4(), progress(shard, 5));
5166 let mut after = before.clone();
5167 apply(&mut after, &before, &[0], &before).unwrap();
5168 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 5)));
5169 }
5170
5171 #[test]
5173 fn each_shard_is_credited_independently() {
5174 let merged = Uuid::new_v4();
5175 let idle = Uuid::new_v4();
5176 let details = MemWalIndexDetails {
5177 compacted_sstables: vec![
5178 CompactedSsTable::new(merged, 4),
5179 CompactedSsTable::new(idle, 0),
5180 ],
5181 ..Default::default()
5182 };
5183 let read = table(&[0], Uuid::new_v4(), details.clone());
5184 let mut after = table(&[0], Uuid::new_v4(), details);
5185 apply(&mut after, &read, &[0], &read).unwrap();
5186 let coverage = coverage_for(&after, "idx").expect("credited");
5187 assert_eq!(
5188 coverage
5189 .iter()
5190 .find(|g| g.shard_id == merged)
5191 .map(|g| g.generation),
5192 Some(4)
5193 );
5194 assert_eq!(
5195 coverage
5196 .iter()
5197 .find(|g| g.shard_id == idle)
5198 .map(|g| g.generation),
5199 Some(0)
5200 );
5201 }
5202
5203 #[test]
5205 fn indexes_are_credited_independently() {
5206 let shard = Uuid::new_v4();
5207 let read = vec![
5208 user_index("fast", Uuid::new_v4(), &[0, 1]),
5209 user_index("slow", Uuid::new_v4(), &[0]),
5210 mem_wal_index(progress(shard, 6)),
5211 ];
5212 let mut after = read.clone();
5213 apply(&mut after, &read, &[0, 1], &read).unwrap();
5214 assert_eq!(coverage_for(&after, "fast"), Some(compacted(shard, 6)));
5215 assert_eq!(coverage_for(&after, "slow"), None);
5216 }
5217
5218 #[test]
5220 fn an_index_without_a_bitmap_is_not_credited() {
5221 let shard = Uuid::new_v4();
5222 let mut idx = user_index("idx", Uuid::new_v4(), &[0]);
5223 idx.fragment_bitmap = None;
5224 let read = vec![idx, mem_wal_index(progress(shard, 5))];
5225 let mut after = read.clone();
5226 apply(&mut after, &read, &[0], &read).unwrap();
5227 assert_eq!(coverage_for(&after, "idx"), None);
5228 }
5229
5230 #[test]
5232 fn no_compaction_progress_writes_no_entries() {
5233 let before = table(&[0], Uuid::new_v4(), MemWalIndexDetails::default());
5234 let mut after = before.clone();
5235 let untouched = after.clone();
5236 apply(&mut after, &before, &[0], &before).unwrap();
5237 assert_eq!(after, untouched);
5238 }
5239
5240 #[test]
5242 fn a_table_without_mem_wal_is_a_no_op() {
5243 let before = vec![user_index("idx", Uuid::new_v4(), &[0])];
5244 let mut after = before.clone();
5245 let untouched = after.clone();
5246 apply(&mut after, &before, &[0], &before).unwrap();
5247 assert_eq!(after, untouched);
5248 }
5249
5250 #[test]
5253 fn without_a_read_version_nothing_changes() {
5254 let shard = Uuid::new_v4();
5255 let uuid = Uuid::new_v4();
5256 let before = table(&[0], uuid, progress_with_catchup(shard, 5, 5));
5257 let mut after = before.clone();
5258 let segments_before = Transaction::logical_index_segments(&before);
5259 Transaction::apply_mem_wal_index_coverage(&mut after, &segments_before, None, 2)
5260 .unwrap();
5261 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 5)));
5262 }
5263
5264 #[test]
5267 fn an_untrained_index_earns_nothing() {
5268 let shard = Uuid::new_v4();
5269 let read = vec![
5270 user_index("untrained", Uuid::new_v4(), &[]),
5271 user_index("trained", Uuid::new_v4(), &[0]),
5272 mem_wal_index(progress(shard, 10)),
5273 ];
5274 let mut after = read.clone();
5275 apply(&mut after, &read, &[0], &read).unwrap();
5276 assert_eq!(coverage_for(&after, "untrained"), None);
5277 assert_eq!(coverage_for(&after, "trained"), Some(compacted(shard, 10)));
5278 }
5279
5280 #[test]
5283 fn a_shard_keeps_its_position_while_another_advances() {
5284 let (advancing, quiet) = (Uuid::new_v4(), Uuid::new_v4());
5285 let uuid = Uuid::new_v4();
5286 let details = |advancing_gen: u64| MemWalIndexDetails {
5287 compacted_sstables: vec![
5288 CompactedSsTable::new(advancing, advancing_gen),
5289 CompactedSsTable::new(quiet, 10),
5290 ],
5291 index_catchup: vec![IndexCatchupProgress::new(
5292 "idx".to_string(),
5293 vec![CompactedSsTable::new(quiet, 7)],
5294 )],
5295 ..Default::default()
5296 };
5297 let read = vec![
5300 user_index("idx", uuid, &[0]),
5301 mem_wal_index(MemWalIndexDetails {
5302 compacted_sstables: vec![CompactedSsTable::new(advancing, 9)],
5303 ..details(9)
5304 }),
5305 ];
5306 let mut after = vec![user_index("idx", uuid, &[0]), mem_wal_index(details(10))];
5307 apply(&mut after, &read, &[0], &read).unwrap();
5308
5309 let mut coverage = coverage_for(&after, "idx").expect("credited");
5310 coverage.sort_unstable_by_key(|sstable| sstable.shard_id);
5311 let mut expected = vec![
5312 CompactedSsTable::new(advancing, 9),
5313 CompactedSsTable::new(quiet, 7),
5314 ];
5315 expected.sort_unstable_by_key(|sstable| sstable.shard_id);
5316 assert_eq!(coverage, expected);
5317 }
5318
5319 #[test]
5323 fn an_ordinary_index_job_is_never_blocked() {
5324 let shard = Uuid::new_v4();
5325 let before = table(&[0, 1], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
5326 let mut after = table(&[0], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
5328 apply(&mut after, &before, &[0, 1], &before).unwrap();
5329 assert_eq!(coverage_for(&after, "idx"), None);
5330 }
5331
5332 #[test]
5336 fn an_index_caught_up_to_nothing_gets_no_entry() {
5337 let shard = Uuid::new_v4();
5338 let uuid = Uuid::new_v4();
5339 let before = table(&[0], uuid, progress_with_catchup(shard, 5, 0));
5340 let mut after = before.clone();
5341 apply(&mut after, &before, &[0, 1], &before).unwrap();
5343 assert_eq!(coverage_for(&after, "idx"), None);
5344 }
5345
5346 #[test]
5349 fn carried_positions_do_not_leak_between_shards() {
5350 let (ahead, behind) = (Uuid::new_v4(), Uuid::new_v4());
5351 let uuid = Uuid::new_v4();
5352 let details = MemWalIndexDetails {
5353 compacted_sstables: vec![
5354 CompactedSsTable::new(ahead, 10),
5355 CompactedSsTable::new(behind, 10),
5356 ],
5357 index_catchup: vec![IndexCatchupProgress::new(
5358 "idx".to_string(),
5359 vec![
5360 CompactedSsTable::new(ahead, 8),
5361 CompactedSsTable::new(behind, 2),
5362 ],
5363 )],
5364 ..Default::default()
5365 };
5366 let before = vec![user_index("idx", uuid, &[0]), mem_wal_index(details)];
5367 let mut after = before.clone();
5368 apply(&mut after, &before, &[0, 1], &before).unwrap();
5370
5371 let mut coverage = coverage_for(&after, "idx").expect("carried");
5372 coverage.sort_unstable_by_key(|sstable| sstable.shard_id);
5373 let mut expected = vec![
5374 CompactedSsTable::new(ahead, 8),
5375 CompactedSsTable::new(behind, 2),
5376 ];
5377 expected.sort_unstable_by_key(|sstable| sstable.shard_id);
5378 assert_eq!(coverage, expected);
5379 }
5380
5381 #[test]
5387 fn a_bitmap_narrowed_after_the_build_loses_its_position() {
5388 let shard = Uuid::new_v4();
5389 let uuid = Uuid::new_v4();
5390 let mut migrated = table(&[0], uuid, progress_with_catchup(shard, 5, 5));
5393
5394 Transaction::withdraw_coverage_invalidated_after_build(
5395 &mut migrated,
5396 &["idx".to_string()],
5397 3,
5398 )
5399 .unwrap();
5400
5401 assert_eq!(coverage_for(&migrated, "idx"), None);
5402 }
5403
5404 #[test]
5408 fn metadata_migration_that_does_not_narrow_keeps_its_position() {
5409 let shard = Uuid::new_v4();
5410 let uuid = Uuid::new_v4();
5411 let mut migrated = table(&[0, 1], uuid, progress_with_catchup(shard, 5, 5));
5412 migrated[0].files = Some(Vec::new());
5413 migrated[0].created_at = Some(chrono::Utc::now());
5414
5415 Transaction::withdraw_coverage_invalidated_after_build(&mut migrated, &[], 3).unwrap();
5417
5418 assert_eq!(coverage_for(&migrated, "idx"), Some(compacted(shard, 5)));
5419 }
5420
5421 #[test]
5424 fn an_unchanged_commit_does_not_rewrite_the_system_index() {
5425 let shard = Uuid::new_v4();
5426 let uuid = Uuid::new_v4();
5427 let before = table(&[0], uuid, progress_with_catchup(shard, 5, 5));
5428 let mut after = before.clone();
5429 apply(&mut after, &before, &[0], &before).unwrap();
5430
5431 let system_uuid = |indices: &[IndexMetadata]| {
5432 indices
5433 .iter()
5434 .find(|idx| idx.name == MEM_WAL_INDEX_NAME)
5435 .unwrap()
5436 .uuid
5437 };
5438 assert_eq!(system_uuid(&after), system_uuid(&before));
5439 }
5440
5441 #[test]
5445 fn without_a_read_version_a_changed_index_still_loses_its_position() {
5446 let shard = Uuid::new_v4();
5447 let before = table(&[0, 1], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
5448 let mut after = table(&[0], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
5449 let segments_before = Transaction::logical_index_segments(&before);
5450 Transaction::apply_mem_wal_index_coverage(&mut after, &segments_before, None, 2)
5451 .unwrap();
5452 assert_eq!(coverage_for(&after, "idx"), None);
5453 }
5454
5455 #[test]
5458 fn the_derivation_is_stable_across_attempts() {
5459 let shard = Uuid::new_v4();
5460 let read = table(&[0, 1], Uuid::new_v4(), progress(shard, 5));
5461 let mut first = table(&[0, 1], Uuid::new_v4(), progress(shard, 5));
5462 let mut second = first.clone();
5463 apply(&mut first, &read, &[0, 1], &read).unwrap();
5464 apply(&mut second, &read, &[0, 1], &read).unwrap();
5465 assert_eq!(coverage_for(&first, "idx"), coverage_for(&second, "idx"));
5466 }
5467 }
5468}