Skip to content

fix: preserve precision in log() with mixed-width float arguments - #23676

Open
U0001F3A2 wants to merge 7 commits into
apache:mainfrom
U0001F3A2:fix/22581-log-mixed-float-precision
Open

U0001F3A2 wants to merge 7 commits into
apache:mainfrom
U0001F3A2:fix/22581-log-mixed-float-precision

Conversation

@U0001F3A2

@U0001F3A2 U0001F3A2 commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

log(base, value) coerced each float argument on its own and computed in the value's type. A base wider than the value was narrowed to the value's float and lost precision:

SELECT log(arrow_cast(16777217,'Float64'), arrow_cast(2.0,'Float32'));  -- 0.041666668           (Float32)
SELECT log(arrow_cast(16777217,'Float64'), arrow_cast(2.0,'Float64'));  -- 0.041666666517376175  (Float64)

16777217 is not representable in f32, so narrowing the base rounds it.

What changes are included in this PR?

  • The (float, float) signature is split into width-specific signatures (suggested by @Jefffrey), so type coercion widens both arguments to the wider float:
    • (Float16, Float16)
    • (Float32, Float32), also accepting Float16
    • (Float64, Float64), accepting any numeric
  • The unary and decimal-value signatures, return_type and invoke_with_args are unchanged.
  • simplify also folds typed NULL literals. The new signatures coerce an untyped NULL argument to a typed NULL, and without this log(NULL, c2) loses its plan-time NULL fold.
  • Upgrade guide entry for the result type change.

Are these changes tested?

Yes.

  • Unit tests in log.rs for the coercion matrix (mixed widths, integer, decimal, dictionary and NULL arguments) and the typed NULL simplification. Both fail without the change.
  • math.slt: the log(base, val) loses precision when value is Float32 #22581 repro plus mixed, homogeneous and NULL cases. order.slt and scalar.slt expectations are updated for the widened types.

The full sqllogictest suite, cargo test -p datafusion-functions --lib, cargo fmt and cargo clippy -D warnings pass locally.

Are there any user-facing changes?

Yes, the result type and value change when the float arguments have different widths.

  • log(16777217::float8, 2.0::float4) was 0.041666668 (Float32) and is now 0.041666666517376175 (Float64).
  • An integer or decimal base with a Float16/Float32 value now returns Float64, e.g. log(2, x::float).

Same-width float arguments and decimal values are unchanged. No Rust API change.

@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) functions Changes to functions implementation labels Jul 17, 2026
@U0001F3A2

Copy link
Copy Markdown
Contributor Author

Hi @alamb — first-time contributor here, so CI needs a committer to trigger it. Could you kick it off when you have a moment? Thanks!

@alamb

alamb commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot added the core Core DataFusion crate label Jul 18, 2026
@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jul 26, 2026
@codecov-commenter

codecov-commenter commented Jul 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.63014% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 82.91%. Comparing base (862843f) to head (37d3fdb).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/functions/src/math/log.rs 98.63% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #23676      +/-   ##
==========================================
- Coverage   82.91%   82.91%   -0.01%     
==========================================
  Files        1155     1155              
  Lines      458091   458161      +70     
  Branches   458091   458161      +70     
==========================================
+ Hits       379821   379877      +56     
- Misses      55231    55239       +8     
- Partials    23039    23045       +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

U0001F3A2 and others added 3 commits July 28, 2026 02:57
`log(base, value)` resolved its result type and computed only from the
value (the last argument). When the base was a wider float than the
value, `calculate_binary_math` narrowed the base to the value's type,
so `log(Float64, Float32)` computed in f32 and lost precision:

  log(16777217::float8, 2.0::float4) => 0.041666668           (Float32)
  log(16777217::float8, 2.0::float8) => 0.041666666517376175  (Float64)

Resolve the result type to the widest float among all arguments and
compute there, widening the value to match instead of narrowing the
base. The `log(base, 1)` / `log(a, a)` simplifications now emit their
literal with that resolved type too, so a wider value no longer leaves
the optimizer with a mismatched plan schema. Integer/decimal arguments
already coerce to Float64; null arguments do not constrain the width.
Homogeneous float widths and the decimal-value paths are unchanged.

Existing math.slt and scalar.slt cases that asserted the narrowed type
for `log(Float64 base, narrower value)` are updated, since they encoded
the bug.

Closes apache#22581

Co-Authored-By: Claude <noreply@anthropic.com>
The log() simplifications `log(x, 1) ===> 0` and `log(x, x) ===> 1` now
emit a constant carrying log's resolved Float64 return type rather than
the base column's Int64 type (issue apache#22581), keeping the simplified
literal consistent with the function's output schema. Update the
corresponding golden expectations in the core integration test that were
still asserting Int64.

Co-Authored-By: Claude <noreply@anthropic.com>
Keep log execution aligned with its planned return type and cover unary Float16 log.

Co-Authored-By: Claude <noreply@anthropic.com>
@U0001F3A2
U0001F3A2 force-pushed the fix/22581-log-mixed-float-precision branch from d58ac21 to e6aef70 Compare July 27, 2026 18:48
@Jefffrey

Copy link
Copy Markdown
Contributor

would something like this also work without needing to modify logic other than the signature?

pub fn new() -> Self {
    // Converts decimals & integers to float64, accepting other floats as is
    let as_float = Coercion::new_implicit(
        TypeSignatureClass::Float,
        vec![TypeSignatureClass::Numeric],
        NativeType::Float64,
    );

    let float16 = Coercion::new_exact(TypeSignatureClass::Native(logical_float16()));
    let float32 = Coercion::new_implicit_native(
        logical_float32(),
        vec![TypeSignatureClass::Native(logical_float16())],
    );
    let float64 = Coercion::new_implicit_native(
        logical_float64(),
        vec![TypeSignatureClass::Numeric],
    );

    Self {
        signature: Signature::one_of(
            // Ensure decimals have precedence over floats since we have
            // a native decimal implementation for log
            vec![
                // log(value)
                TypeSignature::Coercible(vec![Coercion::new_exact(
                    TypeSignatureClass::Decimal,
                )]),
                TypeSignature::Coercible(vec![as_float.clone()]),
                // log(base, value)
                TypeSignature::Coercible(vec![
                    as_float.clone(),
                    Coercion::new_exact(TypeSignatureClass::Decimal),
                ]),
                // Widen floats to the widest type
                TypeSignature::Coercible(vec![float16.clone(), float16.clone()]), // accepts only f16,f16
                TypeSignature::Coercible(vec![float32.clone(), float32.clone()]), // accepts mix of f32 & f16, promoting both to f32
                TypeSignature::Coercible(vec![float64.clone(), float64.clone()]), // both promoted to f64 for all other inputs
            ],
            Volatility::Immutable,
        ),
    }
}

@github-actions

Copy link
Copy Markdown

Thank you for your contribution. Unfortunately, this pull request is stale because it has been open 60 days with no activity. Please remove the stale label or comment or this will be closed in 7 days.

@github-actions github-actions Bot added the Stale PR has not had any activity for some time label Oct 16, 2026
@U0001F3A2

Copy link
Copy Markdown
Contributor Author

Thanks for the suggestion! I tested the width-specific signatures, and they fix the execution issue, so the runtime casts can be removed. The return-type and simplification fixes are still needed: simplify can see uncoerced arguments, and log(Float16 base, Float64 1) otherwise produces a Float16 zero. I also added typed-NULL handling so log(NULL, 1) stays NULL. The local candidate passes 339 function tests and the math/scalar SQL tests.

@github-actions github-actions Bot removed the Stale PR has not had any activity for some time label Oct 17, 2026
U0001F3A2 and others added 4 commits October 19, 2026 18:14
…loat-precision

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the widest-float return type and runtime cast with width-specific
coercible signatures, so mixed-width float arguments are widened during
type coercion. The Float16/32/64 signatures coerce a NULL argument to a
typed NULL literal, so simplify now also folds typed NULL literals to keep
plans like log(NULL, c2) constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c11 is Float32 and c12 is Float64, so log() now widens c11 to Float64 during
coercion. The sorts are still kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add integer, decimal, dictionary, and NULL cases to the coercion test, a
Float32 typed NULL to the simplify test, and Float16/Float32 and NULL/NULL
SQL cases. Add an upgrade guide entry for the log() result type change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@U0001F3A2

Copy link
Copy Markdown
Contributor Author

@Jefffrey pushed your signature approach in 37d3fdb (after merging main). It fixes the types and the precision on its own, so the widest-float return_type and the runtime cast are gone. invoke_with_args and return_type now match main.

This corrects my last comment. The simplification change isn't needed anymore, because #25138 removed the log(a, 1) / log(a, a) rewrites, so it dropped out in the merge.

One small addition was still needed. The Native(...) coercions turn an untyped NULL into a typed one, so log(NULL, c2) became log(Float64(NULL), c2) and lost its plan-time NULL fold (the existing EXPLAIN case in math.slt). simplify now also folds typed NULL literals.

Other expectation changes:

  • order.slt: two plans now show CAST(c11 AS Float64) (c11 is Float32, c12 is Float64). The sorts are still kept.
  • log(2.5::float, NULL) stays Float64, same as main. My previous revision had changed it to Float32.

I also added an upgrade guide entry for the result type change. The full sqllogictest suite, datafusion-functions lib tests and clippy pass locally.

@github-actions github-actions Bot removed the auto detected api change Auto detected API change label Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Core DataFusion crate functions Changes to functions implementation sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

log(base, val) loses precision when value is Float32

4 participants