ソースを参照

[8.x] Support ST_ENVELOPE and related ST_XMIN, etc. (#116964) (#118743)

* Support ST_ENVELOPE and related ST_XMIN, etc. (#116964)

Support ST_ENVELOPE and related ST_XMIN, etc.

Based on the PostGIS equivalents:

https://postgis.net/docs/ST_Envelope.html
https://postgis.net/docs/ST_XMin.html
https://postgis.net/docs/ST_XMax.html
https://postgis.net/docs/ST_YMin.html
https://postgis.net/docs/ST_YMax.html

* Fix off-by-one error reported in #118051
Craig Taverner 10 ヶ月 前
コミット
d6c14a2b8a
69 ファイル変更3558 行追加2 行削除
  1. 6 0
      docs/changelog/116964.yaml
  2. 5 0
      docs/reference/esql/functions/description/st_envelope.asciidoc
  3. 5 0
      docs/reference/esql/functions/description/st_xmax.asciidoc
  4. 5 0
      docs/reference/esql/functions/description/st_xmin.asciidoc
  5. 5 0
      docs/reference/esql/functions/description/st_ymax.asciidoc
  6. 5 0
      docs/reference/esql/functions/description/st_ymin.asciidoc
  7. 13 0
      docs/reference/esql/functions/examples/st_envelope.asciidoc
  8. 13 0
      docs/reference/esql/functions/examples/st_xmax.asciidoc
  9. 13 0
      docs/reference/esql/functions/examples/st_xmin.asciidoc
  10. 13 0
      docs/reference/esql/functions/examples/st_ymax.asciidoc
  11. 13 0
      docs/reference/esql/functions/examples/st_ymin.asciidoc
  12. 61 0
      docs/reference/esql/functions/kibana/definition/st_envelope.json
  13. 61 0
      docs/reference/esql/functions/kibana/definition/st_xmax.json
  14. 61 0
      docs/reference/esql/functions/kibana/definition/st_xmin.json
  15. 61 0
      docs/reference/esql/functions/kibana/definition/st_ymax.json
  16. 61 0
      docs/reference/esql/functions/kibana/definition/st_ymin.json
  17. 13 0
      docs/reference/esql/functions/kibana/docs/st_envelope.md
  18. 15 0
      docs/reference/esql/functions/kibana/docs/st_xmax.md
  19. 15 0
      docs/reference/esql/functions/kibana/docs/st_xmin.md
  20. 15 0
      docs/reference/esql/functions/kibana/docs/st_ymax.md
  21. 15 0
      docs/reference/esql/functions/kibana/docs/st_ymin.md
  22. 15 0
      docs/reference/esql/functions/layout/st_envelope.asciidoc
  23. 15 0
      docs/reference/esql/functions/layout/st_xmax.asciidoc
  24. 15 0
      docs/reference/esql/functions/layout/st_xmin.asciidoc
  25. 15 0
      docs/reference/esql/functions/layout/st_ymax.asciidoc
  26. 15 0
      docs/reference/esql/functions/layout/st_ymin.asciidoc
  27. 6 0
      docs/reference/esql/functions/parameters/st_envelope.asciidoc
  28. 6 0
      docs/reference/esql/functions/parameters/st_xmax.asciidoc
  29. 6 0
      docs/reference/esql/functions/parameters/st_xmin.asciidoc
  30. 6 0
      docs/reference/esql/functions/parameters/st_ymax.asciidoc
  31. 6 0
      docs/reference/esql/functions/parameters/st_ymin.asciidoc
  32. 1 0
      docs/reference/esql/functions/signature/st_envelope.svg
  33. 1 0
      docs/reference/esql/functions/signature/st_xmax.svg
  34. 1 0
      docs/reference/esql/functions/signature/st_xmin.svg
  35. 1 0
      docs/reference/esql/functions/signature/st_ymax.svg
  36. 1 0
      docs/reference/esql/functions/signature/st_ymin.svg
  37. 10 0
      docs/reference/esql/functions/spatial-functions.asciidoc
  38. 12 0
      docs/reference/esql/functions/types/st_envelope.asciidoc
  39. 12 0
      docs/reference/esql/functions/types/st_xmax.asciidoc
  40. 12 0
      docs/reference/esql/functions/types/st_xmin.asciidoc
  41. 12 0
      docs/reference/esql/functions/types/st_ymax.asciidoc
  42. 12 0
      docs/reference/esql/functions/types/st_ymin.asciidoc
  43. 356 0
      libs/geo/src/main/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitor.java
  44. 194 0
      libs/geo/src/test/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitorTests.java
  45. 41 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial_shapes.csv-spec
  46. 126 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeFromWKBEvaluator.java
  47. 126 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeFromWKBGeoEvaluator.java
  48. 127 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxFromWKBEvaluator.java
  49. 127 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxFromWKBGeoEvaluator.java
  50. 127 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinFromWKBEvaluator.java
  51. 127 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinFromWKBGeoEvaluator.java
  52. 127 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxFromWKBEvaluator.java
  53. 127 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxFromWKBGeoEvaluator.java
  54. 127 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinFromWKBEvaluator.java
  55. 127 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinFromWKBGeoEvaluator.java
  56. 5 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  57. 10 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
  58. 10 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  59. 138 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelope.java
  60. 123 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMax.java
  61. 123 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMin.java
  62. 123 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMax.java
  63. 123 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMin.java
  64. 88 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeTests.java
  65. 75 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxTests.java
  66. 75 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinTests.java
  67. 75 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxTests.java
  68. 75 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinTests.java
  69. 2 2
      x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml

+ 6 - 0
docs/changelog/116964.yaml

@@ -0,0 +1,6 @@
+pr: 116964
+summary: "Support ST_ENVELOPE and related (ST_XMIN, ST_XMAX, ST_YMIN, ST_YMAX) functions"
+area: ES|QL
+type: feature
+issues:
+ - 104875

+ 5 - 0
docs/reference/esql/functions/description/st_envelope.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Determines the minimum bounding box of the supplied geometry.

+ 5 - 0
docs/reference/esql/functions/description/st_xmax.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Extracts the maximum value of the `x` coordinates from the supplied geometry. If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the maximum `longitude` value.

+ 5 - 0
docs/reference/esql/functions/description/st_xmin.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Extracts the minimum value of the `x` coordinates from the supplied geometry. If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the minimum `longitude` value.

+ 5 - 0
docs/reference/esql/functions/description/st_ymax.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Extracts the maximum value of the `y` coordinates from the supplied geometry. If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the maximum `latitude` value.

+ 5 - 0
docs/reference/esql/functions/description/st_ymin.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Extracts the minimum value of the `y` coordinates from the supplied geometry. If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the minimum `latitude` value.

+ 13 - 0
docs/reference/esql/functions/examples/st_envelope.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_envelope]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_envelope-result]
+|===
+

+ 13 - 0
docs/reference/esql/functions/examples/st_xmax.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_x_y_min_max]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_x_y_min_max-result]
+|===
+

+ 13 - 0
docs/reference/esql/functions/examples/st_xmin.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_x_y_min_max]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_x_y_min_max-result]
+|===
+

+ 13 - 0
docs/reference/esql/functions/examples/st_ymax.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_x_y_min_max]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_x_y_min_max-result]
+|===
+

+ 13 - 0
docs/reference/esql/functions/examples/st_ymin.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_x_y_min_max]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_x_y_min_max-result]
+|===
+

+ 61 - 0
docs/reference/esql/functions/kibana/definition/st_envelope.json

@@ -0,0 +1,61 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "st_envelope",
+  "description" : "Determines the minimum bounding box of the supplied geometry.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "geometry",
+          "type" : "cartesian_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "cartesian_shape"
+    },
+    {
+      "params" : [
+        {
+          "name" : "geometry",
+          "type" : "cartesian_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "cartesian_shape"
+    },
+    {
+      "params" : [
+        {
+          "name" : "geometry",
+          "type" : "geo_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "geo_shape"
+    },
+    {
+      "params" : [
+        {
+          "name" : "geometry",
+          "type" : "geo_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "geo_shape"
+    }
+  ],
+  "examples" : [
+    "FROM airport_city_boundaries\n| WHERE abbrev == \"CPH\"\n| EVAL envelope = ST_ENVELOPE(city_boundary)\n| KEEP abbrev, airport, envelope"
+  ],
+  "preview" : false,
+  "snapshot_only" : false
+}

+ 61 - 0
docs/reference/esql/functions/kibana/definition/st_xmax.json

@@ -0,0 +1,61 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "st_xmax",
+  "description" : "Extracts the maximum value of the `x` coordinates from the supplied geometry.\nIf the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the maximum `longitude` value.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "cartesian_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "cartesian_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "geo_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "geo_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    }
+  ],
+  "examples" : [
+    "FROM airport_city_boundaries\n| WHERE abbrev == \"CPH\"\n| EVAL envelope = ST_ENVELOPE(city_boundary)\n| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)\n| KEEP abbrev, airport, xmin, xmax, ymin, ymax"
+  ],
+  "preview" : false,
+  "snapshot_only" : false
+}

+ 61 - 0
docs/reference/esql/functions/kibana/definition/st_xmin.json

@@ -0,0 +1,61 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "st_xmin",
+  "description" : "Extracts the minimum value of the `x` coordinates from the supplied geometry.\nIf the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the minimum `longitude` value.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "cartesian_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "cartesian_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "geo_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "geo_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    }
+  ],
+  "examples" : [
+    "FROM airport_city_boundaries\n| WHERE abbrev == \"CPH\"\n| EVAL envelope = ST_ENVELOPE(city_boundary)\n| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)\n| KEEP abbrev, airport, xmin, xmax, ymin, ymax"
+  ],
+  "preview" : false,
+  "snapshot_only" : false
+}

+ 61 - 0
docs/reference/esql/functions/kibana/definition/st_ymax.json

@@ -0,0 +1,61 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "st_ymax",
+  "description" : "Extracts the maximum value of the `y` coordinates from the supplied geometry.\nIf the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the maximum `latitude` value.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "cartesian_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "cartesian_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "geo_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "geo_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    }
+  ],
+  "examples" : [
+    "FROM airport_city_boundaries\n| WHERE abbrev == \"CPH\"\n| EVAL envelope = ST_ENVELOPE(city_boundary)\n| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)\n| KEEP abbrev, airport, xmin, xmax, ymin, ymax"
+  ],
+  "preview" : false,
+  "snapshot_only" : false
+}

+ 61 - 0
docs/reference/esql/functions/kibana/definition/st_ymin.json

@@ -0,0 +1,61 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "st_ymin",
+  "description" : "Extracts the minimum value of the `y` coordinates from the supplied geometry.\nIf the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the minimum `latitude` value.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "cartesian_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "cartesian_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "geo_point",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "point",
+          "type" : "geo_shape",
+          "optional" : false,
+          "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    }
+  ],
+  "examples" : [
+    "FROM airport_city_boundaries\n| WHERE abbrev == \"CPH\"\n| EVAL envelope = ST_ENVELOPE(city_boundary)\n| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)\n| KEEP abbrev, airport, xmin, xmax, ymin, ymax"
+  ],
+  "preview" : false,
+  "snapshot_only" : false
+}

+ 13 - 0
docs/reference/esql/functions/kibana/docs/st_envelope.md

@@ -0,0 +1,13 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### ST_ENVELOPE
+Determines the minimum bounding box of the supplied geometry.
+
+```
+FROM airport_city_boundaries
+| WHERE abbrev == "CPH"
+| EVAL envelope = ST_ENVELOPE(city_boundary)
+| KEEP abbrev, airport, envelope
+```

+ 15 - 0
docs/reference/esql/functions/kibana/docs/st_xmax.md

@@ -0,0 +1,15 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### ST_XMAX
+Extracts the maximum value of the `x` coordinates from the supplied geometry.
+If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the maximum `longitude` value.
+
+```
+FROM airport_city_boundaries
+| WHERE abbrev == "CPH"
+| EVAL envelope = ST_ENVELOPE(city_boundary)
+| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)
+| KEEP abbrev, airport, xmin, xmax, ymin, ymax
+```

+ 15 - 0
docs/reference/esql/functions/kibana/docs/st_xmin.md

@@ -0,0 +1,15 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### ST_XMIN
+Extracts the minimum value of the `x` coordinates from the supplied geometry.
+If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the minimum `longitude` value.
+
+```
+FROM airport_city_boundaries
+| WHERE abbrev == "CPH"
+| EVAL envelope = ST_ENVELOPE(city_boundary)
+| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)
+| KEEP abbrev, airport, xmin, xmax, ymin, ymax
+```

+ 15 - 0
docs/reference/esql/functions/kibana/docs/st_ymax.md

@@ -0,0 +1,15 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### ST_YMAX
+Extracts the maximum value of the `y` coordinates from the supplied geometry.
+If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the maximum `latitude` value.
+
+```
+FROM airport_city_boundaries
+| WHERE abbrev == "CPH"
+| EVAL envelope = ST_ENVELOPE(city_boundary)
+| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)
+| KEEP abbrev, airport, xmin, xmax, ymin, ymax
+```

+ 15 - 0
docs/reference/esql/functions/kibana/docs/st_ymin.md

@@ -0,0 +1,15 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### ST_YMIN
+Extracts the minimum value of the `y` coordinates from the supplied geometry.
+If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the minimum `latitude` value.
+
+```
+FROM airport_city_boundaries
+| WHERE abbrev == "CPH"
+| EVAL envelope = ST_ENVELOPE(city_boundary)
+| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)
+| KEEP abbrev, airport, xmin, xmax, ymin, ymax
+```

+ 15 - 0
docs/reference/esql/functions/layout/st_envelope.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-st_envelope]]
+=== `ST_ENVELOPE`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_envelope.svg[Embedded,opts=inline]
+
+include::../parameters/st_envelope.asciidoc[]
+include::../description/st_envelope.asciidoc[]
+include::../types/st_envelope.asciidoc[]
+include::../examples/st_envelope.asciidoc[]

+ 15 - 0
docs/reference/esql/functions/layout/st_xmax.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-st_xmax]]
+=== `ST_XMAX`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_xmax.svg[Embedded,opts=inline]
+
+include::../parameters/st_xmax.asciidoc[]
+include::../description/st_xmax.asciidoc[]
+include::../types/st_xmax.asciidoc[]
+include::../examples/st_xmax.asciidoc[]

+ 15 - 0
docs/reference/esql/functions/layout/st_xmin.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-st_xmin]]
+=== `ST_XMIN`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_xmin.svg[Embedded,opts=inline]
+
+include::../parameters/st_xmin.asciidoc[]
+include::../description/st_xmin.asciidoc[]
+include::../types/st_xmin.asciidoc[]
+include::../examples/st_xmin.asciidoc[]

+ 15 - 0
docs/reference/esql/functions/layout/st_ymax.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-st_ymax]]
+=== `ST_YMAX`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_ymax.svg[Embedded,opts=inline]
+
+include::../parameters/st_ymax.asciidoc[]
+include::../description/st_ymax.asciidoc[]
+include::../types/st_ymax.asciidoc[]
+include::../examples/st_ymax.asciidoc[]

+ 15 - 0
docs/reference/esql/functions/layout/st_ymin.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-st_ymin]]
+=== `ST_YMIN`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_ymin.svg[Embedded,opts=inline]
+
+include::../parameters/st_ymin.asciidoc[]
+include::../description/st_ymin.asciidoc[]
+include::../types/st_ymin.asciidoc[]
+include::../examples/st_ymin.asciidoc[]

+ 6 - 0
docs/reference/esql/functions/parameters/st_envelope.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`geometry`::
+Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`.

+ 6 - 0
docs/reference/esql/functions/parameters/st_xmax.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`point`::
+Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`.

+ 6 - 0
docs/reference/esql/functions/parameters/st_xmin.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`point`::
+Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`.

+ 6 - 0
docs/reference/esql/functions/parameters/st_ymax.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`point`::
+Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`.

+ 6 - 0
docs/reference/esql/functions/parameters/st_ymin.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`point`::
+Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`.

+ 1 - 0
docs/reference/esql/functions/signature/st_envelope.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="372" height="46" viewbox="0 0 372 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 31h5m152 0h10m32 0h10m116 0h10m32 0h5"/><rect class="s" x="5" y="5" width="152" height="36"/><text class="k" x="15" y="31">ST_ENVELOPE</text><rect class="s" x="167" y="5" width="32" height="36" rx="7"/><text class="syn" x="177" y="31">(</text><rect class="s" x="209" y="5" width="116" height="36" rx="7"/><text class="k" x="219" y="31">geometry</text><rect class="s" x="335" y="5" width="32" height="36" rx="7"/><text class="syn" x="345" y="31">)</text></svg>

+ 1 - 0
docs/reference/esql/functions/signature/st_xmax.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="288" height="46" viewbox="0 0 288 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 31h5m104 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="104" height="36"/><text class="k" x="15" y="31">ST_XMAX</text><rect class="s" x="119" y="5" width="32" height="36" rx="7"/><text class="syn" x="129" y="31">(</text><rect class="s" x="161" y="5" width="80" height="36" rx="7"/><text class="k" x="171" y="31">point</text><rect class="s" x="251" y="5" width="32" height="36" rx="7"/><text class="syn" x="261" y="31">)</text></svg>

+ 1 - 0
docs/reference/esql/functions/signature/st_xmin.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="288" height="46" viewbox="0 0 288 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 31h5m104 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="104" height="36"/><text class="k" x="15" y="31">ST_XMIN</text><rect class="s" x="119" y="5" width="32" height="36" rx="7"/><text class="syn" x="129" y="31">(</text><rect class="s" x="161" y="5" width="80" height="36" rx="7"/><text class="k" x="171" y="31">point</text><rect class="s" x="251" y="5" width="32" height="36" rx="7"/><text class="syn" x="261" y="31">)</text></svg>

+ 1 - 0
docs/reference/esql/functions/signature/st_ymax.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="288" height="46" viewbox="0 0 288 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 31h5m104 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="104" height="36"/><text class="k" x="15" y="31">ST_YMAX</text><rect class="s" x="119" y="5" width="32" height="36" rx="7"/><text class="syn" x="129" y="31">(</text><rect class="s" x="161" y="5" width="80" height="36" rx="7"/><text class="k" x="171" y="31">point</text><rect class="s" x="251" y="5" width="32" height="36" rx="7"/><text class="syn" x="261" y="31">)</text></svg>

+ 1 - 0
docs/reference/esql/functions/signature/st_ymin.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="288" height="46" viewbox="0 0 288 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 31h5m104 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="104" height="36"/><text class="k" x="15" y="31">ST_YMIN</text><rect class="s" x="119" y="5" width="32" height="36" rx="7"/><text class="syn" x="129" y="31">(</text><rect class="s" x="161" y="5" width="80" height="36" rx="7"/><text class="k" x="171" y="31">point</text><rect class="s" x="251" y="5" width="32" height="36" rx="7"/><text class="syn" x="261" y="31">)</text></svg>

+ 10 - 0
docs/reference/esql/functions/spatial-functions.asciidoc

@@ -15,6 +15,11 @@
 * <<esql-st_within>>
 * <<esql-st_x>>
 * <<esql-st_y>>
+* experimental:[] <<esql-st_envelope>>
+* experimental:[] <<esql-st_xmax>>
+* experimental:[] <<esql-st_xmin>>
+* experimental:[] <<esql-st_ymax>>
+* experimental:[] <<esql-st_ymin>>
 // end::spatial_list[]
 
 include::layout/st_distance.asciidoc[]
@@ -24,3 +29,8 @@ include::layout/st_contains.asciidoc[]
 include::layout/st_within.asciidoc[]
 include::layout/st_x.asciidoc[]
 include::layout/st_y.asciidoc[]
+include::layout/st_envelope.asciidoc[]
+include::layout/st_xmax.asciidoc[]
+include::layout/st_xmin.asciidoc[]
+include::layout/st_ymax.asciidoc[]
+include::layout/st_ymin.asciidoc[]

+ 12 - 0
docs/reference/esql/functions/types/st_envelope.asciidoc

@@ -0,0 +1,12 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+geometry | result
+cartesian_point | cartesian_shape
+cartesian_shape | cartesian_shape
+geo_point | geo_shape
+geo_shape | geo_shape
+|===

+ 12 - 0
docs/reference/esql/functions/types/st_xmax.asciidoc

@@ -0,0 +1,12 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+point | result
+cartesian_point | double
+cartesian_shape | double
+geo_point | double
+geo_shape | double
+|===

+ 12 - 0
docs/reference/esql/functions/types/st_xmin.asciidoc

@@ -0,0 +1,12 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+point | result
+cartesian_point | double
+cartesian_shape | double
+geo_point | double
+geo_shape | double
+|===

+ 12 - 0
docs/reference/esql/functions/types/st_ymax.asciidoc

@@ -0,0 +1,12 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+point | result
+cartesian_point | double
+cartesian_shape | double
+geo_point | double
+geo_shape | double
+|===

+ 12 - 0
docs/reference/esql/functions/types/st_ymin.asciidoc

@@ -0,0 +1,12 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+point | result
+cartesian_point | double
+cartesian_shape | double
+geo_point | double
+geo_shape | double
+|===

+ 356 - 0
libs/geo/src/main/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitor.java

@@ -0,0 +1,356 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.geometry.utils;
+
+import org.elasticsearch.geometry.Circle;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.geometry.GeometryCollection;
+import org.elasticsearch.geometry.GeometryVisitor;
+import org.elasticsearch.geometry.Line;
+import org.elasticsearch.geometry.LinearRing;
+import org.elasticsearch.geometry.MultiLine;
+import org.elasticsearch.geometry.MultiPoint;
+import org.elasticsearch.geometry.MultiPolygon;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.Polygon;
+import org.elasticsearch.geometry.Rectangle;
+
+import java.util.Locale;
+import java.util.Optional;
+
+/**
+ * This visitor is designed to determine the spatial envelope (or BBOX or MBR) of a potentially complex geometry.
+ * It has two modes:
+ * <ul>
+ *     <li>
+ *         Cartesian mode: The envelope is determined by the minimum and maximum x/y coordinates.
+ *         Incoming BBOX geometries with minX &gt; maxX are treated as invalid.
+ *         Resulting BBOX geometries will always have minX &lt;= maxX.
+ *     </li>
+ *     <li>
+ *         Geographic mode: The envelope is determined by the minimum and maximum x/y coordinates,
+ *         considering the possibility of wrapping the longitude around the dateline.
+ *         A bounding box can be determined either by wrapping the longitude around the dateline or not,
+ *         and the smaller bounding box is chosen. It is possible to disable the wrapping of the longitude.
+ * </ul>
+ * Usage of this is as simple as:
+ * <code>
+ *     Optional&lt;Rectangle&gt; bbox = SpatialEnvelopeVisitor.visit(geometry);
+ *     if (bbox.isPresent()) {
+ *         Rectangle envelope = bbox.get();
+ *         // Do stuff with the envelope
+ *     }
+ * </code>
+ * It is also possible to create the inner <code>PointVisitor</code> separately, as well as use the visitor for multiple geometries.
+ * <code>
+ *     PointVisitor pointVisitor = new CartesianPointVisitor();
+ *     SpatialEnvelopeVisitor visitor = new SpatialEnvelopeVisitor(pointVisitor);
+ *     for (Geometry geometry : geometries) {
+ *         geometry.visit(visitor);
+ *     }
+ *     if (visitor.isValid()) {
+ *         Rectangle envelope = visitor.getResult();
+ *         // Do stuff with the envelope
+ *     }
+ * </code>
+ * Code that wishes to modify the behaviour of the visitor can implement the <code>PointVisitor</code> interface,
+ * or extend the existing implementations.
+ */
+public class SpatialEnvelopeVisitor implements GeometryVisitor<Boolean, RuntimeException> {
+
+    private final PointVisitor pointVisitor;
+
+    public SpatialEnvelopeVisitor(PointVisitor pointVisitor) {
+        this.pointVisitor = pointVisitor;
+    }
+
+    /**
+     * Determine the BBOX without considering the CRS or wrapping of the longitude.
+     * Note that incoming BBOX's that do cross the dateline (minx>maxx) will be treated as invalid.
+     */
+    public static Optional<Rectangle> visitCartesian(Geometry geometry) {
+        var visitor = new SpatialEnvelopeVisitor(new CartesianPointVisitor());
+        if (geometry.visit(visitor)) {
+            return Optional.of(visitor.getResult());
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Determine the BBOX assuming the CRS is geographic (eg WGS84) and optionally wrapping the longitude around the dateline.
+     */
+    public static Optional<Rectangle> visitGeo(Geometry geometry, boolean wrapLongitude) {
+        var visitor = new SpatialEnvelopeVisitor(new GeoPointVisitor(wrapLongitude));
+        if (geometry.visit(visitor)) {
+            return Optional.of(visitor.getResult());
+        }
+        return Optional.empty();
+    }
+
+    public Rectangle getResult() {
+        return pointVisitor.getResult();
+    }
+
+    /**
+     * Visitor for visiting points and rectangles. This is where the actual envelope calculation happens.
+     * There are two implementations, one for cartesian coordinates and one for geographic coordinates.
+     * The latter can optionally wrap the longitude around the dateline.
+     */
+    public interface PointVisitor {
+        void visitPoint(double x, double y);
+
+        void visitRectangle(double minX, double maxX, double maxY, double minY);
+
+        boolean isValid();
+
+        Rectangle getResult();
+    }
+
+    /**
+     * The cartesian point visitor determines the envelope by the minimum and maximum x/y coordinates.
+     * It also disallows invalid rectangles where minX > maxX.
+     */
+    public static class CartesianPointVisitor implements PointVisitor {
+        private double minX = Double.POSITIVE_INFINITY;
+        private double minY = Double.POSITIVE_INFINITY;
+        private double maxX = Double.NEGATIVE_INFINITY;
+        private double maxY = Double.NEGATIVE_INFINITY;
+
+        public double getMinX() {
+            return minX;
+        }
+
+        public double getMinY() {
+            return minY;
+        }
+
+        public double getMaxX() {
+            return maxX;
+        }
+
+        public double getMaxY() {
+            return maxY;
+        }
+
+        @Override
+        public void visitPoint(double x, double y) {
+            minX = Math.min(minX, x);
+            minY = Math.min(minY, y);
+            maxX = Math.max(maxX, x);
+            maxY = Math.max(maxY, y);
+        }
+
+        @Override
+        public void visitRectangle(double minX, double maxX, double maxY, double minY) {
+            if (minX > maxX) {
+                throw new IllegalArgumentException(
+                    String.format(Locale.ROOT, "Invalid cartesian rectangle: minX (%s) > maxX (%s)", minX, maxX)
+                );
+            }
+            this.minX = Math.min(this.minX, minX);
+            this.minY = Math.min(this.minY, minY);
+            this.maxX = Math.max(this.maxX, maxX);
+            this.maxY = Math.max(this.maxY, maxY);
+        }
+
+        @Override
+        public boolean isValid() {
+            return minY != Double.POSITIVE_INFINITY;
+        }
+
+        @Override
+        public Rectangle getResult() {
+            return new Rectangle(minX, maxX, maxY, minY);
+        }
+    }
+
+    /**
+     * The geographic point visitor determines the envelope by the minimum and maximum x/y coordinates,
+     * while allowing for wrapping the longitude around the dateline.
+     * When longitude wrapping is enabled, the visitor will determine the smallest bounding box between the two choices:
+     * <ul>
+     *     <li>Wrapping around the front of the earth, in which case the result will have minx &lt; maxx</li>
+     *     <li>Wrapping around the back of the earth, crossing the dateline, in which case the result will have minx &gt; maxx</li>
+     * </ul>
+     */
+    public static class GeoPointVisitor implements PointVisitor {
+        private double minY = Double.POSITIVE_INFINITY;
+        private double maxY = Double.NEGATIVE_INFINITY;
+        private double minNegX = Double.POSITIVE_INFINITY;
+        private double maxNegX = Double.NEGATIVE_INFINITY;
+        private double minPosX = Double.POSITIVE_INFINITY;
+        private double maxPosX = Double.NEGATIVE_INFINITY;
+
+        public double getMinY() {
+            return minY;
+        }
+
+        public double getMaxY() {
+            return maxY;
+        }
+
+        public double getMinNegX() {
+            return minNegX;
+        }
+
+        public double getMaxNegX() {
+            return maxNegX;
+        }
+
+        public double getMinPosX() {
+            return minPosX;
+        }
+
+        public double getMaxPosX() {
+            return maxPosX;
+        }
+
+        private final boolean wrapLongitude;
+
+        public GeoPointVisitor(boolean wrapLongitude) {
+            this.wrapLongitude = wrapLongitude;
+        }
+
+        @Override
+        public void visitPoint(double x, double y) {
+            minY = Math.min(minY, y);
+            maxY = Math.max(maxY, y);
+            visitLongitude(x);
+        }
+
+        @Override
+        public void visitRectangle(double minX, double maxX, double maxY, double minY) {
+            this.minY = Math.min(this.minY, minY);
+            this.maxY = Math.max(this.maxY, maxY);
+            visitLongitude(minX);
+            visitLongitude(maxX);
+        }
+
+        private void visitLongitude(double x) {
+            if (x >= 0) {
+                minPosX = Math.min(minPosX, x);
+                maxPosX = Math.max(maxPosX, x);
+            } else {
+                minNegX = Math.min(minNegX, x);
+                maxNegX = Math.max(maxNegX, x);
+            }
+        }
+
+        @Override
+        public boolean isValid() {
+            return minY != Double.POSITIVE_INFINITY;
+        }
+
+        @Override
+        public Rectangle getResult() {
+            return getResult(minNegX, minPosX, maxNegX, maxPosX, maxY, minY, wrapLongitude);
+        }
+
+        private static Rectangle getResult(
+            double minNegX,
+            double minPosX,
+            double maxNegX,
+            double maxPosX,
+            double maxY,
+            double minY,
+            boolean wrapLongitude
+        ) {
+            assert Double.isFinite(maxY);
+            if (Double.isInfinite(minPosX)) {
+                return new Rectangle(minNegX, maxNegX, maxY, minY);
+            } else if (Double.isInfinite(minNegX)) {
+                return new Rectangle(minPosX, maxPosX, maxY, minY);
+            } else if (wrapLongitude) {
+                double unwrappedWidth = maxPosX - minNegX;
+                double wrappedWidth = (180 - minPosX) - (-180 - maxNegX);
+                if (unwrappedWidth <= wrappedWidth) {
+                    return new Rectangle(minNegX, maxPosX, maxY, minY);
+                } else {
+                    return new Rectangle(minPosX, maxNegX, maxY, minY);
+                }
+            } else {
+                return new Rectangle(minNegX, maxPosX, maxY, minY);
+            }
+        }
+    }
+
+    private boolean isValid() {
+        return pointVisitor.isValid();
+    }
+
+    @Override
+    public Boolean visit(Circle circle) throws RuntimeException {
+        // TODO: Support circle, if given CRS (needed for radius to x/y coordinate transformation)
+        throw new UnsupportedOperationException("Circle is not supported");
+    }
+
+    @Override
+    public Boolean visit(GeometryCollection<?> collection) throws RuntimeException {
+        collection.forEach(geometry -> geometry.visit(this));
+        return isValid();
+    }
+
+    @Override
+    public Boolean visit(Line line) throws RuntimeException {
+        for (int i = 0; i < line.length(); i++) {
+            pointVisitor.visitPoint(line.getX(i), line.getY(i));
+        }
+        return isValid();
+    }
+
+    @Override
+    public Boolean visit(LinearRing ring) throws RuntimeException {
+        for (int i = 0; i < ring.length(); i++) {
+            pointVisitor.visitPoint(ring.getX(i), ring.getY(i));
+        }
+        return isValid();
+    }
+
+    @Override
+    public Boolean visit(MultiLine multiLine) throws RuntimeException {
+        multiLine.forEach(line -> line.visit(this));
+        return isValid();
+    }
+
+    @Override
+    public Boolean visit(MultiPoint multiPoint) throws RuntimeException {
+        for (int i = 0; i < multiPoint.size(); i++) {
+            visit(multiPoint.get(i));
+        }
+        return isValid();
+    }
+
+    @Override
+    public Boolean visit(MultiPolygon multiPolygon) throws RuntimeException {
+        multiPolygon.forEach(polygon -> polygon.visit(this));
+        return isValid();
+    }
+
+    @Override
+    public Boolean visit(Point point) throws RuntimeException {
+        pointVisitor.visitPoint(point.getX(), point.getY());
+        return isValid();
+    }
+
+    @Override
+    public Boolean visit(Polygon polygon) throws RuntimeException {
+        visit(polygon.getPolygon());
+        for (int i = 0; i < polygon.getNumberOfHoles(); i++) {
+            visit(polygon.getHole(i));
+        }
+        return isValid();
+    }
+
+    @Override
+    public Boolean visit(Rectangle rectangle) throws RuntimeException {
+        pointVisitor.visitRectangle(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMaxY(), rectangle.getMinY());
+        return isValid();
+    }
+}

+ 194 - 0
libs/geo/src/test/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitorTests.java

@@ -0,0 +1,194 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.geometry.utils;
+
+import org.elasticsearch.geo.GeometryTestUtils;
+import org.elasticsearch.geo.ShapeTestUtils;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.Rectangle;
+import org.elasticsearch.test.ESTestCase;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+
+public class SpatialEnvelopeVisitorTests extends ESTestCase {
+
+    public void testVisitCartesianShape() {
+        for (int i = 0; i < 1000; i++) {
+            var geometry = ShapeTestUtils.randomGeometryWithoutCircle(0, false);
+            var bbox = SpatialEnvelopeVisitor.visitCartesian(geometry);
+            assertNotNull(bbox);
+            assertTrue(i + ": " + geometry, bbox.isPresent());
+            var result = bbox.get();
+            assertThat(i + ": " + geometry, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
+            assertThat(i + ": " + geometry, result.getMinY(), lessThanOrEqualTo(result.getMaxY()));
+        }
+    }
+
+    public void testVisitGeoShapeNoWrap() {
+        for (int i = 0; i < 1000; i++) {
+            var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, false);
+            var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, false);
+            assertNotNull(bbox);
+            assertTrue(i + ": " + geometry, bbox.isPresent());
+            var result = bbox.get();
+            assertThat(i + ": " + geometry, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
+            assertThat(i + ": " + geometry, result.getMinY(), lessThanOrEqualTo(result.getMaxY()));
+        }
+    }
+
+    public void testVisitGeoShapeWrap() {
+        for (int i = 0; i < 1000; i++) {
+            var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, true);
+            var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, false);
+            assertNotNull(bbox);
+            assertTrue(i + ": " + geometry, bbox.isPresent());
+            var result = bbox.get();
+            assertThat(i + ": " + geometry, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
+            assertThat(i + ": " + geometry, result.getMinY(), lessThanOrEqualTo(result.getMaxY()));
+        }
+    }
+
+    public void testVisitCartesianPoints() {
+        var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.CartesianPointVisitor());
+        double minX = Double.MAX_VALUE;
+        double minY = Double.MAX_VALUE;
+        double maxX = -Double.MAX_VALUE;
+        double maxY = -Double.MAX_VALUE;
+        for (int i = 0; i < 1000; i++) {
+            var x = randomFloat();
+            var y = randomFloat();
+            var point = new Point(x, y);
+            visitor.visit(point);
+            minX = Math.min(minX, x);
+            minY = Math.min(minY, y);
+            maxX = Math.max(maxX, x);
+            maxY = Math.max(maxY, y);
+            var result = visitor.getResult();
+            assertThat(i + ": " + point, result.getMinX(), equalTo(minX));
+            assertThat(i + ": " + point, result.getMinY(), equalTo(minY));
+            assertThat(i + ": " + point, result.getMaxX(), equalTo(maxX));
+            assertThat(i + ": " + point, result.getMaxY(), equalTo(maxY));
+        }
+    }
+
+    public void testVisitGeoPointsNoWrapping() {
+        var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(false));
+        double minY = Double.MAX_VALUE;
+        double maxY = -Double.MAX_VALUE;
+        double minX = Double.MAX_VALUE;
+        double maxX = -Double.MAX_VALUE;
+        for (int i = 0; i < 1000; i++) {
+            var point = GeometryTestUtils.randomPoint();
+            visitor.visit(point);
+            minY = Math.min(minY, point.getY());
+            maxY = Math.max(maxY, point.getY());
+            minX = Math.min(minX, point.getX());
+            maxX = Math.max(maxX, point.getX());
+            var result = visitor.getResult();
+            assertThat(i + ": " + point, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
+            assertThat(i + ": " + point, result.getMinX(), equalTo(minX));
+            assertThat(i + ": " + point, result.getMinY(), equalTo(minY));
+            assertThat(i + ": " + point, result.getMaxX(), equalTo(maxX));
+            assertThat(i + ": " + point, result.getMaxY(), equalTo(maxY));
+        }
+    }
+
+    public void testVisitGeoPointsWrapping() {
+        var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true));
+        double minY = Double.POSITIVE_INFINITY;
+        double maxY = Double.NEGATIVE_INFINITY;
+        double minNegX = Double.POSITIVE_INFINITY;
+        double maxNegX = Double.NEGATIVE_INFINITY;
+        double minPosX = Double.POSITIVE_INFINITY;
+        double maxPosX = Double.NEGATIVE_INFINITY;
+        for (int i = 0; i < 1000; i++) {
+            var point = GeometryTestUtils.randomPoint();
+            visitor.visit(point);
+            minY = Math.min(minY, point.getY());
+            maxY = Math.max(maxY, point.getY());
+            if (point.getX() >= 0) {
+                minPosX = Math.min(minPosX, point.getX());
+                maxPosX = Math.max(maxPosX, point.getX());
+            } else {
+                minNegX = Math.min(minNegX, point.getX());
+                maxNegX = Math.max(maxNegX, point.getX());
+            }
+            var result = visitor.getResult();
+            if (Double.isInfinite(minPosX)) {
+                // Only negative x values were considered
+                assertRectangleResult(i + ": " + point, result, minNegX, maxNegX, maxY, minY, false);
+            } else if (Double.isInfinite(minNegX)) {
+                // Only positive x values were considered
+                assertRectangleResult(i + ": " + point, result, minPosX, maxPosX, maxY, minY, false);
+            } else {
+                // Both positive and negative x values exist, we need to decide which way to wrap the bbox
+                double unwrappedWidth = maxPosX - minNegX;
+                double wrappedWidth = (180 - minPosX) - (-180 - maxNegX);
+                if (unwrappedWidth <= wrappedWidth) {
+                    // The smaller bbox is around the front of the planet, no dateline wrapping required
+                    assertRectangleResult(i + ": " + point, result, minNegX, maxPosX, maxY, minY, false);
+                } else {
+                    // The smaller bbox is around the back of the planet, dateline wrapping required (minx > maxx)
+                    assertRectangleResult(i + ": " + point, result, minPosX, maxNegX, maxY, minY, true);
+                }
+            }
+        }
+    }
+
+    public void testWillCrossDateline() {
+        var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true));
+        visitor.visit(new Point(-90.0, 0.0));
+        visitor.visit(new Point(90.0, 0.0));
+        assertCrossesDateline(visitor, false);
+        visitor.visit(new Point(-89.0, 0.0));
+        visitor.visit(new Point(89.0, 0.0));
+        assertCrossesDateline(visitor, false);
+        visitor.visit(new Point(-100.0, 0.0));
+        visitor.visit(new Point(100.0, 0.0));
+        assertCrossesDateline(visitor, true);
+        visitor.visit(new Point(-70.0, 0.0));
+        visitor.visit(new Point(70.0, 0.0));
+        assertCrossesDateline(visitor, false);
+        visitor.visit(new Point(-120.0, 0.0));
+        visitor.visit(new Point(120.0, 0.0));
+        assertCrossesDateline(visitor, true);
+    }
+
+    private void assertCrossesDateline(SpatialEnvelopeVisitor visitor, boolean crossesDateline) {
+        var result = visitor.getResult();
+        if (crossesDateline) {
+            assertThat("Crosses dateline, minx>maxx", result.getMinX(), greaterThanOrEqualTo(result.getMaxX()));
+        } else {
+            assertThat("Does not cross dateline, minx<maxx", result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
+        }
+    }
+
+    private void assertRectangleResult(
+        String s,
+        Rectangle result,
+        double minX,
+        double maxX,
+        double maxY,
+        double minY,
+        boolean crossesDateline
+    ) {
+        if (crossesDateline) {
+            assertThat(s, result.getMinX(), greaterThanOrEqualTo(result.getMaxX()));
+        } else {
+            assertThat(s, result.getMinX(), lessThanOrEqualTo(result.getMaxX()));
+        }
+        assertThat(s, result.getMinX(), equalTo(minX));
+        assertThat(s, result.getMaxX(), equalTo(maxX));
+        assertThat(s, result.getMaxY(), equalTo(maxY));
+        assertThat(s, result.getMinY(), equalTo(minY));
+    }
+}

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

@@ -318,6 +318,47 @@ wkt:keyword                               |pt:geo_shape
 "POINT(111)"                              |null
 ;
 
+###############################################
+# Tests for GEO_SHAPE type with ST_ENVELOPE, ST_XMIN, etc.
+#
+
+polygonEnvelope
+required_capability: st_envelope
+
+// tag::st_envelope[]
+FROM airport_city_boundaries
+| WHERE abbrev == "CPH"
+| EVAL envelope = ST_ENVELOPE(city_boundary)
+| KEEP abbrev, airport, envelope
+// end::st_envelope[]
+| LIMIT 1
+;
+
+// tag::st_envelope-result[]
+abbrev:keyword | airport:text  | envelope:geo_shape
+CPH            |  Copenhagen   | BBOX(12.453, 12.6398, 55.7327, 55.6318)
+// end::st_envelope-result[]
+;
+
+polygonEnvelopeXYMinMax
+required_capability: st_envelope
+
+// tag::st_x_y_min_max[]
+FROM airport_city_boundaries
+| WHERE abbrev == "CPH"
+| EVAL envelope = ST_ENVELOPE(city_boundary)
+| EVAL xmin = ST_XMIN(envelope), xmax = ST_XMAX(envelope), ymin = ST_YMIN(envelope), ymax = ST_YMAX(envelope)
+| KEEP abbrev, airport, xmin, xmax, ymin, ymax
+// end::st_x_y_min_max[]
+| LIMIT 1
+;
+
+// tag::st_x_y_min_max-result[]
+abbrev:keyword | airport:text  | xmin:double | xmax:double | ymin:double | ymax:double 
+CPH            |  Copenhagen   | 12.453      | 12.6398     | 55.6318     | 55.7327
+// end::st_x_y_min_max-result[]
+;
+
 ###############################################
 # Tests for CARTESIAN_SHAPE type
 #

+ 126 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeFromWKBEvaluator.java

@@ -0,0 +1,126 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StEnvelope}.
+ * This class is generated. Do not edit it.
+ */
+public final class StEnvelopeFromWKBEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StEnvelopeFromWKBEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StEnvelopeFromWKB";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantBytesRefBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (BytesRefBlock.Builder builder = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendBytesRef(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static BytesRef evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StEnvelope.fromWellKnownBinary(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (BytesRefBlock.Builder builder = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            BytesRef value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendBytesRef(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static BytesRef evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StEnvelope.fromWellKnownBinary(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StEnvelopeFromWKBEvaluator get(DriverContext context) {
+      return new StEnvelopeFromWKBEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StEnvelopeFromWKBEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 126 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeFromWKBGeoEvaluator.java

@@ -0,0 +1,126 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StEnvelope}.
+ * This class is generated. Do not edit it.
+ */
+public final class StEnvelopeFromWKBGeoEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StEnvelopeFromWKBGeoEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StEnvelopeFromWKBGeo";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantBytesRefBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (BytesRefBlock.Builder builder = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendBytesRef(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static BytesRef evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StEnvelope.fromWellKnownBinaryGeo(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (BytesRefBlock.Builder builder = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            BytesRef value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendBytesRef(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static BytesRef evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StEnvelope.fromWellKnownBinaryGeo(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StEnvelopeFromWKBGeoEvaluator get(DriverContext context) {
+      return new StEnvelopeFromWKBGeoEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StEnvelopeFromWKBGeoEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 127 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxFromWKBEvaluator.java

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StXMax}.
+ * This class is generated. Do not edit it.
+ */
+public final class StXMaxFromWKBEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StXMaxFromWKBEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StXMaxFromWKB";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StXMax.fromWellKnownBinary(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StXMax.fromWellKnownBinary(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StXMaxFromWKBEvaluator get(DriverContext context) {
+      return new StXMaxFromWKBEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StXMaxFromWKBEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 127 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxFromWKBGeoEvaluator.java

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StXMax}.
+ * This class is generated. Do not edit it.
+ */
+public final class StXMaxFromWKBGeoEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StXMaxFromWKBGeoEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StXMaxFromWKBGeo";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StXMax.fromWellKnownBinaryGeo(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StXMax.fromWellKnownBinaryGeo(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StXMaxFromWKBGeoEvaluator get(DriverContext context) {
+      return new StXMaxFromWKBGeoEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StXMaxFromWKBGeoEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 127 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinFromWKBEvaluator.java

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StXMin}.
+ * This class is generated. Do not edit it.
+ */
+public final class StXMinFromWKBEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StXMinFromWKBEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StXMinFromWKB";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StXMin.fromWellKnownBinary(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StXMin.fromWellKnownBinary(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StXMinFromWKBEvaluator get(DriverContext context) {
+      return new StXMinFromWKBEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StXMinFromWKBEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 127 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinFromWKBGeoEvaluator.java

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StXMin}.
+ * This class is generated. Do not edit it.
+ */
+public final class StXMinFromWKBGeoEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StXMinFromWKBGeoEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StXMinFromWKBGeo";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StXMin.fromWellKnownBinaryGeo(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StXMin.fromWellKnownBinaryGeo(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StXMinFromWKBGeoEvaluator get(DriverContext context) {
+      return new StXMinFromWKBGeoEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StXMinFromWKBGeoEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 127 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxFromWKBEvaluator.java

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StYMax}.
+ * This class is generated. Do not edit it.
+ */
+public final class StYMaxFromWKBEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StYMaxFromWKBEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StYMaxFromWKB";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StYMax.fromWellKnownBinary(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StYMax.fromWellKnownBinary(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StYMaxFromWKBEvaluator get(DriverContext context) {
+      return new StYMaxFromWKBEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StYMaxFromWKBEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 127 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxFromWKBGeoEvaluator.java

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StYMax}.
+ * This class is generated. Do not edit it.
+ */
+public final class StYMaxFromWKBGeoEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StYMaxFromWKBGeoEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StYMaxFromWKBGeo";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StYMax.fromWellKnownBinaryGeo(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StYMax.fromWellKnownBinaryGeo(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StYMaxFromWKBGeoEvaluator get(DriverContext context) {
+      return new StYMaxFromWKBGeoEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StYMaxFromWKBGeoEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 127 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinFromWKBEvaluator.java

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StYMin}.
+ * This class is generated. Do not edit it.
+ */
+public final class StYMinFromWKBEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StYMinFromWKBEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StYMinFromWKB";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StYMin.fromWellKnownBinary(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StYMin.fromWellKnownBinary(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StYMinFromWKBEvaluator get(DriverContext context) {
+      return new StYMinFromWKBEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StYMinFromWKBEvaluator[field=" + field + "]";
+    }
+  }
+}

+ 127 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinFromWKBGeoEvaluator.java

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StYMin}.
+ * This class is generated. Do not edit it.
+ */
+public final class StYMinFromWKBGeoEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StYMinFromWKBGeoEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StYMinFromWKBGeo";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StYMin.fromWellKnownBinaryGeo(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StYMin.fromWellKnownBinaryGeo(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StYMinFromWKBGeoEvaluator get(DriverContext context) {
+      return new StYMinFromWKBGeoEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StYMinFromWKBGeoEvaluator[field=" + field + "]";
+    }
+  }
+}

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

@@ -207,6 +207,11 @@ public class EsqlCapabilities {
          */
         SPATIAL_CENTROID_NO_RECORDS,
 
+        /**
+         * Support ST_ENVELOPE function (and related ST_XMIN, etc.).
+         */
+        ST_ENVELOPE,
+
         /**
          * Fix to GROK and DISSECT that allows extracting attributes with the same name as the input
          * https://github.com/elastic/elasticsearch/issues/110184

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

@@ -57,8 +57,13 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialDi
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialIntersects;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialWithin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StDistance;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StEnvelope;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMax;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StY;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StYMax;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StYMin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.ByteLength;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.LTrim;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Length;
@@ -166,6 +171,11 @@ public class ExpressionWritables {
         entries.add(Sinh.ENTRY);
         entries.add(Space.ENTRY);
         entries.add(Sqrt.ENTRY);
+        entries.add(StEnvelope.ENTRY);
+        entries.add(StXMax.ENTRY);
+        entries.add(StXMin.ENTRY);
+        entries.add(StYMax.ENTRY);
+        entries.add(StYMin.ENTRY);
         entries.add(StX.ENTRY);
         entries.add(StY.ENTRY);
         entries.add(Tan.ENTRY);

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

@@ -118,8 +118,13 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialDi
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialIntersects;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialWithin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StDistance;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StEnvelope;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMax;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StY;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StYMax;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StYMin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.BitLength;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.ByteLength;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat;
@@ -352,6 +357,11 @@ public class EsqlFunctionRegistry {
                 def(SpatialIntersects.class, SpatialIntersects::new, "st_intersects"),
                 def(SpatialWithin.class, SpatialWithin::new, "st_within"),
                 def(StDistance.class, StDistance::new, "st_distance"),
+                def(StEnvelope.class, StEnvelope::new, "st_envelope"),
+                def(StXMax.class, StXMax::new, "st_xmax"),
+                def(StXMin.class, StXMin::new, "st_xmin"),
+                def(StYMax.class, StYMax::new, "st_ymax"),
+                def(StYMin.class, StYMin::new, "st_ymin"),
                 def(StX.class, StX::new, "st_x"),
                 def(StY.class, StY::new, "st_y") },
             // conditional

+ 138 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelope.java

@@ -0,0 +1,138 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.compute.ann.ConvertEvaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+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;
+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.UnaryScalarFunction;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.CARTESIAN_SHAPE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.NULL;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatial;
+
+/**
+ * Determines the minimum bounding rectangle of a geometry.
+ * The function `st_envelope` is defined in the <a href="https://www.ogc.org/standard/sfs/">OGC Simple Feature Access</a> standard.
+ * Alternatively it is well described in PostGIS documentation at
+ * <a href="https://postgis.net/docs/ST_ENVELOPE.html">PostGIS:ST_ENVELOPE</a>.
+ */
+public class StEnvelope extends UnaryScalarFunction {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
+        Expression.class,
+        "StEnvelope",
+        StEnvelope::new
+    );
+    private DataType dataType;
+
+    @FunctionInfo(
+        returnType = { "geo_shape", "cartesian_shape" },
+        description = "Determines the minimum bounding box of the supplied geometry.",
+        examples = @Example(file = "spatial_shapes", tag = "st_envelope")
+    )
+    public StEnvelope(
+        Source source,
+        @Param(
+            name = "geometry",
+            type = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" },
+            description = "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. "
+                + "If `null`, the function returns `null`."
+        ) Expression field
+    ) {
+        super(source, field);
+    }
+
+    private StEnvelope(StreamInput in) throws IOException {
+        super(in);
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        var resolution = isSpatial(field(), sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+        if (resolution.resolved()) {
+            this.dataType = switch (field().dataType()) {
+                case GEO_POINT, GEO_SHAPE -> GEO_SHAPE;
+                case CARTESIAN_POINT, CARTESIAN_SHAPE -> CARTESIAN_SHAPE;
+                default -> NULL;
+            };
+        }
+        return resolution;
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+        if (field().dataType() == GEO_POINT || field().dataType() == DataType.GEO_SHAPE) {
+            return new StEnvelopeFromWKBGeoEvaluator.Factory(toEvaluator.apply(field()), source());
+        }
+        return new StEnvelopeFromWKBEvaluator.Factory(toEvaluator.apply(field()), source());
+    }
+
+    @Override
+    public DataType dataType() {
+        return dataType;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new StEnvelope(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, StEnvelope::new, field());
+    }
+
+    @ConvertEvaluator(extraName = "FromWKB", warnExceptions = { IllegalArgumentException.class })
+    static BytesRef fromWellKnownBinary(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point) {
+            return wkb;
+        }
+        var envelope = SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return UNSPECIFIED.asWkb(envelope.get());
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+
+    @ConvertEvaluator(extraName = "FromWKBGeo", warnExceptions = { IllegalArgumentException.class })
+    static BytesRef fromWellKnownBinaryGeo(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point) {
+            return wkb;
+        }
+        var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true);
+        if (envelope.isPresent()) {
+            return UNSPECIFIED.asWkb(envelope.get());
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+}

+ 123 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMax.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.compute.ann.ConvertEvaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+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;
+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.UnaryScalarFunction;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatial;
+
+/**
+ * Determines the maximum value of the x-coordinate from a geometry.
+ * The function `st_xmax` is defined in the <a href="https://www.ogc.org/standard/sfs/">OGC Simple Feature Access</a> standard.
+ * Alternatively it is well described in PostGIS documentation at <a href="https://postgis.net/docs/ST_XMAX.html">PostGIS:ST_XMAX</a>.
+ */
+public class StXMax extends UnaryScalarFunction {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "StXMax", StXMax::new);
+
+    @FunctionInfo(
+        returnType = "double",
+        description = "Extracts the maximum value of the `x` coordinates from the supplied geometry.\n"
+            + "If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the maximum `longitude` value.",
+        examples = @Example(file = "spatial_shapes", tag = "st_x_y_min_max")
+    )
+    public StXMax(
+        Source source,
+        @Param(
+            name = "point",
+            type = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" },
+            description = "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. "
+                + "If `null`, the function returns `null`."
+        ) Expression field
+    ) {
+        super(source, field);
+    }
+
+    private StXMax(StreamInput in) throws IOException {
+        super(in);
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        return isSpatial(field(), sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+        if (field().dataType() == GEO_POINT || field().dataType() == DataType.GEO_SHAPE) {
+            return new StXMaxFromWKBGeoEvaluator.Factory(toEvaluator.apply(field()), source());
+        }
+        return new StXMaxFromWKBEvaluator.Factory(toEvaluator.apply(field()), source());
+    }
+
+    @Override
+    public DataType dataType() {
+        return DOUBLE;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new StXMax(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, StXMax::new, field());
+    }
+
+    @ConvertEvaluator(extraName = "FromWKB", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinary(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getX();
+        }
+        var envelope = SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return envelope.get().getMaxX();
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+
+    @ConvertEvaluator(extraName = "FromWKBGeo", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinaryGeo(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getX();
+        }
+        var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true);
+        if (envelope.isPresent()) {
+            return envelope.get().getMaxX();
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+}

+ 123 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMin.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.compute.ann.ConvertEvaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+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;
+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.UnaryScalarFunction;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatial;
+
+/**
+ * Determines the minimum value of the x-coordinate from a geometry.
+ * The function `st_xmin` is defined in the <a href="https://www.ogc.org/standard/sfs/">OGC Simple Feature Access</a> standard.
+ * Alternatively it is well described in PostGIS documentation at <a href="https://postgis.net/docs/ST_XMIN.html">PostGIS:ST_XMIN</a>.
+ */
+public class StXMin extends UnaryScalarFunction {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "StXMin", StXMin::new);
+
+    @FunctionInfo(
+        returnType = "double",
+        description = "Extracts the minimum value of the `x` coordinates from the supplied geometry.\n"
+            + "If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the minimum `longitude` value.",
+        examples = @Example(file = "spatial_shapes", tag = "st_x_y_min_max")
+    )
+    public StXMin(
+        Source source,
+        @Param(
+            name = "point",
+            type = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" },
+            description = "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. "
+                + "If `null`, the function returns `null`."
+        ) Expression field
+    ) {
+        super(source, field);
+    }
+
+    private StXMin(StreamInput in) throws IOException {
+        super(in);
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        return isSpatial(field(), sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+        if (field().dataType() == GEO_POINT || field().dataType() == DataType.GEO_SHAPE) {
+            return new StXMinFromWKBGeoEvaluator.Factory(toEvaluator.apply(field()), source());
+        }
+        return new StXMinFromWKBEvaluator.Factory(toEvaluator.apply(field()), source());
+    }
+
+    @Override
+    public DataType dataType() {
+        return DOUBLE;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new StXMin(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, StXMin::new, field());
+    }
+
+    @ConvertEvaluator(extraName = "FromWKB", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinary(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getX();
+        }
+        var envelope = SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return envelope.get().getMinX();
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+
+    @ConvertEvaluator(extraName = "FromWKBGeo", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinaryGeo(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getX();
+        }
+        var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true);
+        if (envelope.isPresent()) {
+            return envelope.get().getMinX();
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+}

+ 123 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMax.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.compute.ann.ConvertEvaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+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;
+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.UnaryScalarFunction;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatial;
+
+/**
+ * Determines the maximum value of the y-coordinate from a geometry.
+ * The function `st_ymax` is defined in the <a href="https://www.ogc.org/standard/sfs/">OGC Simple Feature Access</a> standard.
+ * Alternatively it is well described in PostGIS documentation at <a href="https://postgis.net/docs/ST_YMAX.html">PostGIS:ST_YMAX</a>.
+ */
+public class StYMax extends UnaryScalarFunction {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "StYMax", StYMax::new);
+
+    @FunctionInfo(
+        returnType = "double",
+        description = "Extracts the maximum value of the `y` coordinates from the supplied geometry.\n"
+            + "If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the maximum `latitude` value.",
+        examples = @Example(file = "spatial_shapes", tag = "st_x_y_min_max")
+    )
+    public StYMax(
+        Source source,
+        @Param(
+            name = "point",
+            type = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" },
+            description = "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. "
+                + "If `null`, the function returns `null`."
+        ) Expression field
+    ) {
+        super(source, field);
+    }
+
+    private StYMax(StreamInput in) throws IOException {
+        super(in);
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        return isSpatial(field(), sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+        if (field().dataType() == GEO_POINT || field().dataType() == DataType.GEO_SHAPE) {
+            return new StYMaxFromWKBGeoEvaluator.Factory(toEvaluator.apply(field()), source());
+        }
+        return new StYMaxFromWKBEvaluator.Factory(toEvaluator.apply(field()), source());
+    }
+
+    @Override
+    public DataType dataType() {
+        return DOUBLE;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new StYMax(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, StYMax::new, field());
+    }
+
+    @ConvertEvaluator(extraName = "FromWKB", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinary(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getY();
+        }
+        var envelope = SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return envelope.get().getMaxY();
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+
+    @ConvertEvaluator(extraName = "FromWKBGeo", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinaryGeo(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getY();
+        }
+        var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true);
+        if (envelope.isPresent()) {
+            return envelope.get().getMaxY();
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+}

+ 123 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMin.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.compute.ann.ConvertEvaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+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;
+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.UnaryScalarFunction;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatial;
+
+/**
+ * Determines the minimum value of the y-coordinate from a geometry.
+ * The function `st_ymin` is defined in the <a href="https://www.ogc.org/standard/sfs/">OGC Simple Feature Access</a> standard.
+ * Alternatively it is well described in PostGIS documentation at <a href="https://postgis.net/docs/ST_YMIN.html">PostGIS:ST_YMIN</a>.
+ */
+public class StYMin extends UnaryScalarFunction {
+    public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "StYMin", StYMin::new);
+
+    @FunctionInfo(
+        returnType = "double",
+        description = "Extracts the minimum value of the `y` coordinates from the supplied geometry.\n"
+            + "If the geometry is of type `geo_point` or `geo_shape` this is equivalent to extracting the minimum `latitude` value.",
+        examples = @Example(file = "spatial_shapes", tag = "st_x_y_min_max")
+    )
+    public StYMin(
+        Source source,
+        @Param(
+            name = "point",
+            type = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" },
+            description = "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. "
+                + "If `null`, the function returns `null`."
+        ) Expression field
+    ) {
+        super(source, field);
+    }
+
+    private StYMin(StreamInput in) throws IOException {
+        super(in);
+    }
+
+    @Override
+    public String getWriteableName() {
+        return ENTRY.name;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        return isSpatial(field(), sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+        if (field().dataType() == GEO_POINT || field().dataType() == DataType.GEO_SHAPE) {
+            return new StYMinFromWKBGeoEvaluator.Factory(toEvaluator.apply(field()), source());
+        }
+        return new StYMinFromWKBEvaluator.Factory(toEvaluator.apply(field()), source());
+    }
+
+    @Override
+    public DataType dataType() {
+        return DOUBLE;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new StYMin(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, StYMin::new, field());
+    }
+
+    @ConvertEvaluator(extraName = "FromWKB", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinary(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getY();
+        }
+        var envelope = SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return envelope.get().getMinY();
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+
+    @ConvertEvaluator(extraName = "FromWKBGeo", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinaryGeo(BytesRef wkb) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getY();
+        }
+        var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true);
+        if (envelope.isPresent()) {
+            return envelope.get().getMinY();
+        }
+        throw new IllegalArgumentException("Cannot determine envelope of geometry");
+    }
+}

+ 88 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeTests.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.CARTESIAN_SHAPE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+@FunctionName("st_envelope")
+public class StEnvelopeTests extends AbstractScalarFunctionTestCase {
+    public StEnvelopeTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        String expectedGeo = "StEnvelopeFromWKBGeoEvaluator[field=Attribute[channel=0]]";
+        String expectedCartesian = "StEnvelopeFromWKBEvaluator[field=Attribute[channel=0]]";
+        final List<TestCaseSupplier> suppliers = new ArrayList<>();
+        TestCaseSupplier.forUnaryGeoPoint(suppliers, expectedGeo, GEO_SHAPE, StEnvelopeTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianPoint(
+            suppliers,
+            expectedCartesian,
+            CARTESIAN_SHAPE,
+            StEnvelopeTests::valueOfCartesian,
+            List.of()
+        );
+        TestCaseSupplier.forUnaryGeoShape(suppliers, expectedGeo, GEO_SHAPE, StEnvelopeTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianShape(
+            suppliers,
+            expectedCartesian,
+            CARTESIAN_SHAPE,
+            StEnvelopeTests::valueOfCartesian,
+            List.of()
+        );
+        return parameterSuppliersFromTypedDataWithDefaultChecks(
+            false,
+            suppliers,
+            (v, p) -> "geo_point, cartesian_point, geo_shape or cartesian_shape"
+        );
+    }
+
+    private static BytesRef valueOfGeo(BytesRef wkb) {
+        return valueOf(wkb, true);
+    }
+
+    private static BytesRef valueOfCartesian(BytesRef wkb) {
+        return valueOf(wkb, false);
+    }
+
+    private static BytesRef valueOf(BytesRef wkb, boolean geo) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point) {
+            return wkb;
+        }
+        var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return UNSPECIFIED.asWkb(envelope.get());
+        }
+        throw new IllegalArgumentException("Geometry is empty");
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new StEnvelope(source, args.get(0));
+    }
+}

+ 75 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxTests.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+@FunctionName("st_xmax")
+public class StXMaxTests extends AbstractScalarFunctionTestCase {
+    public StXMaxTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        String expectedGeo = "StXMaxFromWKBGeoEvaluator[field=Attribute[channel=0]]";
+        String expectedCartesian = "StXMaxFromWKBEvaluator[field=Attribute[channel=0]]";
+        final List<TestCaseSupplier> suppliers = new ArrayList<>();
+        TestCaseSupplier.forUnaryGeoPoint(suppliers, expectedGeo, DOUBLE, StXMaxTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianPoint(suppliers, expectedCartesian, DOUBLE, StXMaxTests::valueOfCartesian, List.of());
+        TestCaseSupplier.forUnaryGeoShape(suppliers, expectedGeo, DOUBLE, StXMaxTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianShape(suppliers, expectedCartesian, DOUBLE, StXMaxTests::valueOfCartesian, List.of());
+        return parameterSuppliersFromTypedDataWithDefaultChecks(
+            true,
+            suppliers,
+            (v, p) -> "geo_point, cartesian_point, geo_shape or cartesian_shape"
+        );
+    }
+
+    private static double valueOfGeo(BytesRef wkb) {
+        return valueOf(wkb, true);
+    }
+
+    private static double valueOfCartesian(BytesRef wkb) {
+        return valueOf(wkb, false);
+    }
+
+    private static double valueOf(BytesRef wkb, boolean geo) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getX();
+        }
+        var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return envelope.get().getMaxX();
+        }
+        throw new IllegalArgumentException("Geometry is empty");
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new StXMax(source, args.get(0));
+    }
+}

+ 75 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinTests.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+@FunctionName("st_xmin")
+public class StXMinTests extends AbstractScalarFunctionTestCase {
+    public StXMinTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        String expectedGeo = "StXMinFromWKBGeoEvaluator[field=Attribute[channel=0]]";
+        String expectedCartesian = "StXMinFromWKBEvaluator[field=Attribute[channel=0]]";
+        final List<TestCaseSupplier> suppliers = new ArrayList<>();
+        TestCaseSupplier.forUnaryGeoPoint(suppliers, expectedGeo, DOUBLE, StXMinTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianPoint(suppliers, expectedCartesian, DOUBLE, StXMinTests::valueOfCartesian, List.of());
+        TestCaseSupplier.forUnaryGeoShape(suppliers, expectedGeo, DOUBLE, StXMinTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianShape(suppliers, expectedCartesian, DOUBLE, StXMinTests::valueOfCartesian, List.of());
+        return parameterSuppliersFromTypedDataWithDefaultChecks(
+            true,
+            suppliers,
+            (v, p) -> "geo_point, cartesian_point, geo_shape or cartesian_shape"
+        );
+    }
+
+    private static double valueOfGeo(BytesRef wkb) {
+        return valueOf(wkb, true);
+    }
+
+    private static double valueOfCartesian(BytesRef wkb) {
+        return valueOf(wkb, false);
+    }
+
+    private static double valueOf(BytesRef wkb, boolean geo) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getX();
+        }
+        var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return envelope.get().getMinX();
+        }
+        throw new IllegalArgumentException("Geometry is empty");
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new StXMin(source, args.get(0));
+    }
+}

+ 75 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxTests.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+@FunctionName("st_ymax")
+public class StYMaxTests extends AbstractScalarFunctionTestCase {
+    public StYMaxTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        String expectedGeo = "StYMaxFromWKBGeoEvaluator[field=Attribute[channel=0]]";
+        String expectedCartesian = "StYMaxFromWKBEvaluator[field=Attribute[channel=0]]";
+        final List<TestCaseSupplier> suppliers = new ArrayList<>();
+        TestCaseSupplier.forUnaryGeoPoint(suppliers, expectedGeo, DOUBLE, StYMaxTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianPoint(suppliers, expectedCartesian, DOUBLE, StYMaxTests::valueOfCartesian, List.of());
+        TestCaseSupplier.forUnaryGeoShape(suppliers, expectedGeo, DOUBLE, StYMaxTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianShape(suppliers, expectedCartesian, DOUBLE, StYMaxTests::valueOfCartesian, List.of());
+        return parameterSuppliersFromTypedDataWithDefaultChecks(
+            true,
+            suppliers,
+            (v, p) -> "geo_point, cartesian_point, geo_shape or cartesian_shape"
+        );
+    }
+
+    private static double valueOfGeo(BytesRef wkb) {
+        return valueOf(wkb, true);
+    }
+
+    private static double valueOfCartesian(BytesRef wkb) {
+        return valueOf(wkb, false);
+    }
+
+    private static double valueOf(BytesRef wkb, boolean geo) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getY();
+        }
+        var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return envelope.get().getMaxY();
+        }
+        throw new IllegalArgumentException("Geometry is empty");
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new StYMax(source, args.get(0));
+    }
+}

+ 75 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinTests.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+@FunctionName("st_ymin")
+public class StYMinTests extends AbstractScalarFunctionTestCase {
+    public StYMinTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        String expectedGeo = "StYMinFromWKBGeoEvaluator[field=Attribute[channel=0]]";
+        String expectedCartesian = "StYMinFromWKBEvaluator[field=Attribute[channel=0]]";
+        final List<TestCaseSupplier> suppliers = new ArrayList<>();
+        TestCaseSupplier.forUnaryGeoPoint(suppliers, expectedGeo, DOUBLE, StYMinTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianPoint(suppliers, expectedCartesian, DOUBLE, StYMinTests::valueOfCartesian, List.of());
+        TestCaseSupplier.forUnaryGeoShape(suppliers, expectedGeo, DOUBLE, StYMinTests::valueOfGeo, List.of());
+        TestCaseSupplier.forUnaryCartesianShape(suppliers, expectedCartesian, DOUBLE, StYMinTests::valueOfCartesian, List.of());
+        return parameterSuppliersFromTypedDataWithDefaultChecks(
+            true,
+            suppliers,
+            (v, p) -> "geo_point, cartesian_point, geo_shape or cartesian_shape"
+        );
+    }
+
+    private static double valueOfGeo(BytesRef wkb) {
+        return valueOf(wkb, true);
+    }
+
+    private static double valueOfCartesian(BytesRef wkb) {
+        return valueOf(wkb, false);
+    }
+
+    private static double valueOf(BytesRef wkb, boolean geo) {
+        var geometry = UNSPECIFIED.wkbToGeometry(wkb);
+        if (geometry instanceof Point point) {
+            return point.getY();
+        }
+        var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry);
+        if (envelope.isPresent()) {
+            return envelope.get().getMinY();
+        }
+        throw new IllegalArgumentException("Geometry is empty");
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new StYMin(source, args.get(0));
+    }
+}

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

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