Skip to content

Commit

Permalink
Add YEARWEEK Function To OpenSearch SQL (#1417) (#1445)
Browse files Browse the repository at this point in the history
* Add `YEARWEEK` Function To OpenSearch SQL

* Added Tests

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Added Implementation

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Fixed Implementation

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Added Documentation

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Fixed Tests

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Addressed PR Comments

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Cleaned YearweekTest File

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Added Tests and Fixed Docs

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Added And Modified Unit Tests

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Fixed Jacoco In Core

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Reworked convertWeekModeFromMySqlToJava Function

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Refactored To Remove Extra Function

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

---------

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

* Fixed Magic Number

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>

---------

Signed-off-by: GabeFernandez310 <Gabriel.Fernandez@improving.com>
(cherry picked from commit 03e4f97)
  • Loading branch information
GabeFernandez310 authored Mar 16, 2023
1 parent 7122a55 commit b0fdab9
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 1 deletion.
5 changes: 5 additions & 0 deletions core/src/main/java/org/opensearch/sql/expression/DSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,11 @@ public static FunctionExpression year(Expression... expressions) {
return compile(FunctionProperties.None, BuiltinFunctionName.YEAR, expressions);
}

public static FunctionExpression yearweek(
FunctionProperties functionProperties, Expression... expressions) {
return compile(functionProperties, BuiltinFunctionName.YEARWEEK, expressions);
}

public static FunctionExpression divide(Expression... expressions) {
return compile(FunctionProperties.None, BuiltinFunctionName.DIVIDE, expressions);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public void register(BuiltinFunctionRepository repository) {
repository.register(week(BuiltinFunctionName.WEEK_OF_YEAR));
repository.register(weekday());
repository.register(year());
repository.register(yearweek());
}

/**
Expand Down Expand Up @@ -926,6 +927,30 @@ private DefaultFunctionResolver year() {
);
}

/**
* YEARWEEK(DATE[,mode]). return the week number for date.
*/
private DefaultFunctionResolver yearweek() {
return define(BuiltinFunctionName.YEARWEEK.getName(),
implWithProperties(nullMissingHandlingWithProperties((functionProperties, arg)
-> yearweekToday(
DEFAULT_WEEK_OF_YEAR_MODE,
functionProperties.getQueryStartClock())), INTEGER, TIME),
impl(nullMissingHandling(DateTimeFunction::exprYearweekWithoutMode), INTEGER, DATE),
impl(nullMissingHandling(DateTimeFunction::exprYearweekWithoutMode), INTEGER, DATETIME),
impl(nullMissingHandling(DateTimeFunction::exprYearweekWithoutMode), INTEGER, TIMESTAMP),
impl(nullMissingHandling(DateTimeFunction::exprYearweekWithoutMode), INTEGER, STRING),
implWithProperties(nullMissingHandlingWithProperties((functionProperties, time, modeArg)
-> yearweekToday(
modeArg,
functionProperties.getQueryStartClock())), INTEGER, TIME, INTEGER),
impl(nullMissingHandling(DateTimeFunction::exprYearweek), INTEGER, DATE, INTEGER),
impl(nullMissingHandling(DateTimeFunction::exprYearweek), INTEGER, DATETIME, INTEGER),
impl(nullMissingHandling(DateTimeFunction::exprYearweek), INTEGER, TIMESTAMP, INTEGER),
impl(nullMissingHandling(DateTimeFunction::exprYearweek), INTEGER, STRING, INTEGER)
);
}

/**
* Formats date according to format specifier. First argument is date, second is format.
* Detailed supported signatures:
Expand Down Expand Up @@ -1792,7 +1817,7 @@ private Double unixTimeStampOfImpl(ExprValue value) {
* @return ExprValue.
*/
private ExprValue exprWeekWithoutMode(ExprValue date) {
return exprWeek(date, new ExprIntegerValue(0));
return exprWeek(date, DEFAULT_WEEK_OF_YEAR_MODE);
}

/**
Expand All @@ -1805,6 +1830,52 @@ private ExprValue exprYear(ExprValue date) {
return new ExprIntegerValue(date.dateValue().getYear());
}

/**
* Helper function to extract the yearweek output from a given date.
*
* @param date is a LocalDate input argument.
* @param mode is an integer containing the mode used to parse the LocalDate.
* @return is a long containing the formatted output for the yearweek function.
*/
private ExprIntegerValue extractYearweek(LocalDate date, int mode) {
// Needed to align with MySQL. Due to how modes for this function work.
// See description of modes here ...
// https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_week
int modeJava = CalendarLookup.getWeekNumber(mode, date) != 0 ? mode :
mode <= 4 ? 2 :
7;

int formatted = CalendarLookup.getYearNumber(modeJava, date) * 100
+ CalendarLookup.getWeekNumber(modeJava, date);

return new ExprIntegerValue(formatted);
}

/**
* Yearweek for date implementation for ExprValue.
*
* @param date ExprValue of Date/Datetime/Time/Timestamp/String type.
* @param mode ExprValue of Integer type.
*/
private ExprValue exprYearweek(ExprValue date, ExprValue mode) {
return extractYearweek(date.dateValue(), mode.integerValue());
}

/**
* Yearweek for date implementation for ExprValue.
* When mode is not specified default value mode 0 is used.
*
* @param date ExprValue of Date/Datetime/Time/Timestamp/String type.
* @return ExprValue.
*/
private ExprValue exprYearweekWithoutMode(ExprValue date) {
return exprYearweek(date, new ExprIntegerValue(0));
}

private ExprValue yearweekToday(ExprValue mode, Clock clock) {
return extractYearweek(LocalDateTime.now(clock).toLocalDate(), mode.integerValue());
}

private ExprValue monthOfYearToday(Clock clock) {
return new ExprIntegerValue(LocalDateTime.now(clock).getMonthValue());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public enum BuiltinFunctionName {
WEEKOFYEAR(FunctionName.of("weekofyear")),
WEEK_OF_YEAR(FunctionName.of("week_of_year")),
YEAR(FunctionName.of("year")),
YEARWEEK(FunctionName.of("yearweek")),
// `now`-like functions
NOW(FunctionName.of("now")),
CURDATE(FunctionName.of("curdate")),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/


package org.opensearch.sql.expression.datetime;

import static java.time.temporal.ChronoField.ALIGNED_WEEK_OF_YEAR;
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.opensearch.sql.data.model.ExprValueUtils.integerValue;
import static org.opensearch.sql.data.type.ExprCoreType.INTEGER;

import java.time.LocalDate;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.opensearch.sql.data.model.ExprDateValue;
import org.opensearch.sql.data.model.ExprDatetimeValue;
import org.opensearch.sql.data.model.ExprTimeValue;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.exception.SemanticCheckException;
import org.opensearch.sql.expression.DSL;
import org.opensearch.sql.expression.Expression;
import org.opensearch.sql.expression.ExpressionTestBase;
import org.opensearch.sql.expression.FunctionExpression;

class YearweekTest extends ExpressionTestBase {

private void yearweekQuery(String date, int mode, int expectedResult) {
FunctionExpression expression = DSL
.yearweek(
functionProperties,
DSL.literal(new ExprDateValue(date)), DSL.literal(mode));
assertAll(
() -> assertEquals(INTEGER, expression.type()),
() -> assertEquals(
String.format("yearweek(DATE '%s', %d)", date, mode), expression.toString()),
() -> assertEquals(integerValue(expectedResult), eval(expression))
);
}

private static Stream<Arguments> getTestDataForYearweek() {
//Test the behavior of different modes passed into the 'yearweek' function
return Stream.of(
Arguments.of("2019-01-05", 0, 201852),
Arguments.of("2019-01-05", 1, 201901),
Arguments.of("2019-01-05", 2, 201852),
Arguments.of("2019-01-05", 3, 201901),
Arguments.of("2019-01-05", 4, 201901),
Arguments.of("2019-01-05", 5, 201853),
Arguments.of("2019-01-05", 6, 201901),
Arguments.of("2019-01-05", 7, 201853),
Arguments.of("2019-01-06", 0, 201901),
Arguments.of("2019-01-06", 1, 201901),
Arguments.of("2019-01-06", 2, 201901),
Arguments.of("2019-01-06", 3, 201901),
Arguments.of("2019-01-06", 4, 201902),
Arguments.of("2019-01-06", 5, 201853),
Arguments.of("2019-01-06", 6, 201902),
Arguments.of("2019-01-06", 7, 201853),
Arguments.of("2019-01-07", 0, 201901),
Arguments.of("2019-01-07", 1, 201902),
Arguments.of("2019-01-07", 2, 201901),
Arguments.of("2019-01-07", 3, 201902),
Arguments.of("2019-01-07", 4, 201902),
Arguments.of("2019-01-07", 5, 201901),
Arguments.of("2019-01-07", 6, 201902),
Arguments.of("2019-01-07", 7, 201901),
Arguments.of("2000-01-01", 0, 199952),
Arguments.of("2000-01-01", 2, 199952),
Arguments.of("1999-12-31", 0, 199952),
Arguments.of("1999-01-01", 0, 199852),
Arguments.of("1999-01-01", 1, 199852),
Arguments.of("1999-01-01", 4, 199852),
Arguments.of("1999-01-01", 5, 199852),
Arguments.of("1999-01-01", 6, 199852)
);
}

@ParameterizedTest(name = "{0} | {1}")
@MethodSource("getTestDataForYearweek")
public void testYearweak(String date, int mode, int expected) {
yearweekQuery(date, mode, expected);
}

@Test
public void testYearweekWithoutMode() {
LocalDate date = LocalDate.of(2019,1,05);

FunctionExpression expression = DSL
.yearweek(
functionProperties,
DSL.literal(new ExprDateValue(date)), DSL.literal(0));

FunctionExpression expressionWithoutMode = DSL
.yearweek(
functionProperties,
DSL.literal(new ExprDateValue(date)));

assertEquals(eval(expression), eval(expressionWithoutMode));
}

@Test
public void testYearweekWithTimeType() {
int week = LocalDate.now(functionProperties.getQueryStartClock()).get(ALIGNED_WEEK_OF_YEAR);
int year = LocalDate.now(functionProperties.getQueryStartClock()).getYear();
int expected = Integer.parseInt(String.format("%d%02d", year, week));

FunctionExpression expression = DSL
.yearweek(
functionProperties,
DSL.literal(new ExprTimeValue("10:11:12")), DSL.literal(0));

FunctionExpression expressionWithoutMode = DSL
.yearweek(
functionProperties,
DSL.literal(new ExprTimeValue("10:11:12")));

assertAll(
() -> assertEquals(expected, eval(expression).integerValue()),
() -> assertEquals(expected, eval(expressionWithoutMode).integerValue())
);
}

@Test
public void testInvalidYearWeek() {
assertAll(
//test invalid month
() -> assertThrows(
SemanticCheckException.class,
() -> yearweekQuery("2019-13-05 01:02:03", 0, 0)),
//test invalid day
() -> assertThrows(
SemanticCheckException.class,
() -> yearweekQuery("2019-01-50 01:02:03", 0, 0)),
//test invalid leap year
() -> assertThrows(
SemanticCheckException.class,
() -> yearweekQuery("2019-02-29 01:02:03", 0, 0))
);
}

@Test
public void yearweekModeInUnsupportedFormat() {
FunctionExpression expression1 = DSL
.yearweek(
functionProperties,
DSL.literal(new ExprDatetimeValue("2019-01-05 10:11:12")), 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
.yearweek(
functionProperties,
DSL.literal(new ExprDatetimeValue("2019-01-05 10:11:12")), DSL.literal(-1));
exception = assertThrows(SemanticCheckException.class, () -> eval(expression2));
assertEquals("mode:-1 is invalid, please use mode value between 0-7",
exception.getMessage());
}

private ExprValue eval(Expression expression) {
return expression.valueOf();
}
}
22 changes: 22 additions & 0 deletions docs/user/dql/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2787,6 +2787,28 @@ Example::
+----------------------------+


YEARWEEK
--------

Description
>>>>>>>>>>>

Usage: yearweek(date) returns the year and week for date as an integer. It accepts and optional mode arguments aligned with those available for the `WEEK`_ function.

Argument type: STRING/DATE/DATETIME/TIME/TIMESTAMP

Return type: INTEGER

Example::

os> SELECT YEARWEEK('2020-08-26'), YEARWEEK('2019-01-05', 0)
fetched rows / total rows = 1/1
+--------------------------+-----------------------------+
| YEARWEEK('2020-08-26') | YEARWEEK('2019-01-05', 0) |
|--------------------------+-----------------------------|
| 202034 | 201852 |
+--------------------------+-----------------------------+

String Functions
================

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,14 @@ public void testWeekAlternateSyntaxesReturnTheSameResults() throws IOException {
compareWeekResults("CAST(datetime0 AS timestamp)", TEST_INDEX_CALCS);
}

@Test
public void testYearweek() throws IOException {
JSONObject result = executeQuery(
String.format("SELECT yearweek(time0), yearweek(time0, 4) FROM %s LIMIT 2", TEST_INDEX_CALCS));

verifyDataRows(result, rows(189952, 189952), rows(189953, 190001));
}

void verifyDateFormat(String date, String type, String format, String formatted) throws IOException {
String query = String.format("date_format(%s('%s'), '%s')", type, date, format);
JSONObject result = executeQuery("select " + query);
Expand Down
1 change: 1 addition & 0 deletions sql/src/main/antlr/OpenSearchSQLLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ STRCMP: 'STRCMP';

// DATE AND TIME FUNCTIONS
ADDDATE: 'ADDDATE';
YEARWEEK: 'YEARWEEK';

// RELEVANCE FUNCTIONS AND PARAMETERS
ALLOW_LEADING_WILDCARD: 'ALLOW_LEADING_WILDCARD';
Expand Down
1 change: 1 addition & 0 deletions sql/src/main/antlr/OpenSearchSQLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ dateTimeFunctionName
| WEEK_OF_YEAR
| WEEKOFYEAR
| YEAR
| YEARWEEK
;

textFunctionName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,12 @@ public void can_parse_wildcard_query_relevance_function() {
+ "boost=1.5, case_insensitive=true, rewrite=\"scoring_boolean\")"));
}

@Test
public void can_parse_yearweek_function() {
assertNotNull(parser.parse("SELECT yearweek('1987-01-01')"));
assertNotNull(parser.parse("SELECT yearweek('1987-01-01', 1)"));
}

@ParameterizedTest
@MethodSource({
"matchPhraseComplexQueries",
Expand Down

0 comments on commit b0fdab9

Please sign in to comment.