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 aa60a6ac Honor data-file prefix in Python native writes (#932)
aa60a6ac is described below

commit aa60a6ac7cbe9299aca19effd9ba8301c1bc55da
Author: Jingsong Lee <[email protected]>
AuthorDate: Thu Sep 24 17:10:45 2026 +0800

    Honor data-file prefix in Python native writes (#932)
---
 bindings/python/src/table.rs                       |  5 ++
 bindings/python/tests/test_write.py                | 25 +++++++++
 crates/paimon/src/api/rest_api.rs                  |  4 +-
 crates/paimon/src/spec/core_options.rs             | 12 +++++
 crates/paimon/src/spec/manifest_file_meta.rs       | 15 +++---
 crates/paimon/src/spec/objects_file.rs             | 60 ++++++++--------------
 crates/paimon/src/spec/snapshot.rs                 |  8 +++
 crates/paimon/src/table/data_file_writer.rs        | 10 +++-
 crates/paimon/src/table/kv_file_writer.rs          |  4 +-
 crates/paimon/src/table/postpone_file_writer.rs    |  4 +-
 crates/paimon/src/table/rest_env.rs                |  5 ++
 crates/paimon/src/table/snapshot_commit.rs         | 18 +++++--
 crates/paimon/src/table/table_commit.rs            | 29 ++++++++++-
 .../paimon/src/table/table_commit/parity_tests.rs  |  2 +-
 .../src/table/table_commit/recovery_tests.rs       |  2 +-
 crates/paimon/src/table/table_write.rs             | 10 ++--
 crates/paimon/src/table/write_builder.rs           |  2 +-
 17 files changed, 151 insertions(+), 64 deletions(-)

diff --git a/bindings/python/src/table.rs b/bindings/python/src/table.rs
index 06c6316c..47701b5c 100644
--- a/bindings/python/src/table.rs
+++ b/bindings/python/src/table.rs
@@ -110,6 +110,11 @@ impl PyTable {
         self.inner.location().to_string()
     }
 
+    /// REST catalog table identity used by callers with an already resolved 
schema.
+    fn rest_table_uuid(&self) -> Option<&str> {
+        self.inner.rest_env().map(|env| env.uuid())
+    }
+
     fn schema(&self) -> PyTableSchema {
         PyTableSchema::new(self.inner.schema().clone())
     }
diff --git a/bindings/python/tests/test_write.py 
b/bindings/python/tests/test_write.py
index f36fbfdd..052a2c11 100644
--- a/bindings/python/tests/test_write.py
+++ b/bindings/python/tests/test_write.py
@@ -64,6 +64,31 @@ def test_write_commit_read_roundtrip():
         assert result == {"id": [1, 2, 3], "name": ["a", "b", "c"]}
 
 
+
[email protected]("primary_key", [False, True])
+def test_custom_data_file_prefix_matches_table_option(tmp_path, primary_key):
+    ctx = SQLContext()
+    ctx.register_catalog("paimon", {"warehouse": str(tmp_path)})
+    ctx.sql("CREATE SCHEMA paimon.wdb")
+    key = ", PRIMARY KEY (id)" if primary_key else ""
+    bucket = ", 'bucket' = '1'" if primary_key else ""
+    ctx.sql(
+        "CREATE TABLE paimon.wdb.t (id INT, name STRING{}) "
+        "WITH ('data-file.prefix' = 'custom-'{})".format(key, bucket)
+    )
+    table = _get_table(str(tmp_path))
+    builder = table.new_batch_write_builder()
+    writer = builder.new_write()
+    writer.write_arrow(_batch([1], ["a"]))
+    builder.new_commit().commit(writer.prepare_commit())
+
+    files = list(tmp_path.rglob("*.parquet"))
+    assert len(files) == 1
+    assert files[0].name.startswith("custom-")
+    assert pa.Table.from_batches(ctx.sql("SELECT id, name FROM 
paimon.wdb.t")).to_pydict() == {
+        "id": [1], "name": ["a"]}
+
+
 def test_write_multiple_batches():
     with tempfile.TemporaryDirectory() as warehouse:
         ctx = _make_empty_table(warehouse)
diff --git a/crates/paimon/src/api/rest_api.rs 
b/crates/paimon/src/api/rest_api.rs
index 34d76f46..e664ace9 100644
--- a/crates/paimon/src/api/rest_api.rs
+++ b/crates/paimon/src/api/rest_api.rs
@@ -1040,6 +1040,7 @@ impl RESTApi {
         &self,
         identifier: &Identifier,
         table_uuid: &str,
+        base_snapshot_uuid: Option<&str>,
         snapshot: &Snapshot,
         statistics: &[PartitionStatistics],
     ) -> Result<bool> {
@@ -1048,7 +1049,8 @@ impl RESTApi {
         validate_non_empty_multi(&[(database, "database name"), (table, "table 
name")])?;
         let path = self.resource_paths.commit_table(database, table);
         let request = serde_json::json!({
-            "tableUuid": table_uuid,
+            "tableId": table_uuid,
+            "baseSnapshotUuid": base_snapshot_uuid,
             "snapshot": snapshot,
             "statistics": statistics,
         });
diff --git a/crates/paimon/src/spec/core_options.rs 
b/crates/paimon/src/spec/core_options.rs
index 477b7891..86c28c1e 100644
--- a/crates/paimon/src/spec/core_options.rs
+++ b/crates/paimon/src/spec/core_options.rs
@@ -65,6 +65,7 @@ const COMMIT_MAX_RETRY_WAIT_OPTION: &str = 
"commit.max-retry-wait";
 const FILE_COMPRESSION_OPTION: &str = "file.compression";
 const FILE_COMPRESSION_ZSTD_LEVEL_OPTION: &str = "file.compression.zstd-level";
 const FILE_FORMAT_OPTION: &str = "file.format";
+const DATA_FILE_PREFIX_OPTION: &str = "data-file.prefix";
 const VECTOR_FILE_FORMAT_OPTION: &str = "vector.file.format";
 const VECTOR_TARGET_FILE_SIZE_OPTION: &str = "vector.target-file-size";
 const CHANGELOG_FILE_PREFIX_OPTION: &str = "changelog-file.prefix";
@@ -1347,6 +1348,14 @@ impl<'a> CoreOptions<'a> {
             .unwrap_or(1)
     }
 
+    /// File name prefix for data files. Default is `"data-"`.
+    pub fn data_file_prefix(&self) -> &str {
+        self.options
+            .get(DATA_FILE_PREFIX_OPTION)
+            .map(String::as_str)
+            .unwrap_or("data-")
+    }
+
     /// File name prefix for changelog files. Default is `"changelog-"`.
     pub fn changelog_file_prefix(&self) -> &str {
         self.options
@@ -2831,12 +2840,14 @@ mod tests {
         ]);
         let default_core = CoreOptions::new(&default_options);
 
+        assert_eq!(default_core.data_file_prefix(), "data-");
         assert_eq!(default_core.changelog_file_prefix(), "changelog-");
         assert_eq!(default_core.changelog_file_format(), "avro");
         assert_eq!(default_core.changelog_file_compression(), "snappy");
         assert_eq!(default_core.changelog_file_stats_mode(), None);
 
         let custom_options = HashMap::from([
+            (DATA_FILE_PREFIX_OPTION.to_string(), "files-".to_string()),
             (
                 CHANGELOG_FILE_PREFIX_OPTION.to_string(),
                 "custom-".to_string(),
@@ -2856,6 +2867,7 @@ mod tests {
         ]);
         let custom_core = CoreOptions::new(&custom_options);
 
+        assert_eq!(custom_core.data_file_prefix(), "files-");
         assert_eq!(custom_core.changelog_file_prefix(), "custom-");
         assert_eq!(custom_core.changelog_file_format(), "parquet");
         assert_eq!(custom_core.changelog_file_compression(), "zstd");
diff --git a/crates/paimon/src/spec/manifest_file_meta.rs 
b/crates/paimon/src/spec/manifest_file_meta.rs
index 0d37b714..c6ac3f26 100644
--- a/crates/paimon/src/spec/manifest_file_meta.rs
+++ b/crates/paimon/src/spec/manifest_file_meta.rs
@@ -104,11 +104,12 @@ pub struct ManifestFileMeta {
     )]
     max_row_id: Option<i64>,
 
-    /// Common positive bucket count recorded by an external manifest writer.
-    ///
-    /// Rust consumes this field for manifest pruning but intentionally does 
not
-    /// serialize it into manifest lists.
-    #[serde(rename = "_TOTAL_BUCKETS", default, skip_serializing)]
+    /// Common positive bucket count for entries in this manifest.
+    #[serde(
+        rename = "_TOTAL_BUCKETS",
+        default,
+        skip_serializing_if = "Option::is_none"
+    )]
     total_buckets: Option<i32>,
 
     /// Files owned by this manifest and sharing its lifecycle.
@@ -244,8 +245,7 @@ impl ManifestFileMeta {
         self
     }
 
-    /// Attach external manifest metadata in read-path tests.
-    #[cfg(test)]
+    /// Record a common positive bucket count for manifest pruning.
     #[inline]
     #[must_use]
     pub(crate) fn with_total_buckets(mut self, total_buckets: Option<i32>) -> 
Self {
@@ -355,6 +355,7 @@ pub const MANIFEST_FILE_META_SCHEMA: &str = r#"["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": "_TOTAL_BUCKETS", "type": ["null", "int"], "default": null},
         {"name": "_EXTRA_FILES", "type": ["null", {"type": "array", "items": 
"string"}], "default": null}
     ]
 }]"#;
diff --git a/crates/paimon/src/spec/objects_file.rs 
b/crates/paimon/src/spec/objects_file.rs
index 8f5bfaad..c30ecc8f 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",
+                "_TOTAL_BUCKETS",
                 "_EXTRA_FILES",
             ],
         );
@@ -244,9 +245,7 @@ mod tests {
     }
 
     #[test]
-    fn test_read_manifest_file_meta_total_buckets_without_writing_it() {
-        assert!(!MANIFEST_FILE_META_SCHEMA.contains("_TOTAL_BUCKETS"));
-
+    fn test_roundtrip_total_buckets_and_read_legacy_manifest_meta() {
         let original = vec![ManifestFileMeta::new(
             "manifest-java-0".to_string(),
             1024,
@@ -255,48 +254,29 @@ mod tests {
             BinaryTableStats::empty(),
             0,
         )
-        .with_bucket_level_stats(Some(2), Some(2), Some(0), Some(0))];
+        .with_bucket_level_stats(Some(2), Some(2), Some(0), Some(0))
+        .with_total_buckets(Some(8))];
         let bytes = to_avro_bytes(MANIFEST_FILE_META_SCHEMA, 
&original).unwrap();
-        let mut value = Reader::new(bytes.as_slice())
-            .unwrap()
-            .next()
-            .unwrap()
-            .unwrap();
-        let fields = match &mut value {
-            Value::Union(_, record) => match record.as_mut() {
-                Value::Record(fields) => fields,
-                other => panic!("Expected an Avro record, got {other:?}"),
-            },
-            other => panic!("Expected an Avro union, got {other:?}"),
-        };
-        let extra_files_index = fields
-            .iter()
-            .position(|(name, _)| name == "_EXTRA_FILES")
-            .unwrap();
-        fields.insert(
-            extra_files_index,
-            (
-                "_TOTAL_BUCKETS".to_string(),
-                Value::Union(1, Box::new(Value::Int(8))),
-            ),
-        );
+        let decoded = 
from_avro_bytes_fast::<ManifestFileMeta>(&bytes).unwrap();
+        assert_eq!(decoded[0].total_buckets(), Some(8));
 
-        let java_schema = MANIFEST_FILE_META_SCHEMA.replacen(
-            r#"{"name": "_EXTRA_FILES", "type": ["null", {"type": "array", 
"items": "string"}], "default": null}"#,
-            concat!(
-                r#"{"name": "_TOTAL_BUCKETS", "type": ["null", "int"], 
"default": null},"#,
-                "\n        ",
-                r#"{"name": "_EXTRA_FILES", "type": ["null", {"type": "array", 
"items": "string"}], "default": null}"#
-            ),
+        let legacy_schema = MANIFEST_FILE_META_SCHEMA.replacen(
+            "        {\"name\": \"_TOTAL_BUCKETS\", \"type\": [\"null\", 
\"int\"], \"default\": null},\n",
+            "",
             1,
         );
-        let schema = Schema::parse_str(&java_schema).unwrap();
-        let mut writer = Writer::new(&schema, Vec::new());
-        writer.append(value).unwrap();
-        let bytes = writer.into_inner().unwrap();
-
+        assert!(!legacy_schema.contains("_TOTAL_BUCKETS"));
+        let legacy = vec![ManifestFileMeta::new(
+            "manifest-legacy-0".to_string(),
+            1024,
+            5,
+            0,
+            BinaryTableStats::empty(),
+            0,
+        )];
+        let bytes = to_avro_bytes(&legacy_schema, &legacy).unwrap();
         let decoded = 
from_avro_bytes_fast::<ManifestFileMeta>(&bytes).unwrap();
-        assert_eq!(decoded[0].total_buckets(), Some(8));
+        assert_eq!(decoded[0].total_buckets(), None);
     }
 
     #[test]
diff --git a/crates/paimon/src/spec/snapshot.rs 
b/crates/paimon/src/spec/snapshot.rs
index 652d58a1..70b366db 100644
--- a/crates/paimon/src/spec/snapshot.rs
+++ b/crates/paimon/src/spec/snapshot.rs
@@ -54,6 +54,10 @@ impl std::fmt::Display for CommitKind {
 pub struct Snapshot {
     /// version of snapshot
     version: i32,
+    /// Unique identity for optimistic REST catalog publication.
+    #[builder(default = None)]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    uuid: Option<String>,
     id: i64,
     schema_id: i64,
     /// a manifest list recording all changes from the previous snapshots
@@ -118,6 +122,10 @@ pub struct Snapshot {
 }
 
 impl Snapshot {
+    pub fn uuid(&self) -> Option<&str> {
+        self.uuid.as_deref()
+    }
+
     /// Get the version of this snapshot.
     #[inline]
     pub fn version(&self) -> i32 {
diff --git a/crates/paimon/src/table/data_file_writer.rs 
b/crates/paimon/src/table/data_file_writer.rs
index b25c4a04..6039da60 100644
--- a/crates/paimon/src/table/data_file_writer.rs
+++ b/crates/paimon/src/table/data_file_writer.rs
@@ -30,7 +30,7 @@ use crate::io::FileIO;
 use crate::resource::ResourceContext;
 use crate::spec::data_file_to_file_index_file_name;
 use crate::spec::stats::BinaryTableStats;
-use crate::spec::{bucket_path_under, DataField, DataFileMeta, 
EMPTY_SERIALIZED_ROW};
+use crate::spec::{bucket_path_under, CoreOptions, DataField, DataFileMeta, 
EMPTY_SERIALIZED_ROW};
 use crate::Result;
 use arrow_array::RecordBatch;
 use chrono::Utc;
@@ -56,6 +56,7 @@ pub(crate) struct DataFileWriter {
     file_compression_zstd_level: i32,
     write_buffer_size: i64,
     file_format: String,
+    data_file_prefix: String,
     write_fields: Vec<DataField>,
     format_options: HashMap<String, String>,
     file_source: Option<i32>,
@@ -94,6 +95,9 @@ impl DataFileWriter {
         first_row_id: Option<i64>,
         write_cols: Option<Vec<String>>,
     ) -> Self {
+        let data_file_prefix = CoreOptions::new(&format_options)
+            .data_file_prefix()
+            .to_string();
         Self {
             file_io,
             table_location,
@@ -105,6 +109,7 @@ impl DataFileWriter {
             file_compression_zstd_level,
             write_buffer_size,
             file_format,
+            data_file_prefix,
             write_fields,
             format_options,
             file_source,
@@ -182,7 +187,8 @@ impl DataFileWriter {
             .map(|options| options.create_writer())
             .transpose()?;
         let file_name = format!(
-            "data-{}-{}.{}",
+            "{}{}-{}.{}",
+            self.data_file_prefix,
             uuid::Uuid::new_v4(),
             self.written_files.len(),
             self.file_format,
diff --git a/crates/paimon/src/table/kv_file_writer.rs 
b/crates/paimon/src/table/kv_file_writer.rs
index 8cc7efbf..4f2987bb 100644
--- a/crates/paimon/src/table/kv_file_writer.rs
+++ b/crates/paimon/src/table/kv_file_writer.rs
@@ -80,6 +80,7 @@ pub(crate) struct KeyValueWriteConfig {
     pub file_compression_zstd_level: i32,
     pub write_buffer_size: i64,
     pub file_format: String,
+    pub data_file_prefix: String,
     pub input_changelog: bool,
     pub changelog_file_prefix: String,
     pub changelog_file_compression: String,
@@ -324,7 +325,7 @@ impl KeyValueFileWriter {
                 data_seq.as_ref(),
                 &data_indices,
                 IndexedFileWrite {
-                    file_prefix: "data-",
+                    file_prefix: &self.config.data_file_prefix,
                     file_ordinal: self.written_files.len(),
                     file_format: &self.config.file_format,
                     file_compression: &self.config.file_compression,
@@ -1014,6 +1015,7 @@ mod tests {
             file_compression_zstd_level: 0,
             write_buffer_size: 1024,
             file_format: "parquet".to_string(),
+            data_file_prefix: "data-".to_string(),
             input_changelog: false,
             changelog_file_prefix: "changelog-".to_string(),
             changelog_file_compression: "none".to_string(),
diff --git a/crates/paimon/src/table/postpone_file_writer.rs 
b/crates/paimon/src/table/postpone_file_writer.rs
index c683a99c..0329a421 100644
--- a/crates/paimon/src/table/postpone_file_writer.rs
+++ b/crates/paimon/src/table/postpone_file_writer.rs
@@ -20,7 +20,7 @@
 //! Writes data in KV format (`_SEQUENCE_NUMBER`, `_VALUE_KIND` + user columns)
 //! but without sorting or deduplication — compaction assigns real buckets 
later.
 //!
-//! Uses a special file naming prefix: `data-u-{commitUser}-s-0-w-`.
+//! Uses a special file naming prefix: `data--u-{commitUser}-s-0-w-`.
 //!
 //! Reference: 
[PostponeBucketWriter](https://github.com/apache/paimon/blob/release-1.3/paimon-core/src/main/java/org/apache/paimon/table/sink/PostponeBucketWriter.java)
 
@@ -47,7 +47,7 @@ pub(crate) struct PostponeWriteConfig {
     pub file_compression_zstd_level: i32,
     pub write_buffer_size: i64,
     pub file_format: String,
-    /// Data file name prefix: `"data-u-{commitUser}-s-0-w-"`.
+    /// Data file name prefix: `"data--u-{commitUser}-s-0-w-"`.
     pub data_file_prefix: String,
 }
 
diff --git a/crates/paimon/src/table/rest_env.rs 
b/crates/paimon/src/table/rest_env.rs
index e814f966..6440536f 100644
--- a/crates/paimon/src/table/rest_env.rs
+++ b/crates/paimon/src/table/rest_env.rs
@@ -86,6 +86,11 @@ impl RESTEnv {
         &self.identifier
     }
 
+    /// Catalog identity used to reject commits against a replaced table.
+    pub fn uuid(&self) -> &str {
+        &self.uuid
+    }
+
     /// Load a table through the same REST catalog environment.
     pub async fn get_table(&self, identifier: &Identifier) -> Result<Table> {
         Self::load_table(
diff --git a/crates/paimon/src/table/snapshot_commit.rs 
b/crates/paimon/src/table/snapshot_commit.rs
index 9d141f79..942e6d77 100644
--- a/crates/paimon/src/table/snapshot_commit.rs
+++ b/crates/paimon/src/table/snapshot_commit.rs
@@ -36,8 +36,12 @@ use std::sync::Arc;
 pub trait SnapshotCommit: Send + Sync {
     /// Commit the given snapshot. Returns true if successful, false if
     /// another writer won the race.
-    async fn commit(&self, snapshot: &Snapshot, statistics: 
&[PartitionStatistics])
-        -> Result<bool>;
+    async fn commit(
+        &self,
+        base_snapshot_uuid: Option<&str>,
+        snapshot: &Snapshot,
+        statistics: &[PartitionStatistics],
+    ) -> Result<bool>;
 }
 
 /// A SnapshotCommit using file renaming to commit.
@@ -57,6 +61,7 @@ impl RenamingSnapshotCommit {
 impl SnapshotCommit for RenamingSnapshotCommit {
     async fn commit(
         &self,
+        _base_snapshot_uuid: Option<&str>,
         snapshot: &Snapshot,
         _statistics: &[PartitionStatistics],
     ) -> Result<bool> {
@@ -88,11 +93,18 @@ impl RESTSnapshotCommit {
 impl SnapshotCommit for RESTSnapshotCommit {
     async fn commit(
         &self,
+        base_snapshot_uuid: Option<&str>,
         snapshot: &Snapshot,
         statistics: &[PartitionStatistics],
     ) -> Result<bool> {
         self.api
-            .commit_snapshot(&self.identifier, &self.uuid, snapshot, 
statistics)
+            .commit_snapshot(
+                &self.identifier,
+                &self.uuid,
+                base_snapshot_uuid,
+                snapshot,
+                statistics,
+            )
             .await
     }
 }
diff --git a/crates/paimon/src/table/table_commit.rs 
b/crates/paimon/src/table/table_commit.rs
index b1d399b2..183f2926 100644
--- a/crates/paimon/src/table/table_commit.rs
+++ b/crates/paimon/src/table/table_commit.rs
@@ -1137,7 +1137,12 @@ impl TableCommit {
         };
         // Once publication starts its outcome may be unknown. These files must
         // remain available even if the response is lost or a later retry 
fails.
-        let publication_error = match self.snapshot_commit.commit(&snapshot, 
&statistics).await {
+        let base_snapshot_uuid = 
latest_snapshot.as_ref().and_then(Snapshot::uuid);
+        let publication_error = match self
+            .snapshot_commit
+            .commit(base_snapshot_uuid, &snapshot, &statistics)
+            .await
+        {
             Ok(true) => return Ok(CommitAttemptResult::Success),
             Ok(false) => None,
             Err(error) => Some(error),
@@ -1297,6 +1302,7 @@ impl TableCommit {
             .await?;
         let snapshot = Snapshot::builder()
             .version(3)
+            .uuid(Some(uuid::Uuid::new_v4().to_string()))
             .id(new_snapshot_id)
             .schema_id(schema_id)
             .base_manifest_list(base_manifest_list_name)
@@ -1525,6 +1531,8 @@ impl TableCommit {
         let mut min_row_id: Option<i64> = None;
         let mut max_row_id: Option<i64> = None;
         let mut all_entries_have_row_id = !entries.is_empty();
+        let mut total_buckets: Option<i32> = None;
+        let mut total_buckets_known = true;
         let mut schema_id = self.table.schema().id();
         for entry in entries {
             match entry.kind() {
@@ -1533,6 +1541,12 @@ impl TableCommit {
             }
             schema_id = schema_id.max(entry.file().schema_id);
             let b = entry.bucket();
+            let candidate = entry.total_buckets();
+            if candidate <= 0 || total_buckets.is_some_and(|value| value != 
candidate) {
+                total_buckets_known = false;
+            } else {
+                total_buckets = Some(candidate);
+            }
             min_bucket = Some(min_bucket.map_or(b, |cur| cur.min(b)));
             max_bucket = Some(max_bucket.map_or(b, |cur| cur.max(b)));
             let l = entry.file().level;
@@ -1561,6 +1575,11 @@ impl TableCommit {
             schema_id,
         )
         .with_bucket_level_stats(min_bucket, max_bucket, min_level, max_level)
+        .with_total_buckets(if total_buckets_known {
+            total_buckets
+        } else {
+            None
+        })
         .with_row_id_stats(min_row_id, max_row_id)
         .with_extra_files(sidecar_name.map(|name| vec![name])))
     }
@@ -6741,7 +6760,12 @@ mod tests {
         let table_path = "memory:/test_commit_bucket_level_stats";
         setup_dirs(&file_io, table_path).await;
 
-        let commit = setup_commit(&file_io, table_path);
+        let table = test_table_with_options(
+            &file_io,
+            table_path,
+            HashMap::from([("bucket".to_string(), "8".to_string())]),
+        );
+        let commit = TableCommit::new(table, "test-user".to_string());
 
         fn data_file_at_level(name: &str, level: i32) -> DataFileMeta {
             let mut f = test_data_file(name, 1);
@@ -6768,6 +6792,7 @@ mod tests {
         );
         assert_eq!(metas[0].min_bucket(), Some(0));
         assert_eq!(metas[0].max_bucket(), Some(3));
+        assert_eq!(metas[0].total_buckets(), Some(commit.total_buckets));
         assert_eq!(metas[0].min_level(), Some(0));
         assert_eq!(metas[0].max_level(), Some(2));
     }
diff --git a/crates/paimon/src/table/table_commit/parity_tests.rs 
b/crates/paimon/src/table/table_commit/parity_tests.rs
index 8799eddd..bdd9c4d2 100644
--- a/crates/paimon/src/table/table_commit/parity_tests.rs
+++ b/crates/paimon/src/table/table_commit/parity_tests.rs
@@ -307,7 +307,7 @@ struct LostResponseCommit {
 
 #[async_trait::async_trait]
 impl SnapshotCommit for LostResponseCommit {
-    async fn commit(&self, snapshot: &Snapshot, _: &[PartitionStatistics]) -> 
Result<bool> {
+    async fn commit(&self, _: Option<&str>, snapshot: &Snapshot, _: 
&[PartitionStatistics]) -> Result<bool> {
         let attempt = self.calls.fetch_add(1, 
std::sync::atomic::Ordering::SeqCst);
         if attempt == 0 {
             if self.publish_first {
diff --git a/crates/paimon/src/table/table_commit/recovery_tests.rs 
b/crates/paimon/src/table/table_commit/recovery_tests.rs
index 631a5ce9..b202f100 100644
--- a/crates/paimon/src/table/table_commit/recovery_tests.rs
+++ b/crates/paimon/src/table/table_commit/recovery_tests.rs
@@ -399,7 +399,7 @@ struct ConcurrentDvCommit {
 
 #[async_trait::async_trait]
 impl SnapshotCommit for ConcurrentDvCommit {
-    async fn commit(&self, snapshot: &Snapshot, _: &[PartitionStatistics]) -> 
Result<bool> {
+    async fn commit(&self, _: Option<&str>, snapshot: &Snapshot, _: 
&[PartitionStatistics]) -> Result<bool> {
         if self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) == 0 {
             let other = TableCommit::new(self.table.clone(), 
"concurrent".into());
             match self.change {
diff --git a/crates/paimon/src/table/table_write.rs 
b/crates/paimon/src/table/table_write.rs
index 54eb6375..ced9a781 100644
--- a/crates/paimon/src/table/table_write.rs
+++ b/crates/paimon/src/table/table_write.rs
@@ -136,6 +136,7 @@ pub struct TableWrite {
     file_compression_zstd_level: i32,
     write_buffer_size: i64,
     file_format: String,
+    data_file_prefix: String,
     primary_key_indices: Vec<usize>,
     primary_key_types: Vec<DataType>,
     sequence_field_indices: Vec<usize>,
@@ -251,6 +252,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 data_file_prefix = core_options.data_file_prefix().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();
@@ -419,6 +421,7 @@ impl TableWrite {
             file_compression_zstd_level,
             write_buffer_size,
             file_format,
+            data_file_prefix,
             primary_key_indices,
             primary_key_types,
             sequence_field_indices,
@@ -1118,7 +1121,7 @@ impl TableWrite {
 
     /// Create a postpone writer (KV format, no sorting/dedup, special file 
naming).
     fn create_postpone_writer(&self, partition_path: String, bucket: i32) -> 
FileWriter {
-        let data_file_prefix = format!("data-u-{}-s-0-w-", self.commit_user);
+        let data_file_prefix = format!("{}-u-{}-s-0-w-", 
self.data_file_prefix, self.commit_user);
         FileWriter::Postpone(
             PostponeFileWriter::new(
                 self.table.file_io().clone(),
@@ -1178,6 +1181,7 @@ impl TableWrite {
                     file_compression_zstd_level: 
self.file_compression_zstd_level,
                     write_buffer_size: self.write_buffer_size,
                     file_format: self.file_format.clone(),
+                    data_file_prefix: self.data_file_prefix.clone(),
                     input_changelog: self.changelog_producer == 
ChangelogProducer::Input
                         && !self.is_overwrite,
                     changelog_file_prefix: self.changelog_file_prefix.clone(),
@@ -4281,9 +4285,9 @@ pub(in crate::table) mod tests {
         let messages = table_write.prepare_commit().await.unwrap();
         let file = &messages[0].new_files[0];
 
-        // Verify postpone file naming: 
data-u-{commitUser}-s-{writeId}-w-{uuid}-{index}.parquet
+        // Verify postpone file naming: 
data--u-{commitUser}-s-{writeId}-w-{uuid}-{index}.parquet
         assert!(
-            file.file_name.starts_with("data-u-my-commit-user-s-"),
+            file.file_name.starts_with("data--u-my-commit-user-s-"),
             "Expected postpone file prefix, got: {}",
             file.file_name
         );
diff --git a/crates/paimon/src/table/write_builder.rs 
b/crates/paimon/src/table/write_builder.rs
index 07ff11e0..ea365600 100644
--- a/crates/paimon/src/table/write_builder.rs
+++ b/crates/paimon/src/table/write_builder.rs
@@ -443,7 +443,7 @@ mod tests {
         assert!(
             messages[0].new_files[0]
                 .file_name
-                .starts_with("data-u-my-commit-user-s-"),
+                .starts_with("data--u-my-commit-user-s-"),
             "Expected custom commit user in file name, got: {}",
             messages[0].new_files[0].file_name
         );

Reply via email to