Browse Source

ESQL: LEAST and GREATEST functions (#98630)

Adds `LEAST` and `GREATEST` functions to find the min or max of the
values in many columns.
Nik Everett 2 years ago
parent
commit
65ea90d3fd
31 changed files with 1954 additions and 120 deletions
  1. 5 0
      docs/changelog/98630.yaml
  2. 4 0
      docs/reference/esql/esql-functions.asciidoc
  3. 18 0
      docs/reference/esql/functions/greatest.asciidoc
  4. 18 0
      docs/reference/esql/functions/least.asciidoc
  5. 4 0
      x-pack/plugin/esql/build.gradle
  6. 76 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec
  7. 7 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/null.csv-spec
  8. 4 2
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec
  9. 84 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestBooleanEvaluator.java
  10. 93 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestBytesRefEvaluator.java
  11. 84 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestDoubleEvaluator.java
  12. 84 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestIntEvaluator.java
  13. 84 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestLongEvaluator.java
  14. 84 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastBooleanEvaluator.java
  15. 93 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastBytesRefEvaluator.java
  16. 84 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastDoubleEvaluator.java
  17. 84 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastIntEvaluator.java
  18. 84 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastLongEvaluator.java
  19. 4 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  20. 8 7
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Case.java
  21. 195 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Greatest.java
  22. 193 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Least.java
  23. 12 5
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/nulls/Coalesce.java
  24. 23 24
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java
  25. 19 1
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java
  26. 2 1
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
  27. 364 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/VaragsTestCaseBuilder.java
  28. 4 4
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/CaseTests.java
  29. 63 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestTests.java
  30. 62 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastTests.java
  31. 11 76
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/nulls/CoalesceTests.java

+ 5 - 0
docs/changelog/98630.yaml

@@ -0,0 +1,5 @@
+pr: 98630
+summary: "ESQL: LEAST and GREATEST functions"
+area: ES|QL
+type: feature
+issues: []

+ 4 - 0
docs/reference/esql/esql-functions.asciidoc

@@ -26,9 +26,11 @@ these functions:
 * <<esql-date_trunc>>
 * <<esql-e>>
 * <<esql-floor>>
+* <<esql-greatest>>
 * <<esql-is_finite>>
 * <<esql-is_infinite>>
 * <<esql-is_nan>>
+* <<esql-least>>
 * <<esql-length>>
 * <<esql-log10>>
 * <<esql-ltrim>>
@@ -84,9 +86,11 @@ include::functions/date_parse.asciidoc[]
 include::functions/date_trunc.asciidoc[]
 include::functions/e.asciidoc[]
 include::functions/floor.asciidoc[]
+include::functions/greatest.asciidoc[]
 include::functions/is_finite.asciidoc[]
 include::functions/is_infinite.asciidoc[]
 include::functions/is_nan.asciidoc[]
+include::functions/least.asciidoc[]
 include::functions/length.asciidoc[]
 include::functions/log10.asciidoc[]
 include::functions/ltrim.asciidoc[]

+ 18 - 0
docs/reference/esql/functions/greatest.asciidoc

@@ -0,0 +1,18 @@
+[[esql-greatest]]
+=== `GREATEST`
+
+Returns the maximum value from many columns. This is similar to <<esql-mv_max>>
+except it's intended to run on multiple columns at once.
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/math.csv-spec[tag=greatest]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/math.csv-spec[tag=greatest-result]
+|===
+
+NOTE: When run on `keyword` or `text` fields, this'll return the last string
+      in alphabetical order. When run on `boolean` columns this will return
+      `true` if any values are `true`.

+ 18 - 0
docs/reference/esql/functions/least.asciidoc

@@ -0,0 +1,18 @@
+[[esql-least]]
+=== `LEAST`
+
+Returns the minimum value from many columns. This is similar to <<esql-mv_min>>
+except it's intended to run on multiple columns at once.
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/math.csv-spec[tag=least]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/math.csv-spec[tag=least-result]
+|===
+
+NOTE: When run on `keyword` or `text` fields, this'll return the first string
+      in alphabetical order. When run on `boolean` columns this will return
+      `false` if any values are `false`.

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

@@ -45,6 +45,10 @@ tasks.named("compileJava").configure {
   options.compilerArgs.addAll(["-s", "${projectDir}/src/main/java/generated"])
 }
 
+tasks.named("javadoc").configure {
+  include("${projectDir}/src/main/java/generated")
+}
+
 sourceSets.main.java {
   exclude 'generated/**'
 }

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

@@ -1009,3 +1009,79 @@ row d = 1/0.0 | eval s = is_infinite(sqrt(d));
 d:double | s:boolean
 Infinity | true
 ;
+
+least
+// tag::least[]
+ROW a = 10, b = 20
+| EVAL l = LEAST(a, b);
+// end::least[]
+
+// tag::least-result[]
+a:integer | b:integer | l:integer
+       10 |        20 | 10
+// end::least-result[]
+;
+
+leastNull
+ROW l=LEAST(10, 5, null);
+
+l:integer
+null
+;
+
+leastMany
+ROW l=LEAST(10, 5, 1, -100, 0, 1234, -10000);
+
+l:integer
+-10000
+;
+
+
+greatest
+// tag::greatest[]
+ROW a = 10, b = 20
+| EVAL g = GREATEST(a, b);
+// end::greatest[]
+
+// tag::greatest-result[]
+a:integer | b:integer | g:integer
+       10 |        20 | 20
+// end::greatest-result[]
+;
+
+greatestNull
+ROW g=GREATEST(10, 5, null);
+
+g:integer
+null
+;
+
+greatestMany
+ROW g=GREATEST(10, 5, 1, -100, 0, 1234, -10000);
+
+g:integer
+1234
+;
+
+greatestMv
+ROW g=GREATEST([10, 4], 1);
+
+g:integer
+10
+;
+
+leastGreatestMany
+FROM employees
+| EVAL min_plus_max = LEAST(languages, 2) + GREATEST(languages, 2),
+       are_equal = LEAST(languages, 2) + GREATEST(languages, 2) == languages * 2
+| SORT emp_no
+| LIMIT 5
+| KEEP emp_no, min_plus_max, are_equal;
+
+emp_no:integer | min_plus_max:integer | are_equal:boolean
+         10001 |                    4 | true
+         10002 |                    7 | false
+         10003 |                    6 | false
+         10004 |                    7 | false
+         10005 |                    3 | false
+;

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

@@ -89,6 +89,13 @@ emp_no:integer | first_name:keyword
          10033 | X
 ;
 
+coalesceOnce
+ROW a=1 | EVAL a = COALESCE(a);
+
+a:integer
+        1
+;
+
 coalesceBackwards
 FROM employees
 | EVAL first_name = COALESCE("X", first_name)

+ 4 - 2
x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec

@@ -16,9 +16,9 @@ atan                     |atan(arg1)
 atan2                    |atan2(arg1, arg2)
 auto_bucket              |auto_bucket(arg1, arg2, arg3, arg4)
 avg                      |avg(arg1)
-case                     |case(arg1...)
+case                     |case(arg1, arg2...)
 cidr_match               |cidr_match(arg1, arg2...)
-coalesce                 |coalesce(arg1...)
+coalesce                 |coalesce(arg1, arg2...)
 concat                   |concat(arg1, arg2...)
 cos                      |cos(arg1)
 cosh                     |cosh(arg1)
@@ -30,9 +30,11 @@ date_parse               |date_parse(arg1, arg2)
 date_trunc               |date_trunc(arg1, arg2)
 e                        |e()
 floor                    |floor(arg1)
+greatest                 |greatest(arg1, arg2...)
 is_finite                |is_finite(arg1)
 is_infinite              |is_infinite(arg1)
 is_nan                   |is_nan(arg1)
+least                    |least(arg1, arg2...)
 length                   |length(arg1)
 log10                    |log10(arg1)
 ltrim                    |ltrim(arg1)

+ 84 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestBooleanEvaluator.java

@@ -0,0 +1,84 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BooleanVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Greatest}.
+ * This class is generated. Do not edit it.
+ */
+public final class GreatestBooleanEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public GreatestBooleanEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    BooleanBlock[] valuesBlocks = new BooleanBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (BooleanBlock) block;
+    }
+    BooleanVector[] valuesVectors = new BooleanVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public BooleanBlock eval(int positionCount, BooleanBlock[] valuesBlocks) {
+    BooleanBlock.Builder result = BooleanBlock.newBlockBuilder(positionCount);
+    boolean[] valuesValues = new boolean[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getBoolean(o);
+      }
+      result.appendBoolean(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public BooleanVector eval(int positionCount, BooleanVector[] valuesVectors) {
+    BooleanVector.Builder result = BooleanVector.newVectorBuilder(positionCount);
+    boolean[] valuesValues = new boolean[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getBoolean(p);
+      }
+      result.appendBoolean(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "GreatestBooleanEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 93 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestBytesRefEvaluator.java

@@ -0,0 +1,93 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Greatest}.
+ * This class is generated. Do not edit it.
+ */
+public final class GreatestBytesRefEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public GreatestBytesRefEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    BytesRefBlock[] valuesBlocks = new BytesRefBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (BytesRefBlock) block;
+    }
+    BytesRefVector[] valuesVectors = new BytesRefVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public BytesRefBlock eval(int positionCount, BytesRefBlock[] valuesBlocks) {
+    BytesRefBlock.Builder result = BytesRefBlock.newBlockBuilder(positionCount);
+    BytesRef[] valuesValues = new BytesRef[values.length];
+    BytesRef[] valuesScratch = new BytesRef[values.length];
+    for (int i = 0; i < values.length; i++) {
+      valuesScratch[i] = new BytesRef();
+    }
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getBytesRef(o, valuesScratch[i]);
+      }
+      result.appendBytesRef(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public BytesRefVector eval(int positionCount, BytesRefVector[] valuesVectors) {
+    BytesRefVector.Builder result = BytesRefVector.newVectorBuilder(positionCount);
+    BytesRef[] valuesValues = new BytesRef[values.length];
+    BytesRef[] valuesScratch = new BytesRef[values.length];
+    for (int i = 0; i < values.length; i++) {
+      valuesScratch[i] = new BytesRef();
+    }
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getBytesRef(p, valuesScratch[i]);
+      }
+      result.appendBytesRef(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "GreatestBytesRefEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 84 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestDoubleEvaluator.java

@@ -0,0 +1,84 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+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.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Greatest}.
+ * This class is generated. Do not edit it.
+ */
+public final class GreatestDoubleEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public GreatestDoubleEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    DoubleBlock[] valuesBlocks = new DoubleBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (DoubleBlock) block;
+    }
+    DoubleVector[] valuesVectors = new DoubleVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock[] valuesBlocks) {
+    DoubleBlock.Builder result = DoubleBlock.newBlockBuilder(positionCount);
+    double[] valuesValues = new double[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getDouble(o);
+      }
+      result.appendDouble(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector[] valuesVectors) {
+    DoubleVector.Builder result = DoubleVector.newVectorBuilder(positionCount);
+    double[] valuesValues = new double[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getDouble(p);
+      }
+      result.appendDouble(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "GreatestDoubleEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 84 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestIntEvaluator.java

@@ -0,0 +1,84 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+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.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Greatest}.
+ * This class is generated. Do not edit it.
+ */
+public final class GreatestIntEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public GreatestIntEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    IntBlock[] valuesBlocks = new IntBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (IntBlock) block;
+    }
+    IntVector[] valuesVectors = new IntVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public IntBlock eval(int positionCount, IntBlock[] valuesBlocks) {
+    IntBlock.Builder result = IntBlock.newBlockBuilder(positionCount);
+    int[] valuesValues = new int[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getInt(o);
+      }
+      result.appendInt(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public IntVector eval(int positionCount, IntVector[] valuesVectors) {
+    IntVector.Builder result = IntVector.newVectorBuilder(positionCount);
+    int[] valuesValues = new int[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getInt(p);
+      }
+      result.appendInt(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "GreatestIntEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 84 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestLongEvaluator.java

@@ -0,0 +1,84 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+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.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Greatest}.
+ * This class is generated. Do not edit it.
+ */
+public final class GreatestLongEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public GreatestLongEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    LongBlock[] valuesBlocks = new LongBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (LongBlock) block;
+    }
+    LongVector[] valuesVectors = new LongVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public LongBlock eval(int positionCount, LongBlock[] valuesBlocks) {
+    LongBlock.Builder result = LongBlock.newBlockBuilder(positionCount);
+    long[] valuesValues = new long[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getLong(o);
+      }
+      result.appendLong(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public LongVector eval(int positionCount, LongVector[] valuesVectors) {
+    LongVector.Builder result = LongVector.newVectorBuilder(positionCount);
+    long[] valuesValues = new long[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getLong(p);
+      }
+      result.appendLong(Greatest.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "GreatestLongEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 84 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastBooleanEvaluator.java

@@ -0,0 +1,84 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BooleanVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Least}.
+ * This class is generated. Do not edit it.
+ */
+public final class LeastBooleanEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public LeastBooleanEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    BooleanBlock[] valuesBlocks = new BooleanBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (BooleanBlock) block;
+    }
+    BooleanVector[] valuesVectors = new BooleanVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public BooleanBlock eval(int positionCount, BooleanBlock[] valuesBlocks) {
+    BooleanBlock.Builder result = BooleanBlock.newBlockBuilder(positionCount);
+    boolean[] valuesValues = new boolean[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getBoolean(o);
+      }
+      result.appendBoolean(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public BooleanVector eval(int positionCount, BooleanVector[] valuesVectors) {
+    BooleanVector.Builder result = BooleanVector.newVectorBuilder(positionCount);
+    boolean[] valuesValues = new boolean[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getBoolean(p);
+      }
+      result.appendBoolean(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "LeastBooleanEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 93 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastBytesRefEvaluator.java

@@ -0,0 +1,93 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Least}.
+ * This class is generated. Do not edit it.
+ */
+public final class LeastBytesRefEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public LeastBytesRefEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    BytesRefBlock[] valuesBlocks = new BytesRefBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (BytesRefBlock) block;
+    }
+    BytesRefVector[] valuesVectors = new BytesRefVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public BytesRefBlock eval(int positionCount, BytesRefBlock[] valuesBlocks) {
+    BytesRefBlock.Builder result = BytesRefBlock.newBlockBuilder(positionCount);
+    BytesRef[] valuesValues = new BytesRef[values.length];
+    BytesRef[] valuesScratch = new BytesRef[values.length];
+    for (int i = 0; i < values.length; i++) {
+      valuesScratch[i] = new BytesRef();
+    }
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getBytesRef(o, valuesScratch[i]);
+      }
+      result.appendBytesRef(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public BytesRefVector eval(int positionCount, BytesRefVector[] valuesVectors) {
+    BytesRefVector.Builder result = BytesRefVector.newVectorBuilder(positionCount);
+    BytesRef[] valuesValues = new BytesRef[values.length];
+    BytesRef[] valuesScratch = new BytesRef[values.length];
+    for (int i = 0; i < values.length; i++) {
+      valuesScratch[i] = new BytesRef();
+    }
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getBytesRef(p, valuesScratch[i]);
+      }
+      result.appendBytesRef(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "LeastBytesRefEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 84 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastDoubleEvaluator.java

@@ -0,0 +1,84 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+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.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Least}.
+ * This class is generated. Do not edit it.
+ */
+public final class LeastDoubleEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public LeastDoubleEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    DoubleBlock[] valuesBlocks = new DoubleBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (DoubleBlock) block;
+    }
+    DoubleVector[] valuesVectors = new DoubleVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public DoubleBlock eval(int positionCount, DoubleBlock[] valuesBlocks) {
+    DoubleBlock.Builder result = DoubleBlock.newBlockBuilder(positionCount);
+    double[] valuesValues = new double[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getDouble(o);
+      }
+      result.appendDouble(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public DoubleVector eval(int positionCount, DoubleVector[] valuesVectors) {
+    DoubleVector.Builder result = DoubleVector.newVectorBuilder(positionCount);
+    double[] valuesValues = new double[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getDouble(p);
+      }
+      result.appendDouble(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "LeastDoubleEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 84 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastIntEvaluator.java

@@ -0,0 +1,84 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+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.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Least}.
+ * This class is generated. Do not edit it.
+ */
+public final class LeastIntEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public LeastIntEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    IntBlock[] valuesBlocks = new IntBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (IntBlock) block;
+    }
+    IntVector[] valuesVectors = new IntVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public IntBlock eval(int positionCount, IntBlock[] valuesBlocks) {
+    IntBlock.Builder result = IntBlock.newBlockBuilder(positionCount);
+    int[] valuesValues = new int[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getInt(o);
+      }
+      result.appendInt(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public IntVector eval(int positionCount, IntVector[] valuesVectors) {
+    IntVector.Builder result = IntVector.newVectorBuilder(positionCount);
+    int[] valuesValues = new int[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getInt(p);
+      }
+      result.appendInt(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "LeastIntEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

+ 84 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastLongEvaluator.java

@@ -0,0 +1,84 @@
+// 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.conditional;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+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.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Least}.
+ * This class is generated. Do not edit it.
+ */
+public final class LeastLongEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final EvalOperator.ExpressionEvaluator[] values;
+
+  public LeastLongEvaluator(EvalOperator.ExpressionEvaluator[] values) {
+    this.values = values;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    LongBlock[] valuesBlocks = new LongBlock[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      Block block = values[i].eval(page);
+      if (block.areAllValuesNull()) {
+        return Block.constantNullBlock(page.getPositionCount());
+      }
+      valuesBlocks[i] = (LongBlock) block;
+    }
+    LongVector[] valuesVectors = new LongVector[values.length];
+    for (int i = 0; i < valuesBlocks.length; i++) {
+      valuesVectors[i] = valuesBlocks[i].asVector();
+      if (valuesVectors[i] == null) {
+        return eval(page.getPositionCount(), valuesBlocks);
+      }
+    }
+    return eval(page.getPositionCount(), valuesVectors).asBlock();
+  }
+
+  public LongBlock eval(int positionCount, LongBlock[] valuesBlocks) {
+    LongBlock.Builder result = LongBlock.newBlockBuilder(positionCount);
+    long[] valuesValues = new long[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        if (valuesBlocks[i].isNull(p) || valuesBlocks[i].getValueCount(p) != 1) {
+          result.appendNull();
+          continue position;
+        }
+      }
+      // unpack valuesBlocks into valuesValues
+      for (int i = 0; i < valuesBlocks.length; i++) {
+        int o = valuesBlocks[i].getFirstValueIndex(p);
+        valuesValues[i] = valuesBlocks[i].getLong(o);
+      }
+      result.appendLong(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  public LongVector eval(int positionCount, LongVector[] valuesVectors) {
+    LongVector.Builder result = LongVector.newVectorBuilder(positionCount);
+    long[] valuesValues = new long[values.length];
+    position: for (int p = 0; p < positionCount; p++) {
+      // unpack valuesVectors into valuesValues
+      for (int i = 0; i < valuesVectors.length; i++) {
+        valuesValues[i] = valuesVectors[i].getLong(p);
+      }
+      result.appendLong(Least.process(valuesValues));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "LeastLongEvaluator[" + "values=" + Arrays.toString(values) + "]";
+  }
+}

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

@@ -17,6 +17,8 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Min;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.Percentile;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.Sum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
+import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Greatest;
+import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Least;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToBoolean;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDatetime;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDegrees;
@@ -114,10 +116,12 @@ public class EsqlFunctionRegistry extends FunctionRegistry {
                 def(Cosh.class, Cosh::new, "cosh"),
                 def(E.class, E::new, "e"),
                 def(Floor.class, Floor::new, "floor"),
+                def(Greatest.class, Greatest::new, "greatest"),
                 def(IsFinite.class, IsFinite::new, "is_finite"),
                 def(IsInfinite.class, IsInfinite::new, "is_infinite"),
                 def(IsNaN.class, IsNaN::new, "is_nan"),
                 def(Log10.class, Log10::new, "log10"),
+                def(Least.class, Least::new, "least"),
                 def(Pi.class, Pi::new, "pi"),
                 def(Pow.class, Pow::new, "pow"),
                 def(Round.class, Round::new, "round"),

+ 8 - 7
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Case.java

@@ -30,6 +30,7 @@ import java.util.List;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
 import static org.elasticsearch.xpack.ql.type.DataTypes.NULL;
@@ -41,14 +42,14 @@ public class Case extends ScalarFunction implements EvaluatorMapper {
     private final Expression elseValue;
     private DataType dataType;
 
-    public Case(Source source, List<Expression> fields) {
-        super(source, fields);
-        int conditionCount = fields.size() / 2;
+    public Case(Source source, Expression first, List<Expression> rest) {
+        super(source, Stream.concat(Stream.of(first), rest.stream()).toList());
+        int conditionCount = children().size() / 2;
         conditions = new ArrayList<>(conditionCount);
         for (int c = 0; c < conditionCount; c++) {
-            conditions.add(new Condition(fields.get(c * 2), fields.get(c * 2 + 1)));
+            conditions.add(new Condition(children().get(c * 2), children().get(c * 2 + 1)));
         }
-        elseValue = fields.size() % 2 == 0 ? new Literal(source, null, NULL) : fields.get(fields.size() - 1);
+        elseValue = children().size() % 2 == 0 ? new Literal(source, null, NULL) : children().get(children().size() - 1);
     }
 
     @Override
@@ -116,12 +117,12 @@ public class Case extends ScalarFunction implements EvaluatorMapper {
 
     @Override
     public Expression replaceChildren(List<Expression> newChildren) {
-        return new Case(source(), newChildren);
+        return new Case(source(), newChildren.get(0), newChildren.subList(1, newChildren.size()));
     }
 
     @Override
     protected NodeInfo<? extends Expression> info() {
-        return NodeInfo.create(this, Case::new, children());
+        return NodeInfo.create(this, Case::new, children().get(0), children().subList(1, children().size()));
     }
 
     @Override

+ 195 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Greatest.java

@@ -0,0 +1,195 @@
+/*
+ * 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.conditional;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMaxBooleanEvaluator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMaxBytesRefEvaluator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMaxDoubleEvaluator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMaxIntEvaluator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMaxLongEvaluator;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.Expressions;
+import org.elasticsearch.xpack.ql.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
+import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
+import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import static org.elasticsearch.xpack.ql.type.DataTypes.NULL;
+
+/**
+ * Returns the maximum value of multiple columns.
+ */
+public class Greatest extends ScalarFunction implements EvaluatorMapper, OptionalArgument {
+    private DataType dataType;
+
+    public Greatest(Source source, Expression first, List<Expression> rest) {
+        super(source, Stream.concat(Stream.of(first), rest.stream()).toList());
+    }
+
+    @Override
+    public DataType dataType() {
+        if (dataType == null) {
+            resolveType();
+        }
+        return dataType;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        for (int position = 0; position < children().size(); position++) {
+            if (dataType == null || dataType == NULL) {
+                dataType = children().get(position).dataType();
+                continue;
+            }
+            TypeResolution resolution = TypeResolutions.isType(
+                children().get(position),
+                t -> t == dataType,
+                sourceText(),
+                TypeResolutions.ParamOrdinal.fromIndex(position),
+                dataType.typeName()
+            );
+            if (resolution.unresolved()) {
+                return resolution;
+            }
+        }
+        return TypeResolution.TYPE_RESOLVED;
+    }
+
+    @Override
+    public ScriptTemplate asScript() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new Greatest(source(), newChildren.get(0), newChildren.subList(1, newChildren.size()));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, Greatest::new, children().get(0), children().subList(1, children().size()));
+    }
+
+    @Override
+    public boolean foldable() {
+        return Expressions.foldable(children());
+    }
+
+    @Override
+    public Object fold() {
+        return EvaluatorMapper.super.fold();
+    }
+
+    @Override
+    public Supplier<EvalOperator.ExpressionEvaluator> toEvaluator(
+        Function<Expression, Supplier<EvalOperator.ExpressionEvaluator>> toEvaluator
+    ) {
+        List<Supplier<EvalOperator.ExpressionEvaluator>> evaluatorSuppliers = children().stream().map(toEvaluator).toList();
+        Supplier<Stream<EvalOperator.ExpressionEvaluator>> suppliers = () -> evaluatorSuppliers.stream().map(Supplier::get);
+        if (dataType == DataTypes.BOOLEAN) {
+            return () -> new GreatestBooleanEvaluator(
+                suppliers.get().map(MvMaxBooleanEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        if (dataType == DataTypes.DOUBLE) {
+            return () -> new GreatestDoubleEvaluator(
+                suppliers.get().map(MvMaxDoubleEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        if (dataType == DataTypes.INTEGER) {
+            return () -> new GreatestIntEvaluator(
+                suppliers.get().map(MvMaxIntEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        if (dataType == DataTypes.LONG) {
+            return () -> new GreatestLongEvaluator(
+                suppliers.get().map(MvMaxLongEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        if (dataType == NULL) {
+            return () -> EvalOperator.CONSTANT_NULL;
+        }
+        if (dataType == DataTypes.KEYWORD
+            || dataType == DataTypes.TEXT
+            || dataType == DataTypes.IP
+            || dataType == DataTypes.VERSION
+            || dataType == DataTypes.UNSUPPORTED) {
+
+            return () -> new GreatestBytesRefEvaluator(
+                suppliers.get().map(MvMaxBytesRefEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        throw EsqlIllegalArgumentException.illegalDataType(dataType);
+    }
+
+    @Evaluator(extraName = "Boolean")
+    static boolean process(boolean[] values) {
+        for (boolean v : values) {
+            if (v) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Evaluator(extraName = "BytesRef")
+    static BytesRef process(BytesRef[] values) {
+        BytesRef max = values[0];
+        for (int i = 1; i < values.length; i++) {
+            max = max.compareTo(values[i]) > 0 ? max : values[i];
+        }
+        return max;
+    }
+
+    @Evaluator(extraName = "Int")
+    static int process(int[] values) {
+        int max = values[0];
+        for (int i = 1; i < values.length; i++) {
+            max = Math.max(max, values[i]);
+        }
+        return max;
+    }
+
+    @Evaluator(extraName = "Long")
+    static long process(long[] values) {
+        long max = values[0];
+        for (int i = 1; i < values.length; i++) {
+            max = Math.max(max, values[i]);
+        }
+        return max;
+    }
+
+    @Evaluator(extraName = "Double")
+    static double process(double[] values) {
+        double max = values[0];
+        for (int i = 1; i < values.length; i++) {
+            max = Math.max(max, values[i]);
+        }
+        return max;
+    }
+
+    // TODO unsigned long
+}

+ 193 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Least.java

@@ -0,0 +1,193 @@
+/*
+ * 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.conditional;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMinBooleanEvaluator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMinBytesRefEvaluator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMinDoubleEvaluator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMinIntEvaluator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMinLongEvaluator;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.Expressions;
+import org.elasticsearch.xpack.ql.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
+import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
+import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import static org.elasticsearch.xpack.ql.type.DataTypes.NULL;
+
+/**
+ * Returns the minimum value of multiple columns.
+ */
+public class Least extends ScalarFunction implements EvaluatorMapper, OptionalArgument {
+    private DataType dataType;
+
+    public Least(Source source, Expression first, List<Expression> rest) {
+        super(source, Stream.concat(Stream.of(first), rest.stream()).toList());
+    }
+
+    @Override
+    public DataType dataType() {
+        if (dataType == null) {
+            resolveType();
+        }
+        return dataType;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        for (int position = 0; position < children().size(); position++) {
+            if (dataType == null || dataType == NULL) {
+                dataType = children().get(position).dataType();
+                continue;
+            }
+            TypeResolution resolution = TypeResolutions.isType(
+                children().get(position),
+                t -> t == dataType,
+                sourceText(),
+                TypeResolutions.ParamOrdinal.fromIndex(position),
+                dataType.typeName()
+            );
+            if (resolution.unresolved()) {
+                return resolution;
+            }
+        }
+        return TypeResolution.TYPE_RESOLVED;
+    }
+
+    @Override
+    public ScriptTemplate asScript() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new Least(source(), newChildren.get(0), newChildren.subList(1, newChildren.size()));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, Least::new, children().get(0), children().subList(1, children().size()));
+    }
+
+    @Override
+    public boolean foldable() {
+        return Expressions.foldable(children());
+    }
+
+    @Override
+    public Object fold() {
+        return EvaluatorMapper.super.fold();
+    }
+
+    @Override
+    public Supplier<EvalOperator.ExpressionEvaluator> toEvaluator(
+        Function<Expression, Supplier<EvalOperator.ExpressionEvaluator>> toEvaluator
+    ) {
+        List<Supplier<EvalOperator.ExpressionEvaluator>> evaluatorSuppliers = children().stream().map(toEvaluator).toList();
+        Supplier<Stream<EvalOperator.ExpressionEvaluator>> suppliers = () -> evaluatorSuppliers.stream().map(Supplier::get);
+        if (dataType == DataTypes.BOOLEAN) {
+            return () -> new LeastBooleanEvaluator(
+                suppliers.get().map(MvMinBooleanEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        if (dataType == DataTypes.DOUBLE) {
+            return () -> new LeastDoubleEvaluator(
+                suppliers.get().map(MvMinDoubleEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        if (dataType == DataTypes.INTEGER) {
+            return () -> new LeastIntEvaluator(
+                suppliers.get().map(MvMinIntEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        if (dataType == DataTypes.LONG) {
+            return () -> new LeastLongEvaluator(
+                suppliers.get().map(MvMinLongEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        if (dataType == NULL) {
+            return () -> EvalOperator.CONSTANT_NULL;
+        }
+        if (dataType == DataTypes.KEYWORD
+            || dataType == DataTypes.TEXT
+            || dataType == DataTypes.IP
+            || dataType == DataTypes.VERSION
+            || dataType == DataTypes.UNSUPPORTED) {
+
+            return () -> new LeastBytesRefEvaluator(
+                suppliers.get().map(MvMinBytesRefEvaluator::new).toArray(EvalOperator.ExpressionEvaluator[]::new)
+            );
+        }
+        throw EsqlIllegalArgumentException.illegalDataType(dataType);
+    }
+
+    @Evaluator(extraName = "Boolean")
+    static boolean process(boolean[] values) {
+        for (boolean v : values) {
+            if (v == false) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Evaluator(extraName = "BytesRef")
+    static BytesRef process(BytesRef[] values) {
+        BytesRef min = values[0];
+        for (int i = 1; i < values.length; i++) {
+            min = min.compareTo(values[i]) < 0 ? min : values[i];
+        }
+        return min;
+    }
+
+    @Evaluator(extraName = "Int")
+    static int process(int[] values) {
+        int min = values[0];
+        for (int i = 1; i < values.length; i++) {
+            min = Math.min(min, values[i]);
+        }
+        return min;
+    }
+
+    @Evaluator(extraName = "Long")
+    static long process(long[] values) {
+        long min = values[0];
+        for (int i = 1; i < values.length; i++) {
+            min = Math.min(min, values[i]);
+        }
+        return min;
+    }
+
+    @Evaluator(extraName = "Double")
+    static double process(double[] values) {
+        double min = values[0];
+        for (int i = 1; i < values.length; i++) {
+            min = Math.min(min, values[i]);
+        }
+        return min;
+    }
+}

+ 12 - 5
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/nulls/Coalesce.java

@@ -18,6 +18,7 @@ import org.elasticsearch.xpack.ql.expression.Expression;
 import org.elasticsearch.xpack.ql.expression.Expressions;
 import org.elasticsearch.xpack.ql.expression.Nullability;
 import org.elasticsearch.xpack.ql.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
 import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
 import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
 import org.elasticsearch.xpack.ql.tree.NodeInfo;
@@ -28,17 +29,18 @@ import java.util.List;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 import static org.elasticsearch.xpack.ql.type.DataTypes.NULL;
 
 /**
  * Function returning the first non-null value.
  */
-public class Coalesce extends ScalarFunction implements EvaluatorMapper {
+public class Coalesce extends ScalarFunction implements EvaluatorMapper, OptionalArgument {
     private DataType dataType;
 
-    public Coalesce(Source source, List<Expression> expressions) {
-        super(source, expressions);
+    public Coalesce(Source source, Expression first, List<Expression> rest) {
+        super(source, Stream.concat(Stream.of(first), rest.stream()).toList());
     }
 
     @Override
@@ -97,12 +99,12 @@ public class Coalesce extends ScalarFunction implements EvaluatorMapper {
 
     @Override
     public Expression replaceChildren(List<Expression> newChildren) {
-        return new Coalesce(source(), newChildren);
+        return new Coalesce(source(), newChildren.get(0), newChildren.subList(1, newChildren.size()));
     }
 
     @Override
     protected NodeInfo<? extends Expression> info() {
-        return NodeInfo.create(this, Coalesce::new, children());
+        return NodeInfo.create(this, Coalesce::new, children().get(0), children().subList(1, children().size()));
     }
 
     @Override
@@ -159,5 +161,10 @@ public class Coalesce extends ScalarFunction implements EvaluatorMapper {
             }
             return result.build();
         }
+
+        @Override
+        public String toString() {
+            return "CoalesceEvaluator[values=" + evaluators + ']';
+        }
     }
 }

+ 23 - 24
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java

@@ -27,6 +27,8 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Percentile;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.Sum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.UnaryScalarFunction;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
+import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Greatest;
+import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Least;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToBoolean;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDatetime;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDegrees;
@@ -322,15 +324,17 @@ public final class PlanNamedTypes {
             // ScalarFunction
             of(ScalarFunction.class, Atan2.class, PlanNamedTypes::writeAtan2, PlanNamedTypes::readAtan2),
             of(ScalarFunction.class, AutoBucket.class, PlanNamedTypes::writeAutoBucket, PlanNamedTypes::readAutoBucket),
-            of(ScalarFunction.class, Case.class, PlanNamedTypes::writeCase, PlanNamedTypes::readCase),
+            of(ScalarFunction.class, Case.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
             of(ScalarFunction.class, CIDRMatch.class, PlanNamedTypes::writeCIDRMatch, PlanNamedTypes::readCIDRMatch),
-            of(ScalarFunction.class, Coalesce.class, PlanNamedTypes::writeCoalesce, PlanNamedTypes::readCoalesce),
-            of(ScalarFunction.class, Concat.class, PlanNamedTypes::writeConcat, PlanNamedTypes::readConcat),
+            of(ScalarFunction.class, Coalesce.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
+            of(ScalarFunction.class, Concat.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
             of(ScalarFunction.class, DateExtract.class, PlanNamedTypes::writeDateExtract, PlanNamedTypes::readDateExtract),
             of(ScalarFunction.class, DateFormat.class, PlanNamedTypes::writeDateFormat, PlanNamedTypes::readDateFormat),
             of(ScalarFunction.class, DateParse.class, PlanNamedTypes::writeDateTimeParse, PlanNamedTypes::readDateTimeParse),
             of(ScalarFunction.class, DateTrunc.class, PlanNamedTypes::writeDateTrunc, PlanNamedTypes::readDateTrunc),
             of(ScalarFunction.class, E.class, PlanNamedTypes::writeNoArgScalar, PlanNamedTypes::readNoArgScalar),
+            of(ScalarFunction.class, Greatest.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
+            of(ScalarFunction.class, Least.class, PlanNamedTypes::writeVararg, PlanNamedTypes::readVarag),
             of(ScalarFunction.class, Now.class, PlanNamedTypes::writeNow, PlanNamedTypes::readNow),
             of(ScalarFunction.class, Pi.class, PlanNamedTypes::writeNoArgScalar, PlanNamedTypes::readNoArgScalar),
             of(ScalarFunction.class, Round.class, PlanNamedTypes::writeRound, PlanNamedTypes::readRound),
@@ -1124,30 +1128,25 @@ public final class PlanNamedTypes {
         out.writeExpression(bucket.to());
     }
 
-    static Case readCase(PlanStreamInput in) throws IOException {
-        return new Case(Source.EMPTY, in.readList(readerFromPlanReader(PlanStreamInput::readExpression)));
-    }
-
-    static void writeCase(PlanStreamOutput out, Case caseValue) throws IOException {
-        out.writeCollection(caseValue.children(), writerFromPlanWriter(PlanStreamOutput::writeExpression));
-    }
-
-    static Coalesce readCoalesce(PlanStreamInput in) throws IOException {
-        return new Coalesce(Source.EMPTY, in.readList(readerFromPlanReader(PlanStreamInput::readExpression)));
-    }
-
-    static void writeCoalesce(PlanStreamOutput out, Coalesce coalesce) throws IOException {
-        out.writeCollection(coalesce.children(), writerFromPlanWriter(PlanStreamOutput::writeExpression));
-    }
+    static final Map<String, TriFunction<Source, Expression, List<Expression>, ScalarFunction>> VARARG_CTORS = Map.ofEntries(
+        entry(name(Case.class), Case::new),
+        entry(name(Coalesce.class), Coalesce::new),
+        entry(name(Concat.class), Concat::new),
+        entry(name(Greatest.class), Greatest::new),
+        entry(name(Least.class), Least::new)
+    );
 
-    static Concat readConcat(PlanStreamInput in) throws IOException {
-        return new Concat(Source.EMPTY, in.readExpression(), in.readList(readerFromPlanReader(PlanStreamInput::readExpression)));
+    static ScalarFunction readVarag(PlanStreamInput in, String name) throws IOException {
+        return VARARG_CTORS.get(name)
+            .apply(Source.EMPTY, in.readExpression(), in.readList(readerFromPlanReader(PlanStreamInput::readExpression)));
     }
 
-    static void writeConcat(PlanStreamOutput out, Concat concat) throws IOException {
-        List<Expression> fields = concat.children();
-        out.writeExpression(fields.get(0));
-        out.writeCollection(fields.subList(1, fields.size()), writerFromPlanWriter(PlanStreamOutput::writeExpression));
+    static void writeVararg(PlanStreamOutput out, ScalarFunction vararg) throws IOException {
+        out.writeExpression(vararg.children().get(0));
+        out.writeCollection(
+            vararg.children().subList(1, vararg.children().size()),
+            writerFromPlanWriter(PlanStreamOutput::writeExpression)
+        );
     }
 
     static CountDistinct readCountDistinct(PlanStreamInput in) throws IOException {

+ 19 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java

@@ -29,10 +29,28 @@ public class ParsingTests extends ESTestCase {
         new Verifier(new Metrics())
     );
 
+    public void testCaseFunctionInvalidInputs() {
+        assertEquals("1:23: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case()"));
+        assertEquals("1:23: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case(a)"));
+        assertEquals("1:23: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case(1)"));
+    }
+
     public void testConcatFunctionInvalidInputs() {
         assertEquals("1:23: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat()"));
         assertEquals("1:23: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat(a)"));
-        assertEquals("1:23: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat(123)"));
+        assertEquals("1:23: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat(1)"));
+    }
+
+    public void testCoalesceFunctionInvalidInputs() {
+        assertEquals("1:23: error building [coalesce]: expects at least one argument", error("row a = 1 | eval x = coalesce()"));
+    }
+
+    public void testGreatestFunctionInvalidInputs() {
+        assertEquals("1:23: error building [greatest]: expects at least one argument", error("row a = 1 | eval x = greatest()"));
+    }
+
+    public void testLeastFunctionInvalidInputs() {
+        assertEquals("1:23: error building [least]: expects at least one argument", error("row a = 1 | eval x = least()"));
     }
 
     private String error(String query) {

+ 2 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java

@@ -119,7 +119,7 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
     /**
      * This class exists to give a human-readable string representation of the test case.
      */
-    protected static class TestCaseSupplier implements Supplier<TestCase> {
+    public static class TestCaseSupplier implements Supplier<TestCase> {
 
         private String name;
         private final Supplier<TestCase> wrapped;
@@ -202,6 +202,7 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         }
         Layout.Builder builder = new Layout.Builder();
         buildLayout(builder, e);
+        assertTrue(e.resolved());
         return EvalMapper.toEvaluator(e, builder.build());
     }
 

+ 364 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/VaragsTestCaseBuilder.java

@@ -0,0 +1,364 @@
+/*
+ * 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;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+import org.hamcrest.Matcher;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.nullValue;
+
+/**
+ * Builds test cases for variable argument functions.
+ */
+public class VaragsTestCaseBuilder {
+    private static final int MAX_WIDTH = 10;
+
+    private Function<String, String> expectedEvaluatorPrefix;
+    private Function<String, String> expectedEvaluatorValueMap = Function.identity();
+    private Function<String[][], Matcher<Object>> expectedStr;
+    private Function<long[][], Matcher<Object>> expectedLong;
+    private Function<int[][], Matcher<Object>> expectedInt;
+    // TODO double
+    private Function<boolean[][], Matcher<Object>> expectedBoolean;
+
+    /**
+     * Build the builder.
+     * @param expectedEvaluatorPrefix maps from a type name to the name of the matcher
+     */
+    public VaragsTestCaseBuilder(Function<String, String> expectedEvaluatorPrefix) {
+        this.expectedEvaluatorPrefix = expectedEvaluatorPrefix;
+    }
+
+    /**
+     * Wraps each evaluator's toString.
+     */
+    public VaragsTestCaseBuilder expectedEvaluatorValueWrap(Function<String, String> expectedEvaluatorValueMap) {
+        this.expectedEvaluatorValueMap = expectedEvaluatorValueMap;
+        return this;
+    }
+
+    public VaragsTestCaseBuilder expectString(Function<Stream<String[]>, Optional<String[]>> expectedStr) {
+        this.expectedStr = strings -> {
+            if (Arrays.stream(strings).anyMatch(s -> s == null)) {
+                return nullValue();
+            }
+            Optional<String[]> expected = expectedStr.apply(Arrays.stream(strings));
+            if (expected.isPresent() == false) {
+                return nullValue();
+            }
+            if (expected.get().length == 1) {
+                return equalTo(new BytesRef(expected.get()[0]));
+            }
+            return equalTo(Arrays.stream(expected.get()).map(BytesRef::new).toList());
+        };
+        return this;
+    }
+
+    public VaragsTestCaseBuilder expectFlattenedString(Function<Stream<String>, Optional<String>> expectedStr) {
+        this.expectedStr = strings -> {
+            if (Arrays.stream(strings).anyMatch(s -> s == null)) {
+                return nullValue();
+            }
+            Optional<String> expected = expectedStr.apply(Arrays.stream(strings).flatMap(Arrays::stream));
+            if (expected.isPresent() == false) {
+                return nullValue();
+            }
+            return equalTo(new BytesRef(expected.get()));
+        };
+        return this;
+    }
+
+    public VaragsTestCaseBuilder expectLong(Function<Stream<long[]>, Optional<long[]>> expectedLong) {
+        this.expectedLong = longs -> {
+            if (Arrays.stream(longs).anyMatch(l -> l == null)) {
+                return nullValue();
+            }
+            Optional<long[]> expected = expectedLong.apply(Arrays.stream(longs));
+            if (expected.isPresent() == false) {
+                return nullValue();
+            }
+            if (expected.get().length == 1) {
+                return equalTo(expected.get()[0]);
+            }
+            return equalTo(Arrays.stream(expected.get()).mapToObj(Long::valueOf).toList());
+        };
+        return this;
+    }
+
+    public VaragsTestCaseBuilder expectFlattenedLong(Function<LongStream, OptionalLong> expectedLong) {
+        this.expectedLong = longs -> {
+            if (Arrays.stream(longs).anyMatch(l -> l == null)) {
+                return nullValue();
+            }
+            OptionalLong expected = expectedLong.apply(Arrays.stream(longs).flatMapToLong(Arrays::stream));
+            if (expected.isPresent() == false) {
+                return nullValue();
+            }
+            return equalTo(expected.getAsLong());
+        };
+        return this;
+    }
+
+    public VaragsTestCaseBuilder expectInt(Function<Stream<int[]>, Optional<int[]>> expectedInt) {
+        this.expectedInt = ints -> {
+            Optional<int[]> expected = expectedInt.apply(Arrays.stream(ints));
+            if (expected.isPresent() == false) {
+                return nullValue();
+            }
+            if (expected.get().length == 1) {
+                return equalTo(expected.get()[0]);
+            }
+            return equalTo(Arrays.stream(expected.get()).mapToObj(Integer::valueOf).toList());
+        };
+        return this;
+    }
+
+    public VaragsTestCaseBuilder expectFlattenedInt(Function<IntStream, OptionalInt> expectedInt) {
+        this.expectedInt = ints -> {
+            if (Arrays.stream(ints).anyMatch(i -> i == null)) {
+                return nullValue();
+            }
+            OptionalInt expected = expectedInt.apply(Arrays.stream(ints).flatMapToInt(Arrays::stream));
+            if (expected.isPresent() == false) {
+                return nullValue();
+            }
+            return equalTo(expected.getAsInt());
+        };
+        return this;
+    }
+
+    public VaragsTestCaseBuilder expectBoolean(Function<Stream<boolean[]>, Optional<boolean[]>> expectedBoolean) {
+        this.expectedBoolean = booleans -> {
+            Optional<boolean[]> expected = expectedBoolean.apply(Arrays.stream(booleans));
+            if (expected.isPresent() == false) {
+                return nullValue();
+            }
+            if (expected.get().length == 1) {
+                return equalTo(expected.get()[0]);
+            }
+            return equalTo(IntStream.range(0, expected.get().length).mapToObj(i -> expected.get()[i]).toList());
+        };
+        return this;
+    }
+
+    public VaragsTestCaseBuilder expectFlattenedBoolean(Function<Stream<Boolean>, Optional<Boolean>> expectedBoolean) {
+        this.expectedBoolean = booleans -> {
+            if (Arrays.stream(booleans).anyMatch(i -> i == null)) {
+                return nullValue();
+            }
+            Optional<Boolean> expected = expectedBoolean.apply(
+                Arrays.stream(booleans).flatMap(bs -> IntStream.range(0, bs.length).mapToObj(i -> bs[i]))
+            );
+            if (expected.isPresent() == false) {
+                return nullValue();
+            }
+            return equalTo(expected.get());
+        };
+        return this;
+    }
+
+    public List<AbstractFunctionTestCase.TestCaseSupplier> suppliers() {
+        List<AbstractFunctionTestCase.TestCaseSupplier> suppliers = new ArrayList<>();
+        // TODO more types
+        if (expectedStr != null) {
+            strings(suppliers);
+        }
+        if (expectedLong != null) {
+            longs(suppliers);
+        }
+        if (expectedInt != null) {
+            ints(suppliers);
+        }
+        if (expectedBoolean != null) {
+            booleans(suppliers);
+        }
+        return suppliers;
+    }
+
+    private void strings(List<AbstractFunctionTestCase.TestCaseSupplier> suppliers) {
+        for (int count = 1; count < MAX_WIDTH; count++) {
+            for (boolean multivalued : new boolean[] { false, true }) {
+                int paramCount = count;
+                suppliers.add(
+                    new AbstractFunctionTestCase.TestCaseSupplier(
+                        testCaseName(paramCount, multivalued, "kwd"),
+                        () -> stringCase(DataTypes.KEYWORD, paramCount, multivalued)
+                    )
+                );
+                suppliers.add(
+                    new AbstractFunctionTestCase.TestCaseSupplier(
+                        testCaseName(paramCount, multivalued, "text"),
+                        () -> stringCase(DataTypes.TEXT, paramCount, multivalued)
+                    )
+                );
+            }
+        }
+    }
+
+    private AbstractFunctionTestCase.TestCase stringCase(DataType dataType, int paramCount, boolean multivalued) {
+        String[][] data = new String[paramCount][];
+        List<AbstractFunctionTestCase.TypedData> typedData = new ArrayList<>(paramCount);
+        for (int p = 0; p < paramCount; p++) {
+            if (multivalued) {
+                data[p] = ESTestCase.randomList(1, 4, () -> ESTestCase.randomAlphaOfLength(5)).toArray(String[]::new);
+                typedData.add(
+                    new AbstractFunctionTestCase.TypedData(Arrays.stream(data[p]).map(BytesRef::new).toList(), dataType, "field" + p)
+                );
+            } else {
+                data[p] = new String[] { ESTestCase.randomAlphaOfLength(5) };
+                typedData.add(new AbstractFunctionTestCase.TypedData(new BytesRef(data[p][0]), dataType, "field" + p));
+            }
+        }
+        return testCase(typedData, expectedEvaluatorPrefix.apply("BytesRef"), dataType, expectedStr.apply(data));
+    }
+
+    private void longs(List<AbstractFunctionTestCase.TestCaseSupplier> suppliers) {
+        for (int count = 1; count < MAX_WIDTH; count++) {
+            for (boolean multivalued : new boolean[] { false, true }) {
+                int paramCount = count;
+                suppliers.add(
+                    new AbstractFunctionTestCase.TestCaseSupplier(
+                        testCaseName(paramCount, multivalued, "long"),
+                        () -> longCase(paramCount, multivalued)
+                    )
+                );
+            }
+        }
+    }
+
+    private AbstractFunctionTestCase.TestCase longCase(int paramCount, boolean multivalued) {
+        long[][] data = new long[paramCount][];
+        List<AbstractFunctionTestCase.TypedData> typedData = new ArrayList<>(paramCount);
+        for (int p = 0; p < paramCount; p++) {
+            if (multivalued) {
+                List<Long> d = ESTestCase.randomList(1, 4, () -> ESTestCase.randomLong());
+                data[p] = d.stream().mapToLong(Long::longValue).toArray();
+                typedData.add(
+                    new AbstractFunctionTestCase.TypedData(
+                        Arrays.stream(data[p]).mapToObj(Long::valueOf).toList(),
+                        DataTypes.LONG,
+                        "field" + p
+                    )
+                );
+            } else {
+                data[p] = new long[] { ESTestCase.randomLong() };
+                typedData.add(new AbstractFunctionTestCase.TypedData(data[p][0], DataTypes.LONG, "field" + p));
+            }
+        }
+        return testCase(typedData, expectedEvaluatorPrefix.apply("Long"), DataTypes.LONG, expectedLong.apply(data));
+    }
+
+    private void ints(List<AbstractFunctionTestCase.TestCaseSupplier> suppliers) {
+        for (int count = 1; count < MAX_WIDTH; count++) {
+            for (boolean multivalued : new boolean[] { false, true }) {
+                int paramCount = count;
+                suppliers.add(
+                    new AbstractFunctionTestCase.TestCaseSupplier(
+                        testCaseName(paramCount, multivalued, "int"),
+                        () -> intCase(paramCount, multivalued)
+                    )
+                );
+            }
+        }
+    }
+
+    private AbstractFunctionTestCase.TestCase intCase(int paramCount, boolean multivalued) {
+        int[][] data = new int[paramCount][];
+        List<AbstractFunctionTestCase.TypedData> typedData = new ArrayList<>(paramCount);
+        for (int p = 0; p < paramCount; p++) {
+            if (multivalued) {
+                List<Integer> d = ESTestCase.randomList(1, 4, () -> ESTestCase.randomInt());
+                data[p] = d.stream().mapToInt(Integer::intValue).toArray();
+                typedData.add(new AbstractFunctionTestCase.TypedData(d, DataTypes.INTEGER, "field" + p));
+            } else {
+                data[p] = new int[] { ESTestCase.randomInt() };
+                typedData.add(new AbstractFunctionTestCase.TypedData(data[p][0], DataTypes.INTEGER, "field" + p));
+            }
+        }
+        return testCase(typedData, expectedEvaluatorPrefix.apply("Int"), DataTypes.INTEGER, expectedInt.apply(data));
+    }
+
+    private void booleans(List<AbstractFunctionTestCase.TestCaseSupplier> suppliers) {
+        for (int count = 1; count < MAX_WIDTH; count++) {
+            for (boolean multivalued : new boolean[] { false, true }) {
+                int paramCount = count;
+                suppliers.add(
+                    new AbstractFunctionTestCase.TestCaseSupplier(
+                        testCaseName(paramCount, multivalued, "bool"),
+                        () -> booleanCase(paramCount, multivalued)
+                    )
+                );
+            }
+        }
+    }
+
+    private AbstractFunctionTestCase.TestCase booleanCase(int paramCount, boolean multivalued) {
+        boolean[][] data = new boolean[paramCount][];
+        List<AbstractFunctionTestCase.TypedData> typedData = new ArrayList<>(paramCount);
+        for (int p = 0; p < paramCount; p++) {
+            if (multivalued) {
+                int size = ESTestCase.between(1, 5);
+                data[p] = new boolean[size];
+                List<Boolean> paramData = new ArrayList<>(size);
+                for (int i = 0; i < size; i++) {
+                    data[p][i] = ESTestCase.randomBoolean();
+                    paramData.add(data[p][i]);
+                }
+                typedData.add(new AbstractFunctionTestCase.TypedData(paramData, DataTypes.BOOLEAN, "field" + p));
+            } else {
+                data[p] = new boolean[] { ESTestCase.randomBoolean() };
+                typedData.add(new AbstractFunctionTestCase.TypedData(data[p][0], DataTypes.BOOLEAN, "field" + p));
+            }
+        }
+        return testCase(typedData, expectedEvaluatorPrefix.apply("Boolean"), DataTypes.BOOLEAN, expectedBoolean.apply(data));
+    }
+
+    private String testCaseName(int count, boolean multivalued, String type) {
+        return "(" + IntStream.range(0, count).mapToObj(i -> (multivalued ? "mv_" : "") + type).collect(Collectors.joining(", ")) + ")";
+    }
+
+    protected AbstractFunctionTestCase.TestCase testCase(
+        List<AbstractFunctionTestCase.TypedData> typedData,
+        String expectedEvaluatorPrefix,
+        DataType expectedType,
+        Matcher<Object> expectedValue
+    ) {
+        return new AbstractFunctionTestCase.TestCase(
+            typedData,
+            expectedToString(expectedEvaluatorPrefix, typedData.size()),
+            expectedType,
+            expectedValue
+        );
+    }
+
+    private String expectedToString(String expectedEvaluatorPrefix, int attrs) {
+        return expectedEvaluatorPrefix
+            + "Evaluator[values=["
+            + IntStream.range(0, attrs)
+                .mapToObj(i -> expectedEvaluatorValueMap.apply("Attribute[channel=" + i + "]"))
+                .collect(Collectors.joining(", "))
+            + "]]";
+    }
+}

+ 4 - 4
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/CaseTests.java

@@ -81,7 +81,7 @@ public class CaseTests extends AbstractFunctionTestCase {
 
     @Override
     protected Expression build(Source source, List<Expression> args) {
-        return new Case(Source.EMPTY, args.stream().toList());
+        return new Case(Source.EMPTY, args.get(0), args.subList(1, args.size()));
     }
 
     public void testEvalCase() {
@@ -122,7 +122,6 @@ public class CaseTests extends AbstractFunctionTestCase {
     }
 
     public void testCaseWithInvalidCondition() {
-        assertEquals("expected at least two arguments in [<case>] but got 0", resolveCase().message());
         assertEquals("expected at least two arguments in [<case>] but got 1", resolveCase(1).message());
         assertEquals("first argument of [<case>] must be [boolean], found value [1] type [integer]", resolveCase(1, 2).message());
         assertEquals(
@@ -158,12 +157,13 @@ public class CaseTests extends AbstractFunctionTestCase {
     }
 
     private static Case caseExpr(Object... args) {
-        return new Case(Source.synthetic("<case>"), Stream.of(args).<Expression>map(arg -> {
+        List<Expression> exps = Stream.of(args).<Expression>map(arg -> {
             if (arg instanceof Expression e) {
                 return e;
             }
             return new Literal(Source.synthetic(arg == null ? "null" : arg.toString()), arg, EsqlDataTypes.fromJava(arg));
-        }).toList());
+        }).toList();
+        return new Case(Source.synthetic("<case>"), exps.get(0), exps.subList(1, exps.size()));
     }
 
     private static TypeResolution resolveCase(Object... args) {

+ 63 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestTests.java

@@ -0,0 +1,63 @@
+/*
+ * 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.conditional;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.scalar.VaragsTestCaseBuilder;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class GreatestTests extends AbstractFunctionTestCase {
+    public GreatestTests(@Name("TestCase") Supplier<TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        VaragsTestCaseBuilder builder = new VaragsTestCaseBuilder(t -> "Greatest" + t);
+        builder.expectedEvaluatorValueWrap(e -> "MvMax[field=" + e + "]");
+        builder.expectFlattenedString(s -> s.sorted(Comparator.<String>naturalOrder().reversed()).findFirst());
+        builder.expectFlattenedBoolean(s -> s.sorted(Comparator.<Boolean>naturalOrder().reversed()).findFirst());
+        builder.expectFlattenedInt(IntStream::max);
+        builder.expectFlattenedLong(LongStream::max);
+        List<TestCaseSupplier> suppliers = builder.suppliers();
+        suppliers.add(
+            new TestCaseSupplier(
+                "(a, b)",
+                () -> new TestCase(
+                    List.of(
+                        new TypedData(new BytesRef("a"), DataTypes.KEYWORD, "a"),
+                        new TypedData(new BytesRef("b"), DataTypes.KEYWORD, "b")
+                    ),
+                    "GreatestBytesRefEvaluator[values=[MvMax[field=Attribute[channel=0]], MvMax[field=Attribute[channel=1]]]]",
+                    DataTypes.KEYWORD,
+                    equalTo(new BytesRef("b"))
+                )
+            )
+        );
+        return parameterSuppliersFromTypedData(suppliers);
+    }
+
+    @Override
+    protected Greatest build(Source source, List<Expression> args) {
+        return new Greatest(source, args.get(0), args.subList(1, args.size()));
+    }
+}

+ 62 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastTests.java

@@ -0,0 +1,62 @@
+/*
+ * 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.conditional;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.scalar.VaragsTestCaseBuilder;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class LeastTests extends AbstractFunctionTestCase {
+    public LeastTests(@Name("TestCase") Supplier<TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        VaragsTestCaseBuilder builder = new VaragsTestCaseBuilder(t -> "Least" + t);
+        builder.expectedEvaluatorValueWrap(e -> "MvMin[field=" + e + "]");
+        builder.expectFlattenedString(s -> s.sorted().findFirst());
+        builder.expectFlattenedBoolean(s -> s.sorted().findFirst());
+        builder.expectFlattenedInt(IntStream::min);
+        builder.expectFlattenedLong(LongStream::min);
+        List<TestCaseSupplier> suppliers = builder.suppliers();
+        suppliers.add(
+            new TestCaseSupplier(
+                "(a, b)",
+                () -> new TestCase(
+                    List.of(
+                        new TypedData(new BytesRef("a"), DataTypes.KEYWORD, "a"),
+                        new TypedData(new BytesRef("b"), DataTypes.KEYWORD, "b")
+                    ),
+                    "LeastBytesRefEvaluator[values=[MvMin[field=Attribute[channel=0]], MvMin[field=Attribute[channel=1]]]]",
+                    DataTypes.KEYWORD,
+                    equalTo(new BytesRef("a"))
+                )
+            )
+        );
+        return parameterSuppliersFromTypedData(suppliers);
+    }
+
+    @Override
+    protected Least build(Source source, List<Expression> args) {
+        return new Least(source, args.get(0), args.subList(1, args.size()));
+    }
+}

+ 11 - 76
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/nulls/CoalesceTests.java

@@ -10,37 +10,27 @@ package org.elasticsearch.xpack.esql.expression.function.scalar.nulls;
 import com.carrotsearch.randomizedtesting.annotations.Name;
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
 
-import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.compute.data.Block;
-import org.elasticsearch.compute.data.ElementType;
 import org.elasticsearch.xpack.esql.evaluator.EvalMapper;
 import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.scalar.VaragsTestCaseBuilder;
 import org.elasticsearch.xpack.esql.planner.Layout;
 import org.elasticsearch.xpack.ql.expression.Expression;
 import org.elasticsearch.xpack.ql.expression.FieldAttribute;
 import org.elasticsearch.xpack.ql.expression.Literal;
 import org.elasticsearch.xpack.ql.expression.Nullability;
 import org.elasticsearch.xpack.ql.tree.Source;
-import org.elasticsearch.xpack.ql.type.DataType;
-import org.elasticsearch.xpack.ql.type.DataTypes;
 import org.elasticsearch.xpack.ql.type.EsField;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
 
 import static org.elasticsearch.compute.data.BlockUtils.toJavaObject;
 import static org.hamcrest.Matchers.equalTo;
 
 public class CoalesceTests extends AbstractFunctionTestCase {
-    private static final int MAX_WIDTH = 10;
-
     public CoalesceTests(@Name("TestCase") Supplier<TestCase> testCaseSupplier) {
         this.testCase = testCaseSupplier.get();
     }
@@ -51,70 +41,12 @@ public class CoalesceTests extends AbstractFunctionTestCase {
      */
     @ParametersFactory
     public static Iterable<Object[]> parameters() {
-        List<TestCaseSupplier> suppliers = new ArrayList<>();
-        kwds(suppliers);
-        longs(suppliers);
-        ints(suppliers);
-        return parameterSuppliersFromTypedData(suppliers);
-    }
-
-    private static void kwds(List<TestCaseSupplier> suppliers) {
-        for (int i = 1; i < MAX_WIDTH; i++) {
-            suppliers.add(kwds(IntStream.range(0, i).mapToObj(v -> "v" + v).toArray(String[]::new)));
-        }
-    }
-
-    private static TestCaseSupplier kwds(String... data) {
-        int[] i = new int[1];
-        List<TypedData> typedData = Arrays.stream(data)
-            .map(d -> new TypedData(new BytesRef(d), DataTypes.KEYWORD, "field" + i[0]++))
-            .toList();
-        return new TestCaseSupplier(testCaseName(Arrays.stream(data)), () -> testCase(typedData, DataTypes.KEYWORD, ElementType.BYTES_REF));
-    }
-
-    private static void longs(List<TestCaseSupplier> suppliers) {
-        for (int i = 1; i < MAX_WIDTH; i++) {
-            suppliers.add(longs(IntStream.range(0, i).mapToObj(v -> Long.valueOf(v)).toArray(Long[]::new)));
-        }
-    }
-
-    private static TestCaseSupplier longs(Long... data) {
-        int[] i = new int[1];
-        List<TypedData> typedData = Arrays.stream(data).map(d -> new TypedData(d, DataTypes.LONG, "field" + i[0]++)).toList();
-        return new TestCaseSupplier(testCaseName(Arrays.stream(data)), () -> testCase(typedData, DataTypes.LONG, ElementType.LONG));
-    }
-
-    private static void ints(List<TestCaseSupplier> suppliers) {
-        for (int i = 1; i < MAX_WIDTH; i++) {
-            suppliers.add(ints(IntStream.range(0, i).mapToObj(v -> Integer.valueOf(v)).toArray(Integer[]::new)));
-        }
-    }
-
-    private static TestCaseSupplier ints(Integer... data) {
-        int[] i = new int[1];
-        List<TypedData> typedData = Arrays.stream(data).map(d -> new TypedData(d, DataTypes.INTEGER, "field" + i[0]++)).toList();
-        return new TestCaseSupplier(testCaseName(Arrays.stream(data)), () -> testCase(typedData, DataTypes.INTEGER, ElementType.INT));
-    }
-
-    private static String testCaseName(Stream<Object> data) {
-        return "(" + data.map(Objects::toString).collect(Collectors.joining(", ")) + ")";
-    }
-
-    private static TestCase testCase(List<TypedData> typedData, DataType expectedType, ElementType expectedElementType) {
-        return new TestCase(
-            typedData,
-            expectedToString(expectedElementType, typedData.size()),
-            expectedType,
-            equalTo(typedData.stream().map(d -> d.data()).filter(d -> d != null).findFirst().orElse(null))
-        );
-    }
-
-    private static String expectedToString(ElementType resultType, int attrs) {
-        return "CoalesceEvaluator[resultType="
-            + resultType
-            + ", evaluators=["
-            + IntStream.range(0, attrs).mapToObj(i -> "Attribute[channel=" + i + "]").collect(Collectors.joining(", "))
-            + "]]";
+        VaragsTestCaseBuilder builder = new VaragsTestCaseBuilder(type -> "Coalesce");
+        builder.expectString(strings -> strings.filter(v -> v != null).findFirst());
+        builder.expectLong(longs -> longs.filter(v -> v != null).findFirst());
+        builder.expectInt(ints -> ints.filter(v -> v != null).findFirst());
+        builder.expectBoolean(booleans -> booleans.filter(v -> v != null).findFirst());
+        return parameterSuppliersFromTypedData(builder.suppliers());
     }
 
     @Override
@@ -127,6 +59,9 @@ public class CoalesceTests extends AbstractFunctionTestCase {
             if (v == null) {
                 continue;
             }
+            if (v instanceof List<?> l && l.size() == 1) {
+                v = l.get(0);
+            }
             assertThat(toJavaObject(value, 0), equalTo(v));
             return;
         }
@@ -135,7 +70,7 @@ public class CoalesceTests extends AbstractFunctionTestCase {
 
     @Override
     protected Coalesce build(Source source, List<Expression> args) {
-        return new Coalesce(Source.EMPTY, args.stream().toList());
+        return new Coalesce(Source.EMPTY, args.get(0), args.subList(1, args.size()));
     }
 
     public void testCoalesceIsLazy() {