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 7c56827  fix(catalog): handle filesystem catalog existence on object 
stores (#528)
7c56827 is described below

commit 7c568274a4c5df6bd00e8d60edea21395412202b
Author: Pandas <[email protected]>
AuthorDate: Sat Jul 18 10:22:05 2026 +0800

    fix(catalog): handle filesystem catalog existence on object stores (#528)
---
 crates/paimon/src/catalog/filesystem.rs | 108 ++++++++++++++++++++++++++++++--
 crates/paimon/src/io/file_io.rs         |  28 +++++++++
 2 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/crates/paimon/src/catalog/filesystem.rs 
b/crates/paimon/src/catalog/filesystem.rs
index 2fe7bb7..b34d447 100644
--- a/crates/paimon/src/catalog/filesystem.rs
+++ b/crates/paimon/src/catalog/filesystem.rs
@@ -190,12 +190,19 @@ impl FileSystemCatalog {
 
     /// Check if a database exists.
     async fn database_exists(&self, name: &str) -> Result<bool> {
-        self.file_io.exists(&self.database_path(name)).await
+        self.file_io.exists_dir(&self.database_path(name)).await
     }
 
     /// Check if a table exists.
     async fn table_exists(&self, identifier: &Identifier) -> Result<bool> {
-        self.file_io.exists(&self.table_path(identifier)).await
+        let table_path = self.table_path(identifier);
+        let schema_manager = SchemaManager::new(self.file_io.clone(), 
table_path);
+
+        if self.file_io.exists(&schema_manager.schema_path(0)).await? {
+            return Ok(true);
+        }
+
+        Ok(!schema_manager.list_all_ids().await?.is_empty())
     }
 }
 
@@ -273,7 +280,7 @@ impl Catalog for FileSystemCatalog {
             return Ok(());
         }
 
-        let tables = self.list_directories(&path).await?;
+        let tables = self.list_tables(name).await?;
         if !tables.is_empty() && !cascade {
             return Err(Error::DatabaseNotEmpty {
                 database: name.to_string(),
@@ -322,7 +329,14 @@ impl Catalog for FileSystemCatalog {
             });
         }
 
-        self.list_directories(&path).await
+        let mut tables = Vec::new();
+        for table_name in self.list_directories(&path).await? {
+            let identifier = Identifier::new(database_name, &table_name);
+            if self.table_exists(&identifier).await? {
+                tables.push(table_name);
+            }
+        }
+        Ok(tables)
     }
 
     async fn create_table(
@@ -482,6 +496,12 @@ mod tests {
         (temp_dir, catalog)
     }
 
+    fn create_memory_catalog() -> FileSystemCatalog {
+        let mut options = Options::new();
+        options.set(CatalogOptions::WAREHOUSE, "memory:/warehouse");
+        FileSystemCatalog::new(options).unwrap()
+    }
+
     fn testing_schema() -> Schema {
         Schema::builder()
             .column(
@@ -552,6 +572,54 @@ mod tests {
         assert!(!catalog.database_exists("db1").await.unwrap());
     }
 
+    #[tokio::test]
+    async fn test_memory_database_directory_existence() {
+        let catalog = create_memory_catalog();
+
+        catalog
+            .create_database("empty", false, HashMap::new())
+            .await
+            .unwrap();
+        catalog.get_database("empty").await.unwrap();
+
+        let result = catalog
+            .create_database("empty", false, HashMap::new())
+            .await;
+        assert!(matches!(result, Err(Error::DatabaseAlreadyExist { .. })));
+
+        catalog
+            .file_io()
+            .new_output("memory:/warehouse/markerless.db/table/data/file")
+            .unwrap()
+            .write(Bytes::from("data"))
+            .await
+            .unwrap();
+        catalog.get_database("markerless").await.unwrap();
+        assert!(catalog
+            .list_databases()
+            .await
+            .unwrap()
+            .contains(&"markerless".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_drop_database_ignores_incomplete_table_directories() {
+        let catalog = create_memory_catalog();
+        catalog
+            .create_database("db1", false, HashMap::new())
+            .await
+            .unwrap();
+        catalog
+            .file_io()
+            .mkdirs("memory:/warehouse/db1.db/incomplete")
+            .await
+            .unwrap();
+
+        assert!(catalog.list_tables("db1").await.unwrap().is_empty());
+        catalog.drop_database("db1", false, false).await.unwrap();
+        assert!(!catalog.database_exists("db1").await.unwrap());
+    }
+
     #[tokio::test]
     async fn test_create_database_should_reject_path_traversal_name() {
         let (temp_dir, catalog) = create_test_catalog();
@@ -634,6 +702,38 @@ mod tests {
         assert!(!tables.contains(&"table1".to_string()));
     }
 
+    #[tokio::test]
+    async fn test_memory_table_existence_requires_schema() {
+        let catalog = create_memory_catalog();
+        let valid_table = Identifier::new("db1", "valid");
+        let invalid_table = Identifier::new("db1", "invalid");
+        let table_schema = TableSchema::new(1, &testing_schema());
+
+        catalog
+            .file_io()
+            .new_output("memory:/warehouse/db1.db/valid/schema/schema-1")
+            .unwrap()
+            .write(Bytes::from(serde_json::to_string(&table_schema).unwrap()))
+            .await
+            .unwrap();
+        catalog
+            .file_io()
+            .mkdirs("memory:/warehouse/db1.db/invalid")
+            .await
+            .unwrap();
+
+        assert!(catalog.table_exists(&valid_table).await.unwrap());
+        let table = catalog.get_table(&valid_table).await.unwrap();
+        assert_eq!(table.schema().id(), 1);
+        let tables = catalog.list_tables("db1").await.unwrap();
+        assert!(tables.contains(&"valid".to_string()));
+        assert!(!tables.contains(&"invalid".to_string()));
+
+        assert!(!catalog.table_exists(&invalid_table).await.unwrap());
+        let result = catalog.get_table(&invalid_table).await;
+        assert!(matches!(result, Err(Error::TableNotExist { .. })));
+    }
+
     #[tokio::test]
     async fn test_create_table_should_reject_path_traversal_object_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 4a62c73..1e7517b 100644
--- a/crates/paimon/src/io/file_io.rs
+++ b/crates/paimon/src/io/file_io.rs
@@ -215,6 +215,16 @@ impl FileIO {
             })
     }
 
+    /// Check if a directory exists.
+    pub async fn exists_dir(&self, path: &str) -> Result<bool> {
+        let (op, relative_path) = self.storage.create(path)?;
+        let dir_path = normalize_root(relative_path.as_ref());
+
+        op.exists(&dir_path).await.context(IoUnexpectedSnafu {
+            message: format!("Failed to check existence of directory 
'{path}'"),
+        })
+    }
+
     /// Delete a file.
     ///
     /// Reference: 
<https://github.com/apache/paimon/blob/release-0.8.2/paimon-common/src/main/java/org/apache/paimon/fs/FileIO.java#L139>
@@ -622,6 +632,24 @@ mod file_action_test {
         assert!(matches!(result, Err(Error::ConfigInvalid { .. })));
     }
 
+    #[tokio::test]
+    async fn test_exists_dir_memory() {
+        let file_io = setup_memory_file_io();
+
+        file_io.mkdirs("memory:/empty").await.unwrap();
+        assert!(file_io.exists_dir("memory:/empty").await.unwrap());
+
+        file_io
+            .new_output("memory:/markerless/child")
+            .unwrap()
+            .write(Bytes::from("data"))
+            .await
+            .unwrap();
+        assert!(file_io.exists_dir("memory:/markerless").await.unwrap());
+
+        assert!(!file_io.exists_dir("memory:/missing").await.unwrap());
+    }
+
     #[tokio::test]
     async fn test_memory_operator_reuse_across_file_io_calls() {
         let file_io = setup_memory_file_io();

Reply via email to