use time::OffsetDateTime;
use crate::watermark::TieringWatermark;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeRange {
start: Option<OffsetDateTime>,
end: Option<OffsetDateTime>,
}
impl TimeRange {
pub fn new(start: Option<OffsetDateTime>, end: Option<OffsetDateTime>) -> Self {
Self { start, end }
}
pub const fn unbounded() -> Self {
Self {
start: None,
end: None,
}
}
pub fn between(start: OffsetDateTime, end: OffsetDateTime) -> Self {
Self::new(Some(start), Some(end))
}
pub const fn start(self) -> Option<OffsetDateTime> {
self.start
}
pub const fn end(self) -> Option<OffsetDateTime> {
self.end
}
pub fn is_empty(self) -> bool {
matches!((self.start, self.end), (Some(s), Some(e)) if s >= e)
}
pub const fn is_bounded(self) -> bool {
self.start.is_some() && self.end.is_some()
}
fn capped_at(self, limit: OffsetDateTime) -> Self {
let end = match self.end {
Some(e) => Some(e.min(limit)),
None => Some(limit),
};
Self {
start: self.start,
end,
}
}
fn floored_at(self, limit: OffsetDateTime) -> Self {
let start = match self.start {
Some(s) => Some(s.max(limit)),
None => Some(limit),
};
Self {
start,
end: self.end,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TierSplit {
pub cold: Option<TimeRange>,
pub hot: Option<TimeRange>,
}
impl TierSplit {
pub const fn spans_tiers(&self) -> bool {
self.cold.is_some() && self.hot.is_some()
}
pub const fn is_empty(&self) -> bool {
self.cold.is_none() && self.hot.is_none()
}
pub const fn is_cold_only(&self) -> bool {
self.cold.is_some() && self.hot.is_none()
}
}
pub fn split(range: TimeRange, watermark: TieringWatermark) -> TierSplit {
if range.is_empty() {
return TierSplit {
cold: None,
hot: None,
};
}
if watermark == TieringWatermark::empty() {
return TierSplit {
cold: None,
hot: Some(range),
};
}
let boundary = watermark.get();
let cold = match range.start {
Some(s) if s >= boundary => None,
_ => {
let capped = range.capped_at(boundary);
(!capped.is_empty()).then_some(capped)
}
};
let hot = match range.end {
Some(e) if e <= boundary => None,
_ => {
let floored = range.floored_at(boundary);
(!floored.is_empty()).then_some(floored)
}
};
TierSplit { cold, hot }
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::datetime;
const BOUNDARY: OffsetDateTime = datetime!(2026-07-20 00:00 UTC);
fn watermark() -> TieringWatermark {
TieringWatermark::new(BOUNDARY)
}
#[test]
fn a_range_entirely_below_the_watermark_is_cold_only() {
let r = TimeRange::between(
datetime!(2026-07-01 00:00 UTC),
datetime!(2026-07-10 00:00 UTC),
);
let s = split(r, watermark());
assert_eq!(s.cold, Some(r));
assert_eq!(s.hot, None);
assert!(s.is_cold_only());
assert!(!s.spans_tiers());
}
#[test]
fn a_range_entirely_at_or_above_the_watermark_is_hot_only() {
let r = TimeRange::between(
datetime!(2026-07-25 00:00 UTC),
datetime!(2026-07-30 00:00 UTC),
);
let s = split(r, watermark());
assert_eq!(s.hot, Some(r));
assert_eq!(s.cold, None);
assert!(!s.is_cold_only());
}
#[test]
fn a_range_spanning_the_watermark_is_cut_at_it() {
let start = datetime!(2026-07-10 00:00 UTC);
let end = datetime!(2026-07-30 00:00 UTC);
let s = split(TimeRange::between(start, end), watermark());
assert!(s.spans_tiers());
assert_eq!(s.cold, Some(TimeRange::between(start, BOUNDARY)));
assert_eq!(s.hot, Some(TimeRange::between(BOUNDARY, end)));
}
#[test]
fn the_halves_are_disjoint_and_gapless() {
let start = datetime!(2026-07-10 00:00 UTC);
let end = datetime!(2026-07-30 00:00 UTC);
let s = split(TimeRange::between(start, end), watermark());
let cold = s.cold.unwrap();
let hot = s.hot.unwrap();
assert_eq!(cold.end().unwrap(), hot.start().unwrap());
assert_eq!(cold.start().unwrap(), start);
assert_eq!(hot.end().unwrap(), end);
}
#[test]
fn the_boundary_instant_belongs_to_the_hot_tier() {
let s = split(
TimeRange::between(BOUNDARY, datetime!(2026-07-25 00:00 UTC)),
watermark(),
);
assert_eq!(s.cold, None);
assert!(s.hot.is_some());
}
#[test]
fn a_range_ending_exactly_at_the_boundary_is_cold_only() {
let s = split(
TimeRange::between(datetime!(2026-07-10 00:00 UTC), BOUNDARY),
watermark(),
);
assert!(s.is_cold_only());
assert_eq!(s.hot, None);
}
#[test]
fn an_unbounded_query_reads_both_tiers() {
let s = split(TimeRange::unbounded(), watermark());
assert!(s.spans_tiers());
assert_eq!(s.cold.unwrap().end(), Some(BOUNDARY));
assert_eq!(s.cold.unwrap().start(), None);
assert_eq!(s.hot.unwrap().start(), Some(BOUNDARY));
assert_eq!(s.hot.unwrap().end(), None);
}
#[test]
fn a_half_open_range_keeps_its_open_side() {
let after = TimeRange::new(Some(datetime!(2026-07-10 00:00 UTC)), None);
let s = split(after, watermark());
assert_eq!(s.hot.unwrap().end(), None);
let before = TimeRange::new(None, Some(datetime!(2026-07-25 00:00 UTC)));
let s = split(before, watermark());
assert_eq!(s.cold.unwrap().start(), None);
}
#[test]
fn an_empty_range_reads_nothing() {
let inverted = TimeRange::between(
datetime!(2026-02-01 00:00 UTC),
datetime!(2026-01-01 00:00 UTC),
);
assert!(inverted.is_empty());
let s = split(inverted, watermark());
assert!(s.is_empty());
assert!(!s.spans_tiers());
}
#[test]
fn a_zero_width_range_reads_nothing() {
let t = datetime!(2026-07-15 00:00 UTC);
assert!(split(TimeRange::between(t, t), watermark()).is_empty());
}
#[test]
fn an_empty_watermark_sends_everything_to_the_hot_tier() {
let s = split(
TimeRange::between(
datetime!(2026-07-01 00:00 UTC),
datetime!(2026-07-30 00:00 UTC),
),
TieringWatermark::empty(),
);
assert_eq!(s.cold, None);
assert!(s.hot.is_some());
}
#[test]
fn an_unbounded_query_against_an_empty_watermark_plans_no_cold_scan() {
let s = split(TimeRange::unbounded(), TieringWatermark::empty());
assert_eq!(s.cold, None);
assert_eq!(s.hot, Some(TimeRange::unbounded()));
}
#[test]
fn every_instant_in_the_query_lands_in_exactly_one_tier() {
let start = datetime!(2026-07-18 00:00 UTC);
let end = datetime!(2026-07-22 00:00 UTC);
let s = split(TimeRange::between(start, end), watermark());
let mut probe = start;
while probe < end {
let in_cold = s.cold.is_some_and(|r| {
r.start().is_none_or(|x| probe >= x) && r.end().is_none_or(|x| probe < x)
});
let in_hot = s.hot.is_some_and(|r| {
r.start().is_none_or(|x| probe >= x) && r.end().is_none_or(|x| probe < x)
});
assert!(
in_cold ^ in_hot,
"{probe} landed in {} tiers, expected exactly 1",
u8::from(in_cold) + u8::from(in_hot)
);
probe += time::Duration::hours(1);
}
}
}
#[cfg(test)]
mod properties {
use super::*;
use proptest::prelude::*;
fn at(seconds: i64) -> OffsetDateTime {
OffsetDateTime::from_unix_timestamp(seconds).expect("in range")
}
const WINDOW: std::ops::Range<i64> = 0..40;
fn covers(range: TimeRange, t: OffsetDateTime) -> bool {
range.start().is_none_or(|s| t >= s) && range.end().is_none_or(|e| t < e)
}
fn bound() -> impl Strategy<Value = Option<i64>> {
prop_oneof![1 => Just(None), 4 => WINDOW.prop_map(Some)]
}
proptest! {
#[test]
fn every_instant_lands_in_exactly_one_tier(
start in bound(),
end in bound(),
boundary in WINDOW,
probe in WINDOW,
) {
let range = TimeRange::new(start.map(at), end.map(at));
let watermark = TieringWatermark::new(at(boundary));
let split = split(range, watermark);
let probe = at(probe);
prop_assume!(covers(range, probe));
let cold = split.cold.is_some_and(|r| covers(r, probe));
let hot = split.hot.is_some_and(|r| covers(r, probe));
prop_assert!(
cold ^ hot,
"{probe} landed in {} halves of {split:?} at watermark {watermark}",
u8::from(cold) + u8::from(hot),
);
}
#[test]
fn each_half_holds_only_what_the_routing_rule_assigns_it(
start in bound(),
end in bound(),
boundary in WINDOW,
probe in WINDOW,
) {
let range = TimeRange::new(start.map(at), end.map(at));
let watermark = TieringWatermark::new(at(boundary));
let split = split(range, watermark);
let probe = at(probe);
if split.cold.is_some_and(|r| covers(r, probe)) {
prop_assert_eq!(watermark.tier_for(probe), crate::watermark::Tier::Cold);
}
if split.hot.is_some_and(|r| covers(r, probe)) {
prop_assert_eq!(watermark.tier_for(probe), crate::watermark::Tier::Hot);
}
}
}
}