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