You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by li...@apache.org on 2025/04/08 13:19:00 UTC

(iceberg-rust) branch main updated: fix: TableMetadata max sequence number validation (#1167)

This is an automated email from the ASF dual-hosted git repository.

liurenjie1024 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new d7e2685e fix: TableMetadata max sequence number validation (#1167)
d7e2685e is described below

commit d7e2685e7e64be6b5877356587c2cd847d51b0cb
Author: Christian <ch...@hansetag.com>
AuthorDate: Tue Apr 8 06:18:56 2025 -0700

    fix: TableMetadata max sequence number validation (#1167)
    
    ## What changes are included in this PR?
    Ensures that the sequence number of any snapshot in TableMetadata is
    lower or equal to the `last_sequence_number`.
    
    In Java this happens during indexing:
    
    https://github.com/apache/iceberg/blob/0152075268851d397b60bcb66e5a7c346271dfd9/core/src/main/java/org/apache/iceberg/TableMetadata.java#L842
---
 crates/iceberg/src/spec/table_metadata.rs | 105 +++++++++++++++++++++++++++++-
 1 file changed, 103 insertions(+), 2 deletions(-)

diff --git a/crates/iceberg/src/spec/table_metadata.rs b/crates/iceberg/src/spec/table_metadata.rs
index 59d69e0f..47ba7823 100644
--- a/crates/iceberg/src/spec/table_metadata.rs
+++ b/crates/iceberg/src/spec/table_metadata.rs
@@ -434,7 +434,7 @@ impl TableMetadata {
         self.validate_chronological_metadata_logs()?;
         // Normalize location (remove trailing slash)
         self.location = self.location.trim_end_matches('/').to_string();
-        self.validate_format_version_specifics()?;
+        self.validate_snapshot_sequence_number()?;
         self.try_normalize_partition_spec()?;
         self.try_normalize_sort_order()?;
         Ok(self)
@@ -547,7 +547,7 @@ impl TableMetadata {
     }
 
     /// Validate that for V1 Metadata the last_sequence_number is 0
-    fn validate_format_version_specifics(&self) -> Result<()> {
+    fn validate_snapshot_sequence_number(&self) -> Result<()> {
         if self.format_version < FormatVersion::V2 && self.last_sequence_number != 0 {
             return Err(Error::new(
                 ErrorKind::DataInvalid,
@@ -558,6 +558,24 @@ impl TableMetadata {
             ));
         }
 
+        if self.format_version >= FormatVersion::V2 {
+            if let Some(snapshot) = self
+                .snapshots
+                .values()
+                .find(|snapshot| snapshot.sequence_number() > self.last_sequence_number)
+            {
+                return Err(Error::new(
+                    ErrorKind::DataInvalid,
+                    format!(
+                        "Invalid snapshot with id {} and sequence number {} greater than last sequence number {}",
+                        snapshot.snapshot_id(),
+                        snapshot.sequence_number(),
+                        self.last_sequence_number
+                    ),
+                ));
+            }
+        }
+
         Ok(())
     }
 
@@ -2015,6 +2033,89 @@ mod tests {
             .contains("Snapshot for reference foo does not exist in the existing snapshots list"));
     }
 
+    #[test]
+    fn test_v2_wrong_max_snapshot_sequence_number() {
+        let data = r#"
+        {
+            "format-version": 2,
+            "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
+            "location": "s3://bucket/test/location",
+            "last-sequence-number": 1,
+            "last-updated-ms": 1602638573590,
+            "last-column-id": 3,
+            "current-schema-id": 0,
+            "schemas": [
+                {
+                    "type": "struct",
+                    "schema-id": 0,
+                    "fields": [
+                        {
+                            "id": 1,
+                            "name": "x",
+                            "required": true,
+                            "type": "long"
+                        }
+                    ]
+                }
+            ],
+            "default-spec-id": 0,
+            "partition-specs": [
+                {
+                    "spec-id": 0,
+                    "fields": []
+                }
+            ],
+            "last-partition-id": 1000,
+            "default-sort-order-id": 0,
+            "sort-orders": [
+                {
+                    "order-id": 0,
+                    "fields": []
+                }
+            ],
+            "properties": {},
+            "current-snapshot-id": 3055729675574597004,
+            "snapshots": [
+                {
+                    "snapshot-id": 3055729675574597004,
+                    "timestamp-ms": 1555100955770,
+                    "sequence-number": 4,
+                    "summary": {
+                        "operation": "append"
+                    },
+                    "manifest-list": "s3://a/b/2.avro",
+                    "schema-id": 0
+                }
+            ],
+            "statistics": [],
+            "snapshot-log": [],
+            "metadata-log": []
+        }
+    "#;
+
+        let err = serde_json::from_str::<TableMetadata>(data).unwrap_err();
+        println!("{}", err);
+        assert!(err.to_string().contains(
+            "Invalid snapshot with id 3055729675574597004 and sequence number 4 greater than last sequence number 1"
+        ));
+
+        // Change max sequence number to 4 - should work
+        let data = data.replace(
+            r#""last-sequence-number": 1,"#,
+            r#""last-sequence-number": 4,"#,
+        );
+        let metadata = serde_json::from_str::<TableMetadata>(data.as_str()).unwrap();
+        assert_eq!(metadata.last_sequence_number, 4);
+
+        // Change max sequence number to 5 - should work
+        let data = data.replace(
+            r#""last-sequence-number": 4,"#,
+            r#""last-sequence-number": 5,"#,
+        );
+        let metadata = serde_json::from_str::<TableMetadata>(data.as_str()).unwrap();
+        assert_eq!(metadata.last_sequence_number, 5);
+    }
+
     #[test]
     fn test_statistic_files() {
         let data = r#"