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 c1e419280f7 Decode MySQL binlog dates that have a zero month or a zero 
day (#39676)
c1e419280f7 is described below

commit c1e419280f76a03120f89eb406984b2cb0b31a91
Author: ZIHAN DAI <[email protected]>
AuthorDate: Fri Oct 16 03:09:13 2026 +1100

    Decode MySQL binlog dates that have a zero month or a zero day (#39676)
    
    * Decode MySQL binlog dates that have a zero month or a zero day
    
    MySQL stores 2023-00-15 and 2023-05-00 whenever NO_ZERO_IN_DATE is not in
    sql_mode, and writes them to the binlog like any other date. The DATE,
    DATETIME and DATETIME2 decoders pass the parts straight to LocalDate,
    which rejects a zero month or day, so one such row aborts the binlog
    stream rather than affecting one column.
    
    Each decoder already returns text for the all-zero date, which needs the
    same relaxed sql_mode to exist. Return text for a partially zero one too,
    and keep the java.sql types for every date they can hold.
    
    * Format incomplete dates with Locale.ROOT and cover fractional DATETIME2
    
    String.format without a locale uses the default format locale, so under
    an Arabic or Thai locale a zero-month date came out in non-ASCII digits.
    Format both text forms with Locale.ROOT.
    
    Also cover a DATETIME2 value with a zero month or day and a non-zero
    fraction at each storage width, checking the full string and that the
    fraction bytes are consumed.
    
    * Make the date helpers package-private and unwrap the DATE return
    
    The three MySQLTimeValueUtils helpers are only called from decoders in
    the same package. The DATE return expression fits on one line.
    
    * Move readFully after its first caller
---
 .../value/time/MySQLDateBinlogProtocolValue.java   |  5 ++-
 .../time/MySQLDatetime2BinlogProtocolValue.java    |  4 +-
 .../time/MySQLDatetimeBinlogProtocolValue.java     |  7 +--
 .../row/column/value/time/MySQLTimeValueUtils.java | 43 ++++++++++++++++++
 .../time/MySQLDateBinlogProtocolValueTest.java     | 28 ++++++++++++
 .../MySQLDatetime2BinlogProtocolValueTest.java     | 52 ++++++++++++++++++++++
 .../time/MySQLDatetimeBinlogProtocolValueTest.java | 25 +++++++++++
 7 files changed, 159 insertions(+), 5 deletions(-)

diff --git 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDateBinlogProtocolValue.java
 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDateBinlogProtocolValue.java
index 503af06aa97..3ce663f4f1e 100644
--- 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDateBinlogProtocolValue.java
+++ 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDateBinlogProtocolValue.java
@@ -39,6 +39,9 @@ public final class MySQLDateBinlogProtocolValue implements 
MySQLBinlogProtocolVa
         int year = date / 16 / 32;
         int month = date / 32 % 16;
         int day = date % 32;
-        return 0 == date ? MySQLTimeValueUtils.ZERO_OF_DATE : 
Date.valueOf(LocalDate.of(year, month, day));
+        if (0 == date) {
+            return MySQLTimeValueUtils.ZERO_OF_DATE;
+        }
+        return MySQLTimeValueUtils.isCompleteDate(month, day) ? 
Date.valueOf(LocalDate.of(year, month, day)) : 
MySQLTimeValueUtils.formatIncompleteDate(year, month, day);
     }
 }
diff --git 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetime2BinlogProtocolValue.java
 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetime2BinlogProtocolValue.java
index 9520f17b91c..93b60d509f0 100644
--- 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetime2BinlogProtocolValue.java
+++ 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetime2BinlogProtocolValue.java
@@ -68,7 +68,9 @@ public final class MySQLDatetime2BinlogProtocolValue 
implements MySQLBinlogProto
         int minute = (int) ((time >> 6) % (1L << 6L));
         int second = (int) (time % (1L << 6L));
         MySQLFractionalSeconds fractionalSeconds = new 
MySQLFractionalSeconds(columnDef.getColumnMeta(), payload);
-        return Timestamp.valueOf(LocalDateTime.of(year, month, day, hour, 
minute, second, fractionalSeconds.getNanos()));
+        return MySQLTimeValueUtils.isCompleteDate(month, day)
+                ? Timestamp.valueOf(LocalDateTime.of(year, month, day, hour, 
minute, second, fractionalSeconds.getNanos()))
+                : MySQLTimeValueUtils.formatIncompleteDatetime(year, month, 
day, hour, minute, second, fractionalSeconds.getNanos());
     }
     
     private void skipFractionalSeconds(final MySQLBinlogColumnDef columnDef, 
final MySQLPacketPayload payload) {
diff --git 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetimeBinlogProtocolValue.java
 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetimeBinlogProtocolValue.java
index d29f96c6b10..917145dac0f 100644
--- 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetimeBinlogProtocolValue.java
+++ 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetimeBinlogProtocolValue.java
@@ -24,7 +24,6 @@ import 
org.apache.shardingsphere.database.protocol.mysql.payload.MySQLPacketPayl
 import java.io.Serializable;
 import java.sql.Timestamp;
 import java.time.LocalDateTime;
-import java.util.Date;
 
 /**
  * MySQL DATETIME binlog protocol value.
@@ -40,7 +39,7 @@ public final class MySQLDatetimeBinlogProtocolValue 
implements MySQLBinlogProtoc
         return 0L == datetime ? MySQLTimeValueUtils.DATETIME_OF_ZERO : 
readDateTime(datetime);
     }
     
-    private Date readDateTime(final long datetime) {
+    private Serializable readDateTime(final long datetime) {
         int date = (int) (datetime / 1000000L);
         int year = date / 10000;
         int month = (date % 10000) / 100;
@@ -49,6 +48,8 @@ public final class MySQLDatetimeBinlogProtocolValue 
implements MySQLBinlogProtoc
         int hour = time / 10000;
         int minute = (time % 10000) / 100;
         int second = time % 100;
-        return Timestamp.valueOf(LocalDateTime.of(year, month, day, hour, 
minute, second));
+        return MySQLTimeValueUtils.isCompleteDate(month, day)
+                ? Timestamp.valueOf(LocalDateTime.of(year, month, day, hour, 
minute, second))
+                : MySQLTimeValueUtils.formatIncompleteDatetime(year, month, 
day, hour, minute, second, 0);
     }
 }
diff --git 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLTimeValueUtils.java
 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLTimeValueUtils.java
index 3097d285bb8..bb426d447bc 100644
--- 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLTimeValueUtils.java
+++ 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLTimeValueUtils.java
@@ -20,6 +20,8 @@ package 
org.apache.shardingsphere.database.protocol.mysql.packet.binlog.row.colu
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 
+import java.util.Locale;
+
 /**
  * Time value utility class of MySQL.
  */
@@ -33,4 +35,45 @@ public final class MySQLTimeValueUtils {
     public static final String YEAR_OF_ZERO = "0000";
     
     public static final String DATETIME_OF_ZERO = "0000-00-00 00:00:00";
+    
+    /**
+     * Judge whether a date has both a month and a day, which is what {@link 
java.time.LocalDate} requires.
+     * MySQL stores a zero month or a zero day whenever NO_ZERO_IN_DATE is not 
in sql_mode.
+     *
+     * @param month month
+     * @param day day
+     * @return whether the date is complete
+     */
+    static boolean isCompleteDate(final int month, final int day) {
+        return month > 0 && day > 0;
+    }
+    
+    /**
+     * Format a date that {@link java.time.LocalDate} cannot hold, in the text 
form MySQL prints it in, with ASCII digits whatever the default locale.
+     *
+     * @param year year
+     * @param month month
+     * @param day day
+     * @return date in MySQL text form
+     */
+    static String formatIncompleteDate(final int year, final int month, final 
int day) {
+        return String.format(Locale.ROOT, "%04d-%02d-%02d", year, month, day);
+    }
+    
+    /**
+     * Format a datetime whose date part {@link java.time.LocalDate} cannot 
hold, in the text form MySQL prints it in, with ASCII digits whatever the 
default locale.
+     *
+     * @param year year
+     * @param month month
+     * @param day day
+     * @param hour hour
+     * @param minute minute
+     * @param second second
+     * @param nanos nanoseconds, always a whole number of microseconds
+     * @return datetime in MySQL text form
+     */
+    static String formatIncompleteDatetime(final int year, final int month, 
final int day, final int hour, final int minute, final int second, final int 
nanos) {
+        String result = String.format(Locale.ROOT, "%04d-%02d-%02d 
%02d:%02d:%02d", year, month, day, hour, minute, second);
+        return 0 == nanos ? result : result + String.format(Locale.ROOT, 
".%06d", nanos / 1000);
+    }
 }
diff --git 
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDateBinlogProtocolValueTest.java
 
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDateBinlogProtocolValueTest.java
index 5dba3e69bc8..72938f07365 100644
--- 
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDateBinlogProtocolValueTest.java
+++ 
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDateBinlogProtocolValueTest.java
@@ -27,6 +27,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.sql.Date;
 import java.time.LocalDate;
+import java.util.Locale;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
@@ -52,6 +53,33 @@ class MySQLDateBinlogProtocolValueTest {
         assertThat(new MySQLDateBinlogProtocolValue().read(columnDef, 
payload), is(expected));
     }
     
+    @Test
+    void assertReadDateWithZeroMonth() {
+        when(payload.getByteBuf()).thenReturn(byteBuf);
+        when(byteBuf.readUnsignedMediumLE()).thenReturn(2023 * 16 * 32 + 15);
+        assertThat(new MySQLDateBinlogProtocolValue().read(columnDef, 
payload), is("2023-00-15"));
+    }
+    
+    @Test
+    void assertReadDateWithZeroDay() {
+        when(payload.getByteBuf()).thenReturn(byteBuf);
+        when(byteBuf.readUnsignedMediumLE()).thenReturn(2023 * 16 * 32 + 5 * 
32);
+        assertThat(new MySQLDateBinlogProtocolValue().read(columnDef, 
payload), is("2023-05-00"));
+    }
+    
+    @Test
+    void assertReadDateWithZeroMonthUnderNonLatinDefaultFormatLocale() {
+        when(payload.getByteBuf()).thenReturn(byteBuf);
+        when(byteBuf.readUnsignedMediumLE()).thenReturn(2023 * 16 * 32 + 15);
+        Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT);
+        Locale.setDefault(Locale.Category.FORMAT, 
Locale.forLanguageTag("th-TH-u-nu-thai"));
+        try {
+            assertThat(new MySQLDateBinlogProtocolValue().read(columnDef, 
payload), is("2023-00-15"));
+        } finally {
+            Locale.setDefault(Locale.Category.FORMAT, originalLocale);
+        }
+    }
+    
     @Test
     void assertReadNullDate() {
         when(payload.getByteBuf()).thenReturn(byteBuf);
diff --git 
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetime2BinlogProtocolValueTest.java
 
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetime2BinlogProtocolValueTest.java
index 8e199760253..e4a35b397be 100644
--- 
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetime2BinlogProtocolValueTest.java
+++ 
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetime2BinlogProtocolValueTest.java
@@ -18,6 +18,7 @@
 package 
org.apache.shardingsphere.database.protocol.mysql.packet.binlog.row.column.value.time;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
 import 
org.apache.shardingsphere.database.protocol.mysql.constant.MySQLBinaryColumnType;
 import 
org.apache.shardingsphere.database.protocol.mysql.packet.binlog.row.column.MySQLBinlogColumnDef;
 import 
org.apache.shardingsphere.database.protocol.mysql.payload.MySQLPacketPayload;
@@ -31,8 +32,10 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.stubbing.OngoingStubbing;
 
+import java.nio.charset.StandardCharsets;
 import java.sql.Timestamp;
 import java.time.LocalDateTime;
+import java.util.Locale;
 import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -84,6 +87,55 @@ class MySQLDatetime2BinlogProtocolValueTest {
         }
     }
     
+    @Test
+    void assertReadWithZeroMonth() {
+        when(payload.readInt1()).thenReturn(0x99, 0xae, 0xde, 0xa0, 0x00);
+        assertThat(protocolValue.read(columnDef, payload), is("2023-00-15 
10:00:00"));
+    }
+    
+    @Test
+    void assertReadWithZeroDay() {
+        when(payload.readInt1()).thenReturn(0x99, 0xb0, 0x00, 0xa0, 0x00);
+        assertThat(protocolValue.read(columnDef, payload), is("2023-05-00 
10:00:00"));
+    }
+    
+    @Test
+    void assertReadWithZeroMonthAndOneByteFraction() {
+        assertThat(readFully(2, 0x99, 0xae, 0xde, 0xa0, 0x00, 0x2d), 
is("2023-00-15 10:00:00.450000"));
+    }
+    
+    private Object readFully(final int fractionalSecondsPrecision, final 
int... bytes) {
+        ByteBuf buffer = Unpooled.buffer(bytes.length);
+        for (int each : bytes) {
+            buffer.writeByte(each);
+        }
+        columnDef.setColumnMeta(fractionalSecondsPrecision);
+        Object result = protocolValue.read(columnDef, new 
MySQLPacketPayload(buffer, StandardCharsets.UTF_8));
+        assertThat(buffer.readableBytes(), is(0));
+        return result;
+    }
+    
+    @Test
+    void assertReadWithZeroDayAndTwoByteFraction() {
+        assertThat(readFully(4, 0x99, 0xb0, 0x00, 0xa0, 0x00, 0x04, 0xd2), 
is("2023-05-00 10:00:00.123400"));
+    }
+    
+    @Test
+    void assertReadWithZeroMonthAndThreeByteFraction() {
+        assertThat(readFully(6, 0x99, 0xae, 0xde, 0xa0, 0x00, 0x01, 0xe2, 
0x40), is("2023-00-15 10:00:00.123456"));
+    }
+    
+    @Test
+    void assertReadWithZeroMonthUnderNonLatinDefaultFormatLocale() {
+        Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT);
+        Locale.setDefault(Locale.Category.FORMAT, 
Locale.forLanguageTag("th-TH-u-nu-thai"));
+        try {
+            assertThat(readFully(6, 0x99, 0xae, 0xde, 0xa0, 0x00, 0x01, 0xe2, 
0x40), is("2023-00-15 10:00:00.123456"));
+        } finally {
+            Locale.setDefault(Locale.Category.FORMAT, originalLocale);
+        }
+    }
+    
     @Test
     void assertReadWithZeroDatetime() {
         assertThat(protocolValue.read(columnDef, payload), 
is(MySQLTimeValueUtils.DATETIME_OF_ZERO));
diff --git 
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetimeBinlogProtocolValueTest.java
 
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetimeBinlogProtocolValueTest.java
index 82011a475ee..b566da75d2e 100644
--- 
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetimeBinlogProtocolValueTest.java
+++ 
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/time/MySQLDatetimeBinlogProtocolValueTest.java
@@ -26,6 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.sql.Timestamp;
 import java.time.LocalDateTime;
+import java.util.Locale;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
@@ -40,6 +41,30 @@ class MySQLDatetimeBinlogProtocolValueTest {
     @Mock
     private MySQLBinlogColumnDef columnDef;
     
+    @Test
+    void assertReadDatetimeWithZeroMonth() {
+        when(payload.readInt8()).thenReturn(20230015100000L);
+        assertThat(new MySQLDatetimeBinlogProtocolValue().read(columnDef, 
payload), is("2023-00-15 10:00:00"));
+    }
+    
+    @Test
+    void assertReadDatetimeWithZeroDay() {
+        when(payload.readInt8()).thenReturn(20230500100000L);
+        assertThat(new MySQLDatetimeBinlogProtocolValue().read(columnDef, 
payload), is("2023-05-00 10:00:00"));
+    }
+    
+    @Test
+    void assertReadDatetimeWithZeroMonthUnderNonLatinDefaultFormatLocale() {
+        when(payload.readInt8()).thenReturn(20230015100000L);
+        Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT);
+        Locale.setDefault(Locale.Category.FORMAT, 
Locale.forLanguageTag("th-TH-u-nu-thai"));
+        try {
+            assertThat(new MySQLDatetimeBinlogProtocolValue().read(columnDef, 
payload), is("2023-00-15 10:00:00"));
+        } finally {
+            Locale.setDefault(Locale.Category.FORMAT, originalLocale);
+        }
+    }
+    
     @Test
     void assertRead() {
         when(payload.readInt8()).thenReturn(99991231235959L);

Reply via email to