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