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::{DecimalValue, TemporalInfinity, TemporalValue, Value};
12
13use super::IntervalFields;
14
15use crate::error::{Result, SQLError};
16
17/// `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.
18pub fn in_range(val: &Value, base: &Value, offset: &Value, sub: bool, less: bool) -> Result<bool> {
19    in_range_with_engine(val, base, offset, sub, less, None)
20}
21
22/// Apply the ordering family's frame predicate with live session calendar arithmetic.
23pub fn in_range_with_engine(
24    val: &Value,
25    base: &Value,
26    offset: &Value,
27    sub: bool,
28    less: bool,
29    engine: Option<&dyn super::EngineHook>,
30) -> Result<bool> {
31    match (val, base, offset) {
32        (Value::Int(val), Value::Int(base), Value::Int(offset)) => {
33            integer_in_range(*val, *base, *offset, sub, less)
34        }
35        (Value::Float(val), Value::Float(base), Value::Float(offset)) => {
36            float_in_range(*val, *base, *offset, sub, less)
37        }
38        (Value::Decimal(val), Value::Decimal(base), Value::Decimal(offset)) => {
39            numeric_in_range(val, base, offset, sub, less)
40        }
41        (Value::Temporal(val), Value::Temporal(base), Value::Temporal(offset)) => {
42            temporal_in_range(val, base, offset, sub, less, engine)
43        }
44        _ => Err(SQLError::Internal(format!(
45            "RANGE frame offset {offset:?} does not apply to ordering values {val:?} and {base:?}"
46        ))),
47    }
48}
49
50fn invalid_offset() -> SQLError {
51    SQLError::Routine {
52        sqlstate: "22013".into(),
53        message: "invalid preceding or following size in window function".into(),
54    }
55}
56
57/// `numeric_add` and `numeric_sub` beyond the numeric format's range.
58fn numeric_overflow() -> SQLError {
59    SQLError::Routine {
60        sqlstate: "22003".into(),
61        message: "value overflows numeric format".into(),
62    }
63}
64
65fn bound(ordering: Ordering, less: bool) -> bool {
66    if less {
67        ordering.is_le()
68    } else {
69        ordering.is_ge()
70    }
71}
72
73/// `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.
74fn integer_in_range(val: i64, base: i64, offset: i64, sub: bool, less: bool) -> Result<bool> {
75    if offset < 0 {
76        return Err(invalid_offset());
77    }
78    let offset = i128::from(offset);
79    let target = if sub {
80        i128::from(base) - offset
81    } else {
82        i128::from(base) + offset
83    };
84    Ok(bound(i128::from(val).cmp(&target), less))
85}
86
87/// `in_range_float8_float8` and `in_range_float4_float8`.
88fn float_in_range(val: f64, base: f64, offset: f64, sub: bool, less: bool) -> Result<bool> {
89    if offset.is_nan() || offset < 0.0 {
90        return Err(invalid_offset());
91    }
92    // NaN sorts after every other value; the offset cannot change that.
93    if val.is_nan() {
94        return Ok(if base.is_nan() { true } else { !less });
95    }
96    if base.is_nan() {
97        return Ok(less);
98    }
99    // An infinite offset moving an infinite base toward the other infinity would give NaN; every value is taken to lie within such a frame.
100    if offset.is_infinite() && base.is_infinite() && (if sub { base > 0.0 } else { base < 0.0 }) {
101        return Ok(true);
102    }
103    let target = if sub { base - offset } else { base + offset };
104    Ok(if less { val <= target } else { val >= target })
105}
106
107/// `in_range_numeric_numeric`.
108fn numeric_in_range(
109    val: &DecimalValue,
110    base: &DecimalValue,
111    offset: &DecimalValue,
112    sub: bool,
113    less: bool,
114) -> Result<bool> {
115    if offset.is_nan() || offset.is_negative_infinity() || offset.is_negative() {
116        return Err(invalid_offset());
117    }
118    if val.is_nan() {
119        return Ok(if base.is_nan() { true } else { !less });
120    }
121    if base.is_nan() {
122        return Ok(less);
123    }
124    if offset.is_positive_infinity() {
125        if if sub {
126            base.is_positive_infinity()
127        } else {
128            base.is_negative_infinity()
129        } {
130            return Ok(true);
131        }
132        return Ok(if sub {
133            // base - offset is -Infinity.
134            !less || val.is_negative_infinity()
135        } else {
136            // base + offset is +Infinity.
137            less || val.is_positive_infinity()
138        });
139    }
140    if val.is_infinite() {
141        return Ok(if val.is_positive_infinity() {
142            base.is_positive_infinity() || !less
143        } else {
144            base.is_negative_infinity() || less
145        });
146    }
147    if base.is_infinite() {
148        return Ok(if base.is_negative_infinity() {
149            !less
150        } else {
151            less
152        });
153    }
154    let target = if sub {
155        base.checked_sub(offset)
156    } else {
157        base.checked_add(offset)
158    }
159    .ok_or_else(numeric_overflow)?;
160    Ok(bound(val.cmp(&target), less))
161}
162
163/// `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`.
164fn temporal_in_range(
165    val: &TemporalValue,
166    base: &TemporalValue,
167    offset: &TemporalValue,
168    sub: bool,
169    less: bool,
170    engine: Option<&dyn super::EngineHook>,
171) -> Result<bool> {
172    use TemporalValue as T;
173    let offset = interval_fields(offset)?;
174    let micros = offset.micros;
175    match (val, base) {
176        (T::Time { micros: val }, T::Time { micros: base }) => {
177            // Like time +/- interval, only the time field of the offset counts, and the sum does not wrap around midnight.
178            if micros < 0 {
179                return Err(invalid_offset());
180            }
181            let target = if sub {
182                base - micros
183            } else {
184                match base.checked_add(micros) {
185                    Some(target) => target,
186                    None => return Ok(less),
187                }
188            };
189            Ok(bound(val.cmp(&target), less))
190        }
191        (
192            T::TimeTz {
193                micros: val,
194                offset_minutes: val_offset,
195            },
196            T::TimeTz {
197                micros: base,
198                offset_minutes,
199            },
200        ) => {
201            if micros < 0 {
202                return Err(invalid_offset());
203            }
204            let target = if sub {
205                base - micros
206            } else {
207                match base.checked_add(micros) {
208                    Some(target) => target,
209                    None => return Ok(less),
210                }
211            };
212            // This synthetic frame bound can exceed a stored time's domain. PostgreSQL's
213            // timetz_cmp_internal uses signed 64-bit wrap for its UTC adjustment even here.
214            let utc_micros =
215                |time: i64, offset: i32| time.wrapping_sub(i64::from(offset) * 60 * 1_000_000);
216            let ordering = utc_micros(*val, *val_offset)
217                .cmp(&utc_micros(target, *offset_minutes))
218                .then_with(|| offset_minutes.cmp(val_offset));
219            Ok(bound(ordering, less))
220        }
221        (T::Interval { .. }, T::Interval { .. }) => {
222            if offset.value() < IntervalFields::ZERO.value() {
223                return Err(invalid_offset());
224            }
225            if indeterminate_infinite_bound(base, offset, sub) {
226                return Ok(true);
227            }
228            let base = interval_fields(base)?;
229            let target = if sub {
230                base.minus(offset)?
231            } else {
232                base.plus(offset)?
233            };
234            Ok(bound(val.cmp(&target.value()), less))
235        }
236        _ => {
237            let zoned = matches!(base, T::TimestampTz { .. });
238            // date2timestamp promotes both dates before validating the offset.
239            let val = T::Timestamp {
240                micros: timestamp_micros(val)?,
241            };
242            let base = T::Timestamp {
243                micros: timestamp_micros(base)?,
244            };
245            if offset.value() < IntervalFields::ZERO.value() {
246                return Err(invalid_offset());
247            }
248            if indeterminate_infinite_bound(&base, offset, sub) {
249                return Ok(true);
250            }
251            // `timestamp_mi_interval` adds the interval that `interval_um` negates.
252            let offset = if sub { offset.negate()? } else { offset };
253            let target = if zoned {
254                super::time::timestamp_tz_plus_interval(
255                    timestamp_micros(&base)?,
256                    offset,
257                    engine,
258                    &uqa_core::memory::ProductionControl::uncontrolled(),
259                )?
260            } else {
261                super::time::timestamp_plus_interval(
262                    timestamp_micros(&base)?,
263                    offset.months,
264                    offset.days,
265                    offset.micros,
266                )?
267            };
268            Ok(bound(val.cmp(&T::Timestamp { micros: target }), less))
269        }
270    }
271}
272
273fn interval_fields(value: &TemporalValue) -> Result<IntervalFields> {
274    IntervalFields::of(value).ok_or_else(|| {
275        SQLError::Internal(format!("RANGE frame value {value:?} is not an interval"))
276    })
277}
278
279fn indeterminate_infinite_bound(base: &TemporalValue, offset: IntervalFields, sub: bool) -> bool {
280    offset.value().infinity() == Some(TemporalInfinity::Positive)
281        && base.infinity()
282            == Some(if sub {
283                TemporalInfinity::Positive
284            } else {
285                TemporalInfinity::Negative
286            })
287}
288
289/// A date as the timestamp at its midnight, as `date2timestamp` converts it, or a timestamp's own microseconds.
290fn timestamp_micros(value: &TemporalValue) -> Result<i64> {
291    match value {
292        TemporalValue::Date { .. } => {
293            super::time::temporal_timestamp_micros(value).map_err(|_| SQLError::Routine {
294                sqlstate: "22008".into(),
295                message: "date out of range for timestamp".into(),
296            })
297        }
298        TemporalValue::Timestamp { micros } | TemporalValue::TimestampTz { micros } => Ok(*micros),
299        other => Err(SQLError::Internal(format!(
300            "RANGE frame ordering value {other:?} is not a date or timestamp"
301        ))),
302    }
303}
304
305#[cfg(test)]
306mod tests;