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 15af6165b8..2f791f46b6 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -924,6 +924,12 @@ public static FunctionExpression timestampadd(FunctionProperties functionPropert return compile(functionProperties, BuiltinFunctionName.TIMESTAMPADD, expressions); } + public static FunctionExpression timestampdiff(FunctionProperties functionProperties, + Expression... expressions) { + return compile(functionProperties, BuiltinFunctionName.TIMESTAMPDIFF, 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 5ad17c4dcc..36541009f7 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 @@ -235,6 +235,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(timediff()); repository.register(timestamp()); repository.register(timestampadd()); + repository.register(timestampdiff()); repository.register(utc_date()); repository.register(utc_time()); repository.register(utc_timestamp()); @@ -899,6 +900,15 @@ private DefaultFunctionResolver timestamp() { TIMESTAMP, TIMESTAMP, TIMESTAMP)); } + /** + * Adds an interval of time to the provided DATE/DATETIME/TIME/TIMESTAMP/STRING argument. + * The interval of time added is determined by the given first and second arguments. + * The first argument is an interval type, and must be one of the tokens below... + * [MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR] + * The second argument is the amount of the interval type to be added. + * The third argument is the DATE/DATETIME/TIME/TIMESTAMP/STRING to add to. + * @return The DATETIME representing the summed DATE/DATETIME/TIME/TIMESTAMP and interval. + */ private DefaultFunctionResolver timestampadd() { return define(BuiltinFunctionName.TIMESTAMPADD.getName(), impl(nullMissingHandling(DateTimeFunction::exprTimestampAdd), @@ -915,6 +925,35 @@ private DefaultFunctionResolver timestampadd() { DATETIME, STRING, INTEGER, TIME)); } + /** + * Finds the difference between provided DATE/DATETIME/TIME/TIMESTAMP/STRING arguments. + * The first argument is an interval type, and must be one of the tokens below... + * [MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR] + * The second argument the DATE/DATETIME/TIME/TIMESTAMP/STRING representing the start time. + * The third argument is the DATE/DATETIME/TIME/TIMESTAMP/STRING representing the end time. + * @return A LONG representing the difference between arguments, using the given interval type. + */ + private DefaultFunctionResolver timestampdiff() { + return define(BuiltinFunctionName.TIMESTAMPDIFF.getName(), + impl(nullMissingHandling(DateTimeFunction::exprTimestampDiff), + DATETIME, STRING, DATETIME, DATETIME), + impl(nullMissingHandling(DateTimeFunction::exprTimestampDiff), + DATETIME, STRING, DATETIME, TIMESTAMP), + impl(nullMissingHandling(DateTimeFunction::exprTimestampDiff), + DATETIME, STRING, TIMESTAMP, DATETIME), + impl(nullMissingHandling(DateTimeFunction::exprTimestampDiff), + DATETIME, STRING, TIMESTAMP, TIMESTAMP), + implWithProperties( + nullMissingHandlingWithProperties( + (functionProperties, part, startTime, endTime) -> exprTimestampDiffForTimeType( + functionProperties, + part, + startTime, + endTime)), + DATETIME, STRING, TIME, TIME) + ); + } + /** * TO_DAYS(STRING/DATE/DATETIME/TIMESTAMP). return the day number of the given date. */ @@ -1871,6 +1910,62 @@ private ExprValue exprTimestampAddForTimeType(Clock clock, return exprTimestampAdd(partExpr, amountExpr, new ExprDatetimeValue(datetime)); } + private ExprValue getTimeDifference(String part, LocalDateTime startTime, LocalDateTime endTime) { + long returnVal; + switch (part) { + case "MICROSECOND": + returnVal = MICROS.between(startTime, endTime); + break; + case "SECOND": + returnVal = SECONDS.between(startTime, endTime); + break; + case "MINUTE": + returnVal = MINUTES.between(startTime, endTime); + break; + case "HOUR": + returnVal = HOURS.between(startTime, endTime); + break; + case "DAY": + returnVal = DAYS.between(startTime, endTime); + break; + case "WEEK": + returnVal = WEEKS.between(startTime, endTime); + break; + case "MONTH": + returnVal = MONTHS.between(startTime, endTime); + break; + case "QUARTER": + returnVal = MONTHS.between(startTime, endTime) / 3; + break; + case "YEAR": + returnVal = YEARS.between(startTime, endTime); + break; + default: + return ExprNullValue.of(); + } + return new ExprLongValue(returnVal); + } + + private ExprValue exprTimestampDiff( + ExprValue partExpr, + ExprValue startTimeExpr, + ExprValue endTimeExpr) { + return getTimeDifference( + partExpr.stringValue(), + startTimeExpr.datetimeValue(), + endTimeExpr.datetimeValue()); + } + + private ExprValue exprTimestampDiffForTimeType(FunctionProperties fp, + ExprValue partExpr, + ExprValue startTimeExpr, + ExprValue endTimeExpr) { + return getTimeDifference( + partExpr.stringValue(), + extractDateTime(startTimeExpr, fp), + extractDateTime(endTimeExpr, fp)); + } + /** * 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 96245d37a3..359355a898 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 { TIME_TO_SEC(FunctionName.of("time_to_sec")), TIMESTAMP(FunctionName.of("timestamp")), TIMESTAMPADD(FunctionName.of("timestampadd")), + TIMESTAMPDIFF(FunctionName.of("timestampdiff")), TIME_FORMAT(FunctionName.of("time_format")), TO_DAYS(FunctionName.of("to_days")), TO_SECONDS(FunctionName.of("to_seconds")), diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/TimeStampDiffTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/TimeStampDiffTest.java new file mode 100644 index 0000000000..b00792e048 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/TimeStampDiffTest.java @@ -0,0 +1,361 @@ +/* + * 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.time.ZoneOffset; +import java.util.Arrays; +import java.util.stream.Stream; +import org.apache.commons.lang3.ArrayUtils; +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.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; +import org.opensearch.sql.expression.function.FunctionProperties; + +class TimeStampDiffTest extends ExpressionTestBase { + + //Helper function to create an argument based on a passed in interval type + private static ExprValue generateArg(String intervalType, + String argType, + LocalDateTime base, + int added) { + LocalDateTime arg; + switch (intervalType) { + case "MICROSECOND": + arg = base.plusNanos(added * 1000); + break; + case "SECOND": + arg = base.plusSeconds(added); + break; + case "MINUTE": + arg = base.plusMinutes(added); + break; + case "HOUR": + arg = base.plusHours(added); + break; + case "DAY": + arg = base.plusDays(added); + break; + case "WEEK": + arg = base.plusWeeks(added); + break; + case "MONTH": + arg = base.plusMonths(added); + break; + case "QUARTER": + arg = base.plusMonths(added * 3); + break; + case "YEAR": + arg = base.plusYears(added); + break; + default: + throw new SemanticCheckException(String.format( + "%s is not a valid interval type.", + intervalType)); + } + + switch (argType) { + case "TIME": + return new ExprTimeValue(arg.toLocalTime()); + case "TIMESTAMP": + return new ExprTimestampValue(arg.toInstant(ZoneOffset.UTC)); + case "DATE": + return new ExprDateValue(arg.toLocalDate()); + case "DATETIME": + return new ExprDatetimeValue(arg); + case "STRING": + return new ExprStringValue(String.format( + "%04d-%02d-%02d %02d:%02d:%02d.%06d", + arg.getYear(), + arg.getMonthValue(), + arg.getDayOfMonth(), + arg.getHour(), + arg.getMinute(), + arg.getSecond(), + arg.getNano() / 1000)); + default: + throw new SemanticCheckException(String.format( + "%s is not a valid ExprCoreValueType.", + argType)); + } + } + + //Generate test data to test all permutations for args (intervalType, arg1, arg2) + private static Stream getGeneralTestDataForTimestampDiff() { + + //Needs to be initialized with a value to prevent a null pointer exception. + Stream testData = Stream.of(Arguments.of( + "DAY", + new ExprDateValue("2000-01-01 00:00:00"), + new ExprDateValue("2000-01-01"), + 0)); + + final String[] timeIntervalTypes = { + "MICROSECOND", + "SECOND", + "MINUTE", + "HOUR" + }; + + final String[] dateIntervalTypes = { + "DAY", + "WEEK", + "MONTH", + "QUARTER", + "YEAR" + }; + + + final String[] intervalTypes = ArrayUtils.addAll(timeIntervalTypes, dateIntervalTypes); + + //TIME type not included here as it is a special case handled by a different test + final String[] expressionTypes = { + "DATE", + "DATETIME", + "TIMESTAMP", + "STRING" + }; + + final LocalDateTime baseDateTime = LocalDateTime.of(2000, 1, 1, 0, 0, 0); + final int intervalDifference = 5; + + //Iterate through each permutation of argument + for (String intervalType : intervalTypes) { + for (String firstArgExpressionType : expressionTypes) { + for (String secondArgExpressionType : expressionTypes) { + + ExprValue firstArg = generateArg(intervalType, firstArgExpressionType, baseDateTime, 0); + ExprValue secondArg = generateArg( + intervalType, + secondArgExpressionType, + baseDateTime, + intervalDifference); + + //If second arg is a DATE and you are using a unit of TIME to measure then expected is 0. + //The second arg is equal to baseDatetime in this case. + int expected = ( + secondArgExpressionType == "DATE" + && Arrays.asList(timeIntervalTypes).contains(intervalType)) + ? 0 : intervalDifference; + + testData = Stream.concat(testData, Stream.of( + Arguments.of( + intervalType, + firstArg, + secondArg, + expected), + Arguments.of( + intervalType, + secondArg, + firstArg, + -expected) + )); + } + } + } + + return testData; + } + + private static Stream getCornerCaseTestDataForTimestampDiff() { + return Stream.of( + + //Test around Leap Year + Arguments.of( + "DAY", + new ExprDatetimeValue("2019-02-28 00:00:00"), + new ExprDatetimeValue("2019-03-01 00:00:00"), + 1), + Arguments.of( + "DAY", + new ExprDatetimeValue("2020-02-28 00:00:00"), + new ExprDatetimeValue("2020-03-01 00:00:00"), + 2), + + //Test around year change + Arguments.of( + "SECOND", + new ExprDatetimeValue("2019-12-31 23:59:59"), + new ExprDatetimeValue("2020-01-01 00:00:00"), + 1), + Arguments.of( + "DAY", + new ExprDatetimeValue("2019-12-31 23:59:59"), + new ExprDatetimeValue("2020-01-01 00:00:00"), + 0), + Arguments.of( + "DAY", + new ExprDatetimeValue("2019-12-31 00:00:00"), + new ExprDatetimeValue("2020-01-01 00:00:00"), + 1) + ); + } + + private static FunctionExpression timestampdiffQuery(FunctionProperties functionProperties, + String unit, + ExprValue datetimeExpr1, + ExprValue datetimeExpr2) { + return DSL.timestampdiff( + functionProperties, + DSL.literal(unit), + DSL.literal(datetimeExpr1), + DSL.literal(datetimeExpr2) + ); + } + + @ParameterizedTest + @MethodSource({"getGeneralTestDataForTimestampDiff", "getCornerCaseTestDataForTimestampDiff"}) + public void testTimestampdiff(String unit, + ExprValue datetimeExpr1, + ExprValue datetimeExpr2, + long expected) { + FunctionExpression expr = timestampdiffQuery( + functionProperties, + unit, + datetimeExpr1, + datetimeExpr2); + assertEquals(expected, eval(expr).longValue()); + } + + private static Stream getUnits() { + return Stream.of( + Arguments.of("MICROSECOND"), + Arguments.of("SECOND"), + Arguments.of("MINUTE"), + Arguments.of("HOUR"), + Arguments.of("DAY"), + Arguments.of("WEEK"), + Arguments.of("MONTH"), + Arguments.of("QUARTER"), + Arguments.of("YEAR") + ); + } + + //Test that Time arg uses today's date with all interval/part arguments + @ParameterizedTest + @MethodSource("getUnits") + public void testTimestampDiffWithTimeType(String unit) { + LocalDateTime base = LocalDateTime.of(LocalDate.now(), LocalTime.of(10, 11, 12)); + + ExprValue timeExpr = generateArg(unit, "TIME", base, 0); + ExprValue timestampExpr = generateArg(unit, "TIMESTAMP", base, 0); + ExprValue dateExpr = generateArg(unit, "TIMESTAMP", base, 0); + ExprValue datetimeExpr = generateArg(unit, "TIMESTAMP", base, 0); + ExprValue stringExpr = generateArg(unit, "TIMESTAMP", base, 0); + + ExprValue[] expressions = {timeExpr, timestampExpr, dateExpr, datetimeExpr,stringExpr}; + + for (ExprValue arg1 : expressions) { + for (ExprValue arg2 : expressions) { + FunctionExpression funcExpr = timestampdiffQuery( + functionProperties, + unit, + arg1, + arg2 + ); + + assertEquals(0L, eval(funcExpr).longValue()); + } + } + } + + private static Stream getTimestampDiffInvalidArgs() { + return Stream.of( + Arguments.of("SECOND", "2023-13-01 10:11:12", "2000-01-01 10:11:12"), + Arguments.of("SECOND", "2023-01-40 10:11:12", "2000-01-01 10:11:12"), + Arguments.of("SECOND", "2023-01-01 25:11:12", "2000-01-01 10:11:12"), + Arguments.of("SECOND", "2023-01-01 10:70:12", "2000-01-01 10:11:12"), + Arguments.of("SECOND", "2023-01-01 10:11:70", "2000-01-01 10:11:12"), + Arguments.of("SECOND", "2023-01-01 10:11:12", "2000-13-01 10:11:12"), + Arguments.of("SECOND", "2023-01-01 10:11:12", "2000-01-40 10:11:12"), + Arguments.of("SECOND", "2023-01-01 10:11:12", "2000-01-01 25:11:12"), + Arguments.of("SECOND", "2023-01-01 10:11:12", "2000-01-01 10:70:12"), + Arguments.of("SECOND", "2023-01-01 10:11:12", "2000-01-01 10:11:70") + ); + } + + @ParameterizedTest + @MethodSource("getTimestampDiffInvalidArgs") + public void testTimestampDiffWithInvalidTimeArgs(String unit, String arg1, String arg2) { + FunctionExpression expr = timestampdiffQuery( + functionProperties, + unit, + new ExprStringValue(arg1), + new ExprStringValue(arg2) + ); + assertThrows(SemanticCheckException.class, () -> eval(expr)); + } + + @Test + public void testTimestampDiffWithInvalidPartReturnsNull() { + FunctionExpression expr = timestampdiffQuery( + functionProperties, + "INVALID", + new ExprStringValue("2023-01-01 10:11:12"), + new ExprStringValue("2000-01-01 10:11:12") + ); + assertEquals(ExprNullValue.of(), eval(expr)); + } + + //Test that different input types have the same result + @Test + public void testDifferentInputTypesHaveSameResult() { + String part = "SECOND"; + FunctionExpression dateExpr = timestampdiffQuery( + functionProperties, + part, + new ExprDateValue("2000-01-01"), + new ExprDateValue("2000-01-02")); + + FunctionExpression stringExpr = timestampdiffQuery( + functionProperties, + part, + new ExprStringValue("2000-01-01 00:00:00"), + new ExprStringValue("2000-01-02 00:00:00")); + + FunctionExpression datetimeExpr = timestampdiffQuery( + functionProperties, + part, + new ExprDatetimeValue("2000-01-01 00:00:00"), + new ExprDatetimeValue("2000-01-02 00:00:00")); + + FunctionExpression timestampExpr = timestampdiffQuery( + functionProperties, + part, + new ExprTimestampValue("2000-01-01 00:00:00"), + new ExprTimestampValue("2000-01-02 00:00:00")); + + assertAll( + () -> assertEquals(eval(dateExpr), eval(stringExpr)), + () -> assertEquals(eval(dateExpr), eval(datetimeExpr)), + () -> assertEquals(eval(dateExpr), eval(timestampExpr)) + ); + } + + private ExprValue eval(Expression expression) { + return expression.valueOf(); + } +} diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index eb10240ab9..1eb6e9fae5 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -2695,7 +2695,6 @@ Example:: TIMESTAMPADD ------------ - Description >>>>>>>>>>> @@ -2716,6 +2715,30 @@ Examples:: | 2000-01-18 00:00:00 | 1999-10-01 00:00:00 | +------------------------------------------------+----------------------------------------------------+ +TIMESTAMPDIFF +------------- + +Description +>>>>>>>>>>> + +Usage: TIMESTAMPDIFF(interval, start, end) returns the difference between the start and end date/times in interval units. +If a TIME is provided as an argument, it will be converted to a DATETIME with the DATE portion filled in using the current date. +Arguments will be automatically converted to a DATETIME/TIME/TIMESTAMP when appropriate. +Any argument that is a STRING must be formatted as a valid DATETIME. + +Argument type: INTERVAL, DATE/DATETIME/TIME/TIMESTAMP/STRING, 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 TIMESTAMPDIFF(YEAR, '1997-01-01 00:00:00', '2001-03-06 00:00:00'), TIMESTAMPDIFF(SECOND, time('00:00:23'), time('00:00:00')) + fetched rows / total rows = 1/1 + +---------------------------------------------------------------------+-------------------------------------------------------------+ + | TIMESTAMPDIFF(YEAR, '1997-01-01 00:00:00', '2001-03-06 00:00:00') | TIMESTAMPDIFF(SECOND, time('00:00:23'), time('00:00:00')) | + |---------------------------------------------------------------------+-------------------------------------------------------------| + | 4 | -23 | + +---------------------------------------------------------------------+-------------------------------------------------------------+ + 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 ce9a2f2302..b41842f84d 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 @@ -973,6 +973,17 @@ public void testTimstampadd() throws IOException { rows("1900-01-15 18:21:08")); } + @Test + public void testTimstampdiff() throws IOException { + JSONObject result = executeQuery( + String.format("SELECT timestampdiff(DAY, time0, datetime0) FROM %s LIMIT 3", TEST_INDEX_CALCS)); + + verifyDataRows(result, + rows(38176), + rows(38191), + rows(38198)); + } + @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 c0b36b0279..616bfa8a79 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -333,6 +333,7 @@ STATS: 'STATS'; TERM: 'TERM'; TERMS: 'TERMS'; TIMESTAMPADD: 'TIMESTAMPADD'; +TIMESTAMPDIFF: 'TIMESTAMPDIFF'; 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 d4a741d0f2..3861716ac9 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -316,11 +316,16 @@ functionCall | positionFunction #positionFunctionCall | extractFunction #extractFunctionCall | getFormatFunction #getFormatFunctionCall - | timestampAddFunction #timestampAddFunctionCall + | timestampFunction #timestampFunctionCall ; -timestampAddFunction - : TIMESTAMPADD LR_BRACKET simpleDateTimePart COMMA length=functionArg COMMA timestampExpr=functionArg RR_BRACKET +timestampFunction + : timestampFunctionName LR_BRACKET simpleDateTimePart COMMA firstArg=functionArg COMMA secondArg=functionArg RR_BRACKET + ; + +timestampFunctionName + : TIMESTAMPADD + | TIMESTAMPDIFF ; getFormatFunction 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 f22cca995d..d15a9dea0b 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,7 +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.TimestampFunctionCallContext; 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; @@ -176,10 +176,10 @@ public UnresolvedExpression visitHighlightFunctionCall( @Override - public UnresolvedExpression visitTimestampAddFunctionCall(TimestampAddFunctionCallContext ctx) { + public UnresolvedExpression visitTimestampFunctionCall(TimestampFunctionCallContext ctx) { return new Function( - ctx.timestampAddFunction().TIMESTAMPADD().toString(), - timestampAddFunctionArguments(ctx)); + ctx.timestampFunction().timestampFunctionName().getText(), + timestampFunctionArguments(ctx)); } @Override @@ -594,14 +594,14 @@ private List getFormatFunctionArguments( return args; } - private List timestampAddFunctionArguments( - TimestampAddFunctionCallContext ctx) { + private List timestampFunctionArguments( + TimestampFunctionCallContext ctx) { List args = Arrays.asList( new Literal( - ctx.timestampAddFunction().simpleDateTimePart().getText(), + ctx.timestampFunction().simpleDateTimePart().getText(), DataType.STRING), - visitFunctionArg(ctx.timestampAddFunction().length), - visitFunctionArg(ctx.timestampAddFunction().timestampExpr) + visitFunctionArg(ctx.timestampFunction().firstArg), + visitFunctionArg(ctx.timestampFunction().secondArg) ); return args; } 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 6f8c92172f..101f2f36f5 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 @@ -607,6 +607,12 @@ public void can_parse_timestampadd_function() { assertNotNull(parser.parse("SELECT TIMESTAMPADD(WEEK,1,'2003-01-02')")); } + @Test + public void can_parse_timestampdiff_function() { + assertNotNull(parser.parse("SELECT TIMESTAMPDIFF(MINUTE, '2003-01-02', '2003-01-02')")); + assertNotNull(parser.parse("SELECT TIMESTAMPDIFF(WEEK,'2003-01-02','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 29b65cd4cc..52dd5e3572 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 @@ -237,6 +237,18 @@ public void canBuildTimestampAddFunctionCall() { ); } + @Test + public void canBuildTimstampDiffFunctionCall() { + assertEquals( + function( + "timestampdiff", + stringLiteral("WEEK"), + timestampLiteral("2023-03-15 00:00:01"), + dateLiteral("2023-03-14")), + buildExprAst("timestampdiff(WEEK, TIMESTAMP '2023-03-15 00:00:01', DATE '2023-03-14')") + ); + } + @Test public void canBuildComparisonExpression() { assertEquals(