Skip to content

Commit

Permalink
SQL: Implement IIF(<cond>, <result1>, <result2>) (elastic#41420)
Browse files Browse the repository at this point in the history
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: elastic#40917
  • Loading branch information
matriv authored and Gurkan Kaymak committed May 27, 2019
1 parent faed669 commit a42de07
Show file tree
Hide file tree
Showing 16 changed files with 432 additions and 25 deletions.
46 changes: 46 additions & 0 deletions docs/reference/sql/functions/conditional.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions docs/reference/sql/functions/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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>>
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugin/sql/qa/src/main/resources/command.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ CASE |CONDITIONAL
COALESCE |CONDITIONAL
GREATEST |CONDITIONAL
IFNULL |CONDITIONAL
IIF |CONDITIONAL
ISNULL |CONDITIONAL
LEAST |CONDITIONAL
NULLIF |CONDITIONAL
Expand Down
139 changes: 139 additions & 0 deletions x-pack/plugin/sql/qa/src/main/resources/conditionals.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 24 additions & 0 deletions x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ CASE |CONDITIONAL
COALESCE |CONDITIONAL
GREATEST |CONDITIONAL
IFNULL |CONDITIONAL
IIF |CONDITIONAL
ISNULL |CONDITIONAL
LEAST |CONDITIONAL
NULLIF |CONDITIONAL
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,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.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;
Expand Down Expand Up @@ -172,6 +173,7 @@ private void defineDefaultFunctions() {
// 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"),
Expand Down Expand Up @@ -544,10 +546,10 @@ private interface FunctionBuilder {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,28 @@
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;

Expand Down Expand Up @@ -83,7 +83,7 @@ protected TypeResolution resolveType() {
}
}
if (expectedResultDataType == null) {
expectedResultDataType = defaultElse().dataType();
expectedResultDataType = elseResult().dataType();
}

for (IfConditional conditional : conditions) {
Expand All @@ -102,12 +102,12 @@ protected TypeResolution resolveType() {
}
}

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;
Expand All @@ -119,7 +119,7 @@ protected TypeResolution resolveType() {
*/
@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());
}

Expand All @@ -128,7 +128,7 @@ public Object fold() {
if (conditions.isEmpty() == false && conditions.get(0).condition().fold() == Boolean.TRUE) {
return conditions.get(0).result().fold();
}
return defaultElse.fold();
return elseResult.fold();
}

@Override
Expand All @@ -138,7 +138,7 @@ protected Pipe makePipe() {
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);
}

Expand All @@ -149,7 +149,7 @@ public ScriptTemplate asScript() {
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();
Expand Down
Loading

0 comments on commit a42de07

Please sign in to comment.