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");