1use crate::feature_flags::{FLAG_COVERED_INDEX_METADATA, FLAG_STABLE_ROW_IDS, apply_feature_flags};
14use crate::format::overlay::TOMBSTONE_FIELD_ID;
15use crate::format::{
16 DataFile, DataStorageFormat, Fragment, IndexMetadata, Manifest, ManifestBuildConfig,
17 overlay::DataOverlayFile,
18};
19use crate::io::{
20 commit::CommitHandler,
21 manifest::{read_manifest, read_manifest_indexes},
22};
23use crate::rowids::version::build_version_meta;
24use crate::system_index::is_system_index;
25use crate::system_index::mem_wal::{
26 CompactedSsTable, IndexCatchupProgress, MEM_WAL_INDEX_NAME, load_mem_wal_index_details,
27 new_mem_wal_index_meta, update_mem_wal_index_compacted_sstables,
28};
29use crate::transaction::UpdateMode::{RewriteColumns, RewriteRows};
30use crate::transaction::row_version::resolve_update_version_metadata;
31use crate::transaction::update_map::apply_update_map;
32use crate::transaction::validate::merge_fragment_physically_rewritten;
33use crate::transaction::{
34 CoverageIdentity, DataReplacementGroup, LogicalIndexSegments, Operation, ReadVersionState,
35 RewriteGroup, Transaction, UpdatedFragmentOffsets,
36};
37use lance_core::datatypes::{
38 LANCE_UNENFORCED_CLUSTERING_KEY_POSITION, LANCE_UNENFORCED_PRIMARY_KEY,
39 LANCE_UNENFORCED_PRIMARY_KEY_POSITION,
40};
41use lance_core::{Error, Result};
42use lance_file::version::ConcreteFileVersion;
43use lance_io::object_store::ObjectStore;
44use object_store::path::Path;
45use roaring::RoaringBitmap;
46use std::collections::{BTreeMap, HashMap, HashSet};
47use std::sync::Arc;
48use uuid::Uuid;
49
50impl Transaction {
51 pub(super) fn fragments_with_ids<'a, T>(
52 new_fragments: T,
53 fragment_id: &'a mut u64,
54 ) -> impl Iterator<Item = Fragment> + 'a
55 where
56 T: IntoIterator<Item = Fragment> + 'a,
57 {
58 new_fragments.into_iter().map(move |mut f| {
59 if f.id == 0 {
60 f.id = *fragment_id;
61 *fragment_id += 1;
62 }
63 f
64 })
65 }
66
67 fn data_storage_format_from_files(
68 fragments: &[Fragment],
69 user_requested: Option<ConcreteFileVersion>,
70 ) -> Result<DataStorageFormat> {
71 if let Some(file_version) = Fragment::try_infer_version(fragments)? {
72 if let Some(user_requested) = user_requested
74 && user_requested != file_version
75 {
76 return Err(Error::invalid_input(format!(
77 "User requested data storage version ({}) does not match version in data files ({})",
78 user_requested, file_version
79 )));
80 }
81 Ok(DataStorageFormat::new(file_version))
82 } else {
83 Ok(user_requested
85 .map(DataStorageFormat::new)
86 .unwrap_or_default())
87 }
88 }
89
90 pub async fn restore_old_manifest(
91 object_store: &ObjectStore,
92 commit_handler: &dyn CommitHandler,
93 base_path: &Path,
94 version: u64,
95 config: &ManifestBuildConfig,
96 tx_path: &str,
97 current_manifest: &Manifest,
98 ) -> Result<(Manifest, Vec<IndexMetadata>)> {
99 let location = commit_handler
100 .resolve_version_location(base_path, version, &object_store.inner)
101 .await?;
102 let mut manifest = read_manifest(object_store, &location.path, location.size).await?;
103 manifest.set_timestamp(config.timestamp_nanos);
104 manifest.transaction_file = Some(tx_path.to_string());
105 let indices = read_manifest_indexes(object_store, &location, &manifest).await?;
106 manifest.max_fragment_id = manifest
107 .max_fragment_id
108 .max(current_manifest.max_fragment_id);
109 manifest.next_row_id = manifest.next_row_id.max(current_manifest.next_row_id);
111 if current_manifest.uses_stable_row_ids() && !manifest.uses_stable_row_ids() {
114 return Err(Error::invalid_input(format!(
115 "Cannot restore version {version}: stable row ids were enabled \
116 after it, and turning them back off would let row addresses \
117 collide with ids this table has already used"
118 )));
119 }
120 Ok((manifest, indices))
121 }
122
123 pub fn logical_index_segments(indices: &[IndexMetadata]) -> LogicalIndexSegments {
129 let mut by_name: LogicalIndexSegments = BTreeMap::new();
130 for idx in indices.iter().filter(|idx| !is_system_index(idx)) {
131 by_name
132 .entry(idx.name.clone())
133 .or_default()
134 .push(CoverageIdentity {
135 uuid: idx.uuid,
136 fragment_bitmap: idx.fragment_bitmap.clone(),
137 });
138 }
139 for segments in by_name.values_mut() {
140 segments.sort_unstable_by_key(|segment| segment.uuid);
141 }
142 by_name
143 }
144
145 pub fn apply_mem_wal_index_coverage(
169 final_indices: &mut [IndexMetadata],
170 segments_before: &LogicalIndexSegments,
171 read_version_state: Option<ReadVersionState<'_>>,
172 new_version: u64,
173 ) -> Result<()> {
174 let Some(pos) = final_indices
175 .iter()
176 .position(|idx| idx.name == MEM_WAL_INDEX_NAME)
177 else {
178 return Ok(());
181 };
182
183 let mut details = load_mem_wal_index_details(final_indices[pos].clone())?;
184
185 if details.compacted_sstables.is_empty() && details.index_catchup.is_empty() {
188 return Ok(());
189 }
190
191 let segments_after = Self::logical_index_segments(final_indices);
192 let catchup_before = std::mem::take(&mut details.index_catchup);
193
194 let read_details = read_version_state
200 .map(|state| {
201 state
202 .indices
203 .iter()
204 .find(|idx| idx.name == MEM_WAL_INDEX_NAME)
205 .cloned()
206 .map(load_mem_wal_index_details)
207 .transpose()
208 })
209 .transpose()?
210 .flatten();
211 let shards: Vec<(Uuid, u64, u64)> = details
212 .compacted_sstables
213 .iter()
214 .map(|committed| {
215 let at_read = read_details
216 .as_ref()
217 .and_then(|read| {
218 read.compacted_sstables
219 .iter()
220 .find(|s| s.shard_id == committed.shard_id)
221 })
222 .map_or(0, |s| s.generation);
223 (
224 committed.shard_id,
225 committed.generation,
226 at_read.min(committed.generation),
227 )
228 })
229 .collect();
230
231 let read_fragments: Option<RoaringBitmap> = read_version_state.map(|state| {
234 state
235 .manifest
236 .fragments
237 .iter()
238 .map(|fragment| fragment.id as u32)
239 .collect()
240 });
241
242 let covers_read_version = |segments: &[CoverageIdentity]| -> bool {
243 let Some(required) = read_fragments.as_ref() else {
244 return false;
245 };
246 if required.is_empty() {
247 return false;
256 }
257 let mut covered = RoaringBitmap::new();
258 for segment in segments {
259 match segment.fragment_bitmap.as_ref() {
260 Some(bitmap) => covered |= bitmap,
261 None => return false,
263 }
264 }
265 required.is_subset(&covered)
266 };
267
268 let mut rebuilt: Vec<IndexCatchupProgress> = Vec::new();
269 for (name, after) in segments_after.iter() {
270 let unchanged = segments_before.get(name) == Some(after);
275 let carried = unchanged
276 .then(|| catchup_before.iter().find(|e| e.index_name == *name))
277 .flatten();
278 let proven = covers_read_version(after);
279
280 if carried.is_none() && !proven {
281 continue;
284 }
285
286 let generations = shards
287 .iter()
288 .map(|&(shard_id, committed, creditable)| {
289 let prior = carried
290 .and_then(|entry| entry.caught_up_generation_for_shard(&shard_id))
291 .unwrap_or(0);
292 let credited = if proven { creditable } else { 0 };
293 CompactedSsTable::new(shard_id, prior.max(credited).min(committed))
300 })
301 .collect::<Vec<_>>();
302 if generations.iter().all(|g| g.generation == 0) {
303 continue;
304 }
305 rebuilt.push(IndexCatchupProgress::new(name.clone(), generations));
306 }
307 rebuilt.sort_by(|a, b| a.index_name.cmp(&b.index_name));
308
309 let mut before_sorted = catchup_before;
310 before_sorted.sort_by(|a, b| a.index_name.cmp(&b.index_name));
311 if rebuilt == before_sorted {
312 return Ok(());
313 }
314
315 let dropped: Vec<&str> = before_sorted
316 .iter()
317 .map(|e| e.index_name.as_str())
318 .filter(|name| !rebuilt.iter().any(|kept| kept.index_name == *name))
319 .collect();
320 if !dropped.is_empty() {
321 log::info!(
323 "MemWAL index catch-up invalidated at version {new_version} for {dropped:?}: \
324 these indices changed and no longer cover the version this commit read"
325 );
326 }
327
328 details.index_catchup = rebuilt;
329 final_indices[pos] = new_mem_wal_index_meta(new_version, details)?;
330 Ok(())
331 }
332
333 pub fn withdraw_coverage_invalidated_after_build(
341 indices: &mut [IndexMetadata],
342 changed: &[String],
343 new_version: u64,
344 ) -> Result<()> {
345 if changed.is_empty() {
346 return Ok(());
347 }
348 let Some(pos) = indices
349 .iter()
350 .position(|idx| idx.name == MEM_WAL_INDEX_NAME)
351 else {
352 return Ok(());
353 };
354 let mut details = load_mem_wal_index_details(indices[pos].clone())?;
355 let before = details.index_catchup.len();
356 details
357 .index_catchup
358 .retain(|entry| !changed.contains(&entry.index_name));
359 if details.index_catchup.len() == before {
360 return Ok(());
361 }
362 log::info!(
363 "MemWAL index catch-up withdrawn at version {new_version} for {changed:?}: \
364 these indices were recalculated after their coverage was derived"
365 );
366 indices[pos] = new_mem_wal_index_meta(new_version, details)?;
367 Ok(())
368 }
369
370 pub fn build_manifest(
374 &self,
375 current_manifest: Option<&Manifest>,
376 current_indices: Vec<IndexMetadata>,
377 transaction_file_path: &str,
378 config: &ManifestBuildConfig,
379 ) -> Result<(Manifest, Vec<IndexMetadata>)> {
380 self.build_manifest_with_read_version(
381 current_manifest,
382 current_indices,
383 transaction_file_path,
384 config,
385 None,
386 )
387 }
388
389 pub fn build_manifest_with_read_version(
396 &self,
397 current_manifest: Option<&Manifest>,
398 current_indices: Vec<IndexMetadata>,
399 transaction_file_path: &str,
400 config: &ManifestBuildConfig,
401 read_version_state: Option<ReadVersionState<'_>>,
402 ) -> Result<(Manifest, Vec<IndexMetadata>)> {
403 if config.use_stable_row_ids
404 && config.migration_next_row_id.is_none()
405 && current_manifest
406 .map(|m| !m.uses_stable_row_ids())
407 .unwrap_or_default()
408 {
409 return Err(Error::not_supported_source(
410 "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(),
411 ));
412 }
413
414 if config.migration_next_row_id.is_some() && !current_indices.is_empty() {
415 let names: Vec<&str> = current_indices
416 .iter()
417 .map(|idx| idx.name.as_str())
418 .collect();
419 return Err(Error::invalid_input(format!(
420 "Cannot migrate to stable row IDs while indexes exist on the dataset. \
421 Drop the following indexes first, then re-run the migration, and \
422 recreate them afterwards: {}",
423 names.join(", ")
424 )));
425 }
426 let mut reference_paths = match current_manifest {
427 Some(m) => m.base_paths.clone(),
428 None => HashMap::new(),
429 };
430
431 if let Operation::Overwrite {
432 initial_bases: Some(initial_bases),
433 ..
434 } = &self.operation
435 {
436 if current_manifest.is_none() {
437 for base_path in initial_bases.iter() {
441 if reference_paths.contains_key(&base_path.id) {
442 return Err(Error::invalid_input(format!(
443 "Duplicate base path ID {} detected. Base path IDs must be unique.",
444 base_path.id
445 )));
446 }
447 reference_paths.insert(base_path.id, base_path.clone());
448 }
449 } else {
450 return Err(Error::invalid_input(
453 "OVERWRITE mode cannot register new bases. This should have been caught by validation.",
454 ));
455 }
456 }
457
458 let schema = match self.operation {
460 Operation::Overwrite { ref schema, .. } => schema.clone(),
461 Operation::Merge { ref schema, .. } => schema.clone(),
462 Operation::Project { ref schema, .. } => schema.clone(),
463 _ => {
464 if let Some(current_manifest) = current_manifest {
465 current_manifest.schema.clone()
466 } else {
467 return Err(Error::internal(
468 "Cannot create a new dataset without a schema".to_string(),
469 ));
470 }
471 }
472 };
473
474 let mut fragment_id = current_manifest
479 .and_then(|m| m.max_fragment_id())
480 .map(|id| id + 1)
481 .unwrap_or(0);
482 let mut final_fragments = Vec::new();
483 let mut final_indices = current_indices;
484
485 let mem_wal_segments_before = final_indices
490 .iter()
491 .any(|idx| idx.name == MEM_WAL_INDEX_NAME)
492 .then(|| Self::logical_index_segments(&final_indices));
493
494 let mut next_row_id = {
495 match (current_manifest, config.use_stable_row_ids) {
498 (Some(manifest), _) if manifest.reader_feature_flags & FLAG_STABLE_ROW_IDS != 0 => {
499 Some(manifest.next_row_id)
500 }
501 (None, true) => Some(0),
502 (_, false) => None,
503 (Some(_), true) => {
504 if let Some(migration_nri) = config.migration_next_row_id {
506 Some(migration_nri)
507 } else {
508 return Err(Error::not_supported_source(
509 "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(),
510 ));
511 }
512 }
513 }
514 };
515
516 let maybe_existing_fragments =
517 current_manifest
518 .map(|m| m.fragments.as_ref())
519 .ok_or_else(|| {
520 Error::internal(format!(
521 "No current manifest was provided while building manifest for operation {}",
522 self.operation.name()
523 ))
524 });
525
526 let new_version = current_manifest.map_or(1, |m| m.version + 1);
527
528 match &self.operation {
529 Operation::Clone { .. } => {
530 return Err(Error::internal(
531 "Clone operation should not enter build_manifest.".to_string(),
532 ));
533 }
534 Operation::Append { fragments } => {
535 final_fragments.extend(maybe_existing_fragments?.clone());
536 let mut new_fragments =
537 Self::fragments_with_ids(fragments.clone(), &mut fragment_id)
538 .collect::<Vec<_>>();
539 if let Some(next_row_id) = &mut next_row_id {
540 Self::assign_row_ids(next_row_id, new_fragments.as_mut_slice())?;
541 for fragment in new_fragments.iter_mut() {
543 let version_meta = build_version_meta(fragment, new_version);
544 fragment.last_updated_at_version_meta = version_meta.clone();
545 fragment.created_at_version_meta = version_meta;
546 }
547 }
548 final_fragments.extend(new_fragments);
549 }
550 Operation::Delete {
551 updated_fragments,
552 deleted_fragment_ids,
553 ..
554 } => {
555 let deleted_ids: HashSet<u64> = deleted_fragment_ids.iter().copied().collect();
558 let updated_by_id: HashMap<u64, &Fragment> =
559 updated_fragments.iter().map(|f| (f.id, f)).collect();
560 final_fragments.extend(maybe_existing_fragments?.clone());
561 final_fragments.retain(|f| !deleted_ids.contains(&f.id));
562 final_fragments.iter_mut().for_each(|f| {
563 if let Some(updated) = updated_by_id.get(&f.id) {
564 *f = (*updated).clone();
565 }
566 });
567 Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments)
568 }
569 Operation::Update {
570 removed_fragment_ids,
571 updated_fragments,
572 new_fragments,
573 fields_modified,
574 compacted_sstables,
575 fields_for_preserving_frag_bitmap,
576 update_mode,
577 updated_fragment_offsets,
578 ..
579 } => {
580 let existing_fragments = maybe_existing_fragments?;
582
583 let removed_ids: HashSet<u64> = removed_fragment_ids.iter().copied().collect();
586 let mut updated_by_id: HashMap<u64, &Fragment> =
587 HashMap::with_capacity(updated_fragments.len());
588 for fragment in updated_fragments {
589 updated_by_id.entry(fragment.id).or_insert(fragment);
590 }
591 let updated_frags: Vec<Fragment> = existing_fragments
592 .iter()
593 .filter_map(|f| {
594 if removed_ids.contains(&f.id) {
595 return None;
596 }
597 if let Some(&updated) = updated_by_id.get(&f.id) {
598 let mut updated = updated.clone();
599 updated.overlays = f.overlays.clone();
605 if matches!(update_mode, Some(RewriteColumns)) {
606 crate::format::overlay::tombstone_overlay_fields(
607 &mut updated.overlays,
608 fields_modified,
609 );
610 }
611 Some(updated)
612 } else {
613 Some(f.clone())
614 }
615 })
616 .collect();
617
618 if next_row_id.is_some() {
623 }
625
626 final_fragments.extend(updated_frags);
627
628 if next_row_id.is_some()
629 && matches!(update_mode, Some(RewriteColumns))
630 && let Some(UpdatedFragmentOffsets(off_map)) = updated_fragment_offsets
631 && !off_map.is_empty()
632 {
633 let prev_version = current_manifest.map(|m| m.version).unwrap_or(0);
634 for fragment in final_fragments.iter_mut() {
635 let Some(bitmap) = off_map.get(&fragment.id) else {
636 continue;
637 };
638 if !updated_by_id.contains_key(&fragment.id) {
643 continue;
644 }
645 if bitmap.is_empty() {
646 continue;
647 }
648 if fragment.last_updated_at_version_meta.is_none() {
652 continue;
653 }
654 let max_allowed = existing_fragments
655 .iter()
656 .find(|f| f.id == fragment.id)
657 .and_then(|f| f.physical_rows)
658 .unwrap_or(1 << 24);
659 if bitmap.len() as usize > max_allowed {
660 return Err(Error::invalid_input(format!(
661 "updatedFragmentOffsets cardinality {} exceeds fragment {} limit {}",
662 bitmap.len(),
663 fragment.id,
664 max_allowed
665 )));
666 }
667 if let Some(max_off) = bitmap.max()
668 && max_off as usize >= max_allowed
669 {
670 return Err(Error::invalid_input(format!(
671 "updatedFragmentOffsets max offset {} exceeds fragment {} limit {}",
672 max_off, fragment.id, max_allowed
673 )));
674 }
675 let offsets: Vec<usize> = bitmap.iter().map(|o| o as usize).collect();
676 crate::rowids::version::refresh_row_latest_update_meta_for_partial_frag_rewrite_cols(
677 fragment,
678 &offsets,
679 new_version,
680 prev_version,
681 )?;
682 }
683 }
684
685 Self::prune_updated_fields_from_indices(
687 &mut final_indices,
688 updated_fragments,
689 fields_modified,
690 );
691
692 let mut new_fragments =
693 Self::fragments_with_ids(new_fragments.clone(), &mut fragment_id)
694 .collect::<Vec<_>>();
695
696 if let Some(next_row_id) = &mut next_row_id {
699 Self::assign_row_ids(next_row_id, new_fragments.as_mut_slice())?;
700 }
701
702 if next_row_id.is_some() {
703 resolve_update_version_metadata(
704 existing_fragments,
705 new_fragments.as_mut_slice(),
706 new_version,
707 )?;
708 }
709
710 if config.use_stable_row_ids
711 && update_mode.is_some()
712 && *update_mode == Some(RewriteRows)
713 {
714 let pure_updated_frag_ids =
715 Self::collect_pure_rewrite_row_update_frags_ids(&new_fragments)?;
716
717 let original_fragment_ids: Vec<u64> = removed_fragment_ids
719 .iter()
720 .chain(updated_fragments.iter().map(|f| &f.id))
721 .copied()
722 .collect();
723
724 let original_overlaid_frags: HashMap<u32, &Fragment> = existing_fragments
729 .iter()
730 .filter(|f| {
731 (removed_ids.contains(&f.id) || updated_by_id.contains_key(&f.id))
732 && !f.overlays.is_empty()
733 })
734 .map(|f| (f.id as u32, f))
735 .collect();
736
737 Self::register_pure_rewrite_rows_update_frags_in_indices(
738 &mut final_indices,
739 &pure_updated_frag_ids,
740 &original_fragment_ids,
741 fields_for_preserving_frag_bitmap,
742 &original_overlaid_frags,
743 &schema,
744 )?;
745 }
746
747 if let Some(next_row_id) = &mut next_row_id {
748 Self::assign_row_ids(next_row_id, new_fragments.as_mut_slice())?;
749 }
753 let mut target_ids: HashSet<u64> = HashSet::new();
755 target_ids.extend(new_fragments.iter().map(|f| f.id));
756 final_fragments.extend(new_fragments);
757 Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments);
758
759 if !compacted_sstables.is_empty() {
760 update_mem_wal_index_compacted_sstables(
761 &mut final_indices,
762 new_version,
763 compacted_sstables.clone(),
764 )?;
765 }
766 }
767 Operation::Overwrite { fragments, .. } => {
768 let mut new_fragments = fragments.clone();
774 for fragment in new_fragments.iter_mut() {
775 fragment.id = fragment_id;
776 fragment_id += 1;
777 }
778 if let Some(next_row_id) = &mut next_row_id {
779 Self::assign_row_ids(next_row_id, new_fragments.as_mut_slice())?;
780 for fragment in new_fragments.iter_mut() {
782 let version_meta = build_version_meta(fragment, new_version);
783 fragment.last_updated_at_version_meta = version_meta.clone();
784 fragment.created_at_version_meta = version_meta;
785 }
786 }
787 final_fragments.extend(new_fragments);
788 final_indices = Vec::new();
789 }
790 Operation::Rewrite {
791 groups,
792 rewritten_indices,
793 frag_reuse_index,
794 } => {
795 final_fragments.extend(maybe_existing_fragments?.clone());
796 let current_version = current_manifest.map(|m| m.version).unwrap_or_default();
797 Self::handle_rewrite_fragments(
798 &mut final_fragments,
799 groups,
800 &mut fragment_id,
801 current_version,
802 next_row_id.as_ref(),
803 )?;
804
805 if next_row_id.is_some() {
806 debug_assert!(rewritten_indices.is_empty());
808 for index in final_indices.iter_mut() {
809 let results_are_row_addrs = index.results_are_row_addrs();
810 if let Some(fragment_bitmap) = &mut index.fragment_bitmap {
811 *fragment_bitmap = if results_are_row_addrs {
812 Self::drop_rewritten_fragments(fragment_bitmap, groups)
821 } else {
822 Self::recalculate_fragment_bitmap(fragment_bitmap, groups)?
823 };
824 }
825 }
826 } else {
827 Self::handle_rewrite_indices(&mut final_indices, rewritten_indices, groups)?;
828 }
829
830 Self::prune_overlay_stale_fields_from_indices(&mut final_indices, groups);
835
836 if let Some(frag_reuse_index) = frag_reuse_index {
837 final_indices.retain(|idx| idx.name != frag_reuse_index.name);
838 final_indices.push(frag_reuse_index.clone());
839 }
840 }
841 Operation::CreateIndex {
842 new_indices,
843 removed_indices,
844 ..
845 } => {
846 final_fragments.extend(maybe_existing_fragments?.clone());
847 let removed_uuids = removed_indices
848 .iter()
849 .map(|old_index| old_index.uuid)
850 .collect::<HashSet<_>>();
851 let new_uuids = new_indices
852 .iter()
853 .map(|new_index| new_index.uuid)
854 .collect::<HashSet<_>>();
855 final_indices.retain(|existing_index| {
856 !removed_uuids.contains(&existing_index.uuid)
857 && !new_uuids.contains(&existing_index.uuid)
858 });
859 for new_index in new_indices {
860 new_index.validate_covering_fields()?;
861 }
862 final_indices.extend(new_indices.clone());
863 }
864 Operation::ReserveFragments { .. } | Operation::UpdateConfig { .. } => {
865 final_fragments.extend(maybe_existing_fragments?.clone());
866 }
867 Operation::Merge { fragments, .. } => {
868 let existing_fragments = maybe_existing_fragments?;
869 let mut merged_fragments = fragments.clone();
870 if next_row_id.is_some() {
871 let prev_by_id: HashMap<u64, &Fragment> =
872 existing_fragments.iter().map(|f| (f.id, f)).collect();
873 for fragment in merged_fragments.iter_mut() {
874 match prev_by_id.get(&fragment.id) {
875 Some(prev) => {
876 if merge_fragment_physically_rewritten(prev, fragment) {
877 crate::rowids::version::refresh_row_latest_update_meta_for_full_frag_rewrite_cols(
878 fragment,
879 new_version,
880 )?;
881 }
882 }
883 None => {
884 crate::rowids::version::refresh_row_latest_update_meta_for_full_frag_rewrite_cols(
888 fragment,
889 new_version,
890 )?;
891 fragment.created_at_version_meta =
892 fragment.last_updated_at_version_meta.clone();
893 }
894 }
895 }
896 }
897 final_fragments.extend(merged_fragments);
898
899 Self::prune_merge_rewritten_fields_from_indices(
903 &mut final_indices,
904 existing_fragments,
905 fragments,
906 );
907
908 Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments)
911 }
912 Operation::Project { .. } => {
913 final_fragments.extend(maybe_existing_fragments?.clone());
914
915 let remaining_field_ids = schema
918 .fields_pre_order()
919 .map(|f| f.id)
920 .collect::<HashSet<_>>();
921 for fragment in final_fragments.iter_mut() {
922 fragment.files.retain(|file| {
923 file.fields
924 .iter()
925 .any(|field_id| remaining_field_ids.contains(field_id))
926 });
927 }
928
929 Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments)
932 }
933 Operation::Restore { .. } => {
934 unreachable!()
935 }
936 Operation::DataReplacement { replacements } => {
937 log::warn!(
938 "Building manifest with DataReplacement operation. This operation is not stable yet, please use with caution."
939 );
940
941 let (old_fragment_ids, new_datafiles): (Vec<&u64>, Vec<&DataFile>) = replacements
942 .iter()
943 .map(|DataReplacementGroup(fragment_id, new_file)| (fragment_id, new_file))
944 .unzip();
945
946 if new_datafiles
950 .iter()
951 .map(|f| f.fields.clone())
952 .collect::<HashSet<_>>()
953 .len()
954 > 1
955 {
956 let field_info = new_datafiles
957 .iter()
958 .enumerate()
959 .map(|(id, f)| (id, f.fields.clone()))
960 .fold("".to_string(), |acc, (id, fields)| {
961 format!("{}File {}: {:?}\n", acc, id, fields)
962 });
963
964 return Err(Error::invalid_input(format!(
965 "All new data files must have the same fields, but found different fields:\n{field_info}"
966 )));
967 }
968
969 let existing_fragments = maybe_existing_fragments?;
970
971 let replaced_fields: Vec<u32> = new_datafiles
973 .first()
974 .map(|f| {
975 f.fields
976 .iter()
977 .filter(|&&id| id >= 0)
978 .map(|&id| id as u32)
979 .collect()
980 })
981 .unwrap_or_default();
982
983 for (frag_id, new_file) in old_fragment_ids.iter().zip(new_datafiles) {
986 let frag = existing_fragments
987 .iter()
988 .find(|f| f.id == **frag_id)
989 .ok_or_else(|| {
990 Error::invalid_input(
991 "Fragment being replaced not found in existing fragments",
992 )
993 })?;
994 let mut new_frag = frag.clone();
995
996 let mut columns_covered = HashSet::new();
999 let mut replaced_in_place = false;
1002 for file in &mut new_frag.files {
1003 if file.fields == new_file.fields
1004 && file.file_major_version == new_file.file_major_version
1005 && file.file_minor_version == new_file.file_minor_version
1006 {
1007 file.path = new_file.path.clone();
1009 file.file_size_bytes = new_file.file_size_bytes.clone();
1010 file.base_id = new_file.base_id;
1011 replaced_in_place = true;
1012 }
1013 columns_covered.extend(file.fields.iter());
1014 }
1015 new_file.file_version()?;
1018
1019 if columns_covered.is_disjoint(&new_file.fields.iter().collect()) {
1024 new_frag.files.push(new_file.clone());
1025 } else if !replaced_in_place
1026 && new_file.fields.iter().all(|field| {
1027 let mut covering = new_frag
1028 .files
1029 .iter()
1030 .filter(|file| file.fields.contains(field))
1031 .peekable();
1032 covering.peek().is_some()
1036 && covering.all(|file| {
1037 file.file_version()
1038 .is_ok_and(|version| version != ConcreteFileVersion::V1)
1039 })
1040 })
1041 {
1042 for file in &mut new_frag.files {
1054 if file.file_version()? == ConcreteFileVersion::V1 {
1056 continue;
1057 }
1058 file.fields = file
1059 .fields
1060 .iter()
1061 .map(|field| {
1062 if new_file.fields.contains(field) {
1063 TOMBSTONE_FIELD_ID
1064 } else {
1065 *field
1066 }
1067 })
1068 .collect::<Vec<_>>()
1069 .into();
1070 }
1071 let live_ids = schema
1077 .fields_pre_order()
1078 .map(|field| field.id)
1079 .collect::<HashSet<i32>>();
1080 new_frag
1081 .files
1082 .retain(|file| file.fields.iter().any(|f| live_ids.contains(f)));
1083 new_frag.files.push(new_file.clone());
1084 }
1085
1086 if &new_frag == frag {
1088 return Err(Error::invalid_input(
1089 "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",
1090 ));
1091 }
1092
1093 let (mut superseded, newer): (Vec<_>, Vec<_>) = new_frag
1100 .overlays
1101 .drain(..)
1102 .partition(|overlay| overlay.committed_version <= self.read_version);
1103 crate::format::overlay::tombstone_overlay_fields(
1104 &mut superseded,
1105 &replaced_fields,
1106 );
1107 superseded.extend(newer);
1108 new_frag.overlays = superseded;
1109
1110 final_fragments.push(new_frag);
1111 }
1112
1113 let fragments_changed = old_fragment_ids
1114 .iter()
1115 .cloned()
1116 .cloned()
1117 .collect::<HashSet<_>>();
1118
1119 let unmodified_fragments = existing_fragments
1121 .iter()
1122 .filter(|f| !fragments_changed.contains(&f.id))
1123 .cloned()
1124 .collect::<Vec<_>>();
1125
1126 final_fragments.extend(unmodified_fragments);
1127
1128 let modified_fragments: Vec<Fragment> = final_fragments
1130 .iter()
1131 .filter(|f| fragments_changed.contains(&f.id))
1132 .cloned()
1133 .collect();
1134
1135 if next_row_id.is_some() {
1139 let new_version = current_manifest.map_or(1, |m| m.version + 1);
1140 for fragment in final_fragments
1141 .iter_mut()
1142 .filter(|f| fragments_changed.contains(&f.id))
1143 {
1144 crate::rowids::version::refresh_row_latest_update_meta_for_full_frag_rewrite_cols(
1145 fragment,
1146 new_version,
1147 )?;
1148 }
1149 }
1150
1151 Self::prune_updated_fields_from_indices(
1152 &mut final_indices,
1153 &modified_fragments,
1154 &replaced_fields,
1155 );
1156 }
1157 Operation::DataOverlay { groups } => {
1158 let new_version = current_manifest.map_or(1, |m| m.version + 1);
1162
1163 let existing_fragments = maybe_existing_fragments?;
1164 let mut overlays_by_fragment: HashMap<u64, Vec<&DataOverlayFile>> = HashMap::new();
1168 for group in groups {
1169 overlays_by_fragment
1170 .entry(group.fragment_id)
1171 .or_default()
1172 .extend(group.overlays.iter());
1173 }
1174
1175 let existing_fragment_ids: HashSet<u64> =
1179 existing_fragments.iter().map(|f| f.id).collect();
1180 for fragment_id in overlays_by_fragment.keys() {
1181 if !existing_fragment_ids.contains(fragment_id) {
1182 return Err(Error::invalid_input(format!(
1183 "DataOverlay targets fragment {fragment_id}, which does not exist"
1184 )));
1185 }
1186 }
1187
1188 for fragment in existing_fragments {
1189 let mut fragment = fragment.clone();
1190 if let Some(new_overlays) = overlays_by_fragment.get(&fragment.id) {
1191 fragment
1194 .overlays
1195 .extend(new_overlays.iter().map(|&overlay| {
1196 let mut overlay = overlay.clone();
1197 overlay.committed_version = new_version;
1198 overlay
1199 }));
1200 }
1201 final_fragments.push(fragment);
1202 }
1203 }
1204 Operation::UpdateMemWalState {
1205 compacted_sstables, ..
1206 } => {
1207 final_fragments.extend(maybe_existing_fragments?.clone());
1209 update_mem_wal_index_compacted_sstables(
1210 &mut final_indices,
1211 new_version,
1212 compacted_sstables.clone(),
1213 )?;
1214 }
1215 Operation::UpdateBases { .. } => {
1216 final_fragments.extend(maybe_existing_fragments?.clone());
1219 }
1220 };
1221
1222 final_fragments.sort_by_key(|frag| frag.id);
1224
1225 Self::remove_tombstoned_data_files(&mut final_fragments);
1227
1228 for fragment in &final_fragments {
1232 if !fragment.overlays.is_empty() {
1233 crate::format::overlay::verify_overlays_newest_last(&fragment.overlays)?;
1234 }
1235 }
1236
1237 let user_requested_version = match (&config.storage_format, config.use_legacy_format) {
1238 (Some(storage_format), _) => Some(storage_format.lance_file_format()),
1239 (None, Some(true)) => Some(ConcreteFileVersion::V1),
1240 (None, Some(false)) => Some(ConcreteFileVersion::V2_0),
1241 (None, None) => None,
1242 };
1243
1244 if let Some(segments_before) = mem_wal_segments_before.as_ref() {
1248 Self::apply_mem_wal_index_coverage(
1249 &mut final_indices,
1250 segments_before,
1251 read_version_state,
1252 new_version,
1253 )?;
1254 }
1255
1256 let mut manifest = if let Some(current_manifest) = current_manifest {
1257 let mut prev_manifest =
1260 Manifest::new_from_previous(current_manifest, schema, Arc::new(final_fragments));
1261
1262 if let (Some(user_requested_version), Operation::Overwrite { .. }) =
1263 (user_requested_version, &self.operation)
1264 {
1265 prev_manifest.data_storage_format = DataStorageFormat::new(user_requested_version);
1269 }
1270
1271 prev_manifest
1272 } else {
1273 let data_storage_format =
1274 Self::data_storage_format_from_files(&final_fragments, user_requested_version)?;
1275 Manifest::new(
1276 schema,
1277 Arc::new(final_fragments),
1278 data_storage_format,
1279 reference_paths,
1280 )
1281 };
1282
1283 manifest.tag.clone_from(&self.tag);
1284
1285 if config.auto_set_feature_flags {
1286 let inherited = current_manifest
1290 .map(|m| m.uses_stable_row_ids())
1291 .unwrap_or(false);
1292 let use_stable_row_ids = config.use_stable_row_ids || inherited;
1293 apply_feature_flags(
1294 &mut manifest,
1295 use_stable_row_ids,
1296 config.disable_transaction_file,
1297 )?;
1298 }
1299 if final_indices
1315 .iter()
1316 .any(|index| !index.covering_fields.is_empty())
1317 {
1318 manifest.reader_feature_flags |= FLAG_COVERED_INDEX_METADATA;
1319 manifest.writer_feature_flags |= FLAG_COVERED_INDEX_METADATA;
1320 }
1321
1322 manifest.set_timestamp(config.timestamp_nanos);
1323
1324 manifest.update_max_fragment_id();
1325
1326 match &self.operation {
1327 Operation::Overwrite {
1328 config_upsert_values: Some(tm),
1329 ..
1330 } => {
1331 manifest.config_mut().extend(tm.clone());
1332 }
1333 Operation::UpdateConfig {
1334 config_updates,
1335 table_metadata_updates,
1336 schema_metadata_updates,
1337 field_metadata_updates,
1338 } => {
1339 if let Some(config_updates) = config_updates {
1340 let mut config = manifest.config.clone();
1341 apply_update_map(&mut config, config_updates);
1342 manifest.config = config;
1343 }
1344 if let Some(table_metadata_updates) = table_metadata_updates {
1345 let mut table_metadata = manifest.table_metadata.clone();
1346 apply_update_map(&mut table_metadata, table_metadata_updates);
1347 manifest.table_metadata = table_metadata;
1348 }
1349 if let Some(schema_metadata_updates) = schema_metadata_updates {
1350 let mut schema_metadata = manifest.schema.metadata.clone();
1351 apply_update_map(&mut schema_metadata, schema_metadata_updates);
1352 manifest.schema.metadata = schema_metadata;
1353 }
1354 let primary_key_before: Vec<i32> = manifest
1363 .schema
1364 .unenforced_primary_key()
1365 .iter()
1366 .map(|field| field.id)
1367 .collect();
1368 let writes_primary_key = field_metadata_updates.values().any(|update| {
1369 update.update_entries.iter().any(|entry| {
1370 entry.key == LANCE_UNENFORCED_PRIMARY_KEY
1371 || entry.key == LANCE_UNENFORCED_PRIMARY_KEY_POSITION
1372 })
1373 });
1374 let clustering_key_before: Vec<i32> = manifest
1375 .schema
1376 .unenforced_clustering_key()
1377 .iter()
1378 .map(|field| field.id)
1379 .collect();
1380 let writes_clustering_key = field_metadata_updates.values().any(|update| {
1381 update
1382 .update_entries
1383 .iter()
1384 .any(|entry| entry.key == LANCE_UNENFORCED_CLUSTERING_KEY_POSITION)
1385 });
1386 for (field_id, field_metadata_update) in field_metadata_updates {
1387 if let Some(field) = manifest.schema.field_by_id_mut(*field_id) {
1388 apply_update_map(&mut field.metadata, field_metadata_update);
1389 field.unenforced_primary_key_position = field
1391 .metadata
1392 .get(LANCE_UNENFORCED_PRIMARY_KEY_POSITION)
1393 .and_then(|s| s.parse::<u32>().ok())
1394 .or_else(|| {
1395 field
1396 .metadata
1397 .get(LANCE_UNENFORCED_PRIMARY_KEY)
1398 .filter(|s| {
1399 matches!(s.to_lowercase().as_str(), "true" | "1" | "yes")
1400 })
1401 .map(|_| 0)
1402 });
1403 field.unenforced_clustering_key_position = field
1406 .metadata
1407 .get(LANCE_UNENFORCED_CLUSTERING_KEY_POSITION)
1408 .and_then(|s| s.parse::<u32>().ok());
1409 } else {
1410 return Err(Error::invalid_input_source(
1411 format!("Field with id {} does not exist", field_id).into(),
1412 ));
1413 }
1414 }
1415 let primary_key_after: Vec<i32> = manifest
1416 .schema
1417 .unenforced_primary_key()
1418 .iter()
1419 .map(|field| field.id)
1420 .collect();
1421 if !primary_key_before.is_empty() {
1422 if writes_primary_key || primary_key_after != primary_key_before {
1425 return Err(Error::invalid_input(
1426 "the unenforced primary key is a reserved key and cannot be changed once set",
1427 ));
1428 }
1429 } else if writes_primary_key && primary_key_after.is_empty() {
1430 return Err(Error::invalid_input(
1434 "the unenforced primary key is a reserved key and cannot be set to an invalid value",
1435 ));
1436 }
1437 let clustering_key_after: Vec<i32> = manifest
1438 .schema
1439 .unenforced_clustering_key()
1440 .iter()
1441 .map(|field| field.id)
1442 .collect();
1443 if !clustering_key_before.is_empty() {
1444 if writes_clustering_key || clustering_key_after != clustering_key_before {
1447 return Err(Error::invalid_input(
1448 "the unenforced clustering key is a reserved key and cannot be changed once set",
1449 ));
1450 }
1451 } else if writes_clustering_key && clustering_key_after.is_empty() {
1452 return Err(Error::invalid_input(
1456 "the unenforced clustering key is a reserved key and cannot be set to an invalid value",
1457 ));
1458 }
1459 }
1460 _ => {}
1461 }
1462
1463 if let Operation::UpdateBases { new_bases } = &self.operation {
1465 for new_base in new_bases {
1467 if let Some(existing_base) = manifest
1469 .base_paths
1470 .values()
1471 .find(|bp| bp.name == new_base.name || bp.path == new_base.path)
1472 {
1473 return Err(Error::invalid_input(format!(
1474 "Conflict detected: Base path with name '{:?}' or path '{}' already exists. Existing: name='{:?}', path='{}'",
1475 new_base.name, new_base.path, existing_base.name, existing_base.path
1476 )));
1477 }
1478
1479 let mut base_to_add = new_base.clone();
1481 if base_to_add.id == 0 {
1482 let next_id = manifest
1483 .base_paths
1484 .keys()
1485 .max()
1486 .map(|&id| id + 1)
1487 .unwrap_or(1);
1488 base_to_add.id = next_id;
1489 }
1490
1491 manifest.base_paths.insert(base_to_add.id, base_to_add);
1492 }
1493 }
1494
1495 if let Operation::ReserveFragments { num_fragments } = self.operation {
1496 manifest.max_fragment_id = Some(manifest.max_fragment_id.unwrap_or(0) + num_fragments);
1497 }
1498
1499 manifest.transaction_file = Some(transaction_file_path.to_string());
1500
1501 if let Some(next_row_id) = next_row_id {
1502 manifest.next_row_id = next_row_id;
1503 }
1504
1505 Ok((manifest, final_indices))
1506 }
1507
1508 fn remove_tombstoned_data_files(fragments: &mut [Fragment]) {
1511 for fragment in fragments {
1512 fragment.files.retain(|file| {
1513 file.fields.iter().any(|&field_id| field_id != -2)
1515 });
1516 }
1517 }
1518 fn drop_rewritten_fragments(old: &RoaringBitmap, groups: &[RewriteGroup]) -> RoaringBitmap {
1521 let mut new_bitmap = old.clone();
1522 for group in groups {
1523 for old_fragment in &group.old_fragments {
1524 new_bitmap.remove(old_fragment.id as u32);
1525 }
1526 }
1527 new_bitmap
1528 }
1529}
1530
1531#[cfg(test)]
1532mod tests {
1533 use super::*;
1534 use crate::format::overlay::OverlayCoverage;
1535 use crate::format::pb;
1536 use crate::format::{RowDatasetVersionMeta, RowDatasetVersionSequence, RowIdMeta};
1537 use crate::rowids::{RowIdSequence, write_row_ids};
1538 use crate::transaction::test_support::{
1539 default_build_config, make_stable_row_id_manifest, overlay_with_field,
1540 sample_index_metadata, sample_manifest,
1541 };
1542 use crate::transaction::{DataOverlayGroup, UpdateMode, validate_operation};
1543 use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
1544 use lance_core::datatypes::Schema as LanceSchema;
1545 use lance_file::version::{ConcreteFileVersion, LanceFileVersion};
1546 use lance_io::utils::CachedFileSize;
1547 use std::collections::HashMap;
1548 use std::sync::Arc;
1549
1550 fn sample_manifest_with_fragments(ids: std::ops::Range<u64>) -> Manifest {
1551 let schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
1552 Manifest::new(
1553 LanceSchema::try_from(&schema).unwrap(),
1554 Arc::new(ids.map(Fragment::new).collect()),
1555 DataStorageFormat::new(ConcreteFileVersion::V2_0),
1556 HashMap::new(),
1557 )
1558 }
1559
1560 #[test]
1561 fn test_create_index_build_manifest_keeps_unremoved_same_name_indices() {
1562 let manifest = sample_manifest();
1563 let first_index = sample_index_metadata("vector_idx");
1564 let second_index = sample_index_metadata("vector_idx");
1565 let third_index = sample_index_metadata("vector_idx");
1566
1567 let transaction = Transaction::new(
1568 manifest.version,
1569 Operation::CreateIndex {
1570 new_indices: vec![third_index.clone()],
1571 removed_indices: vec![second_index.clone()],
1572 },
1573 None,
1574 );
1575
1576 let (_, final_indices) = transaction
1577 .build_manifest(
1578 Some(&manifest),
1579 vec![first_index.clone(), second_index.clone()],
1580 "txn",
1581 &default_build_config(),
1582 )
1583 .unwrap();
1584
1585 assert_eq!(final_indices.len(), 2);
1586 assert!(final_indices.iter().any(|idx| idx.uuid == first_index.uuid));
1587 assert!(final_indices.iter().any(|idx| idx.uuid == third_index.uuid));
1588 assert!(
1589 !final_indices
1590 .iter()
1591 .any(|idx| idx.uuid == second_index.uuid)
1592 );
1593 }
1594
1595 #[test]
1596 fn test_create_index_build_manifest_deduplicates_relisted_indices_by_uuid() {
1597 let manifest = sample_manifest();
1598 let first_index = sample_index_metadata("vector_idx");
1599 let second_index = sample_index_metadata("vector_idx");
1600 let third_index = sample_index_metadata("vector_idx");
1601
1602 let transaction = Transaction::new(
1603 manifest.version,
1604 Operation::CreateIndex {
1605 new_indices: vec![first_index.clone(), third_index.clone()],
1606 removed_indices: vec![second_index.clone()],
1607 },
1608 None,
1609 );
1610
1611 let (_, final_indices) = transaction
1612 .build_manifest(
1613 Some(&manifest),
1614 vec![first_index.clone(), second_index.clone()],
1615 "txn",
1616 &default_build_config(),
1617 )
1618 .unwrap();
1619
1620 assert_eq!(final_indices.len(), 2);
1621 assert_eq!(
1622 final_indices
1623 .iter()
1624 .filter(|idx| idx.uuid == first_index.uuid)
1625 .count(),
1626 1
1627 );
1628 assert!(final_indices.iter().any(|idx| idx.uuid == third_index.uuid));
1629 assert!(
1630 !final_indices
1631 .iter()
1632 .any(|idx| idx.uuid == second_index.uuid)
1633 );
1634 }
1635
1636 #[test]
1637 fn test_update_build_manifest_replaces_and_removes_fragments() {
1638 let manifest = sample_manifest_with_fragments(0..5);
1639
1640 let mut updated2 = Fragment::new(2);
1641 updated2.physical_rows = Some(42);
1642 let mut updated4 = Fragment::new(4);
1643 updated4.physical_rows = Some(43);
1644
1645 let transaction = Transaction::new(
1646 manifest.version,
1647 Operation::Update {
1648 removed_fragment_ids: vec![1],
1649 updated_fragments: vec![updated2, updated4, Fragment::new(99)],
1652 new_fragments: vec![],
1653 fields_modified: vec![],
1654 compacted_sstables: vec![],
1655 fields_for_preserving_frag_bitmap: vec![],
1656 update_mode: None,
1657 inserted_rows_filter: None,
1658 updated_fragment_offsets: None,
1659 },
1660 None,
1661 );
1662
1663 let (new_manifest, _) = transaction
1664 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
1665 .unwrap();
1666
1667 let ids: Vec<u64> = new_manifest.fragments.iter().map(|f| f.id).collect();
1668 assert_eq!(ids, vec![0, 2, 3, 4]);
1669 let rows: Vec<Option<usize>> = new_manifest
1670 .fragments
1671 .iter()
1672 .map(|f| f.physical_rows)
1673 .collect();
1674 assert_eq!(rows, vec![None, Some(42), None, Some(43)]);
1675 }
1676
1677 #[test]
1678 fn test_delete_build_manifest_replaces_and_removes_fragments() {
1679 let manifest = sample_manifest_with_fragments(0..5);
1680
1681 let mut updated2 = Fragment::new(2);
1682 updated2.physical_rows = Some(42);
1683
1684 let transaction = Transaction::new(
1685 manifest.version,
1686 Operation::Delete {
1687 updated_fragments: vec![updated2],
1688 deleted_fragment_ids: vec![1, 3],
1689 predicate: "id > 0".to_string(),
1690 },
1691 None,
1692 );
1693
1694 let (new_manifest, _) = transaction
1695 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
1696 .unwrap();
1697
1698 let ids: Vec<u64> = new_manifest.fragments.iter().map(|f| f.id).collect();
1699 assert_eq!(ids, vec![0, 2, 4]);
1700 let rows: Vec<Option<usize>> = new_manifest
1701 .fragments
1702 .iter()
1703 .map(|f| f.physical_rows)
1704 .collect();
1705 assert_eq!(rows, vec![None, Some(42), None]);
1706 }
1707
1708 #[test]
1709 fn test_remove_tombstoned_data_files() {
1710 let mut fragment = Fragment::new(1);
1712
1713 fragment.files.push(DataFile {
1715 path: "normal.lance".to_string(),
1716 fields: Arc::from([1, 2, 3]),
1717 column_indices: Arc::from([]),
1718 file_major_version: 2,
1719 file_minor_version: 0,
1720 file_size_bytes: CachedFileSize::new(1000),
1721 base_id: None,
1722 });
1723
1724 fragment.files.push(DataFile {
1726 path: "all_tombstoned.lance".to_string(),
1727 fields: Arc::from([-2, -2, -2]),
1728 column_indices: Arc::from([]),
1729 file_major_version: 2,
1730 file_minor_version: 0,
1731 file_size_bytes: CachedFileSize::new(500),
1732 base_id: None,
1733 });
1734
1735 fragment.files.push(DataFile {
1737 path: "mixed.lance".to_string(),
1738 fields: Arc::from([4, -2, 5]),
1739 column_indices: Arc::from([]),
1740 file_major_version: 2,
1741 file_minor_version: 0,
1742 file_size_bytes: CachedFileSize::new(750),
1743 base_id: None,
1744 });
1745
1746 fragment.files.push(DataFile {
1748 path: "another_tombstoned.lance".to_string(),
1749 fields: Arc::from([-2_i32]),
1750 column_indices: Arc::from([]),
1751 file_major_version: 2,
1752 file_minor_version: 0,
1753 file_size_bytes: CachedFileSize::new(250),
1754 base_id: None,
1755 });
1756
1757 let mut fragments = vec![fragment];
1758
1759 Transaction::remove_tombstoned_data_files(&mut fragments);
1761
1762 assert_eq!(fragments[0].files.len(), 2);
1764 assert_eq!(fragments[0].files[0].path, "normal.lance");
1765 assert_eq!(fragments[0].files[1].path, "mixed.lance");
1766 }
1767
1768 #[test]
1772 fn test_partial_rewrite_skips_fragment_with_no_version_meta() {
1773 let row_ids = RowIdSequence::from([10u64, 11, 12, 13, 14].as_slice());
1774 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
1775
1776 let data_file = DataFile::new(
1777 "data.lance",
1778 vec![0],
1779 vec![0],
1780 LanceFileVersion::Stable.resolve(),
1781 None,
1782 None,
1783 );
1784
1785 let fragment = Fragment {
1786 id: 1,
1787 files: vec![data_file],
1788 overlays: vec![],
1789 deletion_file: None,
1790 row_id_meta,
1791 physical_rows: Some(5),
1792 last_updated_at_version_meta: None,
1793 created_at_version_meta: None,
1794 };
1795
1796 let manifest = make_stable_row_id_manifest(vec![fragment.clone()]);
1797
1798 let off_map = HashMap::from([(1u64, RoaringBitmap::from_iter([1u32, 3]))]);
1800 let tx = Transaction::new(
1801 manifest.version,
1802 Operation::Update {
1803 removed_fragment_ids: vec![],
1804 updated_fragments: vec![fragment],
1805 new_fragments: vec![],
1806 fields_modified: vec![],
1807 compacted_sstables: vec![],
1808 fields_for_preserving_frag_bitmap: vec![],
1809 update_mode: Some(UpdateMode::RewriteColumns),
1810 inserted_rows_filter: None,
1811 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
1812 },
1813 None,
1814 );
1815
1816 let (out, _) = tx
1817 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
1818 .unwrap();
1819
1820 assert!(
1821 out.fragments[0].last_updated_at_version_meta.is_none(),
1822 "fragment with no prior version metadata must not have fabricated prev_version stamped on unmatched rows"
1823 );
1824 }
1825
1826 #[test]
1827 fn test_bitmap_cardinality_exceeds_physical_rows() {
1828 let row_ids = RowIdSequence::from([10u64, 11, 12, 13, 14].as_slice());
1829 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
1830
1831 let data_file = DataFile::new(
1832 "data.lance",
1833 vec![0],
1834 vec![0],
1835 LanceFileVersion::Stable.resolve(),
1836 None,
1837 None,
1838 );
1839
1840 let version_seq = RowDatasetVersionSequence::from_uniform_row_count(5, 1);
1841 let version_meta = RowDatasetVersionMeta::from_sequence(&version_seq).unwrap();
1842
1843 let fragment = Fragment {
1844 id: 1,
1845 files: vec![data_file],
1846 overlays: vec![],
1847 deletion_file: None,
1848 row_id_meta,
1849 physical_rows: Some(5),
1850 last_updated_at_version_meta: Some(version_meta.clone()),
1851 created_at_version_meta: Some(version_meta),
1852 };
1853
1854 let manifest = make_stable_row_id_manifest(vec![fragment.clone()]);
1855
1856 let off_map = HashMap::from([(1u64, RoaringBitmap::from_iter(0u32..10))]);
1858 let tx = Transaction::new(
1859 manifest.version,
1860 Operation::Update {
1861 removed_fragment_ids: vec![],
1862 updated_fragments: vec![fragment],
1863 new_fragments: vec![],
1864 fields_modified: vec![],
1865 compacted_sstables: vec![],
1866 fields_for_preserving_frag_bitmap: vec![],
1867 update_mode: Some(UpdateMode::RewriteColumns),
1868 inserted_rows_filter: None,
1869 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
1870 },
1871 None,
1872 );
1873
1874 let result = tx.build_manifest(Some(&manifest), vec![], "txn", &default_build_config());
1875 assert!(result.is_err());
1876 let msg = result.unwrap_err().to_string();
1877 assert!(
1878 msg.contains("cardinality"),
1879 "expected cardinality error, got: {msg}"
1880 );
1881 }
1882
1883 #[test]
1884 fn test_bitmap_max_offset_exceeds_physical_rows() {
1885 let row_ids = RowIdSequence::from([10u64, 11, 12, 13, 14].as_slice());
1886 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
1887
1888 let data_file = DataFile::new(
1889 "data.lance",
1890 vec![0],
1891 vec![0],
1892 LanceFileVersion::Stable.resolve(),
1893 None,
1894 None,
1895 );
1896
1897 let version_seq = RowDatasetVersionSequence::from_uniform_row_count(5, 1);
1898 let version_meta = RowDatasetVersionMeta::from_sequence(&version_seq).unwrap();
1899
1900 let fragment = Fragment {
1901 id: 1,
1902 files: vec![data_file],
1903 overlays: vec![],
1904 deletion_file: None,
1905 row_id_meta,
1906 physical_rows: Some(5),
1907 last_updated_at_version_meta: Some(version_meta.clone()),
1908 created_at_version_meta: Some(version_meta),
1909 };
1910
1911 let manifest = make_stable_row_id_manifest(vec![fragment.clone()]);
1912
1913 let off_map = HashMap::from([(1u64, RoaringBitmap::from_iter([0u32, 100]))]);
1915 let tx = Transaction::new(
1916 manifest.version,
1917 Operation::Update {
1918 removed_fragment_ids: vec![],
1919 updated_fragments: vec![fragment],
1920 new_fragments: vec![],
1921 fields_modified: vec![],
1922 compacted_sstables: vec![],
1923 fields_for_preserving_frag_bitmap: vec![],
1924 update_mode: Some(UpdateMode::RewriteColumns),
1925 inserted_rows_filter: None,
1926 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
1927 },
1928 None,
1929 );
1930
1931 let result = tx.build_manifest(Some(&manifest), vec![], "txn", &default_build_config());
1932 assert!(result.is_err());
1933 let msg = result.unwrap_err().to_string();
1934 assert!(
1935 msg.contains("max offset"),
1936 "expected max offset error, got: {msg}"
1937 );
1938 }
1939
1940 #[test]
1941 fn test_bitmap_at_exact_physical_rows_boundary_succeeds() {
1942 let row_ids = RowIdSequence::from([10u64, 11, 12, 13, 14].as_slice());
1943 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
1944
1945 let data_file = DataFile::new(
1946 "data.lance",
1947 vec![0],
1948 vec![0],
1949 LanceFileVersion::Stable.resolve(),
1950 None,
1951 None,
1952 );
1953
1954 let version_seq = RowDatasetVersionSequence::from_uniform_row_count(5, 1);
1955 let version_meta = RowDatasetVersionMeta::from_sequence(&version_seq).unwrap();
1956
1957 let fragment = Fragment {
1958 id: 1,
1959 files: vec![data_file],
1960 overlays: vec![],
1961 deletion_file: None,
1962 row_id_meta,
1963 physical_rows: Some(5),
1964 last_updated_at_version_meta: Some(version_meta.clone()),
1965 created_at_version_meta: Some(version_meta),
1966 };
1967
1968 let manifest = make_stable_row_id_manifest(vec![fragment.clone()]);
1969
1970 let off_map = HashMap::from([(1u64, RoaringBitmap::from_iter(0u32..5))]);
1972 let tx = Transaction::new(
1973 manifest.version,
1974 Operation::Update {
1975 removed_fragment_ids: vec![],
1976 updated_fragments: vec![fragment],
1977 new_fragments: vec![],
1978 fields_modified: vec![],
1979 compacted_sstables: vec![],
1980 fields_for_preserving_frag_bitmap: vec![],
1981 update_mode: Some(UpdateMode::RewriteColumns),
1982 inserted_rows_filter: None,
1983 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
1984 },
1985 None,
1986 );
1987
1988 tx.build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
1989 .expect("bitmap at exact physical_rows boundary should succeed");
1990 }
1991
1992 #[test]
1993 fn test_updated_fragment_offsets_key_not_in_updated_fragments_is_rejected() {
1994 let make_fragment = |id: u64| {
1998 let row_ids = RowIdSequence::from([id * 10].as_slice());
1999 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
2000 Fragment {
2001 id,
2002 files: vec![DataFile::new(
2003 format!("{id}.lance"),
2004 vec![0],
2005 vec![0],
2006 LanceFileVersion::Stable.resolve(),
2007 None,
2008 None,
2009 )],
2010 overlays: vec![],
2011 deletion_file: None,
2012 row_id_meta,
2013 physical_rows: Some(5),
2014 last_updated_at_version_meta: None,
2015 created_at_version_meta: None,
2016 }
2017 };
2018
2019 let frag_a = make_fragment(1);
2020 let frag_b = make_fragment(2);
2021 let manifest = make_stable_row_id_manifest(vec![frag_a.clone(), frag_b.clone()]);
2022
2023 let off_map = HashMap::from([(frag_b.id, RoaringBitmap::from_iter([0u32, 1, 2]))]);
2025 let operation = Operation::Update {
2026 removed_fragment_ids: vec![],
2027 updated_fragments: vec![frag_a],
2028 new_fragments: vec![],
2029 fields_modified: vec![],
2030 compacted_sstables: vec![],
2031 fields_for_preserving_frag_bitmap: vec![],
2032 update_mode: Some(UpdateMode::RewriteColumns),
2033 inserted_rows_filter: None,
2034 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map)),
2035 };
2036
2037 let err = validate_operation(Some(&manifest), &operation).unwrap_err();
2038 assert!(
2039 err.to_string().contains("not in updated_fragments"),
2040 "expected key-presence error, got: {err}"
2041 );
2042 }
2043
2044 #[test]
2045 fn test_proto_round_trip_field_10() {
2046 let off_map = HashMap::from([
2047 (1u64, RoaringBitmap::from_iter([1u32, 3, 5])),
2048 (2u64, RoaringBitmap::from_iter([0u32, 2, 4, 6])),
2049 ]);
2050 let tx = Transaction::new(
2051 1,
2052 Operation::Update {
2053 removed_fragment_ids: vec![],
2054 updated_fragments: vec![],
2055 new_fragments: vec![],
2056 fields_modified: vec![],
2057 compacted_sstables: vec![],
2058 fields_for_preserving_frag_bitmap: vec![],
2059 update_mode: Some(UpdateMode::RewriteColumns),
2060 inserted_rows_filter: None,
2061 updated_fragment_offsets: Some(UpdatedFragmentOffsets(off_map.clone())),
2062 },
2063 None,
2064 );
2065
2066 let pb_tx: pb::Transaction = pb::Transaction::from(&tx);
2067
2068 if let Some(pb::transaction::Operation::Update(ref update)) = pb_tx.operation {
2070 assert!(
2071 update.updated_fragment_offsets.is_empty(),
2072 "field 9 should be empty"
2073 );
2074 assert_eq!(update.updated_fragment_offset_bitmaps.len(), 2);
2075 } else {
2076 panic!("expected Update operation");
2077 }
2078
2079 let tx2 = Transaction::try_from(pb_tx).unwrap();
2080 if let Operation::Update {
2081 updated_fragment_offsets: Some(UpdatedFragmentOffsets(m)),
2082 ..
2083 } = &tx2.operation
2084 {
2085 assert_eq!(m.len(), 2);
2086 assert_eq!(*m.get(&1).unwrap(), off_map[&1]);
2087 assert_eq!(*m.get(&2).unwrap(), off_map[&2]);
2088 } else {
2089 panic!("expected Update with offsets");
2090 }
2091 }
2092
2093 #[test]
2094 fn test_proto_legacy_field_9_read() {
2095 let pb_tx = pb::Transaction {
2097 read_version: 1,
2098 uuid: "test".to_string(),
2099 tag: String::new(),
2100 transaction_properties: HashMap::new(),
2101 operation: Some(pb::transaction::Operation::Update(
2102 pb::transaction::Update {
2103 removed_fragment_ids: vec![],
2104 updated_fragments: vec![],
2105 new_fragments: vec![],
2106 fields_modified: vec![],
2107 compacted_sstables: vec![],
2108 fields_for_preserving_frag_bitmap: vec![],
2109 update_mode: 1,
2110 inserted_rows: None,
2111 updated_fragment_offsets: HashMap::from([(
2112 1u64,
2113 pb::transaction::UInt32List {
2114 values: vec![1, 3, 5],
2115 },
2116 )]),
2117 updated_fragment_offset_bitmaps: HashMap::new(),
2118 },
2119 )),
2120 };
2121
2122 let tx = Transaction::try_from(pb_tx).unwrap();
2123 if let Operation::Update {
2124 updated_fragment_offsets: Some(UpdatedFragmentOffsets(m)),
2125 ..
2126 } = &tx.operation
2127 {
2128 assert_eq!(m.len(), 1);
2129 let bitmap = m.get(&1).unwrap();
2130 let offsets: Vec<u32> = bitmap.iter().collect();
2131 assert_eq!(offsets, vec![1, 3, 5]);
2132 } else {
2133 panic!("expected Update with offsets from legacy field 9");
2134 }
2135 }
2136
2137 #[test]
2138 fn test_proto_field_10_takes_precedence_over_field_9() {
2139 let mut bitmap_bytes = Vec::new();
2141 RoaringBitmap::from_iter([10u32, 20, 30])
2142 .serialize_into(&mut bitmap_bytes)
2143 .unwrap();
2144
2145 let pb_tx = pb::Transaction {
2146 read_version: 1,
2147 uuid: "test".to_string(),
2148 tag: String::new(),
2149 transaction_properties: HashMap::new(),
2150 operation: Some(pb::transaction::Operation::Update(
2151 pb::transaction::Update {
2152 removed_fragment_ids: vec![],
2153 updated_fragments: vec![],
2154 new_fragments: vec![],
2155 fields_modified: vec![],
2156 compacted_sstables: vec![],
2157 fields_for_preserving_frag_bitmap: vec![],
2158 update_mode: 1,
2159 inserted_rows: None,
2160 updated_fragment_offsets: HashMap::from([(
2162 1u64,
2163 pb::transaction::UInt32List {
2164 values: vec![99, 100],
2165 },
2166 )]),
2167 updated_fragment_offset_bitmaps: HashMap::from([(1u64, bitmap_bytes)]),
2168 },
2169 )),
2170 };
2171
2172 let tx = Transaction::try_from(pb_tx).unwrap();
2173 if let Operation::Update {
2174 updated_fragment_offsets: Some(UpdatedFragmentOffsets(m)),
2175 ..
2176 } = &tx.operation
2177 {
2178 let offsets: Vec<u32> = m.get(&1).unwrap().iter().collect();
2179 assert_eq!(offsets, vec![10, 20, 30], "field 10 should take precedence");
2180 } else {
2181 panic!("expected Update with offsets from field 10");
2182 }
2183 }
2184
2185 #[test]
2186 fn merge_build_manifest_refreshes_last_updated_when_data_files_change_stable_row_ids() {
2187 use crate::feature_flags::FLAG_STABLE_ROW_IDS;
2188 use lance_file::version::LanceFileVersion;
2189
2190 let mk_file = |path: &str| {
2191 DataFile::new(
2192 path,
2193 vec![0],
2194 vec![0],
2195 LanceFileVersion::Stable.resolve(),
2196 None,
2197 None,
2198 )
2199 };
2200
2201 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
2202 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
2203
2204 let row_ids = RowIdSequence::from([100u64, 101, 102, 103, 104].as_slice());
2205 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
2206
2207 let prev_fragment = Fragment {
2208 id: 0,
2209 files: vec![mk_file("before.lance")],
2210 overlays: vec![],
2211 deletion_file: None,
2212 row_id_meta,
2213 physical_rows: Some(5),
2214 last_updated_at_version_meta: None,
2215 created_at_version_meta: None,
2216 };
2217
2218 let mut manifest = Manifest::new(
2219 lance_schema.clone(),
2220 Arc::new(vec![prev_fragment.clone()]),
2221 DataStorageFormat::new(ConcreteFileVersion::V2_0),
2222 HashMap::new(),
2223 );
2224 manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
2225 manifest.next_row_id = 100;
2226
2227 let merged_fragment = Fragment {
2228 files: vec![mk_file("after.lance")],
2229 ..prev_fragment
2230 };
2231
2232 let tx = Transaction::new(
2233 manifest.version,
2234 Operation::Merge {
2235 fragments: vec![merged_fragment],
2236 schema: lance_schema,
2237 preserves_nullability: true,
2238 },
2239 None,
2240 );
2241
2242 let (out, _) = tx
2243 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2244 .unwrap();
2245
2246 assert_eq!(out.version, 2);
2247 let frag = &out.fragments[0];
2248 let seq = frag
2249 .last_updated_at_version_meta
2250 .as_ref()
2251 .unwrap()
2252 .load_sequence()
2253 .unwrap();
2254 assert_eq!(seq.version_at(0).unwrap(), 2);
2255 assert_eq!(seq.version_at(4).unwrap(), 2);
2256 }
2257
2258 #[test]
2259 fn merge_build_manifest_skips_refresh_when_carry_forward_stable_row_ids() {
2260 use crate::feature_flags::FLAG_STABLE_ROW_IDS;
2261 use crate::rowids::version::{RowDatasetVersionMeta, RowDatasetVersionSequence};
2262 use lance_file::version::LanceFileVersion;
2263
2264 let data_file = DataFile::new(
2265 "same.lance",
2266 vec![0],
2267 vec![0],
2268 LanceFileVersion::Stable.resolve(),
2269 None,
2270 None,
2271 );
2272
2273 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
2274 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
2275
2276 let row_ids = RowIdSequence::from([200u64, 201, 202, 203, 204].as_slice());
2277 let row_id_meta = Some(RowIdMeta::Inline(write_row_ids(&row_ids).into()));
2278
2279 let uniform_v1 = RowDatasetVersionSequence::from_uniform_row_count(5, 1);
2280 let meta_v1 = RowDatasetVersionMeta::from_sequence(&uniform_v1).unwrap();
2281
2282 let prev_fragment = Fragment {
2283 id: 0,
2284 files: vec![data_file.clone()],
2285 overlays: vec![],
2286 deletion_file: None,
2287 row_id_meta: row_id_meta.clone(),
2288 physical_rows: Some(5),
2289 last_updated_at_version_meta: Some(meta_v1.clone()),
2290 created_at_version_meta: None,
2291 };
2292
2293 let mut manifest = Manifest::new(
2294 lance_schema.clone(),
2295 Arc::new(vec![prev_fragment]),
2296 DataStorageFormat::new(ConcreteFileVersion::V2_0),
2297 HashMap::new(),
2298 );
2299 manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
2300 manifest.next_row_id = 100;
2301
2302 let merged_fragment = Fragment {
2303 id: 0,
2304 files: vec![data_file],
2305 overlays: vec![],
2306 deletion_file: None,
2307 row_id_meta,
2308 physical_rows: Some(5),
2309 last_updated_at_version_meta: Some(meta_v1),
2310 created_at_version_meta: None,
2311 };
2312
2313 let tx = Transaction::new(
2314 manifest.version,
2315 Operation::Merge {
2316 fragments: vec![merged_fragment],
2317 schema: lance_schema,
2318 preserves_nullability: true,
2319 },
2320 None,
2321 );
2322
2323 let (out, _) = tx
2324 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2325 .unwrap();
2326
2327 let seq = out.fragments[0]
2328 .last_updated_at_version_meta
2329 .as_ref()
2330 .unwrap()
2331 .load_sequence()
2332 .unwrap();
2333 assert_eq!(seq.version_at(0).unwrap(), 1);
2334 assert_eq!(seq.version_at(4).unwrap(), 1);
2335 }
2336
2337 #[test]
2338 fn merge_build_manifest_no_last_updated_refresh_without_stable_row_ids() {
2339 use crate::feature_flags::FLAG_STABLE_ROW_IDS;
2340 use lance_file::version::LanceFileVersion;
2341
2342 let mk_file = |path: &str| {
2343 DataFile::new(
2344 path,
2345 vec![0],
2346 vec![0],
2347 LanceFileVersion::Stable.resolve(),
2348 None,
2349 None,
2350 )
2351 };
2352
2353 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
2354 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
2355
2356 let prev_fragment = Fragment {
2357 id: 0,
2358 files: vec![mk_file("before.lance")],
2359 overlays: vec![],
2360 deletion_file: None,
2361 row_id_meta: None,
2362 physical_rows: Some(5),
2363 last_updated_at_version_meta: None,
2364 created_at_version_meta: None,
2365 };
2366
2367 let manifest = Manifest::new(
2368 lance_schema.clone(),
2369 Arc::new(vec![prev_fragment.clone()]),
2370 DataStorageFormat::new(ConcreteFileVersion::V2_0),
2371 HashMap::new(),
2372 );
2373 assert_eq!(
2374 manifest.reader_feature_flags & FLAG_STABLE_ROW_IDS,
2375 0,
2376 "manifest must not use stable row IDs for this guard test"
2377 );
2378
2379 let merged_fragment = Fragment {
2380 files: vec![mk_file("after.lance")],
2381 ..prev_fragment
2382 };
2383
2384 let tx = Transaction::new(
2385 manifest.version,
2386 Operation::Merge {
2387 fragments: vec![merged_fragment],
2388 schema: lance_schema,
2389 preserves_nullability: true,
2390 },
2391 None,
2392 );
2393
2394 let (out, _) = tx
2395 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2396 .unwrap();
2397
2398 assert!(
2399 out.fragments[0].last_updated_at_version_meta.is_none(),
2400 "without stable row IDs, Merge must not populate per-row last_updated metadata"
2401 );
2402 }
2403
2404 #[test]
2405 fn merge_build_manifest_sets_both_version_meta_for_new_fragment_id_stable_row_ids() {
2406 use crate::feature_flags::FLAG_STABLE_ROW_IDS;
2407 use lance_file::version::LanceFileVersion;
2408
2409 let mk_file = |path: &str| {
2410 DataFile::new(
2411 path,
2412 vec![0],
2413 vec![0],
2414 LanceFileVersion::Stable.resolve(),
2415 None,
2416 None,
2417 )
2418 };
2419
2420 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
2421 let lance_schema = LanceSchema::try_from(&arrow_schema).unwrap();
2422
2423 let row_ids_0 = RowIdSequence::from([10u64, 11, 12].as_slice());
2425 let existing_fragment = Fragment {
2426 id: 0,
2427 files: vec![mk_file("existing.lance")],
2428 overlays: vec![],
2429 deletion_file: None,
2430 row_id_meta: Some(RowIdMeta::Inline(write_row_ids(&row_ids_0).into())),
2431 physical_rows: Some(3),
2432 last_updated_at_version_meta: None,
2433 created_at_version_meta: None,
2434 };
2435
2436 let mut manifest = Manifest::new(
2437 lance_schema.clone(),
2438 Arc::new(vec![existing_fragment.clone()]),
2439 DataStorageFormat::new(ConcreteFileVersion::V2_0),
2440 HashMap::new(),
2441 );
2442 manifest.reader_feature_flags |= FLAG_STABLE_ROW_IDS;
2443 manifest.next_row_id = 100;
2444 manifest.version = 1;
2445
2446 let row_ids_1 = RowIdSequence::from([20u64, 21, 22, 23].as_slice());
2448 let new_fragment = Fragment {
2449 id: 1,
2450 files: vec![mk_file("new.lance")],
2451 overlays: vec![],
2452 deletion_file: None,
2453 row_id_meta: Some(RowIdMeta::Inline(write_row_ids(&row_ids_1).into())),
2454 physical_rows: Some(4),
2455 last_updated_at_version_meta: None,
2456 created_at_version_meta: None,
2457 };
2458
2459 let tx = Transaction::new(
2460 manifest.version,
2461 Operation::Merge {
2462 fragments: vec![existing_fragment, new_fragment],
2463 schema: lance_schema,
2464 preserves_nullability: true,
2465 },
2466 None,
2467 );
2468
2469 let (out, _) = tx
2470 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2471 .unwrap();
2472
2473 assert_eq!(out.version, 2);
2474
2475 let new_frag = out.fragments.iter().find(|f| f.id == 1).unwrap();
2476
2477 let last_updated_seq = new_frag
2479 .last_updated_at_version_meta
2480 .as_ref()
2481 .expect("new fragment must have last_updated_at_version_meta")
2482 .load_sequence()
2483 .unwrap();
2484 assert_eq!(last_updated_seq.version_at(0).unwrap(), 2);
2485 assert_eq!(last_updated_seq.version_at(3).unwrap(), 2);
2486
2487 let created_seq = new_frag
2489 .created_at_version_meta
2490 .as_ref()
2491 .expect("new fragment must have created_at_version_meta")
2492 .load_sequence()
2493 .unwrap();
2494 assert_eq!(created_seq.version_at(0).unwrap(), 2);
2495 assert_eq!(created_seq.version_at(3).unwrap(), 2);
2496 }
2497
2498 #[test]
2499 fn test_data_overlay_build_manifest_multi_fragment() {
2500 let mut frag0 = Fragment::new(0);
2506 frag0.overlays = vec![overlay_with_field(5, 3)]; let frag1 = Fragment::new(1);
2508 let mut frag2 = Fragment::new(2);
2509 frag2.overlays = vec![overlay_with_field(9, 3)]; let schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
2511 let mut manifest = Manifest::new(
2512 LanceSchema::try_from(&schema).unwrap(),
2513 Arc::new(vec![frag0, frag1, frag2]),
2514 crate::format::DataStorageFormat::new(ConcreteFileVersion::V2_0),
2515 HashMap::new(),
2516 );
2517 manifest.version = 3;
2521
2522 let txn = Transaction::new(
2523 manifest.version,
2524 Operation::DataOverlay {
2525 groups: vec![
2526 DataOverlayGroup {
2527 fragment_id: 0,
2528 overlays: vec![overlay_with_field(1, 0)],
2529 },
2530 DataOverlayGroup {
2531 fragment_id: 1,
2532 overlays: vec![overlay_with_field(2, 0)],
2533 },
2534 ],
2535 },
2536 None,
2537 );
2538
2539 let (result, _) = txn
2540 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2541 .unwrap();
2542
2543 let frag = |id: u64| {
2544 result
2545 .fragments
2546 .iter()
2547 .find(|f| f.id == id)
2548 .unwrap_or_else(|| panic!("fragment {id} missing from result"))
2549 };
2550 assert_eq!(frag(0).overlays.len(), 2);
2553 assert_eq!(frag(0).overlays[0].committed_version, 3);
2554 assert_eq!(frag(0).overlays[1].committed_version, result.version);
2555 assert_eq!(frag(1).overlays.len(), 1);
2557 assert_eq!(frag(1).overlays[0].committed_version, result.version);
2558 assert_eq!(frag(2).overlays.len(), 1);
2560 assert_eq!(frag(2).overlays[0].committed_version, 3);
2561 assert!(result.version > manifest.version);
2562 }
2563
2564 #[test]
2565 fn test_data_replacement_tombstones_overlaid_fields() {
2566 let mut fragment = Fragment::new(0);
2572 fragment.files = vec![
2573 DataFile::new_legacy_from_fields("f3.lance", vec![3], None),
2574 DataFile::new_legacy_from_fields("f5.lance", vec![5], None),
2575 ];
2576 fragment.overlays = vec![
2577 DataOverlayFile {
2578 data_file: DataFile::new_legacy_from_fields("o35.lance", vec![3, 5], None),
2579 coverage: OverlayCoverage::sparse(vec![
2580 roaring::RoaringBitmap::from_iter([0u32]),
2581 roaring::RoaringBitmap::from_iter([0u32]),
2582 ]),
2583 committed_version: 1,
2584 },
2585 DataOverlayFile {
2586 data_file: DataFile::new_legacy_from_fields("o5.lance", vec![5], None),
2587 coverage: OverlayCoverage::dense(roaring::RoaringBitmap::from_iter([0u32])),
2588 committed_version: 1,
2589 },
2590 ];
2591
2592 let schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
2593 let manifest = Manifest::new(
2594 LanceSchema::try_from(&schema).unwrap(),
2595 Arc::new(vec![fragment]),
2596 crate::format::DataStorageFormat::new(ConcreteFileVersion::V2_0),
2597 HashMap::new(),
2598 );
2599
2600 let txn = Transaction::new(
2601 manifest.version,
2602 Operation::DataReplacement {
2603 replacements: vec![DataReplacementGroup(
2604 0,
2605 DataFile::new_legacy_from_fields("f5-new.lance", vec![5], None),
2606 )],
2607 },
2608 None,
2609 );
2610
2611 let (result, _) = txn
2612 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2613 .unwrap();
2614
2615 let frag = &result.fragments[0];
2616 assert!(frag.files.iter().any(|f| f.path == "f5-new.lance"));
2618 assert_eq!(frag.overlays.len(), 1);
2621 assert_eq!(frag.overlays[0].data_file.fields.as_ref(), &[3, -2]);
2622 }
2623
2624 fn replace_fields(
2628 fragment: Fragment,
2629 fields: Vec<i32>,
2630 manifest_version: u64,
2631 read_version: u64,
2632 ) -> Result<Fragment> {
2633 let schema = ArrowSchema::new(vec![
2634 ArrowField::new("x", DataType::Int32, true),
2635 ArrowField::new("a", DataType::Int32, true),
2636 ArrowField::new("v", DataType::Int32, true),
2637 ArrowField::new("y", DataType::Int32, true),
2638 ]);
2639 let mut lance_schema = LanceSchema::try_from(&schema).unwrap();
2640 lance_schema.fields[0].id = 3;
2641 lance_schema.fields[1].id = 4;
2642 lance_schema.fields[2].id = 5;
2643 lance_schema.fields[3].id = 6;
2644 let mut manifest = Manifest::new(
2645 lance_schema,
2646 Arc::new(vec![fragment]),
2647 crate::format::DataStorageFormat::new(ConcreteFileVersion::V2_0),
2648 HashMap::new(),
2649 );
2650 manifest.version = manifest_version;
2651
2652 let column_indices = (0..fields.len() as i32).collect();
2653 let txn = Transaction::new(
2654 read_version,
2655 Operation::DataReplacement {
2656 replacements: vec![DataReplacementGroup(
2657 0,
2658 DataFile::new(
2659 "v-new.lance",
2660 fields,
2661 column_indices,
2662 ConcreteFileVersion::V2_0,
2663 None,
2664 None,
2665 ),
2666 )],
2667 },
2668 None,
2669 );
2670 txn.build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2671 .map(|(manifest, _)| manifest.fragments[0].clone())
2672 }
2673
2674 fn replace_field_5(
2677 fragment: Fragment,
2678 manifest_version: u64,
2679 read_version: u64,
2680 ) -> Result<Fragment> {
2681 replace_fields(fragment, vec![5], manifest_version, read_version)
2682 }
2683
2684 #[test]
2685 fn test_data_replacement_rejects_subset_of_legacy_file() {
2686 let mut fragment = Fragment::new(0);
2691 fragment.files = vec![DataFile::new_legacy_from_fields(
2692 "wide.lance",
2693 vec![4, 5],
2694 None,
2695 )];
2696
2697 let result = replace_field_5(fragment, 1, 1);
2698 assert!(
2699 result.is_err(),
2700 "legacy subset replacement must be rejected, got: {:?}",
2701 result.map(|fragment| fragment.files)
2702 );
2703 }
2704
2705 #[test]
2706 fn test_data_replacement_tombstones_fields_spanning_files() {
2707 let mut fragment = Fragment::new(0);
2711 fragment.files = vec![
2712 DataFile::new(
2713 "ab.lance",
2714 vec![3, 4],
2715 vec![0, 1],
2716 ConcreteFileVersion::V2_0,
2717 None,
2718 None,
2719 ),
2720 DataFile::new(
2721 "cd.lance",
2722 vec![5, 6],
2723 vec![0, 1],
2724 ConcreteFileVersion::V2_0,
2725 None,
2726 None,
2727 ),
2728 ];
2729
2730 let fragment = replace_fields(fragment, vec![4, 5], 1, 1).unwrap();
2731 let file = |path| {
2732 fragment
2733 .files
2734 .iter()
2735 .find(|file| file.path == path)
2736 .unwrap_or_else(|| panic!("{path} survives on its live field"))
2737 };
2738 assert_eq!(file("ab.lance").fields.as_ref(), &[3, TOMBSTONE_FIELD_ID]);
2739 assert_eq!(file("cd.lance").fields.as_ref(), &[TOMBSTONE_FIELD_ID, 6]);
2740 assert!(fragment.files.iter().any(|file| file.path == "v-new.lance"));
2741 }
2742
2743 #[test]
2744 fn test_data_replacement_rejects_fields_spanning_a_legacy_file() {
2745 let mut fragment = Fragment::new(0);
2749 fragment.files = vec![
2750 DataFile::new(
2751 "ab.lance",
2752 vec![3, 4],
2753 vec![0, 1],
2754 ConcreteFileVersion::V2_0,
2755 None,
2756 None,
2757 ),
2758 DataFile::new_legacy_from_fields("cd.lance", vec![5, 6], None),
2759 ];
2760
2761 let result = replace_fields(fragment, vec![4, 5], 1, 1);
2762 assert!(
2763 result.is_err(),
2764 "spanning a legacy file must be rejected, got: {:?}",
2765 result.map(|fragment| fragment.files)
2766 );
2767 }
2768
2769 #[test]
2770 fn test_data_replacement_retombstones_wider_file() {
2771 let mut fragment = Fragment::new(0);
2775 fragment.files = vec![DataFile::new(
2776 "wide.lance",
2777 vec![4, TOMBSTONE_FIELD_ID, 5],
2778 vec![0, 1, 2],
2779 ConcreteFileVersion::V2_0,
2780 None,
2781 None,
2782 )];
2783
2784 let fragment = replace_fields(fragment, vec![5], 1, 1).unwrap();
2785 let wide = fragment
2786 .files
2787 .iter()
2788 .find(|file| file.path == "wide.lance")
2789 .expect("wider file survives on its live field");
2790 assert_eq!(
2791 wide.fields.as_ref(),
2792 &[4, TOMBSTONE_FIELD_ID, TOMBSTONE_FIELD_ID]
2793 );
2794 assert!(fragment.files.iter().any(|file| file.path == "v-new.lance"));
2795 }
2796
2797 #[test]
2798 fn test_data_replacement_preserves_overlay_newer_than_snapshot() {
2799 let mut fragment = Fragment::new(0);
2804 fragment.files = vec![DataFile::new(
2806 "wide.lance",
2807 vec![4, 5],
2808 vec![0, 1],
2809 ConcreteFileVersion::V2_0,
2810 None,
2811 None,
2812 )];
2813 fragment.overlays = vec![DataOverlayFile {
2814 data_file: DataFile::new(
2815 "newer.lance",
2816 vec![5],
2817 vec![0],
2818 ConcreteFileVersion::V2_0,
2819 None,
2820 None,
2821 ),
2822 coverage: OverlayCoverage::dense(roaring::RoaringBitmap::from_iter([0u32])),
2823 committed_version: 7,
2824 }];
2825
2826 let fragment = replace_field_5(fragment, 7, 6).unwrap();
2828 assert!(fragment.files.iter().any(|f| f.path == "v-new.lance"));
2829 assert_eq!(
2830 fragment.overlays.len(),
2831 1,
2832 "overlay committed after the snapshot must survive"
2833 );
2834 assert_eq!(fragment.overlays[0].data_file.fields.as_ref(), &[5]);
2835 }
2836
2837 #[test]
2838 fn test_data_overlay_build_manifest_merges_duplicate_groups() {
2839 let manifest = sample_manifest();
2842 let txn = Transaction::new(
2843 manifest.version,
2844 Operation::DataOverlay {
2845 groups: vec![
2846 DataOverlayGroup {
2847 fragment_id: 0,
2848 overlays: vec![overlay_with_field(1, 0)],
2849 },
2850 DataOverlayGroup {
2851 fragment_id: 0,
2852 overlays: vec![overlay_with_field(2, 0)],
2853 },
2854 ],
2855 },
2856 None,
2857 );
2858
2859 let (result, _) = txn
2860 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2861 .unwrap();
2862
2863 let overlays = &result.fragments[0].overlays;
2864 assert_eq!(overlays.len(), 2);
2865 assert_eq!(overlays[0].data_file.fields.as_ref(), [1i32].as_slice());
2866 assert_eq!(overlays[1].data_file.fields.as_ref(), [2i32].as_slice());
2867 }
2868
2869 #[test]
2870 fn test_data_overlay_build_manifest_rejects_unknown_fragment() {
2871 let manifest = sample_manifest();
2872 let txn = Transaction::new(
2873 manifest.version,
2874 Operation::DataOverlay {
2875 groups: vec![DataOverlayGroup {
2876 fragment_id: 99,
2877 overlays: vec![overlay_with_field(1, 0)],
2878 }],
2879 },
2880 None,
2881 );
2882 let err = txn
2883 .build_manifest(Some(&manifest), vec![], "txn", &default_build_config())
2884 .unwrap_err();
2885 assert!(err.to_string().contains("does not exist"), "{err}");
2886 }
2887
2888 #[test]
2889 fn test_nullability_assertion_defaults_conservative() {
2890 for encoded in [false, true] {
2894 let txn = Transaction::try_from(pb::Transaction {
2895 read_version: 1,
2896 uuid: "test".to_string(),
2897 operation: Some(pb::transaction::Operation::Project(
2898 pb::transaction::Project {
2899 schema: vec![],
2900 preserves_nullability: encoded,
2901 },
2902 )),
2903 ..Default::default()
2904 })
2905 .unwrap();
2906 assert!(
2907 matches!(txn.operation, Operation::Project { preserves_nullability, .. } if preserves_nullability == encoded),
2908 "encoded={encoded:?}"
2909 );
2910
2911 let txn = Transaction::try_from(pb::Transaction {
2912 read_version: 1,
2913 uuid: "test".to_string(),
2914 operation: Some(pb::transaction::Operation::Merge(pb::transaction::Merge {
2915 fragments: vec![],
2916 schema: vec![],
2917 schema_metadata: Default::default(),
2918 preserves_nullability: encoded,
2919 })),
2920 ..Default::default()
2921 })
2922 .unwrap();
2923 assert!(
2924 matches!(txn.operation, Operation::Merge { preserves_nullability, .. } if preserves_nullability == encoded),
2925 "encoded={encoded:?}"
2926 );
2927 }
2928 }
2929
2930 mod mem_wal_index_coverage {
2931 use super::*;
2932 use crate::system_index::mem_wal::{
2933 CompactedSsTable, IndexCatchupProgress, MEM_WAL_INDEX_NAME, MemWalIndexDetails,
2934 };
2935
2936 fn user_index(name: &str, uuid: Uuid, frags: &[u32]) -> IndexMetadata {
2937 IndexMetadata {
2938 uuid,
2939 name: name.to_string(),
2940 fields: vec![0],
2941 covering_fields: vec![],
2942 dataset_version: 1,
2943 fragment_bitmap: Some(RoaringBitmap::from_iter(frags.iter().copied())),
2944 index_details: None,
2945 index_version: 0,
2946 created_at: None,
2947 base_id: None,
2948 files: None,
2949 }
2950 }
2951
2952 fn mem_wal_index(details: MemWalIndexDetails) -> IndexMetadata {
2953 crate::system_index::mem_wal::new_mem_wal_index_meta(1, details).unwrap()
2954 }
2955
2956 fn coverage_for(indices: &[IndexMetadata], name: &str) -> Option<Vec<CompactedSsTable>> {
2957 let meta = indices
2958 .iter()
2959 .find(|idx| idx.name == MEM_WAL_INDEX_NAME)
2960 .expect("mem wal index present");
2961 load_mem_wal_index_details(meta.clone())
2962 .unwrap()
2963 .index_catchup
2964 .into_iter()
2965 .find(|entry| entry.index_name == name)
2966 .map(|entry| entry.caught_up_generations)
2967 }
2968
2969 fn compacted(shard: Uuid, generation: u64) -> Vec<CompactedSsTable> {
2970 vec![CompactedSsTable::new(shard, generation)]
2971 }
2972
2973 fn manifest_with(frags: &[u32]) -> Manifest {
2976 let fragments: Vec<Fragment> =
2977 frags.iter().map(|id| Fragment::new(*id as u64)).collect();
2978 Manifest::new(
2979 LanceSchema::default(),
2980 Arc::new(fragments),
2981 DataStorageFormat::default(),
2982 Default::default(),
2983 )
2984 }
2985
2986 fn apply(
2988 after: &mut [IndexMetadata],
2989 before: &[IndexMetadata],
2990 read_frags: &[u32],
2991 read_indices: &[IndexMetadata],
2992 ) -> Result<()> {
2993 let manifest = manifest_with(read_frags);
2994 let segments_before = Transaction::logical_index_segments(before);
2995 Transaction::apply_mem_wal_index_coverage(
2996 after,
2997 &segments_before,
2998 Some(ReadVersionState {
2999 manifest: &manifest,
3000 indices: read_indices,
3001 }),
3002 2,
3003 )
3004 }
3005
3006 fn table(idx_frags: &[u32], uuid: Uuid, details: MemWalIndexDetails) -> Vec<IndexMetadata> {
3007 vec![user_index("idx", uuid, idx_frags), mem_wal_index(details)]
3008 }
3009
3010 fn progress(shard: Uuid, generation: u64) -> MemWalIndexDetails {
3011 MemWalIndexDetails {
3012 compacted_sstables: compacted(shard, generation),
3013 ..Default::default()
3014 }
3015 }
3016
3017 fn progress_with_catchup(shard: Uuid, generation: u64, caught: u64) -> MemWalIndexDetails {
3018 MemWalIndexDetails {
3019 compacted_sstables: compacted(shard, generation),
3020 index_catchup: vec![IndexCatchupProgress::new(
3021 "idx".to_string(),
3022 compacted(shard, caught),
3023 )],
3024 ..Default::default()
3025 }
3026 }
3027
3028 #[test]
3031 fn an_index_covering_the_read_version_is_credited() {
3032 let shard = Uuid::new_v4();
3033 let read = table(&[0, 1], Uuid::new_v4(), progress(shard, 5));
3034 let mut after = table(&[0, 1], Uuid::new_v4(), progress(shard, 5));
3035 apply(&mut after, &read, &[0, 1], &read).unwrap();
3036 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 5)));
3037 }
3038
3039 #[test]
3042 fn an_index_short_of_the_read_version_is_not_credited() {
3043 let shard = Uuid::new_v4();
3044 let read = table(&[0], Uuid::new_v4(), progress(shard, 5));
3045 let mut after = table(&[0], Uuid::new_v4(), progress(shard, 5));
3046 apply(&mut after, &read, &[0, 1], &read).unwrap();
3047 assert_eq!(coverage_for(&after, "idx"), None);
3048 }
3049
3050 #[test]
3058 fn a_bitmap_pruned_in_place_does_not_keep_its_position() {
3059 let shard = Uuid::new_v4();
3060 let uuid = Uuid::new_v4();
3061 let before = table(&[0, 1], uuid, progress_with_catchup(shard, 5, 5));
3062 let mut after = table(&[0], uuid, progress_with_catchup(shard, 5, 5));
3064 apply(&mut after, &before, &[0, 1], &before).unwrap();
3065 assert_eq!(
3066 coverage_for(&after, "idx"),
3067 None,
3068 "a shrunken index kept a position it no longer earns"
3069 );
3070 }
3071
3072 #[test]
3077 fn an_unchanged_index_is_not_raised_beyond_what_it_proves() {
3078 let shard = Uuid::new_v4();
3079 let uuid = Uuid::new_v4();
3080 let before = table(&[0], uuid, progress_with_catchup(shard, 5, 2));
3082 let mut after = before.clone();
3083 apply(&mut after, &before, &[0, 1], &before).unwrap();
3085 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 2)));
3086 }
3087
3088 #[test]
3092 fn a_carried_position_cannot_exceed_the_committed_progress() {
3093 let shard = Uuid::new_v4();
3094 let uuid = Uuid::new_v4();
3095 let before = table(&[0], uuid, progress_with_catchup(shard, 3, 9));
3096 let mut after = before.clone();
3097 apply(&mut after, &before, &[0], &before).unwrap();
3098 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 3)));
3099 }
3100
3101 #[test]
3104 fn an_unchanged_index_is_never_lowered() {
3105 let shard = Uuid::new_v4();
3106 let uuid = Uuid::new_v4();
3107 let before = table(&[0], uuid, progress_with_catchup(shard, 9, 9));
3108 let mut after = before.clone();
3109 apply(&mut after, &before, &[0, 1], &before).unwrap();
3110 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 9)));
3111 }
3112
3113 #[test]
3117 fn credit_is_capped_by_the_committed_progress() {
3118 let shard = Uuid::new_v4();
3119 let read = table(&[0], Uuid::new_v4(), progress(shard, 9));
3120 let mut after = table(&[0], Uuid::new_v4(), progress(shard, 3));
3121 apply(&mut after, &read, &[0], &read).unwrap();
3122 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 3)));
3123 }
3124
3125 #[test]
3130 fn credit_never_reaches_past_the_read_version() {
3131 let shard = Uuid::new_v4();
3132 let read = table(&[0], Uuid::new_v4(), progress(shard, 2));
3134 let mut after = table(&[0], Uuid::new_v4(), progress(shard, 5));
3135 apply(&mut after, &read, &[0], &read).unwrap();
3136 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 2)));
3137 }
3138
3139 #[test]
3143 fn an_index_with_an_unknown_segment_is_not_credited() {
3144 let shard = Uuid::new_v4();
3145 let mut unknown = user_index("idx", Uuid::new_v4(), &[]);
3146 unknown.fragment_bitmap = None;
3147 let read = vec![
3148 user_index("idx", Uuid::new_v4(), &[0, 1]),
3149 unknown,
3150 mem_wal_index(progress(shard, 5)),
3151 ];
3152 let mut after = read.clone();
3153 apply(&mut after, &read, &[0, 1], &read).unwrap();
3154 assert_eq!(coverage_for(&after, "idx"), None);
3155 }
3156
3157 #[test]
3159 fn a_dropped_index_loses_its_entry() {
3160 let shard = Uuid::new_v4();
3161 let before = table(&[0], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
3162 let mut after = vec![mem_wal_index(progress_with_catchup(shard, 5, 5))];
3163 apply(&mut after, &before, &[0], &before).unwrap();
3164 assert_eq!(coverage_for(&after, "idx"), None);
3165 }
3166
3167 #[test]
3172 fn a_new_index_covering_the_read_version_is_credited() {
3173 let shard = Uuid::new_v4();
3174 let before = vec![mem_wal_index(progress(shard, 5))];
3175 let mut after = table(&[0], Uuid::new_v4(), progress(shard, 5));
3176 apply(&mut after, &before, &[0], &before).unwrap();
3178 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 5)));
3179 }
3180
3181 #[test]
3185 fn a_table_with_no_catchup_entry_earns_one() {
3186 let shard = Uuid::new_v4();
3187 let before = table(&[0], Uuid::new_v4(), progress(shard, 5));
3188 let mut after = before.clone();
3189 apply(&mut after, &before, &[0], &before).unwrap();
3190 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 5)));
3191 }
3192
3193 #[test]
3195 fn each_shard_is_credited_independently() {
3196 let merged = Uuid::new_v4();
3197 let idle = Uuid::new_v4();
3198 let details = MemWalIndexDetails {
3199 compacted_sstables: vec![
3200 CompactedSsTable::new(merged, 4),
3201 CompactedSsTable::new(idle, 0),
3202 ],
3203 ..Default::default()
3204 };
3205 let read = table(&[0], Uuid::new_v4(), details.clone());
3206 let mut after = table(&[0], Uuid::new_v4(), details);
3207 apply(&mut after, &read, &[0], &read).unwrap();
3208 let coverage = coverage_for(&after, "idx").expect("credited");
3209 assert_eq!(
3210 coverage
3211 .iter()
3212 .find(|g| g.shard_id == merged)
3213 .map(|g| g.generation),
3214 Some(4)
3215 );
3216 assert_eq!(
3217 coverage
3218 .iter()
3219 .find(|g| g.shard_id == idle)
3220 .map(|g| g.generation),
3221 Some(0)
3222 );
3223 }
3224
3225 #[test]
3227 fn indexes_are_credited_independently() {
3228 let shard = Uuid::new_v4();
3229 let read = vec![
3230 user_index("fast", Uuid::new_v4(), &[0, 1]),
3231 user_index("slow", Uuid::new_v4(), &[0]),
3232 mem_wal_index(progress(shard, 6)),
3233 ];
3234 let mut after = read.clone();
3235 apply(&mut after, &read, &[0, 1], &read).unwrap();
3236 assert_eq!(coverage_for(&after, "fast"), Some(compacted(shard, 6)));
3237 assert_eq!(coverage_for(&after, "slow"), None);
3238 }
3239
3240 #[test]
3242 fn an_index_without_a_bitmap_is_not_credited() {
3243 let shard = Uuid::new_v4();
3244 let mut idx = user_index("idx", Uuid::new_v4(), &[0]);
3245 idx.fragment_bitmap = None;
3246 let read = vec![idx, mem_wal_index(progress(shard, 5))];
3247 let mut after = read.clone();
3248 apply(&mut after, &read, &[0], &read).unwrap();
3249 assert_eq!(coverage_for(&after, "idx"), None);
3250 }
3251
3252 #[test]
3254 fn no_compaction_progress_writes_no_entries() {
3255 let before = table(&[0], Uuid::new_v4(), MemWalIndexDetails::default());
3256 let mut after = before.clone();
3257 let untouched = after.clone();
3258 apply(&mut after, &before, &[0], &before).unwrap();
3259 assert_eq!(after, untouched);
3260 }
3261
3262 #[test]
3264 fn a_table_without_mem_wal_is_a_no_op() {
3265 let before = vec![user_index("idx", Uuid::new_v4(), &[0])];
3266 let mut after = before.clone();
3267 let untouched = after.clone();
3268 apply(&mut after, &before, &[0], &before).unwrap();
3269 assert_eq!(after, untouched);
3270 }
3271
3272 #[test]
3275 fn without_a_read_version_nothing_changes() {
3276 let shard = Uuid::new_v4();
3277 let uuid = Uuid::new_v4();
3278 let before = table(&[0], uuid, progress_with_catchup(shard, 5, 5));
3279 let mut after = before.clone();
3280 let segments_before = Transaction::logical_index_segments(&before);
3281 Transaction::apply_mem_wal_index_coverage(&mut after, &segments_before, None, 2)
3282 .unwrap();
3283 assert_eq!(coverage_for(&after, "idx"), Some(compacted(shard, 5)));
3284 }
3285
3286 #[test]
3289 fn an_untrained_index_earns_nothing() {
3290 let shard = Uuid::new_v4();
3291 let read = vec![
3292 user_index("untrained", Uuid::new_v4(), &[]),
3293 user_index("trained", Uuid::new_v4(), &[0]),
3294 mem_wal_index(progress(shard, 10)),
3295 ];
3296 let mut after = read.clone();
3297 apply(&mut after, &read, &[0], &read).unwrap();
3298 assert_eq!(coverage_for(&after, "untrained"), None);
3299 assert_eq!(coverage_for(&after, "trained"), Some(compacted(shard, 10)));
3300 }
3301
3302 #[test]
3305 fn a_shard_keeps_its_position_while_another_advances() {
3306 let (advancing, quiet) = (Uuid::new_v4(), Uuid::new_v4());
3307 let uuid = Uuid::new_v4();
3308 let details = |advancing_gen: u64| MemWalIndexDetails {
3309 compacted_sstables: vec![
3310 CompactedSsTable::new(advancing, advancing_gen),
3311 CompactedSsTable::new(quiet, 10),
3312 ],
3313 index_catchup: vec![IndexCatchupProgress::new(
3314 "idx".to_string(),
3315 vec![CompactedSsTable::new(quiet, 7)],
3316 )],
3317 ..Default::default()
3318 };
3319 let read = vec![
3322 user_index("idx", uuid, &[0]),
3323 mem_wal_index(MemWalIndexDetails {
3324 compacted_sstables: vec![CompactedSsTable::new(advancing, 9)],
3325 ..details(9)
3326 }),
3327 ];
3328 let mut after = vec![user_index("idx", uuid, &[0]), mem_wal_index(details(10))];
3329 apply(&mut after, &read, &[0], &read).unwrap();
3330
3331 let mut coverage = coverage_for(&after, "idx").expect("credited");
3332 coverage.sort_unstable_by_key(|sstable| sstable.shard_id);
3333 let mut expected = vec![
3334 CompactedSsTable::new(advancing, 9),
3335 CompactedSsTable::new(quiet, 7),
3336 ];
3337 expected.sort_unstable_by_key(|sstable| sstable.shard_id);
3338 assert_eq!(coverage, expected);
3339 }
3340
3341 #[test]
3345 fn an_ordinary_index_job_is_never_blocked() {
3346 let shard = Uuid::new_v4();
3347 let before = table(&[0, 1], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
3348 let mut after = table(&[0], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
3350 apply(&mut after, &before, &[0, 1], &before).unwrap();
3351 assert_eq!(coverage_for(&after, "idx"), None);
3352 }
3353
3354 #[test]
3358 fn an_index_caught_up_to_nothing_gets_no_entry() {
3359 let shard = Uuid::new_v4();
3360 let uuid = Uuid::new_v4();
3361 let before = table(&[0], uuid, progress_with_catchup(shard, 5, 0));
3362 let mut after = before.clone();
3363 apply(&mut after, &before, &[0, 1], &before).unwrap();
3365 assert_eq!(coverage_for(&after, "idx"), None);
3366 }
3367
3368 #[test]
3371 fn carried_positions_do_not_leak_between_shards() {
3372 let (ahead, behind) = (Uuid::new_v4(), Uuid::new_v4());
3373 let uuid = Uuid::new_v4();
3374 let details = MemWalIndexDetails {
3375 compacted_sstables: vec![
3376 CompactedSsTable::new(ahead, 10),
3377 CompactedSsTable::new(behind, 10),
3378 ],
3379 index_catchup: vec![IndexCatchupProgress::new(
3380 "idx".to_string(),
3381 vec![
3382 CompactedSsTable::new(ahead, 8),
3383 CompactedSsTable::new(behind, 2),
3384 ],
3385 )],
3386 ..Default::default()
3387 };
3388 let before = vec![user_index("idx", uuid, &[0]), mem_wal_index(details)];
3389 let mut after = before.clone();
3390 apply(&mut after, &before, &[0, 1], &before).unwrap();
3392
3393 let mut coverage = coverage_for(&after, "idx").expect("carried");
3394 coverage.sort_unstable_by_key(|sstable| sstable.shard_id);
3395 let mut expected = vec![
3396 CompactedSsTable::new(ahead, 8),
3397 CompactedSsTable::new(behind, 2),
3398 ];
3399 expected.sort_unstable_by_key(|sstable| sstable.shard_id);
3400 assert_eq!(coverage, expected);
3401 }
3402
3403 #[test]
3409 fn a_bitmap_narrowed_after_the_build_loses_its_position() {
3410 let shard = Uuid::new_v4();
3411 let uuid = Uuid::new_v4();
3412 let mut migrated = table(&[0], uuid, progress_with_catchup(shard, 5, 5));
3415
3416 Transaction::withdraw_coverage_invalidated_after_build(
3417 &mut migrated,
3418 &["idx".to_string()],
3419 3,
3420 )
3421 .unwrap();
3422
3423 assert_eq!(coverage_for(&migrated, "idx"), None);
3424 }
3425
3426 #[test]
3430 fn metadata_migration_that_does_not_narrow_keeps_its_position() {
3431 let shard = Uuid::new_v4();
3432 let uuid = Uuid::new_v4();
3433 let mut migrated = table(&[0, 1], uuid, progress_with_catchup(shard, 5, 5));
3434 migrated[0].files = Some(Vec::new());
3435 migrated[0].created_at = Some(chrono::Utc::now());
3436
3437 Transaction::withdraw_coverage_invalidated_after_build(&mut migrated, &[], 3).unwrap();
3439
3440 assert_eq!(coverage_for(&migrated, "idx"), Some(compacted(shard, 5)));
3441 }
3442
3443 #[test]
3446 fn an_unchanged_commit_does_not_rewrite_the_system_index() {
3447 let shard = Uuid::new_v4();
3448 let uuid = Uuid::new_v4();
3449 let before = table(&[0], uuid, progress_with_catchup(shard, 5, 5));
3450 let mut after = before.clone();
3451 apply(&mut after, &before, &[0], &before).unwrap();
3452
3453 let system_uuid = |indices: &[IndexMetadata]| {
3454 indices
3455 .iter()
3456 .find(|idx| idx.name == MEM_WAL_INDEX_NAME)
3457 .unwrap()
3458 .uuid
3459 };
3460 assert_eq!(system_uuid(&after), system_uuid(&before));
3461 }
3462
3463 #[test]
3467 fn without_a_read_version_a_changed_index_still_loses_its_position() {
3468 let shard = Uuid::new_v4();
3469 let before = table(&[0, 1], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
3470 let mut after = table(&[0], Uuid::new_v4(), progress_with_catchup(shard, 5, 5));
3471 let segments_before = Transaction::logical_index_segments(&before);
3472 Transaction::apply_mem_wal_index_coverage(&mut after, &segments_before, None, 2)
3473 .unwrap();
3474 assert_eq!(coverage_for(&after, "idx"), None);
3475 }
3476
3477 #[test]
3480 fn the_derivation_is_stable_across_attempts() {
3481 let shard = Uuid::new_v4();
3482 let read = table(&[0, 1], Uuid::new_v4(), progress(shard, 5));
3483 let mut first = table(&[0, 1], Uuid::new_v4(), progress(shard, 5));
3484 let mut second = first.clone();
3485 apply(&mut first, &read, &[0, 1], &read).unwrap();
3486 apply(&mut second, &read, &[0, 1], &read).unwrap();
3487 assert_eq!(coverage_for(&first, "idx"), coverage_for(&second, "idx"));
3488 }
3489 }
3490}