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 19cd8fd3269..2197f4e1a52 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -350,6 +350,10 @@ public static FunctionExpression month(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.MONTH, expressions); } + public static FunctionExpression month_of_year(Expression... expressions) { + return compile(FunctionProperties.None, BuiltinFunctionName.MONTH_OF_YEAR, expressions); + } + public static FunctionExpression monthname(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.MONTHNAME, 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 0d194e71979..08898b57acb 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 @@ -112,7 +112,8 @@ public void register(BuiltinFunctionRepository repository) { repository.register(maketime()); repository.register(microsecond()); repository.register(minute()); - repository.register(month()); + repository.register(month(BuiltinFunctionName.MONTH)); + repository.register(month(BuiltinFunctionName.MONTH_OF_YEAR)); repository.register(monthName()); repository.register(now()); repository.register(period_add()); @@ -432,8 +433,8 @@ private DefaultFunctionResolver minute() { /** * MONTH(STRING/DATE/DATETIME/TIMESTAMP). return the month for date (1-12). */ - private DefaultFunctionResolver month() { - return define(BuiltinFunctionName.MONTH.getName(), + private DefaultFunctionResolver month(BuiltinFunctionName month) { + return define(month.getName(), impl(nullMissingHandling(DateTimeFunction::exprMonth), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprMonth), INTEGER, DATETIME), impl(nullMissingHandling(DateTimeFunction::exprMonth), 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 9dcd73a4279..062dae00730 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 { MICROSECOND(FunctionName.of("microsecond")), MINUTE(FunctionName.of("minute")), MONTH(FunctionName.of("month")), + MONTH_OF_YEAR(FunctionName.of("month_of_year")), MONTHNAME(FunctionName.of("monthname")), PERIOD_ADD(FunctionName.of("period_add")), PERIOD_DIFF(FunctionName.of("period_diff")), 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 28a7113ca96..3e3bdb0f6e6 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 @@ -8,6 +8,7 @@ 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; @@ -625,6 +626,44 @@ public void month() { assertEquals(integerValue(8), eval(expression)); } + public void testInvalidDates(String date) throws SemanticCheckException { + FunctionExpression expression = DSL.month_of_year(DSL.literal(new ExprDateValue(date))); + eval(expression); + } + + @Test void monthOfYearInvalidDates() { + when(nullRef.type()).thenReturn(DATE); + when(missingRef.type()).thenReturn(DATE); + assertEquals(nullValue(), eval(DSL.month_of_year(nullRef))); + assertEquals(missingValue(), eval(DSL.month_of_year(missingRef))); + + assertThrows(SemanticCheckException.class, () -> testInvalidDates("2019-01-50")); + assertThrows(SemanticCheckException.class, () -> testInvalidDates("2019-02-29")); + assertThrows(SemanticCheckException.class, () -> testInvalidDates("2019-02-31")); + assertThrows(SemanticCheckException.class, () -> testInvalidDates("2019-13-05")); + } + + @Test + public void monthOfYearAlternateArgumentSyntaxes() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + FunctionExpression expression = DSL.month_of_year(DSL.literal(new ExprDateValue("2020-08-07"))); + assertEquals(INTEGER, expression.type()); + assertEquals("month_of_year(DATE '2020-08-07')", expression.toString()); + assertEquals(integerValue(8), eval(expression)); + + expression = DSL.month_of_year(DSL.literal("2020-08-07")); + assertEquals(INTEGER, expression.type()); + assertEquals("month_of_year(\"2020-08-07\")", expression.toString()); + assertEquals(integerValue(8), eval(expression)); + + expression = DSL.month_of_year(DSL.literal("2020-08-07 01:02:03")); + assertEquals(INTEGER, expression.type()); + assertEquals("month_of_year(\"2020-08-07 01:02:03\")", expression.toString()); + assertEquals(integerValue(8), eval(expression)); + } + @Test public void monthName() { when(nullRef.type()).thenReturn(DATE); diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 0644b970f55..802c6ba6fab 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1713,6 +1713,7 @@ Description >>>>>>>>>>> Usage: month(date) returns the month for date, in the range 1 to 12 for January to December. The dates with value 0 such as '0000-00-00' or '2008-00-00' are invalid. +The function month_of_year is also provided as an alias Argument type: STRING/DATE/DATETIME/TIMESTAMP @@ -1729,6 +1730,15 @@ Example:: +-----------------------------+ + os> SELECT MONTH_OF_YEAR(DATE('2020-08-26')) + fetched rows / total rows = 1/1 + +-------------------------------------+ + | MONTH_OF_YEAR(DATE('2020-08-26')) | + |-------------------------------------| + | 8 | + +-------------------------------------+ + + MONTHNAME --------- 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 8c47966e52f..84a60f5f158 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 @@ -322,6 +324,57 @@ public void testMonth() throws IOException { verifyDataRows(result, rows(9)); } + @Test + public void testMonthOfYearTypes() throws IOException { + JSONObject result = executeQuery("select month_of_year(date('2020-09-16'))"); + verifySchema(result, schema("month_of_year(date('2020-09-16'))", null, "integer")); + verifyDataRows(result, rows(9)); + + result = executeQuery("select month_of_year(datetime('2020-09-16 00:00:00'))"); + verifySchema(result, schema("month_of_year(datetime('2020-09-16 00:00:00'))", null, "integer")); + verifyDataRows(result, rows(9)); + + result = executeQuery("select month_of_year(timestamp('2020-09-16 00:00:00'))"); + verifySchema(result, schema("month_of_year(timestamp('2020-09-16 00:00:00'))", null, "integer")); + verifyDataRows(result, rows(9)); + + result = executeQuery("select month_of_year('2020-09-16')"); + verifySchema(result, schema("month_of_year('2020-09-16')", null, "integer")); + verifyDataRows(result, rows(9)); + } + + @Test + public void testMonthAlternateSyntaxesReturnTheSameResults() throws IOException { + JSONObject result1 = executeQuery("SELECT month(date('2022-11-22'))"); + JSONObject result2 = executeQuery("SELECT month_of_year(date('2022-11-22'))"); + verifyDataRows(result1, rows(11)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT month(CAST(date0 AS date)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT month_of_year(CAST(date0 AS date)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT month(datetime(CAST(time0 AS STRING))) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT month_of_year(datetime(CAST(time0 AS STRING))) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT month(CAST(time0 AS STRING)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT month_of_year(CAST(time0 AS STRING)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT month(CAST(datetime0 AS timestamp)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT month_of_year(CAST(datetime0 AS timestamp)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + } + @Test public void testMonthName() throws IOException { JSONObject result = executeQuery("select monthname(date('2020-09-16'))"); diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index b17c25261ac..9cc27e8ab63 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -246,6 +246,7 @@ datetimeConstantLiteral | CURRENT_TIMESTAMP | LOCALTIME | LOCALTIMESTAMP + | MONTH_OF_YEAR | UTC_TIMESTAMP | UTC_DATE | UTC_TIME 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 bb5707539dc..0dcc79ff340 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,22 @@ 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_month_of_year_function() { + assertNotNull(parser.parse("SELECT month('2022-11-18')")); + assertNotNull(parser.parse("SELECT month_of_year('2022-11-18')")); + + assertNotNull(parser.parse("SELECT month(date('2022-11-18'))")); + assertNotNull(parser.parse("SELECT month_of_year(date('2022-11-18'))")); + + assertNotNull(parser.parse("SELECT month(datetime('2022-11-18 00:00:00'))")); + assertNotNull(parser.parse("SELECT month_of_year(datetime('2022-11-18 00:00:00'))")); + + assertNotNull(parser.parse("SELECT month(timestamp('2022-11-18 00:00:00'))")); + assertNotNull(parser.parse("SELECT month_of_year(timestamp('2022-11-18 00:00:00'))")); + + } + @Test public void can_parse_multi_match_relevance_function() { assertNotNull(parser.parse(