diff --git a/docs/changelog/139664.yaml b/docs/changelog/139664.yaml
new file mode 100644
index 0000000000000..35761b8877a7e
--- /dev/null
+++ b/docs/changelog/139664.yaml
@@ -0,0 +1,5 @@
+pr: 139664
+summary: Add MV_UNION Function
+area: ES|QL
+type: enhancement
+issues: []
diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/mv_union.md b/docs/reference/query-languages/esql/_snippets/functions/description/mv_union.md
new file mode 100644
index 0000000000000..35ae0b759de6c
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/description/mv_union.md
@@ -0,0 +1,6 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Description**
+
+Returns all unique values from the combined input fields (set union). Null values are treated as empty sets; returns `null` only if both fields are null.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/mv_union.md b/docs/reference/query-languages/esql/_snippets/functions/examples/mv_union.md
new file mode 100644
index 0000000000000..d25f54b000507
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/examples/mv_union.md
@@ -0,0 +1,55 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Examples**
+
+```esql
+ROW a = [1, 2, 3, 4, 5], b = [2, 3, 4, 5, 6]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+```
+
+| finalValue:integer |
+| --- |
+| [1, 2, 3, 4, 5, 6] |
+
+```esql
+ROW a = [1, 2, 3, 4, 5]::long, b = [2, 3, 4, 5, 6]::long
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+```
+
+| finalValue:long |
+| --- |
+| [1, 2, 3, 4, 5, 6] |
+
+```esql
+ROW a = [true, false], b = [false]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+```
+
+| finalValue:boolean |
+| --- |
+| [true, false] |
+
+```esql
+ROW a = [5.2, 10.5, 1.12345], b = [10.5, 2.6928]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+```
+
+| finalValue:double |
+| --- |
+| [5.2, 10.5, 1.12345, 2.6928] |
+
+```esql
+ROW a = ["one", "two", "three"], b = ["two", "four"]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+```
+
+| finalValue:keyword |
+| --- |
+| ["one", "two", "three", "four"] |
+
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/mv_union.md b/docs/reference/query-languages/esql/_snippets/functions/layout/mv_union.md
new file mode 100644
index 0000000000000..a82b7150c543d
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/layout/mv_union.md
@@ -0,0 +1,27 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+## `MV_UNION` [esql-mv_union]
+```{applies_to}
+stack: preview 9.4.0
+serverless: preview
+```
+
+**Syntax**
+
+:::{image} ../../../images/functions/mv_union.svg
+:alt: Embedded
+:class: text-center
+:::
+
+
+:::{include} ../parameters/mv_union.md
+:::
+
+:::{include} ../description/mv_union.md
+:::
+
+:::{include} ../types/mv_union.md
+:::
+
+:::{include} ../examples/mv_union.md
+:::
diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/mv_union.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/mv_union.md
new file mode 100644
index 0000000000000..a0ca1fdc07df2
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/mv_union.md
@@ -0,0 +1,10 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Parameters**
+
+`field1`
+: Multivalue expression. Null values are treated as empty sets.
+
+`field2`
+: Multivalue expression. Null values are treated as empty sets.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/mv_union.md b/docs/reference/query-languages/esql/_snippets/functions/types/mv_union.md
new file mode 100644
index 0000000000000..64515ccdcbab1
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/types/mv_union.md
@@ -0,0 +1,27 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Supported types**
+
+| field1 | field2 | result |
+| --- | --- | --- |
+| boolean | boolean | boolean |
+| cartesian_point | cartesian_point | cartesian_point |
+| cartesian_shape | cartesian_shape | cartesian_shape |
+| date | date | date |
+| date_nanos | date_nanos | date_nanos |
+| double | double | double |
+| geo_point | geo_point | geo_point |
+| geo_shape | geo_shape | geo_shape |
+| geohash | geohash | geohash |
+| geohex | geohex | geohex |
+| geotile | geotile | geotile |
+| integer | integer | integer |
+| ip | ip | ip |
+| keyword | keyword | keyword |
+| keyword | text | keyword |
+| long | long | long |
+| text | keyword | keyword |
+| text | text | keyword |
+| unsigned_long | unsigned_long | unsigned_long |
+| version | version | version |
+
diff --git a/docs/reference/query-languages/esql/_snippets/lists/mv-functions.md b/docs/reference/query-languages/esql/_snippets/lists/mv-functions.md
index 7c067503cc1d7..6413f7524ce5e 100644
--- a/docs/reference/query-languages/esql/_snippets/lists/mv-functions.md
+++ b/docs/reference/query-languages/esql/_snippets/lists/mv-functions.md
@@ -16,4 +16,5 @@
* [`MV_SORT`](../../functions-operators/mv-functions.md#esql-mv_sort)
* [`MV_SLICE`](../../functions-operators/mv-functions.md#esql-mv_slice)
* [`MV_SUM`](../../functions-operators/mv-functions.md#esql-mv_sum)
+* [`MV_UNION`](../../functions-operators/mv-functions.md#esql-mv_union) {applies_to}`stack: preview 9.4` {applies_to}`serverless: preview`
* [`MV_ZIP`](../../functions-operators/mv-functions.md#esql-mv_zip)
diff --git a/docs/reference/query-languages/esql/functions-operators/mv-functions.md b/docs/reference/query-languages/esql/functions-operators/mv-functions.md
index fa7465ab99513..017856dc4d4a3 100644
--- a/docs/reference/query-languages/esql/functions-operators/mv-functions.md
+++ b/docs/reference/query-languages/esql/functions-operators/mv-functions.md
@@ -68,6 +68,9 @@ mapped_pages:
:::{include} ../_snippets/functions/layout/mv_sum.md
:::
+:::{include} ../_snippets/functions/layout/mv_union.md
+:::
+
:::{include} ../_snippets/functions/layout/mv_zip.md
:::
diff --git a/docs/reference/query-languages/esql/images/functions/mv_union.svg b/docs/reference/query-languages/esql/images/functions/mv_union.svg
new file mode 100644
index 0000000000000..f3899e7c58c59
--- /dev/null
+++ b/docs/reference/query-languages/esql/images/functions/mv_union.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/mv_union.json b/docs/reference/query-languages/esql/kibana/definition/functions/mv_union.json
new file mode 100644
index 0000000000000..ce1aca8b23e80
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/mv_union.json
@@ -0,0 +1,377 @@
+{
+ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.",
+ "type" : "scalar",
+ "name" : "mv_union",
+ "description" : "Returns all unique values from the combined input fields (set union). Null values are treated as empty sets; returns `null` only if both fields are null.",
+ "signatures" : [
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "boolean",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "boolean",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "cartesian_point",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "cartesian_point",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "cartesian_point"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "cartesian_shape",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "cartesian_shape",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "cartesian_shape"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "date",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "date",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "date"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "date_nanos",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "date_nanos",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "date_nanos"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "double"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "geo_point",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "geo_point",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "geo_point"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "geo_shape",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "geo_shape",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "geo_shape"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "geohash",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "geohash",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "geohash"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "geohex",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "geohex",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "geohex"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "geotile",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "geotile",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "geotile"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "integer",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "integer"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "ip",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "ip",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "ip"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "keyword",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "keyword",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "keyword"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "keyword",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "text",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "keyword"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "long",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "long"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "text",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "keyword",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "keyword"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "text",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "text",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "keyword"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "unsigned_long"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field1",
+ "type" : "version",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ },
+ {
+ "name" : "field2",
+ "type" : "version",
+ "optional" : false,
+ "description" : "Multivalue expression. Null values are treated as empty sets."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "version"
+ }
+ ],
+ "examples" : [
+ "ROW a = [1, 2, 3, 4, 5], b = [2, 3, 4, 5, 6]\n| EVAL finalValue = MV_UNION(a, b)\n| KEEP finalValue",
+ "ROW a = [1, 2, 3, 4, 5]::long, b = [2, 3, 4, 5, 6]::long\n| EVAL finalValue = MV_UNION(a, b)\n| KEEP finalValue",
+ "ROW a = [true, false], b = [false]\n| EVAL finalValue = MV_UNION(a, b)\n| KEEP finalValue",
+ "ROW a = [5.2, 10.5, 1.12345], b = [10.5, 2.6928]\n| EVAL finalValue = MV_UNION(a, b)\n| KEEP finalValue",
+ "ROW a = [\"one\", \"two\", \"three\"], b = [\"two\", \"four\"]\n| EVAL finalValue = MV_UNION(a, b)\n| KEEP finalValue"
+ ],
+ "preview" : true,
+ "snapshot_only" : false
+}
diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/mv_union.md b/docs/reference/query-languages/esql/kibana/docs/functions/mv_union.md
new file mode 100644
index 0000000000000..17b297a2d654a
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/docs/functions/mv_union.md
@@ -0,0 +1,10 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+### MV UNION
+Returns all unique values from the combined input fields (set union). Null values are treated as empty sets; returns `null` only if both fields are null.
+
+```esql
+ROW a = [1, 2, 3, 4, 5], b = [2, 3, 4, 5, 6]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+```
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_union.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_union.csv-spec
new file mode 100644
index 0000000000000..0a0a076f46e58
--- /dev/null
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_union.csv-spec
@@ -0,0 +1,209 @@
+testMvUnionWithIntValues
+required_capability: fn_mv_union
+// tag::testMvUnionWithIntValues[]
+ROW a = [1, 2, 3, 4, 5], b = [2, 3, 4, 5, 6]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+// end::testMvUnionWithIntValues[]
+;
+// tag::testMvUnionWithIntValues-result[]
+finalValue:integer
+[1, 2, 3, 4, 5, 6]
+// end::testMvUnionWithIntValues-result[]
+;
+
+testMvUnionWithLongValues
+required_capability: fn_mv_union
+// tag::testMvUnionWithLongValues[]
+ROW a = [1, 2, 3, 4, 5]::long, b = [2, 3, 4, 5, 6]::long
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+// end::testMvUnionWithLongValues[]
+;
+// tag::testMvUnionWithLongValues-result[]
+finalValue:long
+[1, 2, 3, 4, 5, 6]
+// end::testMvUnionWithLongValues-result[]
+;
+
+testMvUnionWithBooleanValues
+required_capability: fn_mv_union
+// tag::testMvUnionWithBooleanValues[]
+ROW a = [true, false], b = [false]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+// end::testMvUnionWithBooleanValues[]
+;
+// tag::testMvUnionWithBooleanValues-result[]
+finalValue:boolean
+[true, false]
+// end::testMvUnionWithBooleanValues-result[]
+;
+
+testMvUnionWithDoubleValues
+required_capability: fn_mv_union
+// tag::testMvUnionWithDoubleValues[]
+ROW a = [5.2, 10.5, 1.12345], b = [10.5, 2.6928]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+// end::testMvUnionWithDoubleValues[]
+;
+// tag::testMvUnionWithDoubleValues-result[]
+finalValue:double
+[5.2, 10.5, 1.12345, 2.6928]
+// end::testMvUnionWithDoubleValues-result[]
+;
+
+testMvUnionWithBytesRefValues
+required_capability: fn_mv_union
+// tag::testMvUnionWithBytesRefValues[]
+ROW a = ["one", "two", "three"], b = ["two", "four"]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue
+// end::testMvUnionWithBytesRefValues[]
+;
+// tag::testMvUnionWithBytesRefValues-result[]
+finalValue:keyword
+["one", "two", "three", "four"]
+// end::testMvUnionWithBytesRefValues-result[]
+;
+
+testMvUnionGeoPoint
+required_capability: fn_mv_union
+
+ROW a = ["POINT(42.97109629958868 14.7552534006536)", "POINT(23.23 14.7)"]::geo_point, b = ["POINT(42.97109629958868 14.7552534006536)", "POINT(12.12 11.22)"]::geo_point
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue;
+
+finalValue:geo_point
+["POINT (42.97109629958868 14.7552534006536)", "POINT (23.23 14.7)", "POINT (12.12 11.22)"]
+;
+
+testMvUnionGeoShape
+required_capability: fn_mv_union
+
+ROW a = "POLYGON((1 1, 9 1, 9 9, 1 9, 1 1))"::geo_shape, b = "POLYGON((1 1, 9 1, 9 9, 1 9, 1 1))"::geo_shape
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue;
+
+finalValue:geo_shape
+POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))
+;
+
+testMvUnionIp
+required_capability: fn_mv_union
+
+ROW a = ["1.1.1.1", "2.2.2.2"]::ip, b = ["3.3.3.3", "2.2.2.2"]::ip
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue;
+
+finalValue:ip
+["1.1.1.1", "2.2.2.2", "3.3.3.3"]
+;
+
+testMvUnionVersion
+required_capability: fn_mv_union
+
+ROW a = ["1.2.3", "9.3.0"]::version, b = ["4.5", "9.3.0"]::version
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue;
+
+finalValue:version
+["1.2.3", "9.3.0", "4.5"]
+;
+
+testMvUnionWithSingleValueParam
+required_capability: fn_mv_union
+
+ROW a = 4, b = [4, 5, 6, 7]
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue;
+
+finalValue:integer
+[4, 5, 6, 7]
+;
+
+testMvUnionWithSecondSingleValueParam
+required_capability: fn_mv_union
+
+ROW a = [1, 2, 3, 4], b = 5
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue;
+
+finalValue:integer
+[1, 2, 3, 4, 5]
+;
+
+testMvUnionWithTwoSingleValues
+required_capability: fn_mv_union
+
+ROW a = 1, b = 2
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue;
+
+finalValue:integer
+[1, 2]
+;
+
+testMvUnionWithIdenticalSingleValues
+required_capability: fn_mv_union
+
+ROW a = 1, b = 1
+| EVAL finalValue = MV_UNION(a, b)
+| KEEP finalValue;
+
+finalValue:integer
+1
+;
+
+testMvUnionAgainstAnIndex
+required_capability: fn_mv_union
+
+FROM employees
+| EVAL union_result = MV_UNION(salary_change, [-7.23, 11.17])
+| EVAL union_count = MV_COUNT(union_result)
+| WHERE union_count > 2
+| SORT emp_no
+| LIMIT 5
+| KEEP emp_no, hire_date, salary_change;
+
+emp_no:integer | hire_date:datetime | salary_change:double
+10001 | 1986-06-26T00:00:00.000Z | 1.19
+10003 | 1986-08-28T00:00:00.000Z | [12.82, 14.68]
+10004 | 1986-12-01T00:00:00.000Z | [-0.35, 1.13, 3.65, 13.48]
+10005 | 1989-09-12T00:00:00.000Z | [-2.14, 13.07]
+10006 | 1989-06-02T00:00:00.000Z | -3.9
+;
+
+testMvUnionNullReturnedWhenFirstArgIsNull
+required_capability: fn_mv_union
+
+ROW a = [1, 2, 3, 4]
+| EVAL finalValue = MV_UNION(null, a)
+| KEEP finalValue;
+
+finalValue:integer
+[1, 2, 3, 4]
+;
+
+testMvUnionNullReturnedWhenSecondArgIsNull
+required_capability: fn_mv_union
+
+ROW a = [1, 2, 3, 4]
+| EVAL finalValue = MV_UNION(a, null)
+| KEEP finalValue;
+
+finalValue:integer
+[1, 2, 3, 4]
+;
+
+testMvUnionNullReturnedWhenBothArgsAreNull
+required_capability: fn_mv_union
+
+ROW a = [1, 2, 3, 4]
+| EVAL finalValue = MV_UNION(null, null)
+| KEEP finalValue;
+
+finalValue:null
+null
+;
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionBooleanEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionBooleanEvaluator.java
new file mode 100644
index 0000000000000..72bfce3bb9f3e
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionBooleanEvaluator.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.multivalue;
+
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link MvUnion}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class MvUnionBooleanEvaluator implements EvalOperator.ExpressionEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(MvUnionBooleanEvaluator.class);
+
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator field1;
+
+ private final EvalOperator.ExpressionEvaluator field2;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public MvUnionBooleanEvaluator(Source source, EvalOperator.ExpressionEvaluator field1,
+ EvalOperator.ExpressionEvaluator field2, DriverContext driverContext) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (BooleanBlock field1Block = (BooleanBlock) field1.eval(page)) {
+ try (BooleanBlock field2Block = (BooleanBlock) field2.eval(page)) {
+ return eval(page.getPositionCount(), field1Block, field2Block);
+ }
+ }
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += field1.baseRamBytesUsed();
+ baseRamBytesUsed += field2.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public BooleanBlock eval(int positionCount, BooleanBlock field1Block, BooleanBlock field2Block) {
+ try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ boolean allBlocksAreNulls = true;
+ if (!field1Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (!field2Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (allBlocksAreNulls) {
+ result.appendNull();
+ continue position;
+ }
+ MvUnion.process(result, p, field1Block, field2Block);
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionBooleanEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(field1, field2);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field1;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field2;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field1,
+ EvalOperator.ExpressionEvaluator.Factory field2) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ }
+
+ @Override
+ public MvUnionBooleanEvaluator get(DriverContext context) {
+ return new MvUnionBooleanEvaluator(source, field1.get(context), field2.get(context), context);
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionBooleanEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionBytesRefEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionBytesRefEvaluator.java
new file mode 100644
index 0000000000000..ef55b4e4d3f12
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionBytesRefEvaluator.java
@@ -0,0 +1,128 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.multivalue;
+
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link MvUnion}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class MvUnionBytesRefEvaluator implements EvalOperator.ExpressionEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(MvUnionBytesRefEvaluator.class);
+
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator field1;
+
+ private final EvalOperator.ExpressionEvaluator field2;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public MvUnionBytesRefEvaluator(Source source, EvalOperator.ExpressionEvaluator field1,
+ EvalOperator.ExpressionEvaluator field2, DriverContext driverContext) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (BytesRefBlock field1Block = (BytesRefBlock) field1.eval(page)) {
+ try (BytesRefBlock field2Block = (BytesRefBlock) field2.eval(page)) {
+ return eval(page.getPositionCount(), field1Block, field2Block);
+ }
+ }
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += field1.baseRamBytesUsed();
+ baseRamBytesUsed += field2.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public BytesRefBlock eval(int positionCount, BytesRefBlock field1Block,
+ BytesRefBlock field2Block) {
+ try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ boolean allBlocksAreNulls = true;
+ if (!field1Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (!field2Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (allBlocksAreNulls) {
+ result.appendNull();
+ continue position;
+ }
+ MvUnion.process(result, p, field1Block, field2Block);
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionBytesRefEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(field1, field2);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field1;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field2;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field1,
+ EvalOperator.ExpressionEvaluator.Factory field2) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ }
+
+ @Override
+ public MvUnionBytesRefEvaluator get(DriverContext context) {
+ return new MvUnionBytesRefEvaluator(source, field1.get(context), field2.get(context), context);
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionBytesRefEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionDoubleEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionDoubleEvaluator.java
new file mode 100644
index 0000000000000..2da98f2957fcd
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionDoubleEvaluator.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.multivalue;
+
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link MvUnion}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class MvUnionDoubleEvaluator implements EvalOperator.ExpressionEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(MvUnionDoubleEvaluator.class);
+
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator field1;
+
+ private final EvalOperator.ExpressionEvaluator field2;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public MvUnionDoubleEvaluator(Source source, EvalOperator.ExpressionEvaluator field1,
+ EvalOperator.ExpressionEvaluator field2, DriverContext driverContext) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (DoubleBlock field1Block = (DoubleBlock) field1.eval(page)) {
+ try (DoubleBlock field2Block = (DoubleBlock) field2.eval(page)) {
+ return eval(page.getPositionCount(), field1Block, field2Block);
+ }
+ }
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += field1.baseRamBytesUsed();
+ baseRamBytesUsed += field2.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public DoubleBlock eval(int positionCount, DoubleBlock field1Block, DoubleBlock field2Block) {
+ try(DoubleBlock.Builder result = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ boolean allBlocksAreNulls = true;
+ if (!field1Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (!field2Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (allBlocksAreNulls) {
+ result.appendNull();
+ continue position;
+ }
+ MvUnion.process(result, p, field1Block, field2Block);
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionDoubleEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(field1, field2);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field1;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field2;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field1,
+ EvalOperator.ExpressionEvaluator.Factory field2) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ }
+
+ @Override
+ public MvUnionDoubleEvaluator get(DriverContext context) {
+ return new MvUnionDoubleEvaluator(source, field1.get(context), field2.get(context), context);
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionDoubleEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionIntEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionIntEvaluator.java
new file mode 100644
index 0000000000000..2ea312a91bf65
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionIntEvaluator.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.multivalue;
+
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.IntBlock;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link MvUnion}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class MvUnionIntEvaluator implements EvalOperator.ExpressionEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(MvUnionIntEvaluator.class);
+
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator field1;
+
+ private final EvalOperator.ExpressionEvaluator field2;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public MvUnionIntEvaluator(Source source, EvalOperator.ExpressionEvaluator field1,
+ EvalOperator.ExpressionEvaluator field2, DriverContext driverContext) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (IntBlock field1Block = (IntBlock) field1.eval(page)) {
+ try (IntBlock field2Block = (IntBlock) field2.eval(page)) {
+ return eval(page.getPositionCount(), field1Block, field2Block);
+ }
+ }
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += field1.baseRamBytesUsed();
+ baseRamBytesUsed += field2.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public IntBlock eval(int positionCount, IntBlock field1Block, IntBlock field2Block) {
+ try(IntBlock.Builder result = driverContext.blockFactory().newIntBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ boolean allBlocksAreNulls = true;
+ if (!field1Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (!field2Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (allBlocksAreNulls) {
+ result.appendNull();
+ continue position;
+ }
+ MvUnion.process(result, p, field1Block, field2Block);
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionIntEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(field1, field2);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field1;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field2;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field1,
+ EvalOperator.ExpressionEvaluator.Factory field2) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ }
+
+ @Override
+ public MvUnionIntEvaluator get(DriverContext context) {
+ return new MvUnionIntEvaluator(source, field1.get(context), field2.get(context), context);
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionIntEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionLongEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionLongEvaluator.java
new file mode 100644
index 0000000000000..f9aae8575f3ad
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnionLongEvaluator.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.multivalue;
+
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link MvUnion}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class MvUnionLongEvaluator implements EvalOperator.ExpressionEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(MvUnionLongEvaluator.class);
+
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator field1;
+
+ private final EvalOperator.ExpressionEvaluator field2;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public MvUnionLongEvaluator(Source source, EvalOperator.ExpressionEvaluator field1,
+ EvalOperator.ExpressionEvaluator field2, DriverContext driverContext) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (LongBlock field1Block = (LongBlock) field1.eval(page)) {
+ try (LongBlock field2Block = (LongBlock) field2.eval(page)) {
+ return eval(page.getPositionCount(), field1Block, field2Block);
+ }
+ }
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += field1.baseRamBytesUsed();
+ baseRamBytesUsed += field2.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public LongBlock eval(int positionCount, LongBlock field1Block, LongBlock field2Block) {
+ try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ boolean allBlocksAreNulls = true;
+ if (!field1Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (!field2Block.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (allBlocksAreNulls) {
+ result.appendNull();
+ continue position;
+ }
+ MvUnion.process(result, p, field1Block, field2Block);
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionLongEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(field1, field2);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field1;
+
+ private final EvalOperator.ExpressionEvaluator.Factory field2;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field1,
+ EvalOperator.ExpressionEvaluator.Factory field2) {
+ this.source = source;
+ this.field1 = field1;
+ this.field2 = field2;
+ }
+
+ @Override
+ public MvUnionLongEvaluator get(DriverContext context) {
+ return new MvUnionLongEvaluator(source, field1.get(context), field2.get(context), context);
+ }
+
+ @Override
+ public String toString() {
+ return "MvUnionLongEvaluator[" + "field1=" + field1 + ", field2=" + field2 + "]";
+ }
+ }
+}
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 10d0b807fdf53..51b7269d973bf 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
@@ -1802,6 +1802,11 @@ public enum Cap {
*/
FN_MV_INTERSECTION,
+ /**
+ * Support for the MV_UNION function which returns the set union of two multivalued fields
+ */
+ FN_MV_UNION,
+
/**
* Enables late materialization on node reduce. See also QueryPragmas.NODE_LEVEL_REDUCTION
*/
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 fc7b7133f79be..f7ea76627b937 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
@@ -172,6 +172,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSlice;
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSort;
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSum;
+import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvUnion;
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvZip;
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
import org.elasticsearch.xpack.esql.expression.function.scalar.score.Decay;
@@ -530,6 +531,7 @@ private static FunctionDefinition[][] functions() {
def(MvPSeriesWeightedSum.class, MvPSeriesWeightedSum::new, "mv_pseries_weighted_sum"),
def(MvSort.class, MvSort::new, "mv_sort"),
def(MvSlice.class, MvSlice::new, "mv_slice"),
+ def(MvUnion.class, MvUnion::new, "mv_union"),
def(MvZip.class, MvZip::new, "mv_zip"),
def(MvSum.class, MvSum::new, "mv_sum"),
def(Split.class, Split::new, "split") },
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvFunctionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvFunctionWritables.java
index 657a6fd4560a0..3a902c839be78 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvFunctionWritables.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvFunctionWritables.java
@@ -32,6 +32,7 @@ public static List getNamedWriteables() {
MvSlice.ENTRY,
MvSort.ENTRY,
MvSum.ENTRY,
+ MvUnion.ENTRY,
MvZip.ENTRY
);
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnion.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnion.java
new file mode 100644
index 0000000000000..df64b68589666
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvUnion.java
@@ -0,0 +1,331 @@
+/*
+ * 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.multivalue;
+
+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.Evaluator;
+import org.elasticsearch.compute.ann.Position;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.IntBlock;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.FoldContext;
+import org.elasticsearch.xpack.esql.core.expression.Nullability;
+import org.elasticsearch.xpack.esql.core.expression.function.scalar.BinaryScalarFunction;
+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.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo;
+import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
+import org.elasticsearch.xpack.esql.planner.PlannerUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isRepresentableExceptCountersDenseVectorAggregateMetricDoubleAndHistogram;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
+
+/**
+ * Returns the union of values from two multi-valued fields (all unique values from both inputs).
+ * Example:
+ * Given set A = {"a","b","c"} and set B = {"b","c","d"}, MV_UNION(A, B) returns {"a", "b", "c", "d"}
+ */
+public class MvUnion extends BinaryScalarFunction implements EvaluatorMapper {
+ public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "MvUnion", MvUnion::new);
+
+ private DataType dataType;
+
+ @FunctionInfo(
+ returnType = {
+ "boolean",
+ "cartesian_point",
+ "cartesian_shape",
+ "date",
+ "date_nanos",
+ "double",
+ "geo_point",
+ "geo_shape",
+ "geohash",
+ "geotile",
+ "geohex",
+ "integer",
+ "ip",
+ "keyword",
+ "long",
+ "unsigned_long",
+ "version" },
+ description = "Returns all unique values from the combined input fields (set union). "
+ + "Null values are treated as empty sets; returns `null` only if both fields are null.",
+ preview = true,
+ examples = {
+ @Example(file = "mv_union", tag = "testMvUnionWithIntValues"),
+ @Example(file = "mv_union", tag = "testMvUnionWithLongValues"),
+ @Example(file = "mv_union", tag = "testMvUnionWithBooleanValues"),
+ @Example(file = "mv_union", tag = "testMvUnionWithDoubleValues"),
+ @Example(file = "mv_union", tag = "testMvUnionWithBytesRefValues") },
+ appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.PREVIEW, version = "9.4.0") }
+ )
+ public MvUnion(
+ Source source,
+ @Param(
+ name = "field1",
+ type = {
+ "boolean",
+ "cartesian_point",
+ "cartesian_shape",
+ "date",
+ "date_nanos",
+ "double",
+ "geo_point",
+ "geo_shape",
+ "geohash",
+ "geotile",
+ "geohex",
+ "integer",
+ "ip",
+ "keyword",
+ "long",
+ "text",
+ "unsigned_long",
+ "version" },
+ description = "Multivalue expression. Null values are treated as empty sets."
+ ) Expression field1,
+ @Param(
+ name = "field2",
+ type = {
+ "boolean",
+ "cartesian_point",
+ "cartesian_shape",
+ "date",
+ "date_nanos",
+ "double",
+ "geo_point",
+ "geo_shape",
+ "geohash",
+ "geotile",
+ "geohex",
+ "integer",
+ "ip",
+ "keyword",
+ "long",
+ "text",
+ "unsigned_long",
+ "version" },
+ description = "Multivalue expression. Null values are treated as empty sets."
+ ) Expression field2
+ ) {
+ super(source, field1, field2);
+ }
+
+ private MvUnion(StreamInput in) throws IOException {
+ this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), in.readNamedWriteable(Expression.class));
+ }
+
+ @Override
+ public Object fold(FoldContext ctx) {
+ Object leftVal = left().fold(ctx);
+ Object rightVal = right().fold(ctx);
+
+ // If both are null, return null
+ if (leftVal == null && rightVal == null) {
+ return null;
+ }
+
+ // Treat null as empty set
+ List> leftList = leftVal == null ? List.of() : (leftVal instanceof List> l ? l : List.of(leftVal));
+ List> rightList = rightVal == null ? List.of() : (rightVal instanceof List> l ? l : List.of(rightVal));
+
+ // Compute union using LinkedHashSet to maintain order
+ Set