blob: b1cbbad8fa70c3c90d9404ac020e5d5940b20861 [file]
// 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::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use std::sync::RwLock;
use async_trait::async_trait;
use datafusion::arrow::datatypes::SchemaRef;
use datafusion::arrow::record_batch::RecordBatch;
use datafusion::catalog::{CatalogProvider, MemorySchemaProvider, SchemaProvider};
use datafusion::common::plan_datafusion_err;
use datafusion::datasource::MemTable;
use datafusion::datasource::TableProvider;
use datafusion::error::Result as DFResult;
use paimon::catalog::{Catalog, Identifier};
use crate::error::to_datafusion_error;
use crate::runtime::{await_with_runtime, block_on_with_runtime};
use crate::system_tables;
use crate::table::PaimonTableProvider;
use crate::DynamicOptions;
/// 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>,
/// Session-scoped dynamic options shared with the SQL context.
dynamic_options: DynamicOptions,
/// In-memory databases for temporary tables, keyed by database name.
temp_databases: RwLock<HashMap<String, Arc<MemorySchemaProvider>>>,
}
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`].
///
/// For standalone use without `SET`/`RESET` support.
/// When used via [`SQLContext`], the handler creates the provider
/// internally with shared dynamic options.
pub fn new(catalog: Arc<dyn Catalog>) -> Self {
PaimonCatalogProvider {
catalog,
dynamic_options: Default::default(),
temp_databases: RwLock::new(HashMap::new()),
}
}
pub(crate) fn with_dynamic_options(
catalog: Arc<dyn Catalog>,
dynamic_options: DynamicOptions,
) -> Self {
PaimonCatalogProvider {
catalog,
dynamic_options,
temp_databases: RwLock::new(HashMap::new()),
}
}
}
impl CatalogProvider for PaimonCatalogProvider {
fn as_any(&self) -> &dyn Any {
self
}
fn schema_names(&self) -> Vec<String> {
let catalog = Arc::clone(&self.catalog);
block_on_with_runtime(
async move {
match catalog.list_databases().await {
Ok(names) => names,
Err(e) => {
log::error!("failed to list databases: {e}");
vec![]
}
}
},
"paimon catalog access thread panicked",
)
}
fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
// First check temp_databases
let databases = self
.temp_databases
.read()
.unwrap_or_else(|e| e.into_inner());
if let Some(schema) = databases.get(name) {
return Some(Arc::clone(schema) as Arc<dyn SchemaProvider>);
}
let catalog = Arc::clone(&self.catalog);
let dynamic_options = Arc::clone(&self.dynamic_options);
let name = name.to_string();
block_on_with_runtime(
async move {
match catalog.get_database(&name).await {
Ok(_) => Some(Arc::new(PaimonSchemaProvider::new(
Arc::clone(&catalog),
name,
dynamic_options,
)) as Arc<dyn SchemaProvider>),
Err(paimon::Error::DatabaseNotExist { .. }) => None,
Err(e) => {
log::error!("failed to get database '{}': {e}", name);
None
}
}
},
"paimon catalog access thread panicked",
)
}
fn register_schema(
&self,
name: &str,
_schema: Arc<dyn SchemaProvider>,
) -> DFResult<Option<Arc<dyn SchemaProvider>>> {
let catalog = Arc::clone(&self.catalog);
let dynamic_options = Arc::clone(&self.dynamic_options);
let name = name.to_string();
block_on_with_runtime(
async move {
catalog
.create_database(&name, false, HashMap::new())
.await
.map_err(to_datafusion_error)?;
Ok(Some(Arc::new(PaimonSchemaProvider::new(
Arc::clone(&catalog),
name,
dynamic_options,
)) as Arc<dyn SchemaProvider>))
},
"paimon catalog access thread panicked",
)
}
fn deregister_schema(
&self,
name: &str,
cascade: bool,
) -> DFResult<Option<Arc<dyn SchemaProvider>>> {
let catalog = Arc::clone(&self.catalog);
let dynamic_options = Arc::clone(&self.dynamic_options);
let name = name.to_string();
block_on_with_runtime(
async move {
catalog
.drop_database(&name, false, cascade)
.await
.map_err(to_datafusion_error)?;
Ok(Some(Arc::new(PaimonSchemaProvider::new(
Arc::clone(&catalog),
name,
dynamic_options,
)) as Arc<dyn SchemaProvider>))
},
"paimon catalog access thread panicked",
)
}
}
impl PaimonCatalogProvider {
/// Creates or returns an existing temporary in-memory database under this catalog.
fn get_or_create_temp_database(&self, name: &str) -> Arc<MemorySchemaProvider> {
let mut databases = self
.temp_databases
.write()
.unwrap_or_else(|e| e.into_inner());
databases
.entry(name.to_string())
.or_insert_with(|| Arc::new(MemorySchemaProvider::new()))
.clone()
}
/// Registers a temporary table in the specified database.
/// Creates the database if it does not exist.
pub fn register_temp_table(
&self,
database: &str,
table_name: &str,
schema_ref: SchemaRef,
batches: Vec<RecordBatch>,
) -> DFResult<()> {
let mem_database = self.get_or_create_temp_database(database);
let mem_table = MemTable::try_new(schema_ref, vec![batches])?;
mem_database.register_table(table_name.to_string(), Arc::new(mem_table))?;
Ok(())
}
/// Deregisters a temporary table from the specified database.
pub fn deregister_temp_table(
&self,
database: &str,
table_name: &str,
) -> DFResult<Option<Arc<dyn TableProvider>>> {
let databases = self
.temp_databases
.read()
.unwrap_or_else(|e| e.into_inner());
let mem_database = databases
.get(database)
.ok_or_else(|| plan_datafusion_err!("Unknown temp database '{database}'"))?;
mem_database.deregister_table(table_name)
}
/// Returns whether a temp database exists with the given name.
pub fn has_temp_database(&self, name: &str) -> bool {
self.temp_databases
.read()
.unwrap_or_else(|e| e.into_inner())
.contains_key(name)
}
}
/// 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,
/// Session-scoped dynamic options shared with the SQL context.
dynamic_options: DynamicOptions,
}
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`] with shared dynamic options.
pub fn new(
catalog: Arc<dyn Catalog>,
database: String,
dynamic_options: DynamicOptions,
) -> Self {
PaimonSchemaProvider {
catalog,
database,
dynamic_options,
}
}
}
#[async_trait]
impl SchemaProvider for PaimonSchemaProvider {
fn as_any(&self) -> &dyn Any {
self
}
fn table_names(&self) -> Vec<String> {
let catalog = Arc::clone(&self.catalog);
let database = self.database.clone();
block_on_with_runtime(
async move {
match catalog.list_tables(&database).await {
Ok(names) => names,
Err(e) => {
log::error!("failed to list tables in '{}': {e}", database);
vec![]
}
}
},
"paimon catalog access thread panicked",
)
}
async fn table(&self, name: &str) -> DFResult<Option<Arc<dyn TableProvider>>> {
let (base, system_name) = system_tables::split_object_name(name);
if let Some(system_name) = system_name {
return await_with_runtime(system_tables::load(
Arc::clone(&self.catalog),
self.database.clone(),
base.to_string(),
system_name.to_string(),
))
.await;
}
let catalog = Arc::clone(&self.catalog);
let dynamic_options = Arc::clone(&self.dynamic_options);
let identifier = Identifier::new(self.database.clone(), base);
await_with_runtime(async move {
match catalog.get_table(&identifier).await {
Ok(table) => {
let opts = dynamic_options.read().unwrap().clone();
let table = if opts.is_empty() {
table
} else {
table.copy_with_options(opts)
};
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)),
}
})
.await
}
fn table_exist(&self, name: &str) -> bool {
let (base, system_name) = system_tables::split_object_name(name);
if let Some(system_name) = system_name {
if !system_tables::is_registered(system_name) {
return false;
}
}
let catalog = Arc::clone(&self.catalog);
let identifier = Identifier::new(self.database.clone(), base.to_string());
block_on_with_runtime(
async move {
match catalog.get_table(&identifier).await {
Ok(_) => true,
Err(paimon::Error::TableNotExist { .. }) => false,
Err(e) => {
log::error!("failed to check table '{}': {e}", identifier);
false
}
}
},
"paimon catalog access thread panicked",
)
}
fn register_table(
&self,
_name: String,
table: Arc<dyn TableProvider>,
) -> DFResult<Option<Arc<dyn TableProvider>>> {
// DataFusion calls register_table after table creation, so we just
// acknowledge it here.
Ok(Some(table))
}
fn deregister_table(&self, name: &str) -> DFResult<Option<Arc<dyn TableProvider>>> {
let catalog = Arc::clone(&self.catalog);
let identifier = Identifier::new(self.database.clone(), name);
block_on_with_runtime(
async move {
// Try to get the table first so we can return it.
let table = match catalog.get_table(&identifier).await {
Ok(t) => t,
Err(paimon::Error::TableNotExist { .. }) => return Ok(None),
Err(e) => return Err(to_datafusion_error(e)),
};
let provider = PaimonTableProvider::try_new(table)?;
catalog
.drop_table(&identifier, false)
.await
.map_err(to_datafusion_error)?;
Ok(Some(Arc::new(provider) as Arc<dyn TableProvider>))
},
"paimon catalog access thread panicked",
)
}
}