Skip to main content

icydb_core/value/ops/
numeric.rs

1//! Module: value::ops::numeric
2//!
3//! Responsibility: representation-local numeric conversion and comparison.
4//! Does not own: predicate-level numeric policy or planner coercion legality.
5//! Boundary: low-level helpers consumed by database numeric semantics.
6
7use crate::{
8    traits::Repr,
9    types::{Decimal, NumericValue},
10    value::{Value, semantics},
11};
12use std::cmp::Ordering;
13
14const F64_SAFE_I64: i64 = 1i64 << 53;
15const F64_SAFE_U64: u64 = 1u64 << 53;
16const F64_SAFE_I128: i128 = 1i128 << 53;
17const F64_SAFE_U128: u128 = 1u128 << 53;
18
19///
20/// NumericRepr
21///
22/// Represents the comparable numeric form available for one `Value`. Decimal
23/// is preferred when exact conversion is available; otherwise a lossless `f64`
24/// is used only for values inside the well-defined integer safety envelope.
25///
26
27enum NumericRepr {
28    Decimal(Decimal),
29    F64(f64),
30    None,
31}
32
33///
34/// NumericArithmeticError
35///
36/// Reports checked numeric arithmetic failures from value-local arithmetic
37/// helpers. The grouped executor maps these variants into its SQL-facing
38/// projection error taxonomy without duplicating arithmetic rules.
39///
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42#[cfg(any(test, feature = "query"))]
43pub(crate) enum NumericArithmeticError {
44    Overflow,
45    NotRepresentable,
46}
47
48fn numeric_repr(value: &Value) -> NumericRepr {
49    // Numeric comparison eligibility is registry-authoritative.
50    if !semantics::supports_numeric_coercion(value) {
51        return NumericRepr::None;
52    }
53
54    if let Some(decimal) = to_decimal(value) {
55        return NumericRepr::Decimal(decimal);
56    }
57    if let Some(float) = to_f64_lossless(value) {
58        return NumericRepr::F64(float);
59    }
60    NumericRepr::None
61}
62
63fn to_decimal(value: &Value) -> Option<Decimal> {
64    match value {
65        Value::Decimal(value) => value.try_to_decimal(),
66        Value::Duration(value) => value.try_to_decimal(),
67        Value::Float64(value) => value.try_to_decimal(),
68        Value::Float32(value) => value.try_to_decimal(),
69        Value::Int64(value) => value.try_to_decimal(),
70        Value::Int128(value) => value.try_to_decimal(),
71        Value::IntBig(value) => value.try_to_decimal(),
72        Value::Timestamp(value) => value.try_to_decimal(),
73        Value::Nat64(value) => value.try_to_decimal(),
74        Value::Nat128(value) => value.try_to_decimal(),
75        Value::NatBig(value) => value.try_to_decimal(),
76
77        _ => None,
78    }
79}
80
81// Internal numeric coercion helper for aggregate arithmetic.
82pub(crate) fn to_numeric_decimal(value: &Value) -> Option<Decimal> {
83    to_decimal(value)
84}
85
86// This helper only returns `Some` inside the integer range exactly representable
87// by `f64`, or for finite float wrappers that already own their precision.
88#[expect(clippy::cast_precision_loss)]
89fn to_f64_lossless(value: &Value) -> Option<f64> {
90    match value {
91        Value::Duration(value) if value.repr() <= F64_SAFE_U64 => Some(value.repr() as f64),
92        Value::Float64(value) => Some(value.get()),
93        Value::Float32(value) => Some(f64::from(value.get())),
94        Value::Int64(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(value) => {
95            Some(*value as f64)
96        }
97        Value::Int128(value) if (-F64_SAFE_I128..=F64_SAFE_I128).contains(value) => {
98            Some(*value as f64)
99        }
100        Value::IntBig(value) => value.to_i128().and_then(|integer| {
101            (-F64_SAFE_I128..=F64_SAFE_I128)
102                .contains(&integer)
103                .then_some(integer as f64)
104        }),
105        Value::Timestamp(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(&value.repr()) => {
106            Some(value.repr() as f64)
107        }
108        Value::Nat64(value) if *value <= F64_SAFE_U64 => Some(*value as f64),
109        Value::Nat128(value) if *value <= F64_SAFE_U128 => Some(*value as f64),
110        Value::NatBig(value) => value
111            .to_u128()
112            .and_then(|integer| (integer <= F64_SAFE_U128).then_some(integer as f64)),
113
114        _ => None,
115    }
116}
117
118/// Compare two runtime values under value-local numeric coercion semantics.
119#[must_use]
120fn cmp_numeric(left: &Value, right: &Value) -> Option<Ordering> {
121    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
122        return None;
123    }
124
125    match (numeric_repr(left), numeric_repr(right)) {
126        (NumericRepr::Decimal(left), NumericRepr::Decimal(right)) => left.partial_cmp(&right),
127        (NumericRepr::F64(left), NumericRepr::F64(right)) => left.partial_cmp(&right),
128        _ => None,
129    }
130}
131
132/// Compare two values after exact decimal numeric coercion.
133#[must_use]
134#[cfg(any(test, feature = "query"))]
135pub(crate) fn compare_decimal_order(left: &Value, right: &Value) -> Option<Ordering> {
136    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
137        return None;
138    }
139
140    let left = to_decimal(left)?;
141    let right = to_decimal(right)?;
142
143    left.partial_cmp(&right)
144}
145
146/// Add two numeric values under checked decimal arithmetic semantics.
147#[cfg(any(test, feature = "query"))]
148pub(crate) fn add(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
149    apply_decimal_arithmetic(left, right, Decimal::checked_add, false)
150}
151
152/// Subtract two numeric values under checked decimal arithmetic semantics.
153#[cfg(any(test, feature = "query"))]
154pub(crate) fn sub(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
155    apply_decimal_arithmetic(left, right, Decimal::checked_sub, false)
156}
157
158/// Multiply two numeric values under checked decimal arithmetic semantics.
159#[cfg(any(test, feature = "query"))]
160pub(crate) fn mul(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
161    apply_decimal_arithmetic(left, right, Decimal::checked_mul, false)
162}
163
164/// Divide two numeric values under checked decimal arithmetic semantics.
165#[cfg(any(test, feature = "query"))]
166pub(crate) fn div(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
167    apply_decimal_arithmetic(left, right, Decimal::checked_div, true)
168}
169
170#[cfg(any(test, feature = "query"))]
171fn apply_decimal_arithmetic(
172    left: &Value,
173    right: &Value,
174    apply: impl FnOnce(Decimal, Decimal) -> Option<Decimal>,
175    division: bool,
176) -> Result<Option<Decimal>, NumericArithmeticError> {
177    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
178        return Ok(None);
179    }
180
181    let Some(left) = to_decimal(left) else {
182        return Ok(None);
183    };
184    let Some(right) = to_decimal(right) else {
185        return Ok(None);
186    };
187    if division && right.is_zero() {
188        return Err(NumericArithmeticError::NotRepresentable);
189    }
190
191    apply(left, right)
192        .map(Some)
193        .ok_or(NumericArithmeticError::Overflow)
194}
195
196impl Value {
197    // Internal numeric coercion helper for aggregate arithmetic.
198    pub(crate) fn to_numeric_decimal(&self) -> Option<Decimal> {
199        to_numeric_decimal(self)
200    }
201
202    /// Compare two runtime values under value-local numeric coercion semantics.
203    ///
204    /// Database execution code should use `db::numeric` helpers as the
205    /// canonical runtime boundary; this method remains the representation-local
206    /// comparison primitive that those higher-level helpers are tested against.
207    #[must_use]
208    pub fn cmp_numeric(&self, other: &Self) -> Option<Ordering> {
209        cmp_numeric(self, other)
210    }
211}