From fe07a17dc6eafe7e89f5b343ce0767fdf19126d4 Mon Sep 17 00:00:00 2001 From: inoth Date: Fri, 6 Dec 2024 15:21:41 +0800 Subject: [PATCH 1/6] feat: Added support for datetime format --- mysql/resultset_helper.go | 49 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/mysql/resultset_helper.go b/mysql/resultset_helper.go index 36c0f0a3b..539d40289 100644 --- a/mysql/resultset_helper.go +++ b/mysql/resultset_helper.go @@ -1,13 +1,54 @@ package mysql import ( + "bytes" + "encoding/binary" "math" "strconv" + "time" "github.com/pingcap/errors" "github.com/siddontang/go/hack" ) +func toBinaryDateTime(t time.Time) ([]byte, error) { + var buf bytes.Buffer + + if t.IsZero() { + return nil, nil + } + + year, month, day := t.Year(), t.Month(), t.Day() + hour, min, sec := t.Hour(), t.Minute(), t.Second() + nanosec := t.Nanosecond() + + if nanosec > 0 { + buf.WriteByte(byte(11)) + binary.Write(&buf, binary.LittleEndian, uint16(year)) + buf.WriteByte(byte(month)) + buf.WriteByte(byte(day)) + buf.WriteByte(byte(hour)) + buf.WriteByte(byte(min)) + buf.WriteByte(byte(sec)) + binary.Write(&buf, binary.LittleEndian, uint32(nanosec/1000)) + } else if hour > 0 || min > 0 || sec > 0 { + buf.WriteByte(byte(7)) + binary.Write(&buf, binary.LittleEndian, uint16(year)) + buf.WriteByte(byte(month)) + buf.WriteByte(byte(day)) + buf.WriteByte(byte(hour)) + buf.WriteByte(byte(min)) + buf.WriteByte(byte(sec)) + } else { + buf.WriteByte(byte(4)) + binary.Write(&buf, binary.LittleEndian, uint16(year)) + buf.WriteByte(byte(month)) + buf.WriteByte(byte(day)) + } + + return buf.Bytes(), nil +} + func FormatTextValue(value interface{}) ([]byte, error) { switch v := value.(type) { case int8: @@ -38,6 +79,8 @@ func FormatTextValue(value interface{}) ([]byte, error) { return v, nil case string: return hack.Slice(v), nil + case time.Time: + return hack.Slice(v.Format(time.DateTime)), nil case nil: return nil, nil default: @@ -75,6 +118,8 @@ func formatBinaryValue(value interface{}) ([]byte, error) { return v, nil case string: return hack.Slice(v), nil + case time.Time: + return toBinaryDateTime(v) default: return nil, errors.Errorf("invalid type %T", value) } @@ -90,6 +135,8 @@ func fieldType(value interface{}) (typ uint8, err error) { typ = MYSQL_TYPE_DOUBLE case string, []byte: typ = MYSQL_TYPE_VAR_STRING + case time.Time: + typ = MYSQL_TYPE_DATETIME case nil: typ = MYSQL_TYPE_NULL default: @@ -109,7 +156,7 @@ func formatField(field *Field, value interface{}) error { case float32, float64: field.Charset = 63 field.Flag = BINARY_FLAG | NOT_NULL_FLAG - case string, []byte: + case string, []byte, time.Time: field.Charset = 33 case nil: field.Charset = 33 From aa48110a611838be6e3a3a642a8e2cefa9ba05fa Mon Sep 17 00:00:00 2001 From: inoth Date: Thu, 12 Dec 2024 11:24:58 +0800 Subject: [PATCH 2/6] fix: handler "Error return value of `binary.Write` is not checked" --- mysql/resultset_helper.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mysql/resultset_helper.go b/mysql/resultset_helper.go index 539d40289..cc59f04de 100644 --- a/mysql/resultset_helper.go +++ b/mysql/resultset_helper.go @@ -24,16 +24,16 @@ func toBinaryDateTime(t time.Time) ([]byte, error) { if nanosec > 0 { buf.WriteByte(byte(11)) - binary.Write(&buf, binary.LittleEndian, uint16(year)) + _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) buf.WriteByte(byte(month)) buf.WriteByte(byte(day)) buf.WriteByte(byte(hour)) buf.WriteByte(byte(min)) buf.WriteByte(byte(sec)) - binary.Write(&buf, binary.LittleEndian, uint32(nanosec/1000)) + _ = binary.Write(&buf, binary.LittleEndian, uint32(nanosec/1000)) } else if hour > 0 || min > 0 || sec > 0 { buf.WriteByte(byte(7)) - binary.Write(&buf, binary.LittleEndian, uint16(year)) + _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) buf.WriteByte(byte(month)) buf.WriteByte(byte(day)) buf.WriteByte(byte(hour)) @@ -41,7 +41,7 @@ func toBinaryDateTime(t time.Time) ([]byte, error) { buf.WriteByte(byte(sec)) } else { buf.WriteByte(byte(4)) - binary.Write(&buf, binary.LittleEndian, uint16(year)) + _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) buf.WriteByte(byte(month)) buf.WriteByte(byte(day)) } From 6bc2416ee2cf01967f5f7bc5d29c9d81ef6416d1 Mon Sep 17 00:00:00 2001 From: inoth Date: Thu, 19 Dec 2024 18:44:30 +0800 Subject: [PATCH 3/6] fix: add toBinaryDateTime test --- mysql/resultset_helper.go | 76 +++++++++++++++---------------- mysql/util_test.go | 94 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 38 deletions(-) diff --git a/mysql/resultset_helper.go b/mysql/resultset_helper.go index cc59f04de..5009a69d8 100644 --- a/mysql/resultset_helper.go +++ b/mysql/resultset_helper.go @@ -11,44 +11,6 @@ import ( "github.com/siddontang/go/hack" ) -func toBinaryDateTime(t time.Time) ([]byte, error) { - var buf bytes.Buffer - - if t.IsZero() { - return nil, nil - } - - year, month, day := t.Year(), t.Month(), t.Day() - hour, min, sec := t.Hour(), t.Minute(), t.Second() - nanosec := t.Nanosecond() - - if nanosec > 0 { - buf.WriteByte(byte(11)) - _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) - buf.WriteByte(byte(month)) - buf.WriteByte(byte(day)) - buf.WriteByte(byte(hour)) - buf.WriteByte(byte(min)) - buf.WriteByte(byte(sec)) - _ = binary.Write(&buf, binary.LittleEndian, uint32(nanosec/1000)) - } else if hour > 0 || min > 0 || sec > 0 { - buf.WriteByte(byte(7)) - _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) - buf.WriteByte(byte(month)) - buf.WriteByte(byte(day)) - buf.WriteByte(byte(hour)) - buf.WriteByte(byte(min)) - buf.WriteByte(byte(sec)) - } else { - buf.WriteByte(byte(4)) - _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) - buf.WriteByte(byte(month)) - buf.WriteByte(byte(day)) - } - - return buf.Bytes(), nil -} - func FormatTextValue(value interface{}) ([]byte, error) { switch v := value.(type) { case int8: @@ -88,6 +50,44 @@ func FormatTextValue(value interface{}) ([]byte, error) { } } +func toBinaryDateTime(t time.Time) ([]byte, error) { + var buf bytes.Buffer + + if t.IsZero() { + return nil, nil + } + + year, month, day := t.Year(), t.Month(), t.Day() + hour, min, sec := t.Hour(), t.Minute(), t.Second() + nanosec := t.Nanosecond() + + if nanosec > 0 { + buf.WriteByte(byte(11)) + _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) + buf.WriteByte(byte(month)) + buf.WriteByte(byte(day)) + buf.WriteByte(byte(hour)) + buf.WriteByte(byte(min)) + buf.WriteByte(byte(sec)) + _ = binary.Write(&buf, binary.LittleEndian, uint32(nanosec/1000)) + } else if hour > 0 || min > 0 || sec > 0 { + buf.WriteByte(byte(7)) + _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) + buf.WriteByte(byte(month)) + buf.WriteByte(byte(day)) + buf.WriteByte(byte(hour)) + buf.WriteByte(byte(min)) + buf.WriteByte(byte(sec)) + } else { + buf.WriteByte(byte(4)) + _ = binary.Write(&buf, binary.LittleEndian, uint16(year)) + buf.WriteByte(byte(month)) + buf.WriteByte(byte(day)) + } + + return buf.Bytes(), nil +} + func formatBinaryValue(value interface{}) ([]byte, error) { switch v := value.(type) { case int8: diff --git a/mysql/util_test.go b/mysql/util_test.go index 22fbe39e4..345817f47 100644 --- a/mysql/util_test.go +++ b/mysql/util_test.go @@ -1,7 +1,11 @@ package mysql import ( + "encoding/binary" + "fmt" + "strings" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -51,3 +55,93 @@ func TestFormatBinaryTime(t *testing.T) { require.Equal(t, test.Expect, string(got), "test case %v", test.Data) } } + +// mysql driver parse binary datetime +func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (time.Time, error) { + switch num { + case 0: + return time.Time{}, nil + case 4: + return time.Date( + int(binary.LittleEndian.Uint16(data[:2])), // year + time.Month(data[2]), // month + int(data[3]), // day + 0, 0, 0, 0, + loc, + ), nil + case 7: + return time.Date( + int(binary.LittleEndian.Uint16(data[:2])), // year + time.Month(data[2]), // month + int(data[3]), // day + int(data[4]), // hour + int(data[5]), // minutes + int(data[6]), // seconds + 0, + loc, + ), nil + case 11: + return time.Date( + int(binary.LittleEndian.Uint16(data[:2])), // year + time.Month(data[2]), // month + int(data[3]), // day + int(data[4]), // hour + int(data[5]), // minutes + int(data[6]), // seconds + int(binary.LittleEndian.Uint32(data[7:11]))*1000, // nanoseconds + loc, + ), nil + } + return time.Time{}, fmt.Errorf("invalid DATETIME packet length %d", num) +} + +func TestToBinaryDateTime(t *testing.T) { + tests := []struct { + name string + input time.Time + expected string + }{ + { + name: "Zero time", + input: time.Time{}, + expected: "", + }, + { + name: "Date with nanoseconds", + input: time.Date(2023, 10, 10, 10, 10, 10, 123456000, time.UTC), + expected: "2023-10-10 10:10:10.123456 +0000 UTC", + }, + { + name: "Date with time", + input: time.Date(2023, 10, 10, 10, 10, 10, 0, time.UTC), + expected: "2023-10-10 10:10:10 +0000 UTC", + }, + { + name: "Date only", + input: time.Date(2023, 10, 10, 0, 0, 0, 0, time.UTC), + expected: "2023-10-10 00:00:00 +0000 UTC", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := toBinaryDateTime(tt.input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result) == 0 { + return + } + num := uint64(result[0]) + data := result[1:] + date, err := parseBinaryDateTime(num, data, time.UTC) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !strings.EqualFold(date.String(), tt.expected) { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} From f0ca5b72606dd25bd36fd885d67b4513d1acf10a Mon Sep 17 00:00:00 2001 From: inoth Date: Wed, 25 Dec 2024 16:01:32 +0800 Subject: [PATCH 4/6] fix: update TestToBinaryDateTime --- mysql/resultset_helper.go | 5 +- mysql/util_test.go | 114 ++++++++++++++------------------------ 2 files changed, 45 insertions(+), 74 deletions(-) diff --git a/mysql/resultset_helper.go b/mysql/resultset_helper.go index baf0a037f..fee929506 100644 --- a/mysql/resultset_helper.go +++ b/mysql/resultset_helper.go @@ -3,6 +3,7 @@ package mysql import ( "bytes" "encoding/binary" + "fmt" "math" "strconv" "time" @@ -43,7 +44,7 @@ func FormatTextValue(value interface{}) ([]byte, error) { case string: return utils.StringToByteSlice(v), nil case time.Time: - return hack.Slice(v.Format(time.DateTime)), nil + return utils.StringToByteSlice(v.Format(time.DateTime)), nil case nil: return nil, nil default: @@ -55,7 +56,7 @@ func toBinaryDateTime(t time.Time) ([]byte, error) { var buf bytes.Buffer if t.IsZero() { - return nil, nil + return nil, fmt.Errorf("zero time") } year, month, day := t.Year(), t.Month(), t.Day() diff --git a/mysql/util_test.go b/mysql/util_test.go index 345817f47..9d326e768 100644 --- a/mysql/util_test.go +++ b/mysql/util_test.go @@ -1,9 +1,6 @@ package mysql import ( - "encoding/binary" - "fmt" - "strings" "testing" "time" @@ -56,92 +53,65 @@ func TestFormatBinaryTime(t *testing.T) { } } -// mysql driver parse binary datetime -func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (time.Time, error) { - switch num { - case 0: - return time.Time{}, nil - case 4: - return time.Date( - int(binary.LittleEndian.Uint16(data[:2])), // year - time.Month(data[2]), // month - int(data[3]), // day - 0, 0, 0, 0, - loc, - ), nil - case 7: - return time.Date( - int(binary.LittleEndian.Uint16(data[:2])), // year - time.Month(data[2]), // month - int(data[3]), // day - int(data[4]), // hour - int(data[5]), // minutes - int(data[6]), // seconds - 0, - loc, - ), nil - case 11: - return time.Date( - int(binary.LittleEndian.Uint16(data[:2])), // year - time.Month(data[2]), // month - int(data[3]), // day - int(data[4]), // hour - int(data[5]), // minutes - int(data[6]), // seconds - int(binary.LittleEndian.Uint32(data[7:11]))*1000, // nanoseconds - loc, - ), nil - } - return time.Time{}, fmt.Errorf("invalid DATETIME packet length %d", num) -} - func TestToBinaryDateTime(t *testing.T) { + var ( + DateTimeNano = "2006-01-02 15:04:05.000000" + formatBinaryDateTime = func(n int, data []byte) string { + date, err := FormatBinaryDateTime(n, data) + if err != nil { + return "" + } + return string(date) + } + ) + tests := []struct { - name string - input time.Time - expected string + Name string + Data time.Time + Expect func(n int, data []byte) string + Error bool }{ { - name: "Zero time", - input: time.Time{}, - expected: "", + Name: "Zero time", + Data: time.Time{}, + Expect: nil, + Error: true, }, { - name: "Date with nanoseconds", - input: time.Date(2023, 10, 10, 10, 10, 10, 123456000, time.UTC), - expected: "2023-10-10 10:10:10.123456 +0000 UTC", + Name: "Date with nanoseconds", + Data: time.Date(2023, 10, 10, 10, 10, 10, 123456000, time.UTC), + Expect: formatBinaryDateTime, }, { - name: "Date with time", - input: time.Date(2023, 10, 10, 10, 10, 10, 0, time.UTC), - expected: "2023-10-10 10:10:10 +0000 UTC", + Name: "Date with time", + Data: time.Date(2023, 10, 10, 10, 10, 10, 0, time.UTC), + Expect: formatBinaryDateTime, }, { - name: "Date only", - input: time.Date(2023, 10, 10, 0, 0, 0, 0, time.UTC), - expected: "2023-10-10 00:00:00 +0000 UTC", + Name: "Date only", + Data: time.Date(2023, 10, 10, 0, 0, 0, 0, time.UTC), + Expect: formatBinaryDateTime, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := toBinaryDateTime(tt.input) - if err != nil { - t.Fatalf("unexpected error: %v", err) + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + got, err := toBinaryDateTime(test.Data) + if test.Error { + require.Error(t, err) + } else { + require.NoError(t, err) } - if len(result) == 0 { + if len(got) == 0 { return } - num := uint64(result[0]) - data := result[1:] - date, err := parseBinaryDateTime(num, data, time.UTC) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if !strings.EqualFold(date.String(), tt.expected) { - t.Errorf("expected %v, got %v", tt.expected, result) + tmp := test.Expect(int(got[0]), got[1:]) + if int(got[0]) < 11 { + require.Equal(t, tmp, test.Data.Format(time.DateTime), "test case %v", test.Data.String()) + } else { + require.Equal(t, tmp, test.Data.Format(DateTimeNano), "test case %v", test.Data.String()) } }) } + } From d5006ded2056dc47a745d05e7da5ecd67ed3e4d0 Mon Sep 17 00:00:00 2001 From: inoth Date: Wed, 25 Dec 2024 17:18:51 +0800 Subject: [PATCH 5/6] fix: allow zero time --- mysql/resultset_helper.go | 3 +-- mysql/util_test.go | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/mysql/resultset_helper.go b/mysql/resultset_helper.go index fee929506..21487afdd 100644 --- a/mysql/resultset_helper.go +++ b/mysql/resultset_helper.go @@ -3,7 +3,6 @@ package mysql import ( "bytes" "encoding/binary" - "fmt" "math" "strconv" "time" @@ -56,7 +55,7 @@ func toBinaryDateTime(t time.Time) ([]byte, error) { var buf bytes.Buffer if t.IsZero() { - return nil, fmt.Errorf("zero time") + return nil, nil } year, month, day := t.Year(), t.Month(), t.Day() diff --git a/mysql/util_test.go b/mysql/util_test.go index 9d326e768..9ad0fa118 100644 --- a/mysql/util_test.go +++ b/mysql/util_test.go @@ -75,7 +75,6 @@ func TestToBinaryDateTime(t *testing.T) { Name: "Zero time", Data: time.Time{}, Expect: nil, - Error: true, }, { Name: "Date with nanoseconds", From 571ae6abf04a6cd1e53c14a5b3f8d1901ad72ed7 Mon Sep 17 00:00:00 2001 From: inoth Date: Wed, 25 Dec 2024 17:23:18 +0800 Subject: [PATCH 6/6] fix: code format --- mysql/util_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/mysql/util_test.go b/mysql/util_test.go index 9ad0fa118..175e907e4 100644 --- a/mysql/util_test.go +++ b/mysql/util_test.go @@ -112,5 +112,4 @@ func TestToBinaryDateTime(t *testing.T) { } }) } - }