From 33e1dad47575d5df486dee031a144432b28f74fc Mon Sep 17 00:00:00 2001 From: GabeFernandez310 Date: Wed, 22 Feb 2023 09:40:21 -0800 Subject: [PATCH] Add `sec_to_time` function to OpenSearch * Added Tests Signed-off-by: GabeFernandez310 * Added Implementation And Documentation Signed-off-by: GabeFernandez310 * Added Integration Test And Fixed Checkstyle Signed-off-by: GabeFernandez310 * Addressed PR Comments Signed-off-by: GabeFernandez310 * Fixed Checkstyle Signed-off-by: GabeFernandez310 * Added Tests And Modified Docs Signed-off-by: GabeFernandez310 * Fixed formatNanos Helper Function Signed-off-by: GabeFernandez310 * Temporarily Removed Failing Tests Signed-off-by: GabeFernandez310 * Fixed Nanoseconds Float/Double Imprecision Issues Signed-off-by: GabeFernandez310 * Fixed Checkstyle Signed-off-by: GabeFernandez310 * Added Tests Signed-off-by: GabeFernandez310 * Altered Implementation To Use BigDecimal Signed-off-by: GabeFernandez310 --------- Signed-off-by: GabeFernandez310 --- .../org/opensearch/sql/expression/DSL.java | 4 ++ .../expression/datetime/DateTimeFunction.java | 50 ++++++++++++++++ .../function/BuiltinFunctionName.java | 1 + .../datetime/DateTimeFunctionTest.java | 59 +++++++++++++++++++ docs/user/dql/functions.rst | 41 +++++++++++++ .../sql/sql/DateTimeFunctionIT.java | 10 ++++ sql/src/main/antlr/OpenSearchSQLLexer.g4 | 1 + sql/src/main/antlr/OpenSearchSQLParser.g4 | 1 + .../sql/sql/antlr/SQLSyntaxParserTest.java | 7 +++ 9 files changed, 174 insertions(+) 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 4d928ef20f..5694c13dcc 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -436,6 +436,10 @@ public static FunctionExpression module(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.MODULES, expressions); } + public static FunctionExpression sec_to_time(Expression... expressions) { + return compile(FunctionProperties.None, BuiltinFunctionName.SEC_TO_TIME, expressions); + } + public static FunctionExpression substr(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.SUBSTR, 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 02bd911fc7..e0a7fbf499 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 @@ -15,6 +15,7 @@ import static org.opensearch.sql.data.type.ExprCoreType.DATE; import static org.opensearch.sql.data.type.ExprCoreType.DATETIME; import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE; +import static org.opensearch.sql.data.type.ExprCoreType.FLOAT; import static org.opensearch.sql.data.type.ExprCoreType.INTEGER; import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL; import static org.opensearch.sql.data.type.ExprCoreType.LONG; @@ -57,6 +58,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import lombok.experimental.UtilityClass; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.opensearch.sql.data.model.ExprDateValue; import org.opensearch.sql.data.model.ExprDatetimeValue; @@ -147,6 +149,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(period_add()); repository.register(period_diff()); repository.register(quarter()); + repository.register(sec_to_time()); repository.register(second(BuiltinFunctionName.SECOND)); repository.register(second(BuiltinFunctionName.SECOND_OF_MINUTE)); repository.register(subdate()); @@ -639,6 +642,15 @@ private DefaultFunctionResolver quarter() { ); } + private DefaultFunctionResolver sec_to_time() { + return define(BuiltinFunctionName.SEC_TO_TIME.getName(), + impl((nullMissingHandling(DateTimeFunction::exprSecToTime)), TIME, INTEGER), + impl((nullMissingHandling(DateTimeFunction::exprSecToTime)), TIME, LONG), + impl((nullMissingHandling(DateTimeFunction::exprSecToTimeWithNanos)), TIME, DOUBLE), + impl((nullMissingHandling(DateTimeFunction::exprSecToTimeWithNanos)), TIME, FLOAT) + ); + } + /** * SECOND(STRING/TIME/DATETIME/TIMESTAMP). return the second value for time. */ @@ -1364,6 +1376,44 @@ private ExprValue exprQuarter(ExprValue date) { return new ExprIntegerValue((month / 3) + ((month % 3) == 0 ? 0 : 1)); } + /** + * Returns TIME value of sec_to_time function for an INTEGER or LONG arguments. + * @param totalSeconds The total number of seconds + * @return A TIME value + */ + private ExprValue exprSecToTime(ExprValue totalSeconds) { + return new ExprTimeValue(LocalTime.MIN.plus(Duration.ofSeconds(totalSeconds.longValue()))); + } + + /** + * Helper function which obtains the decimal portion of the seconds value passed in. + * Uses BigDecimal to prevent issues with math on floating point numbers. + * Return is formatted to be used with Duration.ofSeconds(); + * + * @param seconds and ExprDoubleValue or ExprFloatValue for the seconds + * @return A LONG representing the nanoseconds portion + */ + private long formatNanos(ExprValue seconds) { + //Convert ExprValue to BigDecimal + BigDecimal formattedNanos = BigDecimal.valueOf(seconds.doubleValue()); + //Extract only the nanosecond part + formattedNanos = formattedNanos.subtract(BigDecimal.valueOf(formattedNanos.intValue())); + + return formattedNanos.scaleByPowerOfTen(9).longValue(); + } + + /** + * Returns TIME value of sec_to_time function for FLOAT or DOUBLE arguments. + * @param totalSeconds The total number of seconds + * @return A TIME value + */ + private ExprValue exprSecToTimeWithNanos(ExprValue totalSeconds) { + long nanos = formatNanos(totalSeconds); + + return new ExprTimeValue( + LocalTime.MIN.plus(Duration.ofSeconds(totalSeconds.longValue(), nanos))); + } + /** * Second 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 f9d38a0da3..00bb2baa57 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 @@ -90,6 +90,7 @@ public enum BuiltinFunctionName { PERIOD_ADD(FunctionName.of("period_add")), PERIOD_DIFF(FunctionName.of("period_diff")), QUARTER(FunctionName.of("quarter")), + SEC_TO_TIME(FunctionName.of("sec_to_time")), SECOND(FunctionName.of("second")), SECOND_OF_MINUTE(FunctionName.of("second_of_minute")), SUBDATE(FunctionName.of("subdate")), 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 a8e42d10b8..c5e098b364 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 @@ -40,6 +40,8 @@ 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.ExprDoubleValue; +import org.opensearch.sql.data.model.ExprIntegerValue; import org.opensearch.sql.data.model.ExprLongValue; import org.opensearch.sql.data.model.ExprTimeValue; import org.opensearch.sql.data.model.ExprTimestampValue; @@ -52,6 +54,7 @@ import org.opensearch.sql.expression.FunctionExpression; import org.opensearch.sql.expression.LiteralExpression; import org.opensearch.sql.expression.env.Environment; +import org.opensearch.sql.expression.function.FunctionProperties; @ExtendWith(MockitoExtension.class) class DateTimeFunctionTest extends ExpressionTestBase { @@ -1173,6 +1176,62 @@ public void quarter() { assertEquals(integerValue(4), eval(expression)); } + private static Stream getTestDataForSecToTime() { + return Stream.of( + Arguments.of(1, "00:00:01"), + Arguments.of(2378, "00:39:38"), + Arguments.of(6897, "01:54:57"), + Arguments.of(-82800, "01:00:00"), + Arguments.of(-169200, "01:00:00"), + Arguments.of(3600, "01:00:00"), + Arguments.of(90000, "01:00:00"), + Arguments.of(176400, "01:00:00") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("getTestDataForSecToTime") + public void testSecToTime(int seconds, String expected) { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.sec_to_time( + DSL.literal(new ExprIntegerValue(seconds))); + + assertEquals(TIME, expr.type()); + assertEquals(new ExprTimeValue(expected), eval(expr)); + } + + private static Stream getTestDataForSecToTimeWithDecimal() { + return Stream.of( + Arguments.of(1.123, "00:00:01.123"), + Arguments.of(1.00123, "00:00:01.00123"), + Arguments.of(1.001023, "00:00:01.001023"), + Arguments.of(1.000000042, "00:00:01.000000042"), + Arguments.of(3.14, "00:00:03.14") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("getTestDataForSecToTimeWithDecimal") + public void testSecToTimeWithDecimal(double arg, String expected) { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.sec_to_time(DSL.literal(new ExprDoubleValue(arg))); + + assertEquals(TIME, expr.type()); + assertEquals(new ExprTimeValue(expected), eval(expr)); + } + + @Test + public void testSecToTimeWithNullValue() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + FunctionExpression expr = DSL.sec_to_time( + DSL.literal(nullValue())); + + assertEquals(nullValue(), eval(expr)); + } + @Test public void second() { when(nullRef.type()).thenReturn(TIME); diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 749017078b..77dac183c3 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -2096,6 +2096,47 @@ Example:: | 3 | +-------------------------------+ +SEC_TO_TIME +----------- + +Description +>>>>>>>>>>> + +Usage: sec_to_time(number) returns the time in HH:mm:ssss[.nnnnnn] format. +Note that the function returns a time between 00:00:00 and 23:59:59. +If an input value is too large (greater than 86399), the function will wrap around and begin returning outputs starting from 00:00:00. +If an input value is too small (less than 0), the function will wrap around and begin returning outputs counting down from 23:59:59. + +Argument type: INTEGER, LONG, DOUBLE, FLOAT + +Return type: TIME + +Example:: + + os> SELECT SEC_TO_TIME(3601) + fetched rows / total rows = 1/1 + +---------------------+ + | SEC_TO_TIME(3601) | + |---------------------| + | 01:00:01 | + +---------------------+ + + os> SELECT sec_to_time(1234.123); + fetched rows / total rows = 1/1 + +-------------------------+ + | sec_to_time(1234.123) | + |-------------------------| + | 00:20:34.123 | + +-------------------------+ + + os> SELECT sec_to_time(NULL); + fetched rows / total rows = 1/1 + +---------------------+ + | sec_to_time(NULL) | + |---------------------| + | null | + +---------------------+ + SECOND ------ 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 938f7f664a..b3cefb6c7f 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 @@ -727,6 +727,16 @@ public void testQuarter() throws IOException { verifyDataRows(result, rows(3)); } + @Test + public void testSecToTime() throws IOException { + JSONObject result = executeQuery( + String.format("SELECT sec_to_time(balance) FROM %s LIMIT 3", TEST_INDEX_BANK)); + verifyDataRows(result, + rows("10:53:45"), + rows("01:34:46"), + rows("09:07:18")); + } + @Test public void testSecond() throws IOException { JSONObject result = executeQuery("select second(timestamp('2020-09-16 17:30:00'))"); diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index 941c1a4c4e..2bff342b25 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -246,6 +246,7 @@ RINT: 'RINT'; ROUND: 'ROUND'; RTRIM: 'RTRIM'; REVERSE: 'REVERSE'; +SEC_TO_TIME: 'SEC_TO_TIME'; SIGN: 'SIGN'; SIGNUM: 'SIGNUM'; SIN: 'SIN'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index 3fa223c584..a407750acd 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -453,6 +453,7 @@ dateTimeFunctionName | PERIOD_ADD | PERIOD_DIFF | QUARTER + | SEC_TO_TIME | SECOND | SECOND_OF_MINUTE | SUBDATE 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 16118d2a32..4ed61cb1b4 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 @@ -494,6 +494,13 @@ public void can_parse_minute_of_day_function() { assertNotNull(parser.parse("SELECT minute_of_day('2022-12-14 12:23:34');"));; } + @Test + public void can_parse_sec_to_time_function() { + assertNotNull(parser.parse("SELECT sec_to_time(-6897)")); + assertNotNull(parser.parse("SELECT sec_to_time(6897)")); + assertNotNull(parser.parse("SELECT sec_to_time(6897.123)")); + } + @Test public void can_parse_wildcard_query_relevance_function() { assertNotNull(