*
*
* Make sure to commit them. Add a reference to the
@@ -194,6 +196,9 @@
* for your function. Now add something like {@code required_capability: my_function}
* to all of your csv-spec tests. Run those csv-spec tests as integration tests to double
* check that they run on the main branch.
+ *
+ * **Note:** you may notice tests gated based on Elasticsearch version. This was the old way
+ * of doing things. Now, we use specific capabilities for each function.
*
*
* Open the PR. The subject and description of the PR are important because those'll turn
@@ -201,7 +206,7 @@
* happy. But functions don't need an essay.
*
*
- * Add the {@code >enhancement} and {@code :Query Languages/ES|QL} tags if you are able.
+ * Add the {@code >enhancement} and {@code :Analytics/ES|QL} tags if you are able.
* Request a review if you can, probably from one of the folks that github proposes to you.
*
*
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Reverse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Reverse.java
new file mode 100644
index 0000000000000..bf4e47d8d0de4
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Reverse.java
@@ -0,0 +1,140 @@
+/*
+ * 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.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.lucene.BytesRefs;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.expression.function.scalar.UnaryScalarFunction;
+
+import java.io.IOException;
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import static org.elasticsearch.common.util.ArrayUtils.reverseArray;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString;
+
+/**
+ * Function that reverses a string.
+ */
+public class Reverse extends UnaryScalarFunction {
+ public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Reverse", Reverse::new);
+
+ @FunctionInfo(
+ returnType = { "keyword", "text" },
+ description = "Returns a new string representing the input string in reverse order.",
+ examples = {
+ @Example(file = "string", tag = "reverse"),
+ @Example(
+ file = "string",
+ tag = "reverseEmoji",
+ description = "`REVERSE` works with unicode, too! It keeps unicode grapheme clusters together during reversal."
+ ) }
+ )
+ public Reverse(
+ Source source,
+ @Param(
+ name = "str",
+ type = { "keyword", "text" },
+ description = "String expression. If `null`, the function returns `null`."
+ ) Expression field
+ ) {
+ super(source, field);
+ }
+
+ private Reverse(StreamInput in) throws IOException {
+ super(in);
+ }
+
+ @Override
+ public String getWriteableName() {
+ return ENTRY.name;
+ }
+
+ @Override
+ protected TypeResolution resolveType() {
+ if (childrenResolved() == false) {
+ return new TypeResolution("Unresolved children");
+ }
+
+ return isString(field, sourceText(), DEFAULT);
+ }
+
+ /**
+ * Reverses a unicode string, keeping grapheme clusters together
+ * @param str
+ * @return
+ */
+ public static String reverseStringWithUnicodeCharacters(String str) {
+ BreakIterator boundary = BreakIterator.getCharacterInstance(Locale.ROOT);
+ boundary.setText(str);
+
+ List characters = new ArrayList<>();
+ int start = boundary.first();
+ for (int end = boundary.next(); end != BreakIterator.DONE; start = end, end = boundary.next()) {
+ characters.add(str.substring(start, end));
+ }
+
+ StringBuilder reversed = new StringBuilder(str.length());
+ for (int i = characters.size() - 1; i >= 0; i--) {
+ reversed.append(characters.get(i));
+ }
+
+ return reversed.toString();
+ }
+
+ private static boolean isOneByteUTF8(BytesRef ref) {
+ int end = ref.offset + ref.length;
+ for (int i = ref.offset; i < end; i++) {
+ if (ref.bytes[i] < 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Evaluator
+ static BytesRef process(BytesRef val) {
+ if (isOneByteUTF8(val)) {
+ // this is the fast path. we know we can just reverse the bytes.
+ BytesRef reversed = BytesRef.deepCopyOf(val);
+ reverseArray(reversed.bytes, reversed.offset, reversed.length);
+ return reversed;
+ }
+ return BytesRefs.toBytesRef(reverseStringWithUnicodeCharacters(val.utf8ToString()));
+ }
+
+ @Override
+ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+ var fieldEvaluator = toEvaluator.apply(field);
+ return new ReverseEvaluator.Factory(source(), fieldEvaluator);
+ }
+
+ @Override
+ public Expression replaceChildren(List newChildren) {
+ assert newChildren.size() == 1;
+ return new Reverse(source(), newChildren.get(0));
+ }
+
+ @Override
+ protected NodeInfo extends Expression> info() {
+ return NodeInfo.create(this, Reverse::new, field);
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ReverseSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ReverseSerializationTests.java
new file mode 100644
index 0000000000000..7b1ad8c9dffd0
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ReverseSerializationTests.java
@@ -0,0 +1,19 @@
+/*
+ * 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.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.AbstractUnaryScalarSerializationTests;
+
+public class ReverseSerializationTests extends AbstractUnaryScalarSerializationTests {
+ @Override
+ protected Reverse create(Source source, Expression child) {
+ return new Reverse(source, child);
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ReverseTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ReverseTests.java
new file mode 100644
index 0000000000000..2873f18d53957
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ReverseTests.java
@@ -0,0 +1,65 @@
+/*
+ * 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.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class ReverseTests extends AbstractScalarFunctionTestCase {
+ public ReverseTests(@Name("TestCase") Supplier testCaseSupplier) {
+ this.testCase = testCaseSupplier.get();
+ }
+
+ @ParametersFactory
+ public static Iterable