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 84716a425a..216d3d2278 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 @@ -6,6 +6,7 @@ package org.opensearch.sql.expression.datetime; +import static java.time.temporal.ChronoUnit.MONTHS; 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; @@ -68,6 +69,7 @@ * 2) the implementation should rely on ExprValue. */ @UtilityClass +@SuppressWarnings("unchecked") public class DateTimeFunction { // The number of days from year zero to year 1970. private static final Long DAYS_0000_TO_1970 = (146097 * 5L) - (30L * 365L + 7L); @@ -84,6 +86,11 @@ public class DateTimeFunction { public void register(BuiltinFunctionRepository repository) { repository.register(adddate()); repository.register(convert_tz()); + repository.register(curtime()); + repository.register(curdate()); + repository.register(current_date()); + repository.register(current_time()); + repository.register(current_timestamp()); repository.register(date()); repository.register(datetime()); repository.register(date_add()); @@ -96,15 +103,21 @@ public void register(BuiltinFunctionRepository repository) { repository.register(from_days()); repository.register(from_unixtime()); repository.register(hour()); + repository.register(localtime()); + repository.register(localtimestamp()); repository.register(makedate()); repository.register(maketime()); repository.register(microsecond()); repository.register(minute()); repository.register(month()); repository.register(monthName()); + repository.register(now()); + repository.register(period_add()); + repository.register(period_diff()); repository.register(quarter()); repository.register(second()); repository.register(subdate()); + repository.register(sysdate()); repository.register(time()); repository.register(time_to_sec()); repository.register(timestamp()); @@ -113,84 +126,6 @@ public void register(BuiltinFunctionRepository repository) { repository.register(unix_timestamp()); repository.register(week()); repository.register(year()); - - repository.register(now()); - repository.register(current_timestamp()); - repository.register(localtimestamp()); - repository.register(localtime()); - repository.register(sysdate()); - repository.register(curtime()); - repository.register(current_time()); - repository.register(curdate()); - repository.register(current_date()); - } - - /** - * NOW() returns a constant time that indicates the time at which the statement began to execute. - * `fsp` argument support is removed until refactoring to avoid bug where `now()`, `now(x)` and - * `now(y) return different values. - */ - private FunctionResolver now(FunctionName functionName) { - return define(functionName, - impl(() -> new ExprDatetimeValue(formatNow(null)), DATETIME) - ); - } - - private FunctionResolver now() { - return now(BuiltinFunctionName.NOW.getName()); - } - - private FunctionResolver current_timestamp() { - return now(BuiltinFunctionName.CURRENT_TIMESTAMP.getName()); - } - - private FunctionResolver localtimestamp() { - return now(BuiltinFunctionName.LOCALTIMESTAMP.getName()); - } - - private FunctionResolver localtime() { - return now(BuiltinFunctionName.LOCALTIME.getName()); - } - - /** - * SYSDATE() returns the time at which it executes. - */ - private FunctionResolver sysdate() { - return define(BuiltinFunctionName.SYSDATE.getName(), - impl(() -> new ExprDatetimeValue(formatNow(null)), DATETIME), - impl((v) -> new ExprDatetimeValue(formatNow(v.integerValue())), DATETIME, INTEGER) - ); - } - - /** - * Synonym for @see `now`. - */ - private FunctionResolver curtime(FunctionName functionName) { - return define(functionName, - impl(() -> new ExprTimeValue(formatNow(null).toLocalTime()), TIME) - ); - } - - private FunctionResolver curtime() { - return curtime(BuiltinFunctionName.CURTIME.getName()); - } - - private FunctionResolver current_time() { - return curtime(BuiltinFunctionName.CURRENT_TIME.getName()); - } - - private FunctionResolver curdate(FunctionName functionName) { - return define(functionName, - impl(() -> new ExprDateValue(formatNow(null).toLocalDate()), DATE) - ); - } - - private FunctionResolver curdate() { - return curdate(BuiltinFunctionName.CURDATE.getName()); - } - - private FunctionResolver current_date() { - return curdate(BuiltinFunctionName.CURRENT_DATE.getName()); } /** @@ -236,6 +171,41 @@ private DefaultFunctionResolver convert_tz() { ); } + private FunctionResolver curdate(FunctionName functionName) { + return define(functionName, + impl(() -> new ExprDateValue(formatNow(null).toLocalDate()), DATE) + ); + } + + private FunctionResolver curdate() { + return curdate(BuiltinFunctionName.CURDATE.getName()); + } + + /** + * Synonym for @see `now`. + */ + private FunctionResolver curtime(FunctionName functionName) { + return define(functionName, + impl(() -> new ExprTimeValue(formatNow(null).toLocalTime()), TIME) + ); + } + + private FunctionResolver curtime() { + return curtime(BuiltinFunctionName.CURTIME.getName()); + } + + private FunctionResolver current_date() { + return curdate(BuiltinFunctionName.CURRENT_DATE.getName()); + } + + private FunctionResolver current_time() { + return curtime(BuiltinFunctionName.CURRENT_TIME.getName()); + } + + private FunctionResolver current_timestamp() { + return now(BuiltinFunctionName.CURRENT_TIMESTAMP.getName()); + } + /** * Extracts the date part of a date and time value. * Also to construct a date type. The supported signatures: @@ -386,6 +356,29 @@ private DefaultFunctionResolver hour() { ); } + private FunctionResolver localtime() { + return now(BuiltinFunctionName.LOCALTIME.getName()); + } + + private FunctionResolver localtimestamp() { + return now(BuiltinFunctionName.LOCALTIMESTAMP.getName()); + } + + /** + * NOW() returns a constant time that indicates the time at which the statement began to execute. + * `fsp` argument support is removed until refactoring to avoid bug where `now()`, `now(x)` and + * `now(y) return different values. + */ + private FunctionResolver now(FunctionName functionName) { + return define(functionName, + impl(() -> new ExprDatetimeValue(formatNow(null)), DATETIME) + ); + } + + private FunctionResolver now() { + return now(BuiltinFunctionName.NOW.getName()); + } + private FunctionResolver makedate() { return define(BuiltinFunctionName.MAKEDATE.getName(), impl(nullMissingHandling(DateTimeFunction::exprMakeDate), DATE, DOUBLE, DOUBLE)); @@ -444,6 +437,27 @@ private DefaultFunctionResolver monthName() { ); } + /** + * Add N months to period P (in the format YYMM or YYYYMM). Returns a value in the format YYYYMM. + * (LONG, LONG) -> LONG + */ + private DefaultFunctionResolver period_add() { + return define(BuiltinFunctionName.PERIOD_ADD.getName(), + impl(nullMissingHandling(DateTimeFunction::exprPeriodAdd), LONG, LONG, LONG) + ); + } + + /** + * Returns the number of months between periods P1 and P2. + * P1 and P2 should be in the format YYMM or YYYYMM. + * (LONG, LONG) -> LONG + */ + private DefaultFunctionResolver period_diff() { + return define(BuiltinFunctionName.PERIOD_DIFF.getName(), + impl(nullMissingHandling(DateTimeFunction::exprPeriodDiff), LONG, LONG, LONG) + ); + } + /** * QUARTER(STRING/DATE/DATETIME/TIMESTAMP). return the month for date (1-4). */ @@ -889,6 +903,61 @@ private ExprValue exprMonthName(ExprValue date) { date.dateValue().getMonth().getDisplayName(TextStyle.FULL, Locale.getDefault())); } + private LocalDate parseDatePeriod(Long period) { + var input = period.toString(); + // MySQL undocumented: if year is not specified or has 1 digit - 2000/200x is assumed + if (input.length() <= 5) { + input = String.format("200%05d", period); + } + try { + return LocalDate.parse(input, DATE_FORMATTER_SHORT_YEAR); + } catch (DateTimeParseException ignored) { + // nothing to do, try another format + } + try { + return LocalDate.parse(input, DATE_FORMATTER_LONG_YEAR); + } catch (DateTimeParseException ignored) { + return null; + } + } + + /** + * Adds N months to period P (in the format YYMM or YYYYMM). + * Returns a value in the format YYYYMM. + * + * @param period Period in the format YYMM or YYYYMM. + * @param months Amount of months to add. + * @return ExprLongValue. + */ + private ExprValue exprPeriodAdd(ExprValue period, ExprValue months) { + // We should add a day to make string parsable and remove it afterwards + var input = period.longValue() * 100 + 1; // adds 01 to end of the string + var parsedDate = parseDatePeriod(input); + if (parsedDate == null) { + return ExprNullValue.of(); + } + var res = DATE_FORMATTER_LONG_YEAR.format(parsedDate.plusMonths(months.longValue())); + return new ExprLongValue(Long.parseLong( + res.substring(0, res.length() - 2))); // Remove the day part, .eg. 20070101 -> 200701 + } + + /** + * Returns the number of months between periods P1 and P2. + * P1 and P2 should be in the format YYMM or YYYYMM. + * + * @param period1 Period in the format YYMM or YYYYMM. + * @param period2 Period in the format YYMM or YYYYMM. + * @return ExprLongValue. + */ + private ExprValue exprPeriodDiff(ExprValue period1, ExprValue period2) { + var parsedDate1 = parseDatePeriod(period1.longValue() * 100 + 1); + var parsedDate2 = parseDatePeriod(period2.longValue() * 100 + 1); + if (parsedDate1 == null || parsedDate2 == null) { + return ExprNullValue.of(); + } + return new ExprLongValue(MONTHS.between(parsedDate2, parsedDate1)); + } + /** * Quarter for date implementation for ExprValue. * @@ -936,6 +1005,16 @@ private ExprValue exprSubDateInterval(ExprValue date, ExprValue expr) { : exprValue); } + /** + * SYSDATE() returns the time at which it executes. + */ + private FunctionResolver sysdate() { + return define(BuiltinFunctionName.SYSDATE.getName(), + impl(() -> new ExprDatetimeValue(formatNow(null)), DATETIME), + impl((v) -> new ExprDatetimeValue(formatNow(v.integerValue())), DATETIME, INTEGER) + ); + } + /** * Time implementation for ExprValue. * diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index b5c40b7d78..850780ad21 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 @@ -77,6 +77,8 @@ public enum BuiltinFunctionName { MINUTE(FunctionName.of("minute")), MONTH(FunctionName.of("month")), MONTHNAME(FunctionName.of("monthname")), + PERIOD_ADD(FunctionName.of("period_add")), + PERIOD_DIFF(FunctionName.of("period_diff")), QUARTER(FunctionName.of("quarter")), SECOND(FunctionName.of("second")), SUBDATE(FunctionName.of("subdate")), diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeTestBase.java b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeTestBase.java index 6be1548608..88d30335d8 100644 --- a/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeTestBase.java +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeTestBase.java @@ -5,18 +5,22 @@ package org.opensearch.sql.expression.datetime; +import static org.opensearch.sql.data.model.ExprValueUtils.fromObjectValue; import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE; +import static org.opensearch.sql.data.type.ExprCoreType.LONG; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.temporal.Temporal; import java.util.List; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opensearch.sql.data.model.ExprDateValue; import org.opensearch.sql.data.model.ExprDatetimeValue; +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.expression.DSL; @@ -44,6 +48,48 @@ public class DateTimeTestBase extends ExpressionTestBase { @Autowired protected BuiltinFunctionRepository functionRepository; + protected ExprValue eval(Expression expression) { + return expression.valueOf(env); + } + + protected FunctionExpression fromUnixTime(Expression value) { + var func = functionRepository.resolve(new FunctionSignature(new FunctionName("from_unixtime"), + List.of(value.type()))); + return (FunctionExpression)func.apply(List.of(value)); + } + + protected FunctionExpression fromUnixTime(Expression value, Expression format) { + var func = functionRepository.resolve(new FunctionSignature(new FunctionName("from_unixtime"), + List.of(value.type(), format.type()))); + return (FunctionExpression)func.apply(List.of(value, format)); + } + + protected LocalDateTime fromUnixTime(Long value) { + return fromUnixTime(DSL.literal(value)).valueOf(null).datetimeValue(); + } + + protected LocalDateTime fromUnixTime(Double value) { + return fromUnixTime(DSL.literal(value)).valueOf(null).datetimeValue(); + } + + protected String fromUnixTime(Long value, String format) { + return fromUnixTime(DSL.literal(value), DSL.literal(format)).valueOf(null).stringValue(); + } + + protected String fromUnixTime(Double value, String format) { + return fromUnixTime(DSL.literal(value), DSL.literal(format)).valueOf(null).stringValue(); + } + + protected FunctionExpression makedate(Expression year, Expression dayOfYear) { + var func = functionRepository.resolve(new FunctionSignature(new FunctionName("makedate"), + List.of(DOUBLE, DOUBLE))); + return (FunctionExpression)func.apply(List.of(year, dayOfYear)); + } + + protected LocalDate makedate(Double year, Double dayOfYear) { + return makedate(DSL.literal(year), DSL.literal(dayOfYear)).valueOf(null).dateValue(); + } + protected FunctionExpression maketime(Expression hour, Expression minute, Expression second) { var func = functionRepository.resolve(new FunctionSignature(new FunctionName("maketime"), List.of(DOUBLE, DOUBLE, DOUBLE))); @@ -55,14 +101,24 @@ protected LocalTime maketime(Double hour, Double minute, Double second) { .valueOf(null).timeValue(); } - protected FunctionExpression makedate(Expression year, Expression dayOfYear) { - var func = functionRepository.resolve(new FunctionSignature(new FunctionName("makedate"), - List.of(DOUBLE, DOUBLE))); - return (FunctionExpression)func.apply(List.of(year, dayOfYear)); + protected FunctionExpression period_add(Expression period, Expression months) { + var func = functionRepository.resolve(new FunctionSignature(new FunctionName("period_add"), + List.of(LONG, LONG))); + return (FunctionExpression)func.apply(List.of(period, months)); } - protected LocalDate makedate(Double year, Double dayOfYear) { - return makedate(DSL.literal(year), DSL.literal(dayOfYear)).valueOf(null).dateValue(); + protected Long period_add(Long period, Long months) { + return period_add(DSL.literal(period), DSL.literal(months)).valueOf(null).longValue(); + } + + protected FunctionExpression period_diff(Expression first, Expression second) { + var func = functionRepository.resolve(new FunctionSignature(new FunctionName("period_diff"), + List.of(LONG, LONG))); + return (FunctionExpression)func.apply(List.of(first, second)); + } + + protected Long period_diff(Long first, Long second) { + return period_diff(DSL.literal(first), DSL.literal(second)).valueOf(null).longValue(); } protected FunctionExpression unixTimeStampExpr() { @@ -96,36 +152,4 @@ protected Double unixTimeStampOf(LocalDateTime value) { protected Double unixTimeStampOf(Instant value) { return unixTimeStampOf(DSL.literal(new ExprTimestampValue(value))).valueOf(null).doubleValue(); } - - protected FunctionExpression fromUnixTime(Expression value) { - var func = functionRepository.resolve(new FunctionSignature(new FunctionName("from_unixtime"), - List.of(value.type()))); - return (FunctionExpression)func.apply(List.of(value)); - } - - protected FunctionExpression fromUnixTime(Expression value, Expression format) { - var func = functionRepository.resolve(new FunctionSignature(new FunctionName("from_unixtime"), - List.of(value.type(), format.type()))); - return (FunctionExpression)func.apply(List.of(value, format)); - } - - protected LocalDateTime fromUnixTime(Long value) { - return fromUnixTime(DSL.literal(value)).valueOf(null).datetimeValue(); - } - - protected LocalDateTime fromUnixTime(Double value) { - return fromUnixTime(DSL.literal(value)).valueOf(null).datetimeValue(); - } - - protected String fromUnixTime(Long value, String format) { - return fromUnixTime(DSL.literal(value), DSL.literal(format)).valueOf(null).stringValue(); - } - - protected String fromUnixTime(Double value, String format) { - return fromUnixTime(DSL.literal(value), DSL.literal(format)).valueOf(null).stringValue(); - } - - protected ExprValue eval(Expression expression) { - return expression.valueOf(env); - } } diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/PeriodFunctionsTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/PeriodFunctionsTest.java new file mode 100644 index 0000000000..773e032d2c --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/PeriodFunctionsTest.java @@ -0,0 +1,103 @@ +/* + * 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.junit.jupiter.api.Assertions.assertNull; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opensearch.sql.expression.DSL; + +public class PeriodFunctionsTest extends DateTimeTestBase { + + /** + * Generate sample data for `PERIOD_ADD` function. + * @return A data set. + */ + public static Stream getTestDataForPeriodAdd() { + // arguments are: first arg for `PERIOD_ADD`, second arg and expected result value. + return Stream.of( + Arguments.of(1, 3, 200004), // Jan 2000 + 3 + Arguments.of(3, -1, 200002), // Mar 2000 - 1 + Arguments.of(12, 0, 200012), // Dec 2000 + 0 + Arguments.of(6104, 100, 206908), // Apr 2061 + 100m (8y4m) + Arguments.of(201510, 14, 201612) + ); + } + + @ParameterizedTest + @MethodSource("getTestDataForPeriodAdd") + public void period_add_with_different_data(long period, long months, long expected) { + assertEquals(expected, period_add(period, months)); + } + + /** + * Generate sample data for `PERIOD_DIFF` function. + * @return A data set. + */ + public static Stream getTestDataForPeriodDiff() { + // arguments are: first arg for `PERIOD_DIFF`, second arg and expected result value. + return Stream.of( + Arguments.of(1, 3, -2), // Jan - Mar 2000 + Arguments.of(3, 1, 2), // Mar - Jan 2000 + Arguments.of(12, 111, -11), // Dec 2000 - Nov 2001 + Arguments.of(2212, 201105, 139), // Dec 2022 - May 2011 + Arguments.of(200505, 7505, 360), // May 2005 - May 1975 + Arguments.of(6104, 8509, 907), // Apr 2061 - Sep 1985 + Arguments.of(207707, 7707, 1200) // Jul 2077 - Jul 1977 + ); + } + + @ParameterizedTest + @MethodSource("getTestDataForPeriodDiff") + public void period_diff_with_different_data(long period1, long period2, long expected) { + assertEquals(expected, period_diff(period1, period2)); + } + + @ParameterizedTest + @MethodSource("getTestDataForPeriodDiff") + public void two_way_conversion(long period1, long period2, long expected) { + assertEquals(0, period_diff(period_add(period1, -expected), period2)); + } + + /** + * Generate invalid sample data for test. + * @return A data set. + */ + public static Stream getInvalidTestData() { + return Stream.of( + Arguments.of(0), + Arguments.of(123), + Arguments.of(100), + Arguments.of(1234), + Arguments.of(1000), + Arguments.of(2020), + Arguments.of(12345), + Arguments.of(123456), + Arguments.of(1234567), + Arguments.of(200213), + Arguments.of(200300), + Arguments.of(-1), + Arguments.of(-1234), + Arguments.of(-123401) + ); + } + + /** + * Check that `PERIOD_ADD` and `PERIOD_DIFF` return NULL on invalid input. + * @param period An invalid data. + */ + @ParameterizedTest + @MethodSource("getInvalidTestData") + public void period_add_returns_null_on_invalid_input(long period) { + assertNull(period_add(DSL.literal(period), DSL.literal(1)).valueOf(null).value()); + assertNull(period_diff(DSL.literal(period), DSL.literal(1)).valueOf(null).value()); + assertNull(period_diff(DSL.literal(1), DSL.literal(period)).valueOf(null).value()); + } +} diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index eb49195ff1..ddde9a3559 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1797,6 +1797,52 @@ Example:: +------------------+----------------+ +PERIOD_ADD +---------- + +Description +>>>>>>>>>>> + +Usage: period_add(P, N) add N months to period P (in the format YYMM or YYYYMM). Returns a value in the format YYYYMM. + +Argument type: LONG, LONG + +Return type: LONG + +Example:: + + os> SELECT PERIOD_ADD(200801, 2), PERIOD_ADD(200801, -12) + fetched rows / total rows = 1/1 + +-------------------------+---------------------------+ + | PERIOD_ADD(200801, 2) | PERIOD_ADD(200801, -12) | + |-------------------------+---------------------------| + | 200803 | 200701 | + +-------------------------+---------------------------+ + + +PERIOD_DIFF +----------- + +Description +>>>>>>>>>>> + +Usage: period_diff(P1, P2) returns the number of months between periods P1 and P2 given in the format YYMM or YYYYMM. + +Argument type: LONG, LONG + +Return type: LONG + +Example:: + + os> SELECT PERIOD_DIFF(200802, 200703), PERIOD_DIFF(200802, 201003) + fetched rows / total rows = 1/1 + +-------------------------------+-------------------------------+ + | PERIOD_DIFF(200802, 200703) | PERIOD_DIFF(200802, 201003) | + |-------------------------------+-------------------------------| + | 11 | -25 | + +-------------------------------+-------------------------------+ + + QUARTER ------- diff --git a/docs/user/ppl/functions/datetime.rst b/docs/user/ppl/functions/datetime.rst index a9b61097be..beddad4b35 100644 --- a/docs/user/ppl/functions/datetime.rst +++ b/docs/user/ppl/functions/datetime.rst @@ -957,6 +957,52 @@ Example:: +------------------+----------------+ +PERIOD_ADD +---------- + +Description +>>>>>>>>>>> + +Usage: period_add(P, N) add N months to period P (in the format YYMM or YYYYMM). Returns a value in the format YYYYMM. + +Argument type: LONG, LONG + +Return type: LONG + +Example:: + + os> source=people | eval `PERIOD_ADD(200801, 2)` = PERIOD_ADD(200801, 2), `PERIOD_ADD(200801, -12)` = PERIOD_ADD(200801, -12) | fields `PERIOD_ADD(200801, 2)`, `PERIOD_ADD(200801, -12)` + fetched rows / total rows = 1/1 + +-------------------------+---------------------------+ + | PERIOD_ADD(200801, 2) | PERIOD_ADD(200801, -12) | + |-------------------------+---------------------------| + | 200803 | 200701 | + +-------------------------+---------------------------+ + + +PERIOD_DIFF +----------- + +Description +>>>>>>>>>>> + +Usage: period_diff(P1, P2) returns the number of months between periods P1 and P2 given in the format YYMM or YYYYMM. + +Argument type: LONG, LONG + +Return type: LONG + +Example:: + + os> source=people | eval `PERIOD_DIFF(200802, 200703)` = PERIOD_DIFF(200802, 200703), `PERIOD_DIFF(200802, 201003)` = PERIOD_DIFF(200802, 201003) | fields `PERIOD_DIFF(200802, 200703)`, `PERIOD_DIFF(200802, 201003)` + fetched rows / total rows = 1/1 + +-------------------------------+-------------------------------+ + | PERIOD_DIFF(200802, 200703) | PERIOD_DIFF(200802, 201003) | + |-------------------------------+-------------------------------| + | 11 | -25 | + +-------------------------------+-------------------------------+ + + QUARTER ------- 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 d35dcc566b..a10f6fa3f7 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,12 +13,12 @@ import static org.opensearch.sql.util.MatcherUtils.verifySchema; import static org.opensearch.sql.util.MatcherUtils.verifySome; +import com.google.common.collect.ImmutableMap; import java.io.IOException; +import java.time.LocalDate; import java.time.LocalTime; import java.time.Duration; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.Period; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -30,13 +30,12 @@ import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collectors; -import com.google.common.collect.ImmutableMap; import org.json.JSONArray; -import java.time.LocalTime; import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.opensearch.sql.common.utils.StringUtils; +@SuppressWarnings("unchecked") public class DateTimeFunctionIT extends PPLIntegTestCase { @Override @@ -644,7 +643,6 @@ public void testDateFormat() throws IOException { verifyDateFormat(date, "date", dateFormat, dateFormatted); } - @Test public void testDateFormatISO8601() throws IOException { String timestamp = "1998-01-31 13:14:15.012345"; @@ -661,7 +659,7 @@ public void testDateFormatISO8601() throws IOException { @Test public void testMakeTime() throws IOException { var result = executeQuery(String.format( - "source=%s | eval f1 = MAKETIME(20, 30, 40), f2 = MAKETIME(20.2, 49.5, 42.100502) | fields f1, f2", TEST_INDEX_DATE)); + "source=%s | eval f1 = MAKETIME(20, 30, 40), f2 = MAKETIME(20.2, 49.5, 42.100502) | fields f1, f2", TEST_INDEX_DATE)); verifySchema(result, schema("f1", null, "time"), schema("f2", null, "time")); verifySome(result.getJSONArray("datarows"), rows("20:30:40", "20:50:42.100502")); } @@ -669,7 +667,7 @@ public void testMakeTime() throws IOException { @Test public void testMakeDate() throws IOException { var result = executeQuery(String.format( - "source=%s | eval f1 = MAKEDATE(1945, 5.9), f2 = MAKEDATE(1984, 1984) | fields f1, f2", TEST_INDEX_DATE)); + "source=%s | eval f1 = MAKEDATE(1945, 5.9), f2 = MAKEDATE(1984, 1984) | fields f1, f2", TEST_INDEX_DATE)); verifySchema(result, schema("f1", null, "date"), schema("f2", null, "date")); verifySome(result.getJSONArray("datarows"), rows("1945-01-06", "1989-06-06")); } @@ -862,4 +860,20 @@ public void testUnixTimeStamp() throws IOException { schema("f3", null, "double")); verifySome(result.getJSONArray("datarows"), rows(613094400d, 1072872000d, 3404817525d)); } + + @Test + public void testPeriodAdd() throws IOException { + var result = executeQuery(String.format( + "source=%s | eval f1 = PERIOD_ADD(200801, 2), f2 = PERIOD_ADD(200801, -12) | fields f1, f2", TEST_INDEX_DATE)); + verifySchema(result, schema("f1", null, "long"), schema("f2", null, "long")); + verifySome(result.getJSONArray("datarows"), rows(200803, 200701)); + } + + @Test + public void testPeriodDiff() throws IOException { + var result = executeQuery(String.format( + "source=%s | eval f1 = PERIOD_DIFF(200802, 200703), f2 = PERIOD_DIFF(200802, 201003) | fields f1, f2", TEST_INDEX_DATE)); + verifySchema(result, schema("f1", null, "long"), schema("f2", null, "long")); + verifySome(result.getJSONArray("datarows"), rows(11, -25)); + } } diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java index 207c3beb7d..9684334cf4 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java @@ -7,9 +7,7 @@ package org.opensearch.sql.sql; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK; -import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATE; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_PEOPLE2; -import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATE; import static org.opensearch.sql.legacy.plugin.RestSqlAction.QUERY_API_ENDPOINT; import static org.opensearch.sql.util.MatcherUtils.rows; import static org.opensearch.sql.util.MatcherUtils.schema; @@ -18,6 +16,7 @@ import static org.opensearch.sql.util.MatcherUtils.verifySome; import static org.opensearch.sql.util.TestUtils.getResponseBody; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.time.Duration; import java.time.LocalDate; @@ -34,7 +33,6 @@ import java.util.TimeZone; import java.util.function.BiFunction; import java.util.function.Supplier; -import com.google.common.collect.ImmutableMap; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; @@ -475,18 +473,22 @@ public void testDateFormat() throws IOException { @Test public void testMakeTime() throws IOException { - var result = executeQuery(String.format( - "select MAKETIME(20, 30, 40) as f1, MAKETIME(20.2, 49.5, 42.100502) as f2", TEST_INDEX_DATE)); - verifySchema(result, schema("MAKETIME(20, 30, 40)", "f1", "time"), schema("MAKETIME(20.2, 49.5, 42.100502)", "f2", "time")); - verifySome(result.getJSONArray("datarows"), rows("20:30:40", "20:50:42.100502")); + var result = executeQuery( + "select MAKETIME(20, 30, 40) as f1, MAKETIME(20.2, 49.5, 42.100502) as f2"); + verifySchema(result, + schema("MAKETIME(20, 30, 40)", "f1", "time"), + schema("MAKETIME(20.2, 49.5, 42.100502)", "f2", "time")); + verifyDataRows(result, rows("20:30:40", "20:50:42.100502")); } @Test public void testMakeDate() throws IOException { - var result = executeQuery(String.format( - "select MAKEDATE(1945, 5.9) as f1, MAKEDATE(1984, 1984) as f2", TEST_INDEX_DATE)); - verifySchema(result, schema("MAKEDATE(1945, 5.9)", "f1", "date"), schema("MAKEDATE(1984, 1984)", "f2", "date")); - verifySome(result.getJSONArray("datarows"), rows("1945-01-06", "1989-06-06")); + var result = executeQuery( + "select MAKEDATE(1945, 5.9) as f1, MAKEDATE(1984, 1984) as f2"); + verifySchema(result, + schema("MAKEDATE(1945, 5.9)", "f1", "date"), + schema("MAKEDATE(1984, 1984)", "f2", "date")); + verifyDataRows(result, rows("1945-01-06", "1989-06-06")); } private List> nowLikeFunctionsData() { @@ -674,6 +676,26 @@ public void testUnixTimeStamp() throws IOException { verifySome(result.getJSONArray("datarows"), rows(613094400d, 1072872000d, 3404817525d)); } + @Test + public void testPeriodAdd() throws IOException { + var result = executeQuery( + "select PERIOD_ADD(200801, 2) as f1, PERIOD_ADD(200801, -12) as f2"); + verifySchema(result, + schema("PERIOD_ADD(200801, 2)", "f1", "long"), + schema("PERIOD_ADD(200801, -12)", "f2", "long")); + verifyDataRows(result, rows(200803, 200701)); + } + + @Test + public void testPeriodDiff() throws IOException { + var result = executeQuery( + "select PERIOD_DIFF(200802, 200703) as f1, PERIOD_DIFF(200802, 201003) as f2"); + verifySchema(result, + schema("PERIOD_DIFF(200802, 200703)", "f1", "long"), + schema("PERIOD_DIFF(200802, 201003)", "f2", "long")); + verifyDataRows(result, rows(11, -25)); + } + protected JSONObject executeQuery(String query) throws IOException { Request request = new Request("POST", QUERY_API_ENDPOINT); request.setJsonEntity(String.format(Locale.ROOT, "{\n" + " \"query\": \"%s\"\n" + "}", query)); diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 index 230e183855..582c14767f 100644 --- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 @@ -240,10 +240,10 @@ DATE: 'DATE'; DATE_ADD: 'DATE_ADD'; DATE_FORMAT: 'DATE_FORMAT'; DATE_SUB: 'DATE_SUB'; +DAYNAME: 'DAYNAME'; DAYOFMONTH: 'DAYOFMONTH'; DAYOFWEEK: 'DAYOFWEEK'; DAYOFYEAR: 'DAYOFYEAR'; -DAYNAME: 'DAYNAME'; FROM_DAYS: 'FROM_DAYS'; LOCALTIME: 'LOCALTIME'; LOCALTIMESTAMP: 'LOCALTIMESTAMP'; @@ -252,6 +252,8 @@ MAKEDATE: 'MAKEDATE'; MAKETIME: 'MAKETIME'; MONTHNAME: 'MONTHNAME'; NOW: 'NOW'; +PERIOD_ADD: 'PERIOD_ADD'; +PERIOD_DIFF: 'PERIOD_DIFF'; SUBDATE: 'SUBDATE'; SYSDATE: 'SYSDATE'; TIME: 'TIME'; diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4 index 5773d4975c..0fbc83e047 100644 --- a/ppl/src/main/antlr/OpenSearchPPLParser.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4 @@ -395,10 +395,11 @@ trigonometricFunctionName ; dateAndTimeFunctionBase - : ADDDATE | CONVERT_TZ | DATE | DATETIME | DATE_ADD | DATE_FORMAT | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK - | DAYOFYEAR | FROM_DAYS | FROM_UNIXTIME | HOUR | MAKEDATE | MAKETIME | MICROSECOND | MINUTE - | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | SYSDATE | TIME | TIMESTAMP | TIME_TO_SEC - | TO_DAYS | UNIX_TIMESTAMP | WEEK | YEAR + : ADDDATE | CONVERT_TZ | DATE | DATE_ADD | DATE_FORMAT | DATE_SUB + | DATETIME | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | FROM_UNIXTIME + | HOUR | MAKEDATE | MAKETIME | MICROSECOND | MINUTE | MONTH | MONTHNAME | PERIOD_ADD + | PERIOD_DIFF | QUARTER | SECOND | SUBDATE | SYSDATE | TIME | TIME_TO_SEC + | TIMESTAMP | TO_DAYS | UNIX_TIMESTAMP | WEEK | YEAR ; // Functions which value could be cached in scope of a single query diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index 5f2385bab3..fda08bfd2e 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -197,13 +197,13 @@ CURRENT_DATE: 'CURRENT_DATE'; CURRENT_TIME: 'CURRENT_TIME'; CURRENT_TIMESTAMP: 'CURRENT_TIMESTAMP'; DATE: 'DATE'; -DATE_FORMAT: 'DATE_FORMAT'; DATE_ADD: 'DATE_ADD'; +DATE_FORMAT: 'DATE_FORMAT'; DATE_SUB: 'DATE_SUB'; +DAYNAME: 'DAYNAME'; DAYOFMONTH: 'DAYOFMONTH'; DAYOFWEEK: 'DAYOFWEEK'; DAYOFYEAR: 'DAYOFYEAR'; -DAYNAME: 'DAYNAME'; DEGREES: 'DEGREES'; E: 'E'; EXP: 'EXP'; @@ -231,6 +231,8 @@ MONTHNAME: 'MONTHNAME'; MULTIPLY: 'MULTIPLY'; NOW: 'NOW'; NULLIF: 'NULLIF'; +PERIOD_ADD: 'PERIOD_ADD'; +PERIOD_DIFF: 'PERIOD_DIFF'; PI: 'PI'; POW: 'POW'; POWER: 'POWER'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index 23e2d9288d..2b48fcfbf1 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -394,10 +394,11 @@ trigonometricFunctionName ; dateTimeFunctionName - : ADDDATE | CONVERT_TZ | DATE | DATETIME | DATE_ADD | DATE_FORMAT | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK - | DAYOFYEAR | FROM_DAYS | FROM_UNIXTIME | HOUR | MAKEDATE | MAKETIME | MICROSECOND | MINUTE - | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | SYSDATE | TIME | TIME_TO_SEC | TIMESTAMP - | TO_DAYS | UNIX_TIMESTAMP | WEEK | YEAR + : ADDDATE | CONVERT_TZ | DATE | DATE_ADD | DATE_FORMAT | DATE_SUB + | DATETIME | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | FROM_UNIXTIME + | HOUR | MAKEDATE | MAKETIME | MICROSECOND | MINUTE | MONTH | MONTHNAME | PERIOD_ADD + | PERIOD_DIFF | QUARTER | SECOND | SUBDATE | SYSDATE | TIME | TIME_TO_SEC + | TIMESTAMP | TO_DAYS | UNIX_TIMESTAMP | WEEK | YEAR ; // Functions which value could be cached in scope of a single query