#![cfg_attr(feature = "mocks", expect(clippy::unused_async))]
#![cfg_attr(feature = "mocks", expect(dead_code))]
#[cfg(feature = "mocks")]
mod mock;
use clickhouse_arrow::ArrowConnectionPoolBuilder;
#[cfg(not(feature = "mocks"))]
use clickhouse_arrow::{
ArrowConnectionManager, ArrowFormat, ClickHouseResponse, ConnectionManager,
Error as ClickhouseNativeError, bb8,
};
#[cfg(not(feature = "mocks"))]
use datafusion::arrow::array::RecordBatch;
use datafusion::arrow::datatypes::SchemaRef;
use datafusion::common::DataFusionError;
use datafusion::common::error::GenericError;
use datafusion::error::Result;
use datafusion::physical_plan::SendableRecordBatchStream;
#[cfg(not(feature = "mocks"))]
use datafusion::sql::TableReference;
use futures_util::TryStreamExt;
use tracing::{debug, error};
use crate::sql::JoinPushDown;
use crate::stream::{RecordBatchStreamWrapper, record_batch_stream_from_stream};
#[cfg(not(feature = "mocks"))]
pub type ArrowPoolConnection<'a> = bb8::PooledConnection<'a, ConnectionManager<ArrowFormat>>;
#[cfg(not(feature = "mocks"))]
pub type ArrowPool = bb8::Pool<ArrowConnectionManager>;
#[cfg(feature = "mocks")]
pub type ArrowPoolConnection<'a> = &'a ();
#[cfg(feature = "mocks")]
pub type ArrowPool = ();
#[derive(Debug, Clone)]
pub struct ClickHouseConnectionPool {
pool: ArrowPool,
join_push_down: JoinPushDown,
write_concurrency: usize,
}
impl ClickHouseConnectionPool {
pub fn new(identifier: impl Into<String>, pool: ArrowPool) -> Self {
debug!("Creating new ClickHouse connection pool");
let join_push_down = JoinPushDown::AllowedFor(identifier.into());
Self { pool, join_push_down, write_concurrency: 4 }
}
#[must_use]
pub fn with_write_concurrency(mut self, concurrency: usize) -> Self {
self.write_concurrency = concurrency;
self
}
pub fn write_concurrency(&self) -> usize { self.write_concurrency }
pub async fn from_pool_builder(builder: ArrowConnectionPoolBuilder) -> Result<Self> {
let identifer = builder.connection_identifier();
#[cfg(not(feature = "mocks"))]
let pool = builder
.configure_client(|c| c.with_database("default"))
.build()
.await
.inspect_err(|error| error!(?error, "Error building ClickHouse connection pool"))
.map_err(crate::utils::map_clickhouse_err)?;
#[cfg(feature = "mocks")]
let pool = ();
Ok(Self::new(identifer, pool))
}
pub fn pool(&self) -> &ArrowPool { &self.pool }
pub fn join_push_down(&self) -> JoinPushDown { self.join_push_down.clone() }
}
impl ClickHouseConnectionPool {
pub async fn connect(&self) -> Result<ClickHouseConnection<'_>> {
#[cfg(not(feature = "mocks"))]
let conn = self
.pool
.get()
.await
.inspect_err(|error| error!(?error, "Failed getting connection from pool"))
.map_err(crate::utils::map_external_err)?;
#[cfg(feature = "mocks")]
let conn = &();
Ok(ClickHouseConnection::new(conn))
}
pub async fn connect_static(&self) -> Result<ClickHouseConnection<'static>> {
#[cfg(not(feature = "mocks"))]
let conn = self
.pool
.get_owned()
.await
.inspect_err(|error| error!(?error, "Failed getting connection from pool"))
.map_err(crate::utils::map_external_err)?;
#[cfg(feature = "mocks")]
let conn = &();
Ok(ClickHouseConnection::new_static(conn))
}
}
#[derive(Debug)]
pub struct ClickHouseConnection<'a> {
conn: ArrowPoolConnection<'a>,
}
impl<'a> ClickHouseConnection<'a> {
pub fn new(conn: ArrowPoolConnection<'a>) -> Self { ClickHouseConnection { conn } }
pub fn new_static(conn: ArrowPoolConnection<'static>) -> Self { ClickHouseConnection { conn } }
pub async fn query_arrow_with_schema(
&self,
sql: &str,
params: &[()],
schema: SchemaRef,
coerce_schema: bool,
) -> Result<RecordBatchStreamWrapper, DataFusionError> {
debug!(sql, "Running query");
let batches = Box::pin(
self.query_arrow_raw(sql, params)
.await?
.map_err(|e| DataFusionError::External(Box::new(e))),
);
Ok(RecordBatchStreamWrapper::new_from_stream(batches, schema).with_coercion(coerce_schema))
}
pub async fn query_arrow(
&self,
sql: &str,
params: &[()],
projected_schema: Option<SchemaRef>,
) -> Result<SendableRecordBatchStream, GenericError> {
if let Some(schema) = projected_schema {
return Ok(Box::pin(self.query_arrow_with_schema(sql, params, schema, false).await?));
}
let batches = Box::pin(
self.query_arrow_raw(sql, params)
.await?
.map_err(|e| DataFusionError::External(Box::new(e))),
);
Ok(Box::pin(
record_batch_stream_from_stream(batches)
.await
.inspect_err(|error| error!(?error, "Failed converting batches to stream"))
.map_err(Box::new)?,
))
}
}
#[cfg(not(feature = "mocks"))]
impl ClickHouseConnection<'_> {
pub async fn tables(&self, schema: &str) -> Result<Vec<String>> {
debug!(schema, "Fetching tables");
self.conn
.fetch_tables(Some(schema), None)
.await
.inspect_err(|error| error!(?error, "Fetching tables failed"))
.map_err(crate::utils::map_clickhouse_err)
}
pub async fn schemas(&self) -> Result<Vec<String>> {
debug!("Fetching databases");
self.conn
.fetch_schemas(None)
.await
.inspect_err(|error| error!(?error, "Fetching databases failed"))
.map_err(crate::utils::map_clickhouse_err)
}
pub async fn get_schema(&self, table_reference: &TableReference) -> Result<SchemaRef> {
debug!(%table_reference, "Fetching schema for table");
let db = table_reference.schema();
let table = table_reference.table();
let mut schemas =
self.conn.fetch_schema(db, &[table][..], None).await.map_err(|error| {
if let ClickhouseNativeError::UndefinedTables { .. } = error {
error!(?error, ?db, ?table, "Tables undefined");
} else {
error!(?error, ?db, ?table, "Unknown error occurred while fetching schema");
}
crate::utils::map_clickhouse_err(error)
})?;
schemas
.remove(table)
.ok_or(DataFusionError::External("Schema not found for table".into()))
}
pub async fn query_arrow_raw(
&self,
sql: &str,
_params: &[()],
) -> Result<ClickHouseResponse<RecordBatch>> {
self.conn
.query(sql, None)
.await
.inspect(|_| tracing::trace!("Query executed successfully"))
.inspect_err(|error| error!(?error, "Failed running query"))
.map_err(|e| DataFusionError::External(Box::new(e)))
}
pub async fn execute(&self, sql: &str, _params: &[()]) -> Result<u64, GenericError> {
debug!(sql, "Executing query");
self.conn
.execute(sql, None)
.await
.inspect_err(|error| error!(?error, "Failed executing query"))
.map_err(Box::new)?;
Ok(0)
}
}
#[cfg(test)]
mod tests {
use datafusion::sql::TableReference;
use super::*;
#[test]
fn test_table_reference_schema_extraction() {
let table_ref = TableReference::full("catalog", "schema", "table");
assert_eq!(table_ref.schema(), Some("schema"));
assert_eq!(table_ref.table(), "table");
let partial_ref = TableReference::partial("schema", "table");
assert_eq!(partial_ref.schema(), Some("schema"));
assert_eq!(partial_ref.table(), "table");
let bare_ref = TableReference::bare("table");
assert_eq!(bare_ref.schema(), None);
assert_eq!(bare_ref.table(), "table");
}
#[test]
fn test_error_handling_patterns() {
use clickhouse_arrow::Error as ClickhouseNativeError;
use crate::utils::map_clickhouse_err;
let undefined_tables_error = ClickhouseNativeError::UndefinedTables {
db: "test_db".to_string(),
tables: vec!["test_table".to_string()],
};
let mapped_error = map_clickhouse_err(undefined_tables_error);
match mapped_error {
DataFusionError::External(boxed_error) => {
let error_str = boxed_error.to_string();
assert!(error_str.contains("Tables undefined"));
assert!(error_str.contains("test_db"));
assert!(error_str.contains("test_table"));
}
_ => panic!("Expected External error"),
}
}
#[test]
fn test_join_push_down_creation() {
use crate::sql::JoinPushDown;
let identifier = "test_pool";
let join_push_down = JoinPushDown::AllowedFor(identifier.to_string());
match join_push_down {
JoinPushDown::AllowedFor(id) => assert_eq!(id, "test_pool"),
JoinPushDown::Disallow => panic!("Expected AllowedFor variant"),
}
}
}