SQL: Implement GREATEST and LEAST functions ()

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: 
This commit is contained in:
Marios Trivyzas 2018-11-26 18:21:36 +01:00 committed by GitHub
parent b95a4db6e6
commit 3f7cae3f0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 681 additions and 222 deletions
docs/reference/sql/functions
x-pack/plugin/sql

@ -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]
----

@ -13,11 +13,14 @@ import java.util.regex.Pattern;
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());
@ -25,7 +28,7 @@ public abstract class ShowTestCase extends CliIntegrationTestCase {
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*"));
@ -50,7 +53,8 @@ public abstract class ShowTestCase extends CliIntegrationTestCase {
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*"));
@ -63,7 +67,7 @@ public abstract class ShowTestCase extends CliIntegrationTestCase {
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*"));

@ -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

@ -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
@ -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";

@ -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;

@ -10,7 +10,10 @@ import org.elasticsearch.xpack.sql.type.DataType;
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 {
@ -46,11 +49,18 @@ public abstract class Foldables {
}
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()));
}
return l;
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 values;
}
public static List<Double> doubleValuesOf(List<Expression> list) {

@ -83,7 +83,9 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.string.Space;
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;
@ -157,9 +159,11 @@ public class FunctionRegistry {
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"),

@ -25,7 +25,7 @@ import org.elasticsearch.xpack.sql.expression.gen.processor.ChainingProcessor;
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;
@ -61,7 +61,7 @@ public final class Processors {
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

@ -23,7 +23,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProce
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;
@ -144,7 +144,15 @@ public final class InternalSqlScriptUtils {
// 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) {

@ -0,0 +1,66 @@
/*
* 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.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.expression.predicate.conditional.ConditionalProcessor.ConditionalOperation;
import org.elasticsearch.xpack.sql.tree.Location;
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;
/**
* Base class for conditional predicates with arbitrary number of arguments
*/
public abstract class ArbitraryConditionalFunction extends ConditionalFunction {
private final ConditionalOperation operation;
ArbitraryConditionalFunction(Location location, List<Expression> fields, ConditionalOperation operation) {
super(location, fields);
this.operation = operation;
}
@Override
protected TypeResolution resolveType() {
for (Expression e : children()) {
dataType = DataTypeConversion.commonType(dataType, e.dataType());
}
return TypeResolution.TYPE_RESOLVED;
}
@Override
protected Pipe makePipe() {
return new ConditionalPipe(location(), this, Expressions.pipe(children()), operation);
}
@Override
public ScriptTemplate asScript() {
List<ScriptTemplate> templates = new ArrayList<>();
for (Expression ex : children()) {
templates.add(asScript(ex));
}
StringJoiner template = new StringJoiner(",", "{sql}." + operation.scriptMethodName() +"([", "])");
ParamsBuilder params = paramsBuilder();
for (ScriptTemplate scriptTemplate : templates) {
template.add(scriptTemplate.template());
params.script(scriptTemplate.params());
}
return new ScriptTemplate(template.toString(), params.build(), dataType());
}
}

@ -7,27 +7,17 @@
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.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 class Coalesce extends ArbitraryConditionalFunction {
public Coalesce(Location location, List<Expression> fields) {
super(location, fields);
super(location, fields, COALESCE);
}
@Override
@ -40,19 +30,6 @@ public class Coalesce extends ConditionalFunction {
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
@ -62,37 +39,9 @@ public class Coalesce extends ConditionalFunction {
return (children.isEmpty() || (children.get(0).foldable() && children.get(0).fold() != null));
}
@Override
public boolean nullable() {
return false;
}
@Override
public Object fold() {
List<Expression> children = children();
return children.isEmpty() ? null : children.get(0).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);
}
@Override
protected Pipe makePipe() {
return new CoalescePipe(location(), this, Expressions.pipe(children()));
}
}

@ -1,38 +0,0 @@
/*
* 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.gen.pipeline.MultiPipe;
import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import java.util.List;
public class CoalescePipe extends MultiPipe {
public CoalescePipe(Location location, Expression expression, List<Pipe> children) {
super(location, expression, children);
}
@Override
protected NodeInfo<CoalescePipe> info() {
return NodeInfo.create(this, CoalescePipe::new, expression(), children());
}
@Override
public Pipe replaceChildren(List<Pipe> newChildren) {
return new CoalescePipe(location(), expression(), newChildren);
}
@Override
public Processor asProcessor(List<Processor> procs) {
return new CoalesceProcessor(procs);
}
}

@ -1,81 +0,0 @@
/*
* 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.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
public class CoalesceProcessor implements Processor {
public static final String NAME = "nco";
private final List<Processor> processsors;
public CoalesceProcessor(List<Processor> processors) {
this.processsors = processors;
}
public CoalesceProcessor(StreamInput in) throws IOException {
processsors = in.readNamedWriteableList(Processor.class);
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeNamedWriteableList(processsors);
}
@Override
public Object process(Object input) {
for (Processor proc : processsors) {
Object result = proc.process(input);
if (result != null) {
return result;
}
}
return null;
}
public static Object apply(List<?> values) {
if (values == null || values.isEmpty()) {
return null;
}
for (Object object : values) {
if (object != null) {
return object;
}
}
return null;
}
@Override
public int hashCode() {
return Objects.hash(processsors);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CoalesceProcessor that = (CoalesceProcessor) o;
return Objects.equals(processsors, that.processsors);
}
}

@ -7,8 +7,10 @@
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.function.scalar.ScalarFunction;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.type.DataType;
import java.util.List;
@ -17,7 +19,24 @@ import java.util.List;
*/
public abstract class ConditionalFunction extends ScalarFunction {
protected ConditionalFunction(Location location, List<Expression> fields) {
protected DataType dataType = DataType.NULL;
ConditionalFunction(Location location, List<Expression> fields) {
super(location, fields);
}
@Override
public DataType dataType() {
return dataType;
}
@Override
public boolean foldable() {
return Expressions.foldable(children());
}
@Override
public boolean nullable() {
return false;
}
}

@ -0,0 +1,57 @@
/*
* 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.gen.pipeline.MultiPipe;
import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.ConditionalProcessor.ConditionalOperation;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import java.util.List;
import java.util.Objects;
public class ConditionalPipe extends MultiPipe {
private final ConditionalOperation operation;
public ConditionalPipe(Location location, Expression expression, List<Pipe> children, ConditionalOperation operation) {
super(location, expression, children);
this.operation = operation;
}
@Override
protected NodeInfo<ConditionalPipe> info() {
return NodeInfo.create(this, ConditionalPipe::new, expression(), children(), operation);
}
@Override
public Pipe replaceChildren(List<Pipe> newChildren) {
return new ConditionalPipe(location(), expression(), newChildren, operation);
}
@Override
public Processor asProcessor(List<Processor> procs) {
return new ConditionalProcessor(procs, operation);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), operation);
}
@Override
public boolean equals(Object obj) {
if (super.equals(obj)) {
ConditionalPipe other = (ConditionalPipe) obj;
return Objects.equals(operation, other.operation);
}
return false;
}
}

@ -0,0 +1,101 @@
/*
* 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.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
public class ConditionalProcessor implements Processor {
public enum ConditionalOperation implements Function<Collection<Object>, Object> {
COALESCE(Conditionals::coalesce, Conditionals::coalesceInput),
GREATEST(Conditionals::greatest, Conditionals::greatestInput),
LEAST(Conditionals::least, Conditionals::leastInput);
String scriptMethodName() {
return name().toLowerCase(Locale.ROOT);
}
private final Function<Collection<Object>, Object> process;
private final BiFunction<List<Processor>, Object, Object> inputProcess;
ConditionalOperation(Function<Collection<Object>, Object> process,
BiFunction<List<Processor>, Object, Object> inputProcess) {
this.process = process;
this.inputProcess = inputProcess;
}
@Override
public Object apply(Collection<Object> objects) {
return process.apply(objects);
}
Object applyOnInput(List<Processor> processors, Object input) {
return inputProcess.apply(processors, input);
}
}
public static final String NAME = "nco";
private final List<Processor> processors;
private final ConditionalOperation operation;
public ConditionalProcessor(List<Processor> processors, ConditionalOperation operation) {
this.processors = processors;
this.operation = operation;
}
public ConditionalProcessor(StreamInput in) throws IOException {
processors = in.readNamedWriteableList(Processor.class);
operation = in.readEnum(ConditionalOperation.class);
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeNamedWriteableList(processors);
out.writeEnum(operation);
}
@Override
public Object process(Object input) {
return operation.applyOnInput(processors, input);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ConditionalProcessor that = (ConditionalProcessor) o;
return Objects.equals(processors, that.processors) &&
operation == that.operation;
}
@Override
public int hashCode() {
return Objects.hash(processors, operation);
}
}

@ -0,0 +1,84 @@
/*
* 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.gen.processor.Processor;
import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.Comparisons;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
final class Conditionals {
private Conditionals() {}
static Object coalesce(Collection<Object> values) {
if (values == null || values.isEmpty()) {
return null;
}
for (Object object : values) {
if (object != null) {
return object;
}
}
return null;
}
static Object coalesceInput(List<Processor> processors, Object input) {
for (Processor proc : processors) {
Object result = proc.process(input);
if (result != null) {
return result;
}
}
return null;
}
static Object greatest(Collection<Object> values) {
return extremum(values, Comparisons::gt);
}
static Object greatestInput(Collection<Processor> processors, Object input) {
List<Object> values = new ArrayList<>(processors.size());
for (Processor processor : processors) {
values.add(processor.process(input));
}
return greatest(values);
}
static Object least(Collection<Object> values) {
return extremum(values, Comparisons::lt);
}
static Object leastInput(List<Processor> processors, Object input) {
List<Object> values = new ArrayList<>(processors.size());
for (Processor processor : processors) {
values.add(processor.process(input));
}
return least(values);
}
private static Object extremum(Collection<Object> values, BiFunction<Object, Object, Boolean> comparison) {
if (values == null || values.isEmpty()) {
return null;
}
Object result = null;
boolean isFirst = true;
for (Object value : values) {
if (isFirst || (result == null) || (comparison.apply(value, result) == Boolean.TRUE)) {
result = value;
}
isFirst = false;
}
return result;
}
}

@ -0,0 +1,40 @@
/*
* 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.Foldables;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import static org.elasticsearch.xpack.sql.expression.predicate.conditional.ConditionalProcessor.ConditionalOperation.GREATEST;
public class Greatest extends ArbitraryConditionalFunction {
public Greatest(Location location, List<Expression> fields) {
super(location, new ArrayList<>(new LinkedHashSet<>(fields)), GREATEST);
}
@Override
protected NodeInfo<? extends Greatest> info() {
return NodeInfo.create(this, Greatest::new, children());
}
@Override
public Expression replaceChildren(List<Expression> newChildren) {
return new Greatest(location(), newChildren);
}
@Override
public Object fold() {
return GREATEST.apply(Foldables.valuesOfNoDuplicates(children(), dataType));
}
}

@ -19,12 +19,16 @@ import java.util.List;
public class IfNull extends Coalesce {
public IfNull(Location location, Expression first, Expression second) {
super(location, Arrays.asList(first, second));
this(location, Arrays.asList(first, second));
}
private IfNull(Location location, List<Expression> expressions) {
super(location, expressions);
}
@Override
public Expression replaceChildren(List<Expression> newChildren) {
return new IfNull(location(), newChildren.get(0), newChildren.get(1));
return new IfNull(location(), newChildren);
}
@Override

@ -0,0 +1,40 @@
/*
* 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.Foldables;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import static org.elasticsearch.xpack.sql.expression.predicate.conditional.ConditionalProcessor.ConditionalOperation.LEAST;
public class Least extends ArbitraryConditionalFunction {
public Least(Location location, List<Expression> fields) {
super(location, new ArrayList<>(new LinkedHashSet<>(fields)), LEAST);
}
@Override
protected NodeInfo<? extends Least> info() {
return NodeInfo.create(this, Least::new, children());
}
@Override
public Expression replaceChildren(List<Expression> newChildren) {
return new Least(location(), newChildren);
}
@Override
public Object fold() {
return LEAST.apply(Foldables.valuesOfNoDuplicates(children(), dataType));
}
}

@ -26,8 +26,6 @@ import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.pa
*/
public class NullIf extends ConditionalFunction {
private DataType dataType;
public NullIf(Location location, Expression left, Expression right) {
super(location, Arrays.asList(left, right));
}

@ -32,7 +32,7 @@ public final class Comparisons {
return i == null ? null : i.intValue() != 0;
}
static Boolean lt(Object l, Object r) {
public static Boolean lt(Object l, Object r) {
Integer i = compare(l, r);
return i == null ? null : i.intValue() < 0;
}
@ -42,7 +42,7 @@ public final class Comparisons {
return i == null ? null : i.intValue() <= 0;
}
static Boolean gt(Object l, Object r) {
public static Boolean gt(Object l, Object r) {
Integer i = compare(l, r);
return i == null ? null : i.intValue() > 0;
}

@ -41,6 +41,7 @@ import org.elasticsearch.xpack.sql.expression.predicate.BinaryPredicate;
import org.elasticsearch.xpack.sql.expression.predicate.Negatable;
import org.elasticsearch.xpack.sql.expression.predicate.Predicates;
import org.elasticsearch.xpack.sql.expression.predicate.Range;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.ArbitraryConditionalFunction;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.Coalesce;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.NullIf;
import org.elasticsearch.xpack.sql.expression.predicate.logical.And;
@ -132,7 +133,7 @@ public class Optimizer extends RuleExecutor<LogicalPlan> {
new ReplaceFoldableAttributes(),
new FoldNull(),
new ConstantFolding(),
new SimplifyCoalesce(),
new SimplifyConditional(),
// boolean
new BooleanSimplification(),
new BooleanLiteralsOnTheRight(),
@ -1202,34 +1203,35 @@ public class Optimizer extends RuleExecutor<LogicalPlan> {
}
}
static class SimplifyCoalesce extends OptimizerExpressionRule {
static class SimplifyConditional extends OptimizerExpressionRule {
SimplifyCoalesce() {
SimplifyConditional() {
super(TransformDirection.DOWN);
}
@Override
protected Expression rule(Expression e) {
if (e instanceof Coalesce) {
Coalesce c = (Coalesce) e;
if (e instanceof ArbitraryConditionalFunction) {
ArbitraryConditionalFunction c = (ArbitraryConditionalFunction) e;
// find the first non-null foldable child (if any) and remove the rest
// while at it, exclude any nulls found
// exclude any nulls found
List<Expression> newChildren = new ArrayList<>();
for (Expression child : c.children()) {
if (Expressions.isNull(child) == false) {
newChildren.add(child);
if (child.foldable()) {
// For Coalesce find the first non-null foldable child (if any) and break early
if (e instanceof Coalesce && child.foldable()) {
break;
}
}
}
if (newChildren.size() < c.children().size()) {
return new Coalesce(c.location(), newChildren);
return c.replaceChildren(newChildren);
}
}
return e;
}
}

@ -30,7 +30,6 @@ import org.elasticsearch.xpack.sql.expression.gen.pipeline.AggPathInput;
import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.sql.expression.gen.pipeline.UnaryPipe;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.In;
import org.elasticsearch.xpack.sql.plan.physical.AggregateExec;
import org.elasticsearch.xpack.sql.plan.physical.EsQueryExec;
import org.elasticsearch.xpack.sql.plan.physical.FilterExec;
@ -140,9 +139,6 @@ class QueryFolder extends RuleExecutor<PhysicalPlan> {
if (pj instanceof ScalarFunction) {
ScalarFunction f = (ScalarFunction) pj;
processors.put(f.toAttribute(), Expressions.pipe(f));
} else if (pj instanceof In) {
In in = (In) pj;
processors.put(in.toAttribute(), Expressions.pipe(in));
}
}
}

@ -47,6 +47,8 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS
# Null
#
Object coalesce(java.util.List)
Object greatest(java.util.List)
Object least(java.util.List)
Object nullif(Object, Object)
#

@ -37,7 +37,9 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.string.Repeat;
import org.elasticsearch.xpack.sql.expression.predicate.BinaryOperator;
import org.elasticsearch.xpack.sql.expression.predicate.Range;
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.logical.And;
import org.elasticsearch.xpack.sql.expression.predicate.logical.Not;
@ -71,7 +73,7 @@ import org.elasticsearch.xpack.sql.optimizer.Optimizer.PropagateEquals;
import org.elasticsearch.xpack.sql.optimizer.Optimizer.PruneDuplicateFunctions;
import org.elasticsearch.xpack.sql.optimizer.Optimizer.PruneSubqueryAliases;
import org.elasticsearch.xpack.sql.optimizer.Optimizer.ReplaceFoldableAttributes;
import org.elasticsearch.xpack.sql.optimizer.Optimizer.SimplifyCoalesce;
import org.elasticsearch.xpack.sql.optimizer.Optimizer.SimplifyConditional;
import org.elasticsearch.xpack.sql.plan.logical.Filter;
import org.elasticsearch.xpack.sql.plan.logical.LocalRelation;
import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan;
@ -420,19 +422,19 @@ public class OptimizerTests extends ESTestCase {
}
public void testSimplifyCoalesceNulls() {
Expression e = new SimplifyCoalesce().rule(new Coalesce(EMPTY, asList(Literal.NULL, Literal.NULL)));
Expression e = new SimplifyConditional().rule(new Coalesce(EMPTY, asList(Literal.NULL, Literal.NULL)));
assertEquals(Coalesce.class, e.getClass());
assertEquals(0, e.children().size());
}
public void testSimplifyCoalesceRandomNulls() {
Expression e = new SimplifyCoalesce().rule(new Coalesce(EMPTY, randomListOfNulls()));
Expression e = new SimplifyConditional().rule(new Coalesce(EMPTY, randomListOfNulls()));
assertEquals(Coalesce.class, e.getClass());
assertEquals(0, e.children().size());
}
public void testSimplifyCoalesceRandomNullsWithValue() {
Expression e = new SimplifyCoalesce().rule(new Coalesce(EMPTY,
Expression e = new SimplifyConditional().rule(new Coalesce(EMPTY,
CollectionUtils.combine(
CollectionUtils.combine(randomListOfNulls(), Literal.TRUE, Literal.FALSE, Literal.TRUE),
randomListOfNulls())));
@ -445,7 +447,7 @@ public class OptimizerTests extends ESTestCase {
}
public void testSimplifyCoalesceFirstLiteral() {
Expression e = new SimplifyCoalesce()
Expression e = new SimplifyConditional()
.rule(new Coalesce(EMPTY,
Arrays.asList(Literal.NULL, Literal.TRUE, Literal.FALSE, new Abs(EMPTY, getFieldAttribute()))));
assertEquals(Coalesce.class, e.getClass());
@ -454,17 +456,19 @@ public class OptimizerTests extends ESTestCase {
}
public void testSimplifyIfNullNulls() {
Expression e = new SimplifyCoalesce().rule(new IfNull(EMPTY, Literal.NULL, Literal.NULL));
assertEquals(Coalesce.class, e.getClass());
Expression e = new SimplifyConditional().rule(new IfNull(EMPTY, Literal.NULL, Literal.NULL));
assertEquals(IfNull.class, e.getClass());
assertEquals(0, e.children().size());
}
public void testSimplifyIfNullWithNullAndValue() {
Expression e = new SimplifyCoalesce().rule(new IfNull(EMPTY, Literal.NULL, ONE));
Expression e = new SimplifyConditional().rule(new IfNull(EMPTY, Literal.NULL, ONE));
assertEquals(IfNull.class, e.getClass());
assertEquals(1, e.children().size());
assertEquals(ONE, e.children().get(0));
e = new SimplifyCoalesce().rule(new IfNull(EMPTY, ONE, Literal.NULL));
e = new SimplifyConditional().rule(new IfNull(EMPTY, ONE, Literal.NULL));
assertEquals(IfNull.class, e.getClass());
assertEquals(1, e.children().size());
assertEquals(ONE, e.children().get(0));
}
@ -475,6 +479,48 @@ public class OptimizerTests extends ESTestCase {
assertEquals(orig, f);
}
public void testSimplifyGreatestNulls() {
Expression e = new SimplifyConditional().rule(new Greatest(EMPTY, asList(Literal.NULL, Literal.NULL)));
assertEquals(Greatest.class, e.getClass());
assertEquals(0, e.children().size());
}
public void testSimplifyGreatestRandomNulls() {
Expression e = new SimplifyConditional().rule(new Greatest(EMPTY, randomListOfNulls()));
assertEquals(Greatest.class, e.getClass());
assertEquals(0, e.children().size());
}
public void testSimplifyGreatestRandomNullsWithValue() {
Expression e = new SimplifyConditional().rule(new Greatest(EMPTY,
CollectionUtils.combine(CollectionUtils.combine(randomListOfNulls(), ONE, TWO, ONE), randomListOfNulls())));
assertEquals(Greatest.class, e.getClass());
assertEquals(2, e.children().size());
assertEquals(ONE, e.children().get(0));
assertEquals(TWO, e.children().get(1));
}
public void testSimplifyLeastNulls() {
Expression e = new SimplifyConditional().rule(new Least(EMPTY, asList(Literal.NULL, Literal.NULL)));
assertEquals(Least.class, e.getClass());
assertEquals(0, e.children().size());
}
public void testSimplifyLeastRandomNulls() {
Expression e = new SimplifyConditional().rule(new Least(EMPTY, randomListOfNulls()));
assertEquals(Least.class, e.getClass());
assertEquals(0, e.children().size());
}
public void testSimplifyLeastRandomNullsWithValue() {
Expression e = new SimplifyConditional().rule(new Least(EMPTY,
CollectionUtils.combine(CollectionUtils.combine(randomListOfNulls(), ONE, TWO, ONE), randomListOfNulls())));
assertEquals(Least.class, e.getClass());
assertEquals(2, e.children().size());
assertEquals(ONE, e.children().get(0));
assertEquals(TWO, e.children().get(1));
}
//
// Logical simplifications
//