浏览代码

Add ESQL `MONTH_NAME` function (#132968)

Rômulo Farias 1 月之前
父节点
当前提交
a4bf2206b6
共有 21 个文件被更改,包括 858 次插入1 次删除
  1. 6 0
      docs/reference/query-languages/esql/_snippets/functions/description/month_name.md
  2. 10 0
      docs/reference/query-languages/esql/_snippets/functions/examples/month_name.md
  3. 26 0
      docs/reference/query-languages/esql/_snippets/functions/layout/month_name.md
  4. 7 0
      docs/reference/query-languages/esql/_snippets/functions/parameters/month_name.md
  5. 9 0
      docs/reference/query-languages/esql/_snippets/functions/types/month_name.md
  6. 1 0
      docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md
  7. 3 0
      docs/reference/query-languages/esql/functions-operators/date-time-functions.md
  8. 1 0
      docs/reference/query-languages/esql/images/functions/month_name.svg
  9. 37 0
      docs/reference/query-languages/esql/kibana/definition/functions/month_name.json
  10. 9 0
      docs/reference/query-languages/esql/kibana/docs/functions/month_name.md
  11. 98 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec
  12. 139 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameMillisEvaluator.java
  13. 139 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameNanosEvaluator.java
  14. 5 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  15. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  16. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java
  17. 145 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthName.java
  18. 0 1
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java
  19. 40 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameErrorTests.java
  20. 32 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameSerializationTests.java
  21. 147 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java

+ 6 - 0
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.
+

+ 10 - 0
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);
+```
+
+

+ 26 - 0
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
+:::

+ 7 - 0
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`.
+

+ 9 - 0
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 |
+

+ 1 - 0
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)

+ 3 - 0
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
 :::
 

+ 1 - 0
docs/reference/query-languages/esql/images/functions/month_name.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="312" height="46" viewbox="0 0 312 46"><defs><style type="text/css">.c{fill:none;stroke:#222222;}.k{fill:#000000;font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:20px;}.s{fill:#e4f4ff;stroke:#222222;}.syn{fill:#8D8D8D;font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:20px;}</style></defs><path class="c" d="M0 31h5m140 0h10m32 0h10m68 0h10m32 0h5"/><rect class="s" x="5" y="5" width="140" height="36"/><text class="k" x="15" y="31">MONTH_NAME</text><rect class="s" x="155" y="5" width="32" height="36" rx="7"/><text class="syn" x="165" y="31">(</text><rect class="s" x="197" y="5" width="68" height="36" rx="7"/><text class="k" x="207" y="31">date</text><rect class="s" x="275" y="5" width="32" height="36" rx="7"/><text class="syn" x="285" y="31">)</text></svg>

+ 37 - 0
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
+}

+ 9 - 0
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);
+```

+ 98 - 0
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  
+;

+ 139 - 0
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 + "]";
+    }
+  }
+}

+ 139 - 0
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 + "]";
+    }
+  }
+}

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

@@ -240,6 +240,11 @@ public class EsqlCapabilities {
          */
         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.
          */

+ 2 - 0
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.DateFormat;
 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 @@ public class EsqlFunctionRegistry {
                 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[] {

+ 2 - 0
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.DateFormat;
 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 class ScalarFunctionWritables {
         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);

+ 145 - 0
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<Expression> 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);
+    }
+}

+ 0 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java

@@ -33,7 +33,6 @@ public class DayNameErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
 
     @Override
     protected Matcher<String> expectedTypeErrorMatcher(List<Set<DataType>> validPerPosition, List<DataType> 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 + "]");

+ 40 - 0
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<TestCaseSupplier> cases() {
+        return paramsToSuppliers(MonthNameTests.parameters());
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new MonthName(source, args.get(0), EsqlTestUtils.TEST_CFG);
+    }
+
+    @Override
+    protected Matcher<String> expectedTypeErrorMatcher(List<Set<DataType>> validPerPosition, List<DataType> 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 + "]");
+    }
+}

+ 32 - 0
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<MonthName> {
+
+    @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());
+    }
+
+}

+ 147 - 0
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.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @Override
+    protected Expression buildWithConfiguration(Source source, List<Expression> args, Configuration configuration) {
+        return new MonthName(source, args.get(0), configuration);
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+        suppliers.addAll(generateTest("1994-01-19T00:00:00.00Z", "January"));
+        suppliers.addAll(generateTest("1995-02-20T23:59:59.99Z", "February"));
+        suppliers.addAll(generateTest("1996-03-21T23:12:32.12Z", "March"));
+        suppliers.addAll(generateTest("1997-04-22T07:39:01.28Z", "April"));
+        suppliers.addAll(generateTest("1998-05-23T10:25:33.38Z", "May"));
+        suppliers.addAll(generateTest("1999-06-24T22:55:33.82Z", "June"));
+        suppliers.addAll(generateTest("2000-07-25T01:01:29.49Z", "July"));
+        suppliers.addAll(generateTest("2001-08-25T01:01:29.49Z", "August"));
+        suppliers.addAll(generateTest("2002-09-25T01:01:29.49Z", "September"));
+        suppliers.addAll(generateTest("2003-10-25T01:01:29.49Z", "October"));
+        suppliers.addAll(generateTest("2004-11-25T01:01:29.49Z", "November"));
+        suppliers.addAll(generateTest("2005-12-25T01:01:29.49Z", "December"));
+
+        suppliers.add(
+            new TestCaseSupplier(
+                List.of(DataType.DATETIME),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(new TestCaseSupplier.TypedData(null, DataType.DATETIME, "date")),
+                    Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"),
+                    DataType.KEYWORD,
+                    equalTo(null)
+                )
+            )
+        );
+
+        return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(true, suppliers);
+    }
+
+    private static List<TestCaseSupplier> generateTest(String dateTime, String expectedMonthName) {
+        return List.of(
+            new TestCaseSupplier(
+                List.of(DataType.DATETIME),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(new TestCaseSupplier.TypedData(toMillis(dateTime), DataType.DATETIME, "date")),
+                    Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"),
+                    DataType.KEYWORD,
+                    equalTo(new BytesRef(expectedMonthName))
+                )
+            ),
+            new TestCaseSupplier(
+                List.of(DataType.DATE_NANOS),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(new TestCaseSupplier.TypedData(toNanos(dateTime), DataType.DATE_NANOS, "date")),
+                    Matchers.is("MonthNameNanosEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"),
+                    DataType.KEYWORD,
+                    equalTo(new BytesRef(expectedMonthName))
+                )
+            )
+        );
+    }
+
+    private static long toMillis(String timestamp) {
+        return Instant.parse(timestamp).toEpochMilli();
+    }
+
+    private static long toNanos(String timestamp) {
+        return DateUtils.toLong(Instant.parse(timestamp));
+    }
+
+    public void testRandomLocale() {
+        long randomMillis = randomMillisUpToYear9999();
+        Configuration cfg = configWithZoneAndLocale(randomZone(), randomLocale(random()));
+        String expected = Instant.ofEpochMilli(randomMillis).atZone(cfg.zoneId()).getMonth().getDisplayName(TextStyle.FULL, cfg.locale());
+
+        MonthName func = new MonthName(Source.EMPTY, new Literal(Source.EMPTY, randomMillis, DataType.DATETIME), cfg);
+        assertThat(BytesRefs.toBytesRef(expected), equalTo(func.fold(FoldContext.small())));
+    }
+
+    public void testFixedLocaleAndTime() {
+        long randomMillis = toMillis("1996-03-21T00:00:00.00Z");
+        Configuration cfg = configWithZoneAndLocale(ZoneId.of("America/Sao_Paulo"), Locale.of("pt", "br"));
+        String expected = "março";
+
+        MonthName func = new MonthName(Source.EMPTY, new Literal(Source.EMPTY, randomMillis, DataType.DATETIME), cfg);
+        assertThat(BytesRefs.toBytesRef(expected), equalTo(func.fold(FoldContext.small())));
+    }
+
+    private Configuration configWithZoneAndLocale(ZoneId zone, Locale locale) {
+        return new Configuration(
+            zone,
+            locale,
+            null,
+            null,
+            new QueryPragmas(Settings.EMPTY),
+            EsqlPlugin.QUERY_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY),
+            EsqlPlugin.QUERY_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY),
+            "",
+            false,
+            Map.of(),
+            System.nanoTime(),
+            randomBoolean()
+        );
+    }
+}