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
This commit is contained in:
Benedict Jin 2019-04-10 11:46:29 +08:00 committed by Mingming Qiu
parent 89bb43f382
commit 2f64414ade
11 changed files with 787 additions and 1 deletions

View File

@ -28,6 +28,7 @@ import java.net.URLEncoder;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.IllegalFormatException; import java.util.IllegalFormatException;
import java.util.Locale; import java.util.Locale;
@ -360,4 +361,54 @@ public class StringUtils
{ {
return BASE64_DECODER.decode(input); 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);
}
} }

View File

@ -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 class ParseLong implements Function
{ {
@Override @Override
@ -326,7 +343,6 @@ interface Function
} }
} }
class Div extends DoubleParamMath class Div extends DoubleParamMath
{ {
@Override @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 class ReplaceFunc implements Function
{ {
@Override @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 class IsNullFunc implements Function
{ {
@Override @Override

View File

@ -20,7 +20,9 @@
package org.apache.druid.java.util.common; package org.apache.druid.java.util.common;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException; import java.nio.BufferUnderflowException;
@ -31,6 +33,9 @@ import java.nio.ByteBuffer;
*/ */
public class StringUtilsTest public class StringUtilsTest
{ {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test @Test
public void fromUtf8ConversionTest() throws UnsupportedEncodingException public void fromUtf8ConversionTest() throws UnsupportedEncodingException
{ {
@ -160,4 +165,20 @@ public class StringUtilsTest
Assert.assertEquals(s2, "fff%2Bggg"); Assert.assertEquals(s2, "fff%2Bggg");
Assert.assertEquals("fff+ggg", StringUtils.urlDecode(s2)); 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));
}
} }

View File

@ -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.| |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| |replace|replace(expr, pattern, replacement) replaces pattern with replacement|
|substring|substring(expr, index, length) behaves like java.lang.String's substring| |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| |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.| |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.| |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.| |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| |lower|lower(expr) converts a string to lowercase|
|upper|upper(expr) converts a string to uppercase| |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 ## Time functions

View File

@ -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.| |`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.| |`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.| |`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.| |`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".| |`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>`).| |`BTRIM(expr[, chars])`|Alternate form of `TRIM(BOTH <chars> FROM <expr>`).|
|`LTRIM(expr[, chars])`|Alternate form of `TRIM(LEADING <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>`).| |`RTRIM(expr[, chars])`|Alternate form of `TRIM(TRAILING <chars> FROM <expr>`).|
|`UPPER(expr)`|Returns expr in all uppercase.| |`UPPER(expr)`|Returns expr in all uppercase.|
|`REVERSE(expr)`|Reverses expr.|
|`REPEAT(expr, [N])`|Repeats expr N times|
### Time functions ### Time functions

View File

@ -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)
)
);
}
}

View File

@ -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)
)
);
}
}

View File

@ -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)
)
);
}
}

View File

@ -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)
)
);
}
}

View File

@ -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.ExtractOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.FloorOperatorConversion; 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.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.LikeOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.MillisToTimestampOperatorConversion; import org.apache.druid.sql.calcite.expression.builtin.MillisToTimestampOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.ParseLongOperatorConversion; 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.RTrimOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.RegexpExtractOperatorConversion; 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.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.StringFormatOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion; import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.SubstringOperatorConversion; import org.apache.druid.sql.calcite.expression.builtin.SubstringOperatorConversion;
@ -180,6 +184,10 @@ public class DruidOperatorTable implements SqlOperatorTable
.add(new StringFormatOperatorConversion()) .add(new StringFormatOperatorConversion())
.add(new StrposOperatorConversion()) .add(new StrposOperatorConversion())
.add(new SubstringOperatorConversion()) .add(new SubstringOperatorConversion())
.add(new RightOperatorConversion())
.add(new LeftOperatorConversion())
.add(new ReverseOperatorConversion())
.add(new RepeatOperatorConversion())
.add(new AliasedOperatorConversion(new SubstringOperatorConversion(), "SUBSTR")) .add(new AliasedOperatorConversion(new SubstringOperatorConversion(), "SUBSTR"))
.add(new ConcatOperatorConversion()) .add(new ConcatOperatorConversion())
.add(new TextcatOperatorConversion()) .add(new TextcatOperatorConversion())

View File

@ -36,13 +36,18 @@ import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.druid.common.config.NullHandling; import org.apache.druid.common.config.NullHandling;
import org.apache.druid.java.util.common.DateTimes; 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.ExprEval;
import org.apache.druid.math.expr.Parser; import org.apache.druid.math.expr.Parser;
import org.apache.druid.query.extraction.RegexDimExtractionFn; import org.apache.druid.query.extraction.RegexDimExtractionFn;
import org.apache.druid.segment.column.ValueType; import org.apache.druid.segment.column.ValueType;
import org.apache.druid.sql.calcite.expression.builtin.DateTruncOperatorConversion; 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.ParseLongOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.RegexpExtractOperatorConversion; 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.StringFormatOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion; import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.TimeExtractOperatorConversion; 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.DateTimeZone;
import org.joda.time.Period; import org.joda.time.Period;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Map; import java.util.Map;
@ -69,6 +76,9 @@ import java.util.Map;
public class ExpressionsTest extends CalciteTestBase public class ExpressionsTest extends CalciteTestBase
{ {
@Rule
public ExpectedException expectedException = ExpectedException.none();
private final PlannerContext plannerContext = PlannerContext.create( private final PlannerContext plannerContext = PlannerContext.create(
CalciteTests.createOperatorTable(), CalciteTests.createOperatorTable(),
CalciteTests.createExprMacroTable(), 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) private RexNode inputRef(final String columnName)
{ {
final int columnNumber = rowSignature.getRowOrder().indexOf(columnName); final int columnNumber = rowSignature.getRowOrder().indexOf(columnName);