Skip to content

Commit

Permalink
SQL: Implement GREATEST and LEAST functions (#35879)
Browse files Browse the repository at this point in the history
Add GREATEST(expr1, expr2, ... exprN) and LEAST(expr1, expr2, exprN)
functions which are in the family of CONDITIONAL functions.

Implementation follows PostgreSQL behaviour, so the functions return
`NULL` when all of their arguments evaluate to `NULL`.

Renamed `CoalescePipe` and `CoalesceProcessor` to `ConditionalPipe` and
`ConditionalProcessor` respectively, to be able to reuse them for
`Greatest` and `Least` evaluations. To achieve that `ConditionalOperation`
has been added to differentiate between the functionalities at execution
time.

Closes: #35878
  • Loading branch information
matriv authored Nov 26, 2018
1 parent b95a4db commit 3f7cae3
Show file tree
Hide file tree
Showing 26 changed files with 681 additions and 222 deletions.
84 changes: 84 additions & 0 deletions docs/reference/sql/functions/conditional.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,87 @@ include-tagged::{sql-specs}/docs.csv-spec[nullIfReturnFirst]
----
include-tagged::{sql-specs}/docs.csv-spec[nullIfReturnNull]
----


[[sql-functions-conditional-greatest]]
==== `GREATEST`

.Synopsis
[source, sql]
----
GREATEST ( expression<1>, expression<2>, ... )
----

*Input*:

<1> 1st expression

<2> 2nd expression

...

**N**th expression

GREATEST can take an arbitrary number of arguments and
all of them must be of the same data type.

*Output*: one of the expressions or `null`

.Description

Returns the argument that has the largest value which is not null.
If all arguments are null, then it returns `null`.



["source","sql",subs="attributes,callouts,macros"]
----
include-tagged::{sql-specs}/docs.csv-spec[greatestReturnNonNull]
----

["source","sql",subs="attributes,callouts,macros"]
----
include-tagged::{sql-specs}/docs.csv-spec[greatestReturnNull]
----


[[sql-functions-conditional-least]]
==== `LEAST`

.Synopsis
[source, sql]
----
LEAST ( expression<1>, expression<2>, ... )
----

*Input*:

<1> 1st expression

<2> 2nd expression

...

**N**th expression

LEAST can take an arbitrary number of arguments and
all of them must be of the same data type.

*Output*: one of the expressions or `null`

.Description

Returns the argument that has the smallest value which is not null.
If all arguments are null, then it returns `null`.



["source","sql",subs="attributes,callouts,macros"]
----
include-tagged::{sql-specs}/docs.csv-spec[leastReturnNonNull]
----

["source","sql",subs="attributes,callouts,macros"]
----
include-tagged::{sql-specs}/docs.csv-spec[leastReturnNull]
----
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@
import static org.hamcrest.Matchers.containsString;

public abstract class ShowTestCase extends CliIntegrationTestCase {

private static final String HEADER_SEPARATOR = "----------";

public void testShowTables() throws IOException {
index("test1", body -> body.field("test_field", "test_value"));
index("test2", body -> body.field("test_field", "test_value"));
assertThat(command("SHOW TABLES"), RegexMatcher.matches("\\s*name\\s*"));
assertThat(readLine(), containsString("----------"));
assertThat(readLine(), containsString(HEADER_SEPARATOR));
assertThat(readLine(), RegexMatcher.matches("\\s*test[12]\\s*"));
assertThat(readLine(), RegexMatcher.matches("\\s*test[12]\\s*"));
assertEquals("", readLine());
}

public void testShowFunctions() throws IOException {
assertThat(command("SHOW FUNCTIONS"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*"));
assertThat(readLine(), containsString("----------"));
assertThat(readLine(), containsString(HEADER_SEPARATOR));
assertThat(readLine(), RegexMatcher.matches("\\s*AVG\\s*\\|\\s*AGGREGATE\\s*"));
assertThat(readLine(), RegexMatcher.matches("\\s*COUNT\\s*\\|\\s*AGGREGATE\\s*"));
assertThat(readLine(), RegexMatcher.matches("\\s*MAX\\s*\\|\\s*AGGREGATE\\s*"));
Expand All @@ -50,7 +53,8 @@ public void testShowFunctions() throws IOException {

public void testShowFunctionsLikePrefix() throws IOException {
assertThat(command("SHOW FUNCTIONS LIKE 'L%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*"));
assertThat(readLine(), containsString("----------"));
assertThat(readLine(), containsString(HEADER_SEPARATOR));
assertThat(readLine(), RegexMatcher.matches("\\s*LEAST\\s*\\|\\s*CONDITIONAL\\s*"));
assertThat(readLine(), RegexMatcher.matches("\\s*LOG\\s*\\|\\s*SCALAR\\s*"));
assertThat(readLine(), RegexMatcher.matches("\\s*LOG10\\s*\\|\\s*SCALAR\\s*"));
assertThat(readLine(), RegexMatcher.matches("\\s*LCASE\\s*\\|\\s*SCALAR\\s*"));
Expand All @@ -63,7 +67,7 @@ public void testShowFunctionsLikePrefix() throws IOException {

public void testShowFunctionsLikeInfix() throws IOException {
assertThat(command("SHOW FUNCTIONS LIKE '%DAY%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*"));
assertThat(readLine(), containsString("----------"));
assertThat(readLine(), containsString(HEADER_SEPARATOR));
assertThat(readLine(), RegexMatcher.matches("\\s*DAY\\s*\\|\\s*SCALAR\\s*"));
assertThat(readLine(), RegexMatcher.matches("\\s*DAYNAME\\s*\\|\\s*SCALAR\\s*"));
assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFMONTH\\s*\\|\\s*SCALAR\\s*"));
Expand Down
4 changes: 3 additions & 1 deletion x-pack/plugin/sql/qa/src/main/resources/command.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ STDDEV_POP |AGGREGATE
SUM_OF_SQUARES |AGGREGATE
VAR_POP |AGGREGATE
COALESCE |CONDITIONAL
GREATEST |CONDITIONAL
IFNULL |CONDITIONAL
ISNULL |CONDITIONAL
NVL |CONDITIONAL
LEAST |CONDITIONAL
NULLIF |CONDITIONAL
NVL |CONDITIONAL
DAY |SCALAR
DAYNAME |SCALAR
DAYOFMONTH |SCALAR
Expand Down
46 changes: 45 additions & 1 deletion x-pack/plugin/sql/qa/src/main/resources/docs.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,12 @@ STDDEV_POP |AGGREGATE
SUM_OF_SQUARES |AGGREGATE
VAR_POP |AGGREGATE
COALESCE |CONDITIONAL
GREATEST |CONDITIONAL
IFNULL |CONDITIONAL
ISNULL |CONDITIONAL
NVL |CONDITIONAL
LEAST |CONDITIONAL
NULLIF |CONDITIONAL
NVL |CONDITIONAL
DAY |SCALAR
DAYNAME |SCALAR
DAYOFMONTH |SCALAR
Expand Down Expand Up @@ -1620,6 +1622,48 @@ null
// end::nullIfReturnNull
;

greatestReturnNonNull
// tag::greatestReturnNonNull
SELECT GREATEST(null, 1, 2) AS "greatest";

greatest
---------------
2
// end::greatestReturnNonNull
;


greatestReturnNull
// tag::greatestReturnNull
SELECT GREATEST(null, null, null, null) AS "greatest";

greatest
---------------
null
// end::greatestReturnNull
;

leastReturnNonNull
// tag::leastReturnNonNull
SELECT LEAST(null, 2, 1) AS "least";

least
---------------
1
// end::leastReturnNonNull
;


leastReturnNull
// tag::leastReturnNull
SELECT LEAST(null, null, null, null) AS "least";

least
---------------
null
// end::leastReturnNull
;

nullEqualsCompareWithNull
// tag::nullEqualsCompareWithNull
SELECT 'elastic' <=> null AS "equals";
Expand Down
22 changes: 20 additions & 2 deletions x-pack/plugin/sql/qa/src/main/resources/null.sql-spec
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,28 @@ ifNullField
SELECT IFNULL(null, ABS(emp_no) + 1) AS "ifnull" FROM test_emp ORDER BY emp_no LIMIT 5;

nullIfField
SELECT NULLIF(emp_no - 2 + 3, ABS(emp_no) + 1) AS "nullif1", NULLIF(emp_no + 1, emp_no - 1) as "nullif2" FROM test_emp ORDER BY emp_no LIMIT 5;
SELECT NULLIF(emp_no - 2 + 3, ABS(emp_no) + 1) AS "nullif1", NULLIF(emp_no + 1, emp_no - 1) AS "nullif2" FROM test_emp ORDER BY emp_no LIMIT 5;

nullIfWhere
SELECT NULLIF(10002, ABS(emp_no) + 1) AS c, emp_no FROM test_emp WHERE NULLIF(10003, ABS(emp_no) + 1) IS NOT NULL ORDER BY emp_no NULLS FIRST LIMIT 5;
SELECT NULLIF(10002, ABS(emp_no) + 1) AS c, emp_no FROM test_emp WHERE NULLIF(10003, ABS(emp_no) + 1) IS NOT NULL ORDER BY emp_no LIMIT 5;

nullIfHaving
SELECT NULLIF(10030, ABS(MAX(emp_no)) + 1) AS nif FROM test_emp GROUP BY languages HAVING nif IS NOT NULL ORDER BY languages;

greatestField
SELECT GREATEST(emp_no - 1 + 3, ABS(emp_no) + 1) AS "greatest" FROM test_emp ORDER BY emp_no LIMIT 5;

greatestWhere
SELECT emp_no FROM test_emp WHERE GREATEST(10005, ABS(emp_no) + 1, null, emp_no - 1 + 3) > 10008 ORDER BY emp_no LIMIT 5;

greatestHaving
SELECT GREATEST(10096, ABS(MAX(emp_no)) + 1) AS gt FROM test_emp GROUP BY languages HAVING gt >= 10098 ORDER BY languages;

leastField
SELECT LEAST(emp_no - 1 + 3, ABS(emp_no) + 1) AS "least" FROM test_emp ORDER BY emp_no LIMIT 5;

leastWhere
SELECT emp_no FROM test_emp WHERE LEAST(10005, ABS(emp_no) + 1, null, emp_no - 1 + 3) > 10004 ORDER BY emp_no LIMIT 5;

leastHaving
SELECT LEAST(10098, ABS(MAX(emp_no)) + 1) AS lt FROM test_emp GROUP BY languages HAVING lt >= 10095 ORDER BY languages;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
import org.elasticsearch.xpack.sql.type.DataTypeConversion;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public abstract class Foldables {

Expand Down Expand Up @@ -46,11 +49,18 @@ public static double doubleValueOf(Expression e) {
}

public static <T> List<T> valuesOf(List<Expression> list, DataType to) {
List<T> l = new ArrayList<>(list.size());
for (Expression e : list) {
l.add(valueOf(e, to));
return foldTo(list, to, new ArrayList<>(list.size()));
}

public static <T> Set<T> valuesOfNoDuplicates(List<Expression> list, DataType to) {
return foldTo(list, to, new LinkedHashSet<>(list.size()));
}

private static <T, C extends Collection<T>> C foldTo(Collection<Expression> expressions, DataType to, C values) {
for (Expression e : expressions) {
values.add(valueOf(e, to));
}
return l;
return values;
}

public static List<Double> doubleValuesOf(List<Expression> list) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Substring;
import org.elasticsearch.xpack.sql.expression.function.scalar.string.UCase;
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.IfNull;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.Least;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.NullIf;
import org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic.Mod;
import org.elasticsearch.xpack.sql.parser.ParsingException;
Expand Down Expand Up @@ -157,9 +159,11 @@ private void defineDefaultFunctions() {
def(Kurtosis.class, Kurtosis::new));
// Scalar functions
// conditional
addToMap(def(Coalesce.class, Coalesce::new));
addToMap(def(IfNull.class, IfNull::new, "ISNULL", "NVL"));
addToMap(def(NullIf.class, NullIf::new));
addToMap(def(Coalesce.class, Coalesce::new),
def(IfNull.class, IfNull::new, "ISNULL", "NVL"),
def(NullIf.class, NullIf::new),
def(Greatest.class, Greatest::new),
def(Least.class, Least::new));
// Date
addToMap(def(DayName.class, DayName::new, "DAYNAME"),
def(DayOfMonth.class, DayOfMonth::new, "DAYOFMONTH", "DAY", "DOM"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.elasticsearch.xpack.sql.expression.gen.processor.ConstantProcessor;
import org.elasticsearch.xpack.sql.expression.gen.processor.HitExtractorProcessor;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.CoalesceProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.ConditionalProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.NullIfProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.logical.BinaryLogicProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.logical.NotProcessor;
Expand Down Expand Up @@ -61,7 +61,7 @@ public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
entries.add(new Entry(Processor.class, NotProcessor.NAME, NotProcessor::new));
// null
entries.add(new Entry(Processor.class, CheckNullProcessor.NAME, CheckNullProcessor::new));
entries.add(new Entry(Processor.class, CoalesceProcessor.NAME, CoalesceProcessor::new));
entries.add(new Entry(Processor.class, ConditionalProcessor.NAME, ConditionalProcessor::new));
entries.add(new Entry(Processor.class, NullIfProcessor.NAME, NullIfProcessor::new));

// arithmetic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFunctionProcessor;
import org.elasticsearch.xpack.sql.expression.literal.IntervalDayTime;
import org.elasticsearch.xpack.sql.expression.literal.IntervalYearMonth;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.CoalesceProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.ConditionalProcessor.ConditionalOperation;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.NullIfProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.logical.BinaryLogicProcessor.BinaryLogicOperation;
import org.elasticsearch.xpack.sql.expression.predicate.logical.NotProcessor;
Expand Down Expand Up @@ -144,7 +144,15 @@ public static Boolean in(Object value, List<Object> values) {
// Null
//
public static Object coalesce(List<Object> expressions) {
return CoalesceProcessor.apply(expressions);
return ConditionalOperation.COALESCE.apply(expressions);
}

public static Object greatest(List<Object> expressions) {
return ConditionalOperation.GREATEST.apply(expressions);
}

public static Object least(List<Object> expressions) {
return ConditionalOperation.LEAST.apply(expressions);
}

public static Object nullif(Object left, Object right) {
Expand Down
Loading

0 comments on commit 3f7cae3

Please sign in to comment.