SQL: handle NULL arithmetic operations with INTERVALs (#49633)

(cherry picked from commit ce727615c08cf5ae422feb77f69ea24fb53cd9d1)
This commit is contained in:
Andrei Stefan 2019-12-02 16:05:05 +02:00 committed by Andrei Stefan
parent 34311dd818
commit e2982b2110
15 changed files with 86 additions and 26 deletions

View File

@ -55,7 +55,7 @@ s|Description
==== Operators
Basic arithmetic operators (`+`, `-`, etc) support date/time parameters as indicated below:
Basic arithmetic operators (`+`, `-`, `*`) support date/time parameters as indicated below:
[source, sql]
--------------------------------------------------

View File

@ -18,3 +18,12 @@ SELECT 5 - 2 x FROM test_emp LIMIT 5;
3
;
nullArithmetics
schema::a:i|b:d|c:s|d:s|e:l|f:i|g:i|h:i|i:i|j:i|k:d
SELECT null + 2 AS a, null * 1.5 AS b, null + null AS c, null - null AS d, null - 1234567890123 AS e, 123 - null AS f, null / 5 AS g, 5 / null AS h, null % 5 AS i, 5 % null AS j, null + 5.5 - (null * (null * 3)) AS k;
a | b | c | d | e | f | g | h | i | j | k
---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------
null |null |null |null |null |null |null |null |null |null |null
;

View File

@ -191,6 +191,15 @@ SELECT 4 * -INTERVAL '2' HOURS AS result1, -5 * -INTERVAL '3' HOURS AS result2;
-0 08:00:00.0 | +0 15:00:00.0
;
intervalNullMath
schema::null_multiply:string|null_sub1:string|null_sub2:string|null_add:string
SELECT null * INTERVAL '1 23:45' DAY TO MINUTES AS null_multiply, INTERVAL '1' DAY - null AS null_sub1, null - INTERVAL '1' DAY AS null_sub2, INTERVAL 1 DAY + null AS null_add;
null_multiply | null_sub1 | null_sub2 | null_add
-----------------+-------------+-------------+-------------
null |null |null |null
;
intervalAndFieldMultiply
schema::languages:byte|result:string
SELECT languages, CAST (languages * INTERVAL '1 10:30' DAY TO MINUTES AS string) AS result FROM test_emp ORDER BY emp_no LIMIT 5;

View File

@ -5,8 +5,9 @@
*/
package org.elasticsearch.xpack.sql.expression;
import org.elasticsearch.xpack.sql.expression.Expression.TypeResolution;
import org.elasticsearch.xpack.sql.expression.Expressions.ParamOrdinal;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypes;
import org.elasticsearch.xpack.sql.type.EsField;
import java.util.Locale;
@ -14,8 +15,6 @@ import java.util.StringJoiner;
import java.util.function.Predicate;
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
import static org.elasticsearch.xpack.sql.expression.Expression.TypeResolution;
import static org.elasticsearch.xpack.sql.expression.Expressions.ParamOrdinal;
import static org.elasticsearch.xpack.sql.expression.Expressions.name;
import static org.elasticsearch.xpack.sql.type.DataType.BOOLEAN;
@ -119,7 +118,7 @@ public final class TypeResolutions {
String operationName,
ParamOrdinal paramOrd,
String... acceptedTypes) {
return predicate.test(e.dataType()) || DataTypes.isNull(e.dataType())?
return predicate.test(e.dataType()) || e.dataType().isNull() ?
TypeResolution.TYPE_RESOLVED :
new TypeResolution(format(null, "{}argument of [{}] must be [{}], found value [{}] type [{}]",
paramOrd == null || paramOrd == ParamOrdinal.DEFAULT ? "" : paramOrd.name().toLowerCase(Locale.ROOT) + " ",

View File

@ -13,7 +13,6 @@ import org.elasticsearch.xpack.sql.tree.NodeInfo;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypeConversion;
import org.elasticsearch.xpack.sql.type.DataTypes;
import java.util.Objects;
@ -64,7 +63,7 @@ public class Cast extends UnaryScalarFunction {
@Override
public Nullability nullable() {
if (DataTypes.isNull(from())) {
if (from().isNull()) {
return Nullability.TRUE;
}
return field().nullable();

View File

@ -78,7 +78,7 @@ public class Case extends ConditionalFunction {
protected TypeResolution resolveType() {
DataType expectedResultDataType = null;
for (IfConditional ifConditional : conditions) {
if (DataTypes.isNull(ifConditional.result().dataType()) == false) {
if (ifConditional.result().dataType().isNull() == false) {
expectedResultDataType = ifConditional.result().dataType();
break;
}

View File

@ -12,10 +12,9 @@ import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.expression.gen.script.Scripts;
import org.elasticsearch.xpack.sql.expression.predicate.Negatable;
import org.elasticsearch.xpack.sql.expression.predicate.nulls.CheckNullProcessor.CheckNullOperation;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypes;
public class IsNotNull extends UnaryScalarFunction implements Negatable<UnaryScalarFunction> {
@ -35,7 +34,7 @@ public class IsNotNull extends UnaryScalarFunction implements Negatable<UnarySca
@Override
public Object fold() {
return field().fold() != null && !DataTypes.isNull(field().dataType());
return field().fold() != null && !field().dataType().isNull();
}
@Override

View File

@ -12,10 +12,9 @@ import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.expression.gen.script.Scripts;
import org.elasticsearch.xpack.sql.expression.predicate.Negatable;
import org.elasticsearch.xpack.sql.expression.predicate.nulls.CheckNullProcessor.CheckNullOperation;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypes;
public class IsNull extends UnaryScalarFunction implements Negatable<UnaryScalarFunction> {
@ -35,7 +34,7 @@ public class IsNull extends UnaryScalarFunction implements Negatable<UnaryScalar
@Override
public Object fold() {
return field().fold() == null || DataTypes.isNull(field().dataType());
return field().fold() == null || field().dataType().isNull();
}
@Override

View File

@ -56,7 +56,7 @@ abstract class DateTimeArithmeticOperation extends ArithmeticOperation {
DataType l = left().dataType();
DataType r = right().dataType();
if (!(r.isDateOrTimeBased() || r.isInterval())|| !(l.isDateOrTimeBased() || l.isInterval())) {
if (!(r.isDateOrTimeBased() || r.isInterval() || r.isNull())|| !(l.isDateOrTimeBased() || l.isInterval() || l.isNull())) {
return new TypeResolution(format(null, "[{}] has arguments with incompatible types [{}] and [{}]", symbol(), l, r));
}
return TypeResolution.TYPE_RESOLVED;

View File

@ -34,14 +34,14 @@ public class Mul extends ArithmeticOperation {
DataType r = right().dataType();
// 1. both are numbers
if (l.isNumeric() && r.isNumeric()) {
if (l.isNullOrNumeric() && r.isNullOrNumeric()) {
return TypeResolution.TYPE_RESOLVED;
}
if (l.isInterval() && r.isInteger()) {
if (l.isNullOrInterval() && (r.isInteger() || r.isNull())) {
dataType = l;
return TypeResolution.TYPE_RESOLVED;
} else if (r.isInterval() && l.isInteger()) {
} else if (r.isNullOrInterval() && (l.isInteger() || l.isNull())) {
dataType = r;
return TypeResolution.TYPE_RESOLVED;
}

View File

@ -9,7 +9,6 @@ import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.Foldables;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.type.DataTypes;
import java.util.Collections;
import java.util.LinkedHashSet;
@ -27,7 +26,7 @@ public class TermsQuery extends LeafQuery {
public TermsQuery(Source source, String term, List<Expression> values) {
super(source);
this.term = term;
values.removeIf(e -> DataTypes.isNull(e.dataType()));
values.removeIf(e -> e.dataType().isNull());
if (values.isEmpty()) {
this.values = Collections.emptySet();
} else {

View File

@ -247,6 +247,18 @@ public enum DataType {
return isNumeric();
}
public boolean isNull() {
return this == NULL;
}
public boolean isNullOrNumeric() {
return isNull() || isNumeric();
}
public boolean isNullOrInterval() {
return isNull() || isInterval();
}
public boolean isString() {
return this == KEYWORD || this == TEXT;
}

View File

@ -45,10 +45,10 @@ public abstract class DataTypeConversion {
if (left == right) {
return left;
}
if (DataTypes.isNull(left)) {
if (left.isNull()) {
return right;
}
if (DataTypes.isNull(right)) {
if (right.isNull()) {
return left;
}
if (left.isString() && right.isString()) {

View File

@ -31,10 +31,6 @@ public final class DataTypes {
private DataTypes() {}
public static boolean isNull(DataType from) {
return from == NULL;
}
public static boolean isUnsupported(DataType from) {
return from == UNSUPPORTED;
}

View File

@ -227,6 +227,45 @@ public class BinaryArithmeticTests extends ESTestCase {
Period p = interval.interval();
assertEquals(Period.ofYears(2).negated(), p);
}
public void testMulNullInterval() {
Literal literal = interval(Period.ofMonths(1), INTERVAL_MONTH);
Mul result = new Mul(EMPTY, L(null), literal);
assertTrue(result.foldable());
assertNull(result.fold());
assertEquals(INTERVAL_MONTH, result.dataType());
result = new Mul(EMPTY, literal, L(null));
assertTrue(result.foldable());
assertNull(result.fold());
assertEquals(INTERVAL_MONTH, result.dataType());
}
public void testAddNullInterval() {
Literal literal = interval(Period.ofMonths(1), INTERVAL_MONTH);
Add result = new Add(EMPTY, L(null), literal);
assertTrue(result.foldable());
assertNull(result.fold());
assertEquals(INTERVAL_MONTH, result.dataType());
result = new Add(EMPTY, literal, L(null));
assertTrue(result.foldable());
assertNull(result.fold());
assertEquals(INTERVAL_MONTH, result.dataType());
}
public void testSubNullInterval() {
Literal literal = interval(Period.ofMonths(1), INTERVAL_MONTH);
Sub result = new Sub(EMPTY, L(null), literal);
assertTrue(result.foldable());
assertNull(result.fold());
assertEquals(INTERVAL_MONTH, result.dataType());
result = new Sub(EMPTY, literal, L(null));
assertTrue(result.foldable());
assertNull(result.fold());
assertEquals(INTERVAL_MONTH, result.dataType());
}
@SuppressWarnings("unchecked")
private static <T> T add(Object l, Object r) {