SQL: Implement NULLIF(expr1, expr2) function (#35826)

NULLIF returns null if the 2 expressions are equal or the
expr1 otherwise.

Closes: #35818
This commit is contained in:
Marios Trivyzas 2018-11-23 22:19:27 +01:00 committed by GitHub
parent 51d2e986c5
commit 410f570d5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 316 additions and 17 deletions

View File

@ -155,3 +155,38 @@ include-tagged::{sql-specs}/docs.csv-spec[nvlReturnFirst]
----
include-tagged::{sql-specs}/docs.csv-spec[nvlReturnSecond]
----
[[sql-functions-conditional-nullif]]
==== `NULLIF`
.Synopsis
[source, sql]
----
NULLIF ( expression<1>, expression<2> )
----
*Input*:
<1> 1st expression
<2> 2nd expression
*Output*: `null` if the 2 expressions are equal, otherwise the 1st expression.
.Description
Returns `null` when the two input expressions are equal and
if not, it returns the 1st expression.
["source","sql",subs="attributes,callouts,macros"]
----
include-tagged::{sql-specs}/docs.csv-spec[nullIfReturnFirst]
----
["source","sql",subs="attributes,callouts,macros"]
----
include-tagged::{sql-specs}/docs.csv-spec[nullIfReturnNull]
----

View File

@ -23,6 +23,7 @@ COALESCE |CONDITIONAL
IFNULL |CONDITIONAL
ISNULL |CONDITIONAL
NVL |CONDITIONAL
NULLIF |CONDITIONAL
DAY |SCALAR
DAYNAME |SCALAR
DAYOFMONTH |SCALAR

View File

@ -200,6 +200,7 @@ COALESCE |CONDITIONAL
IFNULL |CONDITIONAL
ISNULL |CONDITIONAL
NVL |CONDITIONAL
NULLIF |CONDITIONAL
DAY |SCALAR
DAYNAME |SCALAR
DAYOFMONTH |SCALAR
@ -1597,3 +1598,24 @@ SELECT NVL(null, 'search') AS "nvl";
search
// end::nvlReturnSecond
;
nullIfReturnFirst
// tag::nullIfReturnFirst
SELECT NULLIF('elastic', 'search') AS "nullif";
nullif
---------------
elastic
// end::nullIfReturnFirst
;
nullIfReturnNull
// tag::nullIfReturnNull
SELECT NULLIF('elastic', 'elastic') AS "nullif";
nullif:s
---------------
null
// end::nullIfReturnNull
;

View File

@ -12,4 +12,13 @@ coalesceWhere
SELECT COALESCE(null, ABS(emp_no) + 1, 123) AS c FROM test_emp WHERE COALESCE(null, ABS(emp_no) + 1, 123, 321) > 100 ORDER BY emp_no NULLS FIRST LIMIT 5;
ifNullField
SELECT IFNULL(null, ABS(emp_no) + 1) AS c FROM test_emp ORDER BY emp_no LIMIT 5;
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;
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;
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;

View File

@ -83,7 +83,8 @@ 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.IFNull;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfNull;
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;
import org.elasticsearch.xpack.sql.tree.Location;
@ -93,11 +94,13 @@ import org.elasticsearch.xpack.sql.util.StringUtils;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
@ -108,6 +111,15 @@ import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;
public class FunctionRegistry {
private static final Set<String> EXCLUDE_FROM_NAME_NORMALIZATION = new HashSet<>();
static {
EXCLUDE_FROM_NAME_NORMALIZATION.add(IfNull.class.getSimpleName());
EXCLUDE_FROM_NAME_NORMALIZATION.add(NullIf.class.getSimpleName());
}
// list of functions grouped by type of functions (aggregate, statistics, math etc) and ordered alphabetically inside each group
// a single function will have one entry for itself with its name associated to its instance and, also, one entry for each alias
// it has with the alias name associated to the FunctionDefinition instance
@ -146,7 +158,8 @@ public class FunctionRegistry {
// Scalar functions
// conditional
addToMap(def(Coalesce.class, Coalesce::new));
addToMap(def(IFNull.class, IFNull::new, "ISNULL", "NVL"));
addToMap(def(IfNull.class, IfNull::new, "ISNULL", "NVL"));
addToMap(def(NullIf.class, NullIf::new));
// Date
addToMap(def(DayName.class, DayName::new, "DAYNAME"),
def(DayOfMonth.class, DayOfMonth::new, "DAYOFMONTH", "DAY", "DOM"),
@ -485,7 +498,11 @@ public class FunctionRegistry {
}
private static String normalize(String name) {
// translate CamelCase to camel_case
if (EXCLUDE_FROM_NAME_NORMALIZATION.contains(name)) {
return name.toUpperCase(Locale.ROOT);
}
// translate CamelCase to CAMEL_CASE
return StringUtils.camelCaseToUnderscore(name);
}
}

View File

@ -26,6 +26,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.NullIfProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.logical.BinaryLogicProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.logical.NotProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.nulls.CheckNullProcessor;
@ -61,6 +62,7 @@ public final class Processors {
// 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, NullIfProcessor.NAME, NullIfProcessor::new));
// arithmetic
entries.add(new Entry(Processor.class, BinaryArithmeticProcessor.NAME, BinaryArithmeticProcessor::new));

View File

@ -24,6 +24,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFu
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.NullIfProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.logical.BinaryLogicProcessor.BinaryLogicOperation;
import org.elasticsearch.xpack.sql.expression.predicate.logical.NotProcessor;
import org.elasticsearch.xpack.sql.expression.predicate.nulls.CheckNullProcessor.CheckNullOperation;
@ -142,6 +143,10 @@ public final class InternalSqlScriptUtils {
return CoalesceProcessor.apply(expressions);
}
public static Object nullif(Object left, Object right) {
return NullIfProcessor.apply(left, right);
}
//
// Regex
//

View File

@ -15,22 +15,20 @@ import java.util.List;
/**
* Variant of {@link Coalesce} with two args used by MySQL and ODBC.
*
* Name is `IFNull` to avoid having it registered as `IF_NULL` instead of `IFNULL`.
*/
public class IFNull extends Coalesce {
public class IfNull extends Coalesce {
public IFNull(Location location, Expression first, Expression second) {
public IfNull(Location location, Expression first, Expression second) {
super(location, Arrays.asList(first, second));
}
@Override
public Expression replaceChildren(List<Expression> newChildren) {
return new IFNull(location(), newChildren.get(0), newChildren.get(1));
return new IfNull(location(), newChildren.get(0), newChildren.get(1));
}
@Override
protected NodeInfo<IFNull> info() {
return NodeInfo.create(this, IFNull::new, children().get(0), children().get(1));
protected NodeInfo<IfNull> info() {
return NodeInfo.create(this, IfNull::new, children().get(0), children().get(1));
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.tree.Location;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import org.elasticsearch.xpack.sql.type.DataType;
import java.util.Arrays;
import java.util.List;
import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder;
/**
* Accepts 2 arguments of any data type and returns null if they are equal,
* and the 1st argument otherwise.
*/
public class NullIf extends ConditionalFunction {
private DataType dataType;
public NullIf(Location location, Expression left, Expression right) {
super(location, Arrays.asList(left, right));
}
@Override
protected NodeInfo<? extends NullIf> info() {
return NodeInfo.create(this, NullIf::new, children().get(0), children().get(1));
}
@Override
public Expression replaceChildren(List<Expression> newChildren) {
return new NullIf(location(), newChildren.get(0), newChildren.get(1));
}
@Override
protected TypeResolution resolveType() {
dataType = children().get(0).dataType();
return TypeResolution.TYPE_RESOLVED;
}
@Override
public DataType dataType() {
return dataType;
}
@Override
public boolean foldable() {
return Expressions.foldable(children());
}
@Override
public boolean nullable() {
return true;
}
@Override
public Object fold() {
return NullIfProcessor.apply(children().get(0).fold(), children().get(1).fold());
}
@Override
public ScriptTemplate asScript() {
ScriptTemplate left = asScript(children().get(0));
ScriptTemplate right = asScript(children().get(1));
String template = "{sql}.nullif(" + left.template() + "," + right.template() + ")";
ParamsBuilder params = paramsBuilder();
params.script(left.params());
params.script(right.params());
return new ScriptTemplate(template, params.build(), dataType);
}
@Override
protected Pipe makePipe() {
return new NullIfPipe(location(), this,
Expressions.pipe(children().get(0)), Expressions.pipe(children().get(1)));
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.BinaryPipe;
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;
public class NullIfPipe extends BinaryPipe {
public NullIfPipe(Location location, Expression expression, Pipe left, Pipe right) {
super(location, expression, left, right);
}
@Override
protected BinaryPipe replaceChildren(Pipe left, Pipe right) {
return new NullIfPipe(location(), expression(), left, right);
}
@Override
protected NodeInfo<NullIfPipe> info() {
return NodeInfo.create(this, NullIfPipe::new, expression(), children().get(0), children().get(1));
}
@Override
public Processor asProcessor() {
return new NullIfProcessor(left().asProcessor(), right().asProcessor());
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.Objects;
import static org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.BinaryComparisonProcessor.BinaryComparisonOperation.EQ;
public class NullIfProcessor implements Processor {
public static final String NAME = "nni";
private final Processor leftProcessor;
private final Processor rightProcessor;
public NullIfProcessor(Processor leftProcessor, Processor rightProcessor) {
this.leftProcessor = leftProcessor;
this.rightProcessor = rightProcessor;
}
public NullIfProcessor(StreamInput in) throws IOException {
leftProcessor = in.readNamedWriteable(Processor.class);
rightProcessor = in.readNamedWriteable(Processor.class);
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeNamedWriteable(leftProcessor);
out.writeNamedWriteable(rightProcessor);
}
@Override
public Object process(Object input) {
Object leftValue = leftProcessor.process(input);
Object rightValue = rightProcessor.process(input);
return apply(leftValue, rightValue);
}
public static Object apply(Object leftValue, Object rightValue) {
if (EQ.apply(leftValue, rightValue) == Boolean.TRUE) {
return null;
}
return leftValue;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NullIfProcessor that = (NullIfProcessor) o;
return Objects.equals(leftProcessor, that.leftProcessor) &&
Objects.equals(rightProcessor, that.rightProcessor);
}
@Override
public int hashCode() {
return Objects.hash(leftProcessor, rightProcessor);
}
}

View File

@ -42,6 +42,7 @@ 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.Coalesce;
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;
import org.elasticsearch.xpack.sql.expression.predicate.logical.Or;
@ -1172,6 +1173,9 @@ public class Optimizer extends RuleExecutor<LogicalPlan> {
return Literal.of(in, null);
}
} else if (e instanceof NullIf) {
return e;
} else if (e.nullable() && Expressions.anyMatch(e.children(), Expressions::isNull)) {
return Literal.of(e, null);
}
@ -1229,6 +1233,7 @@ public class Optimizer extends RuleExecutor<LogicalPlan> {
}
}
static class BooleanSimplification extends OptimizerExpressionRule {
BooleanSimplification() {

View File

@ -46,6 +46,7 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS
# Null
#
Object coalesce(java.util.List)
Object nullif(Object, Object)
#
# Regex

View File

@ -37,7 +37,8 @@ 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.IFNull;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfNull;
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;
import org.elasticsearch.xpack.sql.expression.predicate.logical.Or;
@ -450,21 +451,27 @@ public class OptimizerTests extends ESTestCase {
}
public void testSimplifyIfNullNulls() {
Expression e = new SimplifyCoalesce().rule(new IFNull(EMPTY, Literal.NULL, Literal.NULL));
Expression e = new SimplifyCoalesce().rule(new IfNull(EMPTY, Literal.NULL, Literal.NULL));
assertEquals(Coalesce.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 SimplifyCoalesce().rule(new IfNull(EMPTY, Literal.NULL, ONE));
assertEquals(1, e.children().size());
assertEquals(ONE, e.children().get(0));
e = new SimplifyCoalesce().rule(new IFNull(EMPTY, ONE, Literal.NULL));
e = new SimplifyCoalesce().rule(new IfNull(EMPTY, ONE, Literal.NULL));
assertEquals(1, e.children().size());
assertEquals(ONE, e.children().get(0));
}
public void testFoldNullNotAppliedOnNullIf() {
Expression orig = new NullIf(EMPTY, ONE, Literal.NULL);
Expression f = new FoldNull().rule(orig);
assertEquals(orig, f);
}
//
// Logical simplifications
//

View File

@ -25,7 +25,7 @@ import org.elasticsearch.xpack.sql.expression.gen.pipeline.BinaryPipesTests;
import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.sql.expression.gen.processor.ConstantProcessor;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.IFNull;
import org.elasticsearch.xpack.sql.expression.predicate.conditional.IfNull;
import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.In;
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.FullTextPredicate;
import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.InPipe;
@ -88,7 +88,7 @@ import static org.mockito.Mockito.mock;
public class NodeSubclassTests<T extends B, B extends Node<B>> extends ESTestCase {
private static final List<Class<? extends Node<?>>> CLASSES_WITH_MIN_TWO_CHILDREN = Arrays.asList(
IFNull.class, In.class, InPipe.class, Percentile.class, Percentiles.class, PercentileRanks.class);
IfNull.class, In.class, InPipe.class, Percentile.class, Percentiles.class, PercentileRanks.class);
private final Class<T> subclass;