mod aggregate;
mod filter;
mod join;
mod vector;
use std::sync::{Arc, OnceLock};
use arrow::datatypes::{DataType, Schema, SchemaRef};
use datafusion::error::{DataFusionError, Result};
use datafusion::physical_expr::{EquivalenceProperties, Partitioning};
use datafusion::physical_plan::PlanProperties;
use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
use oxidelake_core::BackendKind;
use oxidelake_core::params::{Literal, Predicate};
use oxidelake_core::telemetry::{OperatorStats, TelemetryHub};
use oxidelake_device::GpuBackend;
pub use aggregate::{GpuAggregateExec, aggregate_output_schema};
pub use filter::GpuFilterExec;
pub use join::GpuHashJoinExec;
pub use vector::GpuVectorDistanceExec;
use crate::backend::local_backend;
use crate::operator::GpuOperator;
pub const MAX_PREDICATE_LEAVES: usize = 64;
#[derive(Debug, Clone)]
pub(crate) struct ExecConfig {
pub target: BackendKind,
pub backend: Option<Arc<dyn GpuBackend>>,
pub telemetry: Option<Arc<TelemetryHub>>,
stats: OnceLock<Arc<OperatorStats>>,
}
impl ExecConfig {
pub(crate) fn new(target: BackendKind) -> Self {
Self {
target,
backend: None,
telemetry: None,
stats: OnceLock::new(),
}
}
pub(crate) fn operator(&self, name: &str) -> Result<GpuOperator> {
let backend = match &self.backend {
Some(b) => Arc::clone(b),
None => local_backend()?,
};
let stats: Option<Arc<OperatorStats>> = self.telemetry.as_ref().map(|hub| {
Arc::clone(
self.stats
.get_or_init(|| hub.register_operator(name, self.target)),
)
});
Ok(GpuOperator::new(backend, stats)?)
}
}
pub(crate) fn plan_properties(
schema: SchemaRef,
partitioning: Partitioning,
emission: EmissionType,
) -> Arc<PlanProperties> {
Arc::new(PlanProperties::new(
EquivalenceProperties::new(schema),
partitioning,
emission,
Boundedness::Bounded,
))
}
pub(crate) fn plan_err<T>(msg: impl Into<String>) -> Result<T> {
Err(DataFusionError::Plan(msg.into()))
}
pub(crate) fn field_type<'a>(schema: &'a Schema, index: usize, what: &str) -> Result<&'a DataType> {
schema
.fields()
.get(index)
.map(|f| f.data_type())
.ok_or_else(|| {
DataFusionError::Plan(format!(
"{what}: column index {index} out of range for {schema}"
))
})
}
pub(crate) fn validate_predicate(schema: &Schema, predicate: &Predicate) -> Result<()> {
let leaves = predicate.leaf_count();
if leaves > MAX_PREDICATE_LEAVES {
return plan_err(format!(
"GpuFilterExec predicate has {leaves} comparisons; at most {MAX_PREDICATE_LEAVES} are supported"
));
}
validate_predicate_leaves(schema, predicate)
}
fn validate_predicate_leaves(schema: &Schema, predicate: &Predicate) -> Result<()> {
match predicate {
Predicate::Compare {
column, literal, ..
} => {
let dt = field_type(schema, *column, "GpuFilterExec predicate")?;
let expected = match literal {
Literal::Int64(_) => DataType::Int64,
Literal::Float64(_) => DataType::Float64,
};
if *dt != expected {
return plan_err(format!(
"GpuFilterExec predicate: column {column} has type {dt:?} but the literal is {expected:?}"
));
}
Ok(())
}
Predicate::And(l, r) => {
validate_predicate_leaves(schema, l)?;
validate_predicate_leaves(schema, r)
}
}
}