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::macros::pattern)):
8///
9/// ```rust,ignore
10/// find!((id: Id, ts: Inline<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 InlineRange {
22    variable: VariableId,
23    min: RawInline,
24    max: RawInline,
25}
26
27impl InlineRange {
28    /// Create a range constraint on `variable` with inclusive bounds.
29    pub fn new<T: InlineEncoding>(variable: Variable<T>, min: Inline<T>, max: Inline<T>) -> Self {
30        InlineRange {
31            variable: variable.index,
32            min: min.raw,
33            max: max.raw,
34        }
35    }
36}
37
38/// Convenience function to create a [`InlineRange`] constraint.
39pub fn value_range<T: InlineEncoding>(
40    variable: Variable<T>,
41    min: Inline<T>,
42    max: Inline<T>,
43) -> InlineRange {
44    InlineRange::new(variable, min, max)
45}
46
47impl<'a> Constraint<'a> for InlineRange {
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<RawInline>) {
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<RawInline>) {
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::inlineencodings::R256;
86    use crate::prelude::*;
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: Inline<R256> = 10i128.to_inline();
99        let v50: Inline<R256> = 50i128.to_inline();
100        let v90: Inline<R256> = 90i128.to_inline();
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<Inline<R256>> = find!(
109            v: Inline<R256>,
110            pattern!(&data, [{ test_score: ?v }])
111        )
112        .collect();
113        assert_eq!(all.len(), 3);
114
115        // With range [20..80]: only v50.
116        let min: Inline<R256> = 20i128.to_inline();
117        let max: Inline<R256> = 80i128.to_inline();
118        let filtered: Vec<Inline<R256>> = find!(
119            v: Inline<R256>,
120            and!(
121                pattern!(&data, [{ test_score: ?v }]),
122                value_range(v, min, max),
123            )
124        )
125        .collect();
126        assert_eq!(filtered.len(), 1);
127        assert_eq!(filtered[0], v50);
128    }
129}