Skip to main content

mz_expr/scalar/func/impls/
numeric.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::fmt;
11
12use dec::{OrderedDecimal, Rounding};
13use mz_expr_derive::sqlfunc;
14use mz_repr::adt::numeric::{self, Numeric, NumericMaxScale};
15use mz_repr::{SqlColumnType, SqlScalarType, strconv};
16use serde::{Deserialize, Serialize};
17
18use crate::EvalError;
19use crate::scalar::func::EagerUnaryFunc;
20
21#[sqlfunc(
22    sqlname = "-",
23    preserves_uniqueness = true,
24    inverse = to_unary!(NegNumeric),
25    is_monotone = true
26)]
27fn neg_numeric(mut a: Numeric) -> Numeric {
28    numeric::cx_datum().neg(&mut a);
29    numeric::munge_numeric(&mut a).unwrap();
30    a
31}
32
33#[sqlfunc(sqlname = "abs")]
34fn abs_numeric(mut a: Numeric) -> Numeric {
35    numeric::cx_datum().abs(&mut a);
36    a
37}
38
39#[sqlfunc(sqlname = "ceilnumeric", is_monotone = true)]
40fn ceil_numeric(mut a: Numeric) -> Numeric {
41    // ceil will be nop if has no fractional digits.
42    if a.exponent() >= 0 {
43        return a;
44    }
45    let mut cx = numeric::cx_datum();
46    cx.set_rounding(Rounding::Ceiling);
47    cx.round(&mut a);
48    numeric::munge_numeric(&mut a).unwrap();
49    a
50}
51
52#[sqlfunc(sqlname = "expnumeric")]
53fn exp_numeric(mut a: Numeric) -> Result<Numeric, EvalError> {
54    let mut cx = numeric::cx_datum();
55    cx.exp(&mut a);
56    let cx_status = cx.status();
57    if cx_status.overflow() {
58        Err(EvalError::FloatOverflow)
59    } else if cx_status.subnormal() {
60        Err(EvalError::FloatUnderflow)
61    } else {
62        numeric::munge_numeric(&mut a).unwrap();
63        Ok(a)
64    }
65}
66
67#[sqlfunc(sqlname = "floornumeric", is_monotone = true)]
68fn floor_numeric(mut a: Numeric) -> Numeric {
69    // floor will be nop if has no fractional digits.
70    if a.exponent() >= 0 {
71        return a;
72    }
73    let mut cx = numeric::cx_datum();
74    cx.set_rounding(Rounding::Floor);
75    cx.round(&mut a);
76    numeric::munge_numeric(&mut a).unwrap();
77    a
78}
79
80fn log_guard_numeric(val: &Numeric, function_name: &str) -> Result<(), EvalError> {
81    if val.is_negative() {
82        return Err(EvalError::NegativeOutOfDomain(function_name.into()));
83    }
84    if val.is_zero() {
85        return Err(EvalError::ZeroOutOfDomain(function_name.into()));
86    }
87    Ok(())
88}
89
90// From the `decNumber` library's documentation:
91// > Inexact results will almost always be correctly rounded, but may be up to 1
92// > ulp (unit in last place) in error in rare cases.
93//
94// See decNumberLog10 documentation at http://speleotrove.com/decimal/dnnumb.html
95fn log_numeric<F>(mut a: Numeric, logic: F, name: &'static str) -> Result<Numeric, EvalError>
96where
97    F: Fn(&mut dec::Context<Numeric>, &mut Numeric),
98{
99    log_guard_numeric(&a, name)?;
100    let mut cx = numeric::cx_datum();
101    logic(&mut cx, &mut a);
102    numeric::munge_numeric(&mut a).unwrap();
103    Ok(a)
104}
105
106#[sqlfunc(sqlname = "lnnumeric")]
107fn ln_numeric(a: Numeric) -> Result<Numeric, EvalError> {
108    log_numeric(a, dec::Context::ln, "ln")
109}
110
111#[sqlfunc(sqlname = "log10numeric")]
112fn log10_numeric(a: Numeric) -> Result<Numeric, EvalError> {
113    log_numeric(a, dec::Context::log10, "log10")
114}
115
116#[sqlfunc(sqlname = "roundnumeric", is_monotone = true)]
117fn round_numeric(mut a: Numeric) -> Numeric {
118    // round will be nop if has no fractional digits.
119    if a.exponent() >= 0 {
120        return a;
121    }
122    numeric::cx_datum().round(&mut a);
123    // Canonicalize: `dec`'s round preserves the sign on zero results, so e.g.
124    // `round(-0.4)` yields `-0`. munge_numeric strips that, ensuring row
125    // encodings match decimal equality.
126    numeric::munge_numeric(&mut a).unwrap();
127    a
128}
129
130#[sqlfunc(sqlname = "truncnumeric", is_monotone = true)]
131fn trunc_numeric(mut a: Numeric) -> Numeric {
132    // trunc will be nop if has no fractional digits.
133    if a.exponent() >= 0 {
134        return a;
135    }
136    let mut cx = numeric::cx_datum();
137    cx.set_rounding(Rounding::Down);
138    cx.round(&mut a);
139    numeric::munge_numeric(&mut a).unwrap();
140    a
141}
142
143#[sqlfunc(sqlname = "sqrtnumeric")]
144fn sqrt_numeric(mut a: Numeric) -> Result<Numeric, EvalError> {
145    if a.is_negative() {
146        return Err(EvalError::NegSqrt);
147    }
148    let mut cx = numeric::cx_datum();
149    cx.sqrt(&mut a);
150    numeric::munge_numeric(&mut a).unwrap();
151    Ok(a)
152}
153
154#[sqlfunc(
155    sqlname = "numeric_to_smallint",
156    preserves_uniqueness = false,
157    inverse = to_unary!(super::CastInt16ToNumeric(None)),
158    is_monotone = true
159)]
160pub fn cast_numeric_to_int16(mut a: Numeric) -> Result<i16, EvalError> {
161    let mut cx = numeric::cx_datum();
162    cx.round(&mut a);
163    cx.clear_status();
164    let i = cx
165        .try_into_i32(a)
166        .or_else(|_| Err(EvalError::Int16OutOfRange(a.to_string().into())))?;
167    i16::try_from(i).or_else(|_| Err(EvalError::Int16OutOfRange(i.to_string().into())))
168}
169
170#[sqlfunc(
171    sqlname = "numeric_to_integer",
172    preserves_uniqueness = false,
173    inverse = to_unary!(super::CastInt32ToNumeric(None)),
174    is_monotone = true
175)]
176pub fn cast_numeric_to_int32(mut a: Numeric) -> Result<i32, EvalError> {
177    let mut cx = numeric::cx_datum();
178    cx.round(&mut a);
179    cx.clear_status();
180    cx.try_into_i32(a)
181        .or_else(|_| Err(EvalError::Int32OutOfRange(a.to_string().into())))
182}
183
184#[sqlfunc(
185    sqlname = "numeric_to_bigint",
186    preserves_uniqueness = false,
187    inverse = to_unary!(super::CastInt64ToNumeric(None)),
188    is_monotone = true
189)]
190pub fn cast_numeric_to_int64(mut a: Numeric) -> Result<i64, EvalError> {
191    let mut cx = numeric::cx_datum();
192    cx.round(&mut a);
193    cx.clear_status();
194    cx.try_into_i64(a)
195        .or_else(|_| Err(EvalError::Int64OutOfRange(a.to_string().into())))
196}
197
198#[sqlfunc(
199    sqlname = "numeric_to_real",
200    preserves_uniqueness = false,
201    inverse = to_unary!(super::CastFloat32ToNumeric(None)),
202    is_monotone = true
203)]
204pub fn cast_numeric_to_float32(a: Numeric) -> Result<f32, EvalError> {
205    let i = a.to_string().parse::<f32>().unwrap();
206    if i.is_infinite() {
207        Err(EvalError::Float32OutOfRange(i.to_string().into()))
208    } else {
209        Ok(i)
210    }
211}
212
213#[sqlfunc(
214    sqlname = "numeric_to_double",
215    preserves_uniqueness = false,
216    inverse = to_unary!(super::CastFloat64ToNumeric(None)),
217    is_monotone = true
218)]
219pub fn cast_numeric_to_float64(a: Numeric) -> Result<f64, EvalError> {
220    let i = a.to_string().parse::<f64>().unwrap();
221    if i.is_infinite() {
222        Err(EvalError::Float64OutOfRange(i.to_string().into()))
223    } else {
224        Ok(i)
225    }
226}
227
228#[sqlfunc(
229    sqlname = "numeric_to_text",
230    preserves_uniqueness = false,
231    inverse = to_unary!(super::CastStringToNumeric(None))
232)]
233fn cast_numeric_to_string(a: Numeric) -> String {
234    let mut buf = String::new();
235    strconv::format_numeric(&mut buf, &OrderedDecimal(a));
236    buf
237}
238
239#[sqlfunc(
240    sqlname = "numeric_to_uint2",
241    preserves_uniqueness = false,
242    inverse = to_unary!(super::CastUint16ToNumeric(None)),
243    is_monotone = true
244)]
245fn cast_numeric_to_uint16(mut a: Numeric) -> Result<u16, EvalError> {
246    let mut cx = numeric::cx_datum();
247    cx.round(&mut a);
248    cx.clear_status();
249    let u = cx
250        .try_into_u32(a)
251        .or_else(|_| Err(EvalError::UInt16OutOfRange(a.to_string().into())))?;
252    u16::try_from(u).or_else(|_| Err(EvalError::UInt16OutOfRange(u.to_string().into())))
253}
254
255#[sqlfunc(
256    sqlname = "numeric_to_uint4",
257    preserves_uniqueness = false,
258    inverse = to_unary!(super::CastUint32ToNumeric(None)),
259    is_monotone = true
260)]
261fn cast_numeric_to_uint32(mut a: Numeric) -> Result<u32, EvalError> {
262    let mut cx = numeric::cx_datum();
263    cx.round(&mut a);
264    cx.clear_status();
265    cx.try_into_u32(a)
266        .or_else(|_| Err(EvalError::UInt32OutOfRange(a.to_string().into())))
267}
268
269#[sqlfunc(
270    sqlname = "numeric_to_uint8",
271    preserves_uniqueness = false,
272    inverse = to_unary!(super::CastUint64ToNumeric(None)),
273    is_monotone = true
274)]
275fn cast_numeric_to_uint64(mut a: Numeric) -> Result<u64, EvalError> {
276    let mut cx = numeric::cx_datum();
277    cx.round(&mut a);
278    cx.clear_status();
279    cx.try_into_u64(a)
280        .or_else(|_| Err(EvalError::UInt64OutOfRange(a.to_string().into())))
281}
282
283#[sqlfunc(sqlname = "pg_size_pretty", preserves_uniqueness = false)]
284fn pg_size_pretty(mut a: Numeric) -> Result<String, EvalError> {
285    let mut cx = numeric::cx_datum();
286    let units = ["bytes", "kB", "MB", "GB", "TB", "PB"];
287
288    for (pos, unit) in units.iter().rev().skip(1).rev().enumerate() {
289        // return if abs(round(a)) < 10 in the next unit it would be converted to.
290        if Numeric::from(-10239.5) < a && a < Numeric::from(10239.5) {
291            // do not round a when the unit is bytes, as no conversion has happened.
292            if pos > 0 {
293                cx.round(&mut a);
294            }
295
296            return Ok(format!("{} {unit}", a.to_standard_notation_string()));
297        }
298
299        cx.div(&mut a, &Numeric::from(1024));
300        numeric::munge_numeric(&mut a).unwrap();
301    }
302
303    cx.round(&mut a);
304    Ok(format!(
305        "{} {}",
306        a.to_standard_notation_string(),
307        units.last().unwrap()
308    ))
309}
310
311#[derive(
312    Ord,
313    PartialOrd,
314    Clone,
315    Debug,
316    Eq,
317    PartialEq,
318    Serialize,
319    Deserialize,
320    Hash
321)]
322pub struct AdjustNumericScale(pub NumericMaxScale);
323
324impl EagerUnaryFunc for AdjustNumericScale {
325    type Input<'a> = Numeric;
326    type Output<'a> = Result<Numeric, EvalError>;
327
328    fn call<'a>(&self, mut d: Self::Input<'a>) -> Self::Output<'a> {
329        if numeric::rescale(&mut d, self.0.into_u8()).is_err() {
330            return Err(EvalError::NumericFieldOverflow);
331        };
332        Ok(d)
333    }
334
335    fn output_sql_type(&self, input: SqlColumnType) -> SqlColumnType {
336        SqlScalarType::Numeric {
337            max_scale: Some(self.0),
338        }
339        .nullable(input.nullable)
340    }
341}
342
343impl fmt::Display for AdjustNumericScale {
344    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
345        f.write_str("adjust_numeric_scale")
346    }
347}