SQL: Implement GREATEST and LEAST functions (#35879)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()));
}
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) {

View File

@ -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"),

View File

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

View File

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

View File

@ -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());
}
}

View File

@ -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()));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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