diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/copy_sign.md b/docs/reference/query-languages/esql/_snippets/functions/description/copy_sign.md
new file mode 100644
index 0000000000000..de5ce6081af2c
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/description/copy_sign.md
@@ -0,0 +1,6 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+**Description**
+
+Returns the first argument with the sign of the second argument.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/copy_sign.md b/docs/reference/query-languages/esql/_snippets/functions/examples/copy_sign.md
new file mode 100644
index 0000000000000..065261fa4c904
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/examples/copy_sign.md
@@ -0,0 +1,14 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+**Example**
+
+```esql
+ROW magnitude = 5.2, sign = -9.0
+| EVAL result = COPY_SIGN(magnitude, sign)
+```
+
+| magnitude:double | sign:double | result:double |
+| --- | --- | --- |
+| 5.2 | -9.0 | -5.2 |
+
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/copy_sign.md b/docs/reference/query-languages/esql/_snippets/functions/layout/copy_sign.md
new file mode 100644
index 0000000000000..d9e0b8cf41070
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/layout/copy_sign.md
@@ -0,0 +1,23 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+## `COPY_SIGN` [esql-copy_sign]
+
+**Syntax**
+
+:::{image} ../../../images/functions/copy_sign.svg
+:alt: Embedded
+:class: text-center
+:::
+
+
+:::{include} ../parameters/copy_sign.md
+:::
+
+:::{include} ../description/copy_sign.md
+:::
+
+:::{include} ../types/copy_sign.md
+:::
+
+:::{include} ../examples/copy_sign.md
+:::
diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/copy_sign.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/copy_sign.md
new file mode 100644
index 0000000000000..15c0e5e4f7edf
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/copy_sign.md
@@ -0,0 +1,10 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+**Parameters**
+
+`magnitude`
+: Numeric expression. If `null`, the function returns `null`.
+
+`sign`
+: Numeric expression. If `null`, the function returns `null`.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/copy_sign.md b/docs/reference/query-languages/esql/_snippets/functions/types/copy_sign.md
new file mode 100644
index 0000000000000..978f57042efe6
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/types/copy_sign.md
@@ -0,0 +1,23 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+**Supported types**
+
+| magnitude | sign | 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 |
+
diff --git a/docs/reference/query-languages/esql/images/functions/copy_sign.svg b/docs/reference/query-languages/esql/images/functions/copy_sign.svg
new file mode 100644
index 0000000000000..82455b59b1eda
--- /dev/null
+++ b/docs/reference/query-languages/esql/images/functions/copy_sign.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/copy_sign.json b/docs/reference/query-languages/esql/kibana/definition/functions/copy_sign.json
new file mode 100644
index 0000000000000..917ddd287a0a4
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/copy_sign.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" : "scalar",
+ "name" : "copy_sign",
+ "description" : "Returns the first argument with the sign of the second argument.",
+ "signatures" : [
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "magnitude",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "sign",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Numeric expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ }
+ ],
+ "examples" : [
+ "ROW magnitude = 5.2, sign = -9.0\n| EVAL result = COPY_SIGN(magnitude, sign)"
+ ],
+ "preview" : false,
+ "snapshot_only" : false
+}
diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/copy_sign.md b/docs/reference/query-languages/esql/kibana/docs/functions/copy_sign.md
new file mode 100644
index 0000000000000..20c19e6115769
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/docs/functions/copy_sign.md
@@ -0,0 +1,11 @@
+
+
+### COPY_SIGN
+Returns the first argument with the sign of the second argument.
+
+```esql
+ROW magnitude = 5.2, sign = -9.0
+| EVAL result = COPY_SIGN(magnitude, sign)
+```
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec
index c80827c372eec..63020f9f7b3b5 100644
--- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec
@@ -1702,3 +1702,96 @@ emp_no:integer | l1:double | l2:double
10002 | -7.23 | null
10003 | 4.0 | null
;
+
+copySignOfPositiveToPositive
+ROW magnitude = 50.0, sign = 3.3
+| EVAL result = COPY_SIGN(magnitude, sign)
+;
+
+magnitude:double | sign:double | result:double
+50.0 | 3.3 | 50.0
+;
+
+copySignOfPositiveToNegative
+// tag::copySign-PosToNeg[]
+ROW magnitude = 5.2, sign = -9.0
+| EVAL result = COPY_SIGN(magnitude, sign)
+// end::copySign-PosToNeg[]
+;
+
+// tag::copySign-PosToNeg-result[]
+magnitude:double | sign:double | result:double
+5.2 | -9.0 | -5.2
+// end::copySign-PosToNeg-result[]
+;
+
+copySignOfNegativeToPositive
+ROW magnitude = -5.12, sign = 3.0
+| EVAL result = COPY_SIGN(magnitude, sign)
+;
+
+magnitude:double | sign:double | result:double
+-5.12 | 3.0 | 5.12
+;
+
+copySignOfNegativeToNegative
+ROW magnitude = -12.0, sign = -800.0
+| EVAL result = COPY_SIGN(magnitude, sign)
+;
+
+magnitude:double | sign:double | result:double
+-12.0 | -800.0 | -12.0
+;
+
+copySignOfZeroToPositive
+ROW magnitude = 0.0, sign = 3.0
+| EVAL result = COPY_SIGN(magnitude, sign)
+;
+
+magnitude:double | sign:double | result:double
+0.0 | 3.0 | 0
+;
+
+copySignOfZeroToNegative
+ROW magnitude = 0.0, sign = -1.0
+| EVAL result = COPY_SIGN(magnitude, sign)
+;
+
+magnitude:double | sign:double | result:double
+0.0 | -1.0 | -0.0
+;
+
+copySignOfIntegerDouble
+ROW magnitude = 11, sign = -7.111
+| EVAL result = COPY_SIGN(magnitude, sign)
+;
+
+magnitude:integer | sign:double | result:double
+11 | -7.111 | -11.0
+;
+
+copySignOfDoubleInteger
+ROW magnitude = 23.1234, sign = 11
+| EVAL result = COPY_SIGN(magnitude, sign)
+;
+
+magnitude:double | sign:integer | result:double
+23.1234 | 11 | 23.1234
+;
+
+copySignOfULDouble
+ROW magnitude = 9223372036854775808, sign = -11.2
+| EVAL result = COPY_SIGN(magnitude, sign)
+;
+
+magnitude:unsigned_long | sign:double | result:double
+9223372036854775808 | -11.2 | -9223372036854775808
+;
+
+copySignAsArgument
+ROW result = SIGNUM(COPY_SIGN(24, -11))
+;
+
+result:double
+-1
+;
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignEvaluator.java
new file mode 100644
index 0000000000000..25b9d5c319b69
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignEvaluator.java
@@ -0,0 +1,158 @@
+// 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.ArithmeticException;
+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.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link CopySign}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class CopySignEvaluator implements EvalOperator.ExpressionEvaluator {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator magnitude;
+
+ private final EvalOperator.ExpressionEvaluator sign;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public CopySignEvaluator(Source source, EvalOperator.ExpressionEvaluator magnitude,
+ EvalOperator.ExpressionEvaluator sign, DriverContext driverContext) {
+ this.source = source;
+ this.magnitude = magnitude;
+ this.sign = sign;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (DoubleBlock magnitudeBlock = (DoubleBlock) magnitude.eval(page)) {
+ try (DoubleBlock signBlock = (DoubleBlock) sign.eval(page)) {
+ DoubleVector magnitudeVector = magnitudeBlock.asVector();
+ if (magnitudeVector == null) {
+ return eval(page.getPositionCount(), magnitudeBlock, signBlock);
+ }
+ DoubleVector signVector = signBlock.asVector();
+ if (signVector == null) {
+ return eval(page.getPositionCount(), magnitudeBlock, signBlock);
+ }
+ return eval(page.getPositionCount(), magnitudeVector, signVector);
+ }
+ }
+ }
+
+ public DoubleBlock eval(int positionCount, DoubleBlock magnitudeBlock, DoubleBlock signBlock) {
+ try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ if (magnitudeBlock.isNull(p)) {
+ result.appendNull();
+ continue position;
+ }
+ if (magnitudeBlock.getValueCount(p) != 1) {
+ if (magnitudeBlock.getValueCount(p) > 1) {
+ warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+ }
+ result.appendNull();
+ continue position;
+ }
+ if (signBlock.isNull(p)) {
+ result.appendNull();
+ continue position;
+ }
+ if (signBlock.getValueCount(p) != 1) {
+ if (signBlock.getValueCount(p) > 1) {
+ warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+ }
+ result.appendNull();
+ continue position;
+ }
+ try {
+ result.appendDouble(CopySign.process(magnitudeBlock.getDouble(magnitudeBlock.getFirstValueIndex(p)), signBlock.getDouble(signBlock.getFirstValueIndex(p))));
+ } catch (ArithmeticException e) {
+ warnings().registerException(e);
+ result.appendNull();
+ }
+ }
+ return result.build();
+ }
+ }
+
+ public DoubleBlock eval(int positionCount, DoubleVector magnitudeVector,
+ DoubleVector signVector) {
+ try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ try {
+ result.appendDouble(CopySign.process(magnitudeVector.getDouble(p), signVector.getDouble(p)));
+ } catch (ArithmeticException e) {
+ warnings().registerException(e);
+ result.appendNull();
+ }
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "CopySignEvaluator[" + "magnitude=" + magnitude + ", sign=" + sign + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(magnitude, sign);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory magnitude;
+
+ private final EvalOperator.ExpressionEvaluator.Factory sign;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory magnitude,
+ EvalOperator.ExpressionEvaluator.Factory sign) {
+ this.source = source;
+ this.magnitude = magnitude;
+ this.sign = sign;
+ }
+
+ @Override
+ public CopySignEvaluator get(DriverContext context) {
+ return new CopySignEvaluator(source, magnitude.get(context), sign.get(context), context);
+ }
+
+ @Override
+ public String toString() {
+ return "CopySignEvaluator[" + "magnitude=" + magnitude + ", sign=" + sign + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
index b93028b3e5897..e48a2d5747530 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
@@ -38,6 +38,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Atan;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cbrt;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Ceil;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.CopySign;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cos;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cosh;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Exp;
@@ -153,6 +154,7 @@ public static List unaryScalars() {
entries.add(ByteLength.ENTRY);
entries.add(Cbrt.ENTRY);
entries.add(Ceil.ENTRY);
+ entries.add(CopySign.ENTRY);
entries.add(Cos.ENTRY);
entries.add(Cosh.ENTRY);
entries.add(Exp.ENTRY);
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
index 764f83a67e925..8b025a5215d68 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
@@ -79,6 +79,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Atan2;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cbrt;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Ceil;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.CopySign;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cos;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cosh;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.E;
@@ -300,6 +301,7 @@ private static FunctionDefinition[][] functions() {
def(Atan2.class, Atan2::new, "atan2"),
def(Cbrt.class, Cbrt::new, "cbrt"),
def(Ceil.class, Ceil::new, "ceil"),
+ def(CopySign.class, CopySign::new, "copy_sign"),
def(Cos.class, Cos::new, "cos"),
def(Cosh.class, Cosh::new, "cosh"),
def(E.class, E::new, "e"),
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySign.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySign.java
new file mode 100644
index 0000000000000..4a31532e79437
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySign.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 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.ExpressionEvaluator;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+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.core.util.NumericUtils;
+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.Arrays;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric;
+
+public class CopySign extends EsqlScalarFunction {
+ public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "CopySign", CopySign::new);
+
+ private final Expression magnitude;
+ private final Expression sign;
+
+ @FunctionInfo(
+ returnType = { "double" },
+ description = "Returns the first argument with the sign of the second argument.",
+ examples = @Example(file = "math", tag = "copySign-PosToNeg")
+ )
+ public CopySign(
+ Source source,
+ @Param(
+ name = "magnitude",
+ type = { "double", "integer", "long", "unsigned_long" },
+ description = "Numeric expression. If `null`, the function returns `null`."
+ ) Expression magnitude,
+ @Param(
+ name = "sign",
+ type = { "double", "integer", "long", "unsigned_long" },
+ description = "Numeric expression. If `null`, the function returns `null`."
+ ) Expression sign
+ ) {
+ super(source, Arrays.asList(magnitude, sign));
+ this.magnitude = magnitude;
+ this.sign = sign;
+ }
+
+ private CopySign(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(magnitude);
+ out.writeNamedWriteable(sign);
+ }
+
+ @Override
+ public String getWriteableName() {
+ return ENTRY.name;
+ }
+
+ @Override
+ protected TypeResolution resolveType() {
+ if (childrenResolved() == false) {
+ return new TypeResolution("Unresolved children");
+ }
+
+ TypeResolution resolution = isNumeric(magnitude, sourceText(), FIRST);
+ if (resolution.unresolved()) {
+ return resolution;
+ }
+
+ return isNumeric(sign, sourceText(), SECOND);
+ }
+
+ @Override
+ public boolean foldable() {
+ return magnitude.foldable() && sign.foldable();
+ }
+
+ @Evaluator(warnExceptions = { ArithmeticException.class })
+ static double process(double magnitude, double sign) {
+ return NumericUtils.asFiniteNumber(Math.copySign(magnitude, sign));
+ }
+
+ @Override
+ public Expression replaceChildren(List newChildren) {
+ return new CopySign(source(), newChildren.get(0), newChildren.get(1));
+ }
+
+ @Override
+ protected NodeInfo extends Expression> info() {
+ return NodeInfo.create(this, CopySign::new, magnitude(), sign());
+ }
+
+ public Expression magnitude() {
+ return magnitude;
+ }
+
+ public Expression sign() {
+ return sign;
+ }
+
+ @Override
+ public DataType dataType() {
+ return DataType.DOUBLE;
+ }
+
+ @Override
+ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+ var magnitudeEval = Cast.cast(source(), magnitude.dataType(), DataType.DOUBLE, toEvaluator.apply(magnitude));
+ var signEval = Cast.cast(source(), sign.dataType(), DataType.DOUBLE, toEvaluator.apply(sign));
+ return new CopySignEvaluator.Factory(source(), magnitudeEval, signEval);
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignErrorTests.java
new file mode 100644
index 0000000000000..d0792951f98e2
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignErrorTests.java
@@ -0,0 +1,37 @@
+/*
+ * 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.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.hamcrest.Matcher;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class CopySignErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
+ @Override
+ protected List cases() {
+ return paramsToSuppliers(CopySignTests.parameters());
+ }
+
+ @Override
+ protected Expression build(Source source, List args) {
+ return new CopySign(source, args.get(0), args.get(1));
+ }
+
+ @Override
+ protected Matcher expectedTypeErrorMatcher(List> validPerPosition, List signature) {
+ return equalTo(typeErrorMessage(true, validPerPosition, signature, (v, i) -> "numeric"));
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignSerializationTests.java
new file mode 100644
index 0000000000000..ef7075092c646
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignSerializationTests.java
@@ -0,0 +1,37 @@
+/*
+ * 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 java.io.IOException;
+
+public class CopySignSerializationTests extends AbstractExpressionSerializationTests {
+ @Override
+ protected CopySign createTestInstance() {
+ Source source = randomSource();
+ Expression magnitude = randomChild();
+ Expression sign = randomChild();
+ return new CopySign(source, magnitude, sign);
+ }
+
+ @Override
+ protected CopySign mutateInstance(CopySign instance) throws IOException {
+ Source source = instance.source();
+ Expression magnitude = instance.magnitude();
+ Expression sign = instance.sign();
+ if (randomBoolean()) {
+ magnitude = randomValueOtherThan(magnitude, AbstractExpressionSerializationTests::randomChild);
+ } else {
+ sign = randomValueOtherThan(sign, AbstractExpressionSerializationTests::randomChild);
+ }
+ return new CopySign(source, magnitude, sign);
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignTests.java
new file mode 100644
index 0000000000000..e2f68ef652130
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CopySignTests.java
@@ -0,0 +1,50 @@
+/*
+ * 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 CopySignTests extends AbstractScalarFunctionTestCase {
+ public CopySignTests(@Name("TestCase") Supplier testCaseSupplier) {
+ this.testCase = testCaseSupplier.get();
+ }
+
+ @ParametersFactory
+ public static Iterable