use std::cmp::Ordering;
use crate::{LogicalType, Value};
pub const MICROS: u8 = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Op {
Equal,
Less,
LessOrEqual,
Greater,
GreaterOrEqual,
}
impl Op {
#[must_use]
pub fn flipped(self) -> Self {
match self {
Self::Equal => Self::Equal,
Self::Less => Self::Greater,
Self::LessOrEqual => Self::GreaterOrEqual,
Self::Greater => Self::Less,
Self::GreaterOrEqual => Self::LessOrEqual,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Bound {
Int(i128),
Real(f64),
Scaled {
unscaled: i128,
scale: u8,
},
Bytes(Vec<u8>),
}
#[must_use]
fn restated(unscaled: i128, scale: u8, into: u8) -> Option<i128> {
let ten = |steps: u8| 10_i128.checked_pow(u32::from(steps));
if into >= scale {
unscaled.checked_mul(ten(into - scale)?)
} else {
let factor = ten(scale - into)?;
(unscaled % factor == 0).then_some(unscaled / factor)
}
}
impl Bound {
#[must_use]
pub fn of_value(value: &Value) -> Option<Self> {
Some(match value {
Value::Boolean(flag) => Self::Int(i128::from(*flag)),
Value::TinyInt(number) => Self::Int(i128::from(*number)),
Value::SmallInt(number) => Self::Int(i128::from(*number)),
Value::Integer(number) => Self::Int(i128::from(*number)),
Value::BigInt(number) => Self::Int(i128::from(*number)),
Value::HugeInt(number) => Self::Int(*number),
Value::UTinyInt(number) => Self::Int(i128::from(*number)),
Value::USmallInt(number) => Self::Int(i128::from(*number)),
Value::UInteger(number) => Self::Int(i128::from(*number)),
Value::UBigInt(number) => Self::Int(i128::from(*number)),
Value::UHugeInt(number) => Self::Int(i128::try_from(*number).ok()?),
Value::Date(days) => Self::Int(i128::from(*days)),
Value::Float(number) => Self::Real(f64::from(*number)),
Value::Double(number) => Self::Real(*number),
Value::Decimal { unscaled, scale, .. } => {
Self::Scaled { unscaled: *unscaled, scale: *scale }
}
Value::Time(micros)
| Value::TimeTz(micros)
| Value::Timestamp(micros)
| Value::TimestampTz(micros) => {
Self::Scaled { unscaled: i128::from(*micros), scale: MICROS }
}
Value::Varchar(text) => Self::Bytes(text.as_bytes().to_vec()),
Value::Blob(bytes) => Self::Bytes(bytes.clone()),
_ => return None,
})
}
#[must_use]
pub fn into_value(&self, ty: &LogicalType) -> Option<Value> {
macro_rules! fit {
($number:expr, $variant:ident) => {
Some(Value::$variant((*$number).try_into().ok()?))
};
}
Some(match (self, ty) {
(Self::Int(number), LogicalType::Boolean) => Value::Boolean(*number != 0),
(Self::Int(number), LogicalType::TinyInt) => return fit!(number, TinyInt),
(Self::Int(number), LogicalType::SmallInt) => return fit!(number, SmallInt),
(Self::Int(number), LogicalType::Integer) => return fit!(number, Integer),
(Self::Int(number), LogicalType::BigInt) => return fit!(number, BigInt),
(Self::Int(number), LogicalType::HugeInt) => Value::HugeInt(*number),
(Self::Int(number), LogicalType::UTinyInt) => return fit!(number, UTinyInt),
(Self::Int(number), LogicalType::USmallInt) => return fit!(number, USmallInt),
(Self::Int(number), LogicalType::UInteger) => return fit!(number, UInteger),
(Self::Int(number), LogicalType::UBigInt) => return fit!(number, UBigInt),
(Self::Int(number), LogicalType::UHugeInt) => return fit!(number, UHugeInt),
(Self::Int(number), LogicalType::Date) => return fit!(number, Date),
(Self::Real(number), LogicalType::Float) => Value::Float(*number as f32),
(Self::Real(number), LogicalType::Double) => Value::Double(*number),
(Self::Scaled { unscaled, scale }, LogicalType::Decimal { width, scale: want }) => {
Value::Decimal {
unscaled: restated(*unscaled, *scale, *want)?,
width: *width,
scale: *want,
}
}
(Self::Scaled { unscaled, scale }, LogicalType::Time) => {
return fit!(&restated(*unscaled, *scale, MICROS)?, Time);
}
(Self::Scaled { unscaled, scale }, LogicalType::TimeTz) => {
return fit!(&restated(*unscaled, *scale, MICROS)?, TimeTz);
}
(Self::Scaled { unscaled, scale }, LogicalType::Timestamp) => {
return fit!(&restated(*unscaled, *scale, MICROS)?, Timestamp);
}
(Self::Scaled { unscaled, scale }, LogicalType::TimestampTz) => {
return fit!(&restated(*unscaled, *scale, MICROS)?, TimestampTz);
}
(Self::Bytes(bytes), LogicalType::Varchar) => {
Value::Varchar(String::from_utf8(bytes.clone()).ok()?)
}
(Self::Bytes(bytes), LogicalType::Blob) => Value::Blob(bytes.clone()),
_ => return None,
})
}
#[must_use]
pub fn order(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Int(left), Self::Int(right)) => Some(left.cmp(right)),
(Self::Real(left), Self::Real(right)) => left.partial_cmp(right),
(Self::Bytes(left), Self::Bytes(right)) => Some(left.as_slice().cmp(right)),
(
Self::Scaled { unscaled: left, scale: from },
Self::Scaled { unscaled: right, scale: to },
) => {
let scale = (*from).max(*to);
Some(restated(*left, *from, scale)?.cmp(&restated(*right, *to, scale)?))
}
_ => None,
}
}
#[must_use]
pub fn smaller(self, other: Self) -> Self {
match self.order(&other) {
Some(Ordering::Greater) => other,
_ => self,
}
}
#[must_use]
pub fn larger(self, other: Self) -> Self {
match self.order(&other) {
Some(Ordering::Less) => other,
_ => self,
}
}
}
#[derive(Debug, Clone)]
pub struct Test {
pub column: usize,
pub op: Op,
pub value: Bound,
}
pub trait Zones: std::fmt::Debug + Send + Sync {
fn column(&self, name: &str) -> Option<usize>;
fn surviving(&self, tests: &[Test]) -> Option<u64>;
fn spread(&self, tests: &[Test]) -> Option<Spread>;
}
#[must_use]
pub fn excluded(op: Op, value: &Bound, low: Option<&Bound>, high: Option<&Bound>) -> bool {
match op {
Op::Less => holds(low, value, &[Ordering::Greater, Ordering::Equal]),
Op::LessOrEqual => holds(low, value, &[Ordering::Greater]),
Op::Greater => holds(high, value, &[Ordering::Less, Ordering::Equal]),
Op::GreaterOrEqual => holds(high, value, &[Ordering::Less]),
Op::Equal => {
holds(low, value, &[Ordering::Greater]) || holds(high, value, &[Ordering::Less])
}
}
}
fn holds(bound: Option<&Bound>, value: &Bound, wanted: &[Ordering]) -> bool {
bound.and_then(|bound| bound.order(value)).is_some_and(|order| wanted.contains(&order))
}
#[must_use]
pub fn kept(tests: &[Test], column: usize, low: &Bound, high: &Bound) -> Option<Spread> {
let ours = || tests.iter().filter(|test| test.column == column);
match (low, high) {
(&Bound::Int(low), &Bound::Int(high)) => {
let clips = ours().filter_map(|test| match test.value {
Bound::Int(value) => Some((test.op, value)),
_ => None,
});
counted(clips, low, high)
}
(&Bound::Real(low), &Bound::Real(high)) => measured(tests, column, low, high),
(
&Bound::Scaled { unscaled: low, scale: lower },
&Bound::Scaled { unscaled: high, scale: upper },
) => {
let scale = ours().fold(lower.max(upper), |scale, test| match test.value {
Bound::Scaled { scale: theirs, .. } => scale.max(theirs),
_ => scale,
});
let clips = ours().filter_map(|test| match test.value {
Bound::Scaled { unscaled, scale: theirs } => {
Some((test.op, restated(unscaled, theirs, scale)?))
}
_ => None,
});
counted(clips, restated(low, lower, scale)?, restated(high, upper, scale)?)
}
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Spread {
pub fraction: f64,
pub read: usize,
}
#[expect(clippy::cast_precision_loss, reason = "a span past two to the fifty third is not a span")]
fn counted(clips: impl Iterator<Item = (Op, i128)>, low: i128, high: i128) -> Option<Spread> {
let whole = high.checked_sub(low)?.checked_add(1)?;
if whole <= 0 {
return None;
}
let (mut first, mut last) = (low, high);
let mut read = 0;
for (op, value) in clips {
match op {
Op::Less => last = last.min(value.saturating_sub(1)),
Op::LessOrEqual => last = last.min(value),
Op::Greater => first = first.max(value.saturating_add(1)),
Op::GreaterOrEqual => first = first.max(value),
Op::Equal => continue,
}
read += 1;
}
let passing = last.saturating_sub(first).saturating_add(1).max(0);
(read > 0).then(|| Spread { fraction: (passing as f64 / whole as f64).clamp(0.0, 1.0), read })
}
fn measured(tests: &[Test], column: usize, low: f64, high: f64) -> Option<Spread> {
let whole = high - low;
if !whole.is_finite() || whole < 0.0 {
return None;
}
let (mut first, mut last) = (low, high);
let mut read = 0;
for test in tests.iter().filter(|test| test.column == column) {
let &Bound::Real(value) = &test.value else { continue };
if !value.is_finite() {
continue;
}
match test.op {
Op::Less | Op::LessOrEqual => last = last.min(value),
Op::Greater | Op::GreaterOrEqual => first = first.max(value),
Op::Equal => continue,
}
read += 1;
}
let fraction = if whole == 0.0 {
f64::from(u8::from(first <= last))
} else {
((last - first).max(0.0) / whole).clamp(0.0, 1.0)
};
(read > 0 && fraction.is_finite()).then_some(Spread { fraction, read })
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use super::{Bound, MICROS, Op, Test, excluded, kept};
use crate::{LogicalType, Value};
fn range() -> (Bound, Bound) {
(Bound::Int(10), Bound::Int(20))
}
#[test]
fn a_constant_below_the_range_rules_out_equality_and_nothing_else() {
let (low, high) = range();
let five = Bound::Int(5);
assert!(excluded(Op::Equal, &five, Some(&low), Some(&high)));
assert!(excluded(Op::Less, &five, Some(&low), Some(&high)), "nothing is below 5");
assert!(excluded(Op::LessOrEqual, &five, Some(&low), Some(&high)));
assert!(!excluded(Op::Greater, &five, Some(&low), Some(&high)), "everything is above 5");
assert!(!excluded(Op::GreaterOrEqual, &five, Some(&low), Some(&high)));
}
#[test]
fn a_constant_above_the_range_rules_out_the_other_direction() {
let (low, high) = range();
let fifty = Bound::Int(50);
assert!(excluded(Op::Equal, &fifty, Some(&low), Some(&high)));
assert!(!excluded(Op::Less, &fifty, Some(&low), Some(&high)));
assert!(excluded(Op::Greater, &fifty, Some(&low), Some(&high)));
assert!(excluded(Op::GreaterOrEqual, &fifty, Some(&low), Some(&high)));
}
#[test]
fn a_constant_at_either_end_of_the_range_is_kept() {
let (low, high) = range();
for value in [Bound::Int(10), Bound::Int(20)] {
assert!(!excluded(Op::Equal, &value, Some(&low), Some(&high)));
assert!(!excluded(Op::LessOrEqual, &value, Some(&low), Some(&high)));
assert!(!excluded(Op::GreaterOrEqual, &value, Some(&low), Some(&high)));
}
assert!(excluded(Op::Less, &Bound::Int(10), Some(&low), Some(&high)));
assert!(excluded(Op::Greater, &Bound::Int(20), Some(&low), Some(&high)));
}
#[test]
fn a_missing_bound_rules_nothing_out() {
let high = Bound::Int(20);
assert!(!excluded(Op::Less, &Bound::Int(5), None, Some(&high)));
assert!(!excluded(Op::Equal, &Bound::Int(5), None, None));
}
#[test]
fn a_bound_of_another_domain_rules_nothing_out() {
let (low, high) = range();
let text = Bound::Bytes(b"x".to_vec());
assert!(!excluded(Op::Equal, &text, Some(&low), Some(&high)));
assert!(!excluded(Op::Less, &text, Some(&low), Some(&high)));
}
#[test]
fn a_nan_bound_rules_nothing_out() {
let nan = Bound::Real(f64::NAN);
for op in [Op::Equal, Op::Less, Op::LessOrEqual, Op::Greater, Op::GreaterOrEqual] {
assert!(!excluded(op, &Bound::Real(1.0), Some(&nan), Some(&nan)));
}
}
#[test]
fn a_null_constant_has_no_bound() {
assert!(Bound::of_value(&Value::Null).is_none());
assert_eq!(Bound::of_value(&Value::Integer(7)), Some(Bound::Int(7)));
}
#[test]
fn a_temporal_constant_carries_the_unit_it_is_counted_in() {
assert_eq!(Bound::of_value(&Value::Timestamp(1)), Some(scaled(1, MICROS)));
assert_eq!(Bound::of_value(&Value::Time(1)), Some(scaled(1, MICROS)));
assert_eq!(Bound::of_value(&Value::Date(1)), Some(Bound::Int(1)));
}
#[test]
fn a_flipped_op_is_the_one_with_its_operands_the_other_way_round() {
assert_eq!(Op::Less.flipped(), Op::Greater);
assert_eq!(Op::GreaterOrEqual.flipped(), Op::LessOrEqual);
assert_eq!(Op::Equal.flipped(), Op::Equal);
}
fn one(op: Op, number: i128) -> Vec<Test> {
vec![Test { column: 0, op, value: Bound::Int(number) }]
}
fn fraction(op: Op, number: i128) -> Option<f64> {
let (low, high) = range();
kept(&one(op, number), 0, &low, &high).map(|spread| spread.fraction)
}
#[test]
fn a_range_of_integers_is_counted_and_not_measured() {
assert_eq!(fraction(Op::Less, 15), Some(5.0 / 11.0));
assert_eq!(fraction(Op::LessOrEqual, 15), Some(6.0 / 11.0));
assert_eq!(fraction(Op::Greater, 15), Some(5.0 / 11.0));
assert_eq!(fraction(Op::GreaterOrEqual, 15), Some(6.0 / 11.0));
}
#[test]
fn a_constant_outside_the_range_keeps_all_of_it_or_none_of_it() {
assert_eq!(fraction(Op::Less, 5), Some(0.0));
assert_eq!(fraction(Op::GreaterOrEqual, 5), Some(1.0));
assert_eq!(fraction(Op::Less, 50), Some(1.0));
assert_eq!(fraction(Op::Greater, 50), Some(0.0));
}
#[test]
fn a_constant_at_either_end_keeps_one_value_or_all_but_one() {
assert_eq!(fraction(Op::Less, 10), Some(0.0), "nothing is below the stretch's own low");
assert_eq!(fraction(Op::LessOrEqual, 10), Some(1.0 / 11.0));
assert_eq!(fraction(Op::Greater, 20), Some(0.0));
assert_eq!(fraction(Op::GreaterOrEqual, 20), Some(1.0 / 11.0));
}
#[test]
fn two_tests_on_one_column_are_intersected_and_not_multiplied() {
let (low, high) = range();
let mut both = one(Op::GreaterOrEqual, 12);
both.extend(one(Op::Less, 15));
let spread = kept(&both, 0, &low, &high).expect("both were read");
assert_eq!(spread.fraction, 3.0 / 11.0);
assert_eq!(spread.read, 2, "and it says both went into it");
let mut empty = one(Op::GreaterOrEqual, 18);
empty.extend(one(Op::Less, 12));
assert_eq!(kept(&empty, 0, &low, &high).map(|spread| spread.fraction), Some(0.0));
}
#[test]
fn a_test_on_another_column_is_not_this_columns_business() {
let (low, high) = range();
let mut mixed = one(Op::Less, 15);
mixed.push(Test { column: 1, op: Op::Less, value: Bound::Int(11) });
let spread = kept(&mixed, 0, &low, &high).expect("the first one was read");
assert_eq!(spread.fraction, 5.0 / 11.0);
assert_eq!(spread.read, 1);
}
#[test]
fn a_range_of_reals_is_measured_and_the_strict_comparisons_answer_the_same() {
let (low, high) = (Bound::Real(0.0), Bound::Real(10.0));
let at = |op| {
let tests = vec![Test { column: 0, op, value: Bound::Real(2.5) }];
kept(&tests, 0, &low, &high).map(|spread| spread.fraction)
};
assert_eq!(at(Op::Less), Some(0.25));
assert_eq!(at(Op::LessOrEqual), Some(0.25));
assert_eq!(at(Op::Greater), Some(0.75));
assert_eq!(at(Op::GreaterOrEqual), Some(0.75));
}
#[test]
fn a_stretch_of_one_value_holds_or_does_not_and_is_not_interpolated() {
let one_int = Bound::Int(7);
let at = |op, number| {
kept(&one(op, number), 0, &one_int, &one_int).map(|spread| spread.fraction)
};
assert_eq!(at(Op::LessOrEqual, 7), Some(1.0));
assert_eq!(at(Op::Less, 7), Some(0.0));
assert_eq!(at(Op::Greater, 6), Some(1.0));
let point = Bound::Real(7.0);
let real = |op, number| {
let tests = vec![Test { column: 0, op, value: Bound::Real(number) }];
kept(&tests, 0, &point, &point).map(|spread| spread.fraction)
};
assert_eq!(real(Op::LessOrEqual, 7.0), Some(1.0));
assert_eq!(real(Op::Less, 6.0), Some(0.0));
}
#[test]
fn what_a_range_cannot_answer_it_says_nothing_about() {
let (low, high) = range();
assert_eq!(fraction(Op::Equal, 15), None);
assert_eq!(kept(&[], 0, &low, &high), None);
let real = vec![Test { column: 0, op: Op::Less, value: Bound::Real(15.0) }];
assert_eq!(kept(&real, 0, &low, &high), None);
let text = vec![Test { column: 0, op: Op::Less, value: Bound::Bytes(b"m".to_vec()) }];
let (first, last) = (Bound::Bytes(b"a".to_vec()), Bound::Bytes(b"z".to_vec()));
assert_eq!(kept(&text, 0, &first, &last), None);
}
#[test]
fn a_nan_bound_interpolates_nothing() {
let nan = Bound::Real(f64::NAN);
let tests = vec![Test { column: 0, op: Op::Less, value: Bound::Real(1.0) }];
assert_eq!(kept(&tests, 0, &nan, &nan), None);
}
fn scaled(unscaled: i128, scale: u8) -> Bound {
Bound::Scaled { unscaled, scale }
}
#[test]
fn two_scales_of_one_number_are_one_number() {
assert_eq!(scaled(1234, 2).order(&scaled(12_340, 3)), Some(Ordering::Equal));
assert_eq!(scaled(1234, 2).order(&scaled(12_350, 3)), Some(Ordering::Less));
assert_eq!(scaled(1235, 2).order(&scaled(12_340, 3)), Some(Ordering::Greater));
}
#[test]
fn a_decimal_constant_rules_out_a_stretch_the_same_way_an_integer_does() {
let (low, high) = (scaled(0, 2), scaled(4, 2));
assert!(excluded(Op::GreaterOrEqual, &scaled(50, 3), Some(&low), Some(&high)));
assert!(!excluded(Op::LessOrEqual, &scaled(70, 3), Some(&low), Some(&high)));
assert!(!excluded(Op::GreaterOrEqual, &scaled(40, 3), Some(&low), Some(&high)));
}
#[test]
fn a_decimal_range_is_counted_over_the_steps_the_scale_gives_it() {
let (low, high) = (scaled(0, 2), scaled(10, 2));
let tests = vec![Test { column: 0, op: Op::LessOrEqual, value: scaled(7, 2) }];
let spread = kept(&tests, 0, &low, &high).expect("a decimal range interpolates");
assert!((spread.fraction - 8.0 / 11.0).abs() < 1e-12, "{spread:?}");
assert_eq!(spread.read, 1);
}
#[test]
fn a_constant_finer_than_the_column_is_counted_at_its_own_scale() {
let (low, high) = (scaled(0, 2), scaled(10, 2));
let tests = vec![Test { column: 0, op: Op::LessOrEqual, value: scaled(75, 3) }];
let spread = kept(&tests, 0, &low, &high).expect("a finer constant interpolates");
assert!((spread.fraction - 76.0 / 101.0).abs() < 1e-12, "{spread:?}");
}
#[test]
fn a_timestamp_in_one_unit_compares_with_a_constant_in_another() {
let (low, high) = (scaled(1_000, 3), scaled(2_000, 3));
let tests = vec![Test { column: 0, op: Op::Less, value: scaled(1_500_000, MICROS) }];
let spread = kept(&tests, 0, &low, &high).expect("a timestamp range interpolates");
assert!((spread.fraction - 0.5).abs() < 1e-3, "{spread:?}");
assert!(excluded(Op::Less, &scaled(1_000_000, MICROS), Some(&low), Some(&high)));
}
#[test]
fn a_number_too_wide_to_restate_answers_nothing_rather_than_wrapping() {
let huge = scaled(i128::MAX / 2, 0);
assert_eq!(huge.order(&scaled(1, 30)), None);
assert!(!excluded(Op::Less, &scaled(1, 30), Some(&huge), Some(&huge)));
}
#[test]
fn a_scaled_bound_orders_against_nothing_from_another_domain() {
assert_eq!(scaled(1234, 2).order(&Bound::Int(12)), None);
assert_eq!(Bound::Real(12.34).order(&scaled(1234, 2)), None);
assert_eq!(kept(&one(Op::Less, 15), 0, &scaled(0, 2), &scaled(100, 2)), None);
}
#[test]
fn a_decimal_value_becomes_a_bound_and_comes_back_at_the_columns_scale() {
let value = Value::Decimal { unscaled: 1234, width: 18, scale: 2 };
let bound = Bound::of_value(&value).expect("a decimal has a bound");
assert_eq!(bound, scaled(1234, 2));
let finer = LogicalType::Decimal { width: 18, scale: 3 };
assert_eq!(
bound.into_value(&finer),
Some(Value::Decimal { unscaled: 12_340, width: 18, scale: 3 })
);
let coarser = LogicalType::Decimal { width: 18, scale: 1 };
assert_eq!(bound.into_value(&coarser), None, "12.34 is not a number of tenths");
assert_eq!(
scaled(1230, 2).into_value(&coarser),
Some(Value::Decimal { unscaled: 123, width: 18, scale: 1 })
);
}
#[test]
fn a_timestamp_value_becomes_a_bound_in_microseconds_and_comes_back() {
let value = Value::Timestamp(1_700_000_000_000_000);
let bound = Bound::of_value(&value).expect("a timestamp has a bound");
assert_eq!(bound, scaled(1_700_000_000_000_000, MICROS));
assert_eq!(bound.into_value(&LogicalType::Timestamp), Some(value));
assert_eq!(
scaled(1_700_000_000_000, 3).into_value(&LogicalType::Timestamp),
Some(Value::Timestamp(1_700_000_000_000_000))
);
}
#[test]
fn a_minimum_and_a_maximum_accumulate() {
let lower = Bound::Int(4).smaller(Bound::Int(9));
let upper = Bound::Int(4).larger(Bound::Int(9));
assert_eq!(lower, Bound::Int(4));
assert_eq!(upper, Bound::Int(9));
assert_eq!(Bound::Int(4).smaller(Bound::Bytes(Vec::new())), Bound::Int(4));
}
}