@@ -102,20 +102,90 @@ impl LogFunc {
102102 }
103103}
104104
105- /// Generic function to calculate logarithm of a decimal value using the given base.
106- ///
107- /// Uses f64 computation which naturally returns NaN for invalid inputs
108- /// (base <= 1, non-finite, value <= 0), matching the behavior of `f64::log`.
109- fn log_decimal < T > ( value : T , scale : i8 , base : f64 ) -> Result < f64 , ArrowError >
110- where
111- T : ToPrimitive + Copy ,
112- {
113- decimal_to_f64 ( & value, scale) . map ( |v| v. log ( base) )
105+ /// Checks if the base is valid for the efficient integer logarithm algorithm.
106+ #[ inline]
107+ fn is_valid_integer_base ( base : f64 ) -> bool {
108+ base. trunc ( ) == base && base >= 2.0 && base <= u32:: MAX as f64
109+ }
110+
111+ /// Calculate logarithm for Decimal32 values.
112+ /// For integer bases >= 2 with non-negative scale, uses the efficient u32 ilog algorithm.
113+ /// Otherwise falls back to f64 computation.
114+ fn log_decimal32 ( value : i32 , scale : i8 , base : f64 ) -> Result < f64 , ArrowError > {
115+ if is_valid_integer_base ( base)
116+ && scale >= 0
117+ && let Some ( unscaled) = unscale_to_u32 ( value, scale)
118+ {
119+ return if unscaled > 0 {
120+ Ok ( unscaled. ilog ( base as u32 ) as f64 )
121+ } else {
122+ Ok ( f64:: NAN )
123+ } ;
124+ }
125+ decimal_to_f64 ( value, scale) . map ( |v| v. log ( base) )
126+ }
127+
128+ /// Calculate logarithm for Decimal64 values.
129+ /// For integer bases >= 2 with non-negative scale, uses the efficient u64 ilog algorithm.
130+ /// Otherwise falls back to f64 computation.
131+ fn log_decimal64 ( value : i64 , scale : i8 , base : f64 ) -> Result < f64 , ArrowError > {
132+ if is_valid_integer_base ( base)
133+ && scale >= 0
134+ && let Some ( unscaled) = unscale_to_u64 ( value, scale)
135+ {
136+ return if unscaled > 0 {
137+ Ok ( unscaled. ilog ( base as u64 ) as f64 )
138+ } else {
139+ Ok ( f64:: NAN )
140+ } ;
141+ }
142+ decimal_to_f64 ( value, scale) . map ( |v| v. log ( base) )
143+ }
144+
145+ /// Calculate logarithm for Decimal128 values.
146+ /// For integer bases >= 2 with non-negative scale, uses the efficient u128 ilog algorithm.
147+ /// Otherwise falls back to f64 computation.
148+ fn log_decimal128 ( value : i128 , scale : i8 , base : f64 ) -> Result < f64 , ArrowError > {
149+ if is_valid_integer_base ( base)
150+ && scale >= 0
151+ && let Some ( unscaled) = unscale_to_u128 ( value, scale)
152+ {
153+ return if unscaled > 0 {
154+ Ok ( unscaled. ilog ( base as u128 ) as f64 )
155+ } else {
156+ Ok ( f64:: NAN )
157+ } ;
158+ }
159+ decimal_to_f64 ( value, scale) . map ( |v| v. log ( base) )
160+ }
161+
162+ /// Unscale a Decimal32 value to u32.
163+ #[ inline]
164+ fn unscale_to_u32 ( value : i32 , scale : i8 ) -> Option < u32 > {
165+ let value_u32 = u32:: try_from ( value) . ok ( ) ?;
166+ let divisor = 10u32 . checked_pow ( scale as u32 ) ?;
167+ Some ( value_u32 / divisor)
168+ }
169+
170+ /// Unscale a Decimal64 value to u64.
171+ #[ inline]
172+ fn unscale_to_u64 ( value : i64 , scale : i8 ) -> Option < u64 > {
173+ let value_u64 = u64:: try_from ( value) . ok ( ) ?;
174+ let divisor = 10u64 . checked_pow ( scale as u32 ) ?;
175+ Some ( value_u64 / divisor)
176+ }
177+
178+ /// Unscale a Decimal128 value to u128.
179+ #[ inline]
180+ fn unscale_to_u128 ( value : i128 , scale : i8 ) -> Option < u128 > {
181+ let value_u128 = u128:: try_from ( value) . ok ( ) ?;
182+ let divisor = 10u128 . checked_pow ( scale as u32 ) ?;
183+ Some ( value_u128 / divisor)
114184}
115185
116186/// Convert a scaled decimal value to f64.
117187#[ inline]
118- fn decimal_to_f64 < T : ToPrimitive > ( value : & T , scale : i8 ) -> Result < f64 , ArrowError > {
188+ fn decimal_to_f64 < T : ToPrimitive + Copy > ( value : T , scale : i8 ) -> Result < f64 , ArrowError > {
119189 let value_f64 = value. to_f64 ( ) . ok_or_else ( || {
120190 ArrowError :: ComputeError ( "Cannot convert value to f64" . to_string ( ) )
121191 } ) ?;
@@ -126,7 +196,7 @@ fn decimal_to_f64<T: ToPrimitive>(value: &T, scale: i8) -> Result<f64, ArrowErro
126196fn log_decimal256 ( value : i256 , scale : i8 , base : f64 ) -> Result < f64 , ArrowError > {
127197 // Try to convert to i128 for the optimized path
128198 match value. to_i128 ( ) {
129- Some ( v) => log_decimal ( v, scale, base) ,
199+ Some ( v) => log_decimal128 ( v, scale, base) ,
130200 None => {
131201 // For very large Decimal256 values, use f64 computation
132202 let value_f64 = value. to_f64 ( ) . ok_or_else ( || {
@@ -228,21 +298,21 @@ impl ScalarUDFImpl for LogFunc {
228298 calculate_binary_math :: < Decimal32Type , Float64Type , Float64Type , _ > (
229299 & value,
230300 & base,
231- |value, base| log_decimal ( value, * scale, base) ,
301+ |value, base| log_decimal32 ( value, * scale, base) ,
232302 ) ?
233303 }
234304 DataType :: Decimal64 ( _, scale) => {
235305 calculate_binary_math :: < Decimal64Type , Float64Type , Float64Type , _ > (
236306 & value,
237307 & base,
238- |value, base| log_decimal ( value, * scale, base) ,
308+ |value, base| log_decimal64 ( value, * scale, base) ,
239309 ) ?
240310 }
241311 DataType :: Decimal128 ( _, scale) => {
242312 calculate_binary_math :: < Decimal128Type , Float64Type , Float64Type , _ > (
243313 & value,
244314 & base,
245- |value, base| log_decimal ( value, * scale, base) ,
315+ |value, base| log_decimal128 ( value, * scale, base) ,
246316 ) ?
247317 }
248318 DataType :: Decimal256 ( _, scale) => {
@@ -377,10 +447,13 @@ mod tests {
377447 #[ test]
378448 fn test_log_decimal_native ( ) {
379449 let value = 10_i128 . pow ( 35 ) ;
380- let expected = ( value as f64 ) . log2 ( ) ;
381- assert_eq ! ( expected, 116.26748332105768 ) ;
382- // Now using f64 computation, we get the precise value
383- assert ! ( ( log_decimal( value, 0 , 2.0 ) . unwrap( ) - expected) . abs( ) < 1e-10 ) ;
450+ assert_eq ! ( ( value as f64 ) . log2( ) , 116.26748332105768 ) ;
451+ assert_eq ! (
452+ log_decimal128( value, 0 , 2.0 ) . unwrap( ) ,
453+ // TODO: see we're losing our decimal points compared to above
454+ // https://github.com/apache/datafusion/issues/18524
455+ 116.0
456+ ) ;
384457 }
385458
386459 #[ test]
@@ -948,8 +1021,7 @@ mod tests {
9481021 assert ! ( ( floats. value( 1 ) - 2.0 ) . abs( ) < 1e-10 ) ;
9491022 assert ! ( ( floats. value( 2 ) - 3.0 ) . abs( ) < 1e-10 ) ;
9501023 assert ! ( ( floats. value( 3 ) - 4.0 ) . abs( ) < 1e-10 ) ;
951- // log10(12600) ≈ 4.1003 (not truncated to 4)
952- assert ! ( ( floats. value( 4 ) - 12600f64 . log10( ) ) . abs( ) < 1e-10 ) ;
1024+ assert ! ( ( floats. value( 4 ) - 4.0 ) . abs( ) < 1e-10 ) ; // Integer rounding
9531025 assert ! ( floats. value( 5 ) . is_nan( ) ) ;
9541026 }
9551027 ColumnarValue :: Scalar ( _) => {
@@ -1084,12 +1156,8 @@ mod tests {
10841156 assert ! ( ( floats. value( 1 ) - 2.0 ) . abs( ) < 1e-10 ) ;
10851157 assert ! ( ( floats. value( 2 ) - 3.0 ) . abs( ) < 1e-10 ) ;
10861158 assert ! ( ( floats. value( 3 ) - 4.0 ) . abs( ) < 1e-10 ) ;
1087- // log10(12600) ≈ 4.1003 (not truncated to 4)
1088- assert ! ( ( floats. value( 4 ) - 12600f64 . log10( ) ) . abs( ) < 1e-10 ) ;
1089- // log10(i128::MAX - 1000) ≈ 38.23 (not truncated to 38)
1090- assert ! (
1091- ( floats. value( 5 ) - ( ( i128 :: MAX - 1000 ) as f64 ) . log10( ) ) . abs( ) < 1e-10
1092- ) ;
1159+ assert ! ( ( floats. value( 4 ) - 4.0 ) . abs( ) < 1e-10 ) ; // Integer rounding for float log
1160+ assert ! ( ( floats. value( 5 ) - 38.0 ) . abs( ) < 1e-10 ) ;
10931161 assert ! ( floats. value( 6 ) . is_nan( ) ) ;
10941162 }
10951163 ColumnarValue :: Scalar ( _) => {
@@ -1098,6 +1166,40 @@ mod tests {
10981166 }
10991167 }
11001168
1169+ #[ test]
1170+ fn test_log_decimal128_invalid_base ( ) {
1171+ // Invalid base (-2.0) should return NaN, matching f64::log behavior
1172+ let arg_fields = vec ! [
1173+ Field :: new( "b" , DataType :: Float64 , false ) . into( ) ,
1174+ Field :: new( "x" , DataType :: Decimal128 ( 38 , 0 ) , false ) . into( ) ,
1175+ ] ;
1176+ let args = ScalarFunctionArgs {
1177+ args : vec ! [
1178+ ColumnarValue :: Scalar ( ScalarValue :: Float64 ( Some ( -2.0 ) ) ) , // base
1179+ ColumnarValue :: Scalar ( ScalarValue :: Decimal128 ( Some ( 64 ) , 38 , 0 ) ) , // num
1180+ ] ,
1181+ arg_fields,
1182+ number_rows : 1 ,
1183+ return_field : Field :: new ( "f" , DataType :: Float64 , true ) . into ( ) ,
1184+ config_options : Arc :: new ( ConfigOptions :: default ( ) ) ,
1185+ } ;
1186+ let result = LogFunc :: new ( )
1187+ . invoke_with_args ( args)
1188+ . expect ( "should not error on invalid base" ) ;
1189+
1190+ match result {
1191+ ColumnarValue :: Array ( arr) => {
1192+ let floats = as_float64_array ( & arr)
1193+ . expect ( "failed to convert result to a Float64Array" ) ;
1194+ assert_eq ! ( floats. len( ) , 1 ) ;
1195+ assert ! ( floats. value( 0 ) . is_nan( ) ) ;
1196+ }
1197+ ColumnarValue :: Scalar ( _) => {
1198+ panic ! ( "Expected an array value" )
1199+ }
1200+ }
1201+ }
1202+
11011203 #[ test]
11021204 fn test_log_decimal256_large ( ) {
11031205 // Large Decimal256 values that don't fit in i128 now use f64 fallback
0 commit comments