use std::sync::Arc;
use arrow::datatypes::{DataType, TimeUnit};
use datafusion::common::tree_node::{Transformed, TreeNode};
use datafusion::common::{Result, ScalarValue};
use datafusion::config::ConfigOptions;
use datafusion::logical_expr::{BinaryExpr, Expr, ExprSchemable, LogicalPlan, Operator};
use datafusion::optimizer::AnalyzerRule;
#[derive(Debug, Default)]
pub struct EpochLiteralRule;
impl AnalyzerRule for EpochLiteralRule {
fn name(&self) -> &str {
"chronix_epoch_literals"
}
fn analyze(&self, plan: LogicalPlan, _config: &ConfigOptions) -> Result<LogicalPlan> {
plan.transform_up_with_subqueries(|plan| {
let schema = Arc::clone(plan.schema());
plan.map_expressions(|expr| expr.transform_up(|e| rewrite_comparison(e, &schema)))
})
.map(|t| t.data)
}
}
fn rewrite_comparison(
expr: Expr,
schema: &datafusion::common::DFSchema,
) -> Result<Transformed<Expr>> {
match &expr {
Expr::Between(between) => {
let Some(unit) = time_unit_of(&between.expr, schema) else {
return Ok(Transformed::no(expr));
};
let low = as_integer(&between.low).map(|v| timestamp_scalar(v, unit));
let high = as_integer(&between.high).map(|v| timestamp_scalar(v, unit));
if low.is_none() && high.is_none() {
return Ok(Transformed::no(expr));
}
let mut rewritten = between.clone();
if let Some(low) = low {
rewritten.low = Box::new(Expr::Literal(low, None));
}
if let Some(high) = high {
rewritten.high = Box::new(Expr::Literal(high, None));
}
return Ok(Transformed::yes(Expr::Between(rewritten)));
}
Expr::InList(in_list) => {
let Some(unit) = time_unit_of(&in_list.expr, schema) else {
return Ok(Transformed::no(expr));
};
if !in_list.list.iter().any(|e| as_integer(e).is_some()) {
return Ok(Transformed::no(expr));
}
let mut rewritten = in_list.clone();
rewritten.list = in_list
.list
.iter()
.map(|e| match as_integer(e) {
Some(v) => Expr::Literal(timestamp_scalar(v, unit), None),
None => e.clone(),
})
.collect();
return Ok(Transformed::yes(Expr::InList(rewritten)));
}
_ => {}
}
let Expr::BinaryExpr(BinaryExpr { left, op, right }) = &expr else {
return Ok(Transformed::no(expr));
};
if !is_comparison(*op) {
return Ok(Transformed::no(expr));
}
let converted = match (time_unit_of(left, schema), as_integer(right)) {
(Some(unit), Some(v)) => Some(Expr::BinaryExpr(BinaryExpr {
left: left.clone(),
op: *op,
right: Box::new(Expr::Literal(timestamp_scalar(v, unit), None)),
})),
_ => match (as_integer(left), time_unit_of(right, schema)) {
(Some(v), Some(unit)) => Some(Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::Literal(timestamp_scalar(v, unit), None)),
op: *op,
right: right.clone(),
})),
_ => None,
},
};
Ok(match converted {
Some(e) => Transformed::yes(e),
None => Transformed::no(expr),
})
}
const fn is_comparison(op: Operator) -> bool {
matches!(
op,
Operator::Eq
| Operator::NotEq
| Operator::Lt
| Operator::LtEq
| Operator::Gt
| Operator::GtEq
)
}
fn time_unit_of(expr: &Expr, schema: &datafusion::common::DFSchema) -> Option<TimeUnit> {
match expr.get_type(schema) {
Ok(DataType::Timestamp(unit, _)) => Some(unit),
_ => None,
}
}
fn as_integer(expr: &Expr) -> Option<i64> {
let Expr::Literal(scalar, _) = expr else {
return None;
};
match scalar {
ScalarValue::Int64(Some(v)) => Some(*v),
ScalarValue::Int32(Some(v)) => Some(i64::from(*v)),
ScalarValue::Int16(Some(v)) => Some(i64::from(*v)),
ScalarValue::Int8(Some(v)) => Some(i64::from(*v)),
ScalarValue::UInt64(Some(v)) => i64::try_from(*v).ok(),
ScalarValue::UInt32(Some(v)) => Some(i64::from(*v)),
ScalarValue::UInt16(Some(v)) => Some(i64::from(*v)),
ScalarValue::UInt8(Some(v)) => Some(i64::from(*v)),
_ => None,
}
}
fn timestamp_scalar(value: i64, unit: TimeUnit) -> ScalarValue {
match unit {
TimeUnit::Second => ScalarValue::TimestampSecond(Some(value), None),
TimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(value), None),
TimeUnit::Microsecond => ScalarValue::TimestampMicrosecond(Some(value), None),
TimeUnit::Nanosecond => ScalarValue::TimestampNanosecond(Some(value), None),
}
}