diff --git a/binlog/event_rows.go b/binlog/event_rows.go index c1b5054..e255061 100644 --- a/binlog/event_rows.go +++ b/binlog/event_rows.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "runtime/debug" - "time" "github.com/localhots/bocadillo/mysql" "github.com/localhots/bocadillo/tools" @@ -191,8 +190,9 @@ func (e *RowsEvent) decodeValue(buf *tools.Buffer, ct mysql.ColumnType, meta uin buf.Skip(n) return v case mysql.ColumnTypeTimestamp: - ts := buf.ReadUint32() - return mysql.FracTime{Time: time.Unix(int64(ts), 0)}.String() + v, n := mysql.DecodeTimestamp(buf.Cur(), meta) + buf.Skip(n) + return v case mysql.ColumnTypeTimestamp2: v, n := mysql.DecodeTimestamp2(buf.Cur(), meta) buf.Skip(n) diff --git a/mysql/time.go b/mysql/time.go index 3fd7595..52e3a45 100644 --- a/mysql/time.go +++ b/mysql/time.go @@ -103,10 +103,17 @@ func DecodeTime2(data []byte, dec uint16) (string, int) { return fmt.Sprintf("%s%02d:%02d:%02d", sign, hour, minute, second), n } +// DecodeTimestamp decodes TIMESTAMP value. +// Spec: https://dev.mysql.com/doc/refman/8.0/en/datetime.html +// Implementation borrowed from https://github.com/siddontang/go-mysql/ +func DecodeTimestamp(data []byte, dec uint16) (time.Time, int) { + return time.Unix(int64(DecodeUint32(data)), 0), 4 +} + // DecodeTimestamp2 decodes TIMESTAMP v2 value. // Spec: https://dev.mysql.com/doc/refman/8.0/en/datetime.html // Implementation borrowed from https://github.com/siddontang/go-mysql/ -func DecodeTimestamp2(data []byte, dec uint16) (string, int) { +func DecodeTimestamp2(data []byte, dec uint16) (time.Time, int) { // get timestamp binary length n := int(4 + (dec+1)/2) sec := int64(binary.BigEndian.Uint32(data[0:4])) @@ -121,18 +128,18 @@ func DecodeTimestamp2(data []byte, dec uint16) (string, int) { } if sec == 0 { - return formatZeroTime(int(usec), int(dec)), n + return time.Time{}, n } - return FracTime{time.Unix(sec, usec*1000), int(dec)}.String(), n + return time.Unix(sec, usec*1000), n } // DecodeDatetime decodes DATETIME value. // Spec: https://dev.mysql.com/doc/refman/8.0/en/datetime.html -func DecodeDatetime(v uint64) string { +func DecodeDatetime(v uint64) time.Time { d := v / 1000000 t := v % 1000000 - return FracTime{Time: time.Date(int(d/10000), + return time.Date(int(d/10000), time.Month((d%10000)/100), int(d%100), int(t/10000), @@ -140,13 +147,13 @@ func DecodeDatetime(v uint64) string { int(t%100), 0, Timezone, - )}.String() + ) } // DecodeDatetime2 decodes DATETIME v2 value. // Spec: https://dev.mysql.com/doc/refman/8.0/en/datetime.html // Implementation borrowed from https://github.com/siddontang/go-mysql/ -func DecodeDatetime2(data []byte, dec uint16) (string, int) { +func DecodeDatetime2(data []byte, dec uint16) (time.Time, int) { const offset int64 = 0x8000000000 // get datetime binary length n := int(5 + (dec+1)/2) @@ -164,7 +171,7 @@ func DecodeDatetime2(data []byte, dec uint16) (string, int) { } if intPart == 0 { - return formatZeroTime(int(frac), int(dec)), n + return time.Time{}, n } tmp := intPart<<24 + frac @@ -188,41 +195,5 @@ func DecodeDatetime2(data []byte, dec uint16) (string, int) { minute := int((hms >> 6) % (1 << 6)) hour := int((hms >> 12)) - return FracTime{time.Date(year, time.Month(month), day, hour, minute, second, int(frac*1000), Timezone), int(dec)}.String(), n -} - -var ( - fracTimeFormat = [...]string{ - "2006-01-02T15:04:05Z", - "2006-01-02T15:04:05.0Z", - "2006-01-02T15:04:05.00Z", - "2006-01-02T15:04:05.000Z", - "2006-01-02T15:04:05.0000Z", - "2006-01-02T15:04:05.00000Z", - "2006-01-02T15:04:05.000000Z", - } - zeroTimes = [...]string{ - "0000-00-00T00:00:00Z", - "0000-00-00T00:00:00.0Z", - "0000-00-00T00:00:00.00Z", - "0000-00-00T00:00:00.000Z", - "0000-00-00T00:00:00.0000Z", - "0000-00-00T00:00:00.00000Z", - "0000-00-00T00:00:00.000000Z", - } -) - -// FracTime is a help structure wrapping Golang Time. -type FracTime struct { - time.Time - Dec int -} - -func (t FracTime) String() string { - return t.Format(fracTimeFormat[t.Dec]) -} - -func formatZeroTime(frac int, dec int) string { - // We are going to ignore frac/dec distinction here - return zeroTimes[dec] + return time.Date(year, time.Month(month), day, hour, minute, second, int(frac*1000), Timezone), n } diff --git a/tests/time_test.go b/tests/time_test.go index 563ba98..3b06382 100644 --- a/tests/time_test.go +++ b/tests/time_test.go @@ -3,6 +3,7 @@ package tests import ( "fmt" "testing" + "time" "github.com/localhots/bocadillo/mysql" ) @@ -61,96 +62,96 @@ func TestTimestamp(t *testing.T) { tbl := suite.createTable(mysql.ColumnTypeTimestamp, "", attrNone) defer tbl.drop(t) - vals := []string{ - "0000-00-00T00:00:00Z", + vals := []time.Time{ + parseTime("0000-00-00T00:00:00Z"), // This is the lowest I could get // Spec says 1970-01-01 00:00:01 should be supported - "1970-01-01T01:00:01Z", - "1975-01-01T00:00:01Z", - "1985-01-01T00:00:01Z", - "1999-12-31T23:59:59Z", - "2018-11-08T19:26:00Z", - "2038-01-19T03:14:07Z", - "2038-01-19T04:14:07Z", // Should be outside supported range? 2038-01-19 03:14:07 + parseTime("1970-01-01T01:00:01Z"), + parseTime("1975-01-01T00:00:01Z"), + parseTime("1985-01-01T00:00:01Z"), + parseTime("1999-12-31T23:59:59Z"), + parseTime("2018-11-08T19:26:00Z"), + parseTime("2038-01-19T03:14:07Z"), + parseTime("2038-01-19T04:14:07Z"), // Should be outside supported range? 2038-01-19 03:14:07 } for _, v := range vals { - t.Run(v, func(t *testing.T) { + t.Run(v.Format(time.RFC3339Nano), func(t *testing.T) { suite.insertAndCompare(t, tbl, v) }) } } func TestDatetime(t *testing.T) { - inputs := map[string][]string{ + inputs := map[string][]time.Time{ "0": { - "0000-00-00T00:00:00Z", - "1000-01-01T00:00:00Z", - "1975-01-01T00:00:01Z", - "1985-01-01T00:00:01Z", - "1999-12-31T23:59:59Z", - "2018-11-08T19:26:00Z", - "2038-01-19T03:14:07Z", - "9999-12-31T23:59:59Z", + parseTime("0000-00-00T00:00:00Z"), + parseTime("1000-01-01T00:00:00Z"), + parseTime("1975-01-01T00:00:01Z"), + parseTime("1985-01-01T00:00:01Z"), + parseTime("1999-12-31T23:59:59Z"), + parseTime("2018-11-08T19:26:00Z"), + parseTime("2038-01-19T03:14:07Z"), + parseTime("9999-12-31T23:59:59Z"), }, "1": { - "0000-00-00T00:00:00.0Z", - "1000-01-01T00:00:00.1Z", - "1975-01-01T00:00:01.1Z", - "1985-01-01T00:00:01.1Z", - "1999-12-31T23:59:59.1Z", - "2018-11-08T19:26:00.1Z", - "2038-01-19T03:14:07.1Z", - "9999-12-31T23:59:59.1Z", + parseTime("0000-00-00T00:00:00.0Z"), + parseTime("1000-01-01T00:00:00.1Z"), + parseTime("1975-01-01T00:00:01.1Z"), + parseTime("1985-01-01T00:00:01.1Z"), + parseTime("1999-12-31T23:59:59.1Z"), + parseTime("2018-11-08T19:26:00.1Z"), + parseTime("2038-01-19T03:14:07.1Z"), + parseTime("9999-12-31T23:59:59.1Z"), }, "2": { - "0000-00-00T00:00:00.00Z", - "1000-01-01T00:00:00.22Z", - "1975-01-01T00:00:01.22Z", - "1985-01-01T00:00:01.22Z", - "1999-12-31T23:59:59.22Z", - "2018-11-08T19:26:00.22Z", - "2038-01-19T03:14:07.22Z", - "9999-12-31T23:59:59.22Z", + parseTime("0000-00-00T00:00:00.00Z"), + parseTime("1000-01-01T00:00:00.22Z"), + parseTime("1975-01-01T00:00:01.22Z"), + parseTime("1985-01-01T00:00:01.22Z"), + parseTime("1999-12-31T23:59:59.22Z"), + parseTime("2018-11-08T19:26:00.22Z"), + parseTime("2038-01-19T03:14:07.22Z"), + parseTime("9999-12-31T23:59:59.22Z"), }, "3": { - "0000-00-00T00:00:00.000Z", - "1000-01-01T00:00:00.333Z", - "1975-01-01T00:00:01.333Z", - "1985-01-01T00:00:01.333Z", - "1999-12-31T23:59:59.333Z", - "2018-11-08T19:26:00.333Z", - "2038-01-19T03:14:07.333Z", - "9999-12-31T23:59:59.333Z", + parseTime("0000-00-00T00:00:00.000Z"), + parseTime("1000-01-01T00:00:00.333Z"), + parseTime("1975-01-01T00:00:01.333Z"), + parseTime("1985-01-01T00:00:01.333Z"), + parseTime("1999-12-31T23:59:59.333Z"), + parseTime("2018-11-08T19:26:00.333Z"), + parseTime("2038-01-19T03:14:07.333Z"), + parseTime("9999-12-31T23:59:59.333Z"), }, "4": { - "0000-00-00T00:00:00.0000Z", - "1000-01-01T00:00:00.4444Z", - "1975-01-01T00:00:01.4444Z", - "1985-01-01T00:00:01.4444Z", - "1999-12-31T23:59:59.4444Z", - "2018-11-08T19:26:00.4444Z", - "2038-01-19T03:14:07.4444Z", - "9999-12-31T23:59:59.4444Z", + parseTime("0000-00-00T00:00:00.0000Z"), + parseTime("1000-01-01T00:00:00.4444Z"), + parseTime("1975-01-01T00:00:01.4444Z"), + parseTime("1985-01-01T00:00:01.4444Z"), + parseTime("1999-12-31T23:59:59.4444Z"), + parseTime("2018-11-08T19:26:00.4444Z"), + parseTime("2038-01-19T03:14:07.4444Z"), + parseTime("9999-12-31T23:59:59.4444Z"), }, "5": { - "0000-00-00T00:00:00.00000Z", - "1000-01-01T00:00:00.55555Z", - "1975-01-01T00:00:01.55555Z", - "1985-01-01T00:00:01.55555Z", - "1999-12-31T23:59:59.55555Z", - "2018-11-08T19:26:00.55555Z", - "2038-01-19T03:14:07.55555Z", - "9999-12-31T23:59:59.55555Z", + parseTime("0000-00-00T00:00:00.00000Z"), + parseTime("1000-01-01T00:00:00.55555Z"), + parseTime("1975-01-01T00:00:01.55555Z"), + parseTime("1985-01-01T00:00:01.55555Z"), + parseTime("1999-12-31T23:59:59.55555Z"), + parseTime("2018-11-08T19:26:00.55555Z"), + parseTime("2038-01-19T03:14:07.55555Z"), + parseTime("9999-12-31T23:59:59.55555Z"), }, "6": { - "0000-00-00T00:00:00.000000Z", - "1000-01-01T00:00:00.666666Z", - "1975-01-01T00:00:01.666666Z", - "1985-01-01T00:00:01.666666Z", - "1999-12-31T23:59:59.666666Z", - "2018-11-08T19:26:00.666666Z", - "2038-01-19T03:14:07.666666Z", - "9999-12-31T23:59:59.666666Z", + parseTime("0000-00-00T00:00:00.000000Z"), + parseTime("1000-01-01T00:00:00.666666Z"), + parseTime("1975-01-01T00:00:01.666666Z"), + parseTime("1985-01-01T00:00:01.666666Z"), + parseTime("1999-12-31T23:59:59.666666Z"), + parseTime("2018-11-08T19:26:00.666666Z"), + parseTime("2038-01-19T03:14:07.666666Z"), + parseTime("9999-12-31T23:59:59.666666Z"), }, } for length, vals := range inputs { @@ -159,10 +160,15 @@ func TestDatetime(t *testing.T) { defer tbl.drop(t) for _, v := range vals { - t.Run(v, func(t *testing.T) { + t.Run(v.Format(time.RFC3339Nano), func(t *testing.T) { suite.insertAndCompare(t, tbl, v) }) } }) } } + +func parseTime(str string) time.Time { + t, _ := time.Parse(str, time.RFC3339Nano) + return t +}