Skip to content

Commit

Permalink
SQL: Implement GREATEST and LEAST functions
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: elastic#35878
  • Loading branch information
matriv committed Nov 25, 2018
1 parent 05620cc commit aa402ce
Show file tree
Hide file tree
Showing 23 changed files with 677 additions and 193 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 @@ -1619,3 +1621,45 @@ SELECT NULLIF('elastic', 'elastic') AS "nullif";
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
;
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 @@ -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 @@ -140,7 +140,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
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,15 @@
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.Expressions;
import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder;
import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypeConversion;

import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;

import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder;
import static org.elasticsearch.xpack.sql.expression.predicate.conditional.ConditionalProcessor.ConditionalOperation.COALESCE;

public class Coalesce extends ConditionalFunction {

private DataType dataType = DataType.NULL;

public Coalesce(Location location, List<Expression> fields) {
super(location, fields);
}
Expand All @@ -40,19 +32,6 @@ public Expression replaceChildren(List<Expression> newChildren) {
return new Coalesce(location(), newChildren);
}

@Override
protected TypeResolution resolveType() {
for (Expression e : children()) {
dataType = DataTypeConversion.commonType(dataType, e.dataType());
}
return TypeResolution.TYPE_RESOLVED;
}

@Override
public DataType dataType() {
return dataType;
}

@Override
public boolean foldable() {
// if the first entry is foldable, so is coalesce
Expand All @@ -62,10 +41,6 @@ public boolean foldable() {
return (children.isEmpty() || (children.get(0).foldable() && children.get(0).fold() != null));
}

@Override
public boolean nullable() {
return false;
}

@Override
public Object fold() {
Expand All @@ -74,25 +49,12 @@ public Object fold() {
}

@Override
public ScriptTemplate asScript() {
List<ScriptTemplate> templates = new ArrayList<>();
for (Expression ex : children()) {
templates.add(asScript(ex));
}

StringJoiner template = new StringJoiner(",", "{sql}.coalesce([", "])");
ParamsBuilder params = paramsBuilder();

for (ScriptTemplate scriptTemplate : templates) {
template.add(scriptTemplate.template());
params.script(scriptTemplate.params());
}

return new ScriptTemplate(template.toString(), params.build(), dataType);
protected String scriptMethodName() {
return COALESCE.scriptMethodName();
}

@Override
protected Pipe makePipe() {
return new CoalescePipe(location(), this, Expressions.pipe(children()));
return new ConditionalPipe(location(), this, Expressions.pipe(children()), COALESCE);
}
}

0 comments on commit aa402ce

Please sign in to comment.