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 19cd8fd326..92300121a5 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -330,6 +330,10 @@ public static FunctionExpression dayofyear(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.DAYOFYEAR, expressions); } + public static FunctionExpression day_of_year(Expression... expressions) { + return compile(FunctionProperties.None, BuiltinFunctionName.DAY_OF_YEAR, expressions); + } + public static FunctionExpression from_days(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.FROM_DAYS, 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 0d194e7197..96771ff0e1 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 @@ -102,7 +102,8 @@ public void register(BuiltinFunctionRepository repository) { repository.register(dayName()); repository.register(dayOfMonth()); repository.register(dayOfWeek()); - repository.register(dayOfYear()); + repository.register(dayOfYear(BuiltinFunctionName.DAYOFYEAR)); + repository.register(dayOfYear(BuiltinFunctionName.DAY_OF_YEAR)); repository.register(from_days()); repository.register(from_unixtime()); repository.register(hour()); @@ -359,8 +360,8 @@ private DefaultFunctionResolver dayOfWeek() { * DAYOFYEAR(STRING/DATE/DATETIME/TIMESTAMP). * return the day of the year for date (1-366). */ - private DefaultFunctionResolver dayOfYear() { - return define(BuiltinFunctionName.DAYOFYEAR.getName(), + private DefaultFunctionResolver dayOfYear(BuiltinFunctionName dayOfYear) { + return define(dayOfYear.getName(), impl(nullMissingHandling(DateTimeFunction::exprDayOfYear), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprDayOfYear), INTEGER, DATETIME), impl(nullMissingHandling(DateTimeFunction::exprDayOfYear), INTEGER, TIMESTAMP), 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 9dcd73a427..6baaaff044 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 @@ -69,6 +69,7 @@ public enum BuiltinFunctionName { DAYOFMONTH(FunctionName.of("dayofmonth")), DAYOFWEEK(FunctionName.of("dayofweek")), DAYOFYEAR(FunctionName.of("dayofyear")), + DAY_OF_YEAR(FunctionName.of("day_of_year")), FROM_DAYS(FunctionName.of("from_days")), FROM_UNIXTIME(FunctionName.of("from_unixtime")), HOUR(FunctionName.of("hour")), 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 28a7113ca9..af68186d5a 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 @@ -6,8 +6,10 @@ 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 static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; import static org.opensearch.sql.data.model.ExprValueUtils.integerValue; import static org.opensearch.sql.data.model.ExprValueUtils.longValue; @@ -479,6 +481,93 @@ public void dayOfYear() { assertEquals(integerValue(220), eval(expression)); } + public void testDayOfYearWithUnderscores(String date, int dayOfYear) { + FunctionExpression expression = DSL.day_of_year(DSL.literal(new ExprDateValue(date))); + assertEquals(INTEGER, expression.type()); + assertEquals(integerValue(dayOfYear), eval(expression)); + } + + @Test + public void dayOfYearWithUnderscoresDifferentArgumentFormats() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + FunctionExpression expression1 = DSL.day_of_year(DSL.literal(new ExprDateValue("2020-08-07"))); + FunctionExpression expression2 = DSL.day_of_year(DSL.literal("2020-08-07")); + FunctionExpression expression3 = DSL.day_of_year(DSL.literal("2020-08-07 01:02:03")); + + assertAll( + () -> testDayOfYearWithUnderscores("2020-08-07", 220), + () -> assertEquals("day_of_year(DATE '2020-08-07')", expression1.toString()), + + () -> testDayOfYearWithUnderscores("2020-08-07", 220), + () -> assertEquals("day_of_year(\"2020-08-07\")", expression2.toString()), + + () -> testDayOfYearWithUnderscores("2020-08-07 01:02:03", 220), + () -> assertEquals("day_of_year(\"2020-08-07 01:02:03\")", expression3.toString()) + ); + } + + @Test + public void dayOfYearWithUnderscoresCornerCaseDates() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + assertAll( + //31st of December during non leap year (should be 365) + () -> testDayOfYearWithUnderscores("2019-12-31", 365), + //Year 1200 + () -> testDayOfYearWithUnderscores("1200-02-28", 59), + //Year 4000 + () -> testDayOfYearWithUnderscores("4000-02-28", 59) + ); + } + + @Test + public void dayOfYearWithUnderscoresLeapYear() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + assertAll( + //28th of Feb + () -> testDayOfYearWithUnderscores("2020-02-28", 59), + + //29th of Feb during leap year + () -> testDayOfYearWithUnderscores("2020-02-29 23:59:59", 60), + () -> testDayOfYearWithUnderscores("2020-02-29", 60), + + //1st of March during leap year + () -> testDayOfYearWithUnderscores("2020-03-01 00:00:00", 61), + () -> testDayOfYearWithUnderscores("2020-03-01", 61), + + //1st of March during non leap year + () -> testDayOfYearWithUnderscores("2019-03-01", 60), + + //31st of December during leap year (should be 366) + () -> testDayOfYearWithUnderscores("2020-12-31", 366) + ); + } + + public void testInvalidDayOfYear(String date) { + FunctionExpression expression = DSL.day_of_year(DSL.literal(new ExprDateValue(date))); + eval(expression); + } + + @Test + public void invalidDayOfYearArgument() { + when(nullRef.type()).thenReturn(DATE); + when(missingRef.type()).thenReturn(DATE); + assertEquals(nullValue(), eval(DSL.day_of_year(nullRef))); + assertEquals(missingValue(), eval(DSL.day_of_year(missingRef))); + + //29th of Feb non-leapyear + assertThrows(SemanticCheckException.class, () -> testInvalidDayOfYear("2019-02-29")); + //13th month + assertThrows(SemanticCheckException.class, () -> testInvalidDayOfYear("2019-13-15")); + //incorrect format for type + assertThrows(SemanticCheckException.class, () -> testInvalidDayOfYear("asdfasdfasdf")); + } + @Test public void from_days() { when(nullRef.type()).thenReturn(LONG); diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 0644b970f5..46d68f28be 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1457,6 +1457,7 @@ Description >>>>>>>>>>> Usage: dayofyear(date) returns the day of the year for date, in the range 1 to 366. +The function `day_of_year`_ is also provided as an alias. Argument type: STRING/DATE/DATETIME/TIMESTAMP @@ -1472,6 +1473,62 @@ Example:: | 239 | +---------------------------------+ + os> SELECT DAYOFYEAR(DATETIME('2020-08-26 00:00:00')) + fetched rows / total rows = 1/1 + +----------------------------------------------+ + | DAYOFYEAR(DATETIME('2020-08-26 00:00:00')) | + |----------------------------------------------| + | 239 | + +----------------------------------------------+ + + os> SELECT DAYOFYEAR(TIMESTAMP('2020-08-26 00:00:00')) + fetched rows / total rows = 1/1 + +-----------------------------------------------+ + | DAYOFYEAR(TIMESTAMP('2020-08-26 00:00:00')) | + |-----------------------------------------------| + | 239 | + +-----------------------------------------------+ + + +DAY_OF_YEAR +--------- + +Description +>>>>>>>>>>> + +This function is an alias to the `dayofyear`_ function + +Argument type: STRING/DATE/DATETIME/TIMESTAMP + +Return type: INTEGER + +Example:: + + os> SELECT DAY_OF_YEAR(DATE('2020-08-26')) + fetched rows / total rows = 1/1 + +-----------------------------------+ + | DAY_OF_YEAR(DATE('2020-08-26')) | + |-----------------------------------| + | 239 | + +-----------------------------------+ + + os> SELECT DAY_OF_YEAR(DATETIME('2020-08-26 00:00:00')) + fetched rows / total rows = 1/1 + +------------------------------------------------+ + | DAY_OF_YEAR(DATETIME('2020-08-26 00:00:00')) | + |------------------------------------------------| + | 239 | + +------------------------------------------------+ + + os> SELECT DAY_OF_YEAR(TIMESTAMP('2020-08-26 00:00:00')) + fetched rows / total rows = 1/1 + +-------------------------------------------------+ + | DAY_OF_YEAR(TIMESTAMP('2020-08-26 00:00:00')) | + |-------------------------------------------------| + | 239 | + +-------------------------------------------------+ + + FROM_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 8c47966e52..8033e72521 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,6 +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_CALCS; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_PEOPLE2; import static org.opensearch.sql.legacy.plugin.RestSqlAction.QUERY_API_ENDPOINT; import static org.opensearch.sql.util.MatcherUtils.rows; @@ -49,6 +50,7 @@ public void init() throws Exception { super.init(); loadIndex(Index.BANK); loadIndex(Index.PEOPLE2); + loadIndex(Index.CALCS); } @Test @@ -225,6 +227,56 @@ public void testDayOfYear() throws IOException { verifyDataRows(result, rows(260)); } + @Test + public void testDayOfYearWithUnderscores() throws IOException { + JSONObject result = executeQuery("select day_of_year(date('2020-09-16'))"); + verifySchema(result, schema("day_of_year(date('2020-09-16'))", null, "integer")); + verifyDataRows(result, rows(260)); + + result = executeQuery("select day_of_year(datetime('2020-09-16 00:00:00'))"); + verifySchema(result, schema("day_of_year(datetime('2020-09-16 00:00:00'))", null, "integer")); + verifyDataRows(result, rows(260)); + + result = executeQuery("select day_of_year(timestamp('2020-09-16 00:00:00'))"); + verifySchema(result, schema("day_of_year(timestamp('2020-09-16 00:00:00'))", null, "integer")); + verifyDataRows(result, rows(260)); + + result = executeQuery("select day_of_year('2020-09-16')"); + verifySchema(result, schema("day_of_year('2020-09-16')", null, "integer")); + verifyDataRows(result, rows(260)); + } + + @Test + public void testDayOfYearAlternateSyntaxesReturnTheSameResults() throws IOException { + JSONObject result1 = executeQuery("SELECT dayofyear(date('2022-11-22'))"); + JSONObject result2 = executeQuery("SELECT day_of_year(date('2022-11-22'))"); + verifyDataRows(result1, rows(326)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT dayofyear(CAST(date0 AS date)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT day_of_year(CAST(date0 AS date)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT dayofyear(datetime(CAST(time0 AS STRING))) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT day_of_year(datetime(CAST(time0 AS STRING))) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT dayofyear(CAST(time0 AS STRING)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT day_of_year(CAST(time0 AS STRING)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT dayofyear(CAST(datetime0 AS timestamp)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT day_of_year(CAST(datetime0 AS timestamp)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + } @Test public void testFromDays() throws IOException { JSONObject result = executeQuery("select from_days(738049)"); diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index b17c25261a..bf529532dc 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -244,6 +244,7 @@ datetimeConstantLiteral : CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP + | DAY_OF_YEAR | LOCALTIME | LOCALTIMESTAMP | UTC_TIMESTAMP 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 bb5707539d..4967df5832 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 @@ -191,6 +191,12 @@ public void can_parse_now_like_functions(String name, Boolean hasFsp, Boolean ha assertNotNull(parser.parse("SELECT id FROM test WHERE " + String.join(" AND ", calls))); } + @Test + public void can_parse_dayofyear_functions() { + assertNotNull(parser.parse("SELECT dayofyear('2022-11-18')")); + assertNotNull(parser.parse("SELECT day_of_year('2022-11-18')")); + } + @Test public void can_parse_multi_match_relevance_function() { assertNotNull(parser.parse(