diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java index 74d726d248..a765e5affb 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java @@ -151,6 +151,12 @@ public FunctionExpression gte( repository.compile(BuiltinFunctionName.GTE.getName(), Arrays.asList(expressions), env); } + public FunctionExpression like( + Environment env, Expression... expressions) { + return (FunctionExpression) + repository.compile(BuiltinFunctionName.LIKE.getName(), Arrays.asList(expressions), env); + } + public Aggregator avg(Environment env, Expression... expressions) { return (Aggregator) repository.compile(BuiltinFunctionName.AVG.getName(), Arrays.asList(expressions), env); diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/OperatorUtils.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/OperatorUtils.java index 2fec6f77ea..fa231c758d 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/OperatorUtils.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/OperatorUtils.java @@ -28,7 +28,6 @@ import java.util.function.BiFunction; import java.util.function.BiPredicate; import java.util.function.Function; -import java.util.regex.Pattern; import java.util.stream.Collectors; import lombok.experimental.UtilityClass; diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/BinaryPredicateOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/BinaryPredicateOperator.java index 8903f02110..2381a0b0ff 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/BinaryPredicateOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/BinaryPredicateOperator.java @@ -19,6 +19,8 @@ import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_MISSING; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_NULL; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_TRUE; +import static com.amazon.opendistroforelasticsearch.sql.expression.operator.OperatorUtils.binaryOperator; +import static com.amazon.opendistroforelasticsearch.sql.utils.OperatorUtils.matches; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprType; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; @@ -66,6 +68,7 @@ public static void register(BuiltinFunctionRepository repository) { repository.register(lte()); repository.register(greater()); repository.register(gte()); + repository.register(like()); } /** @@ -184,7 +187,6 @@ public static void register(BuiltinFunctionRepository repository) { .put(LITERAL_MISSING, LITERAL_MISSING, LITERAL_FALSE) .build(); - private static FunctionResolver and() { FunctionName functionName = BuiltinFunctionName.AND.getName(); return FunctionResolver.builder() @@ -309,6 +311,21 @@ private static FunctionResolver gte() { ); } + private static FunctionResolver like() { + return new FunctionResolver( + BuiltinFunctionName.LIKE.getName(), + predicate( + BuiltinFunctionName.LIKE.getName(), + (v1, v2) -> matches(v2, v1) + ) + ); + } + + /** + * Util method to generate EQUAL/NOT EQUAL operation bundles. + * Applicable for integer, long, float, double, string types of operands + * {@param defaultValue} Default value for one missing/null operand + */ private static Map predicate( FunctionName functionName, Table table, @@ -350,6 +367,11 @@ private static Map predicate( .build(); } + /** + * Util method to generate binary predicate bundles. + * Applicable for integer, long, float, double, string types of operands + * Missing/Null value operands follow as {@param table} lists + */ private static Map predicate( FunctionName functionName, BiFunction integerFunc, @@ -359,24 +381,50 @@ private static Map predicate( BiFunction stringFunc) { ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); return builder - .put(new FunctionSignature(functionName, Arrays.asList(ExprType.INTEGER, ExprType.INTEGER)), - compareValue(functionName, integerFunc, ExprValueUtils::getIntegerValue, - ExprType.BOOLEAN)) - .put(new FunctionSignature(functionName, Arrays.asList(ExprType.LONG, ExprType.LONG)), - compareValue(functionName, longFunc, ExprValueUtils::getLongValue, - ExprType.BOOLEAN)) - .put(new FunctionSignature(functionName, Arrays.asList(ExprType.FLOAT, ExprType.FLOAT)), - compareValue(functionName, floatFunc, ExprValueUtils::getFloatValue, - ExprType.BOOLEAN)) - .put(new FunctionSignature(functionName, Arrays.asList(ExprType.DOUBLE, ExprType.DOUBLE)), - compareValue(functionName, doubleFunc, ExprValueUtils::getDoubleValue, - ExprType.BOOLEAN)) + .put( + new FunctionSignature(functionName, Arrays.asList(ExprType.INTEGER, ExprType.INTEGER)), + binaryOperator( + functionName, integerFunc, ExprValueUtils::getIntegerValue, ExprType.BOOLEAN)) + .put( + new FunctionSignature(functionName, Arrays.asList(ExprType.LONG, ExprType.LONG)), + binaryOperator( + functionName, longFunc, ExprValueUtils::getLongValue, ExprType.BOOLEAN)) + .put( + new FunctionSignature(functionName, Arrays.asList(ExprType.FLOAT, ExprType.FLOAT)), + binaryOperator( + functionName, floatFunc, ExprValueUtils::getFloatValue, ExprType.BOOLEAN)) + .put( + new FunctionSignature(functionName, Arrays.asList(ExprType.DOUBLE, ExprType.DOUBLE)), + binaryOperator( + functionName, doubleFunc, ExprValueUtils::getDoubleValue, ExprType.BOOLEAN)) + .put( + new FunctionSignature(functionName, Arrays.asList(ExprType.STRING, ExprType.STRING)), + binaryOperator( + functionName, stringFunc, ExprValueUtils::getStringValue, ExprType.BOOLEAN)) + .build(); + } + + /** + * Util method to generate LIKE predicate bundles. + * Applicable for string operands. + */ + private static Map predicate( + FunctionName functionName, + BiFunction stringFunc) { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + return builder .put(new FunctionSignature(functionName, Arrays.asList(ExprType.STRING, ExprType.STRING)), - compareValue(functionName, stringFunc, ExprValueUtils::getStringValue, + binaryOperator(functionName, stringFunc, ExprValueUtils::getStringValue, ExprType.BOOLEAN)) .build(); } + + /** + * Building method to construct binary logical predicates AND OR XOR + * Where operands order does not matter. + * Special cases for missing/null operands refer to {@param table}. + */ private static FunctionBuilder binaryPredicate(FunctionName functionName, Table table, ExprType returnType) { @@ -442,37 +490,4 @@ public String toString() { } }; } - - /** - * Building method for operators including. - * less than (<) operator - * less than or equal to (<=) operator - * greater than (>) operator - * greater than or equal to (>=) operator - */ - private static FunctionBuilder compareValue(FunctionName functionName, - BiFunction function, - Function observer, - ExprType returnType) { - return arguments -> new FunctionExpression(functionName, arguments) { - @Override - public ExprValue valueOf(Environment env) { - ExprValue arg1 = arguments.get(0).valueOf(env); - ExprValue arg2 = arguments.get(1).valueOf(env); - return ExprValueUtils.fromObjectValue( - function.apply(observer.apply(arg1), observer.apply(arg2))); - } - - @Override - public ExprType type(Environment env) { - return returnType; - } - - @Override - public String toString() { - return String.format("%s %s %s", arguments.get(0).toString(), functionName, arguments - .get(1).toString()); - } - }; - } } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/utils/OperatorUtils.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/utils/OperatorUtils.java new file mode 100644 index 0000000000..2b5479ebf9 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/utils/OperatorUtils.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.sql.utils; + +import java.util.regex.Pattern; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class OperatorUtils { + /** + * Wildcard pattern matcher util. + * Percent (%) character for wildcard, + * Underscore (_) character for a single character match. + * @param pattern string pattern to match. + * @return if text matches pattern returns true; else return false. + */ + public static boolean matches(String pattern, String text) { + return Pattern.compile(patternToRegex(pattern)).matcher(text).matches(); + } + + private static final char DEFAULT_ESCAPE = '\\'; + + private static String patternToRegex(String patternString) { + StringBuilder regex = new StringBuilder(patternString.length() * 2); + regex.append('^'); + boolean escaped = false; + for (char currentChar : patternString.toCharArray()) { + if (!escaped && currentChar == DEFAULT_ESCAPE) { + escaped = true; + } else { + switch (currentChar) { + case '%': + if (escaped) { + regex.append("%"); + } else { + regex.append(".*"); + } + escaped = false; + break; + case '_': + if (escaped) { + regex.append("_"); + } else { + regex.append('.'); + } + escaped = false; + break; + default: + switch (currentChar) { + case '\\': + case '^': + case '$': + case '.': + case '*': + case '[': + case ']': + case '(': + case ')': + case '|': + case '+': + regex.append('\\'); + break; + default: + } + + regex.append(currentChar); + escaped = false; + } + } + } + regex.append('$'); + return regex.toString(); + } +} diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/config/TestConfig.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/config/TestConfig.java index 1bbf6d2d98..72b556ff22 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/config/TestConfig.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/config/TestConfig.java @@ -41,6 +41,8 @@ public class TestConfig { public static final String INT_TYPE_MISSING_VALUE_FIELD = "int_missing_value"; public static final String BOOL_TYPE_NULL_VALUE_FIELD = "null_value_boolean"; public static final String BOOL_TYPE_MISSING_VALUE_FIELD = "missing_value_boolean"; + public static final String STRING_TYPE_NULL_VALUE_FILED = "string_null_value"; + public static final String STRING_TYPE_MISSING_VALUE_FILED = "string_missing_value"; private static Map typeMapping = new ImmutableMap.Builder() .put("integer_value", ExprType.INTEGER) @@ -53,6 +55,8 @@ public class TestConfig { .put(BOOL_TYPE_NULL_VALUE_FIELD, ExprType.BOOLEAN) .put(BOOL_TYPE_MISSING_VALUE_FIELD, ExprType.BOOLEAN) .put("string_value", ExprType.STRING) + .put(STRING_TYPE_NULL_VALUE_FILED, ExprType.STRING) + .put(STRING_TYPE_MISSING_VALUE_FILED, ExprType.STRING) .put("struct_value", ExprType.STRUCT) .put("array_value", ExprType.ARRAY) .build(); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionTestBase.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionTestBase.java index cb64d1d3fd..02e45fca77 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionTestBase.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionTestBase.java @@ -19,6 +19,8 @@ import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.BOOL_TYPE_NULL_VALUE_FIELD; import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.INT_TYPE_MISSING_VALUE_FIELD; import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.INT_TYPE_NULL_VALUE_FIELD; +import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.STRING_TYPE_MISSING_VALUE_FILED; +import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.STRING_TYPE_NULL_VALUE_FILED; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.booleanValue; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.collectionValue; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.doubleValue; @@ -82,9 +84,11 @@ protected Environment valueEnv() { return collectionValue(ImmutableList.of(1)); case BOOL_TYPE_NULL_VALUE_FIELD: case INT_TYPE_NULL_VALUE_FIELD: + case STRING_TYPE_NULL_VALUE_FILED: return nullValue(); case INT_TYPE_MISSING_VALUE_FIELD: case BOOL_TYPE_MISSING_VALUE_FIELD: + case STRING_TYPE_MISSING_VALUE_FILED: return missingValue(); default: throw new IllegalArgumentException("undefined reference"); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/BinaryPredicateOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/BinaryPredicateOperatorTest.java index 99fbc4190c..6e169427b2 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/BinaryPredicateOperatorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/BinaryPredicateOperatorTest.java @@ -19,6 +19,8 @@ import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.BOOL_TYPE_NULL_VALUE_FIELD; import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.INT_TYPE_MISSING_VALUE_FIELD; import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.INT_TYPE_NULL_VALUE_FIELD; +import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.STRING_TYPE_MISSING_VALUE_FILED; +import static com.amazon.opendistroforelasticsearch.sql.config.TestConfig.STRING_TYPE_NULL_VALUE_FILED; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_FALSE; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_MISSING; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_NULL; @@ -26,18 +28,12 @@ import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.booleanValue; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.fromObjectValue; import static com.amazon.opendistroforelasticsearch.sql.utils.ComparisonUtil.compare; +import static com.amazon.opendistroforelasticsearch.sql.utils.OperatorUtils.matches; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprDoubleValue; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprFloatValue; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprLongValue; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprType; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils; -import com.amazon.opendistroforelasticsearch.sql.exception.ExpressionEvaluationException; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; import com.amazon.opendistroforelasticsearch.sql.expression.FunctionExpression; @@ -99,6 +95,21 @@ private static Stream testCompareValueArguments() { return builder.build(); } + private static Stream testLikeArguments() { + List> arguments = Arrays.asList( + Arrays.asList("foo", "foo"), Arrays.asList("notFoo", "foo"), + Arrays.asList("foobar", "%bar"), Arrays.asList("bar", "%bar"), + Arrays.asList("foo", "fo_"), Arrays.asList("foo", "foo_"), + Arrays.asList("foorbar", "%o_ar"), Arrays.asList("foobar", "%o_a%"), + Arrays.asList("fooba%_\\^$.*[]()|+r", "%\\%\\_\\\\\\^\\$\\.\\*\\[\\]\\(\\)\\|\\+_") + ); + Stream.Builder builder = Stream.builder(); + for (List argPair : arguments) { + builder.add(Arguments.of(fromObjectValue(argPair.get(0)), fromObjectValue(argPair.get(1)))); + } + return builder.build(); + } + @ParameterizedTest(name = "and({0}, {1})") @MethodSource("binaryPredicateArguments") public void test_and(Boolean v1, Boolean v2) { @@ -435,16 +446,47 @@ public void test_less(ExprValue v1, ExprValue v2) { public void test_less_null() { FunctionExpression less = dsl.less(typeEnv(), DSL.literal(1), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); - assertThrows(ExpressionEvaluationException.class, - () -> less.valueOf(valueEnv()), "invalid to call type operation on null value"); + assertEquals(ExprType.BOOLEAN, less.type(typeEnv())); + assertEquals(LITERAL_NULL, less.valueOf(valueEnv())); + + less = dsl.less(typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.literal(1)); + assertEquals(ExprType.BOOLEAN, less.type(typeEnv())); + assertEquals(LITERAL_NULL, less.valueOf(valueEnv())); + + less = dsl.less( + typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, less.type(typeEnv())); + assertEquals(LITERAL_NULL, less.valueOf(valueEnv())); } @Test public void test_less_missing() { FunctionExpression less = dsl.less(typeEnv(), DSL.literal(1), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); - assertThrows(ExpressionEvaluationException.class, - () -> less.valueOf(valueEnv()), "invalid to call type operation on missing value"); + assertEquals(ExprType.BOOLEAN, less.type(typeEnv())); + assertEquals(LITERAL_MISSING, less.valueOf(valueEnv())); + + less = dsl.less(typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.literal(1)); + assertEquals(ExprType.BOOLEAN, less.type(typeEnv())); + assertEquals(LITERAL_MISSING, less.valueOf(valueEnv())); + + less = dsl.less( + typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, less.type(typeEnv())); + assertEquals(LITERAL_MISSING, less.valueOf(valueEnv())); + } + + @Test + public void test_null_less_missing() { + FunctionExpression less = dsl.less( + typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, less.type(typeEnv())); + assertEquals(LITERAL_MISSING, less.valueOf(valueEnv())); + + less = dsl.less( + typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, less.type(typeEnv())); + assertEquals(LITERAL_MISSING, less.valueOf(valueEnv())); } @ParameterizedTest(name = "lte({0}, {1})") @@ -461,16 +503,47 @@ public void test_lte(ExprValue v1, ExprValue v2) { public void test_lte_null() { FunctionExpression lte = dsl.lte(typeEnv(), DSL.literal(1), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); - assertThrows(ExpressionEvaluationException.class, - () -> lte.valueOf(valueEnv()), "invalid to call type operation on null value"); + assertEquals(ExprType.BOOLEAN, lte.type(typeEnv())); + assertEquals(LITERAL_NULL, lte.valueOf(valueEnv())); + + lte = dsl.lte(typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.literal(1)); + assertEquals(ExprType.BOOLEAN, lte.type(typeEnv())); + assertEquals(LITERAL_NULL, lte.valueOf(valueEnv())); + + lte = dsl.lte( + typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, lte.type(typeEnv())); + assertEquals(LITERAL_NULL, lte.valueOf(valueEnv())); } @Test public void test_lte_missing() { FunctionExpression lte = dsl.lte(typeEnv(), DSL.literal(1), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); - assertThrows(ExpressionEvaluationException.class, - () -> lte.valueOf(valueEnv()), "invalid to call type operation on missing value"); + assertEquals(ExprType.BOOLEAN, lte.type(typeEnv())); + assertEquals(LITERAL_MISSING, lte.valueOf(valueEnv())); + + lte = dsl.lte(typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.literal(1)); + assertEquals(ExprType.BOOLEAN, lte.type(typeEnv())); + assertEquals(LITERAL_MISSING, lte.valueOf(valueEnv())); + + lte = dsl.lte( + typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, lte.type(typeEnv())); + assertEquals(LITERAL_MISSING, lte.valueOf(valueEnv())); + } + + @Test + public void test_null_lte_missing() { + FunctionExpression lte = dsl.lte(typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), + DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, lte.type(typeEnv())); + assertEquals(LITERAL_MISSING, lte.valueOf(valueEnv())); + + lte = dsl.lte(typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), + DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, lte.type(typeEnv())); + assertEquals(LITERAL_MISSING, lte.valueOf(valueEnv())); } @ParameterizedTest(name = "greater({0}, {1})") @@ -487,16 +560,47 @@ public void test_greater(ExprValue v1, ExprValue v2) { public void test_greater_null() { FunctionExpression greater = dsl.greater(typeEnv(), DSL.literal(1), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); - assertThrows(ExpressionEvaluationException.class, - () -> greater.valueOf(valueEnv()), "invalid to call type operation on null value"); + assertEquals(ExprType.BOOLEAN, greater.type(typeEnv())); + assertEquals(LITERAL_NULL, greater.valueOf(valueEnv())); + + greater = dsl.greater(typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.literal(1)); + assertEquals(ExprType.BOOLEAN, greater.type(typeEnv())); + assertEquals(LITERAL_NULL, greater.valueOf(valueEnv())); + + greater = dsl.greater( + typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, greater.type(typeEnv())); + assertEquals(LITERAL_NULL, greater.valueOf(valueEnv())); } @Test public void test_greater_missing() { FunctionExpression greater = dsl.greater(typeEnv(), DSL.literal(1), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); - assertThrows(ExpressionEvaluationException.class, - () -> greater.valueOf(valueEnv()), "invalid to call type operation on missing value"); + assertEquals(ExprType.BOOLEAN, greater.type(typeEnv())); + assertEquals(LITERAL_MISSING, greater.valueOf(valueEnv())); + + greater = dsl.greater(typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.literal(1)); + assertEquals(ExprType.BOOLEAN, greater.type(typeEnv())); + assertEquals(LITERAL_MISSING, greater.valueOf(valueEnv())); + + greater = dsl.greater( + typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, greater.type(typeEnv())); + assertEquals(LITERAL_MISSING, greater.valueOf(valueEnv())); + } + + @Test + public void test_null_greater_missing() { + FunctionExpression greater = dsl.greater( + typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, greater.type(typeEnv())); + assertEquals(LITERAL_MISSING, greater.valueOf(valueEnv())); + + greater = dsl.greater( + typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, greater.type(typeEnv())); + assertEquals(LITERAL_MISSING, greater.valueOf(valueEnv())); } @ParameterizedTest(name = "gte({0}, {1})") @@ -513,15 +617,104 @@ public void test_gte(ExprValue v1, ExprValue v2) { public void test_gte_null() { FunctionExpression gte = dsl.gte(typeEnv(), DSL.literal(1), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); - assertThrows(ExpressionEvaluationException.class, - () -> gte.valueOf(valueEnv()), "invalid to call type operation on null value"); + assertEquals(ExprType.BOOLEAN, gte.type(typeEnv())); + assertEquals(LITERAL_NULL, gte.valueOf(valueEnv())); + + gte = dsl.gte(typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.literal(1)); + assertEquals(ExprType.BOOLEAN, gte.type(typeEnv())); + assertEquals(LITERAL_NULL, gte.valueOf(valueEnv())); + + gte = dsl.gte( + typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, gte.type(typeEnv())); + assertEquals(LITERAL_NULL, gte.valueOf(valueEnv())); } @Test public void test_gte_missing() { FunctionExpression gte = dsl.gte(typeEnv(), DSL.literal(1), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); - assertThrows(ExpressionEvaluationException.class, - () -> gte.valueOf(valueEnv()), "invalid to call type operation on missing value"); + assertEquals(ExprType.BOOLEAN, gte.type(typeEnv())); + assertEquals(LITERAL_MISSING, gte.valueOf(valueEnv())); + + gte = dsl.gte(typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.literal(1)); + assertEquals(ExprType.BOOLEAN, gte.type(typeEnv())); + assertEquals(LITERAL_MISSING, gte.valueOf(valueEnv())); + + gte = dsl.gte( + typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, gte.type(typeEnv())); + assertEquals(LITERAL_MISSING, gte.valueOf(valueEnv())); + } + + @Test + public void test_null_gte_missing() { + FunctionExpression gte = dsl.gte( + typeEnv(), DSL.ref(INT_TYPE_NULL_VALUE_FIELD), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, gte.type(typeEnv())); + assertEquals(LITERAL_MISSING, gte.valueOf(valueEnv())); + + gte = dsl.gte( + typeEnv(), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD), DSL.ref(INT_TYPE_NULL_VALUE_FIELD)); + assertEquals(ExprType.BOOLEAN, gte.type(typeEnv())); + assertEquals(LITERAL_MISSING, gte.valueOf(valueEnv())); + } + + @ParameterizedTest(name = "like({0}, {1})") + @MethodSource("testLikeArguments") + public void test_like(ExprValue v1, ExprValue v2) { + FunctionExpression like = dsl.like(typeEnv(), DSL.literal(v1), DSL.literal(v2)); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(matches(((String) v2.value()), (String) v1.value()), + ExprValueUtils.getBooleanValue(like.valueOf(valueEnv()))); + assertEquals(String.format("%s like %s", v1.toString(), v2.toString()), like.toString()); + } + + @Test + public void test_like_null() { + FunctionExpression like = dsl.like( + typeEnv(), DSL.literal("str"), DSL.ref(STRING_TYPE_NULL_VALUE_FILED)); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(LITERAL_NULL, like.valueOf(valueEnv())); + + like = dsl.like(typeEnv(), DSL.ref(STRING_TYPE_NULL_VALUE_FILED), DSL.literal("str")); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(LITERAL_NULL, like.valueOf(valueEnv())); + + like = dsl.like( + typeEnv(), DSL.ref(STRING_TYPE_NULL_VALUE_FILED), DSL.ref(STRING_TYPE_NULL_VALUE_FILED)); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(LITERAL_NULL, like.valueOf(valueEnv())); + } + + @Test + public void test_like_missing() { + FunctionExpression like = dsl.like( + typeEnv(), DSL.literal("str"), DSL.ref(STRING_TYPE_MISSING_VALUE_FILED)); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(LITERAL_MISSING, like.valueOf(valueEnv())); + + like = dsl.like(typeEnv(), DSL.ref(STRING_TYPE_MISSING_VALUE_FILED), DSL.literal("str")); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(LITERAL_MISSING, like.valueOf(valueEnv())); + + like = dsl.like( + typeEnv(), DSL.ref(STRING_TYPE_MISSING_VALUE_FILED), + DSL.ref(STRING_TYPE_MISSING_VALUE_FILED)); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(LITERAL_MISSING, like.valueOf(valueEnv())); + } + + @Test + public void test_null_like_missing() { + FunctionExpression like = dsl.like( + typeEnv(), DSL.ref(STRING_TYPE_NULL_VALUE_FILED), DSL.ref(STRING_TYPE_MISSING_VALUE_FILED)); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(LITERAL_MISSING, like.valueOf(valueEnv())); + + like = dsl.like( + typeEnv(), DSL.ref(STRING_TYPE_MISSING_VALUE_FILED), DSL.ref(STRING_TYPE_NULL_VALUE_FILED)); + assertEquals(ExprType.BOOLEAN, like.type(typeEnv())); + assertEquals(LITERAL_MISSING, like.valueOf(valueEnv())); } } \ No newline at end of file diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/OperatorIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/OperatorIT.java index 2b0db65b4a..5e632ce3a0 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/OperatorIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/OperatorIT.java @@ -264,6 +264,14 @@ public void testGteOperator() throws IOException { verifyDataRows(result, rows(36), rows(36), rows(39)); } + @Test + public void testLikeOperator() throws IOException { + JSONObject result = + executeQuery( + String.format("source=%s firstname like 'Hatti_' | fields firstname", TEST_INDEX_BANK)); + verifyDataRows(result, rows("Hattie")); + } + @Test public void testBinaryPredicateWithNullValue() { queryExecutionShouldThrowExceptionDueToNullOrMissingValue( diff --git a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 index db6eac0404..8b5eb77e10 100644 --- a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 @@ -65,6 +65,7 @@ AND: 'AND'; XOR: 'XOR'; TRUE: 'TRUE'; FALSE: 'FALSE'; +LIKE: 'LIKE'; // DATASET TYPES DATAMODEL: 'DATAMODEL'; diff --git a/ppl/src/main/antlr/OpenDistroPPLParser.g4 b/ppl/src/main/antlr/OpenDistroPPLParser.g4 index 28a99ecbda..f9e3cdb7e3 100644 --- a/ppl/src/main/antlr/OpenDistroPPLParser.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLParser.g4 @@ -216,7 +216,7 @@ textFunctionBase /** operators */ comparisonOperator - : EQUAL | NOT_EQUAL | LESS | NOT_LESS | GREATER | NOT_GREATER + : EQUAL | NOT_EQUAL | LESS | NOT_LESS | GREATER | NOT_GREATER | LIKE ; binaryOperator diff --git a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstExpressionBuilderTest.java b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstExpressionBuilderTest.java index c3ce0249bf..86a061fe2f 100644 --- a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstExpressionBuilderTest.java +++ b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstExpressionBuilderTest.java @@ -110,6 +110,15 @@ public void testLogicalXorExpr() { )); } + @Test + public void testLogicalLikeExpr() { + assertEqual("source=t a like '_a%b%c_d_'", + filter( + relation("t"), + compare("like", field("a"), stringLiteral("_a%b%c_d_")) + )); + } + /** * Todo. search operator should not include functionCall, need to change antlr. */