use crate::ExecutionPlan;
use datafusion_common::{
Result, Statistics, assert_eq_or_internal_err, assert_or_internal_err,
};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
#[derive(Debug, Default)]
struct StatsCache(HashMap<(usize, Option<usize>), Arc<Statistics>>);
impl StatsCache {
fn get(
&self,
plan: &dyn ExecutionPlan,
partition: Option<usize>,
) -> Option<&Arc<Statistics>> {
let key = (
plan as *const dyn ExecutionPlan as *const () as usize,
partition,
);
self.0.get(&key)
}
fn insert(
&mut self,
plan: &dyn ExecutionPlan,
partition: Option<usize>,
stats: Arc<Statistics>,
) {
let key = (
plan as *const dyn ExecutionPlan as *const () as usize,
partition,
);
self.0.insert(key, stats);
}
}
#[derive(Debug, Default, Clone)]
pub struct StatisticsArgs {
partition: Option<usize>,
}
impl StatisticsArgs {
pub fn new() -> Self {
Default::default()
}
pub fn set_partition(&mut self, partition: Option<usize>) {
self.partition = partition;
}
pub fn with_partition(mut self, partition: Option<usize>) -> Self {
self.set_partition(partition);
self
}
pub fn partition(&self) -> Option<usize> {
self.partition
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChildStats {
At(Option<usize>),
Skip,
}
pub struct StatisticsContext {
cache: Rc<RefCell<StatsCache>>,
}
impl Default for StatisticsContext {
fn default() -> Self {
Self::new()
}
}
impl StatisticsContext {
pub fn new() -> Self {
Self {
cache: Rc::new(RefCell::new(StatsCache::default())),
}
}
pub fn reset_cache(&self) {
self.cache.borrow_mut().0.clear();
}
pub fn compute(
&self,
plan: &dyn ExecutionPlan,
args: &StatisticsArgs,
) -> Result<Arc<Statistics>> {
let partition = args.partition();
if let Some(idx) = partition {
let partition_count = plan.properties().partitioning.partition_count();
assert_or_internal_err!(
idx < partition_count,
"Invalid partition index: {}, the partition count is {}",
idx,
partition_count
);
}
if let Some(cached) = self.cache.borrow().get(plan, partition) {
return Ok(Arc::clone(cached));
}
let children = plan.children();
let requests = plan.child_stats_requests(partition);
assert_eq_or_internal_err!(
requests.len(),
children.len(),
"{} child_stats_requests returned {} entries for {} children",
plan.name(),
requests.len(),
children.len()
);
let child_stats = children
.iter()
.zip(requests)
.map(|(child, directive)| match directive {
ChildStats::At(p) => {
self.compute(child.as_ref(), &StatisticsArgs::new().with_partition(p))
}
ChildStats::Skip => {
Ok(Arc::new(Statistics::new_unknown(child.schema().as_ref())))
}
})
.collect::<Result<Vec<_>>>()?;
let result = plan.statistics_from_inputs(&child_stats, args)?;
self.cache
.borrow_mut()
.insert(plan, partition, Arc::clone(&result));
Ok(result)
}
}
#[cfg(all(test, feature = "test_utils"))]
mod tests {
use super::*;
use crate::coalesce_partitions::CoalescePartitionsExec;
use crate::test::exec::StatisticsExec;
use arrow::datatypes::{DataType, Field, Schema};
use datafusion_common::{ColumnStatistics, stats::Precision};
fn make_stats_leaf(num_rows: usize) -> Arc<dyn ExecutionPlan> {
let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
let col_stats = vec![ColumnStatistics {
null_count: Precision::Exact(0),
max_value: Precision::Absent,
min_value: Precision::Absent,
sum_value: Precision::Absent,
distinct_count: Precision::Absent,
byte_size: Precision::Absent,
}];
Arc::new(StatisticsExec::new(
Statistics {
num_rows: Precision::Exact(num_rows),
total_byte_size: Precision::Absent,
column_statistics: col_stats,
},
schema,
))
}
#[test]
fn coalesce_returns_overall_stats_for_any_partition() {
let leaf = make_stats_leaf(100);
let plan: Arc<dyn ExecutionPlan> = Arc::new(CoalescePartitionsExec::new(leaf));
let ctx = StatisticsContext::new();
let stats = ctx
.compute(
plan.as_ref(),
&StatisticsArgs::new().with_partition(Some(0)),
)
.unwrap();
assert_eq!(stats.num_rows, Precision::Exact(100));
let stats_none = ctx.compute(plan.as_ref(), &StatisticsArgs::new()).unwrap();
assert_eq!(stats_none.num_rows, Precision::Exact(100));
}
#[test]
fn context_caches_within_walk() {
let leaf = make_stats_leaf(42);
let ctx = StatisticsContext::new();
let args = StatisticsArgs::new();
let s1 = ctx.compute(leaf.as_ref(), &args).unwrap();
assert!(!ctx.cache.borrow().0.is_empty());
let s2 = ctx.compute(leaf.as_ref(), &args).unwrap();
assert!(Arc::ptr_eq(&s1, &s2));
}
#[test]
fn reset_cache_clears_entries() {
let leaf = make_stats_leaf(10);
let ctx = StatisticsContext::new();
let _ = ctx.compute(leaf.as_ref(), &StatisticsArgs::new()).unwrap();
assert!(!ctx.cache.borrow().0.is_empty());
ctx.reset_cache();
assert!(ctx.cache.borrow().0.is_empty());
}
}