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        self.contains_value_with_control(value, &ProductionControl::uncontrolled())
101            .expect("ordinary range element containment")
102    }
103
104    /// Smallest range containing both operands. Unlike union, `PostgreSQL`'s
105    /// `range_merge` also spans a gap between disjoint ranges.
106    #[must_use]
107    pub fn merge_cover(&self, other: &Self) -> Self {
108        self.merge_cover_with_control(other, &ProductionControl::uncontrolled())
109            .expect("ordinary range cover")
110            .into_uncontrolled()
111            .expect("ordinary range")
112    }
113
114    pub(super) fn to_text_with_control(
115        &self,
116        control: &ProductionControl<'_>,
117    ) -> Result<Produced<String>> {
118        production::range_text(self, control)
119    }
120
121    #[must_use]
122    pub fn to_text(&self) -> String {
123        production::range_text(self, &ProductionControl::uncontrolled())
124            .expect("ordinary range formatting")
125            .into_uncontrolled()
126            .expect("ordinary range text")
127    }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct CanonicalMultirange {
132    subtype: RangeSubtype,
133    ranges: Vec<CanonicalRange>,
134}
135
136impl CanonicalMultirange {
137    #[must_use]
138    pub fn ranges(&self) -> &[CanonicalRange] {
139        &self.ranges
140    }
141
142    #[must_use]
143    pub fn is_empty(&self) -> bool {
144        self.ranges.is_empty()
145    }
146
147    #[must_use]
148    pub fn contains_range(&self, range: &CanonicalRange) -> bool {
149        range.subtype == self.subtype
150            && (range.empty || self.ranges.iter().any(|item| item.contains_range(range)))
151    }
152
153    #[must_use]
154    pub fn contains_multirange(&self, other: &Self) -> bool {
155        self.subtype == other.subtype && other.ranges.iter().all(|range| self.contains_range(range))
156    }
157
158    #[must_use]
159    pub fn overlaps_range(&self, range: &CanonicalRange) -> bool {
160        range.subtype == self.subtype && self.ranges.iter().any(|item| item.overlaps(range))
161    }
162
163    #[must_use]
164    pub fn overlaps_multirange(&self, other: &Self) -> bool {
165        self.subtype == other.subtype
166            && self
167                .ranges
168                .iter()
169                .any(|left| other.ranges.iter().any(|right| left.overlaps(right)))
170    }
171
172    #[must_use]
173    pub fn merge_cover(&self) -> CanonicalRange {
174        self.merge_cover_with_control(&ProductionControl::uncontrolled())
175            .expect("ordinary multirange cover")
176            .into_uncontrolled()
177            .expect("ordinary range")
178    }
179
180    pub(super) fn to_text_with_control(
181        &self,
182        control: &ProductionControl<'_>,
183    ) -> Result<Produced<String>> {
184        production::multirange_text(self, control)
185    }
186
187    #[must_use]
188    pub fn to_text(&self) -> String {
189        production::multirange_text(self, &ProductionControl::uncontrolled())
190            .expect("ordinary multirange formatting")
191            .into_uncontrolled()
192            .expect("ordinary multirange text")
193    }
194}
195
196pub fn parse_range(text: &str, subtype: RangeSubtype) -> Result<CanonicalRange> {
197    Ok(
198        production::parse_range_with_control(text, subtype, &ProductionControl::uncontrolled())?
199            .into_uncontrolled()
200            .expect("ordinary range"),
201    )
202}
203
204pub fn parse_multirange(text: &str, subtype: RangeSubtype) -> Result<CanonicalMultirange> {
205    Ok(production::parse_multirange_with_control(
206        text,
207        subtype,
208        &ProductionControl::uncontrolled(),
209    )?
210    .into_uncontrolled()
211    .expect("ordinary multirange"))
212}
213
214pub fn multirange_from_ranges(
215    subtype: RangeSubtype,
216    ranges: impl IntoIterator<Item = CanonicalRange>,
217) -> CanonicalMultirange {
218    production::normalize_ranges(ranges, subtype, &ProductionControl::uncontrolled(), None)
219        .expect("ordinary multirange normalization")
220        .into_uncontrolled()
221        .expect("ordinary multirange")
222}
223
224fn increment_discrete(value: &Value, subtype: RangeSubtype) -> Result<Value> {
225    match (subtype, value) {
226        (RangeSubtype::Integer, Value::Int(value)) => i32::try_from(*value)
227            .ok()
228            .and_then(|value| value.checked_add(1))
229            .map(|value| Value::Int(i64::from(value)))
230            .ok_or_else(|| range_overflow("integer")),
231        (RangeSubtype::BigInteger, Value::Int(value)) => value
232            .checked_add(1)
233            .map(Value::Int)
234            .ok_or_else(|| range_overflow("bigint")),
235        (RangeSubtype::Date, Value::Temporal(TemporalValue::Date { days })) => days
236            .checked_add(1)
237            .map(|days| Value::Temporal(TemporalValue::Date { days }))
238            .ok_or_else(|| range_overflow("date")),
239        _ => Err(SQLError::Internal(format!(
240            "range subtype {subtype:?} received incompatible bound {value:?}"
241        ))),
242    }
243}
244
245fn is_discrete(subtype: RangeSubtype) -> bool {
246    matches!(
247        subtype,
248        RangeSubtype::Integer | RangeSubtype::BigInteger | RangeSubtype::Date
249    )
250}
251
252fn invalid_range(text: &str, subtype: RangeSubtype) -> SQLError {
253    SQLError::Routine {
254        sqlstate: "22P02".into(),
255        message: format!(
256            "malformed range literal: \"{text}\" for type {}",
257            subtype.range_name()
258        ),
259    }
260}
261
262fn invalid_multirange(text: &str, subtype: RangeSubtype) -> SQLError {
263    SQLError::Routine {
264        sqlstate: "22P02".into(),
265        message: format!(
266            "malformed multirange literal: \"{text}\" for type {}",
267            subtype.multirange_name()
268        ),
269    }
270}
271
272fn range_subtype_error(text: &str, type_name: &str) -> SQLError {
273    SQLError::Routine {
274        sqlstate: "22P02".into(),
275        message: format!("invalid input syntax for type {type_name}: \"{text}\""),
276    }
277}
278
279fn range_overflow(type_name: &str) -> SQLError {
280    SQLError::Routine {
281        sqlstate: "22003".into(),
282        message: format!("{type_name} out of range"),
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn discrete_ranges_canonicalize_to_inclusive_exclusive_bounds() {
292        assert_eq!(
293            parse_range("(1,4]", RangeSubtype::Integer)
294                .unwrap()
295                .to_text(),
296            "[2,5)"
297        );
298        assert_eq!(
299            parse_range("[2024-01-01,2024-01-02]", RangeSubtype::Date)
300                .unwrap()
301                .to_text(),
302            "[2024-01-01,2024-01-03)"
303        );
304    }
305
306    #[test]
307    fn multiranges_merge_overlapping_and_adjacent_members() {
308        assert_eq!(
309            parse_multirange("{[10,12),[1,3),[3,5)}", RangeSubtype::Integer)
310                .unwrap()
311                .to_text(),
312            "{[1,5),[10,12)}"
313        );
314    }
315
316    #[test]
317    fn range_relationships_cover_temporal_constraint_checks() {
318        let left = parse_range("[1,3)", RangeSubtype::Integer).unwrap();
319        let right = parse_range("[3,5)", RangeSubtype::Integer).unwrap();
320        let coverage = multirange_from_ranges(RangeSubtype::Integer, [left, right]);
321        let child = parse_range("[2,4)", RangeSubtype::Integer).unwrap();
322        assert!(coverage.contains_range(&child));
323    }
324}