use datafusion::common::stats::Precision;
use datafusion::physical_plan::ExecutionPlan;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BuildSideEstimate {
pub rows: Option<usize>,
pub bytes: Option<usize>,
}
fn value(p: &Precision<usize>) -> Option<usize> {
match p {
Precision::Exact(v) | Precision::Inexact(v) => Some(*v),
Precision::Absent => None,
}
}
impl BuildSideEstimate {
pub(crate) fn of(build: &Arc<dyn ExecutionPlan>) -> Self {
match build.partition_statistics(None) {
Ok(stats) => Self {
rows: value(&stats.num_rows),
bytes: value(&stats.total_byte_size),
},
Err(_) => Self::UNKNOWN,
}
}
pub(crate) const UNKNOWN: Self = Self {
rows: None,
bytes: None,
};
pub(crate) fn is_unknown(&self) -> bool {
self.rows.is_none() && self.bytes.is_none()
}
pub(crate) fn any_claims_empty(&self) -> bool {
self.rows == Some(0) || self.bytes == Some(0)
}
pub(crate) fn is_wholly_degenerate(&self) -> bool {
let non_positive = |v: Option<usize>| !matches!(v, Some(n) if n > 0);
non_positive(self.rows) && non_positive(self.bytes) && self.any_claims_empty()
}
pub(crate) fn bytes_implied_by_rows(&self, schema: &arrow::datatypes::Schema) -> Option<usize> {
if self.bytes.is_some() {
return None;
}
let rows = self.rows.filter(|r| *r > 0)?;
Some(rows.saturating_mul(estimated_row_width(schema)))
}
}
pub(crate) const ASSUMED_VARLEN_COLUMN_BYTES: usize = 32;
pub(crate) fn estimated_row_width(schema: &arrow::datatypes::Schema) -> usize {
schema
.fields()
.iter()
.map(|f| {
f.data_type()
.primitive_width()
.unwrap_or(ASSUMED_VARLEN_COLUMN_BYTES)
})
.sum::<usize>()
.max(1)
}
#[cfg(test)]
mod tests {
use super::*;
fn estimate(rows: Option<usize>, bytes: Option<usize>) -> BuildSideEstimate {
BuildSideEstimate { rows, bytes }
}
#[test]
fn an_explicit_double_zero_is_degenerate_for_both_rules() {
let q21 = estimate(Some(0), Some(0));
assert!(q21.is_wholly_degenerate(), "broadcast must override this");
assert!(q21.any_claims_empty(), "spill must be pessimistic here");
assert!(!q21.is_unknown(), "a zero is a claim, not an absence");
}
#[test]
fn a_large_positive_row_estimate_is_never_degenerate() {
let q8 = estimate(Some(4_000_000), None);
assert!(
!q8.is_wholly_degenerate(),
"a plausible multi-million-row estimate must be left alone; the \
ceiling is DataFusion's decision, made with these same numbers"
);
assert!(!q8.any_claims_empty());
}
#[test]
fn a_wholly_absent_estimate_is_unknown_not_degenerate() {
let nothing = estimate(None, None);
assert!(nothing.is_unknown());
assert!(
!nothing.is_wholly_degenerate(),
"absent must not be read as empty — that is what made the broadcast \
override and the spill rule disagree about the same plan"
);
assert!(!nothing.any_claims_empty());
}
#[test]
fn zero_rows_with_positive_bytes_splits_the_two_readings() {
let incoherent = estimate(Some(0), Some(500));
assert!(
incoherent.any_claims_empty(),
"spill must not trust a zero row count"
);
assert!(
!incoherent.is_wholly_degenerate(),
"broadcast must defer: DataFusion had a positive byte estimate"
);
}
}