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 ae806e47 fix(spec): preserve manifest extra files (#848)
ae806e47 is described below

commit ae806e47582130aefd0e0cdf88c03130808eb4f3
Author: jianguotian <[email protected]>
AuthorDate: Thu Sep 17 08:08:23 2026 +0800

    fix(spec): preserve manifest extra files (#848)
---
 .../paimon/src/spec/avro/manifest_entry_decode.rs  |  2 +-
 .../src/spec/avro/manifest_file_meta_decode.rs     |  6 ++++
 crates/paimon/src/spec/manifest_file_meta.rs       | 31 +++++++++++++++-
 crates/paimon/src/spec/manifest_list.rs            |  4 ++-
 crates/paimon/src/spec/objects_file.rs             |  6 ++++
 crates/paimon/src/table/referenced_files.rs        | 41 ++++++++++++++++++++--
 6 files changed, 85 insertions(+), 5 deletions(-)

diff --git a/crates/paimon/src/spec/avro/manifest_entry_decode.rs 
b/crates/paimon/src/spec/avro/manifest_entry_decode.rs
index 0de57aae..b4af8a36 100644
--- a/crates/paimon/src/spec/avro/manifest_entry_decode.rs
+++ b/crates/paimon/src/spec/avro/manifest_entry_decode.rs
@@ -409,7 +409,7 @@ fn decode_nullable_string(
     Ok(Some(cursor.read_string()?.to_string()))
 }
 
-fn decode_nullable_string_array(
+pub(super) fn decode_nullable_string_array(
     cursor: &mut AvroCursor,
     nullable: bool,
 ) -> crate::Result<Option<Vec<String>>> {
diff --git a/crates/paimon/src/spec/avro/manifest_file_meta_decode.rs 
b/crates/paimon/src/spec/avro/manifest_file_meta_decode.rs
index fa5cd09e..6beff616 100644
--- a/crates/paimon/src/spec/avro/manifest_file_meta_decode.rs
+++ b/crates/paimon/src/spec/avro/manifest_file_meta_decode.rs
@@ -20,6 +20,7 @@ use super::decode::{neg_count_to_usize, AvroRecordDecode};
 use super::decode_helpers::{
     extract_record_schema, read_bytes_field, read_int_field, read_long_field, 
read_string_field,
 };
+use super::manifest_entry_decode::decode_nullable_string_array;
 use super::schema::{skip_nullable_field, FieldSchema, WriterSchema};
 use crate::spec::stats::BinaryTableStats;
 use crate::spec::ManifestFileMeta;
@@ -39,6 +40,7 @@ impl AvroRecordDecode for ManifestFileMeta {
         let mut max_level: Option<i32> = None;
         let mut min_row_id: Option<i64> = None;
         let mut max_row_id: Option<i64> = None;
+        let mut extra_files: Option<Vec<String>> = None;
 
         for field in &writer_schema.fields {
             match field.name.as_str() {
@@ -62,6 +64,9 @@ impl AvroRecordDecode for ManifestFileMeta {
                 "_MAX_LEVEL" => max_level = read_optional_int(cursor, 
field.nullable)?,
                 "_MIN_ROW_ID" => min_row_id = read_optional_long(cursor, 
field.nullable)?,
                 "_MAX_ROW_ID" => max_row_id = read_optional_long(cursor, 
field.nullable)?,
+                "_EXTRA_FILES" => {
+                    extra_files = decode_nullable_string_array(cursor, 
field.nullable)?
+                }
                 _ => skip_nullable_field(cursor, &field.schema, 
field.nullable)?,
             }
         }
@@ -80,6 +85,7 @@ impl AvroRecordDecode for ManifestFileMeta {
             max_level,
             min_row_id,
             max_row_id,
+            extra_files,
         ))
     }
 }
diff --git a/crates/paimon/src/spec/manifest_file_meta.rs 
b/crates/paimon/src/spec/manifest_file_meta.rs
index 540f4409..adcc95c6 100644
--- a/crates/paimon/src/spec/manifest_file_meta.rs
+++ b/crates/paimon/src/spec/manifest_file_meta.rs
@@ -103,6 +103,17 @@ pub struct ManifestFileMeta {
         skip_serializing_if = "Option::is_none"
     )]
     max_row_id: Option<i64>,
+
+    /// Files owned by this manifest and sharing its lifecycle.
+    ///
+    /// `None` preserves the distinction between legacy manifest lists (where 
the
+    /// field is absent) and an explicitly empty list written by a newer 
writer.
+    #[serde(
+        rename = "_EXTRA_FILES",
+        default,
+        skip_serializing_if = "Option::is_none"
+    )]
+    extra_files: Option<Vec<String>>,
 }
 
 impl ManifestFileMeta {
@@ -184,6 +195,12 @@ impl ManifestFileMeta {
         self.max_row_id
     }
 
+    /// Get files owned by this manifest, if the metadata was recorded.
+    #[inline]
+    pub fn extra_files(&self) -> Option<&[String]> {
+        self.extra_files.as_deref()
+    }
+
     /// Attach bucket / level statistics aggregated from manifest entries.
     ///
     /// Use this in writers that have access to the entries that the manifest 
covers.
@@ -214,6 +231,14 @@ impl ManifestFileMeta {
         self
     }
 
+    /// Attach files whose lifecycle is owned by this manifest.
+    #[inline]
+    #[must_use]
+    pub fn with_extra_files(mut self, extra_files: Option<Vec<String>>) -> 
Self {
+        self.extra_files = extra_files;
+        self
+    }
+
     #[inline]
     pub fn new(
         file_name: String,
@@ -237,6 +262,7 @@ impl ManifestFileMeta {
             max_level: None,
             min_row_id: None,
             max_row_id: None,
+            extra_files: None,
         }
     }
 
@@ -256,6 +282,7 @@ impl ManifestFileMeta {
         max_level: Option<i32>,
         min_row_id: Option<i64>,
         max_row_id: Option<i64>,
+        extra_files: Option<Vec<String>>,
     ) -> ManifestFileMeta {
         Self {
             version,
@@ -271,6 +298,7 @@ impl ManifestFileMeta {
             max_level,
             min_row_id,
             max_row_id,
+            extra_files,
         }
     }
 }
@@ -301,7 +329,8 @@ pub const MANIFEST_FILE_META_SCHEMA: &str = r#"["null", {
         {"name": "_MIN_LEVEL", "type": ["null", "int"], "default": null},
         {"name": "_MAX_LEVEL", "type": ["null", "int"], "default": null},
         {"name": "_MIN_ROW_ID", "type": ["null", "long"], "default": null},
-        {"name": "_MAX_ROW_ID", "type": ["null", "long"], "default": null}
+        {"name": "_MAX_ROW_ID", "type": ["null", "long"], "default": null},
+        {"name": "_EXTRA_FILES", "type": ["null", {"type": "array", "items": 
"string"}], "default": null}
     ]
 }]"#;
 
diff --git a/crates/paimon/src/spec/manifest_list.rs 
b/crates/paimon/src/spec/manifest_list.rs
index 6c59c684..638e8f8b 100644
--- a/crates/paimon/src/spec/manifest_list.rs
+++ b/crates/paimon/src/spec/manifest_list.rs
@@ -97,7 +97,8 @@ mod tests {
                 0,
                 BinaryTableStats::new(value_bytes.clone(), 
value_bytes.clone(), vec![Some(3)]),
                 1,
-            ),
+            )
+            .with_extra_files(Some(Vec::new())),
         ];
 
         ManifestList::write(&file_io, path, &original)
@@ -270,5 +271,6 @@ mod tests {
         assert_eq!(decoded[0].max_bucket(), None);
         assert_eq!(decoded[0].min_level(), None);
         assert_eq!(decoded[0].max_level(), None);
+        assert_eq!(decoded[0].extra_files(), None);
     }
 }
diff --git a/crates/paimon/src/spec/objects_file.rs 
b/crates/paimon/src/spec/objects_file.rs
index 56f2fe4d..78421b0a 100644
--- a/crates/paimon/src/spec/objects_file.rs
+++ b/crates/paimon/src/spec/objects_file.rs
@@ -199,6 +199,7 @@ mod tests {
                 "_MAX_LEVEL",
                 "_MIN_ROW_ID",
                 "_MAX_ROW_ID",
+                "_EXTRA_FILES",
             ],
         );
         assert_record_field_order(
@@ -228,12 +229,17 @@ mod tests {
             None,
             Some(100),
             Some(199),
+            Some(vec!["manifest-row-tracking-0.idx".to_string()]),
         )];
         let bytes = to_avro_bytes(MANIFEST_FILE_META_SCHEMA, 
&original).unwrap();
         let decoded = from_avro_bytes::<ManifestFileMeta>(&bytes).unwrap();
         assert_eq!(original, decoded);
         assert_eq!(decoded[0].min_row_id(), Some(100));
         assert_eq!(decoded[0].max_row_id(), Some(199));
+        assert_eq!(
+            decoded[0].extra_files(),
+            Some(["manifest-row-tracking-0.idx".to_string()].as_slice())
+        );
     }
 
     #[test]
diff --git a/crates/paimon/src/table/referenced_files.rs 
b/crates/paimon/src/table/referenced_files.rs
index d4bb617b..812cf566 100644
--- a/crates/paimon/src/table/referenced_files.rs
+++ b/crates/paimon/src/table/referenced_files.rs
@@ -19,7 +19,7 @@
 //!
 //! Reference: 
[LocalOrphanFilesClean](https://github.com/apache/paimon/blob/master/paimon-core/src/main/java/org/apache/paimon/operation/LocalOrphanFilesClean.java)
 
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::sync::Mutex;
 #[cfg(test)]
 use std::sync::{
@@ -407,6 +407,34 @@ async fn collect_snapshot_files(
             .or_insert(meta.file_size());
     }
 
+    // Manifest-owned sidecars live in the manifest directory and must remain
+    // referenced for exactly as long as their owner manifest. Java Paimon may
+    // add these files even when this Rust process only reads and rewrites the
+    // manifest list metadata.
+    let manifest_extra_names = all_manifest_metas
+        .iter()
+        .flat_map(|meta| meta.extra_files().unwrap_or_default())
+        .cloned()
+        .collect::<HashSet<_>>();
+    if !manifest_extra_names.is_empty() {
+        let manifest_extra_names = 
manifest_extra_names.into_iter().collect::<Vec<_>>();
+        let manifest_extra_paths = manifest_extra_names
+            .iter()
+            .map(|name| manifest_sm.manifest_path(name))
+            .collect::<Vec<_>>();
+        let sizes = try_join_all(
+            manifest_extra_paths
+                .iter()
+                .map(|path| try_stat_file_size(file_io, path)),
+        )
+        .await?;
+        for (name, size) in manifest_extra_names.into_iter().zip(sizes) {
+            if size > 0 {
+                file_set.manifest_files.entry(name).or_insert(size);
+            }
+        }
+    }
+
     // Read manifest files to get data file entries, using cache by full path
     let manifest_paths: Vec<String> = all_manifest_metas
         .iter()
@@ -953,6 +981,7 @@ mod tests {
         let external_dir = "memory:/external_sidecar_references";
         let sidecar_name = "data-0.row.index";
         let sidecar_content = "sidecar-bytes";
+        let manifest_sidecar_name = "manifest-external-sidecar-0.index";
         let file_io = test_file_io();
 
         file_io
@@ -969,6 +998,12 @@ mod tests {
             sidecar_content,
         )
         .await;
+        write_test_file(
+            &file_io,
+            &format!("{table_path}/manifest/{manifest_sidecar_name}"),
+            "manifest-sidecar-bytes",
+        )
+        .await;
 
         let manifest_name = "manifest-external-sidecar-0";
         let manifest_path = format!("{table_path}/manifest/{manifest_name}");
@@ -1009,7 +1044,8 @@ mod tests {
             0,
             BinaryTableStats::empty(),
             0,
-        );
+        )
+        .with_extra_files(Some(vec![manifest_sidecar_name.to_string()]));
         ManifestList::write(&file_io, &manifest_list_path, &[manifest_meta])
             .await
             .unwrap();
@@ -1040,6 +1076,7 @@ mod tests {
         let total = result.iter().find(|r| r.source == "total").unwrap();
         assert_eq!(total.data_file_count, 2);
         assert_eq!(total.data_file_size, 100 + sidecar_content.len() as i64);
+        assert_eq!(total.manifest_file_count, 4);
     }
 
     #[tokio::test]

Reply via email to