Skip to content
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
15 changes: 11 additions & 4 deletions core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public class DateTimeUtils {
* @return Rounded date/time value in utc millis
*/
public static long roundFloor(long utcMillis, long unitMillis) {
return utcMillis - utcMillis % unitMillis;
long res = utcMillis - utcMillis % unitMillis;
return (utcMillis < 0 && res != utcMillis) ? res - unitMillis : res;
}

/**
Expand Down Expand Up @@ -65,7 +66,9 @@ public static long roundMonth(long utcMillis, int interval) {
(zonedDateTime.getYear() - initDateTime.getYear()) * 12L
+ zonedDateTime.getMonthValue()
- initDateTime.getMonthValue();
long monthToAdd = (monthDiff / interval - 1) * interval;
long multiplier = monthDiff / interval - 1;
if (monthDiff < 0 && monthDiff % interval != 0) --multiplier;
long monthToAdd = multiplier * interval;
Comment on lines +69 to +71
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if I understand correct: this can be abstract as

static long roundMonth(long utcMillis, int interval)   { return roundByMonths(utcMillis, interval); }
static long roundQuarter(long utcMillis, int interval) { return roundByMonths(utcMillis, interval * 3); }
static long roundYear(long utcMillis, int interval)    { return roundByMonths(utcMillis, interval * 12); }

Copy link
Collaborator Author

@qianheng-aws qianheng-aws Aug 13, 2025

Choose a reason for hiding this comment

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

Agree, seems that could be abstracted. This focus on bugfix and just keep the exist implementation without too much refinement. Maybe that refinement could be done next time if we need further related change.

return initDateTime.plusMonths(monthToAdd).toInstant().toEpochMilli();
}

Expand All @@ -84,7 +87,9 @@ public static long roundQuarter(long utcMillis, int interval) {
((zonedDateTime.getYear() - initDateTime.getYear()) * 12L
+ zonedDateTime.getMonthValue()
- initDateTime.getMonthValue());
long monthToAdd = (monthDiff / (interval * 3L) - 1) * interval * 3;
long multiplier = monthDiff / (interval * 3L) - 1;
if (monthDiff < 0 && monthDiff % (interval * 3L) != 0) --multiplier;
long monthToAdd = multiplier * interval * 3;
return initDateTime.plusMonths(monthToAdd).toInstant().toEpochMilli();
}

Expand All @@ -99,7 +104,9 @@ public static long roundYear(long utcMillis, int interval) {
ZonedDateTime initDateTime = ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
ZonedDateTime zonedDateTime = Instant.ofEpochMilli(utcMillis).atZone(ZoneOffset.UTC);
int yearDiff = zonedDateTime.getYear() - initDateTime.getYear();
int yearToAdd = (yearDiff / interval) * interval;
int multiplier = yearDiff / interval;
if (yearDiff < 0 && yearDiff % interval != 0) --multiplier;
int yearToAdd = multiplier * interval;
return initDateTime.plusYears(yearToAdd).toInstant().toEpochMilli();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.opensearch.sql.planner.physical.collector.Rounding.DateTimeUnit;

public class DateTimeUtilsTest {
Copy link
Collaborator

Choose a reason for hiding this comment

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

np: this is actually UT for DateTimeUnit?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, It's actually testing the round API for each DateTimeUnit, and that APIs are only called by DateTimeUtilsT, so I put the test case here.

@Test
Expand Down Expand Up @@ -105,4 +106,76 @@ void testRelativeZonedDateTimeWithWrongInput() {
IllegalArgumentException.class, () -> getRelativeZonedDateTime("1d+1y", zonedDateTime));
assertEquals(e.getMessage(), "Unexpected character '1' at position 0 in input: 1d+1y");
}

@Test
void testRoundOnTimestampBeforeEpoch() {
long actual =
LocalDateTime.parse("1961-05-12T23:40:05")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli();
long rounded = DateTimeUnit.MINUTE.round(actual, 1);
assertEquals(
LocalDateTime.parse("1961-05-12T23:40:00")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
Instant.ofEpochMilli(rounded).toEpochMilli());

rounded = DateTimeUnit.HOUR.round(actual, 1);
assertEquals(
LocalDateTime.parse("1961-05-12T23:00:00")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
Instant.ofEpochMilli(rounded).toEpochMilli());

rounded = DateTimeUnit.DAY.round(actual, 1);
assertEquals(
LocalDateTime.parse("1961-05-12T00:00:00")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
Instant.ofEpochMilli(rounded).toEpochMilli());

rounded = DateTimeUnit.DAY.round(actual, 3);
assertEquals(
LocalDateTime.parse("1961-05-12T00:00:00")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
Instant.ofEpochMilli(rounded).toEpochMilli());

rounded = DateTimeUnit.WEEK.round(actual, 1);
assertEquals(
LocalDateTime.parse("1961-05-08T00:00:00")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
Instant.ofEpochMilli(rounded).toEpochMilli());

rounded = DateTimeUnit.MONTH.round(actual, 1);
assertEquals(
LocalDateTime.parse("1961-05-01T00:00:00")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
Instant.ofEpochMilli(rounded).toEpochMilli());

rounded = DateTimeUnit.QUARTER.round(actual, 1);
assertEquals(
LocalDateTime.parse("1961-04-01T00:00:00")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
Instant.ofEpochMilli(rounded).toEpochMilli());

rounded = DateTimeUnit.YEAR.round(actual, 2);
assertEquals(
LocalDateTime.parse("1960-01-01T00:00:00")
.atZone(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
Instant.ofEpochMilli(rounded).toEpochMilli());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -502,13 +502,7 @@ public void testCountBySpanForCustomFormats() throws IOException {
actual,
schema("timestamp_span", "timestamp"),
schema("count(custom_no_delimiter_ts)", "bigint"));
// TODO: Span has different behavior between pushdown and non-pushdown for timestamp before
// 1971. V2 engine will have the same issue.
// https://github.com/opensearch-project/sql/issues/3827
verifyDataRows(
actual,
rows(1, isPushdownEnabled() ? "1961-04-12 09:00:00" : "1961-04-12 10:00:00"),
rows(1, "1984-10-20 15:00:00"));
verifyDataRows(actual, rows(1, "1961-04-12 09:00:00"), rows(1, "1984-10-20 15:00:00"));

actual =
executeQuery(
Expand Down
Loading