Skip to main content

uqa_sql/expr/
in_range.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The `in_range` support functions of the built-in btree operator families, which decide where a `RANGE` frame with an offset starts and ends: whether `val` lies on the `less` side of `base` moved by `offset`, down when `sub` and up otherwise. Each follows its `PostgreSQL` 18 function for the ordering type, including the treatment of NaN, infinities and overflow.
8
9use std::cmp::Ordering;
10
11use uqa_core::memory::ProductionControl;
12use uqa_core::{DecimalValue, TemporalValue, Value};
13
14use super::IntervalFields;
15
16use crate::error::{Result, SQLError};
17
18const MICROS_PER_DAY: i64 = 86_400_000_000;
19
20/// `in_range(val, base, offset, sub, less)` for a non-null `val` and `base` of one ordering type and an `offset` of the type `transformFrameOffset` selected for it.
21pub fn in_range(val: &Value, base: &Value, offset: &Value, sub: bool, less: bool) -> Result<bool> {
22    match (val, base, offset) {
23        (Value::Int(val), Value::Int(base), Value::Int(offset)) => {
24            integer_in_range(*val, *base, *offset, sub, less)
25        }
26        (Value::Float(val), Value::Float(base), Value::Float(offset)) => {
27            float_in_range(*val, *base, *offset, sub, less)
28        }
29        (Value::Decimal(val), Value::Decimal(base), Value::Decimal(offset)) => {
30            numeric_in_range(val, base, offset, sub, less)
31        }
32        (Value::Temporal(val), Value::Temporal(base), Value::Temporal(offset)) => {
33            temporal_in_range(val, base, offset, sub, less)
34        }
35        _ => Err(SQLError::Internal(format!(
36            "RANGE frame offset {offset:?} does not apply to ordering values {val:?} and {base:?}"
37        ))),
38    }
39}
40
41fn invalid_offset() -> SQLError {
42    SQLError::Routine {
43        sqlstate: "22013".into(),
44        message: "invalid preceding or following size in window function".into(),
45    }
46}
47
48/// `numeric_add` and `numeric_sub` beyond the numeric format's range.
49fn numeric_overflow() -> SQLError {
50    SQLError::Routine {
51        sqlstate: "22003".into(),
52        message: "value overflows numeric format".into(),
53    }
54}
55
56fn bound(ordering: Ordering, less: bool) -> bool {
57    if less {
58        ordering.is_le()
59    } else {
60        ordering.is_ge()
61    }
62}
63
64/// `in_range_int2_int2` through `in_range_int8_int8`. The sum is exact here, which gives the answer those functions give when their narrower sum overflows.
65fn integer_in_range(val: i64, base: i64, offset: i64, sub: bool, less: bool) -> Result<bool> {
66    if offset < 0 {
67        return Err(invalid_offset());
68    }
69    let offset = i128::from(offset);
70    let target = if sub {
71        i128::from(base) - offset
72    } else {
73        i128::from(base) + offset
74    };
75    Ok(bound(i128::from(val).cmp(&target), less))
76}
77
78/// `in_range_float8_float8` and `in_range_float4_float8`.
79fn float_in_range(val: f64, base: f64, offset: f64, sub: bool, less: bool) -> Result<bool> {
80    if offset.is_nan() || offset < 0.0 {
81        return Err(invalid_offset());
82    }
83    // NaN sorts after every other value; the offset cannot change that.
84    if val.is_nan() {
85        return Ok(if base.is_nan() { true } else { !less });
86    }
87    if base.is_nan() {
88        return Ok(less);
89    }
90    // An infinite offset moving an infinite base toward the other infinity would give NaN; every value is taken to lie within such a frame.
91    if offset.is_infinite() && base.is_infinite() && (if sub { base > 0.0 } else { base < 0.0 }) {
92        return Ok(true);
93    }
94    let target = if sub { base - offset } else { base + offset };
95    Ok(if less { val <= target } else { val >= target })
96}
97
98/// `in_range_numeric_numeric`.
99fn numeric_in_range(
100    val: &DecimalValue,
101    base: &DecimalValue,
102    offset: &DecimalValue,
103    sub: bool,
104    less: bool,
105) -> Result<bool> {
106    if offset.is_nan() || offset.is_negative_infinity() || offset.is_negative() {
107        return Err(invalid_offset());
108    }
109    if val.is_nan() {
110        return Ok(if base.is_nan() { true } else { !less });
111    }
112    if base.is_nan() {
113        return Ok(less);
114    }
115    if offset.is_positive_infinity() {
116        if if sub {
117            base.is_positive_infinity()
118        } else {
119            base.is_negative_infinity()
120        } {
121            return Ok(true);
122        }
123        return Ok(if sub {
124            // base - offset is -Infinity.
125            !less || val.is_negative_infinity()
126        } else {
127            // base + offset is +Infinity.
128            less || val.is_positive_infinity()
129        });
130    }
131    if val.is_infinite() {
132        return Ok(if val.is_positive_infinity() {
133            base.is_positive_infinity() || !less
134        } else {
135            base.is_negative_infinity() || less
136        });
137    }
138    if base.is_infinite() {
139        return Ok(if base.is_negative_infinity() {
140            !less
141        } else {
142            less
143        });
144    }
145    let target = if sub {
146        base.checked_sub(offset)
147    } else {
148        base.checked_add(offset)
149    }
150    .ok_or_else(numeric_overflow)?;
151    Ok(bound(val.cmp(&target), less))
152}
153
154/// `in_range_date_interval`, `in_range_timestamp_interval`, `in_range_timestamptz_interval`, `in_range_time_interval`, `in_range_timetz_interval` and `in_range_interval_interval`.
155fn temporal_in_range(
156    val: &TemporalValue,
157    base: &TemporalValue,
158    offset: &TemporalValue,
159    sub: bool,
160    less: bool,
161) -> Result<bool> {
162    use TemporalValue as T;
163    let offset = interval_fields(offset)?;
164    let micros = offset.micros;
165    match (val, base) {
166        (T::Time { micros: val }, T::Time { micros: base }) => {
167            // Like time +/- interval, only the time field of the offset counts, and the sum does not wrap around midnight.
168            if micros < 0 {
169                return Err(invalid_offset());
170            }
171            let target = if sub {
172                base - micros
173            } else {
174                match base.checked_add(micros) {
175                    Some(target) => target,
176                    None => return Ok(less),
177                }
178            };
179            Ok(bound(val.cmp(&target), less))
180        }
181        (
182            T::TimeTz { .. },
183            T::TimeTz {
184                micros: base,
185                offset_minutes,
186            },
187        ) => {
188            if micros < 0 {
189                return Err(invalid_offset());
190            }
191            let target = if sub {
192                base - micros
193            } else {
194                match base.checked_add(micros) {
195                    Some(target) => target,
196                    None => return Ok(less),
197                }
198            };
199            let target = Value::Temporal(T::TimeTz {
200                micros: target,
201                offset_minutes: *offset_minutes,
202            });
203            let ordering = crate::expr::compare_typed_values_with_control(
204                &Value::Temporal(val.clone()),
205                &target,
206                &ProductionControl::uncontrolled(),
207            )?;
208            Ok(bound(ordering, less))
209        }
210        (T::Interval { .. }, T::Interval { .. }) => {
211            if interval_span(offset) < 0 {
212                return Err(invalid_offset());
213            }
214            let (val, base) = (interval_fields(val)?, interval_fields(base)?);
215            let target = if sub {
216                base.minus(offset)?
217            } else {
218                base.plus(offset)?
219            };
220            Ok(bound(interval_span(val).cmp(&interval_span(target)), less))
221        }
222        _ => {
223            if interval_span(offset) < 0 {
224                return Err(invalid_offset());
225            }
226            let val = timestamp_micros(val)?;
227            let base = timestamp_micros(base)?;
228            // `timestamp_mi_interval` adds the interval that `interval_um` negates.
229            let offset = if sub { offset.negate()? } else { offset };
230            let target = super::time::timestamp_plus_interval(
231                base,
232                offset.months,
233                offset.days,
234                offset.micros,
235            )?;
236            Ok(bound(val.cmp(&target), less))
237        }
238    }
239}
240
241fn interval_fields(value: &TemporalValue) -> Result<IntervalFields> {
242    IntervalFields::of(value).ok_or_else(|| {
243        SQLError::Internal(format!("RANGE frame value {value:?} is not an interval"))
244    })
245}
246
247/// The interval as `interval_cmp_value` orders it: a month is 30 days and a day is 24 hours.
248fn interval_span(interval: IntervalFields) -> i128 {
249    (i128::from(interval.months) * 30 + i128::from(interval.days)) * i128::from(MICROS_PER_DAY)
250        + i128::from(interval.micros)
251}
252
253/// A date as the timestamp at its midnight, as `date2timestamp` converts it, or a timestamp's own microseconds.
254fn timestamp_micros(value: &TemporalValue) -> Result<i64> {
255    match value {
256        TemporalValue::Date { days } => Ok(i64::from(*days) * MICROS_PER_DAY),
257        TemporalValue::Timestamp { micros } | TemporalValue::TimestampTz { micros } => Ok(*micros),
258        other => Err(SQLError::Internal(format!(
259            "RANGE frame ordering value {other:?} is not a date or timestamp"
260        ))),
261    }
262}
263
264#[cfg(test)]
265mod tests;