use std::cmp::Ordering;
use uqa_core::{DecimalValue, TemporalInfinity, TemporalValue, Value};
use super::IntervalFields;
use crate::error::{Result, SQLError};
pub fn in_range(val: &Value, base: &Value, offset: &Value, sub: bool, less: bool) -> Result<bool> {
in_range_with_engine(val, base, offset, sub, less, None)
}
pub fn in_range_with_engine(
val: &Value,
base: &Value,
offset: &Value,
sub: bool,
less: bool,
engine: Option<&dyn super::EngineHook>,
) -> Result<bool> {
match (val, base, offset) {
(Value::Int(val), Value::Int(base), Value::Int(offset)) => {
integer_in_range(*val, *base, *offset, sub, less)
}
(Value::Float(val), Value::Float(base), Value::Float(offset)) => {
float_in_range(*val, *base, *offset, sub, less)
}
(Value::Decimal(val), Value::Decimal(base), Value::Decimal(offset)) => {
numeric_in_range(val, base, offset, sub, less)
}
(Value::Temporal(val), Value::Temporal(base), Value::Temporal(offset)) => {
temporal_in_range(val, base, offset, sub, less, engine)
}
_ => Err(SQLError::Internal(format!(
"RANGE frame offset {offset:?} does not apply to ordering values {val:?} and {base:?}"
))),
}
}
fn invalid_offset() -> SQLError {
SQLError::Routine {
sqlstate: "22013".into(),
message: "invalid preceding or following size in window function".into(),
}
}
fn numeric_overflow() -> SQLError {
SQLError::Routine {
sqlstate: "22003".into(),
message: "value overflows numeric format".into(),
}
}
fn bound(ordering: Ordering, less: bool) -> bool {
if less {
ordering.is_le()
} else {
ordering.is_ge()
}
}
fn integer_in_range(val: i64, base: i64, offset: i64, sub: bool, less: bool) -> Result<bool> {
if offset < 0 {
return Err(invalid_offset());
}
let offset = i128::from(offset);
let target = if sub {
i128::from(base) - offset
} else {
i128::from(base) + offset
};
Ok(bound(i128::from(val).cmp(&target), less))
}
fn float_in_range(val: f64, base: f64, offset: f64, sub: bool, less: bool) -> Result<bool> {
if offset.is_nan() || offset < 0.0 {
return Err(invalid_offset());
}
if val.is_nan() {
return Ok(if base.is_nan() { true } else { !less });
}
if base.is_nan() {
return Ok(less);
}
if offset.is_infinite() && base.is_infinite() && (if sub { base > 0.0 } else { base < 0.0 }) {
return Ok(true);
}
let target = if sub { base - offset } else { base + offset };
Ok(if less { val <= target } else { val >= target })
}
fn numeric_in_range(
val: &DecimalValue,
base: &DecimalValue,
offset: &DecimalValue,
sub: bool,
less: bool,
) -> Result<bool> {
if offset.is_nan() || offset.is_negative_infinity() || offset.is_negative() {
return Err(invalid_offset());
}
if val.is_nan() {
return Ok(if base.is_nan() { true } else { !less });
}
if base.is_nan() {
return Ok(less);
}
if offset.is_positive_infinity() {
if if sub {
base.is_positive_infinity()
} else {
base.is_negative_infinity()
} {
return Ok(true);
}
return Ok(if sub {
!less || val.is_negative_infinity()
} else {
less || val.is_positive_infinity()
});
}
if val.is_infinite() {
return Ok(if val.is_positive_infinity() {
base.is_positive_infinity() || !less
} else {
base.is_negative_infinity() || less
});
}
if base.is_infinite() {
return Ok(if base.is_negative_infinity() {
!less
} else {
less
});
}
let target = if sub {
base.checked_sub(offset)
} else {
base.checked_add(offset)
}
.ok_or_else(numeric_overflow)?;
Ok(bound(val.cmp(&target), less))
}
fn temporal_in_range(
val: &TemporalValue,
base: &TemporalValue,
offset: &TemporalValue,
sub: bool,
less: bool,
engine: Option<&dyn super::EngineHook>,
) -> Result<bool> {
use TemporalValue as T;
let offset = interval_fields(offset)?;
let micros = offset.micros;
match (val, base) {
(T::Time { micros: val }, T::Time { micros: base }) => {
if micros < 0 {
return Err(invalid_offset());
}
let target = if sub {
base - micros
} else {
match base.checked_add(micros) {
Some(target) => target,
None => return Ok(less),
}
};
Ok(bound(val.cmp(&target), less))
}
(
T::TimeTz {
micros: val,
offset_minutes: val_offset,
},
T::TimeTz {
micros: base,
offset_minutes,
},
) => {
if micros < 0 {
return Err(invalid_offset());
}
let target = if sub {
base - micros
} else {
match base.checked_add(micros) {
Some(target) => target,
None => return Ok(less),
}
};
let utc_micros =
|time: i64, offset: i32| time.wrapping_sub(i64::from(offset) * 60 * 1_000_000);
let ordering = utc_micros(*val, *val_offset)
.cmp(&utc_micros(target, *offset_minutes))
.then_with(|| offset_minutes.cmp(val_offset));
Ok(bound(ordering, less))
}
(T::Interval { .. }, T::Interval { .. }) => {
if offset.value() < IntervalFields::ZERO.value() {
return Err(invalid_offset());
}
if indeterminate_infinite_bound(base, offset, sub) {
return Ok(true);
}
let base = interval_fields(base)?;
let target = if sub {
base.minus(offset)?
} else {
base.plus(offset)?
};
Ok(bound(val.cmp(&target.value()), less))
}
_ => {
let zoned = matches!(base, T::TimestampTz { .. });
let val = T::Timestamp {
micros: timestamp_micros(val)?,
};
let base = T::Timestamp {
micros: timestamp_micros(base)?,
};
if offset.value() < IntervalFields::ZERO.value() {
return Err(invalid_offset());
}
if indeterminate_infinite_bound(&base, offset, sub) {
return Ok(true);
}
let offset = if sub { offset.negate()? } else { offset };
let target = if zoned {
super::time::timestamp_tz_plus_interval(
timestamp_micros(&base)?,
offset,
engine,
&uqa_core::memory::ProductionControl::uncontrolled(),
)?
} else {
super::time::timestamp_plus_interval(
timestamp_micros(&base)?,
offset.months,
offset.days,
offset.micros,
)?
};
Ok(bound(val.cmp(&T::Timestamp { micros: target }), less))
}
}
}
fn interval_fields(value: &TemporalValue) -> Result<IntervalFields> {
IntervalFields::of(value).ok_or_else(|| {
SQLError::Internal(format!("RANGE frame value {value:?} is not an interval"))
})
}
fn indeterminate_infinite_bound(base: &TemporalValue, offset: IntervalFields, sub: bool) -> bool {
offset.value().infinity() == Some(TemporalInfinity::Positive)
&& base.infinity()
== Some(if sub {
TemporalInfinity::Positive
} else {
TemporalInfinity::Negative
})
}
fn timestamp_micros(value: &TemporalValue) -> Result<i64> {
match value {
TemporalValue::Date { .. } => {
super::time::temporal_timestamp_micros(value).map_err(|_| SQLError::Routine {
sqlstate: "22008".into(),
message: "date out of range for timestamp".into(),
})
}
TemporalValue::Timestamp { micros } | TemporalValue::TimestampTz { micros } => Ok(*micros),
other => Err(SQLError::Internal(format!(
"RANGE frame ordering value {other:?} is not a date or timestamp"
))),
}
}
#[cfg(test)]
mod tests;