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