use linkrs_core::value::Value;
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct ValueHistogramBucket {
pub upper_bound: Value,
pub count: u64,
pub distinct_values: u64,
}
impl ValueHistogramBucket {
pub fn new(upper_bound: Value, count: u64, distinct_values: u64) -> Self {
Self {
upper_bound,
count,
distinct_values: distinct_values.max(1),
}
}
}
#[derive(Debug, Clone)]
pub enum RangeCondition {
Lt(Value),
Le(Value),
Gt(Value),
Ge(Value),
Range { low: Value, high: Value },
}
#[derive(Debug, Clone)]
pub struct ValueHistogram {
buckets: Vec<ValueHistogramBucket>,
null_fraction: f64,
total_distinct_values: u64,
total_count: u64,
last_updated: Instant,
}
impl ValueHistogram {
pub fn empty() -> Self {
Self {
buckets: Vec::new(),
null_fraction: 0.0,
total_distinct_values: 0,
total_count: 0,
last_updated: Instant::now(),
}
}
pub fn from_samples(samples: Vec<Value>, num_buckets: usize, total_count: u64) -> Self {
if samples.is_empty() || num_buckets == 0 {
return Self::empty();
}
let mut sorted_samples = samples;
let null_count = sorted_samples.iter().filter(|v| v.is_null()).count();
sorted_samples.retain(|v| !v.is_null());
if sorted_samples.is_empty() {
return Self {
buckets: Vec::new(),
null_fraction: null_count as f64 / total_count.max(1) as f64,
total_distinct_values: 0,
total_count,
last_updated: Instant::now(),
};
}
sorted_samples.sort_by(compare_values);
let distinct_values = calculate_distinct_values(&sorted_samples);
let non_null_count = sorted_samples.len();
let bucket_size = non_null_count / num_buckets;
let remainder = non_null_count % num_buckets;
let mut buckets = Vec::with_capacity(num_buckets);
let mut start_idx = 0;
for i in 0..num_buckets {
let current_bucket_size = bucket_size + if i < remainder { 1 } else { 0 };
if current_bucket_size == 0 {
continue;
}
let end_idx = (start_idx + current_bucket_size).min(sorted_samples.len());
let bucket_samples = &sorted_samples[start_idx..end_idx];
if let Some(upper_bound) = bucket_samples.last().cloned() {
let count = bucket_samples.len() as u64;
let bucket_distinct = calculate_distinct_values(bucket_samples);
buckets.push(ValueHistogramBucket::new(
upper_bound,
count,
bucket_distinct,
));
}
start_idx = end_idx;
}
Self {
buckets,
null_fraction: null_count as f64 / total_count.max(1) as f64,
total_distinct_values: distinct_values,
total_count,
last_updated: Instant::now(),
}
}
pub fn estimate_equality_selectivity(&self, value: &Value) -> f64 {
if value.is_null() {
return self.null_fraction;
}
if self.buckets.is_empty() {
return 0.1; }
match self.find_bucket(value) {
Some(bucket_idx) => {
let bucket = &self.buckets[bucket_idx];
let bucket_selectivity = 1.0 / bucket.distinct_values.max(1) as f64;
let bucket_ratio = bucket.count as f64 / self.total_count.max(1) as f64;
bucket_selectivity * bucket_ratio
}
None => {
1.0 / self.total_distinct_values.max(1) as f64
}
}
}
pub fn estimate_range_selectivity(&self, range: &RangeCondition) -> f64 {
match range {
RangeCondition::Lt(value) | RangeCondition::Le(value) => {
self.estimate_less_than_selectivity(value)
}
RangeCondition::Gt(value) | RangeCondition::Ge(value) => {
1.0 - self.estimate_less_than_selectivity(value)
}
RangeCondition::Range { low, high } => {
let high_selectivity = self.estimate_less_than_selectivity(high);
let low_selectivity = self.estimate_less_than_selectivity(low);
(high_selectivity - low_selectivity).max(0.0)
}
}
}
fn estimate_less_than_selectivity(&self, value: &Value) -> f64 {
if self.buckets.is_empty() {
return 0.333; }
let mut selectivity = 0.0;
let mut found = false;
for bucket in &self.buckets {
if compare_values(value, &bucket.upper_bound) != std::cmp::Ordering::Greater {
let bucket_ratio = bucket.count as f64 / self.total_count.max(1) as f64;
selectivity += bucket_ratio * 0.5; found = true;
break;
} else {
selectivity += bucket.count as f64 / self.total_count.max(1) as f64;
}
}
if !found {
selectivity = 1.0 - self.null_fraction;
}
selectivity.min(1.0 - self.null_fraction)
}
fn find_bucket(&self, value: &Value) -> Option<usize> {
for (idx, bucket) in self.buckets.iter().enumerate() {
if compare_values(value, &bucket.upper_bound) != std::cmp::Ordering::Greater {
return Some(idx);
}
}
None
}
pub fn bucket_count(&self) -> usize {
self.buckets.len()
}
pub fn null_fraction(&self) -> f64 {
self.null_fraction
}
pub fn total_count(&self) -> u64 {
self.total_count
}
pub fn distinct_values(&self) -> u64 {
self.total_distinct_values
}
pub fn last_updated(&self) -> Instant {
self.last_updated
}
}
fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
use std::cmp::Ordering;
fn as_i64(value: &Value) -> Option<i64> {
match value {
Value::SmallInt(v) => Some(*v as i64),
Value::Int(v) => Some(*v as i64),
Value::BigInt(v) => Some(*v),
_ => None,
}
}
fn as_f64(value: &Value) -> Option<f64> {
match value {
Value::SmallInt(v) => Some(*v as f64),
Value::Int(v) => Some(*v as f64),
Value::BigInt(v) => Some(*v as f64),
Value::Float(v) => Some(*v as f64),
Value::Double(v) => Some(*v),
_ => None,
}
}
if let (Some(x), Some(y)) = (as_i64(a), as_i64(b)) {
return x.cmp(&y);
}
if let (Some(x), Some(y)) = (as_f64(a), as_f64(b)) {
return x.partial_cmp(&y).unwrap_or(Ordering::Equal);
}
match (a, b) {
(Value::SmallInt(a), Value::SmallInt(b)) => a.cmp(b),
(Value::Int(a), Value::Int(b)) => a.cmp(b),
(Value::BigInt(a), Value::BigInt(b)) => a.cmp(b),
(Value::Float(a), Value::Float(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
(Value::Double(a), Value::Double(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
(Value::String(a), Value::String(b)) => a.cmp(b),
(Value::Bool(a), Value::Bool(b)) => a.cmp(b),
_ => Value::cmp(a, b),
}
}
fn calculate_distinct_values(values: &[Value]) -> u64 {
use std::collections::HashSet;
let mut seen = HashSet::new();
for value in values {
let key = format!("{:?}", value);
seen.insert(key);
}
seen.len() as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_histogram_empty() {
let hist = ValueHistogram::empty();
assert_eq!(hist.bucket_count(), 0);
assert_eq!(hist.null_fraction(), 0.0);
}
#[test]
fn test_histogram_from_samples() {
let samples: Vec<Value> = (1..=100).map(Value::Int).collect();
let hist = ValueHistogram::from_samples(samples, 10, 100);
assert_eq!(hist.bucket_count(), 10);
assert_eq!(hist.total_count(), 100);
}
#[test]
fn test_estimate_equality_selectivity() {
let samples: Vec<Value> = (1..=100).map(Value::Int).collect();
let hist = ValueHistogram::from_samples(samples, 10, 100);
let selectivity = hist.estimate_equality_selectivity(&Value::Int(50));
assert!(selectivity > 0.0 && selectivity < 0.1);
}
#[test]
fn test_estimate_range_selectivity() {
let samples: Vec<Value> = (1..=100).map(Value::Int).collect();
let hist = ValueHistogram::from_samples(samples, 10, 100);
let range = RangeCondition::Lt(Value::Int(50));
let selectivity = hist.estimate_range_selectivity(&range);
assert!(selectivity > 0.3 && selectivity < 0.7);
}
#[test]
fn test_null_handling() {
let mut samples: Vec<Value> = (1..=90).map(Value::Int).collect();
for _ in 0..10 {
samples.push(Value::Null(linkrs_core::value::NullType::Null));
}
let hist = ValueHistogram::from_samples(samples, 10, 100);
assert!((hist.null_fraction() - 0.1).abs() < 0.01);
let null_selectivity =
hist.estimate_equality_selectivity(&Value::Null(linkrs_core::value::NullType::Null));
assert!((null_selectivity - 0.1).abs() < 0.01);
}
}