From 6d160465b1039b3c6bd3abca8fff38d0bb343396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 3 Jul 2025 23:49:31 +0000 Subject: [PATCH 1/3] replication: return string when datetime's day or month is 00 MySQL supports 0 for day/month when this is an unknown value This cannot be encoded into a time.Time, where time.Date normalizes these 0s (so 2000-00-00 would become 1999-11-30) Pass these values through in their raw string form instead --- replication/row_event.go | 43 ++++++++++++++++++++++------------- replication/row_event_test.go | 2 ++ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/replication/row_event.go b/replication/row_event.go index a83a73ca5..729a68971 100644 --- a/replication/row_event.go +++ b/replication/row_event.go @@ -1316,7 +1316,7 @@ func (e *RowsEvent) decodeValue(data []byte, tp byte, meta uint16, isPartial boo n = 4 t := binary.LittleEndian.Uint32(data) if t == 0 { - v = formatZeroTime(0, 0) + v = "0000-00-00 00:00:00" } else { v = e.parseFracTime(fracTime{ Time: time.Unix(int64(t), 0), @@ -1331,23 +1331,34 @@ func (e *RowsEvent) decodeValue(data []byte, tp byte, meta uint16, isPartial boo n = 8 i64 := binary.LittleEndian.Uint64(data) if i64 == 0 { - v = formatZeroTime(0, 0) + v = "0000-00-00 00:00:00" } else { d := i64 / 1000000 t := i64 % 1000000 - v = e.parseFracTime(fracTime{ - Time: time.Date( - int(d/10000), - time.Month((d%10000)/100), - int(d%100), - int(t/10000), - int((t%10000)/100), - int(t%100), - 0, - time.UTC, - ), - Dec: 0, - }) + years := int(d / 10000) + months := (d % 10000) / 100 + days := int(d % 100) + hours := int(t / 10000) + minutes := int((t % 10000) / 100) + seconds := int(t % 100) + if months == 0 || days == 0 { + v = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", + years, months, days, hours, minutes, seconds) + } else { + v = e.parseFracTime(fracTime{ + Time: time.Date( + int(d/10000), + time.Month((d%10000)/100), + int(d%100), + int(t/10000), + int((t%10000)/100), + int(t%100), + 0, + time.UTC, + ), + Dec: 0, + }) + } } case mysql.MYSQL_TYPE_DATETIME2: v, n, err = decodeDatetime2(data, meta) @@ -1725,7 +1736,7 @@ func decodeDatetime2(data []byte, dec uint16) (interface{}, int, error) { // minute = 0 = 0b000000 // second = 0 = 0b000000 // integer value = 0b1100100000010110000100000000000000000 = 107420450816 - if intPart < 107420450816 { + if intPart < 107420450816 || month == 0 || day == 0 { return formatBeforeUnixZeroTime(year, month, day, hour, minute, second, int(frac), int(dec)), n, nil } diff --git a/replication/row_event_test.go b/replication/row_event_test.go index 7b004c50a..c8859d9dd 100644 --- a/replication/row_event_test.go +++ b/replication/row_event_test.go @@ -675,6 +675,8 @@ func TestDecodeDatetime2(t *testing.T) { }{ {[]byte("\xfe\xf3\xff\x7e\xfb"), 0, true, "9999-12-31 23:59:59"}, {[]byte("\x99\x9a\xb8\xf7\xaa"), 0, true, "2016-10-28 15:30:42"}, + {[]byte("\x99\x98\x38\xf7\xaa"), 0, false, "2016-00-28 15:30:42"}, + {[]byte("\x99\x9a\x80\xf7\xaa"), 0, false, "2016-10-00 15:30:42"}, {[]byte("\x99\x02\xc2\x00\x00"), 0, true, "1970-01-01 00:00:00"}, {[]byte("\x80\x00\x00\x00\x00"), 0, false, "0000-00-00 00:00:00"}, {[]byte("\x80\x00\x02\xf1\x05"), 0, false, "0000-00-01 15:04:05"}, From d01f447df6bcb59a42c60b33a62a29deb9354970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Fri, 4 Jul 2025 00:11:49 +0000 Subject: [PATCH 2/3] feedback --- replication/row_event.go | 10 +++++----- replication/row_event_test.go | 25 ++++++++++++++----------- replication/time.go | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/replication/row_event.go b/replication/row_event.go index 729a68971..e13a3090e 100644 --- a/replication/row_event.go +++ b/replication/row_event.go @@ -1341,7 +1341,7 @@ func (e *RowsEvent) decodeValue(data []byte, tp byte, meta uint16, isPartial boo hours := int(t / 10000) minutes := int((t % 10000) / 100) seconds := int(t % 100) - if months == 0 || days == 0 { + if !e.parseTime || months == 0 || days == 0 { v = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", years, months, days, hours, minutes, seconds) } else { @@ -1361,7 +1361,7 @@ func (e *RowsEvent) decodeValue(data []byte, tp byte, meta uint16, isPartial boo } } case mysql.MYSQL_TYPE_DATETIME2: - v, n, err = decodeDatetime2(data, meta) + v, n, err = decodeDatetime2(data, meta, e.parseTime) v = e.parseFracTime(v) case mysql.MYSQL_TYPE_TIME: n = 3 @@ -1686,7 +1686,7 @@ func decodeTimestamp2(data []byte, dec uint16, timestampStringLocation *time.Loc const DATETIMEF_INT_OFS int64 = 0x8000000000 -func decodeDatetime2(data []byte, dec uint16) (interface{}, int, error) { +func decodeDatetime2(data []byte, dec uint16, parseTime bool) (interface{}, int, error) { // get datetime binary length n := int(5 + (dec+1)/2) @@ -1736,8 +1736,8 @@ func decodeDatetime2(data []byte, dec uint16) (interface{}, int, error) { // minute = 0 = 0b000000 // second = 0 = 0b000000 // integer value = 0b1100100000010110000100000000000000000 = 107420450816 - if intPart < 107420450816 || month == 0 || day == 0 { - return formatBeforeUnixZeroTime(year, month, day, hour, minute, second, int(frac), int(dec)), n, nil + if !parseTime || intPart < 107420450816 || month == 0 || day == 0 { + return formatDatetime(year, month, day, hour, minute, second, int(frac), int(dec)), n, nil } return fracTime{ diff --git a/replication/row_event_test.go b/replication/row_event_test.go index c8859d9dd..60e5cbb9b 100644 --- a/replication/row_event_test.go +++ b/replication/row_event_test.go @@ -686,17 +686,20 @@ func TestDecodeDatetime2(t *testing.T) { {[]byte("\x80\x03\x82\x00\x00\x01\xe2\x40"), uint16(6), false, "0001-01-01 00:00:00.123456"}, } for _, tc := range testcases { - value, _, err := decodeDatetime2(tc.data, tc.dec) - require.NoError(t, err) - switch v := value.(type) { - case fracTime: - require.True(t, tc.getFracTime) - require.Equal(t, tc.expected, v.String()) - case string: - require.False(t, tc.getFracTime) - require.Equal(t, tc.expected, v) - default: - require.FailNow(t, "invalid value type: %T", value) + for _, parseTime := range []bool{true, false} { + value, _, err := decodeDatetime2(tc.data, tc.dec, parseTime) + require.NoError(t, err) + switch v := value.(type) { + case fracTime: + require.True(t, parseTime) + require.True(t, tc.getFracTime) + require.Equal(t, tc.expected, v.String()) + case string: + require.False(t, parseTime && tc.getFracTime) + require.Equal(t, tc.expected, v) + default: + require.FailNow(t, "invalid value type: %T", value) + } } } } diff --git a/replication/time.go b/replication/time.go index 948235666..cfc0a8585 100644 --- a/replication/time.go +++ b/replication/time.go @@ -44,7 +44,7 @@ func formatZeroTime(frac int, dec int) string { return s[0 : len(s)-(6-dec)] } -func formatBeforeUnixZeroTime(year, month, day, hour, minute, second, frac, dec int) string { +func formatDatetime(year, month, day, hour, minute, second, frac, dec int) string { if dec == 0 { return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second) } From 78d1a0071f2dc7ec34f8bf0a4d690f334d555d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Fri, 4 Jul 2025 01:30:26 +0000 Subject: [PATCH 3/3] oops --- replication/row_event.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/replication/row_event.go b/replication/row_event.go index e13a3090e..1972cb217 100644 --- a/replication/row_event.go +++ b/replication/row_event.go @@ -1336,10 +1336,10 @@ func (e *RowsEvent) decodeValue(data []byte, tp byte, meta uint16, isPartial boo d := i64 / 1000000 t := i64 % 1000000 years := int(d / 10000) - months := (d % 10000) / 100 + months := int(d%10000) / 100 days := int(d % 100) hours := int(t / 10000) - minutes := int((t % 10000) / 100) + minutes := int(t%10000) / 100 seconds := int(t % 100) if !e.parseTime || months == 0 || days == 0 { v = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", @@ -1347,12 +1347,12 @@ func (e *RowsEvent) decodeValue(data []byte, tp byte, meta uint16, isPartial boo } else { v = e.parseFracTime(fracTime{ Time: time.Date( - int(d/10000), - time.Month((d%10000)/100), - int(d%100), - int(t/10000), - int((t%10000)/100), - int(t%100), + years, + time.Month(months), + days, + hours, + minutes, + seconds, 0, time.UTC, ),