use std::sync::Arc;
use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion};
use datafusion::common::{DFSchemaRef, plan_err};
use datafusion::datasource::source_as_provider;
use datafusion::error::Result;
use datafusion::logical_expr::{InvariantLevel, LogicalPlan, UserDefinedLogicalNodeCore};
use datafusion::physical_plan::ExecutionPlan;
use datafusion::prelude::Expr;
use datafusion::sql::unparser::Unparser;
use crate::ClickHouseConnectionPool;
use crate::dialect::ClickHouseDialect;
use crate::providers::table::ClickHouseTableProvider;
use crate::sql::ClickHouseSqlExec;
pub const CLICKHOUSE_FUNCTION_NODE_NAME: &str = "ClickHouseFunctionNode";
#[derive(Clone, Debug)]
pub struct ClickHouseFunctionNode {
pub(super) input: LogicalPlan,
schema: DFSchemaRef,
pool: Arc<ClickHouseConnectionPool>,
coerce_schema: bool,
}
impl ClickHouseFunctionNode {
pub fn try_new(input: LogicalPlan) -> Result<Self> {
let schema = Arc::clone(input.schema());
let mut coerce_schema = false;
let mut pool = None;
let _ = input
.apply(|plan| {
if let LogicalPlan::TableScan(scan) = plan {
let provider = source_as_provider(&scan.source)?;
if let Some(provider) =
provider.as_any().downcast_ref::<ClickHouseTableProvider>()
{
coerce_schema = provider.coerce_schema();
pool = Some(Arc::clone(provider.pool()));
return Ok(TreeNodeRecursion::Stop);
}
}
Ok(TreeNodeRecursion::Continue)
})
.unwrap();
let Some(pool) = pool else {
return plan_err!(
"ClickHouseFunctionNode: cannot execute without a connection pool, most likely a \
ClickHouseTableProvider was never found in the plan."
);
};
Ok(Self { input, schema, pool, coerce_schema })
}
pub(crate) fn execute(&self) -> Result<Arc<dyn ExecutionPlan>> {
let sql = Unparser::new(&ClickHouseDialect).plan_to_sql(&self.input)?.to_string();
ClickHouseSqlExec::try_new(None, self.input.schema().inner(), Arc::clone(&self.pool), sql)
.map(|ex| ex.with_coercion(self.coerce_schema))
.map(|ex| Arc::new(ex) as Arc<dyn ExecutionPlan>)
}
}
impl UserDefinedLogicalNodeCore for ClickHouseFunctionNode {
fn name(&self) -> &str { CLICKHOUSE_FUNCTION_NODE_NAME }
fn inputs(&self) -> Vec<&LogicalPlan> {
vec![]
}
fn schema(&self) -> &DFSchemaRef { &self.schema }
fn expressions(&self) -> Vec<Expr> { vec![] }
fn fmt_for_explain(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ClickHouseFunctionNode")
}
fn with_exprs_and_inputs(&self, exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> Result<Self> {
if !exprs.is_empty() {
return plan_err!("ClickHouseFunctionNode expects no expressions");
}
if !inputs.is_empty() {
return plan_err!("ClickHouseFunctionNode expects no inputs");
}
Ok(self.clone())
}
fn check_invariants(&self, _check: InvariantLevel) -> Result<()> {
Ok(())
}
fn necessary_children_exprs(&self, _output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
None
}
fn supports_limit_pushdown(&self) -> bool { false }
}
impl PartialEq for ClickHouseFunctionNode {
fn eq(&self, other: &Self) -> bool {
self.input == other.input
&& self.name() == other.name()
&& self.pool.join_push_down() == other.pool.join_push_down()
}
}
impl Eq for ClickHouseFunctionNode {}
impl std::hash::Hash for ClickHouseFunctionNode {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name().hash(state);
self.input.hash(state);
self.pool.join_push_down().hash(state);
}
}
impl PartialOrd for ClickHouseFunctionNode {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.input.partial_cmp(&other.input)
}
}
#[cfg(all(test, feature = "test-utils"))]
mod tests {
use datafusion::arrow::datatypes::Schema;
use datafusion::datasource::empty::EmptyTable;
use datafusion::datasource::provider_as_source;
use datafusion::logical_expr::LogicalPlanBuilder;
use datafusion::sql::TableReference;
use super::*;
#[test]
fn test_plan_node_requires_provider() {
let provider = Arc::new(EmptyTable::new(Arc::new(Schema::empty())));
let err_plan = LogicalPlanBuilder::scan(
TableReference::bare("test"),
provider_as_source(provider),
None,
)
.unwrap()
.build()
.unwrap();
let result = ClickHouseFunctionNode::try_new(err_plan);
assert!(result.is_err(), "ClickHouseTableProvider required for a ClickHouseFunctionNode");
}
#[cfg(feature = "mocks")]
#[test]
fn test_verify_no_exprs_input() {
use datafusion::common::Column;
let pool = Arc::new(ClickHouseConnectionPool::new("pool".to_string(), ()));
let provider = Arc::new(ClickHouseTableProvider::new_with_schema_unchecked(
Arc::clone(&pool),
"table1".into(),
Arc::new(Schema::empty()),
));
let plan = LogicalPlanBuilder::scan(
TableReference::bare("test"),
provider_as_source(provider),
None,
)
.unwrap()
.build()
.unwrap();
let node = ClickHouseFunctionNode::try_new(plan.clone()).unwrap();
let result =
node.with_exprs_and_inputs(vec![Expr::Column(Column::from_name("test_col"))], vec![]);
assert!(result.is_err(), "ClickHouseFunctionNode must take no exprs");
let result = node.with_exprs_and_inputs(vec![], vec![plan.clone()]);
assert!(result.is_err(), "ClickHouseFunctionNode must take no inputs");
assert!(
!node.supports_limit_pushdown(),
"ClickHouseFunctionNode does not support limit pushdown"
);
let other = ClickHouseFunctionNode::try_new(plan.clone()).unwrap();
assert_eq!(node, other, "PartialEq must be consistent");
assert_eq!(node.partial_cmp(&other), Some(std::cmp::Ordering::Equal), "Same node");
}
}