Browse Source

SQL: Implement IIF(<cond>, <result1>, <result2>) (#41420)

Implement a more trivial case of the CASE expression which is
expressed as a traditional function with 2 or 3 arguments. e.g.:

IIF(a = 1, 'one', 'many')
IIF(a > 0, 'positive')
Closes: #40917
Marios Trivyzas 6 years ago
parent
commit
add02f4f55
16 changed files with 432 additions and 25 deletions
  1. 46 0
      docs/reference/sql/functions/conditional.asciidoc
  2. 1 0
      docs/reference/sql/functions/index.asciidoc
  3. 1 0
      x-pack/plugin/sql/qa/src/main/resources/command.csv-spec
  4. 139 0
      x-pack/plugin/sql/qa/src/main/resources/conditionals.csv-spec
  5. 24 0
      x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec
  6. 5 3
      x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java
  7. 14 14
      x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/Case.java
  8. 59 0
      x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/Iif.java
  9. 1 1
      x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java
  10. 10 0
      x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java
  11. 1 1
      x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/CaseTests.java
  12. 87 0
      x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IifTests.java
  13. 23 0
      x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java
  14. 4 4
      x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/ExpressionTests.java
  15. 14 0
      x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java
  16. 3 2
      x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/tree/NodeSubclassTests.java

+ 46 - 0
docs/reference/sql/functions/conditional.asciidoc

@@ -223,6 +223,52 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[ifNullReturnFirst]
 include-tagged::{sql-specs}/docs/docs.csv-spec[ifNullReturnSecond]
 ----
 
+[[sql-functions-conditional-iif]]
+==== `IFF`
+
+.Synopsis:
+[source, sql]
+----
+IIF(expression<1>, expression<2>, [expression<3>])
+----
+
+*Input*:
+
+<1> boolean condition to check
+
+<2> return value if the boolean condition evaluates to `true`
+
+<3> return value if the boolean condition evaluates `false`; optional
+
+*Output*: 2nd expression if 1st expression (condition) evaluates to `true`. If it evaluates to `false`
+return 3rd expression. If 3rd expression is not provided return `null`.
+
+.Description
+
+Conditional function that implements the standard _IF <condition> THEN <result1> ELSE <result2>_
+logic of programming languages. If the 3rd expression is not provided and the condition evaluates to `false`,
+`null` is returned.
+
+
+["source","sql",subs="attributes,callouts,macros"]
+----
+include-tagged::{sql-specs}/docs/docs.csv-spec[iifWithDefaultValue]
+----
+
+["source","sql",subs="attributes,callouts,macros"]
+----
+include-tagged::{sql-specs}/docs/docs.csv-spec[iifWithoutDefaultValue]
+----
+
+[TIP]
+=================
+*IIF* functions can be combined to implement more complex logic simulating the <<sql-functions-conditional-case>>
+expression. E.g.:
+
+[source, sql]
+IIF(a = 1, 'one', IIF(a = 2, 'two', IIF(a = 3, 'three', 'many')))
+=================
+
 
 [[sql-functions-conditional-isnull]]
 ==== `ISNULL`

+ 1 - 0
docs/reference/sql/functions/index.asciidoc

@@ -131,6 +131,7 @@
 ** <<sql-functions-conditional-coalesce>>
 ** <<sql-functions-conditional-greatest>>
 ** <<sql-functions-conditional-ifnull>>
+** <<sql-functions-conditional-iif>>
 ** <<sql-functions-conditional-isnull>>
 ** <<sql-functions-conditional-least>>
 ** <<sql-functions-conditional-nullif>>

+ 1 - 0
x-pack/plugin/sql/qa/src/main/resources/command.csv-spec

@@ -29,6 +29,7 @@ CASE             |CONDITIONAL
 COALESCE         |CONDITIONAL
 GREATEST         |CONDITIONAL
 IFNULL           |CONDITIONAL
+IIF              |CONDITIONAL
 ISNULL           |CONDITIONAL
 LEAST            |CONDITIONAL
 NULLIF           |CONDITIONAL

+ 139 - 0
x-pack/plugin/sql/qa/src/main/resources/conditionals.csv-spec

@@ -179,3 +179,142 @@ count     |    gender   |   languages
 11        | M           | 3
 11        | M           | 4
 ;
+
+
+iifField
+SELECT emp_no, IIF(emp_no - 10000 < 10, 'First 10', 'Second 10') as "iif_result" FROM test_emp WHERE emp_no >= 10005
+ORDER BY emp_no LIMIT 10;
+
+ emp_no | iif_result
+--------+-----------
+10005   | First 10
+10006   | First 10
+10007   | First 10
+10008   | First 10
+10009   | First 10
+10010   | Second 10
+10011   | Second 10
+10012   | Second 10
+10013   | Second 10
+10014   | Second 10
+;
+
+iifFieldWithoutAlias
+SELECT emp_no, IIF(emp_no - 10000 < 10, emp_no, emp_no % 10) FROM test_emp WHERE emp_no >= 10005
+ORDER BY emp_no LIMIT 10;
+
+ emp_no | IIF(emp_no - 10000 < 10, emp_no, emp_no % 10)
+--------+----------------------------------------------
+10005   | 10005
+10006   | 10006
+10007   | 10007
+10008   | 10008
+10009   | 10009
+10010   | 0
+10011   | 1
+10012   | 2
+10013   | 3
+10014   | 4
+;
+
+iifFieldNoElse
+SELECT emp_no, IIF(emp_no - 10000 < 10, 'First 10') as "iif_result" FROM test_emp WHERE emp_no >= 10005
+ORDER BY emp_no LIMIT 10;
+
+ emp_no | iif_result
+--------+----------
+10005   | First 10
+10006   | First 10
+10007   | First 10
+10008   | First 10
+10009   | First 10
+10010   | null
+10011   | null
+10012   | null
+10013   | null
+10014   | null
+;
+
+iifWhere
+SELECT last_name FROM test_emp WHERE IIF(LENGTH(last_name) < 7, 'ShortName') IS NOT NULL ORDER BY emp_no LIMIT 10;
+
+  last_name
+-----------
+Simmel
+Peac
+Sluis
+Terkki
+Genin
+Peha
+Erde
+Famili
+Pettey
+Heyers
+;
+
+iifOrderBy
+SELECT last_name FROM test_emp ORDER BY IIF(languages >= 3, 'first', 'second'), emp_no LIMIT 10;
+
+ last_name
+-----------
+Simmel
+Bamford
+Koblick
+Preusig
+Zielinski
+Piveteau
+Sluis
+Bridgland
+Genin
+Nooteboom
+;
+
+iifGroupBy
+schema::count:l|lang_skills:s
+SELECT count(*) AS count, IIF(NVL(languages, 0) <= 1 , 'zero-to-one', 'multilingual') as lang_skills FROM test_emp
+GROUP BY lang_skills ORDER BY 2;
+
+     count     |  lang_skills
+---------------+---------------
+75             |multilingual
+25             |zero-to-one
+;
+
+iifGroupByComplexNested
+schema::count:l|lang_skills:s
+SELECT count(*) AS count,
+IIF(NVL(languages, 0) = 0, 'zero',
+  IIF(languages = 1, 'one',
+    IIF(languages = 2, 'bilingual',
+      IIF(languages = 3, 'trilingual', 'multilingual')))) as lang_skills FROM test_emp GROUP BY lang_skills ORDER BY 2;
+
+     count     |  lang_skills
+---------------+---------------
+19             |bilingual
+39             |multilingual
+15             |one
+17             |trilingual
+10             |zero
+;
+
+iifGroupByAndHaving
+schema::count:l|gender:s|languages:byte
+SELECT count(*) AS count, gender, languages FROM test_emp
+GROUP BY 2, 3 HAVING IIF(count(*) > 10, 'many', 'a few') = 'many'
+ORDER BY 2, 3;
+
+     count     |    gender     |   languages
+---------------+---------------+---------------
+11             |M              |2
+11             |M              |3
+11             |M              |4
+;
+
+iifWithConvertAndGroupBy
+SELECT CONVERT(IIF(languages > 1, IIF(languages = 3, '3')), SQL_BIGINT) AS cond FROM test_emp GROUP BY cond ORDER BY cond DESC;
+
+ cond:l
+-------
+3
+null
+;

+ 24 - 0
x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec

@@ -206,6 +206,7 @@ CASE             |CONDITIONAL
 COALESCE         |CONDITIONAL
 GREATEST         |CONDITIONAL
 IFNULL           |CONDITIONAL
+IIF              |CONDITIONAL
 ISNULL           |CONDITIONAL
 LEAST            |CONDITIONAL
 NULLIF           |CONDITIONAL
@@ -2095,6 +2096,29 @@ elastic
 ;
 
 
+iifWithDefaultValue
+schema::result1:s|result2:s
+// tag::iifWithDefaultValue
+SELECT IIF(1 < 2, 'TRUE', 'FALSE') AS result1, IIF(1 > 2, 'TRUE', 'FALSE') AS result2;
+
+    result1    |    result2
+---------------+---------------
+TRUE           |FALSE
+// end::iifWithDefaultValue
+;
+
+iifWithoutDefaultValue
+schema::result1:s|result2:s
+// tag::iifWithoutDefaultValue
+SELECT IIF(1 < 2, 'TRUE') AS result1, IIF(1 > 2 , 'TRUE') AS result2;
+
+    result1    |    result2
+---------------+---------------
+TRUE           |null
+// end::iifWithoutDefaultValue
+;
+
+
 ifNullReturnSecond
 // tag::ifNullReturnSecond
 SELECT IFNULL(null, 'search') AS "ifnull";

+ 5 - 3
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java

@@ -97,6 +97,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.string.UCase;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.Case;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.Coalesce;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.Greatest;
+import org.elasticsearch.xpack.sql.expression.predicate.conditional.Iif;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfNull;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.Least;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.NullIf;
@@ -172,6 +173,7 @@ public class FunctionRegistry {
         // Conditional
         addToMap(def(Case.class, Case::new, "CASE"),
                 def(Coalesce.class, Coalesce::new, "COALESCE"),
+                def(Iif.class, Iif::new, "IIF"),
                 def(IfNull.class, IfNull::new, "IFNULL", "ISNULL", "NVL"),
                 def(NullIf.class, NullIf::new, "NULLIF"),
                 def(Greatest.class, Greatest::new, "GREATEST"),
@@ -544,10 +546,10 @@ public class FunctionRegistry {
     static <T extends Function> FunctionDefinition def(Class<T> function,
             ThreeParametersFunctionBuilder<T> ctorRef, String... names) {
         FunctionBuilder builder = (source, children, distinct, cfg) -> {
-            boolean isLocateFunction = function.isAssignableFrom(Locate.class);
-            if (isLocateFunction && (children.size() > 3 || children.size() < 2)) {
+            boolean hasMinimumTwo = function.isAssignableFrom(Locate.class) || function.isAssignableFrom(Iif.class);
+            if (hasMinimumTwo && (children.size() > 3 || children.size() < 2)) {
                 throw new SqlIllegalArgumentException("expects two or three arguments");
-            } else if (!isLocateFunction && children.size() != 3) {
+            } else if (!hasMinimumTwo && children.size() != 3) {
                 throw new SqlIllegalArgumentException("expects exactly three arguments");
             }
             if (distinct) {

+ 14 - 14
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/Case.java

@@ -30,28 +30,28 @@ import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.pa
 public class Case extends ConditionalFunction {
 
     private final List<IfConditional> conditions;
-    private final Expression defaultElse;
+    private final Expression elseResult;
 
     @SuppressWarnings("unchecked")
     public Case(Source source, List<Expression> expressions) {
         super(source, expressions);
         this.conditions = (List<IfConditional>) (List<?>) expressions.subList(0, expressions.size() - 1);
-        this.defaultElse = expressions.get(expressions.size() - 1);
+        this.elseResult = expressions.get(expressions.size() - 1);
     }
 
     public List<IfConditional> conditions() {
         return conditions;
     }
 
-    public Expression defaultElse() {
-        return defaultElse;
+    public Expression elseResult() {
+        return elseResult;
     }
 
     @Override
     public DataType dataType() {
         if (dataType == null) {
             if (conditions.isEmpty()) {
-                dataType = defaultElse().dataType();
+                dataType = elseResult().dataType();
             } else {
                 dataType = DataType.NULL;
 
@@ -83,7 +83,7 @@ public class Case extends ConditionalFunction {
             }
         }
         if (expectedResultDataType == null) {
-            expectedResultDataType = defaultElse().dataType();
+            expectedResultDataType = elseResult().dataType();
         }
 
         for (IfConditional conditional : conditions) {
@@ -102,12 +102,12 @@ public class Case extends ConditionalFunction {
             }
         }
 
-        if (DataTypes.areTypesCompatible(expectedResultDataType, defaultElse.dataType()) == false) {
+        if (DataTypes.areTypesCompatible(expectedResultDataType, elseResult.dataType()) == false) {
             return new TypeResolution(format(null, "ELSE clause of [{}] must be [{}], found value [{}] type [{}]",
-                defaultElse.sourceText(),
+                elseResult.sourceText(),
                 expectedResultDataType.typeName,
-                Expressions.name(defaultElse),
-                defaultElse.dataType().typeName));
+                Expressions.name(elseResult),
+                elseResult.dataType().typeName));
         }
 
         return TypeResolution.TYPE_RESOLVED;
@@ -119,7 +119,7 @@ public class Case extends ConditionalFunction {
      */
     @Override
     public boolean foldable() {
-        return (conditions.isEmpty() && defaultElse.foldable()) ||
+        return (conditions.isEmpty() && elseResult.foldable()) ||
             (conditions.size() == 1 && conditions.get(0).condition().foldable() && conditions.get(0).result().foldable());
     }
 
@@ -128,7 +128,7 @@ public class Case extends ConditionalFunction {
         if (conditions.isEmpty() == false && conditions.get(0).condition().fold() == Boolean.TRUE) {
             return conditions.get(0).result().fold();
         }
-        return defaultElse.fold();
+        return elseResult.fold();
     }
 
     @Override
@@ -138,7 +138,7 @@ public class Case extends ConditionalFunction {
             pipes.add(Expressions.pipe(ifConditional.condition()));
             pipes.add(Expressions.pipe(ifConditional.result()));
         }
-        pipes.add(Expressions.pipe(defaultElse));
+        pipes.add(Expressions.pipe(elseResult));
         return new CasePipe(source(), this, pipes);
     }
 
@@ -149,7 +149,7 @@ public class Case extends ConditionalFunction {
             templates.add(asScript(ifConditional.condition()));
             templates.add(asScript(ifConditional.result()));
         }
-        templates.add(asScript(defaultElse));
+        templates.add(asScript(elseResult));
 
         StringJoiner template = new StringJoiner(",", "{sql}.caseFunction([", "])");
         ParamsBuilder params = paramsBuilder();

+ 59 - 0
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/Iif.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.sql.expression.predicate.conditional;
+
+import org.elasticsearch.xpack.sql.expression.Expression;
+import org.elasticsearch.xpack.sql.expression.Expressions;
+import org.elasticsearch.xpack.sql.expression.Literal;
+import org.elasticsearch.xpack.sql.tree.NodeInfo;
+import org.elasticsearch.xpack.sql.tree.Source;
+import org.elasticsearch.xpack.sql.type.DataType;
+import org.elasticsearch.xpack.sql.type.DataTypes;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
+import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isBoolean;
+
+public class Iif extends Case {
+
+    public Iif(Source source, Expression condition, Expression thenResult, Expression elseResult) {
+        super(source, Arrays.asList(new IfConditional(source, condition, thenResult), elseResult != null ? elseResult : Literal.NULL));
+    }
+
+    private Iif(Source source, List<Expression> expressions) {
+        super(source, expressions);
+    }
+
+    @Override
+    protected NodeInfo<? extends Iif> info() {
+        return NodeInfo.create(this, Iif::new, conditions().get(0).condition(), conditions().get(0).result(), elseResult());
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new Iif(source(), newChildren);
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        TypeResolution conditionTypeResolution = isBoolean(conditions().get(0).condition(), sourceText(), Expressions.ParamOrdinal.FIRST);
+        if (conditionTypeResolution.unresolved()) {
+            return conditionTypeResolution;
+        }
+
+        DataType resultDataType = conditions().get(0).dataType();
+        if (DataTypes.areTypesCompatible(resultDataType, elseResult().dataType()) == false) {
+            return new TypeResolution(format(null, "third argument of [{}] must be [{}], found value [{}] type [{}]",
+                sourceText(),
+                resultDataType.typeName,
+                Expressions.name(elseResult()),
+                elseResult().dataType().typeName));
+        }
+        return TypeResolution.TYPE_RESOLVED;
+    }
+}

+ 1 - 1
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java

@@ -1257,7 +1257,7 @@ public class Optimizer extends RuleExecutor<LogicalPlan> {
                 }
 
                 if (newConditions.size() < c.children().size()) {
-                    return c.replaceChildren(combine(newConditions, c.defaultElse()));
+                    return c.replaceChildren(combine(newConditions, c.elseResult()));
                 }
             }
 

+ 10 - 0
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java

@@ -618,6 +618,16 @@ public class VerifierErrorMessagesTests extends ESTestCase {
             error("SELECT CASE WHEN int > 20 THEN null WHEN int > 10 THEN null WHEN int > 5 THEN 'foo' ELSE date END FROM test"));
     }
 
+    public void testIifWithNonBooleanConditionExpression() {
+        assertEquals("1:8: first argument of [IIF(int, 'one', 'zero')] must be [boolean], found value [int] type [integer]",
+            error("SELECT IIF(int, 'one', 'zero') FROM test"));
+    }
+
+    public void testIifWithDifferentResultAndDefaultValueDataTypes() {
+        assertEquals("1:8: third argument of [IIF(int > 20, 'foo', date)] must be [keyword], found value [date] type [datetime]",
+            error("SELECT IIF(int > 20, 'foo', date) FROM test"));
+    }
+
     public void testAggsInWhere() {
         assertEquals("1:33: Cannot use WHERE filtering on aggregate function [MAX(int)], use HAVING instead",
                 error("SELECT MAX(int) FROM test WHERE MAX(int) > 10 GROUP BY bool"));

+ 1 - 1
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/CaseTests.java

@@ -94,7 +94,7 @@ public class CaseTests extends AbstractNodeTestCase<Case, Expression> {
                 }
             }
         }
-        expressions.add(c.defaultElse());
+        expressions.add(c.elseResult());
         return expressions;
     }
 }

+ 87 - 0
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/predicate/conditional/IifTests.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.sql.expression.predicate.conditional;
+
+import org.elasticsearch.xpack.sql.expression.Expression;
+import org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils;
+import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.Equals;
+import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase;
+import org.elasticsearch.xpack.sql.tree.NodeSubclassTests;
+import org.elasticsearch.xpack.sql.tree.Source;
+import org.elasticsearch.xpack.sql.tree.SourceTests;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomIntLiteral;
+import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral;
+import static org.elasticsearch.xpack.sql.tree.SourceTests.randomSource;
+
+/**
+ * Needed to override tests in {@link NodeSubclassTests} as If is special since its children are not usual
+ * expressions but {@link IfConditional}s.
+ */
+public class IifTests extends AbstractNodeTestCase<Iif, Expression> {
+
+    public static Iif randomIif() {
+        return new Iif(randomSource(), new Equals(randomSource(), randomStringLiteral(), randomStringLiteral()),
+            randomIntLiteral(), randomIntLiteral());
+    }
+
+    @Override
+    protected Iif randomInstance() {
+        return randomIif();
+    }
+
+    @Override
+    protected Iif mutate(Iif instance) {
+        Iif iif = randomIif();
+        List<Expression> mutatedChildren = mutateChildren(iif);
+        return new Iif(iif.source(), mutatedChildren.get(0), mutatedChildren.get(1), mutatedChildren.get(2));
+    }
+
+    @Override
+    protected Iif copy(Iif instance) {
+        return new Iif(instance.source(), instance.conditions().get(0).condition(), instance.conditions().get(0).result(),
+            instance.elseResult());
+    }
+
+    @Override
+    public void testTransform() {
+        Iif iif = randomIif();
+
+        Source newSource = randomValueOtherThan(iif.source(), SourceTests::randomSource);
+        assertEquals(new Iif(iif.source(), iif.conditions().get(0).condition(), iif.conditions().get(0).result(), iif.elseResult()),
+            iif.transformPropertiesOnly(p -> Objects.equals(p, iif.source()) ? newSource: p, Object.class));
+
+        String newName = randomValueOtherThan(iif.name(), () -> randomAlphaOfLength(5));
+        assertEquals(new Iif(iif.source(), iif.conditions().get(0).condition(), iif.conditions().get(0).result(), iif.elseResult()),
+            iif.transformPropertiesOnly(p -> Objects.equals(p, iif.name()) ? newName : p, Object.class));
+    }
+
+    @Override
+    public void testReplaceChildren() {
+        Iif iif = randomIif();
+
+        List<Expression> newChildren = mutateChildren(iif);
+        assertEquals(new Iif(iif.source(), newChildren.get(0), newChildren.get(1), newChildren.get(2)),
+            iif.replaceChildren(Arrays.asList(new IfConditional(iif.source(), newChildren.get(0), newChildren.get(1)),
+                newChildren.get(2))));
+    }
+
+    private List<Expression> mutateChildren(Iif iif) {
+        List<Expression> expressions = new ArrayList<>(3);
+        Equals eq = (Equals) iif.conditions().get(0).condition();
+        expressions.add(new Equals(randomSource(),
+            randomValueOtherThan(eq.left(), FunctionTestUtils::randomStringLiteral),
+            randomValueOtherThan(eq.right(), FunctionTestUtils::randomStringLiteral)));
+        expressions.add(randomValueOtherThan(iif.conditions().get(0).result(), FunctionTestUtils::randomIntLiteral));
+        expressions.add(randomValueOtherThan(iif.elseResult(), FunctionTestUtils::randomIntLiteral));
+        return expressions;
+    }
+}

+ 23 - 0
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java

@@ -48,6 +48,7 @@ import org.elasticsearch.xpack.sql.expression.predicate.conditional.Case;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.Coalesce;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.ConditionalFunction;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.Greatest;
+import org.elasticsearch.xpack.sql.expression.predicate.conditional.Iif;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfConditional;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfNull;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.Least;
@@ -685,6 +686,28 @@ public class OptimizerTests extends ESTestCase {
         assertEquals("foo2", c.fold());
     }
 
+    public void testSimplifyIif_ConditionTrue() {
+        SimplifyCase rule = new SimplifyCase();
+        Iif iif = new Iif(EMPTY, new Equals(EMPTY, ONE, ONE), Literal.of(EMPTY, "foo"), Literal.of(EMPTY, "bar"));
+        Expression e = rule.rule(iif);
+        assertEquals(Iif.class, e.getClass());
+        iif = (Iif) e;
+        assertEquals(1, iif.conditions().size());
+        assertTrue(iif.foldable());
+        assertEquals("foo", iif.fold());
+    }
+
+    public void testSimplifyIif_ConditionFalse() {
+        SimplifyCase rule = new SimplifyCase();
+        Iif iif = new Iif(EMPTY, new Equals(EMPTY, ONE, TWO), Literal.of(EMPTY, "foo"), Literal.of(EMPTY, "bar"));
+        Expression e = rule.rule(iif);
+        assertEquals(Iif.class, e.getClass());
+        iif = (Iif) e;
+        assertEquals(0, iif.conditions().size());
+        assertTrue(iif.foldable());
+        assertEquals("bar", iif.fold());
+    }
+
     //
     // Logical simplifications
     //

+ 4 - 4
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/ExpressionTests.java

@@ -465,7 +465,7 @@ public class ExpressionTests extends ESTestCase {
         assertEquals("WHEN a = 1 THEN 'one'", ifc.sourceText());
         assertThat(ifc.condition().toString(), startsWith("Equals[?a,1]#"));
         assertEquals("'one'=one", ifc.result().toString());
-        assertEquals(Literal.NULL, c.defaultElse());
+        assertEquals(Literal.NULL, c.elseResult());
 
         expr = parser.createExpression(
             "CASE WHEN a = 1 THEN 'one'" +
@@ -478,7 +478,7 @@ public class ExpressionTests extends ESTestCase {
         assertEquals(2, c.conditions().size());
         ifc = c.conditions().get(0);
         assertEquals("WHEN a = 1 THEN 'one'", ifc.sourceText());
-        assertEquals("'many'=many", c.defaultElse().toString());
+        assertEquals("'many'=many", c.elseResult().toString());
     }
 
     public void testCaseWithOperand() {
@@ -495,7 +495,7 @@ public class ExpressionTests extends ESTestCase {
         assertEquals("WHEN 1 THEN 'one'", ifc.sourceText());
         assertThat(ifc.condition().toString(), startsWith("Equals[?a,1]#"));
         assertEquals("'one'=one", ifc.result().toString());
-        assertEquals(Literal.NULL, c.defaultElse());
+        assertEquals(Literal.NULL, c.elseResult());
 
         expr = parser.createExpression(
             "CASE a WHEN 1 THEN 'one'" +
@@ -507,6 +507,6 @@ public class ExpressionTests extends ESTestCase {
         assertEquals(2, c.conditions().size());
         ifc = c.conditions().get(0);
         assertEquals("WHEN 1 THEN 'one'", ifc.sourceText());
-        assertEquals("'many'=many", c.defaultElse().toString());
+        assertEquals("'many'=many", c.elseResult().toString());
     }
 }

+ 14 - 0
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java

@@ -602,6 +602,20 @@ public class QueryTranslatorTests extends ESTestCase {
         assertEquals("[{v=int}, {v=10}, {v=foo}, {v=int}, {v=20}, {v=bar}, {v=default}]", scriptTemplate.params().toString());
     }
 
+    public void testTranslateIif_GroupBy_Painless() {
+        LogicalPlan p = plan("SELECT IIF(int > 20, 'foo', 'bar') FROM test GROUP BY 1");
+        assertTrue(p instanceof Aggregate);
+        Expression condition = ((Aggregate) p).groupings().get(0);
+        assertFalse(condition.foldable());
+        QueryTranslator.GroupingContext groupingContext = QueryTranslator.groupBy(((Aggregate) p).groupings());
+        assertNotNull(groupingContext);
+        ScriptTemplate scriptTemplate = groupingContext.tail.script();
+        assertEquals("InternalSqlScriptUtils.caseFunction([InternalSqlScriptUtils.gt("  +
+                "InternalSqlScriptUtils.docValue(doc,params.v0),params.v1),params.v2,params.v3])",
+            scriptTemplate.toString());
+        assertEquals("[{v=int}, {v=20}, {v=foo}, {v=bar}]", scriptTemplate.params().toString());
+    }
+
     public void testGroupByDateHistogram() {
         LogicalPlan p = plan("SELECT MAX(int) FROM test GROUP BY HISTOGRAM(int, 1000)");
         assertTrue(p instanceof Aggregate);

+ 3 - 2
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/tree/NodeSubclassTests.java

@@ -28,6 +28,7 @@ import org.elasticsearch.xpack.sql.expression.gen.pipeline.BinaryPipesTests;
 import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe;
 import org.elasticsearch.xpack.sql.expression.gen.processor.ConstantProcessor;
 import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
+import org.elasticsearch.xpack.sql.expression.predicate.conditional.Iif;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfConditional;
 import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfNull;
 import org.elasticsearch.xpack.sql.expression.predicate.fulltext.FullTextPredicate;
@@ -92,8 +93,8 @@ import static org.mockito.Mockito.mock;
  */
 public class NodeSubclassTests<T extends B, B extends Node<B>> extends ESTestCase {
 
-    private static final List<Class<?>> CLASSES_WITH_MIN_TWO_CHILDREN = Arrays.asList(IfConditional.class, IfNull.class,
-        In.class, InPipe.class, Percentile.class, Percentiles.class, PercentileRanks.class);
+    private static final List<Class<?>> CLASSES_WITH_MIN_TWO_CHILDREN = Arrays.asList(Iif.class, IfConditional.class,
+        IfNull.class, In.class, InPipe.class, Percentile.class, Percentiles.class, PercentileRanks.class);
 
     private final Class<T> subclass;