Skip to main content

nodedb_array/segment/mbr_index/
predicate.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `DomainBound` comparison + bbox intersection.
4//!
5//! `DomainBound` is `PartialOrd` only because Float64 carries NaN; in
6//! practice `TileMBR::dim_mins/maxs` are produced by the MBR builder
7//! and never contain NaN. We treat any non-`Some(Ordering)` from
8//! `partial_cmp` as "not less than" — conservative, never prunes a
9//! valid tile.
10
11use super::node::BBox;
12use crate::types::domain::DomainBound;
13
14/// Strict less-than for two `DomainBound`s. `false` if not comparable.
15pub fn lt_bound(a: &DomainBound, b: &DomainBound) -> bool {
16    a.partial_cmp(b)
17        .is_some_and(|o| o == std::cmp::Ordering::Less)
18}
19
20fn le_bound(a: &DomainBound, b: &DomainBound) -> bool {
21    a.partial_cmp(b)
22        .is_some_and(|o| o != std::cmp::Ordering::Greater)
23}
24
25/// Per-dim closed-interval predicate the caller supplies to query the
26/// tree. `None` on either side means "unbounded".
27#[derive(Debug, Clone, Default)]
28pub struct MbrQueryPredicate {
29    pub per_dim: Vec<DimPredicate>,
30}
31
32#[derive(Debug, Clone, Default)]
33pub struct DimPredicate {
34    pub lo: Option<DomainBound>,
35    pub hi: Option<DomainBound>,
36}
37
38impl MbrQueryPredicate {
39    pub fn new(per_dim: Vec<DimPredicate>) -> Self {
40        Self { per_dim }
41    }
42
43    /// Does this predicate's per-dim range intersect `bbox`?
44    ///
45    /// True if for every dim where the predicate has a bound, the
46    /// tile's [min, max] overlaps the predicate's [lo, hi]. Dims the
47    /// predicate doesn't constrain are accepted by default. If the
48    /// predicate has more dims than the bbox, extra dims are ignored
49    /// (stale predicate after schema change → conservative pass).
50    pub fn intersects(&self, bbox: &BBox) -> bool {
51        for (i, dp) in self.per_dim.iter().enumerate() {
52            if i >= bbox.arity() {
53                break;
54            }
55            if let Some(lo) = &dp.lo
56                && lt_bound(&bbox.max[i], lo)
57            {
58                return false;
59            }
60            if let Some(hi) = &dp.hi
61                && lt_bound(hi, &bbox.min[i])
62            {
63                return false;
64            }
65            // bounds equal-touching is overlap — handled by `lt_bound`
66            // returning false on equality.
67            let _ = le_bound; // keep helper compiled for future symmetric checks
68        }
69        true
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn b(min: i64, max: i64) -> BBox {
78        BBox {
79            min: vec![DomainBound::Int64(min)],
80            max: vec![DomainBound::Int64(max)],
81        }
82    }
83
84    fn pred(lo: Option<i64>, hi: Option<i64>) -> MbrQueryPredicate {
85        MbrQueryPredicate::new(vec![DimPredicate {
86            lo: lo.map(DomainBound::Int64),
87            hi: hi.map(DomainBound::Int64),
88        }])
89    }
90
91    #[test]
92    fn fully_inside_intersects() {
93        assert!(pred(Some(2), Some(8)).intersects(&b(0, 10)));
94    }
95
96    #[test]
97    fn fully_outside_left() {
98        assert!(!pred(Some(20), Some(30)).intersects(&b(0, 10)));
99    }
100
101    #[test]
102    fn fully_outside_right() {
103        assert!(!pred(Some(-10), Some(-5)).intersects(&b(0, 10)));
104    }
105
106    #[test]
107    fn touching_intersects() {
108        assert!(pred(Some(10), Some(20)).intersects(&b(0, 10)));
109    }
110
111    #[test]
112    fn unbounded_intersects() {
113        assert!(pred(None, None).intersects(&b(5, 7)));
114    }
115}