diff --git a/docs/changelog/116964.yaml b/docs/changelog/116964.yaml
new file mode 100644
index 0000000000000..2e3ecd06fa098
--- /dev/null
+++ b/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
diff --git a/docs/reference/esql/functions/description/st_envelope.asciidoc b/docs/reference/esql/functions/description/st_envelope.asciidoc
new file mode 100644
index 0000000000000..6b7cf8d97538a
--- /dev/null
+++ b/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.
diff --git a/docs/reference/esql/functions/description/st_xmax.asciidoc b/docs/reference/esql/functions/description/st_xmax.asciidoc
new file mode 100644
index 0000000000000..f33ec590bf2d4
--- /dev/null
+++ b/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.
diff --git a/docs/reference/esql/functions/description/st_xmin.asciidoc b/docs/reference/esql/functions/description/st_xmin.asciidoc
new file mode 100644
index 0000000000000..b06cbfacde7bf
--- /dev/null
+++ b/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.
diff --git a/docs/reference/esql/functions/description/st_ymax.asciidoc b/docs/reference/esql/functions/description/st_ymax.asciidoc
new file mode 100644
index 0000000000000..f9475dd967562
--- /dev/null
+++ b/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.
diff --git a/docs/reference/esql/functions/description/st_ymin.asciidoc b/docs/reference/esql/functions/description/st_ymin.asciidoc
new file mode 100644
index 0000000000000..7228c63a16030
--- /dev/null
+++ b/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.
diff --git a/docs/reference/esql/functions/examples/st_envelope.asciidoc b/docs/reference/esql/functions/examples/st_envelope.asciidoc
new file mode 100644
index 0000000000000..df8c0ad5607fa
--- /dev/null
+++ b/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]
+|===
+
diff --git a/docs/reference/esql/functions/examples/st_xmax.asciidoc b/docs/reference/esql/functions/examples/st_xmax.asciidoc
new file mode 100644
index 0000000000000..5bba1761cf29c
--- /dev/null
+++ b/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]
+|===
+
diff --git a/docs/reference/esql/functions/examples/st_xmin.asciidoc b/docs/reference/esql/functions/examples/st_xmin.asciidoc
new file mode 100644
index 0000000000000..5bba1761cf29c
--- /dev/null
+++ b/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]
+|===
+
diff --git a/docs/reference/esql/functions/examples/st_ymax.asciidoc b/docs/reference/esql/functions/examples/st_ymax.asciidoc
new file mode 100644
index 0000000000000..5bba1761cf29c
--- /dev/null
+++ b/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]
+|===
+
diff --git a/docs/reference/esql/functions/examples/st_ymin.asciidoc b/docs/reference/esql/functions/examples/st_ymin.asciidoc
new file mode 100644
index 0000000000000..5bba1761cf29c
--- /dev/null
+++ b/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]
+|===
+
diff --git a/docs/reference/esql/functions/kibana/definition/st_envelope.json b/docs/reference/esql/functions/kibana/definition/st_envelope.json
new file mode 100644
index 0000000000000..6c00dda265ac7
--- /dev/null
+++ b/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
+}
diff --git a/docs/reference/esql/functions/kibana/definition/st_xmax.json b/docs/reference/esql/functions/kibana/definition/st_xmax.json
new file mode 100644
index 0000000000000..7be22617c0992
--- /dev/null
+++ b/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
+}
diff --git a/docs/reference/esql/functions/kibana/definition/st_xmin.json b/docs/reference/esql/functions/kibana/definition/st_xmin.json
new file mode 100644
index 0000000000000..8052fdb861cea
--- /dev/null
+++ b/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
+}
diff --git a/docs/reference/esql/functions/kibana/definition/st_ymax.json b/docs/reference/esql/functions/kibana/definition/st_ymax.json
new file mode 100644
index 0000000000000..1a53f7388ea56
--- /dev/null
+++ b/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
+}
diff --git a/docs/reference/esql/functions/kibana/definition/st_ymin.json b/docs/reference/esql/functions/kibana/definition/st_ymin.json
new file mode 100644
index 0000000000000..e11722a8f9c07
--- /dev/null
+++ b/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
+}
diff --git a/docs/reference/esql/functions/kibana/docs/st_envelope.md b/docs/reference/esql/functions/kibana/docs/st_envelope.md
new file mode 100644
index 0000000000000..5f4c3e4809a82
--- /dev/null
+++ b/docs/reference/esql/functions/kibana/docs/st_envelope.md
@@ -0,0 +1,13 @@
+
+
+### 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
+```
diff --git a/docs/reference/esql/functions/kibana/docs/st_xmax.md b/docs/reference/esql/functions/kibana/docs/st_xmax.md
new file mode 100644
index 0000000000000..bbde89df76fd0
--- /dev/null
+++ b/docs/reference/esql/functions/kibana/docs/st_xmax.md
@@ -0,0 +1,15 @@
+
+
+### 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
+```
diff --git a/docs/reference/esql/functions/kibana/docs/st_xmin.md b/docs/reference/esql/functions/kibana/docs/st_xmin.md
new file mode 100644
index 0000000000000..1a6cee7dcfd62
--- /dev/null
+++ b/docs/reference/esql/functions/kibana/docs/st_xmin.md
@@ -0,0 +1,15 @@
+
+
+### 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
+```
diff --git a/docs/reference/esql/functions/kibana/docs/st_ymax.md b/docs/reference/esql/functions/kibana/docs/st_ymax.md
new file mode 100644
index 0000000000000..61c9b6c288ca5
--- /dev/null
+++ b/docs/reference/esql/functions/kibana/docs/st_ymax.md
@@ -0,0 +1,15 @@
+
+
+### 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
+```
diff --git a/docs/reference/esql/functions/kibana/docs/st_ymin.md b/docs/reference/esql/functions/kibana/docs/st_ymin.md
new file mode 100644
index 0000000000000..f5817f10f20a5
--- /dev/null
+++ b/docs/reference/esql/functions/kibana/docs/st_ymin.md
@@ -0,0 +1,15 @@
+
+
+### 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
+```
diff --git a/docs/reference/esql/functions/layout/st_envelope.asciidoc b/docs/reference/esql/functions/layout/st_envelope.asciidoc
new file mode 100644
index 0000000000000..a20d4275e0c9f
--- /dev/null
+++ b/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[]
diff --git a/docs/reference/esql/functions/layout/st_xmax.asciidoc b/docs/reference/esql/functions/layout/st_xmax.asciidoc
new file mode 100644
index 0000000000000..b0c5e7695521e
--- /dev/null
+++ b/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[]
diff --git a/docs/reference/esql/functions/layout/st_xmin.asciidoc b/docs/reference/esql/functions/layout/st_xmin.asciidoc
new file mode 100644
index 0000000000000..55fbad88c4cf0
--- /dev/null
+++ b/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[]
diff --git a/docs/reference/esql/functions/layout/st_ymax.asciidoc b/docs/reference/esql/functions/layout/st_ymax.asciidoc
new file mode 100644
index 0000000000000..e1022de4ba664
--- /dev/null
+++ b/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[]
diff --git a/docs/reference/esql/functions/layout/st_ymin.asciidoc b/docs/reference/esql/functions/layout/st_ymin.asciidoc
new file mode 100644
index 0000000000000..65511e1925e27
--- /dev/null
+++ b/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[]
diff --git a/docs/reference/esql/functions/parameters/st_envelope.asciidoc b/docs/reference/esql/functions/parameters/st_envelope.asciidoc
new file mode 100644
index 0000000000000..a31c6a85de367
--- /dev/null
+++ b/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`.
diff --git a/docs/reference/esql/functions/parameters/st_xmax.asciidoc b/docs/reference/esql/functions/parameters/st_xmax.asciidoc
new file mode 100644
index 0000000000000..788f3485af297
--- /dev/null
+++ b/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`.
diff --git a/docs/reference/esql/functions/parameters/st_xmin.asciidoc b/docs/reference/esql/functions/parameters/st_xmin.asciidoc
new file mode 100644
index 0000000000000..788f3485af297
--- /dev/null
+++ b/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`.
diff --git a/docs/reference/esql/functions/parameters/st_ymax.asciidoc b/docs/reference/esql/functions/parameters/st_ymax.asciidoc
new file mode 100644
index 0000000000000..788f3485af297
--- /dev/null
+++ b/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`.
diff --git a/docs/reference/esql/functions/parameters/st_ymin.asciidoc b/docs/reference/esql/functions/parameters/st_ymin.asciidoc
new file mode 100644
index 0000000000000..788f3485af297
--- /dev/null
+++ b/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`.
diff --git a/docs/reference/esql/functions/signature/st_envelope.svg b/docs/reference/esql/functions/signature/st_envelope.svg
new file mode 100644
index 0000000000000..885a60e6fd86f
--- /dev/null
+++ b/docs/reference/esql/functions/signature/st_envelope.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/esql/functions/signature/st_xmax.svg b/docs/reference/esql/functions/signature/st_xmax.svg
new file mode 100644
index 0000000000000..348d5a7f72763
--- /dev/null
+++ b/docs/reference/esql/functions/signature/st_xmax.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/esql/functions/signature/st_xmin.svg b/docs/reference/esql/functions/signature/st_xmin.svg
new file mode 100644
index 0000000000000..13d479b0458be
--- /dev/null
+++ b/docs/reference/esql/functions/signature/st_xmin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/esql/functions/signature/st_ymax.svg b/docs/reference/esql/functions/signature/st_ymax.svg
new file mode 100644
index 0000000000000..e6ecb00185c84
--- /dev/null
+++ b/docs/reference/esql/functions/signature/st_ymax.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/esql/functions/signature/st_ymin.svg b/docs/reference/esql/functions/signature/st_ymin.svg
new file mode 100644
index 0000000000000..ae722f1edc3d4
--- /dev/null
+++ b/docs/reference/esql/functions/signature/st_ymin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/esql/functions/spatial-functions.asciidoc b/docs/reference/esql/functions/spatial-functions.asciidoc
index eee44d337b4c6..c6a8467b39996 100644
--- a/docs/reference/esql/functions/spatial-functions.asciidoc
+++ b/docs/reference/esql/functions/spatial-functions.asciidoc
@@ -15,6 +15,11 @@
* <>
* <>
* <>
+* experimental:[] <>
+* experimental:[] <>
+* experimental:[] <>
+* experimental:[] <>
+* experimental:[] <>
// 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[]
diff --git a/docs/reference/esql/functions/types/st_envelope.asciidoc b/docs/reference/esql/functions/types/st_envelope.asciidoc
new file mode 100644
index 0000000000000..43355394c6015
--- /dev/null
+++ b/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
+|===
diff --git a/docs/reference/esql/functions/types/st_xmax.asciidoc b/docs/reference/esql/functions/types/st_xmax.asciidoc
new file mode 100644
index 0000000000000..418c5cafae6f3
--- /dev/null
+++ b/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
+|===
diff --git a/docs/reference/esql/functions/types/st_xmin.asciidoc b/docs/reference/esql/functions/types/st_xmin.asciidoc
new file mode 100644
index 0000000000000..418c5cafae6f3
--- /dev/null
+++ b/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
+|===
diff --git a/docs/reference/esql/functions/types/st_ymax.asciidoc b/docs/reference/esql/functions/types/st_ymax.asciidoc
new file mode 100644
index 0000000000000..418c5cafae6f3
--- /dev/null
+++ b/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
+|===
diff --git a/docs/reference/esql/functions/types/st_ymin.asciidoc b/docs/reference/esql/functions/types/st_ymin.asciidoc
new file mode 100644
index 0000000000000..418c5cafae6f3
--- /dev/null
+++ b/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
+|===
diff --git a/libs/geo/src/main/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitor.java b/libs/geo/src/main/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitor.java
new file mode 100644
index 0000000000000..eee4a62c7d588
--- /dev/null
+++ b/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:
+ *
+ *
+ * Cartesian mode: The envelope is determined by the minimum and maximum x/y coordinates.
+ * Incoming BBOX geometries with minX > maxX are treated as invalid.
+ * Resulting BBOX geometries will always have minX <= maxX.
+ *
+ *
+ * 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.
+ *
+ * Usage of this is as simple as:
+ *
+ * Optional<Rectangle> bbox = SpatialEnvelopeVisitor.visit(geometry);
+ * if (bbox.isPresent()) {
+ * Rectangle envelope = bbox.get();
+ * // Do stuff with the envelope
+ * }
+ *
+ * It is also possible to create the inner PointVisitor separately, as well as use the visitor for multiple geometries.
+ *
+ * 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 that wishes to modify the behaviour of the visitor can implement the PointVisitor interface,
+ * or extend the existing implementations.
+ */
+public class SpatialEnvelopeVisitor implements GeometryVisitor {
+
+ 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 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 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:
+ *
+ *
Wrapping around the front of the earth, in which case the result will have minx < maxx
+ *
Wrapping around the back of the earth, crossing the dateline, in which case the result will have minx > maxx
+ *
+ */
+ 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();
+ }
+}
diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitorTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitorTests.java
new file mode 100644
index 0000000000000..fc35df295e566
--- /dev/null
+++ b/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 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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeFromWKBGeoEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeFromWKBGeoEvaluator.java
new file mode 100644
index 0000000000000..c61e825c0ee71
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxFromWKBEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxFromWKBEvaluator.java
new file mode 100644
index 0000000000000..0d51ef709c217
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxFromWKBGeoEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxFromWKBGeoEvaluator.java
new file mode 100644
index 0000000000000..3707bf421d550
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinFromWKBEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinFromWKBEvaluator.java
new file mode 100644
index 0000000000000..699402ad68dee
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinFromWKBGeoEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinFromWKBGeoEvaluator.java
new file mode 100644
index 0000000000000..6a8c041595c1c
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxFromWKBEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxFromWKBEvaluator.java
new file mode 100644
index 0000000000000..e8b50099f38f6
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxFromWKBGeoEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxFromWKBGeoEvaluator.java
new file mode 100644
index 0000000000000..00e75f862a86c
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinFromWKBEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinFromWKBEvaluator.java
new file mode 100644
index 0000000000000..cab66683261aa
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinFromWKBGeoEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinFromWKBGeoEvaluator.java
new file mode 100644
index 0000000000000..8bae9d369fbb4
--- /dev/null
+++ b/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 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
index f64c2c2cdbcd4..6853747171048 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
@@ -207,6 +207,11 @@ public enum Cap {
*/
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
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
index 7e2de0094c2ab..febeccdad9d78 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
+++ b/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.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 static List unaryScalars() {
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);
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
index 98dea0ec08db3..e715bda60532a 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
+++ b/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.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 @@ private static FunctionDefinition[][] functions() {
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
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelope.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelope.java
new file mode 100644
index 0000000000000..934991f3a8088
--- /dev/null
+++ b/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 OGC Simple Feature Access standard.
+ * Alternatively it is well described in PostGIS documentation at
+ * PostGIS:ST_ENVELOPE.
+ */
+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 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");
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMax.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMax.java
new file mode 100644
index 0000000000000..d6d710b175113
--- /dev/null
+++ b/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 OGC Simple Feature Access standard.
+ * Alternatively it is well described in PostGIS documentation at PostGIS:ST_XMAX.
+ */
+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 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");
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMin.java
new file mode 100644
index 0000000000000..a5fa11bc11b0f
--- /dev/null
+++ b/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 OGC Simple Feature Access standard.
+ * Alternatively it is well described in PostGIS documentation at PostGIS:ST_XMIN.
+ */
+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 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");
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMax.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMax.java
new file mode 100644
index 0000000000000..fbbea8e024a6b
--- /dev/null
+++ b/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 OGC Simple Feature Access standard.
+ * Alternatively it is well described in PostGIS documentation at PostGIS:ST_YMAX.
+ */
+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 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");
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMin.java
new file mode 100644
index 0000000000000..1707d3b4f2fb9
--- /dev/null
+++ b/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 OGC Simple Feature Access standard.
+ * Alternatively it is well described in PostGIS documentation at PostGIS:ST_YMIN.
+ */
+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 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");
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeTests.java
new file mode 100644
index 0000000000000..ac87d45491447
--- /dev/null
+++ b/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) {
+ this.testCase = testCaseSupplier.get();
+ }
+
+ @ParametersFactory
+ public static Iterable