Browse Source

ESQL: ROUND_TO function (#128278) (#128397)

Creates a `ROUND_TO` function that rounds it's input to one of the
provided values. Like so:

```
ROUND_TO(v, 0, 5000, 10000, 20000, 40000, 100000)

   v   | ROUND_TO
     0 | 0
   100 | 0
  6000 | 5000
 45001 | 40000
999999 | 100000
```

For some sequences of numbers you could do this with the `/` operator -
but for arbitrary sequences of numbers you needed `CASE` which is quite
slow. And hard to read!

Rewriting the example above would look like:

```
CASE (
  v <   5000,     0,
  v <  10000,  5000,
  v <  20000, 10000,
  v <  40000, 20000,
  v < 100000, 40000,
  100000
)
```

Even better, this is *fast*:

```
        (operation)  Mode  Cnt    Score   Error  Units
round_to_4_via_case  avgt    7  138.124 ± 0.738  ns/op
         round_to_4  avgt    7    0.805 ± 0.011  ns/op
         round_to_3  avgt    7    0.739 ± 0.011  ns/op
         round_to_2  avgt    7    0.651 ± 0.009  ns/op
         date_trunc  avgt    7    2.425 ± 0.018  ns/op
```

I've included a comparison to `DATE_TRUNC` above because we should be
able to rewrite `DATE_TRUNC` into `ROUND_TO` when we know the date range
of the index. This doesn't do it now, but it should be possible.
Nik Everett 4 tháng trước cách đây
mục cha
commit
fb6bccee4b
48 tập tin đã thay đổi với 4114 bổ sung6 xóa
  1. 150 1
      benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/EvalBenchmark.java
  2. 5 0
      docs/changelog/128278.yaml
  3. 5 0
      docs/changelog/128397.yaml
  4. 5 0
      docs/reference/esql/functions/description/round_to.asciidoc
  5. 13 0
      docs/reference/esql/functions/examples/round_to.asciidoc
  6. 211 0
      docs/reference/esql/functions/kibana/definition/round_to.json
  7. 21 0
      docs/reference/esql/functions/kibana/docs/round_to.md
  8. 15 0
      docs/reference/esql/functions/layout/round_to.asciidoc
  9. 2 0
      docs/reference/esql/functions/math-functions.asciidoc
  10. 9 0
      docs/reference/esql/functions/parameters/round_to.asciidoc
  11. 1 0
      docs/reference/esql/functions/signature/round_to.svg
  12. 19 0
      docs/reference/esql/functions/types/round_to.asciidoc
  13. 23 0
      docs/reference/query-languages/esql/_snippets/functions/layout/round_to.md
  14. 10 0
      docs/reference/query-languages/esql/_snippets/functions/parameters/round_to.md
  15. 18 0
      docs/reference/query-languages/esql/_snippets/functions/types/round_to.md
  16. 1 0
      docs/reference/query-languages/esql/images/functions/round_to.svg
  17. 17 0
      x-pack/plugin/esql/build.gradle
  18. 140 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec
  19. 105 0
      x-pack/plugin/esql/src/main/generated-src/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToDouble.java
  20. 105 0
      x-pack/plugin/esql/src/main/generated-src/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToInt.java
  21. 105 0
      x-pack/plugin/esql/src/main/generated-src/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToLong.java
  22. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToDouble1Evaluator.java
  23. 135 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToDouble2Evaluator.java
  24. 141 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToDouble3Evaluator.java
  25. 147 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToDouble4Evaluator.java
  26. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToDoubleBinarySearchEvaluator.java
  27. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToDoubleLinearSearchEvaluator.java
  28. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToInt1Evaluator.java
  29. 134 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToInt2Evaluator.java
  30. 141 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToInt3Evaluator.java
  31. 147 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToInt4Evaluator.java
  32. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToIntBinarySearchEvaluator.java
  33. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToIntLinearSearchEvaluator.java
  34. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToLong1Evaluator.java
  35. 135 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToLong2Evaluator.java
  36. 141 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToLong3Evaluator.java
  37. 147 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToLong4Evaluator.java
  38. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToLongBinarySearchEvaluator.java
  39. 128 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToLongLinearSearchEvaluator.java
  40. 2 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  41. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  42. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java
  43. 191 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundTo.java
  44. 105 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/X-RoundTo.java.st
  45. 45 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToErrorTests.java
  46. 59 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToSerializationTests.java
  47. 305 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToTests.java
  48. 3 3
      x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml

+ 150 - 1
benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/EvalBenchmark.java

@@ -12,6 +12,7 @@ package org.elasticsearch.benchmark.compute.operator;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.breaker.NoopCircuitBreaker;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.unit.ByteSizeUnit;
 import org.elasticsearch.common.util.BigArrays;
 import org.elasticsearch.compute.data.Block;
 import org.elasticsearch.compute.data.BlockFactory;
@@ -41,6 +42,7 @@ import org.elasticsearch.xpack.esql.evaluator.EvalMapper;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
 import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Abs;
+import org.elasticsearch.xpack.esql.expression.function.scalar.math.RoundTo;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike;
@@ -48,6 +50,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToLower;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToUpper;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
+import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThan;
 import org.elasticsearch.xpack.esql.planner.Layout;
 import org.elasticsearch.xpack.esql.plugin.EsqlPlugin;
 import org.elasticsearch.xpack.esql.session.Configuration;
@@ -116,6 +119,10 @@ public class EvalBenchmark {
             "long_equal_to_int",
             "mv_min",
             "mv_min_ascending",
+            "round_to_4_via_case",
+            "round_to_2",
+            "round_to_3",
+            "round_to_4",
             "rlike",
             "to_lower",
             "to_lower_ords",
@@ -228,6 +235,65 @@ public class EvalBenchmark {
                 RLike rlike = new RLike(Source.EMPTY, keywordField, new RLikePattern(".ar"));
                 yield EvalMapper.toEvaluator(FOLD_CONTEXT, rlike, layout(keywordField)).get(driverContext);
             }
+            case "round_to_4_via_case" -> {
+                FieldAttribute f = longField();
+
+                Expression ltkb = new LessThan(Source.EMPTY, f, kb());
+                Expression ltmb = new LessThan(Source.EMPTY, f, mb());
+                Expression ltgb = new LessThan(Source.EMPTY, f, gb());
+                EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
+                    FOLD_CONTEXT,
+                    new Case(Source.EMPTY, ltkb, List.of(b(), ltmb, kb(), ltgb, mb(), gb())),
+                    layout(f)
+                ).get(driverContext);
+                String desc = "CaseLazyEvaluator";
+                if (evaluator.toString().contains(desc) == false) {
+                    throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
+                }
+                yield evaluator;
+            }
+            case "round_to_2" -> {
+                FieldAttribute f = longField();
+
+                EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
+                    FOLD_CONTEXT,
+                    new RoundTo(Source.EMPTY, f, List.of(b(), kb())),
+                    layout(f)
+                ).get(driverContext);
+                String desc = "RoundToLong2";
+                if (evaluator.toString().contains(desc) == false) {
+                    throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
+                }
+                yield evaluator;
+            }
+            case "round_to_3" -> {
+                FieldAttribute f = longField();
+
+                EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
+                    FOLD_CONTEXT,
+                    new RoundTo(Source.EMPTY, f, List.of(b(), kb(), mb())),
+                    layout(f)
+                ).get(driverContext);
+                String desc = "RoundToLong3";
+                if (evaluator.toString().contains(desc) == false) {
+                    throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
+                }
+                yield evaluator;
+            }
+            case "round_to_4" -> {
+                FieldAttribute f = longField();
+
+                EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
+                    FOLD_CONTEXT,
+                    new RoundTo(Source.EMPTY, f, List.of(b(), kb(), mb(), gb())),
+                    layout(f)
+                ).get(driverContext);
+                String desc = "RoundToLong4";
+                if (evaluator.toString().contains(desc) == false) {
+                    throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
+                }
+                yield evaluator;
+            }
             case "to_lower", "to_lower_ords" -> {
                 FieldAttribute keywordField = keywordField();
                 ToLower toLower = new ToLower(Source.EMPTY, keywordField, configuration());
@@ -390,6 +456,69 @@ public class EvalBenchmark {
                     }
                 }
             }
+            case "round_to_4_via_case", "round_to_4" -> {
+                long b = 1;
+                long kb = ByteSizeUnit.KB.toBytes(1);
+                long mb = ByteSizeUnit.MB.toBytes(1);
+                long gb = ByteSizeUnit.GB.toBytes(1);
+
+                LongVector f = actual.<LongBlock>getBlock(0).asVector();
+                LongVector result = actual.<LongBlock>getBlock(1).asVector();
+                for (int i = 0; i < BLOCK_LENGTH; i++) {
+                    long expected = f.getLong(i);
+                    if (expected < kb) {
+                        expected = b;
+                    } else if (expected < mb) {
+                        expected = kb;
+                    } else if (expected < gb) {
+                        expected = mb;
+                    } else {
+                        expected = gb;
+                    }
+                    if (result.getLong(i) != expected) {
+                        throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
+                    }
+                }
+            }
+            case "round_to_3" -> {
+                long b = 1;
+                long kb = ByteSizeUnit.KB.toBytes(1);
+                long mb = ByteSizeUnit.MB.toBytes(1);
+
+                LongVector f = actual.<LongBlock>getBlock(0).asVector();
+                LongVector result = actual.<LongBlock>getBlock(1).asVector();
+                for (int i = 0; i < BLOCK_LENGTH; i++) {
+                    long expected = f.getLong(i);
+                    if (expected < kb) {
+                        expected = b;
+                    } else if (expected < mb) {
+                        expected = kb;
+                    } else {
+                        expected = mb;
+                    }
+                    if (result.getLong(i) != expected) {
+                        throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
+                    }
+                }
+            }
+            case "round_to_2" -> {
+                long b = 1;
+                long kb = ByteSizeUnit.KB.toBytes(1);
+
+                LongVector f = actual.<LongBlock>getBlock(0).asVector();
+                LongVector result = actual.<LongBlock>getBlock(1).asVector();
+                for (int i = 0; i < BLOCK_LENGTH; i++) {
+                    long expected = f.getLong(i);
+                    if (expected < kb) {
+                        expected = b;
+                    } else {
+                        expected = kb;
+                    }
+                    if (result.getLong(i) != expected) {
+                        throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
+                    }
+                }
+            }
             case "to_lower" -> checkBytes(operation, actual, false, new BytesRef[] { new BytesRef("foo"), new BytesRef("bar") });
             case "to_lower_ords" -> checkBytes(operation, actual, true, new BytesRef[] { new BytesRef("foo"), new BytesRef("bar") });
             case "to_upper" -> checkBytes(operation, actual, false, new BytesRef[] { new BytesRef("FOO"), new BytesRef("BAR") });
@@ -421,7 +550,7 @@ public class EvalBenchmark {
 
     private static Page page(String operation) {
         return switch (operation) {
-            case "abs", "add", "date_trunc", "equal_to_const" -> {
+            case "abs", "add", "date_trunc", "equal_to_const", "round_to_4_via_case", "round_to_2", "round_to_3", "round_to_4" -> {
                 var builder = blockFactory.newLongBlockBuilder(BLOCK_LENGTH);
                 for (int i = 0; i < BLOCK_LENGTH; i++) {
                     builder.appendLong(i * 100_000);
@@ -511,6 +640,26 @@ public class EvalBenchmark {
         };
     }
 
+    private static Literal b() {
+        return lit(1L);
+    }
+
+    private static Literal kb() {
+        return lit(ByteSizeUnit.KB.toBytes(1));
+    }
+
+    private static Literal mb() {
+        return lit(ByteSizeUnit.MB.toBytes(1));
+    }
+
+    private static Literal gb() {
+        return lit(ByteSizeUnit.GB.toBytes(1));
+    }
+
+    private static Literal lit(long v) {
+        return new Literal(Source.EMPTY, v, DataType.LONG);
+    }
+
     @Benchmark
     @OperationsPerInvocation(1024 * BLOCK_LENGTH)
     public void run() {

+ 5 - 0
docs/changelog/128278.yaml

@@ -0,0 +1,5 @@
+pr: 128278
+summary: ROUND_TO function
+area: ES|QL
+type: enhancement
+issues: []

+ 5 - 0
docs/changelog/128397.yaml

@@ -0,0 +1,5 @@
+pr: 128397
+summary: ROUND_TO function
+area: ES|QL
+type: enhancement
+issues: []

+ 5 - 0
docs/reference/esql/functions/description/round_to.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*
+
+Rounds down to one of a list of fixed points.

+ 13 - 0
docs/reference/esql/functions/examples/round_to.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=round_to]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/math.csv-spec[tag=round_to-result]
+|===
+

+ 211 - 0
docs/reference/esql/functions/kibana/definition/round_to.json

@@ -0,0 +1,211 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "round_to",
+  "description" : "Rounds down to one of a list of fixed points.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "date",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "date",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "date"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "date_nanos",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "date_nanos"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "double",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "double",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "double",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "integer"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "long"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "long",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "double",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "long",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "integer",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "long"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
+          "type" : "long",
+          "optional" : false,
+          "description" : "The numeric value to round. If `null`, the function returns `null`."
+        },
+        {
+          "name" : "points",
+          "type" : "long",
+          "optional" : false,
+          "description" : "Remaining rounding points. Must be constants."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "long"
+    }
+  ],
+  "examples" : [
+    "FROM employees\n| STATS COUNT(*) BY birth_window=ROUND_TO(\n    birth_date,\n    \"1900-01-01T00:00:00Z\"::DATETIME,\n    \"1950-01-01T00:00:00Z\"::DATETIME,\n    \"1955-01-01T00:00:00Z\"::DATETIME,\n    \"1960-01-01T00:00:00Z\"::DATETIME,\n    \"1965-01-01T00:00:00Z\"::DATETIME,\n    \"1970-01-01T00:00:00Z\"::DATETIME,\n    \"1975-01-01T00:00:00Z\"::DATETIME\n)\n| SORT birth_window ASC"
+  ],
+  "preview" : false,
+  "snapshot_only" : false
+}

+ 21 - 0
docs/reference/esql/functions/kibana/docs/round_to.md

@@ -0,0 +1,21 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### ROUND_TO
+Rounds down to one of a list of fixed points.
+
+```
+FROM employees
+| STATS COUNT(*) BY birth_window=ROUND_TO(
+    birth_date,
+    "1900-01-01T00:00:00Z"::DATETIME,
+    "1950-01-01T00:00:00Z"::DATETIME,
+    "1955-01-01T00:00:00Z"::DATETIME,
+    "1960-01-01T00:00:00Z"::DATETIME,
+    "1965-01-01T00:00:00Z"::DATETIME,
+    "1970-01-01T00:00:00Z"::DATETIME,
+    "1975-01-01T00:00:00Z"::DATETIME
+)
+| SORT birth_window ASC
+```

+ 15 - 0
docs/reference/esql/functions/layout/round_to.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-round_to]]
+=== `ROUND_TO`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/round_to.svg[Embedded,opts=inline]
+
+include::../parameters/round_to.asciidoc[]
+include::../description/round_to.asciidoc[]
+include::../types/round_to.asciidoc[]
+include::../examples/round_to.asciidoc[]

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

@@ -26,6 +26,7 @@
 * <<esql-pi>>
 * <<esql-pow>>
 * <<esql-round>>
+* <<esql-round_to>>
 * <<esql-signum>>
 * <<esql-sin>>
 * <<esql-sinh>>
@@ -53,6 +54,7 @@ include::layout/log10.asciidoc[]
 include::layout/pi.asciidoc[]
 include::layout/pow.asciidoc[]
 include::layout/round.asciidoc[]
+include::layout/round_to.asciidoc[]
 include::layout/signum.asciidoc[]
 include::layout/sin.asciidoc[]
 include::layout/sinh.asciidoc[]

+ 9 - 0
docs/reference/esql/functions/parameters/round_to.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*
+
+`field`::
+The numeric value to round. If `null`, the function returns `null`.
+
+`points`::
+Remaining rounding points. Must be constants.

+ 1 - 0
docs/reference/esql/functions/signature/round_to.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 31h5m116 0h10m32 0h10m80 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="116" height="36"/><text class="k" x="15" y="31">ROUND_TO</text><rect class="s" x="131" y="5" width="32" height="36" rx="7"/><text class="syn" x="141" y="31">(</text><rect class="s" x="173" y="5" width="80" height="36" rx="7"/><text class="k" x="183" y="31">field</text><rect class="s" x="263" y="5" width="32" height="36" rx="7"/><text class="syn" x="273" y="31">,</text><rect class="s" x="305" y="5" width="92" height="36" rx="7"/><text class="k" x="315" y="31">points</text><rect class="s" x="407" y="5" width="32" height="36" rx="7"/><text class="syn" x="417" y="31">)</text></svg>

+ 19 - 0
docs/reference/esql/functions/types/round_to.asciidoc

@@ -0,0 +1,19 @@
+// 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=|]
+|===
+field | points | result
+date | date | date
+date_nanos | date_nanos | date_nanos
+double | double | double
+double | integer | double
+double | long | double
+integer | double | double
+integer | integer | integer
+integer | long | long
+long | double | double
+long | integer | long
+long | long | long
+|===

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

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

+ 10 - 0
docs/reference/query-languages/esql/_snippets/functions/parameters/round_to.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**
+
+`field`
+:   The numeric value to round. If `null`, the function returns `null`.
+
+`points`
+:   Remaining rounding points. Must be constants.
+

+ 18 - 0
docs/reference/query-languages/esql/_snippets/functions/types/round_to.md

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

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

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="484" height="61" viewbox="0 0 484 61"><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 46h5m116 0h10m32 0h10m80 0h30m-5 0q-5 0-5-5v-26q0-5 5-5h144q5 0 5 5v26q0 5-5 5m-107 0h10m92 0h30m32 0h5"/><rect class="s" x="5" y="20" width="116" height="36"/><text class="k" x="15" y="46">ROUND_TO</text><rect class="s" x="131" y="20" width="32" height="36" rx="7"/><text class="syn" x="141" y="46">(</text><rect class="s" x="173" y="20" width="80" height="36" rx="7"/><text class="k" x="183" y="46">field</text><rect class="s" x="283" y="20" width="32" height="36" rx="7"/><text class="syn" x="293" y="46">,</text><rect class="s" x="325" y="20" width="92" height="36" rx="7"/><text class="k" x="335" y="46">points</text><rect class="s" x="447" y="20" width="32" height="36" rx="7"/><text class="syn" x="457" y="46">)</text></svg>

+ 17 - 0
x-pack/plugin/esql/build.gradle

@@ -371,4 +371,21 @@ tasks.named('stringTemplates').configure {
     it.inputFile =  coalesceInputFile
     it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/nulls/CoalesceBytesRefEvaluator.java"
   }
+
+  File roundToInput = file("src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/X-RoundTo.java.st")
+  template {
+    it.properties = intProperties
+    it.inputFile = roundToInput
+    it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToInt.java"
+  }
+  template {
+    it.properties = longProperties
+    it.inputFile = roundToInput
+    it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToLong.java"
+  }
+  template {
+    it.properties = doubleProperties
+    it.inputFile = roundToInput
+    it.outputFile = "org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundToDouble.java"
+  }
 }

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

@@ -813,6 +813,146 @@ ul:ul
 null
 ;
 
+roundToInt
+required_capability: round_to
+ROW v = 1
+| EVAL r1 = ROUND_TO(v, 0, 1, 2)
+     , r2 = ROUND_TO(v, 2, 3)
+     , r3 = ROUND_TO(v, 0, 10, 20, 30, 40, 50, 60, 70, 90)
+     , rl = ROUND_TO(v, 0::LONG, 100)
+     , rd = ROUND_TO(v, 0, 1.1, 2.2);
+
+v:integer | r1:integer | r2:integer | r3:integer | rl:long | rd:double
+        1 |          1 |          2 |          0 |       0 | 0.0
+;
+
+roundToLong
+required_capability: round_to
+ROW v = 1000000000000::LONG
+| EVAL r1 = ROUND_TO(v, 0, 1000000000000, 2000000000000)
+     , r2 = ROUND_TO(v, 2000000000000, 3000000000000)
+     , r3 = ROUND_TO(v, 0, 1000000000000, 2000000000000, 3000000000000, 4000000000000, 5000000000000, 6000000000000)
+     , rd = ROUND_TO(v, 0, 1.1, 2.2);
+
+   v:long     |    r1:long    |    r2:long    |    r3:long    | rd:double
+1000000000000 | 1000000000000 | 2000000000000 | 1000000000000 | 2.2
+;
+
+roundToDouble
+required_capability: round_to
+ROW v = 1.1
+| EVAL r1 = ROUND_TO(v, 0, 1, 2)
+     , r2 = ROUND_TO(v, 2, 3)
+     , r3 = ROUND_TO(v, 0, 10, 20, 30, 40, 50, 60, 70, 90)
+     , rl = ROUND_TO(v, 0::LONG, 100)
+     , rd = ROUND_TO(v, 0, 1.1, 2.2);
+
+v:double | r1:double | r2:double | r3:double | rl:double | rd:double
+     1.1 |       1.0 |       2.0 |       0.0 |       0.0 | 1.1
+;
+
+roundToDate
+required_capability: round_to
+ROW v = "2025-02-02T01:00:00Z"::DATE
+| EVAL r1 = ROUND_TO(v, "2025-01-01T00:00:00Z"::DATE, "2025-02-01T00:00:00Z"::DATE, "2025-03-01T00:00:00Z"::DATE)
+     , r2 = ROUND_TO(v, "2025-04-01T00:00:00Z"::DATE, "2025-05-01T00:00:00Z"::DATE, "2025-06-01T00:00:00Z"::DATE)
+     , r3 = ROUND_TO(v, "2025-01-01T00:00:00Z"::DATE, "2025-02-01T00:00:00Z"::DATE, "2025-03-01T00:00:00Z"::DATE, "2025-04-01T00:00:00Z"::DATE, "2025-05-01T00:00:00Z"::DATE, "2025-06-01T00:00:00Z"::DATE);
+
+        v:date       |        r1:date       |        r2:date       |        r3:date
+2025-02-02T01:00:00Z | 2025-02-01T00:00:00Z | 2025-04-01T00:00:00Z | 2025-02-01T00:00:00Z
+;
+
+roundToDateNanos
+required_capability: round_to
+ROW v = "2025-02-02T01:00:00.543123456Z"::DATE_NANOS
+| EVAL r1 = ROUND_TO(v, "2025-01-01T00:00:00Z"::DATE_NANOS, "2025-02-01T00:00:00Z"::DATE_NANOS, "2025-03-01T00:00:00Z"::DATE_NANOS)
+     , r2 = ROUND_TO(v, "2025-04-01T00:00:00Z"::DATE_NANOS, "2025-05-01T00:00:00Z"::DATE_NANOS, "2025-06-01T00:00:00Z"::DATE_NANOS)
+     , r3 = ROUND_TO(v, "2025-01-01T00:00:00Z"::DATE_NANOS, "2025-02-01T00:00:00Z"::DATE_NANOS, "2025-03-01T00:00:00Z"::DATE_NANOS, "2025-04-01T00:00:00Z"::DATE_NANOS, "2025-05-01T00:00:00Z"::DATE_NANOS, "2025-06-01T00:00:00Z"::DATE_NANOS);
+
+          v:date_nanos         |          r1:date_nanos         |          r2:date_nanos         |         r3:date_nanos
+2025-02-02T01:00:00.543123456Z | 2025-02-01T00:00:00.000000000Z | 2025-04-01T00:00:00.000000000Z | 2025-02-01T00:00:00.000000000Z
+;
+
+roundToBirthWindow
+required_capability: round_to
+// tag::round_to[]
+FROM employees
+| STATS COUNT(*) BY birth_window=ROUND_TO(
+    birth_date,
+    "1900-01-01T00:00:00Z"::DATETIME,
+    "1950-01-01T00:00:00Z"::DATETIME,
+    "1955-01-01T00:00:00Z"::DATETIME,
+    "1960-01-01T00:00:00Z"::DATETIME,
+    "1965-01-01T00:00:00Z"::DATETIME,
+    "1970-01-01T00:00:00Z"::DATETIME,
+    "1975-01-01T00:00:00Z"::DATETIME
+)
+| SORT birth_window ASC
+// end::round_to[]
+;
+
+// tag::round_to-result[]
+COUNT(*):long | birth_window:datetime
+           27 | 1950-01-01T00:00:00Z
+           29 | 1955-01-01T00:00:00Z
+           33 | 1960-01-01T00:00:00Z
+            1 | 1965-01-01T00:00:00Z
+           10 | null
+// end::round_to-result[]
+;
+
+roundToSalaryWindow
+required_capability: round_to
+FROM employees
+| STATS COUNT(*) BY salary=ROUND_TO(
+    salary,
+    0,
+    20000,
+    25000,
+    30000,
+    40000,
+    50000,
+    70000,
+    90000
+)
+| SORT salary ASC
+;
+
+COUNT(*):long | salary:integer
+            9 | 25000
+           27 | 30000
+           22 | 40000
+           34 | 50000
+            8 | 70000
+;
+
+roundToNanos
+required_capability: round_to
+FROM sample_data_ts_nanos
+| STATS COUNT(*) BY @timestamp=ROUND_TO(
+    @timestamp,
+    "2023-10-23T13:33:34.937123456Z"::DATE_NANOS,
+    "2023-10-23T13:52:55.015123456Z"::DATE_NANOS,
+    "2023-10-23T13:55:01.543123456Z"::DATE_NANOS
+)
+| SORT @timestamp ASC
+;
+
+COUNT(*):long | @timestamp:date_nanos
+            4 | 2023-10-23T13:33:34.937123456Z
+            2 | 2023-10-23T13:52:55.015123456Z
+            1 | 2023-10-23T13:55:01.543123456Z
+;
+
+roundToUnsorted
+required_capability: round_to
+ROW v = 1
+| EVAL r = ROUND_TO(v, 2, -1, 9, 0, 100);
+
+v:integer | r:integer
+        1 |         0
+;
+
 mvAvg
 from employees | where emp_no > 10008 | eval salary_change = mv_avg(salary_change) | sort emp_no | keep emp_no, salary_change.int, salary_change | limit 7;
 

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

@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+// begin generated imports
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+
+import java.util.Arrays;
+// end generated imports
+
+/**
+ * Implementations of {@link RoundTo} for specific types.
+ * <p>
+ *   We have specializations for when there are very few rounding points because
+ *   those are very fast and quite common.
+ * </p>
+ * This class is generated. Edit {@code X-RoundTo.java.st} instead.
+ */
+class RoundToDouble {
+    static final RoundTo.Build BUILD = (source, field, points) -> {
+        double[] f = points.stream().mapToDouble(p -> ((Number) p).doubleValue()).toArray();
+        Arrays.sort(f);
+        return switch (f.length) {
+            // TODO should be a consistent way to do the 0 version - is CASE(MV_COUNT(f) == 1, f[0])
+            case 1 -> new RoundToDouble1Evaluator.Factory(source, field, f[0]);
+            /*
+             * These hand-unrolled implementations are even faster than the linear scan implementations.
+             */
+            case 2 -> new RoundToDouble2Evaluator.Factory(source, field, f[0], f[1]);
+            case 3 -> new RoundToDouble3Evaluator.Factory(source, field, f[0], f[1], f[2]);
+            case 4 -> new RoundToDouble4Evaluator.Factory(source, field, f[0], f[1], f[2], f[3]);
+            /*
+             * Break point of 10 experimentally derived on Nik's laptop (13th Gen Intel(R) Core(TM) i7-1370P)
+             * on 2025-05-22.
+             */
+            case 5, 6, 7, 8, 9, 10 -> new RoundToDoubleLinearSearchEvaluator.Factory(source, field, f);
+            default -> new RoundToDoubleBinarySearchEvaluator.Factory(source, field, f);
+        };
+    };
+
+    /**
+     * Search the points array for the match linearly. This is faster for smaller arrays even
+     * when finding a position late in the array. Presumably because this is super-SIMD-able.
+     */
+    @Evaluator(extraName = "LinearSearch")
+    static double processLinear(double field, @Fixed(includeInToString = false) double[] points) {
+        // points is always longer than 3 or we use one of the specialized methods below
+        for (int i = 1; i < points.length; i++) {
+            if (field < points[i]) {
+                return points[i - 1];
+            }
+        }
+        return points[points.length - 1];
+    }
+
+    @Evaluator(extraName = "BinarySearch")
+    static double process(double field, @Fixed(includeInToString = false) double[] points) {
+        int idx = Arrays.binarySearch(points, field);
+        return points[idx >= 0 ? idx : Math.max(0, -idx - 2)];
+    }
+
+    @Evaluator(extraName = "1")
+    static double process(double field, @Fixed double p0) {
+        return p0;
+    }
+
+    @Evaluator(extraName = "2")
+    static double process(double field, @Fixed double p0, @Fixed double p1) {
+        if (field < p1) {
+            return p0;
+        }
+        return p1;
+    }
+
+    @Evaluator(extraName = "3")
+    static double process(double field, @Fixed double p0, @Fixed double p1, @Fixed double p2) {
+        if (field < p1) {
+            return p0;
+        }
+        if (field < p2) {
+            return p1;
+        }
+        return p2;
+    }
+
+    @Evaluator(extraName = "4")
+    static double process(double field, @Fixed double p0, @Fixed double p1, @Fixed double p2, @Fixed double p3) {
+        if (field < p1) {
+            return p0;
+        }
+        if (field < p2) {
+            return p1;
+        }
+        if (field < p3) {
+            return p2;
+        }
+        return p3;
+    }
+}

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

@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+// begin generated imports
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+
+import java.util.Arrays;
+// end generated imports
+
+/**
+ * Implementations of {@link RoundTo} for specific types.
+ * <p>
+ *   We have specializations for when there are very few rounding points because
+ *   those are very fast and quite common.
+ * </p>
+ * This class is generated. Edit {@code X-RoundTo.java.st} instead.
+ */
+class RoundToInt {
+    static final RoundTo.Build BUILD = (source, field, points) -> {
+        int[] f = points.stream().mapToInt(p -> ((Number) p).intValue()).toArray();
+        Arrays.sort(f);
+        return switch (f.length) {
+            // TODO should be a consistent way to do the 0 version - is CASE(MV_COUNT(f) == 1, f[0])
+            case 1 -> new RoundToInt1Evaluator.Factory(source, field, f[0]);
+            /*
+             * These hand-unrolled implementations are even faster than the linear scan implementations.
+             */
+            case 2 -> new RoundToInt2Evaluator.Factory(source, field, f[0], f[1]);
+            case 3 -> new RoundToInt3Evaluator.Factory(source, field, f[0], f[1], f[2]);
+            case 4 -> new RoundToInt4Evaluator.Factory(source, field, f[0], f[1], f[2], f[3]);
+            /*
+             * Break point of 10 experimentally derived on Nik's laptop (13th Gen Intel(R) Core(TM) i7-1370P)
+             * on 2025-05-22.
+             */
+            case 5, 6, 7, 8, 9, 10 -> new RoundToIntLinearSearchEvaluator.Factory(source, field, f);
+            default -> new RoundToIntBinarySearchEvaluator.Factory(source, field, f);
+        };
+    };
+
+    /**
+     * Search the points array for the match linearly. This is faster for smaller arrays even
+     * when finding a position late in the array. Presumably because this is super-SIMD-able.
+     */
+    @Evaluator(extraName = "LinearSearch")
+    static int processLinear(int field, @Fixed(includeInToString = false) int[] points) {
+        // points is always longer than 3 or we use one of the specialized methods below
+        for (int i = 1; i < points.length; i++) {
+            if (field < points[i]) {
+                return points[i - 1];
+            }
+        }
+        return points[points.length - 1];
+    }
+
+    @Evaluator(extraName = "BinarySearch")
+    static int process(int field, @Fixed(includeInToString = false) int[] points) {
+        int idx = Arrays.binarySearch(points, field);
+        return points[idx >= 0 ? idx : Math.max(0, -idx - 2)];
+    }
+
+    @Evaluator(extraName = "1")
+    static int process(int field, @Fixed int p0) {
+        return p0;
+    }
+
+    @Evaluator(extraName = "2")
+    static int process(int field, @Fixed int p0, @Fixed int p1) {
+        if (field < p1) {
+            return p0;
+        }
+        return p1;
+    }
+
+    @Evaluator(extraName = "3")
+    static int process(int field, @Fixed int p0, @Fixed int p1, @Fixed int p2) {
+        if (field < p1) {
+            return p0;
+        }
+        if (field < p2) {
+            return p1;
+        }
+        return p2;
+    }
+
+    @Evaluator(extraName = "4")
+    static int process(int field, @Fixed int p0, @Fixed int p1, @Fixed int p2, @Fixed int p3) {
+        if (field < p1) {
+            return p0;
+        }
+        if (field < p2) {
+            return p1;
+        }
+        if (field < p3) {
+            return p2;
+        }
+        return p3;
+    }
+}

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

@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+// begin generated imports
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+
+import java.util.Arrays;
+// end generated imports
+
+/**
+ * Implementations of {@link RoundTo} for specific types.
+ * <p>
+ *   We have specializations for when there are very few rounding points because
+ *   those are very fast and quite common.
+ * </p>
+ * This class is generated. Edit {@code X-RoundTo.java.st} instead.
+ */
+class RoundToLong {
+    static final RoundTo.Build BUILD = (source, field, points) -> {
+        long[] f = points.stream().mapToLong(p -> ((Number) p).longValue()).toArray();
+        Arrays.sort(f);
+        return switch (f.length) {
+            // TODO should be a consistent way to do the 0 version - is CASE(MV_COUNT(f) == 1, f[0])
+            case 1 -> new RoundToLong1Evaluator.Factory(source, field, f[0]);
+            /*
+             * These hand-unrolled implementations are even faster than the linear scan implementations.
+             */
+            case 2 -> new RoundToLong2Evaluator.Factory(source, field, f[0], f[1]);
+            case 3 -> new RoundToLong3Evaluator.Factory(source, field, f[0], f[1], f[2]);
+            case 4 -> new RoundToLong4Evaluator.Factory(source, field, f[0], f[1], f[2], f[3]);
+            /*
+             * Break point of 10 experimentally derived on Nik's laptop (13th Gen Intel(R) Core(TM) i7-1370P)
+             * on 2025-05-22.
+             */
+            case 5, 6, 7, 8, 9, 10 -> new RoundToLongLinearSearchEvaluator.Factory(source, field, f);
+            default -> new RoundToLongBinarySearchEvaluator.Factory(source, field, f);
+        };
+    };
+
+    /**
+     * Search the points array for the match linearly. This is faster for smaller arrays even
+     * when finding a position late in the array. Presumably because this is super-SIMD-able.
+     */
+    @Evaluator(extraName = "LinearSearch")
+    static long processLinear(long field, @Fixed(includeInToString = false) long[] points) {
+        // points is always longer than 3 or we use one of the specialized methods below
+        for (int i = 1; i < points.length; i++) {
+            if (field < points[i]) {
+                return points[i - 1];
+            }
+        }
+        return points[points.length - 1];
+    }
+
+    @Evaluator(extraName = "BinarySearch")
+    static long process(long field, @Fixed(includeInToString = false) long[] points) {
+        int idx = Arrays.binarySearch(points, field);
+        return points[idx >= 0 ? idx : Math.max(0, -idx - 2)];
+    }
+
+    @Evaluator(extraName = "1")
+    static long process(long field, @Fixed long p0) {
+        return p0;
+    }
+
+    @Evaluator(extraName = "2")
+    static long process(long field, @Fixed long p0, @Fixed long p1) {
+        if (field < p1) {
+            return p0;
+        }
+        return p1;
+    }
+
+    @Evaluator(extraName = "3")
+    static long process(long field, @Fixed long p0, @Fixed long p1, @Fixed long p2) {
+        if (field < p1) {
+            return p0;
+        }
+        if (field < p2) {
+            return p1;
+        }
+        return p2;
+    }
+
+    @Evaluator(extraName = "4")
+    static long process(long field, @Fixed long p0, @Fixed long p1, @Fixed long p2, @Fixed long p3) {
+        if (field < p1) {
+            return p0;
+        }
+        if (field < p2) {
+            return p1;
+        }
+        if (field < p3) {
+            return p2;
+        }
+        return p3;
+    }
+}

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

@@ -0,0 +1,128 @@
+// 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.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link RoundToDouble}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToDouble1Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final double p0;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToDouble1Evaluator(Source source, EvalOperator.ExpressionEvaluator field, double p0,
+      DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock fieldBlock = (DoubleBlock) field.eval(page)) {
+      DoubleVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock fieldBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendDouble(RoundToDouble.process(fieldBlock.getDouble(fieldBlock.getFirstValueIndex(p)), this.p0));
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector fieldVector) {
+    try(DoubleVector.FixedBuilder result = driverContext.blockFactory().newDoubleVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendDouble(p, RoundToDouble.process(fieldVector.getDouble(p), this.p0));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToDouble1Evaluator[" + "field=" + field + ", p0=" + p0 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final double p0;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, double p0) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+    }
+
+    @Override
+    public RoundToDouble1Evaluator get(DriverContext context) {
+      return new RoundToDouble1Evaluator(source, field.get(context), p0, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToDouble1Evaluator[" + "field=" + field + ", p0=" + p0 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,135 @@
+// 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.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link RoundToDouble}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToDouble2Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final double p0;
+
+  private final double p1;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToDouble2Evaluator(Source source, EvalOperator.ExpressionEvaluator field, double p0,
+      double p1, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock fieldBlock = (DoubleBlock) field.eval(page)) {
+      DoubleVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock fieldBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendDouble(RoundToDouble.process(fieldBlock.getDouble(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1));
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector fieldVector) {
+    try(DoubleVector.FixedBuilder result = driverContext.blockFactory().newDoubleVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendDouble(p, RoundToDouble.process(fieldVector.getDouble(p), this.p0, this.p1));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToDouble2Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final double p0;
+
+    private final double p1;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, double p0,
+        double p1) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+    }
+
+    @Override
+    public RoundToDouble2Evaluator get(DriverContext context) {
+      return new RoundToDouble2Evaluator(source, field.get(context), p0, p1, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToDouble2Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,141 @@
+// 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.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link RoundToDouble}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToDouble3Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final double p0;
+
+  private final double p1;
+
+  private final double p2;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToDouble3Evaluator(Source source, EvalOperator.ExpressionEvaluator field, double p0,
+      double p1, double p2, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.p2 = p2;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock fieldBlock = (DoubleBlock) field.eval(page)) {
+      DoubleVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock fieldBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendDouble(RoundToDouble.process(fieldBlock.getDouble(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1, this.p2));
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector fieldVector) {
+    try(DoubleVector.FixedBuilder result = driverContext.blockFactory().newDoubleVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendDouble(p, RoundToDouble.process(fieldVector.getDouble(p), this.p0, this.p1, this.p2));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToDouble3Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final double p0;
+
+    private final double p1;
+
+    private final double p2;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, double p0,
+        double p1, double p2) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+      this.p2 = p2;
+    }
+
+    @Override
+    public RoundToDouble3Evaluator get(DriverContext context) {
+      return new RoundToDouble3Evaluator(source, field.get(context), p0, p1, p2, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToDouble3Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,147 @@
+// 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.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link RoundToDouble}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToDouble4Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final double p0;
+
+  private final double p1;
+
+  private final double p2;
+
+  private final double p3;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToDouble4Evaluator(Source source, EvalOperator.ExpressionEvaluator field, double p0,
+      double p1, double p2, double p3, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.p2 = p2;
+    this.p3 = p3;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock fieldBlock = (DoubleBlock) field.eval(page)) {
+      DoubleVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock fieldBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendDouble(RoundToDouble.process(fieldBlock.getDouble(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1, this.p2, this.p3));
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector fieldVector) {
+    try(DoubleVector.FixedBuilder result = driverContext.blockFactory().newDoubleVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendDouble(p, RoundToDouble.process(fieldVector.getDouble(p), this.p0, this.p1, this.p2, this.p3));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToDouble4Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + ", p3=" + p3 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final double p0;
+
+    private final double p1;
+
+    private final double p2;
+
+    private final double p3;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, double p0,
+        double p1, double p2, double p3) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+      this.p2 = p2;
+      this.p3 = p3;
+    }
+
+    @Override
+    public RoundToDouble4Evaluator get(DriverContext context) {
+      return new RoundToDouble4Evaluator(source, field.get(context), p0, p1, p2, p3, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToDouble4Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + ", p3=" + p3 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,128 @@
+// 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.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link RoundToDouble}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToDoubleBinarySearchEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final double[] points;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToDoubleBinarySearchEvaluator(Source source, EvalOperator.ExpressionEvaluator field,
+      double[] points, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.points = points;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock fieldBlock = (DoubleBlock) field.eval(page)) {
+      DoubleVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock fieldBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendDouble(RoundToDouble.process(fieldBlock.getDouble(fieldBlock.getFirstValueIndex(p)), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector fieldVector) {
+    try(DoubleVector.FixedBuilder result = driverContext.blockFactory().newDoubleVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendDouble(p, RoundToDouble.process(fieldVector.getDouble(p), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToDoubleBinarySearchEvaluator[" + "field=" + field + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final double[] points;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, double[] points) {
+      this.source = source;
+      this.field = field;
+      this.points = points;
+    }
+
+    @Override
+    public RoundToDoubleBinarySearchEvaluator get(DriverContext context) {
+      return new RoundToDoubleBinarySearchEvaluator(source, field.get(context), points, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToDoubleBinarySearchEvaluator[" + "field=" + field + "]";
+    }
+  }
+}

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

@@ -0,0 +1,128 @@
+// 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.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link RoundToDouble}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToDoubleLinearSearchEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final double[] points;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToDoubleLinearSearchEvaluator(Source source, EvalOperator.ExpressionEvaluator field,
+      double[] points, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.points = points;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (DoubleBlock fieldBlock = (DoubleBlock) field.eval(page)) {
+      DoubleVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock fieldBlock) {
+    try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendDouble(RoundToDouble.processLinear(fieldBlock.getDouble(fieldBlock.getFirstValueIndex(p)), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector fieldVector) {
+    try(DoubleVector.FixedBuilder result = driverContext.blockFactory().newDoubleVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendDouble(p, RoundToDouble.processLinear(fieldVector.getDouble(p), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToDoubleLinearSearchEvaluator[" + "field=" + field + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final double[] points;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, double[] points) {
+      this.source = source;
+      this.field = field;
+      this.points = points;
+    }
+
+    @Override
+    public RoundToDoubleLinearSearchEvaluator get(DriverContext context) {
+      return new RoundToDoubleLinearSearchEvaluator(source, field.get(context), points, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToDoubleLinearSearchEvaluator[" + "field=" + field + "]";
+    }
+  }
+}

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

@@ -0,0 +1,128 @@
+// 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.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 RoundToInt}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToInt1Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final int p0;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToInt1Evaluator(Source source, EvalOperator.ExpressionEvaluator field, int p0,
+      DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (IntBlock fieldBlock = (IntBlock) field.eval(page)) {
+      IntVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public IntBlock eval(int positionCount, IntBlock fieldBlock) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendInt(RoundToInt.process(fieldBlock.getInt(fieldBlock.getFirstValueIndex(p)), this.p0));
+      }
+      return result.build();
+    }
+  }
+
+  public IntVector eval(int positionCount, IntVector fieldVector) {
+    try(IntVector.FixedBuilder result = driverContext.blockFactory().newIntVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendInt(p, RoundToInt.process(fieldVector.getInt(p), this.p0));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToInt1Evaluator[" + "field=" + field + ", p0=" + p0 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final int p0;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, int p0) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+    }
+
+    @Override
+    public RoundToInt1Evaluator get(DriverContext context) {
+      return new RoundToInt1Evaluator(source, field.get(context), p0, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToInt1Evaluator[" + "field=" + field + ", p0=" + p0 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,134 @@
+// 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.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 RoundToInt}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToInt2Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final int p0;
+
+  private final int p1;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToInt2Evaluator(Source source, EvalOperator.ExpressionEvaluator field, int p0, int p1,
+      DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (IntBlock fieldBlock = (IntBlock) field.eval(page)) {
+      IntVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public IntBlock eval(int positionCount, IntBlock fieldBlock) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendInt(RoundToInt.process(fieldBlock.getInt(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1));
+      }
+      return result.build();
+    }
+  }
+
+  public IntVector eval(int positionCount, IntVector fieldVector) {
+    try(IntVector.FixedBuilder result = driverContext.blockFactory().newIntVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendInt(p, RoundToInt.process(fieldVector.getInt(p), this.p0, this.p1));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToInt2Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final int p0;
+
+    private final int p1;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, int p0, int p1) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+    }
+
+    @Override
+    public RoundToInt2Evaluator get(DriverContext context) {
+      return new RoundToInt2Evaluator(source, field.get(context), p0, p1, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToInt2Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,141 @@
+// 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.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 RoundToInt}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToInt3Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final int p0;
+
+  private final int p1;
+
+  private final int p2;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToInt3Evaluator(Source source, EvalOperator.ExpressionEvaluator field, int p0, int p1,
+      int p2, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.p2 = p2;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (IntBlock fieldBlock = (IntBlock) field.eval(page)) {
+      IntVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public IntBlock eval(int positionCount, IntBlock fieldBlock) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendInt(RoundToInt.process(fieldBlock.getInt(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1, this.p2));
+      }
+      return result.build();
+    }
+  }
+
+  public IntVector eval(int positionCount, IntVector fieldVector) {
+    try(IntVector.FixedBuilder result = driverContext.blockFactory().newIntVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendInt(p, RoundToInt.process(fieldVector.getInt(p), this.p0, this.p1, this.p2));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToInt3Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final int p0;
+
+    private final int p1;
+
+    private final int p2;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, int p0, int p1,
+        int p2) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+      this.p2 = p2;
+    }
+
+    @Override
+    public RoundToInt3Evaluator get(DriverContext context) {
+      return new RoundToInt3Evaluator(source, field.get(context), p0, p1, p2, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToInt3Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,147 @@
+// 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.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 RoundToInt}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToInt4Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final int p0;
+
+  private final int p1;
+
+  private final int p2;
+
+  private final int p3;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToInt4Evaluator(Source source, EvalOperator.ExpressionEvaluator field, int p0, int p1,
+      int p2, int p3, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.p2 = p2;
+    this.p3 = p3;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (IntBlock fieldBlock = (IntBlock) field.eval(page)) {
+      IntVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public IntBlock eval(int positionCount, IntBlock fieldBlock) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendInt(RoundToInt.process(fieldBlock.getInt(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1, this.p2, this.p3));
+      }
+      return result.build();
+    }
+  }
+
+  public IntVector eval(int positionCount, IntVector fieldVector) {
+    try(IntVector.FixedBuilder result = driverContext.blockFactory().newIntVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendInt(p, RoundToInt.process(fieldVector.getInt(p), this.p0, this.p1, this.p2, this.p3));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToInt4Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + ", p3=" + p3 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final int p0;
+
+    private final int p1;
+
+    private final int p2;
+
+    private final int p3;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, int p0, int p1,
+        int p2, int p3) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+      this.p2 = p2;
+      this.p3 = p3;
+    }
+
+    @Override
+    public RoundToInt4Evaluator get(DriverContext context) {
+      return new RoundToInt4Evaluator(source, field.get(context), p0, p1, p2, p3, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToInt4Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + ", p3=" + p3 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,128 @@
+// 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.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 RoundToInt}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToIntBinarySearchEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final int[] points;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToIntBinarySearchEvaluator(Source source, EvalOperator.ExpressionEvaluator field,
+      int[] points, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.points = points;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (IntBlock fieldBlock = (IntBlock) field.eval(page)) {
+      IntVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public IntBlock eval(int positionCount, IntBlock fieldBlock) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendInt(RoundToInt.process(fieldBlock.getInt(fieldBlock.getFirstValueIndex(p)), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  public IntVector eval(int positionCount, IntVector fieldVector) {
+    try(IntVector.FixedBuilder result = driverContext.blockFactory().newIntVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendInt(p, RoundToInt.process(fieldVector.getInt(p), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToIntBinarySearchEvaluator[" + "field=" + field + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final int[] points;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, int[] points) {
+      this.source = source;
+      this.field = field;
+      this.points = points;
+    }
+
+    @Override
+    public RoundToIntBinarySearchEvaluator get(DriverContext context) {
+      return new RoundToIntBinarySearchEvaluator(source, field.get(context), points, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToIntBinarySearchEvaluator[" + "field=" + field + "]";
+    }
+  }
+}

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

@@ -0,0 +1,128 @@
+// 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.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 RoundToInt}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToIntLinearSearchEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final int[] points;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToIntLinearSearchEvaluator(Source source, EvalOperator.ExpressionEvaluator field,
+      int[] points, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.points = points;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (IntBlock fieldBlock = (IntBlock) field.eval(page)) {
+      IntVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public IntBlock eval(int positionCount, IntBlock fieldBlock) {
+    try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendInt(RoundToInt.processLinear(fieldBlock.getInt(fieldBlock.getFirstValueIndex(p)), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  public IntVector eval(int positionCount, IntVector fieldVector) {
+    try(IntVector.FixedBuilder result = driverContext.blockFactory().newIntVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendInt(p, RoundToInt.processLinear(fieldVector.getInt(p), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToIntLinearSearchEvaluator[" + "field=" + field + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final int[] points;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, int[] points) {
+      this.source = source;
+      this.field = field;
+      this.points = points;
+    }
+
+    @Override
+    public RoundToIntLinearSearchEvaluator get(DriverContext context) {
+      return new RoundToIntLinearSearchEvaluator(source, field.get(context), points, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToIntLinearSearchEvaluator[" + "field=" + field + "]";
+    }
+  }
+}

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

@@ -0,0 +1,128 @@
+// 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.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 RoundToLong}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToLong1Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final long p0;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToLong1Evaluator(Source source, EvalOperator.ExpressionEvaluator field, long p0,
+      DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock fieldBlock = (LongBlock) field.eval(page)) {
+      LongVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongBlock fieldBlock) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendLong(RoundToLong.process(fieldBlock.getLong(fieldBlock.getFirstValueIndex(p)), this.p0));
+      }
+      return result.build();
+    }
+  }
+
+  public LongVector eval(int positionCount, LongVector fieldVector) {
+    try(LongVector.FixedBuilder result = driverContext.blockFactory().newLongVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendLong(p, RoundToLong.process(fieldVector.getLong(p), this.p0));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToLong1Evaluator[" + "field=" + field + ", p0=" + p0 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final long p0;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, long p0) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+    }
+
+    @Override
+    public RoundToLong1Evaluator get(DriverContext context) {
+      return new RoundToLong1Evaluator(source, field.get(context), p0, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToLong1Evaluator[" + "field=" + field + ", p0=" + p0 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,135 @@
+// 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.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 RoundToLong}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToLong2Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final long p0;
+
+  private final long p1;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToLong2Evaluator(Source source, EvalOperator.ExpressionEvaluator field, long p0,
+      long p1, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock fieldBlock = (LongBlock) field.eval(page)) {
+      LongVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongBlock fieldBlock) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendLong(RoundToLong.process(fieldBlock.getLong(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1));
+      }
+      return result.build();
+    }
+  }
+
+  public LongVector eval(int positionCount, LongVector fieldVector) {
+    try(LongVector.FixedBuilder result = driverContext.blockFactory().newLongVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendLong(p, RoundToLong.process(fieldVector.getLong(p), this.p0, this.p1));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToLong2Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final long p0;
+
+    private final long p1;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, long p0,
+        long p1) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+    }
+
+    @Override
+    public RoundToLong2Evaluator get(DriverContext context) {
+      return new RoundToLong2Evaluator(source, field.get(context), p0, p1, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToLong2Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,141 @@
+// 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.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 RoundToLong}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToLong3Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final long p0;
+
+  private final long p1;
+
+  private final long p2;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToLong3Evaluator(Source source, EvalOperator.ExpressionEvaluator field, long p0,
+      long p1, long p2, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.p2 = p2;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock fieldBlock = (LongBlock) field.eval(page)) {
+      LongVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongBlock fieldBlock) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendLong(RoundToLong.process(fieldBlock.getLong(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1, this.p2));
+      }
+      return result.build();
+    }
+  }
+
+  public LongVector eval(int positionCount, LongVector fieldVector) {
+    try(LongVector.FixedBuilder result = driverContext.blockFactory().newLongVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendLong(p, RoundToLong.process(fieldVector.getLong(p), this.p0, this.p1, this.p2));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToLong3Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final long p0;
+
+    private final long p1;
+
+    private final long p2;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, long p0, long p1,
+        long p2) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+      this.p2 = p2;
+    }
+
+    @Override
+    public RoundToLong3Evaluator get(DriverContext context) {
+      return new RoundToLong3Evaluator(source, field.get(context), p0, p1, p2, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToLong3Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,147 @@
+// 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.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 RoundToLong}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToLong4Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final long p0;
+
+  private final long p1;
+
+  private final long p2;
+
+  private final long p3;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToLong4Evaluator(Source source, EvalOperator.ExpressionEvaluator field, long p0,
+      long p1, long p2, long p3, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.p0 = p0;
+    this.p1 = p1;
+    this.p2 = p2;
+    this.p3 = p3;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock fieldBlock = (LongBlock) field.eval(page)) {
+      LongVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongBlock fieldBlock) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendLong(RoundToLong.process(fieldBlock.getLong(fieldBlock.getFirstValueIndex(p)), this.p0, this.p1, this.p2, this.p3));
+      }
+      return result.build();
+    }
+  }
+
+  public LongVector eval(int positionCount, LongVector fieldVector) {
+    try(LongVector.FixedBuilder result = driverContext.blockFactory().newLongVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendLong(p, RoundToLong.process(fieldVector.getLong(p), this.p0, this.p1, this.p2, this.p3));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToLong4Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + ", p3=" + p3 + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final long p0;
+
+    private final long p1;
+
+    private final long p2;
+
+    private final long p3;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, long p0, long p1,
+        long p2, long p3) {
+      this.source = source;
+      this.field = field;
+      this.p0 = p0;
+      this.p1 = p1;
+      this.p2 = p2;
+      this.p3 = p3;
+    }
+
+    @Override
+    public RoundToLong4Evaluator get(DriverContext context) {
+      return new RoundToLong4Evaluator(source, field.get(context), p0, p1, p2, p3, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToLong4Evaluator[" + "field=" + field + ", p0=" + p0 + ", p1=" + p1 + ", p2=" + p2 + ", p3=" + p3 + "]";
+    }
+  }
+}

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

@@ -0,0 +1,128 @@
+// 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.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 RoundToLong}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToLongBinarySearchEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final long[] points;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToLongBinarySearchEvaluator(Source source, EvalOperator.ExpressionEvaluator field,
+      long[] points, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.points = points;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock fieldBlock = (LongBlock) field.eval(page)) {
+      LongVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongBlock fieldBlock) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendLong(RoundToLong.process(fieldBlock.getLong(fieldBlock.getFirstValueIndex(p)), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  public LongVector eval(int positionCount, LongVector fieldVector) {
+    try(LongVector.FixedBuilder result = driverContext.blockFactory().newLongVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendLong(p, RoundToLong.process(fieldVector.getLong(p), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToLongBinarySearchEvaluator[" + "field=" + field + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final long[] points;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, long[] points) {
+      this.source = source;
+      this.field = field;
+      this.points = points;
+    }
+
+    @Override
+    public RoundToLongBinarySearchEvaluator get(DriverContext context) {
+      return new RoundToLongBinarySearchEvaluator(source, field.get(context), points, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToLongBinarySearchEvaluator[" + "field=" + field + "]";
+    }
+  }
+}

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

@@ -0,0 +1,128 @@
+// 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.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 RoundToLong}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class RoundToLongLinearSearchEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Source source;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final long[] points;
+
+  private final DriverContext driverContext;
+
+  private Warnings warnings;
+
+  public RoundToLongLinearSearchEvaluator(Source source, EvalOperator.ExpressionEvaluator field,
+      long[] points, DriverContext driverContext) {
+    this.source = source;
+    this.field = field;
+    this.points = points;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock fieldBlock = (LongBlock) field.eval(page)) {
+      LongVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public LongBlock eval(int positionCount, LongBlock fieldBlock) {
+    try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendLong(RoundToLong.processLinear(fieldBlock.getLong(fieldBlock.getFirstValueIndex(p)), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  public LongVector eval(int positionCount, LongVector fieldVector) {
+    try(LongVector.FixedBuilder result = driverContext.blockFactory().newLongVectorFixedBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendLong(p, RoundToLong.processLinear(fieldVector.getLong(p), this.points));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "RoundToLongLinearSearchEvaluator[" + "field=" + field + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  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 field;
+
+    private final long[] points;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field, long[] points) {
+      this.source = source;
+      this.field = field;
+      this.points = points;
+    }
+
+    @Override
+    public RoundToLongLinearSearchEvaluator get(DriverContext context) {
+      return new RoundToLongLinearSearchEvaluator(source, field.get(context), points, context);
+    }
+
+    @Override
+    public String toString() {
+      return "RoundToLongLinearSearchEvaluator[" + "field=" + field + "]";
+    }
+  }
+}

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

@@ -870,9 +870,9 @@ public class EsqlCapabilities {
         KEEP_REGEX_EXTRACT_ATTRIBUTES,
 
         /**
-         * Full text functions in STATS
+         * The {@code ROUND_TO} function.
          */
-        FULL_TEXT_FUNCTIONS_IN_STATS_WHERE;
+        ROUND_TO;
 
         private final boolean enabled;
 

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

@@ -88,6 +88,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.math.Log10;
 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.Signum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Sinh;
@@ -311,6 +312,7 @@ public class EsqlFunctionRegistry {
                 def(Pi.class, Pi::new, "pi"),
                 def(Pow.class, Pow::new, "pow"),
                 def(Round.class, Round::new, "round"),
+                def(RoundTo.class, RoundTo::new, "round_to"),
                 def(Signum.class, Signum::new, "signum"),
                 def(Sin.class, Sin::new, "sin"),
                 def(Sinh.class, Sinh::new, "sinh"),

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

@@ -28,6 +28,7 @@ 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;
 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.Tau;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.BitLength;
@@ -94,6 +95,7 @@ public class ScalarFunctionWritables {
         entries.add(Replace.ENTRY);
         entries.add(Reverse.ENTRY);
         entries.add(Round.ENTRY);
+        entries.add(RoundTo.ENTRY);
         entries.add(Sha1.ENTRY);
         entries.add(Sha256.ENTRY);
         entries.add(Split.ENTRY);

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

@@ -0,0 +1,191 @@
+/*
+ * 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.collect.Iterators;
+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.operator.EvalOperator.ExpressionEvaluator;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.Foldables;
+import org.elasticsearch.xpack.esql.core.expression.Nullability;
+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 java.util.Map;
+
+import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isFoldable;
+import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME;
+import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS;
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER;
+import static org.elasticsearch.xpack.esql.core.type.DataType.LONG;
+import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.commonType;
+
+/**
+ * Round down to one of a list of values.
+ */
+public class RoundTo extends EsqlScalarFunction {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "RoundTo", RoundTo::new);
+
+    private final Expression field;
+    private final List<Expression> points;
+
+    private DataType resultType;
+
+    @FunctionInfo(returnType = { "double", "integer", "long", "date", "date_nanos" }, description = """
+        Rounds down to one of a list of fixed points.""", examples = @Example(file = "math", tag = "round_to"))
+    public RoundTo(
+        Source source,
+        @Param(
+            name = "field",
+            type = { "double", "integer", "long", "date", "date_nanos" },
+            description = "The numeric value to round. If `null`, the function returns `null`."
+        ) Expression field,
+        @Param(
+            name = "points",
+            type = { "double", "integer", "long", "date", "date_nanos" },
+            description = "Remaining rounding points. Must be constants."
+        ) List<Expression> points
+    ) {
+        super(source, Iterators.toList(Iterators.concat(Iterators.single(field), points.iterator())));
+        this.field = field;
+        this.points = points;
+    }
+
+    private RoundTo(StreamInput in) throws IOException {
+        this(
+            Source.readFrom((PlanStreamInput) in),
+            in.readNamedWriteable(Expression.class),
+            in.readNamedWriteableCollectionAsList(Expression.class)
+        );
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        source().writeTo(out);
+        out.writeNamedWriteable(field);
+        out.writeNamedWriteableCollection(points);
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        int index = 1;
+        for (Expression f : points) {
+            if (f.dataType() == DataType.NULL) {
+                continue;
+            }
+            TypeResolution resolution = isFoldable(f, sourceText(), TypeResolutions.ParamOrdinal.fromIndex(index));
+            if (resolution.unresolved()) {
+                return resolution;
+            }
+        }
+
+        DataType dataType = dataType();
+        if (dataType == null || SIGNATURES.containsKey(dataType) == false) {
+            return new TypeResolution(format(null, "all arguments must be numeric, date, or data_nanos"));
+        }
+
+        return TypeResolution.TYPE_RESOLVED;
+    }
+
+    @Override
+    public DataType dataType() {
+        if (resultType != null) {
+            return resultType;
+        }
+        resultType = field.dataType();
+        for (Expression f : points) {
+            if (resultType == DataType.UNSIGNED_LONG || resultType == null || f.dataType() == UNSIGNED_LONG) {
+                return null;
+            }
+            resultType = commonType(resultType, f.dataType());
+        }
+        return resultType;
+    }
+
+    @Override
+    public boolean foldable() {
+        for (Expression c : children()) {
+            if (c.foldable() == false) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public Nullability nullable() {
+        return Nullability.TRUE;
+    }
+
+    @Override
+    public final Expression replaceChildren(List<Expression> newChildren) {
+        return new RoundTo(source(), newChildren.get(0), newChildren.subList(1, newChildren.size()));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, RoundTo::new, field(), points());
+    }
+
+    public Expression field() {
+        return field;
+    }
+
+    public List<Expression> points() {
+        return points;
+    }
+
+    @Override
+    public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+        DataType dataType = dataType();
+        Build build = SIGNATURES.get(dataType);
+        if (build == null) {
+            throw new IllegalStateException("unsupported type");
+        }
+
+        ExpressionEvaluator.Factory field = toEvaluator.apply(field());
+        field = Cast.cast(source(), field().dataType(), dataType, field);
+        List<Object> points = Iterators.toList(Iterators.map(points().iterator(), p -> Foldables.valueOf(toEvaluator.foldCtx(), p)));
+        return build.build(source(), field, points);
+    }
+
+    interface Build {
+        ExpressionEvaluator.Factory build(Source source, ExpressionEvaluator.Factory field, List<Object> points);
+    }
+
+    private static final Map<DataType, Build> SIGNATURES = Map.ofEntries(
+        Map.entry(DATETIME, RoundToLong.BUILD),
+        Map.entry(DATE_NANOS, RoundToLong.BUILD),
+        Map.entry(INTEGER, RoundToInt.BUILD),
+        Map.entry(LONG, RoundToLong.BUILD),
+        Map.entry(DOUBLE, RoundToDouble.BUILD)
+    );
+}

+ 105 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/X-RoundTo.java.st

@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+// begin generated imports
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+
+import java.util.Arrays;
+// end generated imports
+
+/**
+ * Implementations of {@link RoundTo} for specific types.
+ * <p>
+ *   We have specializations for when there are very few rounding points because
+ *   those are very fast and quite common.
+ * </p>
+ * This class is generated. Edit {@code X-RoundTo.java.st} instead.
+ */
+class RoundTo$Type$ {
+    static final RoundTo.Build BUILD = (source, field, points) -> {
+        $type$[] f = points.stream().mapTo$Type$(p -> ((Number) p).$type$Value()).toArray();
+        Arrays.sort(f);
+        return switch (f.length) {
+            // TODO should be a consistent way to do the 0 version - is CASE(MV_COUNT(f) == 1, f[0])
+            case 1 -> new RoundTo$Type$1Evaluator.Factory(source, field, f[0]);
+            /*
+             * These hand-unrolled implementations are even faster than the linear scan implementations.
+             */
+            case 2 -> new RoundTo$Type$2Evaluator.Factory(source, field, f[0], f[1]);
+            case 3 -> new RoundTo$Type$3Evaluator.Factory(source, field, f[0], f[1], f[2]);
+            case 4 -> new RoundTo$Type$4Evaluator.Factory(source, field, f[0], f[1], f[2], f[3]);
+            /*
+             * Break point of 10 experimentally derived on Nik's laptop (13th Gen Intel(R) Core(TM) i7-1370P)
+             * on 2025-05-22.
+             */
+            case 5, 6, 7, 8, 9, 10 -> new RoundTo$Type$LinearSearchEvaluator.Factory(source, field, f);
+            default -> new RoundTo$Type$BinarySearchEvaluator.Factory(source, field, f);
+        };
+    };
+
+    /**
+     * Search the points array for the match linearly. This is faster for smaller arrays even
+     * when finding a position late in the array. Presumably because this is super-SIMD-able.
+     */
+    @Evaluator(extraName = "LinearSearch")
+    static $type$ processLinear($type$ field, @Fixed(includeInToString = false) $type$[] points) {
+        // points is always longer than 3 or we use one of the specialized methods below
+        for (int i = 1; i < points.length; i++) {
+            if (field < points[i]) {
+                return points[i - 1];
+            }
+        }
+        return points[points.length - 1];
+    }
+
+    @Evaluator(extraName = "BinarySearch")
+    static $type$ process($type$ field, @Fixed(includeInToString = false) $type$[] points) {
+        int idx = Arrays.binarySearch(points, field);
+        return points[idx >= 0 ? idx : Math.max(0, -idx - 2)];
+    }
+
+    @Evaluator(extraName = "1")
+    static $type$ process($type$ field, @Fixed $type$ p0) {
+        return p0;
+    }
+
+    @Evaluator(extraName = "2")
+    static $type$ process($type$ field, @Fixed $type$ p0, @Fixed $type$ p1) {
+        if (field < p1) {
+            return p0;
+        }
+        return p1;
+    }
+
+    @Evaluator(extraName = "3")
+    static $type$ process($type$ field, @Fixed $type$ p0, @Fixed $type$ p1, @Fixed $type$ p2) {
+        if (field < p1) {
+            return p0;
+        }
+        if (field < p2) {
+            return p1;
+        }
+        return p2;
+    }
+
+    @Evaluator(extraName = "4")
+    static $type$ process($type$ field, @Fixed $type$ p0, @Fixed $type$ p1, @Fixed $type$ p2, @Fixed $type$ p3) {
+        if (field < p1) {
+            return p0;
+        }
+        if (field < p2) {
+            return p1;
+        }
+        if (field < p3) {
+            return p2;
+        }
+        return p3;
+    }
+}

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

@@ -0,0 +1,45 @@
+/*
+ * 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 RoundToErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
+    @Override
+    protected List<TestCaseSupplier> cases() {
+        return paramsToSuppliers(RoundToTests.parameters()).stream()
+            /*
+             * We pick the common type across all parameters, but we don't
+             * test mixes with more than three parameters. We test cases
+             * with more than three parameters - just not mixes with more
+             * than three.
+             */
+            .filter(s -> s.types().size() < 3)
+            .toList();
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new RoundTo(source, args.get(0), args.subList(1, args.size()));
+    }
+
+    @Override
+    protected Matcher<String> expectedTypeErrorMatcher(List<Set<DataType>> validPerPosition, List<DataType> signature) {
+        return equalTo("all arguments must be numeric, date, or data_nanos");
+    }
+}

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

@@ -0,0 +1,59 @@
+/*
+ * 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.expression.ReferenceAttribute;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.EsqlTestUtils.randomLiteral;
+
+public class RoundToSerializationTests extends AbstractExpressionSerializationTests<RoundTo> {
+    @Override
+    protected RoundTo createTestInstance() {
+        Source source = randomSource();
+        DataType type = randomFrom(DataType.INTEGER, DataType.LONG, DataType.DOUBLE, DataType.DATETIME, DataType.DATE_NANOS);
+        Expression field = randomField(type);
+        List<Expression> points = randomPoints(type);
+        return new RoundTo(source, field, points);
+    }
+
+    private Expression randomField(DataType type) {
+        return new ReferenceAttribute(Source.EMPTY, randomAlphanumericOfLength(4), randomLiteral(type).dataType());
+    }
+
+    private List<Expression> randomPoints(DataType type) {
+        int length = between(1, 100);
+        List<Expression> points = new ArrayList<>(length);
+        while (points.size() < length) {
+            points.add(randomLiteral(type));
+        }
+        ;
+        return points;
+    }
+
+    @Override
+    protected RoundTo mutateInstance(RoundTo instance) throws IOException {
+        Source source = instance.source();
+        Expression field = instance.field();
+        List<Expression> points = instance.points();
+        DataType type = field.dataType();
+        if (randomBoolean()) {
+            field = randomValueOtherThan(field, () -> randomField(type));
+        } else {
+            points = randomValueOtherThan(points, () -> randomPoints(type));
+        }
+        return new RoundTo(source, field, points);
+    }
+}

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

@@ -0,0 +1,305 @@
+/*
+ * 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.test.ESTestCase;
+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 org.hamcrest.Matcher;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
+
+public class RoundToTests extends AbstractScalarFunctionTestCase {
+    public RoundToTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+
+        for (int p = 1; p < 20; p++) {
+            int points = p;
+            suppliers.add(
+                doubles(
+                    "<double, " + points + " doubles>",
+                    DataType.DOUBLE,
+                    () -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true),
+                    IntStream.range(0, points).mapToObj(i -> DataType.DOUBLE).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true)).toList()
+                )
+            );
+            suppliers.add(
+                doubles(
+                    "<double, " + points + " longs>",
+                    DataType.DOUBLE,
+                    () -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true),
+                    IntStream.range(0, points).mapToObj(i -> DataType.LONG).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> (double) randomLong()).toList()
+                )
+            );
+            suppliers.add(
+                doubles(
+                    "<double, " + points + " ints>",
+                    DataType.DOUBLE,
+                    () -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true),
+                    IntStream.range(0, points).mapToObj(i -> DataType.INTEGER).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> (double) randomInt()).toList()
+                )
+            );
+            suppliers.add(
+                doubles(
+                    "<long, " + points + " doubles>",
+                    DataType.LONG,
+                    ESTestCase::randomLong,
+                    IntStream.range(0, points).mapToObj(i -> DataType.DOUBLE).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true)).toList()
+                )
+            );
+            suppliers.add(
+                doubles(
+                    "<int, " + points + " doubles>",
+                    DataType.INTEGER,
+                    ESTestCase::randomInt,
+                    IntStream.range(0, points).mapToObj(i -> DataType.DOUBLE).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true)).toList()
+                )
+            );
+            suppliers.add(
+                longs(
+                    "<long, " + points + " longs>",
+                    DataType.LONG,
+                    ESTestCase::randomLong,
+                    IntStream.range(0, points).mapToObj(i -> DataType.LONG).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> randomLong()).toList()
+                )
+            );
+            suppliers.add(
+                longs(
+                    "<int, " + points + " longs>",
+                    DataType.INTEGER,
+                    ESTestCase::randomInt,
+                    IntStream.range(0, points).mapToObj(i -> DataType.LONG).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> randomLong()).toList()
+                )
+            );
+            suppliers.add(
+                longs(
+                    "<date, " + points + " dates>",
+                    DataType.DATETIME,
+                    ESTestCase::randomMillisUpToYear9999,
+                    IntStream.range(0, points).mapToObj(i -> DataType.DATETIME).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> randomMillisUpToYear9999()).toList()
+                )
+            );
+            suppliers.add(
+                longs(
+                    "<date_nanos, " + points + " date_nanos>",
+                    DataType.DATE_NANOS,
+                    () -> randomLongBetween(0, Long.MAX_VALUE),
+                    IntStream.range(0, points).mapToObj(i -> DataType.DATE_NANOS).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> randomLongBetween(0, Long.MAX_VALUE)).toList()
+                )
+            );
+            suppliers.add(
+                longs(
+                    "<long, " + points + " ints>",
+                    DataType.LONG,
+                    ESTestCase::randomLong,
+                    IntStream.range(0, points).mapToObj(i -> DataType.INTEGER).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> (long) randomInt()).toList()
+                )
+            );
+            suppliers.add(
+                ints(
+                    "<int, " + points + " ints>",
+                    DataType.INTEGER,
+                    ESTestCase::randomInt,
+                    IntStream.range(0, points).mapToObj(i -> DataType.INTEGER).toList(),
+                    () -> IntStream.range(0, points).mapToObj(i -> randomInt()).toList()
+                )
+            );
+        }
+        suppliers.add(supplier(1.0, 0.0, 0.0, 100.0));
+        suppliers.add(supplier(1.0, 1.0, 0.0, 1.0, 100.0));
+        suppliers.add(supplier(0.5, 0.0, 0.0, 1.0, 100.0));
+        suppliers.add(supplier(1.5, 1.0, 0.0, 1.0, 100.0));
+        suppliers.add(supplier(200, 100, 0.0, 1.0, 100.0));
+
+        return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(
+            (int nullPosition, DataType nullValueDataType, TestCaseSupplier.TestCase original) -> {
+                if (nullValueDataType != DataType.NULL) {
+                    return original.expectedType();
+                }
+                List<DataType> types = original.getData().stream().map(TestCaseSupplier.TypedData::type).collect(Collectors.toList());
+                types.set(nullPosition, DataType.NULL);
+                return expectedType(types);
+            },
+            (int nullPosition, TestCaseSupplier.TypedData nullData, Matcher<String> original) -> {
+                if (nullPosition == 0) {
+                    return original;
+                }
+                return equalTo("LiteralsEvaluator[lit=null]");
+            },
+            randomizeBytesRefsOffset(suppliers)
+        );
+    }
+
+    private static TestCaseSupplier supplier(double f, double expected, double... points) {
+        StringBuilder name = new StringBuilder("round(");
+        name.append(f);
+        for (double p : points) {
+            name.append(", ").append(p);
+        }
+        name.append(") -> ").append(expected);
+        return supplier(
+            name.toString(),
+            DataType.DOUBLE,
+            () -> f,
+            IntStream.range(0, points.length).mapToObj(i -> DataType.DOUBLE).toList(),
+            () -> Arrays.stream(points).boxed().toList(),
+            (value, de) -> expected
+        );
+    }
+
+    private static TestCaseSupplier doubles(
+        String name,
+        DataType fieldType,
+        Supplier<Number> fieldSupplier,
+        List<DataType> pointsTypes,
+        Supplier<List<Double>> pointsSupplier
+    ) {
+        return supplier(name, fieldType, fieldSupplier, pointsTypes, pointsSupplier, (f, p) -> {
+            double max = p.stream().mapToDouble(d -> d).min().getAsDouble();
+            for (double d : p) {
+                if (d > max && f.doubleValue() > d) {
+                    max = d;
+                }
+            }
+            return max;
+        });
+    }
+
+    private static TestCaseSupplier longs(
+        String name,
+        DataType fieldType,
+        Supplier<Number> fieldSupplier,
+        List<DataType> pointsTypes,
+        Supplier<List<Long>> pointsSupplier
+    ) {
+        return supplier(name, fieldType, fieldSupplier, pointsTypes, pointsSupplier, (f, p) -> {
+            long max = p.stream().mapToLong(l -> l).min().getAsLong();
+            for (long l : p) {
+                if (l > max && f.doubleValue() > l) {
+                    max = l;
+                }
+            }
+            return max;
+        });
+    }
+
+    private static TestCaseSupplier ints(
+        String name,
+        DataType fieldType,
+        Supplier<Number> fieldSupplier,
+        List<DataType> pointsTypes,
+        Supplier<List<Integer>> pointsSupplier
+    ) {
+        return supplier(name, fieldType, fieldSupplier, pointsTypes, pointsSupplier, (f, p) -> {
+            int max = p.stream().mapToInt(i -> i).min().getAsInt();
+            for (int l : p) {
+                if (l > max && f.doubleValue() > l) {
+                    max = l;
+                }
+            }
+            return max;
+        });
+    }
+
+    private static <P> TestCaseSupplier supplier(
+        String name,
+        DataType fieldType,
+        Supplier<Number> fieldSupplier,
+        List<DataType> pointsTypes,
+        Supplier<List<P>> pointsSupplier,
+        BiFunction<Number, List<P>, Number> expected
+    ) {
+        List<DataType> types = new ArrayList<>(pointsTypes.size() + 1);
+        types.add(fieldType);
+        types.addAll(pointsTypes);
+        return new TestCaseSupplier(name, types, () -> {
+            Number field = fieldSupplier.get();
+            List<P> points = pointsSupplier.get();
+
+            List<TestCaseSupplier.TypedData> params = new ArrayList<>(1 + points.size());
+            params.add(new TestCaseSupplier.TypedData(field, fieldType, "field"));
+            for (int i = 0; i < points.size(); i++) {
+                params.add(new TestCaseSupplier.TypedData(points.get(i), pointsTypes.get(i), "point" + i).forceLiteral());
+            }
+
+            DataType expectedType = expectedType(types);
+            String type = switch (expectedType) {
+                case DOUBLE -> "Double";
+                case INTEGER -> "Int";
+                case DATETIME, DATE_NANOS, LONG -> "Long";
+                default -> throw new UnsupportedOperationException();
+            };
+            Matcher<String> expectedEvaluatorName = startsWith("RoundTo" + type + specialization(points.size()) + "Evaluator");
+            return new TestCaseSupplier.TestCase(params, expectedEvaluatorName, expectedType, equalTo(expected.apply(field, points)));
+        });
+    }
+
+    private static String specialization(int pointsSize) {
+        if (pointsSize < 5) {
+            return Integer.toString(pointsSize);
+        }
+        if (pointsSize < 11) {
+            return "LinearSearch";
+        }
+        return "BinarySearch";
+    }
+
+    private static DataType expectedType(List<DataType> types) {
+        if (types.stream().anyMatch(t -> t == DataType.DOUBLE)) {
+            return DataType.DOUBLE;
+        }
+        if (types.stream().anyMatch(t -> t == DataType.LONG)) {
+            return DataType.LONG;
+        }
+        if (types.stream().anyMatch(t -> t == DataType.INTEGER)) {
+            return DataType.INTEGER;
+        }
+        if (types.stream().anyMatch(t -> t == DataType.DATETIME)) {
+            return DataType.DATETIME;
+        }
+        if (types.stream().anyMatch(t -> t == DataType.DATE_NANOS)) {
+            return DataType.DATE_NANOS;
+        }
+        throw new UnsupportedOperationException("can't build expected types for " + types);
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new RoundTo(source, args.get(0), args.subList(1, args.size()));
+    }
+}

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

@@ -100,8 +100,8 @@ setup:
   - match: {esql.functions.cos: $functions_cos}
   - gt: {esql.functions.to_long: $functions_to_long}
   - match: {esql.functions.coalesce: $functions_coalesce}
-  # Testing for the entire function set isn't feasbile, so we just check that we return the correct count as an approximation.
-  - length: {esql.functions: 134} # check the "sister" test below for a likely update to the same esql.functions length check
+  # 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
 
 ---
 "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: 131} # check the "sister" test above for a likely update to the same esql.functions length check
+  - length: {esql.functions: 132} # check the "sister" test above for a likely update to the same esql.functions length check