-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Fix regression for negative-scale decimal128 in log #19315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Jefffrey
merged 6 commits into
apache:main
from
shifluxxc:fix-negative-scale-decimal-log
Dec 19, 2025
Merged
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6f0c72f
Fix regression for negative-scale decimals in log
shifluxxc ad51962
simplified log evaluation
shifluxxc d19a3d4
simplification of code structure
shifluxxc 6171303
Apply suggestion from @Jefffrey
Jefffrey 2eacdf0
Update datafusion/functions/src/math/log.rs
shifluxxc b190136
Apply suggestion from @Jefffrey
Jefffrey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -116,13 +116,38 @@ fn log_decimal128(value: i128, scale: i8, base: f64) -> Result<f64, ArrowError> | |
| ))); | ||
| } | ||
|
|
||
| let unscaled_value = decimal128_to_i128(value, scale)?; | ||
| if unscaled_value > 0 { | ||
| let log_value: u32 = unscaled_value.ilog(base as i128); | ||
| Ok(log_value as f64) | ||
| // Handle negative scales using logarithmic property: | ||
| // log_base(value * 10^(-scale)) = log_base(value) + (-scale) * log_base(10) | ||
| if scale < 0 { | ||
| // For negative scale, the actual value is value * 10^(-scale) where -scale > 0 | ||
| // Use property: log_base(a * 10^(-s)) = log_base(a) + (-s) * log_base(10) | ||
| if value > 0 { | ||
| let value_f64 = value as f64; | ||
|
|
||
| // Compute log_base(value) - use natural log and convert | ||
| // log_base(x) = ln(x) / ln(base) | ||
| let log_value = value_f64.ln() / base.ln(); | ||
|
|
||
| // Add the adjustment: (-scale) * log_base(10) | ||
| // log_base(10) = ln(10) / ln(base) | ||
| let log_10_base = 10.0_f64.ln() / base.ln(); | ||
| let adjustment = (-scale as f64) * log_10_base; | ||
|
||
|
|
||
| Ok(log_value + adjustment) | ||
| } else { | ||
| // Reflect f64::log behaviour | ||
| Ok(f64::NAN) | ||
Jefffrey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } else { | ||
| // Reflect f64::log behaviour | ||
| Ok(f64::NAN) | ||
| // For non-negative scales, use existing logic | ||
| let unscaled_value = decimal128_to_i128(value, scale)?; | ||
| if unscaled_value > 0 { | ||
| let log_value: u32 = unscaled_value.ilog(base as i128); | ||
| Ok(log_value as f64) | ||
| } else { | ||
| // Reflect f64::log behaviour | ||
| Ok(f64::NAN) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -291,6 +316,49 @@ impl ScalarUDFImpl for LogFunc { | |
| } | ||
Jefffrey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let number = args.pop().unwrap(); | ||
| let number_datatype = arg_types.pop().unwrap(); | ||
|
|
||
| // Check if base has negative scale (if provided) | ||
| let base_has_negative_scale = if num_args == 2 { | ||
Jefffrey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if let Some( | ||
| DataType::Decimal32(_, scale) | ||
| | DataType::Decimal64(_, scale) | ||
| | DataType::Decimal128(_, scale) | ||
| | DataType::Decimal256(_, scale), | ||
| ) = arg_types.last() | ||
| { | ||
| *scale < 0 | ||
| } else { | ||
| false | ||
| } | ||
| } else { | ||
| false | ||
| }; | ||
|
|
||
| // Skip simplification for negative scale decimals as ScalarValue doesn't support them yet | ||
| let has_negative_scale = match &number_datatype { | ||
| DataType::Decimal32(_, scale) | ||
| | DataType::Decimal64(_, scale) | ||
| | DataType::Decimal128(_, scale) | ||
| | DataType::Decimal256(_, scale) => *scale < 0, | ||
| _ => false, | ||
| }; | ||
|
|
||
| if has_negative_scale || base_has_negative_scale { | ||
Jefffrey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let args = match num_args { | ||
| 1 => vec![number], | ||
| 2 => { | ||
| let base = args.pop().unwrap(); | ||
| vec![base, number] | ||
| } | ||
| _ => { | ||
| return internal_err!( | ||
| "Unexpected number of arguments in log::simplify" | ||
| ); | ||
| } | ||
| }; | ||
| return Ok(ExprSimplifyResult::Original(args)); | ||
| } | ||
|
|
||
| // default to base 10 | ||
| let base = if let Some(base) = args.pop() { | ||
| base | ||
|
|
@@ -1125,4 +1193,120 @@ mod tests { | |
| "Arrow error: Not yet implemented: Log of Decimal256 larger than Decimal128 is not yet supported: 170141183460469231731687303715884106727" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_log_decimal128_negative_scale() { | ||
Jefffrey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Test log with negative scale: value=100, scale=-2 means 100 * 10^2 = 10000 | ||
| // log10(10000) = 4.0 | ||
| let arg_field = Field::new("a", DataType::Decimal128(38, -2), false).into(); | ||
| let args = ScalarFunctionArgs { | ||
| args: vec![ | ||
| ColumnarValue::Array(Arc::new( | ||
| Decimal128Array::from(vec![100, 1000, 10]) | ||
| .with_precision_and_scale(38, -2) | ||
| .unwrap(), | ||
| )), // num: represents 10000, 100000, 1000 | ||
| ], | ||
| arg_fields: vec![arg_field], | ||
| number_rows: 3, | ||
| return_field: Field::new("f", DataType::Float64, true).into(), | ||
| config_options: Arc::new(ConfigOptions::default()), | ||
| }; | ||
| let result = LogFunc::new() | ||
| .invoke_with_args(args) | ||
| .expect("failed to initialize function log"); | ||
|
|
||
| match result { | ||
| ColumnarValue::Array(arr) => { | ||
| let floats = as_float64_array(&arr) | ||
| .expect("failed to convert result to a Float64Array"); | ||
|
|
||
| assert_eq!(floats.len(), 3); | ||
| // log10(10000) = 4.0 | ||
| assert!((floats.value(0) - 4.0).abs() < 1e-10); | ||
| // log10(100000) = 5.0 | ||
| assert!((floats.value(1) - 5.0).abs() < 1e-10); | ||
| // log10(1000) = 3.0 | ||
| assert!((floats.value(2) - 3.0).abs() < 1e-10); | ||
| } | ||
| ColumnarValue::Scalar(_) => { | ||
| panic!("Expected an array value") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_log_decimal128_negative_scale_base2() { | ||
| // Test log base 2 with negative scale: value=8, scale=-1 means 8 * 10^1 = 80 | ||
| // log2(80) ≈ 6.321928 | ||
| let arg_fields = vec![ | ||
| Field::new("b", DataType::Float64, false).into(), | ||
| Field::new("x", DataType::Decimal128(38, -1), false).into(), | ||
| ]; | ||
| let args = ScalarFunctionArgs { | ||
| args: vec![ | ||
| ColumnarValue::Scalar(ScalarValue::Float64(Some(2.0))), // base | ||
| ColumnarValue::Array(Arc::new( | ||
| Decimal128Array::from(vec![8, 16]) | ||
| .with_precision_and_scale(38, -1) | ||
| .unwrap(), | ||
| )), // num: represents 80, 160 | ||
| ], | ||
| arg_fields, | ||
| number_rows: 2, | ||
| return_field: Field::new("f", DataType::Float64, true).into(), | ||
| config_options: Arc::new(ConfigOptions::default()), | ||
| }; | ||
| let result = LogFunc::new() | ||
| .invoke_with_args(args) | ||
| .expect("failed to initialize function log"); | ||
|
|
||
| match result { | ||
| ColumnarValue::Array(arr) => { | ||
| let floats = as_float64_array(&arr) | ||
| .expect("failed to convert result to a Float64Array"); | ||
|
|
||
| assert_eq!(floats.len(), 2); | ||
| // log2(80) ≈ 6.321928 | ||
| assert!((floats.value(0) - 80.0_f64.log2()).abs() < 1e-10); | ||
| // log2(160) ≈ 7.321928 | ||
| assert!((floats.value(1) - 160.0_f64.log2()).abs() < 1e-10); | ||
| } | ||
| ColumnarValue::Scalar(_) => { | ||
| panic!("Expected an array value") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_log_decimal128_negative_scale_scalar() { | ||
| // Test scalar with negative scale | ||
| let arg_field = Field::new("a", DataType::Decimal128(38, -3), false).into(); | ||
| let args = ScalarFunctionArgs { | ||
| args: vec![ | ||
| ColumnarValue::Scalar(ScalarValue::Decimal128(Some(5), 38, -3)), // num: represents 5000 | ||
| ], | ||
| arg_fields: vec![arg_field], | ||
| number_rows: 1, | ||
| return_field: Field::new("f", DataType::Float64, true).into(), | ||
| config_options: Arc::new(ConfigOptions::default()), | ||
| }; | ||
| let result = LogFunc::new() | ||
| .invoke_with_args(args) | ||
| .expect("failed to initialize function log"); | ||
|
|
||
| match result { | ||
| ColumnarValue::Array(arr) => { | ||
| let floats = as_float64_array(&arr) | ||
| .expect("failed to convert result to a Float64Array"); | ||
|
|
||
| assert_eq!(floats.len(), 1); | ||
| // log10(5000) ≈ 3.69897 | ||
| assert!((floats.value(0) - 5000.0_f64.log10()).abs() < 1e-10); | ||
| } | ||
| ColumnarValue::Scalar(_) => { | ||
| panic!("Expected an array value") | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.