diff --git a/core/src/main/java/org/opensearch/sql/expression/DSL.java b/core/src/main/java/org/opensearch/sql/expression/DSL.java index 3b601f98a3..fc425c6c20 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -486,6 +486,10 @@ public static FunctionExpression replace(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.REPLACE, expressions); } + public static FunctionExpression reverse(Expression... expressions) { + return compile(FunctionProperties.None, BuiltinFunctionName.REVERSE, expressions); + } + public static FunctionExpression and(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.AND, expressions); } diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index 0b7701d8a9..b23c7613d6 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -167,6 +167,7 @@ public enum BuiltinFunctionName { POSITION(FunctionName.of("position")), REGEXP(FunctionName.of("regexp")), REPLACE(FunctionName.of("replace")), + REVERSE(FunctionName.of("reverse")), RIGHT(FunctionName.of("right")), RTRIM(FunctionName.of("rtrim")), STRCMP(FunctionName.of("strcmp")), diff --git a/core/src/main/java/org/opensearch/sql/expression/text/TextFunction.java b/core/src/main/java/org/opensearch/sql/expression/text/TextFunction.java index 5915700bf1..25eb25489c 100644 --- a/core/src/main/java/org/opensearch/sql/expression/text/TextFunction.java +++ b/core/src/main/java/org/opensearch/sql/expression/text/TextFunction.java @@ -49,6 +49,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(ltrim()); repository.register(position()); repository.register(replace()); + repository.register(reverse()); repository.register(right()); repository.register(rtrim()); repository.register(strcmp()); @@ -268,6 +269,17 @@ private DefaultFunctionResolver replace() { impl(nullMissingHandling(TextFunction::exprReplace), STRING, STRING, STRING, STRING)); } + /** + * REVERSE(str) returns reversed string of the string supplied as an argument + * Returns NULL if the argument is NULL. + * Supports the following signature: + * (STRING) -> STRING + */ + private DefaultFunctionResolver reverse() { + return define(BuiltinFunctionName.REVERSE.getName(), + impl(nullMissingHandling(TextFunction::exprReverse), STRING, STRING)); + } + private static ExprValue exprSubstrStart(ExprValue exprValue, ExprValue start) { int startIdx = start.integerValue(); if (startIdx == 0) { @@ -331,5 +343,9 @@ private static ExprValue exprLocate(ExprValue subStr, ExprValue str, ExprValue p private static ExprValue exprReplace(ExprValue str, ExprValue from, ExprValue to) { return new ExprStringValue(str.stringValue().replaceAll(from.stringValue(), to.stringValue())); } + + private static ExprValue exprReverse(ExprValue str) { + return new ExprStringValue(new StringBuilder(str.stringValue()).reverse().toString()); + } } diff --git a/core/src/test/java/org/opensearch/sql/expression/text/TextFunctionTest.java b/core/src/test/java/org/opensearch/sql/expression/text/TextFunctionTest.java index 5e32678b94..515b436c82 100644 --- a/core/src/test/java/org/opensearch/sql/expression/text/TextFunctionTest.java +++ b/core/src/test/java/org/opensearch/sql/expression/text/TextFunctionTest.java @@ -412,6 +412,18 @@ void replace() { assertEquals(missingValue(), eval(DSL.replace(missingRef, DSL.literal("a"), DSL.literal("b")))); } + @Test + void reverse() { + FunctionExpression expression = DSL.reverse(DSL.literal("abcde")); + assertEquals(STRING, expression.type()); + assertEquals("edcba", eval(expression).stringValue()); + + when(nullRef.type()).thenReturn(STRING); + assertEquals(nullValue(), eval(DSL.reverse(nullRef))); + when(missingRef.type()).thenReturn(STRING); + assertEquals(missingValue(), eval(DSL.reverse(missingRef))); + } + void testConcatString(List strings) { String expected = null; if (strings.stream().noneMatch(Objects::isNull)) { diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 7be50ccffb..843d6c7e45 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -2468,6 +2468,29 @@ Example:: +--------------------------------------------------+ +REVERSE +------- + +Description +>>>>>>>>>>> + +Usage: REVERSE(str) returns reversed string of the string supplied as an argument. Returns NULL if the argument is NULL. + +Argument type: STRING + +Return type: STRING + +Example:: + + os> SELECT REVERSE('abcde'), REVERSE(null) + fetched rows / total rows = 1/1 + +--------------------+-----------------+ + | REVERSE('abcde') | REVERSE(null) | + |--------------------+-----------------| + | edcba | null | + +--------------------+-----------------+ + + RIGHT ----- diff --git a/docs/user/ppl/functions/string.rst b/docs/user/ppl/functions/string.rst index b14acc88e0..361bc2ef37 100644 --- a/docs/user/ppl/functions/string.rst +++ b/docs/user/ppl/functions/string.rst @@ -175,6 +175,29 @@ Example:: +-------------------------------------+---------------------------------------+ +REVERSE +----- + +Description +>>>>>>>>>>> + +Usage: REVERSE(str) returns reversed string of the string supplied as an argument. + +Argument type: STRING + +Return type: STRING + +Example:: + + os> source=people | eval `REVERSE('abcde')` = REVERSE('abcde') | fields `REVERSE('abcde')` + fetched rows / total rows = 1/1 + +--------------------+ + | REVERSE('abcde') | + |--------------------| + | edcba | + +--------------------+ + + RIGHT ----- diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/TextFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/TextFunctionIT.java index 84717900ca..7c48bceab0 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/TextFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/TextFunctionIT.java @@ -139,4 +139,9 @@ public void testLocate() throws IOException { public void testReplace() throws IOException { verifyQuery("replace", "", ", 'world', ' opensearch'", "hello", " opensearch", "hello opensearch"); } + + @Test + public void testReverse() throws IOException { + verifyQuery("reverse", "", "", "olleh", "dlrow", "dlrowolleh"); + } } diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/TextFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/TextFunctionIT.java index c907b36a63..175cafd31e 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/TextFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/TextFunctionIT.java @@ -41,12 +41,26 @@ void verifyQuery(String query, String type, Integer output) throws IOException { verifyDataRows(result, rows(output)); } + void verifyQueryWithNullOutput(String query, String type) throws IOException { + JSONObject result = executeQuery("select 'test null'," + query); + verifySchema(result, schema(query, null, type), + schema("'test null'", null, type)); + verifyDataRows(result, rows("test null", null)); + } + @Test public void testRegexp() throws IOException { verifyQuery("'a' regexp 'b'", "integer", 0); verifyQuery("'a' regexp '.*'", "integer", 1); } + @Test + public void testReverse() throws IOException { + verifyQuery("reverse('hello')", "keyword", "olleh"); + verifyQuery("reverse('')", "keyword", ""); + verifyQueryWithNullOutput("reverse(null)", "keyword"); + } + @Test public void testSubstr() throws IOException { verifyQuery("substr('hello', 2)", "keyword", "ello"); diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 index 8c0340e7f1..9282c42308 100644 --- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 @@ -290,6 +290,7 @@ LEFT: 'LEFT'; ASCII: 'ASCII'; LOCATE: 'LOCATE'; REPLACE: 'REPLACE'; +REVERSE: 'REVERSE'; CAST: 'CAST'; // BOOL FUNCTIONS diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4 index 6dba1ae783..a0d6553875 100644 --- a/ppl/src/main/antlr/OpenSearchPPLParser.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4 @@ -487,7 +487,7 @@ systemFunctionBase textFunctionBase : SUBSTR | SUBSTRING | TRIM | LTRIM | RTRIM | LOWER | UPPER | CONCAT | CONCAT_WS | LENGTH | STRCMP - | RIGHT | LEFT | ASCII | LOCATE | REPLACE + | RIGHT | LEFT | ASCII | LOCATE | REPLACE | REVERSE ; positionFunctionName diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index a18aee8f10..a359f48be3 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -243,6 +243,7 @@ REPLACE: 'REPLACE'; RINT: 'RINT'; ROUND: 'ROUND'; RTRIM: 'RTRIM'; +REVERSE: 'REVERSE'; SIGN: 'SIGN'; SIGNUM: 'SIGNUM'; SIN: 'SIN'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index 6e8e0e08fe..58d4be1813 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -454,7 +454,7 @@ dateTimeFunctionName textFunctionName : SUBSTR | SUBSTRING | TRIM | LTRIM | RTRIM | LOWER | UPPER | CONCAT | CONCAT_WS | SUBSTR | LENGTH | STRCMP | RIGHT | LEFT - | ASCII | LOCATE | REPLACE + | ASCII | LOCATE | REPLACE | REVERSE ; flowControlFunctionName