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 super::{division_by_zero, BinaryOp, Result, SQLError, Value};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FloatWidth {
13    Real,
14    DoublePrecision,
15}
16
17pub(super) fn to_float(value: &Value, width: FloatWidth) -> Result<f64> {
18    match value {
19        Value::Float(value) => match width {
20            FloatWidth::Real => narrow_real(*value).map(f64::from),
21            FloatWidth::DoublePrecision => Ok(*value),
22        },
23        Value::Int(value) => Ok(match width {
24            FloatWidth::Real => f64::from(*value as f32),
25            FloatWidth::DoublePrecision => *value as f64,
26        }),
27        Value::Bool(value) => Ok(f64::from(u8::from(*value))),
28        Value::Str(value) | Value::FixedChar(value) => parse_float(value, width),
29        Value::Decimal(value) => parse_float(&value.to_sql_string(), width),
30        other => Err(SQLError::TypeMismatch(format!(
31            "expected number, got {other:?}"
32        ))),
33    }
34}
35
36fn narrow_real(value: f64) -> Result<f32> {
37    let narrowed = value as f32;
38    if narrowed.is_infinite() && !value.is_infinite() {
39        return Err(range_error("overflow"));
40    }
41    if narrowed == 0.0 && value != 0.0 {
42        return Err(range_error("underflow"));
43    }
44    Ok(narrowed)
45}
46
47fn parse_float(input: &str, width: FloatWidth) -> Result<f64> {
48    let text = input.trim_matches(|c: char| c.is_ascii_whitespace());
49    let value = match width {
50        FloatWidth::Real => text.parse::<f32>().map(f64::from),
51        FloatWidth::DoublePrecision => text.parse::<f64>(),
52    }
53    .map_err(|_| SQLError::Routine {
54        sqlstate: "22P02".into(),
55        message: format!(
56            "invalid input syntax for type {}: \"{input}\"",
57            type_name(width)
58        ),
59    })?;
60    let special = text
61        .trim_start_matches(['+', '-'])
62        .eq_ignore_ascii_case("inf")
63        || text
64            .trim_start_matches(['+', '-'])
65            .eq_ignore_ascii_case("infinity");
66    let mantissa = text.split(['e', 'E']).next().unwrap_or(text);
67    let nonzero = mantissa.bytes().any(|byte| matches!(byte, b'1'..=b'9'));
68    if (value.is_infinite() && !special) || (value == 0.0 && nonzero) {
69        return Err(SQLError::Routine {
70            sqlstate: "22003".into(),
71            message: format!("\"{text}\" is out of range for type {}", type_name(width)),
72        });
73    }
74    Ok(value)
75}
76
77fn type_name(width: FloatWidth) -> &'static str {
78    match width {
79        FloatWidth::Real => "real",
80        FloatWidth::DoublePrecision => "double precision",
81    }
82}
83
84fn range_error(kind: &str) -> SQLError {
85    SQLError::Routine {
86        sqlstate: "22003".into(),
87        message: format!("value out of range: {kind}"),
88    }
89}
90
91/// Apply arithmetic at its resolved float width before widening the storage carrier.
92pub fn eval_float_arithmetic(
93    op: BinaryOp,
94    left: &Value,
95    right: &Value,
96    width: FloatWidth,
97) -> Result<Value> {
98    if matches!(left, Value::Null) || matches!(right, Value::Null) {
99        return Ok(Value::Null);
100    }
101    let left = to_float(left, width)?;
102    let right = to_float(right, width)?;
103    if matches!(op, BinaryOp::Divide) && right == 0.0 && !left.is_nan() {
104        return Err(division_by_zero());
105    }
106    let result = match width {
107        FloatWidth::Real => {
108            let left = left as f32;
109            let right = right as f32;
110            f64::from(match op {
111                BinaryOp::Add => left + right,
112                BinaryOp::Subtract => left - right,
113                BinaryOp::Multiply => left * right,
114                BinaryOp::Divide => left / right,
115                _ => return Err(non_arithmetic(op)),
116            })
117        }
118        FloatWidth::DoublePrecision => match op {
119            BinaryOp::Add => left + right,
120            BinaryOp::Subtract => left - right,
121            BinaryOp::Multiply => left * right,
122            BinaryOp::Divide => left / right,
123            _ => return Err(non_arithmetic(op)),
124        },
125    };
126    if result.is_infinite() && !left.is_infinite() && !right.is_infinite() {
127        return Err(range_error("overflow"));
128    }
129    if result == 0.0
130        && left != 0.0
131        && match op {
132            BinaryOp::Multiply => right != 0.0,
133            BinaryOp::Divide => !right.is_infinite(),
134            _ => false,
135        }
136    {
137        return Err(range_error("underflow"));
138    }
139    Ok(Value::Float(result))
140}
141
142fn non_arithmetic(op: BinaryOp) -> SQLError {
143    SQLError::Internal(format!(
144        "non-arithmetic operator {op:?} reached floating arithmetic"
145    ))
146}
147
148/// Format a real value using `PostgreSQL`'s shortest decimal and exponent thresholds.
149#[must_use]
150pub fn format_real(value: f32) -> String {
151    if value.is_nan() {
152        return "NaN".into();
153    }
154    if value.is_infinite() {
155        return if value.is_sign_negative() {
156            "-Infinity"
157        } else {
158            "Infinity"
159        }
160        .into();
161    }
162    let scientific = format!("{value:e}");
163    let (mantissa, exponent) = scientific.split_once('e').expect("scientific float output");
164    let exponent: i32 = exponent.parse().expect("scientific exponent");
165    if (-4..6).contains(&exponent) {
166        return value.to_string();
167    }
168    let sign = if exponent >= 0 { '+' } else { '-' };
169    format!("{mantissa}e{sign}{:02}", exponent.abs())
170}