From 7b83e50f77bc28642afae793ff4a9b8aa942a47e Mon Sep 17 00:00:00 2001 From: GabeFernandez310 Date: Mon, 20 Mar 2023 10:37:45 -0700 Subject: [PATCH] Add `TIMESTAMPADD` Function To OpenSearch SQL Plugin (#1451) * Add `TIMESTAMPADD` Function To OpenSearch SQL Plugin Added Testing And Implementation for TIMESTAMPADD Signed-off-by: GabeFernandez310 * Fixed Formatting Signed-off-by: GabeFernandez310 --------- Signed-off-by: GabeFernandez310 --- .../org/opensearch/sql/expression/DSL.java | 9 + .../expression/datetime/DateTimeFunction.java | 75 +++++ .../function/BuiltinFunctionName.java | 1 + .../sql/expression/function/FunctionDSL.java | 137 ++++++++++ .../function/SerializableQuadFunction.java | 31 +++ .../expression/datetime/TimeStampAddTest.java | 258 ++++++++++++++++++ .../function/FunctionDSLTestBase.java | 8 + .../function/FunctionDSLimplFourArgTest.java | 32 +++ ...ionDSLimplWithPropertiesThreeArgsTest.java | 33 +++ .../FunctionDSLnullMissingHandlingTest.java | 50 ++++ docs/user/dql/functions.rst | 23 ++ .../sql/sql/DateTimeFunctionIT.java | 11 + sql/src/main/antlr/OpenSearchSQLLexer.g4 | 1 + sql/src/main/antlr/OpenSearchSQLParser.g4 | 17 +- .../sql/sql/parser/AstExpressionBuilder.java | 21 ++ .../sql/sql/antlr/SQLSyntaxParserTest.java | 6 + .../sql/parser/AstExpressionBuilderTest.java | 8 + 17 files changed, 719 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/opensearch/sql/expression/function/SerializableQuadFunction.java create mode 100644 core/src/test/java/org/opensearch/sql/expression/datetime/TimeStampAddTest.java create mode 100644 core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLimplFourArgTest.java create mode 100644 core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLimplWithPropertiesThreeArgsTest.java 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 b3b9ba016d..3321ec8483 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -919,6 +919,15 @@ public static FunctionExpression time_format(FunctionProperties functionProperti return compile(functionProperties, BuiltinFunctionName.TIME_FORMAT, expressions); } + public static FunctionExpression timestampadd(Expression... expressions) { + return timestampadd(FunctionProperties.None, expressions); + } + + public static FunctionExpression timestampadd(FunctionProperties functionProperties, + Expression... expressions) { + return compile(functionProperties, BuiltinFunctionName.TIMESTAMPADD, expressions); + } + public static FunctionExpression utc_date(FunctionProperties functionProperties, Expression... args) { return compile(functionProperties, BuiltinFunctionName.UTC_DATE, args); 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 f837d1b580..5ad17c4dcc 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 @@ -9,9 +9,12 @@ import static java.time.temporal.ChronoUnit.DAYS; import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MICROS; import static java.time.temporal.ChronoUnit.MINUTES; import static java.time.temporal.ChronoUnit.MONTHS; import static java.time.temporal.ChronoUnit.SECONDS; +import static java.time.temporal.ChronoUnit.WEEKS; +import static java.time.temporal.ChronoUnit.YEARS; 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; @@ -62,6 +65,7 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.format.TextStyle; +import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAmount; import java.util.Arrays; import java.util.Locale; @@ -84,6 +88,7 @@ import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.data.type.ExprCoreType; import org.opensearch.sql.exception.ExpressionEvaluationException; +import org.opensearch.sql.exception.SemanticCheckException; import org.opensearch.sql.expression.function.BuiltinFunctionName; import org.opensearch.sql.expression.function.BuiltinFunctionRepository; import org.opensearch.sql.expression.function.DefaultFunctionResolver; @@ -229,6 +234,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(time_to_sec()); repository.register(timediff()); repository.register(timestamp()); + repository.register(timestampadd()); repository.register(utc_date()); repository.register(utc_time()); repository.register(utc_timestamp()); @@ -893,6 +899,22 @@ private DefaultFunctionResolver timestamp() { TIMESTAMP, TIMESTAMP, TIMESTAMP)); } + private DefaultFunctionResolver timestampadd() { + return define(BuiltinFunctionName.TIMESTAMPADD.getName(), + impl(nullMissingHandling(DateTimeFunction::exprTimestampAdd), + DATETIME, STRING, INTEGER, DATETIME), + impl(nullMissingHandling(DateTimeFunction::exprTimestampAdd), + DATETIME, STRING, INTEGER, TIMESTAMP), + implWithProperties( + nullMissingHandlingWithProperties( + (functionProperties, part, amount, time) -> exprTimestampAddForTimeType( + functionProperties.getQueryStartClock(), + part, + amount, + time)), + DATETIME, STRING, INTEGER, TIME)); + } + /** * TO_DAYS(STRING/DATE/DATETIME/TIMESTAMP). return the day number of the given date. */ @@ -1796,6 +1818,59 @@ private ExprValue exprTimeToSec(ExprValue time) { return new ExprLongValue(time.timeValue().toSecondOfDay()); } + private ExprValue exprTimestampAdd(ExprValue partExpr, + ExprValue amountExpr, + ExprValue datetimeExpr) { + String part = partExpr.stringValue(); + int amount = amountExpr.integerValue(); + LocalDateTime datetime = datetimeExpr.datetimeValue(); + ChronoUnit temporalUnit; + + switch (part) { + case "MICROSECOND": + temporalUnit = MICROS; + break; + case "SECOND": + temporalUnit = SECONDS; + break; + case "MINUTE": + temporalUnit = MINUTES; + break; + case "HOUR": + temporalUnit = HOURS; + break; + case "DAY": + temporalUnit = DAYS; + break; + case "WEEK": + temporalUnit = WEEKS; + break; + case "MONTH": + temporalUnit = MONTHS; + break; + case "QUARTER": + temporalUnit = MONTHS; + amount *= 3; + break; + case "YEAR": + temporalUnit = YEARS; + break; + default: + return ExprNullValue.of(); + } + return new ExprDatetimeValue(datetime.plus(amount, temporalUnit)); + } + + private ExprValue exprTimestampAddForTimeType(Clock clock, + ExprValue partExpr, + ExprValue amountExpr, + ExprValue timeExpr) { + LocalDateTime datetime = LocalDateTime.of( + formatNow(clock).toLocalDate(), + timeExpr.timeValue()); + return exprTimestampAdd(partExpr, amountExpr, new ExprDatetimeValue(datetime)); + } + /** * UTC_DATE 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 f1ac66f396..c0b2becf0f 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 @@ -108,6 +108,7 @@ public enum BuiltinFunctionName { TIMEDIFF(FunctionName.of("timediff")), TIME_TO_SEC(FunctionName.of("time_to_sec")), TIMESTAMP(FunctionName.of("timestamp")), + TIMESTAMPADD(FunctionName.of("timestampadd")), TIME_FORMAT(FunctionName.of("time_format")), TO_DAYS(FunctionName.of("to_days")), TO_SECONDS(FunctionName.of("to_seconds")), diff --git a/core/src/main/java/org/opensearch/sql/expression/function/FunctionDSL.java b/core/src/main/java/org/opensearch/sql/expression/function/FunctionDSL.java index d94d7cdf60..c57d96caea 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/FunctionDSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/FunctionDSL.java @@ -183,6 +183,60 @@ public String toString() { }; } + /** + * Implementation of a function that takes three arguments, returns a value, and + * requires FunctionProperties to complete. + * + * @param function {@link ExprValue} based Binary function. + * @param returnType return type. + * @param args1Type first argument type. + * @param args2Type second argument type. + * @param args3Type third argument type. + * @return Binary Function Implementation. + */ + public static SerializableFunction> + implWithProperties( + SerializableQuadFunction< + FunctionProperties, + ExprValue, + ExprValue, + ExprValue, + ExprValue> function, + ExprType returnType, + ExprType args1Type, + ExprType args2Type, + ExprType args3Type) { + + return functionName -> { + FunctionSignature functionSignature = + new FunctionSignature(functionName, Arrays.asList(args1Type, args2Type, args3Type)); + FunctionBuilder functionBuilder = + (functionProperties, arguments) -> new FunctionExpression(functionName, arguments) { + @Override + public ExprValue valueOf(Environment valueEnv) { + ExprValue arg1 = arguments.get(0).valueOf(valueEnv); + ExprValue arg2 = arguments.get(1).valueOf(valueEnv); + ExprValue arg3 = arguments.get(2).valueOf(valueEnv); + return function.apply(functionProperties, arg1, arg2, arg3); + } + + @Override + public ExprType type() { + return returnType; + } + + @Override + public String toString() { + return String.format("%s(%s)", functionName, + arguments.stream() + .map(Object::toString) + .collect(Collectors.joining(", "))); + } + }; + return Pair.of(functionSignature, functionBuilder); + }; + } + /** * No Arg Function Implementation. * @@ -275,6 +329,59 @@ public String toString() { }; } + /** + * Quadruple Function Implementation. + * + * @param function {@link ExprValue} based unary function. + * @param returnType return type. + * @param args1Type argument type. + * @param args2Type argument type. + * @param args3Type argument type. + * @return Quadruple Function Implementation. + */ + public static SerializableFunction> impl( + SerializableQuadFunction function, + ExprType returnType, + ExprType args1Type, + ExprType args2Type, + ExprType args3Type, + ExprType args4Type) { + + return functionName -> { + FunctionSignature functionSignature = + new FunctionSignature(functionName, Arrays.asList( + args1Type, + args2Type, + args3Type, + args4Type)); + FunctionBuilder functionBuilder = + (functionProperties, arguments) -> new FunctionExpression(functionName, arguments) { + @Override + public ExprValue valueOf(Environment valueEnv) { + ExprValue arg1 = arguments.get(0).valueOf(valueEnv); + ExprValue arg2 = arguments.get(1).valueOf(valueEnv); + ExprValue arg3 = arguments.get(2).valueOf(valueEnv); + ExprValue arg4 = arguments.get(3).valueOf(valueEnv); + return function.apply(arg1, arg2, arg3, arg4); + } + + @Override + public ExprType type() { + return returnType; + } + + @Override + public String toString() { + return String.format("%s(%s, %s, %s, %s)", functionName, arguments.get(0).toString(), + arguments.get(1).toString(), + arguments.get(2).toString(), + arguments.get(3).toString()); + } + }; + return Pair.of(functionSignature, functionBuilder); + }; + } + /** * Wrapper the unary ExprValue function with default NULL and MISSING handling. */ @@ -358,4 +465,34 @@ public SerializableTriFunction nullM } }; } + + /** + * Wrapper for the ExprValue function that takes 3 arguments and is aware of FunctionProperties, + * with default NULL and MISSING handling. + */ + public static SerializableQuadFunction< + FunctionProperties, + ExprValue, + ExprValue, + ExprValue, + ExprValue> + nullMissingHandlingWithProperties( + SerializableQuadFunction< + FunctionProperties, + ExprValue, + ExprValue, + ExprValue, + ExprValue> implementation) { + return (functionProperties, v1, v2, v3) -> { + if (v1.isMissing() || v2.isMissing() || v3.isMissing()) { + return ExprValueUtils.missingValue(); + } + + if (v1.isNull() || v2.isNull() || v3.isNull()) { + return ExprValueUtils.nullValue(); + } + + return implementation.apply(functionProperties, v1, v2, v3); + }; + } } diff --git a/core/src/main/java/org/opensearch/sql/expression/function/SerializableQuadFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/SerializableQuadFunction.java new file mode 100644 index 0000000000..056a17d5b3 --- /dev/null +++ b/core/src/main/java/org/opensearch/sql/expression/function/SerializableQuadFunction.java @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.sql.expression.function; + +import java.io.Serializable; + +/** + * Serializable Triple Function. + * + * @param the type of the first argument to the function + * @param the type of the second argument to the function + * @param the type of the third argument to the function + * @param the type of the fourth argument to the function + * @param the type of the result of the function + */ +public interface SerializableQuadFunction extends Serializable { + /** + * Applies this function to the given arguments. + * + * @param t the first function argument + * @param u the second function argument + * @param v the third function argument + * @param w the fourth function argument + * @return the function result + */ + R apply(T t, U u, V v, W w); +} diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/TimeStampAddTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/TimeStampAddTest.java new file mode 100644 index 0000000000..63514ab352 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/TimeStampAddTest.java @@ -0,0 +1,258 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.sql.expression.datetime; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opensearch.sql.data.model.ExprDateValue; +import org.opensearch.sql.data.model.ExprDatetimeValue; +import org.opensearch.sql.data.model.ExprIntegerValue; +import org.opensearch.sql.data.model.ExprNullValue; +import org.opensearch.sql.data.model.ExprStringValue; +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.exception.SemanticCheckException; +import org.opensearch.sql.expression.DSL; +import org.opensearch.sql.expression.Expression; +import org.opensearch.sql.expression.ExpressionTestBase; +import org.opensearch.sql.expression.FunctionExpression; + +class TimeStampAddTest extends ExpressionTestBase { + + private static Stream getTestDataForTimestampAdd() { + return Stream.of( + Arguments.of("MINUTE", 1, new ExprStringValue("2003-01-02 00:00:00"), + "2003-01-02 00:01:00"), + Arguments.of("WEEK", 1, new ExprStringValue("2003-01-02 00:00:00"), + "2003-01-09 00:00:00"), + + //Date + Arguments.of("MINUTE", 1, new ExprDateValue("2003-01-02"), + "2003-01-02 00:01:00"), + Arguments.of("WEEK", 1, new ExprDateValue("2003-01-02"), + "2003-01-09 00:00:00"), + + //Datetime + Arguments.of("MINUTE", 1, new ExprDatetimeValue("2003-01-02 00:00:00"), + "2003-01-02 00:01:00"), + Arguments.of("WEEK", 1, new ExprDatetimeValue("2003-01-02 00:00:00"), + "2003-01-09 00:00:00"), + + //Timestamp + Arguments.of("MINUTE", 1, new ExprTimestampValue("2003-01-02 00:00:00"), + "2003-01-02 00:01:00"), + Arguments.of("WEEK", 1, new ExprTimestampValue("2003-01-02 00:00:00"), + "2003-01-09 00:00:00"), + + //Cases surrounding leap year + Arguments.of("SECOND", 1, new ExprTimestampValue("2020-02-28 23:59:59"), + "2020-02-29 00:00:00"), + Arguments.of("MINUTE", 1, new ExprTimestampValue("2020-02-28 23:59:59"), + "2020-02-29 00:00:59"), + Arguments.of("HOUR", 1, new ExprTimestampValue("2020-02-28 23:59:59"), + "2020-02-29 00:59:59"), + Arguments.of("DAY", 1, new ExprTimestampValue("2020-02-28 23:59:59"), + "2020-02-29 23:59:59"), + Arguments.of("WEEK", 1, new ExprTimestampValue("2020-02-28 23:59:59"), + "2020-03-06 23:59:59"), + + //Cases surrounding end-of-year + Arguments.of("SECOND", 1, new ExprTimestampValue("2020-12-31 23:59:59"), + "2021-01-01 00:00:00"), + Arguments.of("MINUTE", 1, new ExprTimestampValue("2020-12-31 23:59:59"), + "2021-01-01 00:00:59"), + Arguments.of("HOUR", 1, new ExprTimestampValue("2020-12-31 23:59:59"), + "2021-01-01 00:59:59"), + Arguments.of("DAY", 1, new ExprTimestampValue("2020-12-31 23:59:59"), + "2021-01-01 23:59:59"), + Arguments.of("WEEK", 1, new ExprTimestampValue("2020-12-31 23:59:59"), + "2021-01-07 23:59:59"), + + //Test adding a month (including special cases) + Arguments.of("MONTH", 1, new ExprStringValue("2003-01-02 00:00:00"), + "2003-02-02 00:00:00"), + Arguments.of("MONTH", 1, new ExprDateValue("2024-03-30"), + "2024-04-30 00:00:00"), + Arguments.of("MONTH", 1, new ExprDateValue("2024-03-31"), + "2024-04-30 00:00:00"), + + //Test remaining interval types + Arguments.of("MICROSECOND", 123, new ExprStringValue("2003-01-02 00:00:00"), + "2003-01-02 00:00:00.000123"), + Arguments.of("QUARTER", 1, new ExprStringValue("2003-01-02 00:00:00"), + "2003-04-02 00:00:00"), + Arguments.of("YEAR", 1, new ExprStringValue("2003-01-02 00:00:00"), + "2004-01-02 00:00:00"), + + //Test negative value for amount (Test for all intervals) + Arguments.of("MICROSECOND", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-12-31 23:59:59.999999"), + Arguments.of("SECOND", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-12-31 23:59:59"), + Arguments.of("MINUTE", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-12-31 23:59:00"), + Arguments.of("HOUR", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-12-31 23:00:00"), + Arguments.of("DAY", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-12-31 00:00:00"), + Arguments.of("WEEK", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-12-25 00:00:00"), + Arguments.of("MONTH", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-12-01 00:00:00"), + Arguments.of("QUARTER", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-10-01 00:00:00"), + Arguments.of("YEAR", -1, new ExprStringValue("2000-01-01 00:00:00"), + "1999-01-01 00:00:00") + ); + } + + private static FunctionExpression timestampaddQuery(String unit, + int amount, + ExprValue datetimeExpr) { + return DSL.timestampadd( + DSL.literal(unit), + DSL.literal(new ExprIntegerValue(amount)), + DSL.literal(datetimeExpr) + ); + } + + @ParameterizedTest + @MethodSource("getTestDataForTimestampAdd") + public void testTimestampadd(String unit, int amount, ExprValue datetimeExpr, String expected) { + FunctionExpression expr = timestampaddQuery(unit, amount, datetimeExpr); + assertEquals(new ExprDatetimeValue(expected), eval(expr)); + } + + private static Stream getTestDataForTestAddingDatePartToTime() { + return Stream.of( + Arguments.of("DAY", 1, "10:11:12", LocalDate.now().plusDays(1)), + Arguments.of("DAY", 5, "10:11:12", LocalDate.now().plusDays(5)), + Arguments.of("DAY", 10, "10:11:12", LocalDate.now().plusDays(10)), + Arguments.of("DAY", -10, "10:11:12", LocalDate.now().plusDays(-10)), + Arguments.of("WEEK", 1, "10:11:12", LocalDate.now().plusWeeks(1)), + Arguments.of("WEEK", 5, "10:11:12", LocalDate.now().plusWeeks(5)), + Arguments.of("WEEK", 10, "10:11:12", LocalDate.now().plusWeeks(10)), + Arguments.of("WEEK", -10, "10:11:12", LocalDate.now().plusWeeks(-10)), + Arguments.of("MONTH", 1, "10:11:12", LocalDate.now().plusMonths(1)), + Arguments.of("MONTH", 5, "10:11:12", LocalDate.now().plusMonths(5)), + Arguments.of("MONTH", 10, "10:11:12", LocalDate.now().plusMonths(10)), + Arguments.of("MONTH", -10, "10:11:12", LocalDate.now().plusMonths(-10)), + Arguments.of("QUARTER", 1, "10:11:12", LocalDate.now().plusMonths(3 * 1)), + Arguments.of("QUARTER", 3, "10:11:12", LocalDate.now().plusMonths(3 * 3)), + Arguments.of("QUARTER", 5, "10:11:12", LocalDate.now().plusMonths(3 * 5)), + Arguments.of("QUARTER", -5, "10:11:12", LocalDate.now().plusMonths(3 * -5)), + Arguments.of("YEAR", 1, "10:11:12", LocalDate.now().plusYears(1)), + Arguments.of("YEAR", 5, "10:11:12", LocalDate.now().plusYears(5)), + Arguments.of("YEAR", 10, "10:11:12", LocalDate.now().plusYears(10)), + Arguments.of("YEAR", -10, "10:11:12", LocalDate.now().plusYears(-10)) + ); + } + + @ParameterizedTest + @MethodSource("getTestDataForTestAddingDatePartToTime") + public void testAddingDatePartToTime(String interval, + int addedInterval, + String timeArg, + LocalDate expectedDate) { + FunctionExpression expr = DSL.timestampadd( + functionProperties, + DSL.literal(interval), + DSL.literal(new ExprIntegerValue(addedInterval)), + DSL.literal(new ExprTimeValue(timeArg)) + ); + + LocalDateTime expected1 = LocalDateTime.of(expectedDate, LocalTime.parse(timeArg)); + + assertEquals(new ExprDatetimeValue(expected1), eval(expr)); + } + + @Test + public void testAddingTimePartToTime() { + String interval = "MINUTE"; + int addedInterval = 1; + String timeArg = "10:11:12"; + + FunctionExpression expr = DSL.timestampadd( + functionProperties, + DSL.literal(interval), + DSL.literal(new ExprIntegerValue(addedInterval)), + DSL.literal(new ExprTimeValue(timeArg)) + ); + + LocalDateTime expected = LocalDateTime.of( + LocalDate.now(), + LocalTime.parse(timeArg).plusMinutes(addedInterval)); + + assertEquals(new ExprDatetimeValue(expected), eval(expr)); + } + + @Test + public void testDifferentInputTypesHaveSameResult() { + String part = "SECOND"; + int amount = 1; + FunctionExpression dateExpr = timestampaddQuery( + part, + amount, + new ExprDateValue("2000-01-01")); + + FunctionExpression stringExpr = timestampaddQuery( + part, + amount, + new ExprStringValue("2000-01-01 00:00:00")); + + FunctionExpression datetimeExpr = timestampaddQuery( + part, + amount, + new ExprDatetimeValue("2000-01-01 00:00:00")); + + FunctionExpression timestampExpr = timestampaddQuery( + part, + amount, + new ExprTimestampValue("2000-01-01 00:00:00")); + + assertAll( + () -> assertEquals(eval(dateExpr), eval(stringExpr)), + () -> assertEquals(eval(dateExpr), eval(datetimeExpr)), + () -> assertEquals(eval(dateExpr), eval(timestampExpr)) + ); + } + + private static Stream getInvalidTestDataForTimestampAdd() { + return Stream.of( + Arguments.of("WEEK", 1, new ExprStringValue("2000-13-01")), + Arguments.of("WEEK", 1, new ExprStringValue("2000-01-40")) + ); + } + + @ParameterizedTest + @MethodSource("getInvalidTestDataForTimestampAdd") + public void testInvalidArguments(String interval, int amount, ExprValue datetimeExpr) { + FunctionExpression expr = timestampaddQuery(interval, amount, datetimeExpr); + assertThrows(SemanticCheckException.class, () -> eval(expr)); + } + + @Test + public void testNullReturnValue() { + FunctionExpression expr = timestampaddQuery("INVALID", 1, new ExprDateValue("2000-01-01")); + assertEquals(ExprNullValue.of(), eval(expr)); + } + + private ExprValue eval(Expression expression) { + return expression.valueOf(); + } +} diff --git a/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLTestBase.java b/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLTestBase.java index 63c6ea3329..f5f4128451 100644 --- a/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLTestBase.java +++ b/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLTestBase.java @@ -55,10 +55,18 @@ public int compareTo(ExprValue o) { static final SerializableTriFunction twoArgWithProperties = (functionProperties, v1, v2) -> ANY; + static final SerializableQuadFunction + + threeArgsWithProperties = (functionProperties, v1, v2, v3) -> ANY; + static final SerializableBiFunction twoArgs = (v1, v2) -> ANY; static final SerializableTriFunction threeArgs = (v1, v2, v3) -> ANY; + + static final SerializableQuadFunction + fourArgs = (v1, v2, v3, v4) -> ANY; + @Mock FunctionProperties mockProperties; } diff --git a/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLimplFourArgTest.java b/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLimplFourArgTest.java new file mode 100644 index 0000000000..ea3cbc1166 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLimplFourArgTest.java @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function; + +import static org.opensearch.sql.expression.function.FunctionDSL.impl; + +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import org.opensearch.sql.expression.DSL; +import org.opensearch.sql.expression.Expression; + +class FunctionDSLimplFourArgTest extends FunctionDSLimplTestBase { + + @Override + SerializableFunction> + getImplementationGenerator() { + return impl(fourArgs, ANY_TYPE, ANY_TYPE, ANY_TYPE, ANY_TYPE, ANY_TYPE); + } + + @Override + List getSampleArguments() { + return List.of(DSL.literal(ANY), DSL.literal(ANY), DSL.literal(ANY), DSL.literal(ANY)); + } + + @Override + String getExpected_toString() { + return "sample(ANY, ANY, ANY, ANY)"; + } +} diff --git a/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLimplWithPropertiesThreeArgsTest.java b/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLimplWithPropertiesThreeArgsTest.java new file mode 100644 index 0000000000..b79860abc2 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLimplWithPropertiesThreeArgsTest.java @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function; + +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import org.opensearch.sql.data.model.ExprValue; +import org.opensearch.sql.expression.DSL; +import org.opensearch.sql.expression.Expression; + +class FunctionDSLimplWithPropertiesThreeArgsTest extends FunctionDSLimplTestBase { + + @Override + SerializableFunction> + getImplementationGenerator() { + SerializableQuadFunction + functionBody = (fp, arg1, arg2, arg3) -> ANY; + return FunctionDSL.implWithProperties(functionBody, ANY_TYPE, ANY_TYPE, ANY_TYPE, ANY_TYPE); + } + + @Override + List getSampleArguments() { + return List.of(DSL.literal(ANY), DSL.literal(ANY), DSL.literal(ANY)); + } + + @Override + String getExpected_toString() { + return "sample(ANY, ANY, ANY)"; + } +} diff --git a/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLnullMissingHandlingTest.java b/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLnullMissingHandlingTest.java index 17f1de355f..0cea222843 100644 --- a/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLnullMissingHandlingTest.java +++ b/core/src/test/java/org/opensearch/sql/expression/function/FunctionDSLnullMissingHandlingTest.java @@ -82,6 +82,56 @@ void nullMissingHandling_twoArgs_FunctionProperties_apply() { .apply(functionProperties, ANY, ANY)); } + @Test + void nullMissingHandling_threeArgs_FunctionProperties_nullValue_firstArg() { + assertEquals(NULL, + nullMissingHandlingWithProperties(threeArgsWithProperties) + .apply(functionProperties, NULL, ANY, ANY)); + } + + @Test + void nullMissingHandling_threeArgs_FunctionProperties_nullValue_secondArg() { + assertEquals(NULL, + nullMissingHandlingWithProperties(threeArgsWithProperties) + .apply(functionProperties, ANY, NULL, ANY)); + } + + @Test + void nullMissingHandling_threeArgs_FunctionProperties_nullValue_thirdArg() { + assertEquals(NULL, + nullMissingHandlingWithProperties(threeArgsWithProperties) + .apply(functionProperties, ANY, ANY, NULL)); + } + + + @Test + void nullMissingHandling_threeArgs_FunctionProperties_missingValue_firstArg() { + assertEquals(MISSING, + nullMissingHandlingWithProperties(threeArgsWithProperties) + .apply(functionProperties, MISSING, ANY, ANY)); + } + + @Test + void nullMissingHandling_threeArgs_FunctionProperties_missingValue_secondArg() { + assertEquals(MISSING, + nullMissingHandlingWithProperties(threeArgsWithProperties) + .apply(functionProperties, ANY, MISSING, ANY)); + } + + @Test + void nullMissingHandling_threeArgs_FunctionProperties_missingValue_thirdArg() { + assertEquals(MISSING, + nullMissingHandlingWithProperties(threeArgsWithProperties) + .apply(functionProperties, ANY, ANY, MISSING)); + } + + @Test + void nullMissingHandling_threeArgs_FunctionProperties_apply() { + assertEquals(ANY, + nullMissingHandlingWithProperties(threeArgsWithProperties) + .apply(functionProperties, ANY, ANY, ANY)); + } + @Test void nullMissingHandling_twoArgs_firstArg_nullValue() { assertEquals(NULL, nullMissingHandling(twoArgs).apply(NULL, ANY)); diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index ee39f47aa5..4404bcf2de 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -2704,6 +2704,29 @@ Example:: | 2020-08-26 13:49:00 | 2020-08-27 02:04:42 | +------------------------------------+------------------------------------------------------+ +TIMESTAMPADD +------------ + + +Description +>>>>>>>>>>> + +Usage: Returns a DATETIME value based on a passed in DATE/DATETIME/TIME/TIMESTAMP/STRING argument and an INTERVAL and INTEGER argument which determine the amount of time to be added. +If the third argument is a STRING, it must be formatted as a valid DATETIME. If only a TIME is provided, a DATETIME is still returned with the DATE portion filled in using the current date. +If the third argument is a DATE, it will be automatically converted to a DATETIME. + +Argument type: INTERVAL, INTEGER, DATE/DATETIME/TIME/TIMESTAMP/STRING +INTERVAL must be one of the following tokens: [MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR] + +Examples:: + + os> SELECT TIMESTAMPADD(DAY, 17, '2000-01-01 00:00:00'), TIMESTAMPADD(QUARTER, -1, '2000-01-01 00:00:00') + fetched rows / total rows = 1/1 + +------------------------------------------------+----------------------------------------------------+ + | TIMESTAMPADD(DAY, 17, '2000-01-01 00:00:00') | TIMESTAMPADD(QUARTER, -1, '2000-01-01 00:00:00') | + |------------------------------------------------+----------------------------------------------------| + | 2000-01-18 00:00:00 | 1999-10-01 00:00:00 | + +------------------------------------------------+----------------------------------------------------+ TO_DAYS ------- 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 98166283fa..ce9a2f2302 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 @@ -962,6 +962,17 @@ public void testSubDateWithInterval() throws IOException { .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))); } + @Test + public void testTimstampadd() throws IOException { + JSONObject result = executeQuery( + String.format("SELECT timestampadd(WEEK, 2, time0) FROM %s LIMIT 3", TEST_INDEX_CALCS)); + + verifyDataRows(result, + rows("1900-01-13 21:07:32"), + rows("1900-01-15 13:48:48"), + rows("1900-01-15 18:21:08")); + } + @Test public void testTimeToSec() throws IOException { JSONObject result = executeQuery("select time_to_sec(time('17:30:00'))"); diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index 55e239a065..c0b36b0279 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -332,6 +332,7 @@ SECOND_OF_MINUTE: 'SECOND_OF_MINUTE'; STATS: 'STATS'; TERM: 'TERM'; TERMS: 'TERMS'; +TIMESTAMPADD: 'TIMESTAMPADD'; TOPHITS: 'TOPHITS'; TYPEOF: 'TYPEOF'; WEEK_OF_YEAR: 'WEEK_OF_YEAR'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index 3eded575cd..71f573b9f4 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -311,6 +311,11 @@ functionCall | positionFunction #positionFunctionCall | extractFunction #extractFunctionCall | getFormatFunction #getFormatFunctionCall + | timestampAddFunction #timestampAddFunctionCall + ; + +timestampAddFunction + : TIMESTAMPADD LR_BRACKET simpleDateTimePart COMMA length=functionArg COMMA timestampExpr=functionArg RR_BRACKET ; getFormatFunction @@ -328,7 +333,7 @@ extractFunction : EXTRACT LR_BRACKET datetimePart FROM functionArg RR_BRACKET ; -datetimePart +simpleDateTimePart : MICROSECOND | SECOND | MINUTE @@ -338,7 +343,10 @@ datetimePart | MONTH | QUARTER | YEAR - | SECOND_MICROSECOND + ; + +complexDateTimePart + : SECOND_MICROSECOND | MINUTE_MICROSECOND | MINUTE_SECOND | HOUR_MICROSECOND @@ -351,6 +359,11 @@ datetimePart | YEAR_MONTH ; +datetimePart + : simpleDateTimePart + | complexDateTimePart + ; + highlightFunction : HIGHLIGHT LR_BRACKET relevanceField (COMMA highlightArg)* RR_BRACKET ; diff --git a/sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java b/sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java index d1afd90246..ad8b78487b 100644 --- a/sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java +++ b/sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java @@ -61,6 +61,7 @@ import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.StringLiteralContext; import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.TableFilterContext; import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.TimeLiteralContext; +import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.TimestampAddFunctionCallContext; import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.TimestampLiteralContext; import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.WindowFunctionClauseContext; import static org.opensearch.sql.sql.parser.ParserUtils.createSortOption; @@ -173,6 +174,14 @@ public UnresolvedExpression visitHighlightFunctionCall( builder.build()); } + + @Override + public UnresolvedExpression visitTimestampAddFunctionCall(TimestampAddFunctionCallContext ctx) { + return new Function( + ctx.timestampAddFunction().TIMESTAMPADD().toString(), + timestampAddFunctionArguments(ctx)); + } + @Override public UnresolvedExpression visitPositionFunction( PositionFunctionContext ctx) { @@ -581,6 +590,18 @@ private List getFormatFunctionArguments( return args; } + private List timestampAddFunctionArguments( + TimestampAddFunctionCallContext ctx) { + List args = Arrays.asList( + new Literal( + ctx.timestampAddFunction().simpleDateTimePart().getText(), + DataType.STRING), + visitFunctionArg(ctx.timestampAddFunction().length), + visitFunctionArg(ctx.timestampAddFunction().timestampExpr) + ); + return args; + } + /** * Adds support for multi_match alternate syntax like * MULTI_MATCH('query'='Dale', 'fields'='*name'). diff --git a/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java b/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java index 415a77e17c..c722d1314f 100644 --- a/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java +++ b/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java @@ -602,6 +602,12 @@ public void can_parse_last_day_function() { assertNotNull(parser.parse("SELECT last_day('2004-01-01 01:01:01')")); } + @Test + public void can_parse_timestampadd_function() { + assertNotNull(parser.parse("SELECT TIMESTAMPADD(MINUTE, 1, '2003-01-02')")); + assertNotNull(parser.parse("SELECT TIMESTAMPADD(WEEK,1,'2003-01-02')")); + } + @Test public void can_parse_to_seconds_function() { assertNotNull(parser.parse("SELECT to_seconds(\"2023-02-20\")")); diff --git a/sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java b/sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java index f55b92dde7..29b65cd4cc 100644 --- a/sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java +++ b/sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java @@ -229,6 +229,14 @@ public void canBuildDateAndTimeFunctionCall() { ); } + @Test + public void canBuildTimestampAddFunctionCall() { + assertEquals( + function("timestampadd", stringLiteral("WEEK"), intLiteral(1), dateLiteral("2023-03-14")), + buildExprAst("timestampadd(WEEK, 1, DATE '2023-03-14')") + ); + } + @Test public void canBuildComparisonExpression() { assertEquals(