use std::cmp::Ordering;
use std::sync::{Arc, OnceLock, RwLock};
use rudb_common::bounds::{Bound, Op};
use rudb_plan::{ColumnBinding, Expr, Plan, Slice, SortKey};
#[derive(Debug, Default)]
pub(crate) struct Cutoff {
about: OnceLock<(ColumnBinding, Op)>,
worst: RwLock<Option<Bound>>,
}
impl Cutoff {
pub(crate) fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub(crate) fn about(&self, binding: ColumnBinding, op: Op) {
let _ = self.about.set((binding, op));
}
pub(crate) fn armed(&self) -> bool {
self.about.get().is_some()
}
pub(crate) fn reached(&self, bound: Bound) {
let Some(&(_, op)) = self.about.get() else { return };
if let Ok(held) = self.worst.read() {
if held.as_ref().is_some_and(|held| !improves(op, held, &bound)) {
return;
}
}
let Ok(mut held) = self.worst.write() else { return };
*held = Some(match held.take() {
Some(old) => tightest(op, old, bound),
None => bound,
});
}
pub(crate) fn ordered(&self, index: u32) -> Option<(usize, Op)> {
let &(binding, op) = self.about.get()?;
(binding.table == index).then_some((binding.column as usize, op))
}
pub(crate) fn probe(&self, index: u32) -> Option<(usize, Op, Bound)> {
let &(binding, op) = self.about.get()?;
if binding.table != index {
return None;
}
let worst = self.worst.read().ok()?.clone()?;
Some((binding.column as usize, op, worst))
}
}
pub(crate) fn ordering(plan: &Plan, keys: Slice) -> Option<(ColumnBinding, Op)> {
let &SortKey { expr, descending, nulls_first } = plan.sort_key_list(keys).first()?;
if nulls_first {
return None;
}
let Expr::Column(binding) = *plan.expr(expr) else { return None };
Some((binding, if descending { Op::GreaterOrEqual } else { Op::LessOrEqual }))
}
fn improves(op: Op, held: &Bound, bound: &Bound) -> bool {
match op {
Op::LessOrEqual => held.order(bound) == Some(Ordering::Greater),
_ => held.order(bound) == Some(Ordering::Less),
}
}
fn tightest(op: Op, held: Bound, bound: Bound) -> Bound {
match op {
Op::LessOrEqual => held.smaller(bound),
_ => held.larger(bound),
}
}
#[cfg(test)]
mod tests {
use rudb_common::bounds::{Bound, Op};
use rudb_plan::ColumnBinding;
use super::Cutoff;
#[test]
fn an_unarmed_cutoff_answers_nothing_and_keeps_nothing() {
let cutoff = Cutoff::new();
assert!(!cutoff.armed());
cutoff.reached(Bound::Int(7));
assert!(cutoff.probe(0).is_none(), "nothing was ever said about a column");
}
#[test]
fn a_cutoff_with_no_candidates_yet_excludes_nothing() {
let cutoff = Cutoff::new();
cutoff.about(ColumnBinding::new(0, 3), Op::LessOrEqual);
assert!(cutoff.armed());
assert!(cutoff.probe(0).is_none(), "no instance has filled its candidates");
}
#[test]
fn a_scan_of_another_table_is_told_nothing() {
let cutoff = Cutoff::new();
cutoff.about(ColumnBinding::new(1, 3), Op::LessOrEqual);
cutoff.reached(Bound::Int(7));
assert!(cutoff.probe(0).is_none());
assert!(cutoff.probe(1).is_some());
}
#[test]
fn ascending_keeps_the_smallest_worst_any_instance_has_had() {
let cutoff = Cutoff::new();
cutoff.about(ColumnBinding::new(0, 0), Op::LessOrEqual);
cutoff.reached(Bound::Int(40));
cutoff.reached(Bound::Int(10));
cutoff.reached(Bound::Int(30));
assert_eq!(cutoff.probe(0), Some((0, Op::LessOrEqual, Bound::Int(10))));
}
#[test]
fn descending_keeps_the_largest() {
let cutoff = Cutoff::new();
cutoff.about(ColumnBinding::new(0, 0), Op::GreaterOrEqual);
cutoff.reached(Bound::Int(10));
cutoff.reached(Bound::Int(40));
cutoff.reached(Bound::Int(30));
assert_eq!(cutoff.probe(0), Some((0, Op::GreaterOrEqual, Bound::Int(40))));
}
}