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 bd2d075613..e09bef0c11 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 469f7e2011..4506e70636 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 @@ -19,11 +19,18 @@ import static org.opensearch.sql.expression.function.FunctionDSL.impl; import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling; +import java.time.DateTimeException; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; import java.time.format.TextStyle; import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import lombok.experimental.UtilityClass; import org.opensearch.sql.data.model.ExprDateValue; @@ -35,6 +42,8 @@ import org.opensearch.sql.data.model.ExprTimeValue; 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; @@ -48,6 +57,9 @@ */ @UtilityClass public class DateTimeFunction { + String timeZoneMax = "+14:00"; + String timeZoneMin = "-13:59"; + String timeZoneZero = "+00:00"; // The number of days from year zero to year 1970. private static final Long DAYS_0000_TO_1970 = (146097 * 5L) - (30L * 365L + 7L); @@ -59,7 +71,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()); @@ -95,7 +109,7 @@ public void register(BuiltinFunctionRepository repository) { * (STRING/DATETIME/TIMESTAMP, LONG) -> DATETIME */ - private DefaultFunctionResolver add_date(FunctionName functionName) { + private FunctionResolver add_date(FunctionName functionName) { return define(functionName, impl(nullMissingHandling(DateTimeFunction::exprAddDateInterval), DATETIME, STRING, INTERVAL), @@ -111,16 +125,31 @@ private DefaultFunctionResolver add_date(FunctionName functionName) { ); } - private DefaultFunctionResolver adddate() { + private FunctionResolver 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 FunctionResolver 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: * STRING/DATE/DATETIME/TIMESTAMP -> DATE */ - private DefaultFunctionResolver date() { + private FunctionResolver date() { return define(BuiltinFunctionName.DATE.getName(), impl(nullMissingHandling(DateTimeFunction::exprDate), DATE, STRING), impl(nullMissingHandling(DateTimeFunction::exprDate), DATE, DATE), @@ -128,7 +157,22 @@ private DefaultFunctionResolver date() { impl(nullMissingHandling(DateTimeFunction::exprDate), DATE, TIMESTAMP)); } - private DefaultFunctionResolver date_add() { + /** + * 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 FunctionResolver date_add() { return add_date(BuiltinFunctionName.DATE_ADD.getName()); } @@ -139,7 +183,7 @@ private DefaultFunctionResolver date_add() { * (DATE, LONG) -> DATE * (STRING/DATETIME/TIMESTAMP, LONG) -> DATETIME */ - private DefaultFunctionResolver sub_date(FunctionName functionName) { + private FunctionResolver sub_date(FunctionName functionName) { return define(functionName, impl(nullMissingHandling(DateTimeFunction::exprSubDateInterval), DATETIME, STRING, INTERVAL), @@ -155,14 +199,14 @@ private DefaultFunctionResolver sub_date(FunctionName functionName) { ); } - private DefaultFunctionResolver date_sub() { + private FunctionResolver date_sub() { return sub_date(BuiltinFunctionName.DATE_SUB.getName()); } /** * DAY(STRING/DATE/DATETIME/TIMESTAMP). return the day of the month (1-31). */ - private DefaultFunctionResolver day() { + private FunctionResolver day() { return define(BuiltinFunctionName.DAY.getName(), impl(nullMissingHandling(DateTimeFunction::exprDayOfMonth), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprDayOfMonth), INTEGER, DATETIME), @@ -176,7 +220,7 @@ private DefaultFunctionResolver day() { * return the name of the weekday for date, including Monday, Tuesday, Wednesday, * Thursday, Friday, Saturday and Sunday. */ - private DefaultFunctionResolver dayName() { + private FunctionResolver dayName() { return define(BuiltinFunctionName.DAYNAME.getName(), impl(nullMissingHandling(DateTimeFunction::exprDayName), STRING, DATE), impl(nullMissingHandling(DateTimeFunction::exprDayName), STRING, DATETIME), @@ -188,7 +232,7 @@ private DefaultFunctionResolver dayName() { /** * DAYOFMONTH(STRING/DATE/DATETIME/TIMESTAMP). return the day of the month (1-31). */ - private DefaultFunctionResolver dayOfMonth() { + private FunctionResolver dayOfMonth() { return define(BuiltinFunctionName.DAYOFMONTH.getName(), impl(nullMissingHandling(DateTimeFunction::exprDayOfMonth), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprDayOfMonth), INTEGER, DATETIME), @@ -201,7 +245,7 @@ private DefaultFunctionResolver dayOfMonth() { * DAYOFWEEK(STRING/DATE/DATETIME/TIMESTAMP). * return the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday). */ - private DefaultFunctionResolver dayOfWeek() { + private FunctionResolver dayOfWeek() { return define(BuiltinFunctionName.DAYOFWEEK.getName(), impl(nullMissingHandling(DateTimeFunction::exprDayOfWeek), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprDayOfWeek), INTEGER, DATETIME), @@ -214,7 +258,7 @@ private DefaultFunctionResolver dayOfWeek() { * DAYOFYEAR(STRING/DATE/DATETIME/TIMESTAMP). * return the day of the year for date (1-366). */ - private DefaultFunctionResolver dayOfYear() { + private FunctionResolver dayOfYear() { return define(BuiltinFunctionName.DAYOFYEAR.getName(), impl(nullMissingHandling(DateTimeFunction::exprDayOfYear), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprDayOfYear), INTEGER, DATETIME), @@ -226,7 +270,7 @@ private DefaultFunctionResolver dayOfYear() { /** * FROM_DAYS(LONG). return the date value given the day number N. */ - private DefaultFunctionResolver from_days() { + private FunctionResolver from_days() { return define(BuiltinFunctionName.FROM_DAYS.getName(), impl(nullMissingHandling(DateTimeFunction::exprFromDays), DATE, LONG)); } @@ -234,7 +278,7 @@ private DefaultFunctionResolver from_days() { /** * HOUR(STRING/TIME/DATETIME/TIMESTAMP). return the hour value for time. */ - private DefaultFunctionResolver hour() { + private FunctionResolver hour() { return define(BuiltinFunctionName.HOUR.getName(), impl(nullMissingHandling(DateTimeFunction::exprHour), INTEGER, STRING), impl(nullMissingHandling(DateTimeFunction::exprHour), INTEGER, TIME), @@ -256,7 +300,7 @@ private FunctionResolver maketime() { /** * MICROSECOND(STRING/TIME/DATETIME/TIMESTAMP). return the microsecond value for time. */ - private DefaultFunctionResolver microsecond() { + private FunctionResolver microsecond() { return define(BuiltinFunctionName.MICROSECOND.getName(), impl(nullMissingHandling(DateTimeFunction::exprMicrosecond), INTEGER, STRING), impl(nullMissingHandling(DateTimeFunction::exprMicrosecond), INTEGER, TIME), @@ -268,7 +312,7 @@ private DefaultFunctionResolver microsecond() { /** * MINUTE(STRING/TIME/DATETIME/TIMESTAMP). return the minute value for time. */ - private DefaultFunctionResolver minute() { + private FunctionResolver minute() { return define(BuiltinFunctionName.MINUTE.getName(), impl(nullMissingHandling(DateTimeFunction::exprMinute), INTEGER, STRING), impl(nullMissingHandling(DateTimeFunction::exprMinute), INTEGER, TIME), @@ -280,7 +324,7 @@ private DefaultFunctionResolver minute() { /** * MONTH(STRING/DATE/DATETIME/TIMESTAMP). return the month for date (1-12). */ - private DefaultFunctionResolver month() { + private FunctionResolver month() { return define(BuiltinFunctionName.MONTH.getName(), impl(nullMissingHandling(DateTimeFunction::exprMonth), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprMonth), INTEGER, DATETIME), @@ -292,7 +336,7 @@ private DefaultFunctionResolver month() { /** * MONTHNAME(STRING/DATE/DATETIME/TIMESTAMP). return the full name of the month for date. */ - private DefaultFunctionResolver monthName() { + private FunctionResolver monthName() { return define(BuiltinFunctionName.MONTHNAME.getName(), impl(nullMissingHandling(DateTimeFunction::exprMonthName), STRING, DATE), impl(nullMissingHandling(DateTimeFunction::exprMonthName), STRING, DATETIME), @@ -304,7 +348,7 @@ private DefaultFunctionResolver monthName() { /** * QUARTER(STRING/DATE/DATETIME/TIMESTAMP). return the month for date (1-4). */ - private DefaultFunctionResolver quarter() { + private FunctionResolver quarter() { return define(BuiltinFunctionName.QUARTER.getName(), impl(nullMissingHandling(DateTimeFunction::exprQuarter), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprQuarter), INTEGER, DATETIME), @@ -316,7 +360,7 @@ private DefaultFunctionResolver quarter() { /** * SECOND(STRING/TIME/DATETIME/TIMESTAMP). return the second value for time. */ - private DefaultFunctionResolver second() { + private FunctionResolver second() { return define(BuiltinFunctionName.SECOND.getName(), impl(nullMissingHandling(DateTimeFunction::exprSecond), INTEGER, STRING), impl(nullMissingHandling(DateTimeFunction::exprSecond), INTEGER, TIME), @@ -325,7 +369,7 @@ private DefaultFunctionResolver second() { ); } - private DefaultFunctionResolver subdate() { + private FunctionResolver subdate() { return sub_date(BuiltinFunctionName.SUBDATE.getName()); } @@ -334,7 +378,7 @@ private DefaultFunctionResolver subdate() { * Also to construct a time type. The supported signatures: * STRING/DATE/DATETIME/TIME/TIMESTAMP -> TIME */ - private DefaultFunctionResolver time() { + private FunctionResolver time() { return define(BuiltinFunctionName.TIME.getName(), impl(nullMissingHandling(DateTimeFunction::exprTime), TIME, STRING), impl(nullMissingHandling(DateTimeFunction::exprTime), TIME, DATE), @@ -346,7 +390,7 @@ private DefaultFunctionResolver time() { /** * TIME_TO_SEC(STRING/TIME/DATETIME/TIMESTAMP). return the time argument, converted to seconds. */ - private DefaultFunctionResolver time_to_sec() { + private FunctionResolver time_to_sec() { return define(BuiltinFunctionName.TIME_TO_SEC.getName(), impl(nullMissingHandling(DateTimeFunction::exprTimeToSec), LONG, STRING), impl(nullMissingHandling(DateTimeFunction::exprTimeToSec), LONG, TIME), @@ -360,7 +404,7 @@ private DefaultFunctionResolver time_to_sec() { * Also to construct a date type. The supported signatures: * STRING/DATE/DATETIME/TIMESTAMP -> DATE */ - private DefaultFunctionResolver timestamp() { + private FunctionResolver timestamp() { return define(BuiltinFunctionName.TIMESTAMP.getName(), impl(nullMissingHandling(DateTimeFunction::exprTimestamp), TIMESTAMP, STRING), impl(nullMissingHandling(DateTimeFunction::exprTimestamp), TIMESTAMP, DATE), @@ -371,7 +415,7 @@ private DefaultFunctionResolver timestamp() { /** * TO_DAYS(STRING/DATE/DATETIME/TIMESTAMP). return the day number of the given date. */ - private DefaultFunctionResolver to_days() { + private FunctionResolver to_days() { return define(BuiltinFunctionName.TO_DAYS.getName(), impl(nullMissingHandling(DateTimeFunction::exprToDays), LONG, STRING), impl(nullMissingHandling(DateTimeFunction::exprToDays), LONG, TIMESTAMP), @@ -382,7 +426,7 @@ private DefaultFunctionResolver to_days() { /** * WEEK(DATE[,mode]). return the week number for date. */ - private DefaultFunctionResolver week() { + private FunctionResolver week() { return define(BuiltinFunctionName.WEEK.getName(), impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), INTEGER, DATETIME), @@ -398,7 +442,7 @@ private DefaultFunctionResolver week() { /** * YEAR(STRING/DATE/DATETIME/TIMESTAMP). return the year for date (1000-9999). */ - private DefaultFunctionResolver year() { + private FunctionResolver year() { return define(BuiltinFunctionName.YEAR.getName(), impl(nullMissingHandling(DateTimeFunction::exprYear), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprYear), INTEGER, DATETIME), @@ -415,7 +459,7 @@ private DefaultFunctionResolver year() { * (DATETIME, STRING) -> STRING * (TIMESTAMP, STRING) -> STRING */ - private DefaultFunctionResolver date_format() { + private FunctionResolver date_format() { return define(BuiltinFunctionName.DATE_FORMAT.getName(), impl(nullMissingHandling(DateTimeFormatterUtil::getFormattedDate), STRING, STRING, STRING), @@ -454,6 +498,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 (!isValidMySqlTimeZoneId(convertedFromTz) + || !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. * @@ -468,6 +548,61 @@ 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(); + DateTimeFormatter formatDT = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss[xxx]") + .withResolverStyle(ResolverStyle.STRICT); + + try { + LocalDateTime ldtFormatted = LocalDateTime.parse(dateTime.stringValue(), formatDT); + 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(), formatDT); + 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. * @@ -712,7 +847,6 @@ private ExprValue exprToDays(ExprValue date) { /** * Week for date implementation for ExprValue. - * * @param date ExprValue of Date/Datetime/Timestamp/String type. * @param mode ExprValue of Integer type. */ @@ -724,7 +858,6 @@ private ExprValue exprWeek(ExprValue date, ExprValue mode) { /** * Week for date implementation for ExprValue. * When mode is not specified default value mode 0 is used for default_week_format. - * * @param date ExprValue of Date/Datetime/Timestamp/String type. * @return ExprValue. */ @@ -742,4 +875,29 @@ private ExprValue exprYear(ExprValue date) { return new ExprIntegerValue(date.dateValue().getYear()); } + /** + * isValidMySqlTimeZoneId for timezones which match timezone the range set by MySQL. + * + * @param zone ZoneId of ZoneId type. + * @return Boolean. + */ + private Boolean isValidMySqlTimeZoneId(ZoneId zone) { + 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/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index b3821d6e41..93499d68c3 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/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/DateTimeFunctionTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java index 89415e0560..79efa2a015 100644 --- a/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java @@ -16,7 +16,6 @@ import static org.opensearch.sql.data.model.ExprValueUtils.stringValue; import static org.opensearch.sql.data.type.ExprCoreType.DATE; import static org.opensearch.sql.data.type.ExprCoreType.DATETIME; -import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE; import static org.opensearch.sql.data.type.ExprCoreType.INTEGER; import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL; import static org.opensearch.sql.data.type.ExprCoreType.LONG; @@ -25,15 +24,7 @@ import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP; import com.google.common.collect.ImmutableList; -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.Year; -import java.util.HashSet; import java.util.List; -import java.util.Random; -import java.util.Set; -import java.util.stream.IntStream; import lombok.AllArgsConstructor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -52,10 +43,7 @@ import org.opensearch.sql.expression.Expression; import org.opensearch.sql.expression.ExpressionTestBase; import org.opensearch.sql.expression.FunctionExpression; -import org.opensearch.sql.expression.config.ExpressionConfig; import org.opensearch.sql.expression.env.Environment; -import org.opensearch.sql.expression.function.FunctionName; -import org.opensearch.sql.expression.function.FunctionSignature; @ExtendWith(MockitoExtension.class) class DateTimeFunctionTest extends ExpressionTestBase { 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 736b1f148e..1a8e2b1c62 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -859,6 +859,95 @@ 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("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 +982,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 -------- @@ -2560,3 +2743,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 3680dc2272..803704e076 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 7e0169d174..990c25a1cf 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 @@ -13,8 +13,6 @@ import static org.opensearch.sql.util.MatcherUtils.verifySome; import java.io.IOException; -import java.time.LocalTime; - import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.opensearch.sql.common.utils.StringUtils; @@ -53,6 +51,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 = @@ -80,6 +153,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 = @@ -90,12 +262,12 @@ public void testDateSub() throws IOException { verifySome(result.getJSONArray("datarows"), rows("2020-09-15 17:30:00")); result = executeQuery(String.format( - "source=%s | eval f = date_sub(date('2020-09-16'), 1) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = date_sub(date('2020-09-16'), 1) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "date")); verifySome(result.getJSONArray("datarows"), rows("2020-09-15")); result = executeQuery(String.format( - "source=%s | eval f = date_sub('2020-09-16', 1) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = date_sub('2020-09-16', 1) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "datetime")); verifySome(result.getJSONArray("datarows"), rows("2020-09-15")); @@ -110,12 +282,12 @@ public void testDateSub() throws IOException { @Test public void testDay() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = day(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = day(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(16)); result = executeQuery(String.format( - "source=%s | eval f = day('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = day('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(16)); } @@ -123,12 +295,12 @@ public void testDay() throws IOException { @Test public void testDayName() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = dayname(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = dayname(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "string")); verifySome(result.getJSONArray("datarows"), rows("Wednesday")); result = executeQuery(String.format( - "source=%s | eval f = dayname('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = dayname('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "string")); verifySome(result.getJSONArray("datarows"), rows("Wednesday")); } @@ -136,12 +308,12 @@ public void testDayName() throws IOException { @Test public void testDayOfMonth() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = dayofmonth(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = dayofmonth(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(16)); result = executeQuery(String.format( - "source=%s | eval f = dayofmonth('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = dayofmonth('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(16)); } @@ -149,12 +321,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)); } @@ -162,12 +334,12 @@ public void testDayOfWeek() throws IOException { @Test public void testDayOfYear() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = dayofyear(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = dayofyear(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(260)); result = executeQuery(String.format( - "source=%s | eval f = dayofyear('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = dayofyear('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(260)); } @@ -175,7 +347,7 @@ public void testDayOfYear() throws IOException { @Test public void testFromDays() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = from_days(738049) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = from_days(738049) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "date")); verifySome(result.getJSONArray("datarows"), rows("2020-09-16")); } @@ -183,22 +355,22 @@ public void testFromDays() throws IOException { @Test public void testHour() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = hour(timestamp('2020-09-16 17:30:00')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = hour(timestamp('2020-09-16 17:30:00')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(17)); result = executeQuery(String.format( - "source=%s | eval f = hour(time('17:30:00')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = hour(time('17:30:00')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(17)); result = executeQuery(String.format( - "source=%s | eval f = hour('2020-09-16 17:30:00') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = hour('2020-09-16 17:30:00') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(17)); result = executeQuery(String.format( - "source=%s | eval f = hour('17:30:00') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = hour('17:30:00') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(17)); } @@ -206,18 +378,18 @@ public void testHour() throws IOException { @Test public void testMicrosecond() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = microsecond(timestamp('2020-09-16 17:30:00.123456')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = microsecond(timestamp('2020-09-16 17:30:00.123456')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(123456)); // Explicit timestamp value with less than 6 microsecond digits result = executeQuery(String.format( - "source=%s | eval f = microsecond(timestamp('2020-09-16 17:30:00.1234')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = microsecond(timestamp('2020-09-16 17:30:00.1234')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(123400)); result = executeQuery(String.format( - "source=%s | eval f = microsecond(time('17:30:00.000010')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = microsecond(time('17:30:00.000010')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(10)); @@ -228,7 +400,7 @@ public void testMicrosecond() throws IOException { verifySome(result.getJSONArray("datarows"), rows(123400)); result = executeQuery(String.format( - "source=%s | eval f = microsecond('2020-09-16 17:30:00.123456') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = microsecond('2020-09-16 17:30:00.123456') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(123456)); @@ -239,13 +411,13 @@ public void testMicrosecond() throws IOException { verifySome(result.getJSONArray("datarows"), rows(123400)); result = executeQuery(String.format( - "source=%s | eval f = microsecond('17:30:00.000010') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = microsecond('17:30:00.000010') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(10)); // Implicit time value with less than 6 microsecond digits result = executeQuery(String.format( - "source=%s | eval f = microsecond('17:30:00.1234') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = microsecond('17:30:00.1234') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(123400)); } @@ -253,22 +425,22 @@ public void testMicrosecond() throws IOException { @Test public void testMinute() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = minute(timestamp('2020-09-16 17:30:00')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = minute(timestamp('2020-09-16 17:30:00')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(30)); result = executeQuery(String.format( - "source=%s | eval f = minute(time('17:30:00')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = minute(time('17:30:00')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(30)); result = executeQuery(String.format( - "source=%s | eval f = minute('2020-09-16 17:30:00') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = minute('2020-09-16 17:30:00') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(30)); result = executeQuery(String.format( - "source=%s | eval f = minute('17:30:00') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = minute('17:30:00') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(30)); } @@ -276,12 +448,12 @@ public void testMinute() throws IOException { @Test public void testMonth() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = month(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = month(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(9)); result = executeQuery(String.format( - "source=%s | eval f = month('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = month('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(9)); } @@ -289,12 +461,12 @@ public void testMonth() throws IOException { @Test public void testMonthName() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = monthname(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = monthname(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "string")); verifySome(result.getJSONArray("datarows"), rows("September")); result = executeQuery(String.format( - "source=%s | eval f = monthname('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = monthname('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "string")); verifySome(result.getJSONArray("datarows"), rows("September")); } @@ -302,12 +474,12 @@ public void testMonthName() throws IOException { @Test public void testQuarter() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = quarter(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = quarter(date('2020-09-16')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(3)); result = executeQuery(String.format( - "source=%s | eval f = quarter('2020-09-16') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = quarter('2020-09-16') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(3)); } @@ -315,22 +487,22 @@ public void testQuarter() throws IOException { @Test public void testSecond() throws IOException { JSONObject result = executeQuery(String.format( - "source=%s | eval f = second(timestamp('2020-09-16 17:30:00')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = second(timestamp('2020-09-16 17:30:00')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(0)); result = executeQuery(String.format( - "source=%s | eval f = second(time('17:30:00')) | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = second(time('17:30:00')) | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(0)); result = executeQuery(String.format( - "source=%s | eval f = second('2020-09-16 17:30:00') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = second('2020-09-16 17:30:00') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(0)); result = executeQuery(String.format( - "source=%s | eval f = second('17:30:00') | fields f", TEST_INDEX_DATE)); + "source=%s | eval f = second('17:30:00') | fields f", TEST_INDEX_DATE)); verifySchema(result, schema("f", null, "integer")); verifySome(result.getJSONArray("datarows"), rows(0)); } @@ -345,7 +517,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")); @@ -357,7 +529,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")); } @@ -365,12 +537,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)); } @@ -378,12 +550,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)); } @@ -412,26 +584,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/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 93df64d0b3..f06f535905 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 c83297459d..1dbb90c33d 100644 --- a/ppl/src/main/antlr/OpenSearchPPLParser.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4 @@ -373,7 +373,7 @@ trigonometricFunctionName ; dateAndTimeFunctionBase - : ADDDATE | DATE | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS + : ADDDATE | CONVERT_TZ | DATE | DATETIME | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | HOUR | MICROSECOND | MINUTE | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | TIME | TIME_TO_SEC | TIMESTAMP | TO_DAYS | YEAR | WEEK | DATE_FORMAT | MAKETIME | MAKEDATE ; diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index 6d2d7d8a64..a2e7c7ad90 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 40207df82a..abc978fb3f 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -383,7 +383,7 @@ trigonometricFunctionName ; dateTimeFunctionName - : ADDDATE | DATE | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS + : ADDDATE | CONVERT_TZ | DATE | DATETIME | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | HOUR | MICROSECOND | MINUTE | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | TIME | TIME_TO_SEC | TIMESTAMP | TO_DAYS | YEAR | WEEK | DATE_FORMAT | MAKETIME | MAKEDATE ;