Explorar el Código

[ES|QL] Add hypot function (#114382) (#114658)

Adds a hypotenuse function
Larisa Motova hace 1 año
padre
commit
42aa343daf
Se han modificado 18 ficheros con 800 adiciones y 0 borrados
  1. 5 0
      docs/changelog/114382.yaml
  2. 5 0
      docs/reference/esql/functions/description/hypot.asciidoc
  3. 13 0
      docs/reference/esql/functions/examples/hypot.asciidoc
  4. 301 0
      docs/reference/esql/functions/kibana/definition/hypot.json
  5. 12 0
      docs/reference/esql/functions/kibana/docs/hypot.md
  6. 15 0
      docs/reference/esql/functions/layout/hypot.asciidoc
  7. 2 0
      docs/reference/esql/functions/math-functions.asciidoc
  8. 9 0
      docs/reference/esql/functions/parameters/hypot.asciidoc
  9. 1 0
      docs/reference/esql/functions/signature/hypot.svg
  10. 24 0
      docs/reference/esql/functions/types/hypot.asciidoc
  11. 57 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec
  12. 132 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/HypotEvaluator.java
  13. 5 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  14. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  15. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/EsqlScalarFunction.java
  16. 130 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Hypot.java
  17. 39 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/HypotSerializationTests.java
  18. 46 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/HypotTests.java

+ 5 - 0
docs/changelog/114382.yaml

@@ -0,0 +1,5 @@
+pr: 114382
+summary: "[ES|QL] Add hypot function"
+area: ES|QL
+type: enhancement
+issues: []

+ 5 - 0
docs/reference/esql/functions/description/hypot.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Returns the hypotenuse of two numbers. The input can be any numeric values, the return value is always a double. Hypotenuses of infinities are null.

+ 13 - 0
docs/reference/esql/functions/examples/hypot.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/math.csv-spec[tag=hypot]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/math.csv-spec[tag=hypot-result]
+|===
+

+ 301 - 0
docs/reference/esql/functions/kibana/definition/hypot.json

@@ -0,0 +1,301 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "hypot",
+  "description" : "Returns the hypotenuse of two numbers. The input can be any numeric values, the return value is always a double.\nHypotenuses of infinities are null.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number1",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "number2",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    }
+  ],
+  "examples" : [
+    "ROW a = 3.0, b = 4.0\n| EVAL c = HYPOT(a, b)"
+  ],
+  "preview" : false,
+  "snapshot_only" : false
+}

+ 12 - 0
docs/reference/esql/functions/kibana/docs/hypot.md

@@ -0,0 +1,12 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### HYPOT
+Returns the hypotenuse of two numbers. The input can be any numeric values, the return value is always a double.
+Hypotenuses of infinities are null.
+
+```
+ROW a = 3.0, b = 4.0
+| EVAL c = HYPOT(a, b)
+```

+ 15 - 0
docs/reference/esql/functions/layout/hypot.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-hypot]]
+=== `HYPOT`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/hypot.svg[Embedded,opts=inline]
+
+include::../parameters/hypot.asciidoc[]
+include::../description/hypot.asciidoc[]
+include::../types/hypot.asciidoc[]
+include::../examples/hypot.asciidoc[]

+ 2 - 0
docs/reference/esql/functions/math-functions.asciidoc

@@ -20,6 +20,7 @@
 * <<esql-e>>
 * <<esql-exp>>
 * <<esql-floor>>
+* <<esql-hypot>>
 * <<esql-log>>
 * <<esql-log10>>
 * <<esql-pi>>
@@ -46,6 +47,7 @@ include::layout/cosh.asciidoc[]
 include::layout/e.asciidoc[]
 include::layout/exp.asciidoc[]
 include::layout/floor.asciidoc[]
+include::layout/hypot.asciidoc[]
 include::layout/log.asciidoc[]
 include::layout/log10.asciidoc[]
 include::layout/pi.asciidoc[]

+ 9 - 0
docs/reference/esql/functions/parameters/hypot.asciidoc

@@ -0,0 +1,9 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`number1`::
+Numeric expression. If `null`, the function returns `null`.
+
+`number2`::
+Numeric expression. If `null`, the function returns `null`.

+ 1 - 0
docs/reference/esql/functions/signature/hypot.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="444" height="46" viewbox="0 0 444 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m80 0h10m32 0h10m104 0h10m32 0h10m104 0h10m32 0h5"/><rect class="s" x="5" y="5" width="80" height="36"/><text class="k" x="15" y="31">HYPOT</text><rect class="s" x="95" y="5" width="32" height="36" rx="7"/><text class="syn" x="105" y="31">(</text><rect class="s" x="137" y="5" width="104" height="36" rx="7"/><text class="k" x="147" y="31">number1</text><rect class="s" x="251" y="5" width="32" height="36" rx="7"/><text class="syn" x="261" y="31">,</text><rect class="s" x="293" y="5" width="104" height="36" rx="7"/><text class="k" x="303" y="31">number2</text><rect class="s" x="407" y="5" width="32" height="36" rx="7"/><text class="syn" x="417" y="31">)</text></svg>

+ 24 - 0
docs/reference/esql/functions/types/hypot.asciidoc

@@ -0,0 +1,24 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+number1 | number2 | result
+double | double | double
+double | integer | double
+double | long | double
+double | unsigned_long | double
+integer | double | double
+integer | integer | double
+integer | long | double
+integer | unsigned_long | double
+long | double | double
+long | integer | double
+long | long | double
+long | unsigned_long | double
+unsigned_long | double | double
+unsigned_long | integer | double
+unsigned_long | long | double
+unsigned_long | unsigned_long | double
+|===

+ 57 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec

@@ -1379,6 +1379,63 @@ d:double | c:double
 -0.0     | -0.0
 ;
 
+hypot
+required_capability: fn_hypot
+// tag::hypot[]
+ROW a = 3.0, b = 4.0
+| EVAL c = HYPOT(a, b)
+// end::hypot[]
+;
+
+// tag::hypot-result[]
+a:double | b:double | c:double
+3.0      | 4.0      | 5.0
+// end::hypot-result[]
+;
+
+hypotWithFrom
+required_capability: fn_hypot
+FROM ul_logs
+| WHERE id > 95
+| EVAL bytes_hypot = HYPOT(bytes_in, bytes_out)
+| SORT id ASC
+| LIMIT 5
+| KEEP id, bytes_in, bytes_out, bytes_hypot
+;
+
+id:integer | bytes_in:unsigned_long | bytes_out:unsigned_long | bytes_hypot:double
+96         | 9932469097722733505    | 14925592145374204307    | 1.792839209932874E19
+97         | 11620953158540412267   | 3809712277266935082     | 1.2229491401875583E19
+98         | 3448205404634246112    | 5409549730889481641     | 6.415087591258227E18
+99         | 1957665857956635540    | 352442273299370793      | 1.9891382977102218E18
+100        | 16462768484251021236   | 15616395223975497926    | 2.2691287886707827E19
+;
+
+hypotBothNull
+required_capability: fn_hypot
+FROM ul_logs
+| WHERE bytes_in IS NULL and bytes_out IS NULL
+| LIMIT 1
+| EVAL bytes_hypot = HYPOT(bytes_in, bytes_out)
+| KEEP bytes_in, bytes_out, bytes_hypot
+;
+
+bytes_in:unsigned_long | bytes_out:unsigned_long | bytes_hypot:double
+null                   | null                    | null
+;
+
+hypotOneNull
+required_capability: fn_hypot
+FROM ul_logs
+| WHERE id == 41
+| EVAL confused_hypot = HYPOT(id, bytes_in)
+| KEEP id, bytes_in, confused_hypot
+;
+
+id:integer | bytes_in:unsigned_long | confused_hypot:double
+41         | null                   | null
+;
+
 least
 // tag::least[]
 ROW a = 10, b = 20

+ 132 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/HypotEvaluator.java

@@ -0,0 +1,132 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.math;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.DoubleVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Hypot}.
+ * This class is generated. Do not edit it.
+ */
+public final class HypotEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator n1;
+
+  private final EvalOperator.ExpressionEvaluator n2;
+
+  private final DriverContext driverContext;
+
+  public HypotEvaluator(Source source, EvalOperator.ExpressionEvaluator n1,
+      EvalOperator.ExpressionEvaluator n2, DriverContext driverContext) {
+    this.n1 = n1;
+    this.n2 = n2;
+    this.driverContext = driverContext;
+    this.warnings = Warnings.createWarnings(driverContext.warningsMode(), source);
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock n1Block = (DoubleBlock) n1.eval(page)) {
+      try (DoubleBlock n2Block = (DoubleBlock) n2.eval(page)) {
+        DoubleVector n1Vector = n1Block.asVector();
+        if (n1Vector == null) {
+          return eval(page.getPositionCount(), n1Block, n2Block);
+        }
+        DoubleVector n2Vector = n2Block.asVector();
+        if (n2Vector == null) {
+          return eval(page.getPositionCount(), n1Block, n2Block);
+        }
+        return eval(page.getPositionCount(), n1Vector, n2Vector).asBlock();
+      }
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock n1Block, DoubleBlock n2Block) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (n1Block.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (n1Block.getValueCount(p) != 1) {
+          if (n1Block.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (n2Block.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (n2Block.getValueCount(p) != 1) {
+          if (n2Block.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendDouble(Hypot.process(n1Block.getDouble(n1Block.getFirstValueIndex(p)), n2Block.getDouble(n2Block.getFirstValueIndex(p))));
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector n1Vector, DoubleVector n2Vector) {
+    try(DoubleVector.FixedBuilder result = driverContext.blockFactory().newDoubleVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendDouble(p, Hypot.process(n1Vector.getDouble(p), n2Vector.getDouble(p)));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "HypotEvaluator[" + "n1=" + n1 + ", n2=" + n2 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(n1, n2);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory n1;
+
+    private final EvalOperator.ExpressionEvaluator.Factory n2;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory n1,
+        EvalOperator.ExpressionEvaluator.Factory n2) {
+      this.source = source;
+      this.n1 = n1;
+      this.n2 = n2;
+    }
+
+    @Override
+    public HypotEvaluator get(DriverContext context) {
+      return new HypotEvaluator(source, n1.get(context), n2.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "HypotEvaluator[" + "n1=" + n1 + ", n2=" + n2 + "]";
+    }
+  }
+}

+ 5 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

@@ -37,6 +37,11 @@ public class EsqlCapabilities {
          */
         FN_CBRT,
 
+        /**
+         * Support for function {@code HYPOT}.
+         */
+        FN_HYPOT,
+
         /**
          * Support for {@code MV_APPEND} function. #107001
          */

+ 2 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java

@@ -78,6 +78,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cosh;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.E;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Exp;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Floor;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Hypot;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log10;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Pi;
@@ -285,6 +286,7 @@ public class EsqlFunctionRegistry {
                 def(Exp.class, Exp::new, "exp"),
                 def(Floor.class, Floor::new, "floor"),
                 def(Greatest.class, Greatest::new, "greatest"),
+                def(Hypot.class, Hypot::new, "hypot"),
                 def(Log.class, Log::new, "log"),
                 def(Log10.class, Log10::new, "log10"),
                 def(Least.class, Least::new, "least"),

+ 2 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/EsqlScalarFunction.java

@@ -30,6 +30,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
 import org.elasticsearch.xpack.esql.expression.function.scalar.ip.IpPrefix;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Atan2;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.E;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Hypot;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Pi;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Pow;
@@ -82,6 +83,7 @@ public abstract class EsqlScalarFunction extends ScalarFunction implements Evalu
         entries.add(E.ENTRY);
         entries.add(EndsWith.ENTRY);
         entries.add(Greatest.ENTRY);
+        entries.add(Hypot.ENTRY);
         entries.add(In.ENTRY);
         entries.add(InsensitiveEquals.ENTRY);
         entries.add(DateExtract.ENTRY);

+ 130 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Hypot.java

@@ -0,0 +1,130 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.math;
+
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.Expressions;
+import org.elasticsearch.xpack.esql.core.expression.TypeResolutions;
+import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction;
+import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric;
+
+/**
+ * Returns the hypotenuse of the numbers given as parameters.
+ */
+public class Hypot extends EsqlScalarFunction {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Hypot", Hypot::new);
+
+    private final Expression n1;
+    private final Expression n2;
+
+    @FunctionInfo(returnType = "double", description = """
+        Returns the hypotenuse of two numbers. The input can be any numeric values, the return value is always a double.
+        Hypotenuses of infinities are null.""", examples = @Example(file = "math", tag = "hypot"))
+    public Hypot(
+        Source source,
+        @Param(
+            name = "number1",
+            type = { "double", "integer", "long", "unsigned_long" },
+            description = "Numeric expression. If `null`, the function returns `null`."
+        ) Expression n1,
+        @Param(
+            name = "number2",
+            type = { "double", "integer", "long", "unsigned_long" },
+            description = "Numeric expression. If `null`, the function returns `null`."
+        ) Expression n2
+    ) {
+        super(source, List.of(n1, n2));
+        this.n1 = n1;
+        this.n2 = n2;
+    }
+
+    private Hypot(StreamInput in) throws IOException {
+        this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), in.readNamedWriteable(Expression.class));
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        source().writeTo(out);
+        out.writeNamedWriteable(n1);
+        out.writeNamedWriteable(n2);
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new Hypot(source(), newChildren.get(0), newChildren.get(1));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, Hypot::new, n1, n2);
+    }
+
+    @Evaluator
+    static double process(double n1, double n2) {
+        return Math.hypot(n1, n2);
+    }
+
+    @Override
+    public DataType dataType() {
+        return DataType.DOUBLE;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        TypeResolution resolution = isNumeric(n1, sourceText(), TypeResolutions.ParamOrdinal.FIRST);
+        if (resolution.unresolved()) {
+            return resolution;
+        }
+        return isNumeric(n2, sourceText(), TypeResolutions.ParamOrdinal.SECOND);
+    }
+
+    @Override
+    public boolean foldable() {
+        return Expressions.foldable(children());
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+        var n1Eval = Cast.cast(source(), n1.dataType(), DataType.DOUBLE, toEvaluator.apply(n1));
+        var n2Eval = Cast.cast(source(), n2.dataType(), DataType.DOUBLE, toEvaluator.apply(n2));
+        return new HypotEvaluator.Factory(source(), n1Eval, n2Eval);
+    }
+
+    public Expression n1() {
+        return n1;
+    }
+
+    public Expression n2() {
+        return n2;
+    }
+}

+ 39 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/HypotSerializationTests.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.math;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests;
+import org.elasticsearch.xpack.esql.expression.AbstractUnaryScalarSerializationTests;
+
+import java.io.IOException;
+
+public class HypotSerializationTests extends AbstractExpressionSerializationTests<Hypot> {
+
+    @Override
+    protected Hypot createTestInstance() {
+        Source source = randomSource();
+        Expression n1 = randomChild();
+        Expression n2 = randomChild();
+        return new Hypot(source, n1, n2);
+    }
+
+    @Override
+    protected Hypot mutateInstance(Hypot instance) throws IOException {
+        Source source = instance.source();
+        Expression n1 = instance.n1();
+        Expression n2 = instance.n2();
+        if (randomBoolean()) {
+            n1 = randomValueOtherThan(n1, AbstractUnaryScalarSerializationTests::randomChild);
+        } else {
+            n2 = randomValueOtherThan(n2, AbstractUnaryScalarSerializationTests::randomChild);
+        }
+        return new Hypot(source, n1, n2);
+    }
+}

+ 46 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/HypotTests.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.math;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+public class HypotTests extends AbstractScalarFunctionTestCase {
+    public HypotTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = TestCaseSupplier.forBinaryCastingToDouble(
+            "HypotEvaluator",
+            "n1",
+            "n2",
+            Math::hypot,
+            Double.NEGATIVE_INFINITY,
+            Double.POSITIVE_INFINITY,
+            Double.NEGATIVE_INFINITY,
+            Double.POSITIVE_INFINITY,
+            List.of()
+        );
+        return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers, (v, p) -> "numeric");
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new Hypot(source, args.get(0), args.get(1));
+    }
+}