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 fc425c6c20b..7176dfaa959 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -354,6 +354,10 @@ public static FunctionExpression minute(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.MINUTE, expressions); } + public static FunctionExpression minute_of_hour(Expression... expressions) { + return compile(FunctionProperties.None, BuiltinFunctionName.MINUTE_OF_HOUR, expressions); + } + public static FunctionExpression month(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.MONTH, expressions); } diff --git a/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java b/core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java index a111f672af1..27dd25fa924 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.MINUTES; 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; @@ -112,7 +113,8 @@ public void register(BuiltinFunctionRepository repository) { repository.register(makedate()); repository.register(maketime()); repository.register(microsecond()); - repository.register(minute()); + repository.register(minute(BuiltinFunctionName.MINUTE)); + repository.register(minute(BuiltinFunctionName.MINUTE_OF_HOUR)); repository.register(month(BuiltinFunctionName.MONTH)); repository.register(month(BuiltinFunctionName.MONTH_OF_YEAR)); repository.register(monthName()); @@ -423,11 +425,12 @@ private DefaultFunctionResolver microsecond() { /** * MINUTE(STRING/TIME/DATETIME/TIMESTAMP). return the minute value for time. */ - private DefaultFunctionResolver minute() { - return define(BuiltinFunctionName.MINUTE.getName(), + private DefaultFunctionResolver minute(BuiltinFunctionName name) { + return define(name.getName(), impl(nullMissingHandling(DateTimeFunction::exprMinute), INTEGER, STRING), impl(nullMissingHandling(DateTimeFunction::exprMinute), INTEGER, TIME), impl(nullMissingHandling(DateTimeFunction::exprMinute), INTEGER, DATETIME), + impl(nullMissingHandling(DateTimeFunction::exprMinute), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprMinute), INTEGER, TIMESTAMP) ); } @@ -899,7 +902,8 @@ private ExprValue exprMicrosecond(ExprValue time) { * @return ExprValue. */ private ExprValue exprMinute(ExprValue time) { - return new ExprIntegerValue(time.timeValue().getMinute()); + return new ExprIntegerValue( + (MINUTES.between(LocalTime.MIN, time.timeValue()) % 60)); } /** 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 b23c7613d68..a091dfed4c2 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,7 @@ public enum BuiltinFunctionName { MAKETIME(FunctionName.of("maketime")), MICROSECOND(FunctionName.of("microsecond")), MINUTE(FunctionName.of("minute")), + MINUTE_OF_HOUR(FunctionName.of("minute_of_hour")), MONTH(FunctionName.of("month")), MONTH_OF_YEAR(FunctionName.of("month_of_year")), MONTHNAME(FunctionName.of("monthname")), diff --git a/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java index 092b64d5d75..36ea81655c5 100644 --- a/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java +++ b/core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java @@ -28,10 +28,14 @@ import com.google.common.collect.ImmutableList; import java.time.LocalDate; import java.util.List; +import java.util.stream.Stream; import lombok.AllArgsConstructor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opensearch.sql.data.model.ExprDateValue; @@ -46,6 +50,7 @@ import org.opensearch.sql.expression.Expression; import org.opensearch.sql.expression.ExpressionTestBase; import org.opensearch.sql.expression.FunctionExpression; +import org.opensearch.sql.expression.LiteralExpression; import org.opensearch.sql.expression.env.Environment; @ExtendWith(MockitoExtension.class) @@ -691,6 +696,85 @@ public void minute() { assertEquals("minute(\"2020-08-17 01:02:03\")", expression.toString()); } + private void minuteOfHourQuery(FunctionExpression dateExpression, int minute, String testExpr) { + assertAll( + () -> assertEquals(INTEGER, dateExpression.type()), + () -> assertEquals(integerValue(minute), eval(dateExpression)), + () -> assertEquals(testExpr, dateExpression.toString()) + ); + } + + private static Stream getTestDataForMinuteOfHour() { + return Stream.of( + Arguments.of( + DSL.literal(new ExprTimeValue("01:02:03")), + 2, + "minute_of_hour(TIME '01:02:03')"), + Arguments.of( + DSL.literal("01:02:03"), + 2, + "minute_of_hour(\"01:02:03\")"), + Arguments.of( + DSL.literal(new ExprTimestampValue("2020-08-17 01:02:03")), + 2, + "minute_of_hour(TIMESTAMP '2020-08-17 01:02:03')"), + Arguments.of( + DSL.literal(new ExprDatetimeValue("2020-08-17 01:02:03")), + 2, + "minute_of_hour(DATETIME '2020-08-17 01:02:03')"), + Arguments.of( + DSL.literal("2020-08-17 01:02:03"), + 2, + "minute_of_hour(\"2020-08-17 01:02:03\")") + ); + } + + @ParameterizedTest(name = "{2}") + @MethodSource("getTestDataForMinuteOfHour") + public void minuteOfHour(LiteralExpression arg, int expectedResult, String expectedString) { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + minuteOfHourQuery(DSL.minute_of_hour(arg), expectedResult, expectedString); + } + + private void invalidMinuteOfHourQuery(String time) { + FunctionExpression expression = DSL.minute_of_hour(DSL.literal(new ExprTimeValue(time))); + eval(expression); + } + + @Test + public void minuteOfHourInvalidArguments() { + when(nullRef.type()).thenReturn(TIME); + when(missingRef.type()).thenReturn(TIME); + + assertAll( + () -> assertEquals(nullValue(), eval(DSL.minute_of_hour(nullRef))), + () -> assertEquals(missingValue(), eval(DSL.minute_of_hour(missingRef))), + + //Invalid Seconds + () -> assertThrows( + SemanticCheckException.class, + () -> invalidMinuteOfHourQuery("12:23:61")), + + //Invalid Minutes + () -> assertThrows( + SemanticCheckException.class, + () -> invalidMinuteOfHourQuery("12:61:34")), + + //Invalid Hours + () -> assertThrows( + SemanticCheckException.class, + () -> invalidMinuteOfHourQuery("25:23:34")), + + //incorrect format + () -> assertThrows( + SemanticCheckException.class, + () -> invalidMinuteOfHourQuery("asdfasdf")) + ); + } + + @Test public void month() { when(nullRef.type()).thenReturn(DATE); diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 843d6c7e455..ef5cbc22ccd 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1747,6 +1747,7 @@ Description >>>>>>>>>>> Usage: minute(time) returns the minute for time, in the range 0 to 59. +The `minute_of_hour` function is provided as an alias. Argument type: STRING/TIME/DATETIME/TIMESTAMP @@ -1754,14 +1755,13 @@ Return type: INTEGER Example:: - os> SELECT MINUTE((TIME '01:02:03')) + os> SELECT MINUTE(time('01:02:03')), MINUTE_OF_HOUR(time('01:02:03')) fetched rows / total rows = 1/1 - +-----------------------------+ - | MINUTE((TIME '01:02:03')) | - |-----------------------------| - | 2 | - +-----------------------------+ - + +----------------------------+------------------------------------+ + | MINUTE(time('01:02:03')) | MINUTE_OF_HOUR(time('01:02:03')) | + |----------------------------+------------------------------------| + | 2 | 2 | + +----------------------------+------------------------------------+ MONTH ----- 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 3503877d647..a6ca776ae08 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 @@ -364,6 +364,52 @@ public void testMinute() throws IOException { verifyDataRows(result, rows(30)); } + @Test + public void testMinuteOfHour() throws IOException { + JSONObject result = executeQuery("select minute_of_hour(timestamp('2020-09-16 17:30:00'))"); + verifySchema(result, schema( + "minute_of_hour(timestamp('2020-09-16 17:30:00'))", null, "integer")); + verifyDataRows(result, rows(30)); + + result = executeQuery("select minute_of_hour(time('17:30:00'))"); + verifySchema(result, schema("minute_of_hour(time('17:30:00'))", null, "integer")); + verifyDataRows(result, rows(30)); + + result = executeQuery("select minute_of_hour('2020-09-16 17:30:00')"); + verifySchema(result, schema("minute_of_hour('2020-09-16 17:30:00')", null, "integer")); + verifyDataRows(result, rows(30)); + + result = executeQuery("select minute_of_hour('17:30:00')"); + verifySchema(result, schema("minute_of_hour('17:30:00')", null, "integer")); + verifyDataRows(result, rows(30)); + } + + @Test + public void testMinuteFunctionAliasesReturnTheSameResults() throws IOException { + JSONObject result1 = executeQuery("SELECT minute('11:30:00')"); + JSONObject result2 = executeQuery("SELECT minute_of_hour('11:30:00')"); + verifyDataRows(result1, rows(30)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT minute(datetime(CAST(time0 AS STRING))) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT minute_of_hour(datetime(CAST(time0 AS STRING))) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT minute(CAST(time0 AS STRING)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT minute_of_hour(CAST(time0 AS STRING)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT minute(CAST(datetime0 AS timestamp)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT minute_of_hour(CAST(datetime0 AS timestamp)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + } + @Test public void testMonth() throws IOException { JSONObject result = executeQuery("select month(date('2020-09-16'))"); diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index 47a43362eaa..d68423b658d 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -435,6 +435,7 @@ dateTimeFunctionName | MAKETIME | MICROSECOND | MINUTE + | MINUTE_OF_HOUR | MONTH | MONTHNAME | NOW 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 bfd0f93ec98..4afafa71ed9 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 @@ -203,6 +203,15 @@ public void can_parse_dayofyear_functions() { assertNotNull(parser.parse("SELECT day_of_year('2022-11-18')")); } + @Test + public void can_parse_minute_functions() { + assertNotNull(parser.parse("SELECT minute('12:23:34')")); + assertNotNull(parser.parse("SELECT minute_of_hour('12:23:34')")); + + assertNotNull(parser.parse("SELECT minute('2022-12-20 12:23:34')")); + assertNotNull(parser.parse("SELECT minute_of_hour('2022-12-20 12:23:34')")); + } + @Test public void can_parse_month_of_year_function() { assertNotNull(parser.parse("SELECT month('2022-11-18')"));