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 6ef1faaa fix(io): delete only the named directory in 
FileIO::delete_dir (#1135)
6ef1faaa is described below

commit 6ef1faaa1674974687cf11c97a131f206d633ffa
Author: jackylee <[email protected]>
AuthorDate: Fri Oct 16 12:10:33 2026 +0800

    fix(io): delete only the named directory in FileIO::delete_dir (#1135)
---
 .../datafusion/tests/rest_format_partition_sql.rs  | 27 +++++++++++
 crates/paimon/src/catalog/filesystem.rs            | 25 ++++++++++
 crates/paimon/src/io/file_io.rs                    | 55 +++++++++++++++++++++-
 crates/paimon/src/table/branch_manager.rs          | 17 +++++++
 4 files changed, 123 insertions(+), 1 deletion(-)

diff --git a/crates/integrations/datafusion/tests/rest_format_partition_sql.rs 
b/crates/integrations/datafusion/tests/rest_format_partition_sql.rs
index 12fabbfa..55a9c380 100644
--- a/crates/integrations/datafusion/tests/rest_format_partition_sql.rs
+++ b/crates/integrations/datafusion/tests/rest_format_partition_sql.rs
@@ -562,6 +562,33 @@ async fn 
test_drop_partition_leaves_a_custom_location_in_place() {
     assert!(temp_dir.path().join("dt=b").is_dir());
 }
 
+#[cfg(not(windows))]
+#[tokio::test]
+async fn test_drop_partition_deletes_the_directory_it_unregisters() {
+    let temp_dir = tempfile::tempdir().unwrap();
+    let (server, context) =
+        setup_rest_table(&temp_dir, format_table_schema(&[("dt", 
varchar())])).await;
+    // The two partitions differ only by a trailing space, which the deleted 
path must keep.
+    common::exec(
+        &context,
+        &format!("ALTER TABLE {TABLE_NAME} ADD PARTITION (dt = 'a') PARTITION 
(dt = 'a ')"),
+    )
+    .await;
+    assert!(temp_dir.path().join("dt=a ").is_dir());
+
+    common::exec(
+        &context,
+        &format!("ALTER TABLE {TABLE_NAME} DROP PARTITION (dt = 'a ')"),
+    )
+    .await;
+    assert_eq!(
+        server.table_partition_specs(DATABASE, TABLE),
+        vec![spec(&[("dt", "a")])]
+    );
+    assert!(temp_dir.path().join("dt=a").is_dir());
+    assert!(!temp_dir.path().join("dt=a ").exists());
+}
+
 #[cfg(not(windows))]
 #[tokio::test]
 async fn test_msck_repair_reconciles_registrations_with_directories() {
diff --git a/crates/paimon/src/catalog/filesystem.rs 
b/crates/paimon/src/catalog/filesystem.rs
index a95a8244..a957c500 100644
--- a/crates/paimon/src/catalog/filesystem.rs
+++ b/crates/paimon/src/catalog/filesystem.rs
@@ -989,6 +989,31 @@ mod tests {
         assert!(!catalog.database_exists("db1").await.unwrap());
     }
 
+    #[tokio::test]
+    async fn test_drop_table_keeps_tables_sharing_its_name_prefix() {
+        // The memory backend deletes recursively by key prefix, as S3 and OSS 
do.
+        let catalog = create_memory_catalog();
+        catalog
+            .create_database("db1", false, HashMap::new())
+            .await
+            .unwrap();
+        for name in ["orders", "orders_bak", "orders2024"] {
+            catalog
+                .create_table(&Identifier::new("db1", name), testing_schema(), 
false)
+                .await
+                .unwrap();
+        }
+
+        catalog
+            .drop_table(&Identifier::new("db1", "orders"), false)
+            .await
+            .unwrap();
+
+        let mut tables = catalog.list_tables("db1").await.unwrap();
+        tables.sort();
+        assert_eq!(tables, ["orders2024", "orders_bak"]);
+    }
+
     #[tokio::test]
     async fn test_create_database_should_reject_path_traversal_name() {
         let (temp_dir, catalog) = create_test_catalog();
diff --git a/crates/paimon/src/io/file_io.rs b/crates/paimon/src/io/file_io.rs
index d7e6ce19..5ac15c01 100644
--- a/crates/paimon/src/io/file_io.rs
+++ b/crates/paimon/src/io/file_io.rs
@@ -656,8 +656,12 @@ impl FileIO {
     /// Reference: 
<https://github.com/apache/paimon/blob/release-0.8.2/paimon-common/src/main/java/org/apache/paimon/fs/FileIO.java#L139>
     pub async fn delete_dir(&self, path: &str) -> Result<()> {
         let (op, relative_path, cache_path) = 
self.create_with_cache_path(path).await?;
+        // Without a trailing `/`, OpenDAL trims the path's surrounding 
whitespace,
+        // and a recursive delete it emulates by listing removes every key with
+        // this prefix.
+        let dir_path = normalize_root(relative_path.as_ref());
 
-        op.delete_with(relative_path.as_ref())
+        op.delete_with(&dir_path)
             .recursive(true)
             .await
             .map_err(|error| {
@@ -2044,6 +2048,55 @@ mod file_action_test {
             assert!(file_io.exists(&path).await.unwrap());
         });
     }
+
+    #[tokio::test]
+    async fn test_delete_dir_keeps_directories_sharing_its_prefix() {
+        // The memory backend deletes recursively by key prefix, as S3 and OSS 
do.
+        let file_io = setup_memory_file_io();
+        let partitions = ["month=1", "month=10", "month=2"];
+        for partition in partitions {
+            file_io
+                
.new_output(&format!("memory:/delete_dir_prefix/{partition}/data"))
+                .unwrap()
+                .write(Bytes::from("data"))
+                .await
+                .unwrap();
+        }
+
+        file_io
+            .delete_dir("memory:/delete_dir_prefix/month=1")
+            .await
+            .unwrap();
+
+        let mut kept = Vec::new();
+        for partition in partitions {
+            let path = format!("memory:/delete_dir_prefix/{partition}/data");
+            if file_io.exists(&path).await.unwrap() {
+                kept.push(partition);
+            }
+        }
+        assert_eq!(kept, ["month=10", "month=2"]);
+    }
+
+    // Windows drops trailing spaces from file names.
+    #[cfg(not(windows))]
+    #[tokio::test]
+    async fn test_delete_dir_keeps_the_name_without_its_trailing_space() {
+        let dir = tempdir().unwrap();
+        for name in ["t1", "t1 "] {
+            fs::create_dir(dir.path().join(name)).unwrap();
+            fs::write(dir.path().join(name).join("data"), b"data").unwrap();
+        }
+        let file_io = setup_fs_file_io();
+
+        file_io
+            .delete_dir(&local_file_path(&dir.path().join("t1 ")))
+            .await
+            .unwrap();
+
+        assert!(dir.path().join("t1").exists());
+        assert!(!dir.path().join("t1 ").exists());
+    }
 }
 
 #[cfg(all(
diff --git a/crates/paimon/src/table/branch_manager.rs 
b/crates/paimon/src/table/branch_manager.rs
index 489e224e..ca35ec4c 100644
--- a/crates/paimon/src/table/branch_manager.rs
+++ b/crates/paimon/src/table/branch_manager.rs
@@ -602,4 +602,21 @@ mod tests {
         assert!(!bm.branch_exists("branch2").await.unwrap());
         assert!(bm.branch_exists("branch3").await.unwrap());
     }
+
+    #[tokio::test]
+    async fn test_rename_branch_fallback_deletes_only_the_source_directory() {
+        // The memory backend has no native rename, so this takes the copy and
+        // recursive delete fallback that S3 and OSS take.
+        let file_io = test_file_io();
+        let table_path = "memory:/test_rename_fallback_exact".to_string();
+        let schema_manager = SchemaManager::new(file_io.clone(), 
table_path.clone());
+        write_schema(&file_io, &schema_manager, &test_schema()).await;
+
+        let bm = BranchManager::new(file_io, table_path);
+        bm.create_branch("b1").await.unwrap();
+        bm.create_branch("b10").await.unwrap();
+        bm.rename_branch("b1", "renamed").await.unwrap();
+
+        assert_eq!(bm.list_all().await.unwrap(), vec!["b10", "renamed"]);
+    }
 }

Reply via email to