viirya commented on code in PR #2726:
URL: https://github.com/apache/iceberg-rust/pull/2726#discussion_r3493180570


##########
crates/iceberg/src/spec/snapshot_summary.rs:
##########
@@ -506,22 +506,31 @@ fn update_totals(
         },
     };
 
-    let added = summary
-        .additional_properties
-        .get(added_property)
-        .map_or(0, |value| {
-            value
-                .parse::<u64>()
-                .expect("must be parsable as it was just serialized")
-        });
-    let removed = summary
-        .additional_properties
-        .get(removed_property)
-        .map_or(0, |value| {
-            value
-                .parse::<u64>()
-                .expect("must be parsable as it was just serialized")
-        });
+    // Parse the added/removed deltas, tolerating an unparsable value by 
skipping
+    // the total entirely rather than panicking. Computed metrics always 
overwrite
+    // user-supplied summary properties (see `SnapshotProducer::summary`), so 
a bad
+    // value should only ever come from a previous snapshot's summary; matching
+    // iceberg-java's `updateTotal`, we ignore it instead of failing the 
commit.
+    let parse_delta = |property: &str| -> Option<u64> {
+        match summary.additional_properties.get(property) {
+            None => Some(0),
+            Some(value) => match value.parse::<u64>() {
+                Ok(v) => Some(v),
+                Err(parse_err) => {
+                    tracing::warn!(
+                        "Property '{property}' could not be parsed when 
computing '{total_property}': {parse_err}. \
+                         Skipping total computation.",
+                    );
+                    None
+                }
+            },
+        }
+    };
+
+    let (Some(added), Some(removed)) = (parse_delta(added_property), 
parse_delta(removed_property))
+    else {
+        return;
+    };

Review Comment:
   No worries — and thanks for the context on the original `expect`. You're 
right that the intent (we just wrote these, so they should be parsable) was 
sound; it only broke once user properties could reach the same keys. Tolerating 
the bad value here is a cheap safety net regardless, since a stale/garbage 
value can also arrive from a previous snapshot's summary.
   
   Agreed that collapsing total computation into the same place as the delta 
calculation (so we never round-trip through strings) is the cleaner long-term 
shape — happy to leave that as a separate follow-up as you suggest.
   



##########
crates/iceberg/src/transaction/snapshot.rs:
##########
@@ -404,8 +404,18 @@ impl<'a> SnapshotProducer<'a> {
 
         let previous_snapshot = table_metadata.current_snapshot();
 
-        let mut additional_properties = summary_collector.build();
-        additional_properties.extend(self.snapshot_properties.clone());
+        // User-supplied snapshot properties are applied first, then the 
computed
+        // metrics overwrite any colliding keys. This matches iceberg-java
+        // (`SnapshotProducer.summary`), where computed `added-*`/`total-*` 
values
+        // are written after user properties so a user cannot shadow them with 
a
+        // bad (or merely wrong) value that would corrupt the snapshot summary.
+        // User-supplied snapshot properties are applied first, then the 
computed
+        // metrics overwrite any colliding keys. This matches iceberg-java
+        // (`SnapshotProducer.summary`), where computed `added-*`/`total-*` 
values
+        // are written after user properties so a user cannot shadow them with 
a
+        // bad (or merely wrong) value that would corrupt the snapshot summary.
+        let mut additional_properties = self.snapshot_properties.clone();
+        additional_properties.extend(summary_collector.build());

Review Comment:
   Good catch — that was a bad copy/paste on my part. Removed the duplicated 
block in 5b6a124.
   



##########
crates/iceberg/src/transaction/snapshot.rs:
##########
@@ -404,8 +404,18 @@ impl<'a> SnapshotProducer<'a> {
 
         let previous_snapshot = table_metadata.current_snapshot();
 
-        let mut additional_properties = summary_collector.build();
-        additional_properties.extend(self.snapshot_properties.clone());
+        // User-supplied snapshot properties are applied first, then the 
computed
+        // metrics overwrite any colliding keys. This matches iceberg-java
+        // (`SnapshotProducer.summary`), where computed `added-*`/`total-*` 
values
+        // are written after user properties so a user cannot shadow them with 
a
+        // bad (or merely wrong) value that would corrupt the snapshot summary.
+        // User-supplied snapshot properties are applied first, then the 
computed
+        // metrics overwrite any colliding keys. This matches iceberg-java
+        // (`SnapshotProducer.summary`), where computed `added-*`/`total-*` 
values
+        // are written after user properties so a user cannot shadow them with 
a
+        // bad (or merely wrong) value that would corrupt the snapshot summary.
+        let mut additional_properties = self.snapshot_properties.clone();
+        additional_properties.extend(summary_collector.build());

Review Comment:
   You're right, and worth being precise about the residual gap: 
`summary_collector.build()` only emits `added-*`/`removed-*` for metrics that 
actually changed this commit (`set_if_positive` skips zeros), and 
`update_totals` only overwrites a `total-*` when the previous summary had it. 
So a user value for a metric key that *isn't* computed/updated this commit 
still survives.
   
   I checked iceberg-java for comparison: `SnapshotSummary.Builder.build()` has 
no reserved-key filtering either — it relies purely on `putAll(properties)` 
then `metrics.addTo(builder)` overwrite ordering, so a user property for a 
metric not computed this commit survives there too. So after this PR we match 
Java's behavior.
   
   Proactively dropping any user property whose key collides with a reserved 
metric name would go a step beyond Java and close the gap fully. I'd prefer to 
keep that as a follow-up rather than widen this PR — agreed it's worth doing. I 
can open an issue to track it.
   



-- 
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