瀏覽代碼

ESQL: Add TO_UPPER and TO_LOWER functions (#104309)

Luigi Dell'Aquila 1 年之前
父節點
當前提交
ad28dc9a6c
共有 18 個文件被更改,包括 782 次插入2 次删除
  1. 5 0
      docs/changelog/104309.yaml
  2. 1 0
      docs/reference/esql/functions/signature/to_lower.svg
  3. 1 0
      docs/reference/esql/functions/signature/to_upper.svg
  4. 4 0
      docs/reference/esql/functions/string-functions.asciidoc
  5. 20 0
      docs/reference/esql/functions/to_lower.asciidoc
  6. 20 0
      docs/reference/esql/functions/to_upper.asciidoc
  7. 6 0
      docs/reference/esql/functions/types/to_lower.asciidoc
  8. 6 0
      docs/reference/esql/functions/types/to_upper.asciidoc
  9. 5 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec
  10. 53 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec
  11. 118 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerEvaluator.java
  12. 118 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperEvaluator.java
  13. 5 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  14. 109 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLower.java
  15. 109 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpper.java
  16. 20 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java
  17. 91 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java
  18. 91 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java

+ 5 - 0
docs/changelog/104309.yaml

@@ -0,0 +1,5 @@
+pr: 104309
+summary: "ESQL: Add TO_UPPER and TO_LOWER functions"
+area: ES|QL
+type: enhancement
+issues: []

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

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="276" height="46" viewbox="0 0 276 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m116 0h10m32 0h10m56 0h10m32 0h5"/><rect class="s" x="5" y="5" width="116" height="36"/><text class="k" x="15" y="31">TO_LOWER</text><rect class="s" x="131" y="5" width="32" height="36" rx="7"/><text class="syn" x="141" y="31">(</text><rect class="s" x="173" y="5" width="56" height="36" rx="7"/><text class="k" x="183" y="31">str</text><rect class="s" x="239" y="5" width="32" height="36" rx="7"/><text class="syn" x="249" y="31">)</text></svg>

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

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="276" height="46" viewbox="0 0 276 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m116 0h10m32 0h10m56 0h10m32 0h5"/><rect class="s" x="5" y="5" width="116" height="36"/><text class="k" x="15" y="31">TO_UPPER</text><rect class="s" x="131" y="5" width="32" height="36" rx="7"/><text class="syn" x="141" y="31">(</text><rect class="s" x="173" y="5" width="56" height="36" rx="7"/><text class="k" x="183" y="31">str</text><rect class="s" x="239" y="5" width="32" height="36" rx="7"/><text class="syn" x="249" y="31">)</text></svg>

+ 4 - 0
docs/reference/esql/functions/string-functions.asciidoc

@@ -17,6 +17,8 @@
 * <<esql-rtrim>>
 * <<esql-split>>
 * <<esql-substring>>
+* <<esql-to_lower>>
+* <<esql-to_upper>>
 * <<esql-trim>>
 // end::string_list[]
 
@@ -29,4 +31,6 @@ include::right.asciidoc[]
 include::rtrim.asciidoc[]
 include::split.asciidoc[]
 include::substring.asciidoc[]
+include::to_lower.asciidoc[]
+include::to_upper.asciidoc[]
 include::trim.asciidoc[]

+ 20 - 0
docs/reference/esql/functions/to_lower.asciidoc

@@ -0,0 +1,20 @@
+[discrete]
+[[esql-to_lower]]
+=== `TO_LOWER`
+[.text-center]
+image::esql/functions/signature/to_lower.svg[Embedded,opts=inline]
+
+Returns a new string representing the input string converted to lower case
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/string.csv-spec[tag=to_lower]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/string.csv-spec[tag=to_lower-result]
+|===
+
+Supported types:
+
+include::types/to_lower.asciidoc[]

+ 20 - 0
docs/reference/esql/functions/to_upper.asciidoc

@@ -0,0 +1,20 @@
+[discrete]
+[[esql-to_upper]]
+=== `TO_UPPER`
+[.text-center]
+image::esql/functions/signature/to_upper.svg[Embedded,opts=inline]
+
+Returns a new string representing the input string converted to upper case
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/string.csv-spec[tag=to_upper]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/string.csv-spec[tag=to_upper-result]
+|===
+
+Supported types:
+
+include::types/to_upper.asciidoc[]

+ 6 - 0
docs/reference/esql/functions/types/to_lower.asciidoc

@@ -0,0 +1,6 @@
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+str | result
+keyword | keyword
+text | text
+|===

+ 6 - 0
docs/reference/esql/functions/types/to_upper.asciidoc

@@ -0,0 +1,6 @@
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+str | result
+keyword | keyword
+text | text
+|===

+ 5 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec

@@ -85,12 +85,14 @@ to_int                   |"integer to_int(v:boolean|date|keyword|text|double|lon
 to_integer               |"integer to_integer(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"              |v   |"boolean|date|keyword|text|double|long|unsigned_long|integer"                                              |                                                    |integer                          | "Converts an input value to an integer value."                                                                                      |false                       |false           | false
 to_ip                    |"ip to_ip(v:ip|keyword|text)"                                                                    |v   |"ip|keyword|text"                                                                                          |                                                    |ip                               | "Converts an input string to an IP value."                                                                                      |false                       |false           | false
 to_long                  |"long to_long(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"                    |v   |"boolean|date|keyword|text|double|long|unsigned_long|integer"                                              |                                                    |long                             | "Converts an input value to a long value."                                                                                      |false                       |false | false
+to_lower                 |"keyword|text to_lower(str:keyword|text)"                                                        |str |"keyword|text"                                                                                             | "The input string"                                 |"keyword|text"                   | "Returns a new string representing the input string converted to lower case."                                                   |false                       |false | false
 to_radians               |"double to_radians(v:double|integer|long|unsigned_long)"                                         |v   |"double|integer|long|unsigned_long"                                                                        |                                                    |double                           | "Converts a number in degrees to radians."                                                                                      |false                       |false | false
 to_str                   |"keyword to_str(v:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)"                  |v                         |"boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version"          |                                                    |keyword                          | "Converts a field into a string."                                                                                      |false                       |false | false
 to_string                |"keyword to_string(v:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)"               |v                         |"boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version"          |                                                    |keyword                          | "Converts a field into a string."                                                                                      |false                       |false | false
 to_ul                    |"unsigned_long to_ul(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"             |v   |"boolean|date|keyword|text|double|long|unsigned_long|integer"                                              |                                                    |unsigned_long                    | "Converts an input value to an unsigned long value."                                                                                      |false                       |false           | false
 to_ulong                 |"unsigned_long to_ulong(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"          |v   |"boolean|date|keyword|text|double|long|unsigned_long|integer"                                              |                                                    |unsigned_long                    | "Converts an input value to an unsigned long value."                                                                                      |false                       |false           | false
 to_unsigned_long         |"unsigned_long to_unsigned_long(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"  |v   |"boolean|date|keyword|text|double|long|unsigned_long|integer"                                              |                                                    |unsigned_long                    | "Converts an input value to an unsigned long value."                                                                                      |false                       |false           | false
+to_upper                 |"keyword|text to_upper(str:keyword|text)"                                                        |str |"keyword|text"                                                                                             | "The input string"                                 |"keyword|text"                   | "Returns a new string representing the input string converted to upper case."                                                   |false                       |false | false
 to_ver                   |"version to_ver(v:keyword|text|version)"                                                         |v   |"keyword|text|version"                                                                                     |                                                    |version                          | "Converts an input string to a version value."                                                                                     |false                       |false           | false
 to_version               |"version to_version(v:keyword|text|version)"                                                     |v   |"keyword|text|version"                                                                                     |                                                    |version                          | "Converts an input string to a version value."                                                                                       |false                       |false           | false
 trim                     |"keyword|text trim(str:keyword|text)"                   |str                      |"keyword|text"    | ""                                                 |"keyword|text"       | "Removes leading and trailing whitespaces from a string." | false | false | false
@@ -176,12 +178,14 @@ double tau()
 "integer to_integer(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"
 "ip to_ip(v:ip|keyword|text)"
 "long to_long(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"
+"keyword|text to_lower(str:keyword|text)"
 "double to_radians(v:double|integer|long|unsigned_long)"
 "keyword to_str(v:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)"
 "keyword to_string(v:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)"
 "unsigned_long to_ul(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"
 "unsigned_long to_ulong(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"
 "unsigned_long to_unsigned_long(v:boolean|date|keyword|text|double|long|unsigned_long|integer)"
+"keyword|text to_upper(str:keyword|text)"
 "version to_ver(v:keyword|text|version)"
 "version to_version(v:keyword|text|version)"
 "keyword|text trim(str:keyword|text)"
@@ -208,5 +212,5 @@ countFunctions#[skip:-8.12.99]
 show functions |  stats  a = count(*), b = count(*), c = count(*) |  mv_expand c;
 
 a:long | b:long | c:long
-84     | 84     | 84
+86     | 86     | 86
 ;

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

@@ -952,3 +952,56 @@ Bamford        |true
 Bernatsky      |false
 // end::endsWith-result[]
 ;
+
+
+
+toLowerRow#[skip:-8.12.99]
+// tag::to_lower[]
+ROW message = "Some Text"
+| EVAL message_lower = TO_LOWER(message)
+// end::to_lower[]
+;
+
+// tag::to_lower-result[]
+message:keyword | message_lower:keyword
+Some Text       | some text
+// end::to_lower-result[]
+;
+
+
+toLower#[skip:-8.12.99]
+from employees | sort emp_no | eval name_lower = TO_LOWER(first_name) | keep emp_no, first_name, name_lower | limit 1;
+
+emp_no:integer  | first_name:keyword    | name_lower:keyword
+10001           | Georgi                | georgi
+;
+
+
+toUpperRow#[skip:-8.12.99]
+// tag::to_upper[]
+ROW message = "Some Text"
+| EVAL message_upper = TO_UPPER(message)
+// end::to_upper[]
+;
+
+// tag::to_upper-result[]
+message:keyword | message_upper:keyword
+Some Text       | SOME TEXT
+// end::to_upper-result[]
+;
+
+
+toUpper#[skip:-8.12.99]
+from employees | sort emp_no | eval name_upper = TO_UPPER(first_name) | keep emp_no, first_name, name_upper | limit 1;
+
+emp_no:integer  | first_name:keyword    | name_upper:keyword
+10001           | Georgi                | GEORGI
+;
+
+
+toUpperLowerUnicode#[skip:-8.12.99]
+row a = "π/2 + a + B + Λ ºC" | eval lower = to_lower(a), upper = to_upper(a) | keep a, upper, lower;
+
+a:keyword           | upper:keyword         | lower:keyword
+π/2 + a + B + Λ ºC  | Π/2 + A + B + Λ ºC    | π/2 + a + b + λ ºc
+;

+ 118 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerEvaluator.java

@@ -0,0 +1,118 @@
+// 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.string;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Locale;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link ToLower}.
+ * This class is generated. Do not edit it.
+ */
+public final class ToLowerEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator val;
+
+  private final Locale locale;
+
+  private final DriverContext driverContext;
+
+  public ToLowerEvaluator(Source source, EvalOperator.ExpressionEvaluator val, Locale locale,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.val = val;
+    this.locale = locale;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock valBlock = (BytesRefBlock) val.eval(page)) {
+      BytesRefVector valVector = valBlock.asVector();
+      if (valVector == null) {
+        return eval(page.getPositionCount(), valBlock);
+      }
+      return eval(page.getPositionCount(), valVector).asBlock();
+    }
+  }
+
+  public BytesRefBlock eval(int positionCount, BytesRefBlock valBlock) {
+    try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      BytesRef valScratch = new BytesRef();
+      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(ToLower.process(valBlock.getBytesRef(valBlock.getFirstValueIndex(p), valScratch), locale));
+      }
+      return result.build();
+    }
+  }
+
+  public BytesRefVector eval(int positionCount, BytesRefVector valVector) {
+    try(BytesRefVector.Builder result = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) {
+      BytesRef valScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendBytesRef(ToLower.process(valVector.getBytesRef(p, valScratch), locale));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ToLowerEvaluator[" + "val=" + val + ", locale=" + locale + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(val);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory val;
+
+    private final Locale locale;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val, Locale locale) {
+      this.source = source;
+      this.val = val;
+      this.locale = locale;
+    }
+
+    @Override
+    public ToLowerEvaluator get(DriverContext context) {
+      return new ToLowerEvaluator(source, val.get(context), locale, context);
+    }
+
+    @Override
+    public String toString() {
+      return "ToLowerEvaluator[" + "val=" + val + ", locale=" + locale + "]";
+    }
+  }
+}

+ 118 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperEvaluator.java

@@ -0,0 +1,118 @@
+// 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.string;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Locale;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link ToUpper}.
+ * This class is generated. Do not edit it.
+ */
+public final class ToUpperEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator val;
+
+  private final Locale locale;
+
+  private final DriverContext driverContext;
+
+  public ToUpperEvaluator(Source source, EvalOperator.ExpressionEvaluator val, Locale locale,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.val = val;
+    this.locale = locale;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock valBlock = (BytesRefBlock) val.eval(page)) {
+      BytesRefVector valVector = valBlock.asVector();
+      if (valVector == null) {
+        return eval(page.getPositionCount(), valBlock);
+      }
+      return eval(page.getPositionCount(), valVector).asBlock();
+    }
+  }
+
+  public BytesRefBlock eval(int positionCount, BytesRefBlock valBlock) {
+    try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      BytesRef valScratch = new BytesRef();
+      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(ToUpper.process(valBlock.getBytesRef(valBlock.getFirstValueIndex(p), valScratch), locale));
+      }
+      return result.build();
+    }
+  }
+
+  public BytesRefVector eval(int positionCount, BytesRefVector valVector) {
+    try(BytesRefVector.Builder result = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) {
+      BytesRef valScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendBytesRef(ToUpper.process(valVector.getBytesRef(p, valScratch), locale));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ToUpperEvaluator[" + "val=" + val + ", locale=" + locale + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(val);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory val;
+
+    private final Locale locale;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val, Locale locale) {
+      this.source = source;
+      this.val = val;
+      this.locale = locale;
+    }
+
+    @Override
+    public ToUpperEvaluator get(DriverContext context) {
+      return new ToUpperEvaluator(source, val.get(context), locale, context);
+    }
+
+    @Override
+    public String toString() {
+      return "ToUpperEvaluator[" + "val=" + val + ", locale=" + locale + "]";
+    }
+  }
+}

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

@@ -82,6 +82,8 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.Right;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Split;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.StartsWith;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Substring;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToLower;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToUpper;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Trim;
 import org.elasticsearch.xpack.esql.plan.logical.show.ShowFunctions;
 import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
@@ -154,7 +156,9 @@ public final class EsqlFunctionRegistry extends FunctionRegistry {
                 def(Replace.class, Replace::new, "replace"),
                 def(Right.class, Right::new, "right"),
                 def(StartsWith.class, StartsWith::new, "starts_with"),
-                def(EndsWith.class, EndsWith::new, "ends_with") },
+                def(EndsWith.class, EndsWith::new, "ends_with"),
+                def(ToLower.class, ToLower::new, "to_lower"),
+                def(ToUpper.class, ToUpper::new, "to_upper") },
             // date
             new FunctionDefinition[] {
                 def(DateDiff.class, DateDiff::new, "date_diff"),

+ 109 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLower.java

@@ -0,0 +1,109 @@
+/*
+ * 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.string;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.lucene.BytesRefs;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.session.EsqlConfiguration;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.function.scalar.ConfigurationFunction;
+import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
+import org.elasticsearch.xpack.ql.session.Configuration;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.DEFAULT;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
+
+public class ToLower extends ConfigurationFunction implements EvaluatorMapper {
+
+    private final Expression field;
+
+    @FunctionInfo(
+        returnType = { "keyword", "text" },
+        description = "Returns a new string representing the input string converted to lower case."
+    )
+    public ToLower(
+        Source source,
+        @Param(name = "str", type = { "keyword", "text" }, description = "The input string") Expression field,
+        Configuration configuration
+    ) {
+        super(source, List.of(field), configuration);
+        this.field = field;
+    }
+
+    @Override
+    public DataType dataType() {
+        return field.dataType();
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        return isString(field, sourceText(), DEFAULT);
+    }
+
+    @Override
+    public boolean foldable() {
+        return field.foldable();
+    }
+
+    @Override
+    public Object fold() {
+        return EvaluatorMapper.super.fold();
+    }
+
+    @Evaluator
+    static BytesRef process(BytesRef val, @Fixed Locale locale) {
+        return BytesRefs.toBytesRef(val.utf8ToString().toLowerCase(locale));
+    }
+
+    @Override
+    public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
+        var fieldEvaluator = toEvaluator.apply(field);
+        return new ToLowerEvaluator.Factory(source(), fieldEvaluator, ((EsqlConfiguration) configuration()).locale());
+    }
+
+    public Expression field() {
+        return field;
+    }
+
+    public ToLower replaceChild(Expression child) {
+        return new ToLower(source(), child, configuration());
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        assert newChildren.size() == 1;
+        return replaceChild(newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, ToLower::new, field, configuration());
+    }
+
+    @Override
+    public ScriptTemplate asScript() {
+        throw new UnsupportedOperationException("functions do not support scripting");
+    }
+}

+ 109 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpper.java

@@ -0,0 +1,109 @@
+/*
+ * 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.string;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.lucene.BytesRefs;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.session.EsqlConfiguration;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.function.scalar.ConfigurationFunction;
+import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
+import org.elasticsearch.xpack.ql.session.Configuration;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.DEFAULT;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
+
+public class ToUpper extends ConfigurationFunction implements EvaluatorMapper {
+
+    private final Expression field;
+
+    @FunctionInfo(
+        returnType = { "keyword", "text" },
+        description = "Returns a new string representing the input string converted to upper case."
+    )
+    public ToUpper(
+        Source source,
+        @Param(name = "str", type = { "keyword", "text" }, description = "The input string") Expression field,
+        Configuration configuration
+    ) {
+        super(source, List.of(field), configuration);
+        this.field = field;
+    }
+
+    @Override
+    public DataType dataType() {
+        return field.dataType();
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        return isString(field, sourceText(), DEFAULT);
+    }
+
+    @Override
+    public boolean foldable() {
+        return field.foldable();
+    }
+
+    @Override
+    public Object fold() {
+        return EvaluatorMapper.super.fold();
+    }
+
+    @Evaluator
+    static BytesRef process(BytesRef val, @Fixed Locale locale) {
+        return BytesRefs.toBytesRef(val.utf8ToString().toUpperCase(locale));
+    }
+
+    @Override
+    public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
+        var fieldEvaluator = toEvaluator.apply(field);
+        return new ToUpperEvaluator.Factory(source(), fieldEvaluator, ((EsqlConfiguration) configuration()).locale());
+    }
+
+    public Expression field() {
+        return field;
+    }
+
+    public ToUpper replaceChild(Expression child) {
+        return new ToUpper(source(), child, configuration());
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        assert newChildren.size() == 1;
+        return replaceChild(newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, ToUpper::new, field, configuration());
+    }
+
+    @Override
+    public ScriptTemplate asScript() {
+        throw new UnsupportedOperationException("functions do not support scripting");
+    }
+}

+ 20 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java

@@ -102,6 +102,8 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.Right;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Split;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.StartsWith;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Substring;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToLower;
+import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToUpper;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Trim;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div;
@@ -367,6 +369,8 @@ public final class PlanNamedTypes {
             of(ScalarFunction.class, Split.class, PlanNamedTypes::writeSplit, PlanNamedTypes::readSplit),
             of(ScalarFunction.class, Tau.class, PlanNamedTypes::writeNoArgScalar, PlanNamedTypes::readNoArgScalar),
             of(ScalarFunction.class, Replace.class, PlanNamedTypes::writeReplace, PlanNamedTypes::readReplace),
+            of(ScalarFunction.class, ToLower.class, PlanNamedTypes::writeToLower, PlanNamedTypes::readToLower),
+            of(ScalarFunction.class, ToUpper.class, PlanNamedTypes::writeToUpper, PlanNamedTypes::readToUpper),
             // ArithmeticOperations
             of(ArithmeticOperation.class, Add.class, PlanNamedTypes::writeArithmeticOperation, PlanNamedTypes::readArithmeticOperation),
             of(ArithmeticOperation.class, Sub.class, PlanNamedTypes::writeArithmeticOperation, PlanNamedTypes::readArithmeticOperation),
@@ -1450,6 +1454,22 @@ public final class PlanNamedTypes {
         out.writeExpression(fields.get(2));
     }
 
+    static ToLower readToLower(PlanStreamInput in) throws IOException {
+        return new ToLower(Source.EMPTY, in.readExpression(), in.configuration());
+    }
+
+    static void writeToLower(PlanStreamOutput out, ToLower toLower) throws IOException {
+        out.writeExpression(toLower.field());
+    }
+
+    static ToUpper readToUpper(PlanStreamInput in) throws IOException {
+        return new ToUpper(Source.EMPTY, in.readExpression(), in.configuration());
+    }
+
+    static void writeToUpper(PlanStreamOutput out, ToUpper toUpper) throws IOException {
+        out.writeExpression(toUpper.field());
+    }
+
     static Left readLeft(PlanStreamInput in) throws IOException {
         return new Left(in.readSource(), in.readExpression(), in.readExpression());
     }

+ 91 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java

@@ -0,0 +1,91 @@
+/*
+ * 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.string;
+
+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.xpack.esql.EsqlTestUtils;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.plugin.EsqlPlugin;
+import org.elasticsearch.xpack.esql.plugin.QueryPragmas;
+import org.elasticsearch.xpack.esql.session.EsqlConfiguration;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.Literal;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+import org.elasticsearch.xpack.ql.type.DateUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class ToLowerTests extends AbstractFunctionTestCase {
+    public ToLowerTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+
+        suppliers.add(supplier("keyword ascii", DataTypes.KEYWORD, () -> randomAlphaOfLengthBetween(1, 10)));
+        suppliers.add(supplier("keyword unicode", DataTypes.KEYWORD, () -> randomUnicodeOfLengthBetween(1, 10)));
+        suppliers.add(supplier("text ascii", DataTypes.TEXT, () -> randomAlphaOfLengthBetween(1, 10)));
+        suppliers.add(supplier("text unicode", DataTypes.TEXT, () -> randomUnicodeOfLengthBetween(1, 10)));
+
+        // add null as parameter
+        return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(anyNullIsNull(false, suppliers)));
+    }
+
+    public void testRandomLocale() {
+        String testString = randomAlphaOfLength(10);
+        EsqlConfiguration cfg = randomLocaleConfig();
+        ToLower func = new ToLower(Source.EMPTY, new Literal(Source.EMPTY, testString, DataTypes.KEYWORD), cfg);
+        assertThat(BytesRefs.toBytesRef(testString.toLowerCase(cfg.locale())), equalTo(func.fold()));
+    }
+
+    private EsqlConfiguration randomLocaleConfig() {
+        return new EsqlConfiguration(
+            DateUtils.UTC,
+            randomLocale(random()),
+            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
+        );
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new ToLower(source, args.get(0), EsqlTestUtils.TEST_CFG);
+    }
+
+    private static TestCaseSupplier supplier(String name, DataType type, Supplier<String> valueSupplier) {
+        return new TestCaseSupplier(name, List.of(type), () -> {
+            List<TestCaseSupplier.TypedData> values = new ArrayList<>();
+            String expectedToString = "ToLowerEvaluator[val=Attribute[channel=0], locale=en_US]";
+
+            String value = valueSupplier.get();
+            values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0"));
+
+            String expectedValue = value.toLowerCase(EsqlTestUtils.TEST_CFG.locale());
+            return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue)));
+        });
+    }
+}

+ 91 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java

@@ -0,0 +1,91 @@
+/*
+ * 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.string;
+
+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.xpack.esql.EsqlTestUtils;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.plugin.EsqlPlugin;
+import org.elasticsearch.xpack.esql.plugin.QueryPragmas;
+import org.elasticsearch.xpack.esql.session.EsqlConfiguration;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.Literal;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+import org.elasticsearch.xpack.ql.type.DateUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class ToUpperTests extends AbstractFunctionTestCase {
+    public ToUpperTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+
+        suppliers.add(supplier("keyword ascii", DataTypes.KEYWORD, () -> randomAlphaOfLengthBetween(1, 10)));
+        suppliers.add(supplier("keyword unicode", DataTypes.KEYWORD, () -> randomUnicodeOfLengthBetween(1, 10)));
+        suppliers.add(supplier("text ascii", DataTypes.TEXT, () -> randomAlphaOfLengthBetween(1, 10)));
+        suppliers.add(supplier("text unicode", DataTypes.TEXT, () -> randomUnicodeOfLengthBetween(1, 10)));
+
+        // add null as parameter
+        return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(anyNullIsNull(false, suppliers)));
+    }
+
+    public void testRandomLocale() {
+        String testString = randomAlphaOfLength(10);
+        EsqlConfiguration cfg = randomLocaleConfig();
+        ToUpper func = new ToUpper(Source.EMPTY, new Literal(Source.EMPTY, testString, DataTypes.KEYWORD), cfg);
+        assertThat(BytesRefs.toBytesRef(testString.toUpperCase(cfg.locale())), equalTo(func.fold()));
+    }
+
+    private EsqlConfiguration randomLocaleConfig() {
+        return new EsqlConfiguration(
+            DateUtils.UTC,
+            randomLocale(random()),
+            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
+        );
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new ToUpper(source, args.get(0), EsqlTestUtils.TEST_CFG);
+    }
+
+    private static TestCaseSupplier supplier(String name, DataType type, Supplier<String> valueSupplier) {
+        return new TestCaseSupplier(name, List.of(type), () -> {
+            List<TestCaseSupplier.TypedData> values = new ArrayList<>();
+            String expectedToString = "ToUpperEvaluator[val=Attribute[channel=0], locale=en_US]";
+
+            String value = valueSupplier.get();
+            values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0"));
+
+            String expectedValue = value.toUpperCase(EsqlTestUtils.TEST_CFG.locale());
+            return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue)));
+        });
+    }
+}