فهرست منبع

ESQL: Implement 'right' function (#98974)

Add the 'right' function, which extracts a substring beginning from its
right end (opposite function of 'left').
---------

Co-authored-by: Alexander Spies <alexander.spies@elastic.co>
dreamquster 2 سال پیش
والد
کامیت
04381664c1
18فایلهای تغییر یافته به همراه658 افزوده شده و 60 حذف شده
  1. 5 0
      docs/changelog/98974.yaml
  2. 6 4
      docs/reference/esql/esql-functions.asciidoc
  3. 7 2
      docs/reference/esql/functions/left.asciidoc
  4. 19 0
      docs/reference/esql/functions/right.asciidoc
  5. 1 1
      docs/reference/esql/functions/signature/left.svg
  6. 1 0
      docs/reference/esql/functions/signature/right.svg
  7. 1 1
      docs/reference/esql/functions/types/left.asciidoc
  8. 5 0
      docs/reference/esql/functions/types/right.asciidoc
  9. 2 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec
  10. 20 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec
  11. 12 24
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/LeftEvaluator.java
  12. 93 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/RightEvaluator.java
  13. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  14. 12 6
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Left.java
  15. 137 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Right.java
  16. 13 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java
  17. 112 21
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/LeftTests.java
  18. 210 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RightTests.java

+ 5 - 0
docs/changelog/98974.yaml

@@ -0,0 +1,5 @@
+pr: 98974
+summary: "ESQL: RIGHT function"
+area: ES|QL
+type: feature
+issues: []

+ 6 - 4
docs/reference/esql/esql-functions.asciidoc

@@ -32,10 +32,10 @@ these functions:
 * <<esql-is_infinite>>
 * <<esql-is_nan>>
 * <<esql-least>>
+* <<esql-left>>
 * <<esql-length>>
 * <<esql-log10>>
 * <<esql-ltrim>>
-* <<esql-rtrim>>
 * <<esql-mv_avg>>
 * <<esql-mv_concat>>
 * <<esql-mv_count>>
@@ -47,13 +47,14 @@ these functions:
 * <<esql-now>>
 * <<esql-pi>>
 * <<esql-pow>>
+* <<esql-right>>
 * <<esql-round>>
+* <<esql-rtrim>>
 * <<esql-sin>>
 * <<esql-sinh>>
 * <<esql-split>>
 * <<esql-starts_with>>
 * <<esql-substring>>
-* <<esql-left>>
 * <<esql-tan>>
 * <<esql-tanh>>
 * <<esql-tau>>
@@ -94,10 +95,10 @@ include::functions/is_finite.asciidoc[]
 include::functions/is_infinite.asciidoc[]
 include::functions/is_nan.asciidoc[]
 include::functions/least.asciidoc[]
+include::functions/left.asciidoc[]
 include::functions/length.asciidoc[]
 include::functions/log10.asciidoc[]
 include::functions/ltrim.asciidoc[]
-include::functions/rtrim.asciidoc[]
 include::functions/mv_avg.asciidoc[]
 include::functions/mv_concat.asciidoc[]
 include::functions/mv_count.asciidoc[]
@@ -109,14 +110,15 @@ include::functions/mv_sum.asciidoc[]
 include::functions/now.asciidoc[]
 include::functions/pi.asciidoc[]
 include::functions/pow.asciidoc[]
+include::functions/right.asciidoc[]
 include::functions/round.asciidoc[]
+include::functions/rtrim.asciidoc[]
 include::functions/sin.asciidoc[]
 include::functions/sinh.asciidoc[]
 include::functions/split.asciidoc[]
 include::functions/sqrt.asciidoc[]
 include::functions/starts_with.asciidoc[]
 include::functions/substring.asciidoc[]
-include::functions/left.asciidoc[]
 include::functions/tan.asciidoc[]
 include::functions/tanh.asciidoc[]
 include::functions/tau.asciidoc[]

+ 7 - 2
docs/reference/esql/functions/left.asciidoc

@@ -1,8 +1,9 @@
 [[esql-left]]
 === `LEFT`
+[.text-center]
+image::esql/functions/signature/left.svg[Embedded,opts=inline]
 
-Return the substring that extract 'length' chars
-from string starting from 0.
+Return the substring that extracts 'length' chars from the 'string' starting from the left.
 
 [source.merge.styled,esql]
 ----
@@ -12,3 +13,7 @@ include::{esql-specs}/string.csv-spec[tag=left]
 |===
 include::{esql-specs}/string.csv-spec[tag=left-result]
 |===
+
+Supported types:
+
+include::types/left.asciidoc[]

+ 19 - 0
docs/reference/esql/functions/right.asciidoc

@@ -0,0 +1,19 @@
+[[esql-right]]
+=== `RIGHT`
+[.text-center]
+image::esql/functions/signature/right.svg[Embedded,opts=inline]
+
+Return the substring that extracts 'length' chars from the 'string' starting from the right.
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/string.csv-spec[tag=right]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/string.csv-spec[tag=right-result]
+|===
+
+Supported types:
+
+include::types/right.asciidoc[]

+ 1 - 1
docs/reference/esql/functions/signature/left.svg

@@ -1 +1 @@
-<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="360" height="46" viewbox="0 0 360 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 31h5m68 0h10m32 0h10m68 0h10m32 0h10m68 0h10m32 0h5"/><rect class="s" x="5" y="5" width="68" height="36"/><text class="k" x="15" y="31">LEFT</text><rect class="s" x="83" y="5" width="32" height="36" rx="7"/><text class="syn" x="93" y="31">(</text><rect class="s" x="125" y="5" width="68" height="36" rx="7"/><text class="k" x="135" y="31">arg1</text><rect class="s" x="203" y="5" width="32" height="36" rx="7"/><text class="syn" x="213" y="31">,</text><rect class="s" x="245" y="5" width="68" height="36" rx="7"/><text class="k" x="255" y="31">arg2</text><rect class="s" x="323" y="5" width="32" height="36" rx="7"/><text class="syn" x="333" y="31">)</text></svg>
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="408" height="46" viewbox="0 0 408 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 31h5m68 0h10m32 0h10m92 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="68" height="36"/><text class="k" x="15" y="31">LEFT</text><rect class="s" x="83" y="5" width="32" height="36" rx="7"/><text class="syn" x="93" y="31">(</text><rect class="s" x="125" y="5" width="92" height="36" rx="7"/><text class="k" x="135" y="31">string</text><rect class="s" x="227" y="5" width="32" height="36" rx="7"/><text class="syn" x="237" y="31">,</text><rect class="s" x="269" y="5" width="92" height="36" rx="7"/><text class="k" x="279" y="31">length</text><rect class="s" x="371" y="5" width="32" height="36" rx="7"/><text class="syn" x="381" y="31">)</text></svg>

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

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="420" height="46" viewbox="0 0 420 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 31h5m80 0h10m32 0h10m92 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="80" height="36"/><text class="k" x="15" y="31">RIGHT</text><rect class="s" x="95" y="5" width="32" height="36" rx="7"/><text class="syn" x="105" y="31">(</text><rect class="s" x="137" y="5" width="92" height="36" rx="7"/><text class="k" x="147" y="31">string</text><rect class="s" x="239" y="5" width="32" height="36" rx="7"/><text class="syn" x="249" y="31">,</text><rect class="s" x="281" y="5" width="92" height="36" rx="7"/><text class="k" x="291" y="31">length</text><rect class="s" x="383" y="5" width="32" height="36" rx="7"/><text class="syn" x="393" y="31">)</text></svg>

+ 1 - 1
docs/reference/esql/functions/types/left.asciidoc

@@ -1,5 +1,5 @@
 [%header.monospaced.styled,format=dsv,separator=|]
 |===
-arg1 | arg2 | result
+string | length | result
 keyword | integer | keyword
 |===

+ 5 - 0
docs/reference/esql/functions/types/right.asciidoc

@@ -0,0 +1,5 @@
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+string | length | result
+keyword | integer | keyword
+|===

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

@@ -36,7 +36,7 @@ is_finite                |is_finite(arg1)
 is_infinite              |is_infinite(arg1)
 is_nan                   |is_nan(arg1)
 least                    |least(first, rest...)
-left                     |left(arg1, arg2)
+left                     |left(string, length)
 length                   |length(arg1)
 log10                    |log10(n)
 ltrim                    |ltrim(arg1)
@@ -56,6 +56,7 @@ now                      |now()
 percentile               |percentile(arg1, arg2)
 pi                       |pi()
 pow                      |pow(base, exponent)
+right                    |right(string, length)
 round                    |round(arg1, arg2)
 rtrim                    |rtrim(arg1)
 sin                      |sin(n)

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

@@ -733,3 +733,23 @@ Bamford           |Bam
 Bernatsky         |Ber
 // end::left-result[]
 ;
+
+right
+// tag::right[]
+FROM employees
+| KEEP last_name
+| EVAL right = RIGHT(last_name, 3)
+| SORT last_name ASC
+| LIMIT 5
+// end::right[]
+;
+
+// tag::right-result[]
+last_name:keyword | right:keyword
+Awdeh             |deh
+Azuma             |uma
+Baek              |aek
+Bamford           |ord
+Bernatsky         |sky
+// end::right-result[]
+;

+ 12 - 24
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/LeftEvaluator.java

@@ -4,10 +4,10 @@
 // 2.0.
 package org.elasticsearch.xpack.esql.expression.function.scalar.string;
 
-import java.lang.IllegalArgumentException;
 import java.lang.Override;
 import java.lang.String;
 import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.UnicodeUtil;
 import org.elasticsearch.compute.data.Block;
 import org.elasticsearch.compute.data.BytesRefBlock;
 import org.elasticsearch.compute.data.BytesRefVector;
@@ -15,26 +15,24 @@ import org.elasticsearch.compute.data.IntBlock;
 import org.elasticsearch.compute.data.IntVector;
 import org.elasticsearch.compute.data.Page;
 import org.elasticsearch.compute.operator.EvalOperator;
-import org.elasticsearch.xpack.esql.expression.function.Warnings;
-import org.elasticsearch.xpack.ql.tree.Source;
 
 /**
  * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Left}.
  * This class is generated. Do not edit it.
  */
 public final class LeftEvaluator implements EvalOperator.ExpressionEvaluator {
-  private final Warnings warnings;
-
   private final BytesRef out;
 
+  private final UnicodeUtil.UTF8CodePoint cp;
+
   private final EvalOperator.ExpressionEvaluator str;
 
   private final EvalOperator.ExpressionEvaluator length;
 
-  public LeftEvaluator(Source source, BytesRef out, EvalOperator.ExpressionEvaluator str,
-      EvalOperator.ExpressionEvaluator length) {
-    this.warnings = new Warnings(source);
+  public LeftEvaluator(BytesRef out, UnicodeUtil.UTF8CodePoint cp,
+      EvalOperator.ExpressionEvaluator str, EvalOperator.ExpressionEvaluator length) {
     this.out = out;
+    this.cp = cp;
     this.str = str;
     this.length = length;
   }
@@ -59,7 +57,7 @@ public final class LeftEvaluator implements EvalOperator.ExpressionEvaluator {
     if (lengthVector == null) {
       return eval(page.getPositionCount(), strBlock, lengthBlock);
     }
-    return eval(page.getPositionCount(), strVector, lengthVector);
+    return eval(page.getPositionCount(), strVector, lengthVector).asBlock();
   }
 
   public BytesRefBlock eval(int positionCount, BytesRefBlock strBlock, IntBlock lengthBlock) {
@@ -74,32 +72,22 @@ public final class LeftEvaluator implements EvalOperator.ExpressionEvaluator {
         result.appendNull();
         continue position;
       }
-      try {
-        result.appendBytesRef(Left.process(out, strBlock.getBytesRef(strBlock.getFirstValueIndex(p), strScratch), lengthBlock.getInt(lengthBlock.getFirstValueIndex(p))));
-      } catch (IllegalArgumentException e) {
-        warnings.registerException(e);
-        result.appendNull();
-      }
+      result.appendBytesRef(Left.process(out, cp, strBlock.getBytesRef(strBlock.getFirstValueIndex(p), strScratch), lengthBlock.getInt(lengthBlock.getFirstValueIndex(p))));
     }
     return result.build();
   }
 
-  public BytesRefBlock eval(int positionCount, BytesRefVector strVector, IntVector lengthVector) {
-    BytesRefBlock.Builder result = BytesRefBlock.newBlockBuilder(positionCount);
+  public BytesRefVector eval(int positionCount, BytesRefVector strVector, IntVector lengthVector) {
+    BytesRefVector.Builder result = BytesRefVector.newVectorBuilder(positionCount);
     BytesRef strScratch = new BytesRef();
     position: for (int p = 0; p < positionCount; p++) {
-      try {
-        result.appendBytesRef(Left.process(out, strVector.getBytesRef(p, strScratch), lengthVector.getInt(p)));
-      } catch (IllegalArgumentException e) {
-        warnings.registerException(e);
-        result.appendNull();
-      }
+      result.appendBytesRef(Left.process(out, cp, strVector.getBytesRef(p, strScratch), lengthVector.getInt(p)));
     }
     return result.build();
   }
 
   @Override
   public String toString() {
-    return "LeftEvaluator[" + "out=" + out + ", str=" + str + ", length=" + length + "]";
+    return "LeftEvaluator[" + "str=" + str + ", length=" + length + "]";
   }
 }

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

@@ -0,0 +1,93 @@
+// 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.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.UnicodeUtil;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.IntBlock;
+import org.elasticsearch.compute.data.IntVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.EvalOperator;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Right}.
+ * This class is generated. Do not edit it.
+ */
+public final class RightEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final BytesRef out;
+
+  private final UnicodeUtil.UTF8CodePoint cp;
+
+  private final EvalOperator.ExpressionEvaluator str;
+
+  private final EvalOperator.ExpressionEvaluator length;
+
+  public RightEvaluator(BytesRef out, UnicodeUtil.UTF8CodePoint cp,
+      EvalOperator.ExpressionEvaluator str, EvalOperator.ExpressionEvaluator length) {
+    this.out = out;
+    this.cp = cp;
+    this.str = str;
+    this.length = length;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    Block strUncastBlock = str.eval(page);
+    if (strUncastBlock.areAllValuesNull()) {
+      return Block.constantNullBlock(page.getPositionCount());
+    }
+    BytesRefBlock strBlock = (BytesRefBlock) strUncastBlock;
+    Block lengthUncastBlock = length.eval(page);
+    if (lengthUncastBlock.areAllValuesNull()) {
+      return Block.constantNullBlock(page.getPositionCount());
+    }
+    IntBlock lengthBlock = (IntBlock) lengthUncastBlock;
+    BytesRefVector strVector = strBlock.asVector();
+    if (strVector == null) {
+      return eval(page.getPositionCount(), strBlock, lengthBlock);
+    }
+    IntVector lengthVector = lengthBlock.asVector();
+    if (lengthVector == null) {
+      return eval(page.getPositionCount(), strBlock, lengthBlock);
+    }
+    return eval(page.getPositionCount(), strVector, lengthVector).asBlock();
+  }
+
+  public BytesRefBlock eval(int positionCount, BytesRefBlock strBlock, IntBlock lengthBlock) {
+    BytesRefBlock.Builder result = BytesRefBlock.newBlockBuilder(positionCount);
+    BytesRef strScratch = new BytesRef();
+    position: for (int p = 0; p < positionCount; p++) {
+      if (strBlock.isNull(p) || strBlock.getValueCount(p) != 1) {
+        result.appendNull();
+        continue position;
+      }
+      if (lengthBlock.isNull(p) || lengthBlock.getValueCount(p) != 1) {
+        result.appendNull();
+        continue position;
+      }
+      result.appendBytesRef(Right.process(out, cp, strBlock.getBytesRef(strBlock.getFirstValueIndex(p), strScratch), lengthBlock.getInt(lengthBlock.getFirstValueIndex(p))));
+    }
+    return result.build();
+  }
+
+  public BytesRefVector eval(int positionCount, BytesRefVector strVector, IntVector lengthVector) {
+    BytesRefVector.Builder result = BytesRefVector.newVectorBuilder(positionCount);
+    BytesRef strScratch = new BytesRef();
+    position: for (int p = 0; p < positionCount; p++) {
+      result.appendBytesRef(Right.process(out, cp, strVector.getBytesRef(p, strScratch), lengthVector.getInt(p)));
+    }
+    return result.build();
+  }
+
+  @Override
+  public String toString() {
+    return "RightEvaluator[" + "str=" + str + ", length=" + length + "]";
+  }
+}

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

@@ -74,6 +74,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.LTrim;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Left;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Length;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.RTrim;
+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;
@@ -143,6 +144,7 @@ public class EsqlFunctionRegistry extends FunctionRegistry {
                 def(RTrim.class, RTrim::new, "rtrim"),
                 def(Trim.class, Trim::new, "trim"),
                 def(Left.class, Left::new, "left"),
+                def(Right.class, Right::new, "right"),
                 def(StartsWith.class, StartsWith::new, "starts_with") },
             // date
             new FunctionDefinition[] {

+ 12 - 6
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Left.java

@@ -13,6 +13,7 @@ import org.elasticsearch.compute.ann.Evaluator;
 import org.elasticsearch.compute.ann.Fixed;
 import org.elasticsearch.compute.operator.EvalOperator;
 import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.Named;
 import org.elasticsearch.xpack.ql.expression.Expression;
 import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
 import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
@@ -32,7 +33,7 @@ import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isInteger;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
 
 /**
- * left(foo, len) is a alias that substring(foo, 0, len)
+ * {code left(foo, len)} is an alias to {code substring(foo, 0, len)}
  */
 public class Left extends ScalarFunction implements EvaluatorMapper {
 
@@ -42,20 +43,24 @@ public class Left extends ScalarFunction implements EvaluatorMapper {
 
     private final Expression length;
 
-    public Left(Source source, Expression str, Expression length) {
+    public Left(Source source, @Named("string") Expression str, @Named("length") Expression length) {
         super(source, Arrays.asList(str, length));
         this.source = source;
         this.str = str;
         this.length = length;
     }
 
-    @Evaluator(warnExceptions = IllegalArgumentException.class)
-    static BytesRef process(@Fixed BytesRef out, BytesRef str, int length) {
+    @Evaluator
+    static BytesRef process(
+        @Fixed(includeInToString = false) BytesRef out,
+        @Fixed(includeInToString = false) UnicodeUtil.UTF8CodePoint cp,
+        BytesRef str,
+        int length
+    ) {
         out.bytes = str.bytes;
         out.offset = str.offset;
         out.length = str.length;
         int curLenStart = 0;
-        UnicodeUtil.UTF8CodePoint cp = new UnicodeUtil.UTF8CodePoint();
         for (int i = 0; i < length && curLenStart < out.length; i++, curLenStart += cp.numBytes) {
             UnicodeUtil.codePointAt(out.bytes, out.offset + curLenStart, cp);
         }
@@ -72,7 +77,8 @@ public class Left extends ScalarFunction implements EvaluatorMapper {
         Supplier<EvalOperator.ExpressionEvaluator> lengthSupplier = toEvaluator.apply(length);
         return () -> {
             BytesRef out = new BytesRef();
-            return new LeftEvaluator(source, out, strSupplier.get(), lengthSupplier.get());
+            UnicodeUtil.UTF8CodePoint cp = new UnicodeUtil.UTF8CodePoint();
+            return new LeftEvaluator(out, cp, strSupplier.get(), lengthSupplier.get());
         };
     }
 

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

@@ -0,0 +1,137 @@
+/*
+ * 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.apache.lucene.util.UnicodeUtil;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+import org.elasticsearch.xpack.esql.expression.function.Named;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
+import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.FIRST;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isInteger;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
+
+/**
+ * {code right(foo, len)} is an alias to {code substring(foo, foo.length-len, len)}
+ */
+public class Right extends ScalarFunction implements EvaluatorMapper {
+
+    private final Source source;
+
+    private final Expression str;
+
+    private final Expression length;
+
+    public Right(Source source, @Named("string") Expression str, @Named("length") Expression length) {
+        super(source, Arrays.asList(str, length));
+        this.source = source;
+        this.str = str;
+        this.length = length;
+    }
+
+    @Evaluator
+    static BytesRef process(
+        @Fixed(includeInToString = false) BytesRef out,
+        @Fixed(includeInToString = false) UnicodeUtil.UTF8CodePoint cp,
+        BytesRef str,
+        int length
+    ) {
+        out.bytes = str.bytes;
+        out.offset = str.offset;
+        out.length = str.length;
+        int codeLen = UnicodeUtil.codePointCount(str);
+        // skip the first skipLen codePoint
+        int skipLen = Math.max(codeLen - length, 0);
+        int endOffset = str.offset + str.length;
+        for (int i = 0; i < skipLen && out.offset < endOffset; i++) {
+            UnicodeUtil.codePointAt(out.bytes, out.offset, cp);
+            out.offset += cp.numBytes;
+            out.length -= cp.numBytes;
+        }
+        return out;
+    }
+
+    @Override
+    public Supplier<EvalOperator.ExpressionEvaluator> toEvaluator(
+        Function<Expression, Supplier<EvalOperator.ExpressionEvaluator>> toEvaluator
+    ) {
+
+        Supplier<EvalOperator.ExpressionEvaluator> strSupplier = toEvaluator.apply(str);
+        Supplier<EvalOperator.ExpressionEvaluator> lengthSupplier = toEvaluator.apply(length);
+        return () -> {
+            BytesRef out = new BytesRef();
+            UnicodeUtil.UTF8CodePoint cp = new UnicodeUtil.UTF8CodePoint();
+            return new RightEvaluator(out, cp, strSupplier.get(), lengthSupplier.get());
+        };
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new Right(source(), newChildren.get(0), newChildren.get(1));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, Right::new, str, length);
+    }
+
+    @Override
+    public DataType dataType() {
+        return DataTypes.KEYWORD;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        TypeResolution resolution = isString(str, sourceText(), FIRST);
+        if (resolution.unresolved()) {
+            return resolution;
+        }
+
+        resolution = isInteger(length, sourceText(), SECOND);
+        if (resolution.unresolved()) {
+            return resolution;
+        }
+
+        return TypeResolution.TYPE_RESOLVED;
+    }
+
+    @Override
+    public boolean foldable() {
+        return str.foldable() && length.foldable();
+    }
+
+    @Override
+    public Object fold() {
+        return EvaluatorMapper.super.fold();
+    }
+
+    @Override
+    public ScriptTemplate asScript() {
+        throw new UnsupportedOperationException();
+    }
+}

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

@@ -92,6 +92,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.LTrim;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Left;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Length;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.RTrim;
+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;
@@ -346,6 +347,7 @@ public final class PlanNamedTypes {
             of(ScalarFunction.class, StartsWith.class, PlanNamedTypes::writeStartsWith, PlanNamedTypes::readStartsWith),
             of(ScalarFunction.class, Substring.class, PlanNamedTypes::writeSubstring, PlanNamedTypes::readSubstring),
             of(ScalarFunction.class, Left.class, PlanNamedTypes::writeLeft, PlanNamedTypes::readLeft),
+            of(ScalarFunction.class, Right.class, PlanNamedTypes::writeRight, PlanNamedTypes::readRight),
             of(ScalarFunction.class, Split.class, PlanNamedTypes::writeSplit, PlanNamedTypes::readSplit),
             of(ScalarFunction.class, Tau.class, PlanNamedTypes::writeNoArgScalar, PlanNamedTypes::readNoArgScalar),
             // ArithmeticOperations
@@ -1304,6 +1306,17 @@ public final class PlanNamedTypes {
         out.writeExpression(fields.get(1));
     }
 
+    static Right readRight(PlanStreamInput in) throws IOException {
+        return new Right(Source.EMPTY, in.readExpression(), in.readExpression());
+    }
+
+    static void writeRight(PlanStreamOutput out, Right right) throws IOException {
+        List<Expression> fields = right.children();
+        assert fields.size() == 2;
+        out.writeExpression(fields.get(0));
+        out.writeExpression(fields.get(1));
+    }
+
     static Split readSplit(PlanStreamInput in) throws IOException {
         return new Split(Source.EMPTY, in.readExpression(), in.readExpression());
     }

+ 112 - 21
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/LeftTests.java

@@ -36,35 +36,138 @@ public class LeftTests extends AbstractScalarFunctionTestCase {
     @ParametersFactory
     public static Iterable<Object[]> parameters() {
         List<TestCaseSupplier> suppliers = new ArrayList<>();
-        suppliers.add(new TestCaseSupplier("long", () -> {
-            int length = between(1, 10);
-            String text = randomAlphaOfLength(10);
+
+        suppliers.add(new TestCaseSupplier("empty string", () -> {
+            int length = between(-64, 64);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(""), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+
+        suppliers.add(new TestCaseSupplier("ascii", () -> {
+            String text = randomAlphaOfLengthBetween(1, 64);
+            int length = between(1, text.length());
             return new TestCaseSupplier.TestCase(
                 List.of(
                     new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
                     new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
                 ),
-                "LeftEvaluator[out=[], str=Attribute[channel=0], length=Attribute[channel=1]]",
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
                 DataTypes.KEYWORD,
-                equalTo(new BytesRef(text.substring(0, length)))
+                equalTo(new BytesRef(unicodeLeftSubstring(text, length)))
             );
         }));
-        suppliers.add(new TestCaseSupplier("short", () -> {
-            int length = between(2, 10);
-            String text = randomAlphaOfLength(1);
+        suppliers.add(new TestCaseSupplier("ascii longer than string", () -> {
+            String text = randomAlphaOfLengthBetween(1, 64);
+            int length = between(text.length(), 128);
             return new TestCaseSupplier.TestCase(
                 List.of(
                     new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
                     new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
                 ),
-                "LeftEvaluator[out=[], str=Attribute[channel=0], length=Attribute[channel=1]]",
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
                 DataTypes.KEYWORD,
                 equalTo(new BytesRef(text))
             );
         }));
+        suppliers.add(new TestCaseSupplier("ascii zero length", () -> {
+            String text = randomAlphaOfLengthBetween(1, 64);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(0, DataTypes.INTEGER, "length")
+                ),
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("ascii negative length", () -> {
+            String text = randomAlphaOfLengthBetween(1, 64);
+            int length = between(-128, -1);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+
+        suppliers.add(new TestCaseSupplier("unicode", () -> {
+            String text = randomUnicodeOfLengthBetween(1, 64);
+            int length = between(1, text.length());
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(unicodeLeftSubstring(text, length)))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("unicode longer than string", () -> {
+            String text = randomUnicodeOfLengthBetween(1, 64);
+            int length = between(text.length(), 128);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(text))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("unicode zero length", () -> {
+            String text = randomUnicodeOfLengthBetween(1, 64);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(0, DataTypes.INTEGER, "length")
+                ),
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("unicode negative length", () -> {
+            String text = randomUnicodeOfLengthBetween(1, 64);
+            int length = between(-128, -1);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "LeftEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+
         return parameterSuppliersFromTypedData(suppliers);
     }
 
+    private static String unicodeLeftSubstring(String str, int length) {
+        if (length < 0) {
+            return "";
+        } else {
+            return str.codePoints()
+                .limit(length)
+                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+                .toString();
+        }
+    }
+
     @Override
     protected Expression build(Source source, List<Expression> args) {
         return new Left(source, args.get(0), args.get(1));
@@ -86,18 +189,6 @@ public class LeftTests extends AbstractScalarFunctionTestCase {
         return equalTo(new BytesRef(str.substring(0, length)));
     }
 
-    public void testReasonableLength() {
-        assertThat(process("a fox call", 5), equalTo("a fox"));
-    }
-
-    public void testMassiveLength() {
-        assertThat(process("a fox call", 10), equalTo("a fox call"));
-    }
-
-    public void testNegativeLength() {
-        assertThat(process("a fox call", -1), equalTo(""));
-    }
-
     public void testUnicode() {
         final String s = "a\ud83c\udf09tiger";
         assert s.codePointCount(0, s.length()) == 7;

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

@@ -0,0 +1,210 @@
+/*
+ * 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.compute.data.Block;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase;
+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.hamcrest.Matcher;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.compute.data.BlockUtils.toJavaObject;
+import static org.hamcrest.Matchers.equalTo;
+
+public class RightTests extends AbstractScalarFunctionTestCase {
+    public RightTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+
+        suppliers.add(new TestCaseSupplier("empty string", () -> {
+            int length = between(-64, 64);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(""), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+
+        suppliers.add(new TestCaseSupplier("ascii", () -> {
+            String text = randomAlphaOfLengthBetween(1, 64);
+            int length = between(1, text.length());
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(unicodeRightSubstring(text, length)))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("ascii longer than string", () -> {
+            String text = randomAlphaOfLengthBetween(1, 64);
+            int length = between(text.length(), 128);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(text))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("ascii zero length", () -> {
+            String text = randomAlphaOfLengthBetween(1, 64);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(0, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("ascii negative length", () -> {
+            String text = randomAlphaOfLengthBetween(1, 64);
+            int length = between(-128, -1);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+
+        suppliers.add(new TestCaseSupplier("unicode", () -> {
+            String text = randomUnicodeOfLengthBetween(1, 64);
+            int length = between(1, text.length());
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(unicodeRightSubstring(text, length)))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("unicode longer than string", () -> {
+            String text = randomUnicodeOfLengthBetween(1, 64);
+            int length = between(text.length(), 128);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(text))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("unicode zero length", () -> {
+            String text = randomUnicodeOfLengthBetween(1, 64);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(0, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+        suppliers.add(new TestCaseSupplier("unicode negative length", () -> {
+            String text = randomUnicodeOfLengthBetween(1, 64);
+            int length = between(-128, -1);
+            return new TestCaseSupplier.TestCase(
+                List.of(
+                    new TestCaseSupplier.TypedData(new BytesRef(text), DataTypes.KEYWORD, "str"),
+                    new TestCaseSupplier.TypedData(length, DataTypes.INTEGER, "length")
+                ),
+                "RightEvaluator[str=Attribute[channel=0], length=Attribute[channel=1]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(""))
+            );
+        }));
+
+        return parameterSuppliersFromTypedData(suppliers);
+    }
+
+    private static String unicodeRightSubstring(String str, int length) {
+        int codepointCount = str.codePointCount(0, str.length());
+        int codePointsToSkip = codepointCount - length;
+        if (codePointsToSkip < 0) {
+            return str;
+        } else {
+            return str.codePoints()
+                .skip(codePointsToSkip)
+                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+                .toString();
+        }
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new Right(source, args.get(0), args.get(1));
+    }
+
+    @Override
+    protected List<ArgumentSpec> argSpec() {
+        return List.of(required(strings()), required(integers()));
+    }
+
+    @Override
+    protected DataType expectedType(List<DataType> argTypes) {
+        return DataTypes.KEYWORD;
+    }
+
+    public Matcher<Object> resultsMatcher(List<TestCaseSupplier.TypedData> typedData) {
+        String str = ((BytesRef) typedData.get(0).data()).utf8ToString();
+        int length = (Integer) typedData.get(1).data();
+        return equalTo(new BytesRef(str.substring(str.length() - length)));
+    }
+
+    public void testUnicode() {
+        final String s = "a\ud83c\udf09tiger";
+        assert s.codePointCount(0, s.length()) == 7;
+        assertThat(process(s, 6), equalTo("\ud83c\udf09tiger"));
+    }
+
+    private String process(String str, int length) {
+        Block result = evaluator(
+            new Right(Source.EMPTY, field("str", DataTypes.KEYWORD), new Literal(Source.EMPTY, length, DataTypes.INTEGER))
+        ).get().eval(row(List.of(new BytesRef(str))));
+        if (null == result) {
+            return null;
+        }
+        BytesRef resultByteRef = ((BytesRef) toJavaObject(result, 0));
+        return resultByteRef == null ? null : resultByteRef.utf8ToString();
+    }
+}