diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java index 6d123b45a8..9f32331673 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java @@ -52,7 +52,6 @@ import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionRepository; import com.amazon.opendistroforelasticsearch.sql.expression.function.FunctionName; import com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation.AggregateWindowFunction; -import com.amazon.opendistroforelasticsearch.sql.expression.window.ranking.RankingWindowFunction; import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.Arrays; 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 0d03ddc536..107ffa2533 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 @@ -461,6 +461,14 @@ public FunctionExpression notLike(Expression... expressions) { return function(BuiltinFunctionName.NOT_LIKE, expressions); } + public FunctionExpression between(Expression... expressions) { + return function(BuiltinFunctionName.BETWEEN, expressions); + } + + public FunctionExpression not_between(Expression... expressions) { + return not(between(expressions)); + } + public Aggregator avg(Expression... expressions) { return aggregate(BuiltinFunctionName.AVG, expressions); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java index 6b29c68da1..a7252c4b69 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java @@ -92,7 +92,7 @@ public enum BuiltinFunctionName { MODULES(FunctionName.of("%")), /** - * Boolean Operators. + * Comparison Operators. */ AND(FunctionName.of("and")), OR(FunctionName.of("or")), @@ -106,6 +106,7 @@ public enum BuiltinFunctionName { GTE(FunctionName.of(">=")), LIKE(FunctionName.of("like")), NOT_LIKE(FunctionName.of("not like")), + BETWEEN(FunctionName.of("between")), /** * Aggregation Function. 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 d08c3fab8f..7fe90bc8c2 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 @@ -20,8 +20,13 @@ 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.data.type.ExprCoreType.BOOLEAN; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DATE; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DATETIME; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DOUBLE; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIME; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIMESTAMP; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprBooleanValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; @@ -63,6 +68,7 @@ public static void register(BuiltinFunctionRepository repository) { repository.register(like()); repository.register(notLike()); repository.register(regexp()); + repository.register(between()); } /** @@ -262,6 +268,22 @@ private static FunctionResolver notLike() { STRING)); } + private static FunctionResolver between() { + return FunctionDSL.define(BuiltinFunctionName.BETWEEN.getName(), + FunctionDSL.impl(FunctionDSL.nullMissingHandling(OperatorUtils::between), + BOOLEAN, DOUBLE, DOUBLE, DOUBLE), + FunctionDSL.impl(FunctionDSL.nullMissingHandling(OperatorUtils::between), + BOOLEAN, STRING, STRING, STRING), + FunctionDSL.impl(FunctionDSL.nullMissingHandling(OperatorUtils::between), + BOOLEAN, DATE, DATE, DATE), + FunctionDSL.impl(FunctionDSL.nullMissingHandling(OperatorUtils::between), + BOOLEAN, DATETIME, DATETIME, DATETIME), + FunctionDSL.impl(FunctionDSL.nullMissingHandling(OperatorUtils::between), + BOOLEAN, TIME, TIME, TIME), + FunctionDSL.impl(FunctionDSL.nullMissingHandling(OperatorUtils::between), + BOOLEAN, TIMESTAMP, TIMESTAMP, TIMESTAMP)); + } + private static ExprValue lookupTableFunction(ExprValue arg1, ExprValue arg2, Table table) { if (table.contains(arg1, arg2)) { 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 index d887d5c391..ecc554442c 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/utils/OperatorUtils.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/utils/OperatorUtils.java @@ -15,9 +15,12 @@ package com.amazon.opendistroforelasticsearch.sql.utils; +import com.amazon.opendistroforelasticsearch.sql.data.model.AbstractExprNumberValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprBooleanValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import java.sql.Timestamp; import java.util.regex.Pattern; import lombok.experimental.UtilityClass; @@ -99,4 +102,26 @@ private static String patternToRegex(String patternString) { regex.append('$'); return regex.toString(); } + + /** + * BETWEEN ... AND ... operator util. + * Expression { expr BETWEEN min AND max } is to judge if min <= expr <= max. + */ + public static ExprBooleanValue between(ExprValue expr, ExprValue min, ExprValue max) { + return ExprBooleanValue.of(isBetween(expr, min, max)); + } + + private static boolean isBetween(ExprValue expr, ExprValue min, ExprValue max) { + if (expr instanceof AbstractExprNumberValue) { + return ((AbstractExprNumberValue) expr).compare(min) >= 0 + && ((AbstractExprNumberValue) expr).compare(max) <= 0; + } else if (expr instanceof ExprStringValue) { + return ((ExprStringValue) expr).compare(min) >= 0 + && ((ExprStringValue) expr).compare(max) <= 0; + } else { + return expr.compareTo(min) >= 0 && expr.compareTo(max) <= 0; + } + } } + + diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java index 7e6fa063ea..f1739d3e1a 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java @@ -22,6 +22,7 @@ import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_TRUE; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.integerValue; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.BOOLEAN; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.FLOAT; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRUCT; import static java.util.Collections.emptyList; 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 aa7402142c..71da6a59b1 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 @@ -27,14 +27,18 @@ import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_TRUE; 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.data.model.ExprValueUtils.missingValue; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.BOOLEAN; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DATE; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DATETIME; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIME; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIMESTAMP; 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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprBooleanValue; @@ -49,14 +53,15 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; 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.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; import com.amazon.opendistroforelasticsearch.sql.expression.FunctionExpression; +import com.amazon.opendistroforelasticsearch.sql.utils.OperatorUtils; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; -import com.sun.org.apache.xpath.internal.Arg; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; @@ -72,7 +77,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mock; class BinaryPredicateOperatorTest extends ExpressionTestBase { @@ -166,6 +170,45 @@ private static Stream testLikeArguments() { return builder.build(); } + private static Stream testBetweenArguments() { + List arguments = Arrays.asList( + Arrays.asList(1, 0, 2), Arrays.asList(1, 2, 0), + Arrays.asList(1L, 1L, 2L), Arrays.asList(2L, 1L, 2L), + Arrays.asList(3F, 1F, 2F), Arrays.asList(0F, 1F, 2F), + Arrays.asList(1D, 1D, 1D), Arrays.asList(1D, 2D, 2D), + Arrays.asList("b", "a", "c"), Arrays.asList("b", "c", "a"), + Arrays.asList("a", "a", "b"), Arrays.asList("b", "a", "b"), + Arrays.asList("c", "a", "b"), Arrays.asList("a", "b", "c"), + Arrays.asList("a", "a", "a"), Arrays.asList("b", "a", "a")); + Stream.Builder builder = Stream.builder(); + for (List argGroup: arguments) { + builder.add(Arguments.of(fromObjectValue(argGroup.get(0)), fromObjectValue(argGroup.get(1)), + fromObjectValue(argGroup.get(2)))); + } + builder + .add(Arguments.of(fromObjectValue("2021-01-02", DATE), + fromObjectValue("2021-01-01", DATE), fromObjectValue("2021-01-03", DATE))) + .add(Arguments.of(fromObjectValue("2021-01-02", DATE), + fromObjectValue("2021-01-03", DATE), fromObjectValue("2021-01-01", DATE))) + .add(Arguments.of(fromObjectValue("01:00:00", TIME), + fromObjectValue("01:00:00", TIME), fromObjectValue("02:00:00", TIME))) + .add(Arguments.of(fromObjectValue("02:00:00", TIME), + fromObjectValue("01:00:00", TIME), fromObjectValue("02:00:00", TIME))) + .add(Arguments.of(fromObjectValue("2021-01-01 03:00:00", DATETIME), + fromObjectValue("2021-01-01 01:00:00", DATETIME), + fromObjectValue("2021-01-01 02:00:00", DATETIME))) + .add(Arguments.of(fromObjectValue("2021-01-01 00:00:00", DATETIME), + fromObjectValue("2021-01-01 01:00:00", DATETIME), + fromObjectValue("2021-01-01 02:00:00", DATETIME))) + .add(Arguments.of(fromObjectValue("2021-01-01 01:00:00", TIMESTAMP), + fromObjectValue("2021-01-01 01:00:00", TIMESTAMP), + fromObjectValue("2021-01-01 01:00:00", TIMESTAMP))) + .add(Arguments.of(fromObjectValue("2021-01-01 00:00:00", TIMESTAMP), + fromObjectValue("2021-01-01 01:00:00", TIMESTAMP), + fromObjectValue("2021-01-01 01:00:00", TIMESTAMP))); + return builder.build(); + } + @ParameterizedTest(name = "and({0}, {1})") @MethodSource("binaryPredicateArguments") public void test_and(Boolean v1, Boolean v2) { @@ -832,4 +875,44 @@ public void compare_int_long() { FunctionExpression equal = dsl.equal(DSL.literal(1), DSL.literal(1L)); assertTrue(equal.valueOf(valueEnv()).booleanValue()); } + + @ParameterizedTest(name = "between({0}, {1}, {2})") + @MethodSource("testBetweenArguments") + public void between(ExprValue value, ExprValue minValue, ExprValue maxValue) { + FunctionExpression between = dsl.between( + DSL.literal(value), DSL.literal(minValue), DSL.literal(maxValue)); + assertEquals(BOOLEAN, between.type()); + assertEquals(OperatorUtils.between(value, minValue, maxValue), between.valueOf(valueEnv())); + } + + @ParameterizedTest(name = "not between({0}, {1}, {2})") + @MethodSource("testBetweenArguments") + public void not_between(ExprValue value, ExprValue minValue, ExprValue maxValue) { + FunctionExpression notBetween = dsl.not_between( + DSL.literal(value), DSL.literal(minValue), DSL.literal(maxValue)); + assertEquals(BOOLEAN, notBetween.type()); + assertEquals(!OperatorUtils.between(value, minValue, maxValue).booleanValue(), + notBetween.valueOf(valueEnv()).booleanValue()); + } + + @Test + public void between_different_types() { + assertThrows(ExpressionEvaluationException.class, () -> + dsl.between(DSL.literal(1), DSL.literal(1), DSL.literal("1"))); + } + + @Test + public void between_null_missing() { + FunctionExpression between = dsl.between( + DSL.literal(1), DSL.literal(0), DSL.ref(INT_TYPE_NULL_VALUE_FIELD, INTEGER)); + assertTrue(between.valueOf(valueEnv()).isNull()); + + between = dsl.between( + DSL.literal(1), DSL.literal(0), DSL.ref(INT_TYPE_MISSING_VALUE_FIELD, INTEGER)); + assertTrue(between.valueOf(valueEnv()).isMissing()); + + between = dsl.between(DSL.literal(1), DSL.ref(INT_TYPE_NULL_VALUE_FIELD, INTEGER), + DSL.ref(INT_TYPE_MISSING_VALUE_FIELD, INTEGER)); + assertTrue(between.valueOf(valueEnv()).isMissing()); + } } \ No newline at end of file diff --git a/docs/user/dql/expressions.rst b/docs/user/dql/expressions.rst index 861e233d1f..f58f160d67 100644 --- a/docs/user/dql/expressions.rst +++ b/docs/user/dql/expressions.rst @@ -130,6 +130,8 @@ Operators +----------------+----------------------------------------+ | REGEXP | String matches regular expression test | +----------------+----------------------------------------+ +| BETWEEN AND | In the range of two values | ++----------------+----------------------------------------+ Basic Comparison Operator @@ -185,6 +187,21 @@ expr REGEXP pattern. The expr is string value, pattern is supports regular expre | 1 | 0 | +------------------------+------------------+ + +BETWEEN AND +----------- + +expr BETWEEN min AND max. This operator is to judge if expr is in the range from min to max (min <= expr <= max), and returns 1 for true, 0 for false. expr NOT BETWEEN min AND max is the equivalent to NOT expr BETWEEN min AND max. The three expressions expr, min and max should be consistent in their types for value comparisons, or expression evaluation exception would be thrown. The supported types in this operator include number, string, and date and time related types. Implicit casting is not supported yet, so you would need to explicitly specifies the types of compared values. Here follow some examples:: + + od> SELECT 1 BETWEEN 0 AND 2 AS res1, '1' BETWEEN '2' AND '0' AS res2, date('2021-03-05') BETWEEN date('2021-03-05') AND date('2021-03-05') AS res3; + fetched rows / total rows = 1/1 + +--------+--------+--------+ + | res1 | res2 | res3 | + |--------+--------+--------| + | True | False | True | + +--------+--------+--------+ + + Function Call ============= diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/FilterQueryBuilder.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/FilterQueryBuilder.java index 4cc8be3512..b47f4fdda1 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/FilterQueryBuilder.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/FilterQueryBuilder.java @@ -62,6 +62,7 @@ public class FilterQueryBuilder extends ExpressionNodeVisitor= 2) && (func.getArguments().get(0) instanceof ReferenceExpression) && (func.getArguments().get(1) instanceof LiteralExpression); } @@ -53,8 +55,14 @@ public boolean canSupport(FunctionExpression func) { */ public QueryBuilder build(FunctionExpression func) { ReferenceExpression ref = (ReferenceExpression) func.getArguments().get(0); - LiteralExpression literal = (LiteralExpression) func.getArguments().get(1); - return doBuild(ref.getAttr(), ref.type(), literal.valueOf(null)); + if (func.getArguments().size() > 2) { + List literalList = func.getArguments().stream().skip(1) + .map(v -> v.valueOf(null)).collect(Collectors.toList()); + return doBuild(ref.getAttr(), ref.type(), literalList); + } else { + LiteralExpression literal = (LiteralExpression) func.getArguments().get(1); + return doBuild(ref.getAttr(), ref.type(), literal.valueOf(null)); + } } /** @@ -71,6 +79,11 @@ protected QueryBuilder doBuild(String fieldName, ExprType fieldType, ExprValue l "Subclass doesn't implement this and build method either"); } + protected QueryBuilder doBuild(String fieldName, ExprType fieldType, List literals) { + throw new UnsupportedOperationException( + "Subclass doesn't implement this and build method either"); + } + /** * Convert multi-field text field name to its inner keyword field. The limitation and assumption * is that the keyword field name is always "keyword" which is true by default. diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/RangeQuery.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/RangeQuery.java index 4d156311e3..b4848738da 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/RangeQuery.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/RangeQuery.java @@ -18,6 +18,7 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; +import java.util.List; import lombok.RequiredArgsConstructor; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -53,7 +54,24 @@ protected QueryBuilder doBuild(String fieldName, ExprType fieldType, ExprValue l case GTE: return query.gte(value); default: - throw new IllegalStateException("Comparison is supported by range query: " + comparison); + throw new IllegalStateException( + "Comparison is not supported by range query or improper number of arguments for " + + comparison); + } + } + + @Override + public QueryBuilder doBuild(String fieldName, ExprType fieldType, List literals) { + Object minValue = literals.get(0).value(); + Object maxValue = literals.get(1).value(); + RangeQueryBuilder query = QueryBuilders.rangeQuery(fieldName); + switch (comparison) { + case BETWEEN: + return query.gte(minValue).lte(maxValue); + default: + throw new IllegalStateException( + "Comparison is not supported by range query or improper number of arguments for " + + comparison); } } diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/FilterQueryBuilderTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/FilterQueryBuilderTest.java index 4de1956cdc..9fc818b47c 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/FilterQueryBuilderTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/FilterQueryBuilderTest.java @@ -33,6 +33,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; import com.google.common.collect.ImmutableMap; import java.util.Map; +import org.elasticsearch.index.query.QueryBuilder; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; @@ -99,6 +100,45 @@ void should_build_range_query_for_comparison_expression() { buildQuery(expr))); } + @Test + void should_build_range_query_for_between_operator() { + assertJsonEquals( + "{\n" + + " \"range\" : {\n" + + " \"age\" : {\n" + + " \"from\" : 20,\n" + + " \"to\" : 30,\n" + + " \"include_lower\" : true,\n" + + " \"include_upper\" : true,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + "}", + buildQuery(dsl.between(ref("age", INTEGER), literal(20), literal(30)))); + + assertJsonEquals( + "{\n" + + " \"bool\" : {\n" + + " \"must_not\" : [\n" + + " {\n" + + " \"range\" : {\n" + + " \"age\" : {\n" + + " \"from\" : 20,\n" + + " \"to\" : 30,\n" + + " \"include_lower\" : true,\n" + + " \"include_upper\" : true,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"adjust_pure_negative\" : true,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}", + buildQuery(dsl.not_between(ref("age", INTEGER), literal(20), literal(30)))); + } + @Test void should_build_wildcard_query_for_like_expression() { assertJsonEquals( @@ -271,7 +311,8 @@ private static void assertJsonEquals(String expected, String actual) { } private String buildQuery(Expression expr) { - return filterQueryBuilder.build(expr).toString(); + QueryBuilder builder = filterQueryBuilder.build(expr); + return builder.toString(); } private void mockToStringSerializer() { diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/LuceneQueryTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/LuceneQueryTest.java index f8d194f76a..84d77d53f8 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/LuceneQueryTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/LuceneQueryTest.java @@ -20,8 +20,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; +import java.util.List; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -38,7 +40,10 @@ void should_not_support_single_argument_by_default() { @Test void should_throw_exception_if_not_implemented() { assertThrows(UnsupportedOperationException.class, () -> - new LuceneQuery(){}.doBuild(null, null, null)); + new LuceneQuery(){}.doBuild(null, null, (ExprValue) null)); + + assertThrows(UnsupportedOperationException.class, () -> + new LuceneQuery(){}.doBuild(null, null, (List) null)); } } \ No newline at end of file diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/RangeQueryTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/RangeQueryTest.java index c1ed7e5393..9450719fa6 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/RangeQueryTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/lucene/RangeQueryTest.java @@ -16,11 +16,14 @@ package com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.filter.lucene; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; import static org.junit.jupiter.api.Assertions.assertThrows; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.filter.lucene.RangeQuery.Comparison; +import java.util.Arrays; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -29,11 +32,17 @@ class RangeQueryTest { @Test - void should_throw_exception_for_unsupported_comparison() { - // Note that since we do switch check on enum comparison, this should'be impossible + void should_throw_exception_for_unsupported_comparison_or_incorrect_argument_number() { assertThrows(IllegalStateException.class, () -> new RangeQuery(Comparison.BETWEEN) - .doBuild("name", STRING, ExprValueUtils.stringValue("John"))); + .doBuild("name", STRING, ExprValueUtils.stringValue("John")), + "Comparison is not supported by range query or improper number of arguments for BETWEEN"); + + assertThrows(IllegalStateException.class, () -> + new RangeQuery(Comparison.LT) + .doBuild("age", INTEGER, Arrays.asList(ExprValueUtils.integerValue(30), + ExprValueUtils.integerValue(20))), + "Comparison is not supported by range query or improper number of arguments for LT"); } } \ No newline at end of file diff --git a/sql/src/main/antlr/OpenDistroSQLParser.g4 b/sql/src/main/antlr/OpenDistroSQLParser.g4 index 4f01c657c9..a169983078 100644 --- a/sql/src/main/antlr/OpenDistroSQLParser.g4 +++ b/sql/src/main/antlr/OpenDistroSQLParser.g4 @@ -260,6 +260,7 @@ predicate | predicate IS nullNotnull #isNullPredicate | left=predicate NOT? LIKE right=predicate #likePredicate | left=predicate REGEXP right=predicate #regexpPredicate + | expr=predicate NOT? BETWEEN min=predicate AND max=predicate #betweenPredicate ; expressionAtom diff --git a/sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilder.java b/sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilder.java index 84e58d9535..b69ee169e9 100644 --- a/sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilder.java +++ b/sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilder.java @@ -18,11 +18,14 @@ import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.qualifiedName; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.stringLiteral; +import static com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName.BETWEEN; import static com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName.IS_NOT_NULL; import static com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName.IS_NULL; import static com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName.LIKE; +import static com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName.NOT; import static com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName.NOT_LIKE; import static com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName.REGEXP; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.BetweenPredicateContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.BinaryComparisonPredicateContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.BooleanContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.CaseFuncAlternativeContext; @@ -233,6 +236,14 @@ public UnresolvedExpression visitRegexpPredicate(RegexpPredicateContext ctx) { Arrays.asList(visit(ctx.left), visit(ctx.right))); } + @Override + public UnresolvedExpression visitBetweenPredicate(BetweenPredicateContext ctx) { + Function between = new Function(BETWEEN.getName().getFunctionName(), + Arrays.asList(visit(ctx.expr), visit(ctx.min), visit(ctx.max))); + return ctx.NOT() == null ? between : + new Function(NOT.getName().getFunctionName(), Collections.singletonList(between)); + } + @Override public UnresolvedExpression visitAndExpression(AndExpressionContext ctx) { return new And(visit(ctx.left), visit(ctx.right)); diff --git a/sql/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilderTest.java b/sql/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilderTest.java index 8ff5c50ce6..75a1ba494c 100644 --- a/sql/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilderTest.java +++ b/sql/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilderTest.java @@ -240,6 +240,22 @@ public void canBuildRegexpExpression() { ); } + @Test + public void canBuildBetweenPredicate() { + assertEquals( + function("between", intLiteral(1), intLiteral(0), intLiteral(2)), + buildExprAst("1 between 0 and 2") + ); + } + + @Test + public void canBuildNotBetweenPredicate() { + assertEquals( + function("not", function("between", intLiteral(1), intLiteral(0), intLiteral(2))), + buildExprAst("1 not between 0 and 2") + ); + } + @Test public void canBuildLogicalExpression() { assertEquals(