This is an automated email from the ASF dual-hosted git repository.
lzljs3620320 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 6be17e8 feat: support catalog provider in datafusion (#176)
6be17e8 is described below
commit 6be17e859d656b412a1cb0dd7d336da4ebad0298
Author: yuxia Luo <[email protected]>
AuthorDate: Thu Apr 2 11:08:02 2026 +0800
feat: support catalog provider in datafusion (#176)
---
crates/integrations/datafusion/src/catalog.rs | 148 +++++++++++++++++++++
crates/integrations/datafusion/src/lib.rs | 2 +
.../integrations/datafusion/tests/read_tables.rs | 33 ++++-
crates/paimon/examples/rest_catalog_example.rs | 2 +-
4 files changed, 183 insertions(+), 2 deletions(-)
diff --git a/crates/integrations/datafusion/src/catalog.rs
b/crates/integrations/datafusion/src/catalog.rs
new file mode 100644
index 0000000..af5aa3d
--- /dev/null
+++ b/crates/integrations/datafusion/src/catalog.rs
@@ -0,0 +1,148 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Paimon catalog integration for DataFusion.
+
+use std::any::Any;
+use std::fmt::Debug;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use datafusion::catalog::{CatalogProvider, SchemaProvider};
+use datafusion::datasource::TableProvider;
+use datafusion::error::Result as DFResult;
+use paimon::catalog::{Catalog, Identifier};
+
+use crate::error::to_datafusion_error;
+use crate::table::PaimonTableProvider;
+
+/// Provides an interface to manage and access multiple schemas (databases)
+/// within a Paimon [`Catalog`].
+///
+/// This provider uses lazy loading - databases and tables are fetched
+/// on-demand from the catalog, ensuring data is always fresh.
+pub struct PaimonCatalogProvider {
+ /// Reference to the Paimon catalog.
+ catalog: Arc<dyn Catalog>,
+}
+
+impl Debug for PaimonCatalogProvider {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("PaimonCatalogProvider").finish()
+ }
+}
+
+impl PaimonCatalogProvider {
+ /// Creates a new [`PaimonCatalogProvider`].
+ ///
+ /// All data is loaded lazily when accessed.
+ pub fn new(catalog: Arc<dyn Catalog>) -> Self {
+ PaimonCatalogProvider { catalog }
+ }
+}
+
+impl CatalogProvider for PaimonCatalogProvider {
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn schema_names(&self) -> Vec<String> {
+ futures::executor::block_on(async {
+ self.catalog.list_databases().await.unwrap_or_default()
+ })
+ }
+
+ fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
+ futures::executor::block_on(async {
+ match self.catalog.get_database(name).await {
+ Ok(_) => Some(Arc::new(PaimonSchemaProvider::new(
+ self.catalog.clone(),
+ name.to_string(),
+ )) as Arc<dyn SchemaProvider>),
+ Err(paimon::Error::DatabaseNotExist { .. }) => None,
+ Err(_) => None,
+ }
+ })
+ }
+}
+
+/// Represents a [`SchemaProvider`] for the Paimon [`Catalog`], managing
+/// access to table providers within a specific database.
+///
+/// Tables are loaded lazily when accessed via the `table()` method.
+pub struct PaimonSchemaProvider {
+ /// Reference to the Paimon catalog.
+ catalog: Arc<dyn Catalog>,
+ /// Database name this schema represents.
+ database: String,
+}
+
+impl Debug for PaimonSchemaProvider {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("PaimonSchemaProvider")
+ .field("database", &self.database)
+ .finish()
+ }
+}
+
+impl PaimonSchemaProvider {
+ /// Creates a new [`PaimonSchemaProvider`] for the given database.
+ pub fn new(catalog: Arc<dyn Catalog>, database: String) -> Self {
+ PaimonSchemaProvider { catalog, database }
+ }
+}
+
+#[async_trait]
+impl SchemaProvider for PaimonSchemaProvider {
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn table_names(&self) -> Vec<String> {
+ // Use blocking call to fetch table names synchronously.
+ // This is acceptable as table name listing is lightweight.
+ futures::executor::block_on(async {
+ self.catalog
+ .list_tables(&self.database)
+ .await
+ .unwrap_or_default()
+ })
+ }
+
+ async fn table(&self, name: &str) -> DFResult<Option<Arc<dyn
TableProvider>>> {
+ let identifier = Identifier::new(self.database.clone(), name);
+ match self.catalog.get_table(&identifier).await {
+ Ok(table) => {
+ let provider = PaimonTableProvider::try_new(table)?;
+ Ok(Some(Arc::new(provider) as Arc<dyn TableProvider>))
+ }
+ Err(paimon::Error::TableNotExist { .. }) => Ok(None),
+ Err(e) => Err(to_datafusion_error(e)),
+ }
+ }
+
+ fn table_exist(&self, name: &str) -> bool {
+ let identifier = Identifier::new(self.database.clone(), name);
+ futures::executor::block_on(async {
+ match self.catalog.get_table(&identifier).await {
+ Ok(_) => true,
+ Err(paimon::Error::TableNotExist { .. }) => false,
+ Err(_) => false,
+ }
+ })
+ }
+}
diff --git a/crates/integrations/datafusion/src/lib.rs
b/crates/integrations/datafusion/src/lib.rs
index 6dd5272..e9f8955 100644
--- a/crates/integrations/datafusion/src/lib.rs
+++ b/crates/integrations/datafusion/src/lib.rs
@@ -35,11 +35,13 @@
//!
//! This version does not support write or predicate pushdown.
+mod catalog;
mod error;
mod physical_plan;
mod schema;
mod table;
+pub use catalog::{PaimonCatalogProvider, PaimonSchemaProvider};
pub use error::to_datafusion_error;
pub use physical_plan::PaimonTableScan;
pub use schema::paimon_schema_to_arrow;
diff --git a/crates/integrations/datafusion/tests/read_tables.rs
b/crates/integrations/datafusion/tests/read_tables.rs
index 2ac12dc..be43ded 100644
--- a/crates/integrations/datafusion/tests/read_tables.rs
+++ b/crates/integrations/datafusion/tests/read_tables.rs
@@ -18,10 +18,11 @@
use std::sync::Arc;
use datafusion::arrow::array::{Int32Array, StringArray};
+use datafusion::catalog::CatalogProvider;
use datafusion::prelude::SessionContext;
use paimon::catalog::Identifier;
use paimon::{Catalog, CatalogOptions, FileSystemCatalog, Options};
-use paimon_datafusion::PaimonTableProvider;
+use paimon_datafusion::{PaimonCatalogProvider, PaimonTableProvider};
fn get_test_warehouse() -> String {
std::env::var("PAIMON_TEST_WAREHOUSE").unwrap_or_else(|_|
"/tmp/paimon-warehouse".to_string())
@@ -213,3 +214,33 @@ async fn
test_scan_partition_count_respects_session_config() {
"target_partitions=1 should coalesce all splits into exactly 1
partition"
);
}
+
+// ======================= Catalog Provider Tests =======================
+#[tokio::test]
+async fn test_query_via_catalog_provider() {
+ let catalog = create_catalog();
+ let provider = PaimonCatalogProvider::new(Arc::new(catalog));
+
+ let ctx = SessionContext::new();
+ ctx.register_catalog("paimon", Arc::new(provider));
+
+ let df = ctx
+ .sql("SELECT id, name FROM paimon.default.simple_log_table")
+ .await
+ .expect("Failed to execute query");
+
+ let batches = df.collect().await.expect("Failed to collect results");
+ let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
+ assert_eq!(total_rows, 3, "Expected 3 rows from simple_log_table");
+}
+
+#[tokio::test]
+async fn test_missing_database_returns_no_schema() {
+ let catalog = create_catalog();
+ let provider = PaimonCatalogProvider::new(Arc::new(catalog));
+
+ assert!(
+ provider.schema("definitely_missing_database").is_none(),
+ "missing databases should not resolve to a schema provider"
+ );
+}
diff --git a/crates/paimon/examples/rest_catalog_example.rs
b/crates/paimon/examples/rest_catalog_example.rs
index e03f269..cca3cbd 100644
--- a/crates/paimon/examples/rest_catalog_example.rs
+++ b/crates/paimon/examples/rest_catalog_example.rs
@@ -105,7 +105,7 @@ async fn main() {
let catalog = match CatalogFactory::create(options).await {
Ok(catalog) => catalog,
Err(err) => {
- eprintln!("Failed to create catalog: {err}");
+ eprintln!("Failed to create RESTCatalog: {err}");
return;
}
};