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 78994db0 fix(spec): align index manifest field order with Java (#797)
78994db0 is described below

commit 78994db04d3baeb52e50a6c9c41b1ce6e672de6a
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue Sep 8 17:04:19 2026 +0800

    fix(spec): align index manifest field order with Java (#797)
---
 crates/paimon/src/spec/index_manifest.rs |  14 ++-
 crates/paimon/src/spec/objects_file.rs   | 174 ++++++++++++++++++++++++++++++-
 2 files changed, 184 insertions(+), 4 deletions(-)

diff --git a/crates/paimon/src/spec/index_manifest.rs 
b/crates/paimon/src/spec/index_manifest.rs
index 3cb3ead4..4050ece5 100644
--- a/crates/paimon/src/spec/index_manifest.rs
+++ b/crates/paimon/src/spec/index_manifest.rs
@@ -83,6 +83,10 @@ pub const INDEX_MANIFEST_ENTRY_SCHEMA: &str = r#"{
 /// Impl Reference: 
<https://github.com/apache/paimon/blob/release-0.8.2/paimon-core/src/main/java/org/apache/paimon/manifest/IndexManifestEntry.java>
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct IndexManifestEntry {
+    // Keep serde field order aligned with Java's IndexManifestEntrySerializer.
+    #[serde(rename = "_VERSION")]
+    pub version: i32,
+
     #[serde(rename = "_KIND")]
     pub kind: FileKind,
 
@@ -94,9 +98,6 @@ pub struct IndexManifestEntry {
 
     #[serde(flatten)]
     pub index_file: IndexFileMeta,
-
-    #[serde(rename = "_VERSION")]
-    pub version: i32,
 }
 
 impl Display for IndexManifestEntry {
@@ -304,6 +305,13 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_index_manifest_serializes_version_first() {
+        let entry = global_index_entry(None);
+        let json = serde_json::to_string(&entry).unwrap();
+        assert!(json.starts_with(r#"{"_VERSION":1,"_KIND":0,"#), "{json}");
+    }
+
     #[test]
     fn source_meta_round_trips_through_index_manifest() {
         // New-format writer schema carries _SOURCE_META; a Some(..) value 
must round-trip.
diff --git a/crates/paimon/src/spec/objects_file.rs 
b/crates/paimon/src/spec/objects_file.rs
index d5d0d9fe..56f2fe4d 100644
--- a/crates/paimon/src/spec/objects_file.rs
+++ b/crates/paimon/src/spec/objects_file.rs
@@ -81,13 +81,33 @@ pub(crate) fn avro_codec(compression: &str) -> 
crate::Result<Codec> {
 mod tests {
     use super::*;
     use crate::spec::avro::from_avro_bytes_fast;
+    use crate::spec::index_manifest::INDEX_MANIFEST_ENTRY_SCHEMA;
     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;
     use crate::spec::stats::BinaryTableStats;
-    use crate::spec::{DataFileMeta, ManifestFileMeta};
+    use crate::spec::{DataFileMeta, IndexManifestEntry, ManifestFileMeta};
+    use apache_avro::types::Value;
     use chrono::{DateTime, Utc};
 
+    // Check the record decoded from the OCF writer schema, including fields 
whose
+    // values were omitted by serde and filled from Avro defaults.
+    fn assert_record_field_order<'a>(value: &'a Value, expected: &[&str]) -> 
&'a [(String, Value)] {
+        let fields = match value {
+            Value::Union(_, value) => return assert_record_field_order(value, 
expected),
+            Value::Record(fields) => fields,
+            other => panic!("Expected an Avro record, got {other:?}"),
+        };
+        assert_eq!(
+            fields
+                .iter()
+                .map(|(name, _)| name.as_str())
+                .collect::<Vec<_>>(),
+            expected
+        );
+        fields
+    }
+
     fn manifest_entry() -> ManifestEntry {
         manifest_entry_with_sequences(None)
     }
@@ -157,6 +177,34 @@ mod tests {
             0,
         )];
         let bytes = to_avro_bytes(MANIFEST_FILE_META_SCHEMA, 
&original).unwrap();
+        let value = Reader::new(bytes.as_slice())
+            .unwrap()
+            .next()
+            .unwrap()
+            .unwrap();
+        // ManifestFileMetaSerializer.toRow, including optional pruning 
statistics.
+        let fields = assert_record_field_order(
+            &value,
+            &[
+                "_VERSION",
+                "_FILE_NAME",
+                "_FILE_SIZE",
+                "_NUM_ADDED_FILES",
+                "_NUM_DELETED_FILES",
+                "_PARTITION_STATS",
+                "_SCHEMA_ID",
+                "_MIN_BUCKET",
+                "_MAX_BUCKET",
+                "_MIN_LEVEL",
+                "_MAX_LEVEL",
+                "_MIN_ROW_ID",
+                "_MAX_ROW_ID",
+            ],
+        );
+        assert_record_field_order(
+            &fields[5].1,
+            &["_MIN_VALUES", "_MAX_VALUES", "_NULL_COUNTS"],
+        );
         let decoded = from_avro_bytes::<ManifestFileMeta>(&bytes).unwrap();
         assert_eq!(original, decoded);
     }
@@ -192,10 +240,134 @@ mod tests {
     fn test_roundtrip_manifest_entry() {
         let original = vec![manifest_entry()];
         let bytes = to_avro_bytes(MANIFEST_ENTRY_SCHEMA, &original).unwrap();
+        let value = Reader::new(bytes.as_slice())
+            .unwrap()
+            .next()
+            .unwrap()
+            .unwrap();
+        // ManifestEntrySerializer.toRow and DataFileMetaSerializer.toRow.
+        let fields = assert_record_field_order(
+            &value,
+            &[
+                "_VERSION",
+                "_KIND",
+                "_PARTITION",
+                "_BUCKET",
+                "_TOTAL_BUCKETS",
+                "_FILE",
+            ],
+        );
+        let file = assert_record_field_order(
+            &fields[5].1,
+            &[
+                "_FILE_NAME",
+                "_FILE_SIZE",
+                "_ROW_COUNT",
+                "_MIN_KEY",
+                "_MAX_KEY",
+                "_KEY_STATS",
+                "_VALUE_STATS",
+                "_MIN_SEQUENCE_NUMBER",
+                "_MAX_SEQUENCE_NUMBER",
+                "_SCHEMA_ID",
+                "_LEVEL",
+                "_EXTRA_FILES",
+                "_CREATION_TIME",
+                "_DELETE_ROW_COUNT",
+                "_EMBEDDED_FILE_INDEX",
+                "_FILE_SOURCE",
+                "_VALUE_STATS_COLS",
+                "_EXTERNAL_PATH",
+                "_FIRST_ROW_ID",
+                "_WRITE_COLS",
+                "_WRITE_COLS_SEQUENCES",
+            ],
+        );
+        for stats in [&file[5].1, &file[6].1] {
+            assert_record_field_order(stats, &["_MIN_VALUES", "_MAX_VALUES", 
"_NULL_COUNTS"]);
+        }
         let decoded = from_avro_bytes::<ManifestEntry>(&bytes).unwrap();
         assert_eq!(original, decoded);
     }
 
+    #[test]
+    fn test_roundtrip_index_manifest_field_order() {
+        let entry: IndexManifestEntry = 
serde_json::from_value(serde_json::json!({
+            "_VERSION": 1,
+            "_KIND": 0,
+            "_PARTITION": [0, 0, 0, 0],
+            "_BUCKET": 2,
+            "_INDEX_TYPE": "DELETION_VECTORS",
+            "_FILE_NAME": "index-0",
+            "_FILE_SIZE": 42,
+            "_ROW_COUNT": 7,
+            "_DELETIONS_VECTORS_RANGES": [{"f0": "data-0", "f1": 1, "f2": 24, 
"_CARDINALITY": 3}],
+            "_EXTERNAL_PATH": "s3://bucket/index-0"
+        }))
+        .unwrap();
+        let global_entry: IndexManifestEntry = 
serde_json::from_value(serde_json::json!({
+            "_INDEX_TYPE": "GLOBAL_INDEX",
+            "_FILE_NAME": "index-1",
+            "_GLOBAL_INDEX": {
+                "_ROW_RANGE_START": 10, "_ROW_RANGE_END": 20, 
"_INDEX_FIELD_ID": 3,
+                "_EXTRA_FIELD_IDS": [4], "_INDEX_META": [5, 6], 
"_SOURCE_META": [7, 8]
+            },
+            "_VERSION": 1, "_KIND": 0, "_PARTITION": [0, 0, 0, 0], "_BUCKET": 
-1,
+            "_FILE_SIZE": 64, "_ROW_COUNT": 11
+        }))
+        .unwrap();
+        let original = vec![entry, global_entry];
+        let bytes = to_avro_bytes(INDEX_MANIFEST_ENTRY_SCHEMA, 
&original).unwrap();
+        for value in Reader::new(bytes.as_slice()).unwrap() {
+            let value = value.unwrap();
+            // IndexManifestEntrySerializer.toRow flattens IndexFileMeta after 
_BUCKET.
+            let fields = assert_record_field_order(
+                &value,
+                &[
+                    "_VERSION",
+                    "_KIND",
+                    "_PARTITION",
+                    "_BUCKET",
+                    "_INDEX_TYPE",
+                    "_FILE_NAME",
+                    "_FILE_SIZE",
+                    "_ROW_COUNT",
+                    "_DELETIONS_VECTORS_RANGES",
+                    "_EXTERNAL_PATH",
+                    "_GLOBAL_INDEX",
+                ],
+            );
+            if let Value::Union(_, ranges) = &fields[8].1 {
+                if let Value::Array(ranges) = ranges.as_ref() {
+                    assert_record_field_order(&ranges[0], &["f0", "f1", "f2", 
"_CARDINALITY"]);
+                }
+            }
+            if let Value::Union(_, global) = &fields[10].1 {
+                if !matches!(global.as_ref(), Value::Null) {
+                    assert_record_field_order(
+                        global,
+                        &[
+                            "_ROW_RANGE_START",
+                            "_ROW_RANGE_END",
+                            "_INDEX_FIELD_ID",
+                            "_EXTRA_FIELD_IDS",
+                            "_INDEX_META",
+                            "_SOURCE_META",
+                        ],
+                    );
+                }
+            }
+        }
+        assert_eq!(
+            from_avro_bytes::<IndexManifestEntry>(&bytes).unwrap(),
+            original
+        );
+        assert_eq!(
+            from_avro_bytes_fast::<IndexManifestEntry>(&bytes).unwrap(),
+            original
+        );
+    }
+
     /// The Avro manifest record carries `_WRITE_COLS_SEQUENCES`, so a 
populated value must
     /// survive a write/read round trip rather than being dropped on either 
side.
     #[test]

Reply via email to