Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 182 additions & 21 deletions datafusion/functions/src/math/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@ impl LogFunc {
}
}

/// Matches PostgreSQL: `log(0::float8)` is undefined (IEEE 754 would yield -inf).
const LOG_OF_ZERO_ERROR: &str = "cannot take logarithm of zero";

#[inline]
fn log_of_zero_error() -> ArrowError {
ArrowError::ComputeError(LOG_OF_ZERO_ERROR.to_string())
}

/// Compute `value.log(base)` after rejecting a zero value.
///
/// Only the value (the number being logged) is checked. A zero *base*
/// (`log(0, x)`) is left as IEEE 754 infinity / NaN; that is a separate
/// compatibility case from issue #22261.
#[inline]
fn compute_float_log<F: Float>(value: F, base: F) -> Result<F, ArrowError> {
if value == F::zero() {
return Err(log_of_zero_error());
}
Ok(value.log(base))
}

/// Checks if the base is valid for the efficient integer logarithm algorithm.
#[inline]
fn is_valid_integer_base(base: f64) -> bool {
Expand All @@ -110,6 +131,9 @@ fn is_valid_integer_base(base: f64) -> bool {
/// For integer bases >= 2 with zero scale, return an exact integer log when the
/// value is a perfect power of the base. Otherwise falls back to f64 computation.
fn log_decimal32(value: i32, scale: i8, base: f64) -> Result<f64, ArrowError> {
if value == 0 {
return Err(log_of_zero_error());
}
if scale == 0
&& is_valid_integer_base(base)
&& let Ok(unscaled) = u32::try_from(value)
Expand All @@ -128,6 +152,9 @@ fn log_decimal32(value: i32, scale: i8, base: f64) -> Result<f64, ArrowError> {
/// For integer bases >= 2 with zero scale, return an exact integer log when the
/// value is a perfect power of the base. Otherwise falls back to f64 computation.
fn log_decimal64(value: i64, scale: i8, base: f64) -> Result<f64, ArrowError> {
if value == 0 {
return Err(log_of_zero_error());
}
if scale == 0
&& is_valid_integer_base(base)
&& let Ok(unscaled) = u64::try_from(value)
Expand All @@ -146,6 +173,9 @@ fn log_decimal64(value: i64, scale: i8, base: f64) -> Result<f64, ArrowError> {
/// For integer bases >= 2 with zero scale, return an exact integer log when the
/// value is a perfect power of the base. Otherwise falls back to f64 computation.
fn log_decimal128(value: i128, scale: i8, base: f64) -> Result<f64, ArrowError> {
if value == 0 {
return Err(log_of_zero_error());
}
if scale == 0
&& is_valid_integer_base(base)
&& let Ok(unscaled) = u128::try_from(value)
Expand All @@ -171,6 +201,9 @@ fn decimal_to_f64<T: ToPrimitive + Copy>(value: T, scale: i8) -> Result<f64, Arr
}

fn log_decimal256(value: i256, scale: i8, base: f64) -> Result<f64, ArrowError> {
if value == i256::ZERO {
return Err(log_of_zero_error());
}
// Try to convert to i128 for the optimized path
match value.to_i128() {
Some(v) => log_decimal128(v, scale, base),
Expand Down Expand Up @@ -251,27 +284,24 @@ impl ScalarUDFImpl for LogFunc {
let value = value.to_array(args.number_rows)?;

let output: ArrayRef = match value.data_type() {
DataType::Float16 => {
calculate_binary_math::<Float16Type, Float16Type, Float16Type, _>(
&value,
&base,
|value, base| Ok(value.log(base)),
)?
}
DataType::Float32 => {
calculate_binary_math::<Float32Type, Float32Type, Float32Type, _>(
&value,
&base,
|value, base| Ok(value.log(base)),
)?
}
DataType::Float64 => {
calculate_binary_math::<Float64Type, Float64Type, Float64Type, _>(
&value,
&base,
|value, base| Ok(value.log(base)),
)?
}
DataType::Float16 => calculate_binary_math::<
Float16Type,
Float16Type,
Float16Type,
_,
>(&value, &base, compute_float_log)?,
DataType::Float32 => calculate_binary_math::<
Float32Type,
Float32Type,
Float32Type,
_,
>(&value, &base, compute_float_log)?,
DataType::Float64 => calculate_binary_math::<
Float64Type,
Float64Type,
Float64Type,
_,
>(&value, &base, compute_float_log)?,
DataType::Decimal32(_, scale) => {
calculate_binary_math::<Decimal32Type, Float64Type, Float64Type, _>(
&value,
Expand Down Expand Up @@ -1213,4 +1243,135 @@ mod tests {
}
}
}

fn invoke_log(
args: Vec<ColumnarValue>,
data_types: Vec<DataType>,
) -> Result<ColumnarValue> {
let number_rows = args
.iter()
.map(|a| match a {
ColumnarValue::Array(arr) => arr.len(),
ColumnarValue::Scalar(_) => 1,
})
.max()
.unwrap_or(1);
let arg_fields = data_types
.into_iter()
.map(|dt| Field::new("a", dt, false).into())
.collect();
let args = ScalarFunctionArgs {
args,
arg_fields,
number_rows,
return_field: Field::new("f", DataType::Float64, true).into(),
config_options: Arc::new(ConfigOptions::default()),
};
LogFunc::new().invoke_with_args(args)
}

fn assert_log_of_zero(err: datafusion_common::DataFusionError) {
let message = err.to_string();
assert!(
message.contains(LOG_OF_ZERO_ERROR),
"expected '{LOG_OF_ZERO_ERROR}' in error, got {message}"
);
}

#[test]
fn test_log_zero_float64_unary_errors() {
let err = invoke_log(
vec![ColumnarValue::Scalar(ScalarValue::Float64(Some(0.0)))],
vec![DataType::Float64],
)
.expect_err("log(0.0) should be a domain error");
assert_log_of_zero(err);
}

#[test]
fn test_log_negative_zero_float64_errors() {
let err = invoke_log(
vec![ColumnarValue::Scalar(ScalarValue::Float64(Some(-0.0)))],
vec![DataType::Float64],
)
.expect_err("log(-0.0) should be a domain error");
assert_log_of_zero(err);
}

#[test]
fn test_log_zero_float32_unary_errors() {
let err = invoke_log(
vec![ColumnarValue::Scalar(ScalarValue::Float32(Some(0.0)))],
vec![DataType::Float32],
)
.expect_err("log(0.0f32) should be a domain error");
assert_log_of_zero(err);
}

#[test]
fn test_log_zero_float64_binary_errors() {
let err = invoke_log(
vec![
ColumnarValue::Scalar(ScalarValue::Float64(Some(10.0))),
ColumnarValue::Scalar(ScalarValue::Float64(Some(0.0))),
],
vec![DataType::Float64, DataType::Float64],
)
.expect_err("log(10, 0.0) should be a domain error");
assert_log_of_zero(err);
}

#[test]
fn test_log_zero_array_errors() {
let err = invoke_log(
vec![ColumnarValue::Array(Arc::new(Float64Array::from(vec![
10.0, 0.0, 100.0,
])))],
vec![DataType::Float64],
)
.expect_err("log() of an array containing 0 should be a domain error");
assert_log_of_zero(err);
}

#[test]
fn test_log_zero_decimal128_errors() {
let err = invoke_log(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see the new coverage for Float32/64 and Decimal128/256. Since this change also touches the separate Float16, Decimal32, and Decimal64 branches, could we add a small table-driven unit test that exercises every changed physical type?
That should help catch branch-specific regressions in the future.

vec![ColumnarValue::Scalar(ScalarValue::Decimal128(
Some(0),
38,
0,
))],
vec![DataType::Decimal128(38, 0)],
)
.expect_err("log(0::decimal) should be a domain error");
assert_log_of_zero(err);
}

#[test]
fn test_log_zero_decimal256_errors() {
let err = invoke_log(
vec![ColumnarValue::Scalar(ScalarValue::Decimal256(
Some(i256::ZERO),
DECIMAL256_MAX_PRECISION,
0,
))],
vec![DataType::Decimal256(DECIMAL256_MAX_PRECISION, 0)],
)
.expect_err("log(0::decimal256) should be a domain error");
assert_log_of_zero(err);
}

#[test]
fn test_log_zero_base_is_not_this_issue() {
// log(0, 64) takes log of 64 (nonzero). A zero *base* is out of
// scope for #22261 and keeps the previous IEEE result.
invoke_log(
vec![
ColumnarValue::Scalar(ScalarValue::Float64(Some(0.0))),
ColumnarValue::Scalar(ScalarValue::Float64(Some(64.0))),
],
vec![DataType::Float64, DataType::Float64],
)
.expect("zero base should not error in this change");
}
}
20 changes: 20 additions & 0 deletions datafusion/sqllogictest/test_files/math.slt
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,26 @@ select
----
39.0625 Float64

# log(0::float8) - issue #22261
query error DataFusion error: Arrow error: Compute error: cannot take logarithm of zero
SELECT log(0.0::float8)

# two-arg form: logarithm of zero value
query error DataFusion error: Arrow error: Compute error: cannot take logarithm of zero
SELECT log(10.0::float8, 0.0::float8)

# negative zero is still zero
query error DataFusion error: Arrow error: Compute error: cannot take logarithm of zero
SELECT log((-0.0)::float8)

# column / non-literal path
query error DataFusion error: Arrow error: Compute error: cannot take logarithm of zero
SELECT log(x) FROM (VALUES (0.0::float8)) AS t(x)

# decimal zero
query error DataFusion error: Arrow error: Compute error: cannot take logarithm of zero
SELECT log(0::decimal(10, 2))

# factorial negative (PostgreSQL-compatible domain error)
query error DataFusion error: Execution error: factorial of a negative number is undefined
select factorial(-1);
Expand Down
11 changes: 8 additions & 3 deletions datafusion/sqllogictest/test_files/scalar.slt
Original file line number Diff line number Diff line change
Expand Up @@ -644,10 +644,15 @@ select log(2, 2.0/3) a, log(10, 2.0/3) b;

# log scalar ops with zero edgecases
# please see https://github.com/apache/datafusion/pull/5245#issuecomment-1426828382
query RR rowsort
select log(0) a, log(1, 64) b;
# log(0) is a domain error (PostgreSQL compatibility, #22261)
query error cannot take logarithm of zero
select log(0);

# log(1, 64) remains IEEE infinity (base-1 is a separate issue)
query R
select log(1, 64);
----
-Infinity Infinity
Infinity

# log with columns #1
query RRR rowsort
Expand Down
19 changes: 8 additions & 11 deletions docs/source/user-guide/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,14 @@ but these operators always return a `bool` which makes them not work with the ex
| trunc(x) | truncate toward zero |

:::{note}
Unlike to some databases the math functions in Datafusion works the same way as Rust math functions, avoiding failing on corner cases e.g.

```sql
select log(-1), log(0), sqrt(-1);
+----------------+---------------+-----------------+
| log(Int64(-1)) | log(Int64(0)) | sqrt(Int64(-1)) |
+----------------+---------------+-----------------+
| NaN | -inf | NaN |
+----------------+---------------+-----------------+
```

Most math functions in DataFusion follow Rust / IEEE 754 semantics for
corner cases. For example `log(-1)` returns `NaN`. Some domain errors
match PostgreSQL instead and fail the query:

* `log(0)` / `log(0.0::float8)` — cannot take logarithm of zero
* `sqrt` of a negative number
* `power(0, negative)`
* `factorial` of a negative number
:::

## Conditional Expressions
Expand Down
Loading