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::Int(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::Uint(value) => value.try_to_decimal(),
73        Value::Uint128(value) => value.try_to_decimal(),
74        Value::UintBig(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::Int(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(value) => Some(*value as f64),
94        Value::Int128(value) if (-F64_SAFE_I128..=F64_SAFE_I128).contains(&value.get()) => {
95            Some(value.get() as f64)
96        }
97        Value::IntBig(value) => value.to_i128().and_then(|integer| {
98            (-F64_SAFE_I128..=F64_SAFE_I128)
99                .contains(&integer)
100                .then_some(integer as f64)
101        }),
102        Value::Timestamp(value) if (-F64_SAFE_I64..=F64_SAFE_I64).contains(&value.repr()) => {
103            Some(value.repr() as f64)
104        }
105        Value::Uint(value) if *value <= F64_SAFE_U64 => Some(*value as f64),
106        Value::Uint128(value) if value.get() <= F64_SAFE_U128 => Some(value.get() as f64),
107        Value::UintBig(value) => value
108            .to_u128()
109            .and_then(|integer| (integer <= F64_SAFE_U128).then_some(integer as f64)),
110
111        _ => None,
112    }
113}
114
115/// Compare two runtime values under value-local numeric coercion semantics.
116#[must_use]
117pub fn cmp_numeric(left: &Value, right: &Value) -> Option<Ordering> {
118    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
119        return None;
120    }
121
122    match (numeric_repr(left), numeric_repr(right)) {
123        (NumericRepr::Decimal(left), NumericRepr::Decimal(right)) => left.partial_cmp(&right),
124        (NumericRepr::F64(left), NumericRepr::F64(right)) => left.partial_cmp(&right),
125        _ => None,
126    }
127}
128
129/// Compare two values after exact decimal numeric coercion.
130#[must_use]
131pub(crate) fn compare_decimal_order(left: &Value, right: &Value) -> Option<Ordering> {
132    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
133        return None;
134    }
135
136    let left = to_decimal(left)?;
137    let right = to_decimal(right)?;
138
139    left.partial_cmp(&right)
140}
141
142/// Add two numeric values under checked decimal arithmetic semantics.
143pub(crate) fn add(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
144    apply_decimal_arithmetic(left, right, Decimal::checked_add, false)
145}
146
147/// Subtract two numeric values under checked decimal arithmetic semantics.
148pub(crate) fn sub(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
149    apply_decimal_arithmetic(left, right, Decimal::checked_sub, false)
150}
151
152/// Multiply two numeric values under checked decimal arithmetic semantics.
153pub(crate) fn mul(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
154    apply_decimal_arithmetic(left, right, Decimal::checked_mul, false)
155}
156
157/// Divide two numeric values under checked decimal arithmetic semantics.
158pub(crate) fn div(left: &Value, right: &Value) -> Result<Option<Decimal>, NumericArithmeticError> {
159    apply_decimal_arithmetic(left, right, Decimal::checked_div, true)
160}
161
162fn apply_decimal_arithmetic(
163    left: &Value,
164    right: &Value,
165    apply: impl FnOnce(Decimal, Decimal) -> Option<Decimal>,
166    division: bool,
167) -> Result<Option<Decimal>, NumericArithmeticError> {
168    if !semantics::supports_numeric_coercion(left) || !semantics::supports_numeric_coercion(right) {
169        return Ok(None);
170    }
171
172    let Some(left) = to_decimal(left) else {
173        return Ok(None);
174    };
175    let Some(right) = to_decimal(right) else {
176        return Ok(None);
177    };
178    if division && right.is_zero() {
179        return Err(NumericArithmeticError::NotRepresentable);
180    }
181
182    apply(left, right)
183        .map(Some)
184        .ok_or(NumericArithmeticError::Overflow)
185}
186
187impl Value {
188    // Internal numeric coercion helper for aggregate arithmetic.
189    pub(crate) fn to_numeric_decimal(&self) -> Option<Decimal> {
190        to_numeric_decimal(self)
191    }
192
193    /// Compare two runtime values under value-local numeric coercion semantics.
194    ///
195    /// Database execution code should use `db::numeric` helpers as the
196    /// canonical runtime boundary; this method remains the representation-local
197    /// comparison primitive that those higher-level helpers are tested against.
198    #[must_use]
199    pub fn cmp_numeric(&self, other: &Self) -> Option<Ordering> {
200        cmp_numeric(self, other)
201    }
202}