Skip to main content

mz_expr/scalar/func/impls/
float64.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 chrono::{DateTime, Utc};
13use mz_expr_derive::sqlfunc;
14use mz_ore::cast::TryCastFrom;
15use mz_repr::adt::numeric::{self, Numeric, NumericMaxScale};
16use mz_repr::adt::timestamp::CheckedTimestamp;
17use mz_repr::{SqlColumnType, SqlScalarType, strconv};
18use serde::{Deserialize, Serialize};
19
20use crate::EvalError;
21use crate::scalar::DomainLimit;
22use crate::scalar::func::EagerUnaryFunc;
23
24#[sqlfunc(
25    sqlname = "-",
26    preserves_uniqueness = false,
27    inverse = to_unary!(NegFloat64),
28    is_monotone = true
29)]
30fn neg_float64(a: f64) -> f64 {
31    -a
32}
33
34#[sqlfunc(sqlname = "abs")]
35fn abs_float64(a: f64) -> f64 {
36    a.abs()
37}
38
39#[sqlfunc(sqlname = "roundf64")]
40fn round_float64(a: f64) -> f64 {
41    a.round_ties_even()
42}
43
44#[sqlfunc(sqlname = "truncf64")]
45fn trunc_float64(a: f64) -> f64 {
46    a.trunc()
47}
48
49#[sqlfunc(sqlname = "ceilf64")]
50fn ceil_float64(a: f64) -> f64 {
51    a.ceil()
52}
53
54#[sqlfunc(sqlname = "floorf64")]
55fn floor_float64(a: f64) -> f64 {
56    a.floor()
57}
58
59#[sqlfunc(
60    sqlname = "double_to_smallint",
61    preserves_uniqueness = false,
62    inverse = to_unary!(super::CastInt16ToFloat64),
63    is_monotone = true
64)]
65fn cast_float64_to_int16(a: f64) -> Result<i16, EvalError> {
66    let f = round_float64(a);
67    // TODO(benesch): remove potentially dangerous usage of `as`.
68    #[allow(clippy::as_conversions)]
69    if (f >= (i16::MIN as f64)) && (f < -(i16::MIN as f64)) {
70        Ok(f as i16)
71    } else {
72        Err(EvalError::Int16OutOfRange(f.to_string().into()))
73    }
74}
75
76#[sqlfunc(
77    sqlname = "double_to_integer",
78    preserves_uniqueness = false,
79    inverse = to_unary!(super::CastInt32ToFloat64),
80    is_monotone = true
81)]
82fn cast_float64_to_int32(a: f64) -> Result<i32, EvalError> {
83    let f = round_float64(a);
84    // This condition is delicate because i32::MIN can be represented exactly by
85    // an f64 but not i32::MAX. We follow PostgreSQL's approach here.
86    //
87    // See: https://github.com/postgres/postgres/blob/ca3b37487/src/include/c.h#L1074-L1096
88    // TODO(benesch): remove potentially dangerous usage of `as`.
89    #[allow(clippy::as_conversions)]
90    if (f >= (i32::MIN as f64)) && (f < -(i32::MIN as f64)) {
91        Ok(f as i32)
92    } else {
93        Err(EvalError::Int32OutOfRange(f.to_string().into()))
94    }
95}
96
97#[sqlfunc(
98    sqlname = "f64toi64",
99    preserves_uniqueness = false,
100    inverse = to_unary!(super::CastInt64ToFloat64),
101    is_monotone = true
102)]
103fn cast_float64_to_int64(a: f64) -> Result<i64, EvalError> {
104    let f = round_float64(a);
105    // This condition is delicate because i64::MIN can be represented exactly by
106    // an f64 but not i64::MAX. We follow PostgreSQL's approach here.
107    //
108    // See: https://github.com/postgres/postgres/blob/ca3b37487/src/include/c.h#L1074-L1096
109    // TODO(benesch): remove potentially dangerous usage of `as`.
110    #[allow(clippy::as_conversions)]
111    if (f >= (i64::MIN as f64)) && (f < -(i64::MIN as f64)) {
112        Ok(f as i64)
113    } else {
114        Err(EvalError::Int64OutOfRange(f.to_string().into()))
115    }
116}
117
118#[sqlfunc(
119    sqlname = "double_to_real",
120    preserves_uniqueness = false,
121    inverse = to_unary!(super::CastFloat32ToFloat64),
122    is_monotone = true
123)]
124fn cast_float64_to_float32(a: f64) -> Result<f32, EvalError> {
125    // TODO(benesch): remove potentially dangerous usage of `as`.
126    #[allow(clippy::as_conversions)]
127    let result = a as f32;
128    if result.is_infinite() && !a.is_infinite() {
129        Err(EvalError::FloatOverflow)
130    } else if result == 0.0 && a != 0.0 {
131        Err(EvalError::FloatUnderflow)
132    } else {
133        Ok(result)
134    }
135}
136
137#[sqlfunc(
138    sqlname = "double_to_text",
139    preserves_uniqueness = false,
140    inverse = to_unary!(super::CastStringToFloat64)
141)]
142fn cast_float64_to_string(a: f64) -> String {
143    let mut s = String::new();
144    strconv::format_float64(&mut s, a);
145    s
146}
147
148#[sqlfunc(
149    sqlname = "double_to_uint2",
150    preserves_uniqueness = false,
151    inverse = to_unary!(super::CastUint16ToFloat64),
152    is_monotone = true
153)]
154fn cast_float64_to_uint16(a: f64) -> Result<u16, EvalError> {
155    let f = round_float64(a);
156    // TODO(benesch): remove potentially dangerous usage of `as`.
157    #[allow(clippy::as_conversions)]
158    if (f >= 0.0) && (f <= (u16::MAX as f64)) {
159        Ok(f as u16)
160    } else {
161        Err(EvalError::UInt16OutOfRange(f.to_string().into()))
162    }
163}
164
165#[sqlfunc(
166    sqlname = "double_to_uint4",
167    preserves_uniqueness = false,
168    inverse = to_unary!(super::CastUint32ToFloat64),
169    is_monotone = true
170)]
171fn cast_float64_to_uint32(a: f64) -> Result<u32, EvalError> {
172    let f = round_float64(a);
173    // TODO(benesch): remove potentially dangerous usage of `as`.
174    #[allow(clippy::as_conversions)]
175    if (f >= 0.0) && (f <= (u32::MAX as f64)) {
176        Ok(f as u32)
177    } else {
178        Err(EvalError::UInt32OutOfRange(f.to_string().into()))
179    }
180}
181
182#[sqlfunc(
183    sqlname = "double_to_uint8",
184    preserves_uniqueness = false,
185    inverse = to_unary!(super::CastUint64ToFloat64),
186    is_monotone = true
187)]
188fn cast_float64_to_uint64(a: f64) -> Result<u64, EvalError> {
189    let f = round_float64(a);
190    // This condition is delicate because u64::MAX cannot be represented exactly by
191    // an f64: `u64::MAX as f64` rounds up to 2^64. A `<=` bound would therefore let
192    // 2^64 pass, and the subsequent `as u64` cast saturates it to u64::MAX. Comparing
193    // with `<` keeps 2^64 (and larger) out of range, mirroring the f64 -> i64 cast.
194    // TODO(benesch): remove potentially dangerous usage of `as`.
195    #[allow(clippy::as_conversions)]
196    if (f >= 0.0) && (f < (u64::MAX as f64)) {
197        Ok(f as u64)
198    } else {
199        Err(EvalError::UInt64OutOfRange(f.to_string().into()))
200    }
201}
202
203#[derive(
204    Ord,
205    PartialOrd,
206    Clone,
207    Debug,
208    Eq,
209    PartialEq,
210    Serialize,
211    Deserialize,
212    Hash
213)]
214pub struct CastFloat64ToNumeric(pub Option<NumericMaxScale>);
215
216impl EagerUnaryFunc for CastFloat64ToNumeric {
217    type Input<'a> = f64;
218    type Output<'a> = Result<Numeric, EvalError>;
219
220    fn call<'a>(&self, a: Self::Input<'a>) -> Self::Output<'a> {
221        if a.is_infinite() {
222            return Err(EvalError::InfinityOutOfDomain(
223                "casting double precision to numeric".into(),
224            ));
225        }
226        let mut a = Numeric::from(a);
227        if let Some(scale) = self.0 {
228            if numeric::rescale(&mut a, scale.into_u8()).is_err() {
229                return Err(EvalError::NumericFieldOverflow);
230            }
231        }
232        match numeric::munge_numeric(&mut a) {
233            Ok(_) => Ok(a),
234            Err(_) => Err(EvalError::NumericFieldOverflow),
235        }
236    }
237
238    fn output_sql_type(&self, input: SqlColumnType) -> SqlColumnType {
239        SqlScalarType::Numeric { max_scale: self.0 }.nullable(input.nullable)
240    }
241
242    fn inverse(&self) -> Option<crate::UnaryFunc> {
243        to_unary!(super::CastNumericToFloat64)
244    }
245
246    fn is_monotone(&self) -> bool {
247        true
248    }
249}
250
251impl fmt::Display for CastFloat64ToNumeric {
252    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
253        f.write_str("double_to_numeric")
254    }
255}
256
257#[sqlfunc(sqlname = "sqrtf64")]
258fn sqrt_float64(a: f64) -> Result<f64, EvalError> {
259    if a < 0.0 {
260        return Err(EvalError::NegSqrt);
261    }
262    Ok(a.sqrt())
263}
264
265#[sqlfunc(sqlname = "cbrtf64")]
266fn cbrt_float64(a: f64) -> f64 {
267    a.cbrt()
268}
269
270#[sqlfunc]
271fn cos(a: f64) -> Result<f64, EvalError> {
272    if a.is_infinite() {
273        return Err(EvalError::InfinityOutOfDomain("cos".into()));
274    }
275    Ok(a.cos())
276}
277
278#[sqlfunc]
279fn acos(a: f64) -> Result<f64, EvalError> {
280    if a < -1.0 || 1.0 < a {
281        return Err(EvalError::OutOfDomain(
282            DomainLimit::Inclusive(-1),
283            DomainLimit::Inclusive(1),
284            "acos".into(),
285        ));
286    }
287    Ok(a.acos())
288}
289
290#[sqlfunc]
291fn cosh(a: f64) -> f64 {
292    a.cosh()
293}
294
295#[sqlfunc]
296fn acosh(a: f64) -> Result<f64, EvalError> {
297    if a < 1.0 {
298        return Err(EvalError::OutOfDomain(
299            DomainLimit::Inclusive(1),
300            DomainLimit::None,
301            "acosh".into(),
302        ));
303    }
304    Ok(a.acosh())
305}
306
307#[sqlfunc]
308fn sin(a: f64) -> Result<f64, EvalError> {
309    if a.is_infinite() {
310        return Err(EvalError::InfinityOutOfDomain("sin".into()));
311    }
312    Ok(a.sin())
313}
314
315#[sqlfunc]
316fn asin(a: f64) -> Result<f64, EvalError> {
317    if a < -1.0 || 1.0 < a {
318        return Err(EvalError::OutOfDomain(
319            DomainLimit::Inclusive(-1),
320            DomainLimit::Inclusive(1),
321            "asin".into(),
322        ));
323    }
324    Ok(a.asin())
325}
326
327#[sqlfunc]
328fn sinh(a: f64) -> f64 {
329    a.sinh()
330}
331
332#[sqlfunc]
333fn asinh(a: f64) -> f64 {
334    a.asinh()
335}
336
337#[sqlfunc]
338fn tan(a: f64) -> Result<f64, EvalError> {
339    if a.is_infinite() {
340        return Err(EvalError::InfinityOutOfDomain("tan".into()));
341    }
342    Ok(a.tan())
343}
344
345#[sqlfunc]
346fn atan(a: f64) -> f64 {
347    a.atan()
348}
349
350#[sqlfunc]
351fn tanh(a: f64) -> f64 {
352    a.tanh()
353}
354
355#[sqlfunc]
356fn atanh(a: f64) -> Result<f64, EvalError> {
357    if a < -1.0 || 1.0 < a {
358        return Err(EvalError::OutOfDomain(
359            DomainLimit::Inclusive(-1),
360            DomainLimit::Inclusive(1),
361            "atanh".into(),
362        ));
363    }
364    Ok(a.atanh())
365}
366
367#[sqlfunc]
368fn cot(a: f64) -> Result<f64, EvalError> {
369    if a.is_infinite() {
370        return Err(EvalError::InfinityOutOfDomain("cot".into()));
371    }
372    Ok(1.0 / a.tan())
373}
374
375#[sqlfunc]
376fn radians(a: f64) -> f64 {
377    a.to_radians()
378}
379
380#[sqlfunc]
381fn degrees(a: f64) -> f64 {
382    a.to_degrees()
383}
384
385#[sqlfunc(sqlname = "log10f64")]
386fn log10(a: f64) -> Result<f64, EvalError> {
387    if a.is_sign_negative() {
388        return Err(EvalError::NegativeOutOfDomain("log10".into()));
389    }
390    if a == 0.0 {
391        return Err(EvalError::ZeroOutOfDomain("log10".into()));
392    }
393    Ok(a.log10())
394}
395
396#[sqlfunc(sqlname = "lnf64")]
397fn ln(a: f64) -> Result<f64, EvalError> {
398    if a.is_sign_negative() {
399        return Err(EvalError::NegativeOutOfDomain("ln".into()));
400    }
401    if a == 0.0 {
402        return Err(EvalError::ZeroOutOfDomain("ln".into()));
403    }
404    Ok(a.ln())
405}
406
407#[sqlfunc(sqlname = "expf64")]
408fn exp(a: f64) -> Result<f64, EvalError> {
409    let r = a.exp();
410    if r.is_infinite() {
411        return Err(EvalError::FloatOverflow);
412    }
413    if r == 0.0 {
414        return Err(EvalError::FloatUnderflow);
415    }
416    Ok(r)
417}
418
419#[sqlfunc(sqlname = "mz_sleep")]
420fn sleep(a: f64) -> Result<Option<CheckedTimestamp<DateTime<Utc>>>, EvalError> {
421    let duration = std::time::Duration::try_from_secs_f64(a).map_err(|_| {
422        let mut val = String::new();
423        strconv::format_float64(&mut val, a);
424        EvalError::InvalidParameterValue(format!("cannot sleep for {val} seconds").into())
425    })?;
426    std::thread::sleep(duration);
427    Ok(None)
428}
429
430#[sqlfunc(sqlname = "tots")]
431fn to_timestamp(f: f64) -> Result<CheckedTimestamp<DateTime<Utc>>, EvalError> {
432    const NANO_SECONDS_PER_SECOND: i64 = 1_000_000_000;
433    if f.is_nan() {
434        Err(EvalError::TimestampCannotBeNan)
435    } else if f.is_infinite() {
436        // TODO(jkosh44) implement infinite timestamps
437        Err(EvalError::TimestampOutOfRange)
438    } else {
439        let mut secs = i64::try_cast_from(f.trunc()).ok_or(EvalError::TimestampOutOfRange)?;
440        // NOTE(benesch): PostgreSQL has microsecond precision in its timestamps,
441        // while chrono has nanosecond precision. While we normally accept
442        // nanosecond precision, here we round to the nearest microsecond because
443        // f64s lose quite a bit of accuracy in the nanosecond digits when dealing
444        // with common Unix timestamp values (> 1 billion).
445        let microsecs = (f.fract() * 1_000_000.0).round();
446        let mut nanosecs =
447            i64::try_cast_from(microsecs * 1_000.0).ok_or(EvalError::TimestampOutOfRange)?;
448        if nanosecs < 0 {
449            secs = secs.checked_sub(1).ok_or(EvalError::TimestampOutOfRange)?;
450            nanosecs = NANO_SECONDS_PER_SECOND
451                .checked_add(nanosecs)
452                .ok_or(EvalError::TimestampOutOfRange)?;
453        }
454        // Ensure `nanosecs` is less than 1 second.
455        secs = secs
456            .checked_add(nanosecs / NANO_SECONDS_PER_SECOND)
457            .ok_or(EvalError::TimestampOutOfRange)?;
458        nanosecs %= NANO_SECONDS_PER_SECOND;
459        let nanosecs = u32::try_from(nanosecs).map_err(|_| EvalError::TimestampOutOfRange)?;
460        match DateTime::from_timestamp(secs, nanosecs) {
461            Some(dt) => {
462                CheckedTimestamp::from_timestamplike(dt).map_err(|_| EvalError::TimestampOutOfRange)
463            }
464            None => Err(EvalError::TimestampOutOfRange),
465        }
466    }
467}