Skip to content

Commit d9114f5

Browse files
Add last_day Function To OpenSearch SQL Plugin (#1344)
Signed-off-by: GabeFernandez310 <[email protected]>
1 parent 672c72f commit d9114f5

File tree

9 files changed

+179
-0
lines changed

9 files changed

+179
-0
lines changed

core/src/main/java/org/opensearch/sql/expression/DSL.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,11 @@ public static FunctionExpression hour_of_day(Expression... expressions) {
361361
return compile(FunctionProperties.None, BuiltinFunctionName.HOUR_OF_DAY, expressions);
362362
}
363363

364+
public static FunctionExpression last_day(FunctionProperties functionProperties,
365+
Expression... expressions) {
366+
return compile(functionProperties, BuiltinFunctionName.LAST_DAY, expressions);
367+
}
368+
364369
public static FunctionExpression microsecond(Expression... expressions) {
365370
return compile(FunctionProperties.None, BuiltinFunctionName.MICROSECOND, expressions);
366371
}

core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public void register(BuiltinFunctionRepository repository) {
160160
repository.register(get_format());
161161
repository.register(hour(BuiltinFunctionName.HOUR));
162162
repository.register(hour(BuiltinFunctionName.HOUR_OF_DAY));
163+
repository.register(last_day());
163164
repository.register(localtime());
164165
repository.register(localtimestamp());
165166
repository.register(makedate());
@@ -566,6 +567,18 @@ private DefaultFunctionResolver hour(BuiltinFunctionName name) {
566567
);
567568
}
568569

570+
private DefaultFunctionResolver last_day() {
571+
return define(BuiltinFunctionName.LAST_DAY.getName(),
572+
impl(nullMissingHandling(DateTimeFunction::exprLastDay), DATE, STRING),
573+
implWithProperties(nullMissingHandlingWithProperties((functionProperties, arg)
574+
-> DateTimeFunction.exprLastDayToday(
575+
functionProperties.getQueryStartClock())), DATE, TIME),
576+
impl(nullMissingHandling(DateTimeFunction::exprLastDay), DATE, DATE),
577+
impl(nullMissingHandling(DateTimeFunction::exprLastDay), DATE, DATETIME),
578+
impl(nullMissingHandling(DateTimeFunction::exprLastDay), DATE, TIMESTAMP)
579+
);
580+
}
581+
569582
private FunctionResolver makedate() {
570583
return define(BuiltinFunctionName.MAKEDATE.getName(),
571584
impl(nullMissingHandling(DateTimeFunction::exprMakeDate), DATE, DOUBLE, DOUBLE));
@@ -1287,6 +1300,39 @@ private ExprValue exprHour(ExprValue time) {
12871300
HOURS.between(LocalTime.MIN, time.timeValue()));
12881301
}
12891302

1303+
/**
1304+
* Helper function to retrieve the last day of a month based on a LocalDate argument.
1305+
*
1306+
* @param today a LocalDate.
1307+
* @return a LocalDate associated with the last day of the month for the given input.
1308+
*/
1309+
private LocalDate getLastDay(LocalDate today) {
1310+
return LocalDate.of(
1311+
today.getYear(),
1312+
today.getMonth(),
1313+
today.getMonth().length(today.isLeapYear()));
1314+
}
1315+
1316+
/**
1317+
* Returns a DATE for the last day of the month of a given argument.
1318+
*
1319+
* @param datetime A DATE/DATETIME/TIMESTAMP/STRING ExprValue.
1320+
* @return An DATE value corresponding to the last day of the month of the given argument.
1321+
*/
1322+
private ExprValue exprLastDay(ExprValue datetime) {
1323+
return new ExprDateValue(getLastDay(datetime.dateValue()));
1324+
}
1325+
1326+
/**
1327+
* Returns a DATE for the last day of the current month.
1328+
*
1329+
* @param clock The clock for the query start time from functionProperties.
1330+
* @return An DATE value corresponding to the last day of the month of the given argument.
1331+
*/
1332+
private ExprValue exprLastDayToday(Clock clock) {
1333+
return new ExprDateValue(getLastDay(formatNow(clock).toLocalDate()));
1334+
}
1335+
12901336
/**
12911337
* Following MySQL, function receives arguments of type double and rounds them before use.
12921338
* Furthermore:

core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public enum BuiltinFunctionName {
8181
GET_FORMAT(FunctionName.of("get_format")),
8282
HOUR(FunctionName.of("hour")),
8383
HOUR_OF_DAY(FunctionName.of("hour_of_day")),
84+
LAST_DAY(FunctionName.of("last_day")),
8485
MAKEDATE(FunctionName.of("makedate")),
8586
MAKETIME(FunctionName.of("maketime")),
8687
MICROSECOND(FunctionName.of("microsecond")),

core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,80 @@ public void hourOfDayInvalidArguments() {
862862

863863
}
864864

865+
private void checkForExpectedDay(
866+
FunctionExpression functionExpression,
867+
String expectedDay,
868+
String testExpr) {
869+
assertEquals(DATE, functionExpression.type());
870+
assertEquals(new ExprDateValue(expectedDay), eval(functionExpression));
871+
assertEquals(testExpr, functionExpression.toString());
872+
}
873+
874+
private static Stream<Arguments> getTestDataForLastDay() {
875+
return Stream.of(
876+
Arguments.of(new ExprDateValue("2017-01-20"), "2017-01-31", "last_day(DATE '2017-01-20')"),
877+
//Leap year
878+
Arguments.of(new ExprDateValue("2020-02-20"), "2020-02-29", "last_day(DATE '2020-02-20')"),
879+
//Non leap year
880+
Arguments.of(new ExprDateValue("2017-02-20"), "2017-02-28", "last_day(DATE '2017-02-20')"),
881+
Arguments.of(new ExprDateValue("2017-03-20"), "2017-03-31", "last_day(DATE '2017-03-20')"),
882+
Arguments.of(new ExprDateValue("2017-04-20"), "2017-04-30", "last_day(DATE '2017-04-20')"),
883+
Arguments.of(new ExprDateValue("2017-05-20"), "2017-05-31", "last_day(DATE '2017-05-20')"),
884+
Arguments.of(new ExprDateValue("2017-06-20"), "2017-06-30", "last_day(DATE '2017-06-20')"),
885+
Arguments.of(new ExprDateValue("2017-07-20"), "2017-07-31", "last_day(DATE '2017-07-20')"),
886+
Arguments.of(new ExprDateValue("2017-08-20"), "2017-08-31", "last_day(DATE '2017-08-20')"),
887+
Arguments.of(new ExprDateValue("2017-09-20"), "2017-09-30", "last_day(DATE '2017-09-20')"),
888+
Arguments.of(new ExprDateValue("2017-10-20"), "2017-10-31", "last_day(DATE '2017-10-20')"),
889+
Arguments.of(new ExprDateValue("2017-11-20"), "2017-11-30", "last_day(DATE '2017-11-20')"),
890+
Arguments.of(new ExprDateValue("2017-12-20"), "2017-12-31", "last_day(DATE '2017-12-20')")
891+
);
892+
}
893+
894+
@ParameterizedTest(name = "{2}")
895+
@MethodSource("getTestDataForLastDay")
896+
public void testLastDay(ExprValue testedDateTime, String expectedResult, String expectedQuery) {
897+
lenient().when(nullRef.valueOf(env)).thenReturn(nullValue());
898+
lenient().when(missingRef.valueOf(env)).thenReturn(missingValue());
899+
900+
checkForExpectedDay(
901+
DSL.last_day(functionProperties, DSL.literal(testedDateTime)),
902+
expectedResult,
903+
expectedQuery
904+
);
905+
}
906+
907+
@Test
908+
public void testLastDayWithTimeType() {
909+
lenient().when(nullRef.valueOf(env)).thenReturn(nullValue());
910+
lenient().when(missingRef.valueOf(env)).thenReturn(missingValue());
911+
912+
FunctionExpression expression = DSL.last_day(
913+
functionProperties, DSL.literal(new ExprTimeValue("12:23:34")));
914+
915+
LocalDate expected = LocalDate.now(functionProperties.getQueryStartClock());
916+
LocalDate result = eval(expression).dateValue();
917+
918+
919+
assertAll(
920+
() -> assertEquals((expected.lengthOfMonth()), result.getDayOfMonth()),
921+
() -> assertEquals("last_day(TIME '12:23:34')", expression.toString())
922+
);
923+
}
924+
925+
private void lastDay(String date) {
926+
FunctionExpression expression = DSL.day_of_week(
927+
functionProperties, DSL.literal(new ExprDateValue(date)));
928+
eval(expression);
929+
}
930+
931+
@Test
932+
public void testLastDayInvalidArgument() {
933+
lenient().when(nullRef.valueOf(env)).thenReturn(nullValue());
934+
lenient().when(missingRef.valueOf(env)).thenReturn(missingValue());
935+
936+
assertThrows(SemanticCheckException.class, () -> lastDay("asdfasdf"));
937+
}
938+
865939
@Test
866940
public void microsecond() {
867941
when(nullRef.type()).thenReturn(TIME);

docs/user/dql/functions.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2745,6 +2745,24 @@ Example::
27452745
| hello,world |
27462746
+------------------------------------+
27472747

2748+
LAST_DAY
2749+
--------
2750+
2751+
Usage: Returns the last day of the month as a DATE for a valid argument.
2752+
2753+
Argument type: DATE/DATETIME/STRING/TIMESTAMP/TIME
2754+
2755+
Return type: DATE
2756+
2757+
Example::
2758+
2759+
os> SELECT last_day('2023-02-06');
2760+
fetched rows / total rows = 1/1
2761+
+--------------------------+
2762+
| last_day('2023-02-06') |
2763+
|--------------------------|
2764+
| 2023-02-28 |
2765+
+--------------------------+
27482766

27492767
LEFT
27502768
----

integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,33 @@ public void testHourFunctionAliasesReturnTheSameResults() throws IOException {
513513
result1.getJSONArray("datarows").similar(result2.getJSONArray("datarows"));
514514
}
515515

516+
@Test
517+
public void testLastDay() throws IOException {
518+
JSONObject result = executeQuery(
519+
String.format("SELECT last_day(cast(date0 as date)) FROM %s LIMIT 3",
520+
TEST_INDEX_CALCS));
521+
verifyDataRows(result,
522+
rows("2004-04-30"),
523+
rows("1972-07-31"),
524+
rows("1975-11-30"));
525+
526+
result = executeQuery(
527+
String.format("SELECT last_day(datetime(cast(date0 AS string))) FROM %s LIMIT 3",
528+
TEST_INDEX_CALCS));
529+
verifyDataRows(result,
530+
rows("2004-04-30"),
531+
rows("1972-07-31"),
532+
rows("1975-11-30"));
533+
534+
result = executeQuery(
535+
String.format("SELECT last_day(cast(date0 AS timestamp)) FROM %s LIMIT 3",
536+
TEST_INDEX_CALCS));
537+
verifyDataRows(result,
538+
rows("2004-04-30"),
539+
rows("1972-07-31"),
540+
rows("1975-11-30"));
541+
}
542+
516543
@Test
517544
public void testMicrosecond() throws IOException {
518545
JSONObject result = executeQuery("select microsecond(timestamp('2020-09-16 17:30:00.123456'))");

sql/src/main/antlr/OpenSearchSQLLexer.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ GET_FORMAT: 'GET_FORMAT';
217217
IF: 'IF';
218218
IFNULL: 'IFNULL';
219219
ISNULL: 'ISNULL';
220+
LAST_DAY: 'LAST_DAY';
220221
LENGTH: 'LENGTH';
221222
LN: 'LN';
222223
LOCALTIME: 'LOCALTIME';

sql/src/main/antlr/OpenSearchSQLParser.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ dateTimeFunctionName
452452
| FROM_UNIXTIME
453453
| HOUR
454454
| HOUR_OF_DAY
455+
| LAST_DAY
455456
| MAKEDATE
456457
| MAKETIME
457458
| MICROSECOND

sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,12 @@ public void can_parse_minute_of_day_function() {
521521
assertNotNull(parser.parse("SELECT minute_of_day('2022-12-14 12:23:34');"));;
522522
}
523523

524+
@Test
525+
public void can_parse_last_day_function() {
526+
assertNotNull(parser.parse("SELECT last_day(\"2017-06-20\")"));
527+
assertNotNull(parser.parse("SELECT last_day('2004-01-01 01:01:01')"));
528+
}
529+
524530
@Test
525531
public void can_parse_wildcard_query_relevance_function() {
526532
assertNotNull(

0 commit comments

Comments
 (0)