diff --git a/core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java b/core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java index 6b7e64a8865..671be3c647d 100644 --- a/core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java +++ b/core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java @@ -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; } /** @@ -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; return initDateTime.plusMonths(monthToAdd).toInstant().toEpochMilli(); } @@ -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(); } @@ -99,7 +104,9 @@ public static long roundYear(long utcMillis, int interval) { ZonedDateTime initDateTime = ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, UTC_ZONE_ID); ZonedDateTime zonedDateTime = Instant.ofEpochMilli(utcMillis).atZone(UTC_ZONE_ID); 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(); } diff --git a/core/src/test/java/org/opensearch/sql/utils/DateTimeUtilsTest.java b/core/src/test/java/org/opensearch/sql/utils/DateTimeUtilsTest.java index 80e1f90eb9a..3130ac001f0 100644 --- a/core/src/test/java/org/opensearch/sql/utils/DateTimeUtilsTest.java +++ b/core/src/test/java/org/opensearch/sql/utils/DateTimeUtilsTest.java @@ -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 { @Test @@ -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()); + } } diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLAggregationIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLAggregationIT.java index 77cb7372c04..0e64983861b 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLAggregationIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLAggregationIT.java @@ -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(