You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/07/26 03:27:18 UTC

(datafusion) 01/01: feat: add Spark-compatible hypot function (#23774)

This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-23774-e3e2cb227928ffa498c2845db6ce2aa86ee174b4
in repository https://gitbox.apache.org/repos/asf/datafusion.git

commit e660118996630bf7173b31a67e97dee97dac1fe4
Author: Karpagam Balasubramaniam <kk...@trulyh.com>
AuthorDate: Sat Jul 25 20:26:54 2026 -0700

    feat: add Spark-compatible hypot function (#23774)
    
    ## Which issue does this PR close?
    
    - Closes #23770
    - Part of #15914
    
    ## Rationale for this change
    
    Spark provides [`hypot(expr1,
    expr2)`](https://spark.apache.org/docs/latest/api/sql/#hypot), which
    returns `sqrt(expr1^2 + expr2^2)` computed without intermediate overflow
    or underflow. It was not yet implemented in `datafusion-spark` — only an
    auto-generated test stub existed at `spark/math/hypot.slt` with its
    query commented out.
    
    ## What changes are included in this PR?
    
    - Add `SparkHypot` (implementing `ScalarUDFImpl`) in
    `datafusion/spark/src/function/math/hypot.rs`, backed by Rust's
    `f64::hypot` — the same overflow-safe algorithm as Java/Spark's
    `Math.hypot`.
    - Register it in `datafusion/spark/src/function/math/mod.rs`.
    - Enable the `hypot.slt` sqllogictest.
    
    The signature is `exact(Float64, Float64) -> Float64`, following the
    `datafusion-spark` convention of only accepting types Spark supports.
    Computation uses the Arrow `binary` kernel so NULL in either argument
    propagates to a NULL result, matching Spark.
    
    ## Are these changes tested?
    
    Yes — `datafusion/sqllogictest/test_files/spark/math/hypot.slt` covers:
    - scalar Pythagorean triples (`hypot(3, 4)` → 5, `hypot(5, 12)` → 13),
    - double inputs,
    - NULL propagation when either argument is NULL,
    - the array path (including a NULL row),
    - overflow-safety: `hypot(3e200, 4e200)` stays finite, whereas a naive
    `sqrt(a^2 + b^2)` would overflow to `Infinity`.
    
    ## Are there any user-facing changes?
    
    Yes — adds the Spark-compatible `hypot` scalar function to
    `datafusion-spark`. No breaking changes to public APIs.
---
 datafusion/spark/src/function/math/hypot.rs        |  84 +++++++++++++++
 datafusion/spark/src/function/math/mod.rs          |   4 +
 .../sqllogictest/test_files/spark/math/hypot.slt   | 116 ++++++++++++++++++++-
 3 files changed, 200 insertions(+), 4 deletions(-)

diff --git a/datafusion/spark/src/function/math/hypot.rs b/datafusion/spark/src/function/math/hypot.rs
new file mode 100644
index 0000000000..a1e30a7e4a
--- /dev/null
+++ b/datafusion/spark/src/function/math/hypot.rs
@@ -0,0 +1,84 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::sync::Arc;
+
+use arrow::array::{ArrayRef, AsArray, Float64Array};
+use arrow::compute::kernels::arity::binary;
+use arrow::datatypes::{DataType, Float64Type};
+use datafusion_common::Result;
+use datafusion_common::utils::take_function_args;
+use datafusion_expr::{
+    ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
+};
+use datafusion_functions::utils::make_scalar_function;
+
+/// Spark-compatible `hypot` function.
+///
+/// <https://spark.apache.org/docs/latest/api/sql/index.html#hypot>
+///
+/// Returns `sqrt(expr1^2 + expr2^2)` computed without intermediate overflow or
+/// underflow, matching Spark's use of `java.lang.Math.hypot`.
+#[derive(Debug, PartialEq, Eq, Hash)]
+pub struct SparkHypot {
+    signature: Signature,
+}
+
+impl Default for SparkHypot {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl SparkHypot {
+    pub fn new() -> Self {
+        Self {
+            // Spark only defines hypot over doubles
+            signature: Signature::exact(
+                vec![DataType::Float64, DataType::Float64],
+                Volatility::Immutable,
+            ),
+        }
+    }
+}
+
+impl ScalarUDFImpl for SparkHypot {
+    fn name(&self) -> &str {
+        "hypot"
+    }
+
+    fn signature(&self) -> &Signature {
+        &self.signature
+    }
+
+    fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
+        Ok(DataType::Float64)
+    }
+
+    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
+        make_scalar_function(spark_hypot, vec![])(&args.args)
+    }
+}
+
+fn spark_hypot(args: &[ArrayRef]) -> Result<ArrayRef> {
+    let [x, y] = take_function_args("hypot", args)?;
+
+    let x = x.as_primitive::<Float64Type>();
+    let y = y.as_primitive::<Float64Type>();
+    let result: Float64Array = binary(x, y, |a, b| a.hypot(b))?;
+    Ok(Arc::new(result))
+}
diff --git a/datafusion/spark/src/function/math/mod.rs b/datafusion/spark/src/function/math/mod.rs
index 0079ef0fc9..fb57b536f2 100644
--- a/datafusion/spark/src/function/math/mod.rs
+++ b/datafusion/spark/src/function/math/mod.rs
@@ -22,6 +22,7 @@ pub mod expm1;
 pub mod factorial;
 pub mod floor;
 pub mod hex;
+pub mod hypot;
 pub mod modulus;
 pub mod negative;
 pub mod pow;
@@ -41,6 +42,7 @@ make_udf_function!(expm1::SparkExpm1, expm1);
 make_udf_function!(factorial::SparkFactorial, factorial);
 make_udf_function!(floor::SparkFloor, floor);
 make_udf_function!(hex::SparkHex, hex);
+make_udf_function!(hypot::SparkHypot, hypot);
 make_udf_function!(modulus::SparkMod, modulus);
 make_udf_function!(modulus::SparkPmod, pmod);
 make_udf_function!(pow::SparkPow, pow);
@@ -66,6 +68,7 @@ pub mod expr_fn {
     ));
     export_functions!((floor, "Returns floor of expr.", arg1));
     export_functions!((hex, "Computes hex value of the given column.", arg1));
+    export_functions!((hypot, "Returns sqrt(a^2 + b^2) without intermediate overflow or underflow.", arg1 arg2));
     export_functions!((modulus, "Returns the remainder of division of the first argument by the second argument.", arg1 arg2));
     export_functions!((pmod, "Returns the positive remainder of division of the first argument by the second argument.", arg1 arg2));
     export_functions!((
@@ -107,6 +110,7 @@ pub fn functions() -> Vec<Arc<ScalarUDF>> {
         factorial(),
         floor(),
         hex(),
+        hypot(),
         modulus(),
         pmod(),
         pow(),
diff --git a/datafusion/sqllogictest/test_files/spark/math/hypot.slt b/datafusion/sqllogictest/test_files/spark/math/hypot.slt
index 1349be0a95..564b34add8 100644
--- a/datafusion/sqllogictest/test_files/spark/math/hypot.slt
+++ b/datafusion/sqllogictest/test_files/spark/math/hypot.slt
@@ -21,7 +21,115 @@
 # For more information, please see:
 #   https://github.com/apache/datafusion/issues/15914
 
-## Original Query: SELECT hypot(3, 4);
-## PySpark 3.5.5 Result: {'HYPOT(3, 4)': 5.0, 'typeof(HYPOT(3, 4))': 'double', 'typeof(3)': 'int', 'typeof(4)': 'int'}
-#query
-#SELECT hypot(3::int, 4::int);
+# Scalar: classic Pythagorean triples (3-4-5, 5-12-13)
+query R
+SELECT hypot(3, 4);
+----
+5
+
+query R
+SELECT hypot(5, 12);
+----
+13
+
+# Double inputs
+query R
+SELECT hypot(3.0::double, 4.0::double);
+----
+5
+
+# NULL if either argument is NULL
+query R
+SELECT hypot(NULL::double, 4.0::double);
+----
+NULL
+
+query R
+SELECT hypot(3.0::double, NULL::double);
+----
+NULL
+
+# Array path, including a NULL row
+query R
+SELECT hypot(a, b) FROM (VALUES (3.0::double, 4.0::double), (6.0::double, 8.0::double), (NULL::double, 1.0::double)) AS t(a, b);
+----
+5
+10
+NULL
+
+# Overflow-safe: naive sqrt(a*a + b*b) overflows to Infinity here; hypot stays finite (matches Spark's Math.hypot)
+query B
+SELECT hypot(3e200::double, 4e200::double) < 'Infinity'::double;
+----
+true
+
+# any infinite input yields +Infinity, even when the other is NaN
+query R
+SELECT hypot('Infinity'::double, 4.0::double);
+----
+Infinity
+
+query R
+SELECT hypot(4.0::double, '-Infinity'::double);
+----
+Infinity
+
+query R
+SELECT hypot('Infinity'::double, 'NaN'::double);
+----
+Infinity
+
+# NaN propagates when neither input is infinite
+query R
+SELECT hypot('NaN'::double, 4.0::double);
+----
+NaN
+
+# signed zeros
+query RRR
+SELECT hypot(0.0::double, 0.0::double), hypot(-0.0::double, 0.0::double), hypot(3.0::double, -0.0::double);
+----
+0 0 3
+
+# NULL propagates even when the other input is Infinity
+query R
+SELECT hypot(NULL::double, 'Infinity'::double);
+----
+NULL
+
+# negative inputs yield the positive magnitude
+query RR
+SELECT hypot(-3.0::double, -4.0::double), hypot(-3.0::double, 4.0::double);
+----
+5 5
+
+# Underflow-safe: naive sqrt(a*a + b*b) underflows to 0 for tiny inputs; hypot stays nonzero (matches Spark's Math.hypot)
+query B
+SELECT hypot(3e-200::double, 4e-200::double) > 0;
+----
+true
+
+# Array path with special values (normal, +Infinity, NaN, NULL)
+query R
+SELECT hypot(a, b) FROM (VALUES
+  (3.0::double, 4.0::double),
+  ('Infinity'::double, 1.0::double),
+  ('NaN'::double, 1.0::double),
+  (NULL::double, 1.0::double)) AS t(a, b);
+----
+5
+Infinity
+NaN
+NULL
+
+# both inputs NaN -> NaN
+query R
+SELECT hypot('NaN'::double, 'NaN'::double);
+----
+NaN
+
+# both inputs infinite -> +Infinity
+query R
+SELECT hypot('Infinity'::double, '-Infinity'::double);
+----
+Infinity
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org