diff --git a/core/src/main/java/org/opensearch/sql/data/model/ExprDatetimeValue.java b/core/src/main/java/org/opensearch/sql/data/model/ExprDatetimeValue.java index 02a584fe5b..f5f80f133f 100644 --- a/core/src/main/java/org/opensearch/sql/data/model/ExprDatetimeValue.java +++ b/core/src/main/java/org/opensearch/sql/data/model/ExprDatetimeValue.java @@ -33,7 +33,7 @@ public class ExprDatetimeValue extends AbstractExprValue { static { FORMATTER_VARIABLE_NANOS = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd HH:mm:ss") + .appendPattern("uuuu-MM-dd HH:mm:ss[xxx]") .appendFraction( ChronoField.NANO_OF_SECOND, MIN_FRACTION_SECONDS, diff --git a/core/src/main/java/org/opensearch/sql/expression/DSL.java b/core/src/main/java/org/opensearch/sql/expression/DSL.java index a094d2e487..56642a1869 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -271,10 +271,18 @@ public FunctionExpression adddate(Expression... expressions) { return function(BuiltinFunctionName.ADDDATE, expressions); } + public FunctionExpression convert_tz(Expression... expressions) { + return function(BuiltinFunctionName.CONVERT_TZ, expressions); + } + public FunctionExpression date(Expression... expressions) { return function(BuiltinFunctionName.DATE, expressions); } + public FunctionExpression datetime(Expression... expressions) { + return function(BuiltinFunctionName.DATETIME, expressions); + } + public FunctionExpression date_add(Expression... expressions) { return function(BuiltinFunctionName.DATE_ADD, expressions); } diff --git a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java index 732585d99a..84716a425a 100644 --- a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java +++ b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java @@ -22,20 +22,24 @@ import static org.opensearch.sql.utils.DateTimeFormatters.DATE_FORMATTER_SHORT_YEAR; import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_LONG_YEAR; import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_SHORT_YEAR; +import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_STRICT_WITH_TZ; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DecimalFormat; +import java.time.DateTimeException; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.format.TextStyle; import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import lombok.experimental.UtilityClass; @@ -50,11 +54,13 @@ import org.opensearch.sql.data.model.ExprTimestampValue; import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.data.type.ExprCoreType; +import org.opensearch.sql.exception.ExpressionEvaluationException; import org.opensearch.sql.expression.function.BuiltinFunctionName; import org.opensearch.sql.expression.function.BuiltinFunctionRepository; import org.opensearch.sql.expression.function.DefaultFunctionResolver; import org.opensearch.sql.expression.function.FunctionName; import org.opensearch.sql.expression.function.FunctionResolver; +import org.opensearch.sql.utils.DateTimeUtils; /** * The definition of date and time functions. @@ -63,7 +69,6 @@ */ @UtilityClass public class DateTimeFunction { - // The number of days from year zero to year 1970. private static final Long DAYS_0000_TO_1970 = (146097 * 5L) - (30L * 365L + 7L); @@ -78,7 +83,9 @@ public class DateTimeFunction { */ public void register(BuiltinFunctionRepository repository) { repository.register(adddate()); + repository.register(convert_tz()); repository.register(date()); + repository.register(datetime()); repository.register(date_add()); repository.register(date_sub()); repository.register(day()); @@ -214,6 +221,21 @@ private DefaultFunctionResolver adddate() { return add_date(BuiltinFunctionName.ADDDATE.getName()); } + /** + * Converts date/time from a specified timezone to another specified timezone. + * The supported signatures: + * (DATETIME, STRING, STRING) -> DATETIME + * (STRING, STRING, STRING) -> DATETIME + */ + private DefaultFunctionResolver convert_tz() { + return define(BuiltinFunctionName.CONVERT_TZ.getName(), + impl(nullMissingHandling(DateTimeFunction::exprConvertTZ), + DATETIME, DATETIME, STRING, STRING), + impl(nullMissingHandling(DateTimeFunction::exprConvertTZ), + DATETIME, STRING, STRING, STRING) + ); + } + /** * Extracts the date part of a date and time value. * Also to construct a date type. The supported signatures: @@ -227,6 +249,21 @@ private DefaultFunctionResolver date() { impl(nullMissingHandling(DateTimeFunction::exprDate), DATE, TIMESTAMP)); } + /** + * Specify a datetime with time zone field and a time zone to convert to. + * Returns a local date time. + * (STRING, STRING) -> DATETIME + * (STRING) -> DATETIME + */ + private FunctionResolver datetime() { + return define(BuiltinFunctionName.DATETIME.getName(), + impl(nullMissingHandling(DateTimeFunction::exprDateTime), + DATETIME, STRING, STRING), + impl(nullMissingHandling(DateTimeFunction::exprDateTimeNoTimezone), + DATETIME, STRING) + ); + } + private DefaultFunctionResolver date_add() { return add_date(BuiltinFunctionName.DATE_ADD.getName()); } @@ -570,6 +607,42 @@ private ExprValue exprAddDateDays(ExprValue date, ExprValue days) { : exprValue); } + /** + * CONVERT_TZ function implementation for ExprValue. + * Returns null for time zones outside of +13:00 and -12:00. + * + * @param startingDateTime ExprValue of DateTime that is being converted from + * @param fromTz ExprValue of time zone, representing the time to convert from. + * @param toTz ExprValue of time zone, representing the time to convert to. + * @return DateTime that has been converted to the to_tz timezone. + */ + private ExprValue exprConvertTZ(ExprValue startingDateTime, ExprValue fromTz, ExprValue toTz) { + if (startingDateTime.type() == ExprCoreType.STRING) { + startingDateTime = exprDateTimeNoTimezone(startingDateTime); + } + try { + ZoneId convertedFromTz = ZoneId.of(fromTz.stringValue()); + ZoneId convertedToTz = ZoneId.of(toTz.stringValue()); + + // isValidMySqlTimeZoneId checks if the timezone is within the range accepted by + // MySQL standard. + if (!DateTimeUtils.isValidMySqlTimeZoneId(convertedFromTz) + || !DateTimeUtils.isValidMySqlTimeZoneId(convertedToTz)) { + return ExprNullValue.of(); + } + ZonedDateTime zonedDateTime = + startingDateTime.datetimeValue().atZone(convertedFromTz); + return new ExprDatetimeValue( + zonedDateTime.withZoneSameInstant(convertedToTz).toLocalDateTime()); + + + // Catches exception for invalid timezones. + // ex. "+0:00" is an invalid timezone and would result in this exception being thrown. + } catch (ExpressionEvaluationException | DateTimeException e) { + return ExprNullValue.of(); + } + } + /** * Date implementation for ExprValue. * @@ -584,6 +657,62 @@ private ExprValue exprDate(ExprValue exprValue) { } } + /** + * DateTime implementation for ExprValue. + * + * @param dateTime ExprValue of String type. + * @param timeZone ExprValue of String type (or null). + * @return ExprValue of date type. + */ + private ExprValue exprDateTime(ExprValue dateTime, ExprValue timeZone) { + String defaultTimeZone = TimeZone.getDefault().getID(); + + + try { + LocalDateTime ldtFormatted = + LocalDateTime.parse(dateTime.stringValue(), DATE_TIME_FORMATTER_STRICT_WITH_TZ); + if (timeZone.isNull()) { + return new ExprDatetimeValue(ldtFormatted); + } + + // Used if datetime field is invalid format. + } catch (DateTimeParseException e) { + return ExprNullValue.of(); + } + + ExprValue convertTZResult; + ExprDatetimeValue ldt; + String toTz; + + try { + ZonedDateTime zdtWithZoneOffset = + ZonedDateTime.parse(dateTime.stringValue(), DATE_TIME_FORMATTER_STRICT_WITH_TZ); + ZoneId fromTZ = zdtWithZoneOffset.getZone(); + + ldt = new ExprDatetimeValue(zdtWithZoneOffset.toLocalDateTime()); + toTz = String.valueOf(fromTZ); + } catch (DateTimeParseException e) { + ldt = new ExprDatetimeValue(dateTime.stringValue()); + toTz = defaultTimeZone; + } + convertTZResult = exprConvertTZ( + ldt, + new ExprStringValue(toTz), + timeZone); + + return convertTZResult; + } + + /** + * DateTime implementation for ExprValue without a timezone to convert to. + * + * @param dateTime ExprValue of String type. + * @return ExprValue of date type. + */ + private ExprValue exprDateTimeNoTimezone(ExprValue dateTime) { + return exprDateTime(dateTime, ExprNullValue.of()); + } + /** * Name of the Weekday implementation for ExprValue. * diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index 88de359bf2..b5c40b7d78 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -58,7 +58,9 @@ public enum BuiltinFunctionName { * Date and Time Functions. */ ADDDATE(FunctionName.of("adddate")), + CONVERT_TZ(FunctionName.of("convert_tz")), DATE(FunctionName.of("date")), + DATETIME(FunctionName.of("datetime")), DATE_ADD(FunctionName.of("date_add")), DATE_SUB(FunctionName.of("date_sub")), DAY(FunctionName.of("day")), diff --git a/core/src/main/java/org/opensearch/sql/utils/DateTimeFormatters.java b/core/src/main/java/org/opensearch/sql/utils/DateTimeFormatters.java index ecb52f7f98..2efdbb3755 100644 --- a/core/src/main/java/org/opensearch/sql/utils/DateTimeFormatters.java +++ b/core/src/main/java/org/opensearch/sql/utils/DateTimeFormatters.java @@ -162,4 +162,10 @@ public class DateTimeFormatters { .appendPattern("MMddHHmmss") .toFormatter() .withResolverStyle(ResolverStyle.STRICT); + + public static final DateTimeFormatter DATE_TIME_FORMATTER_STRICT_WITH_TZ = + new DateTimeFormatterBuilder() + .appendPattern("uuuu-MM-dd HH:mm:ss[xxx]") + .toFormatter() + .withResolverStyle(ResolverStyle.STRICT); } diff --git a/core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java b/core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java index 8aaa082a09..fbcf7deca4 100644 --- a/core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java +++ b/core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java @@ -6,6 +6,7 @@ package org.opensearch.sql.utils; import java.time.Instant; +import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import lombok.experimental.UtilityClass; @@ -84,4 +85,34 @@ public static long roundYear(long utcMillis, int interval) { return initDateTime.plusYears(yearToAdd).toInstant().toEpochMilli(); } + + /** + * isValidMySqlTimeZoneId for timezones which match timezone the range set by MySQL. + * + * @param zone ZoneId of ZoneId type. + * @return Boolean. + */ + public Boolean isValidMySqlTimeZoneId(ZoneId zone) { + String timeZoneMax = "+14:00"; + String timeZoneMin = "-13:59"; + String timeZoneZero = "+00:00"; + + ZoneId maxTz = ZoneId.of(timeZoneMax); + ZoneId minTz = ZoneId.of(timeZoneMin); + ZoneId defaultTz = ZoneId.of(timeZoneZero); + + ZonedDateTime defaultDateTime = LocalDateTime.of(2000, 1, 2, 12, 0).atZone(defaultTz); + + ZonedDateTime maxTzValidator = + defaultDateTime.withZoneSameInstant(maxTz).withZoneSameLocal(defaultTz); + ZonedDateTime minTzValidator = + defaultDateTime.withZoneSameInstant(minTz).withZoneSameLocal(defaultTz); + ZonedDateTime passedTzValidator = + defaultDateTime.withZoneSameInstant(zone).withZoneSameLocal(defaultTz); + + return (passedTzValidator.isBefore(maxTzValidator) + || passedTzValidator.isEqual(maxTzValidator)) + && (passedTzValidator.isAfter(minTzValidator) + || passedTzValidator.isEqual(minTzValidator)); + } } diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/ConvertTZTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/ConvertTZTest.java new file mode 100644 index 0000000000..0da651fb54 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/ConvertTZTest.java @@ -0,0 +1,183 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.sql.expression.datetime; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opensearch.sql.data.model.ExprValueUtils.nullValue; +import static org.opensearch.sql.data.type.ExprCoreType.DATETIME; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.sql.data.model.ExprDatetimeValue; +import org.opensearch.sql.data.model.ExprValue; +import org.opensearch.sql.expression.DSL; +import org.opensearch.sql.expression.Expression; +import org.opensearch.sql.expression.ExpressionTestBase; +import org.opensearch.sql.expression.FunctionExpression; +import org.opensearch.sql.expression.env.Environment; + + + +@ExtendWith(MockitoExtension.class) +class ConvertTZTest extends ExpressionTestBase { + + @Mock + Environment env; + + @Test + public void invalidDate() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2021-04-31 10:00:00")), + DSL.literal("+00:00"), + DSL.literal("+00:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void conversionFromNoOffset() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("+00:00"), + DSL.literal("+10:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2008-05-16 08:00:00"), expr.valueOf(env)); + } + + @Test + public void conversionToInvalidInput3Over() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("+00:00"), + DSL.literal("+16:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void conversionToInvalidInput3Under() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("+00:00"), + DSL.literal("-16:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void conversionFromPositiveToPositive() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("+15:00"), + DSL.literal("+01:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void invalidInput2Under() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("-15:00"), + DSL.literal("+01:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void invalidInput3Over() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("-12:00"), + DSL.literal("+15:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void conversionToPositiveEdge() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("+00:00"), + DSL.literal("+14:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2008-05-16 12:00:00"), expr.valueOf(env)); + } + + @Test + public void conversionToNegativeEdge() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("+00:01"), + DSL.literal("-13:59")); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2008-05-15 08:00:00"), expr.valueOf(env)); + } + + @Test + public void invalidInput2() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("+)()"), + DSL.literal("+12:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void invalidInput3() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2008-05-15 22:00:00")), + DSL.literal("+00:00"), + DSL.literal("test")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void invalidInput1() { + FunctionExpression expr = dsl.convert_tz( + DSL.literal("test"), + DSL.literal("+00:00"), + DSL.literal("+00:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void invalidDateFeb30() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2021-02-30 10:00:00")), + DSL.literal("+00:00"), + DSL.literal("+00:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void invalidDateApril31() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2021-04-31 10:00:00")), + DSL.literal("+00:00"), + DSL.literal("+00:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void invalidMonth13() { + FunctionExpression expr = dsl.convert_tz(dsl.datetime( + DSL.literal("2021-13-03 10:00:00")), + DSL.literal("+00:00"), + DSL.literal("+00:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } +} diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeTest.java new file mode 100644 index 0000000000..7728a1cac3 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeTest.java @@ -0,0 +1,114 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.sql.expression.datetime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opensearch.sql.data.model.ExprValueUtils.nullValue; +import static org.opensearch.sql.data.type.ExprCoreType.DATETIME; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.sql.data.model.ExprDatetimeValue; +import org.opensearch.sql.data.model.ExprValue; +import org.opensearch.sql.expression.DSL; +import org.opensearch.sql.expression.Expression; +import org.opensearch.sql.expression.ExpressionTestBase; +import org.opensearch.sql.expression.FunctionExpression; +import org.opensearch.sql.expression.env.Environment; + + + +@ExtendWith(MockitoExtension.class) +class DateTimeTest extends ExpressionTestBase { + + @Mock + Environment env; + + @Test + public void noTimeZoneNoField2() { + FunctionExpression expr = dsl.datetime(DSL.literal("2008-05-15 22:00:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2008-05-15 22:00:00"), expr.valueOf(env)); + } + + @Test + public void positiveTimeZoneNoField2() { + FunctionExpression expr = dsl.datetime(DSL.literal("2008-05-15 22:00:00+01:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2008-05-15 22:00:00"), expr.valueOf(env)); + } + + @Test + public void positiveField1WrittenField2() { + FunctionExpression expr = dsl.datetime(DSL.literal("2008-05-15 22:00:00+01:00"), + DSL.literal("America/Los_Angeles")); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2008-05-15 14:00:00"), expr.valueOf(env)); + } + + // When no timezone argument is passed inside the datetime field, it assumes local time. + @Test + public void localDateTimeConversion() { + // needs to work for all time zones because it defaults to local timezone. + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String dt = "2008-05-15 22:00:00"; + String timeZone = "America/Los_Angeles"; + LocalDateTime timeConverted = LocalDateTime.parse(dt, formatter); + ZonedDateTime timeZoneLocal = timeConverted.atZone(ZoneId.of(TimeZone.getDefault().getID())) + .withZoneSameInstant(ZoneId.of(timeZone)); + FunctionExpression expr = dsl.datetime(DSL.literal(dt), + DSL.literal(timeZone)); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue(timeZoneLocal.toLocalDateTime()), expr.valueOf(env)); + } + + @Test + public void negativeField1WrittenField2() { + FunctionExpression expr = dsl.datetime(DSL.literal("2008-05-15 22:00:00-11:00"), + DSL.literal("America/Los_Angeles")); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2008-05-16 02:00:00"), expr.valueOf(env)); + } + + @Test + public void negativeField1PositiveField2() { + FunctionExpression expr = dsl.datetime(DSL.literal("2008-05-15 22:00:00-12:00"), + DSL.literal("+15:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void twentyFourHourDifference() { + FunctionExpression expr = dsl.datetime(DSL.literal("2008-05-15 22:00:00-14:00"), + DSL.literal("+10:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void negativeToNull() { + FunctionExpression expr = dsl.datetime(DSL.literal("2008-05-15 22:00:00-11:00"), + DSL.literal(nullValue())); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } + + @Test + public void invalidDate() { + FunctionExpression expr = dsl.datetime(DSL.literal("2008-04-31 22:00:00-11:00")); + assertEquals(DATETIME, expr.type()); + assertEquals(nullValue(), expr.valueOf(env)); + } +} diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index bf791b4925..eb49195ff1 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -859,6 +859,104 @@ Example:: +------------------------------------------------+----------------------------------+------------------------------------------------+ +CONVERT_TZ +---------- + +Description +>>>>>>>>>>> + +Usage: convert_tz(datetime, from_timezone, to_timezone) constructs a datetime object converted from the from_timezone to the to_timezone. + +Argument type: DATETIME, STRING, STRING + +Return type: DATETIME + +Example:: + + os> SELECT CONVERT_TZ('2008-12-25 05:30:00', '+00:00', 'America/Los_Angeles') + fetched rows / total rows = 1/1 + +----------------------------------------------------------------------+ + | CONVERT_TZ('2008-12-25 05:30:00', '+00:00', 'America/Los_Angeles') | + |----------------------------------------------------------------------| + | 2008-12-24 21:30:00 | + +----------------------------------------------------------------------+ + + + os> SELECT CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "-10:00") + fetched rows / total rows = 1/1 + +---------------------------------------------------------+ + | CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "-10:00") | + |---------------------------------------------------------| + | 2010-10-09 23:10:10 | + +---------------------------------------------------------+ + +When the datedate, or either of the two time zone fields are invalid format, then the result is null. In this example any datetime that is not will result in null. +Example:: + + os> SELECT CONVERT_TZ("test", "+01:00", "-10:00") + fetched rows / total rows = 1/1 + +------------------------------------------+ + | CONVERT_TZ("test", "+01:00", "-10:00") | + |------------------------------------------| + | null | + +------------------------------------------+ + +When the datetime, or either of the two time zone fields are invalid format, then the result is null. In this example any timezone that is not <+HH:mm> or <-HH:mm> will result in null. +Example:: + + os> SELECT CONVERT_TZ("2010-10-10 10:10:10", "test", "-10:00") + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | CONVERT_TZ("2010-10-10 10:10:10", "test", "-10:00") | + |-------------------------------------------------------| + | null | + +-------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range will result in null. +Example:: + + os> SELECT CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "+14:00") + fetched rows / total rows = 1/1 + +---------------------------------------------------------+ + | CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "+14:00") | + |---------------------------------------------------------| + | 2010-10-10 23:10:10 | + +---------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range will result in null. +Example:: + + os> SELECT CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "+14:01") + fetched rows / total rows = 1/1 + +---------------------------------------------------------+ + | CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "+14:01") | + |---------------------------------------------------------| + | null | + +---------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range will result in null. +Example:: + + os> SELECT CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "-13:59") + fetched rows / total rows = 1/1 + +---------------------------------------------------------+ + | CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "-13:59") | + |---------------------------------------------------------| + | 2010-10-09 19:11:10 | + +---------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range will result in null. +Example:: + + os> SELECT CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "-14:00") + fetched rows / total rows = 1/1 + +---------------------------------------------------------+ + | CONVERT_TZ("2010-10-10 10:10:10", "+01:00", "-14:00") | + |---------------------------------------------------------| + | null | + +---------------------------------------------------------+ + + CURDATE ------- @@ -893,6 +991,100 @@ Example:: +----------------------+------------------------------------------+ +DATETIME +-------- + +Description +>>>>>>>>>>> + +Usage: datetime(datetime)/ datetime(date, to_timezone) Converts the datetime to a new timezone + +Argument type: DATETIME/STRING + +Return type map: + +DATETIME, STRING -> DATETIME + +DATETIME -> DATETIME + +Example:: + + os> SELECT DATETIME('2008-12-25 05:30:00+00:00', 'America/Los_Angeles') + fetched rows / total rows = 1/1 + +----------------------------------------------------------------+ + | DATETIME('2008-12-25 05:30:00+00:00', 'America/Los_Angeles') | + |----------------------------------------------------------------| + | 2008-12-24 21:30:00 | + +----------------------------------------------------------------+ + +This example converts from -10:00 timezone to +10:00 timezone. +Example:: + + os> SELECT DATETIME('2004-02-28 23:00:00-10:00', '+10:00') + fetched rows / total rows = 1/1 + +---------------------------------------------------+ + | DATETIME('2004-02-28 23:00:00-10:00', '+10:00') | + |---------------------------------------------------| + | 2004-02-29 19:00:00 | + +---------------------------------------------------+ + +This example uses the timezone -14:00, which is outside of the range -13:59 and +14:00. This results in a null value. +Example:: + + os> SELECT DATETIME('2008-01-01 02:00:00', '-14:00') + fetched rows / total rows = 1/1 + +---------------------------------------------+ + | DATETIME('2008-01-01 02:00:00', '-14:00') | + |---------------------------------------------| + | null | + +---------------------------------------------+ + +February 30th is not a day, so it returns null. +Example:: + + os> SELECT DATETIME('2008-02-30 02:00:00', '-00:00') + fetched rows / total rows = 1/1 + +---------------------------------------------+ + | DATETIME('2008-02-30 02:00:00', '-00:00') | + |---------------------------------------------| + | null | + +---------------------------------------------+ + +DATETIME(datetime) examples + +DATETIME with no timezone specified does no conversion. +Example:: + + os> SELECT DATETIME('2008-02-10 02:00:00') + fetched rows / total rows = 1/1 + +-----------------------------------+ + | DATETIME('2008-02-10 02:00:00') | + |-----------------------------------| + | 2008-02-10 02:00:00 | + +-----------------------------------+ + +February 30th is not a day, so it returns null. +Example:: + + os> SELECT DATETIME('2008-02-30 02:00:00') + fetched rows / total rows = 1/1 + +-----------------------------------+ + | DATETIME('2008-02-30 02:00:00') | + |-----------------------------------| + | null | + +-----------------------------------+ + +DATETIME with a datetime and no seperate timezone to convert to returns the datetime object without a timezone. +Example:: + + os> SELECT DATETIME('2008-02-10 02:00:00+04:00') + fetched rows / total rows = 1/1 + +-----------------------------------------+ + | DATETIME('2008-02-10 02:00:00+04:00') | + |-----------------------------------------| + | 2008-02-10 02:00:00 | + +-----------------------------------------+ + DATE_ADD -------- @@ -2811,3 +3003,4 @@ Example searching for field Tags:: | [The House at Pooh Corner] | | [Winnie-the-Pooh] | +----------------------------------------------+ + diff --git a/docs/user/ppl/functions/datetime.rst b/docs/user/ppl/functions/datetime.rst index 3ee4317865..a9b61097be 100644 --- a/docs/user/ppl/functions/datetime.rst +++ b/docs/user/ppl/functions/datetime.rst @@ -38,6 +38,138 @@ Example:: | 2020-08-26 01:00:00 | 2020-08-27 | 2020-08-27 01:01:01 | +------------------------------------------------+----------------------------------+------------------------------------------------+ +CONVERT_TZ +---- + +Description +>>>>>>>>>>> + +Usage: convert_tz(datetime, from_timezone, to_timezone) constructs a local datetime converted from the from_timezone to the to_timezone. CONVERT_TZ returns null when any of the three function arguments are invalid, i.e. datetime is not in the format yyyy-MM-dd HH:mm:ss or the timeszone is not in (+/-)HH:mm. It also is invalid for invalid dates, such as February 30th and invalid timezones, which are ones outside of -13:59 and +14:00. + +Argument type: DATETIME, STRING, STRING + +Return type: DATETIME + +Conversion from +00:00 timezone to +10:00 timezone. Returns the datetime argument converted from +00:00 to +10:00 +Example:: + + os> source=people | eval `convert_tz('2008-05-15 12:00:00','+00:00','+10:00')` = convert_tz('2008-05-15 12:00:00','+00:00','+10:00') | fields `convert_tz('2008-05-15 12:00:00','+00:00','+10:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-05-15 12:00:00','+00:00','+10:00') | + |-------------------------------------------------------| + | 2008-05-15 22:00:00 | + +-------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range, such as +15:00 in this example will return null. +Example:: + + os> source=people | eval `convert_tz('2008-05-15 12:00:00','+00:00','+15:00')` = convert_tz('2008-05-15 12:00:00','+00:00','+15:00')| fields `convert_tz('2008-05-15 12:00:00','+00:00','+15:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-05-15 12:00:00','+00:00','+15:00') | + |-------------------------------------------------------| + | null | + +-------------------------------------------------------+ + +Conversion from a positive timezone to a negative timezone that goes over date line. +Example:: + + os> source=people | eval `convert_tz('2008-05-15 12:00:00','+03:30','-10:00')` = convert_tz('2008-05-15 12:00:00','+03:30','-10:00') | fields `convert_tz('2008-05-15 12:00:00','+03:30','-10:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-05-15 12:00:00','+03:30','-10:00') | + |-------------------------------------------------------| + | 2008-05-14 22:30:00 | + +-------------------------------------------------------+ + +Valid dates are required in convert_tz, invalid dates such as April 31st (not a date in the Gregorian calendar) will result in null. +Example:: + + os> source=people | eval `convert_tz('2008-04-31 12:00:00','+03:30','-10:00')` = convert_tz('2008-04-31 12:00:00','+03:30','-10:00') | fields `convert_tz('2008-04-31 12:00:00','+03:30','-10:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-04-31 12:00:00','+03:30','-10:00') | + |-------------------------------------------------------| + | null | + +-------------------------------------------------------+ + +Valid dates are required in convert_tz, invalid dates such as February 30th (not a date in the Gregorian calendar) will result in null. +Example:: + + os> source=people | eval `convert_tz('2008-02-30 12:00:00','+03:30','-10:00')` = convert_tz('2008-02-30 12:00:00','+03:30','-10:00') | fields `convert_tz('2008-02-30 12:00:00','+03:30','-10:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-02-30 12:00:00','+03:30','-10:00') | + |-------------------------------------------------------| + | null | + +-------------------------------------------------------+ + +February 29th 2008 is a valid date because it is a leap year. +Example:: + + os> source=people | eval `convert_tz('2008-02-29 12:00:00','+03:30','-10:00')` = convert_tz('2008-02-29 12:00:00','+03:30','-10:00') | fields `convert_tz('2008-02-29 12:00:00','+03:30','-10:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-02-29 12:00:00','+03:30','-10:00') | + |-------------------------------------------------------| + | 2008-02-28 22:30:00 | + +-------------------------------------------------------+ + +Valid dates are required in convert_tz, invalid dates such as February 29th 2007 (2007 is not a leap year) will result in null. +Example:: + + os> source=people | eval `convert_tz('2007-02-29 12:00:00','+03:30','-10:00')` = convert_tz('2007-02-29 12:00:00','+03:30','-10:00') | fields `convert_tz('2007-02-29 12:00:00','+03:30','-10:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2007-02-29 12:00:00','+03:30','-10:00') | + |-------------------------------------------------------| + | null | + +-------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range, such as +14:01 in this example will return null. +Example:: + + os> source=people | eval `convert_tz('2008-02-01 12:00:00','+14:01','+00:00')` = convert_tz('2008-02-01 12:00:00','+14:01','+00:00') | fields `convert_tz('2008-02-01 12:00:00','+14:01','+00:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-02-01 12:00:00','+14:01','+00:00') | + |-------------------------------------------------------| + | null | + +-------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range, such as +14:00 in this example will return a correctly converted date time object. +Example:: + + os> source=people | eval `convert_tz('2008-02-01 12:00:00','+14:00','+00:00')` = convert_tz('2008-02-01 12:00:00','+14:00','+00:00') | fields `convert_tz('2008-02-01 12:00:00','+14:00','+00:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-02-01 12:00:00','+14:00','+00:00') | + |-------------------------------------------------------| + | 2008-01-31 22:00:00 | + +-------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range, such as -14:00 will result in null +Example:: + + os> source=people | eval `convert_tz('2008-02-01 12:00:00','-14:00','+00:00')` = convert_tz('2008-02-01 12:00:00','-14:00','+00:00') | fields `convert_tz('2008-02-01 12:00:00','-14:00','+00:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-02-01 12:00:00','-14:00','+00:00') | + |-------------------------------------------------------| + | null | + +-------------------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. This timezone is within range so it is valid and will convert the time. +Example:: + + os> source=people | eval `convert_tz('2008-02-01 12:00:00','-13:59','+00:00')` = convert_tz('2008-02-01 12:00:00','-13:59','+00:00') | fields `convert_tz('2008-02-01 12:00:00','-13:59','+00:00')` + fetched rows / total rows = 1/1 + +-------------------------------------------------------+ + | convert_tz('2008-02-01 12:00:00','-13:59','+00:00') | + |-------------------------------------------------------| + | 2008-02-02 01:59:00 | + +-------------------------------------------------------+ DATE ---- @@ -191,6 +323,58 @@ Example:: +-----------------------------------------------+----------------------------------------------------------------+ + +DATETIME +-------- + +Description +>>>>>>>>>>> + +Usage: DATETIME(datetime)/ DATETIME(date, to_timezone) Converts the datetime to a new timezone + +Argument type: DATETIME/STRING + +Return type map: + +DATETIME, STRING -> DATETIME + +DATETIME -> DATETIME + + +Converting datetime with timezone to the second argument timezone. +Example:: + + os> source=people | eval `DATETIME('2004-02-28 23:00:00-10:00', '+10:00')` = DATETIME('2004-02-28 23:00:00-10:00', '+10:00') | fields `DATETIME('2004-02-28 23:00:00-10:00', '+10:00')` + fetched rows / total rows = 1/1 + +---------------------------------------------------+ + | DATETIME('2004-02-28 23:00:00-10:00', '+10:00') | + |---------------------------------------------------| + | 2004-02-29 19:00:00 | + +---------------------------------------------------+ + + + The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range will result in null. +Example:: + + os> source=people | eval `DATETIME('2008-01-01 02:00:00', '-14:00')` = DATETIME('2008-01-01 02:00:00', '-14:00') | fields `DATETIME('2008-01-01 02:00:00', '-14:00')` + fetched rows / total rows = 1/1 + +---------------------------------------------+ + | DATETIME('2008-01-01 02:00:00', '-14:00') | + |---------------------------------------------| + | null | + +---------------------------------------------+ + +The valid timezone range for convert_tz is (-13:59, +14:00) inclusive. Timezones outside of the range will result in null. +Example:: + + os> source=people | eval `DATETIME('2008-02-30 02:00:00', '-00:00')` = DATETIME('2008-02-30 02:00:00', '-00:00') | fields `DATETIME('2008-02-30 02:00:00', '-00:00')` + fetched rows / total rows = 1/1 + +---------------------------------------------+ + | DATETIME('2008-02-30 02:00:00', '-00:00') | + |---------------------------------------------| + | null | + +---------------------------------------------+ + DATE_SUB -------- diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/ConvertTZFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/ConvertTZFunctionIT.java new file mode 100644 index 0000000000..48cdb9684f --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/ConvertTZFunctionIT.java @@ -0,0 +1,184 @@ + /* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.ppl; + + import org.json.JSONObject; + import org.junit.Test; + import org.opensearch.sql.legacy.SQLIntegTestCase; + + import java.io.IOException; + + import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATE; + import static org.opensearch.sql.util.MatcherUtils.rows; + import static org.opensearch.sql.util.MatcherUtils.schema; + import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; + import static org.opensearch.sql.util.MatcherUtils.verifySchema; + import static org.opensearch.sql.util.MatcherUtils.verifySome; + + public class ConvertTZFunctionIT extends PPLIntegTestCase { + + + @Override + public void init() throws IOException { + loadIndex(Index.DATE); + } + + + @Test + public void inRangeZeroToPositive() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2008-05-15 12:00:00','+00:00','+10:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-05-15 22:00:00")); + } + + @Test + public void inRangeZeroToZero() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 00:00:00','-00:00','+00:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-12 00:00:00")); + } + + @Test + public void inRangePositiveToPositive() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 00:00:00','+10:00','+11:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-12 01:00:00")); + } + + @Test + public void inRangeNegativeToPositive() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','-08:00','+09:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-13 04:34:50")); + } + + @Test + public void inRangeNoTZChange() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','+09:00','+09:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-12 11:34:50")); + } + + @Test + public void inRangeTwentyFourHourChange() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','-12:00','+12:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-13 11:34:50")); + } + + @Test + public void inRangeFifteenMinuteTZ() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 13:00:00','+09:30','+05:45') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-12 09:15:00")); + } + + @Test + public void nullFromFieldUnder() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-30 11:34:50','-17:00','+08:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullToFieldOver() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','-12:00','+15:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullFromGarbageInput1() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','-12:00','test') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullFromGarbageInput2() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021test','-12:00','+00:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueFebruary() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-02-30 10:00:00','+00:00','+00:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueApril() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-04-31 10:00:00','+00:00','+00:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueMonth() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-13-03 10:00:00','+00:00','+00:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + } diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeFunctionIT.java index b35db2a6e7..d35dcc566b 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeFunctionIT.java @@ -72,6 +72,81 @@ public void testAddDate() throws IOException { verifySome(result.getJSONArray("datarows"), rows("2020-09-17 17:30:00")); } + @Test + public void testConvertTZ() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2008-05-15 12:00:00','+00:00','+10:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-05-15 22:00:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 00:00:00','-00:00','+00:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-12 00:00:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 00:00:00','+10:00','+11:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-12 01:00:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','-08:00','+09:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-13 04:34:50")); + + result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','+09:00','+09:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-12 11:34:50")); + + result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','-12:00','+12:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-13 11:34:50")); + + result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 13:00:00','+09:30','+05:45') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2021-05-12 09:15:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-30 11:34:50','-17:00','+08:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + + result = + executeQuery(String.format( + "source=%s | eval f = convert_tz('2021-05-12 11:34:50','-12:00','+15:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + @Test public void testDateAdd() throws IOException { JSONObject result = @@ -99,6 +174,105 @@ public void testDateAdd() throws IOException { verifySome(result.getJSONArray("datarows"), rows("2020-09-17 17:30:00")); } + @Test + public void testDateTime() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-12-25 05:30:00+00:00', 'America/Los_Angeles') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-12-24 21:30:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-12-25 05:30:00+00:00', '+01:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-12-25 06:30:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-12-25 05:30:00-05:00', '+05:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-12-25 15:30:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2004-02-28 23:00:00-10:00', '+10:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2004-02-29 19:00:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2003-02-28 23:00:00-10:00', '+10:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2003-03-01 19:00:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-12-25 05:30:00+00:00', '+14:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-12-25 19:30:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-01-01 02:00:00+10:00', '-10:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2007-12-31 06:00:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-01-01 02:00:00+10:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-01-01 02:00:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-01-01 02:00:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-01-01 02:00:00")); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-01-01 02:00:00+15:00', '-12:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-01-01 02:00:00+10:00', '-14:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + + result = + executeQuery(String.format( + "source=%s | eval f = DATETIME('2008-01-01 02:00:00', '-14:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, + schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + @Test public void testDateSub() throws IOException { JSONObject result = @@ -168,12 +342,12 @@ public void testDayOfMonth() throws IOException { @Test public void testDayOfWeek() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = dayofweek(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = dayofweek(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(4)); result = executeQuery(String.format( - "source=%s | eval f = dayofweek('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = dayofweek('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(4)); } @@ -364,7 +538,7 @@ public void testSubDate() throws IOException { verifySome(result.getJSONArray("datarows"), rows("2020-09-15 17:30:00")); result = executeQuery(String.format( - "source=%s | eval f = subdate(date('2020-09-16'), 1) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = subdate(date('2020-09-16'), 1) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "date")); verifySome(result.getJSONArray("datarows"), rows("2020-09-15")); @@ -376,7 +550,7 @@ public void testSubDate() throws IOException { verifySome(result.getJSONArray("datarows"), rows("2020-09-15 17:30:00")); result = executeQuery(String.format( - "source=%s | eval f = subdate('2020-09-16', 1) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = subdate('2020-09-16', 1) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "datetime")); verifySome(result.getJSONArray("datarows"), rows("2020-09-15")); } @@ -384,12 +558,12 @@ public void testSubDate() throws IOException { @Test public void testTimeToSec() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = time_to_sec(time('17:30:00')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = time_to_sec(time('17:30:00')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "long")); verifySome(result.getJSONArray("datarows"), rows(63000)); result = executeQuery(String.format( - "source=%s | eval f = time_to_sec('17:30:00') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = time_to_sec('17:30:00') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "long")); verifySome(result.getJSONArray("datarows"), rows(63000)); } @@ -397,12 +571,12 @@ public void testTimeToSec() throws IOException { @Test public void testToDays() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = to_days(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = to_days(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "long")); verifySome(result.getJSONArray("datarows"), rows(738049)); result = executeQuery(String.format( - "source=%s | eval f = to_days('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = to_days('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "long")); verifySome(result.getJSONArray("datarows"), rows(738049)); } @@ -431,26 +605,26 @@ public void testWeek() throws IOException { @Test public void testYear() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = year(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = year(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(2020)); result = executeQuery(String.format( - "source=%s | eval f = year('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = year('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(2020)); } void verifyDateFormat(String date, String type, String format, String formatted) throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = date_format(%s('%s'), '%s') | fields f", - TEST_INDEX_DATE, type, date, format)); + "source=%s | eval f = date_format(%s('%s'), '%s') | fields f", + TEST_INDEX_DATE, type, date, format)); verifySchema(result, schema("f", null, "string")); verifySome(result.getJSONArray("datarows"), rows(formatted)); result = executeQuery(String.format( - "source=%s | eval f = date_format('%s', '%s') | fields f", - TEST_INDEX_DATE, date, format)); + "source=%s | eval f = date_format('%s', '%s') | fields f", + TEST_INDEX_DATE, date, format)); verifySchema(result, schema("f", null, "string")); verifySome(result.getJSONArray("datarows"), rows(formatted)); } diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeImplementationIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeImplementationIT.java new file mode 100644 index 0000000000..158f25aadf --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/DateTimeImplementationIT.java @@ -0,0 +1,149 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.ppl; + +import org.json.JSONObject; +import org.junit.Test; + +import java.io.IOException; + +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATE; +import static org.opensearch.sql.util.MatcherUtils.rows; +import static org.opensearch.sql.util.MatcherUtils.schema; +import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; +import static org.opensearch.sql.util.MatcherUtils.verifySchema; +import static org.opensearch.sql.util.MatcherUtils.verifySome; + +public class DateTimeImplementationIT extends PPLIntegTestCase { + + + @Override + public void init() throws IOException { + loadIndex(Index.DATE); + } + + + @Test + public void inRangeZeroToStringTZ() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-12-25 05:30:00+00:00', 'America/Los_Angeles') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-12-24 21:30:00")); + } + + @Test + public void inRangeZeroToPositive() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-12-25 05:30:00+00:00', '+01:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-12-25 06:30:00")); + } + + @Test + public void inRangeNegativeToPositive() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-12-25 05:30:00-05:00', '+05:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-12-25 15:30:00")); + } + + @Test + public void inRangeTwentyHourOffset() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2004-02-28 23:00:00-10:00', '+10:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2004-02-29 19:00:00")); + } + + + @Test + public void inRangeYearChange() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-01-01 02:00:00+10:00', '-10:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2007-12-31 06:00:00")); + } + + @Test + public void inRangeZeroToMax() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-12-25 05:30:00+00:00', '+14:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-12-25 19:30:00")); + } + + @Test + public void inRangeNoToTZ() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-01-01 02:00:00+10:00') | fields f", TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-01-01 02:00:00")); + } + + @Test + public void inRangeNoTZ() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-01-01 02:00:00') | fields f", TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows("2008-01-01 02:00:00")); + } + + @Test + public void nullField3Over() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-01-01 02:00:00+15:00', '-12:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullField2Under() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-01-01 02:00:00+10:00', '-14:00') | fields f", + TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullTField3Over() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2008-01-01 02:00:00', '+15:00') | fields f", TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueFebruary() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2021-02-30 10:00:00') | fields f", TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueApril() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2021-04-31 10:00:00') | fields f", TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueMonth() throws IOException { + JSONObject result = executeQuery( + String.format("source=%s | eval f = DATETIME('2021-13-03 10:00:00') | fields f", TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "datetime")); + verifySome(result.getJSONArray("datarows"), rows(new Object[]{null})); + } +} diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/ConvertTZFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/ConvertTZFunctionIT.java new file mode 100644 index 0000000000..308fe7cdcd --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/sql/ConvertTZFunctionIT.java @@ -0,0 +1,184 @@ + /* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.sql; + +import org.junit.Test; +import org.opensearch.sql.legacy.SQLIntegTestCase; +import java.io.IOException; +import static org.opensearch.sql.util.MatcherUtils.rows; +import static org.opensearch.sql.util.MatcherUtils.schema; +import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; +import static org.opensearch.sql.util.MatcherUtils.verifySchema; + +public class ConvertTZFunctionIT extends SQLIntegTestCase { + + + @Override + public void init() throws Exception { + super.init(); + loadIndex(Index.BANK); + } + + + @Test + public void inRangeZeroToPositive() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2008-05-15 12:00:00','+00:00','+10:00')"); + verifySchema(result, + schema("convert_tz('2008-05-15 12:00:00','+00:00','+10:00')", null, "datetime")); + verifyDataRows(result, rows("2008-05-15 22:00:00")); + } + + @Test + public void inRangeNegativeZeroToPositiveZero() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 00:00:00','-00:00','+00:00')"); + verifySchema(result, + schema("convert_tz('2021-05-12 00:00:00','-00:00','+00:00')", null, "datetime")); + verifyDataRows(result, rows("2021-05-12 00:00:00")); + } + + @Test + public void inRangePositiveToPositive() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 00:00:00','+10:00','+11:00')"); + verifySchema(result, + schema("convert_tz('2021-05-12 00:00:00','+10:00','+11:00')", null, "datetime")); + verifyDataRows(result, rows("2021-05-12 01:00:00")); + } + + @Test + public void inRangeNegativeToPositive() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 11:34:50','-08:00','+09:00')"); + verifySchema(result, + schema("convert_tz('2021-05-12 11:34:50','-08:00','+09:00')", null, "datetime")); + verifyDataRows(result, rows("2021-05-13 04:34:50")); + } + + @Test + public void inRangeSameTimeZone() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 11:34:50','+09:00','+09:00')"); + verifySchema(result, + schema("convert_tz('2021-05-12 11:34:50','+09:00','+09:00')", null, "datetime")); + verifyDataRows(result, rows("2021-05-12 11:34:50")); + } + + @Test + public void inRangeTwentyFourHourTimeOffset() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 11:34:50','-12:00','+12:00')"); + verifySchema(result, + schema("convert_tz('2021-05-12 11:34:50','-12:00','+12:00')", null, "datetime")); + verifyDataRows(result, rows("2021-05-13 11:34:50")); + } + + @Test + public void inRangeFifteenMinuteTimeZones() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 13:00:00','+09:30','+05:45')"); + verifySchema(result, + schema("convert_tz('2021-05-12 13:00:00','+09:30','+05:45')", null, "datetime")); + verifyDataRows(result, rows("2021-05-12 09:15:00")); + } + + @Test + public void inRangeRandomTimes() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 13:00:00','+09:31','+05:11')"); + verifySchema(result, + schema("convert_tz('2021-05-12 13:00:00','+09:31','+05:11')", null, "datetime")); + verifyDataRows(result, rows("2021-05-12 08:40:00")); + } + + @Test + public void nullField2Under() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-30 11:34:50','-14:00','+08:00')"); + verifySchema(result, + schema("convert_tz('2021-05-30 11:34:50','-14:00','+08:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullField3Over() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 11:34:50','-12:00','+14:01')"); + verifySchema(result, + schema("convert_tz('2021-05-12 11:34:50','-12:00','+14:01')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void inRangeMinOnPoint() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 15:00:00','-13:59','-13:59')"); + verifySchema(result, + schema("convert_tz('2021-05-12 15:00:00','-13:59','-13:59')", null, "datetime")); + verifyDataRows(result, rows("2021-05-12 15:00:00")); + } + + // Invalid is any invalid input in a field. In the timezone fields it also includes all + // non-timezone characters including `****` as well as `+10:0` which is missing an extra + // value on the end to make it `HH:mm` timezone. + // Invalid input returns null. + @Test + public void nullField3InvalidInput() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 11:34:50','+10:0','+14:01')"); + verifySchema(result, + schema("convert_tz('2021-05-12 11:34:50','+10:0','+14:01')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullField2InvalidInput() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-05-12 11:34:50','+14:01','****')"); + verifySchema(result, + schema("convert_tz('2021-05-12 11:34:50','+14:01','****')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + // Invalid input in the datetime field of CONVERT_TZ results in a null field. It is any input + // which is not of the format `yyyy-MM-dd HH:mm:ss` + @Test + public void nullDateTimeInvalidInput() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021----','+00:00','+00:00')"); + verifySchema(result, + schema("convert_tz('2021----','+00:00','+00:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueFebruary() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-02-30 10:00:00','+00:00','+00:00')"); + verifySchema(result, + schema("convert_tz('2021-02-30 10:00:00','+00:00','+00:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueApril() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-04-31 10:00:00','+00:00','+00:00')"); + verifySchema(result, + schema("convert_tz('2021-04-31 10:00:00','+00:00','+00:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueMonth() throws IOException { + var result = executeJdbcRequest( + "SELECT convert_tz('2021-13-03 10:00:00','+00:00','+00:00')"); + verifySchema(result, + schema("convert_tz('2021-13-03 10:00:00','+00:00','+00:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } +} diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java index 2c9ab69779..207c3beb7d 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java @@ -56,25 +56,25 @@ public void init() throws Exception { @Test public void testDateInGroupBy() throws IOException{ JSONObject result = - executeQuery(String.format("SELECT DATE(birthdate) FROM %s GROUP BY DATE(birthdate)",TEST_INDEX_BANK) ); + executeQuery(String.format("SELECT DATE(birthdate) FROM %s GROUP BY DATE(birthdate)",TEST_INDEX_BANK) ); verifySchema(result, - schema("DATE(birthdate)", null, "date")); + schema("DATE(birthdate)", null, "date")); verifyDataRows(result, - rows("2017-10-23"), - rows("2017-11-20"), - rows("2018-06-23"), - rows("2018-11-13"), - rows("2018-06-27"), - rows("2018-08-19"), - rows("2018-08-11")); + rows("2017-10-23"), + rows("2017-11-20"), + rows("2018-06-23"), + rows("2018-11-13"), + rows("2018-06-27"), + rows("2018-08-19"), + rows("2018-08-11")); } @Test public void testDateWithHavingClauseOnly() throws IOException { JSONObject result = - executeQuery(String.format("SELECT (TO_DAYS(DATE('2050-01-01')) - 693961) FROM %s HAVING (COUNT(1) > 0)",TEST_INDEX_BANK) ); + executeQuery(String.format("SELECT (TO_DAYS(DATE('2050-01-01')) - 693961) FROM %s HAVING (COUNT(1) > 0)",TEST_INDEX_BANK) ); verifySchema(result, - schema("(TO_DAYS(DATE('2050-01-01')) - 693961)", null, "long")); + schema("(TO_DAYS(DATE('2050-01-01')) - 693961)", null, "long")); verifyDataRows(result, rows(54787)); } @@ -132,17 +132,17 @@ public void testDateAdd() throws IOException { verifyDataRows(result, rows("2020-09-17")); result = - executeQuery(String.format("SELECT DATE_ADD(birthdate, INTERVAL 1 YEAR) FROM %s GROUP BY 1",TEST_INDEX_BANK) ); + executeQuery(String.format("SELECT DATE_ADD(birthdate, INTERVAL 1 YEAR) FROM %s GROUP BY 1",TEST_INDEX_BANK) ); verifySchema(result, - schema("DATE_ADD(birthdate, INTERVAL 1 YEAR)", null, "datetime")); + schema("DATE_ADD(birthdate, INTERVAL 1 YEAR)", null, "datetime")); verifyDataRows(result, - rows("2018-10-23 00:00:00"), - rows("2018-11-20 00:00:00"), - rows("2019-06-23 00:00:00"), - rows("2019-11-13 23:33:20"), - rows("2019-06-27 00:00:00"), - rows("2019-08-19 00:00:00"), - rows("2019-08-11 00:00:00")); + rows("2018-10-23 00:00:00"), + rows("2018-11-20 00:00:00"), + rows("2019-06-23 00:00:00"), + rows("2019-11-13 23:33:20"), + rows("2019-06-27 00:00:00"), + rows("2019-08-19 00:00:00"), + rows("2019-08-11 00:00:00")); } @Test diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeImplementationIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeImplementationIT.java new file mode 100644 index 0000000000..ff2c4c07a6 --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeImplementationIT.java @@ -0,0 +1,161 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.sql; + +import org.junit.Test; +import org.opensearch.sql.legacy.SQLIntegTestCase; + +import java.io.IOException; + +import static org.opensearch.sql.util.MatcherUtils.rows; +import static org.opensearch.sql.util.MatcherUtils.schema; +import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; +import static org.opensearch.sql.util.MatcherUtils.verifySchema; + +public class DateTimeImplementationIT extends SQLIntegTestCase { + + + @Override + public void init() throws Exception { + super.init(); + loadIndex(Index.BANK); + } + + @Test + public void inRangeZeroToStringTZ() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-12-25 05:30:00+00:00', 'America/Los_Angeles')"); + verifySchema(result, + schema("DATETIME('2008-12-25 05:30:00+00:00', 'America/Los_Angeles')", null, "datetime")); + verifyDataRows(result, rows("2008-12-24 21:30:00")); + } + + @Test + public void inRangeZeroToPositive() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-12-25 05:30:00+00:00', '+01:00')"); + verifySchema(result, + schema("DATETIME('2008-12-25 05:30:00+00:00', '+01:00')", null, "datetime")); + verifyDataRows(result, rows("2008-12-25 06:30:00")); + } + + @Test + public void inRangeNegativeToPositive() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-12-25 05:30:00-05:00', '+05:00')"); + verifySchema(result, + schema("DATETIME('2008-12-25 05:30:00-05:00', '+05:00')", null, "datetime")); + verifyDataRows(result, rows("2008-12-25 15:30:00")); + } + + @Test + public void inRangeTwentyHourOffset() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2004-02-28 23:00:00-10:00', '+10:00')"); + verifySchema(result, + schema("DATETIME('2004-02-28 23:00:00-10:00', '+10:00')", null, "datetime")); + verifyDataRows(result, rows("2004-02-29 19:00:00")); + } + + @Test + public void inRangeYearChange() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-01-01 02:00:00+10:00', '-10:00')"); + verifySchema(result, + schema("DATETIME('2008-01-01 02:00:00+10:00', '-10:00')", null, "datetime")); + verifyDataRows(result, rows("2007-12-31 06:00:00")); + } + + @Test + public void inRangeZeroNoToTZ() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-01-01 02:00:00+10:00')"); + verifySchema(result, + schema("DATETIME('2008-01-01 02:00:00+10:00')", null, "datetime")); + verifyDataRows(result, rows("2008-01-01 02:00:00")); + } + + @Test + public void inRangeZeroNoTZ() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-01-01 02:00:00')"); + verifySchema(result, + schema("DATETIME('2008-01-01 02:00:00')", null, "datetime")); + verifyDataRows(result, rows("2008-01-01 02:00:00")); + } + + @Test + public void inRangeZeroDayConvert() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-01-01 02:00:00+12:00', '-12:00')"); + verifySchema(result, + schema("DATETIME('2008-01-01 02:00:00+12:00', '-12:00')", null, "datetime")); + verifyDataRows(result, rows("2007-12-31 02:00:00")); + } + + @Test + public void inRangeJustInRangeNegative() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-01-01 02:00:00+10:00', '-13:59')"); + verifySchema(result, + schema("DATETIME('2008-01-01 02:00:00+10:00', '-13:59')", null, "datetime")); + verifyDataRows(result, rows("2007-12-31 02:01:00")); + } + + @Test + public void inRangeJustInRangePositive() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-01-01 02:00:00+14:00', '-10:00')"); + verifySchema(result, + schema("DATETIME('2008-01-01 02:00:00+14:00', '-10:00')", null, "datetime")); + verifyDataRows(result, rows("2007-12-31 02:00:00")); + } + + @Test + public void nullField3Under() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-01-01 02:00:00+10:00', '-14:01')"); + verifySchema(result, + schema("DATETIME('2008-01-01 02:00:00+10:00', '-14:01')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullField1Over() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2008-01-01 02:00:00+14:01', '-10:00')"); + verifySchema(result, + schema("DATETIME('2008-01-01 02:00:00+14:01', '-10:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueFebruary() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2021-02-30 10:00:00')"); + verifySchema(result, + schema("DATETIME('2021-02-30 10:00:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueApril() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2021-04-31 10:00:00')"); + verifySchema(result, + schema("DATETIME('2021-04-31 10:00:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } + + @Test + public void nullDateTimeInvalidDateValueMonth() throws IOException { + var result = executeJdbcRequest( + "SELECT DATETIME('2021-13-03 10:00:00')"); + verifySchema(result, + schema("DATETIME('2021-13-03 10:00:00')", null, "datetime")); + verifyDataRows(result, rows(new Object[]{null})); + } +} diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 index 66741fc820..e05788fa74 100644 --- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 @@ -102,6 +102,7 @@ DAY_SECOND: 'DAY_SECOND'; DAY_MINUTE: 'DAY_MINUTE'; DAY_HOUR: 'DAY_HOUR'; YEAR_MONTH: 'YEAR_MONTH'; +CONVERT_TZ: 'CONVERT_TZ'; // DATASET TYPES DATAMODEL: 'DATAMODEL'; diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4 index 68b1d3cd17..32ad1e55e7 100644 --- a/ppl/src/main/antlr/OpenSearchPPLParser.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4 @@ -378,9 +378,9 @@ trigonometricFunctionName ; dateAndTimeFunctionBase - : ADDDATE | DATE | DATE_ADD | DATE_FORMAT | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK + : ADDDATE | CONVERT_TZ | DATE | DATETIME | DATE_ADD | DATE_FORMAT | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | FROM_UNIXTIME | HOUR | MAKEDATE | MAKETIME | MICROSECOND | MINUTE - | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | SYSDATE | TIME | TIME_TO_SEC | TIMESTAMP + | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | SYSDATE | TIME | TIMESTAMP | TIME_TO_SEC | TO_DAYS | UNIX_TIMESTAMP | WEEK | YEAR ; diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index de53bfffe5..5f2385bab3 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -186,6 +186,7 @@ CEILING: 'CEILING'; CONCAT: 'CONCAT'; CONCAT_WS: 'CONCAT_WS'; CONV: 'CONV'; +CONVERT_TZ: 'CONVERT_TZ'; COS: 'COS'; COSH: 'COSH'; COT: 'COT'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index 1100c07642..23e2d9288d 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -394,7 +394,7 @@ trigonometricFunctionName ; dateTimeFunctionName - : ADDDATE | DATE | DATE_ADD | DATE_FORMAT | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK + : ADDDATE | CONVERT_TZ | DATE | DATETIME | DATE_ADD | DATE_FORMAT | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | FROM_UNIXTIME | HOUR | MAKEDATE | MAKETIME | MICROSECOND | MINUTE | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | SYSDATE | TIME | TIME_TO_SEC | TIMESTAMP | TO_DAYS | UNIX_TIMESTAMP | WEEK | YEAR