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 30f5ab8cf6f..850e5a44ccd 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. + *

+ * If count or length is zero then the empty string is returned. + *

+ * 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 4378ac1e479..000b8939162 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 69d209a7b69..6f5a3b553c7 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 53d5a433184..ce3389f1b57 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 a9e781bbf51..bac04eca029 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] [ 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 FROM `).| |`LTRIM(expr[, chars])`|Alternate form of `TRIM(LEADING FROM `).| |`RTRIM(expr[, chars])`|Alternate form of `TRIM(TRAILING FROM `).| |`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 00000000000..65aeecaa3d2 --- /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 00000000000..a15cab11fcf --- /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 00000000000..9b3a434b296 --- /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 00000000000..a2274bb83c9 --- /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 f6868413490..c6759b5f021 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 c48bcb19b79..fa890a10379 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);