浏览代码

Added esql scalb function. (#127696) (#128815)

Co-authored-by: shmuelhanoch <shmuelhanoch@gmail.com>
Co-authored-by: Shmuel Hanoch <shmuel.hanoch@elastic.co>
Luigi Dell'Aquila 4 月之前
父节点
当前提交
040d629e8d
共有 20 个文件被更改,包括 1252 次插入3 次删除
  1. 6 0
      docs/reference/esql/functions/description/scalb.md
  2. 13 0
      docs/reference/esql/functions/examples/scalb.md
  3. 157 0
      docs/reference/esql/functions/kibana/definition/scalb.json
  4. 11 0
      docs/reference/esql/functions/kibana/docs/scalb.md
  5. 23 0
      docs/reference/query-languages/esql/_snippets/functions/layout/scalb.md
  6. 10 0
      docs/reference/query-languages/esql/_snippets/functions/parameters/scalb.md
  7. 15 0
      docs/reference/query-languages/esql/_snippets/functions/types/scalb.md
  8. 1 0
      docs/reference/query-languages/esql/images/functions/scalb.svg
  9. 100 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/floats.csv-spec
  10. 139 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbConstantIntEvaluator.java
  11. 139 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbConstantLongEvaluator.java
  12. 159 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbIntEvaluator.java
  13. 159 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbLongEvaluator.java
  14. 5 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  15. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
  16. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  17. 0 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java
  18. 172 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Scalb.java
  19. 137 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java
  20. 2 2
      x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml

+ 6 - 0
docs/reference/esql/functions/description/scalb.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 result of `d * 2 ^ scaleFactor`, Similar to Java's `scalb` function. Result is rounded as if performed by a single correctly rounded floating-point multiply to a member of the double value set.
+

+ 13 - 0
docs/reference/esql/functions/examples/scalb.md

@@ -0,0 +1,13 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+**Example**
+
+```esql
+row x = 3.0, y = 10 | eval z = scalb(x, y)
+```
+
+| x:double | y:integer | z:double |
+| --- | --- | --- |
+| 3.0 | 10 | 3072.0 |
+
+

+ 157 - 0
docs/reference/esql/functions/kibana/definition/scalb.json

@@ -0,0 +1,157 @@
+{
+  "comment" : "This is generated by ESQL’s AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "scalar",
+  "name" : "scalb",
+  "description" : "Returns the result of `d * 2 ^ scaleFactor`,\nSimilar to Java's `scalb` function. Result is rounded as if\nperformed by a single correctly rounded floating-point multiply\nto a member of the double value set.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "d",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "scaleFactor",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "d",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "scaleFactor",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "d",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "scaleFactor",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "d",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "scaleFactor",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "d",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "scaleFactor",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "d",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "scaleFactor",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "d",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "scaleFactor",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "d",
+          "type" : "unsigned_long",
+          "optional" : false,
+          "description" : "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "scaleFactor",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    }
+  ],
+  "examples" : [
+    "row x = 3.0, y = 10 | eval z = scalb(x, y)"
+  ],
+  "preview" : false,
+  "snapshot_only" : false
+}

+ 11 - 0
docs/reference/esql/functions/kibana/docs/scalb.md

@@ -0,0 +1,11 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+### SCALB
+Returns the result of `d * 2 ^ scaleFactor`,
+Similar to Java's `scalb` function. Result is rounded as if
+performed by a single correctly rounded floating-point multiply
+to a member of the double value set.
+
+```esql
+row x = 3.0, y = 10 | eval z = scalb(x, y)
+```

+ 23 - 0
docs/reference/query-languages/esql/_snippets/functions/layout/scalb.md

@@ -0,0 +1,23 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+## `SCALB` [esql-scalb]
+
+**Syntax**
+
+:::{image} ../../../images/functions/scalb.svg
+:alt: Embedded
+:class: text-center
+:::
+
+
+:::{include} ../parameters/scalb.md
+:::
+
+:::{include} ../description/scalb.md
+:::
+
+:::{include} ../types/scalb.md
+:::
+
+:::{include} ../examples/scalb.md
+:::

+ 10 - 0
docs/reference/query-languages/esql/_snippets/functions/parameters/scalb.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**
+
+`d`
+:   Numeric expression for the multiplier. If `null`, the function returns `null`.
+
+`scaleFactor`
+:   Numeric expression for the scale factor. If `null`, the function returns `null`.
+

+ 15 - 0
docs/reference/query-languages/esql/_snippets/functions/types/scalb.md

@@ -0,0 +1,15 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+**Supported types**
+
+| d | scaleFactor | result |
+| --- | --- | --- |
+| double | integer | double |
+| double | long | double |
+| integer | integer | double |
+| integer | long | double |
+| long | integer | double |
+| long | long | double |
+| unsigned_long | integer | double |
+| unsigned_long | long | double |
+

+ 1 - 0
docs/reference/query-languages/esql/images/functions/scalb.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="420" height="46" viewbox="0 0 420 46"><defs><style type="text/css">.c{fill:none;stroke:#222222;}.k{fill:#000000;font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:20px;}.s{fill:#e4f4ff;stroke:#222222;}.syn{fill:#8D8D8D;font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:20px;}</style></defs><path class="c" d="M0 31h5m80 0h10m32 0h10m32 0h10m32 0h10m152 0h10m32 0h5"/><rect class="s" x="5" y="5" width="80" height="36"/><text class="k" x="15" y="31">SCALB</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="32" height="36" rx="7"/><text class="k" x="147" y="31">d</text><rect class="s" x="179" y="5" width="32" height="36" rx="7"/><text class="syn" x="189" y="31">,</text><rect class="s" x="221" y="5" width="152" height="36" rx="7"/><text class="k" x="231" y="31">scaleFactor</text><rect class="s" x="383" y="5" width="32" height="36" rx="7"/><text class="syn" x="393" y="31">)</text></svg>

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

@@ -600,3 +600,103 @@ s:double | emp_no:integer | salary:integer  | salary_change:double
 -1.0     | 10004          | 36174           | [-0.35, 1.13, 3.65, 13.48]
 -1.0     | 10005          | 63528           | [-2.14, 13.07]
 ;
+
+docsScalb
+required_capability: fn_scalb
+// tag::scalb[]
+row x = 3.0, y = 10 | eval z = scalb(x, y)
+// end::scalb[]
+;
+
+// tag::scalb-result[]
+x:double | y:integer | z:double
+3.0      | 10        | 3072.0
+// end::scalb-result[]
+;
+
+scalbConstant
+required_capability: fn_scalb
+row y = "foo" | eval x = scalb(3, 10) | keep x;
+
+x:double
+3072.0
+;
+
+scalbWithNonFoldableArgsAndWhere
+required_capability: fn_scalb
+
+from employees
+| eval s = scalb(mv_min(salary_change), emp_no / 1000)
+| where scalb(mv_min(salary_change), 3) > 100
+| sort s
+| keep s;
+
+s:double
+13127.68
+13936.64
+;
+
+scalbWithEvalAndSort
+required_capability: fn_scalb
+from employees
+| eval s = scalb(mv_min(salary_change), 5)
+| where scalb(mv_max(salary_change), 5) > 100
+| keep s, emp_no, salary, salary_change
+| sort s, emp_no
+| limit 3;
+
+s:double | emp_no:integer | salary:integer  | salary_change:double
+-313.92  | 10065          | 50249          | [-9.81, -1.47, 14.44]   
+-296.96  | 10042          | 30404          | [-9.28, 9.42]           
+-295.36  | 10091          | 38645          | [-9.23, 5.19, 5.85, 7.5]
+;
+
+scalbWithHugeScaleFactor
+required_capability: fn_scalb
+
+from employees
+| eval s = scalb(abs(mv_min(salary_change)), 2147483648 + emp_no)
+| keep s
+| limit 3
+;
+warning: Line 2:12: evaluation of [scalb(abs(mv_min(salary_change)), 2147483648 + emp_no)] failed, treating result as null. Only first 20 failures recorded.
+warning: Line 2:12: java.lang.ArithmeticException: integer overflow
+
+s:double
+null
+null
+null
+;
+scalbWithHugeConstantScaleFactor
+required_capability: fn_scalb
+
+from employees
+| eval s = scalb(abs(mv_min(salary_change)), 2147483648)
+| keep s
+| limit 3
+;
+warning: Line 2:12: evaluation of [scalb(abs(mv_min(salary_change)), 2147483648)] failed, treating result as null. Only first 20 failures recorded.
+warning: Line 2:12: java.lang.ArithmeticException: integer overflow
+
+s:double
+null
+null
+null
+;
+
+scalbWithHugeconstantFirstArgument
+required_capability: fn_scalb
+
+from employees
+| eval s = scalb(2147483648, emp_no)
+| keep s
+| limit 3
+;
+warning: Line 2:12: evaluation of [scalb(2147483648, emp_no)] failed, treating result as null. Only first 20 failures recorded.
+warning: Line 2:12: java.lang.ArithmeticException: not a finite double number: Infinity
+
+s:double
+null
+null
+null
+;

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

@@ -0,0 +1,139 @@
+// 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 Scalb}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class ScalbConstantIntEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator d;
+
+  private final int scaleFactor;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public ScalbConstantIntEvaluator(Source source, EvalOperator.ExpressionEvaluator d,
+      int scaleFactor, DriverContext driverContext) {
+    this.source = source;
+    this.d = d;
+    this.scaleFactor = scaleFactor;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock dBlock = (DoubleBlock) d.eval(page)) {
+      DoubleVector dVector = dBlock.asVector();
+      if (dVector == null) {
+        return eval(page.getPositionCount(), dBlock);
+      }
+      return eval(page.getPositionCount(), dVector);
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock dBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (dBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (dBlock.getValueCount(p) != 1) {
+          if (dBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendDouble(Scalb.processConstantInt(dBlock.getDouble(dBlock.getFirstValueIndex(p)), this.scaleFactor));
+        } catch (ArithmeticException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleVector dVector) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendDouble(Scalb.processConstantInt(dVector.getDouble(p), this.scaleFactor));
+        } catch (ArithmeticException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ScalbConstantIntEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(d);
+  }
+
+  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 d;
+
+    private final int scaleFactor;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory d, int scaleFactor) {
+      this.source = source;
+      this.d = d;
+      this.scaleFactor = scaleFactor;
+    }
+
+    @Override
+    public ScalbConstantIntEvaluator get(DriverContext context) {
+      return new ScalbConstantIntEvaluator(source, d.get(context), scaleFactor, context);
+    }
+
+    @Override
+    public String toString() {
+      return "ScalbConstantIntEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]";
+    }
+  }
+}

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

@@ -0,0 +1,139 @@
+// 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 Scalb}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class ScalbConstantLongEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator d;
+
+  private final long scaleFactor;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public ScalbConstantLongEvaluator(Source source, EvalOperator.ExpressionEvaluator d,
+      long scaleFactor, DriverContext driverContext) {
+    this.source = source;
+    this.d = d;
+    this.scaleFactor = scaleFactor;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock dBlock = (DoubleBlock) d.eval(page)) {
+      DoubleVector dVector = dBlock.asVector();
+      if (dVector == null) {
+        return eval(page.getPositionCount(), dBlock);
+      }
+      return eval(page.getPositionCount(), dVector);
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock dBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (dBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (dBlock.getValueCount(p) != 1) {
+          if (dBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendDouble(Scalb.processConstantLong(dBlock.getDouble(dBlock.getFirstValueIndex(p)), this.scaleFactor));
+        } catch (ArithmeticException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleVector dVector) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendDouble(Scalb.processConstantLong(dVector.getDouble(p), this.scaleFactor));
+        } catch (ArithmeticException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ScalbConstantLongEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(d);
+  }
+
+  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 d;
+
+    private final long scaleFactor;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory d, long scaleFactor) {
+      this.source = source;
+      this.d = d;
+      this.scaleFactor = scaleFactor;
+    }
+
+    @Override
+    public ScalbConstantLongEvaluator get(DriverContext context) {
+      return new ScalbConstantLongEvaluator(source, d.get(context), scaleFactor, context);
+    }
+
+    @Override
+    public String toString() {
+      return "ScalbConstantLongEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]";
+    }
+  }
+}

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

@@ -0,0 +1,159 @@
+// 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.IntBlock;
+import org.elasticsearch.compute.data.IntVector;
+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 Scalb}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class ScalbIntEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator d;
+
+  private final EvalOperator.ExpressionEvaluator scaleFactor;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public ScalbIntEvaluator(Source source, EvalOperator.ExpressionEvaluator d,
+      EvalOperator.ExpressionEvaluator scaleFactor, DriverContext driverContext) {
+    this.source = source;
+    this.d = d;
+    this.scaleFactor = scaleFactor;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock dBlock = (DoubleBlock) d.eval(page)) {
+      try (IntBlock scaleFactorBlock = (IntBlock) scaleFactor.eval(page)) {
+        DoubleVector dVector = dBlock.asVector();
+        if (dVector == null) {
+          return eval(page.getPositionCount(), dBlock, scaleFactorBlock);
+        }
+        IntVector scaleFactorVector = scaleFactorBlock.asVector();
+        if (scaleFactorVector == null) {
+          return eval(page.getPositionCount(), dBlock, scaleFactorBlock);
+        }
+        return eval(page.getPositionCount(), dVector, scaleFactorVector);
+      }
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock dBlock, IntBlock scaleFactorBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (dBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (dBlock.getValueCount(p) != 1) {
+          if (dBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (scaleFactorBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (scaleFactorBlock.getValueCount(p) != 1) {
+          if (scaleFactorBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendDouble(Scalb.process(dBlock.getDouble(dBlock.getFirstValueIndex(p)), scaleFactorBlock.getInt(scaleFactorBlock.getFirstValueIndex(p))));
+        } catch (ArithmeticException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleVector dVector, IntVector scaleFactorVector) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendDouble(Scalb.process(dVector.getDouble(p), scaleFactorVector.getInt(p)));
+        } catch (ArithmeticException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ScalbIntEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(d, scaleFactor);
+  }
+
+  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 d;
+
+    private final EvalOperator.ExpressionEvaluator.Factory scaleFactor;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory d,
+        EvalOperator.ExpressionEvaluator.Factory scaleFactor) {
+      this.source = source;
+      this.d = d;
+      this.scaleFactor = scaleFactor;
+    }
+
+    @Override
+    public ScalbIntEvaluator get(DriverContext context) {
+      return new ScalbIntEvaluator(source, d.get(context), scaleFactor.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "ScalbIntEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]";
+    }
+  }
+}

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

@@ -0,0 +1,159 @@
+// 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.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+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 Scalb}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class ScalbLongEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator d;
+
+  private final EvalOperator.ExpressionEvaluator scaleFactor;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public ScalbLongEvaluator(Source source, EvalOperator.ExpressionEvaluator d,
+      EvalOperator.ExpressionEvaluator scaleFactor, DriverContext driverContext) {
+    this.source = source;
+    this.d = d;
+    this.scaleFactor = scaleFactor;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock dBlock = (DoubleBlock) d.eval(page)) {
+      try (LongBlock scaleFactorBlock = (LongBlock) scaleFactor.eval(page)) {
+        DoubleVector dVector = dBlock.asVector();
+        if (dVector == null) {
+          return eval(page.getPositionCount(), dBlock, scaleFactorBlock);
+        }
+        LongVector scaleFactorVector = scaleFactorBlock.asVector();
+        if (scaleFactorVector == null) {
+          return eval(page.getPositionCount(), dBlock, scaleFactorBlock);
+        }
+        return eval(page.getPositionCount(), dVector, scaleFactorVector);
+      }
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock dBlock, LongBlock scaleFactorBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (dBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (dBlock.getValueCount(p) != 1) {
+          if (dBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (scaleFactorBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (scaleFactorBlock.getValueCount(p) != 1) {
+          if (scaleFactorBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendDouble(Scalb.process(dBlock.getDouble(dBlock.getFirstValueIndex(p)), scaleFactorBlock.getLong(scaleFactorBlock.getFirstValueIndex(p))));
+        } catch (ArithmeticException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleVector dVector, LongVector scaleFactorVector) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendDouble(Scalb.process(dVector.getDouble(p), scaleFactorVector.getLong(p)));
+        } catch (ArithmeticException e) {
+          warnings().registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ScalbLongEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(d, scaleFactor);
+  }
+
+  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 d;
+
+    private final EvalOperator.ExpressionEvaluator.Factory scaleFactor;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory d,
+        EvalOperator.ExpressionEvaluator.Factory scaleFactor) {
+      this.source = source;
+      this.d = d;
+      this.scaleFactor = scaleFactor;
+    }
+
+    @Override
+    public ScalbLongEvaluator get(DriverContext context) {
+      return new ScalbLongEvaluator(source, d.get(context), scaleFactor.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "ScalbLongEvaluator[" + "d=" + d + ", scaleFactor=" + scaleFactor + "]";
+    }
+  }
+}

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

@@ -91,6 +91,11 @@ public class EsqlCapabilities {
          */
         FN_ROUND_UL_FIXES,
 
+        /**
+         * Support for function {@code SCALB}.
+         */
+        FN_SCALB,
+
         /**
          * All functions that take TEXT should never emit TEXT, only KEYWORD. #114334
          */

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

@@ -43,6 +43,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Cosh;
 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.Log10;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Scalb;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Signum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sinh;
@@ -167,6 +168,7 @@ public class ExpressionWritables {
         entries.add(Not.ENTRY);
         entries.add(RLike.ENTRY);
         entries.add(RTrim.ENTRY);
+        entries.add(Scalb.ENTRY);
         entries.add(Signum.ENTRY);
         entries.add(Sin.ENTRY);
         entries.add(Sinh.ENTRY);

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

@@ -89,6 +89,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Pi;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Pow;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Round;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.RoundTo;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.Scalb;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Signum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sinh;
@@ -313,6 +314,7 @@ public class EsqlFunctionRegistry {
                 def(Pow.class, Pow::new, "pow"),
                 def(Round.class, Round::new, "round"),
                 def(RoundTo.class, RoundTo::new, "round_to"),
+                def(Scalb.class, Scalb::new, "scalb"),
                 def(Signum.class, Signum::new, "signum"),
                 def(Sin.class, Sin::new, "sin"),
                 def(Sinh.class, Sinh::new, "sinh"),

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

@@ -47,7 +47,6 @@ public class Round extends EsqlScalarFunction implements OptionalArgument {
 
     private final Expression field, decimals;
 
-    // @TODO: add support for "integer", "long", "unsigned_long" once tests are fixed
     @FunctionInfo(returnType = { "double", "integer", "long", "unsigned_long" }, description = """
         Rounds a number to the specified number of decimal places.
         Defaults to 0, which returns the nearest integer. If the

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

@@ -0,0 +1,172 @@
+/*
+ * 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.Strings;
+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.ann.Fixed;
+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;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
+
+public class Scalb extends EsqlScalarFunction {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Scalb", Scalb::new);
+
+    private final Expression d;
+    private final Expression scaleFactor;
+
+    @FunctionInfo(returnType = "double", description = """
+        Returns the result of `d * 2 ^ scaleFactor`,
+        Similar to Java's `scalb` function. Result is rounded as if
+        performed by a single correctly rounded floating-point multiply
+        to a member of the double value set.""", examples = @Example(file = "floats", tag = "scalb"))
+
+    public Scalb(
+        Source source,
+        @Param(
+            name = "d",
+            type = { "double", "integer", "long", "unsigned_long" },
+            description = "Numeric expression for the multiplier. If `null`, the function returns `null`."
+        ) Expression d,
+        @Param(
+            name = "scaleFactor",
+            type = { "integer", "long" },
+            description = "Numeric expression for the scale factor. If `null`, the function returns `null`."
+        ) Expression scaleFactor
+    ) {
+        super(source, Arrays.asList(d, scaleFactor));
+        this.d = d;
+        this.scaleFactor = scaleFactor;
+    }
+
+    private Scalb(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(d);
+        out.writeNamedWriteable(scaleFactor);
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        TypeResolution resolution = isNumeric(d, sourceText(), FIRST);
+        if (resolution.unresolved()) {
+            return resolution;
+        }
+
+        return scaleFactor == null
+            ? TypeResolution.TYPE_RESOLVED
+            : isType(
+                scaleFactor,
+                dt -> dt.isWholeNumber() && dt != DataType.UNSIGNED_LONG,
+                sourceText(),
+                SECOND,
+                "whole number except unsigned_long or counter types"
+            );
+    }
+
+    @Override
+    public boolean foldable() {
+        return d.foldable() && scaleFactor.foldable();
+    }
+
+    @Evaluator(extraName = "Int", warnExceptions = { ArithmeticException.class })
+    static double process(double d, int scaleFactor) {
+        return NumericUtils.asFiniteNumber(Math.scalb(d, scaleFactor));
+    }
+
+    @Evaluator(extraName = "Long", warnExceptions = { ArithmeticException.class })
+    static double process(double d, long scaleFactor) {
+        return NumericUtils.asFiniteNumber(Math.scalb(d, Math.toIntExact(scaleFactor)));
+    }
+
+    @Evaluator(extraName = "ConstantInt", warnExceptions = { ArithmeticException.class })
+    static double processConstantInt(double d, @Fixed int scaleFactor) {
+        return NumericUtils.asFiniteNumber(Math.scalb(d, scaleFactor));
+    }
+
+    @Evaluator(extraName = "ConstantLong", warnExceptions = { ArithmeticException.class })
+    static double processConstantLong(double d, @Fixed long scaleFactor) {
+        return NumericUtils.asFiniteNumber(Math.scalb(d, Math.toIntExact(scaleFactor)));
+    }
+
+    @Override
+    public final Expression replaceChildren(List<Expression> newChildren) {
+        return new Scalb(source(), newChildren.get(0), newChildren.get(1));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, Scalb::new, d(), scaleFactor());
+    }
+
+    public Expression d() {
+        return d;
+    }
+
+    public Expression scaleFactor() {
+        return scaleFactor;
+    }
+
+    @Override
+    public DataType dataType() {
+        return DataType.DOUBLE;
+    }
+
+    @Override
+    public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+        var dEval = Cast.cast(source(), d.dataType(), DataType.DOUBLE, toEvaluator.apply(d));
+        if (scaleFactor.foldable()) {
+            return switch (scaleFactor.dataType()) {
+                case INTEGER -> new ScalbConstantIntEvaluator.Factory(source(), dEval, (Integer) (scaleFactor.fold(toEvaluator.foldCtx())));
+                case LONG -> new ScalbConstantLongEvaluator.Factory(source(), dEval, (Long) (scaleFactor.fold(toEvaluator.foldCtx())));
+                default -> throw new IllegalStateException("Invalid type for scaleFactor, should be int or long.");
+            };
+        }
+        var scaleFactorEval = toEvaluator.apply(scaleFactor);
+        return switch (scaleFactor.dataType()) {
+            case INTEGER -> new ScalbIntEvaluator.Factory(source(), dEval, scaleFactorEval);
+            case LONG -> new ScalbLongEvaluator.Factory(source(), dEval, scaleFactorEval);
+            default -> throw new IllegalStateException(
+                Strings.format("Invalid type for scaleFactor: %s, should be int or long.", scaleFactor.dataType())
+            );
+        };
+    }
+}

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

@@ -0,0 +1,137 @@
+/*
+ * 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.common.Strings;
+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.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class ScalbTests extends AbstractScalarFunctionTestCase {
+    public ScalbTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(
+            true,
+            List.of(
+                // Int scale factor
+                test(DataType.DOUBLE, DataType.INTEGER, 3.14d, 6, 200.96d),
+                test(DataType.INTEGER, DataType.INTEGER, 666, 6, 42624.0),
+                test(DataType.LONG, DataType.INTEGER, 23L, 5, 736.d),
+                test(DataType.UNSIGNED_LONG, DataType.INTEGER, 24325342L, 34, 1.584563250289466E29),
+
+                // Long scale factor
+                test(DataType.DOUBLE, DataType.LONG, 3.14d, 6L, 200.96d),
+                test(DataType.INTEGER, DataType.LONG, 666, 6L, 42624.0),
+                test(DataType.LONG, DataType.LONG, 23L, 5L, 736.d),
+                test(DataType.UNSIGNED_LONG, DataType.LONG, 24325342L, 34L, 1.584563250289466E29),
+
+                // null values
+                test(DataType.LONG, DataType.LONG, null, 34L, null),
+                test(DataType.LONG, DataType.LONG, 23L, null, null),
+
+                // Overflow - Result
+                test(
+                    DataType.LONG,
+                    DataType.LONG,
+                    24325342L,
+                    344444L,
+                    null,
+                    List.of(
+                        "Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded.",
+                        "Line 1:1: java.lang.ArithmeticException: not a finite double number: Infinity"
+                    )
+                ),
+
+                // Overflow in a constant scale factor
+
+                new TestCaseSupplier(
+                    "<long>, <long>",
+                    List.of(DataType.LONG, DataType.LONG),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(
+                            new TestCaseSupplier.TypedData(18446744L, DataType.LONG, "number"),
+                            new TestCaseSupplier.TypedData(2147483648L, DataType.LONG, "decimals")
+                        ),
+                        "ScalbLongEvaluator[d=CastLongToDoubleEvaluator[v=Attribute[channel=0]], scaleFactor=Attribute[channel=1]]",
+                        DataType.DOUBLE,
+                        equalTo(null)
+                    ).withWarning("Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded.")
+                        .withWarning("Line 1:1: java.lang.ArithmeticException: integer overflow")
+
+                )
+            )
+        );
+    }
+
+    private static TestCaseSupplier test(DataType dType, DataType scaleType, Number d, Number scaleFactor, Number res) {
+        return test(dType, scaleType, d, scaleFactor, res, List.of());
+    }
+
+    private static TestCaseSupplier test(
+        DataType dType,
+        DataType scaleType,
+        Number d,
+        Number scaleFactor,
+        Number res,
+        Iterable<String> warns
+    ) {
+        var supplier = new TestCaseSupplier.TestCase(
+            List.of(new TestCaseSupplier.TypedData(d, dType, "d"), new TestCaseSupplier.TypedData(scaleFactor, scaleType, "scaleFactor")),
+            Strings.format(
+                "Scalb%sEvaluator[d=%s, scaleFactor=Attribute[channel=1]]",
+                pascalize(scaleType),
+                cast(dType)
+
+            ),
+            DataType.DOUBLE,
+            equalTo(res)
+        );
+        for (var warn : warns) {
+            supplier = supplier.withWarning(warn);
+        }
+        var finalSupplier = supplier;
+        return new TestCaseSupplier(
+            Strings.format("<%s>, <%s>", dType.typeName(), scaleType.typeName()),
+            List.of(dType, scaleType),
+            () -> finalSupplier
+        );
+    }
+
+    private static String cast(DataType from) {
+        return from == DataType.DOUBLE
+            ? "Attribute[channel=0]"
+            : Strings.format("Cast%sToDoubleEvaluator[v=Attribute[channel=0]]", pascalize(from));
+    }
+
+    private static String pascalize(DataType type) {
+        return switch (type) {
+            case UNSIGNED_LONG -> "UnsignedLong";
+            case INTEGER -> "Int";
+            default -> Strings.capitalize(type.typeName());
+        };
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new Scalb(source, args.get(0), args.get(1));
+    }
+}

+ 2 - 2
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml

@@ -101,7 +101,7 @@ setup:
   - gt: {esql.functions.to_long: $functions_to_long}
   - match: {esql.functions.coalesce: $functions_coalesce}
   # Testing for the entire function set isn't feasible, so we just check that we return the correct count as an approximation.
-  - length: {esql.functions: 135} # check the "sister" test below for a likely update to the same esql.functions length check
+  - length: {esql.functions: 136} # check the "sister" test below for a likely update to the same esql.functions length check
 
 ---
 "Basic ESQL usage output (telemetry) non-snapshot version":
@@ -180,4 +180,4 @@ setup:
   - match: {esql.functions.cos: $functions_cos}
   - gt: {esql.functions.to_long: $functions_to_long}
   - match: {esql.functions.coalesce: $functions_coalesce}
-  - length: {esql.functions: 132} # check the "sister" test above for a likely update to the same esql.functions length check
+  - length: {esql.functions: 133} # check the "sister" test above for a likely update to the same esql.functions length check