From 5dcd6bd75b15526df54cefe78aee64dc8c09d7be Mon Sep 17 00:00:00 2001 From: Moritz Hoffmann Date: Wed, 23 Sep 2026 20:35:52 +0200 Subject: [PATCH] expr: Convert CastFloat64ToNumeric to the sqlfunc macro `CastFloat64ToNumeric` carries an optional `NumericMaxScale`, so the annotated function takes a `&self` receiver and the macro generates `call` as a method on the existing struct instead of defining a unit struct. The hand-written `Display` wrote the fixed string `double_to_numeric` without reading the payload, so `sqlname` reproduces it and the impl is deleted. The macro then generates `FuncName` with `NAME` from the annotated function's name, which is the `"cast_float64_to_numeric"` the manual `func_name!` entry declared, so that entry is deleted to avoid a duplicate impl. Every method the deleted `EagerUnaryFunc` impl overrode is accounted for: * `call`: the annotated function's body, unchanged. * `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and the `input_type` binding. The generated tail ORs in `propagates_nulls && input_type.nullable`, and `propagates_nulls` derives to `true` from the non-nullable `f64` input, so the result is the same `nullable(input_type.nullable)` the deleted body returned. * `inverse`: carried across. * `is_monotone`: carried across. `introduces_nulls` is not overridden: `Result` derives `false`, which is what the impl inherited. `could_error` was not overridden and keeps deriving to `true` from the `Result` output, as do `propagates_nulls` and `preserves_uniqueness`. The registry reports only `sqlfunc_signature` moving, on both the unscaled and the scaled `cast_float64_to_numeric` record. No declared property changed. Co-Authored-By: Claude Opus 5 (1M context) --- .../tests/snapshots/func_registry.json | 4 +- .../snapshots/func_registry_digests.json | 2 +- .../tests/snapshots/func_registry_source.json | 4 +- src/expr/src/scalar/func.rs | 1 - src/expr/src/scalar/func/impls/float64.rs | 62 +++++++------------ 5 files changed, 27 insertions(+), 46 deletions(-) diff --git a/src/compute-types/tests/snapshots/func_registry.json b/src/compute-types/tests/snapshots/func_registry.json index f6a7b6e5495d4..c5db42990d5de 100644 --- a/src/compute-types/tests/snapshots/func_registry.json +++ b/src/compute-types/tests/snapshots/func_registry.json @@ -276,8 +276,8 @@ "cast_float64_to_int16": {"variant":"CastFloat64ToInt16","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_int16_to_float64","input_types":["Float64:NotNull"],"output_type":"Int16:NotNull","sqlfunc_signature":"fn(f64) -> Result"}, "cast_float64_to_int32": {"variant":"CastFloat64ToInt32","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_int32_to_float64","input_types":["Float64:NotNull"],"output_type":"Int32:NotNull","sqlfunc_signature":"fn(f64) -> Result"}, "cast_float64_to_int64": {"variant":"CastFloat64ToInt64","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_int64_to_float64","input_types":["Float64:NotNull"],"output_type":"Int64:NotNull","sqlfunc_signature":"fn(f64) -> Result"}, - "cast_float64_to_numeric": {"variant":"CastFloat64ToNumeric","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_numeric_to_float64","input_types":["Float64:NotNull"],"output_type":"Numeric { max_scale: None }:NotNull","sqlfunc_signature":null}, - "cast_float64_to_numeric[scale]": {"variant":"CastFloat64ToNumeric","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_numeric_to_float64","input_types":["Float64:NotNull"],"output_type":"Numeric { max_scale: Some(NumericMaxScale(2)) }:NotNull","sqlfunc_signature":null}, + "cast_float64_to_numeric": {"variant":"CastFloat64ToNumeric","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_numeric_to_float64","input_types":["Float64:NotNull"],"output_type":"Numeric { max_scale: None }:NotNull","sqlfunc_signature":"fn(f64) -> Result"}, + "cast_float64_to_numeric[scale]": {"variant":"CastFloat64ToNumeric","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_numeric_to_float64","input_types":["Float64:NotNull"],"output_type":"Numeric { max_scale: Some(NumericMaxScale(2)) }:NotNull","sqlfunc_signature":"fn(f64) -> Result"}, "cast_float64_to_string": {"variant":"CastFloat64ToString","propagates_nulls":true,"introduces_nulls":false,"could_error":false,"preserves_uniqueness":false,"is_monotone":false,"is_eliminable_cast":false,"inverse":"cast_string_to_float64","input_types":["Float64:NotNull"],"output_type":"String:NotNull","sqlfunc_signature":"fn(f64) -> String"}, "cast_float64_to_uint16": {"variant":"CastFloat64ToUint16","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_uint16_to_float64","input_types":["Float64:NotNull"],"output_type":"UInt16:NotNull","sqlfunc_signature":"fn(f64) -> Result"}, "cast_float64_to_uint32": {"variant":"CastFloat64ToUint32","propagates_nulls":true,"introduces_nulls":false,"could_error":true,"preserves_uniqueness":false,"is_monotone":true,"is_eliminable_cast":false,"inverse":"cast_uint32_to_float64","input_types":["Float64:NotNull"],"output_type":"UInt32:NotNull","sqlfunc_signature":"fn(f64) -> Result"}, diff --git a/src/compute-types/tests/snapshots/func_registry_digests.json b/src/compute-types/tests/snapshots/func_registry_digests.json index f3a204324ac44..ea7c4546375fc 100644 --- a/src/compute-types/tests/snapshots/func_registry_digests.json +++ b/src/compute-types/tests/snapshots/func_registry_digests.json @@ -1,3 +1,3 @@ { - "1": "sha256:0fc0e0c7157e370bba0b67c9c83b5b0fe080306c11740407c87a5a4d6f305ba7" + "1": "sha256:141689446c30d1b9b7dac2584e000a4024930bf4affd401c40069fd81427a9f9" } diff --git a/src/compute-types/tests/snapshots/func_registry_source.json b/src/compute-types/tests/snapshots/func_registry_source.json index 1f856d5050044..61ee0ea0132ac 100644 --- a/src/compute-types/tests/snapshots/func_registry_source.json +++ b/src/compute-types/tests/snapshots/func_registry_source.json @@ -276,8 +276,8 @@ "cast_float64_to_int16": {"display":"double_to_smallint","sqlfunc_decl":"#[sqlfunc(sqlname = \"double_to_smallint\", preserves_uniqueness = false, inverse = super::CastInt16ToFloat64, is_monotone = true)] fn cast_float64_to_int16(a: f64) -> Result","body_fingerprint":"7b6999b1278d8e02"}, "cast_float64_to_int32": {"display":"double_to_integer","sqlfunc_decl":"#[sqlfunc(sqlname = \"double_to_integer\", preserves_uniqueness = false, inverse = super::CastInt32ToFloat64, is_monotone = true)] fn cast_float64_to_int32(a: f64) -> Result","body_fingerprint":"3e35668ff852238a"}, "cast_float64_to_int64": {"display":"f64toi64","sqlfunc_decl":"#[sqlfunc(sqlname = \"f64toi64\", preserves_uniqueness = false, inverse = super::CastInt64ToFloat64, is_monotone = true)] fn cast_float64_to_int64(a: f64) -> Result","body_fingerprint":"eff404954eb17e94"}, - "cast_float64_to_numeric": {"display":"double_to_numeric","sqlfunc_decl":null,"body_fingerprint":null}, - "cast_float64_to_numeric[scale]": {"display":"double_to_numeric","sqlfunc_decl":null,"body_fingerprint":null}, + "cast_float64_to_numeric": {"display":"double_to_numeric","sqlfunc_decl":"#[sqlfunc(CastFloat64ToNumeric, sqlname = \"double_to_numeric\", inverse = super::CastNumericToFloat64, is_monotone = true, output_type_expr = SqlScalarType::Numeric {max_scale: self.0}.nullable(input_type.nullable))] fn cast_float64_to_numeric(&self, a: f64) -> Result","body_fingerprint":"67dd94453626a5b9"}, + "cast_float64_to_numeric[scale]": {"display":"double_to_numeric","sqlfunc_decl":"#[sqlfunc(CastFloat64ToNumeric, sqlname = \"double_to_numeric\", inverse = super::CastNumericToFloat64, is_monotone = true, output_type_expr = SqlScalarType::Numeric {max_scale: self.0}.nullable(input_type.nullable))] fn cast_float64_to_numeric(&self, a: f64) -> Result","body_fingerprint":"67dd94453626a5b9"}, "cast_float64_to_string": {"display":"double_to_text","sqlfunc_decl":"#[sqlfunc(sqlname = \"double_to_text\", preserves_uniqueness = false, inverse = super::CastStringToFloat64)] fn cast_float64_to_string(a: f64) -> String","body_fingerprint":"ed1946d3de6a3555"}, "cast_float64_to_uint16": {"display":"double_to_uint2","sqlfunc_decl":"#[sqlfunc(sqlname = \"double_to_uint2\", preserves_uniqueness = false, inverse = super::CastUint16ToFloat64, is_monotone = true)] fn cast_float64_to_uint16(a: f64) -> Result","body_fingerprint":"9cdb81df1272809a"}, "cast_float64_to_uint32": {"display":"double_to_uint4","sqlfunc_decl":"#[sqlfunc(sqlname = \"double_to_uint4\", preserves_uniqueness = false, inverse = super::CastUint32ToFloat64, is_monotone = true)] fn cast_float64_to_uint32(a: f64) -> Result","body_fingerprint":"1fd96b053ec266ec"}, diff --git a/src/expr/src/scalar/func.rs b/src/expr/src/scalar/func.rs index 75f42cc8d4915..2716440fb0b79 100644 --- a/src/expr/src/scalar/func.rs +++ b/src/expr/src/scalar/func.rs @@ -142,7 +142,6 @@ func_name! { CastArrayToString => "cast_array_to_string", CastDateToTimestamp => "cast_date_to_timestamp", CastDateToTimestampTz => "cast_date_to_timestamp_tz", - CastFloat64ToNumeric => "cast_float64_to_numeric", CastInt32ToNumeric => "cast_int32_to_numeric", CastInt64ToNumeric => "cast_int64_to_numeric", CastJsonbToNumeric => "cast_jsonb_to_numeric", diff --git a/src/expr/src/scalar/func/impls/float64.rs b/src/expr/src/scalar/func/impls/float64.rs index d616898494f51..7fe60e01a41a3 100644 --- a/src/expr/src/scalar/func/impls/float64.rs +++ b/src/expr/src/scalar/func/impls/float64.rs @@ -7,19 +7,16 @@ // the Business Source License, use of this software will be governed // by the Apache License, Version 2.0. -use std::fmt; - use chrono::{DateTime, Utc}; use mz_expr_derive::sqlfunc; use mz_ore::cast::TryCastFrom; use mz_repr::adt::numeric::{self, Numeric, NumericMaxScale}; use mz_repr::adt::timestamp::CheckedTimestamp; -use mz_repr::{RowArena, SqlColumnType, SqlScalarType, strconv}; +use mz_repr::{SqlScalarType, strconv}; use serde::{Deserialize, Serialize}; use crate::EvalError; use crate::scalar::DomainLimit; -use crate::scalar::func::EagerUnaryFunc; #[sqlfunc( sqlname = "-", @@ -213,44 +210,29 @@ fn cast_float64_to_uint64(a: f64) -> Result { )] pub struct CastFloat64ToNumeric(pub Option); -impl EagerUnaryFunc for CastFloat64ToNumeric { - type Input<'a> = f64; - type Output<'a> = Result; - - fn call<'a>(&self, a: Self::Input<'a>, _temp_storage: &'a RowArena) -> Self::Output<'a> { - if a.is_infinite() { - return Err(EvalError::InfinityOutOfDomain( - "casting double precision to numeric".into(), - )); - } - let mut a = Numeric::from(a); - if let Some(scale) = self.0 { - if numeric::rescale(&mut a, scale.into_u8()).is_err() { - return Err(EvalError::NumericFieldOverflow); - } - } - match numeric::munge_numeric(&mut a) { - Ok(_) => Ok(a), - Err(_) => Err(EvalError::NumericFieldOverflow), - } - } - - fn output_sql_type(&self, input: SqlColumnType) -> SqlColumnType { - SqlScalarType::Numeric { max_scale: self.0 }.nullable(input.nullable) - } - - fn inverse(&self) -> Option { - to_unary!(super::CastNumericToFloat64) +#[sqlfunc( + CastFloat64ToNumeric, + sqlname = "double_to_numeric", + inverse = super::CastNumericToFloat64, + is_monotone = true, + output_type_expr = SqlScalarType::Numeric { max_scale: self.0 } + .nullable(input_type.nullable) +)] +fn cast_float64_to_numeric(&self, a: f64) -> Result { + if a.is_infinite() { + return Err(EvalError::InfinityOutOfDomain( + "casting double precision to numeric".into(), + )); } - - fn is_monotone(&self) -> bool { - true + let mut a = Numeric::from(a); + if let Some(scale) = self.0 { + if numeric::rescale(&mut a, scale.into_u8()).is_err() { + return Err(EvalError::NumericFieldOverflow); + } } -} - -impl fmt::Display for CastFloat64ToNumeric { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("double_to_numeric") + match numeric::munge_numeric(&mut a) { + Ok(_) => Ok(a), + Err(_) => Err(EvalError::NumericFieldOverflow), } }