SQL: make date/datetime and interval types compatible in conditional functions (#47595)

(cherry picked from commit 6ff953e6396d7cc90640419aee5d036954e2eae3)
This commit is contained in:
Andrei Stefan 2019-10-10 13:57:44 +03:00 committed by Andrei Stefan
parent 9eac8bf2a8
commit 6a4bf5de2c
11 changed files with 162 additions and 57 deletions

View File

@ -348,3 +348,104 @@ SELECT CONVERT(IIF(languages > 1, IIF(languages = 3, '3')), SQL_BIGINT) AS cond
3
null
;
ifNullWithCompatibleDateBasedValues
schema::replacement:ts
SELECT IFNULL(birth_date, {d '2110-04-12'}) AS replacement FROM test_emp GROUP BY 1 ORDER BY replacement DESC LIMIT 5;
replacement
------------------------
2110-04-12T00:00:00.000Z
1965-01-03T00:00:00.000Z
1964-10-18T00:00:00.000Z
1964-06-11T00:00:00.000Z
1964-06-02T00:00:00.000Z
;
caseWithCompatibleIntervals_1
schema::date_math:ts|c:l
SELECT birth_date + (CASE WHEN gender='M' THEN INTERVAL 1 YEAR ELSE INTERVAL 6 MONTH END) AS date_math, COUNT(*) c FROM test_emp GROUP BY 1 ORDER BY 1 DESC LIMIT 5;
date_math | c
------------------------+---------------
1966-01-03T00:00:00.000Z|1
1965-06-11T00:00:00.000Z|1
1965-04-18T00:00:00.000Z|2
1964-12-02T00:00:00.000Z|1
1964-11-26T00:00:00.000Z|1
;
caseWithCompatibleIntervals_2
SELECT hire_date, birth_date, (CASE WHEN birth_date > {d '1960-01-01'} THEN INTERVAL 1 YEAR ELSE INTERVAL 1 MONTH END) AS x FROM test_emp WHERE x + hire_date > {d '1995-01-01'} ORDER BY hire_date;
hire_date | birth_date | x
------------------------+------------------------+---------------
1994-04-09T00:00:00.000Z|1962-11-07T00:00:00.000Z|+1-0
1995-01-27T00:00:00.000Z|1961-05-02T00:00:00.000Z|+1-0
1995-03-13T00:00:00.000Z|1957-04-04T00:00:00.000Z|+0-1
1995-03-20T00:00:00.000Z|1953-04-03T00:00:00.000Z|+0-1
1995-08-22T00:00:00.000Z|1952-07-08T00:00:00.000Z|+0-1
1995-12-15T00:00:00.000Z|1960-05-25T00:00:00.000Z|+1-0
1996-11-05T00:00:00.000Z|1964-06-11T00:00:00.000Z|+1-0
1997-05-19T00:00:00.000Z|1958-09-05T00:00:00.000Z|+0-1
1999-04-30T00:00:00.000Z|1953-01-23T00:00:00.000Z|+0-1
;
iifWithCompatibleIntervals
schema::hire_date + IIF(salary > 70000, INTERVAL 2 HOURS, INTERVAL 2 DAYS):ts|salary:i
SELECT hire_date + IIF(salary > 70000, INTERVAL 2 HOURS, INTERVAL 2 DAYS), salary FROM test_emp ORDER BY salary DESC LIMIT 10;
hire_date + IIF(salary > 70000, INTERVAL 2 HOURS, INTERVAL 2 DAYS)| salary
------------------------------------------------------------------+---------------
1985-11-20T02:00:00.000Z |74999
1989-09-02T02:00:00.000Z |74970
1989-02-10T02:00:00.000Z |74572
1989-07-07T02:00:00.000Z |73851
1999-04-30T02:00:00.000Z |73717
1988-10-18T02:00:00.000Z |73578
1990-09-15T02:00:00.000Z |71165
1987-03-18T02:00:00.000Z |70011
1987-05-28T00:00:00.000Z |69904
1990-02-18T00:00:00.000Z |68547
;
isNullWithIntervalMath
SELECT ISNULL(birth_date, INTERVAL '23:45' HOUR TO MINUTES + {d '2019-09-17'}) AS c, salary, birth_date, hire_date FROM test_emp ORDER BY salary DESC LIMIT 5;
c:ts | salary:i | birth_date:ts | hire_date:ts
------------------------+-----------------+------------------------+------------------------
1956-12-13T00:00:00.000Z|74999 |1956-12-13T00:00:00.000Z|1985-11-20T00:00:00.000Z
2019-09-17T00:00:00.000Z|74970 |null |1989-09-02T00:00:00.000Z
1957-05-23T00:00:00.000Z|74572 |1957-05-23T00:00:00.000Z|1989-02-10T00:00:00.000Z
1962-07-10T00:00:00.000Z|73851 |1962-07-10T00:00:00.000Z|1989-07-07T00:00:00.000Z
1953-01-23T00:00:00.000Z|73717 |1953-01-23T00:00:00.000Z|1999-04-30T00:00:00.000Z
;
coalesceWithCompatibleDateBasedTypes
SELECT COALESCE(birth_date, CAST(birth_date AS DATE), CAST(hire_date AS DATETIME)) AS coalesce FROM test_emp ORDER BY 1 LIMIT 5;
coalesce:ts
------------------------
1952-02-27T00:00:00.000Z
1952-04-19T00:00:00.000Z
1952-05-15T00:00:00.000Z
1952-06-13T00:00:00.000Z
1952-07-08T00:00:00.000Z
;
greatestWithCompatibleDateBasedTypes
SELECT GREATEST(null, null, birth_date + INTERVAL 25 YEARS, hire_date + INTERVAL 2 DAYS, CAST(hire_date + INTERVAL 2 DAYS AS DATE)) AS greatest, birth_date, hire_date FROM test_emp ORDER BY 1 LIMIT 10;
greatest:ts | birth_date:ts | hire_date:ts
------------------------+------------------------+------------------------
1985-02-20T00:00:00.000Z|1952-04-19T00:00:00.000Z|1985-02-18T00:00:00.000Z
1985-02-26T00:00:00.000Z|null |1985-02-24T00:00:00.000Z
1985-07-11T00:00:00.000Z|1952-06-13T00:00:00.000Z|1985-07-09T00:00:00.000Z
1985-10-16T00:00:00.000Z|1955-08-20T00:00:00.000Z|1985-10-14T00:00:00.000Z
1985-11-21T00:00:00.000Z|1957-12-03T00:00:00.000Z|1985-11-19T00:00:00.000Z
1985-11-22T00:00:00.000Z|1956-12-13T00:00:00.000Z|1985-11-20T00:00:00.000Z
1985-11-22T00:00:00.000Z|1959-04-07T00:00:00.000Z|1985-11-20T00:00:00.000Z
1986-02-06T00:00:00.000Z|1954-09-13T00:00:00.000Z|1986-02-04T00:00:00.000Z
1986-02-28T00:00:00.000Z|1952-11-13T00:00:00.000Z|1986-02-26T00:00:00.000Z
1986-05-30T00:00:00.000Z|1961-05-30T00:00:00.000Z|1986-03-14T00:00:00.000Z
;

View File

@ -119,3 +119,23 @@ SELECT COUNT(*), TRUNCATE(emp_no, -2) t FROM test_emp WHERE 'aaabbb' RLIKE 'a{2,
99 |10000
1 |10100
;
inWithCompatibleDateTypes
SELECT birth_date FROM test_emp WHERE birth_date IN ({d '1959-07-23'},CAST('1959-12-25T12:12:12' AS TIMESTAMP)) OR birth_date IS NULL ORDER BY birth_date;
birth_date:ts
------------------------
1959-07-23T00:00:00.000Z
1959-07-23T00:00:00.000Z
1959-12-25T00:00:00.000Z
null
null
null
null
null
null
null
null
null
null
;

View File

@ -12,7 +12,6 @@ import org.elasticsearch.xpack.sql.expression.Literal;
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;
import java.time.ZoneId;
import java.util.Collections;
@ -48,7 +47,7 @@ public class Histogram extends GroupingFunction {
if (resolution == TypeResolution.TYPE_RESOLVED) {
// interval must be Literal interval
if (field().dataType().isDateBased()) {
resolution = isType(interval, DataTypes::isInterval, "(Date) HISTOGRAM", ParamOrdinal.SECOND, "interval");
resolution = isType(interval, DataType::isInterval, "(Date) HISTOGRAM", ParamOrdinal.SECOND, "interval");
} else {
resolution = isNumeric(interval, "(Numeric) HISTOGRAM", ParamOrdinal.SECOND);
}

View File

@ -11,7 +11,6 @@ import org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic.Bina
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 static org.elasticsearch.common.logging.LoggerMessageFormat.format;
@ -41,7 +40,7 @@ abstract class DateTimeArithmeticOperation extends ArithmeticOperation {
return TypeResolution.TYPE_RESOLVED;
}
// 2. 3. 4. intervals
if ((DataTypes.isInterval(l) || DataTypes.isInterval(r))) {
if (l.isInterval() || r.isInterval()) {
if (DataTypeConversion.commonType(l, r) == null) {
return new TypeResolution(format(null, "[{}] has arguments with incompatible types [{}] and [{}]", symbol(), l, r));
} else {
@ -57,7 +56,7 @@ abstract class DateTimeArithmeticOperation extends ArithmeticOperation {
DataType l = left().dataType();
DataType r = right().dataType();
if (!(r.isDateOrTimeBased() || DataTypes.isInterval(r))|| !(l.isDateOrTimeBased() || DataTypes.isInterval(l))) {
if (!(r.isDateOrTimeBased() || r.isInterval())|| !(l.isDateOrTimeBased() || l.isInterval())) {
return new TypeResolution(format(null, "[{}] has arguments with incompatible types [{}] and [{}]", symbol(), l, r));
}
return TypeResolution.TYPE_RESOLVED;

View File

@ -7,10 +7,9 @@ package org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic.BinaryArithmeticProcessor.BinaryArithmeticOperation;
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;
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
@ -39,10 +38,10 @@ public class Mul extends ArithmeticOperation {
return TypeResolution.TYPE_RESOLVED;
}
if (DataTypes.isInterval(l) && r.isInteger()) {
if (l.isInterval() && r.isInteger()) {
dataType = l;
return TypeResolution.TYPE_RESOLVED;
} else if (DataTypes.isInterval(r) && l.isInteger()) {
} else if (r.isInterval() && l.isInteger()) {
dataType = r;
return TypeResolution.TYPE_RESOLVED;
}

View File

@ -9,7 +9,6 @@ import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic.BinaryArithmeticProcessor.BinaryArithmeticOperation;
import org.elasticsearch.xpack.sql.tree.NodeInfo;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.type.DataTypes;
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
@ -38,7 +37,7 @@ public class Sub extends DateTimeArithmeticOperation {
if (resolution.unresolved()) {
return resolution;
}
if ((right().dataType().isDateOrTimeBased()) && DataTypes.isInterval(left().dataType())) {
if ((right().dataType().isDateOrTimeBased()) && left().dataType().isInterval()) {
return new TypeResolution(format(null, "Cannot subtract a {}[{}] from an interval[{}]; do you mean the reverse?",
right().dataType().typeName, right().source().text(), left().source().text()));
}

View File

@ -271,6 +271,21 @@ public enum DataType {
return isDateBased() || isTimeBased();
}
public boolean isInterval() {
int ordinal = this.ordinal();
return ordinal >= INTERVAL_YEAR.ordinal() && ordinal <= INTERVAL_MINUTE_TO_SECOND.ordinal();
}
public boolean isYearMonthInterval() {
return this == INTERVAL_YEAR || this == INTERVAL_MONTH || this == INTERVAL_YEAR_TO_MONTH;
}
public boolean isDayTimeInterval() {
int ordinal = this.ordinal();
return (ordinal >= INTERVAL_DAY.ordinal() && ordinal <= INTERVAL_SECOND.ordinal())
|| (ordinal >= INTERVAL_DAY_TO_HOUR.ordinal() && ordinal <= INTERVAL_MINUTE_TO_SECOND.ordinal());
}
// data type extract-able from _source or from docvalue_fields
public boolean isFromDocValuesOnly() {
return this == KEYWORD // because of ignore_above. Extracting this from _source wouldn't make sense if it wasn't indexed at all.

View File

@ -87,12 +87,12 @@ public abstract class DataTypeConversion {
// interval and dates
if (left == DATE) {
if (DataTypes.isInterval(right)) {
if (right.isInterval()) {
return left;
}
}
if (right == DATE) {
if (DataTypes.isInterval(left)) {
if (left.isInterval()) {
return right;
}
}
@ -100,7 +100,7 @@ public abstract class DataTypeConversion {
if (right == DATE) {
return DATETIME;
}
if (DataTypes.isInterval(right)) {
if (right.isInterval()) {
return left;
}
}
@ -108,7 +108,7 @@ public abstract class DataTypeConversion {
if (left == DATE) {
return DATETIME;
}
if (DataTypes.isInterval(left)) {
if (left.isInterval()) {
return right;
}
}
@ -116,7 +116,7 @@ public abstract class DataTypeConversion {
if (right == DATE || right == TIME) {
return left;
}
if (DataTypes.isInterval(right)) {
if (right.isInterval()) {
return left;
}
}
@ -124,24 +124,24 @@ public abstract class DataTypeConversion {
if (left == DATE || left == TIME) {
return right;
}
if (DataTypes.isInterval(left)) {
if (left.isInterval()) {
return right;
}
}
// Interval * integer is a valid operation
if (DataTypes.isInterval(left)) {
if (left.isInterval()) {
if (right.isInteger()) {
return left;
}
}
if (DataTypes.isInterval(right)) {
if (right.isInterval()) {
if (left.isInteger()) {
return right;
}
}
if (DataTypes.isInterval(left)) {
if (left.isInterval()) {
// intervals widening
if (DataTypes.isInterval(right)) {
if (right.isInterval()) {
// null returned for incompatible intervals
return DataTypes.compatibleInterval(left, right);
}

View File

@ -18,12 +18,6 @@ import static org.elasticsearch.xpack.sql.type.DataType.DATETIME;
import static org.elasticsearch.xpack.sql.type.DataType.DOUBLE;
import static org.elasticsearch.xpack.sql.type.DataType.FLOAT;
import static org.elasticsearch.xpack.sql.type.DataType.INTEGER;
import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_DAY;
import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_DAY_TO_HOUR;
import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_MINUTE_TO_SECOND;
import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_MONTH;
import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_SECOND;
import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_YEAR;
import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_YEAR_TO_MONTH;
import static org.elasticsearch.xpack.sql.type.DataType.KEYWORD;
import static org.elasticsearch.xpack.sql.type.DataType.LONG;
@ -88,18 +82,6 @@ public final class DataTypes {
throw new SqlIllegalArgumentException("No idea what's the DataType for {}", value.getClass());
}
//
// Interval utilities
//
// some of the methods below could have used an EnumSet however isDayTime would have required a large initialization block
// for this reason, these use the ordinal directly (and thus avoid the type check in EnumSet)
public static boolean isInterval(DataType type) {
int ordinal = type.ordinal();
return ordinal >= INTERVAL_YEAR.ordinal() && ordinal <= INTERVAL_MINUTE_TO_SECOND.ordinal();
}
// return the compatible interval between the two - it is assumed the types are intervals
// YEAR and MONTH -> YEAR_TO_MONTH
// DAY... SECOND -> DAY_TIME
@ -108,11 +90,11 @@ public final class DataTypes {
if (left == right) {
return left;
}
if (isYearMonthInterval(left) && isYearMonthInterval(right)) {
if (left.isYearMonthInterval() && right.isYearMonthInterval()) {
// no need to look at YEAR/YEAR or MONTH/MONTH as these are equal and already handled
return INTERVAL_YEAR_TO_MONTH;
}
if (isDayTimeInterval(left) && isDayTimeInterval(right)) {
if (left.isDayTimeInterval() && right.isDayTimeInterval()) {
// to avoid specifying the combinations, extract the leading and trailing unit from the name
// D > H > S > M which is also the alphabetical order
String lName = left.name().substring(9);
@ -141,16 +123,6 @@ public final class DataTypes {
return null;
}
private static boolean isYearMonthInterval(DataType type) {
return type == INTERVAL_YEAR || type == INTERVAL_MONTH || type == INTERVAL_YEAR_TO_MONTH;
}
private static boolean isDayTimeInterval(DataType type) {
int ordinal = type.ordinal();
return (ordinal >= INTERVAL_DAY.ordinal() && ordinal <= INTERVAL_SECOND.ordinal())
|| (ordinal >= INTERVAL_DAY_TO_HOUR.ordinal() && ordinal <= INTERVAL_MINUTE_TO_SECOND.ordinal());
}
private static String intervalUnit(char unitChar) {
switch (unitChar) {
case 'D':
@ -240,9 +212,11 @@ public final class DataTypes {
return true;
} else {
return
(left == DataType.NULL || right == DataType.NULL) ||
(left.isString() && right.isString()) ||
(left.isNumeric() && right.isNumeric());
(left == DataType.NULL || right == DataType.NULL)
|| (left.isString() && right.isString())
|| (left.isNumeric() && right.isNumeric())
|| (left.isDateBased() && right.isDateBased())
|| (left.isInterval() && right.isInterval() && compatibleInterval(left, right) != null);
}
}
}

View File

@ -684,6 +684,6 @@ public class DataTypeConversionTests extends ESTestCase {
}
private DataType randomInterval() {
return randomFrom(Stream.of(DataType.values()).filter(DataTypes::isInterval).collect(Collectors.toList()));
return randomFrom(Stream.of(DataType.values()).filter(DataType::isInterval).collect(Collectors.toList()));
}
}

View File

@ -32,7 +32,6 @@ import static org.elasticsearch.xpack.sql.type.DataType.INTERVAL_YEAR_TO_MONTH;
import static org.elasticsearch.xpack.sql.type.DataType.KEYWORD;
import static org.elasticsearch.xpack.sql.type.DataType.LONG;
import static org.elasticsearch.xpack.sql.type.DataTypes.compatibleInterval;
import static org.elasticsearch.xpack.sql.type.DataTypes.isInterval;
import static org.elasticsearch.xpack.sql.type.DataTypes.metaSqlDataType;
import static org.elasticsearch.xpack.sql.type.DataTypes.metaSqlDateTimeSub;
import static org.elasticsearch.xpack.sql.type.DataTypes.metaSqlMaximumScale;
@ -77,7 +76,7 @@ public class DataTypesTests extends ESTestCase {
// type checks
public void testIsInterval() throws Exception {
for (DataType dataType : EnumSet.range(INTERVAL_YEAR, INTERVAL_MINUTE_TO_SECOND)) {
assertTrue(isInterval(dataType));
assertTrue(dataType.isInterval());
}
}