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..ffdb6d416d 100644 --- a/core/src/main/java/org/opensearch/sql/expression/DSL.java +++ b/core/src/main/java/org/opensearch/sql/expression/DSL.java @@ -390,6 +390,10 @@ public static FunctionExpression week(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.WEEK, expressions); } + public static FunctionExpression week_of_year(Expression... expressions) { + return compile(FunctionProperties.None, BuiltinFunctionName.WEEK_OF_YEAR, expressions); + } + public static FunctionExpression year(Expression... expressions) { return compile(FunctionProperties.None, BuiltinFunctionName.YEAR, 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..c004967c10 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 @@ -127,7 +127,8 @@ public void register(BuiltinFunctionRepository repository) { repository.register(date_format()); repository.register(to_days()); repository.register(unix_timestamp()); - repository.register(week()); + repository.register(week(BuiltinFunctionName.WEEK)); + repository.register(week(BuiltinFunctionName.WEEK_OF_YEAR)); repository.register(year()); } @@ -566,8 +567,8 @@ private FunctionResolver unix_timestamp() { /** * WEEK(DATE[,mode]). return the week number for date. */ - private DefaultFunctionResolver week() { - return define(BuiltinFunctionName.WEEK.getName(), + private DefaultFunctionResolver week(BuiltinFunctionName week) { + return define(week.getName(), impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), INTEGER, DATE), impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), INTEGER, DATETIME), impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), 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..b8225b61fd 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 { TO_DAYS(FunctionName.of("to_days")), UNIX_TIMESTAMP(FunctionName.of("unix_timestamp")), WEEK(FunctionName.of("week")), + WEEK_OF_YEAR(FunctionName.of("week_of_year")), YEAR(FunctionName.of("year")), // `now`-like functions NOW(FunctionName.of("now")), 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..9564f49a11 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; @@ -949,6 +950,124 @@ public void modeInUnsupportedFormat() { exception.getMessage()); } + private void testWeekOfYear(String date, int mode, int expectedResult) { + FunctionExpression expression = DSL + .week_of_year(DSL.literal(new ExprDateValue(date)), DSL.literal(mode)); + assertEquals(INTEGER, expression.type()); + assertEquals(String.format("week_of_year(DATE '%s', %d)", date, mode), expression.toString()); + assertEquals(integerValue(expectedResult), eval(expression)); + } + + private void testNullMissingWeekOfYear(ExprCoreType date) { + when(nullRef.type()).thenReturn(date); + when(missingRef.type()).thenReturn(date); + assertEquals(nullValue(), eval(DSL.week_of_year(nullRef))); + assertEquals(missingValue(), eval(DSL.week_of_year(missingRef))); + } + + @Test + public void testInvalidWeekOfYear() { + testNullMissingWeekOfYear(DATE); + testNullMissingWeekOfYear(DATETIME); + testNullMissingWeekOfYear(TIMESTAMP); + testNullMissingWeekOfYear(STRING); + + when(nullRef.type()).thenReturn(INTEGER); + when(missingRef.type()).thenReturn(INTEGER); + assertEquals(nullValue(), eval(DSL.week_of_year(DSL.literal("2019-01-05"), nullRef))); + assertEquals(missingValue(), eval(DSL.week_of_year(DSL.literal("2019-01-05"), missingRef))); + + when(nullRef.type()).thenReturn(DATE); + when(missingRef.type()).thenReturn(INTEGER); + assertEquals(missingValue(), eval(DSL.week_of_year(nullRef, missingRef))); + + //test invalid month + assertThrows(SemanticCheckException.class, () -> testWeekOfYear("2019-13-05 01:02:03", 0, 0)); + //test invalid day + assertThrows(SemanticCheckException.class, () -> testWeekOfYear("2019-01-50 01:02:03", 0, 0)); + //test invalid leap year + assertThrows(SemanticCheckException.class, () -> testWeekOfYear("2019-02-29 01:02:03", 0, 0)); + } + + @Test + public void testWeekOfYearAlternateArgumentFormats() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + FunctionExpression expression = DSL + .week_of_year(DSL.literal(new ExprTimestampValue("2019-01-05 01:02:03"))); + assertEquals(INTEGER, expression.type()); + assertEquals("week_of_year(TIMESTAMP '2019-01-05 01:02:03')", expression.toString()); + assertEquals(integerValue(0), eval(expression)); + + expression = DSL.week_of_year(DSL.literal("2019-01-05")); + assertEquals(INTEGER, expression.type()); + assertEquals("week_of_year(\"2019-01-05\")", expression.toString()); + assertEquals(integerValue(0), eval(expression)); + + expression = DSL.week_of_year(DSL.literal("2019-01-05 00:01:00")); + assertEquals(INTEGER, expression.type()); + assertEquals("week_of_year(\"2019-01-05 00:01:00\")", expression.toString()); + assertEquals(integerValue(0), eval(expression)); + } + + @Test + public void testWeekOfYearDifferentModes() { + lenient().when(nullRef.valueOf(env)).thenReturn(nullValue()); + lenient().when(missingRef.valueOf(env)).thenReturn(missingValue()); + + //Test the behavior of different modes passed into the 'week_of_year' function + testWeekOfYear("2019-01-05", 0, 0); + testWeekOfYear("2019-01-05", 1, 1); + testWeekOfYear("2019-01-05", 2, 52); + testWeekOfYear("2019-01-05", 3, 1); + testWeekOfYear("2019-01-05", 4, 1); + testWeekOfYear("2019-01-05", 5, 0); + testWeekOfYear("2019-01-05", 6, 1); + testWeekOfYear("2019-01-05", 7, 53); + + testWeekOfYear("2019-01-06", 0, 1); + testWeekOfYear("2019-01-06", 1, 1); + testWeekOfYear("2019-01-06", 2, 1); + testWeekOfYear("2019-01-06", 3, 1); + testWeekOfYear("2019-01-06", 4, 2); + testWeekOfYear("2019-01-06", 5, 0); + testWeekOfYear("2019-01-06", 6, 2); + testWeekOfYear("2019-01-06", 7, 53); + + testWeekOfYear("2019-01-07", 0, 1); + testWeekOfYear("2019-01-07", 1, 2); + testWeekOfYear("2019-01-07", 2, 1); + testWeekOfYear("2019-01-07", 3, 2); + testWeekOfYear("2019-01-07", 4, 2); + testWeekOfYear("2019-01-07", 5, 1); + testWeekOfYear("2019-01-07", 6, 2); + testWeekOfYear("2019-01-07", 7, 1); + + testWeekOfYear("2000-01-01", 0, 0); + testWeekOfYear("2000-01-01", 2, 52); + testWeekOfYear("1999-12-31", 0, 52); + + } + + @Test + public void weekOfYearModeInUnsupportedFormat() { + testNullMissingWeekOfYear(DATE); + + FunctionExpression expression1 = DSL + .week_of_year(DSL.literal(new ExprDateValue("2019-01-05")), DSL.literal(8)); + SemanticCheckException exception = + assertThrows(SemanticCheckException.class, () -> eval(expression1)); + assertEquals("mode:8 is invalid, please use mode value between 0-7", + exception.getMessage()); + + FunctionExpression expression2 = DSL + .week_of_year(DSL.literal(new ExprDateValue("2019-01-05")), DSL.literal(-1)); + exception = assertThrows(SemanticCheckException.class, () -> eval(expression2)); + assertEquals("mode:-1 is invalid, please use mode value between 0-7", + exception.getMessage()); + } + @Test public void to_days() { when(nullRef.type()).thenReturn(DATE); diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 0644b970f5..215152b11f 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -2059,6 +2059,7 @@ Description >>>>>>>>>>> Usage: week(date[, mode]) returns the week number for date. If the mode argument is omitted, the default mode 0 is used. +The function `week_of_year` is also provided as an alias. .. list-table:: The following table describes how the mode argument works. :widths: 25 50 25 75 @@ -2107,14 +2108,36 @@ Return type: INTEGER Example:: - >od SELECT WEEK(DATE('2008-02-20')), WEEK(DATE('2008-02-20'), 1) + os> SELECT WEEK(DATE('2008-02-20')), WEEK(DATE('2008-02-20'), 1) fetched rows / total rows = 1/1 +----------------------------+-------------------------------+ | WEEK(DATE('2008-02-20')) | WEEK(DATE('2008-02-20'), 1) | - |----------------------------|-------------------------------| + |----------------------------+-------------------------------| | 7 | 8 | +----------------------------+-------------------------------+ +WEEK_OF_YEAR +---- + +Description +>>>>>>>>>>> + +The week_of_year function is a synonym for the `week`_ function. + +Argument type: DATE/DATETIME/TIMESTAMP/STRING + +Return type: INTEGER + +Example:: + + os> SELECT WEEK_OF_YEAR(DATE('2008-02-20')), WEEK_OF_YEAR(DATE('2008-02-20'), 1) + fetched rows / total rows = 1/1 + +------------------------------------+---------------------------------------+ + | WEEK_OF_YEAR(DATE('2008-02-20')) | WEEK_OF_YEAR(DATE('2008-02-20'), 1) | + |------------------------------------+---------------------------------------| + | 7 | 8 | + +------------------------------------+---------------------------------------+ + YEAR ---- 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..acfbc8ef6d 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; @@ -48,6 +49,7 @@ public class DateTimeFunctionIT extends SQLIntegTestCase { public void init() throws Exception { super.init(); loadIndex(Index.BANK); + loadIndex(Index.CALCS); loadIndex(Index.PEOPLE2); } @@ -423,11 +425,11 @@ public void testYear() throws IOException { verifyDataRows(result, rows(2020)); } - private void week(String date, int mode, int expectedResult) throws IOException { - JSONObject result = executeQuery(StringUtils.format("select week(date('%s'), %d)", date, + private void week(String date, int mode, int expectedResult, String functionName) throws IOException { + JSONObject result = executeQuery(StringUtils.format("select %s(date('%s'), %d)", functionName, date, mode)); verifySchema(result, - schema(StringUtils.format("week(date('%s'), %d)", date, mode), null, "integer")); + schema(StringUtils.format("%s(date('%s'), %d)", functionName, date, mode), null, "integer")); verifyDataRows(result, rows(expectedResult)); } @@ -437,11 +439,56 @@ public void testWeek() throws IOException { verifySchema(result, schema("week(date('2008-02-20'))", null, "integer")); verifyDataRows(result, rows(7)); - week("2008-02-20", 0, 7); - week("2008-02-20", 1, 8); - week("2008-12-31", 1, 53); - week("2000-01-01", 0, 0); - week("2000-01-01", 2, 52); + week("2008-02-20", 0, 7, "week"); + week("2008-02-20", 1, 8, "week"); + week("2008-12-31", 1, 53, "week"); + week("2000-01-01", 0, 0, "week"); + week("2000-01-01", 2, 52, "week"); + } + + @Test + public void testWeekOfYear() throws IOException { + JSONObject result = executeQuery("select week_of_year(date('2008-02-20'))"); + verifySchema(result, schema("week_of_year(date('2008-02-20'))", null, "integer")); + verifyDataRows(result, rows(7)); + + week("2008-02-20", 0, 7, "week_of_year"); + week("2008-02-20", 1, 8, "week_of_year"); + week("2008-12-31", 1, 53, "week_of_year"); + week("2000-01-01", 0, 0, "week_of_year"); + week("2000-01-01", 2, 52, "week_of_year"); + } + + @Test + public void testWeekAlternateSyntaxesReturnTheSameResults() throws IOException { + JSONObject result1 = executeQuery("SELECT week(date('2022-11-22'))"); + JSONObject result2 = executeQuery("SELECT week_of_year(date('2022-11-22'))"); + verifyDataRows(result1, rows(47)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT week(CAST(date0 AS date)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT week_of_year(CAST(date0 AS date)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT week(datetime(CAST(time0 AS STRING))) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT week_of_year(datetime(CAST(time0 AS STRING))) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT week(CAST(time0 AS STRING)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT week_of_year(CAST(time0 AS STRING)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); + + result1 = executeQuery(String.format( + "SELECT week(CAST(datetime0 AS timestamp)) FROM %s", TEST_INDEX_CALCS)); + result2 = executeQuery(String.format( + "SELECT week_of_year(CAST(datetime0 AS timestamp)) FROM %s", TEST_INDEX_CALCS)); + result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows")); } void verifyDateFormat(String date, String type, String format, String formatted) throws IOException { diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index b17c25261a..358808a4f5 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -249,6 +249,7 @@ datetimeConstantLiteral | UTC_TIMESTAMP | UTC_DATE | UTC_TIME + | WEEK_OF_YEAR ; intervalLiteral 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..cf23b66942 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,13 @@ 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_week_of_year_functions() { + assertNotNull(parser.parse("SELECT week('2022-11-18')")); + assertNotNull(parser.parse("SELECT week_of_year('2022-11-18')")); + } + @Test public void can_parse_multi_match_relevance_function() { assertNotNull(parser.parse(