diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/month_name.md b/docs/reference/query-languages/esql/_snippets/functions/description/month_name.md
new file mode 100644
index 0000000000000..ba0b299c2cad4
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/description/month_name.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 the month name for the provided date based on the configured Locale.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/month_name.md b/docs/reference/query-languages/esql/_snippets/functions/examples/month_name.md
new file mode 100644
index 0000000000000..81fad547bcfb8
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/examples/month_name.md
@@ -0,0 +1,10 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Example**
+
+```esql
+ROW dt = to_datetime("1996-03-21T00:00:00.000Z")
+| EVAL monthName = MONTH_NAME(dt);
+```
+
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/month_name.md b/docs/reference/query-languages/esql/_snippets/functions/layout/month_name.md
new file mode 100644
index 0000000000000..bdd7b834e0ee3
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/layout/month_name.md
@@ -0,0 +1,26 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+## `MONTH_NAME` [esql-month_name]
+```{applies_to}
+stack: ga 9.2.0
+```
+
+**Syntax**
+
+:::{image} ../../../images/functions/month_name.svg
+:alt: Embedded
+:class: text-center
+:::
+
+
+:::{include} ../parameters/month_name.md
+:::
+
+:::{include} ../description/month_name.md
+:::
+
+:::{include} ../types/month_name.md
+:::
+
+:::{include} ../examples/month_name.md
+:::
diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/month_name.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/month_name.md
new file mode 100644
index 0000000000000..43182ad4b2d26
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/month_name.md
@@ -0,0 +1,7 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Parameters**
+
+`date`
+: Date expression. If `null`, the function returns `null`.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/month_name.md b/docs/reference/query-languages/esql/_snippets/functions/types/month_name.md
new file mode 100644
index 0000000000000..097955acd2cfa
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/types/month_name.md
@@ -0,0 +1,9 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Supported types**
+
+| date | result |
+| --- | --- |
+| date | keyword |
+| date_nanos | keyword |
+
diff --git a/docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md b/docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md
index b59db4d50bf58..137e14e3fa52e 100644
--- a/docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md
+++ b/docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md
@@ -4,4 +4,5 @@
* [`DATE_PARSE`](../../functions-operators/date-time-functions.md#esql-date_parse)
* [`DATE_TRUNC`](../../functions-operators/date-time-functions.md#esql-date_trunc)
* [`DAY_NAME`](../../functions-operators/date-time-functions.md#esql-day_name)
+* [`MONTH_NAME`](../../functions-operators/date-time-functions.md#esql-month_name)
* [`NOW`](../../functions-operators/date-time-functions.md#esql-now)
diff --git a/docs/reference/query-languages/esql/functions-operators/date-time-functions.md b/docs/reference/query-languages/esql/functions-operators/date-time-functions.md
index 4823468b0bb97..284b475909c35 100644
--- a/docs/reference/query-languages/esql/functions-operators/date-time-functions.md
+++ b/docs/reference/query-languages/esql/functions-operators/date-time-functions.md
@@ -31,6 +31,9 @@ mapped_pages:
:::{include} ../_snippets/functions/layout/day_name.md
:::
+:::{include} ../_snippets/functions/layout/month_name.md
+:::
+
:::{include} ../_snippets/functions/layout/now.md
:::
diff --git a/docs/reference/query-languages/esql/images/functions/month_name.svg b/docs/reference/query-languages/esql/images/functions/month_name.svg
new file mode 100644
index 0000000000000..7b603e15902cb
--- /dev/null
+++ b/docs/reference/query-languages/esql/images/functions/month_name.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/month_name.json b/docs/reference/query-languages/esql/kibana/definition/functions/month_name.json
new file mode 100644
index 0000000000000..a8aae09948877
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/month_name.json
@@ -0,0 +1,37 @@
+{
+ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.",
+ "type" : "scalar",
+ "name" : "month_name",
+ "description" : "Returns the month name for the provided date based on the configured Locale.",
+ "signatures" : [
+ {
+ "params" : [
+ {
+ "name" : "date",
+ "type" : "date",
+ "optional" : false,
+ "description" : "Date expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "keyword"
+ },
+ {
+ "params" : [
+ {
+ "name" : "date",
+ "type" : "date_nanos",
+ "optional" : false,
+ "description" : "Date expression. If `null`, the function returns `null`."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "keyword"
+ }
+ ],
+ "examples" : [
+ "ROW dt = to_datetime(\"1996-03-21T00:00:00.000Z\")\n| EVAL monthName = MONTH_NAME(dt);"
+ ],
+ "preview" : false,
+ "snapshot_only" : false
+}
diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/month_name.md b/docs/reference/query-languages/esql/kibana/docs/functions/month_name.md
new file mode 100644
index 0000000000000..84f7709322091
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/docs/functions/month_name.md
@@ -0,0 +1,9 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+### MONTH NAME
+Returns the month name for the provided date based on the configured Locale.
+
+```esql
+ROW dt = to_datetime("1996-03-21T00:00:00.000Z")
+| EVAL monthName = MONTH_NAME(dt);
+```
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec
index 2b97b2985e7b9..e5efc9e7a37aa 100644
--- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec
@@ -1930,3 +1930,101 @@ from employees | where emp_no == 10040 | eval x = day_name(birth_date) | keep em
emp_no:integer | birth_date:date | hire_date:date | x:keyword
10040 | null | 1993-02-14T00:00:00.000Z | null
;
+
+
+monthNameRowTest
+required_capability:fn_month_name
+// tag::docsMonthName[]
+ROW dt = to_datetime("1996-03-21T00:00:00.000Z")
+| EVAL monthName = MONTH_NAME(dt);
+// end::docsMonthName[]
+
+dt:date | monthName:keyword
+1996-03-21T00:00:00.000Z | March
+;
+
+monthNameSimple
+required_capability:fn_month_name
+from employees
+| sort emp_no
+| keep emp_no, hire_date
+| eval monthName = month_name(hire_date)
+| limit 1;
+
+emp_no:integer | hire_date:date | monthName:keyword
+10001 | 1986-06-26T00:00:00.000Z | June
+;
+
+monthNameTruncate
+required_capability:fn_month_name
+FROM employees
+| EVAL a = hire_date
+| RENAME hire_date as b
+| WHERE a > "1987-01-01" and a < "1988-01-01"
+| EVAL y = date_trunc(1 month, b)
+| STATS c = count(emp_no) by y
+| EVAL monthName = month_name(y)
+| SORT y
+| KEEP y, c, monthName
+| LIMIT 5;
+
+y:date | c:long | monthName:keyword
+1987-03-01T00:00:00.000Z | 5 | March
+1987-04-01T00:00:00.000Z | 3 | April
+1987-05-01T00:00:00.000Z | 1 | May
+1987-07-01T00:00:00.000Z | 1 | July
+1987-08-01T00:00:00.000Z | 2 | August
+;
+
+monthNameNestedCall
+required_capability:fn_month_name
+from employees
+| sort emp_no
+| eval monthName = month_name(date_trunc(1 month, hire_date))
+| keep emp_no, hire_date, monthName
+| limit 10;
+
+emp_no:integer | hire_date:datetime | monthName:keyword
+10001 | 1986-06-26T00:00:00.000Z | June
+10002 | 1985-11-21T00:00:00.000Z | November
+10003 | 1986-08-28T00:00:00.000Z | August
+10004 | 1986-12-01T00:00:00.000Z | December
+10005 | 1989-09-12T00:00:00.000Z | September
+10006 | 1989-06-02T00:00:00.000Z | June
+10007 | 1989-02-10T00:00:00.000Z | February
+10008 | 1994-09-15T00:00:00.000Z | September
+10009 | 1985-02-18T00:00:00.000Z | February
+10010 | 1989-08-24T00:00:00.000Z | August
+;
+
+monthNameNestedCall
+required_capability:fn_month_name
+from employees
+| sort emp_no desc
+| eval monthName = to_upper(month_name(date_trunc(1 month, hire_date)))
+| keep emp_no, hire_date, monthName
+| limit 10;
+
+emp_no:integer | hire_date:datetime | monthName:keyword
+10100 | 1987-09-21T00:00:00.000Z | SEPTEMBER
+10099 | 1988-10-18T00:00:00.000Z | OCTOBER
+10098 | 1985-05-13T00:00:00.000Z | MAY
+10097 | 1990-09-15T00:00:00.000Z | SEPTEMBER
+10096 | 1990-01-14T00:00:00.000Z | JANUARY
+10095 | 1986-07-15T00:00:00.000Z | JULY
+10094 | 1987-04-18T00:00:00.000Z | APRIL
+10093 | 1996-11-05T00:00:00.000Z | NOVEMBER
+10092 | 1989-09-22T00:00:00.000Z | SEPTEMBER
+10091 | 1992-11-18T00:00:00.000Z | NOVEMBER
+;
+
+monthNameNull
+required_capability:fn_month_name
+from employees
+| where emp_no == 10040
+| eval monthName = month_name(birth_date)
+| keep emp_no, birth_date, hire_date, monthName;
+
+emp_no:integer | birth_date:date | hire_date:date | monthName:keyword
+10040 | null | 1993-02-14T00:00:00.000Z | null
+;
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameMillisEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameMillisEvaluator.java
new file mode 100644
index 0000000000000..e5c5eac0c1b13
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameMillisEvaluator.java
@@ -0,0 +1,139 @@
+// 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.date;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import java.time.ZoneId;
+import java.util.Locale;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+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 MonthName}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class MonthNameMillisEvaluator implements EvalOperator.ExpressionEvaluator {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator val;
+
+ private final ZoneId zoneId;
+
+ private final Locale locale;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public MonthNameMillisEvaluator(Source source, EvalOperator.ExpressionEvaluator val,
+ ZoneId zoneId, Locale locale, DriverContext driverContext) {
+ this.source = source;
+ this.val = val;
+ this.zoneId = zoneId;
+ this.locale = locale;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (LongBlock valBlock = (LongBlock) val.eval(page)) {
+ LongVector valVector = valBlock.asVector();
+ if (valVector == null) {
+ return eval(page.getPositionCount(), valBlock);
+ }
+ return eval(page.getPositionCount(), valVector).asBlock();
+ }
+ }
+
+ public BytesRefBlock eval(int positionCount, LongBlock valBlock) {
+ try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ if (valBlock.isNull(p)) {
+ result.appendNull();
+ continue position;
+ }
+ if (valBlock.getValueCount(p) != 1) {
+ if (valBlock.getValueCount(p) > 1) {
+ warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+ }
+ result.appendNull();
+ continue position;
+ }
+ result.appendBytesRef(MonthName.processMillis(valBlock.getLong(valBlock.getFirstValueIndex(p)), this.zoneId, this.locale));
+ }
+ return result.build();
+ }
+ }
+
+ public BytesRefVector eval(int positionCount, LongVector valVector) {
+ try(BytesRefVector.Builder result = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ result.appendBytesRef(MonthName.processMillis(valVector.getLong(p), this.zoneId, this.locale));
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MonthNameMillisEvaluator[" + "val=" + val + ", zoneId=" + zoneId + ", locale=" + locale + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(val);
+ }
+
+ 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 val;
+
+ private final ZoneId zoneId;
+
+ private final Locale locale;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val, ZoneId zoneId,
+ Locale locale) {
+ this.source = source;
+ this.val = val;
+ this.zoneId = zoneId;
+ this.locale = locale;
+ }
+
+ @Override
+ public MonthNameMillisEvaluator get(DriverContext context) {
+ return new MonthNameMillisEvaluator(source, val.get(context), zoneId, locale, context);
+ }
+
+ @Override
+ public String toString() {
+ return "MonthNameMillisEvaluator[" + "val=" + val + ", zoneId=" + zoneId + ", locale=" + locale + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameNanosEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameNanosEvaluator.java
new file mode 100644
index 0000000000000..e8f44271fe4c0
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameNanosEvaluator.java
@@ -0,0 +1,139 @@
+// 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.date;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import java.time.ZoneId;
+import java.util.Locale;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+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 MonthName}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class MonthNameNanosEvaluator implements EvalOperator.ExpressionEvaluator {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator val;
+
+ private final ZoneId zoneId;
+
+ private final Locale locale;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public MonthNameNanosEvaluator(Source source, EvalOperator.ExpressionEvaluator val, ZoneId zoneId,
+ Locale locale, DriverContext driverContext) {
+ this.source = source;
+ this.val = val;
+ this.zoneId = zoneId;
+ this.locale = locale;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (LongBlock valBlock = (LongBlock) val.eval(page)) {
+ LongVector valVector = valBlock.asVector();
+ if (valVector == null) {
+ return eval(page.getPositionCount(), valBlock);
+ }
+ return eval(page.getPositionCount(), valVector).asBlock();
+ }
+ }
+
+ public BytesRefBlock eval(int positionCount, LongBlock valBlock) {
+ try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ if (valBlock.isNull(p)) {
+ result.appendNull();
+ continue position;
+ }
+ if (valBlock.getValueCount(p) != 1) {
+ if (valBlock.getValueCount(p) > 1) {
+ warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+ }
+ result.appendNull();
+ continue position;
+ }
+ result.appendBytesRef(MonthName.processNanos(valBlock.getLong(valBlock.getFirstValueIndex(p)), this.zoneId, this.locale));
+ }
+ return result.build();
+ }
+ }
+
+ public BytesRefVector eval(int positionCount, LongVector valVector) {
+ try(BytesRefVector.Builder result = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ result.appendBytesRef(MonthName.processNanos(valVector.getLong(p), this.zoneId, this.locale));
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MonthNameNanosEvaluator[" + "val=" + val + ", zoneId=" + zoneId + ", locale=" + locale + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(val);
+ }
+
+ 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 val;
+
+ private final ZoneId zoneId;
+
+ private final Locale locale;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val, ZoneId zoneId,
+ Locale locale) {
+ this.source = source;
+ this.val = val;
+ this.zoneId = zoneId;
+ this.locale = locale;
+ }
+
+ @Override
+ public MonthNameNanosEvaluator get(DriverContext context) {
+ return new MonthNameNanosEvaluator(source, val.get(context), zoneId, locale, context);
+ }
+
+ @Override
+ public String toString() {
+ return "MonthNameNanosEvaluator[" + "val=" + val + ", zoneId=" + zoneId + ", locale=" + locale + "]";
+ }
+ }
+}
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 a397a84343e43..6d88b479b59f4 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
@@ -240,6 +240,11 @@ public enum Cap {
*/
FN_DAY_NAME,
+ /**
+ * Support for function MONTH_NAME
+ */
+ FN_MONTH_NAME,
+
/**
* Fixes for multiple functions not serializing their source, and emitting warnings with wrong line number and text.
*/
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 f52c51cf8d3f9..0202661acf76d 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
@@ -88,6 +88,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateParse;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DayName;
+import org.elasticsearch.xpack.esql.expression.function.scalar.date.MonthName;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.Now;
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.IpPrefix;
@@ -395,6 +396,7 @@ private static FunctionDefinition[][] functions() {
def(DateParse.class, DateParse::new, "date_parse"),
def(DateTrunc.class, DateTrunc::new, "date_trunc"),
def(DayName.class, DayName::new, "day_name"),
+ def(MonthName.class, MonthName::new, "month_name"),
def(Now.class, Now::new, "now") },
// spatial
new FunctionDefinition[] {
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java
index 684e685e2eafb..4e0d28fd7d6a6 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java
@@ -19,6 +19,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateParse;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DayName;
+import org.elasticsearch.xpack.esql.expression.function.scalar.date.MonthName;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.Now;
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.IpPrefix;
@@ -84,6 +85,7 @@ public static List getNamedWriteables() {
entries.add(DateParse.ENTRY);
entries.add(DateTrunc.ENTRY);
entries.add(DayName.ENTRY);
+ entries.add(MonthName.ENTRY);
entries.add(IpPrefix.ENTRY);
entries.add(Least.ENTRY);
entries.add(Left.ENTRY);
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthName.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthName.java
new file mode 100644
index 0000000000000..84ee8130d13dc
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthName.java
@@ -0,0 +1,145 @@
+/*
+ * 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.date;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.compute.operator.EvalOperator;
+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.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.expression.function.scalar.EsqlConfigurationFunction;
+import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
+import org.elasticsearch.xpack.esql.session.Configuration;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.TextStyle;
+import java.util.List;
+import java.util.Locale;
+
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
+
+public class MonthName extends EsqlConfigurationFunction {
+ public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
+ Expression.class,
+ "MonthName",
+ MonthName::new
+ );
+
+ private final Expression field;
+
+ @FunctionInfo(
+ returnType = "keyword",
+ description = "Returns the month name for the provided date based on the configured Locale.",
+ examples = @Example(file = "date", tag = "docsMonthName"),
+ appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.GA, version = "9.2.0") }
+ )
+ public MonthName(
+ Source source,
+ @Param(
+ name = "date",
+ type = { "date", "date_nanos" },
+ description = "Date expression. If `null`, the function returns `null`."
+ ) Expression date,
+ Configuration configuration
+ ) {
+ super(source, List.of(date), configuration);
+ this.field = date;
+ }
+
+ private MonthName(StreamInput in) throws IOException {
+ this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), ((PlanStreamInput) in).configuration());
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ source().writeTo(out);
+ out.writeNamedWriteable(field);
+ }
+
+ @Override
+ public String getWriteableName() {
+ return ENTRY.name;
+ }
+
+ Expression field() {
+ return field;
+ }
+
+ @Override
+ public DataType dataType() {
+ return DataType.KEYWORD;
+ }
+
+ @Override
+ protected TypeResolution resolveType() {
+ if (childrenResolved() == false) {
+ return new TypeResolution("Unresolved children");
+ }
+
+ String operationName = sourceText();
+ TypeResolution resolution = TypeResolutions.isType(field, DataType::isDate, operationName, FIRST, "datetime or date_nanos");
+ if (resolution.unresolved()) {
+ return resolution;
+ }
+
+ return TypeResolution.TYPE_RESOLVED;
+ }
+
+ @Override
+ public boolean foldable() {
+ return field.foldable();
+ }
+
+ @Override
+ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+ var fieldEvaluator = toEvaluator.apply(field);
+ if (field().dataType() == DataType.DATE_NANOS) {
+ return new MonthNameNanosEvaluator.Factory(source(), fieldEvaluator, configuration().zoneId(), configuration().locale());
+ }
+ return new MonthNameMillisEvaluator.Factory(source(), fieldEvaluator, configuration().zoneId(), configuration().locale());
+ }
+
+ @Override
+ public Expression replaceChildren(List newChildren) {
+ return new MonthName(source(), newChildren.get(0), configuration());
+ }
+
+ @Override
+ protected NodeInfo extends Expression> info() {
+ return NodeInfo.create(this, MonthName::new, field, configuration());
+ }
+
+ @Evaluator(extraName = "Millis")
+ static BytesRef processMillis(long val, @Fixed ZoneId zoneId, @Fixed Locale locale) {
+ ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(val), zoneId);
+ String displayName = dateTime.getMonth().getDisplayName(TextStyle.FULL, locale);
+ return new BytesRef(displayName);
+ }
+
+ @Evaluator(extraName = "Nanos")
+ static BytesRef processNanos(long val, @Fixed ZoneId zoneId, @Fixed Locale locale) {
+ ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L, val), zoneId);
+ String displayName = dateTime.getMonth().getDisplayName(TextStyle.FULL, locale);
+ return new BytesRef(displayName);
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java
index b47f8dd8ec1cb..f443fc041e67c 100644
--- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java
@@ -33,7 +33,6 @@ protected Expression build(Source source, List args) {
@Override
protected Matcher expectedTypeErrorMatcher(List> validPerPosition, List signature) {
- // Single argument version
String source = sourceForSignature(signature);
String name = signature.get(0).typeName();
return equalTo("first argument of [" + source + "] must be [datetime or date_nanos], found value [] type [" + name + "]");
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameErrorTests.java
new file mode 100644
index 0000000000000..945cc0a0806a2
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameErrorTests.java
@@ -0,0 +1,40 @@
+/*
+ * 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.date;
+
+import org.elasticsearch.xpack.esql.EsqlTestUtils;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.hamcrest.Matcher;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class MonthNameErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
+ @Override
+ protected List cases() {
+ return paramsToSuppliers(MonthNameTests.parameters());
+ }
+
+ @Override
+ protected Expression build(Source source, List args) {
+ return new MonthName(source, args.get(0), EsqlTestUtils.TEST_CFG);
+ }
+
+ @Override
+ protected Matcher expectedTypeErrorMatcher(List> validPerPosition, List signature) {
+ String source = sourceForSignature(signature);
+ String name = signature.get(0).typeName();
+ return equalTo("first argument of [" + source + "] must be [datetime or date_nanos], found value [] type [" + name + "]");
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameSerializationTests.java
new file mode 100644
index 0000000000000..348c7f9be8cc6
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameSerializationTests.java
@@ -0,0 +1,32 @@
+/*
+ * 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.date;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests;
+
+import java.io.IOException;
+
+public class MonthNameSerializationTests extends AbstractExpressionSerializationTests {
+
+ @Override
+ protected MonthName createTestInstance() {
+ Source source = randomSource();
+ Expression date = randomChild();
+ return new MonthName(source, date, configuration());
+ }
+
+ @Override
+ protected MonthName mutateInstance(MonthName instance) throws IOException {
+ Source source = instance.source();
+ Expression date = instance.field();
+ return new MonthName(source, randomValueOtherThan(date, AbstractExpressionSerializationTests::randomChild), configuration());
+ }
+
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java
new file mode 100644
index 0000000000000..0b29c1d3053b7
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java
@@ -0,0 +1,147 @@
+/*
+ * 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.date;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.lucene.BytesRefs;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.time.DateUtils;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.FoldContext;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase;
+import org.elasticsearch.xpack.esql.plugin.EsqlPlugin;
+import org.elasticsearch.xpack.esql.plugin.QueryPragmas;
+import org.elasticsearch.xpack.esql.session.Configuration;
+import org.hamcrest.Matchers;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.TextStyle;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class MonthNameTests extends AbstractConfigurationFunctionTestCase {
+
+ public MonthNameTests(@Name("TestCase") Supplier testCaseSupplier) {
+ this.testCase = testCaseSupplier.get();
+ }
+
+ @Override
+ protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) {
+ return new MonthName(source, args.get(0), configuration);
+ }
+
+ @ParametersFactory
+ public static Iterable