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::{NumericValue, Repr},
9    types::Decimal,
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)]
42pub(crate) enum NumericArithmeticError {
43    Overflow,
44    NotRepresentable,
45}
46
47fn numeric_repr(value: &Value) -> NumericRepr {
48    // Numeric comparison eligibility is registry-authoritative.
49    if !semantics::supports_numeric_coercion(value) {
50        return NumericRepr::None;
51    }
52
53    if let Some(decimal) = to_decimal(value) {
54        return NumericRepr::Decimal(decimal);
55    }
56    if let Some(float) = to_f64_lossless(value) {
57        return NumericRepr::F64(float);
58    }
59    NumericRepr::None
60}
61
62fn to_decimal(value: &Value) -> Option<Decimal> {
63    match value {
64        Value::Decimal(value) => value.try_to_decimal(),
65        Value::Duration(value) => value.try_to_decimal(),
66        Value::Float64(value) => value.try_to_decimal(),
67        Value::Float32(value) => value.try_to_decimal(),
68        Value::Int64(value) => value.try_to_decimal(),
69        Value::Int128(value) => value.try_to_decimal(),
70        Value::IntBig(value) => value.try_to_decimal(),
71        Value::Timestamp(value) => value.try_to_decimal(),
72        Value::Nat64(value) => value.try_to_decimal(),
73        Value::Nat128(value) => value.try_to_decimal(),
74        Value::NatBig(value) => value.try_to_decimal(),
75
76        _ => None,
77    }
78}
79
80// Internal numeric coercion helper for aggregate arithmetic.
81pub(crate) fn to_numeric_decimal(value: &Value) -> Option<Decimal> {
82    to_decimal(value)
83}
84
85// This helper only returns `Some` inside the integer range exactly representable
86// by `f64`, or for finite float wrappers that already own their precision.
87#[expect(clippy::cast_precision_loss)]
88fn to_f64_lossless(value: &Value) -> Option<f64> {
89    match value {
90        Value::Duration(value) if value.repr() <= F64_SAFE_U64 => Some(value.repr() as f64),
91        Value::Float64(value) => Some(value.get()),
92        Value::Float32(value) => Some(f64::from(value.get())),
93        Value::Int64(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(value) => {
94            Some(*value as f64)
95        }
96        Value::Int128(value) if (-F64_SAFE_I128..=F64_SAFE_I128).contains(value) => {
97            Some(*value as f64)
98        }
99        Value::IntBig(value) => value.to_i128().and_then(|integer| {
100            (-F64_SAFE_I128..=F64_SAFE_I128)
101                .contains(&integer)
102                .then_some(integer as f64)
103        }),
104        Value::Timestamp(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(&value.repr()) => {
105            Some(value.repr() as f64)
106        }
107        Value::Nat64(value) if *value <= F64_SAFE_U64 => Some(*value as f64),
108        Value::Nat128(value) if *value <= F64_SAFE_U128 => Some(*value as f64),
109        Value::NatBig(value) => value
110            .to_u128()
111            .and_then(|integer| (integer <= F64_SAFE_U128).then_some(integer as f64)),
112
113        _ => None,
114    }
115}
116
117/// Compare two runtime values under value-local numeric coercion semantics.
118#[must_use]
119fn cmp_numeric(left: &Value, right: &Value) -> Option<Ordering> {
120    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
121        return None;
122    }
123
124    match (numeric_repr(left), numeric_repr(right)) {
125        (NumericRepr::Decimal(left), NumericRepr::Decimal(right)) => left.partial_cmp(&right),
126        (NumericRepr::F64(left), NumericRepr::F64(right)) => left.partial_cmp(&right),
127        _ => None,
128    }
129}
130
131/// Compare two values after exact decimal numeric coercion.
132#[must_use]
133pub(crate) fn compare_decimal_order(left: &Value, right: &Value) -> Option<Ordering> {
134    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
135        return None;
136    }
137
138    let left = to_decimal(left)?;
139    let right = to_decimal(right)?;
140
141    left.partial_cmp(&right)
142}
143
144/// Add two numeric values under checked decimal arithmetic semantics.
145pub(crate) fn add(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
146    apply_decimal_arithmetic(left, right, Decimal::checked_add, false)
147}
148
149/// Subtract two numeric values under checked decimal arithmetic semantics.
150pub(crate) fn sub(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
151    apply_decimal_arithmetic(left, right, Decimal::checked_sub, false)
152}
153
154/// Multiply two numeric values under checked decimal arithmetic semantics.
155pub(crate) fn mul(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
156    apply_decimal_arithmetic(left, right, Decimal::checked_mul, false)
157}
158
159/// Divide two numeric values under checked decimal arithmetic semantics.
160pub(crate) fn div(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
161    apply_decimal_arithmetic(left, right, Decimal::checked_div, true)
162}
163
164fn apply_decimal_arithmetic(
165    left: &Value,
166    right: &Value,
167    apply: impl FnOnce(Decimal, Decimal) -> Option<Decimal>,
168    division: bool,
169) -> Result<Option<Decimal>, NumericArithmeticError> {
170    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
171        return Ok(None);
172    }
173
174    let Some(left) = to_decimal(left) else {
175        return Ok(None);
176    };
177    let Some(right) = to_decimal(right) else {
178        return Ok(None);
179    };
180    if division && right.is_zero() {
181        return Err(NumericArithmeticError::NotRepresentable);
182    }
183
184    apply(left, right)
185        .map(Some)
186        .ok_or(NumericArithmeticError::Overflow)
187}
188
189impl Value {
190    // Internal numeric coercion helper for aggregate arithmetic.
191    pub(crate) fn to_numeric_decimal(&self) -> Option<Decimal> {
192        to_numeric_decimal(self)
193    }
194
195    /// Compare two runtime values under value-local numeric coercion semantics.
196    ///
197    /// Database execution code should use `db::numeric` helpers as the
198    /// canonical runtime boundary; this method remains the representation-local
199    /// comparison primitive that those higher-level helpers are tested against.
200    #[must_use]
201    pub fn cmp_numeric(&self, other: &Self) -> Option<Ordering> {
202        cmp_numeric(self, other)
203    }
204}