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

jerry-024 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 b36d533f fix: align manifest entry schema with Java Paimon (#742)
b36d533f is described below

commit b36d533f744d01b8988e2240a022faf78325d0c4
Author: shyjsarah <[email protected]>
AuthorDate: Mon Aug 24 14:16:02 2026 +0800

    fix: align manifest entry schema with Java Paimon (#742)
---
 crates/paimon/src/spec/manifest_entry.rs |  38 ++++++++--
 crates/paimon/src/spec/objects_file.rs   | 115 +++++++++++++++++++------------
 crates/paimon/src/table/table_commit.rs  |  42 ++++++++++-
 3 files changed, 142 insertions(+), 53 deletions(-)

diff --git a/crates/paimon/src/spec/manifest_entry.rs 
b/crates/paimon/src/spec/manifest_entry.rs
index 194fc314..0e66891a 100644
--- a/crates/paimon/src/spec/manifest_entry.rs
+++ b/crates/paimon/src/spec/manifest_entry.rs
@@ -55,6 +55,10 @@ impl Hash for Identifier {
 /// Impl Reference: 
<https://github.com/apache/paimon/blob/release-0.8.2/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestEntry.java>
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct ManifestEntry {
+    // Java's VersionedObjectSerializer requires the version field at position 
0.
+    #[serde(rename = "_VERSION")]
+    version: i32,
+
     #[serde(rename = "_KIND")]
     kind: FileKind,
 
@@ -69,9 +73,6 @@ pub struct ManifestEntry {
 
     #[serde(rename = "_FILE")]
     pub(crate) file: DataFileMeta,
-
-    #[serde(rename = "_VERSION")]
-    version: i32,
 }
 
 #[allow(dead_code)]
@@ -151,12 +152,12 @@ impl ManifestEntry {
         version: i32,
     ) -> Self {
         ManifestEntry {
+            version,
             kind,
             partition,
             bucket,
             total_buckets,
             file,
-            version,
         }
     }
 
@@ -186,6 +187,7 @@ pub const MANIFEST_ENTRY_SCHEMA: &str = r#"["null", {
     "name": "record",
     "namespace": "org.apache.paimon.avro.generated",
     "fields": [
+        {"name": "_VERSION", "type": "int"},
         {"name": "_KIND", "type": "int"},
         {"name": "_PARTITION", "type": "bytes"},
         {"name": "_BUCKET", "type": "int"},
@@ -231,16 +233,38 @@ pub const MANIFEST_ENTRY_SCHEMA: &str = r#"["null", {
                 {"name": "_FIRST_ROW_ID", "type": ["null", "long"], "default": 
null},
                 {"name": "_WRITE_COLS", "type": ["null", {"type": "array", 
"items": "string"}], "default": null}
             ]
-        }], "default": null},
-        {"name": "_VERSION", "type": "int"}
+        }], "default": null}
     ]
 }]"#;
 
 #[cfg(test)]
 mod tests {
-    use super::Identifier;
+    use super::{Identifier, MANIFEST_ENTRY_SCHEMA};
+    use crate::spec::avro::schema::WriterSchema;
     use std::collections::HashSet;
 
+    #[test]
+    fn test_manifest_entry_schema_matches_java_field_order() {
+        let schema = WriterSchema::parse(MANIFEST_ENTRY_SCHEMA).unwrap();
+        let field_names = schema
+            .fields
+            .iter()
+            .map(|field| field.name.as_str())
+            .collect::<Vec<_>>();
+
+        assert_eq!(
+            field_names,
+            vec![
+                "_VERSION",
+                "_KIND",
+                "_PARTITION",
+                "_BUCKET",
+                "_TOTAL_BUCKETS",
+                "_FILE"
+            ]
+        );
+    }
+
     fn ident(file_name: &str, level: i32) -> Identifier {
         Identifier {
             partition: vec![1, 2, 3],
diff --git a/crates/paimon/src/spec/objects_file.rs 
b/crates/paimon/src/spec/objects_file.rs
index 2b16f55c..7f2c1436 100644
--- a/crates/paimon/src/spec/objects_file.rs
+++ b/crates/paimon/src/spec/objects_file.rs
@@ -80,6 +80,7 @@ pub(crate) fn avro_codec(compression: &str) -> 
crate::Result<Codec> {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::spec::avro::from_avro_bytes_fast;
     use crate::spec::manifest_common::FileKind;
     use crate::spec::manifest_entry::{ManifestEntry, MANIFEST_ENTRY_SCHEMA};
     use crate::spec::manifest_file_meta::MANIFEST_FILE_META_SCHEMA;
@@ -87,6 +88,54 @@ mod tests {
     use crate::spec::{DataFileMeta, ManifestFileMeta};
     use chrono::{DateTime, Utc};
 
+    fn manifest_entry() -> ManifestEntry {
+        let value_bytes = vec![
+            0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 129, 1, 
0, 0, 0, 0, 0, 0, 0,
+        ];
+        let single_value = vec![0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 
0, 0, 0, 0, 0];
+        ManifestEntry::new(
+            FileKind::Add,
+            single_value.clone(),
+            1,
+            10,
+            DataFileMeta {
+                file_name: "test.parquet".to_string(),
+                file_size: 100,
+                row_count: 50,
+                min_key: single_value.clone(),
+                max_key: single_value,
+                key_stats: BinaryTableStats::new(
+                    value_bytes.clone(),
+                    value_bytes.clone(),
+                    vec![Some(1), Some(2)],
+                ),
+                value_stats: BinaryTableStats::new(
+                    value_bytes.clone(),
+                    value_bytes,
+                    vec![Some(1), Some(2)],
+                ),
+                min_sequence_number: 1,
+                max_sequence_number: 50,
+                schema_id: 0,
+                level: 0,
+                extra_files: vec![],
+                creation_time: Some(
+                    "2024-09-06T07:45:55.039+00:00"
+                        .parse::<DateTime<Utc>>()
+                        .unwrap(),
+                ),
+                delete_row_count: Some(0),
+                embedded_index: None,
+                first_row_id: None,
+                write_cols: None,
+                external_path: None,
+                file_source: None,
+                value_stats_cols: None,
+            },
+            2,
+        )
+    }
+
     #[test]
     fn test_roundtrip_manifest_file_meta() {
         let value_bytes = vec![
@@ -134,56 +183,32 @@ mod tests {
 
     #[test]
     fn test_roundtrip_manifest_entry() {
-        let value_bytes = vec![
-            0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 129, 1, 
0, 0, 0, 0, 0, 0, 0,
-        ];
-        let single_value = vec![0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 
0, 0, 0, 0, 0];
-        let original = vec![ManifestEntry::new(
-            FileKind::Add,
-            single_value.clone(),
-            1,
-            10,
-            DataFileMeta {
-                file_name: "test.parquet".to_string(),
-                file_size: 100,
-                row_count: 50,
-                min_key: single_value.clone(),
-                max_key: single_value.clone(),
-                key_stats: BinaryTableStats::new(
-                    value_bytes.clone(),
-                    value_bytes.clone(),
-                    vec![Some(1), Some(2)],
-                ),
-                value_stats: BinaryTableStats::new(
-                    value_bytes.clone(),
-                    value_bytes.clone(),
-                    vec![Some(1), Some(2)],
-                ),
-                min_sequence_number: 1,
-                max_sequence_number: 50,
-                schema_id: 0,
-                level: 0,
-                extra_files: vec![],
-                creation_time: Some(
-                    "2024-09-06T07:45:55.039+00:00"
-                        .parse::<DateTime<Utc>>()
-                        .unwrap(),
-                ),
-                delete_row_count: Some(0),
-                embedded_index: None,
-                first_row_id: None,
-                write_cols: None,
-                external_path: None,
-                file_source: None,
-                value_stats_cols: None,
-            },
-            2,
-        )];
+        let original = vec![manifest_entry()];
         let bytes = to_avro_bytes(MANIFEST_ENTRY_SCHEMA, &original).unwrap();
         let decoded = from_avro_bytes::<ManifestEntry>(&bytes).unwrap();
         assert_eq!(original, decoded);
     }
 
+    #[test]
+    fn test_read_manifest_entry_with_legacy_rust_field_order() {
+        let mut schema: serde_json::Value = 
serde_json::from_str(MANIFEST_ENTRY_SCHEMA).unwrap();
+        let fields = schema.as_array_mut().unwrap()[1]
+            .as_object_mut()
+            .unwrap()
+            .get_mut("fields")
+            .unwrap()
+            .as_array_mut()
+            .unwrap();
+        let version = fields.remove(0);
+        fields.push(version);
+        let legacy_schema = serde_json::to_string(&schema).unwrap();
+
+        let original = vec![manifest_entry()];
+        let bytes = to_avro_bytes(&legacy_schema, &original).unwrap();
+        let decoded = from_avro_bytes_fast::<ManifestEntry>(&bytes).unwrap();
+        assert_eq!(original, decoded);
+    }
+
     #[tokio::test]
     async fn test_read_manifest_list() {
         let workdir =
diff --git a/crates/paimon/src/table/table_commit.rs 
b/crates/paimon/src/table/table_commit.rs
index 39757ced..e5f09f9e 100644
--- a/crates/paimon/src/table/table_commit.rs
+++ b/crates/paimon/src/table/table_commit.rs
@@ -2870,7 +2870,7 @@ impl TableCommit {
                         msg.bucket,
                         msg.total_buckets.unwrap_or(self.total_buckets),
                         file.clone(),
-                        0,
+                        2,
                     )
                 })
             })
@@ -3142,6 +3142,7 @@ mod tests {
         BinaryRowBuilder, DataFileMeta, DeletionVectorMeta, GlobalIndexMeta, 
IndexFileMeta,
         ManifestList, TableSchema, POSTPONE_BUCKET,
     };
+    use apache_avro::types::Value;
     use chrono::{DateTime, Utc};
 
     #[tokio::test]
@@ -5366,6 +5367,45 @@ mod tests {
         assert_eq!(snapshot.changelog_record_count(), Some(3));
         assert!(snapshot.changelog_manifest_list().is_some());
         assert!(snapshot.changelog_manifest_list_size().unwrap() > 0);
+
+        let manifest_dir = format!("{table_path}/manifest");
+        let changelog_manifest_list = 
snapshot.changelog_manifest_list().unwrap();
+        let changelog_metas = ManifestList::read(
+            &file_io,
+            &format!("{manifest_dir}/{changelog_manifest_list}"),
+        )
+        .await
+        .unwrap();
+        assert_eq!(changelog_metas.len(), 1);
+
+        let manifest_bytes = file_io
+            .new_input(&format!(
+                "{manifest_dir}/{}",
+                changelog_metas[0].file_name()
+            ))
+            .unwrap()
+            .read()
+            .await
+            .unwrap();
+        let values = apache_avro::Reader::new(manifest_bytes.as_ref())
+            .unwrap()
+            .collect::<std::result::Result<Vec<_>, _>>()
+            .unwrap();
+        assert_eq!(values.len(), 1);
+
+        let value = match &values[0] {
+            Value::Union(_, value) => value.as_ref(),
+            value => value,
+        };
+        let Value::Record(fields) = value else {
+            panic!("manifest entry must be an Avro record");
+        };
+        let version = fields
+            .iter()
+            .find(|(name, _)| name == "_VERSION")
+            .map(|(_, value)| value)
+            .expect("manifest entry format identifier");
+        assert_eq!(version, &Value::Int(2));
     }
 
     #[tokio::test]

Reply via email to