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::{DecimalValue, TemporalValue, Value};
12
13use crate::ast::RangeSubtype;
14use crate::error::Result;
15use crate::SQLError;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct CanonicalRange {
19    subtype: RangeSubtype,
20    lower: Option<Value>,
21    upper: Option<Value>,
22    lower_inclusive: bool,
23    upper_inclusive: bool,
24    empty: bool,
25}
26
27impl CanonicalRange {
28    fn empty(subtype: RangeSubtype) -> Self {
29        Self {
30            subtype,
31            lower: None,
32            upper: None,
33            lower_inclusive: false,
34            upper_inclusive: false,
35            empty: true,
36        }
37    }
38
39    #[must_use]
40    pub const fn subtype(&self) -> RangeSubtype {
41        self.subtype
42    }
43
44    #[must_use]
45    pub const fn is_empty(&self) -> bool {
46        self.empty
47    }
48
49    #[must_use]
50    pub const fn lower_inclusive(&self) -> bool {
51        self.lower_inclusive
52    }
53
54    #[must_use]
55    pub const fn upper_inclusive(&self) -> bool {
56        self.upper_inclusive
57    }
58
59    #[must_use]
60    pub fn lower(&self) -> Option<&Value> {
61        self.lower.as_ref()
62    }
63
64    #[must_use]
65    pub fn upper(&self) -> Option<&Value> {
66        self.upper.as_ref()
67    }
68
69    #[must_use]
70    pub fn overlaps(&self, other: &Self) -> bool {
71        self.subtype == other.subtype
72            && !self.empty
73            && !other.empty
74            && !upper_before_lower(self, other)
75            && !upper_before_lower(other, self)
76    }
77
78    #[must_use]
79    pub fn adjacent(&self, other: &Self) -> bool {
80        if self.subtype != other.subtype || self.empty || other.empty || self.overlaps(other) {
81            return false;
82        }
83        touching_bounds(
84            self.upper(),
85            self.upper_inclusive,
86            other.lower(),
87            other.lower_inclusive,
88        ) || touching_bounds(
89            other.upper(),
90            other.upper_inclusive,
91            self.lower(),
92            self.lower_inclusive,
93        )
94    }
95
96    #[must_use]
97    pub fn contains_range(&self, other: &Self) -> bool {
98        if self.subtype != other.subtype || self.empty {
99            return false;
100        }
101        if other.empty {
102            return true;
103        }
104        lower_contains(self, other) && upper_contains(self, other)
105    }
106
107    #[must_use]
108    pub fn contains_value(&self, value: &Value) -> bool {
109        if self.empty {
110            return false;
111        }
112        let lower = self
113            .lower
114            .as_ref()
115            .is_none_or(|lower| match value.cmp(lower) {
116                Ordering::Greater => true,
117                Ordering::Equal => self.lower_inclusive,
118                Ordering::Less => false,
119            });
120        let upper = self
121            .upper
122            .as_ref()
123            .is_none_or(|upper| match value.cmp(upper) {
124                Ordering::Less => true,
125                Ordering::Equal => self.upper_inclusive,
126                Ordering::Greater => false,
127            });
128        lower && upper
129    }
130
131    fn merge(&self, other: &Self) -> Self {
132        debug_assert!(self.overlaps(other) || self.adjacent(other));
133        let (lower, lower_inclusive) = minimum_lower(self, other);
134        let (upper, upper_inclusive) = maximum_upper(self, other);
135        Self {
136            subtype: self.subtype,
137            lower,
138            upper,
139            lower_inclusive,
140            upper_inclusive,
141            empty: false,
142        }
143    }
144
145    /// Smallest range containing both operands. Unlike union, `PostgreSQL`'s
146    /// `range_merge` also spans a gap between disjoint ranges.
147    #[must_use]
148    pub fn merge_cover(&self, other: &Self) -> Self {
149        if self.empty {
150            return other.clone();
151        }
152        if other.empty {
153            return self.clone();
154        }
155        let (lower, lower_inclusive) = minimum_lower(self, other);
156        let (upper, upper_inclusive) = maximum_upper(self, other);
157        Self {
158            subtype: self.subtype,
159            lower,
160            upper,
161            lower_inclusive,
162            upper_inclusive,
163            empty: false,
164        }
165    }
166
167    #[must_use]
168    pub fn to_text(&self) -> String {
169        if self.empty {
170            return "empty".into();
171        }
172        let mut text = String::new();
173        text.push(if self.lower_inclusive { '[' } else { '(' });
174        if let Some(lower) = &self.lower {
175            text.push_str(&format_bound(lower));
176        }
177        text.push(',');
178        if let Some(upper) = &self.upper {
179            text.push_str(&format_bound(upper));
180        }
181        text.push(if self.upper_inclusive { ']' } else { ')' });
182        text
183    }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct CanonicalMultirange {
188    subtype: RangeSubtype,
189    ranges: Vec<CanonicalRange>,
190}
191
192impl CanonicalMultirange {
193    #[must_use]
194    pub fn ranges(&self) -> &[CanonicalRange] {
195        &self.ranges
196    }
197
198    #[must_use]
199    pub fn is_empty(&self) -> bool {
200        self.ranges.is_empty()
201    }
202
203    #[must_use]
204    pub fn contains_range(&self, range: &CanonicalRange) -> bool {
205        range.subtype == self.subtype
206            && (range.empty || self.ranges.iter().any(|item| item.contains_range(range)))
207    }
208
209    #[must_use]
210    pub fn contains_multirange(&self, other: &Self) -> bool {
211        self.subtype == other.subtype && other.ranges.iter().all(|range| self.contains_range(range))
212    }
213
214    #[must_use]
215    pub fn overlaps_range(&self, range: &CanonicalRange) -> bool {
216        range.subtype == self.subtype && self.ranges.iter().any(|item| item.overlaps(range))
217    }
218
219    #[must_use]
220    pub fn overlaps_multirange(&self, other: &Self) -> bool {
221        self.subtype == other.subtype
222            && self
223                .ranges
224                .iter()
225                .any(|left| other.ranges.iter().any(|right| left.overlaps(right)))
226    }
227
228    #[must_use]
229    pub fn merge_cover(&self) -> CanonicalRange {
230        self.ranges
231            .iter()
232            .cloned()
233            .reduce(|left, right| left.merge_cover(&right))
234            .unwrap_or_else(|| CanonicalRange::empty(self.subtype))
235    }
236
237    #[must_use]
238    pub fn to_text(&self) -> String {
239        format!(
240            "{{{}}}",
241            self.ranges
242                .iter()
243                .map(CanonicalRange::to_text)
244                .collect::<Vec<_>>()
245                .join(",")
246        )
247    }
248}
249
250pub fn parse_range(text: &str, subtype: RangeSubtype) -> Result<CanonicalRange> {
251    let text = text.trim();
252    if text.eq_ignore_ascii_case("empty") {
253        return Ok(CanonicalRange {
254            subtype,
255            lower: None,
256            upper: None,
257            lower_inclusive: false,
258            upper_inclusive: false,
259            empty: true,
260        });
261    }
262    let mut chars = text.chars();
263    let opening = chars.next().ok_or_else(|| invalid_range(text, subtype))?;
264    let closing = text
265        .chars()
266        .next_back()
267        .ok_or_else(|| invalid_range(text, subtype))?;
268    if !matches!(opening, '[' | '(') || !matches!(closing, ']' | ')') || text.len() < 2 {
269        return Err(invalid_range(text, subtype));
270    }
271    let body = &text[opening.len_utf8()..text.len() - closing.len_utf8()];
272    let (lower_text, upper_text) =
273        split_range_bounds(body).ok_or_else(|| invalid_range(text, subtype))?;
274    let mut lower = parse_bound(lower_text, subtype, text)?;
275    let mut upper = parse_bound(upper_text, subtype, text)?;
276    let mut lower_inclusive = opening == '[' && lower.is_some();
277    let mut upper_inclusive = closing == ']' && upper.is_some();
278    if is_discrete(subtype) {
279        if !lower_inclusive {
280            if let Some(value) = lower.as_ref() {
281                lower = Some(increment_discrete(value, subtype)?);
282                lower_inclusive = true;
283            }
284        }
285        if upper_inclusive {
286            if let Some(value) = upper.as_ref() {
287                upper = Some(increment_discrete(value, subtype)?);
288                upper_inclusive = false;
289            }
290        }
291    }
292    let empty = match (&lower, &upper) {
293        (Some(lower), Some(upper)) => match lower.cmp(upper) {
294            Ordering::Greater => true,
295            Ordering::Equal => !(lower_inclusive && upper_inclusive),
296            Ordering::Less => false,
297        },
298        _ => false,
299    };
300    if empty {
301        return parse_range("empty", subtype);
302    }
303    Ok(CanonicalRange {
304        subtype,
305        lower,
306        upper,
307        lower_inclusive,
308        upper_inclusive,
309        empty: false,
310    })
311}
312
313pub fn parse_multirange(text: &str, subtype: RangeSubtype) -> Result<CanonicalMultirange> {
314    let text = text.trim();
315    if !text.starts_with('{') || !text.ends_with('}') {
316        return Err(invalid_multirange(text, subtype));
317    }
318    let body = &text[1..text.len() - 1];
319    let mut ranges = split_multirange_items(body)
320        .ok_or_else(|| invalid_multirange(text, subtype))?
321        .into_iter()
322        .map(|item| parse_range(item, subtype))
323        .collect::<Result<Vec<_>>>()?;
324    ranges.retain(|range| !range.empty);
325    ranges.sort_by(compare_lower_bounds);
326    let mut normalized: Vec<CanonicalRange> = Vec::with_capacity(ranges.len());
327    for range in ranges {
328        if let Some(previous) = normalized.last_mut() {
329            if previous.overlaps(&range) || previous.adjacent(&range) {
330                *previous = previous.merge(&range);
331                continue;
332            }
333        }
334        normalized.push(range);
335    }
336    Ok(CanonicalMultirange {
337        subtype,
338        ranges: normalized,
339    })
340}
341
342pub fn multirange_from_ranges(
343    subtype: RangeSubtype,
344    ranges: impl IntoIterator<Item = CanonicalRange>,
345) -> CanonicalMultirange {
346    let mut ranges = ranges
347        .into_iter()
348        .filter(|range| !range.empty)
349        .collect::<Vec<_>>();
350    ranges.sort_by(compare_lower_bounds);
351    let mut normalized: Vec<CanonicalRange> = Vec::with_capacity(ranges.len());
352    for range in ranges {
353        if let Some(previous) = normalized.last_mut() {
354            if previous.overlaps(&range) || previous.adjacent(&range) {
355                *previous = previous.merge(&range);
356                continue;
357            }
358        }
359        normalized.push(range);
360    }
361    CanonicalMultirange {
362        subtype,
363        ranges: normalized,
364    }
365}
366
367fn split_range_bounds(body: &str) -> Option<(&str, &str)> {
368    let mut quoted = false;
369    let mut escaped = false;
370    for (index, character) in body.char_indices() {
371        if escaped {
372            escaped = false;
373            continue;
374        }
375        if character == '\\' {
376            escaped = true;
377            continue;
378        }
379        if character == '"' {
380            quoted = !quoted;
381            continue;
382        }
383        if character == ',' && !quoted {
384            return Some((&body[..index], &body[index + 1..]));
385        }
386    }
387    None
388}
389
390fn split_multirange_items(body: &str) -> Option<Vec<&str>> {
391    if body.trim().is_empty() {
392        return Some(Vec::new());
393    }
394    let mut items = Vec::new();
395    let mut start = None;
396    let mut quoted = false;
397    let mut escaped = false;
398    for (index, character) in body.char_indices() {
399        if escaped {
400            escaped = false;
401            continue;
402        }
403        if character == '\\' {
404            escaped = true;
405            continue;
406        }
407        if character == '"' {
408            quoted = !quoted;
409            continue;
410        }
411        if quoted {
412            continue;
413        }
414        match character {
415            '[' | '(' if start.is_none() => start = Some(index),
416            ']' | ')' => {
417                let item_start = start.take()?;
418                items.push(body[item_start..=index].trim());
419            }
420            ',' if start.is_none() => {}
421            _ => {}
422        }
423    }
424    if quoted || escaped || start.is_some() || items.is_empty() {
425        None
426    } else {
427        Some(items)
428    }
429}
430
431fn parse_bound(raw: &str, subtype: RangeSubtype, whole: &str) -> Result<Option<Value>> {
432    let raw = raw.trim();
433    if raw.is_empty() {
434        return Ok(None);
435    }
436    let text = unquote_bound(raw).ok_or_else(|| invalid_range(whole, subtype))?;
437    let value = match subtype {
438        RangeSubtype::Integer => text
439            .parse::<i32>()
440            .map(|value| Value::Int(i64::from(value)))
441            .map_err(|_| range_subtype_error(&text, "integer"))?,
442        RangeSubtype::BigInteger => text
443            .parse::<i64>()
444            .map(Value::Int)
445            .map_err(|_| range_subtype_error(&text, "bigint"))?,
446        RangeSubtype::Numeric => DecimalValue::parse(&text)
447            .map(Value::Decimal)
448            .ok_or_else(|| range_subtype_error(&text, "numeric"))?,
449        RangeSubtype::Date => TemporalValue::try_parse_date(&text)
450            .map(Value::Temporal)
451            .map_err(|_| range_subtype_error(&text, "date"))?,
452        RangeSubtype::Timestamp => TemporalValue::parse_timestamp(&text)
453            .map(Value::Temporal)
454            .ok_or_else(|| range_subtype_error(&text, "timestamp without time zone"))?,
455        RangeSubtype::TimestampTz => TemporalValue::parse_timestamp_tz(&text)
456            .map(Value::Temporal)
457            .ok_or_else(|| range_subtype_error(&text, "timestamp with time zone"))?,
458    };
459    Ok(Some(value))
460}
461
462fn unquote_bound(raw: &str) -> Option<String> {
463    if !raw.starts_with('"') {
464        return (!raw.contains('"')).then(|| raw.to_string());
465    }
466    if raw.len() < 2 || !raw.ends_with('"') {
467        return None;
468    }
469    let mut value = String::new();
470    let mut chars = raw[1..raw.len() - 1].chars();
471    while let Some(character) = chars.next() {
472        if character == '\\' {
473            value.push(chars.next()?);
474        } else {
475            value.push(character);
476        }
477    }
478    Some(value)
479}
480
481fn increment_discrete(value: &Value, subtype: RangeSubtype) -> Result<Value> {
482    match (subtype, value) {
483        (RangeSubtype::Integer, Value::Int(value)) => i32::try_from(*value)
484            .ok()
485            .and_then(|value| value.checked_add(1))
486            .map(|value| Value::Int(i64::from(value)))
487            .ok_or_else(|| range_overflow("integer")),
488        (RangeSubtype::BigInteger, Value::Int(value)) => value
489            .checked_add(1)
490            .map(Value::Int)
491            .ok_or_else(|| range_overflow("bigint")),
492        (RangeSubtype::Date, Value::Temporal(TemporalValue::Date { days })) => days
493            .checked_add(1)
494            .map(|days| Value::Temporal(TemporalValue::Date { days }))
495            .ok_or_else(|| range_overflow("date")),
496        _ => Err(SQLError::Internal(format!(
497            "range subtype {subtype:?} received incompatible bound {value:?}"
498        ))),
499    }
500}
501
502fn is_discrete(subtype: RangeSubtype) -> bool {
503    matches!(
504        subtype,
505        RangeSubtype::Integer | RangeSubtype::BigInteger | RangeSubtype::Date
506    )
507}
508
509fn upper_before_lower(left: &CanonicalRange, right: &CanonicalRange) -> bool {
510    match (left.upper(), right.lower()) {
511        (None, _) | (_, None) => false,
512        (Some(upper), Some(lower)) => match upper.cmp(lower) {
513            Ordering::Less => true,
514            Ordering::Greater => false,
515            Ordering::Equal => !(left.upper_inclusive && right.lower_inclusive),
516        },
517    }
518}
519
520fn touching_bounds(
521    upper: Option<&Value>,
522    upper_inclusive: bool,
523    lower: Option<&Value>,
524    lower_inclusive: bool,
525) -> bool {
526    matches!((upper, lower), (Some(upper), Some(lower)) if upper == lower)
527        && upper_inclusive != lower_inclusive
528}
529
530fn lower_contains(outer: &CanonicalRange, inner: &CanonicalRange) -> bool {
531    match (outer.lower(), inner.lower()) {
532        (None, _) => true,
533        (Some(_), None) => false,
534        (Some(left), Some(right)) => match left.cmp(right) {
535            Ordering::Less => true,
536            Ordering::Greater => false,
537            Ordering::Equal => outer.lower_inclusive || !inner.lower_inclusive,
538        },
539    }
540}
541
542fn upper_contains(outer: &CanonicalRange, inner: &CanonicalRange) -> bool {
543    match (outer.upper(), inner.upper()) {
544        (None, _) => true,
545        (Some(_), None) => false,
546        (Some(left), Some(right)) => match left.cmp(right) {
547            Ordering::Greater => true,
548            Ordering::Less => false,
549            Ordering::Equal => outer.upper_inclusive || !inner.upper_inclusive,
550        },
551    }
552}
553
554fn minimum_lower(left: &CanonicalRange, right: &CanonicalRange) -> (Option<Value>, bool) {
555    match (left.lower(), right.lower()) {
556        (None, _) | (_, None) => (None, false),
557        (Some(left_value), Some(right_value)) => match left_value.cmp(right_value) {
558            Ordering::Less => (Some(left_value.clone()), left.lower_inclusive),
559            Ordering::Greater => (Some(right_value.clone()), right.lower_inclusive),
560            Ordering::Equal => (
561                Some(left_value.clone()),
562                left.lower_inclusive || right.lower_inclusive,
563            ),
564        },
565    }
566}
567
568fn maximum_upper(left: &CanonicalRange, right: &CanonicalRange) -> (Option<Value>, bool) {
569    match (left.upper(), right.upper()) {
570        (None, _) | (_, None) => (None, false),
571        (Some(left_value), Some(right_value)) => match left_value.cmp(right_value) {
572            Ordering::Greater => (Some(left_value.clone()), left.upper_inclusive),
573            Ordering::Less => (Some(right_value.clone()), right.upper_inclusive),
574            Ordering::Equal => (
575                Some(left_value.clone()),
576                left.upper_inclusive || right.upper_inclusive,
577            ),
578        },
579    }
580}
581
582fn compare_lower_bounds(left: &CanonicalRange, right: &CanonicalRange) -> Ordering {
583    match (left.lower(), right.lower()) {
584        (None, None) => Ordering::Equal,
585        (None, Some(_)) => Ordering::Less,
586        (Some(_), None) => Ordering::Greater,
587        (Some(left_value), Some(right_value)) => left_value
588            .cmp(right_value)
589            .then_with(|| right.lower_inclusive.cmp(&left.lower_inclusive)),
590    }
591}
592
593fn format_bound(value: &Value) -> String {
594    let raw = match value {
595        Value::Int(value) => value.to_string(),
596        Value::Decimal(value) => value.to_sql_string(),
597        Value::Temporal(value) => value.to_sql_string(),
598        other => super::value_to_string(other),
599    };
600    if raw.is_empty()
601        || raw.chars().any(|character| {
602            character.is_whitespace()
603                || matches!(character, ',' | '[' | ']' | '(' | ')' | '"' | '\\')
604        })
605    {
606        format!("\"{}\"", raw.replace('\\', "\\\\").replace('"', "\\\""))
607    } else {
608        raw
609    }
610}
611
612fn invalid_range(text: &str, subtype: RangeSubtype) -> SQLError {
613    SQLError::Routine {
614        sqlstate: "22P02".into(),
615        message: format!(
616            "malformed range literal: \"{text}\" for type {}",
617            subtype.range_name()
618        ),
619    }
620}
621
622fn invalid_multirange(text: &str, subtype: RangeSubtype) -> SQLError {
623    SQLError::Routine {
624        sqlstate: "22P02".into(),
625        message: format!(
626            "malformed multirange literal: \"{text}\" for type {}",
627            subtype.multirange_name()
628        ),
629    }
630}
631
632fn range_subtype_error(text: &str, type_name: &str) -> SQLError {
633    SQLError::Routine {
634        sqlstate: "22P02".into(),
635        message: format!("invalid input syntax for type {type_name}: \"{text}\""),
636    }
637}
638
639fn range_overflow(type_name: &str) -> SQLError {
640    SQLError::Routine {
641        sqlstate: "22003".into(),
642        message: format!("{type_name} out of range"),
643    }
644}
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649
650    #[test]
651    fn discrete_ranges_canonicalize_to_inclusive_exclusive_bounds() {
652        assert_eq!(
653            parse_range("(1,4]", RangeSubtype::Integer)
654                .unwrap()
655                .to_text(),
656            "[2,5)"
657        );
658        assert_eq!(
659            parse_range("[2024-01-01,2024-01-02]", RangeSubtype::Date)
660                .unwrap()
661                .to_text(),
662            "[2024-01-01,2024-01-03)"
663        );
664    }
665
666    #[test]
667    fn multiranges_merge_overlapping_and_adjacent_members() {
668        assert_eq!(
669            parse_multirange("{[10,12),[1,3),[3,5)}", RangeSubtype::Integer)
670                .unwrap()
671                .to_text(),
672            "{[1,5),[10,12)}"
673        );
674    }
675
676    #[test]
677    fn range_relationships_cover_temporal_constraint_checks() {
678        let left = parse_range("[1,3)", RangeSubtype::Integer).unwrap();
679        let right = parse_range("[3,5)", RangeSubtype::Integer).unwrap();
680        let coverage = multirange_from_ranges(RangeSubtype::Integer, [left, right]);
681        let child = parse_range("[2,4)", RangeSubtype::Integer).unwrap();
682        assert!(coverage.contains_range(&child));
683    }
684}