Browse Source

ESQL: Add boolean support to Max and Min aggs (#110527)

- Added support for Booleans on Max and Min
- Added some helper methods to BitArray (`set(index, value)` and `fill(from, to, value)`). This way, the container is more similar to other BigArrays, and it's easier to work with

Part of https://github.com/elastic/elasticsearch/issues/110346, as Max
and Min are dependencies of Top.
Iván Cea Fontenla 1 year ago
parent
commit
2901711c46
43 changed files with 1537 additions and 88 deletions
  1. 5 0
      docs/changelog/110527.yaml
  2. 1 1
      docs/reference/esql/functions/description/max.asciidoc
  3. 1 1
      docs/reference/esql/functions/description/min.asciidoc
  4. 17 5
      docs/reference/esql/functions/kibana/definition/max.json
  5. 17 5
      docs/reference/esql/functions/kibana/definition/min.json
  6. 1 1
      docs/reference/esql/functions/kibana/docs/max.md
  7. 1 1
      docs/reference/esql/functions/kibana/docs/min.md
  8. 1 1
      docs/reference/esql/functions/parameters/max.asciidoc
  9. 1 1
      docs/reference/esql/functions/parameters/min.asciidoc
  10. 1 1
      docs/reference/esql/functions/signature/max.svg
  11. 1 1
      docs/reference/esql/functions/signature/min.svg
  12. 2 1
      docs/reference/esql/functions/types/max.asciidoc
  13. 2 1
      docs/reference/esql/functions/types/min.asciidoc
  14. 73 0
      server/src/main/java/org/elasticsearch/common/util/BitArray.java
  15. 93 0
      server/src/test/java/org/elasticsearch/common/util/BitArrayTests.java
  16. 10 0
      x-pack/plugin/esql/compute/build.gradle
  17. 8 2
      x-pack/plugin/esql/compute/gen/src/main/java/org/elasticsearch/compute/gen/AggregatorImplementer.java
  18. 3 2
      x-pack/plugin/esql/compute/gen/src/main/java/org/elasticsearch/compute/gen/GroupingAggregatorImplementer.java
  19. 121 0
      x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/aggregation/BooleanArrayState.java
  20. 55 0
      x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/aggregation/BooleanState.java
  21. 136 0
      x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MaxBooleanAggregatorFunction.java
  22. 38 0
      x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MaxBooleanAggregatorFunctionSupplier.java
  23. 204 0
      x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MaxBooleanGroupingAggregatorFunction.java
  24. 136 0
      x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MinBooleanAggregatorFunction.java
  25. 38 0
      x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MinBooleanAggregatorFunctionSupplier.java
  26. 206 0
      x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MinBooleanGroupingAggregatorFunction.java
  27. 25 0
      x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/MaxBooleanAggregator.java
  28. 25 0
      x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/MinBooleanAggregator.java
  29. 24 2
      x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/X-ArrayState.java.st
  30. 4 0
      x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/X-State.java.st
  31. 43 0
      x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MaxBooleanAggregatorFunctionTests.java
  32. 43 0
      x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MinBooleanAggregatorFunctionTests.java
  33. 14 16
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec
  34. 38 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec
  35. 5 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  36. 35 18
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Max.java
  37. 37 20
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Min.java
  38. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java
  39. 4 4
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java
  40. 4 2
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java
  41. 22 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/MultiRowTestCaseSupplier.java
  42. 20 1
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MaxTests.java
  43. 20 1
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MinTests.java

+ 5 - 0
docs/changelog/110527.yaml

@@ -0,0 +1,5 @@
+pr: 110527
+summary: "ESQL: Add boolean support to Max and Min aggs"
+area: ES|QL
+type: feature
+issues: []

+ 1 - 1
docs/reference/esql/functions/description/max.asciidoc

@@ -2,4 +2,4 @@
 
 *Description*
 
-The maximum value of a numeric field.
+The maximum value of a field.

+ 1 - 1
docs/reference/esql/functions/description/min.asciidoc

@@ -2,4 +2,4 @@
 
 *Description*
 
-The minimum value of a numeric field.
+The minimum value of a field.

+ 17 - 5
docs/reference/esql/functions/kibana/definition/max.json

@@ -2,12 +2,24 @@
   "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
   "type" : "agg",
   "name" : "max",
-  "description" : "The maximum value of a numeric field.",
+  "description" : "The maximum value of a field.",
   "signatures" : [
     {
       "params" : [
         {
-          "name" : "number",
+          "name" : "field",
+          "type" : "boolean",
+          "optional" : false,
+          "description" : ""
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "boolean"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
           "type" : "datetime",
           "optional" : false,
           "description" : ""
@@ -19,7 +31,7 @@
     {
       "params" : [
         {
-          "name" : "number",
+          "name" : "field",
           "type" : "double",
           "optional" : false,
           "description" : ""
@@ -31,7 +43,7 @@
     {
       "params" : [
         {
-          "name" : "number",
+          "name" : "field",
           "type" : "integer",
           "optional" : false,
           "description" : ""
@@ -43,7 +55,7 @@
     {
       "params" : [
         {
-          "name" : "number",
+          "name" : "field",
           "type" : "long",
           "optional" : false,
           "description" : ""

+ 17 - 5
docs/reference/esql/functions/kibana/definition/min.json

@@ -2,12 +2,24 @@
   "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
   "type" : "agg",
   "name" : "min",
-  "description" : "The minimum value of a numeric field.",
+  "description" : "The minimum value of a field.",
   "signatures" : [
     {
       "params" : [
         {
-          "name" : "number",
+          "name" : "field",
+          "type" : "boolean",
+          "optional" : false,
+          "description" : ""
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "boolean"
+    },
+    {
+      "params" : [
+        {
+          "name" : "field",
           "type" : "datetime",
           "optional" : false,
           "description" : ""
@@ -19,7 +31,7 @@
     {
       "params" : [
         {
-          "name" : "number",
+          "name" : "field",
           "type" : "double",
           "optional" : false,
           "description" : ""
@@ -31,7 +43,7 @@
     {
       "params" : [
         {
-          "name" : "number",
+          "name" : "field",
           "type" : "integer",
           "optional" : false,
           "description" : ""
@@ -43,7 +55,7 @@
     {
       "params" : [
         {
-          "name" : "number",
+          "name" : "field",
           "type" : "long",
           "optional" : false,
           "description" : ""

+ 1 - 1
docs/reference/esql/functions/kibana/docs/max.md

@@ -3,7 +3,7 @@ This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../READ
 -->
 
 ### MAX
-The maximum value of a numeric field.
+The maximum value of a field.
 
 ```
 FROM employees

+ 1 - 1
docs/reference/esql/functions/kibana/docs/min.md

@@ -3,7 +3,7 @@ This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../READ
 -->
 
 ### MIN
-The minimum value of a numeric field.
+The minimum value of a field.
 
 ```
 FROM employees

+ 1 - 1
docs/reference/esql/functions/parameters/max.asciidoc

@@ -2,5 +2,5 @@
 
 *Parameters*
 
-`number`::
+`field`::
 

+ 1 - 1
docs/reference/esql/functions/parameters/min.asciidoc

@@ -2,5 +2,5 @@
 
 *Parameters*
 
-`number`::
+`field`::
 

+ 1 - 1
docs/reference/esql/functions/signature/max.svg

@@ -1 +1 @@
-<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="252" height="46" viewbox="0 0 252 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m56 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="56" height="36"/><text class="k" x="15" y="31">MAX</text><rect class="s" x="71" y="5" width="32" height="36" rx="7"/><text class="syn" x="81" y="31">(</text><rect class="s" x="113" y="5" width="92" height="36" rx="7"/><text class="k" x="123" y="31">number</text><rect class="s" x="215" y="5" width="32" height="36" rx="7"/><text class="syn" x="225" y="31">)</text></svg>
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="240" height="46" viewbox="0 0 240 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m56 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="56" height="36"/><text class="k" x="15" y="31">MAX</text><rect class="s" x="71" y="5" width="32" height="36" rx="7"/><text class="syn" x="81" y="31">(</text><rect class="s" x="113" y="5" width="80" height="36" rx="7"/><text class="k" x="123" y="31">field</text><rect class="s" x="203" y="5" width="32" height="36" rx="7"/><text class="syn" x="213" y="31">)</text></svg>

+ 1 - 1
docs/reference/esql/functions/signature/min.svg

@@ -1 +1 @@
-<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="252" height="46" viewbox="0 0 252 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m56 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="56" height="36"/><text class="k" x="15" y="31">MIN</text><rect class="s" x="71" y="5" width="32" height="36" rx="7"/><text class="syn" x="81" y="31">(</text><rect class="s" x="113" y="5" width="92" height="36" rx="7"/><text class="k" x="123" y="31">number</text><rect class="s" x="215" y="5" width="32" height="36" rx="7"/><text class="syn" x="225" y="31">)</text></svg>
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="240" height="46" viewbox="0 0 240 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m56 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="56" height="36"/><text class="k" x="15" y="31">MIN</text><rect class="s" x="71" y="5" width="32" height="36" rx="7"/><text class="syn" x="81" y="31">(</text><rect class="s" x="113" y="5" width="80" height="36" rx="7"/><text class="k" x="123" y="31">field</text><rect class="s" x="203" y="5" width="32" height="36" rx="7"/><text class="syn" x="213" y="31">)</text></svg>

+ 2 - 1
docs/reference/esql/functions/types/max.asciidoc

@@ -4,7 +4,8 @@
 
 [%header.monospaced.styled,format=dsv,separator=|]
 |===
-number | result
+field | result
+boolean | boolean
 datetime | datetime
 double | double
 integer | integer

+ 2 - 1
docs/reference/esql/functions/types/min.asciidoc

@@ -4,7 +4,8 @@
 
 [%header.monospaced.styled,format=dsv,separator=|]
 |===
-number | result
+field | result
+boolean | boolean
 datetime | datetime
 double | double
 integer | integer

+ 73 - 0
server/src/main/java/org/elasticsearch/common/util/BitArray.java

@@ -64,6 +64,17 @@ public final class BitArray implements Accountable, Releasable, Writeable {
         bits.writeTo(out);
     }
 
+    /**
+     * Set or clear the {@code index}th bit based on the specified value.
+     */
+    public void set(long index, boolean value) {
+        if (value) {
+            set(index);
+        } else {
+            clear(index);
+        }
+    }
+
     /**
      * Set the {@code index}th bit.
      */
@@ -158,6 +169,68 @@ public final class BitArray implements Accountable, Releasable, Writeable {
         return (bits.get(wordNum) & bitmask) != 0;
     }
 
+    /**
+     * Set or clear slots between {@code fromIndex} inclusive to {@code toIndex} based on {@code value}.
+     */
+    public void fill(long fromIndex, long toIndex, boolean value) {
+        if (fromIndex > toIndex) {
+            throw new IllegalArgumentException("From should be less than or equal to toIndex");
+        }
+        long currentSize = size();
+        if (value == false) {
+            // There's no need to grow the array just to clear bits.
+            toIndex = Math.min(toIndex, currentSize);
+        }
+        if (fromIndex == toIndex) {
+            return; // Empty range
+        }
+
+        if (toIndex > currentSize) {
+            bits = bigArrays.grow(bits, wordNum(toIndex) + 1);
+        }
+
+        int wordLength = Long.BYTES * Byte.SIZE;
+        long fullWord = 0xFFFFFFFFFFFFFFFFL;
+
+        long firstWordIndex = fromIndex % wordLength;
+        long lastWordIndex = toIndex % wordLength;
+
+        long firstWordNum = wordNum(fromIndex);
+        long lastWordNum = wordNum(toIndex - 1);
+
+        // Mask first word
+        if (firstWordIndex > 0) {
+            long mask = fullWord << firstWordIndex;
+
+            if (firstWordNum == lastWordNum) {
+                mask &= fullWord >>> (wordLength - lastWordIndex);
+            }
+
+            if (value) {
+                bits.set(firstWordNum, bits.get(firstWordNum) | mask);
+            } else {
+                bits.set(firstWordNum, bits.get(firstWordNum) & ~mask);
+            }
+
+            firstWordNum++;
+        }
+
+        // Mask last word
+        if (firstWordNum <= lastWordNum) {
+            long mask = fullWord >>> (wordLength - lastWordIndex);
+
+            if (value) {
+                bits.set(lastWordNum, bits.get(lastWordNum) | mask);
+            } else {
+                bits.set(lastWordNum, bits.get(lastWordNum) & ~mask);
+            }
+        }
+
+        if (firstWordNum < lastWordNum) {
+            bits.fill(firstWordNum, lastWordNum, value ? fullWord : 0L);
+        }
+    }
+
     public long size() {
         return bits.size() * (long) Long.BYTES * Byte.SIZE;
     }

+ 93 - 0
server/src/test/java/org/elasticsearch/common/util/BitArrayTests.java

@@ -51,6 +51,27 @@ public class BitArrayTests extends ESTestCase {
         }
     }
 
+    public void testRandomSetValue() {
+        try (BitArray bitArray = new BitArray(1, BigArrays.NON_RECYCLING_INSTANCE)) {
+            int numBits = randomIntBetween(1000, 10000);
+            for (int step = 0; step < 3; step++) {
+                boolean[] bits = new boolean[numBits];
+                List<Integer> slots = new ArrayList<>();
+                for (int i = 0; i < numBits; i++) {
+                    bits[i] = randomBoolean();
+                    slots.add(i);
+                }
+                Collections.shuffle(slots, random());
+                for (int i : slots) {
+                    bitArray.set(i, bits[i]);
+                }
+                for (int i = 0; i < numBits; i++) {
+                    assertEquals(bitArray.get(i), bits[i]);
+                }
+            }
+        }
+    }
+
     public void testVeryLarge() {
         assumeThat(Runtime.getRuntime().maxMemory(), greaterThanOrEqualTo(ByteSizeUnit.MB.toBytes(512)));
         try (BitArray bitArray = new BitArray(1, BigArrays.NON_RECYCLING_INSTANCE)) {
@@ -183,6 +204,78 @@ public class BitArrayTests extends ESTestCase {
         }
     }
 
+    public void testFillTrueRandom() {
+        try (BitArray bitArray = new BitArray(1, BigArrays.NON_RECYCLING_INSTANCE)) {
+            int from = randomIntBetween(0, 1000);
+            int to = randomIntBetween(from, 1000);
+
+            bitArray.fill(0, 1000, false);
+            bitArray.fill(from, to, true);
+
+            for (int i = 0; i < 1000; i++) {
+                if (i < from || i >= to) {
+                    assertFalse(bitArray.get(i));
+                } else {
+                    assertTrue(bitArray.get(i));
+                }
+            }
+        }
+    }
+
+    public void testFillFalseRandom() {
+        try (BitArray bitArray = new BitArray(1, BigArrays.NON_RECYCLING_INSTANCE)) {
+            int from = randomIntBetween(0, 1000);
+            int to = randomIntBetween(from, 1000);
+
+            bitArray.fill(0, 1000, true);
+            bitArray.fill(from, to, false);
+
+            for (int i = 0; i < 1000; i++) {
+                if (i < from || i >= to) {
+                    assertTrue(bitArray.get(i));
+                } else {
+                    assertFalse(bitArray.get(i));
+                }
+            }
+        }
+    }
+
+    public void testFillTrueSingleWord() {
+        try (BitArray bitArray = new BitArray(1, BigArrays.NON_RECYCLING_INSTANCE)) {
+            int from = 8;
+            int to = 56;
+
+            bitArray.fill(0, 64, false);
+            bitArray.fill(from, to, true);
+
+            for (int i = 0; i < 64; i++) {
+                if (i < from || i >= to) {
+                    assertFalse(bitArray.get(i));
+                } else {
+                    assertTrue(bitArray.get(i));
+                }
+            }
+        }
+    }
+
+    public void testFillFalseSingleWord() {
+        try (BitArray bitArray = new BitArray(1, BigArrays.NON_RECYCLING_INSTANCE)) {
+            int from = 8;
+            int to = 56;
+
+            bitArray.fill(0, 64, true);
+            bitArray.fill(from, to, false);
+
+            for (int i = 0; i < 64; i++) {
+                if (i < from || i >= to) {
+                    assertTrue(bitArray.get(i));
+                } else {
+                    assertFalse(bitArray.get(i));
+                }
+            }
+        }
+    }
+
     public void testSerialize() throws Exception {
         int initial = randomIntBetween(1, 100_000);
         BitArray bits1 = new BitArray(initial, BigArrays.NON_RECYCLING_INSTANCE);

+ 10 - 0
x-pack/plugin/esql/compute/build.gradle

@@ -400,6 +400,11 @@ tasks.named('stringTemplates').configure {
     it.outputFile = "org/elasticsearch/compute/data/BooleanVectorFixedBuilder.java"
   }
   File stateInputFile = new File("${projectDir}/src/main/java/org/elasticsearch/compute/aggregation/X-State.java.st")
+  template {
+    it.properties = booleanProperties
+    it.inputFile =  stateInputFile
+    it.outputFile = "org/elasticsearch/compute/aggregation/BooleanState.java"
+  }
   template {
     it.properties = intProperties
     it.inputFile =  stateInputFile
@@ -453,6 +458,11 @@ tasks.named('stringTemplates').configure {
     it.outputFile = "org/elasticsearch/compute/data/BooleanLookup.java"
   }
   File arrayStateInputFile = new File("${projectDir}/src/main/java/org/elasticsearch/compute/aggregation/X-ArrayState.java.st")
+  template {
+    it.properties = booleanProperties
+    it.inputFile =  arrayStateInputFile
+    it.outputFile = "org/elasticsearch/compute/aggregation/BooleanArrayState.java"
+  }
   template {
     it.properties = intProperties
     it.inputFile =  arrayStateInputFile

+ 8 - 2
x-pack/plugin/esql/compute/gen/src/main/java/org/elasticsearch/compute/gen/AggregatorImplementer.java

@@ -445,6 +445,8 @@ public class AggregatorImplementer {
 
     private String primitiveStateMethod() {
         switch (stateType.toString()) {
+            case "org.elasticsearch.compute.aggregation.BooleanState":
+                return "booleanValue";
             case "org.elasticsearch.compute.aggregation.IntState":
                 return "intValue";
             case "org.elasticsearch.compute.aggregation.LongState":
@@ -494,6 +496,9 @@ public class AggregatorImplementer {
 
     private void primitiveStateToResult(MethodSpec.Builder builder) {
         switch (stateType.toString()) {
+            case "org.elasticsearch.compute.aggregation.BooleanState":
+                builder.addStatement("blocks[offset] = driverContext.blockFactory().newConstantBooleanBlockWith(state.booleanValue(), 1)");
+                return;
             case "org.elasticsearch.compute.aggregation.IntState":
                 builder.addStatement("blocks[offset] = driverContext.blockFactory().newConstantIntBlockWith(state.intValue(), 1)");
                 return;
@@ -531,8 +536,9 @@ public class AggregatorImplementer {
 
     private boolean hasPrimitiveState() {
         return switch (stateType.toString()) {
-            case "org.elasticsearch.compute.aggregation.IntState", "org.elasticsearch.compute.aggregation.LongState",
-                "org.elasticsearch.compute.aggregation.DoubleState", "org.elasticsearch.compute.aggregation.FloatState" -> true;
+            case "org.elasticsearch.compute.aggregation.BooleanState", "org.elasticsearch.compute.aggregation.IntState",
+                "org.elasticsearch.compute.aggregation.LongState", "org.elasticsearch.compute.aggregation.DoubleState",
+                "org.elasticsearch.compute.aggregation.FloatState" -> true;
             default -> false;
         };
     }

+ 3 - 2
x-pack/plugin/esql/compute/gen/src/main/java/org/elasticsearch/compute/gen/GroupingAggregatorImplementer.java

@@ -584,8 +584,9 @@ public class GroupingAggregatorImplementer {
 
     private boolean hasPrimitiveState() {
         return switch (stateType.toString()) {
-            case "org.elasticsearch.compute.aggregation.IntArrayState", "org.elasticsearch.compute.aggregation.LongArrayState",
-                "org.elasticsearch.compute.aggregation.DoubleArrayState", "org.elasticsearch.compute.aggregation.FloatArrayState" -> true;
+            case "org.elasticsearch.compute.aggregation.BooleanArrayState", "org.elasticsearch.compute.aggregation.IntArrayState",
+                "org.elasticsearch.compute.aggregation.LongArrayState", "org.elasticsearch.compute.aggregation.DoubleArrayState",
+                "org.elasticsearch.compute.aggregation.FloatArrayState" -> true;
             default -> false;
         };
     }

+ 121 - 0
x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/aggregation/BooleanArrayState.java

@@ -0,0 +1,121 @@
+/*
+ * 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.compute.aggregation;
+
+import org.elasticsearch.common.util.BigArrays;
+import org.elasticsearch.common.util.BitArray;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.IntVector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.core.Releasables;
+
+/**
+ * Aggregator state for an array of booleans. It is created in a mode where it
+ * won't track the {@code groupId}s that are sent to it and it is the
+ * responsibility of the caller to only fetch values for {@code groupId}s
+ * that it has sent using the {@code selected} parameter when building the
+ * results. This is fine when there are no {@code null} values in the input
+ * data. But once there are null values in the input data it is
+ * <strong>much</strong> more convenient to only send non-null values and
+ * the tracking built into the grouping code can't track that. In that case
+ * call {@link #enableGroupIdTracking} to transition the state into a mode
+ * where it'll track which {@code groupIds} have been written.
+ * <p>
+ * This class is generated. Do not edit it.
+ * </p>
+ */
+final class BooleanArrayState extends AbstractArrayState implements GroupingAggregatorState {
+    private final boolean init;
+
+    private BitArray values;
+    private int size;
+
+    BooleanArrayState(BigArrays bigArrays, boolean init) {
+        super(bigArrays);
+        this.values = new BitArray(1, bigArrays);
+        this.size = 1;
+        this.values.set(0, init);
+        this.init = init;
+    }
+
+    boolean get(int groupId) {
+        return values.get(groupId);
+    }
+
+    boolean getOrDefault(int groupId) {
+        return groupId < values.size() ? values.get(groupId) : init;
+    }
+
+    void set(int groupId, boolean value) {
+        ensureCapacity(groupId);
+        values.set(groupId, value);
+        trackGroupId(groupId);
+    }
+
+    Block toValuesBlock(org.elasticsearch.compute.data.IntVector selected, DriverContext driverContext) {
+        if (false == trackingGroupIds()) {
+            try (var builder = driverContext.blockFactory().newBooleanVectorFixedBuilder(selected.getPositionCount())) {
+                for (int i = 0; i < selected.getPositionCount(); i++) {
+                    builder.appendBoolean(i, values.get(selected.getInt(i)));
+                }
+                return builder.build().asBlock();
+            }
+        }
+        try (BooleanBlock.Builder builder = driverContext.blockFactory().newBooleanBlockBuilder(selected.getPositionCount())) {
+            for (int i = 0; i < selected.getPositionCount(); i++) {
+                int group = selected.getInt(i);
+                if (hasValue(group)) {
+                    builder.appendBoolean(values.get(group));
+                } else {
+                    builder.appendNull();
+                }
+            }
+            return builder.build();
+        }
+    }
+
+    private void ensureCapacity(int groupId) {
+        if (groupId >= size) {
+            values.fill(size, groupId + 1, init);
+            size = groupId + 1;
+        }
+    }
+
+    /** Extracts an intermediate view of the contents of this state.  */
+    @Override
+    public void toIntermediate(
+        Block[] blocks,
+        int offset,
+        IntVector selected,
+        org.elasticsearch.compute.operator.DriverContext driverContext
+    ) {
+        assert blocks.length >= offset + 2;
+        try (
+            var valuesBuilder = driverContext.blockFactory().newBooleanBlockBuilder(selected.getPositionCount());
+            var hasValueBuilder = driverContext.blockFactory().newBooleanVectorFixedBuilder(selected.getPositionCount())
+        ) {
+            for (int i = 0; i < selected.getPositionCount(); i++) {
+                int group = selected.getInt(i);
+                if (group < values.size()) {
+                    valuesBuilder.appendBoolean(values.get(group));
+                } else {
+                    valuesBuilder.appendBoolean(false); // TODO can we just use null?
+                }
+                hasValueBuilder.appendBoolean(i, hasValue(group));
+            }
+            blocks[offset + 0] = valuesBuilder.build();
+            blocks[offset + 1] = hasValueBuilder.build().asBlock();
+        }
+    }
+
+    @Override
+    public void close() {
+        Releasables.close(values, super::close);
+    }
+}

+ 55 - 0
x-pack/plugin/esql/compute/src/main/generated-src/org/elasticsearch/compute/aggregation/BooleanState.java

@@ -0,0 +1,55 @@
+/*
+ * 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.compute.aggregation;
+
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.operator.DriverContext;
+
+/**
+ * Aggregator state for a single boolean.
+ * This class is generated. Do not edit it.
+ */
+final class BooleanState implements AggregatorState {
+    private boolean value;
+    private boolean seen;
+
+    BooleanState() {
+        this(false);
+    }
+
+    BooleanState(boolean init) {
+        this.value = init;
+    }
+
+    boolean booleanValue() {
+        return value;
+    }
+
+    void booleanValue(boolean value) {
+        this.value = value;
+    }
+
+    boolean seen() {
+        return seen;
+    }
+
+    void seen(boolean seen) {
+        this.seen = seen;
+    }
+
+    /** Extracts an intermediate view of the contents of this state.  */
+    @Override
+    public void toIntermediate(Block[] blocks, int offset, DriverContext driverContext) {
+        assert blocks.length >= offset + 2;
+        blocks[offset + 0] = driverContext.blockFactory().newConstantBooleanBlockWith(value, 1);
+        blocks[offset + 1] = driverContext.blockFactory().newConstantBooleanBlockWith(seen, 1);
+    }
+
+    @Override
+    public void close() {}
+}

+ 136 - 0
x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MaxBooleanAggregatorFunction.java

@@ -0,0 +1,136 @@
+// 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.compute.aggregation;
+
+import java.lang.Integer;
+import java.lang.Override;
+import java.lang.String;
+import java.lang.StringBuilder;
+import java.util.List;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BooleanVector;
+import org.elasticsearch.compute.data.ElementType;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+
+/**
+ * {@link AggregatorFunction} implementation for {@link MaxBooleanAggregator}.
+ * This class is generated. Do not edit it.
+ */
+public final class MaxBooleanAggregatorFunction implements AggregatorFunction {
+  private static final List<IntermediateStateDesc> INTERMEDIATE_STATE_DESC = List.of(
+      new IntermediateStateDesc("max", ElementType.BOOLEAN),
+      new IntermediateStateDesc("seen", ElementType.BOOLEAN)  );
+
+  private final DriverContext driverContext;
+
+  private final BooleanState state;
+
+  private final List<Integer> channels;
+
+  public MaxBooleanAggregatorFunction(DriverContext driverContext, List<Integer> channels,
+      BooleanState state) {
+    this.driverContext = driverContext;
+    this.channels = channels;
+    this.state = state;
+  }
+
+  public static MaxBooleanAggregatorFunction create(DriverContext driverContext,
+      List<Integer> channels) {
+    return new MaxBooleanAggregatorFunction(driverContext, channels, new BooleanState(MaxBooleanAggregator.init()));
+  }
+
+  public static List<IntermediateStateDesc> intermediateStateDesc() {
+    return INTERMEDIATE_STATE_DESC;
+  }
+
+  @Override
+  public int intermediateBlockCount() {
+    return INTERMEDIATE_STATE_DESC.size();
+  }
+
+  @Override
+  public void addRawInput(Page page) {
+    BooleanBlock block = page.getBlock(channels.get(0));
+    BooleanVector vector = block.asVector();
+    if (vector != null) {
+      addRawVector(vector);
+    } else {
+      addRawBlock(block);
+    }
+  }
+
+  private void addRawVector(BooleanVector vector) {
+    state.seen(true);
+    for (int i = 0; i < vector.getPositionCount(); i++) {
+      state.booleanValue(MaxBooleanAggregator.combine(state.booleanValue(), vector.getBoolean(i)));
+    }
+  }
+
+  private void addRawBlock(BooleanBlock block) {
+    for (int p = 0; p < block.getPositionCount(); p++) {
+      if (block.isNull(p)) {
+        continue;
+      }
+      state.seen(true);
+      int start = block.getFirstValueIndex(p);
+      int end = start + block.getValueCount(p);
+      for (int i = start; i < end; i++) {
+        state.booleanValue(MaxBooleanAggregator.combine(state.booleanValue(), block.getBoolean(i)));
+      }
+    }
+  }
+
+  @Override
+  public void addIntermediateInput(Page page) {
+    assert channels.size() == intermediateBlockCount();
+    assert page.getBlockCount() >= channels.get(0) + intermediateStateDesc().size();
+    Block maxUncast = page.getBlock(channels.get(0));
+    if (maxUncast.areAllValuesNull()) {
+      return;
+    }
+    BooleanVector max = ((BooleanBlock) maxUncast).asVector();
+    assert max.getPositionCount() == 1;
+    Block seenUncast = page.getBlock(channels.get(1));
+    if (seenUncast.areAllValuesNull()) {
+      return;
+    }
+    BooleanVector seen = ((BooleanBlock) seenUncast).asVector();
+    assert seen.getPositionCount() == 1;
+    if (seen.getBoolean(0)) {
+      state.booleanValue(MaxBooleanAggregator.combine(state.booleanValue(), max.getBoolean(0)));
+      state.seen(true);
+    }
+  }
+
+  @Override
+  public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) {
+    state.toIntermediate(blocks, offset, driverContext);
+  }
+
+  @Override
+  public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) {
+    if (state.seen() == false) {
+      blocks[offset] = driverContext.blockFactory().newConstantNullBlock(1);
+      return;
+    }
+    blocks[offset] = driverContext.blockFactory().newConstantBooleanBlockWith(state.booleanValue(), 1);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(getClass().getSimpleName()).append("[");
+    sb.append("channels=").append(channels);
+    sb.append("]");
+    return sb.toString();
+  }
+
+  @Override
+  public void close() {
+    state.close();
+  }
+}

+ 38 - 0
x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MaxBooleanAggregatorFunctionSupplier.java

@@ -0,0 +1,38 @@
+// 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.compute.aggregation;
+
+import java.lang.Integer;
+import java.lang.Override;
+import java.lang.String;
+import java.util.List;
+import org.elasticsearch.compute.operator.DriverContext;
+
+/**
+ * {@link AggregatorFunctionSupplier} implementation for {@link MaxBooleanAggregator}.
+ * This class is generated. Do not edit it.
+ */
+public final class MaxBooleanAggregatorFunctionSupplier implements AggregatorFunctionSupplier {
+  private final List<Integer> channels;
+
+  public MaxBooleanAggregatorFunctionSupplier(List<Integer> channels) {
+    this.channels = channels;
+  }
+
+  @Override
+  public MaxBooleanAggregatorFunction aggregator(DriverContext driverContext) {
+    return MaxBooleanAggregatorFunction.create(driverContext, channels);
+  }
+
+  @Override
+  public MaxBooleanGroupingAggregatorFunction groupingAggregator(DriverContext driverContext) {
+    return MaxBooleanGroupingAggregatorFunction.create(channels, driverContext);
+  }
+
+  @Override
+  public String describe() {
+    return "max of booleans";
+  }
+}

+ 204 - 0
x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MaxBooleanGroupingAggregatorFunction.java

@@ -0,0 +1,204 @@
+// 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.compute.aggregation;
+
+import java.lang.Integer;
+import java.lang.Override;
+import java.lang.String;
+import java.lang.StringBuilder;
+import java.util.List;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BooleanVector;
+import org.elasticsearch.compute.data.ElementType;
+import org.elasticsearch.compute.data.IntBlock;
+import org.elasticsearch.compute.data.IntVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+
+/**
+ * {@link GroupingAggregatorFunction} implementation for {@link MaxBooleanAggregator}.
+ * This class is generated. Do not edit it.
+ */
+public final class MaxBooleanGroupingAggregatorFunction implements GroupingAggregatorFunction {
+  private static final List<IntermediateStateDesc> INTERMEDIATE_STATE_DESC = List.of(
+      new IntermediateStateDesc("max", ElementType.BOOLEAN),
+      new IntermediateStateDesc("seen", ElementType.BOOLEAN)  );
+
+  private final BooleanArrayState state;
+
+  private final List<Integer> channels;
+
+  private final DriverContext driverContext;
+
+  public MaxBooleanGroupingAggregatorFunction(List<Integer> channels, BooleanArrayState state,
+      DriverContext driverContext) {
+    this.channels = channels;
+    this.state = state;
+    this.driverContext = driverContext;
+  }
+
+  public static MaxBooleanGroupingAggregatorFunction create(List<Integer> channels,
+      DriverContext driverContext) {
+    return new MaxBooleanGroupingAggregatorFunction(channels, new BooleanArrayState(driverContext.bigArrays(), MaxBooleanAggregator.init()), driverContext);
+  }
+
+  public static List<IntermediateStateDesc> intermediateStateDesc() {
+    return INTERMEDIATE_STATE_DESC;
+  }
+
+  @Override
+  public int intermediateBlockCount() {
+    return INTERMEDIATE_STATE_DESC.size();
+  }
+
+  @Override
+  public GroupingAggregatorFunction.AddInput prepareProcessPage(SeenGroupIds seenGroupIds,
+      Page page) {
+    BooleanBlock valuesBlock = page.getBlock(channels.get(0));
+    BooleanVector valuesVector = valuesBlock.asVector();
+    if (valuesVector == null) {
+      if (valuesBlock.mayHaveNulls()) {
+        state.enableGroupIdTracking(seenGroupIds);
+      }
+      return new GroupingAggregatorFunction.AddInput() {
+        @Override
+        public void add(int positionOffset, IntBlock groupIds) {
+          addRawInput(positionOffset, groupIds, valuesBlock);
+        }
+
+        @Override
+        public void add(int positionOffset, IntVector groupIds) {
+          addRawInput(positionOffset, groupIds, valuesBlock);
+        }
+      };
+    }
+    return new GroupingAggregatorFunction.AddInput() {
+      @Override
+      public void add(int positionOffset, IntBlock groupIds) {
+        addRawInput(positionOffset, groupIds, valuesVector);
+      }
+
+      @Override
+      public void add(int positionOffset, IntVector groupIds) {
+        addRawInput(positionOffset, groupIds, valuesVector);
+      }
+    };
+  }
+
+  private void addRawInput(int positionOffset, IntVector groups, BooleanBlock values) {
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      int groupId = Math.toIntExact(groups.getInt(groupPosition));
+      if (values.isNull(groupPosition + positionOffset)) {
+        continue;
+      }
+      int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset);
+      int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset);
+      for (int v = valuesStart; v < valuesEnd; v++) {
+        state.set(groupId, MaxBooleanAggregator.combine(state.getOrDefault(groupId), values.getBoolean(v)));
+      }
+    }
+  }
+
+  private void addRawInput(int positionOffset, IntVector groups, BooleanVector values) {
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      int groupId = Math.toIntExact(groups.getInt(groupPosition));
+      state.set(groupId, MaxBooleanAggregator.combine(state.getOrDefault(groupId), values.getBoolean(groupPosition + positionOffset)));
+    }
+  }
+
+  private void addRawInput(int positionOffset, IntBlock groups, BooleanBlock values) {
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      if (groups.isNull(groupPosition)) {
+        continue;
+      }
+      int groupStart = groups.getFirstValueIndex(groupPosition);
+      int groupEnd = groupStart + groups.getValueCount(groupPosition);
+      for (int g = groupStart; g < groupEnd; g++) {
+        int groupId = Math.toIntExact(groups.getInt(g));
+        if (values.isNull(groupPosition + positionOffset)) {
+          continue;
+        }
+        int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset);
+        int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset);
+        for (int v = valuesStart; v < valuesEnd; v++) {
+          state.set(groupId, MaxBooleanAggregator.combine(state.getOrDefault(groupId), values.getBoolean(v)));
+        }
+      }
+    }
+  }
+
+  private void addRawInput(int positionOffset, IntBlock groups, BooleanVector values) {
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      if (groups.isNull(groupPosition)) {
+        continue;
+      }
+      int groupStart = groups.getFirstValueIndex(groupPosition);
+      int groupEnd = groupStart + groups.getValueCount(groupPosition);
+      for (int g = groupStart; g < groupEnd; g++) {
+        int groupId = Math.toIntExact(groups.getInt(g));
+        state.set(groupId, MaxBooleanAggregator.combine(state.getOrDefault(groupId), values.getBoolean(groupPosition + positionOffset)));
+      }
+    }
+  }
+
+  @Override
+  public void addIntermediateInput(int positionOffset, IntVector groups, Page page) {
+    state.enableGroupIdTracking(new SeenGroupIds.Empty());
+    assert channels.size() == intermediateBlockCount();
+    Block maxUncast = page.getBlock(channels.get(0));
+    if (maxUncast.areAllValuesNull()) {
+      return;
+    }
+    BooleanVector max = ((BooleanBlock) maxUncast).asVector();
+    Block seenUncast = page.getBlock(channels.get(1));
+    if (seenUncast.areAllValuesNull()) {
+      return;
+    }
+    BooleanVector seen = ((BooleanBlock) seenUncast).asVector();
+    assert max.getPositionCount() == seen.getPositionCount();
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      int groupId = Math.toIntExact(groups.getInt(groupPosition));
+      MaxBooleanAggregator.combineIntermediate(state, groupId, max.getBoolean(groupPosition + positionOffset), seen.getBoolean(groupPosition + positionOffset));
+    }
+  }
+
+  @Override
+  public void addIntermediateRowInput(int groupId, GroupingAggregatorFunction input, int position) {
+    if (input.getClass() != getClass()) {
+      throw new IllegalArgumentException("expected " + getClass() + "; got " + input.getClass());
+    }
+    BooleanArrayState inState = ((MaxBooleanGroupingAggregatorFunction) input).state;
+    state.enableGroupIdTracking(new SeenGroupIds.Empty());
+    if (inState.hasValue(position)) {
+      state.set(groupId, MaxBooleanAggregator.combine(state.getOrDefault(groupId), inState.get(position)));
+    }
+  }
+
+  @Override
+  public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) {
+    state.toIntermediate(blocks, offset, selected, driverContext);
+  }
+
+  @Override
+  public void evaluateFinal(Block[] blocks, int offset, IntVector selected,
+      DriverContext driverContext) {
+    blocks[offset] = state.toValuesBlock(selected, driverContext);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(getClass().getSimpleName()).append("[");
+    sb.append("channels=").append(channels);
+    sb.append("]");
+    return sb.toString();
+  }
+
+  @Override
+  public void close() {
+    state.close();
+  }
+}

+ 136 - 0
x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MinBooleanAggregatorFunction.java

@@ -0,0 +1,136 @@
+// 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.compute.aggregation;
+
+import java.lang.Integer;
+import java.lang.Override;
+import java.lang.String;
+import java.lang.StringBuilder;
+import java.util.List;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BooleanVector;
+import org.elasticsearch.compute.data.ElementType;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+
+/**
+ * {@link AggregatorFunction} implementation for {@link MinBooleanAggregator}.
+ * This class is generated. Do not edit it.
+ */
+public final class MinBooleanAggregatorFunction implements AggregatorFunction {
+  private static final List<IntermediateStateDesc> INTERMEDIATE_STATE_DESC = List.of(
+      new IntermediateStateDesc("min", ElementType.BOOLEAN),
+      new IntermediateStateDesc("seen", ElementType.BOOLEAN)  );
+
+  private final DriverContext driverContext;
+
+  private final BooleanState state;
+
+  private final List<Integer> channels;
+
+  public MinBooleanAggregatorFunction(DriverContext driverContext, List<Integer> channels,
+      BooleanState state) {
+    this.driverContext = driverContext;
+    this.channels = channels;
+    this.state = state;
+  }
+
+  public static MinBooleanAggregatorFunction create(DriverContext driverContext,
+      List<Integer> channels) {
+    return new MinBooleanAggregatorFunction(driverContext, channels, new BooleanState(MinBooleanAggregator.init()));
+  }
+
+  public static List<IntermediateStateDesc> intermediateStateDesc() {
+    return INTERMEDIATE_STATE_DESC;
+  }
+
+  @Override
+  public int intermediateBlockCount() {
+    return INTERMEDIATE_STATE_DESC.size();
+  }
+
+  @Override
+  public void addRawInput(Page page) {
+    BooleanBlock block = page.getBlock(channels.get(0));
+    BooleanVector vector = block.asVector();
+    if (vector != null) {
+      addRawVector(vector);
+    } else {
+      addRawBlock(block);
+    }
+  }
+
+  private void addRawVector(BooleanVector vector) {
+    state.seen(true);
+    for (int i = 0; i < vector.getPositionCount(); i++) {
+      state.booleanValue(MinBooleanAggregator.combine(state.booleanValue(), vector.getBoolean(i)));
+    }
+  }
+
+  private void addRawBlock(BooleanBlock block) {
+    for (int p = 0; p < block.getPositionCount(); p++) {
+      if (block.isNull(p)) {
+        continue;
+      }
+      state.seen(true);
+      int start = block.getFirstValueIndex(p);
+      int end = start + block.getValueCount(p);
+      for (int i = start; i < end; i++) {
+        state.booleanValue(MinBooleanAggregator.combine(state.booleanValue(), block.getBoolean(i)));
+      }
+    }
+  }
+
+  @Override
+  public void addIntermediateInput(Page page) {
+    assert channels.size() == intermediateBlockCount();
+    assert page.getBlockCount() >= channels.get(0) + intermediateStateDesc().size();
+    Block minUncast = page.getBlock(channels.get(0));
+    if (minUncast.areAllValuesNull()) {
+      return;
+    }
+    BooleanVector min = ((BooleanBlock) minUncast).asVector();
+    assert min.getPositionCount() == 1;
+    Block seenUncast = page.getBlock(channels.get(1));
+    if (seenUncast.areAllValuesNull()) {
+      return;
+    }
+    BooleanVector seen = ((BooleanBlock) seenUncast).asVector();
+    assert seen.getPositionCount() == 1;
+    if (seen.getBoolean(0)) {
+      state.booleanValue(MinBooleanAggregator.combine(state.booleanValue(), min.getBoolean(0)));
+      state.seen(true);
+    }
+  }
+
+  @Override
+  public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) {
+    state.toIntermediate(blocks, offset, driverContext);
+  }
+
+  @Override
+  public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) {
+    if (state.seen() == false) {
+      blocks[offset] = driverContext.blockFactory().newConstantNullBlock(1);
+      return;
+    }
+    blocks[offset] = driverContext.blockFactory().newConstantBooleanBlockWith(state.booleanValue(), 1);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(getClass().getSimpleName()).append("[");
+    sb.append("channels=").append(channels);
+    sb.append("]");
+    return sb.toString();
+  }
+
+  @Override
+  public void close() {
+    state.close();
+  }
+}

+ 38 - 0
x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MinBooleanAggregatorFunctionSupplier.java

@@ -0,0 +1,38 @@
+// 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.compute.aggregation;
+
+import java.lang.Integer;
+import java.lang.Override;
+import java.lang.String;
+import java.util.List;
+import org.elasticsearch.compute.operator.DriverContext;
+
+/**
+ * {@link AggregatorFunctionSupplier} implementation for {@link MinBooleanAggregator}.
+ * This class is generated. Do not edit it.
+ */
+public final class MinBooleanAggregatorFunctionSupplier implements AggregatorFunctionSupplier {
+  private final List<Integer> channels;
+
+  public MinBooleanAggregatorFunctionSupplier(List<Integer> channels) {
+    this.channels = channels;
+  }
+
+  @Override
+  public MinBooleanAggregatorFunction aggregator(DriverContext driverContext) {
+    return MinBooleanAggregatorFunction.create(driverContext, channels);
+  }
+
+  @Override
+  public MinBooleanGroupingAggregatorFunction groupingAggregator(DriverContext driverContext) {
+    return MinBooleanGroupingAggregatorFunction.create(channels, driverContext);
+  }
+
+  @Override
+  public String describe() {
+    return "min of booleans";
+  }
+}

+ 206 - 0
x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/MinBooleanGroupingAggregatorFunction.java

@@ -0,0 +1,206 @@
+// 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.compute.aggregation;
+
+import java.lang.Integer;
+import java.lang.Override;
+import java.lang.String;
+import java.lang.StringBuilder;
+import java.util.List;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BooleanVector;
+import org.elasticsearch.compute.data.ElementType;
+import org.elasticsearch.compute.data.IntBlock;
+import org.elasticsearch.compute.data.IntVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+
+/**
+ * {@link GroupingAggregatorFunction} implementation for {@link MinBooleanAggregator}.
+ * This class is generated. Do not edit it.
+ */
+public final class MinBooleanGroupingAggregatorFunction implements GroupingAggregatorFunction {
+  private static final List<IntermediateStateDesc> INTERMEDIATE_STATE_DESC = List.of(
+      new IntermediateStateDesc("min", ElementType.BOOLEAN),
+      new IntermediateStateDesc("seen", ElementType.BOOLEAN)  );
+
+  private final BooleanArrayState state;
+
+  private final List<Integer> channels;
+
+  private final DriverContext driverContext;
+
+  public MinBooleanGroupingAggregatorFunction(List<Integer> channels, BooleanArrayState state,
+      DriverContext driverContext) {
+    this.channels = channels;
+    this.state = state;
+    this.driverContext = driverContext;
+  }
+
+  public static MinBooleanGroupingAggregatorFunction create(List<Integer> channels,
+      DriverContext driverContext) {
+    return new MinBooleanGroupingAggregatorFunction(channels, new BooleanArrayState(driverContext.bigArrays(), MinBooleanAggregator.init()), driverContext);
+  }
+
+  public static List<IntermediateStateDesc> intermediateStateDesc() {
+    return INTERMEDIATE_STATE_DESC;
+  }
+
+  @Override
+  public int intermediateBlockCount() {
+    return INTERMEDIATE_STATE_DESC.size();
+  }
+
+  @Override
+  public GroupingAggregatorFunction.AddInput prepareProcessPage(SeenGroupIds seenGroupIds,
+      Page page) {
+    BooleanBlock valuesBlock = page.getBlock(channels.get(0));
+    BooleanVector valuesVector = valuesBlock.asVector();
+    if (valuesVector == null) {
+      if (valuesBlock.mayHaveNulls()) {
+        state.enableGroupIdTracking(seenGroupIds);
+      }
+      return new GroupingAggregatorFunction.AddInput() {
+        @Override
+        public void add(int positionOffset, IntBlock groupIds) {
+          addRawInput(positionOffset, groupIds, valuesBlock);
+        }
+
+        @Override
+        public void add(int positionOffset, IntVector groupIds) {
+          addRawInput(positionOffset, groupIds, valuesBlock);
+        }
+      };
+    }
+    return new GroupingAggregatorFunction.AddInput() {
+      @Override
+      public void add(int positionOffset, IntBlock groupIds) {
+        addRawInput(positionOffset, groupIds, valuesVector);
+      }
+
+      @Override
+      public void add(int positionOffset, IntVector groupIds) {
+        addRawInput(positionOffset, groupIds, valuesVector);
+      }
+    };
+  }
+
+  private void addRawInput(int positionOffset, IntVector groups, BooleanBlock values) {
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      int groupId = Math.toIntExact(groups.getInt(groupPosition));
+      if (values.isNull(groupPosition + positionOffset)) {
+        continue;
+      }
+      int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset);
+      int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset);
+      for (int v = valuesStart; v < valuesEnd; v++) {
+        state.set(groupId, MinBooleanAggregator.combine(state.getOrDefault(groupId), values.getBoolean(v)));
+      }
+    }
+  }
+
+  private void addRawInput(int positionOffset, IntVector groups, BooleanVector values) {
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      int groupId = Math.toIntExact(groups.getInt(groupPosition));
+      state.set(groupId, MinBooleanAggregator.combine(state.getOrDefault(groupId), values.getBoolean(groupPosition + positionOffset)));
+    }
+  }
+
+  private void addRawInput(int positionOffset, IntBlock groups, BooleanBlock values) {
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      if (groups.isNull(groupPosition)) {
+        continue;
+      }
+      int groupStart = groups.getFirstValueIndex(groupPosition);
+      int groupEnd = groupStart + groups.getValueCount(groupPosition);
+      for (int g = groupStart; g < groupEnd; g++) {
+        int groupId = Math.toIntExact(groups.getInt(g));
+        if (values.isNull(groupPosition + positionOffset)) {
+          continue;
+        }
+        int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset);
+        int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset);
+        for (int v = valuesStart; v < valuesEnd; v++) {
+          state.set(groupId, MinBooleanAggregator.combine(state.getOrDefault(groupId), values.getBoolean(v)));
+        }
+      }
+    }
+  }
+
+  private void addRawInput(int positionOffset, IntBlock groups, BooleanVector values) {
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      if (groups.isNull(groupPosition)) {
+        continue;
+      }
+      int groupStart = groups.getFirstValueIndex(groupPosition);
+      int groupEnd = groupStart + groups.getValueCount(groupPosition);
+      for (int g = groupStart; g < groupEnd; g++) {
+        int groupId = Math.toIntExact(groups.getInt(g));
+        state.set(groupId, MinBooleanAggregator.combine(state.getOrDefault(groupId), values.getBoolean(groupPosition + positionOffset)));
+      }
+    }
+  }
+
+  @Override
+  public void addIntermediateInput(int positionOffset, IntVector groups, Page page) {
+    state.enableGroupIdTracking(new SeenGroupIds.Empty());
+    assert channels.size() == intermediateBlockCount();
+    Block minUncast = page.getBlock(channels.get(0));
+    if (minUncast.areAllValuesNull()) {
+      return;
+    }
+    BooleanVector min = ((BooleanBlock) minUncast).asVector();
+    Block seenUncast = page.getBlock(channels.get(1));
+    if (seenUncast.areAllValuesNull()) {
+      return;
+    }
+    BooleanVector seen = ((BooleanBlock) seenUncast).asVector();
+    assert min.getPositionCount() == seen.getPositionCount();
+    for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) {
+      int groupId = Math.toIntExact(groups.getInt(groupPosition));
+      if (seen.getBoolean(groupPosition + positionOffset)) {
+        state.set(groupId, MinBooleanAggregator.combine(state.getOrDefault(groupId), min.getBoolean(groupPosition + positionOffset)));
+      }
+    }
+  }
+
+  @Override
+  public void addIntermediateRowInput(int groupId, GroupingAggregatorFunction input, int position) {
+    if (input.getClass() != getClass()) {
+      throw new IllegalArgumentException("expected " + getClass() + "; got " + input.getClass());
+    }
+    BooleanArrayState inState = ((MinBooleanGroupingAggregatorFunction) input).state;
+    state.enableGroupIdTracking(new SeenGroupIds.Empty());
+    if (inState.hasValue(position)) {
+      state.set(groupId, MinBooleanAggregator.combine(state.getOrDefault(groupId), inState.get(position)));
+    }
+  }
+
+  @Override
+  public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) {
+    state.toIntermediate(blocks, offset, selected, driverContext);
+  }
+
+  @Override
+  public void evaluateFinal(Block[] blocks, int offset, IntVector selected,
+      DriverContext driverContext) {
+    blocks[offset] = state.toValuesBlock(selected, driverContext);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(getClass().getSimpleName()).append("[");
+    sb.append("channels=").append(channels);
+    sb.append("]");
+    return sb.toString();
+  }
+
+  @Override
+  public void close() {
+    state.close();
+  }
+}

+ 25 - 0
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/MaxBooleanAggregator.java

@@ -0,0 +1,25 @@
+/*
+ * 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.compute.aggregation;
+
+import org.elasticsearch.compute.ann.Aggregator;
+import org.elasticsearch.compute.ann.GroupingAggregator;
+import org.elasticsearch.compute.ann.IntermediateState;
+
+@Aggregator({ @IntermediateState(name = "max", type = "BOOLEAN"), @IntermediateState(name = "seen", type = "BOOLEAN") })
+@GroupingAggregator
+class MaxBooleanAggregator {
+
+    public static boolean init() {
+        return false;
+    }
+
+    public static boolean combine(boolean current, boolean v) {
+        return current || v;
+    }
+}

+ 25 - 0
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/MinBooleanAggregator.java

@@ -0,0 +1,25 @@
+/*
+ * 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.compute.aggregation;
+
+import org.elasticsearch.compute.ann.Aggregator;
+import org.elasticsearch.compute.ann.GroupingAggregator;
+import org.elasticsearch.compute.ann.IntermediateState;
+
+@Aggregator({ @IntermediateState(name = "min", type = "BOOLEAN"), @IntermediateState(name = "seen", type = "BOOLEAN") })
+@GroupingAggregator
+class MinBooleanAggregator {
+
+    public static boolean init() {
+        return true;
+    }
+
+    public static boolean combine(boolean current, boolean v) {
+        return current && v;
+    }
+}

+ 24 - 2
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/X-ArrayState.java.st

@@ -8,7 +8,11 @@
 package org.elasticsearch.compute.aggregation;
 
 import org.elasticsearch.common.util.BigArrays;
+$if(boolean)$
+import org.elasticsearch.common.util.BitArray;
+$else$
 import org.elasticsearch.common.util.$Type$Array;
+$endif$
 import org.elasticsearch.compute.data.Block;
 $if(long)$
 import org.elasticsearch.compute.data.IntVector;
@@ -17,7 +21,7 @@ import org.elasticsearch.compute.data.$Type$Block;
 $if(int)$
 import org.elasticsearch.compute.data.$Type$Vector;
 $endif$
-$if(double||float)$
+$if(boolean||double||float)$
 import org.elasticsearch.compute.data.IntVector;
 $endif$
 import org.elasticsearch.compute.operator.DriverContext;
@@ -41,11 +45,22 @@ import org.elasticsearch.core.Releasables;
 final class $Type$ArrayState extends AbstractArrayState implements GroupingAggregatorState {
     private final $type$ init;
 
+$if(boolean)$
+    private BitArray values;
+    private int size;
+
+$else$
     private $Type$Array values;
+$endif$
 
     $Type$ArrayState(BigArrays bigArrays, $type$ init) {
         super(bigArrays);
+$if(boolean)$
+        this.values = new BitArray(1, bigArrays);
+        this.size = 1;
+$else$
         this.values = bigArrays.new$Type$Array(1, false);
+$endif$
         this.values.set(0, init);
         this.init = init;
     }
@@ -95,11 +110,18 @@ $endif$
     }
 
     private void ensureCapacity(int groupId) {
+$if(boolean)$
+        if (groupId >= size) {
+            values.fill(size, groupId + 1, init);
+            size = groupId + 1;
+        }
+$else$
         if (groupId >= values.size()) {
             long prevSize = values.size();
             values = bigArrays.grow(values, groupId + 1);
             values.fill(prevSize, values.size(), init);
         }
+$endif$
     }
 
     /** Extracts an intermediate view of the contents of this state.  */
@@ -120,7 +142,7 @@ $endif$
                 if (group < values.size()) {
                     valuesBuilder.append$Type$(values.get(group));
                 } else {
-                    valuesBuilder.append$Type$(0); // TODO can we just use null?
+                    valuesBuilder.append$Type$($if(boolean)$false$else$0$endif$); // TODO can we just use null?
                 }
                 hasValueBuilder.appendBoolean(i, hasValue(group));
             }

+ 4 - 0
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/X-State.java.st

@@ -19,7 +19,11 @@ final class $Type$State implements AggregatorState {
     private boolean seen;
 
     $Type$State() {
+$if(boolean)$
+        this(false);
+$else$
         this(0);
+$endif$
     }
 
     $Type$State($type$ init) {

+ 43 - 0
x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MaxBooleanAggregatorFunctionTests.java

@@ -0,0 +1,43 @@
+/*
+ * 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.compute.aggregation;
+
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BlockFactory;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.operator.SequenceBooleanBlockSourceOperator;
+import org.elasticsearch.compute.operator.SourceOperator;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.IntStream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class MaxBooleanAggregatorFunctionTests extends AggregatorFunctionTestCase {
+    @Override
+    protected SourceOperator simpleInput(BlockFactory blockFactory, int size) {
+        return new SequenceBooleanBlockSourceOperator(blockFactory, IntStream.range(0, size).mapToObj(l -> randomBoolean()).toList());
+    }
+
+    @Override
+    protected AggregatorFunctionSupplier aggregatorFunction(List<Integer> inputChannels) {
+        return new MaxBooleanAggregatorFunctionSupplier(inputChannels);
+    }
+
+    @Override
+    protected String expectedDescriptionOfAggregator() {
+        return "max of booleans";
+    }
+
+    @Override
+    public void assertSimpleOutput(List<Block> input, Block result) {
+        Boolean max = input.stream().flatMap(b -> allBooleans(b)).max(Comparator.naturalOrder()).get();
+        assertThat(((BooleanBlock) result).getBoolean(0), equalTo(max));
+    }
+}

+ 43 - 0
x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MinBooleanAggregatorFunctionTests.java

@@ -0,0 +1,43 @@
+/*
+ * 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.compute.aggregation;
+
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BlockFactory;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.operator.SequenceBooleanBlockSourceOperator;
+import org.elasticsearch.compute.operator.SourceOperator;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.IntStream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class MinBooleanAggregatorFunctionTests extends AggregatorFunctionTestCase {
+    @Override
+    protected SourceOperator simpleInput(BlockFactory blockFactory, int size) {
+        return new SequenceBooleanBlockSourceOperator(blockFactory, IntStream.range(0, size).mapToObj(l -> randomBoolean()).toList());
+    }
+
+    @Override
+    protected AggregatorFunctionSupplier aggregatorFunction(List<Integer> inputChannels) {
+        return new MinBooleanAggregatorFunctionSupplier(inputChannels);
+    }
+
+    @Override
+    protected String expectedDescriptionOfAggregator() {
+        return "min of booleans";
+    }
+
+    @Override
+    public void assertSimpleOutput(List<Block> input, Block result) {
+        Boolean min = input.stream().flatMap(b -> allBooleans(b)).min(Comparator.naturalOrder()).get();
+        assertThat(((BooleanBlock) result).getBoolean(0), equalTo(min));
+    }
+}

+ 14 - 16
x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec

@@ -1,4 +1,4 @@
-metaFunctionsSynopsis#[skip:-8.14.99]
+metaFunctionsSynopsis#[skip:-8.15.99]
 meta functions | keep synopsis;
 
 synopsis:keyword
@@ -38,10 +38,10 @@ double e()
 "double log(?base:integer|unsigned_long|long|double, number:integer|unsigned_long|long|double)"
 "double log10(number:double|integer|long|unsigned_long)"
 "keyword|text ltrim(string:keyword|text)"
-"double|integer|long|date max(number:double|integer|long|date)"
+"boolean|double|integer|long|date max(field:boolean|double|integer|long|date)"
 "double|integer|long median(number:double|integer|long)"
 "double|integer|long median_absolute_deviation(number:double|integer|long)"
-"double|integer|long|date min(number:double|integer|long|date)"
+"boolean|double|integer|long|date min(field:boolean|double|integer|long|date)"
 "boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version mv_append(field1:boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version, field2:boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version)"
 "double mv_avg(number:double|integer|long|unsigned_long)"
 "keyword mv_concat(string:text|keyword, delim:text|keyword)"
@@ -116,7 +116,7 @@ double tau()
 "double weighted_avg(number:double|integer|long, weight:double|integer|long)"
 ;
 
-metaFunctionsArgs#[skip:-8.14.99]
+metaFunctionsArgs#[skip:-8.15.99]
   META functions
 | EVAL name = SUBSTRING(name, 0, 14)
 | KEEP name, argNames, argTypes, argDescriptions;
@@ -158,10 +158,10 @@ locate        |[string, substring, start]          |["keyword|text", "keyword|te
 log           |[base, number]                      |["integer|unsigned_long|long|double", "integer|unsigned_long|long|double"]                                                        |["Base of logarithm. If `null`\, the function returns `null`. If not provided\, this function returns the natural logarithm (base e) of a value.", "Numeric expression. If `null`\, the function returns `null`."]
 log10         |number                              |"double|integer|long|unsigned_long"                                                                                               |Numeric expression. If `null`, the function returns `null`.
 ltrim         |string                              |"keyword|text"                                                                                                                    |String expression. If `null`, the function returns `null`.
-max           |number                              |"double|integer|long|date"                                                                                                        |[""]
+max           |field                               |"boolean|double|integer|long|date"                                                                                                |[""]
 median        |number                              |"double|integer|long"                                                                                                             |[""]
 median_absolut|number                              |"double|integer|long"                                                                                                             |[""]
-min           |number                              |"double|integer|long|date"                                                                                                        |[""]
+min           |field                               |"boolean|double|integer|long|date"                                                                                                |[""]
 mv_append     |[field1, field2]                    |["boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version", "boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version"]                 | ["", ""]
 mv_avg        |number                              |"double|integer|long|unsigned_long"                                                                                               |Multivalue expression.
 mv_concat     |[string, delim]                     |["text|keyword", "text|keyword"]                                                                                                  |[Multivalue expression., Delimiter.]
@@ -236,7 +236,7 @@ values        |field                               |"boolean|date|double|integer
 weighted_avg  |[number, weight]                    |["double|integer|long", "double|integer|long"]                                                                                    |[A numeric value., A numeric weight.]
 ;
 
-metaFunctionsDescription#[skip:-8.14.99]
+metaFunctionsDescription#[skip:-8.15.99]
   META functions
 | EVAL name = SUBSTRING(name, 0, 14)
 | KEEP name, description
@@ -279,10 +279,10 @@ locate        |Returns an integer that indicates the position of a keyword subst
 log           |Returns the logarithm of a value to a base. The input can be any numeric value, the return value is always a double.  Logs of zero, negative numbers, and base of one return `null` as well as a warning.
 log10         |Returns the logarithm of a value to base 10. The input can be any numeric value, the return value is always a double.  Logs of 0 and negative numbers return `null` as well as a warning.
 ltrim         |Removes leading whitespaces from a string.
-max           |The maximum value of a numeric field.
+max           |The maximum value of a field.
 median        |The value that is greater than half of all values and less than half of all values.
 median_absolut|The median absolute deviation, a measure of variability.
-min           |The minimum value of a numeric field.
+min           |The minimum value of a field.
 mv_append     |Concatenates values of two multi-value fields.
 mv_avg        |Converts a multivalued field into a single valued field containing the average of all of the values.
 mv_concat     |Converts a multivalued string expression into a single valued column containing the concatenation of all values separated by a delimiter.
@@ -357,7 +357,7 @@ values        |Collect values for a field.
 weighted_avg  |The weighted average of a numeric field.
 ;
 
-metaFunctionsRemaining#[skip:-8.14.99]
+metaFunctionsRemaining#[skip:-8.15.99]
   META functions
 | EVAL name = SUBSTRING(name, 0, 14)
 | KEEP name, *
@@ -401,10 +401,10 @@ locate        |integer
 log           |double                                                                                                                      |[true, false]               |false           |false
 log10         |double                                                                                                                      |false                       |false           |false
 ltrim         |"keyword|text"                                                                                                              |false                       |false           |false
-max           |"double|integer|long|date"                                                                                                  |false                       |false           |true
+max           |"boolean|double|integer|long|date"                                                                                          |false                       |false           |true
 median        |"double|integer|long"                                                                                                       |false                       |false           |true
 median_absolut|"double|integer|long"                                                                                                       |false                       |false           |true
-min           |"double|integer|long|date"                                                                                                  |false                       |false           |true
+min           |"boolean|double|integer|long|date"                                                                                          |false                       |false           |true
 mv_append     |"boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version"              |[false, false]              |false 		     |false
 mv_avg        |double                                                                                                                      |false                       |false           |false
 mv_concat     |keyword                                                                                                                     |[false, false]              |false           |false
@@ -479,7 +479,7 @@ values        |"boolean|date|double|integer|ip|keyword|long|text|version"
 weighted_avg  |"double"                                                                                                                    |[false, false]              |false           |true
 ;
 
-metaFunctionsFiltered#[skip:-8.14.99]
+metaFunctionsFiltered#[skip:-8.15.99]
 META FUNCTIONS
 | WHERE STARTS_WITH(name, "sin")
 ;
@@ -489,9 +489,7 @@ sin          |"double sin(angle:double|integer|long|unsigned_long)"   |angle
 sinh         |"double sinh(angle:double|integer|long|unsigned_long)"  |angle             |"double|integer|long|unsigned_long" | "An angle, in radians. If `null`, the function returns `null`." | double             | "Returns the {wikipedia}/Hyperbolic_functions[hyperbolic sine] of an angle."        | false                | false            | false
 ;
 
-
-// see https://github.com/elastic/elasticsearch/issues/102120
-countFunctions#[skip:-8.14.99, reason:BIN added]
+countFunctions#[skip:-8.15.99]
 meta functions |  stats  a = count(*), b = count(*), c = count(*) |  mv_expand c;
 
 a:long | b:long | c:long

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

@@ -31,6 +31,44 @@ MIN(languages):integer
 // end::min-result[]
 ;
 
+maxOfBoolean
+required_capability: agg_max_min_boolean_support
+from employees | stats s = max(still_hired);
+
+s:boolean
+true
+;
+
+maxOfBooleanExpression
+required_capability: agg_max_min_boolean_support
+from employees
+| eval x = salary is not null
+| where emp_no > 10050
+| stats a = max(salary is not null), b = max(x), c = max(case(salary is null, true, false)), d = max(is_rehired);
+
+a:boolean | b:boolean | c:boolean | d:boolean
+true      | true      | false     | true
+;
+
+minOfBooleanExpression
+required_capability: agg_max_min_boolean_support
+from employees
+| eval x = salary is not null
+| where emp_no > 10050
+| stats a = min(salary is not null), b = min(x), c = min(case(salary is null, true, false)), d = min(is_rehired);
+
+a:boolean | b:boolean | c:boolean | d:boolean
+true      | true      | false     | false
+;
+
+minOfBoolean
+required_capability: agg_max_min_boolean_support
+from employees | stats s = min(still_hired);
+
+s:boolean
+false
+;
+
 maxOfShort
 // short becomes int until https://github.com/elastic/elasticsearch-internal/issues/724
 from employees | stats l = max(languages.short);

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

@@ -49,6 +49,11 @@ public class EsqlCapabilities {
          */
         AGG_TOP,
 
+        /**
+         * Support for booleans in aggregations {@code MAX} and {@code MIN}.
+         */
+        AGG_MAX_MIN_BOOLEAN_SUPPORT,
+
         /**
          * Optimization for ST_CENTROID changed some results in cartesian data. #108713
          */

+ 35 - 18
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Max.java

@@ -10,10 +10,13 @@ package org.elasticsearch.xpack.esql.expression.function.aggregate;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier;
+import org.elasticsearch.compute.aggregation.MaxBooleanAggregatorFunctionSupplier;
 import org.elasticsearch.compute.aggregation.MaxDoubleAggregatorFunctionSupplier;
 import org.elasticsearch.compute.aggregation.MaxIntAggregatorFunctionSupplier;
 import org.elasticsearch.compute.aggregation.MaxLongAggregatorFunctionSupplier;
+import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.TypeResolutions;
 import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
 import org.elasticsearch.xpack.esql.core.tree.Source;
 import org.elasticsearch.xpack.esql.core.type.DataType;
@@ -22,16 +25,19 @@ import org.elasticsearch.xpack.esql.expression.function.Example;
 import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMax;
+import org.elasticsearch.xpack.esql.planner.ToAggregator;
 
 import java.io.IOException;
 import java.util.List;
 
-public class Max extends NumericAggregate implements SurrogateExpression {
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT;
+
+public class Max extends AggregateFunction implements ToAggregator, SurrogateExpression {
     public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Max", Max::new);
 
     @FunctionInfo(
-        returnType = { "double", "integer", "long", "date" },
-        description = "The maximum value of a numeric field.",
+        returnType = { "boolean", "double", "integer", "long", "date" },
+        description = "The maximum value of a field.",
         isAggregation = true,
         examples = {
             @Example(file = "stats", tag = "max"),
@@ -43,7 +49,7 @@ public class Max extends NumericAggregate implements SurrogateExpression {
                 tag = "docsStatsMaxNestedExpression"
             ) }
     )
-    public Max(Source source, @Param(name = "number", type = { "double", "integer", "long", "date" }) Expression field) {
+    public Max(Source source, @Param(name = "field", type = { "boolean", "double", "integer", "long", "date" }) Expression field) {
         super(source, field);
     }
 
@@ -67,8 +73,16 @@ public class Max extends NumericAggregate implements SurrogateExpression {
     }
 
     @Override
-    protected boolean supportsDates() {
-        return true;
+    protected TypeResolution resolveType() {
+        return TypeResolutions.isType(
+            this,
+            e -> e == DataType.BOOLEAN || e == DataType.DATETIME || (e.isNumeric() && e != DataType.UNSIGNED_LONG),
+            sourceText(),
+            DEFAULT,
+            "boolean",
+            "datetime",
+            "numeric except unsigned_long or counter types"
+        );
     }
 
     @Override
@@ -77,18 +91,21 @@ public class Max extends NumericAggregate implements SurrogateExpression {
     }
 
     @Override
-    protected AggregatorFunctionSupplier longSupplier(List<Integer> inputChannels) {
-        return new MaxLongAggregatorFunctionSupplier(inputChannels);
-    }
-
-    @Override
-    protected AggregatorFunctionSupplier intSupplier(List<Integer> inputChannels) {
-        return new MaxIntAggregatorFunctionSupplier(inputChannels);
-    }
-
-    @Override
-    protected AggregatorFunctionSupplier doubleSupplier(List<Integer> inputChannels) {
-        return new MaxDoubleAggregatorFunctionSupplier(inputChannels);
+    public final AggregatorFunctionSupplier supplier(List<Integer> inputChannels) {
+        DataType type = field().dataType();
+        if (type == DataType.BOOLEAN) {
+            return new MaxBooleanAggregatorFunctionSupplier(inputChannels);
+        }
+        if (type == DataType.LONG || type == DataType.DATETIME) {
+            return new MaxLongAggregatorFunctionSupplier(inputChannels);
+        }
+        if (type == DataType.INTEGER) {
+            return new MaxIntAggregatorFunctionSupplier(inputChannels);
+        }
+        if (type == DataType.DOUBLE) {
+            return new MaxDoubleAggregatorFunctionSupplier(inputChannels);
+        }
+        throw EsqlIllegalArgumentException.illegalDataType(type);
     }
 
     @Override

+ 37 - 20
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Min.java

@@ -10,10 +10,13 @@ package org.elasticsearch.xpack.esql.expression.function.aggregate;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier;
+import org.elasticsearch.compute.aggregation.MinBooleanAggregatorFunctionSupplier;
 import org.elasticsearch.compute.aggregation.MinDoubleAggregatorFunctionSupplier;
 import org.elasticsearch.compute.aggregation.MinIntAggregatorFunctionSupplier;
 import org.elasticsearch.compute.aggregation.MinLongAggregatorFunctionSupplier;
+import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.TypeResolutions;
 import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
 import org.elasticsearch.xpack.esql.core.tree.Source;
 import org.elasticsearch.xpack.esql.core.type.DataType;
@@ -22,16 +25,19 @@ import org.elasticsearch.xpack.esql.expression.function.Example;
 import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMin;
+import org.elasticsearch.xpack.esql.planner.ToAggregator;
 
 import java.io.IOException;
 import java.util.List;
 
-public class Min extends NumericAggregate implements SurrogateExpression {
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT;
+
+public class Min extends AggregateFunction implements ToAggregator, SurrogateExpression {
     public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Min", Min::new);
 
     @FunctionInfo(
-        returnType = { "double", "integer", "long", "date" },
-        description = "The minimum value of a numeric field.",
+        returnType = { "boolean", "double", "integer", "long", "date" },
+        description = "The minimum value of a field.",
         isAggregation = true,
         examples = {
             @Example(file = "stats", tag = "min"),
@@ -43,7 +49,7 @@ public class Min extends NumericAggregate implements SurrogateExpression {
                 tag = "docsStatsMinNestedExpression"
             ) }
     )
-    public Min(Source source, @Param(name = "number", type = { "double", "integer", "long", "date" }) Expression field) {
+    public Min(Source source, @Param(name = "field", type = { "boolean", "double", "integer", "long", "date" }) Expression field) {
         super(source, field);
     }
 
@@ -67,28 +73,39 @@ public class Min extends NumericAggregate implements SurrogateExpression {
     }
 
     @Override
-    public DataType dataType() {
-        return field().dataType();
-    }
-
-    @Override
-    protected boolean supportsDates() {
-        return true;
+    protected TypeResolution resolveType() {
+        return TypeResolutions.isType(
+            this,
+            e -> e == DataType.BOOLEAN || e == DataType.DATETIME || (e.isNumeric() && e != DataType.UNSIGNED_LONG),
+            sourceText(),
+            DEFAULT,
+            "boolean",
+            "datetime",
+            "numeric except unsigned_long or counter types"
+        );
     }
 
     @Override
-    protected AggregatorFunctionSupplier longSupplier(List<Integer> inputChannels) {
-        return new MinLongAggregatorFunctionSupplier(inputChannels);
-    }
-
-    @Override
-    protected AggregatorFunctionSupplier intSupplier(List<Integer> inputChannels) {
-        return new MinIntAggregatorFunctionSupplier(inputChannels);
+    public DataType dataType() {
+        return field().dataType();
     }
 
     @Override
-    protected AggregatorFunctionSupplier doubleSupplier(List<Integer> inputChannels) {
-        return new MinDoubleAggregatorFunctionSupplier(inputChannels);
+    public final AggregatorFunctionSupplier supplier(List<Integer> inputChannels) {
+        DataType type = field().dataType();
+        if (type == DataType.BOOLEAN) {
+            return new MinBooleanAggregatorFunctionSupplier(inputChannels);
+        }
+        if (type == DataType.LONG || type == DataType.DATETIME) {
+            return new MinLongAggregatorFunctionSupplier(inputChannels);
+        }
+        if (type == DataType.INTEGER) {
+            return new MinIntAggregatorFunctionSupplier(inputChannels);
+        }
+        if (type == DataType.DOUBLE) {
+            return new MinDoubleAggregatorFunctionSupplier(inputChannels);
+        }
+        throw EsqlIllegalArgumentException.illegalDataType(type);
     }
 
     @Override

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

@@ -146,6 +146,8 @@ final class AggregateMapper {
         List<String> extraConfigs = List.of("");
         if (NumericAggregate.class.isAssignableFrom(clazz)) {
             types = NUMERIC;
+        } else if (Max.class.isAssignableFrom(clazz) || Min.class.isAssignableFrom(clazz)) {
+            types = List.of("Boolean", "Int", "Long", "Double");
         } else if (clazz == Count.class) {
             types = List.of(""); // no extra type distinction
         } else if (SpatialAggregateFunction.class.isAssignableFrom(clazz)) {

+ 4 - 4
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java

@@ -1832,13 +1832,13 @@ public class AnalyzerTests extends ESTestCase {
              found value [x] type [unsigned_long]
             line 2:20: argument of [count_distinct(x)] must be [any exact type except unsigned_long or counter types],\
              found value [x] type [unsigned_long]
-            line 2:39: argument of [max(x)] must be [datetime or numeric except unsigned_long or counter types],\
+            line 2:39: argument of [max(x)] must be [boolean, datetime or numeric except unsigned_long or counter types],\
              found value [max(x)] type [unsigned_long]
             line 2:47: argument of [median(x)] must be [numeric except unsigned_long or counter types],\
              found value [x] type [unsigned_long]
             line 2:58: argument of [median_absolute_deviation(x)] must be [numeric except unsigned_long or counter types],\
              found value [x] type [unsigned_long]
-            line 2:88: argument of [min(x)] must be [datetime or numeric except unsigned_long or counter types],\
+            line 2:88: argument of [min(x)] must be [boolean, datetime or numeric except unsigned_long or counter types],\
              found value [min(x)] type [unsigned_long]
             line 2:96: first argument of [percentile(x, 10)] must be [numeric except unsigned_long],\
              found value [x] type [unsigned_long]
@@ -1852,13 +1852,13 @@ public class AnalyzerTests extends ESTestCase {
             Found 7 problems
             line 2:10: argument of [avg(x)] must be [numeric except unsigned_long or counter types],\
              found value [x] type [version]
-            line 2:18: argument of [max(x)] must be [datetime or numeric except unsigned_long or counter types],\
+            line 2:18: argument of [max(x)] must be [boolean, datetime or numeric except unsigned_long or counter types],\
              found value [max(x)] type [version]
             line 2:26: argument of [median(x)] must be [numeric except unsigned_long or counter types],\
              found value [x] type [version]
             line 2:37: argument of [median_absolute_deviation(x)] must be [numeric except unsigned_long or counter types],\
              found value [x] type [version]
-            line 2:67: argument of [min(x)] must be [datetime or numeric except unsigned_long or counter types],\
+            line 2:67: argument of [min(x)] must be [boolean, datetime or numeric except unsigned_long or counter types],\
              found value [min(x)] type [version]
             line 2:75: first argument of [percentile(x, 10)] must be [numeric except unsigned_long], found value [x] type [version]
             line 2:94: argument of [sum(x)] must be [numeric except unsigned_long or counter types], found value [x] type [version]""");

+ 4 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

@@ -493,7 +493,8 @@ public class VerifierTests extends ESTestCase {
         assertThat(
             error("FROM tests | STATS min(network.bytes_in)", tsdb),
             equalTo(
-                "1:20: argument of [min(network.bytes_in)] must be [datetime or numeric except unsigned_long or counter types],"
+                "1:20: argument of [min(network.bytes_in)] must be"
+                    + " [boolean, datetime or numeric except unsigned_long or counter types],"
                     + " found value [min(network.bytes_in)] type [counter_long]"
             )
         );
@@ -501,7 +502,8 @@ public class VerifierTests extends ESTestCase {
         assertThat(
             error("FROM tests | STATS max(network.bytes_in)", tsdb),
             equalTo(
-                "1:20: argument of [max(network.bytes_in)] must be [datetime or numeric except unsigned_long or counter types],"
+                "1:20: argument of [max(network.bytes_in)] must be"
+                    + " [boolean, datetime or numeric except unsigned_long or counter types],"
                     + " found value [max(network.bytes_in)] type [counter_long]"
             )
         );

+ 22 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/MultiRowTestCaseSupplier.java

@@ -278,4 +278,26 @@ public final class MultiRowTestCaseSupplier {
 
         return cases;
     }
+
+    public static List<TypedDataSupplier> booleanCases(int minRows, int maxRows) {
+        List<TypedDataSupplier> cases = new ArrayList<>();
+
+        cases.add(new TypedDataSupplier("<true booleans>", () -> randomList(minRows, maxRows, () -> true), DataType.BOOLEAN, false, true));
+
+        cases.add(
+            new TypedDataSupplier("<false booleans>", () -> randomList(minRows, maxRows, () -> false), DataType.BOOLEAN, false, true)
+        );
+
+        cases.add(
+            new TypedDataSupplier(
+                "<random booleans>",
+                () -> randomList(minRows, maxRows, ESTestCase::randomBoolean),
+                DataType.BOOLEAN,
+                false,
+                true
+            )
+        );
+
+        return cases;
+    }
 }

+ 20 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MaxTests.java

@@ -39,7 +39,8 @@ public class MaxTests extends AbstractAggregationTestCase {
             MultiRowTestCaseSupplier.intCases(1, 1000, Integer.MIN_VALUE, Integer.MAX_VALUE, true),
             MultiRowTestCaseSupplier.longCases(1, 1000, Long.MIN_VALUE, Long.MAX_VALUE, true),
             MultiRowTestCaseSupplier.doubleCases(1, 1000, -Double.MAX_VALUE, Double.MAX_VALUE, true),
-            MultiRowTestCaseSupplier.dateCases(1, 1000)
+            MultiRowTestCaseSupplier.dateCases(1, 1000),
+            MultiRowTestCaseSupplier.booleanCases(1, 1000)
         ).flatMap(List::stream).map(MaxTests::makeSupplier).collect(Collectors.toCollection(() -> suppliers));
 
         suppliers.addAll(
@@ -81,6 +82,15 @@ public class MaxTests extends AbstractAggregationTestCase {
                         equalTo(200L)
                     )
                 ),
+                new TestCaseSupplier(
+                    List.of(DataType.BOOLEAN),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(TestCaseSupplier.TypedData.multiRow(List.of(true, false, false, true), DataType.BOOLEAN, "field")),
+                        "Max[field=Attribute[channel=0]]",
+                        DataType.BOOLEAN,
+                        equalTo(true)
+                    )
+                ),
 
                 // Folding
                 new TestCaseSupplier(
@@ -118,6 +128,15 @@ public class MaxTests extends AbstractAggregationTestCase {
                         DataType.DATETIME,
                         equalTo(200L)
                     )
+                ),
+                new TestCaseSupplier(
+                    List.of(DataType.BOOLEAN),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(TestCaseSupplier.TypedData.multiRow(List.of(true), DataType.BOOLEAN, "field")),
+                        "Max[field=Attribute[channel=0]]",
+                        DataType.BOOLEAN,
+                        equalTo(true)
+                    )
                 )
             )
         );

+ 20 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MinTests.java

@@ -39,7 +39,8 @@ public class MinTests extends AbstractAggregationTestCase {
             MultiRowTestCaseSupplier.intCases(1, 1000, Integer.MIN_VALUE, Integer.MAX_VALUE, true),
             MultiRowTestCaseSupplier.longCases(1, 1000, Long.MIN_VALUE, Long.MAX_VALUE, true),
             MultiRowTestCaseSupplier.doubleCases(1, 1000, -Double.MAX_VALUE, Double.MAX_VALUE, true),
-            MultiRowTestCaseSupplier.dateCases(1, 1000)
+            MultiRowTestCaseSupplier.dateCases(1, 1000),
+            MultiRowTestCaseSupplier.booleanCases(1, 1000)
         ).flatMap(List::stream).map(MinTests::makeSupplier).collect(Collectors.toCollection(() -> suppliers));
 
         suppliers.addAll(
@@ -81,6 +82,15 @@ public class MinTests extends AbstractAggregationTestCase {
                         equalTo(0L)
                     )
                 ),
+                new TestCaseSupplier(
+                    List.of(DataType.BOOLEAN),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(TestCaseSupplier.TypedData.multiRow(List.of(true, false, false, true), DataType.BOOLEAN, "field")),
+                        "Min[field=Attribute[channel=0]]",
+                        DataType.BOOLEAN,
+                        equalTo(false)
+                    )
+                ),
 
                 // Folding
                 new TestCaseSupplier(
@@ -118,6 +128,15 @@ public class MinTests extends AbstractAggregationTestCase {
                         DataType.DATETIME,
                         equalTo(200L)
                     )
+                ),
+                new TestCaseSupplier(
+                    List.of(DataType.BOOLEAN),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(TestCaseSupplier.TypedData.multiRow(List.of(true), DataType.BOOLEAN, "field")),
+                        "Min[field=Attribute[channel=0]]",
+                        DataType.BOOLEAN,
+                        equalTo(true)
+                    )
                 )
             )
         );