You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by qm...@apache.org on 2019/04/10 03:46:35 UTC

[incubator-druid] branch master updated: Add "REVERSE" / "REPEAT" / "RIGHT" / "LEFT" functions (#7334)

This is an automated email from the ASF dual-hosted git repository.

qmm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 2f64414  Add "REVERSE" / "REPEAT" / "RIGHT" / "LEFT" functions (#7334)
2f64414 is described below

commit 2f64414ade164576e0ca60dfbd7ba43262d9c373
Author: Benedict Jin <as...@apache.org>
AuthorDate: Wed Apr 10 11:46:29 2019 +0800

    Add "REVERSE" / "REPEAT" / "RIGHT" / "LEFT" functions (#7334)
    
    * Add "REVERSE" / "REPEAT" / "RIGHT" / "LEFT" functions
    
    * Fix ImportOrder
    
    * Use RuntimeException instead of OutOfMemoryError according to "Effective Java"
    
    * Simplify
    
    * Patch suggestions
---
 .../apache/druid/java/util/common/StringUtils.java |  51 ++++
 .../java/org/apache/druid/math/expr/Function.java  |  98 ++++++-
 .../druid/java/util/common/StringUtilsTest.java    |  21 ++
 docs/content/misc/math-expr.md                     |   4 +
 docs/content/querying/sql.md                       |   4 +
 .../expression/builtin/LeftOperatorConversion.java |  80 ++++++
 .../builtin/RepeatOperatorConversion.java          |  80 ++++++
 .../builtin/ReverseOperatorConversion.java         |  66 +++++
 .../builtin/RightOperatorConversion.java           |  80 ++++++
 .../sql/calcite/planner/DruidOperatorTable.java    |   8 +
 .../sql/calcite/expression/ExpressionsTest.java    | 296 +++++++++++++++++++++
 11 files changed, 787 insertions(+), 1 deletion(-)

diff --git a/core/src/main/java/org/apache/druid/java/util/common/StringUtils.java b/core/src/main/java/org/apache/druid/java/util/common/StringUtils.java
index 30f5ab8..850e5a4 100644
--- a/core/src/main/java/org/apache/druid/java/util/common/StringUtils.java
+++ b/core/src/main/java/org/apache/druid/java/util/common/StringUtils.java
@@ -28,6 +28,7 @@ import java.net.URLEncoder;
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.Base64;
 import java.util.IllegalFormatException;
 import java.util.Locale;
@@ -360,4 +361,54 @@ public class StringUtils
   {
     return BASE64_DECODER.decode(input);
   }
+
+  /**
+   * Returns a string whose value is the concatenation of the
+   * string {@code s} repeated {@code count} times.
+   * <p>
+   * If count or length is zero then the empty string is returned.
+   * <p>
+   * This method may be used to create space padding for
+   * formatting text or zero padding for formatting numbers.
+   *
+   * @param count number of times to repeat
+   *
+   * @return A string composed of this string repeated
+   * {@code count} times or the empty string if count
+   * or length is zero.
+   *
+   * @throws IllegalArgumentException if the {@code count} is negative.
+   * @link https://bugs.openjdk.java.net/browse/JDK-8197594
+   */
+  public static String repeat(String s, int count)
+  {
+    if (count < 0) {
+      throw new IllegalArgumentException("count is negative, " + count);
+    }
+    if (count == 1) {
+      return s;
+    }
+    byte[] value = s.getBytes(StandardCharsets.UTF_8);
+    final int len = value.length;
+    if (len == 0 || count == 0) {
+      return "";
+    }
+    if (len == 1) {
+      final byte[] single = new byte[count];
+      Arrays.fill(single, value[0]);
+      return new String(single, StandardCharsets.UTF_8);
+    }
+    if (Integer.MAX_VALUE / count < len) {
+      throw new RuntimeException("The produced string is too large.");
+    }
+    final int limit = len * count;
+    final byte[] multiple = new byte[limit];
+    System.arraycopy(value, 0, multiple, 0, len);
+    int copied = len;
+    for (; copied < limit - copied; copied <<= 1) {
+      System.arraycopy(multiple, 0, multiple, copied, copied);
+    }
+    System.arraycopy(multiple, 0, multiple, copied, limit - copied);
+    return new String(multiple, StandardCharsets.UTF_8);
+  }
 }
diff --git a/core/src/main/java/org/apache/druid/math/expr/Function.java b/core/src/main/java/org/apache/druid/math/expr/Function.java
index 4378ac1..000b893 100644
--- a/core/src/main/java/org/apache/druid/math/expr/Function.java
+++ b/core/src/main/java/org/apache/druid/math/expr/Function.java
@@ -122,6 +122,23 @@ interface Function
     }
   }
 
+  abstract class DoubleParamString extends DoubleParam
+  {
+    @Override
+    protected final ExprEval eval(ExprEval x, ExprEval y)
+    {
+      if (x.type() != ExprType.STRING || y.type() != ExprType.LONG) {
+        throw new IAE(
+            "Function[%s] needs a string as first argument and an integer as second argument",
+            name()
+        );
+      }
+      return eval(x.asString(), y.asInt());
+    }
+
+    protected abstract ExprEval eval(String x, int y);
+  }
+
   class ParseLong implements Function
   {
     @Override
@@ -326,7 +343,6 @@ interface Function
     }
   }
 
-
   class Div extends DoubleParamMath
   {
     @Override
@@ -1126,6 +1142,49 @@ interface Function
     }
   }
 
+  class RightFunc extends DoubleParamString
+  {
+    @Override
+    public String name()
+    {
+      return "right";
+    }
+
+    @Override
+    protected ExprEval eval(String x, int y)
+    {
+      if (y < 0) {
+        throw new IAE(
+            "Function[%s] needs a postive integer as second argument",
+            name()
+        );
+      }
+      int len = x.length();
+      return ExprEval.of(y < len ? x.substring(len - y) : x);
+    }
+  }
+
+  class LeftFunc extends DoubleParamString
+  {
+    @Override
+    public String name()
+    {
+      return "left";
+    }
+
+    @Override
+    protected ExprEval eval(String x, int y)
+    {
+      if (y < 0) {
+        throw new IAE(
+            "Function[%s] needs a postive integer as second argument",
+            name()
+        );
+      }
+      return ExprEval.of(y < x.length() ? x.substring(0, y) : x);
+    }
+  }
+
   class ReplaceFunc implements Function
   {
     @Override
@@ -1197,6 +1256,43 @@ interface Function
     }
   }
 
+  class ReverseFunc extends SingleParam
+  {
+    @Override
+    public String name()
+    {
+      return "reverse";
+    }
+
+    @Override
+    protected ExprEval eval(ExprEval param)
+    {
+      if (param.type() != ExprType.STRING) {
+        throw new IAE(
+            "Function[%s] needs a string argument",
+            name()
+        );
+      }
+      final String arg = param.asString();
+      return ExprEval.of(arg == null ? NullHandling.defaultStringValue() : new StringBuilder(arg).reverse().toString());
+    }
+  }
+
+  class RepeatFunc extends DoubleParamString
+  {
+    @Override
+    public String name()
+    {
+      return "repeat";
+    }
+
+    @Override
+    protected ExprEval eval(String x, int y)
+    {
+      return ExprEval.of(y < 1 ? NullHandling.defaultStringValue() : StringUtils.repeat(x, y));
+    }
+  }
+
   class IsNullFunc implements Function
   {
     @Override
diff --git a/core/src/test/java/org/apache/druid/java/util/common/StringUtilsTest.java b/core/src/test/java/org/apache/druid/java/util/common/StringUtilsTest.java
index 69d209a..6f5a3b5 100644
--- a/core/src/test/java/org/apache/druid/java/util/common/StringUtilsTest.java
+++ b/core/src/test/java/org/apache/druid/java/util/common/StringUtilsTest.java
@@ -20,7 +20,9 @@
 package org.apache.druid.java.util.common;
 
 import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import java.io.UnsupportedEncodingException;
 import java.nio.BufferUnderflowException;
@@ -31,6 +33,9 @@ import java.nio.ByteBuffer;
  */
 public class StringUtilsTest
 {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   @Test
   public void fromUtf8ConversionTest() throws UnsupportedEncodingException
   {
@@ -160,4 +165,20 @@ public class StringUtilsTest
     Assert.assertEquals(s2, "fff%2Bggg");
     Assert.assertEquals("fff+ggg", StringUtils.urlDecode(s2));
   }
+
+  @Test
+  public void testRepeat()
+  {
+    Assert.assertEquals("", StringUtils.repeat("foo", 0));
+    Assert.assertEquals("foo", StringUtils.repeat("foo", 1));
+    Assert.assertEquals("foofoofoo", StringUtils.repeat("foo", 3));
+
+    Assert.assertEquals("", StringUtils.repeat("", 0));
+    Assert.assertEquals("", StringUtils.repeat("", 1));
+    Assert.assertEquals("", StringUtils.repeat("", 3));
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("count is negative, -1");
+    Assert.assertEquals("", StringUtils.repeat("foo", -1));
+  }
 }
diff --git a/docs/content/misc/math-expr.md b/docs/content/misc/math-expr.md
index 53d5a43..ce3389f 100644
--- a/docs/content/misc/math-expr.md
+++ b/docs/content/misc/math-expr.md
@@ -74,6 +74,8 @@ The following built-in functions are available.
 |regexp_extract|regexp_extract(expr, pattern[, index]) applies a regular expression pattern and extracts a capture group index, or null if there is no match. If index is unspecified or zero, returns the substring that matched the pattern.|
 |replace|replace(expr, pattern, replacement) replaces pattern with replacement|
 |substring|substring(expr, index, length) behaves like java.lang.String's substring|
+|right|right(expr, length) returns the rightmost length characters from a string|
+|left|left(expr, length) returns the leftmost length characters from a string|
 |strlen|strlen(expr) returns length of a string in UTF-16 code units|
 |strpos|strpos(haystack, needle[, fromIndex]) returns the position of the needle within the haystack, with indexes starting from 0. The search will begin at fromIndex, or 0 if fromIndex is not specified. If the needle is not found then the function returns -1.|
 |trim|trim(expr[, chars]) remove leading and trailing characters from `expr` if they are present in `chars`. `chars` defaults to ' ' (space) if not provided.|
@@ -81,6 +83,8 @@ The following built-in functions are available.
 |rtrim|rtrim(expr[, chars]) remove trailing characters from `expr` if they are present in `chars`. `chars` defaults to ' ' (space) if not provided.|
 |lower|lower(expr) converts a string to lowercase|
 |upper|upper(expr) converts a string to uppercase|
+|reverse|reverse(expr) reverses a string|
+|repeat|repeat(expr, N) repeats a string N times|
 
 ## Time functions
 
diff --git a/docs/content/querying/sql.md b/docs/content/querying/sql.md
index a9e781b..bac04ec 100644
--- a/docs/content/querying/sql.md
+++ b/docs/content/querying/sql.md
@@ -187,12 +187,16 @@ String functions accept strings, and return a type appropriate to the function.
 |`REPLACE(expr, pattern, replacement)`|Replaces pattern with replacement in expr, and returns the result.|
 |`STRPOS(haystack, needle)`|Returns the index of needle within haystack, with indexes starting from 1. If the needle is not found, returns 0.|
 |`SUBSTRING(expr, index, [length])`|Returns a substring of expr starting at index, with a max length, both measured in UTF-16 code units.|
+|`RIGHT(expr, [length])`|Returns the rightmost length characters from expr.|
+|`LEFT(expr, [length])`|Returns the leftmost length characters from expr.|
 |`SUBSTR(expr, index, [length])`|Synonym for SUBSTRING.|
 |`TRIM([BOTH \| LEADING \| TRAILING] [<chars> FROM] expr)`|Returns expr with characters removed from the leading, trailing, or both ends of "expr" if they are in "chars". If "chars" is not provided, it defaults to " " (a space). If the directional argument is not provided, it defaults to "BOTH".|
 |`BTRIM(expr[, chars])`|Alternate form of `TRIM(BOTH <chars> FROM <expr>`).|
 |`LTRIM(expr[, chars])`|Alternate form of `TRIM(LEADING <chars> FROM <expr>`).|
 |`RTRIM(expr[, chars])`|Alternate form of `TRIM(TRAILING <chars> FROM <expr>`).|
 |`UPPER(expr)`|Returns expr in all uppercase.|
+|`REVERSE(expr)`|Reverses expr.|
+|`REPEAT(expr, [N])`|Repeats expr N times|
 
 ### Time functions
 
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/LeftOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/LeftOperatorConversion.java
new file mode 100644
index 0000000..65aeeca
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/LeftOperatorConversion.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.expression.builtin;
+
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.sql.calcite.expression.DruidExpression;
+import org.apache.druid.sql.calcite.expression.Expressions;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.sql.calcite.table.RowSignature;
+
+public class LeftOperatorConversion implements SqlOperatorConversion
+{
+  private static final SqlFunction SQL_FUNCTION = OperatorConversions
+      .operatorBuilder("LEFT")
+      .operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.INTEGER)
+      .functionCategory(SqlFunctionCategory.STRING)
+      .returnType(SqlTypeName.VARCHAR)
+      .build();
+
+  @Override
+  public SqlOperator calciteOperator()
+  {
+    return SQL_FUNCTION;
+  }
+
+  @Override
+  public DruidExpression toDruidExpression(
+      final PlannerContext plannerContext,
+      final RowSignature rowSignature,
+      final RexNode rexNode
+  )
+  {
+    final RexCall call = (RexCall) rexNode;
+    final DruidExpression input = Expressions.toDruidExpression(
+        plannerContext,
+        rowSignature,
+        call.getOperands().get(0)
+    );
+    if (input == null) {
+      return null;
+    }
+    if (call.getOperands().size() != 2) {
+      return null;
+    }
+    return OperatorConversions.convertCall(
+        plannerContext,
+        rowSignature,
+        rexNode,
+        druidExpressions -> DruidExpression.of(
+            null,
+            DruidExpression.functionCall("left", druidExpressions)
+        )
+    );
+  }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RepeatOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RepeatOperatorConversion.java
new file mode 100644
index 0000000..a15cab1
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RepeatOperatorConversion.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.expression.builtin;
+
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.sql.calcite.expression.DruidExpression;
+import org.apache.druid.sql.calcite.expression.Expressions;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.sql.calcite.table.RowSignature;
+
+public class RepeatOperatorConversion implements SqlOperatorConversion
+{
+  private static final SqlFunction SQL_FUNCTION = OperatorConversions
+      .operatorBuilder("REPEAT")
+      .operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.INTEGER)
+      .functionCategory(SqlFunctionCategory.STRING)
+      .returnType(SqlTypeName.VARCHAR)
+      .build();
+
+  @Override
+  public SqlOperator calciteOperator()
+  {
+    return SQL_FUNCTION;
+  }
+
+  @Override
+  public DruidExpression toDruidExpression(
+      final PlannerContext plannerContext,
+      final RowSignature rowSignature,
+      final RexNode rexNode
+  )
+  {
+    final RexCall call = (RexCall) rexNode;
+    final DruidExpression input = Expressions.toDruidExpression(
+        plannerContext,
+        rowSignature,
+        call.getOperands().get(0)
+    );
+    if (input == null) {
+      return null;
+    }
+    if (call.getOperands().size() != 2) {
+      return null;
+    }
+    return OperatorConversions.convertCall(
+        plannerContext,
+        rowSignature,
+        rexNode,
+        druidExpressions -> DruidExpression.of(
+            null,
+            DruidExpression.functionCall("repeat", druidExpressions)
+        )
+    );
+  }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ReverseOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ReverseOperatorConversion.java
new file mode 100644
index 0000000..9b3a434
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ReverseOperatorConversion.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.expression.builtin;
+
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.sql.calcite.expression.DruidExpression;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.sql.calcite.table.RowSignature;
+
+public class ReverseOperatorConversion implements SqlOperatorConversion
+{
+  private static final SqlFunction SQL_FUNCTION = OperatorConversions
+      .operatorBuilder("REVERSE")
+      .operandTypes(SqlTypeFamily.CHARACTER)
+      .functionCategory(SqlFunctionCategory.STRING)
+      .returnType(SqlTypeName.VARCHAR)
+      .build();
+
+  @Override
+  public SqlOperator calciteOperator()
+  {
+    return SQL_FUNCTION;
+  }
+
+  @Override
+  public DruidExpression toDruidExpression(
+      final PlannerContext plannerContext,
+      final RowSignature rowSignature,
+      final RexNode rexNode
+  )
+  {
+    return OperatorConversions.convertCall(
+        plannerContext,
+        rowSignature,
+        rexNode,
+        druidExpressions -> DruidExpression.of(
+            null,
+            DruidExpression.functionCall("reverse", druidExpressions)
+        )
+    );
+  }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RightOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RightOperatorConversion.java
new file mode 100644
index 0000000..a2274bb
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RightOperatorConversion.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.expression.builtin;
+
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.sql.calcite.expression.DruidExpression;
+import org.apache.druid.sql.calcite.expression.Expressions;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.sql.calcite.table.RowSignature;
+
+public class RightOperatorConversion implements SqlOperatorConversion
+{
+  private static final SqlFunction SQL_FUNCTION = OperatorConversions
+      .operatorBuilder("RIGHT")
+      .operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.INTEGER)
+      .functionCategory(SqlFunctionCategory.STRING)
+      .returnType(SqlTypeName.VARCHAR)
+      .build();
+
+  @Override
+  public SqlOperator calciteOperator()
+  {
+    return SQL_FUNCTION;
+  }
+
+  @Override
+  public DruidExpression toDruidExpression(
+      final PlannerContext plannerContext,
+      final RowSignature rowSignature,
+      final RexNode rexNode
+  )
+  {
+    final RexCall call = (RexCall) rexNode;
+    final DruidExpression input = Expressions.toDruidExpression(
+        plannerContext,
+        rowSignature,
+        call.getOperands().get(0)
+    );
+    if (input == null) {
+      return null;
+    }
+    if (call.getOperands().size() != 2) {
+      return null;
+    }
+    return OperatorConversions.convertCall(
+        plannerContext,
+        rowSignature,
+        rexNode,
+        druidExpressions -> DruidExpression.of(
+            null,
+            DruidExpression.functionCall("right", druidExpressions)
+        )
+    );
+  }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
index f686841..c6759b5 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
@@ -54,6 +54,7 @@ import org.apache.druid.sql.calcite.expression.builtin.DateTruncOperatorConversi
 import org.apache.druid.sql.calcite.expression.builtin.ExtractOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.FloorOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.LTrimOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.LeftOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.LikeOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.MillisToTimestampOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.ParseLongOperatorConversion;
@@ -61,6 +62,9 @@ import org.apache.druid.sql.calcite.expression.builtin.PositionOperatorConversio
 import org.apache.druid.sql.calcite.expression.builtin.RTrimOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.RegexpExtractOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.ReinterpretOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.RepeatOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.ReverseOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.RightOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.StringFormatOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.SubstringOperatorConversion;
@@ -180,6 +184,10 @@ public class DruidOperatorTable implements SqlOperatorTable
           .add(new StringFormatOperatorConversion())
           .add(new StrposOperatorConversion())
           .add(new SubstringOperatorConversion())
+          .add(new RightOperatorConversion())
+          .add(new LeftOperatorConversion())
+          .add(new ReverseOperatorConversion())
+          .add(new RepeatOperatorConversion())
           .add(new AliasedOperatorConversion(new SubstringOperatorConversion(), "SUBSTR"))
           .add(new ConcatOperatorConversion())
           .add(new TextcatOperatorConversion())
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
index c48bcb1..fa890a1 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
@@ -36,13 +36,18 @@ import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.druid.common.config.NullHandling;
 import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.IAE;
 import org.apache.druid.math.expr.ExprEval;
 import org.apache.druid.math.expr.Parser;
 import org.apache.druid.query.extraction.RegexDimExtractionFn;
 import org.apache.druid.segment.column.ValueType;
 import org.apache.druid.sql.calcite.expression.builtin.DateTruncOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.LeftOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.ParseLongOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.RegexpExtractOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.RepeatOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.ReverseOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.RightOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.StringFormatOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TimeExtractOperatorConversion;
@@ -61,7 +66,9 @@ import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.Period;
 import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import java.math.BigDecimal;
 import java.util.Map;
@@ -69,6 +76,9 @@ import java.util.Map;
 public class ExpressionsTest extends CalciteTestBase
 {
 
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   private final PlannerContext plannerContext = PlannerContext.create(
       CalciteTests.createOperatorTable(),
       CalciteTests.createExprMacroTable(),
@@ -895,6 +905,292 @@ public class ExpressionsTest extends CalciteTestBase
     );
   }
 
+  @Test
+  public void testReverse()
+  {
+    testExpression(
+        rexBuilder.makeCall(
+            new ReverseOperatorConversion().calciteOperator(),
+            inputRef("s")
+        ),
+        DruidExpression.fromExpression("reverse(\"s\")"),
+        "oof"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new ReverseOperatorConversion().calciteOperator(),
+            inputRef("spacey")
+        ),
+        DruidExpression.fromExpression("reverse(\"spacey\")"),
+        "  ereht yeh  "
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new ReverseOperatorConversion().calciteOperator(),
+            inputRef("tstr")
+        ),
+        DruidExpression.fromExpression("reverse(\"tstr\")"),
+        "60:50:40 30-20-0002"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new ReverseOperatorConversion().calciteOperator(),
+            inputRef("dstr")
+        ),
+        DruidExpression.fromExpression("reverse(\"dstr\")"),
+        "30-20-0002"
+    );
+  }
+
+  @Test
+  public void testAbnormalReverseWithWrongType()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("Function[reverse] needs a string argument");
+
+    testExpression(
+        rexBuilder.makeCall(
+            new ReverseOperatorConversion().calciteOperator(),
+            inputRef("a")
+        ),
+        DruidExpression.fromExpression("reverse(\"a\")"),
+        null
+    );
+  }
+
+  @Test
+  public void testRight()
+  {
+    testExpression(
+        rexBuilder.makeCall(
+            new RightOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(1)
+        ),
+        DruidExpression.fromExpression("right(\"s\",1)"),
+        "o"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RightOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(2)
+        ),
+        DruidExpression.fromExpression("right(\"s\",2)"),
+        "oo"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RightOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(3)
+        ),
+        DruidExpression.fromExpression("right(\"s\",3)"),
+        "foo"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RightOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(4)
+        ),
+        DruidExpression.fromExpression("right(\"s\",4)"),
+        "foo"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RightOperatorConversion().calciteOperator(),
+            inputRef("tstr"),
+            integerLiteral(5)
+        ),
+        DruidExpression.fromExpression("right(\"tstr\",5)"),
+        "05:06"
+    );
+  }
+
+  @Test
+  public void testAbnormalRightWithNegativeNumber()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("Function[right] needs a postive integer as second argument");
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RightOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(-1)
+        ),
+        DruidExpression.fromExpression("right(\"s\",-1)"),
+        null
+    );
+  }
+
+  @Test
+  public void testAbnormalRightWithWrongType()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("Function[right] needs a string as first argument "
+                                    + "and an integer as second argument");
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RightOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            inputRef("s")
+        ),
+        DruidExpression.fromExpression("right(\"s\",\"s\")"),
+        null
+    );
+  }
+
+  @Test
+  public void testLeft()
+  {
+    testExpression(
+        rexBuilder.makeCall(
+            new LeftOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(1)
+        ),
+        DruidExpression.fromExpression("left(\"s\",1)"),
+        "f"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new LeftOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(2)
+        ),
+        DruidExpression.fromExpression("left(\"s\",2)"),
+        "fo"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new LeftOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(3)
+        ),
+        DruidExpression.fromExpression("left(\"s\",3)"),
+        "foo"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new LeftOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(4)
+        ),
+        DruidExpression.fromExpression("left(\"s\",4)"),
+        "foo"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new LeftOperatorConversion().calciteOperator(),
+            inputRef("tstr"),
+            integerLiteral(10)
+        ),
+        DruidExpression.fromExpression("left(\"tstr\",10)"),
+        "2000-02-03"
+    );
+  }
+
+  @Test
+  public void testAbnormalLeftWithNegativeNumber()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("Function[left] needs a postive integer as second argument");
+
+    testExpression(
+        rexBuilder.makeCall(
+            new LeftOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(-1)
+        ),
+        DruidExpression.fromExpression("left(\"s\",-1)"),
+        null
+    );
+  }
+
+  @Test
+  public void testAbnormalLeftWithWrongType()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("Function[left] needs a string as first argument "
+                                    + "and an integer as second argument");
+
+    testExpression(
+        rexBuilder.makeCall(
+            new LeftOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            inputRef("s")
+        ),
+        DruidExpression.fromExpression("left(\"s\",\"s\")"),
+        null
+    );
+  }
+
+  @Test
+  public void testRepeat()
+  {
+    testExpression(
+        rexBuilder.makeCall(
+            new RepeatOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(1)
+        ),
+        DruidExpression.fromExpression("repeat(\"s\",1)"),
+        "foo"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RepeatOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(3)
+        ),
+        DruidExpression.fromExpression("repeat(\"s\",3)"),
+        "foofoofoo"
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RepeatOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            integerLiteral(-1)
+        ),
+        DruidExpression.fromExpression("repeat(\"s\",-1)"),
+        null
+    );
+  }
+
+  @Test
+  public void testAbnormalRepeatWithWrongType()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("Function[repeat] needs a string as first argument "
+                                    + "and an integer as second argument");
+
+    testExpression(
+        rexBuilder.makeCall(
+            new RepeatOperatorConversion().calciteOperator(),
+            inputRef("s"),
+            inputRef("s")
+        ),
+        DruidExpression.fromExpression("repeat(\"s\",\"s\")"),
+        null
+    );
+  }
+
   private RexNode inputRef(final String columnName)
   {
     final int columnNumber = rowSignature.getRowOrder().indexOf(columnName);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org