use std::collections::HashMap;
use std::sync::Arc;
use clickhouse_arrow::prelude::ClickHouseEngine;
use clickhouse_arrow::{
ArrowConnectionManager, ArrowConnectionPoolBuilder, ArrowOptions, ArrowPoolBuilder,
ClientBuilder, CreateOptions, Destination,
};
use datafusion::arrow::datatypes::SchemaRef;
use datafusion::catalog::{CatalogProvider, TableProviderFactory};
use datafusion::common::{Constraints, DFSchema};
use datafusion::error::{DataFusionError, Result};
use datafusion::logical_expr::CreateExternalTable;
use datafusion::prelude::*;
use datafusion::sql::TableReference;
use tracing::{debug, error};
use crate::connection::ClickHouseConnectionPool;
use crate::providers::catalog::ClickHouseCatalogProvider;
use crate::providers::table_factory::ClickHouseTableFactory;
use crate::table_factory::ClickHouseTableProviderFactory;
use crate::utils::{self, ENDPOINT_PARAM, default_str_to_expr, register_builtins};
pub const DEFAULT_CLICKHOUSE_CATALOG: &str = "clickhouse";
pub fn default_arrow_options() -> ArrowOptions {
ArrowOptions::default()
.with_strings_as_strings(true)
.with_strict_schema(false)
.with_disable_strict_schema_ddl(true)
.with_nullable_array_default_empty(true)
}
pub struct ClickHouseBuilder {
endpoint: Destination,
pool_builder: ArrowConnectionPoolBuilder,
factory: ClickHouseTableProviderFactory,
write_concurrency: Option<usize>,
}
impl ClickHouseBuilder {
pub fn new(endpoint: impl Into<Destination>) -> Self {
let endpoint = endpoint.into();
let pool_builder = ArrowConnectionPoolBuilder::new(endpoint.clone())
.configure_client(|c| c.with_arrow_options(default_arrow_options()));
Self {
endpoint,
pool_builder,
factory: ClickHouseTableProviderFactory::new(),
write_concurrency: None,
}
}
pub fn new_with_pool_builder(
endpoint: impl Into<Destination>,
pool_builder: ArrowConnectionPoolBuilder,
) -> Self {
let endpoint = endpoint.into();
Self {
endpoint,
pool_builder,
factory: ClickHouseTableProviderFactory::new(),
write_concurrency: None,
}
}
#[must_use]
pub fn with_coercion(mut self, coerce: bool) -> Self {
self.factory = self.factory.with_coercion(coerce);
self
}
#[must_use]
pub fn with_write_concurrency(mut self, concurrency: usize) -> Self {
self.write_concurrency = Some(concurrency);
self
}
#[must_use]
pub fn configure_client(mut self, f: impl FnOnce(ClientBuilder) -> ClientBuilder) -> Self {
self.pool_builder = self.pool_builder.configure_client(f);
self
}
#[must_use]
pub fn configure_pool(mut self, f: impl FnOnce(ArrowPoolBuilder) -> ArrowPoolBuilder) -> Self {
self.pool_builder = self.pool_builder.configure_pool(f);
self
}
#[must_use]
pub fn configure_arrow_options(mut self, f: impl FnOnce(ArrowOptions) -> ArrowOptions) -> Self {
self.pool_builder = self.pool_builder.configure_client(|c| {
let options = c.options().ext.arrow.unwrap_or(default_arrow_options());
c.with_arrow_options(f(options))
});
self
}
pub async fn build_catalog_from_pool(
ctx: &SessionContext,
endpoint: impl Into<String>,
catalog: Option<&str>,
pool_identifier: impl Into<String>,
pool: clickhouse_arrow::bb8::Pool<ArrowConnectionManager>,
) -> Result<ClickHouseCatalogBuilder> {
register_builtins(ctx);
let catalog = catalog.unwrap_or(DEFAULT_CLICKHOUSE_CATALOG).to_string();
debug!(catalog, "Attaching pool to ClickHouse table factory");
let endpoint = endpoint.into();
let factory = ClickHouseTableProviderFactory::new();
let pool = Arc::new(factory.attach_pool(&endpoint, pool_identifier, pool));
ClickHouseCatalogBuilder::try_new(ctx, catalog, "", endpoint, pool, factory).await
}
pub async fn build_catalog(
self,
ctx: &SessionContext,
catalog: Option<&str>,
) -> Result<ClickHouseCatalogBuilder> {
register_builtins(ctx);
let catalog = catalog.unwrap_or(DEFAULT_CLICKHOUSE_CATALOG).to_string();
let database = self.pool_builder.client_options().default_database.clone();
debug!(catalog, database, "Attaching pool to ClickHouse table factory");
let endpoint = self.endpoint.to_string();
let factory = self.factory;
let mut pool = factory.attach_pool_builder(self.endpoint, self.pool_builder).await?;
if let Some(concurrency) = self.write_concurrency {
pool = pool.with_write_concurrency(concurrency);
}
let pool = Arc::new(pool);
ClickHouseCatalogBuilder::try_new(ctx, catalog, database, endpoint, pool, factory).await
}
}
#[derive(Clone)]
pub struct ClickHouseCatalogBuilder {
catalog: String,
schema: String,
endpoint: String,
pool: Arc<ClickHouseConnectionPool>,
factory: ClickHouseTableProviderFactory,
provider: Arc<ClickHouseCatalogProvider>,
}
impl ClickHouseCatalogBuilder {
async fn try_new(
ctx: &SessionContext,
catalog: impl Into<String>,
default_schema: impl Into<String>,
endpoint: impl Into<String>,
pool: Arc<ClickHouseConnectionPool>,
factory: ClickHouseTableProviderFactory,
) -> Result<Self> {
let schema = default_schema.into();
let catalog = catalog.into();
let endpoint = endpoint.into();
let schema = if schema.is_empty() { "default".to_string() } else { schema };
if schema != "default" {
debug!(schema, "Database not default, attempting create");
utils::create_database(&schema, &pool).await?;
}
let provider = if factory.coerce_schemas() {
ClickHouseCatalogProvider::try_new_with_coercion(Arc::clone(&pool)).await
} else {
ClickHouseCatalogProvider::try_new(Arc::clone(&pool)).await
}
.inspect_err(|error| error!(?error, "Failed to register catalog {catalog}"))?;
let provider = Arc::new(provider);
drop(
ctx.register_catalog(
catalog.as_str(),
Arc::clone(&provider) as Arc<dyn CatalogProvider>,
),
);
Ok(ClickHouseCatalogBuilder { catalog, schema, endpoint, pool, factory, provider })
}
pub fn name(&self) -> &str { &self.catalog }
pub fn schema(&self) -> &str { &self.schema }
pub async fn with_schema(mut self, name: impl Into<String>) -> Result<Self> {
let name = name.into();
if name == self.schema {
return Ok(self);
}
self.schema = name;
if self.schema != "default" {
debug!(schema = self.schema, "Database not default, attempting create");
utils::create_database(&self.schema, &self.pool).await?;
}
self.provider.refresh_catalog(&self.pool).await?;
Ok(self)
}
pub fn with_new_table(
&self,
name: impl Into<String>,
engine: impl Into<ClickHouseEngine>,
schema: SchemaRef,
) -> ClickHouseTableCreator {
let table = name.into();
let options = CreateOptions::new(engine.into().to_string());
debug!(schema = self.schema, table, ?options, "Initializing table creator");
ClickHouseTableCreator {
name: table,
builder: self.clone(),
options,
schema,
replace: false,
}
}
pub fn with_new_table_and_options(
&self,
name: impl Into<String>,
schema: SchemaRef,
options: CreateOptions,
) -> ClickHouseTableCreator {
let table = name.into();
debug!(schema = self.schema, table, ?options, "Initializing table creator");
ClickHouseTableCreator {
name: table,
builder: self.clone(),
options,
schema,
replace: false,
}
}
pub async fn register_existing_table(
&self,
name: impl Into<TableReference>,
name_as: Option<impl Into<TableReference>>,
ctx: &SessionContext,
) -> Result<()> {
let name = name.into();
let database = name.schema().unwrap_or(&self.schema);
let exists =
self.pool.connect().await?.tables(database).await?.contains(&name.table().to_string());
if !exists {
return Err(DataFusionError::Plan(format!(
"Table '{name}' does not exist in ClickHouse database '{database}', use \
`table_creator` instead"
)));
}
let table = TableReference::full(self.catalog.as_str(), database, name.table());
let table_as = name_as.map(Into::into).unwrap_or(table.clone());
let factory = ClickHouseTableFactory::new(Arc::clone(&self.pool));
let provider = factory.table_provider(table).await?;
debug!(?table_as, "Registering ClickHouse table provider");
drop(ctx.register_table(table_as, provider)?);
Ok(())
}
pub async fn build_schema(
mut self,
new_schema: Option<String>,
ctx: &SessionContext,
) -> Result<Self> {
let _catalog = self.build_internal(ctx).await?;
self.schema = new_schema.unwrap_or(self.schema);
Ok(self)
}
pub async fn build(&self, ctx: &SessionContext) -> Result<Arc<ClickHouseCatalogProvider>> {
#[cfg(feature = "federation")]
{
use datafusion::common::exec_err;
use crate::federation::FederatedContext as _;
if !ctx.is_federated() {
return exec_err!(
"Building this schema with federation enabled but no federated SessionContext \
will fail. Call `ctx.federate()` before providing a context to build with."
);
}
}
self.build_internal(ctx).await
}
async fn build_internal(&self, ctx: &SessionContext) -> Result<Arc<ClickHouseCatalogProvider>> {
let catalog = Arc::clone(&self.provider);
debug!(catalog = self.catalog, "ClickHouse catalog created");
register_builtins(ctx);
catalog.refresh_catalog(&self.pool).await?;
drop(ctx.register_catalog(&self.catalog, Arc::clone(&catalog) as Arc<dyn CatalogProvider>));
Ok(catalog)
}
}
#[derive(Clone)]
pub struct ClickHouseTableCreator {
name: String,
builder: ClickHouseCatalogBuilder,
schema: SchemaRef,
options: CreateOptions,
replace: bool,
}
impl ClickHouseTableCreator {
#[must_use]
pub fn update_create_options(
mut self,
update: impl Fn(CreateOptions) -> CreateOptions,
) -> Self {
self.options = update(self.options);
self
}
#[must_use]
pub fn set_or_replace(mut self, replace: bool) -> Self {
self.replace = replace;
self
}
pub async fn create(self, ctx: &SessionContext) -> Result<ClickHouseCatalogBuilder> {
let schema = self.builder.schema.clone();
let table = self.name;
let column_defaults = self
.options
.clone()
.defaults
.unwrap_or_default()
.into_iter()
.map(|(col, value)| (col, default_str_to_expr(&value)))
.collect::<HashMap<_, _>>();
let mut options = utils::create_options_to_params(self.options).into_params();
drop(options.insert(ENDPOINT_PARAM.into(), self.builder.endpoint.clone()));
let table_ref = TableReference::partial(schema.as_str(), table.as_str());
let cmd = CreateExternalTable {
name: table_ref.clone(),
schema: Arc::new(DFSchema::try_from(Arc::clone(&self.schema))?),
options,
column_defaults,
or_replace: self.replace,
constraints: Constraints::default(),
table_partition_cols: vec![],
if_not_exists: false,
location: String::new(),
file_type: String::new(),
temporary: false,
definition: None,
order_exprs: vec![],
unbounded: false,
};
let _provider = self
.builder
.factory
.create(&ctx.state(), &cmd)
.await
.inspect_err(|error| error!(?error, table, "Factory error creating table"))?;
debug!(table, "Table created, catalog will be refreshed in `build`");
drop(self.builder.build_internal(ctx).await?);
Ok(self.builder)
}
}