diff --git a/docs/reference/sql/functions/date-time.asciidoc b/docs/reference/sql/functions/date-time.asciidoc index 6a9b0c6f8a8e3..b0be286f39c0a 100644 --- a/docs/reference/sql/functions/date-time.asciidoc +++ b/docs/reference/sql/functions/date-time.asciidoc @@ -500,18 +500,19 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[datePartDateTimeTzOffsetMinus] -------------------------------------------------- DATE_TRUNC( string_exp, <1> - datetime_exp) <2> + datetime_exp/interval_exp) <2> -------------------------------------------------- *Input*: -<1> string expression denoting the unit to which the date/datetime should be truncated to -<2> date/datetime expression +<1> string expression denoting the unit to which the date/datetime/interval should be truncated to +<2> date/datetime/interval expression -*Output*: datetime +*Output*: datetime/interval -*Description*: Truncate the date/datetime to the specified unit by setting all fields that are less significant than the specified +*Description*: Truncate the date/datetime/interval to the specified unit by setting all fields that are less significant than the specified one to zero (or one, for day, day of week and month). If any of the two arguments is `null` a `null` is returned. +If the first argument is `week` and the second argument is of `interval` type, an error is thrown since the `interval` data type doesn't support a `week` time unit. [cols="^,^"] |=== @@ -563,6 +564,21 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[truncateDateDecades] include-tagged::{sql-specs}/docs/docs.csv-spec[truncateDateQuarter] -------------------------------------------------- +[source, sql] +-------------------------------------------------- +include-tagged::{sql-specs}/docs/docs.csv-spec[truncateIntervalCenturies] +-------------------------------------------------- + +[source, sql] +-------------------------------------------------- +include-tagged::{sql-specs}/docs/docs.csv-spec[truncateIntervalHour] +-------------------------------------------------- + +[source, sql] +-------------------------------------------------- +include-tagged::{sql-specs}/docs/docs.csv-spec[truncateIntervalDay] +-------------------------------------------------- + [[sql-functions-datetime-day]] ==== `DAY_OF_MONTH/DOM/DAY` diff --git a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/util/DateUtils.java b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/util/DateUtils.java index 583d8de43f32d..a568e4039755f 100644 --- a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/util/DateUtils.java +++ b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/util/DateUtils.java @@ -34,7 +34,7 @@ public class DateUtils { public static final ZoneId UTC = ZoneId.of("Z"); public static final String EMPTY = ""; - + public static final DateTimeFormatter ISO_DATE_WITH_MILLIS = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(ISO_LOCAL_DATE) @@ -72,9 +72,9 @@ public class DateUtils { .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; + public static final int SECONDS_PER_MINUTE = 60; + public static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60; + public static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * 24; private DateUtils() {} @@ -82,7 +82,7 @@ public static String toString(Object value) { if (value == null) { return "null"; } - + if (value instanceof ZonedDateTime) { return ((ZonedDateTime) value).format(ISO_DATE_WITH_MILLIS); } diff --git a/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec index 16550c3e9144e..1dfc1497ebc76 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec @@ -499,6 +499,16 @@ DATE_TRUNC('week', '2019-09-04'::date) as dt_week, DATE_TRUNC('day', '2019-09-0 2000-01-01T00:00:00.000Z | 2000-01-01T00:00:00.000Z | 2010-01-01T00:00:00.000Z | 2019-01-01T00:00:00.000Z | 2019-07-01T00:00:00.000Z | 2019-09-01T00:00:00.000Z | 2019-09-02T00:00:00.000Z | 2019-09-04T00:00:00.000Z ; +selectDateTruncWithInterval +SELECT DATE_TRUNC('hour', INTERVAL '1 12:43:21' DAY TO SECONDS) as dt_hour, DATE_TRUNC('minute', INTERVAL '1 12:43:21' DAY TO SECONDS) as dt_min, +DATE_TRUNC('seconds', INTERVAL '1 12:43:21' DAY TO SECONDS) as dt_sec, DATE_TRUNC('ms', INTERVAL '1 12:43:21' DAY TO SECONDS)::string as dt_millis, +DATE_TRUNC('mcs', INTERVAL '1 12:43:21' DAY TO SECONDS)::string as dt_micro, DATE_TRUNC('nanoseconds', INTERVAL '1 12:43:21' DAY TO SECONDS)::string as dt_nano; + + dt_hour | dt_min | dt_sec | dt_millis | dt_micro | dt_nano +---------------+---------------+---------------+---------------+---------------+--------------- ++1 12:00:00 |+1 12:43:00 |+1 12:43:21 |+1 12:43:21 |+1 12:43:21 |+1 12:43:21 +; + selectDateTruncWithField schema::emp_no:i|birth_date:ts|dt_mil:ts|dt_cent:ts|dt_dec:ts|dt_year:ts|dt_quarter:ts|dt_month:ts|dt_week:ts|dt_day:ts SELECT emp_no, birth_date, DATE_TRUNC('millennium', birth_date) as dt_mil, DATE_TRUNC('centuries', birth_date) as dt_cent, @@ -585,6 +595,21 @@ SELECT emp_no, hire_date, DATE_TRUNC('quarter', hire_date) as dt FROM test_emp O 10076 | 1985-07-09 00:00:00.000Z | 1985-07-01 00:00:00.000Z ; +dateTruncOrderByWithInterval +schema::first_name:s|dt:ts|hire_date:ts|languages:byte +SELECT first_name, hire_date + DATE_TRUNC('centuries', CASE WHEN languages = 5 THEN INTERVAL '18-3' YEAR TO MONTH +WHEN languages = 4 THEN INTERVAL '108-4' YEAR TO MONTH WHEN languages = 3 THEN INTERVAL '212-3' YEAR TO MONTH +ELSE INTERVAL '318-6' YEAR TO MONTH END) as dt, hire_date, languages FROM test_emp WHERE emp_no <= 10006 ORDER BY dt NULLS LAST LIMIT 5; + + first_name | dt | hire_date | languages +--------------+--------------------------+--------------------------+----------- +Bezalel | 1985-11-21 00:00:00.000Z | 1985-11-21T00:00:00.000Z | 5 +Chirstian | 1986-12-01 00:00:00.000Z | 1986-12-01T00:00:00.000Z | 5 +Parto | 2086-08-28 00:00:00.000Z | 1986-08-28T00:00:00.000Z | 4 +Anneke | 2189-06-02 00:00:00.000Z | 1989-06-02T00:00:00.000Z | 3 +Georgi | 2286-06-26 00:00:00.000Z | 1986-06-26T00:00:00.000Z | 2 +; + dateTruncFilter schema::emp_no:i|hire_date:ts|dt:ts SELECT emp_no, hire_date, DATE_TRUNC('quarter', hire_date) as dt FROM test_emp WHERE DATE_TRUNC('quarter', hire_date) > '1994-07-01T00:00:00.000Z'::timestamp ORDER BY emp_no; @@ -601,6 +626,24 @@ SELECT emp_no, hire_date, DATE_TRUNC('quarter', hire_date) as dt FROM test_emp W 10093 | 1996-11-05 00:00:00.000Z | 1996-10-01 00:00:00.000Z ; +dateTruncFilterWithInterval +schema::first_name:s|hire_date:ts +SELECT first_name, hire_date FROM test_emp WHERE hire_date > '2090-03-05T10:11:22.123Z'::datetime - DATE_TRUNC('centuries', INTERVAL 190 YEARS) ORDER BY first_name DESC, hire_date ASC LIMIT 10; + + first_name | hire_date +---------------+------------------------- +null | 1990-06-20 00:00:00.000Z +null | 1990-12-05 00:00:00.000Z +null | 1991-09-01 00:00:00.000Z +null | 1992-01-03 00:00:00.000Z +null | 1994-02-17 00:00:00.000Z +Yongqiao | 1995-03-20 00:00:00.000Z +Yishay | 1990-10-20 00:00:00.000Z +Yinghua | 1990-12-25 00:00:00.000Z +Weiyi | 1993-02-14 00:00:00.000Z +Tuval | 1995-12-15 00:00:00.000Z +; + dateTruncGroupBy schema::count:l|dt:ts SELECT count(*) as count, DATE_TRUNC('decade', hire_date) dt FROM test_emp GROUP BY dt ORDER BY 2; @@ -611,6 +654,24 @@ SELECT count(*) as count, DATE_TRUNC('decade', hire_date) dt FROM test_emp GROUP 41 | 1990-01-01 00:00:00.000Z ; +dateTruncGroupByWithInterval +schema::count:l|dt:ts +SELECT count(*) as count, birth_date + DATE_TRUNC('hour', INTERVAL '1 12:43:21' DAY TO SECONDS) dt FROM test_emp GROUP BY dt ORDER BY 2 LIMIT 10; + + count | dt +--------+------------------------- +10 | null +1 | 1952-02-28 12:00:00.000Z +1 | 1952-04-20 12:00:00.000Z +1 | 1952-05-16 12:00:00.000Z +1 | 1952-06-14 12:00:00.000Z +1 | 1952-07-09 12:00:00.000Z +1 | 1952-08-07 12:00:00.000Z +1 | 1952-11-14 12:00:00.000Z +1 | 1952-12-25 12:00:00.000Z +1 | 1953-01-08 12:00:00.000Z +; + dateTruncHaving schema::gender:s|dt:ts SELECT gender, max(hire_date) AS dt FROM test_emp GROUP BY gender HAVING DATE_TRUNC('year', max(hire_date)) >= '1997-01-01T00:00:00.000Z'::timestamp ORDER BY 1; @@ -621,6 +682,16 @@ null | 1999-04-30 00:00:00.000Z F | 1997-05-19 00:00:00.000Z ; +// Awaits fix: https://github.com/elastic/elasticsearch/issues/53565 +dateTruncHavingWithInterval-Ignore +schema::gender:s|dt:ts +SELECT gender, max(hire_date) AS dt FROM test_emp GROUP BY gender HAVING max(hire_date) - DATE_TRUNC('hour', INTERVAL 1 YEARS) >= '1997-01-01T00:00:00.000Z'::timestamp ORDER BY 1; + + gender | dt +--------+------------------------- +null | 1999-04-30 00:00:00.000Z +; + selectDatePartWithDate SELECT DATE_PART('year', '2019-09-04'::date) as dp_years, DATE_PART('quarter', '2019-09-04'::date) as dp_quarter, DATE_PART('month', '2019-09-04'::date) as dp_month, DATE_PART('dayofyear', '2019-09-04'::date) as dp_doy, DATE_PART('day', '2019-09-04'::date) as dp_day, DATE_PART('week', '2019-09-04'::date) as dp_week, diff --git a/x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec index 8f92e1ce481ac..daa8aa573436b 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec @@ -2675,6 +2675,36 @@ SELECT DATETRUNC('quarters', CAST('2019-09-04' AS DATE)) AS quarter; // end::truncateDateQuarter ; +truncateIntervalCenturies +// tag::truncateIntervalCenturies +SELECT DATE_TRUNC('centuries', INTERVAL '199-5' YEAR TO MONTH) AS centuries; + + centuries +------------------ + +100-0 +// end::truncateIntervalCenturies +; + +truncateIntervalHour +// tag::truncateIntervalHour +SELECT DATE_TRUNC('hours', INTERVAL '17 22:13:12' DAY TO SECONDS) AS hour; + + hour +------------------ ++17 22:00:00 +// end::truncateIntervalHour +; + +truncateIntervalDay +// tag::truncateIntervalDay +SELECT DATE_TRUNC('days', INTERVAL '19 15:24:19' DAY TO SECONDS) AS day; + + day +------------------ ++19 00:00:00 +// end::truncateIntervalDay +; + constantDayOfWeek // tag::dayOfWeek SELECT DAY_OF_WEEK(CAST('2018-02-19T10:23:27Z' AS TIMESTAMP)) AS day; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/SqlTypeResolutions.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/SqlTypeResolutions.java index 1d5af2446fb05..63dd6ce2f756e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/SqlTypeResolutions.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/SqlTypeResolutions.java @@ -16,7 +16,7 @@ public final class SqlTypeResolutions { private SqlTypeResolutions() {} - + public static TypeResolution isDate(Expression e, String operationName, ParamOrdinal paramOrd) { return isType(e, SqlDataTypes::isDateBased, operationName, paramOrd, "date", "datetime"); } @@ -25,6 +25,10 @@ public static TypeResolution isDateOrTime(Expression e, String operationName, Pa return isType(e, SqlDataTypes::isDateOrTimeBased, operationName, paramOrd, "date", "time", "datetime"); } + public static TypeResolution isDateOrInterval(Expression e, String operationName, ParamOrdinal paramOrd) { + return isType(e, SqlDataTypes::isDateOrIntervalBased, operationName, paramOrd, "date", "datetime", "an interval data type"); + } + public static TypeResolution isNumericOrDate(Expression e, String operationName, ParamOrdinal paramOrd) { return isType(e, dt -> dt.isNumeric() || SqlDataTypes.isDateBased(dt), operationName, paramOrd, "date", "datetime", "numeric"); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BinaryDateTimeFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BinaryDateTimeFunction.java index c6855eccee8cd..8c6e4b22360db 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BinaryDateTimeFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BinaryDateTimeFunction.java @@ -19,7 +19,6 @@ import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString; import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder; -import static org.elasticsearch.xpack.sql.expression.SqlTypeResolutions.isDate; public abstract class BinaryDateTimeFunction extends BinaryScalarFunction { @@ -54,10 +53,7 @@ protected TypeResolution resolveType() { } } } - resolution = isDate(right(), sourceText(), Expressions.ParamOrdinal.SECOND); - if (resolution.unresolved()) { - return resolution; - } + return TypeResolution.TYPE_RESOLVED; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DatePart.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DatePart.java index 2349dc9dd9c9b..568adfac427de 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DatePart.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DatePart.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; import org.elasticsearch.xpack.ql.expression.Expression; +import org.elasticsearch.xpack.ql.expression.Expressions; import org.elasticsearch.xpack.ql.expression.function.scalar.BinaryScalarFunction; import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe; import org.elasticsearch.xpack.ql.tree.NodeInfo; @@ -23,6 +24,8 @@ import java.util.Set; import java.util.function.ToIntFunction; +import static org.elasticsearch.xpack.sql.expression.SqlTypeResolutions.isDate; + public class DatePart extends BinaryDateTimeFunction { public enum Part implements DateTimeField { @@ -84,6 +87,19 @@ public DataType dataType() { return DataTypes.INTEGER; } + @Override + protected TypeResolution resolveType() { + TypeResolution resolution = super.resolveType(); + if (resolution.unresolved()) { + return resolution; + } + resolution = isDate(right(), sourceText(), Expressions.ParamOrdinal.SECOND); + if (resolution.unresolved()) { + return resolution; + } + return TypeResolution.TYPE_RESOLVED; + } + @Override protected BinaryScalarFunction replaceChildren(Expression newDateTimePart, Expression newTimestamp) { return new DatePart(source(), newDateTimePart, newTimestamp, zoneId()); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTrunc.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTrunc.java index 9b336ddaed0e7..de033bfce6dac 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTrunc.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTrunc.java @@ -6,96 +6,158 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; import org.elasticsearch.xpack.ql.expression.Expression; +import org.elasticsearch.xpack.ql.expression.Expressions; import org.elasticsearch.xpack.ql.expression.function.scalar.BinaryScalarFunction; import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe; import org.elasticsearch.xpack.ql.tree.NodeInfo; import org.elasticsearch.xpack.ql.tree.Source; import org.elasticsearch.xpack.ql.type.DataType; import org.elasticsearch.xpack.ql.type.DataTypes; - +import org.elasticsearch.xpack.sql.expression.literal.interval.IntervalDayTime; +import org.elasticsearch.xpack.sql.expression.literal.interval.IntervalYearMonth; +import java.time.Duration; +import java.time.Period; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.UnaryOperator; +import static org.elasticsearch.xpack.ql.util.DateUtils.SECONDS_PER_DAY; +import static org.elasticsearch.xpack.ql.util.DateUtils.SECONDS_PER_HOUR; +import static org.elasticsearch.xpack.ql.util.DateUtils.SECONDS_PER_MINUTE; +import static org.elasticsearch.xpack.sql.expression.SqlTypeResolutions.isDateOrInterval; +import static org.elasticsearch.xpack.sql.type.SqlDataTypes.isInterval; + public class DateTrunc extends BinaryDateTimeFunction { public enum Part implements DateTimeField { MILLENNIUM(dt -> { - int year = dt.getYear(); - int firstYearOfMillenium = year - (year % 1000); - return dt - .with(ChronoField.YEAR, firstYearOfMillenium) - .with(ChronoField.MONTH_OF_YEAR, 1) - .with(ChronoField.DAY_OF_MONTH, 1) - .toLocalDate().atStartOfDay(dt.getZone()); - },"millennia"), + int year = dt.getYear(); + int firstYearOfMillennium = year - (year % 1000); + return dt + .with(ChronoField.YEAR, firstYearOfMillennium) + .with(ChronoField.MONTH_OF_YEAR, 1) + .with(ChronoField.DAY_OF_MONTH, 1) + .toLocalDate().atStartOfDay(dt.getZone()); + }, + idt -> new IntervalDayTime(Duration.ZERO, idt.dataType()), + iym -> { + Period period = iym.interval(); + int year = period.getYears(); + int firstYearOfMillennium = year - (year % 1000); + return new IntervalYearMonth(Period.ZERO.plusYears(firstYearOfMillennium), iym.dataType()); + }, "millennia"), CENTURY(dt -> { - int year = dt.getYear(); - int firstYearOfCentury = year - (year % 100); - return dt - .with(ChronoField.YEAR, firstYearOfCentury) - .with(ChronoField.MONTH_OF_YEAR, 1) - .with(ChronoField.DAY_OF_MONTH, 1) - .toLocalDate().atStartOfDay(dt.getZone()); - }, "centuries"), + int year = dt.getYear(); + int firstYearOfCentury = year - (year % 100); + return dt + .with(ChronoField.YEAR, firstYearOfCentury) + .with(ChronoField.MONTH_OF_YEAR, 1) + .with(ChronoField.DAY_OF_MONTH, 1) + .toLocalDate().atStartOfDay(dt.getZone()); + }, + idt -> new IntervalDayTime(Duration.ZERO, idt.dataType()), + iym -> { + Period period = iym.interval(); + int year = period.getYears(); + int firstYearOfCentury = year - (year % 100); + return new IntervalYearMonth(Period.ZERO.plusYears(firstYearOfCentury), iym.dataType()); + }, "centuries"), DECADE(dt -> { - int year = dt.getYear(); - int firstYearOfDecade = year - (year % 10); - return dt - .with(ChronoField.YEAR, firstYearOfDecade) - .with(ChronoField.MONTH_OF_YEAR, 1) - .with(ChronoField.DAY_OF_MONTH, 1) - .toLocalDate().atStartOfDay(dt.getZone()); - }, "decades"), - YEAR(dt -> dt - .with(ChronoField.MONTH_OF_YEAR, 1) - .with(ChronoField.DAY_OF_MONTH, 1) - .toLocalDate().atStartOfDay(dt.getZone()), - "years", "yy", "yyyy"), + int year = dt.getYear(); + int firstYearOfDecade = year - (year % 10); + return dt + .with(ChronoField.YEAR, firstYearOfDecade) + .with(ChronoField.MONTH_OF_YEAR, 1) + .with(ChronoField.DAY_OF_MONTH, 1) + .toLocalDate().atStartOfDay(dt.getZone()); + }, + idt -> new IntervalDayTime(Duration.ZERO, idt.dataType()), + iym -> { + Period period = iym.interval(); + int year = period.getYears(); + int firstYearOfDecade = year - (year % 10); + return new IntervalYearMonth(Period.ZERO.plusYears(firstYearOfDecade), iym.dataType()); + }, "decades"), + YEAR(dt -> { + return dt.with(ChronoField.MONTH_OF_YEAR, 1) + .with(ChronoField.DAY_OF_MONTH, 1) + .toLocalDate().atStartOfDay(dt.getZone()); + }, + idt -> new IntervalDayTime(Duration.ZERO, idt.dataType()), + iym -> { + Period period = iym.interval(); + int year = period.getYears(); + return new IntervalYearMonth(Period.ZERO.plusYears(year), iym.dataType()); + }, "years", "yy", "yyyy"), QUARTER(dt -> { - int month = dt.getMonthValue(); - int firstMonthOfQuarter = (((month - 1) / 3) * 3) + 1; - return dt - .with(ChronoField.MONTH_OF_YEAR, firstMonthOfQuarter) - .with(ChronoField.DAY_OF_MONTH, 1) - .toLocalDate().atStartOfDay(dt.getZone()); - }, "quarters", "qq", "q"), - MONTH(dt -> dt - .with(ChronoField.DAY_OF_MONTH, 1) - .toLocalDate().atStartOfDay(dt.getZone()), - "months", "mm", "m"), - WEEK(dt -> dt - .with(ChronoField.DAY_OF_WEEK, 1) - .toLocalDate().atStartOfDay(dt.getZone()), - "weeks", "wk", "ww"), - DAY(dt -> dt.toLocalDate().atStartOfDay(dt.getZone()), "days", "dd", "d"), + int month = dt.getMonthValue(); + int firstMonthOfQuarter = (((month - 1) / 3) * 3) + 1; + return dt + .with(ChronoField.MONTH_OF_YEAR, firstMonthOfQuarter) + .with(ChronoField.DAY_OF_MONTH, 1) + .toLocalDate().atStartOfDay(dt.getZone()); + }, + idt -> new IntervalDayTime(Duration.ZERO, (idt.dataType())), + iym -> { + Period period = iym.interval(); + int month = period.getMonths(); + int year = period.getYears(); + int firstMonthOfQuarter = (month / 3) * 3; + return new IntervalYearMonth(Period.ZERO.plusYears(year).plusMonths(firstMonthOfQuarter), iym.dataType()); + }, "quarters", "qq", "q"), + MONTH(dt -> { + return dt.with(ChronoField.DAY_OF_MONTH, 1) + .toLocalDate().atStartOfDay(dt.getZone()); + }, + idt -> new IntervalDayTime(Duration.ZERO, idt.dataType()), + iym -> iym, "months", "mm", "m"), + WEEK(dt -> { + return dt.with(ChronoField.DAY_OF_WEEK, 1) + .toLocalDate().atStartOfDay(dt.getZone()); + }, + idt -> new IntervalDayTime(Duration.ZERO, idt.dataType()), + iym -> iym, "weeks", "wk", "ww"), + DAY(dt -> dt.toLocalDate().atStartOfDay(dt.getZone()), + idt -> truncateIntervalSmallerThanWeek(idt, ChronoUnit.DAYS), + iym -> iym, "days", "dd", "d"), HOUR(dt -> { - int hour = dt.getHour(); - return dt.toLocalDate().atStartOfDay(dt.getZone()) - .with(ChronoField.HOUR_OF_DAY, hour); - }, "hours", "hh"), + int hour = dt.getHour(); + return dt.toLocalDate().atStartOfDay(dt.getZone()) + .with(ChronoField.HOUR_OF_DAY, hour); + }, + idt -> truncateIntervalSmallerThanWeek(idt, ChronoUnit.HOURS), + iym -> iym, "hours", "hh"), MINUTE(dt -> { - int hour = dt.getHour(); - int minute = dt.getMinute(); - return dt.toLocalDate().atStartOfDay(dt.getZone()) - .with(ChronoField.HOUR_OF_DAY, hour) - .with(ChronoField.MINUTE_OF_HOUR, minute); - }, "minutes", "mi", "n"), - SECOND(dt -> dt.with(ChronoField.NANO_OF_SECOND, 0), "seconds", "ss", "s"), + int hour = dt.getHour(); + int minute = dt.getMinute(); + return dt.toLocalDate().atStartOfDay(dt.getZone()) + .with(ChronoField.HOUR_OF_DAY, hour) + .with(ChronoField.MINUTE_OF_HOUR, minute); + }, + idt -> truncateIntervalSmallerThanWeek(idt, ChronoUnit.MINUTES), + iym -> iym, "minutes", "mi", "n"), + SECOND(dt -> dt.with(ChronoField.NANO_OF_SECOND, 0), + idt -> truncateIntervalSmallerThanWeek(idt, ChronoUnit.SECONDS), + iym -> iym, "seconds", "ss", "s"), MILLISECOND(dt -> { - int micros = dt.get(ChronoField.MICRO_OF_SECOND); - return dt.with(ChronoField.MILLI_OF_SECOND, (micros / 1000)); - }, "milliseconds", "ms"), + int micros = dt.get(ChronoField.MICRO_OF_SECOND); + return dt.with(ChronoField.MILLI_OF_SECOND, (micros / 1000)); + }, + idt -> truncateIntervalSmallerThanWeek(idt, ChronoUnit.MILLIS), + iym -> iym, "milliseconds", "ms"), MICROSECOND(dt -> { - int nanos = dt.getNano(); - return dt.with(ChronoField.MICRO_OF_SECOND, (nanos / 1000)); - }, "microseconds", "mcs"), - NANOSECOND(dt -> dt, "nanoseconds", "ns"); + int nanos = dt.getNano(); + return dt.with(ChronoField.MICRO_OF_SECOND, (nanos / 1000)); + }, + idt -> idt, iym -> iym, "microseconds", "mcs"), + NANOSECOND(dt -> dt, idt -> idt, iym -> iym, "nanoseconds", "ns"); private static final Map NAME_TO_PART; private static final List VALID_VALUES; @@ -105,11 +167,16 @@ public enum Part implements DateTimeField { VALID_VALUES = DateTimeField.initializeValidValues(values()); } - private UnaryOperator truncateFunction; + private UnaryOperator truncateFunctionIntervalYearMonth; + private UnaryOperator truncateFunctionZonedDateTime; + private UnaryOperator truncateFunctionIntervalDayTime; private Set aliases; - Part(UnaryOperator truncateFunction, String... aliases) { - this.truncateFunction = truncateFunction; + Part(UnaryOperator truncateFunctionZonedDateTime, UnaryOperator truncateFunctionIntervalDayTime, + UnaryOperator truncateFunctionIntervalYearMonth, String... aliases) { + this.truncateFunctionIntervalYearMonth = truncateFunctionIntervalYearMonth; + this.truncateFunctionZonedDateTime = truncateFunctionZonedDateTime; + this.truncateFunctionIntervalDayTime = truncateFunctionIntervalDayTime; this.aliases = Set.of(aliases); } @@ -127,7 +194,50 @@ public static Part resolve(String truncateTo) { } public ZonedDateTime truncate(ZonedDateTime dateTime) { - return truncateFunction.apply(dateTime); + return truncateFunctionZonedDateTime.apply(dateTime); + } + + public IntervalDayTime truncate(IntervalDayTime dateTime) { + return truncateFunctionIntervalDayTime.apply(dateTime); + } + + public IntervalYearMonth truncate(IntervalYearMonth dateTime) { + return truncateFunctionIntervalYearMonth.apply(dateTime); + } + + private static IntervalDayTime truncateIntervalSmallerThanWeek(IntervalDayTime r, ChronoUnit unit) { + Duration d = r.interval(); + int isNegative = 1; + if (d.isNegative()) { + d = d.negated(); + isNegative = -1; + } + long durationInSec = d.getSeconds(); + long day = durationInSec / SECONDS_PER_DAY; + durationInSec = durationInSec % SECONDS_PER_DAY; + long hour = durationInSec / SECONDS_PER_HOUR; + durationInSec = durationInSec % SECONDS_PER_HOUR; + long min = durationInSec / SECONDS_PER_MINUTE; + durationInSec = durationInSec % SECONDS_PER_MINUTE; + long sec = durationInSec; + long miliseccond = TimeUnit.NANOSECONDS.toMillis(d.getNano()); + Duration newDuration = Duration.ZERO; + if (unit.ordinal() <= ChronoUnit.DAYS.ordinal()) { + newDuration = newDuration.plusDays(day * isNegative); + } + if (unit.ordinal() <= ChronoUnit.HOURS.ordinal()) { + newDuration = newDuration.plusHours(hour * isNegative); + } + if (unit.ordinal() <= ChronoUnit.MINUTES.ordinal()) { + newDuration = newDuration.plusMinutes(min * isNegative); + } + if (unit.ordinal() <= ChronoUnit.SECONDS.ordinal()) { + newDuration = newDuration.plusSeconds(sec * isNegative); + } + if (unit.ordinal() <= ChronoUnit.MILLIS.ordinal()) { + newDuration = newDuration.plusMillis(miliseccond * isNegative); + } + return new IntervalDayTime(newDuration, r.dataType()); } } @@ -137,9 +247,25 @@ public DateTrunc(Source source, Expression truncateTo, Expression timestamp, Zon @Override public DataType dataType() { + if (isInterval(right().dataType())) { + return right().dataType(); + } return DataTypes.DATETIME; } + @Override + protected TypeResolution resolveType() { + TypeResolution resolution = super.resolveType(); + if (resolution.unresolved()) { + return resolution; + } + resolution = isDateOrInterval(right(), sourceText(), Expressions.ParamOrdinal.SECOND); + if (resolution.unresolved()) { + return resolution; + } + return TypeResolution.TYPE_RESOLVED; + } + @Override protected BinaryScalarFunction replaceChildren(Expression newTruncateTo, Expression newTimestamp) { return new DateTrunc(source(), newTruncateTo, newTimestamp, zoneId()); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTruncProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTruncProcessor.java index 23cc096878ef5..97379d3bc73d0 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTruncProcessor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTruncProcessor.java @@ -6,15 +6,18 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.xpack.ql.expression.gen.processor.Processor; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTrunc.Part; +import org.elasticsearch.xpack.ql.expression.gen.processor.Processor; +import org.elasticsearch.xpack.sql.expression.literal.interval.IntervalDayTime; +import org.elasticsearch.xpack.sql.expression.literal.interval.IntervalYearMonth; import java.io.IOException; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.List; +import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTrunc.Part; + public class DateTruncProcessor extends BinaryDateTimeProcessor { public static final String NAME = "dtrunc"; @@ -59,10 +62,21 @@ public static Object process(Object truncateTo, Object timestamp, ZoneId zoneId) } } - if (timestamp instanceof ZonedDateTime == false) { - throw new SqlIllegalArgumentException("A date/datetime is required; received [{}]", timestamp); + if (timestamp instanceof ZonedDateTime == false && timestamp instanceof IntervalYearMonth == false + && timestamp instanceof IntervalDayTime == false) { + throw new SqlIllegalArgumentException("A date/datetime/interval is required; received [{}]", timestamp); + } + if (truncateDateField == Part.WEEK && (timestamp instanceof IntervalDayTime || timestamp instanceof IntervalYearMonth)) { + throw new SqlIllegalArgumentException("Truncating intervals is not supported for {} units", truncateTo); + } + + if (timestamp instanceof ZonedDateTime) { + return truncateDateField.truncate(((ZonedDateTime) timestamp).withZoneSameInstant(zoneId)); + } else if (timestamp instanceof IntervalYearMonth) { + return truncateDateField.truncate((IntervalYearMonth) timestamp); + } else { + return truncateDateField.truncate((IntervalDayTime) timestamp); } - return truncateDateField.truncate(((ZonedDateTime) timestamp).withZoneSameInstant(zoneId)); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java index 82a41fb2024da..d741e01896afd 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java @@ -309,8 +309,11 @@ public static Integer dateDiff(String dateField, Object dateTime1, Object dateTi return (Integer) DateDiffProcessor.process(dateField, asDateTime(dateTime1), asDateTime(dateTime2) , ZoneId.of(tzId)); } - public static ZonedDateTime dateTrunc(String truncateTo, Object dateTime, String tzId) { - return (ZonedDateTime) DateTruncProcessor.process(truncateTo, asDateTime(dateTime) , ZoneId.of(tzId)); + public static Object dateTrunc(String truncateTo, Object dateTimeOrInterval, String tzId) { + if (dateTimeOrInterval instanceof IntervalDayTime || dateTimeOrInterval instanceof IntervalYearMonth) { + return DateTruncProcessor.process(truncateTo, dateTimeOrInterval, ZoneId.of(tzId)); + } + return DateTruncProcessor.process(truncateTo, asDateTime(dateTimeOrInterval), ZoneId.of(tzId)); } public static Integer datePart(String dateField, Object dateTime, String tzId) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/SqlDataTypes.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/SqlDataTypes.java index 616b2de40810a..d39c295b3101f 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/SqlDataTypes.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/type/SqlDataTypes.java @@ -255,6 +255,10 @@ public static boolean isDateOrTimeBased(DataType type) { return isDateBased(type) || isTimeBased(type); } + public static boolean isDateOrIntervalBased(DataType type) { + return isDateBased(type) || isInterval(type); + } + public static boolean isGeo(DataType type) { return type == GEO_POINT || type == GEO_SHAPE || type == SHAPE; } @@ -262,7 +266,6 @@ public static boolean isGeo(DataType type) { public static String format(DataType type) { return isDateOrTimeBased(type) ? "epoch_millis" : null; } - public static boolean isFromDocValuesOnly(DataType dataType) { return dataType == KEYWORD // because of ignore_above. Extracting this from _source wouldn't make sense diff --git a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt index ea522a0e44084..6e9490dda1a94 100644 --- a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt +++ b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt @@ -118,7 +118,7 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS Integer weekOfYear(Object, String) ZonedDateTime dateAdd(String, Integer, Object, String) Integer dateDiff(String, Object, Object, String) - ZonedDateTime dateTrunc(String, Object, String) + def dateTrunc(String, Object, String) Integer datePart(String, Object, String) IntervalDayTime intervalDayTime(String, String) IntervalYearMonth intervalYearMonth(String, String) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java index bb54f5c7c719f..d21668aa92e47 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java @@ -215,8 +215,8 @@ public void testExtractNonDateTime() { public void testDateTruncInvalidArgs() { assertEquals("1:8: first argument of [DATE_TRUNC(int, date)] must be [string], found value [int] type [integer]", error("SELECT DATE_TRUNC(int, date) FROM test")); - assertEquals("1:8: second argument of [DATE_TRUNC(keyword, keyword)] must be [date or datetime], found value [keyword] " + - "type [keyword]", error("SELECT DATE_TRUNC(keyword, keyword) FROM test")); + assertEquals("1:8: second argument of [DATE_TRUNC(keyword, keyword)] must be [date, datetime or an interval data type]," + + " found value [keyword] type [keyword]", error("SELECT DATE_TRUNC(keyword, keyword) FROM test")); assertEquals("1:8: first argument of [DATE_TRUNC('invalid', keyword)] must be one of [MILLENNIUM, CENTURY, DECADE, " + "" + "YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND, MICROSECOND, NANOSECOND] " + "or their aliases; found value ['invalid']", @@ -535,11 +535,11 @@ public void testUnsupportedTypeInFilter() { assertEquals("1:26: Cannot use field [unsupported] with unsupported type [ip_range]", error("SELECT * FROM test WHERE unsupported > 1")); } - + public void testValidRootFieldWithUnsupportedChildren() { accept("SELECT x FROM test"); } - + public void testUnsupportedTypeInHierarchy() { assertEquals("1:8: Cannot use field [x.y.z.w] with unsupported type [foobar] in hierarchy (field [y])", error("SELECT x.y.z.w FROM test")); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTruncProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTruncProcessorTests.java index 9c7f11499a9dd..2d05ddfffb3be 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTruncProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTruncProcessorTests.java @@ -11,18 +11,29 @@ import org.elasticsearch.xpack.ql.expression.Literal; import org.elasticsearch.xpack.ql.expression.gen.processor.ConstantProcessor; import org.elasticsearch.xpack.ql.tree.Source; +import org.elasticsearch.xpack.ql.type.DataType; import org.elasticsearch.xpack.sql.AbstractSqlWireSerializingTestCase; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.literal.interval.IntervalDayTime; +import org.elasticsearch.xpack.sql.expression.literal.interval.IntervalYearMonth; +import org.elasticsearch.xpack.sql.proto.StringUtils; +import org.elasticsearch.xpack.sql.type.SqlDataTypes; import org.elasticsearch.xpack.sql.util.DateUtils; +import java.time.Duration; +import java.time.Period; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.temporal.TemporalAmount; import static org.elasticsearch.xpack.ql.expression.Literal.NULL; import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.l; import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.randomDatetimeLiteral; +import static org.elasticsearch.xpack.ql.tree.Source.EMPTY; import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.dateTime; import static org.elasticsearch.xpack.sql.proto.StringUtils.ISO_DATE_WITH_NANOS; +import static org.elasticsearch.xpack.sql.type.SqlDataTypes.INTERVAL_DAY_TO_SECOND; +import static org.elasticsearch.xpack.sql.type.SqlDataTypes.INTERVAL_YEAR_TO_MONTH; public class DateTruncProcessorTests extends AbstractSqlWireSerializingTestCase { @@ -57,13 +68,18 @@ protected DateTruncProcessor mutateInstance(DateTruncProcessor instance) { } public void testInvalidInputs() { + TemporalAmount period = Period.ofYears(2018).plusMonths(11); + Literal yearToMonth = intervalLiteral(period, INTERVAL_YEAR_TO_MONTH); + TemporalAmount duration = Duration.ofDays(42).plusHours(12).plusMinutes(23).plusSeconds(12).plusNanos(143000000); + Literal dayToSecond = intervalLiteral(duration, INTERVAL_DAY_TO_SECOND); + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, () -> new DateTrunc(Source.EMPTY, l(5), randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); assertEquals("A string is required; received [5]", siae.getMessage()); siae = expectThrows(SqlIllegalArgumentException.class, () -> new DateTrunc(Source.EMPTY, l("days"), l("foo"), randomZone()).makePipe().asProcessor().process(null)); - assertEquals("A date/datetime is required; received [foo]", siae.getMessage()); + assertEquals("A date/datetime/interval is required; received [foo]", siae.getMessage()); siae = expectThrows(SqlIllegalArgumentException.class, () -> new DateTrunc(Source.EMPTY, l("invalid"), randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); @@ -75,6 +91,16 @@ public void testInvalidInputs() { () -> new DateTrunc(Source.EMPTY, l("dacede"), randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); assertEquals("Received value [dacede] is not valid date part for truncation; did you mean [decade, decades]?", siae.getMessage()); + + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new DateTrunc(Source.EMPTY, l("weeks"), yearToMonth, null).makePipe().asProcessor().process(null)); + assertEquals("Truncating intervals is not supported for weeks units", + siae.getMessage()); + + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new DateTrunc(Source.EMPTY, l("week"), dayToSecond, null).makePipe().asProcessor().process(null)); + assertEquals("Truncating intervals is not supported for week units", + siae.getMessage()); } public void testWithNulls() { @@ -86,6 +112,10 @@ public void testWithNulls() { public void testTruncation() { ZoneId zoneId = ZoneId.of("Etc/GMT-10"); Literal dateTime = l(dateTime(2019, 9, 3, 18, 10, 37, 123456789)); + TemporalAmount period = Period.ofYears(2019).plusMonths(10); + Literal yearToMonth = intervalLiteral(period, INTERVAL_YEAR_TO_MONTH); + TemporalAmount duration = Duration.ofDays(105).plusHours(2).plusMinutes(45).plusSeconds(55).plusNanos(123456789); + Literal dayToSecond = intervalLiteral(duration, INTERVAL_DAY_TO_SECOND); assertEquals("2000-01-01T00:00:00.000+10:00", DateUtils.toString((ZonedDateTime) new DateTrunc(Source.EMPTY, l("millennia"), dateTime, zoneId) @@ -129,6 +159,86 @@ public void testTruncation() { assertEquals("2019-09-04T04:10:37.123456789+10:00", toString((ZonedDateTime) new DateTrunc(Source.EMPTY, l("nanoseconds"), dateTime, zoneId) .makePipe().asProcessor().process(null))); + + assertEquals("+2000-0", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("millennia"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2000-0", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("CENTURY"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2010-0", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("decades"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-0", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("years"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-9", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("quarters"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-10", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("month"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-10", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("days"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-10", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("hh"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-10", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("mi"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-10", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("second"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-10", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("ms"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-10", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("mcs"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + assertEquals("+2019-10", + toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("nanoseconds"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + + assertEquals("+0 00:00:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("millennia"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+0 00:00:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("CENTURY"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+0 00:00:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("decades"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+0 00:00:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("years"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+0 00:00:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("quarters"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+0 00:00:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("month"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+105 00:00:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("days"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+105 02:00:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("hh"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+105 02:45:00", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("mi"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+105 02:45:55", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("second"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+105 02:45:55.123", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("ms"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+105 02:45:55.123", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("microseconds"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + assertEquals("+105 02:45:55.123", + toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("nanoseconds"), dayToSecond, null) + .makePipe().asProcessor().process(null))); } public void testTruncationEdgeCases() { @@ -152,9 +262,58 @@ public void testTruncationEdgeCases() { assertEquals("-1234-08-29T00:00:00.000+10:00", DateUtils.toString((ZonedDateTime) new DateTrunc(Source.EMPTY, l("week"), dateTime, zoneId) .makePipe().asProcessor().process(null))); + + Literal yearToMonth = intervalLiteral(Period.ofYears(-12523).minusMonths(10), INTERVAL_YEAR_TO_MONTH); + assertEquals("-12000-0", toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("millennia"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + + yearToMonth = intervalLiteral(Period.ofYears(-32543).minusMonths(10), INTERVAL_YEAR_TO_MONTH); + assertEquals("-32500-0", toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("centuries"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + + yearToMonth = intervalLiteral(Period.ofYears(-24321).minusMonths(10), INTERVAL_YEAR_TO_MONTH); + assertEquals("-24320-0", toString((IntervalYearMonth) new DateTrunc(Source.EMPTY, l("decades"), yearToMonth, null) + .makePipe().asProcessor().process(null))); + + Literal dayToSecond = intervalLiteral(Duration.ofDays(-435).minusHours(23).minusMinutes(45).minusSeconds(55).minusNanos(123000000), + INTERVAL_DAY_TO_SECOND); + assertEquals("-435 00:00:00", toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("days"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + + dayToSecond = intervalLiteral(Duration.ofDays(-4231).minusHours(23).minusMinutes(45).minusSeconds(55).minusNanos(234000000), + INTERVAL_DAY_TO_SECOND); + assertEquals("-4231 23:00:00", toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("hh"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + + dayToSecond = intervalLiteral(Duration.ofDays(-124).minusHours(0).minusMinutes(59).minusSeconds(11).minusNanos(564000000), + INTERVAL_DAY_TO_SECOND); + assertEquals("-124 00:59:00", toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("mi"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + + dayToSecond = intervalLiteral(Duration.ofDays(-534).minusHours(23).minusMinutes(59).minusSeconds(59).minusNanos(245000000), + INTERVAL_DAY_TO_SECOND); + assertEquals("-534 23:59:59", toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("seconds"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + + dayToSecond = intervalLiteral(Duration.ofDays(-127).minusHours(17).minusMinutes(59).minusSeconds(59).minusNanos(987654321), + INTERVAL_DAY_TO_SECOND); + assertEquals("-127 17:59:59.987", toString((IntervalDayTime) new DateTrunc(Source.EMPTY, l("ms"), dayToSecond, null) + .makePipe().asProcessor().process(null))); + } + private String toString(IntervalYearMonth intervalYearMonth) { + return StringUtils.toString(intervalYearMonth); } + private String toString(IntervalDayTime intervalDayTime) { + return StringUtils.toString(intervalDayTime); + } private String toString(ZonedDateTime dateTime) { return ISO_DATE_WITH_NANOS.format(dateTime); } + + private static Literal intervalLiteral(TemporalAmount value, DataType intervalType) { + Object interval = value instanceof Period ? new IntervalYearMonth((Period) value, intervalType) + : new IntervalDayTime((Duration) value, intervalType); + return new Literal(EMPTY, interval, SqlDataTypes.fromJava(interval)); + } }