use std::cmp::Ordering;
use rudb_common::{Error, LogicalType, Result, Value};
use crate::compare::order;
use crate::quantile::{Column, Whole};
#[derive(Debug, Clone)]
pub(crate) struct Binned {
bins: Option<Vec<Value>>,
counts: Vec<u64>,
exact: bool,
key: LogicalType,
plain: Plain,
}
#[derive(Debug, Clone)]
enum Plain {
Neither,
Wholes(Whole, Vec<i64>),
Reals(Vec<f64>),
}
impl Binned {
pub(crate) const fn new(exact: bool, key: LogicalType) -> Self {
Self { bins: None, counts: Vec::new(), exact, key, plain: Plain::Neither }
}
pub(crate) fn update(&mut self, value: &Value, bins: Option<&Value>) -> Result<()> {
if self.bins.is_none() {
let fixed = boundaries(bins)?;
self.counts = vec![0; fixed.len() + 1];
self.plain = plain(&fixed);
self.bins = Some(fixed);
}
let bins = self.bins.as_deref().unwrap_or_default();
let at = lower_bound(bins, value)?;
let at = if self.exact && at < bins.len() && !equal(&bins[at], value)? {
bins.len()
} else {
at
};
self.counts[at] += 1;
Ok(())
}
pub(crate) fn push_column(&mut self, column: Column<'_>, row: usize) -> bool {
let at = match (&self.plain, column) {
(Plain::Wholes(kind, bins), Column::Wholes(theirs, numbers)) if *kind == theirs => {
let n = numbers.at(row);
let at = bins.partition_point(|&bin| bin < n);
if self.exact && bins.get(at) != Some(&n) { bins.len() } else { at }
}
(Plain::Reals(bins), Column::Reals(reals)) => {
let real = reals[row];
let at = bins.partition_point(|&bin| bin < real);
#[expect(clippy::float_cmp, reason = "an exact bin takes only an equal value")]
let missed = self.exact && bins.get(at).is_none_or(|&bin| bin != real);
if missed { bins.len() } else { at }
}
_ => return false,
};
self.counts[at] += 1;
true
}
pub(crate) fn combine(&mut self, other: &Self) -> Result<()> {
let Some(theirs) = &other.bins else { return Ok(()) };
let Some(bins) = &self.bins else {
self.bins = Some(theirs.clone());
self.counts.clone_from(&other.counts);
return Ok(());
};
if bins != theirs {
return Err(Error::not_implemented(
"Histogram - cannot combine histograms with different bin boundaries. Bin \
boundaries must be the same for all histograms within the same group",
));
}
for (count, more) in self.counts.iter_mut().zip(&other.counts) {
*count += more;
}
Ok(())
}
pub(crate) fn finish(&self) -> Value {
let Some(bins) = &self.bins else { return Value::Null };
let mut entries: Vec<(Value, Value)> = bins
.iter()
.zip(&self.counts)
.map(|(bin, &count)| (bin.clone(), Value::UBigInt(count)))
.collect();
let others = self.counts.last().copied().unwrap_or(0);
if others > 0
&& let Some(other) = other_bin(&self.key)
{
entries.push((other, Value::UBigInt(others)));
}
Value::map(self.key.clone(), LogicalType::UBigInt, entries)
}
}
fn boundaries(list: Option<&Value>) -> Result<Vec<Value>> {
let Some(Value::List { values, .. }) = list else {
return Err(Error::binder("Histogram bin list cannot be NULL"));
};
if values.iter().any(Value::is_null) {
return Err(Error::binder("Histogram bin entry cannot be NULL"));
}
let mut bins = values.clone();
let mut failure = None;
bins.sort_by(|left, right| {
order(left, right).unwrap_or_else(|error| {
failure.get_or_insert(error);
Ordering::Equal
})
});
if let Some(error) = failure {
return Err(error);
}
let mut kept: Vec<Value> = Vec::with_capacity(bins.len());
for bin in bins {
match kept.last() {
Some(last) if order(last, &bin)? == Ordering::Equal => {}
_ => kept.push(bin),
}
}
Ok(kept)
}
fn plain(bins: &[Value]) -> Plain {
if let Some(reals) = bins
.iter()
.map(|bin| if let Value::Double(real) = bin { Some(*real) } else { None })
.collect::<Option<Vec<f64>>>()
{
return Plain::Reals(reals);
}
let Some((kind, _)) = bins.first().and_then(Whole::of) else { return Plain::Neither };
bins.iter()
.map(|bin| Whole::of(bin).filter(|(theirs, _)| *theirs == kind).map(|(_, n)| n))
.collect::<Option<Vec<i64>>>()
.map_or(Plain::Neither, |wholes| Plain::Wholes(kind, wholes))
}
fn less(bin: &Value, value: &Value) -> Result<bool> {
Ok(match (bin, value) {
(Value::Double(bin), Value::Double(value)) => bin < value,
(Value::Float(bin), Value::Float(value)) => bin < value,
_ => order(bin, value)? == Ordering::Less,
})
}
#[expect(clippy::float_cmp, reason = "an exact bin takes only an equal value")]
fn equal(bin: &Value, value: &Value) -> Result<bool> {
Ok(match (bin, value) {
(Value::Double(bin), Value::Double(value)) => bin == value,
(Value::Float(bin), Value::Float(value)) => bin == value,
_ => order(bin, value)? == Ordering::Equal,
})
}
fn lower_bound(bins: &[Value], value: &Value) -> Result<usize> {
let (mut low, mut high) = (0, bins.len());
while low < high {
let middle = low + (high - low) / 2;
if less(&bins[middle], value)? {
low = middle + 1;
} else {
high = middle;
}
}
Ok(low)
}
pub(crate) fn other_bin(key: &LogicalType) -> Option<Value> {
Some(match key {
LogicalType::TinyInt => Value::TinyInt(i8::MAX),
LogicalType::SmallInt => Value::SmallInt(i16::MAX),
LogicalType::Integer => Value::Integer(i32::MAX),
LogicalType::BigInt => Value::BigInt(i64::MAX),
LogicalType::HugeInt => Value::HugeInt(i128::MAX),
LogicalType::UTinyInt => Value::UTinyInt(u8::MAX),
LogicalType::USmallInt => Value::USmallInt(u16::MAX),
LogicalType::UInteger => Value::UInteger(u32::MAX),
LogicalType::UBigInt => Value::UBigInt(u64::MAX),
LogicalType::UHugeInt => Value::UHugeInt(u128::MAX),
LogicalType::Time => Value::Time(86_400_000_000),
LogicalType::Date => Value::Date(i32::MAX),
LogicalType::Timestamp => Value::Timestamp(i64::MAX),
LogicalType::TimestampTz => Value::TimestampTz(i64::MAX),
LogicalType::Float => Value::Float(f32::INFINITY),
LogicalType::Double => Value::Double(f64::INFINITY),
LogicalType::Varchar => Value::Varchar(String::new()),
LogicalType::Blob => Value::Blob(Vec::new()),
LogicalType::List(element) => {
Value::List { element: (**element).clone(), values: Vec::new() }
}
LogicalType::Struct(fields) => {
Value::Struct(fields.iter().map(|field| (field.name.clone(), Value::Null)).collect())
}
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn bins(values: &[i32]) -> Value {
Value::List {
element: LogicalType::Integer,
values: values.iter().copied().map(Value::Integer).collect(),
}
}
fn counted(exact: bool, values: &[i32], edges: &[i32]) -> Value {
let mut state = Binned::new(exact, LogicalType::Integer);
for &value in values {
state.update(&Value::Integer(value), Some(&bins(edges))).expect("counts");
}
state.finish()
}
#[test]
fn a_binned_histogram_counts_into_the_first_bin_a_value_fits_and_the_rest_at_the_end() {
let values = [0, 1, 2, 3, 4, 6];
assert_eq!(
counted(false, &values, &[5, 3, 1, 3]).to_string(),
"{1=2, 3=2, 5=1, 2147483647=1}"
);
assert_eq!(counted(true, &values, &[1, 3, 5]).to_string(), "{1=1, 3=1, 5=0, 2147483647=4}");
assert_eq!(counted(false, &[1, 2], &[2]).to_string(), "{2=2}");
assert_eq!(counted(false, &[], &[2]), Value::Null);
}
#[test]
fn a_binned_histogram_refuses_null_bins_and_to_combine_different_ones() {
let mut state = Binned::new(false, LogicalType::Integer);
assert!(state.update(&Value::Integer(1), Some(&Value::Null)).is_err());
let with_null = Value::List {
element: LogicalType::Integer,
values: vec![Value::Integer(1), Value::Null],
};
assert!(state.update(&Value::Integer(1), Some(&with_null)).is_err());
let (mut mine, mut theirs) = (state.clone(), state);
mine.update(&Value::Integer(1), Some(&bins(&[1]))).expect("counts");
theirs.update(&Value::Integer(1), Some(&bins(&[2]))).expect("counts");
assert!(mine.combine(&theirs).is_err());
}
}