Skip to main content

triblespace_core/query/
rangeconstraint.rs

1use super::*;
2
3/// Restricts a variable's raw value to a byte-lexicographic range.
4///
5/// This constraint only **confirms** — it never proposes candidates.
6/// Use it with [`and!`](crate::and) alongside a constraint that does
7/// propose (e.g. a [`pattern!`](crate::pattern)):
8///
9/// ```rust,ignore
10/// find!((id: Id, ts: Value<NsTAIInterval>),
11///     and!(
12///         pattern!(data, [{ ?id @ exec::requested_at: ?ts }]),
13///         value_range(ts, min_ts, max_ts),
14///     )
15/// )
16/// ```
17///
18/// The estimate returns `usize::MAX` so the intersection sorts this
19/// constraint last — the tighter TribleSet constraint proposes first,
20/// then this range constraint filters.
21pub struct ValueRange {
22    variable: VariableId,
23    min: RawValue,
24    max: RawValue,
25}
26
27impl ValueRange {
28    /// Create a range constraint on `variable` with inclusive bounds.
29    pub fn new<T: ValueSchema>(variable: Variable<T>, min: Value<T>, max: Value<T>) -> Self {
30        ValueRange {
31            variable: variable.index,
32            min: min.raw,
33            max: max.raw,
34        }
35    }
36}
37
38/// Convenience function to create a [`ValueRange`] constraint.
39pub fn value_range<T: ValueSchema>(
40    variable: Variable<T>,
41    min: Value<T>,
42    max: Value<T>,
43) -> ValueRange {
44    ValueRange::new(variable, min, max)
45}
46
47impl<'a> Constraint<'a> for ValueRange {
48    fn variables(&self) -> VariableSet {
49        VariableSet::new_singleton(self.variable)
50    }
51
52    /// Returns `usize::MAX` so the intersection never chooses this
53    /// constraint as the proposer — it only confirms.
54    fn estimate(&self, variable: VariableId, _binding: &Binding) -> Option<usize> {
55        if self.variable == variable {
56            Some(usize::MAX)
57        } else {
58            None
59        }
60    }
61
62    /// Does not propose — the paired TribleSet constraint handles proposals.
63    fn propose(&self, _variable: VariableId, _binding: &Binding, _proposals: &mut Vec<RawValue>) {
64        // Intentionally empty: this constraint only confirms.
65    }
66
67    /// Retains only proposals whose raw bytes fall within [min, max] inclusive.
68    fn confirm(&self, variable: VariableId, _binding: &Binding, proposals: &mut Vec<RawValue>) {
69        if self.variable == variable {
70            proposals.retain(|v| *v >= self.min && *v <= self.max);
71        }
72    }
73
74    /// Returns `false` when the variable is bound to a value outside the range.
75    fn satisfied(&self, binding: &Binding) -> bool {
76        match binding.get(self.variable) {
77            Some(v) => *v >= self.min && *v <= self.max,
78            None => true,
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use crate::prelude::*;
86    use crate::prelude::valueschemas::R256;
87
88    attributes! {
89        "AA00000000000000AA00000000000000" as test_score: R256;
90    }
91
92    #[test]
93    fn value_range_filters_correctly() {
94        let e1 = ufoid();
95        let e2 = ufoid();
96        let e3 = ufoid();
97
98        let v10: Value<R256> = 10i128.to_value();
99        let v50: Value<R256> = 50i128.to_value();
100        let v90: Value<R256> = 90i128.to_value();
101
102        let mut data = TribleSet::new();
103        data += entity! { &e1 @ test_score: v10 };
104        data += entity! { &e2 @ test_score: v50 };
105        data += entity! { &e3 @ test_score: v90 };
106
107        // Without range: all 3 results.
108        let all: Vec<Value<R256>> = find!(
109            v: Value<R256>,
110            pattern!(&data, [{ test_score: ?v }])
111        ).collect();
112        assert_eq!(all.len(), 3);
113
114        // With range [20..80]: only v50.
115        let min: Value<R256> = 20i128.to_value();
116        let max: Value<R256> = 80i128.to_value();
117        let filtered: Vec<Value<R256>> = find!(
118            v: Value<R256>,
119            and!(
120                pattern!(&data, [{ test_score: ?v }]),
121                value_range(v, min, max),
122            )
123        ).collect();
124        assert_eq!(filtered.len(), 1);
125        assert_eq!(filtered[0], v50);
126    }
127}