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

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


The following commit(s) were added to refs/heads/main by this push:
     new fb82a4cc test(spec): cover untested partial-update and blob-view error 
paths (#688)
fb82a4cc is described below

commit fb82a4ccac0481bdbfd75bf4bdec0709bc2eda6f
Author: jackylee <[email protected]>
AuthorDate: Sun Aug 9 21:06:29 2026 +0800

    test(spec): cover untested partial-update and blob-view error paths (#688)
---
 crates/paimon/src/spec/blob_view_struct.rs | 123 +++++++++++++++++++++++++++++
 crates/paimon/src/spec/partial_update.rs   |  85 ++++++++++++++++++++
 2 files changed, 208 insertions(+)

diff --git a/crates/paimon/src/spec/blob_view_struct.rs 
b/crates/paimon/src/spec/blob_view_struct.rs
index c76ff69b..ce37848c 100644
--- a/crates/paimon/src/spec/blob_view_struct.rs
+++ b/crates/paimon/src/spec/blob_view_struct.rs
@@ -214,4 +214,127 @@ mod tests {
             matches!(err, Error::DataInvalid { message, .. } if 
message.contains("trailing bytes"))
         );
     }
+
+    /// Assemble a payload from parts so that individual header fields can be
+    /// corrupted independently of what [`BlobViewStruct::serialize`] would 
emit.
+    fn payload(
+        version: u8,
+        magic: u64,
+        identifier_length: i32,
+        identifier_bytes: &[u8],
+        field_id: i32,
+        row_id: i64,
+    ) -> Vec<u8> {
+        let mut buf = Vec::new();
+        buf.push(version);
+        buf.extend_from_slice(&magic.to_le_bytes());
+        buf.extend_from_slice(&identifier_length.to_le_bytes());
+        buf.extend_from_slice(identifier_bytes);
+        buf.extend_from_slice(&field_id.to_le_bytes());
+        buf.extend_from_slice(&row_id.to_le_bytes());
+        buf
+    }
+
+    #[test]
+    fn test_rejects_empty_payload() {
+        let err = BlobViewStruct::deserialize(&[]).unwrap_err();
+        assert!(
+            matches!(err, Error::DataInvalid { ref message, .. } if 
message.contains("too short")),
+            "expected too-short error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_rejects_unsupported_version() {
+        let bytes = payload(CURRENT_VERSION + 1, MAGIC, 9, b"db.source", 3, 
42);
+
+        let err = BlobViewStruct::deserialize(&bytes).unwrap_err();
+        assert!(
+            matches!(err, Error::Unsupported { ref message }
+                if message.contains("Expecting BlobViewStruct version")),
+            "expected version error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_rejects_bad_magic() {
+        let bytes = payload(CURRENT_VERSION, !MAGIC, 9, b"db.source", 3, 42);
+
+        let err = BlobViewStruct::deserialize(&bytes).unwrap_err();
+        assert!(
+            matches!(err, Error::DataInvalid { ref message, .. }
+                if message.contains("missing magic header")),
+            "expected magic error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_rejects_negative_identifier_length() {
+        let bytes = payload(CURRENT_VERSION, MAGIC, -1, b"db.source", 3, 42);
+
+        let err = BlobViewStruct::deserialize(&bytes).unwrap_err();
+        assert!(
+            matches!(err, Error::DataInvalid { ref message, .. }
+                if message.contains("negative identifier length")),
+            "expected negative-length error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_rejects_identifier_length_exceeding_data_size() {
+        let bytes = payload(CURRENT_VERSION, MAGIC, 64, b"db.source", 3, 42);
+
+        let err = BlobViewStruct::deserialize(&bytes).unwrap_err();
+        assert!(
+            matches!(err, Error::DataInvalid { ref message, .. }
+                if message.contains("identifier length exceeds data size")),
+            "expected length-overflow error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_rejects_non_utf8_identifier() {
+        let invalid = [b'd', b'b', b'.', 0xFF];
+        let bytes = payload(CURRENT_VERSION, MAGIC, 4, &invalid, 3, 42);
+
+        let err = BlobViewStruct::deserialize(&bytes).unwrap_err();
+        assert!(
+            matches!(err, Error::DataInvalid { ref message, .. }
+                if message.contains("Invalid UTF-8 in BlobViewStruct 
identifier")),
+            "expected UTF-8 error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_rejects_identifier_without_database() {
+        let bytes = payload(CURRENT_VERSION, MAGIC, 6, b"source", 3, 42);
+
+        let err = BlobViewStruct::deserialize(&bytes).unwrap_err();
+        assert!(
+            matches!(err, Error::DataInvalid { ref message, .. }
+                if message.contains("must be 'database.table'")),
+            "expected identifier-shape error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_is_blob_view_struct_rejects_short_and_mismatched_payloads() {
+        assert!(!BlobViewStruct::is_blob_view_struct(&[0u8; 8]));
+        assert!(!BlobViewStruct::is_blob_view_struct(&payload(
+            CURRENT_VERSION + 1,
+            MAGIC,
+            9,
+            b"db.source",
+            3,
+            42
+        )));
+        assert!(!BlobViewStruct::is_blob_view_struct(&payload(
+            CURRENT_VERSION,
+            !MAGIC,
+            9,
+            b"db.source",
+            3,
+            42
+        )));
+    }
 }
diff --git a/crates/paimon/src/spec/partial_update.rs 
b/crates/paimon/src/spec/partial_update.rs
index b8a0018e..d7fd9d73 100644
--- a/crates/paimon/src/spec/partial_update.rs
+++ b/crates/paimon/src/spec/partial_update.rs
@@ -933,4 +933,89 @@ mod tests {
             ])
         );
     }
+
+    #[test]
+    fn test_validate_sequence_groups_rejects_unknown_field() {
+        let options = 
partial_update_options(&[("fields.version.sequence-group", "prcie")]);
+        let config = PartialUpdateConfig::new(&options);
+        let fields = vec![
+            DataField::new(0, "id".to_string(), DataType::Int(IntType::new())),
+            DataField::new(1, "version".to_string(), 
DataType::Int(IntType::new())),
+            DataField::new(2, "price".to_string(), 
DataType::Int(IntType::new())),
+        ];
+
+        let err = config
+            .validated_sequence_groups(&fields, &["id".to_string()])
+            .unwrap_err();
+
+        assert!(
+            matches!(err, crate::Error::ConfigInvalid { ref message }
+                if message.contains("prcie")
+                    && message.contains("does not exist in the table schema")),
+            "expected unknown sequence-group field error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_validate_aggregate_functions_rejects_empty_listagg_field() {
+        let options = partial_update_options(&[("fields..list-agg-delimiter", 
";")]);
+        let config = PartialUpdateConfig::new(&options);
+        let fields = vec![
+            DataField::new(0, "id".to_string(), DataType::Int(IntType::new())),
+            DataField::new(1, "note".to_string(), 
DataType::Int(IntType::new())),
+        ];
+
+        let err = config
+            .validated_aggregate_functions(&fields, &["id".to_string()])
+            .unwrap_err();
+
+        assert!(
+            matches!(err, crate::Error::ConfigInvalid { ref message }
+                if message.contains("Invalid partial-update listagg option")
+                    && message.contains("fields..list-agg-delimiter")),
+            "expected invalid listagg option error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn 
test_validate_aggregate_functions_rejects_empty_aggregate_function_field() {
+        let options = partial_update_options(&[("fields..aggregate-function", 
"sum")]);
+        let config = PartialUpdateConfig::new(&options);
+        let fields = vec![
+            DataField::new(0, "id".to_string(), DataType::Int(IntType::new())),
+            DataField::new(1, "price".to_string(), 
DataType::Int(IntType::new())),
+        ];
+
+        let err = config
+            .validated_aggregate_functions(&fields, &["id".to_string()])
+            .unwrap_err();
+
+        assert!(
+            matches!(err, crate::Error::ConfigInvalid { ref message }
+                if message.contains("Invalid partial-update aggregate-function 
option")
+                    && message.contains("fields..aggregate-function")),
+            "expected invalid aggregate-function option error, got {err:?}"
+        );
+    }
+
+    #[test]
+    fn test_validate_aggregate_functions_rejects_unknown_default_function() {
+        let options = 
partial_update_options(&[(FIELDS_DEFAULT_AGG_FUNCTION_OPTION, "sume")]);
+        let config = PartialUpdateConfig::new(&options);
+        let fields = vec![
+            DataField::new(0, "id".to_string(), DataType::Int(IntType::new())),
+            DataField::new(1, "price".to_string(), 
DataType::Int(IntType::new())),
+        ];
+
+        let err = config
+            .validated_aggregate_functions(&fields, &["id".to_string()])
+            .unwrap_err();
+
+        assert!(
+            matches!(err, crate::Error::ConfigInvalid { ref message }
+                if message.contains("sume")
+                    && message.contains(FIELDS_DEFAULT_AGG_FUNCTION_OPTION)),
+            "expected unknown default aggregate function error, got {err:?}"
+        );
+    }
 }

Reply via email to