This is an automated email from the ASF dual-hosted git repository.

terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new 2112ae3de60 Truncate timestamps towards the past when comparing them 
for consistency (#39610)
2112ae3de60 is described below

commit 2112ae3de60f7ea2ae1be4a008e6190d3bf228c8
Author: ZIHAN DAI <[email protected]>
AuthorDate: Thu Sep 3 19:32:35 2026 +1000

    Truncate timestamps towards the past when comparing them for consistency 
(#39610)
    
    * Truncate timestamps towards the past when comparing them for consistency
    
    isMatched drops sub-second precision with getTime() / 1000 * 1000. Integer
    division truncates towards zero, so a pre-epoch value is rounded up into the
    second above the one it belongs to.
    
    The comparison then goes wrong both ways: -1500 and -1000 are half a second
    apart in different seconds and are reported as matching, while -1000 and 
-999
    are a millisecond apart in the same second and are reported as different.
    
    Use Math.floorDiv, which is unchanged above the epoch.
    
    * Address review: drop line comments and split the pre-epoch test per 
scenario
    
    CODE_OF_CONDUCT.md permits only Javadoc, TODO and FIXME comments, so the two
    line comments are gone and the helper carries the intent in its name 
instead:
    toSeconds is now toFlooredSeconds.
    
    testing.md requires one scenario per test method with a single call to the
    target. The four cases are now a parameterized test with name = "{0}", each
    naming its scenario and invoking isMatched once.
    
    * Assert the boolean result with assertTrue and assertFalse
    
    CODE_OF_CONDUCT.md requires assertTrue and assertFalse for boolean
    values and does not permit the Matchers.is static import, so the
    scenarios split into a matching and a non-matching parameterized test.
    Each execution still invokes isMatched once.
    
    ---------
    
    Co-authored-by: Zihan Dai <[email protected]>
---
 .../DataConsistencyCheckUtils.java                 |  6 ++++-
 .../DataConsistencyCheckUtilsTest.java             | 28 ++++++++++++++++++++++
 2 files changed, 33 insertions(+), 1 deletion(-)

diff --git 
a/kernel/data-pipeline/core/src/main/java/org/apache/shardingsphere/data/pipeline/core/consistencycheck/DataConsistencyCheckUtils.java
 
b/kernel/data-pipeline/core/src/main/java/org/apache/shardingsphere/data/pipeline/core/consistencycheck/DataConsistencyCheckUtils.java
index 9fd5c4d7c32..7bb79e37376 100644
--- 
a/kernel/data-pipeline/core/src/main/java/org/apache/shardingsphere/data/pipeline/core/consistencycheck/DataConsistencyCheckUtils.java
+++ 
b/kernel/data-pipeline/core/src/main/java/org/apache/shardingsphere/data/pipeline/core/consistencycheck/DataConsistencyCheckUtils.java
@@ -107,7 +107,7 @@ public final class DataConsistencyCheckUtils {
          * strategies with different database types could be considered.
          */
         if (thisColumnValue instanceof Timestamp && thatColumnValue instanceof 
Timestamp) {
-            return ((Timestamp) thisColumnValue).getTime() / 1000L * 1000L == 
((Timestamp) thatColumnValue).getTime() / 1000L * 1000L;
+            return toFlooredSeconds((Timestamp) thisColumnValue) == 
toFlooredSeconds((Timestamp) thatColumnValue);
         }
         if (thisColumnValue instanceof Array && thatColumnValue instanceof 
Array) {
             return Objects.deepEquals(((Array) thisColumnValue).getArray(), 
((Array) thatColumnValue).getArray());
@@ -115,6 +115,10 @@ public final class DataConsistencyCheckUtils {
         return equalsBuilder.append(thisColumnValue, 
thatColumnValue).isEquals();
     }
     
+    private static long toFlooredSeconds(final Timestamp timestamp) {
+        return Math.floorDiv(timestamp.getTime(), 1000L);
+    }
+    
     private static boolean isNumberEquals(final Number one, final Number 
another) {
         if (isInteger(one) && isInteger(another)) {
             return one.longValue() == another.longValue();
diff --git 
a/kernel/data-pipeline/core/src/test/java/org/apache/shardingsphere/data/pipeline/core/consistencycheck/DataConsistencyCheckUtilsTest.java
 
b/kernel/data-pipeline/core/src/test/java/org/apache/shardingsphere/data/pipeline/core/consistencycheck/DataConsistencyCheckUtilsTest.java
index 37706c513cb..f55b4a0c5e5 100644
--- 
a/kernel/data-pipeline/core/src/test/java/org/apache/shardingsphere/data/pipeline/core/consistencycheck/DataConsistencyCheckUtilsTest.java
+++ 
b/kernel/data-pipeline/core/src/test/java/org/apache/shardingsphere/data/pipeline/core/consistencycheck/DataConsistencyCheckUtilsTest.java
@@ -20,12 +20,16 @@ package 
org.apache.shardingsphere.data.pipeline.core.consistencycheck;
 import com.google.common.collect.ImmutableMap;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 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;
 
 import java.math.BigDecimal;
 import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Stream;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -59,6 +63,30 @@ class DataConsistencyCheckUtilsTest {
         assertFalse(DataConsistencyCheckUtils.isMatched(equalsBuilder, new 
Timestamp(time), new Timestamp(time + 1000L)));
     }
     
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("matchedPreEpochTimestampCases")
+    void assertTimestampMatchedBeforeEpoch(final String scenario, final long 
thisMillis, final long thatMillis) {
+        assertTrue(DataConsistencyCheckUtils.isMatched(new EqualsBuilder(), 
new Timestamp(thisMillis), new Timestamp(thatMillis)));
+    }
+    
+    private static Stream<Arguments> matchedPreEpochTimestampCases() {
+        return Stream.of(
+                Arguments.of("same second before epoch", -1000L, -999L),
+                Arguments.of("sub second tolerance before epoch", -1999L, 
-1001L));
+    }
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("notMatchedPreEpochTimestampCases")
+    void assertTimestampNotMatchedBeforeEpoch(final String scenario, final 
long thisMillis, final long thatMillis) {
+        assertFalse(DataConsistencyCheckUtils.isMatched(new EqualsBuilder(), 
new Timestamp(thisMillis), new Timestamp(thatMillis)));
+    }
+    
+    private static Stream<Arguments> notMatchedPreEpochTimestampCases() {
+        return Stream.of(
+                Arguments.of("different seconds before epoch", -1500L, -1000L),
+                Arguments.of("different seconds across epoch", -500L, 400L));
+    }
+    
     @Test
     void assertIsFirstUniqueKeysValueMatched() {
         Map<String, Object> record1 = ImmutableMap.of("order_id", 101, 
"user_id", 1, "status", "ok");

Reply via email to