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 52ec8673 fix(spec): normalize file-format options to lowercase (#663)
52ec8673 is described below

commit 52ec8673d4cc01f065911ea8c540c70405a34074
Author: jackylee <[email protected]>
AuthorDate: Tue Aug 4 13:33:07 2026 +0800

    fix(spec): normalize file-format options to lowercase (#663)
---
 crates/paimon/src/spec/core_options.rs           | 70 +++++++++++++++++++++---
 crates/paimon/src/table/data_evolution_writer.rs |  4 +-
 crates/paimon/src/table/format_table_scan.rs     |  2 +-
 crates/paimon/src/table/table_write.rs           |  2 +-
 4 files changed, 64 insertions(+), 14 deletions(-)

diff --git a/crates/paimon/src/spec/core_options.rs 
b/crates/paimon/src/spec/core_options.rs
index 20216368..dfb64911 100644
--- a/crates/paimon/src/spec/core_options.rs
+++ b/crates/paimon/src/spec/core_options.rs
@@ -974,11 +974,15 @@ impl<'a> CoreOptions<'a> {
     ///
     /// Java leaves this unset by default. When present, vector columns are
     /// written to files named `*.vector.<format>`.
-    pub fn vector_file_format(&self) -> Option<&str> {
+    /// Dedicated vector file format, normalized to lowercase.
+    ///
+    /// Mirrors Java `CoreOptions.normalizeFileFormat`, which lowercases every
+    /// file-format option so the value can be used directly in a file name.
+    pub fn vector_file_format(&self) -> Option<String> {
         self.options
             .get(VECTOR_FILE_FORMAT_OPTION)
-            .map(String::as_str)
-            .filter(|format| !format.trim().is_empty())
+            .map(|format| format.trim().to_ascii_lowercase())
+            .filter(|format| !format.is_empty())
     }
 
     pub fn vector_target_file_size(&self) -> i64 {
@@ -990,11 +994,12 @@ impl<'a> CoreOptions<'a> {
 
     /// File format for data files (e.g. "parquet", "orc", "avro", "vortex").
     /// Default is "parquet".
-    pub fn file_format(&self) -> &str {
+    pub fn file_format(&self) -> String {
         self.options
             .get(FILE_FORMAT_OPTION)
-            .map(String::as_str)
-            .unwrap_or("parquet")
+            .map(|format| format.trim().to_ascii_lowercase())
+            .filter(|format| !format.is_empty())
+            .unwrap_or_else(|| "parquet".to_string())
     }
 
     /// File compression codec (e.g. "lz4", "zstd", "snappy", "none").
@@ -1027,10 +1032,11 @@ impl<'a> CoreOptions<'a> {
     ///
     /// When `changelog-file.format` is not configured, Java Paimon falls back
     /// to the table `file.format`.
-    pub fn changelog_file_format(&self) -> &str {
+    pub fn changelog_file_format(&self) -> String {
         self.options
             .get(CHANGELOG_FILE_FORMAT_OPTION)
-            .map(String::as_str)
+            .map(|format| format.trim().to_ascii_lowercase())
+            .filter(|format| !format.is_empty())
             .unwrap_or_else(|| self.file_format())
     }
 
@@ -1638,6 +1644,52 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_file_formats_are_normalized_to_lowercase() {
+        // Java routes every file-format option through
+        // `CoreOptions.normalizeFileFormat`, which lowercases it. The value 
ends
+        // up in the data file name, so an uppercase option would otherwise 
write
+        // `data-<uuid>-0.PARQUET` where Java writes `.parquet`.
+        for raw in ["PARQUET", "Parquet", " parquet ", "parquet"] {
+            let options = HashMap::from([(FILE_FORMAT_OPTION.to_string(), 
raw.to_string())]);
+            assert_eq!(
+                CoreOptions::new(&options).file_format(),
+                "parquet",
+                "file.format = {raw:?} should normalize to \"parquet\""
+            );
+        }
+
+        let options = HashMap::from([(
+            CHANGELOG_FILE_FORMAT_OPTION.to_string(),
+            " AVRO ".to_string(),
+        )]);
+        assert_eq!(CoreOptions::new(&options).changelog_file_format(), "avro");
+
+        let options =
+            HashMap::from([(VECTOR_FILE_FORMAT_OPTION.to_string(), 
"VORTEX".to_string())]);
+        assert_eq!(
+            CoreOptions::new(&options).vector_file_format().as_deref(),
+            Some("vortex")
+        );
+    }
+
+    #[test]
+    fn test_file_format_defaults_and_blank_handling() {
+        let empty = HashMap::new();
+        assert_eq!(CoreOptions::new(&empty).file_format(), "parquet");
+        // `changelog-file.format` falls back to `file.format`, still 
normalized.
+        let options = HashMap::from([(FILE_FORMAT_OPTION.to_string(), 
"ORC".to_string())]);
+        assert_eq!(CoreOptions::new(&options).changelog_file_format(), "orc");
+
+        // A blank value is not a format: fall back rather than return "".
+        for raw in ["", "   "] {
+            let options = HashMap::from([(FILE_FORMAT_OPTION.to_string(), 
raw.to_string())]);
+            assert_eq!(CoreOptions::new(&options).file_format(), "parquet");
+            let options = 
HashMap::from([(VECTOR_FILE_FORMAT_OPTION.to_string(), raw.to_string())]);
+            assert_eq!(CoreOptions::new(&options).vector_file_format(), None);
+        }
+    }
+
     #[test]
     fn test_parse_memory_size() {
         assert_eq!(parse_memory_size("1024"), Some(1024));
@@ -1950,7 +2002,7 @@ mod tests {
             ),
         ]);
         let custom_core = CoreOptions::new(&custom_options);
-        assert_eq!(custom_core.vector_file_format(), Some("vortex"));
+        assert_eq!(custom_core.vector_file_format().as_deref(), 
Some("vortex"));
         assert_eq!(custom_core.vector_target_file_size(), 64 * 1024 * 1024);
     }
 
diff --git a/crates/paimon/src/table/data_evolution_writer.rs 
b/crates/paimon/src/table/data_evolution_writer.rs
index 47752e3f..5e16524f 100644
--- a/crates/paimon/src/table/data_evolution_writer.rs
+++ b/crates/paimon/src/table/data_evolution_writer.rs
@@ -980,9 +980,7 @@ impl DataEvolutionPartialWriter {
         fields: &[DataField],
         core_options: &CoreOptions<'_>,
     ) -> Result<Vec<PartialWriteSet>> {
-        let vector_file_format = core_options
-            .vector_file_format()
-            .map(|format| format.trim().to_ascii_lowercase());
+        let vector_file_format = core_options.vector_file_format();
         let mut normal_fields = Vec::new();
         let mut normal_columns = Vec::new();
         let mut normal_indices = Vec::new();
diff --git a/crates/paimon/src/table/format_table_scan.rs 
b/crates/paimon/src/table/format_table_scan.rs
index 4190526a..d3dbff36 100644
--- a/crates/paimon/src/table/format_table_scan.rs
+++ b/crates/paimon/src/table/format_table_scan.rs
@@ -65,7 +65,7 @@ impl<'a> FormatTableScan<'a> {
 
     async fn plan_inner(&self, trace: Option<&mut ScanTrace>) -> 
crate::Result<Plan> {
         let core_options = CoreOptions::new(self.table.schema().options());
-        let format_extension = 
supported_format_table_extension(core_options.file_format())?;
+        let format_extension = 
supported_format_table_extension(&core_options.file_format())?;
         let schema_id = self.table.schema().id();
         let table_path = core_options
             .path()
diff --git a/crates/paimon/src/table/table_write.rs 
b/crates/paimon/src/table/table_write.rs
index 490de635..bb152a67 100644
--- a/crates/paimon/src/table/table_write.rs
+++ b/crates/paimon/src/table/table_write.rs
@@ -208,7 +208,7 @@ impl TableWrite {
         let file_compression = core_options.file_compression().to_string();
         let file_compression_zstd_level = 
core_options.file_compression_zstd_level();
         let file_format = core_options.file_format().to_string();
-        let vector_file_format = 
core_options.vector_file_format().map(str::to_string);
+        let vector_file_format = core_options.vector_file_format();
         let changelog_file_prefix = 
core_options.changelog_file_prefix().to_string();
         let changelog_file_format = 
core_options.changelog_file_format().to_string();
         let changelog_file_compression = 
core_options.changelog_file_compression().to_string();

Reply via email to