Skip to main content

uqa_sql/expr/
floating.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Floating-point input, arithmetic, and output at the declared SQL width.
8
9use uqa_core::memory::{Produced, ProductionControl};
10
11use super::{division_by_zero, BinaryOp, Result, SQLError, Value};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum FloatWidth {
15    Real,
16    DoublePrecision,
17}
18
19pub(super) fn to_float_with_control(
20    value: &Value,
21    width: FloatWidth,
22    control: &ProductionControl<'_>,
23) -> Result<f64> {
24    control.check()?;
25    match value {
26        Value::Float(value) => match width {
27            FloatWidth::Real => narrow_real(*value).map(f64::from),
28            FloatWidth::DoublePrecision => Ok(*value),
29        },
30        Value::Int(value) => Ok(match width {
31            FloatWidth::Real => f64::from(*value as f32),
32            FloatWidth::DoublePrecision => *value as f64,
33        }),
34        Value::Bool(value) => Ok(f64::from(u8::from(*value))),
35        Value::Str(value) | Value::FixedChar(value) => parse_float(value, width, control),
36        Value::Decimal(value) => {
37            parse_float(&value.to_sql_string_with_control(control)?, width, control)
38        }
39        other => Err(SQLError::TypeMismatch(format!(
40            "expected number, got {other:?}"
41        ))),
42    }
43}
44
45fn narrow_real(value: f64) -> Result<f32> {
46    let narrowed = value as f32;
47    if narrowed.is_infinite() && !value.is_infinite() {
48        return Err(range_error("overflow"));
49    }
50    if narrowed == 0.0 && value != 0.0 {
51        return Err(range_error("underflow"));
52    }
53    Ok(narrowed)
54}
55
56fn parse_float(input: &str, width: FloatWidth, control: &ProductionControl<'_>) -> Result<f64> {
57    for _ in input.as_bytes().chunks(4096) {
58        control.check()?;
59    }
60    let text = input.trim_matches(|c: char| c.is_ascii_whitespace());
61    let value = match width {
62        FloatWidth::Real => text.parse::<f32>().map(f64::from),
63        FloatWidth::DoublePrecision => text.parse::<f64>(),
64    }
65    .map_err(|_| SQLError::Routine {
66        sqlstate: "22P02".into(),
67        message: format!(
68            "invalid input syntax for type {}: \"{input}\"",
69            type_name(width)
70        ),
71    })?;
72    let special = text
73        .trim_start_matches(['+', '-'])
74        .eq_ignore_ascii_case("inf")
75        || text
76            .trim_start_matches(['+', '-'])
77            .eq_ignore_ascii_case("infinity");
78    let mantissa = text.split(['e', 'E']).next().unwrap_or(text);
79    let nonzero = mantissa.bytes().any(|byte| matches!(byte, b'1'..=b'9'));
80    if (value.is_infinite() && !special) || (value == 0.0 && nonzero) {
81        return Err(SQLError::Routine {
82            sqlstate: "22003".into(),
83            message: format!("\"{text}\" is out of range for type {}", type_name(width)),
84        });
85    }
86    Ok(value)
87}
88
89fn type_name(width: FloatWidth) -> &'static str {
90    match width {
91        FloatWidth::Real => "real",
92        FloatWidth::DoublePrecision => "double precision",
93    }
94}
95
96fn range_error(kind: &str) -> SQLError {
97    SQLError::Routine {
98        sqlstate: "22003".into(),
99        message: format!("value out of range: {kind}"),
100    }
101}
102
103pub(super) fn square_root(value: f64) -> Result<f64> {
104    if value < 0.0 {
105        return Err(SQLError::Routine {
106            sqlstate: "2201F".into(),
107            message: "cannot take square root of a negative number".into(),
108        });
109    }
110    Ok(value.sqrt())
111}
112
113pub(super) fn power(base: f64, exponent: f64) -> Result<f64> {
114    let invalid = if base == 0.0 && exponent < 0.0 {
115        Some("zero raised to a negative power is undefined")
116    } else if base < 0.0 && !exponent.is_nan() && exponent.floor() != exponent {
117        Some("a negative number raised to a non-integer power yields a complex result")
118    } else {
119        None
120    };
121    if let Some(message) = invalid {
122        return Err(SQLError::Routine {
123            sqlstate: "2201F".into(),
124            message: message.into(),
125        });
126    }
127    let value = libm::pow(base, exponent);
128    if base.is_finite() && exponent.is_finite() {
129        if value.is_infinite() {
130            return Err(range_error("overflow"));
131        }
132        if value == 0.0 && base != 0.0 {
133            return Err(range_error("underflow"));
134        }
135    }
136    Ok(value)
137}
138
139/// Apply arithmetic at its resolved float width before widening the storage carrier.
140pub fn eval_float_arithmetic(
141    op: BinaryOp,
142    left: &Value,
143    right: &Value,
144    width: FloatWidth,
145) -> Result<Value> {
146    eval_float_arithmetic_with_control(op, left, right, width, &ProductionControl::uncontrolled())
147}
148
149/// Evaluate the same width-specific arithmetic with controlled numeric coercion workspace. The resulting float carrier is inline.
150pub fn eval_float_arithmetic_with_control(
151    op: BinaryOp,
152    left: &Value,
153    right: &Value,
154    width: FloatWidth,
155    control: &ProductionControl<'_>,
156) -> Result<Value> {
157    control.check()?;
158    if matches!(left, Value::Null) || matches!(right, Value::Null) {
159        return Ok(Value::Null);
160    }
161    let left = to_float_with_control(left, width, control)?;
162    let right = to_float_with_control(right, width, control)?;
163    if matches!(op, BinaryOp::Divide) && right == 0.0 && !left.is_nan() {
164        return Err(division_by_zero());
165    }
166    let result = match width {
167        FloatWidth::Real => {
168            let left = left as f32;
169            let right = right as f32;
170            f64::from(match op {
171                BinaryOp::Add => left + right,
172                BinaryOp::Subtract => left - right,
173                BinaryOp::Multiply => left * right,
174                BinaryOp::Divide => left / right,
175                _ => return Err(non_arithmetic(op)),
176            })
177        }
178        FloatWidth::DoublePrecision => match op {
179            BinaryOp::Add => left + right,
180            BinaryOp::Subtract => left - right,
181            BinaryOp::Multiply => left * right,
182            BinaryOp::Divide => left / right,
183            _ => return Err(non_arithmetic(op)),
184        },
185    };
186    if result.is_infinite() && !left.is_infinite() && !right.is_infinite() {
187        return Err(range_error("overflow"));
188    }
189    if result == 0.0
190        && left != 0.0
191        && match op {
192            BinaryOp::Multiply => right != 0.0,
193            BinaryOp::Divide => !right.is_infinite(),
194            _ => false,
195        }
196    {
197        return Err(range_error("underflow"));
198    }
199    Ok(Value::Float(result))
200}
201
202fn non_arithmetic(op: BinaryOp) -> SQLError {
203    SQLError::Internal(format!(
204        "non-arithmetic operator {op:?} reached floating arithmetic"
205    ))
206}
207
208/// Format a real value using `PostgreSQL`'s shortest decimal and exponent thresholds.
209#[must_use]
210pub fn format_real(value: f32) -> String {
211    format_real_with_control(value, &ProductionControl::uncontrolled())
212        .expect("ordinary real formatting")
213        .into_uncontrolled()
214        .expect("ordinary real text")
215}
216
217pub(super) fn format_real_with_control(
218    value: f32,
219    control: &ProductionControl<'_>,
220) -> Result<Produced<String>> {
221    if value.is_nan() {
222        return Ok(control.copy_text("NaN")?);
223    }
224    if value.is_infinite() {
225        return Ok(control.copy_text(if value.is_sign_negative() {
226            "-Infinity"
227        } else {
228            "Infinity"
229        })?);
230    }
231    let scientific = control.format(format_args!("{value:e}"))?;
232    let (mantissa, exponent) = scientific.split_once('e').expect("scientific float output");
233    let exponent: i32 = exponent.parse().expect("scientific exponent");
234    if (-4..6).contains(&exponent) {
235        return Ok(control.format(format_args!("{value}"))?);
236    }
237    let sign = if exponent >= 0 { '+' } else { '-' };
238    Ok(control.format(format_args!("{mantissa}e{sign}{:02}", exponent.abs()))?)
239}