diff --git a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/transformation/dag/column/unary/scalar/DateBinFunctionColumnTransformer.java b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/transformation/dag/column/unary/scalar/DateBinFunctionColumnTransformer.java index 445ca0560adf4..061543fd16614 100644 --- a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/transformation/dag/column/unary/scalar/DateBinFunctionColumnTransformer.java +++ b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/transformation/dag/column/unary/scalar/DateBinFunctionColumnTransformer.java @@ -90,19 +90,22 @@ private static LocalDateTime convertToLocalDateTime(long timestamp, ZoneId zoneI } private static long convertToTimestamp(LocalDateTime dateTime, ZoneId zoneId) { - // Converting LocalDateTime to Seconds since epoch - long epochMilliSecond = dateTime.atZone(zoneId).toInstant().toEpochMilli(); - // Get the nanoseconds section - long nanoAdjustment = dateTime.getNano(); + // Encode from the epoch second and the nanosecond-of-second. toEpochMilli() already + // contains the whole milliseconds of that second, so adding getNano() on top counts + // those milliseconds a second time. + Instant instant = dateTime.atZone(zoneId).toInstant(); + long seconds = instant.getEpochSecond(); + long nanoOfSecond = instant.getNano(); switch (CommonDescriptor.getInstance().getConfig().getTimestampPrecision()) { case "ms": - return epochMilliSecond + nanoAdjustment / NANOSECONDS_IN_MILLISECOND; + return Math.addExact( + Math.multiplyExact(seconds, 1000L), nanoOfSecond / NANOSECONDS_IN_MILLISECOND); case "us": - return TimeUnit.MILLISECONDS.toMicros(epochMilliSecond) - + nanoAdjustment / NANOSECONDS_IN_MICROSECOND; + return Math.addExact( + Math.multiplyExact(seconds, 1_000_000L), nanoOfSecond / NANOSECONDS_IN_MICROSECOND); case "ns": - return TimeUnit.MILLISECONDS.toNanos(epochMilliSecond) + nanoAdjustment; + return Math.addExact(Math.multiplyExact(seconds, 1_000_000_000L), nanoOfSecond); default: throw new IllegalArgumentException( CalcMessages.EXCEPTION_UNKNOWN_PRECISION_0119BEB0 diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/transformation/dag/column/unary/scalar/DateBinFunctionTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/transformation/dag/column/unary/scalar/DateBinFunctionTest.java index 013e8c8429081..1c222623c7720 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/transformation/dag/column/unary/scalar/DateBinFunctionTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/transformation/dag/column/unary/scalar/DateBinFunctionTest.java @@ -19,6 +19,10 @@ package org.apache.iotdb.db.queryengine.transformation.dag.column.unary.scalar; +import org.apache.iotdb.calc.transformation.dag.column.leaf.NullColumnTransformer; +import org.apache.iotdb.calc.transformation.dag.column.unary.scalar.DateBinFunctionColumnTransformer; +import org.apache.iotdb.commons.conf.CommonDescriptor; + import org.junit.Before; import org.junit.Test; @@ -28,6 +32,9 @@ import java.util.TimeZone; import static org.apache.iotdb.calc.transformation.dag.column.unary.scalar.DateBinFunctionColumnTransformer.dateBin; +import static org.apache.iotdb.calc.transformation.dag.column.unary.scalar.DateBinFunctionColumnTransformer.nextDateBin; +import static org.apache.tsfile.read.common.type.TimestampType.TIMESTAMP; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; public class DateBinFunctionTest { @@ -126,4 +133,53 @@ public void testSourceBeforeUnixEpoch() throws ParseException { zoneId); assertEquals(getTimestamp("1969-12-31 00:00:00.000"), result); } + + @Test + public void testMonthBinKeepsFractionalOrigin() throws ParseException { + long origin = 500L; + long oneMonthLater = getTimestamp("1970-02-01 00:00:00.500"); + long justBeforeNextMonth = oneMonthLater - 1; + + assertEquals(origin, dateBin(origin, origin, 1, 0, zoneId)); + assertEquals(origin, dateBin(justBeforeNextMonth, origin, 1, 0, zoneId)); + assertEquals(oneMonthLater, dateBin(oneMonthLater, origin, 1, 0, zoneId)); + + long marchOrigin = getTimestamp("2024-03-15 00:00:00.500"); + long aprilOrigin = getTimestamp("2024-04-15 00:00:00.500"); + assertEquals(marchOrigin, dateBin(marchOrigin, marchOrigin, 1, 0, zoneId)); + assertEquals(aprilOrigin, dateBin(aprilOrigin, marchOrigin, 1, 0, zoneId)); + + long exactSecond = getTimestamp("2024-03-15 00:00:00.000"); + long nextExactMonth = getTimestamp("2024-04-15 00:00:00.000"); + assertEquals(exactSecond, dateBin(exactSecond, exactSecond, 1, 0, zoneId)); + assertEquals(nextExactMonth, dateBin(nextExactMonth, exactSecond, 1, 0, zoneId)); + } + + @Test + public void testMonthBinStartEndAndGapFillKeepFractionalMillis() throws ParseException { + long origin = 500L; + DateBinFunctionColumnTransformer transformer = + new DateBinFunctionColumnTransformer( + TIMESTAMP, 1, 0, new NullColumnTransformer(), origin, zoneId); + + long oneMonthLater = getTimestamp("1970-02-01 00:00:00.500"); + assertArrayEquals(new long[] {origin, oneMonthLater}, transformer.dateBinStartEnd(origin)); + assertEquals(oneMonthLater, nextDateBin(1, zoneId, origin)); + } + + @Test + public void testMonthBinKeepsSubMillisecondOriginUnderUsAndNs() { + String originalPrecision = CommonDescriptor.getInstance().getConfig().getTimestampPrecision(); + try { + // 1.5 ms after the epoch, expressed in microseconds and nanoseconds. + CommonDescriptor.getInstance().getConfig().setTimestampPrecision("us"); + assertEquals(1500L, dateBin(1500L, 1500L, 1, 0, zoneId)); + assertEquals(1500L, nextDateBin(1, zoneId, 1500L) - 2678400000000L); + + CommonDescriptor.getInstance().getConfig().setTimestampPrecision("ns"); + assertEquals(1_500_000L, dateBin(1_500_000L, 1_500_000L, 1, 0, zoneId)); + } finally { + CommonDescriptor.getInstance().getConfig().setTimestampPrecision(originalPrecision); + } + } }