Validate that files with pre-assigned `first_row_id` (e.g. partial-column files from MERGE INTO) still match existing files in the current snapshot. When MERGE INTO and COMPACT run concurrently, compaction may rewrite the original files that partial-column files reference. If the original file's row ID range no longer exists, the partial-column files become invalid and the commit must be rejected
(
&self,
commit_entries: &[ManifestEntry],
latest_snapshot: &Option<Snapshot>,
)
| 712 | /// row ID range no longer exists, the partial-column files become invalid and |
| 713 | /// the commit must be rejected. |
| 714 | async fn validate_row_id_alignment( |
| 715 | &self, |
| 716 | commit_entries: &[ManifestEntry], |
| 717 | latest_snapshot: &Option<Snapshot>, |
| 718 | ) -> Result<()> { |
| 719 | // Collect files that already have first_row_id assigned (pre-set by writer). |
| 720 | let files_to_check: Vec<_> = commit_entries |
| 721 | .iter() |
| 722 | .filter(|e| *e.kind() == FileKind::Add && e.file().first_row_id.is_some()) |
| 723 | .collect(); |
| 724 | |
| 725 | if files_to_check.is_empty() { |
| 726 | return Ok(()); |
| 727 | } |
| 728 | |
| 729 | let snap = match latest_snapshot { |
| 730 | Some(s) => s, |
| 731 | None => { |
| 732 | // No existing snapshot means no existing files — any pre-assigned |
| 733 | // first_row_id cannot match anything. |
| 734 | let entry = &files_to_check[0]; |
| 735 | return Err(crate::Error::DataInvalid { |
| 736 | message: format!( |
| 737 | "Row ID conflict: file '{}' has pre-assigned first_row_id={} \ |
| 738 | but no snapshot exists. The referenced files may have been removed \ |
| 739 | by a concurrent compaction.", |
| 740 | entry.file().file_name, |
| 741 | entry.file().first_row_id.unwrap(), |
| 742 | ), |
| 743 | source: None, |
| 744 | }); |
| 745 | } |
| 746 | }; |
| 747 | |
| 748 | // Read current files from the latest snapshot, filtered by partitions. |
| 749 | let partition_filter = self.build_entries_partition_filter(&files_to_check)?; |
| 750 | let scan = TableScan::new(&self.table, partition_filter, vec![], None, None, None) |
| 751 | .with_scan_all_files(); |
| 752 | let existing_entries = scan.plan_manifest_entries(snap).await?; |
| 753 | |
| 754 | // Build index: (partition, bucket, first_row_id, row_count) |
| 755 | let existing_index: HashSet<(&[u8], i32, i64, i64)> = existing_entries |
| 756 | .iter() |
| 757 | .filter_map(|e| { |
| 758 | e.file() |
| 759 | .first_row_id |
| 760 | .map(|fid| (e.partition(), e.bucket(), fid, e.file().row_count)) |
| 761 | }) |
| 762 | .collect(); |
| 763 | |
| 764 | for entry in &files_to_check { |
| 765 | let fid = entry.file().first_row_id.unwrap(); |
| 766 | let key = ( |
| 767 | entry.partition(), |
| 768 | entry.bucket(), |
| 769 | fid, |
| 770 | entry.file().row_count, |
| 771 | ); |
no test coverage detected