laskoviymishka commented on code in PR #3300:
URL: https://github.com/apache/iceberg-rust/pull/3300#discussion_r4153160120


##########
crates/iceberg/src/spec/manifest/writer.rs:
##########
@@ -361,6 +361,19 @@ impl ManifestWriter {
         Ok(())
     }
 
+    /// Add a manifest entry as a tombstone, preserving its original sequence 
numbers and
+    /// snapshot id.
+    ///
+    /// The caller owns `snapshot_id`: set it to the deleting snapshot for a 
file this commit
+    /// removes, and leave it untouched when carrying an earlier tombstone 
forward. Use
+    /// `add_delete_entry` instead to have this manifest's snapshot id stamped 
on the entry.

Review Comment:
   The rename reads well, and since this is `pub(crate)` there's no 
published-surface cost to it. One thing: this doc points at `add_delete_entry`, 
which is `#[allow(dead_code)]` — so we're sending readers to a function that 
isn't really live. I'd describe the behavior directly instead ("set 
`entry.snapshot_id` to this snapshot to credit the delete to this commit, or 
leave it to carry a prior tombstone forward") and drop the sibling reference.



##########
crates/iceberg/src/transaction/overwrite.rs:
##########
@@ -0,0 +1,912 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use uuid::Uuid;
+
+use crate::error::Result;
+use crate::spec::{
+    DataFile, Manifest, ManifestEntry, ManifestFile, Operation, 
PartitionSpecRef, SchemaRef,
+};
+use crate::table::Table;
+use crate::transaction::snapshot::{
+    DefaultManifestProcess, SnapshotProduceOperation, SnapshotProducer,
+};
+use crate::transaction::{ActionCommit, TransactionAction};
+use crate::{Error, ErrorKind};
+
+/// OverwriteAction is a transaction action for overwriting data files in the 
table.
+///
+/// Creates a snapshot with `Operation::Overwrite` semantics — adds new data 
files and
+/// optionally removes existing data files by rewriting affected manifests 
with those
+/// entries marked as `ManifestStatus::Deleted`.
+pub struct OverwriteAction {
+    check_duplicate: bool,
+    commit_uuid: Option<Uuid>,
+    snapshot_properties: HashMap<String, String>,
+    added_data_files: Vec<DataFile>,
+    deleted_data_files: Vec<DataFile>,
+}
+
+impl OverwriteAction {
+    pub(crate) fn new() -> Self {
+        Self {
+            check_duplicate: true,
+            commit_uuid: None,
+            snapshot_properties: HashMap::default(),
+            added_data_files: vec![],
+            deleted_data_files: vec![],
+        }
+    }
+
+    /// Set whether to check duplicate files.
+    pub fn with_check_duplicate(mut self, v: bool) -> Self {
+        self.check_duplicate = v;
+        self
+    }
+
+    /// Add data files to the snapshot.
+    pub fn add_data_files(mut self, data_files: impl IntoIterator<Item = 
DataFile>) -> Self {
+        self.added_data_files.extend(data_files);
+        self
+    }
+
+    /// Specify data files to be removed from the table in this overwrite.
+    pub fn delete_data_files(mut self, data_files: impl IntoIterator<Item = 
DataFile>) -> Self {
+        self.deleted_data_files.extend(data_files);
+        self
+    }
+
+    /// Set commit UUID for the snapshot.
+    pub fn set_commit_uuid(mut self, commit_uuid: Uuid) -> Self {
+        self.commit_uuid = Some(commit_uuid);
+        self
+    }
+
+    /// Set snapshot summary properties.
+    pub fn set_snapshot_properties(mut self, snapshot_properties: 
HashMap<String, String>) -> Self {
+        self.snapshot_properties = snapshot_properties;
+        self
+    }
+}
+
+#[async_trait]
+impl TransactionAction for OverwriteAction {
+    async fn commit(self: Arc<Self>, table: &Table) -> Result<ActionCommit> {
+        let snapshot_producer = SnapshotProducer::new(
+            table,
+            self.commit_uuid.unwrap_or_else(Uuid::now_v7),
+            self.snapshot_properties.clone(),
+            self.added_data_files.clone(),
+            self.deleted_data_files.clone(),
+        );
+
+        snapshot_producer.validate_added_data_files()?;

Review Comment:
   `validate_added_data_files` errors with "Only data content type is allowed 
for fast append", and that string now surfaces from overwrite commits too. I'd 
drop the "for fast append" so it reads right from both callers.



##########
crates/iceberg/src/transaction/overwrite.rs:
##########
@@ -0,0 +1,912 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use uuid::Uuid;
+
+use crate::error::Result;
+use crate::spec::{
+    DataFile, Manifest, ManifestEntry, ManifestFile, Operation, 
PartitionSpecRef, SchemaRef,
+};
+use crate::table::Table;
+use crate::transaction::snapshot::{
+    DefaultManifestProcess, SnapshotProduceOperation, SnapshotProducer,
+};
+use crate::transaction::{ActionCommit, TransactionAction};
+use crate::{Error, ErrorKind};
+
+/// OverwriteAction is a transaction action for overwriting data files in the 
table.
+///
+/// Creates a snapshot with `Operation::Overwrite` semantics — adds new data 
files and
+/// optionally removes existing data files by rewriting affected manifests 
with those
+/// entries marked as `ManifestStatus::Deleted`.
+pub struct OverwriteAction {
+    check_duplicate: bool,
+    commit_uuid: Option<Uuid>,
+    snapshot_properties: HashMap<String, String>,
+    added_data_files: Vec<DataFile>,
+    deleted_data_files: Vec<DataFile>,
+}
+
+impl OverwriteAction {
+    pub(crate) fn new() -> Self {
+        Self {
+            check_duplicate: true,
+            commit_uuid: None,
+            snapshot_properties: HashMap::default(),
+            added_data_files: vec![],
+            deleted_data_files: vec![],
+        }
+    }
+
+    /// Set whether to check duplicate files.
+    pub fn with_check_duplicate(mut self, v: bool) -> Self {
+        self.check_duplicate = v;
+        self
+    }
+
+    /// Add data files to the snapshot.
+    pub fn add_data_files(mut self, data_files: impl IntoIterator<Item = 
DataFile>) -> Self {
+        self.added_data_files.extend(data_files);
+        self
+    }
+
+    /// Specify data files to be removed from the table in this overwrite.
+    pub fn delete_data_files(mut self, data_files: impl IntoIterator<Item = 
DataFile>) -> Self {
+        self.deleted_data_files.extend(data_files);
+        self
+    }
+
+    /// Set commit UUID for the snapshot.
+    pub fn set_commit_uuid(mut self, commit_uuid: Uuid) -> Self {
+        self.commit_uuid = Some(commit_uuid);
+        self
+    }
+
+    /// Set snapshot summary properties.
+    pub fn set_snapshot_properties(mut self, snapshot_properties: 
HashMap<String, String>) -> Self {
+        self.snapshot_properties = snapshot_properties;
+        self
+    }
+}
+
+#[async_trait]
+impl TransactionAction for OverwriteAction {
+    async fn commit(self: Arc<Self>, table: &Table) -> Result<ActionCommit> {
+        let snapshot_producer = SnapshotProducer::new(
+            table,
+            self.commit_uuid.unwrap_or_else(Uuid::now_v7),
+            self.snapshot_properties.clone(),
+            self.added_data_files.clone(),

Review Comment:
   `FastAppendAction` runs `dedupe_added_files()` before writing; the overwrite 
path passes `added_data_files` straight through, so calling `add_data_files` 
twice — or handing it a list with a repeated path — silently writes duplicate 
entries into the manifest (the `check_duplicate` guard only catches 
cross-snapshot dupes). I'd mirror the fast-append dedup here, or push it down 
into `SnapshotProducer::new` so both paths get it for free.



##########
crates/iceberg/src/transaction/overwrite.rs:
##########
@@ -0,0 +1,912 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use uuid::Uuid;
+
+use crate::error::Result;
+use crate::spec::{
+    DataFile, Manifest, ManifestEntry, ManifestFile, Operation, 
PartitionSpecRef, SchemaRef,
+};
+use crate::table::Table;
+use crate::transaction::snapshot::{
+    DefaultManifestProcess, SnapshotProduceOperation, SnapshotProducer,
+};
+use crate::transaction::{ActionCommit, TransactionAction};
+use crate::{Error, ErrorKind};
+
+/// OverwriteAction is a transaction action for overwriting data files in the 
table.
+///
+/// Creates a snapshot with `Operation::Overwrite` semantics — adds new data 
files and
+/// optionally removes existing data files by rewriting affected manifests 
with those
+/// entries marked as `ManifestStatus::Deleted`.
+pub struct OverwriteAction {
+    check_duplicate: bool,
+    commit_uuid: Option<Uuid>,
+    snapshot_properties: HashMap<String, String>,
+    added_data_files: Vec<DataFile>,
+    deleted_data_files: Vec<DataFile>,
+}
+
+impl OverwriteAction {
+    pub(crate) fn new() -> Self {
+        Self {
+            check_duplicate: true,
+            commit_uuid: None,
+            snapshot_properties: HashMap::default(),
+            added_data_files: vec![],
+            deleted_data_files: vec![],
+        }
+    }
+
+    /// Set whether to check duplicate files.
+    pub fn with_check_duplicate(mut self, v: bool) -> Self {
+        self.check_duplicate = v;
+        self
+    }
+
+    /// Add data files to the snapshot.
+    pub fn add_data_files(mut self, data_files: impl IntoIterator<Item = 
DataFile>) -> Self {
+        self.added_data_files.extend(data_files);
+        self
+    }
+
+    /// Specify data files to be removed from the table in this overwrite.
+    pub fn delete_data_files(mut self, data_files: impl IntoIterator<Item = 
DataFile>) -> Self {
+        self.deleted_data_files.extend(data_files);
+        self
+    }
+
+    /// Set commit UUID for the snapshot.
+    pub fn set_commit_uuid(mut self, commit_uuid: Uuid) -> Self {
+        self.commit_uuid = Some(commit_uuid);
+        self
+    }
+
+    /// Set snapshot summary properties.
+    pub fn set_snapshot_properties(mut self, snapshot_properties: 
HashMap<String, String>) -> Self {
+        self.snapshot_properties = snapshot_properties;
+        self
+    }
+}
+
+#[async_trait]
+impl TransactionAction for OverwriteAction {
+    async fn commit(self: Arc<Self>, table: &Table) -> Result<ActionCommit> {
+        let snapshot_producer = SnapshotProducer::new(
+            table,
+            self.commit_uuid.unwrap_or_else(Uuid::now_v7),
+            self.snapshot_properties.clone(),
+            self.added_data_files.clone(),
+            self.deleted_data_files.clone(),
+        );
+
+        snapshot_producer.validate_added_data_files()?;
+
+        if self.check_duplicate {
+            snapshot_producer.validate_duplicate_files().await?;
+        }
+
+        let deleted_file_paths: HashSet<String> = self
+            .deleted_data_files
+            .iter()
+            .map(|f| f.file_path.clone())
+            .collect();
+
+        let snapshot_id = snapshot_producer.snapshot_id();
+        snapshot_producer
+            .commit(
+                OverwriteOperation {
+                    deleted_file_paths,
+                    has_added_data_files: !self.added_data_files.is_empty(),
+                    snapshot_id,
+                    removed_data_files: vec![],
+                },
+                DefaultManifestProcess,
+            )
+            .await
+    }
+}
+
+struct OverwriteOperation {
+    deleted_file_paths: HashSet<String>,
+    has_added_data_files: bool,
+    snapshot_id: i64,
+    removed_data_files: Vec<(DataFile, SchemaRef, PartitionSpecRef)>,
+}
+
+impl SnapshotProduceOperation for OverwriteOperation {
+    fn operation(&self) -> Operation {
+        match (
+            self.has_added_data_files,
+            !self.deleted_file_paths.is_empty(),
+        ) {
+            (true, true) => Operation::Overwrite,
+            (false, true) => Operation::Delete,
+            _ => Operation::Append,

Review Comment:
   This mapping looks right to me and lines up with `BaseOverwriteFiles` — 
adds+deletes is Overwrite, deletes-only is Delete, add-only is Append. The one 
case the `_` arm quietly folds in is a property-only overwrite (no adds, no 
deletes), which then reports `Append`; that's the existing #1548 workaround 
leaking through rather than anything this PR introduced. Fine to leave as-is, 
but a one-line comment on the `_` arm noting it also covers the property-only 
case would save the next reader the double-take.



##########
crates/iceberg/src/transaction/snapshot.rs:
##########
@@ -113,26 +129,33 @@ pub(crate) struct SnapshotProducer<'a> {
     commit_uuid: Uuid,
     snapshot_properties: HashMap<String, String>,
     added_data_files: Vec<DataFile>,
+    deleted_data_files: Vec<DataFile>,

Review Comment:
   This field is carrying less than it looks. `SnapshotProducer` never reads 
`deleted_data_files` for any accounting — the real delete tracking runs through 
`OverwriteOperation::removed_data_files`, populated separately during 
`existing_manifest`. The only use here is `self.deleted_data_files.is_empty()` 
in the empty-commit guard, yet we deep-clone the whole `Vec<DataFile>` (stats 
maps, bounds, bloom offsets and all) into the producer just to answer a boolean.
   
   I'd replace it with `has_deleted_files: bool` — FastAppend passes `false`, 
overwrite passes `!deleted_data_files.is_empty()`. That kills the clone and 
closes the trap where a future action sets `deleted_data_files` expecting it to 
drive the summary and silently gets wrong counts. If we do keep a list, at 
least rename it — `deleted_data_files` sitting next to `removed_data_files` 
while actually meaning "delete candidates" is asking for a mix-up.



##########
crates/iceberg/src/transaction/overwrite.rs:
##########
@@ -0,0 +1,912 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use uuid::Uuid;
+
+use crate::error::Result;
+use crate::spec::{
+    DataFile, Manifest, ManifestEntry, ManifestFile, Operation, 
PartitionSpecRef, SchemaRef,
+};
+use crate::table::Table;
+use crate::transaction::snapshot::{
+    DefaultManifestProcess, SnapshotProduceOperation, SnapshotProducer,
+};
+use crate::transaction::{ActionCommit, TransactionAction};
+use crate::{Error, ErrorKind};
+
+/// OverwriteAction is a transaction action for overwriting data files in the 
table.
+///
+/// Creates a snapshot with `Operation::Overwrite` semantics — adds new data 
files and
+/// optionally removes existing data files by rewriting affected manifests 
with those
+/// entries marked as `ManifestStatus::Deleted`.
+pub struct OverwriteAction {
+    check_duplicate: bool,
+    commit_uuid: Option<Uuid>,
+    snapshot_properties: HashMap<String, String>,
+    added_data_files: Vec<DataFile>,
+    deleted_data_files: Vec<DataFile>,
+}
+
+impl OverwriteAction {
+    pub(crate) fn new() -> Self {
+        Self {
+            check_duplicate: true,
+            commit_uuid: None,
+            snapshot_properties: HashMap::default(),
+            added_data_files: vec![],
+            deleted_data_files: vec![],
+        }
+    }
+
+    /// Set whether to check duplicate files.
+    pub fn with_check_duplicate(mut self, v: bool) -> Self {
+        self.check_duplicate = v;
+        self
+    }
+
+    /// Add data files to the snapshot.
+    pub fn add_data_files(mut self, data_files: impl IntoIterator<Item = 
DataFile>) -> Self {
+        self.added_data_files.extend(data_files);
+        self
+    }
+
+    /// Specify data files to be removed from the table in this overwrite.
+    pub fn delete_data_files(mut self, data_files: impl IntoIterator<Item = 
DataFile>) -> Self {
+        self.deleted_data_files.extend(data_files);
+        self
+    }
+
+    /// Set commit UUID for the snapshot.
+    pub fn set_commit_uuid(mut self, commit_uuid: Uuid) -> Self {
+        self.commit_uuid = Some(commit_uuid);
+        self
+    }
+
+    /// Set snapshot summary properties.
+    pub fn set_snapshot_properties(mut self, snapshot_properties: 
HashMap<String, String>) -> Self {
+        self.snapshot_properties = snapshot_properties;
+        self
+    }
+}
+
+#[async_trait]
+impl TransactionAction for OverwriteAction {
+    async fn commit(self: Arc<Self>, table: &Table) -> Result<ActionCommit> {
+        let snapshot_producer = SnapshotProducer::new(
+            table,
+            self.commit_uuid.unwrap_or_else(Uuid::now_v7),
+            self.snapshot_properties.clone(),
+            self.added_data_files.clone(),
+            self.deleted_data_files.clone(),
+        );
+
+        snapshot_producer.validate_added_data_files()?;
+
+        if self.check_duplicate {
+            snapshot_producer.validate_duplicate_files().await?;
+        }
+
+        let deleted_file_paths: HashSet<String> = self
+            .deleted_data_files
+            .iter()
+            .map(|f| f.file_path.clone())
+            .collect();
+
+        let snapshot_id = snapshot_producer.snapshot_id();
+        snapshot_producer
+            .commit(
+                OverwriteOperation {
+                    deleted_file_paths,
+                    has_added_data_files: !self.added_data_files.is_empty(),
+                    snapshot_id,
+                    removed_data_files: vec![],
+                },
+                DefaultManifestProcess,
+            )
+            .await
+    }
+}
+
+struct OverwriteOperation {
+    deleted_file_paths: HashSet<String>,
+    has_added_data_files: bool,
+    snapshot_id: i64,
+    removed_data_files: Vec<(DataFile, SchemaRef, PartitionSpecRef)>,
+}
+
+impl SnapshotProduceOperation for OverwriteOperation {
+    fn operation(&self) -> Operation {
+        match (
+            self.has_added_data_files,
+            !self.deleted_file_paths.is_empty(),
+        ) {
+            (true, true) => Operation::Overwrite,
+            (false, true) => Operation::Delete,
+            _ => Operation::Append,
+        }
+    }
+
+    async fn delete_entries(
+        &self,
+        _snapshot_produce: &SnapshotProducer<'_>,
+    ) -> Result<Vec<ManifestEntry>> {
+        Ok(vec![])
+    }
+
+    fn removed_data_files(&self) -> &[(DataFile, SchemaRef, PartitionSpecRef)] 
{
+        &self.removed_data_files
+    }
+
+    async fn existing_manifest(
+        &mut self,
+        snapshot_produce: &SnapshotProducer<'_>,
+    ) -> Result<Vec<ManifestFile>> {
+        let Some(snapshot) = 
snapshot_produce.table.metadata().current_snapshot() else {
+            self.validate_deletes_matched()?;
+            return Ok(vec![]);
+        };
+
+        let manifest_list = snapshot_produce
+            .table
+            .manifest_list_reader(snapshot)
+            .load()
+            .await?;
+
+        if self.deleted_file_paths.is_empty() {
+            return Ok(manifest_list
+                .entries()
+                .iter()
+                .filter(|entry| {
+                    // Delete-only manifests record which files were removed 
and must survive
+                    // until `expire_snapshots` cleans them up (see #2148).
+                    entry.has_added_files()
+                        || entry.has_existing_files()
+                        || entry.has_deleted_files()
+                })
+                .cloned()
+                .collect());
+        }
+
+        let mut result = Vec::new();
+
+        for manifest_file in manifest_list.entries() {
+            if !manifest_file.has_added_files()
+                && !manifest_file.has_existing_files()
+                && !manifest_file.has_deleted_files()
+            {
+                continue;
+            }
+
+            let manifest = snapshot_produce
+                .table
+                .manifest_reader()
+                .read(manifest_file)
+                .await?;
+
+            let has_deletes = manifest.entries().iter().any(|entry| {
+                entry.is_alive() && 
self.deleted_file_paths.contains(entry.file_path())
+            });
+
+            if has_deletes {
+                let rewritten = self
+                    .rewrite_manifest(snapshot_produce, manifest_file, 
&manifest)
+                    .await?;
+                result.push(rewritten);
+            } else {
+                result.push(manifest_file.clone());
+            }
+        }
+
+        self.validate_deletes_matched()?;
+
+        Ok(result)
+    }
+}
+
+impl OverwriteOperation {
+    /// Fails when a requested delete path was not live in any manifest, 
matching the existence
+    /// check Java's `BaseOverwriteFiles` performs before committing.
+    fn validate_deletes_matched(&self) -> Result<()> {
+        let removed: HashSet<&str> = self
+            .removed_data_files
+            .iter()
+            .map(|(data_file, _, _)| data_file.file_path.as_str())
+            .collect();
+        let mut missing: Vec<&str> = self
+            .deleted_file_paths
+            .iter()
+            .map(String::as_str)
+            .filter(|path| !removed.contains(path))
+            .collect();
+
+        if missing.is_empty() {
+            return Ok(());
+        }
+
+        missing.sort_unstable();
+        Err(Error::new(
+            ErrorKind::DataInvalid,
+            format!("Missing required files to delete: {}", missing.join(", 
")),
+        ))
+    }
+
+    /// Rewrite a manifest, marking entries whose file paths are in 
`deleted_file_paths`
+    /// as `ManifestStatus::Deleted`.
+    async fn rewrite_manifest(
+        &mut self,
+        snapshot_produce: &SnapshotProducer<'_>,
+        manifest_file: &ManifestFile,
+        manifest: &Manifest,
+    ) -> Result<ManifestFile> {
+        let partition_spec: PartitionSpecRef = 
Arc::new(manifest.metadata().partition_spec.clone());
+        let mut writer = snapshot_produce.new_manifest_writer(
+            manifest_file.content,
+            manifest.metadata().schema.clone(),
+            partition_spec.as_ref().clone(),
+        )?;
+
+        for entry in manifest.entries() {
+            let entry: ManifestEntry = (**entry).clone();
+            if !entry.is_alive() {
+                // A tombstone keeps its original snapshot id so the delete 
stays attributed
+                // to the snapshot that made it.
+                writer.add_tombstone_entry(entry)?;
+            } else if self.deleted_file_paths.contains(entry.file_path()) {
+                let mut deleted = entry;
+                deleted.snapshot_id = Some(self.snapshot_id);
+                self.removed_data_files.push((
+                    deleted.data_file().clone(),
+                    manifest.metadata().schema.clone(),
+                    partition_spec.clone(),
+                ));
+                writer.add_tombstone_entry(deleted)?;
+            } else {
+                writer.add_existing_entry(entry)?;
+            }
+        }
+
+        writer.write_manifest_file().await

Review Comment:
   For V3 tables we need to carry the original `first_row_id` onto the 
rewritten manifest here — `write_manifest_file()` returns it as `None`, and 
`add_manifests` then stamps a fresh `first_row_id` equal to the current 
`next_row_id`.
   
   The surviving (Existing) entries in this manifest keep the row IDs they were 
assigned relative to the *original* manifest's `first_row_id`, which is lower. 
So the manifest-list entry ends up advertising a lower bound higher than the 
row IDs actually inside it, and any reader doing row-ID-range pruning (deletion 
vectors, targeted MOR lookups down the line) skips this manifest and misses 
those rows. Java and PyIceberg read the same field, so it's not just an 
iceberg-rust read.
   
   Simplest fix is to preserve it right after the rewrite:
   
   ```rust
   let mut rewritten = writer.write_manifest_file().await?;
   // Alive entries keep their original row IDs; preserve the lower bound.
   rewritten.first_row_id = manifest_file.first_row_id;
   Ok(rewritten)
   ```
   
   That puts `assign_first_row_id` on the `(Some, Some)` branch so it stops 
re-advancing. It's latent today since nothing here exercises row-ID pruning, 
but I'd rather lock it down before MOR lands on top.



##########
crates/iceberg/src/transaction/snapshot.rs:
##########
@@ -301,7 +328,9 @@ impl<'a> SnapshotProducer<'a> {
 
     // Write manifest file for added data files and return the ManifestFile 
for ManifestList.
     async fn write_added_manifest(&mut self) -> Result<ManifestFile> {
-        let added_data_files = std::mem::take(&mut self.added_data_files);
+        // Cloned rather than taken: the summary is built after the manifests, 
so the added
+        // files must still be here.
+        let added_data_files = self.added_data_files.clone();

Review Comment:
   Flipping to produce-then-summarize means `write_added_manifest` can't 
`mem::take` here anymore and now `.clone()`s the entire added-file vec on every 
commit — and this path is shared with `FastAppendAction`, so every append now 
pays a deep copy of all the `DataFile` stats/bounds maps it used to just move. 
I'd keep the `mem::take` and instead feed `summary()` the added files 
explicitly (pass the slice, or collect the summary counts during manifest 
construction), so the reorder doesn't tax the common append path.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to