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 40afa7eb12..0d03ddc536 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 @@ -526,6 +526,10 @@ public FunctionExpression nullif(Expression... expressions) { return function(BuiltinFunctionName.NULLIF, expressions); } + public FunctionExpression iffunction(Expression... expressions) { + return function(BuiltinFunctionName.IF, expressions); + } + public static Expression cases(Expression defaultResult, WhenClause... whenClauses) { return new CaseClause(Arrays.asList(whenClauses), defaultResult); 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 3bf5a64602..6b29c68da1 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 @@ -139,6 +139,7 @@ public enum BuiltinFunctionName { IS_NULL(FunctionName.of("is null")), IS_NOT_NULL(FunctionName.of("is not null")), IFNULL(FunctionName.of("ifnull")), + IF(FunctionName.of("if")), NULLIF(FunctionName.of("nullif")), ISNULL(FunctionName.of("isnull")), diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/UnaryPredicateOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/UnaryPredicateOperator.java index 8378bf1ad4..01e9aa99ca 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/UnaryPredicateOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/UnaryPredicateOperator.java @@ -15,8 +15,6 @@ package com.amazon.opendistroforelasticsearch.sql.expression.operator.predicate; -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; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_TRUE; @@ -59,6 +57,7 @@ public static void register(BuiltinFunctionRepository repository) { repository.register(nullIf()); repository.register(isNull(BuiltinFunctionName.IS_NULL)); repository.register(isNull(BuiltinFunctionName.ISNULL)); + repository.register(ifFunction()); } private static FunctionResolver not() { @@ -100,21 +99,28 @@ private static FunctionResolver isNotNull() { Collectors.toList())); } - private static FunctionResolver ifNull() { - FunctionName functionName = BuiltinFunctionName.IFNULL.getName(); + private static FunctionResolver ifFunction() { + FunctionName functionName = BuiltinFunctionName.IF.getName(); List typeList = ExprCoreType.coreTypes(); List>> functionsOne = typeList.stream().map(v -> - impl((UnaryPredicateOperator::exprIfNull), v, v, v)) + impl((UnaryPredicateOperator::exprIf), v, BOOLEAN, v, v)) .collect(Collectors.toList()); + FunctionResolver functionResolver = FunctionDSL.define(functionName, functionsOne); + return functionResolver; + } + + private static FunctionResolver ifNull() { + FunctionName functionName = BuiltinFunctionName.IFNULL.getName(); + List typeList = ExprCoreType.coreTypes(); + List>> functionsTwo = typeList.stream().map(v -> - impl((UnaryPredicateOperator::exprIfNull), v, UNKNOWN, v)) + FunctionBuilder>>> functionsOne = typeList.stream().map(v -> + impl((UnaryPredicateOperator::exprIfNull), v, v, v)) .collect(Collectors.toList()); - functionsOne.addAll(functionsTwo); FunctionResolver functionResolver = FunctionDSL.define(functionName, functionsOne); return functionResolver; } @@ -149,4 +155,8 @@ public static ExprValue exprNullIf(ExprValue v1, ExprValue v2) { return v1.equals(v2) ? LITERAL_NULL : v1; } + public static ExprValue exprIf(ExprValue v1, ExprValue v2, ExprValue v3) { + return !v1.isNull() && !v1.isMissing() && LITERAL_TRUE.equals(v1) ? v2 : v3; + } + } diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/UnaryPredicateOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/UnaryPredicateOperatorTest.java index 6936e2ab50..70cde0d886 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/UnaryPredicateOperatorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/operator/predicate/UnaryPredicateOperatorTest.java @@ -80,9 +80,9 @@ private static Stream isNullArguments() { private static Stream ifNullArguments() { ArrayList exprValueArrayList = new ArrayList<>(); exprValueArrayList.add(DSL.literal(123)); - exprValueArrayList.add(DSL.literal("test")); + exprValueArrayList.add(DSL.literal(LITERAL_NULL)); exprValueArrayList.add(DSL.literal(321)); - exprValueArrayList.add(DSL.literal("")); + exprValueArrayList.add(DSL.literal(LITERAL_NULL)); return Lists.cartesianProduct(exprValueArrayList, exprValueArrayList).stream() .map(list -> { @@ -115,12 +115,30 @@ private static Stream nullIfArguments() { }); } + private static Stream ifArguments() { + ArrayList exprValueArrayList = new ArrayList<>(); + exprValueArrayList.add(DSL.literal(LITERAL_TRUE)); + exprValueArrayList.add(DSL.literal(LITERAL_FALSE)); + exprValueArrayList.add(DSL.literal(LITERAL_NULL)); + exprValueArrayList.add(DSL.literal(LITERAL_MISSING)); + + return Lists.cartesianProduct(exprValueArrayList, exprValueArrayList).stream() + .map(list -> { + Expression e1 = list.get(0); + if (e1.valueOf(valueEnv()).value() == LITERAL_TRUE.value()) { + return Arguments.of(e1, DSL.literal("123"), DSL.literal("321"), DSL.literal("123")); + } else { + return Arguments.of(e1, DSL.literal("123"), DSL.literal("321"), DSL.literal("321")); + } + }); + } + private static Stream exprIfNullArguments() { ArrayList exprValues = new ArrayList<>(); exprValues.add(LITERAL_NULL); exprValues.add(LITERAL_MISSING); exprValues.add(ExprValueUtils.integerValue(123)); - exprValues.add(ExprValueUtils.stringValue("test")); + exprValues.add(ExprValueUtils.integerValue(456)); return Lists.cartesianProduct(exprValues, exprValues).stream() .map(list -> { @@ -200,18 +218,24 @@ public void test_ifnull_predicate(Expression v1, Expression v2, Expression expec assertEquals(expected.valueOf(valueEnv()), dsl.ifnull(v1, v2).valueOf(valueEnv())); } - @ParameterizedTest - @MethodSource("exprIfNullArguments") - public void test_exprIfNull_predicate(ExprValue v1, ExprValue v2, ExprValue expected) { - assertEquals(expected.value(), UnaryPredicateOperator.exprIfNull(v1, v2).value()); - } - @ParameterizedTest @MethodSource("nullIfArguments") public void test_nullif_predicate(Expression v1, Expression v2, Expression expected) { assertEquals(expected.valueOf(valueEnv()), dsl.nullif(v1, v2).valueOf(valueEnv())); } + @ParameterizedTest + @MethodSource("ifArguments") + public void test_if_predicate(Expression v1, Expression v2, Expression v3, Expression expected) { + assertEquals(expected.valueOf(valueEnv()), dsl.iffunction(v1, v2, v3).valueOf(valueEnv())); + } + + @ParameterizedTest + @MethodSource("exprIfNullArguments") + public void test_exprIfNull_predicate(ExprValue v1, ExprValue v2, ExprValue expected) { + assertEquals(expected.value(), UnaryPredicateOperator.exprIfNull(v1, v2).value()); + } + @ParameterizedTest @MethodSource("exprNullIfArguments") public void test_exprNullIf_predicate(ExprValue v1, ExprValue v2, ExprValue expected) { diff --git a/docs/experiment/ppl/functions/condition.rst b/docs/experiment/ppl/functions/condition.rst index e287854dad..3ec153065e 100644 --- a/docs/experiment/ppl/functions/condition.rst +++ b/docs/experiment/ppl/functions/condition.rst @@ -145,3 +145,39 @@ Example:: | False | Quility | Nanette | | True | null | Dale | +----------+------------+-------------+ + +IF +------ + +Description +>>>>>>>>>>> + +Usage: if(condition, expr1, expr2) return expr1 if condition is true, otherwiser return expr2. + +Argument type: all the supported data type, (NOTE : if expr1 and expr2 are different type, you will fail semantic check + +Return type: any + +Example:: + + od> source=accounts | eval result = if(true, firstname, lastname) | fields result, firstname, lastname + fetched rows / total rows = 4/4 + +----------+-------------+------------+ + | result | firstname | lastname | + |----------+-------------+------------| + | Amber | Amber | Duke | + | Hattie | Hattie | Bond | + | Nanette | Nanette | Bates | + | Dale | Dale | Adams | + +----------+-------------+------------+ + + od> source=accounts | eval result = if(false, firstname, lastname) | fields result, firstname, lastname + fetched rows / total rows = 4/4 + +----------+-------------+------------+ + | result | firstname | lastname | + |----------+-------------+------------| + | Duke | Amber | Duke | + | Bond | Hattie | Bond | + | Bates | Nanette | Bates | + | Adams | Dale | Adams | + +----------+-------------+------------+ diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 3ab134a1ae..e07e354b97 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1982,6 +1982,40 @@ Example:: | True | False | +---------------+---------------+ +IF +------ + +Description +>>>>>>>>>>> + +Specifications: + +1. IF(condition, ES_TYPE1, ES_TYPE2) -> ES_TYPE1 or ES_TYPE2 + +Usage: if first parameter is true, return second parameter, otherwise return third one. + +Argument type: condition as BOOLEAN, second and third can by any type + +Return type: Any (NOTE : if parameters #2 and #3 has different type, you will fail semantic check" + +Example:: + + od> SELECT IF(100 > 200, '100', '200') + fetched rows / total rows = 1/1 + +-------------------------------+ + | IF(100 > 200, '100', '200') | + |-------------------------------| + | 200 | + +-------------------------------+ + + od> SELECT IF(200 > 100, '100', '200') + fetched rows / total rows = 1/1 + +-------------------------------+ + | IF(200 > 100, '100', '200') | + |-------------------------------| + | 100 | + +-------------------------------+ + CASE ---- diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/SQLFunctionsIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/SQLFunctionsIT.java index 705d75d13a..fc9088a24a 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/SQLFunctionsIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/SQLFunctionsIT.java @@ -714,12 +714,13 @@ public void right() throws IOException { @Test public void ifFuncShouldPassJDBC() { + Assume.assumeTrue(isNewQueryEngineEabled()); JSONObject response = executeJdbcRequest( "SELECT IF(age > 30, 'True', 'False') AS Ages FROM " + TEST_INDEX_ACCOUNT + " WHERE age IS NOT NULL GROUP BY Ages"); - assertEquals("Ages", response.query("/schema/0/name")); + assertEquals("IF(age > 30, \'True\', \'False\')", response.query("/schema/0/name")); assertEquals("Ages", response.query("/schema/0/alias")); - assertEquals("double", response.query("/schema/0/type")); + assertEquals("keyword", response.query("/schema/0/type")); } @Test diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/ConditionalIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/ConditionalIT.java index 9c0648990a..b9f1a348fc 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/ConditionalIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/ConditionalIT.java @@ -63,16 +63,18 @@ public void ifnullShouldPassJDBC() throws IOException { @Test public void ifnullWithNullInputTest() { Assume.assumeTrue(isNewQueryEngineEabled()); + JSONObject response = new JSONObject(executeQuery( - "SELECT IFNULL(1/0, firstname) as IFNULL1 ," - + " IFNULL(firstname, 1/0) as IFNULL2 ," - + " IFNULL(1/0, 1/0) as IFNULL3 " + "SELECT IFNULL(null, firstname) as IFNULL1 ," + + " IFNULL(firstname, null) as IFNULL2 ," + + " IFNULL(null, null) as IFNULL3 " + " FROM " + TEST_INDEX_BANK_WITH_NULL_VALUES + " WHERE balance is null limit 2", "jdbc")); + verifySchema(response, - schema("IFNULL(1/0, firstname)", "IFNULL1", "keyword"), - schema("IFNULL(firstname, 1/0)", "IFNULL2", "integer"), - schema("IFNULL(1/0, 1/0)", "IFNULL3", "integer")); + schema("IFNULL(null, firstname)", "IFNULL1", "keyword"), + schema("IFNULL(firstname, null)", "IFNULL2", "keyword"), + schema("IFNULL(null, null)", "IFNULL3", "byte")); verifyDataRows(response, rows("Hattie", "Hattie", LITERAL_NULL.value()), rows( "Elinor", "Elinor", LITERAL_NULL.value()) @@ -83,18 +85,19 @@ public void ifnullWithNullInputTest() { public void ifnullWithMissingInputTest() { Assume.assumeTrue(isNewQueryEngineEabled()); JSONObject response = new JSONObject(executeQuery( - "SELECT IFNULL(balance, firstname) as IFNULL1 ," - + " IFNULL(firstname, balance) as IFNULL2 ," + "SELECT IFNULL(balance, 100) as IFNULL1, " + + " IFNULL(200, balance) as IFNULL2, " + " IFNULL(balance, balance) as IFNULL3 " + " FROM " + TEST_INDEX_BANK_WITH_NULL_VALUES - + " WHERE balance is null limit 2", "jdbc")); + + " WHERE balance is null limit 3", "jdbc")); verifySchema(response, - schema("IFNULL(balance, firstname)", "IFNULL1", "keyword"), - schema("IFNULL(firstname, balance)", "IFNULL2", "long"), + schema("IFNULL(balance, 100)", "IFNULL1", "long"), + schema("IFNULL(200, balance)", "IFNULL2", "long"), schema("IFNULL(balance, balance)", "IFNULL3", "long")); verifyDataRows(response, - rows("Hattie", "Hattie", LITERAL_NULL.value()), - rows( "Elinor", "Elinor", LITERAL_NULL.value()) + rows(100, 200, null), + rows(100, 200, null), + rows(100, 200, null) ); } @@ -113,8 +116,8 @@ public void nullifWithNotNullInputTestOne(){ Assume.assumeTrue(isNewQueryEngineEabled()); JSONObject response = new JSONObject(executeQuery( "SELECT NULLIF(firstname, 'Amber JOHnny') as testnullif " - + "FROM " + TEST_INDEX_BANK_WITH_NULL_VALUES - + " limit 2 ", "jdbc")); + + "FROM " + TEST_INDEX_BANK_WITH_NULL_VALUES + + " limit 2 ", "jdbc")); verifySchema(response, schema("NULLIF(firstname, 'Amber JOHnny')", "testnullif", "keyword")); verifyDataRows(response, @@ -193,6 +196,39 @@ public void isnullWithMathExpr() throws IOException{ ); } + @Test + public void ifShouldPassJDBC() throws IOException { + Assume.assumeTrue(isNewQueryEngineEabled()); + JSONObject response = executeJdbcRequest( + "SELECT IF(2 > 0, \'hello\', \'world\') AS name FROM " + TEST_INDEX_ACCOUNT); + assertEquals("IF(2 > 0, \'hello\', \'world\')", response.query("/schema/0/name")); + assertEquals("name", response.query("/schema/0/alias")); + assertEquals("keyword", response.query("/schema/0/type")); + } + + @Test + public void ifWithTrueAndFalseCondition() throws IOException { + Assume.assumeTrue(isNewQueryEngineEabled()); + JSONObject response = new JSONObject(executeQuery( + "SELECT IF(2 < 0, firstname, lastname) as IF0, " + + " IF(2 > 0, firstname, lastname) as IF1, " + + " firstname as IF2, " + + " lastname as IF3 " + + " FROM " + TEST_INDEX_BANK_WITH_NULL_VALUES + + " limit 2 ", "jdbc" )); + verifySchema(response, + schema("IF(2 < 0, firstname, lastname)", "IF0", "keyword"), + schema("IF(2 > 0, firstname, lastname)", "IF1", "keyword"), + schema("firstname", "IF2", "text"), + schema("lastname", "IF3", "keyword") + ); + verifyDataRows(response, + rows("Duke Willmington", "Amber JOHnny", "Amber JOHnny", "Duke Willmington"), + rows("Bond", "Hattie", "Hattie", "Bond") + ); + + } + private SearchHits query(String query) throws IOException { final String rsp = executeQueryWithStringOutput(query); diff --git a/integ-test/src/test/resources/correctness/expressions/conditionals.txt b/integ-test/src/test/resources/correctness/expressions/conditionals.txt index 40d7cf4598..76c3562a01 100644 --- a/integ-test/src/test/resources/correctness/expressions/conditionals.txt +++ b/integ-test/src/test/resources/correctness/expressions/conditionals.txt @@ -13,6 +13,6 @@ CASE WHEN 'test' = 'hello world' THEN 'Hello' WHEN 1.0 = 2.0 THEN 'One' ELSE TRI CASE 1 WHEN 1 THEN 'One' ELSE NULL END AS cases CASE 1 WHEN 1 THEN 'One' WHEN 2 THEN 'Two' ELSE NULL END AS cases CASE 2 WHEN 1 THEN NULL WHEN 2 THEN 'Two' ELSE NULL END AS cases -IFNULL(1/0, AvgTicketPrice) from kibana_sample_data_flights -IFNULL(FlightTimeHour, AvgTicketPrice) from kibana_sample_data_flights -IFNULL(AvgTicketPrice, 1/0) from kibana_sample_data_flights \ No newline at end of file +IFNULL(null, AvgTicketPrice) from kibana_sample_data_flights +IFNULL(AvgTicketPrice, 100) from kibana_sample_data_flights +IFNULL(AvgTicketPrice, null) from kibana_sample_data_flights \ No newline at end of file diff --git a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 index 6db0cb86d9..9866085710 100644 --- a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 @@ -238,6 +238,8 @@ ISNOTNULL: 'ISNOTNULL'; // FLOWCONTROL FUNCTIONS IFNULL: 'IFNULL'; NULLIF: 'NULLIF'; +IF: 'IF'; + // LITERALS AND VALUES //STRING_LITERAL: DQUOTA_STRING | SQUOTA_STRING | BQUOTA_STRING; diff --git a/ppl/src/main/antlr/OpenDistroPPLParser.g4 b/ppl/src/main/antlr/OpenDistroPPLParser.g4 index 42d80a625a..dafc05c7e2 100644 --- a/ppl/src/main/antlr/OpenDistroPPLParser.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLParser.g4 @@ -254,7 +254,7 @@ dateAndTimeFunctionBase /** condition function return boolean value */ conditionFunctionBase : LIKE - | ISNULL | ISNOTNULL | IFNULL | NULLIF + | IF | ISNULL | ISNOTNULL | IFNULL | NULLIF ; textFunctionBase diff --git a/sql/src/main/antlr/OpenDistroSQLParser.g4 b/sql/src/main/antlr/OpenDistroSQLParser.g4 index 6964304bd2..c6d2b15085 100644 --- a/sql/src/main/antlr/OpenDistroSQLParser.g4 +++ b/sql/src/main/antlr/OpenDistroSQLParser.g4 @@ -358,7 +358,7 @@ textFunctionName ; flowControlFunctionName - : IFNULL | NULLIF | ISNULL + : IF | IFNULL | NULLIF | ISNULL ; functionArgs