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 The TO_SECONDS Function To The SQL Plugin #232

Merged
Merged
Show file tree
Hide file tree
Changes from 11 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
10 changes: 10 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 @@ -434,6 +434,16 @@ public static FunctionExpression to_days(Expression... expressions) {
return compile(FunctionProperties.None, BuiltinFunctionName.TO_DAYS, expressions);
}

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

public static FunctionExpression to_seconds(Expression... expressions) {
return to_seconds(FunctionProperties.None, expressions);
}


public static FunctionExpression week(
FunctionProperties functionProperties, Expression... expressions) {
return compile(functionProperties, BuiltinFunctionName.WEEK, expressions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling;
import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandlingWithProperties;
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_FORMATTER_LONG_YEAR;
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_FORMATTER_NO_YEAR;
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_FORMATTER_SHORT_YEAR;
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_FORMATTER_SINGLE_DIGIT_MONTH;
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_FORMATTER_SINGLE_DIGIT_YEAR;
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_LONG_YEAR;
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;
Expand Down Expand Up @@ -103,6 +106,9 @@ public class DateTimeFunction {
// Mode used for week/week_of_year function by default when no argument is provided
private static final ExprIntegerValue DEFAULT_WEEK_OF_YEAR_MODE = new ExprIntegerValue(0);

//The number of seconds per day
private static final long SECONDS_PER_DAY = 86400;

// Map used to determine format output for the get_format function
private static final Table<String, String, String> formats =
ImmutableTable.<String, String, String>builder()
Expand Down Expand Up @@ -191,6 +197,7 @@ public void register(BuiltinFunctionRepository repository) {
repository.register(utc_timestamp());
repository.register(date_format());
repository.register(to_days());
repository.register(to_seconds());
repository.register(unix_timestamp());
repository.register(week(BuiltinFunctionName.WEEK));
repository.register(week(BuiltinFunctionName.WEEKOFYEAR));
Expand Down Expand Up @@ -826,6 +833,15 @@ private DefaultFunctionResolver to_days() {
impl(nullMissingHandling(DateTimeFunction::exprToDays), LONG, DATETIME));
}

/**
* TO_SECONDS(STRING/DATE/DATETIME/TIMESTAMP/LONG). return the seconds number of the given date.
*/
private DefaultFunctionResolver to_seconds() {
return define(BuiltinFunctionName.TO_SECONDS.getName(),
impl(nullMissingHandling(DateTimeFunction::exprToSeconds), LONG, TIMESTAMP),
impl(nullMissingHandling(DateTimeFunction::exprToSecondsForIntType), LONG, LONG));
}

private FunctionResolver unix_timestamp() {
return define(BuiltinFunctionName.UNIX_TIMESTAMP.getName(),
implWithProperties(functionProperties
Expand Down Expand Up @@ -1626,6 +1642,84 @@ private ExprValue exprToDays(ExprValue date) {
return new ExprLongValue(date.dateValue().toEpochDay() + DAYS_0000_TO_1970);
}

/**
* To_seconds implementation for ExprValue.
*
* @param date ExprValue of Date/Datetime/Timestamp/String type.
* @return ExprValue.
*/
private ExprValue exprToSeconds(ExprValue date) {
return new ExprLongValue(
date.datetimeValue().toEpochSecond(ZoneOffset.UTC) + DAYS_0000_TO_1970 * SECONDS_PER_DAY);
}

/**
* To_seconds implementation with an integer argument for ExprValue.
*
* @param dateExpr ExprValue of an Integer/Long formatted for a date (e.g., 950501 = 1995-05-01)
* @return ExprValue.
*/
private ExprValue exprToSecondsForIntType(ExprValue dateExpr) {
try {
if (dateExpr.longValue() < 0 || dateExpr.longValue() > 99999999) {
throw new DateTimeException("Integer argument was out of range");
}

try {
LocalDate date = LocalDate.parse(String.valueOf(dateExpr.longValue()),
DATE_FORMATTER_LONG_YEAR);

return new ExprLongValue(
date.toEpochSecond(LocalTime.MIN, ZoneOffset.UTC)
+ DAYS_0000_TO_1970 * SECONDS_PER_DAY);
} catch (DateTimeParseException ignored) {
//ignore parse exception and try next format
}

try {
LocalDate date = LocalDate.parse(String.valueOf(dateExpr.longValue()),
DATE_FORMATTER_SHORT_YEAR);

return new ExprLongValue(
date.toEpochSecond(LocalTime.MIN, ZoneOffset.UTC)
+ DAYS_0000_TO_1970 * SECONDS_PER_DAY);
} catch (DateTimeParseException ignored) {
//ignore parse exception and try next format
}

try {
LocalDate date = LocalDate.parse(String.valueOf(dateExpr.longValue()),
DATE_FORMATTER_SINGLE_DIGIT_YEAR);

return new ExprLongValue(
date.toEpochSecond(LocalTime.MIN, ZoneOffset.UTC)
+ DAYS_0000_TO_1970 * SECONDS_PER_DAY);
} catch (DateTimeParseException ignored) {
//ignore parse exception and try next format
}

try {
LocalDate date = LocalDate.parse(String.valueOf(dateExpr.longValue()),
DATE_FORMATTER_NO_YEAR);

return new ExprLongValue(
date.toEpochSecond(LocalTime.MIN, ZoneOffset.UTC)
+ DAYS_0000_TO_1970 * SECONDS_PER_DAY);
} catch (DateTimeParseException ignored) {
//ignore parse exception and try next format
}

LocalDate date = LocalDate.parse(String.valueOf(dateExpr.longValue()),
DATE_FORMATTER_SINGLE_DIGIT_MONTH);

return new ExprLongValue(
date.toEpochSecond(LocalTime.MIN, ZoneOffset.UTC)
+ DAYS_0000_TO_1970 * SECONDS_PER_DAY);
} catch (DateTimeException e) {
return ExprNullValue.of();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be refactored to reduce duplication of try catches

Copy link
Author

@GabeFernandez310 GabeFernandez310 Mar 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ec908ad. Github formatted the diff strangely so I would suggest just looking at the file.

}

/**
* Week for date implementation for ExprValue.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public enum BuiltinFunctionName {
TIMESTAMP(FunctionName.of("timestamp")),
TIME_FORMAT(FunctionName.of("time_format")),
TO_DAYS(FunctionName.of("to_days")),
TO_SECONDS(FunctionName.of("to_seconds")),
UTC_DATE(FunctionName.of("utc_date")),
UTC_TIME(FunctionName.of("utc_time")),
UTC_TIMESTAMP(FunctionName.of("utc_timestamp")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,30 @@ public class DateTimeFormatters {
.toFormatter(Locale.ROOT)
.withResolverStyle(ResolverStyle.STRICT);

// MMDD
public static final DateTimeFormatter DATE_FORMATTER_SINGLE_DIGIT_MONTH =
new DateTimeFormatterBuilder()
.parseDefaulting(YEAR, 2000)
.appendPattern("Mdd")
.toFormatter()
.withResolverStyle(ResolverStyle.STRICT);

// MMDD
public static final DateTimeFormatter DATE_FORMATTER_NO_YEAR =
new DateTimeFormatterBuilder()
.parseDefaulting(YEAR, 2000)
.appendPattern("MMdd")
.toFormatter()
.withResolverStyle(ResolverStyle.STRICT);

// YMMDD
public static final DateTimeFormatter DATE_FORMATTER_SINGLE_DIGIT_YEAR =
new DateTimeFormatterBuilder()
.appendValueReduced(YEAR, 1, 1, 2000)
.appendPattern("MMdd")
.toFormatter()
.withResolverStyle(ResolverStyle.STRICT);

// YYMMDD
public static final DateTimeFormatter DATE_FORMATTER_SHORT_YEAR =
new DateTimeFormatterBuilder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.junit.jupiter.api.Assertions.assertThrows;
import static org.opensearch.sql.data.type.ExprCoreType.LONG;

import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.sql.data.model.ExprDateValue;
import org.opensearch.sql.data.model.ExprDatetimeValue;
import org.opensearch.sql.data.model.ExprLongValue;
import org.opensearch.sql.data.model.ExprNullValue;
import org.opensearch.sql.data.model.ExprStringValue;
import org.opensearch.sql.data.model.ExprTimeValue;
import org.opensearch.sql.data.model.ExprTimestampValue;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.expression.DSL;
import org.opensearch.sql.expression.Expression;
import org.opensearch.sql.expression.ExpressionTestBase;
import org.opensearch.sql.expression.FunctionExpression;
import org.opensearch.sql.expression.env.Environment;


@ExtendWith(MockitoExtension.class)
class ToSecondsTest extends ExpressionTestBase {

@Mock
Environment<Expression, ExprValue> env;

private static final long SECONDS_FROM_0001_01_01_TO_EPOCH_START = 62167219200L;

private static Stream<Arguments> getTestDataForToSeconds() {
return Stream.of(
Arguments.of(new ExprLongValue(101), new ExprLongValue(63113904000L)),
Arguments.of(new ExprLongValue(1030), new ExprLongValue(63140083200L)),
Arguments.of(new ExprLongValue(50101), new ExprLongValue(63271756800L)),
Arguments.of(new ExprLongValue(950501), new ExprLongValue(62966505600L)),
Arguments.of(new ExprLongValue(19950501), new ExprLongValue(62966505600L)),
Arguments.of(new ExprLongValue(9950501), ExprNullValue.of()),
Arguments.of(new ExprStringValue("2009-11-29 00:00:00"), new ExprLongValue(63426672000L)),
Arguments.of(new ExprStringValue("2009-11-29 13:43:32"), new ExprLongValue(63426721412L)),
Arguments.of(new ExprDateValue("2009-11-29"), new ExprLongValue(63426672000L)),
Arguments.of(new ExprDatetimeValue("2009-11-29 13:43:32"),
new ExprLongValue(63426721412L)),
Arguments.of(new ExprTimestampValue("2009-11-29 13:43:32"),
new ExprLongValue(63426721412L))
);
}

@ParameterizedTest
@MethodSource("getTestDataForToSeconds")
public void testToSeconds(ExprValue arg, ExprValue expected) {
FunctionExpression expr = DSL.to_seconds(DSL.literal(arg));
assertEquals(LONG, expr.type());
assertEquals(expected, eval(expr));
}

@Test
public void testToSecondsWithTimeType() {
FunctionExpression expr = DSL.to_seconds(functionProperties,
DSL.literal(new ExprTimeValue("10:11:12")));

long expected = SECONDS_FROM_0001_01_01_TO_EPOCH_START
+ LocalDate.now(functionProperties.getQueryStartClock())
.toEpochSecond(LocalTime.parse("10:11:12"), ZoneOffset.UTC);

assertEquals(expected, eval(expr).longValue());
}

private static Stream<Arguments> getInvalidTestDataForToSeconds() {
return Stream.of(
Arguments.of(new ExprLongValue(-123L))
);
}

@ParameterizedTest
@MethodSource("getInvalidTestDataForToSeconds")
public void testToSecondsInvalidArg(ExprValue arg) {
FunctionExpression expr = DSL.to_seconds(DSL.literal(arg));
assertThrows(DateTimeException.class, () -> eval(expr));
}

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

TO_SECONDS
----------

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

Usage: to_seconds(date) returns the number of seconds since the year 0 of the given value. Returns NULL if value is invalid.
An argument of a LONG type can be used. It must be formatted as YMMDD, YYMMDD, or YYYYMMDD.

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

Return type: LONG

Example::

os> SELECT TO_SECONDS(DATE '2008-10-07'), TO_SECONDS(950228)
fetched rows / total rows = 1/1
+---------------------------------+----------------------+
| TO_SECONDS(DATE '2008-10-07') | TO_SECONDS(950228) |
|---------------------------------+----------------------|
| 63390556800 | 62961148800 |
+---------------------------------+----------------------+

UNIX_TIMESTAMP
--------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,21 @@ public void testToDays() throws IOException {
verifyDataRows(result, rows(738049));
}

@Test
public void testToSeconds() throws IOException {
JSONObject result = executeQuery(
String.format("select to_seconds(date(date0)) FROM %s LIMIT 2", TEST_INDEX_CALCS));
verifyDataRows(result, rows(63249206400L), rows(62246275200L));

result = executeQuery(
String.format("SELECT to_seconds(datetime(cast(datetime0 AS string))) FROM %s LIMIT 2", TEST_INDEX_CALCS));
verifyDataRows(result, rows(63256587455L), rows(63258064234L));

result = executeQuery(String.format(
"select to_seconds(timestamp(datetime0)) FROM %s LIMIT 2", TEST_INDEX_CALCS));
verifyDataRows(result, rows(63256587455L), rows(63258064234L));
}

@Test
public void testYear() throws IOException {
JSONObject result = executeQuery("select year(date('2020-09-16'))");
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 @@ -265,6 +265,7 @@ TIME_TO_SEC: 'TIME_TO_SEC';
TIMESTAMP: 'TIMESTAMP';
TRUNCATE: 'TRUNCATE';
TO_DAYS: 'TO_DAYS';
TO_SECONDS: 'TO_SECONDS';
UNIX_TIMESTAMP: 'UNIX_TIMESTAMP';
UPPER: 'UPPER';
UTC_DATE: 'UTC_DATE';
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 @@ -477,6 +477,7 @@ dateTimeFunctionName
| TIMEDIFF
| TIMESTAMP
| TO_DAYS
| TO_SECONDS
| UNIX_TIMESTAMP
| WEEK
| WEEK_OF_YEAR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,12 @@ public void can_parse_last_day_function() {
assertNotNull(parser.parse("SELECT last_day('2004-01-01 01:01:01')"));
}

@Test
public void can_parse_to_seconds_function() {
assertNotNull(parser.parse("SELECT to_seconds(\"2023-02-20\")"));
assertNotNull(parser.parse("SELECT to_seconds(950501)"));
}

@Test
public void can_parse_wildcard_query_relevance_function() {
assertNotNull(
Expand Down