Skip to main content

uqa_sql/expr/
range.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` built-in range and multirange text carriers.
8
9use std::cmp::Ordering;
10
11use uqa_core::{
12    memory::{Produced, ProductionControl},
13    TemporalValue, Value,
14};
15
16mod production;
17mod relationships;
18pub(super) use production::{
19    canonical_multirange_text_with_control, canonical_range_as_multirange_text_with_control,
20    canonical_range_text_with_control, multirange_from_produced_ranges,
21    parse_multirange_with_control, parse_range_with_control,
22};
23
24use crate::ast::RangeSubtype;
25use crate::error::Result;
26use crate::SQLError;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct CanonicalRange {
30    subtype: RangeSubtype,
31    lower: Option<Value>,
32    upper: Option<Value>,
33    lower_inclusive: bool,
34    upper_inclusive: bool,
35    empty: bool,
36}
37
38impl CanonicalRange {
39    fn empty(subtype: RangeSubtype) -> Self {
40        Self {
41            subtype,
42            lower: None,
43            upper: None,
44            lower_inclusive: false,
45            upper_inclusive: false,
46            empty: true,
47        }
48    }
49
50    #[must_use]
51    pub const fn subtype(&self) -> RangeSubtype {
52        self.subtype
53    }
54
55    #[must_use]
56    pub const fn is_empty(&self) -> bool {
57        self.empty
58    }
59
60    #[must_use]
61    pub const fn lower_inclusive(&self) -> bool {
62        self.lower_inclusive
63    }
64
65    #[must_use]
66    pub const fn upper_inclusive(&self) -> bool {
67        self.upper_inclusive
68    }
69
70    #[must_use]
71    pub fn lower(&self) -> Option<&Value> {
72        self.lower.as_ref()
73    }
74
75    #[must_use]
76    pub fn upper(&self) -> Option<&Value> {
77        self.upper.as_ref()
78    }
79
80    #[must_use]
81    pub fn overlaps(&self, other: &Self) -> bool {
82        self.overlaps_with_control(other, &ProductionControl::uncontrolled())
83            .expect("ordinary range overlap")
84    }
85
86    #[must_use]
87    pub fn adjacent(&self, other: &Self) -> bool {
88        self.adjacent_with_control(other, &ProductionControl::uncontrolled())
89            .expect("ordinary range adjacency")
90    }
91
92    #[must_use]
93    pub fn contains_range(&self, other: &Self) -> bool {
94        self.contains_range_with_control(other, &ProductionControl::uncontrolled())
95            .expect("ordinary range containment")
96    }
97
98    #[must_use]
99    pub fn contains_value(&self, value: &Value) -> bool {
100        if self.empty {
101            return false;
102        }
103        let lower = self
104            .lower
105            .as_ref()
106            .is_none_or(|lower| match value.cmp(lower) {
107                Ordering::Greater => true,
108                Ordering::Equal => self.lower_inclusive,
109                Ordering::Less => false,
110            });
111        let upper = self
112            .upper
113            .as_ref()
114            .is_none_or(|upper| match value.cmp(upper) {
115                Ordering::Less => true,
116                Ordering::Equal => self.upper_inclusive,
117                Ordering::Greater => false,
118            });
119        lower && upper
120    }
121
122    /// Smallest range containing both operands. Unlike union, `PostgreSQL`'s
123    /// `range_merge` also spans a gap between disjoint ranges.
124    #[must_use]
125    pub fn merge_cover(&self, other: &Self) -> Self {
126        self.merge_cover_with_control(other, &ProductionControl::uncontrolled())
127            .expect("ordinary range cover")
128            .into_uncontrolled()
129            .expect("ordinary range")
130    }
131
132    pub(super) fn to_text_with_control(
133        &self,
134        control: &ProductionControl<'_>,
135    ) -> Result<Produced<String>> {
136        production::range_text(self, control)
137    }
138
139    #[must_use]
140    pub fn to_text(&self) -> String {
141        production::range_text(self, &ProductionControl::uncontrolled())
142            .expect("ordinary range formatting")
143            .into_uncontrolled()
144            .expect("ordinary range text")
145    }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct CanonicalMultirange {
150    subtype: RangeSubtype,
151    ranges: Vec<CanonicalRange>,
152}
153
154impl CanonicalMultirange {
155    #[must_use]
156    pub fn ranges(&self) -> &[CanonicalRange] {
157        &self.ranges
158    }
159
160    #[must_use]
161    pub fn is_empty(&self) -> bool {
162        self.ranges.is_empty()
163    }
164
165    #[must_use]
166    pub fn contains_range(&self, range: &CanonicalRange) -> bool {
167        range.subtype == self.subtype
168            && (range.empty || self.ranges.iter().any(|item| item.contains_range(range)))
169    }
170
171    #[must_use]
172    pub fn contains_multirange(&self, other: &Self) -> bool {
173        self.subtype == other.subtype && other.ranges.iter().all(|range| self.contains_range(range))
174    }
175
176    #[must_use]
177    pub fn overlaps_range(&self, range: &CanonicalRange) -> bool {
178        range.subtype == self.subtype && self.ranges.iter().any(|item| item.overlaps(range))
179    }
180
181    #[must_use]
182    pub fn overlaps_multirange(&self, other: &Self) -> bool {
183        self.subtype == other.subtype
184            && self
185                .ranges
186                .iter()
187                .any(|left| other.ranges.iter().any(|right| left.overlaps(right)))
188    }
189
190    #[must_use]
191    pub fn merge_cover(&self) -> CanonicalRange {
192        self.merge_cover_with_control(&ProductionControl::uncontrolled())
193            .expect("ordinary multirange cover")
194            .into_uncontrolled()
195            .expect("ordinary range")
196    }
197
198    pub(super) fn to_text_with_control(
199        &self,
200        control: &ProductionControl<'_>,
201    ) -> Result<Produced<String>> {
202        production::multirange_text(self, control)
203    }
204
205    #[must_use]
206    pub fn to_text(&self) -> String {
207        production::multirange_text(self, &ProductionControl::uncontrolled())
208            .expect("ordinary multirange formatting")
209            .into_uncontrolled()
210            .expect("ordinary multirange text")
211    }
212}
213
214pub fn parse_range(text: &str, subtype: RangeSubtype) -> Result<CanonicalRange> {
215    Ok(
216        production::parse_range_with_control(text, subtype, &ProductionControl::uncontrolled())?
217            .into_uncontrolled()
218            .expect("ordinary range"),
219    )
220}
221
222pub fn parse_multirange(text: &str, subtype: RangeSubtype) -> Result<CanonicalMultirange> {
223    Ok(production::parse_multirange_with_control(
224        text,
225        subtype,
226        &ProductionControl::uncontrolled(),
227    )?
228    .into_uncontrolled()
229    .expect("ordinary multirange"))
230}
231
232pub fn multirange_from_ranges(
233    subtype: RangeSubtype,
234    ranges: impl IntoIterator<Item = CanonicalRange>,
235) -> CanonicalMultirange {
236    production::normalize_ranges(ranges, subtype, &ProductionControl::uncontrolled(), None)
237        .expect("ordinary multirange normalization")
238        .into_uncontrolled()
239        .expect("ordinary multirange")
240}
241
242fn increment_discrete(value: &Value, subtype: RangeSubtype) -> Result<Value> {
243    match (subtype, value) {
244        (RangeSubtype::Integer, Value::Int(value)) => i32::try_from(*value)
245            .ok()
246            .and_then(|value| value.checked_add(1))
247            .map(|value| Value::Int(i64::from(value)))
248            .ok_or_else(|| range_overflow("integer")),
249        (RangeSubtype::BigInteger, Value::Int(value)) => value
250            .checked_add(1)
251            .map(Value::Int)
252            .ok_or_else(|| range_overflow("bigint")),
253        (RangeSubtype::Date, Value::Temporal(TemporalValue::Date { days })) => days
254            .checked_add(1)
255            .map(|days| Value::Temporal(TemporalValue::Date { days }))
256            .ok_or_else(|| range_overflow("date")),
257        _ => Err(SQLError::Internal(format!(
258            "range subtype {subtype:?} received incompatible bound {value:?}"
259        ))),
260    }
261}
262
263fn is_discrete(subtype: RangeSubtype) -> bool {
264    matches!(
265        subtype,
266        RangeSubtype::Integer | RangeSubtype::BigInteger | RangeSubtype::Date
267    )
268}
269
270fn invalid_range(text: &str, subtype: RangeSubtype) -> SQLError {
271    SQLError::Routine {
272        sqlstate: "22P02".into(),
273        message: format!(
274            "malformed range literal: \"{text}\" for type {}",
275            subtype.range_name()
276        ),
277    }
278}
279
280fn invalid_multirange(text: &str, subtype: RangeSubtype) -> SQLError {
281    SQLError::Routine {
282        sqlstate: "22P02".into(),
283        message: format!(
284            "malformed multirange literal: \"{text}\" for type {}",
285            subtype.multirange_name()
286        ),
287    }
288}
289
290fn range_subtype_error(text: &str, type_name: &str) -> SQLError {
291    SQLError::Routine {
292        sqlstate: "22P02".into(),
293        message: format!("invalid input syntax for type {type_name}: \"{text}\""),
294    }
295}
296
297fn range_overflow(type_name: &str) -> SQLError {
298    SQLError::Routine {
299        sqlstate: "22003".into(),
300        message: format!("{type_name} out of range"),
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn discrete_ranges_canonicalize_to_inclusive_exclusive_bounds() {
310        assert_eq!(
311            parse_range("(1,4]", RangeSubtype::Integer)
312                .unwrap()
313                .to_text(),
314            "[2,5)"
315        );
316        assert_eq!(
317            parse_range("[2024-01-01,2024-01-02]", RangeSubtype::Date)
318                .unwrap()
319                .to_text(),
320            "[2024-01-01,2024-01-03)"
321        );
322    }
323
324    #[test]
325    fn multiranges_merge_overlapping_and_adjacent_members() {
326        assert_eq!(
327            parse_multirange("{[10,12),[1,3),[3,5)}", RangeSubtype::Integer)
328                .unwrap()
329                .to_text(),
330            "{[1,5),[10,12)}"
331        );
332    }
333
334    #[test]
335    fn range_relationships_cover_temporal_constraint_checks() {
336        let left = parse_range("[1,3)", RangeSubtype::Integer).unwrap();
337        let right = parse_range("[3,5)", RangeSubtype::Integer).unwrap();
338        let coverage = multirange_from_ranges(RangeSubtype::Integer, [left, right]);
339        let child = parse_range("[2,4)", RangeSubtype::Integer).unwrap();
340        assert!(coverage.contains_range(&child));
341    }
342}