SQL: Introduce SQL TIME data type ()

Support ANSI SQL's TIME type by introductin a runtime-only
ES SQL time type.

Closes: 
(cherry picked from commit 046ccd4cf0a251b2a3ddff6b072ab539a6711900)
This commit is contained in:
Marios Trivyzas 2019-04-01 23:30:39 +02:00
parent 12bf3b2025
commit 899ed2bf81
55 changed files with 1116 additions and 251 deletions
docs/reference/sql
x-pack/plugin/sql
jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc
qa/src/main
sql-proto/src/main/java/org/elasticsearch/xpack/sql/proto
src

@ -80,3 +80,7 @@ When the histogram in SQL is applied on **DATE** type instead of **DATETIME**, t
the multiple of a day. E.g.: for `HISTOGRAM(CAST(birth_date AS DATE), INTERVAL '2 3:04' DAY TO MINUTE)` the interval
actually used will be `INTERVAL '2' DAY`. If the interval specified is less than 1 day, e.g.:
`HISTOGRAM(CAST(birth_date AS DATE), INTERVAL '20' HOUR)` then the interval used will be `INTERVAL '1' DAY`.
[IMPORTANT]
Histogram in SQL cannot be applied applied on **TIME** type.
E.g.: `HISTOGRAM(CAST(birth_date AS TIME), INTERVAL '10' MINUTES)` is currently not supported.

@ -44,9 +44,10 @@ s|SQL precision
Most of {es} <<mapping-types, data types>> are available in {es-sql}, as indicated above.
As one can see, all of {es} <<mapping-types, data types>> are mapped to the data type with the same
name in {es-sql}, with the exception of **date** data type which is mapped to **datetime** in {es-sql}.
This is to avoid confusion with the ANSI SQL **DATE** (date only) type, which is also supported by {es-sql}
in queries (with the use of <<sql-functions-type-conversion-cast>>/<<sql-functions-type-conversion-convert>>),
but doesn't correspond to an actual mapping in {es} (see the <<es-sql-only-types, `table`>> below).
This is to avoid confusion with the ANSI SQL types **DATE** (date only) and **TIME** (time only), which are also
supported by {es-sql} in queries (with the use of
<<sql-functions-type-conversion-cast>>/<<sql-functions-type-conversion-convert>>), but don't correspond to an
actual mapping in {es} (see the <<es-sql-only-types, `table`>> below).
Obviously, not all types in {es} have an equivalent in SQL and vice-versa hence why, {es-sql}
uses the data type _particularities_ of the former over the latter as ultimately {es} is the backing store.
@ -66,6 +67,7 @@ s|SQL precision
| date | 24
| time | 18
| interval_year | 7
| interval_month | 7
| interval_day | 23

@ -113,3 +113,28 @@ FROM (SELECT ...) WHERE [simple_condition]`, this is currently **un-supported**.
Using `FIRST` and `LAST` in the `HAVING` clause is not supported. The same applies to
<<sql-functions-aggs-min,`MIN`>> and <<sql-functions-aggs-max,`MAX`>> when their target column
is of type <<keyword, `keyword`>> as they are internally translated to `FIRST` and `LAST`.
[float]
=== Using TIME data type in GROUP BY or <<sql-functions-grouping-histogram>>
Using `TIME` data type as a grouping key is currently not supported. For example:
[source, sql]
-------------------------------------------------------------
SELECT count(*) FROM test GROUP BY CAST(date_created AS TIME);
-------------------------------------------------------------
On the other hand, it can still be used if it's wrapped with a scalar function that returns another data type,
for example:
[source, sql]
-------------------------------------------------------------
SELECT count(*) FROM test GROUP BY MINUTE((CAST(date_created AS TIME));
-------------------------------------------------------------
`TIME` data type is also currently not supported in histogram grouping function. For example:
[source, sql]
-------------------------------------------------------------
SELECT HISTOGRAM(CAST(birth_date AS TIME), INTERVAL '10' MINUTES) as h, COUNT(*) FROM t GROUP BY h
-------------------------------------------------------------

@ -29,6 +29,7 @@ public enum EsType implements SQLType {
NESTED( Types.STRUCT),
BINARY( Types.VARBINARY),
DATE( Types.DATE),
TIME( Types.TIME),
DATETIME( Types.TIMESTAMP),
IP( Types.VARCHAR),
INTERVAL_YEAR( ExtraTypes.INTERVAL_YEAR),

@ -10,17 +10,12 @@ import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Locale;
import java.util.function.Function;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
import static org.elasticsearch.xpack.sql.proto.StringUtils.ISO_DATE_WITH_MILLIS;
import static org.elasticsearch.xpack.sql.proto.StringUtils.ISO_TIME_WITH_MILLIS;
/**
* JDBC specific datetime specific utility methods. Because of lack of visibility, this class borrows code
@ -30,29 +25,21 @@ final class JdbcDateUtils {
private JdbcDateUtils() {}
// In Java 8 LocalDate.EPOCH is not available, introduced with later Java versions
private static final LocalDate EPOCH = LocalDate.of(1970, 1, 1);
static final DateTimeFormatter ISO_WITH_MILLIS = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(ISO_LOCAL_DATE)
.appendLiteral('T')
.appendValue(HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(MINUTE_OF_HOUR, 2)
.appendLiteral(':')
.appendValue(SECOND_OF_MINUTE, 2)
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
.appendOffsetId()
.toFormatter(Locale.ROOT);
private static ZonedDateTime asDateTime(String date) {
return ISO_WITH_MILLIS.parse(date, ZonedDateTime::from);
return ISO_DATE_WITH_MILLIS.parse(date, ZonedDateTime::from);
}
static long asMillisSinceEpoch(String date) {
static long dateTimeAsMillisSinceEpoch(String date) {
return asDateTime(date).toInstant().toEpochMilli();
}
static long timeAsMillisSinceEpoch(String date) {
return ISO_TIME_WITH_MILLIS.parse(date, OffsetTime::from).atDate(EPOCH).toInstant().toEpochMilli();
}
static Date asDate(String date) {
ZonedDateTime zdt = asDateTime(date);
return new Date(zdt.toLocalDate().atStartOfDay(zdt.getZone()).toInstant().toEpochMilli());
@ -63,14 +50,22 @@ final class JdbcDateUtils {
return new Time(zdt.toLocalTime().atDate(EPOCH).atZone(zdt.getZone()).toInstant().toEpochMilli());
}
static Time timeAsTime(String date) {
OffsetTime ot = ISO_TIME_WITH_MILLIS.parse(date, OffsetTime::from);
return new Time(ot.atDate(EPOCH).toInstant().toEpochMilli());
}
static Timestamp asTimestamp(long millisSinceEpoch) {
return new Timestamp(millisSinceEpoch);
}
static Timestamp asTimestamp(String date) {
return new Timestamp(asMillisSinceEpoch(date));
return new Timestamp(dateTimeAsMillisSinceEpoch(date));
}
static Timestamp timeAsTimestamp(String date) {
return new Timestamp(timeAsMillisSinceEpoch(date));
}
/*
* Handles the value received as parameter, as either String (a ZonedDateTime formatted in ISO 8601 standard with millis) -
* date fields being returned formatted like this. Or a Long value, in case of Histograms.

@ -33,8 +33,15 @@ import java.util.Map;
import java.util.function.Function;
import static java.lang.String.format;
import static org.elasticsearch.xpack.sql.jdbc.EsType.DATE;
import static org.elasticsearch.xpack.sql.jdbc.EsType.DATETIME;
import static org.elasticsearch.xpack.sql.jdbc.EsType.TIME;
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.asDateTimeField;
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.asMillisSinceEpoch;
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.dateTimeAsMillisSinceEpoch;
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.asTimestamp;
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.timeAsMillisSinceEpoch;
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.timeAsTime;
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.timeAsTimestamp;
class JdbcResultSet implements ResultSet, JdbcWrapper {
@ -251,17 +258,20 @@ class JdbcResultSet implements ResultSet, JdbcWrapper {
// TODO: the B6 appendix of the jdbc spec does mention CHAR, VARCHAR, LONGVARCHAR, DATE, TIMESTAMP as supported
// jdbc types that should be handled by getDate and getTime methods. From all of those we support VARCHAR and
// TIMESTAMP. Should we consider the VARCHAR conversion as a later enhancement?
if (EsType.DATETIME == type) {
if (DATETIME == type) {
// the cursor can return an Integer if the date-since-epoch is small enough, XContentParser (Jackson) will
// return the "smallest" data type for numbers when parsing
// TODO: this should probably be handled server side
if (val == null) {
return null;
}
return asDateTimeField(val, JdbcDateUtils::asMillisSinceEpoch, Function.identity());
return asDateTimeField(val, JdbcDateUtils::dateTimeAsMillisSinceEpoch, Function.identity());
}
if (EsType.DATE == type) {
return asMillisSinceEpoch(val.toString());
if (DATE == type) {
return dateTimeAsMillisSinceEpoch(val.toString());
}
if (TIME == type) {
return timeAsMillisSinceEpoch(val.toString());
}
return val == null ? null : (Long) val;
} catch (ClassCastException cce) {
@ -277,10 +287,15 @@ class JdbcResultSet implements ResultSet, JdbcWrapper {
return null;
}
EsType type = columnType(columnIndex);
if (type == TIME) {
return new Date(0L);
}
try {
return JdbcDateUtils.asDate(val.toString());
} catch (Exception e) {
EsType type = columnType(columnIndex);
throw new SQLException(
format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Date", val, type.getName()), e);
}
@ -294,11 +309,14 @@ class JdbcResultSet implements ResultSet, JdbcWrapper {
}
EsType type = columnType(columnIndex);
if (type == EsType.DATE) {
if (type == DATE) {
return new Time(0L);
}
try {
if (type == TIME) {
return timeAsTime(val.toString());
}
return JdbcDateUtils.asTime(val.toString());
} catch (Exception e) {
throw new SQLException(
@ -313,13 +331,16 @@ class JdbcResultSet implements ResultSet, JdbcWrapper {
return null;
}
EsType type = columnType(columnIndex);
try {
if (val instanceof Number) {
return JdbcDateUtils.asTimestamp(((Number) val).longValue());
return asTimestamp(((Number) val).longValue());
}
return JdbcDateUtils.asTimestamp(val.toString());
if (type == TIME) {
return timeAsTimestamp(val.toString());
}
return asTimestamp(val.toString());
} catch (Exception e) {
EsType type = columnType(columnIndex);
throw new SQLException(
format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Timestamp", val, type.getName()), e);
}
@ -342,7 +363,7 @@ class JdbcResultSet implements ResultSet, JdbcWrapper {
@Override
public Time getTime(int columnIndex, Calendar cal) throws SQLException {
EsType type = columnType(columnIndex);
if (type == EsType.DATE) {
if (type == DATE) {
return new Time(0L);
}
return TypeConverter.convertTime(dateTimeAsMillis(columnIndex), safeCalendar(cal));

@ -35,6 +35,10 @@ import static java.util.Calendar.MINUTE;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.SECOND;
import static java.util.Calendar.YEAR;
import static org.elasticsearch.xpack.sql.jdbc.EsType.DATE;
import static org.elasticsearch.xpack.sql.jdbc.EsType.DATETIME;
import static org.elasticsearch.xpack.sql.jdbc.EsType.TIME;
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.asDateTimeField;
/**
* Conversion utilities for conversion of JDBC types to Java type and back
@ -214,9 +218,11 @@ final class TypeConverter {
case FLOAT:
return floatValue(v); // Float might be represented as string for infinity and NaN values
case DATE:
return JdbcDateUtils.asDateTimeField(v, JdbcDateUtils::asDate, Date::new);
return asDateTimeField(v, JdbcDateUtils::asDate, Date::new);
case TIME:
return asDateTimeField(v, JdbcDateUtils::asTime, Time::new);
case DATETIME:
return JdbcDateUtils.asDateTimeField(v, JdbcDateUtils::asTimestamp, Timestamp::new);
return asDateTimeField(v, JdbcDateUtils::asTimestamp, Timestamp::new);
case INTERVAL_YEAR:
case INTERVAL_MONTH:
case INTERVAL_YEAR_TO_MONTH:
@ -471,25 +477,34 @@ final class TypeConverter {
}
private static Date asDate(Object val, EsType columnType, String typeString) throws SQLException {
if (columnType == EsType.DATETIME || columnType == EsType.DATE) {
return JdbcDateUtils.asDateTimeField(val, JdbcDateUtils::asDate, Date::new);
if (columnType == DATETIME || columnType == DATE) {
return asDateTimeField(val, JdbcDateUtils::asDate, Date::new);
}
if (columnType == TIME) {
return new Date(0L);
}
return failConversion(val, columnType, typeString, Date.class);
}
private static Time asTime(Object val, EsType columnType, String typeString) throws SQLException {
if (columnType == EsType.DATETIME) {
return JdbcDateUtils.asDateTimeField(val, JdbcDateUtils::asTime, Time::new);
if (columnType == DATETIME) {
return asDateTimeField(val, JdbcDateUtils::asTime, Time::new);
}
if (columnType == EsType.DATE) {
if (columnType == TIME) {
return asDateTimeField(val, JdbcDateUtils::timeAsTime, Time::new);
}
if (columnType == DATE) {
return new Time(0L);
}
return failConversion(val, columnType, typeString, Time.class);
}
private static Timestamp asTimestamp(Object val, EsType columnType, String typeString) throws SQLException {
if (columnType == EsType.DATETIME || columnType == EsType.DATE) {
return JdbcDateUtils.asDateTimeField(val, JdbcDateUtils::asTimestamp, Timestamp::new);
if (columnType == DATETIME || columnType == DATE) {
return asDateTimeField(val, JdbcDateUtils::asTimestamp, Timestamp::new);
}
if (columnType == TIME) {
return asDateTimeField(val, JdbcDateUtils::timeAsTimestamp, Timestamp::new);
}
return failConversion(val, columnType, typeString, Timestamp.class);
}

@ -72,6 +72,18 @@ public abstract class SqlProtocolTestCase extends ESRestTestCase {
"datetime", "1119-01-15T12:37:29.000Z", 24);
assertQuery("SELECT CAST(CAST('-26853765751000' AS BIGINT) AS DATETIME)", "CAST(CAST('-26853765751000' AS BIGINT) AS DATETIME)",
"datetime", "1119-01-15T12:37:29.000Z", 24);
assertQuery("SELECT CAST('2019-01-14' AS DATE)", "CAST('2019-01-14' AS DATE)",
"date", "2019-01-14T00:00:00.000Z", 24);
assertQuery("SELECT CAST(-26853765751000 AS DATE)", "CAST(-26853765751000 AS DATE)",
"date", "1119-01-15T00:00:00.000Z", 24);
assertQuery("SELECT CAST('12:29:25.123Z' AS TIME)", "CAST('12:29:25.123Z' AS TIME)",
"time", "12:29:25.123Z", 18);
assertQuery("SELECT CAST('12:29:25.123456789+05:00' AS TIME)", "CAST('12:29:25.123456789+05:00' AS TIME)",
"time", "12:29:25.123+05:00", 18);
assertQuery("SELECT CAST(-26853765751000 AS TIME)", "CAST(-26853765751000 AS TIME)",
"time", "12:37:29.000Z", 18);
}
public void testIPs() throws IOException {

@ -6,7 +6,6 @@
package org.elasticsearch.xpack.sql.qa.jdbc;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.xpack.sql.qa.jdbc.CsvTestUtils.CsvTestCase;

@ -7,7 +7,6 @@
package org.elasticsearch.xpack.sql.qa.jdbc;
import com.carrotsearch.hppc.IntObjectHashMap;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.xpack.sql.jdbc.EsType;
import org.elasticsearch.xpack.sql.proto.StringUtils;
@ -208,6 +207,9 @@ public class JdbcAssert {
case "Date":
columnClassName = "java.sql.Date";
break;
case "Time":
columnClassName = "java.sql.Time";
break;
case "Timestamp":
columnClassName = "java.sql.Timestamp";
break;

@ -1151,6 +1151,35 @@ public class ResultSetTestCase extends JdbcIntegrationTestCase {
assertEquals(expectedTimestamp, results.getObject("date", java.sql.Timestamp.class));
});
}
public void testGetTimeType() throws Exception {
createIndex("test");
updateMapping("test", builder -> builder.startObject("test_date").field("type", "date").endObject());
// 2018-03-12 17:20:30.123 UTC
Long timeInMillis = 1520875230123L;
index("test", "1", builder -> builder.field("test_date", timeInMillis));
// UTC +10 hours
String timeZoneId1 = "Etc/GMT-10";
doWithQueryAndTimezone("SELECT CAST(test_date AS TIME) as time FROM test", timeZoneId1, results -> {
results.next();
java.sql.Date expectedDate = new java.sql.Date(0L);
assertEquals(expectedDate, results.getDate("time"));
assertEquals(expectedDate, results.getObject("time", java.sql.Date.class));
java.sql.Time expectedTime = asTime(timeInMillis, ZoneId.of("Etc/GMT-10"));
assertEquals(expectedTime, results.getTime("time"));
assertEquals(expectedTime, results.getObject("time", java.sql.Time.class));
java.sql.Timestamp expectedTimestamp = new java.sql.Timestamp(expectedTime.getTime());
assertEquals(expectedTimestamp, results.getTimestamp("time"));
assertEquals(expectedTimestamp, results.getObject("time", java.sql.Timestamp.class));
});
}
public void testValidGetObjectCalls() throws Exception {
createIndex("test");
updateMappingForNumericValuesTests("test");

@ -0,0 +1,83 @@
//
// TIME
//
timeExtractTimeParts
SELECT
SECOND(CAST(birth_date AS TIME)) d,
MINUTE(CAST(birth_date AS TIME)) m,
HOUR(CAST(birth_date AS TIME)) h
FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no;
d:i | m:i | h:i
0 |0 |0
0 |0 |0
0 |0 |0
0 |0 |0
0 |0 |0
0 |0 |0
0 |0 |0
0 |0 |0
0 |0 |0
;
timeAsFilter
SELECT birth_date, last_name FROM "test_emp" WHERE birth_date::TIME = CAST('00:00:00' AS TIME) ORDER BY emp_no LIMIT 5;
birth_date:ts | last_name:s
1953-09-02 00:00:00Z | Facello
1964-06-02 00:00:00Z | Simmel
1959-12-03 00:00:00Z | Bamford
1954-05-01 00:00:00Z | Koblick
1955-01-21 00:00:00Z | Maliniak
;
timeAsFilter_NoMatch
SELECT count(*) FROM "test_emp" WHERE birth_date::TIME = CAST('12:34:56.789' AS TIME);
count(*):l
0
;
timeAsOrderBy
SELECT last_name FROM "test_emp" ORDER BY birth_date::TIME, emp_no LIMIT 5;
last_name:s
Meriste
Lenart
Stamatiou
Tzvieli
Casley
;
timeAndFunctionAsGroupingKey
SELECT HOUR(CAST(birth_date AS TIME)) AS m, CAST(SUM(emp_no) AS INT) s FROM test_emp GROUP BY m ORDER BY m LIMIT 5;
m:i | s:i
null |100445
0 |904605
;
timeAsHavingFilter
SELECT MINUTE_OF_HOUR(MAX(birth_date)::TIME + INTERVAL 10 MINUTES) as minute, gender FROM test_emp GROUP BY gender HAVING CAST(MAX(birth_date) AS TIME) = CAST('00:00:00.000' AS TIME) ORDER BY gender;
minute:i | gender:s
10 | null
10 | F
10 | M
;
timeAsHavingFilterNoMatch
SELECT MINUTE_OF_HOUR(MAX(birth_date)::TIME) as minute, gender FROM test_emp GROUP BY gender HAVING CAST(MAX(birth_date) AS TIME) > CAST('00:00:00.000' AS TIME);
minute:i | gender:s
;
timeAndInterval
SELECT HOUR(CAST('10:11:12.345' AS TIME) + INTERVAL '20' HOURS) AS h, SECOND(INTERVAL '40' SECONDS + CAST('10:11:12.345' AS TIME)) AS m;
h:i | m:i
6 | 52
;

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.sql.proto;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.OffsetTime;
import java.time.Period;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@ -26,7 +27,7 @@ public final class StringUtils {
public static final String EMPTY = "";
private static final DateTimeFormatter ISO_WITH_MILLIS = new DateTimeFormatterBuilder()
public static final DateTimeFormatter ISO_DATE_WITH_MILLIS = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(ISO_LOCAL_DATE)
.appendLiteral('T')
@ -39,6 +40,17 @@ public final class StringUtils {
.appendOffsetId()
.toFormatter(Locale.ROOT);
public static final DateTimeFormatter ISO_TIME_WITH_MILLIS = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendValue(HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(MINUTE_OF_HOUR, 2)
.appendLiteral(':')
.appendValue(SECOND_OF_MINUTE, 2)
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
.appendOffsetId()
.toFormatter(Locale.ROOT);
private static final int SECONDS_PER_MINUTE = 60;
private static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60;
private static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
@ -49,16 +61,18 @@ public final class StringUtils {
if (value == null) {
return "null";
}
if (value instanceof ZonedDateTime) {
return ((ZonedDateTime) value).format(ISO_DATE_WITH_MILLIS);
}
if (value instanceof OffsetTime) {
return ((OffsetTime) value).format(ISO_TIME_WITH_MILLIS);
}
if (value instanceof Timestamp) {
Timestamp ts = (Timestamp) value;
return ts.toInstant().toString();
}
if (value instanceof ZonedDateTime) {
return ((ZonedDateTime) value).format(ISO_WITH_MILLIS);
}
// handle intervals
// YEAR/MONTH/YEAR TO MONTH -> YEAR TO MONTH
if (value instanceof Period) {
@ -112,4 +126,4 @@ public final class StringUtils {
private static String indent(long timeUnit) {
return timeUnit < 10 ? "0" + timeUnit : Long.toString(timeUnit);
}
}
}

@ -298,7 +298,8 @@ public final class Verifier {
return checkGroupByInexactField(p, localFailures)
&& checkGroupByAgg(p, localFailures, resolvedFunctions)
&& checkGroupByOrder(p, localFailures, groupingFailures)
&& checkGroupByHaving(p, localFailures, groupingFailures, resolvedFunctions);
&& checkGroupByHaving(p, localFailures, groupingFailures, resolvedFunctions)
&& checkGroupByTime(p, localFailures);
}
// check whether an orderBy failed or if it occurs on a non-key
@ -473,14 +474,30 @@ public final class Verifier {
a.groupings().forEach(e -> e.forEachUp(c -> {
EsField.Exact exact = c.getExactInfo();
if (exact.hasExact() == false) {
localFailures.add(fail(c, "Field of data type [" + c.dataType().typeName + "] cannot be used for grouping; " +
exact.errorMsg()));
localFailures.add(fail(c, "Field [" + c.sourceText() + "] of data type [" + c.dataType().typeName + "] " +
"cannot be used for grouping; " + exact.errorMsg()));
}
}, FieldAttribute.class));
}
return true;
}
private static boolean checkGroupByTime(LogicalPlan p, Set<Failure> localFailures) {
if (p instanceof Aggregate) {
Aggregate a = (Aggregate) p;
// TIME data type is not allowed for grouping key
// https://github.com/elastic/elasticsearch/issues/40639
a.groupings().forEach(f -> {
if (f.dataType().isTimeBased()) {
localFailures.add(fail(f, "Function [" + f.sourceText() + "] with data type [" + f.dataType().typeName +
"] " + "cannot be used for grouping"));
}
});
}
return true;
}
// check whether plain columns specified in an agg are mentioned in the group-by
private static boolean checkGroupByAgg(LogicalPlan p, Set<Failure> localFailures, Map<String, Function> functions) {
if (p instanceof Aggregate) {

@ -75,6 +75,8 @@ public class TopHitsAggExtractor implements BucketExtractor {
Object value = agg.getHits().getAt(0).getFields().values().iterator().next().getValue();
if (fieldDataType.isDateBased()) {
return DateUtils.asDateTime(Long.parseLong(value.toString()), zoneId);
} else if (fieldDataType.isTimeBased()) {
return DateUtils.asTimeOnly(Long.parseLong(value.toString()), zoneId);
} else {
return value;
}

@ -43,8 +43,18 @@ public final class TypeResolutions {
return isType(e, DataType::isDateBased, operationName, paramOrd, "date", "datetime");
}
public static TypeResolution isDateOrTime(Expression e, String operationName, ParamOrdinal paramOrd) {
return isType(e, DataType::isDateOrTimeBased, operationName, paramOrd, "date", "time", "datetime");
}
public static TypeResolution isNumericOrDate(Expression e, String operationName, ParamOrdinal paramOrd) {
return isType(e, dt -> dt.isNumeric() || dt.isDateBased(), operationName, paramOrd, "date", "datetime", "numeric");
return isType(e, dt -> dt.isNumeric() || dt.isDateBased(), operationName, paramOrd,
"date", "datetime", "numeric");
}
public static TypeResolution isNumericOrDateOrTime(Expression e, String operationName, ParamOrdinal paramOrd) {
return isType(e, dt -> dt.isNumeric() || dt.isDateOrTimeBased(), operationName, paramOrd,
"date", "time", "datetime", "numeric");
}
public static TypeResolution isExact(Expression e, String message) {

@ -14,7 +14,7 @@ import org.elasticsearch.xpack.sql.type.DataType;
import java.util.List;
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isExact;
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isNumericOrDate;
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isNumericOrDateOrTime;
/**
* Find the maximum value in matching documents.
@ -50,7 +50,7 @@ public class Max extends NumericAggregate implements EnclosedAgg {
if (field().dataType().isString()) {
return isExact(field(), sourceText(), ParamOrdinal.DEFAULT);
} else {
return isNumericOrDate(field(), sourceText(), ParamOrdinal.DEFAULT);
return isNumericOrDateOrTime(field(), sourceText(), ParamOrdinal.DEFAULT);
}
}
}

@ -14,7 +14,7 @@ import org.elasticsearch.xpack.sql.type.DataType;
import java.util.List;
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isExact;
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isNumericOrDate;
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isNumericOrDateOrTime;
/**
* Find the minimum value in matched documents.
@ -53,7 +53,7 @@ public class Min extends NumericAggregate implements EnclosedAgg {
if (field().dataType().isString()) {
return isExact(field(), sourceText(), ParamOrdinal.DEFAULT);
} else {
return isNumericOrDate(field(), sourceText(), ParamOrdinal.DEFAULT);
return isNumericOrDateOrTime(field(), sourceText(), ParamOrdinal.DEFAULT);
}
}
}

@ -11,6 +11,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeP
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NonIsoDateTimeProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.TimeProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryOptionalMathProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor;
@ -78,6 +79,7 @@ public final class Processors {
// datetime
entries.add(new Entry(Processor.class, DateTimeProcessor.NAME, DateTimeProcessor::new));
entries.add(new Entry(Processor.class, TimeProcessor.NAME, TimeProcessor::new));
entries.add(new Entry(Processor.class, NamedDateTimeProcessor.NAME, NamedDateTimeProcessor::new));
entries.add(new Entry(Processor.class, NonIsoDateTimeProcessor.NAME, NonIsoDateTimeProcessor::new));
entries.add(new Entry(Processor.class, QuarterProcessor.NAME, QuarterProcessor::new));

@ -13,13 +13,12 @@ import org.elasticsearch.xpack.sql.tree.NodeInfo;
import org.elasticsearch.xpack.sql.tree.Source;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Objects;
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isDate;
abstract class BaseDateTimeFunction extends UnaryScalarFunction {
private final ZoneId zoneId;
BaseDateTimeFunction(Source source, Expression field, ZoneId zoneId) {
@ -50,17 +49,9 @@ abstract class BaseDateTimeFunction extends UnaryScalarFunction {
@Override
public Object fold() {
ZonedDateTime folded = (ZonedDateTime) field().fold();
if (folded == null) {
return null;
}
return doFold(folded.withZoneSameInstant(zoneId));
return makeProcessor().process(field().fold());
}
protected abstract Object doFold(ZonedDateTime dateTime);
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
@ -68,7 +59,7 @@ abstract class BaseDateTimeFunction extends UnaryScalarFunction {
}
BaseDateTimeFunction other = (BaseDateTimeFunction) obj;
return Objects.equals(other.field(), field())
&& Objects.equals(other.zoneId(), zoneId());
&& Objects.equals(other.zoneId(), zoneId());
}
@Override

@ -18,11 +18,11 @@ import java.time.ZonedDateTime;
public abstract class BaseDateTimeProcessor implements Processor {
private final ZoneId zoneId;
BaseDateTimeProcessor(ZoneId zoneId) {
this.zoneId = zoneId;
}
BaseDateTimeProcessor(StreamInput in) throws IOException {
zoneId = ZoneId.of(in.readString());
}
@ -31,7 +31,7 @@ public abstract class BaseDateTimeProcessor implements Processor {
public void writeTo(StreamOutput out) throws IOException {
out.writeString(zoneId.getId());
}
ZoneId zoneId() {
return zoneId;
}
@ -43,11 +43,11 @@ public abstract class BaseDateTimeProcessor implements Processor {
}
if (!(input instanceof ZonedDateTime)) {
throw new SqlIllegalArgumentException("A date is required; received {}", input);
throw new SqlIllegalArgumentException("A [date], a [time] or a [datetime] is required; received {}", input);
}
return doProcess(((ZonedDateTime) input).withZoneSameInstant(zoneId));
}
abstract Object doProcess(ZonedDateTime dateTime);
}
}

@ -16,6 +16,7 @@ import org.elasticsearch.xpack.sql.type.DataType;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.Temporal;
import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder;
@ -28,17 +29,12 @@ public abstract class DateTimeFunction extends BaseDateTimeFunction {
this.extractor = extractor;
}
@Override
protected Object doFold(ZonedDateTime dateTime) {
return dateTimeChrono(dateTime, extractor.chronoField());
}
public static Integer dateTimeChrono(ZonedDateTime dateTime, String tzId, String chronoName) {
ZonedDateTime zdt = dateTime.withZoneSameInstant(ZoneId.of(tzId));
return dateTimeChrono(zdt, ChronoField.valueOf(chronoName));
}
private static Integer dateTimeChrono(ZonedDateTime dateTime, ChronoField field) {
protected static Integer dateTimeChrono(Temporal dateTime, ChronoField field) {
return Integer.valueOf(dateTime.get(field));
}
@ -68,4 +64,8 @@ public abstract class DateTimeFunction extends BaseDateTimeFunction {
// used for applying ranges
public abstract String dateTimeFormat();
}
protected DateTimeExtractor extractor() {
return extractor;
}
}

@ -9,6 +9,7 @@ import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
@ -38,6 +39,10 @@ public class DateTimeProcessor extends BaseDateTimeProcessor {
return dt.get(field);
}
public int extract(OffsetTime time) {
return time.get(field);
}
public ChronoField chronoField() {
return field;
}
@ -95,4 +100,4 @@ public class DateTimeProcessor extends BaseDateTimeProcessor {
public String toString() {
return extractor.toString();
}
}
}

@ -7,15 +7,15 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor.DateTimeExtractor;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.tree.NodeInfo.NodeCtor2;
import org.elasticsearch.xpack.sql.tree.Source;
import java.time.ZoneId;
/**
* Extract the hour of the day from a datetime.
*/
public class HourOfDay extends DateTimeFunction {
public class HourOfDay extends TimeFunction {
public HourOfDay(Source source, Expression field, ZoneId zoneId) {
super(source, field, zoneId, DateTimeExtractor.HOUR_OF_DAY);
}

@ -7,15 +7,15 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor.DateTimeExtractor;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.tree.NodeInfo.NodeCtor2;
import org.elasticsearch.xpack.sql.tree.Source;
import java.time.ZoneId;
/**
* Extract the minute of the day from a datetime.
*/
public class MinuteOfDay extends DateTimeFunction {
public class MinuteOfDay extends TimeFunction {
public MinuteOfDay(Source source, Expression field, ZoneId zoneId) {
super(source, field, zoneId, DateTimeExtractor.MINUTE_OF_DAY);

@ -7,15 +7,15 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor.DateTimeExtractor;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.tree.NodeInfo.NodeCtor2;
import org.elasticsearch.xpack.sql.tree.Source;
import java.time.ZoneId;
/**
* Exract the minute of the hour from a datetime.
*/
public class MinuteOfHour extends DateTimeFunction {
public class MinuteOfHour extends TimeFunction {
public MinuteOfHour(Source source, Expression field, ZoneId zoneId) {
super(source, field, zoneId, DateTimeExtractor.MINUTE_OF_HOUR);
}

@ -15,7 +15,6 @@ import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.util.StringUtils;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Locale;
import static java.lang.String.format;
@ -33,11 +32,6 @@ abstract class NamedDateTimeFunction extends BaseDateTimeFunction {
this.nameExtractor = nameExtractor;
}
@Override
protected Object doFold(ZonedDateTime dateTime) {
return nameExtractor.extract(dateTime);
}
@Override
public ScriptTemplate scriptWithField(FieldAttribute field) {
return new ScriptTemplate(
@ -58,4 +52,4 @@ abstract class NamedDateTimeFunction extends BaseDateTimeFunction {
public DataType dataType() {
return DataType.KEYWORD;
}
}
}

@ -15,7 +15,6 @@ import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.util.StringUtils;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Locale;
import static java.lang.String.format;
@ -33,11 +32,6 @@ abstract class NonIsoDateTimeFunction extends BaseDateTimeFunction {
this.extractor = extractor;
}
@Override
protected Object doFold(ZonedDateTime dateTime) {
return extractor.extract(dateTime);
}
@Override
public ScriptTemplate scriptWithField(FieldAttribute field) {
return new ScriptTemplate(
@ -58,4 +52,4 @@ abstract class NonIsoDateTimeFunction extends BaseDateTimeFunction {
public DataType dataType() {
return DataType.INTEGER;
}
}
}

@ -10,14 +10,12 @@ import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.FieldAttribute;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.tree.NodeInfo.NodeCtor2;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.type.DataType;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor.quarter;
import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder;
public class Quarter extends BaseDateTimeFunction {
@ -26,11 +24,6 @@ public class Quarter extends BaseDateTimeFunction {
super(source, field, zoneId);
}
@Override
protected Object doFold(ZonedDateTime dateTime) {
return quarter(dateTime);
}
@Override
public ScriptTemplate scriptWithField(FieldAttribute field) {
return new ScriptTemplate(formatTemplate("{sql}.quarter(doc[{}].value, {})"),
@ -60,4 +53,4 @@ public class Quarter extends BaseDateTimeFunction {
public DataType dataType() {
return DataType.INTEGER;
}
}
}

@ -7,15 +7,15 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor.DateTimeExtractor;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.tree.NodeInfo.NodeCtor2;
import org.elasticsearch.xpack.sql.tree.Source;
import java.time.ZoneId;
/**
* Extract the second of the minute from a datetime.
*/
public class SecondOfMinute extends DateTimeFunction {
public class SecondOfMinute extends TimeFunction {
public SecondOfMinute(Source source, Expression field, ZoneId zoneId) {
super(source, field, zoneId, DateTimeExtractor.SECOND_OF_MINUTE);
}

@ -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.function.scalar.datetime;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.Expressions;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor.DateTimeExtractor;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;
import org.elasticsearch.xpack.sql.tree.Source;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isDateOrTime;
import static org.elasticsearch.xpack.sql.util.DateUtils.asTimeAtZone;
public abstract class TimeFunction extends DateTimeFunction {
TimeFunction(Source source, Expression field, ZoneId zoneId, DateTimeExtractor extractor) {
super(source, field, zoneId, extractor);
}
public static Integer dateTimeChrono(OffsetTime time, String tzId, String chronoName) {
return dateTimeChrono(asTimeAtZone(time, ZoneId.of(tzId)), ChronoField.valueOf(chronoName));
}
@Override
protected TypeResolution resolveType() {
return isDateOrTime(field(), sourceText(), Expressions.ParamOrdinal.DEFAULT);
}
@Override
protected Processor makeProcessor() {
return new TimeProcessor(extractor(), zoneId());
}
}

@ -0,0 +1,56 @@
/*
* 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.function.scalar.datetime;
import org.elasticsearch.common.io.stream.StreamInput;
import java.io.IOException;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.util.Objects;
import static org.elasticsearch.xpack.sql.util.DateUtils.asTimeAtZone;
public class TimeProcessor extends DateTimeProcessor {
public static final String NAME = "time";
public TimeProcessor(DateTimeExtractor extractor, ZoneId zoneId) {
super(extractor, zoneId);
}
public TimeProcessor(StreamInput in) throws IOException {
super(in);
}
@Override
public Object process(Object input) {
if (input instanceof OffsetTime) {
return doProcess(asTimeAtZone((OffsetTime) input, zoneId()));
}
return super.process(input);
}
private Object doProcess(OffsetTime time) {
return extractor().extract(time);
}
@Override
public int hashCode() {
return Objects.hash(extractor(), zoneId());
}
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
TimeProcessor other = (TimeProcessor) obj;
return Objects.equals(extractor(), other.extractor())
&& Objects.equals(zoneId(), other.zoneId());
}
}

@ -12,6 +12,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeF
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NonIsoDateTimeProcessor.NonIsoDateTimeExtractor;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.TimeFunction;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor.BinaryMathOperation;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryOptionalMathProcessor.BinaryOptionalMathOperation;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor.MathOperation;
@ -41,6 +42,7 @@ import org.elasticsearch.xpack.sql.util.DateUtils;
import org.elasticsearch.xpack.sql.util.StringUtils;
import java.time.Duration;
import java.time.OffsetTime;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.List;
@ -316,6 +318,9 @@ public final class InternalSqlScriptUtils {
if (dateTime == null || tzId == null || chronoName == null) {
return null;
}
if (dateTime instanceof OffsetTime) {
return TimeFunction.dateTimeChrono((OffsetTime) dateTime, tzId, chronoName);
}
return DateTimeFunction.dateTimeChrono(asDateTime(dateTime), tzId, chronoName);
}
@ -396,6 +401,10 @@ public final class InternalSqlScriptUtils {
return new IntervalYearMonth(Period.parse(text), DataType.fromTypeName(typeName));
}
public static OffsetTime asTime(String time) {
return OffsetTime.parse(time);
}
//
// String functions
//

@ -20,6 +20,7 @@ import org.elasticsearch.xpack.sql.expression.literal.IntervalYearMonth;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.util.DateUtils;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder;
@ -80,12 +81,19 @@ public interface ScriptWeaver {
return new ScriptTemplate(processScript("{sql}.intervalYearMonth({},{})"),
paramsBuilder().variable(iym.interval().toString()).variable(iym.dataType().name()).build(),
dataType());
} else if (fold instanceof IntervalDayTime) {
}
if (fold instanceof IntervalDayTime) {
IntervalDayTime idt = (IntervalDayTime) fold;
return new ScriptTemplate(processScript("{sql}.intervalDayTime({},{})"),
paramsBuilder().variable(idt.interval().toString()).variable(idt.dataType().name()).build(),
dataType());
}
if (fold instanceof OffsetTime) {
OffsetTime ot = (OffsetTime) fold;
return new ScriptTemplate(processScript("{sql}.asTime({})"),
paramsBuilder().variable(ot.toString()).build(),
dataType());
}
return new ScriptTemplate(processScript("{}"),
paramsBuilder().variable(fold).build(),

@ -6,14 +6,24 @@
package org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic;
import java.time.Duration;
import java.time.OffsetTime;
import java.time.Period;
import java.time.ZonedDateTime;
import java.time.temporal.Temporal;
import static org.elasticsearch.xpack.sql.util.DateUtils.DAY_IN_MILLIS;
/**
* Arithmetic operation using the type widening rules of the JLS 5.6.2 namely
* widen to double or float or long or int in this order.
*/
public abstract class Arithmetics {
public final class Arithmetics {
private Arithmetics() {}
private enum IntervalOperation {
ADD,
SUB
}
static Number add(Number l, Number r) {
if (l == null || r == null) {
@ -33,20 +43,12 @@ public abstract class Arithmetics {
return Integer.valueOf(Math.addExact(l.intValue(), r.intValue()));
}
static ZonedDateTime add(ZonedDateTime l, Period r) {
if (l == null || r == null) {
return null;
}
return l.plus(r);
static Temporal add(Temporal l, Period r) {
return periodArithmetics(l, r, IntervalOperation.ADD);
}
static ZonedDateTime add(ZonedDateTime l, Duration r) {
if (l == null || r == null) {
return null;
}
return l.plus(r);
static Temporal add(Temporal l, Duration r) {
return durationArithmetics(l, r, IntervalOperation.ADD);
}
static Number sub(Number l, Number r) {
@ -67,20 +69,12 @@ public abstract class Arithmetics {
return Integer.valueOf(Math.subtractExact(l.intValue(), r.intValue()));
}
static ZonedDateTime sub(ZonedDateTime l, Period r) {
if (l == null || r == null) {
return null;
}
return l.minus(r);
static Temporal sub(Temporal l, Period r) {
return periodArithmetics(l, r, IntervalOperation.SUB);
}
static ZonedDateTime sub(ZonedDateTime l, Duration r) {
if (l == null || r == null) {
return null;
}
return l.minus(r);
static Temporal sub(Temporal l, Duration r) {
return durationArithmetics(l, r, IntervalOperation.SUB);
}
static Number mul(Number l, Number r) {
@ -162,4 +156,36 @@ public abstract class Arithmetics {
return Integer.valueOf(Math.negateExact(n.intValue()));
}
private static Temporal periodArithmetics(Temporal l, Period r, IntervalOperation operation) {
if (l == null || r == null) {
return null;
}
if (l instanceof OffsetTime) {
return l;
}
if (operation == IntervalOperation.ADD) {
return l.plus(r);
} else {
return l.minus(r);
}
}
private static Temporal durationArithmetics(Temporal l, Duration r, IntervalOperation operation) {
if (l == null || r == null) {
return null;
}
if (l instanceof OffsetTime) {
r = Duration.ofMillis(r.toMillis() % DAY_IN_MILLIS);
}
if (operation == IntervalOperation.ADD) {
return l.plus(r);
} else {
return l.minus(r);
}
}
}

@ -17,7 +17,9 @@ import org.elasticsearch.xpack.sql.expression.predicate.PredicateBiFunction;
import org.elasticsearch.xpack.sql.expression.predicate.operator.arithmetic.BinaryArithmeticProcessor.BinaryArithmeticOperation;
import java.io.IOException;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.temporal.Temporal;
import java.util.function.BiFunction;
public class BinaryArithmeticProcessor extends FunctionalBinaryProcessor<Object, Object, Object, BinaryArithmeticOperation> {
@ -41,17 +43,17 @@ public class BinaryArithmeticProcessor extends FunctionalBinaryProcessor<Object,
}
l = unwrapJodaTime(l);
r = unwrapJodaTime(r);
if (l instanceof ZonedDateTime && r instanceof IntervalYearMonth) {
return Arithmetics.add((ZonedDateTime) l, ((IntervalYearMonth) r).interval());
if ((l instanceof ZonedDateTime || l instanceof OffsetTime) && r instanceof IntervalYearMonth) {
return Arithmetics.add((Temporal) l, ((IntervalYearMonth) r).interval());
}
if (l instanceof ZonedDateTime && r instanceof IntervalDayTime) {
return Arithmetics.add((ZonedDateTime) l, ((IntervalDayTime) r).interval());
if ((l instanceof ZonedDateTime || l instanceof OffsetTime) && r instanceof IntervalDayTime) {
return Arithmetics.add((Temporal) l, ((IntervalDayTime) r).interval());
}
if (r instanceof ZonedDateTime && l instanceof IntervalYearMonth) {
return Arithmetics.add((ZonedDateTime) r, ((IntervalYearMonth) l).interval());
if ((r instanceof ZonedDateTime || r instanceof OffsetTime) && l instanceof IntervalYearMonth) {
return Arithmetics.add((Temporal) r, ((IntervalYearMonth) l).interval());
}
if (r instanceof ZonedDateTime && l instanceof IntervalDayTime) {
return Arithmetics.add((ZonedDateTime) r, ((IntervalDayTime) l).interval());
if ((r instanceof ZonedDateTime || r instanceof OffsetTime) && l instanceof IntervalDayTime) {
return Arithmetics.add((Temporal) r, ((IntervalDayTime) l).interval());
}
throw new SqlIllegalArgumentException("Cannot compute [+] between [{}] [{}]", l.getClass().getSimpleName(),
@ -69,13 +71,13 @@ public class BinaryArithmeticProcessor extends FunctionalBinaryProcessor<Object,
}
l = unwrapJodaTime(l);
r = unwrapJodaTime(r);
if (l instanceof ZonedDateTime && r instanceof IntervalYearMonth) {
return Arithmetics.sub((ZonedDateTime) l, ((IntervalYearMonth) r).interval());
if ((l instanceof ZonedDateTime || l instanceof OffsetTime) && r instanceof IntervalYearMonth) {
return Arithmetics.sub((Temporal) l, ((IntervalYearMonth) r).interval());
}
if (l instanceof ZonedDateTime && r instanceof IntervalDayTime) {
return Arithmetics.sub((ZonedDateTime) l, ((IntervalDayTime) r).interval());
if ((l instanceof ZonedDateTime || l instanceof OffsetTime) && r instanceof IntervalDayTime) {
return Arithmetics.sub((Temporal) l, ((IntervalDayTime) r).interval());
}
if (r instanceof ZonedDateTime && l instanceof Interval<?>) {
if ((r instanceof ZonedDateTime || r instanceof OffsetTime) && l instanceof Interval<?>) {
throw new SqlIllegalArgumentException("Cannot subtract a date from an interval; do you mean the reverse?");
}
@ -181,4 +183,4 @@ public class BinaryArithmeticProcessor extends FunctionalBinaryProcessor<Object,
// this should not occur
throw new SqlIllegalArgumentException("Cannot perform arithmetic operation due to arguments");
}
}
}

@ -34,7 +34,7 @@ public class Sub extends DateTimeArithmeticOperation {
@Override
protected TypeResolution resolveWithIntervals() {
if (right().dataType().isDateBased() && DataTypes.isInterval(left().dataType())) {
if ((right().dataType().isDateOrTimeBased()) && DataTypes.isInterval(left().dataType())) {
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()));
}

@ -114,7 +114,6 @@ import org.elasticsearch.xpack.sql.type.DataTypes;
import org.elasticsearch.xpack.sql.util.StringUtils;
import java.time.Duration;
import java.time.LocalTime;
import java.time.Period;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAmount;
@ -124,11 +123,11 @@ import java.util.Locale;
import java.util.Map;
import java.util.StringJoiner;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.elasticsearch.xpack.sql.type.DataTypeConversion.conversionFor;
import static org.elasticsearch.xpack.sql.util.DateUtils.asDateOnly;
import static org.elasticsearch.xpack.sql.util.DateUtils.asTimeOnly;
import static org.elasticsearch.xpack.sql.util.DateUtils.ofEscapedLiteral;
abstract class ExpressionBuilder extends IdentifierBuilder {
@ -768,14 +767,11 @@ abstract class ExpressionBuilder extends IdentifierBuilder {
Source source = source(ctx);
// parse HH:mm:ss
LocalTime lt = null;
try {
lt = LocalTime.parse(string, ISO_LOCAL_TIME);
return new Literal(source, asTimeOnly(string), DataType.TIME);
} catch (DateTimeParseException ex) {
throw new ParsingException(source, "Invalid time received; {}", ex.getMessage());
}
throw new SqlIllegalArgumentException("Time (only) literals are not supported; a date component is required as well");
}
@Override

@ -291,7 +291,7 @@ final class QueryTranslator {
if (h.dataType() == DATE) {
intervalAsMillis = DateUtils.minDayInterval(intervalAsMillis);
}
// TODO: set timezone
if (field instanceof FieldAttribute) {
key = new GroupByDateHistogram(aggId, nameOf(field), intervalAsMillis, h.zoneId());
} else if (field instanceof Function) {

@ -41,6 +41,8 @@ public abstract class GroupByKey extends Agg {
builder.valueType(ValueType.STRING);
} else if (script.outputType() == DataType.DATE) {
builder.valueType(ValueType.LONG);
} else if (script.outputType() == DataType.TIME) {
builder.valueType(ValueType.LONG);
} else if (script.outputType() == DataType.DATETIME) {
builder.valueType(ValueType.LONG);
} else if (script.outputType() == DataType.BOOLEAN) {

@ -45,6 +45,7 @@ public enum DataType {
NESTED( "nested", JDBCType.STRUCT, -1, 0, 0, false, false, false),
BINARY( "binary", JDBCType.VARBINARY, -1, Integer.MAX_VALUE, Integer.MAX_VALUE, false, false, false),
DATE( JDBCType.DATE, Long.BYTES, 24, 24, false, false, true),
TIME( JDBCType.TIME, Long.BYTES, 3, 18, false, false, true),
// since ODBC and JDBC interpret precision for Date as display size
// the precision is 23 (number of chars in ISO8601 with millis) + Z (the UTC timezone)
// see https://github.com/elastic/elasticsearch/issues/30386#issuecomment-386807288
@ -104,7 +105,7 @@ public enum DataType {
// Date
ODBC_TO_ES.put("SQL_DATE", DATE);
ODBC_TO_ES.put("SQL_TIME", DATETIME);
ODBC_TO_ES.put("SQL_TIME", TIME);
ODBC_TO_ES.put("SQL_TIMESTAMP", DATETIME);
// Intervals
@ -253,6 +254,14 @@ public enum DataType {
public boolean isDateBased() {
return this == DATE || this == DATETIME;
}
public boolean isTimeBased() {
return this == TIME;
}
public boolean isDateOrTimeBased() {
return isDateBased() || isTimeBased();
}
public static DataType fromOdbcType(String odbcType) {
return ODBC_TO_ES.get(odbcType);
@ -278,6 +287,6 @@ public enum DataType {
}
public String format() {
return isDateBased() ? DateUtils.DATE_PARSE_FORMAT : null;
return isDateOrTimeBased() ? DateUtils.DATE_PARSE_FORMAT : null;
}
}

@ -10,6 +10,7 @@ import org.elasticsearch.common.network.InetAddresses;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.util.DateUtils;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.Locale;
@ -22,6 +23,7 @@ import static org.elasticsearch.xpack.sql.type.DataType.DATE;
import static org.elasticsearch.xpack.sql.type.DataType.DATETIME;
import static org.elasticsearch.xpack.sql.type.DataType.LONG;
import static org.elasticsearch.xpack.sql.type.DataType.NULL;
import static org.elasticsearch.xpack.sql.type.DataType.TIME;
/**
* Conversions from one Elasticsearch data type to another Elasticsearch data types.
@ -87,8 +89,24 @@ public abstract class DataTypeConversion {
return right;
}
}
if (left == DATETIME) {
if (left == TIME) {
if (right == DATE) {
return DATETIME;
}
if (DataTypes.isInterval(right)) {
return left;
}
}
if (right == TIME) {
if (left == DATE) {
return DATETIME;
}
if (DataTypes.isInterval(left)) {
return right;
}
}
if (left == DATETIME) {
if (right == DATE || right == TIME) {
return left;
}
if (DataTypes.isInterval(right)) {
@ -96,7 +114,7 @@ public abstract class DataTypeConversion {
}
}
if (right == DATETIME) {
if (left == DATE) {
if (left == DATE || left == TIME) {
return right;
}
if (DataTypes.isInterval(left)) {
@ -144,7 +162,7 @@ public abstract class DataTypeConversion {
Conversion conversion = conversion(from, to);
if (conversion == null) {
throw new SqlIllegalArgumentException("cannot convert from [" + from + "] to [" + to + "]");
throw new SqlIllegalArgumentException("cannot convert from [" + from.typeName + "] to [" + to.typeName + "]");
}
return conversion;
}
@ -170,6 +188,8 @@ public abstract class DataTypeConversion {
return conversionToDouble(from);
case DATE:
return conversionToDate(from);
case TIME:
return conversionToTime(from);
case DATETIME:
return conversionToDateTime(from);
case BOOLEAN:
@ -184,6 +204,9 @@ public abstract class DataTypeConversion {
if (from == DATE) {
return Conversion.DATE_TO_STRING;
}
if (from == TIME) {
return Conversion.TIME_TO_STRING;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_STRING;
}
@ -213,6 +236,9 @@ public abstract class DataTypeConversion {
if (from == DATE) {
return Conversion.DATE_TO_LONG;
}
if (from == TIME) {
return Conversion.TIME_TO_LONG;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_LONG;
}
@ -235,6 +261,9 @@ public abstract class DataTypeConversion {
if (from == DATE) {
return Conversion.DATE_TO_INT;
}
if (from == TIME) {
return Conversion.TIME_TO_INT;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_INT;
}
@ -257,6 +286,9 @@ public abstract class DataTypeConversion {
if (from == DATE) {
return Conversion.DATE_TO_SHORT;
}
if (from == TIME) {
return Conversion.TIME_TO_SHORT;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_SHORT;
}
@ -279,6 +311,9 @@ public abstract class DataTypeConversion {
if (from == DATE) {
return Conversion.DATE_TO_BYTE;
}
if (from == TIME) {
return Conversion.TIME_TO_BYTE;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_BYTE;
}
@ -301,6 +336,9 @@ public abstract class DataTypeConversion {
if (from == DATE) {
return Conversion.DATE_TO_FLOAT;
}
if (from == TIME) {
return Conversion.TIME_TO_FLOAT;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_FLOAT;
}
@ -323,6 +361,9 @@ public abstract class DataTypeConversion {
if (from == DATE) {
return Conversion.DATE_TO_DOUBLE;
}
if (from == TIME) {
return Conversion.TIME_TO_DOUBLE;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_DOUBLE;
}
@ -348,6 +389,28 @@ public abstract class DataTypeConversion {
return null;
}
private static Conversion conversionToTime(DataType from) {
if (from.isRational()) {
return Conversion.RATIONAL_TO_TIME;
}
if (from.isInteger()) {
return Conversion.INTEGER_TO_TIME;
}
if (from == BOOLEAN) {
return Conversion.BOOL_TO_TIME; // We emit an int here which is ok because of Java's casting rules
}
if (from.isString()) {
return Conversion.STRING_TO_TIME;
}
if (from == DATE) {
return Conversion.DATE_TO_TIME;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_TIME;
}
return null;
}
private static Conversion conversionToDateTime(DataType from) {
if (from.isRational()) {
return Conversion.RATIONAL_TO_DATETIME;
@ -377,6 +440,9 @@ public abstract class DataTypeConversion {
if (from == DATE) {
return Conversion.DATE_TO_BOOLEAN;
}
if (from == TIME) {
return Conversion.TIME_TO_BOOLEAN;
}
if (from == DATETIME) {
return Conversion.DATETIME_TO_BOOLEAN;
}
@ -456,6 +522,7 @@ public abstract class DataTypeConversion {
NULL(value -> null),
DATE_TO_STRING(o -> DateUtils.toDateString((ZonedDateTime) o)),
TIME_TO_STRING(o -> DateUtils.toTimeString((OffsetTime) o)),
DATETIME_TO_STRING(o -> DateUtils.toString((ZonedDateTime) o)),
OTHER_TO_STRING(String::valueOf),
@ -463,6 +530,7 @@ public abstract class DataTypeConversion {
INTEGER_TO_LONG(fromLong(value -> value)),
STRING_TO_LONG(fromString(Long::valueOf, "long")),
DATE_TO_LONG(fromDateTime(value -> value)),
TIME_TO_LONG(fromTime(value -> value)),
DATETIME_TO_LONG(fromDateTime(value -> value)),
RATIONAL_TO_INT(fromDouble(value -> safeToInt(safeToLong(value)))),
@ -470,6 +538,7 @@ public abstract class DataTypeConversion {
BOOL_TO_INT(fromBool(value -> value ? 1 : 0)),
STRING_TO_INT(fromString(Integer::valueOf, "integer")),
DATE_TO_INT(fromDateTime(DataTypeConversion::safeToInt)),
TIME_TO_INT(fromTime(DataTypeConversion::safeToInt)),
DATETIME_TO_INT(fromDateTime(DataTypeConversion::safeToInt)),
RATIONAL_TO_SHORT(fromDouble(value -> safeToShort(safeToLong(value)))),
@ -477,6 +546,7 @@ public abstract class DataTypeConversion {
BOOL_TO_SHORT(fromBool(value -> value ? (short) 1 : (short) 0)),
STRING_TO_SHORT(fromString(Short::valueOf, "short")),
DATE_TO_SHORT(fromDateTime(DataTypeConversion::safeToShort)),
TIME_TO_SHORT(fromTime(DataTypeConversion::safeToShort)),
DATETIME_TO_SHORT(fromDateTime(DataTypeConversion::safeToShort)),
RATIONAL_TO_BYTE(fromDouble(value -> safeToByte(safeToLong(value)))),
@ -484,6 +554,7 @@ public abstract class DataTypeConversion {
BOOL_TO_BYTE(fromBool(value -> value ? (byte) 1 : (byte) 0)),
STRING_TO_BYTE(fromString(Byte::valueOf, "byte")),
DATE_TO_BYTE(fromDateTime(DataTypeConversion::safeToByte)),
TIME_TO_BYTE(fromTime(DataTypeConversion::safeToByte)),
DATETIME_TO_BYTE(fromDateTime(DataTypeConversion::safeToByte)),
// TODO floating point conversions are lossy but conversions to integer conversions are not. Are we ok with that?
@ -492,6 +563,7 @@ public abstract class DataTypeConversion {
BOOL_TO_FLOAT(fromBool(value -> value ? 1f : 0f)),
STRING_TO_FLOAT(fromString(Float::valueOf, "float")),
DATE_TO_FLOAT(fromDateTime(value -> (float) value)),
TIME_TO_FLOAT(fromTime(value -> (float) value)),
DATETIME_TO_FLOAT(fromDateTime(value -> (float) value)),
RATIONAL_TO_DOUBLE(fromDouble(Double::valueOf)),
@ -499,6 +571,7 @@ public abstract class DataTypeConversion {
BOOL_TO_DOUBLE(fromBool(value -> value ? 1d : 0d)),
STRING_TO_DOUBLE(fromString(Double::valueOf, "double")),
DATE_TO_DOUBLE(fromDateTime(Double::valueOf)),
TIME_TO_DOUBLE(fromTime(Double::valueOf)),
DATETIME_TO_DOUBLE(fromDateTime(Double::valueOf)),
RATIONAL_TO_DATE(toDate(RATIONAL_TO_LONG)),
@ -507,6 +580,13 @@ public abstract class DataTypeConversion {
STRING_TO_DATE(fromString(DateUtils::asDateOnly, "date")),
DATETIME_TO_DATE(fromDatetimeToDate()),
RATIONAL_TO_TIME(toTime(RATIONAL_TO_LONG)),
INTEGER_TO_TIME(toTime(INTEGER_TO_LONG)),
BOOL_TO_TIME(toTime(BOOL_TO_INT)),
STRING_TO_TIME(fromString(DateUtils::asTimeOnly, "time")),
DATE_TO_TIME(fromDatetimeToTime()),
DATETIME_TO_TIME(fromDatetimeToTime()),
RATIONAL_TO_DATETIME(toDateTime(RATIONAL_TO_LONG)),
INTEGER_TO_DATETIME(toDateTime(INTEGER_TO_LONG)),
BOOL_TO_DATETIME(toDateTime(BOOL_TO_INT)),
@ -516,6 +596,7 @@ public abstract class DataTypeConversion {
NUMERIC_TO_BOOLEAN(fromLong(value -> value != 0)),
STRING_TO_BOOLEAN(fromString(DataTypeConversion::convertToBoolean, "boolean")),
DATE_TO_BOOLEAN(fromDateTime(value -> value != 0)),
TIME_TO_BOOLEAN(fromTime(value -> value != 0)),
DATETIME_TO_BOOLEAN(fromDateTime(value -> value != 0)),
BOOL_TO_LONG(fromBool(value -> value ? 1L : 0L)),
@ -557,22 +638,34 @@ public abstract class DataTypeConversion {
return (Object l) -> converter.apply(((Boolean) l));
}
private static Function<Object, Object> fromDateTime(Function<Long, Object> converter) {
return l -> converter.apply(((ZonedDateTime) l).toInstant().toEpochMilli());
private static Function<Object, Object> fromTime(Function<Long, Object> converter) {
return l -> converter.apply(((OffsetTime) l).atDate(DateUtils.EPOCH).toInstant().toEpochMilli());
}
private static Function<Object, Object> toDateTime(Conversion conversion) {
return l -> DateUtils.asDateTime(((Number) conversion.convert(l)).longValue());
private static Function<Object, Object> fromDateTime(Function<Long, Object> converter) {
return l -> converter.apply(((ZonedDateTime) l).toInstant().toEpochMilli());
}
private static Function<Object, Object> toDate(Conversion conversion) {
return l -> DateUtils.asDateOnly(((Number) conversion.convert(l)).longValue());
}
private static Function<Object, Object> toTime(Conversion conversion) {
return l -> DateUtils.asTimeOnly(((Number) conversion.convert(l)).longValue());
}
private static Function<Object, Object> toDateTime(Conversion conversion) {
return l -> DateUtils.asDateTime(((Number) conversion.convert(l)).longValue());
}
private static Function<Object, Object> fromDatetimeToDate() {
return l -> DateUtils.asDateOnly((ZonedDateTime) l);
}
private static Function<Object, Object> fromDatetimeToTime() {
return l -> ((ZonedDateTime) l).toOffsetDateTime().toOffsetTime();
}
public Object convert(Object l) {
if (l == null) {
return null;

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.sql.type;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.expression.literal.Interval;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import static org.elasticsearch.xpack.sql.type.DataType.BOOLEAN;
@ -27,6 +28,7 @@ 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.DataType.NULL;
import static org.elasticsearch.xpack.sql.type.DataType.SHORT;
import static org.elasticsearch.xpack.sql.type.DataType.TIME;
import static org.elasticsearch.xpack.sql.type.DataType.UNSUPPORTED;
import static org.elasticsearch.xpack.sql.type.DataType.fromTypeName;
@ -67,6 +69,9 @@ public final class DataTypes {
if (value instanceof Short) {
return SHORT;
}
if (value instanceof OffsetTime) {
return TIME;
}
if (value instanceof ZonedDateTime) {
return DATETIME;
}

@ -12,6 +12,7 @@ import org.elasticsearch.xpack.sql.proto.StringUtils;
import java.time.Instant;
import java.time.LocalDate;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@ -19,11 +20,15 @@ import java.time.format.DateTimeFormatterBuilder;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
import static java.time.format.DateTimeFormatter.ISO_TIME;
public final class DateUtils {
public static final ZoneId UTC = ZoneId.of("Z");
public static final String DATE_PARSE_FORMAT = "epoch_millis";
// In Java 8 LocalDate.EPOCH is not available, introduced with later Java versions
public static final LocalDate EPOCH = LocalDate.of(1970, 1, 1);
public static final long DAY_IN_MILLIS = 60 * 60 * 24 * 1000L;
private static final DateTimeFormatter DATE_TIME_ESCAPED_LITERAL_FORMATTER = new DateTimeFormatterBuilder()
.append(ISO_LOCAL_DATE)
@ -33,8 +38,6 @@ public final class DateUtils {
private static final DateFormatter UTC_DATE_TIME_FORMATTER = DateFormatter.forPattern("date_optional_time").withZone(UTC);
private static final long DAY_IN_MILLIS = 60 * 60 * 24 * 1000L;
private DateUtils() {}
/**
@ -44,6 +47,24 @@ public final class DateUtils {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), UTC).toLocalDate().atStartOfDay(UTC);
}
/**
* Creates an date for SQL TIME type from the millis since epoch.
*/
public static OffsetTime asTimeOnly(long millis) {
return OffsetTime.ofInstant(Instant.ofEpochMilli(millis % DAY_IN_MILLIS), UTC);
}
/**
* Creates an date for SQL TIME type from the millis since epoch.
*/
public static OffsetTime asTimeOnly(long millis, ZoneId zoneId) {
return OffsetTime.ofInstant(Instant.ofEpochMilli(millis % DAY_IN_MILLIS), zoneId);
}
public static OffsetTime asTimeAtZone(OffsetTime time, ZoneId zonedId) {
return time.atDate(DateUtils.EPOCH).atZoneSameInstant(zonedId).toOffsetDateTime().toOffsetTime();
}
/**
* Creates a datetime from the millis since epoch (thus the time-zone is UTC).
*/
@ -69,6 +90,10 @@ public final class DateUtils {
return zdt.toLocalDate().atStartOfDay(zdt.getZone());
}
public static OffsetTime asTimeOnly(String timeFormat) {
return DateFormatters.from(ISO_TIME.parse(timeFormat)).toOffsetDateTime().toOffsetTime();
}
/**
* Parses the given string into a DateTime using UTC as a default timezone.
*/
@ -88,6 +113,10 @@ public final class DateUtils {
return date.format(ISO_LOCAL_DATE);
}
public static String toTimeString(OffsetTime time) {
return time.format(ISO_LOCAL_TIME);
}
public static long minDayInterval(long l) {
if (l < DAY_IN_MILLIS ) {
return DAY_IN_MILLIS;

@ -12,6 +12,9 @@ class org.elasticsearch.xpack.sql.expression.literal.IntervalDayTime {
class org.elasticsearch.xpack.sql.expression.literal.IntervalYearMonth {
}
class java.time.OffsetTime {
}
class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalSqlScriptUtils {
#
@ -107,6 +110,7 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS
IntervalDayTime intervalDayTime(String, String)
IntervalYearMonth intervalYearMonth(String, String)
ZonedDateTime asDateTime(Object)
OffsetTime asTime(String)
#
# ASCII Functions

@ -203,10 +203,42 @@ public class VerifierErrorMessagesTests extends ESTestCase {
assertEquals("1:8: Invalid datetime field [ABS]. Use any datetime function.", error("SELECT EXTRACT(ABS FROM date) FROM test"));
}
public void testValidDateTimeFunctionsOnTime() {
accept("SELECT HOUR_OF_DAY(CAST(date AS TIME)) FROM test");
accept("SELECT MINUTE_OF_HOUR(CAST(date AS TIME)) FROM test");
accept("SELECT MINUTE_OF_DAY(CAST(date AS TIME)) FROM test");
accept("SELECT SECOND_OF_MINUTE(CAST(date AS TIME)) FROM test");
}
public void testInvalidDateTimeFunctionsOnTime() {
assertEquals("1:8: argument of [DAY_OF_YEAR(CAST(date AS TIME))] must be [date or datetime], " +
"found value [CAST(date AS TIME)] type [time]",
error("SELECT DAY_OF_YEAR(CAST(date AS TIME)) FROM test"));
}
public void testGroupByOnTimeNotAllowed() {
assertEquals("1:36: Function [CAST(date AS TIME)] with data type [time] cannot be used for grouping",
error("SELECT count(*) FROM test GROUP BY CAST(date AS TIME)"));
}
public void testGroupByOnTimeWrappedWithScalar() {
accept("SELECT count(*) FROM test GROUP BY MINUTE(CAST(date AS TIME))");
}
public void testHistogramOnTimeNotAllowed() {
assertEquals("1:8: first argument of [HISTOGRAM] must be [date, datetime or numeric], " +
"found value [CAST(date AS TIME)] type [time]",
error("SELECT HISTOGRAM(CAST(date AS TIME), INTERVAL 1 MONTH), COUNT(*) FROM test GROUP BY 1"));
}
public void testSubtractFromInterval() {
assertEquals("1:8: Cannot subtract a datetime[CAST('2000-01-01' AS DATETIME)] " +
"from an interval[INTERVAL 1 MONTH]; do you mean the reverse?",
error("SELECT INTERVAL 1 MONTH - CAST('2000-01-01' AS DATETIME)"));
assertEquals("1:8: Cannot subtract a time[CAST('12:23:56.789' AS TIME)] " +
"from an interval[INTERVAL 1 MONTH]; do you mean the reverse?",
error("SELECT INTERVAL 1 MONTH - CAST('12:23:56.789' AS TIME)"));
}
public void testMultipleColumns() {
@ -293,7 +325,7 @@ public class VerifierErrorMessagesTests extends ESTestCase {
}
public void testGroupByOnInexact() {
assertEquals("1:36: Field of data type [text] cannot be used for grouping; " +
assertEquals("1:36: Field [text] of data type [text] cannot be used for grouping; " +
"No keyword/multi-field defined exact matches for [text]; define one or use MATCH/QUERY instead",
error("SELECT COUNT(*) FROM test GROUP BY text"));
}

@ -7,17 +7,20 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
import org.elasticsearch.common.io.stream.Writeable.Reader;
import org.elasticsearch.test.AbstractWireSerializingTestCase;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor.DateTimeExtractor;
import java.io.IOException;
import java.time.OffsetTime;
import java.time.ZoneId;
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.dateTime;
import static org.elasticsearch.xpack.sql.util.DateUtils.UTC;
import static org.hamcrest.Matchers.startsWith;
public class DateTimeProcessorTests extends AbstractWireSerializingTestCase<DateTimeProcessor> {
public static DateTimeProcessor randomDateTimeProcessor() {
return new DateTimeProcessor(randomFrom(DateTimeExtractor.values()), UTC);
return new DateTimeProcessor(randomFrom(DateTimeExtractor.values()), randomZone());
}
@Override
@ -31,12 +34,12 @@ public class DateTimeProcessorTests extends AbstractWireSerializingTestCase<Date
}
@Override
protected DateTimeProcessor mutateInstance(DateTimeProcessor instance) throws IOException {
protected DateTimeProcessor mutateInstance(DateTimeProcessor instance) {
DateTimeExtractor replaced = randomValueOtherThan(instance.extractor(), () -> randomFrom(DateTimeExtractor.values()));
return new DateTimeProcessor(replaced, UTC);
return new DateTimeProcessor(replaced, randomZone());
}
public void testApply() {
public void testApply_withTimezoneUTC() {
DateTimeProcessor proc = new DateTimeProcessor(DateTimeExtractor.YEAR, UTC);
assertEquals(1970, proc.process(dateTime(0L)));
assertEquals(2017, proc.process(dateTime(2017, 01, 02, 10, 10)));
@ -46,4 +49,21 @@ public class DateTimeProcessorTests extends AbstractWireSerializingTestCase<Date
assertEquals(2, proc.process(dateTime(2017, 01, 02, 10, 10)));
assertEquals(31, proc.process(dateTime(2017, 01, 31, 10, 10)));
}
public void testApply_withTimezoneOtherThanUTC() {
ZoneId zoneId = ZoneId.of("Etc/GMT-10");
DateTimeProcessor proc = new DateTimeProcessor(DateTimeExtractor.YEAR, zoneId);
assertEquals(2018, proc.process(dateTime(2017, 12, 31, 18, 10)));
proc = new DateTimeProcessor(DateTimeExtractor.DAY_OF_MONTH, zoneId);
assertEquals(1, proc.process(dateTime(2017, 12, 31, 20, 30)));
}
public void testFailOnTime() {
DateTimeProcessor proc = new DateTimeProcessor(DateTimeExtractor.YEAR, UTC);
SqlIllegalArgumentException e = expectThrows(SqlIllegalArgumentException.class, () -> {
proc.process(OffsetTime.now(UTC));
});
assertThat(e.getMessage(), startsWith("A [date], a [time] or a [datetime] is required; received "));
}
}

@ -8,6 +8,8 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
import org.elasticsearch.xpack.sql.util.DateUtils;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
public class DateTimeTestUtils {
@ -25,4 +27,12 @@ public class DateTimeTestUtils {
public static ZonedDateTime date(long millisSinceEpoch) {
return DateUtils.asDateOnly(millisSinceEpoch);
}
public static OffsetTime time(long millisSinceEpoch) {
return DateUtils.asTimeOnly(millisSinceEpoch);
}
public static OffsetTime time(int hour, int minute, int second, int nano) {
return OffsetTime.of(hour, minute, second, nano, ZoneOffset.UTC);
}
}

@ -0,0 +1,77 @@
/*
* 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.function.scalar.datetime;
import org.elasticsearch.common.io.stream.Writeable.Reader;
import org.elasticsearch.test.AbstractWireSerializingTestCase;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor.DateTimeExtractor;
import java.time.ZoneId;
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.time;
import static org.elasticsearch.xpack.sql.util.DateUtils.UTC;
public class TimeProcessorTests extends AbstractWireSerializingTestCase<TimeProcessor> {
public static TimeProcessor randomTimeProcessor() {
return new TimeProcessor(randomFrom(DateTimeExtractor.values()), randomZone());
}
@Override
protected TimeProcessor createTestInstance() {
return randomTimeProcessor();
}
@Override
protected Reader<TimeProcessor> instanceReader() {
return TimeProcessor::new;
}
@Override
protected TimeProcessor mutateInstance(TimeProcessor instance) {
DateTimeExtractor replaced = randomValueOtherThan(instance.extractor(), () -> randomFrom(DateTimeExtractor.values()));
return new TimeProcessor(replaced, randomZone());
}
public void testApply_withTimeZoneUTC() {
TimeProcessor proc = new TimeProcessor(DateTimeExtractor.SECOND_OF_MINUTE, UTC);
assertEquals(0, proc.process(time(0L)));
assertEquals(2, proc.process(time(2345L)));
proc = new TimeProcessor(DateTimeExtractor.MINUTE_OF_DAY, UTC);
assertEquals(0, proc.process(time(0L)));
assertEquals(620, proc.process(time(10, 20, 30, 123456789)));
proc = new TimeProcessor(DateTimeExtractor.MINUTE_OF_HOUR, UTC);
assertEquals(0, proc.process(time(0L)));
assertEquals(20, proc.process(time(10, 20, 30, 123456789)));
proc = new TimeProcessor(DateTimeExtractor.HOUR_OF_DAY, UTC);
assertEquals(0, proc.process(time(0L)));
assertEquals(10, proc.process(time(10, 20, 30, 123456789)));
}
public void testApply_withTimeZoneOtherThanUTC() {
ZoneId zoneId = ZoneId.of("Etc/GMT-10");
TimeProcessor proc = new TimeProcessor(DateTimeExtractor.SECOND_OF_MINUTE, zoneId);
assertEquals(0, proc.process(time(0L)));
assertEquals(2, proc.process(time(2345L)));
proc = new TimeProcessor(DateTimeExtractor.MINUTE_OF_DAY, zoneId);
assertEquals(600, proc.process(time(0L)));
assertEquals(1220, proc.process(time(10, 20, 30, 123456789)));
proc = new TimeProcessor(DateTimeExtractor.MINUTE_OF_HOUR, zoneId);
assertEquals(0, proc.process(time(0L)));
assertEquals(20, proc.process(time(10, 20, 30, 123456789)));
proc = new TimeProcessor(DateTimeExtractor.HOUR_OF_DAY, zoneId);
assertEquals(10, proc.process(time(0L)));
assertEquals(20, proc.process(time(10, 20, 30, 123456789)));;
assertEquals(4, proc.process(time(18, 20, 30, 123456789)));
}
}

@ -15,6 +15,7 @@ import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.util.DateUtils;
import java.time.Duration;
import java.time.OffsetTime;
import java.time.Period;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAmount;
@ -104,6 +105,33 @@ public class BinaryArithmeticTests extends ESTestCase {
assertEquals(L(now.plus(t)), L(x));
}
public void testAddYearMonthIntervalToTime() {
OffsetTime now = OffsetTime.now(DateUtils.UTC);
Literal l = L(now);
TemporalAmount t = Period.ofYears(100).plusMonths(50);
Literal r = interval(t, INTERVAL_HOUR);
OffsetTime x = add(l, r);
assertEquals(L(now), L(x));
}
public void testAddDayTimeIntervalToTime() {
OffsetTime now = OffsetTime.now(DateUtils.UTC);
Literal l = L(now);
TemporalAmount t = Duration.ofHours(32);
Literal r = interval(Duration.ofHours(32), INTERVAL_HOUR);
OffsetTime x = add(l, r);
assertEquals(L(now.plus(t)), L(x));
}
public void testAddDayTimeIntervalToTimeReverse() {
OffsetTime now = OffsetTime.now(DateUtils.UTC);
Literal l = L(now);
TemporalAmount t = Duration.ofHours(45);
Literal r = interval(Duration.ofHours(45), INTERVAL_HOUR);
OffsetTime x = add(r, l);
assertEquals(L(now.plus(t)), L(x));
}
public void testAddNumberToIntervalIllegal() {
Literal r = interval(Duration.ofHours(2), INTERVAL_HOUR);
SqlIllegalArgumentException expect = expectThrows(SqlIllegalArgumentException.class, () -> add(r, L(1)));
@ -142,12 +170,6 @@ public class BinaryArithmeticTests extends ESTestCase {
assertEquals("Cannot subtract a date from an interval; do you mean the reverse?", ex.getMessage());
}
public void testSubNumberFromIntervalIllegal() {
Literal r = interval(Duration.ofHours(2), INTERVAL_HOUR);
SqlIllegalArgumentException expect = expectThrows(SqlIllegalArgumentException.class, () -> sub(r, L(1)));
assertEquals("Cannot compute [-] between [IntervalDayTime] [Integer]", expect.getMessage());
}
public void testSubDayTimeIntervalToDateTime() {
ZonedDateTime now = ZonedDateTime.now(DateUtils.UTC);
Literal l = L(now);
@ -157,7 +179,40 @@ public class BinaryArithmeticTests extends ESTestCase {
assertEquals(L(now.minus(t)), L(x));
}
public void testMulIntervalNumber() throws Exception {
public void testSubYearMonthIntervalToTime() {
OffsetTime now = OffsetTime.now(DateUtils.UTC);
Literal l = L(now);
TemporalAmount t = Period.ofYears(100).plusMonths(50);
Literal r = interval(t, INTERVAL_HOUR);
OffsetTime x = sub(l, r);
assertEquals(L(now), L(x));
}
public void testSubYearMonthIntervalToTimeIllegal() {
OffsetTime now = OffsetTime.now(DateUtils.UTC);
Literal l = L(now);
TemporalAmount t = Period.ofYears(100).plusMonths(50);
Literal r = interval(t, INTERVAL_HOUR);
SqlIllegalArgumentException ex = expectThrows(SqlIllegalArgumentException.class, () -> sub(r, l));
assertEquals("Cannot subtract a date from an interval; do you mean the reverse?", ex.getMessage());
}
public void testSubDayTimeIntervalToTime() {
OffsetTime now = OffsetTime.now(DateUtils.UTC);
Literal l = L(now);
TemporalAmount t = Duration.ofHours(36);
Literal r = interval(Duration.ofHours(36), INTERVAL_HOUR);
OffsetTime x = sub(l, r);
assertEquals(L(now.minus(t)), L(x));
}
public void testSubNumberFromIntervalIllegal() {
Literal r = interval(Duration.ofHours(2), INTERVAL_HOUR);
SqlIllegalArgumentException expect = expectThrows(SqlIllegalArgumentException.class, () -> sub(r, L(1)));
assertEquals("Cannot compute [-] between [IntervalDayTime] [Integer]", expect.getMessage());
}
public void testMulIntervalNumber() {
Literal l = interval(Duration.ofHours(2), INTERVAL_HOUR);
IntervalDayTime interval = mul(l, -1);
assertEquals(INTERVAL_HOUR, interval.dataType());
@ -165,7 +220,7 @@ public class BinaryArithmeticTests extends ESTestCase {
assertEquals(Duration.ofHours(2).negated(), p);
}
public void testMulNumberInterval() throws Exception {
public void testMulNumberInterval() {
Literal r = interval(Period.ofYears(1), INTERVAL_YEAR);
IntervalYearMonth interval = mul(-2, r);
assertEquals(INTERVAL_YEAR, interval.dataType());

@ -6,7 +6,6 @@
package org.elasticsearch.xpack.sql.parser;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.Literal;
import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute;
@ -180,9 +179,9 @@ public class EscapedFunctionsTests extends ESTestCase {
ex.getMessage());
}
public void testTimeLiteralUnsupported() {
SqlIllegalArgumentException ex = expectThrows(SqlIllegalArgumentException.class, () -> timeLiteral("10:10:10"));
assertThat(ex.getMessage(), is("Time (only) literals are not supported; a date component is required as well"));
public void testTimeLiteral() {
Literal l = timeLiteral("12:23:56");
assertThat(l.dataType(), is(DataType.TIME));
}
public void testTimeLiteralValidation() {

@ -57,11 +57,11 @@ public class SysParserTests extends ESTestCase {
return new Tuple<>(cmd, session);
}
public void testSysTypes() throws Exception {
public void testSysTypes() {
Command cmd = sql("SYS TYPES").v1();
List<String> names = asList("BYTE", "LONG", "BINARY", "NULL", "INTEGER", "SHORT", "HALF_FLOAT", "FLOAT", "DOUBLE", "SCALED_FLOAT",
"KEYWORD", "TEXT", "IP", "BOOLEAN", "DATE", "DATETIME",
"KEYWORD", "TEXT", "IP", "BOOLEAN", "DATE", "TIME", "DATETIME",
"INTERVAL_YEAR", "INTERVAL_MONTH", "INTERVAL_DAY", "INTERVAL_HOUR", "INTERVAL_MINUTE", "INTERVAL_SECOND",
"INTERVAL_YEAR_TO_MONTH", "INTERVAL_DAY_TO_HOUR", "INTERVAL_DAY_TO_MINUTE", "INTERVAL_DAY_TO_SECOND",
"INTERVAL_HOUR_TO_MINUTE", "INTERVAL_HOUR_TO_SECOND", "INTERVAL_MINUTE_TO_SECOND",
@ -86,11 +86,11 @@ public class SysParserTests extends ESTestCase {
}, ex -> fail(ex.getMessage())));
}
public void testSysColsNoArgs() throws Exception {
public void testSysColsNoArgs() {
runSysColumns("SYS COLUMNS");
}
public void testSysColumnEmptyCatalog() throws Exception {
public void testSysColumnEmptyCatalog() {
Tuple<Command, SqlSession> sql = sql("SYS COLUMNS CATALOG '' TABLE LIKE '%' LIKE '%'");
sql.v1().execute(sql.v2(), ActionListener.wrap(r -> {
@ -99,7 +99,7 @@ public class SysParserTests extends ESTestCase {
}, ex -> fail(ex.getMessage())));
}
public void testSysColsTableOnlyCatalog() throws Exception {
public void testSysColsTableOnlyCatalog() {
Tuple<Command, SqlSession> sql = sql("SYS COLUMNS CATALOG 'catalog'");
sql.v1().execute(sql.v2(), ActionListener.wrap(r -> {
@ -108,20 +108,20 @@ public class SysParserTests extends ESTestCase {
}, ex -> fail(ex.getMessage())));
}
public void testSysColsTableOnlyPattern() throws Exception {
public void testSysColsTableOnlyPattern() {
runSysColumns("SYS COLUMNS TABLE LIKE 'test'");
}
public void testSysColsColOnlyPattern() throws Exception {
public void testSysColsColOnlyPattern() {
runSysColumns("SYS COLUMNS LIKE '%'");
}
public void testSysColsTableAndColsPattern() throws Exception {
public void testSysColsTableAndColsPattern() {
runSysColumns("SYS COLUMNS TABLE LIKE 'test' LIKE '%'");
}
private void runSysColumns(String commandVariation) throws Exception {
private void runSysColumns(String commandVariation) {
Tuple<Command, SqlSession> sql = sql(commandVariation);
List<String> names = asList("bool",
"int",

@ -44,7 +44,7 @@ public class SysTypesTests extends ESTestCase {
Command cmd = sql("SYS TYPES").v1();
List<String> names = asList("BYTE", "LONG", "BINARY", "NULL", "INTEGER", "SHORT", "HALF_FLOAT", "FLOAT", "DOUBLE", "SCALED_FLOAT",
"KEYWORD", "TEXT", "IP", "BOOLEAN", "DATE", "DATETIME",
"KEYWORD", "TEXT", "IP", "BOOLEAN", "DATE", "TIME", "DATETIME",
"INTERVAL_YEAR", "INTERVAL_MONTH", "INTERVAL_DAY", "INTERVAL_HOUR", "INTERVAL_MINUTE", "INTERVAL_SECOND",
"INTERVAL_YEAR_TO_MONTH", "INTERVAL_DAY_TO_HOUR", "INTERVAL_DAY_TO_MINUTE", "INTERVAL_DAY_TO_SECOND",
"INTERVAL_HOUR_TO_MINUTE", "INTERVAL_HOUR_TO_SECOND", "INTERVAL_MINUTE_TO_SECOND",

@ -662,7 +662,7 @@ public class QueryTranslatorTests extends ESTestCase {
assertEquals(259200000L, ((GroupByDateHistogram) eqe.queryContainer().aggs().groups().get(0)).interval());
}
}
public void testCountAndCountDistinctFolding() {
PhysicalPlan p = optimizeAndPlan("SELECT COUNT(DISTINCT keyword) dkey, COUNT(keyword) key FROM test");
assertEquals(EsQueryExec.class, p.getClass());
@ -807,57 +807,56 @@ public class QueryTranslatorTests extends ESTestCase {
}
}
public void testGlobalCountInImplicitGroupByForcesTrackHits() throws Exception {
public void testGlobalCountInImplicitGroupByForcesTrackHits() {
PhysicalPlan p = optimizeAndPlan("SELECT COUNT(*) FROM test");
assertEquals(EsQueryExec.class, p.getClass());
EsQueryExec eqe = (EsQueryExec) p;
assertTrue("Should be tracking hits", eqe.queryContainer().shouldTrackHits());
}
public void testGlobalCountAllInImplicitGroupByForcesTrackHits() throws Exception {
public void testGlobalCountAllInImplicitGroupByForcesTrackHits() {
PhysicalPlan p = optimizeAndPlan("SELECT COUNT(ALL *) FROM test");
assertEquals(EsQueryExec.class, p.getClass());
EsQueryExec eqe = (EsQueryExec) p;
assertTrue("Should be tracking hits", eqe.queryContainer().shouldTrackHits());
}
public void testGlobalCountInSpecificGroupByDoesNotForceTrackHits() throws Exception {
public void testGlobalCountInSpecificGroupByDoesNotForceTrackHits() {
PhysicalPlan p = optimizeAndPlan("SELECT COUNT(*) FROM test GROUP BY int");
assertEquals(EsQueryExec.class, p.getClass());
EsQueryExec eqe = (EsQueryExec) p;
assertFalse("Should NOT be tracking hits", eqe.queryContainer().shouldTrackHits());
}
public void testFieldAllCountDoesNotTrackHits() throws Exception {
public void testFieldAllCountDoesNotTrackHits() {
PhysicalPlan p = optimizeAndPlan("SELECT COUNT(ALL int) FROM test");
assertEquals(EsQueryExec.class, p.getClass());
EsQueryExec eqe = (EsQueryExec) p;
assertFalse("Should NOT be tracking hits", eqe.queryContainer().shouldTrackHits());
}
public void testFieldCountDoesNotTrackHits() throws Exception {
public void testFieldCountDoesNotTrackHits() {
PhysicalPlan p = optimizeAndPlan("SELECT COUNT(int) FROM test");
assertEquals(EsQueryExec.class, p.getClass());
EsQueryExec eqe = (EsQueryExec) p;
assertFalse("Should NOT be tracking hits", eqe.queryContainer().shouldTrackHits());
}
public void testDistinctCountDoesNotTrackHits() throws Exception {
public void testDistinctCountDoesNotTrackHits() {
PhysicalPlan p = optimizeAndPlan("SELECT COUNT(DISTINCT int) FROM test");
assertEquals(EsQueryExec.class, p.getClass());
EsQueryExec eqe = (EsQueryExec) p;
assertFalse("Should NOT be tracking hits", eqe.queryContainer().shouldTrackHits());
}
public void testNoCountDoesNotTrackHits() throws Exception {
public void testNoCountDoesNotTrackHits() {
PhysicalPlan p = optimizeAndPlan("SELECT int FROM test");
assertEquals(EsQueryExec.class, p.getClass());
EsQueryExec eqe = (EsQueryExec) p;
assertFalse("Should NOT be tracking hits", eqe.queryContainer().shouldTrackHits());
}
public void testZonedDateTimeInScripts() throws Exception {
public void testZonedDateTimeInScripts() {
PhysicalPlan p = optimizeAndPlan(
"SELECT date FROM test WHERE date + INTERVAL 1 YEAR > CAST('2019-03-11T12:34:56.000Z' AS DATETIME)");
assertEquals(EsQueryExec.class, p.getClass());

@ -12,14 +12,16 @@ import org.elasticsearch.xpack.sql.expression.Literal;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.tree.Source;
import org.elasticsearch.xpack.sql.type.DataTypeConversion.Conversion;
import org.elasticsearch.xpack.sql.util.DateUtils;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.date;
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.dateTime;
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.time;
import static org.elasticsearch.xpack.sql.type.DataType.BOOLEAN;
import static org.elasticsearch.xpack.sql.type.DataType.BYTE;
import static org.elasticsearch.xpack.sql.type.DataType.DATE;
@ -39,12 +41,15 @@ import static org.elasticsearch.xpack.sql.type.DataType.LONG;
import static org.elasticsearch.xpack.sql.type.DataType.NULL;
import static org.elasticsearch.xpack.sql.type.DataType.SHORT;
import static org.elasticsearch.xpack.sql.type.DataType.TEXT;
import static org.elasticsearch.xpack.sql.type.DataType.TIME;
import static org.elasticsearch.xpack.sql.type.DataType.UNSUPPORTED;
import static org.elasticsearch.xpack.sql.type.DataType.fromTypeName;
import static org.elasticsearch.xpack.sql.type.DataType.values;
import static org.elasticsearch.xpack.sql.type.DataTypeConversion.commonType;
import static org.elasticsearch.xpack.sql.type.DataTypeConversion.conversionFor;
import static org.elasticsearch.xpack.sql.util.DateUtils.asDateOnly;
import static org.elasticsearch.xpack.sql.util.DateUtils.asDateTime;
import static org.elasticsearch.xpack.sql.util.DateUtils.asTimeOnly;
public class DataTypeConversionTests extends ESTestCase {
@ -59,8 +64,16 @@ public class DataTypeConversionTests extends ESTestCase {
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals("1973-11-29", conversion.convert(DateUtils.asDateOnly(123456789101L)));
assertEquals("1966-02-02", conversion.convert(DateUtils.asDateOnly(-123456789101L)));
assertEquals("1973-11-29", conversion.convert(asDateOnly(123456789101L)));
assertEquals("1966-02-02", conversion.convert(asDateOnly(-123456789101L)));
}
{
Conversion conversion = conversionFor(TIME, to);
assertNull(conversion.convert(null));
assertEquals("00:02:03.456", conversion.convert(asTimeOnly(123456L)));
assertEquals("21:33:09.101", conversion.convert(asTimeOnly(123456789101L)));
assertEquals("23:57:56.544", conversion.convert(asTimeOnly(-123456L)));
assertEquals("02:26:50.899", conversion.convert(asTimeOnly(-123456789101L)));
}
{
Conversion conversion = conversionFor(DATETIME, to);
@ -99,8 +112,16 @@ public class DataTypeConversionTests extends ESTestCase {
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals(123379200000L, conversion.convert(DateUtils.asDateOnly(123456789101L)));
assertEquals(-123465600000L, conversion.convert(DateUtils.asDateOnly(-123456789101L)));
assertEquals(123379200000L, conversion.convert(asDateOnly(123456789101L)));
assertEquals(-123465600000L, conversion.convert(asDateOnly(-123456789101L)));
}
{
Conversion conversion = conversionFor(TIME, to);
assertNull(conversion.convert(null));
assertEquals(123456L, conversion.convert(asTimeOnly(123456L)));
assertEquals(77589101L, conversion.convert(asTimeOnly(123456789101L)));
assertEquals(86276544L, conversion.convert(asTimeOnly(-123456L)));
assertEquals(8810899L, conversion.convert(asTimeOnly(-123456789101L)));
}
{
Conversion conversion = conversionFor(DATETIME, to);
@ -141,6 +162,10 @@ public class DataTypeConversionTests extends ESTestCase {
assertEquals(date(1), conversion.convert(true));
assertEquals(date(0), conversion.convert(false));
}
{
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversionFor(TIME, to));
assertEquals("cannot convert from [time] to [date]", e.getMessage());
}
{
Conversion conversion = conversionFor(DATETIME, to);
assertNull(conversion.convert(null));
@ -160,12 +185,67 @@ public class DataTypeConversionTests extends ESTestCase {
ZonedDateTime zdt = TestUtils.now();
Conversion forward = conversionFor(DATE, KEYWORD);
Conversion back = conversionFor(KEYWORD, DATE);
assertEquals(DateUtils.asDateOnly(zdt), back.convert(forward.convert(zdt)));
assertEquals(asDateOnly(zdt), back.convert(forward.convert(zdt)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert("0xff"));
assertEquals("cannot cast [0xff] to [date]: Text '0xff' could not be parsed at index 0", e.getMessage());
}
}
public void testConversionToTime() {
DataType to = TIME;
{
Conversion conversion = conversionFor(DOUBLE, to);
assertNull(conversion.convert(null));
assertEquals(time(10L), conversion.convert(10.0));
assertEquals(time(10L), conversion.convert(10.1));
assertEquals(time(11L), conversion.convert(10.6));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(Double.MAX_VALUE));
assertEquals("[" + Double.MAX_VALUE + "] out of [long] range", e.getMessage());
}
{
Conversion conversion = conversionFor(INTEGER, to);
assertNull(conversion.convert(null));
assertEquals(time(10L), conversion.convert(10));
assertEquals(time(-134L), conversion.convert(-134));
}
{
Conversion conversion = conversionFor(BOOLEAN, to);
assertNull(conversion.convert(null));
assertEquals(time(1), conversion.convert(true));
assertEquals(time(0), conversion.convert(false));
}
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals(time(123379200000L), conversion.convert(asDateOnly(123456789101L)));
assertEquals(time(-123465600000L), conversion.convert(asDateOnly(-123456789101L)));
}
{
Conversion conversion = conversionFor(DATETIME, to);
assertNull(conversion.convert(null));
assertEquals(time(77589101L), conversion.convert(asDateTime(123456789101L)));
assertEquals(time(8810899L), conversion.convert(asDateTime(-123456789101L)));
}
{
Conversion conversion = conversionFor(KEYWORD, to);
assertNull(conversion.convert(null));
assertEquals(time(0L), conversion.convert("00:00:00Z"));
assertEquals(time(1000L), conversion.convert("00:00:01Z"));
assertEquals(time(1234L), conversion.convert("00:00:01.234Z"));
assertEquals(time(63296789L).withOffsetSameInstant(ZoneOffset.ofHours(-5)), conversion.convert("12:34:56.789-05:00"));
// double check back and forth conversion
OffsetTime ot = org.elasticsearch.common.time.DateUtils.nowWithMillisResolution().toOffsetDateTime().toOffsetTime();
Conversion forward = conversionFor(TIME, KEYWORD);
Conversion back = conversionFor(KEYWORD, TIME);
assertEquals(ot, back.convert(forward.convert(ot)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert("0xff"));
assertEquals("cannot cast [0xff] to [time]: Text '0xff' could not be parsed at index 0",
e.getMessage());
}
}
public void testConversionToDateTime() {
DataType to = DATETIME;
{
@ -192,8 +272,12 @@ public class DataTypeConversionTests extends ESTestCase {
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals(dateTime(123379200000L), conversion.convert(DateUtils.asDateOnly(123456789101L)));
assertEquals(dateTime(-123465600000L), conversion.convert(DateUtils.asDateOnly(-123456789101L)));
assertEquals(dateTime(123379200000L), conversion.convert(asDateOnly(123456789101L)));
assertEquals(dateTime(-123465600000L), conversion.convert(asDateOnly(-123456789101L)));
}
{
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversionFor(TIME, to));
assertEquals("cannot convert from [time] to [datetime]", e.getMessage());
}
{
Conversion conversion = conversionFor(KEYWORD, to);
@ -216,6 +300,58 @@ public class DataTypeConversionTests extends ESTestCase {
}
}
public void testConversionToFloat() {
DataType to = FLOAT;
{
Conversion conversion = conversionFor(DOUBLE, to);
assertNull(conversion.convert(null));
assertEquals(10.0f, (float) conversion.convert(10.0d), 0.00001);
assertEquals(10.1f, (float) conversion.convert(10.1d), 0.00001);
assertEquals(10.6f, (float) conversion.convert(10.6d), 0.00001);
}
{
Conversion conversion = conversionFor(INTEGER, to);
assertNull(conversion.convert(null));
assertEquals(10.0f, (float) conversion.convert(10), 0.00001);
assertEquals(-134.0f, (float) conversion.convert(-134), 0.00001);
}
{
Conversion conversion = conversionFor(BOOLEAN, to);
assertNull(conversion.convert(null));
assertEquals(1.0f, (float) conversion.convert(true), 0);
assertEquals(0.0f, (float) conversion.convert(false), 0);
}
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals(1.233792E11f, (float) conversion.convert(asDateOnly(123456789101L)), 0);
assertEquals(-1.234656E11f, (float) conversion.convert(asDateOnly(-123456789101L)), 0);
}
{
Conversion conversion = conversionFor(TIME, to);
assertNull(conversion.convert(null));
assertEquals(123456.0f, (float) conversion.convert(asTimeOnly(123456L)), 0);
assertEquals(7.7589104E7f, (float) conversion.convert(asTimeOnly(123456789101L)), 0);
assertEquals(8.6276544E7f, (float) conversion.convert(asTimeOnly(-123456L)), 0);
assertEquals(8810899.0f, (float) conversion.convert(asTimeOnly(-123456789101L)), 0);
}
{
Conversion conversion = conversionFor(DATETIME, to);
assertNull(conversion.convert(null));
assertEquals(1.23456789101E11f, (float) conversion.convert(asDateTime(123456789101L)), 0);
assertEquals(-1.23456789101E11f, (float) conversion.convert(asDateTime(-123456789101L)), 0);
}
{
Conversion conversion = conversionFor(KEYWORD, to);
assertNull(conversion.convert(null));
assertEquals(1.0f, (float) conversion.convert("1"), 0);
assertEquals(0.0f, (float) conversion.convert("-0"), 0);
assertEquals(12.776f, (float) conversion.convert("12.776"), 0.00001);
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert("0xff"));
assertEquals("cannot cast [0xff] to [float]", e.getMessage());
}
}
public void testConversionToDouble() {
DataType to = DOUBLE;
{
@ -240,8 +376,16 @@ public class DataTypeConversionTests extends ESTestCase {
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals(1.233792E11, (double) conversion.convert(DateUtils.asDateOnly(123456789101L)), 0);
assertEquals(-1.234656E11, (double) conversion.convert(DateUtils.asDateOnly(-123456789101L)), 0);
assertEquals(1.233792E11, (double) conversion.convert(asDateOnly(123456789101L)), 0);
assertEquals(-1.234656E11, (double) conversion.convert(asDateOnly(-123456789101L)), 0);
}
{
Conversion conversion = conversionFor(TIME, to);
assertNull(conversion.convert(null));
assertEquals(123456.0, (double) conversion.convert(asTimeOnly(123456L)), 0);
assertEquals(7.7589101E7, (double) conversion.convert(asTimeOnly(123456789101L)), 0);
assertEquals(8.6276544E7, (double) conversion.convert(asTimeOnly(-123456L)), 0);
assertEquals(8810899.0, (double) conversion.convert(asTimeOnly(-123456789101L)), 0);
}
{
Conversion conversion = conversionFor(DATETIME, to);
@ -293,9 +437,16 @@ public class DataTypeConversionTests extends ESTestCase {
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals(true, conversion.convert(DateUtils.asDateOnly(123456789101L)));
assertEquals(true, conversion.convert(DateUtils.asDateOnly(-123456789101L)));
assertEquals(false, conversion.convert(DateUtils.asDateOnly(0L)));
assertEquals(true, conversion.convert(asDateOnly(123456789101L)));
assertEquals(true, conversion.convert(asDateOnly(-123456789101L)));
assertEquals(false, conversion.convert(asDateOnly(0L)));
}
{
Conversion conversion = conversionFor(TIME, to);
assertNull(conversion.convert(null));
assertEquals(true, conversion.convert(asTimeOnly(123456789101L)));
assertEquals(true, conversion.convert(asTimeOnly(-123456789101L)));
assertEquals(false, conversion.convert(asTimeOnly(0L)));
}
{
Conversion conversion = conversionFor(DATETIME, to);
@ -342,20 +493,29 @@ public class DataTypeConversionTests extends ESTestCase {
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals(0, conversion.convert(DateUtils.asDateOnly(12345678L)));
assertEquals(86400000, conversion.convert(DateUtils.asDateOnly(123456789L)));
assertEquals(172800000, conversion.convert(DateUtils.asDateOnly(223456789L)));
assertEquals(-172800000, conversion.convert(DateUtils.asDateOnly(-123456789L)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(DateUtils.asDateOnly(Long.MAX_VALUE)));
assertEquals(0, conversion.convert(asDateOnly(12345678L)));
assertEquals(86400000, conversion.convert(asDateOnly(123456789L)));
assertEquals(172800000, conversion.convert(asDateOnly(223456789L)));
assertEquals(-172800000, conversion.convert(asDateOnly(-123456789L)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(asDateOnly(Long.MAX_VALUE)));
assertEquals("[9223372036828800000] out of [integer] range", e.getMessage());
}
{
Conversion conversion = conversionFor(TIME, to);
assertNull(conversion.convert(null));
assertEquals(123456, conversion.convert(asTimeOnly(123456L)));
assertEquals(77589101, conversion.convert(asTimeOnly(123456789101L)));
assertEquals(86276544, conversion.convert(asTimeOnly(-123456L)));
assertEquals(8810899, conversion.convert(asTimeOnly(-123456789101L)));
assertEquals(25975807, conversion.convert(asTimeOnly(Long.MAX_VALUE)));
}
{
Conversion conversion = conversionFor(DATETIME, to);
assertNull(conversion.convert(null));
assertEquals(12345678, conversion.convert(DateUtils.asDateTime(12345678L)));
assertEquals(223456789, conversion.convert(DateUtils.asDateTime(223456789L)));
assertEquals(-123456789, conversion.convert(DateUtils.asDateTime(-123456789L)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(DateUtils.asDateTime(Long.MAX_VALUE)));
assertEquals(12345678, conversion.convert(asDateTime(12345678L)));
assertEquals(223456789, conversion.convert(asDateTime(223456789L)));
assertEquals(-123456789, conversion.convert(asDateTime(-123456789L)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(asDateTime(Long.MAX_VALUE)));
assertEquals("[" + Long.MAX_VALUE + "] out of [integer] range", e.getMessage());
}
}
@ -374,17 +534,26 @@ public class DataTypeConversionTests extends ESTestCase {
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals((short) 0, conversion.convert(DateUtils.asDateOnly(12345678L)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(DateUtils.asDateOnly(123456789L)));
assertEquals((short) 0, conversion.convert(asDateOnly(12345678L)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(asDateOnly(123456789L)));
assertEquals("[86400000] out of [short] range", e.getMessage());
}
{
Conversion conversion = conversionFor(TIME, to);
assertNull(conversion.convert(null));
assertEquals((short) 12345, conversion.convert(asTimeOnly(12345L)));
Exception e1 = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(asTimeOnly(-123456789L)));
assertEquals("[49343211] out of [short] range", e1.getMessage());
Exception e2 = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(asTimeOnly(123456789L)));
assertEquals("[37056789] out of [short] range", e2.getMessage());
}
{
Conversion conversion = conversionFor(DATETIME, to);
assertNull(conversion.convert(null));
assertEquals((short) 12345, conversion.convert(DateUtils.asDateTime(12345L)));
assertEquals((short) -12345, conversion.convert(DateUtils.asDateTime(-12345L)));
assertEquals((short) 12345, conversion.convert(asDateTime(12345L)));
assertEquals((short) -12345, conversion.convert(asDateTime(-12345L)));
Exception e = expectThrows(SqlIllegalArgumentException.class,
() -> conversion.convert(DateUtils.asDateTime(Integer.MAX_VALUE)));
() -> conversion.convert(asDateTime(Integer.MAX_VALUE)));
assertEquals("[" + Integer.MAX_VALUE + "] out of [short] range", e.getMessage());
}
}
@ -403,17 +572,26 @@ public class DataTypeConversionTests extends ESTestCase {
{
Conversion conversion = conversionFor(DATE, to);
assertNull(conversion.convert(null));
assertEquals((byte) 0, conversion.convert(DateUtils.asDateOnly(12345678L)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(DateUtils.asDateOnly(123456789L)));
assertEquals((byte) 0, conversion.convert(asDateOnly(12345678L)));
Exception e = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(asDateOnly(123456789L)));
assertEquals("[86400000] out of [byte] range", e.getMessage());
}
{
Conversion conversion = conversionFor(TIME, to);
assertNull(conversion.convert(null));
assertEquals((byte) 123, conversion.convert(asTimeOnly(123L)));
Exception e1 = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(asTimeOnly(-123L)));
assertEquals("[86399877] out of [byte] range", e1.getMessage());
Exception e2 = expectThrows(SqlIllegalArgumentException.class, () -> conversion.convert(asTimeOnly(123456789L)));
assertEquals("[37056789] out of [byte] range", e2.getMessage());
}
{
Conversion conversion = conversionFor(DATETIME, to);
assertNull(conversion.convert(null));
assertEquals((byte) 123, conversion.convert(DateUtils.asDateTime(123L)));
assertEquals((byte) -123, conversion.convert(DateUtils.asDateTime(-123L)));
assertEquals((byte) 123, conversion.convert(asDateTime(123L)));
assertEquals((byte) -123, conversion.convert(asDateTime(-123L)));
Exception e = expectThrows(SqlIllegalArgumentException.class,
() -> conversion.convert(DateUtils.asDateTime(Integer.MAX_VALUE)));
() -> conversion.convert(asDateTime(Integer.MAX_VALUE)));
assertEquals("[" + Integer.MAX_VALUE + "] out of [byte] range", e.getMessage());
}
}
@ -452,10 +630,16 @@ public class DataTypeConversionTests extends ESTestCase {
// dates/datetimes and intervals
assertEquals(DATETIME, commonType(DATE, DATETIME));
assertEquals(DATETIME, commonType(DATETIME, DATE));
assertEquals(DATETIME, commonType(TIME, DATETIME));
assertEquals(DATETIME, commonType(DATETIME, TIME));
assertEquals(DATETIME, commonType(DATETIME, randomInterval()));
assertEquals(DATETIME, commonType(randomInterval(), DATETIME));
assertEquals(DATETIME, commonType(DATE, TIME));
assertEquals(DATETIME, commonType(TIME, DATE));
assertEquals(DATE, commonType(DATE, randomInterval()));
assertEquals(DATE, commonType(randomInterval(), DATE));
assertEquals(TIME, commonType(TIME, randomInterval()));
assertEquals(TIME, commonType(randomInterval(), TIME));
assertEquals(INTERVAL_YEAR_TO_MONTH, commonType(INTERVAL_YEAR_TO_MONTH, INTERVAL_MONTH));
assertEquals(INTERVAL_HOUR_TO_SECOND, commonType(INTERVAL_HOUR_TO_MINUTE, INTERVAL_HOUR_TO_SECOND));
@ -473,7 +657,7 @@ public class DataTypeConversionTests extends ESTestCase {
public void testConversionToUnsupported() {
Exception e = expectThrows(SqlIllegalArgumentException.class,
() -> conversionFor(INTEGER, UNSUPPORTED));
assertEquals("cannot convert from [INTEGER] to [UNSUPPORTED]", e.getMessage());
assertEquals("cannot convert from [integer] to [unsupported]", e.getMessage());
}
public void testStringToIp() {