Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functions ADDTIME and SUBTIME. (#132) #1194

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_SHORT_YEAR;
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_STRICT_WITH_TZ;
import static org.opensearch.sql.utils.DateTimeUtils.extractDate;
import static org.opensearch.sql.utils.DateTimeUtils.extractDateTime;

import java.math.BigDecimal;
import java.math.RoundingMode;
Expand Down Expand Up @@ -95,6 +96,7 @@ public class DateTimeFunction {
*/
public void register(BuiltinFunctionRepository repository) {
repository.register(adddate());
repository.register(addtime());
repository.register(convert_tz());
repository.register(curtime());
repository.register(curdate());
Expand Down Expand Up @@ -134,6 +136,7 @@ public void register(BuiltinFunctionRepository repository) {
repository.register(second(BuiltinFunctionName.SECOND));
repository.register(second(BuiltinFunctionName.SECOND_OF_MINUTE));
repository.register(subdate());
repository.register(subtime());
repository.register(sysdate());
repository.register(time());
repository.register(time_to_sec());
Expand Down Expand Up @@ -249,6 +252,52 @@ private DefaultFunctionResolver adddate() {
return add_date(BuiltinFunctionName.ADDDATE.getName());
}

/**
* Adds expr2 to expr1 and returns the result.
* (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
* (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
* TODO: MySQL has these signatures too
* (STRING, STRING/TIME) -> STRING // second arg - string with time only
* (x, STRING) -> NULL // second arg - string with timestamp
* (x, STRING/DATE) -> x // second arg - string with date only
*/
private DefaultFunctionResolver addtime() {
return define(BuiltinFunctionName.ADDTIME.getName(),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
TIME, TIME, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
TIME, TIME, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
TIME, TIME, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
TIME, TIME, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATETIME, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATETIME, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATETIME, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATETIME, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATE, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATE, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATE, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATE, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, TIMESTAMP, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, TIMESTAMP, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, TIMESTAMP, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, TIMESTAMP, TIMESTAMP)
);
}

/**
* Converts date/time from a specified timezone to another specified timezone.
* The supported signatures:
Expand Down Expand Up @@ -579,6 +628,52 @@ private DefaultFunctionResolver subdate() {
return sub_date(BuiltinFunctionName.SUBDATE.getName());
}

/**
* Subtracts expr2 from expr1 and returns the result.
* (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
* (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
* TODO: MySQL has these signatures too
* (STRING, STRING/TIME) -> STRING // second arg - string with time only
* (x, STRING) -> NULL // second arg - string with timestamp
* (x, STRING/DATE) -> x // second arg - string with date only
*/
private DefaultFunctionResolver subtime() {
return define(BuiltinFunctionName.SUBTIME.getName(),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
TIME, TIME, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
TIME, TIME, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
TIME, TIME, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
TIME, TIME, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATETIME, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATETIME, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATETIME, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATETIME, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATE, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATE, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATE, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATE, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, TIMESTAMP, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, TIMESTAMP, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, TIMESTAMP, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, TIMESTAMP, TIMESTAMP)
);
}

/**
* Extracts the time part of a date and time value.
* Also to construct a time type. The supported signatures:
Expand Down Expand Up @@ -768,6 +863,39 @@ private ExprValue exprAddDateDays(ExprValue date, ExprValue days) {
: exprValue);
}

/**
* Adds or subtracts time to/from date and returns the result.
*
* @param functionProperties A FunctionProperties object.
* @param temporal A Date/Time/Datetime/Timestamp value to change.
* @param temporalDelta A Date/Time/Datetime/Timestamp object to add/subtract time from.
* @param isAdd A flag: true to add, false to subtract.
* @return A value calculated.
*/
private ExprValue exprApplyTime(FunctionProperties functionProperties,
ExprValue temporal, ExprValue temporalDelta, Boolean isAdd) {
var interval = Duration.between(LocalTime.MIN, temporalDelta.timeValue());
var result = isAdd
? extractDateTime(temporal, functionProperties).plus(interval)
: extractDateTime(temporal, functionProperties).minus(interval);
return temporal.type() == TIME
? new ExprTimeValue(result.toLocalTime())
: new ExprDatetimeValue(result);
}

/**
* Adds time to date and returns the result.
*
* @param functionProperties A FunctionProperties object.
* @param temporal A Date/Time/Datetime/Timestamp value to change.
* @param temporalDelta A Date/Time/Datetime/Timestamp object to add time from.
* @return A value calculated.
*/
private ExprValue exprAddTime(FunctionProperties functionProperties,
ExprValue temporal, ExprValue temporalDelta) {
return exprApplyTime(functionProperties, temporal, temporalDelta, true);
}

/**
* CONVERT_TZ function implementation for ExprValue.
* Returns null for time zones outside of +13:00 and -12:00.
Expand Down Expand Up @@ -1181,6 +1309,18 @@ private ExprValue exprSubDateInterval(ExprValue date, ExprValue expr) {
: exprValue);
}

/**
* Subtracts expr2 from expr1 and returns the result.
*
* @param temporal A Date/Time/Datetime/Timestamp value to change.
* @param temporalDelta A Date/Time/Datetime/Timestamp to subtract time from.
* @return A value calculated.
*/
private ExprValue exprSubTime(FunctionProperties functionProperties,
ExprValue temporal, ExprValue temporalDelta) {
return exprApplyTime(functionProperties, temporal, temporalDelta, false);
}

/**
* Time implementation for ExprValue.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public enum BuiltinFunctionName {
* Date and Time Functions.
*/
ADDDATE(FunctionName.of("adddate")),
ADDTIME(FunctionName.of("addtime")),
CONVERT_TZ(FunctionName.of("convert_tz")),
DATE(FunctionName.of("date")),
DATEDIFF(FunctionName.of("datediff")),
Expand Down Expand Up @@ -90,6 +91,7 @@ public enum BuiltinFunctionName {
SECOND(FunctionName.of("second")),
SECOND_OF_MINUTE(FunctionName.of("second_of_minute")),
SUBDATE(FunctionName.of("subdate")),
SUBTIME(FunctionName.of("subtime")),
TIME(FunctionName.of("time")),
TIMEDIFF(FunctionName.of("timediff")),
TIME_TO_SEC(FunctionName.of("time_to_sec")),
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ public Boolean isValidMySqlTimeZoneId(ZoneId zone) {
|| passedTzValidator.isEqual(minTzValidator));
}

/**
* Extracts LocalDateTime from a datetime ExprValue.
* Uses `FunctionProperties` for `ExprTimeValue`.
*/
public static LocalDateTime extractDateTime(ExprValue value,
FunctionProperties functionProperties) {
return value instanceof ExprTimeValue
? ((ExprTimeValue) value).datetimeValue(functionProperties)
: value.datetimeValue();
}

/**
* Extracts LocalDate from a datetime ExprValue.
* Uses `FunctionProperties` for `ExprTimeValue`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.datetime;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opensearch.sql.data.type.ExprCoreType.DATETIME;
import static org.opensearch.sql.data.type.ExprCoreType.TIME;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.Temporal;
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;

public class AddTimeAndSubTimeTest extends DateTimeTestBase {

@Test
// (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
public void return_time_when_first_arg_is_time() {
var res = addtime(LocalTime.of(21, 0), LocalTime.of(0, 5));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(21, 5), res.timeValue());

res = subtime(LocalTime.of(21, 0), LocalTime.of(0, 5));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(20, 55), res.timeValue());

res = addtime(LocalTime.of(12, 20), Instant.ofEpochSecond(42));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(12, 20, 42), res.timeValue());

res = subtime(LocalTime.of(10, 0), Instant.ofEpochSecond(42));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(9, 59, 18), res.timeValue());

res = addtime(LocalTime.of(2, 3, 4), LocalDateTime.of(1961, 4, 12, 9, 7));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(11, 10, 4), res.timeValue());

res = subtime(LocalTime.of(12, 3, 4), LocalDateTime.of(1961, 4, 12, 9, 7));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(2, 56, 4), res.timeValue());

res = addtime(LocalTime.of(9, 7), LocalDate.now());
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(9, 7), res.timeValue());

res = subtime(LocalTime.of(9, 7), LocalDate.of(1961, 4, 12));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(9, 7), res.timeValue());
}

@Test
public void time_limited_by_24_hours() {
var res = addtime(LocalTime.of(21, 0), LocalTime.of(14, 5));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(11, 5), res.timeValue());

res = subtime(LocalTime.of(14, 0), LocalTime.of(21, 5));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(16, 55), res.timeValue());
}

// Function signature is:
// (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
private static Stream<Arguments> getTestData() {
return Stream.of(
// DATETIME and TIME/DATE/DATETIME/TIMESTAMP
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalTime.of(1, 48),
LocalDateTime.of(1961, 4, 12, 10, 55), LocalDateTime.of(1961, 4, 12, 7, 19)),
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalDate.of(2000, 1, 1),
LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1961, 4, 12, 9, 7)),
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1235, 5, 6, 1, 48),
LocalDateTime.of(1961, 4, 12, 10, 55), LocalDateTime.of(1961, 4, 12, 7, 19)),
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), Instant.ofEpochSecond(42),
LocalDateTime.of(1961, 4, 12, 9, 7, 42), LocalDateTime.of(1961, 4, 12, 9, 6, 18)),
// DATE and TIME/DATE/DATETIME/TIMESTAMP
Arguments.of(LocalDate.of(1961, 4, 12), LocalTime.of(9, 7),
LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1961, 4, 11, 14, 53)),
Arguments.of(LocalDate.of(1961, 4, 12), LocalDate.of(2000, 1, 1),
LocalDateTime.of(1961, 4, 12, 0, 0), LocalDateTime.of(1961, 4, 12, 0, 0)),
Arguments.of(LocalDate.of(1961, 4, 12), LocalDateTime.of(1235, 5, 6, 1, 48),
LocalDateTime.of(1961, 4, 12, 1, 48), LocalDateTime.of(1961, 4, 11, 22, 12)),
Arguments.of(LocalDate.of(1961, 4, 12), Instant.ofEpochSecond(42),
LocalDateTime.of(1961, 4, 12, 0, 0, 42), LocalDateTime.of(1961, 4, 11, 23, 59, 18)),
// TIMESTAMP and TIME/DATE/DATETIME/TIMESTAMP
Arguments.of(Instant.ofEpochSecond(42), LocalTime.of(9, 7),
LocalDateTime.of(1970, 1, 1, 9, 7, 42), LocalDateTime.of(1969, 12, 31, 14, 53, 42)),
Arguments.of(Instant.ofEpochSecond(42), LocalDate.of(1961, 4, 12),
LocalDateTime.of(1970, 1, 1, 0, 0, 42), LocalDateTime.of(1970, 1, 1, 0, 0, 42)),
Arguments.of(Instant.ofEpochSecond(42), LocalDateTime.of(1961, 4, 12, 9, 7),
LocalDateTime.of(1970, 1, 1, 9, 7, 42), LocalDateTime.of(1969, 12, 31, 14, 53, 42)),
Arguments.of(Instant.ofEpochSecond(42), Instant.ofEpochMilli(42),
LocalDateTime.of(1970, 1, 1, 0, 0, 42, 42000000),
LocalDateTime.of(1970, 1, 1, 0, 0, 41, 958000000))
);
}

/**
* Check that `ADDTIME` and `SUBTIME` functions result value and type.
* @param arg1 First argument.
* @param arg2 Second argument.
* @param addTimeExpectedResult Expected result for `ADDTIME`.
* @param subTimeExpectedResult Expected result for `SUBTIME`.
*/
@ParameterizedTest
@MethodSource("getTestData")
public void return_datetime_when_first_arg_is_not_time(Temporal arg1, Temporal arg2,
LocalDateTime addTimeExpectedResult,
LocalDateTime subTimeExpectedResult) {
var res = addtime(arg1, arg2);
assertEquals(DATETIME, res.type());
assertEquals(addTimeExpectedResult, res.datetimeValue());

res = subtime(arg1, arg2);
assertEquals(DATETIME, res.type());
assertEquals(subTimeExpectedResult, res.datetimeValue());
}
}
Loading