Skip to main content

mz_expr/scalar/func/impls/
int32.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 mz_expr_derive::sqlfunc;
13use mz_ore::cast::ReinterpretCast;
14use mz_repr::adt::numeric::{self, Numeric, NumericMaxScale};
15use mz_repr::adt::system::{Oid, PgLegacyChar};
16use mz_repr::{SqlColumnType, SqlScalarType, strconv};
17use serde::{Deserialize, Serialize};
18
19use crate::EvalError;
20use crate::scalar::func::EagerUnaryFunc;
21
22#[sqlfunc(
23    sqlname = "-",
24    preserves_uniqueness = true,
25    inverse = to_unary!(NegInt32),
26    is_monotone = true
27)]
28fn neg_int32(a: i32) -> Result<i32, EvalError> {
29    a.checked_neg()
30        .ok_or_else(|| EvalError::Int32OutOfRange(a.to_string().into()))
31}
32
33#[sqlfunc(
34    sqlname = "~",
35    preserves_uniqueness = true,
36    inverse = to_unary!(BitNotInt32)
37)]
38fn bit_not_int32(a: i32) -> i32 {
39    !a
40}
41
42#[sqlfunc(sqlname = "abs")]
43fn abs_int32(a: i32) -> Result<i32, EvalError> {
44    a.checked_abs()
45        .ok_or_else(|| EvalError::Int32OutOfRange(a.to_string().into()))
46}
47
48#[sqlfunc(
49    sqlname = "integer_to_boolean",
50    preserves_uniqueness = false,
51    inverse = to_unary!(super::CastBoolToInt32)
52)]
53fn cast_int32_to_bool(a: i32) -> bool {
54    a != 0
55}
56
57#[sqlfunc(
58    sqlname = "integer_to_real",
59    preserves_uniqueness = false,
60    inverse = to_unary!(super::CastFloat32ToInt32),
61    is_monotone = true
62)]
63fn cast_int32_to_float32(a: i32) -> f32 {
64    // TODO(benesch): remove potentially dangerous usage of `as`.
65    #[allow(clippy::as_conversions)]
66    {
67        a as f32
68    }
69}
70
71#[sqlfunc(
72    sqlname = "integer_to_double",
73    preserves_uniqueness = true,
74    inverse = to_unary!(super::CastFloat64ToInt32),
75    is_monotone = true
76)]
77fn cast_int32_to_float64(a: i32) -> f64 {
78    f64::from(a)
79}
80
81#[sqlfunc(
82    sqlname = "integer_to_smallint",
83    preserves_uniqueness = true,
84    inverse = to_unary!(super::CastInt16ToInt32),
85    is_monotone = true
86)]
87fn cast_int32_to_int16(a: i32) -> Result<i16, EvalError> {
88    i16::try_from(a).or_else(|_| Err(EvalError::Int16OutOfRange(a.to_string().into())))
89}
90
91#[sqlfunc(
92    sqlname = "integer_to_bigint",
93    preserves_uniqueness = true,
94    inverse = to_unary!(super::CastInt64ToInt32),
95    is_monotone = true
96)]
97fn cast_int32_to_int64(a: i32) -> i64 {
98    i64::from(a)
99}
100
101#[sqlfunc(
102    sqlname = "integer_to_text",
103    preserves_uniqueness = true,
104    inverse = to_unary!(super::CastStringToInt32)
105)]
106fn cast_int32_to_string(a: i32) -> String {
107    let mut buf = String::new();
108    strconv::format_int32(&mut buf, a);
109    buf
110}
111
112#[sqlfunc(
113    sqlname = "integer_to_uint2",
114    preserves_uniqueness = true,
115    inverse = to_unary!(super::CastUint16ToInt32),
116    is_monotone = true
117)]
118fn cast_int32_to_uint16(a: i32) -> Result<u16, EvalError> {
119    u16::try_from(a).or_else(|_| Err(EvalError::UInt16OutOfRange(a.to_string().into())))
120}
121
122#[sqlfunc(
123    sqlname = "integer_to_uint4",
124    preserves_uniqueness = true,
125    inverse = to_unary!(super::CastUint32ToInt32),
126    is_monotone = true
127)]
128fn cast_int32_to_uint32(a: i32) -> Result<u32, EvalError> {
129    u32::try_from(a).or_else(|_| Err(EvalError::UInt32OutOfRange(a.to_string().into())))
130}
131
132#[sqlfunc(
133    sqlname = "integer_to_uint8",
134    preserves_uniqueness = true,
135    inverse = to_unary!(super::CastUint64ToInt32),
136    is_monotone = true
137)]
138fn cast_int32_to_uint64(a: i32) -> Result<u64, EvalError> {
139    u64::try_from(a).or_else(|_| Err(EvalError::UInt64OutOfRange(a.to_string().into())))
140}
141
142#[derive(
143    Ord,
144    PartialOrd,
145    Clone,
146    Debug,
147    Eq,
148    PartialEq,
149    Serialize,
150    Deserialize,
151    Hash
152)]
153pub struct CastInt32ToNumeric(pub Option<NumericMaxScale>);
154
155impl EagerUnaryFunc for CastInt32ToNumeric {
156    type Input<'a> = i32;
157    type Output<'a> = Result<Numeric, EvalError>;
158
159    fn call<'a>(&self, a: Self::Input<'a>) -> Self::Output<'a> {
160        let mut a = Numeric::from(a);
161        if let Some(scale) = self.0 {
162            if numeric::rescale(&mut a, scale.into_u8()).is_err() {
163                return Err(EvalError::NumericFieldOverflow);
164            }
165        }
166        // Besides `rescale`, cast is infallible.
167        Ok(a)
168    }
169
170    fn output_sql_type(&self, input: SqlColumnType) -> SqlColumnType {
171        SqlScalarType::Numeric { max_scale: self.0 }.nullable(input.nullable)
172    }
173
174    fn could_error(&self) -> bool {
175        self.0.is_some()
176    }
177
178    fn inverse(&self) -> Option<crate::UnaryFunc> {
179        to_unary!(super::CastNumericToInt32)
180    }
181
182    fn is_monotone(&self) -> bool {
183        true
184    }
185}
186
187impl fmt::Display for CastInt32ToNumeric {
188    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
189        f.write_str("integer_to_numeric")
190    }
191}
192
193#[sqlfunc(
194    sqlname = "integer_to_oid",
195    preserves_uniqueness = true,
196    inverse = to_unary!(super::CastOidToInt32)
197)]
198fn cast_int32_to_oid(a: i32) -> Oid {
199    // For historical reasons in PostgreSQL, the bytes of the `i32` are
200    // reinterpreted as a `u32` without bounds checks, so negative `i32`s
201    // become very large positive OIDs.
202    //
203    // Do not use this as a model for behavior in other contexts. OIDs
204    // should not in general be thought of as freely convertible from
205    // `i32`s.
206    Oid(u32::reinterpret_cast(a))
207}
208
209#[sqlfunc(
210    sqlname = "integer_to_\"char\"",
211    preserves_uniqueness = true,
212    inverse = to_unary!(super::CastPgLegacyCharToInt32)
213)]
214fn cast_int32_to_pg_legacy_char(a: i32) -> Result<PgLegacyChar, EvalError> {
215    // Per PostgreSQL, casts to `PgLegacyChar` are performed as if
216    // `PgLegacyChar` is signed.
217    // See: https://github.com/postgres/postgres/blob/791b1b71da35d9d4264f72a87e4078b85a2fcfb4/src/backend/utils/adt/char.c#L91-L96
218    let a = i8::try_from(a).map_err(|_| EvalError::CharOutOfRange)?;
219    Ok(PgLegacyChar(u8::reinterpret_cast(a)))
220}
221
222#[sqlfunc]
223fn chr(a: i32) -> Result<String, EvalError> {
224    // This error matches the behavior of Postgres 13/14 (and potentially earlier versions)
225    // Postgres 15 will have a different error message for negative values
226    let codepoint = u32::try_from(a).map_err(|_| EvalError::CharacterTooLargeForEncoding(a))?;
227    if codepoint == 0 {
228        Err(EvalError::NullCharacterNotPermitted)
229    } else if 0xd800 <= codepoint && codepoint < 0xe000 {
230        // Postgres returns a different error message for inputs in this range
231        Err(EvalError::CharacterNotValidForEncoding(a))
232    } else {
233        char::from_u32(codepoint)
234            .map(|u| u.to_string())
235            .ok_or(EvalError::CharacterTooLargeForEncoding(a))
236    }
237}