Skip to content

Commit ad990db

Browse files
committed
fixed the way of parsing datetime when byte slice string
The benchmark results $ go test -benchmem . -bench "^BenchmarkParseByte" goos: darwin goarch: amd64 pkg: github.com/go-sql-driver/mysql BenchmarkParseByteDateTime-4 12023173 104 ns/op 0 B/op 0 allocs/op BenchmarkParseByteDateTimeStringCast-4 3394239 355 ns/op 32 B/op 1 allocs/op
1 parent 8c3a2d9 commit ad990db

File tree

4 files changed

+230
-5
lines changed

4 files changed

+230
-5
lines changed

nulltime.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func (nt *NullTime) Scan(value interface{}) (err error) {
2828
nt.Time, nt.Valid = v, true
2929
return
3030
case []byte:
31-
nt.Time, err = parseDateTime(string(v), time.UTC)
31+
nt.Time, err = parseByteDateTime(v, time.UTC)
3232
nt.Valid = (err == nil)
3333
return
3434
case string:

packets.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -777,8 +777,8 @@ func (rows *textRows) readRow(dest []driver.Value) error {
777777
switch rows.rs.columns[i].fieldType {
778778
case fieldTypeTimestamp, fieldTypeDateTime,
779779
fieldTypeDate, fieldTypeNewDate:
780-
dest[i], err = parseDateTime(
781-
string(dest[i].([]byte)),
780+
dest[i], err = parseByteDateTime(
781+
dest[i].([]byte),
782782
mc.cfg.Loc,
783783
)
784784
if err == nil {

utils.go

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package mysql
1010

1111
import (
12+
"bytes"
1213
"crypto/tls"
1314
"database/sql"
1415
"database/sql/driver"
@@ -106,11 +107,15 @@ func readBool(input string) (value bool, valid bool) {
106107
* Time related utils *
107108
******************************************************************************/
108109

110+
var (
111+
nullTimeBaseStr = "0000-00-00 00:00:00.0000000"
112+
nullTimeBaseByte = []byte(nullTimeBaseStr)
113+
)
114+
109115
func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
110-
base := "0000-00-00 00:00:00.0000000"
111116
switch len(str) {
112117
case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
113-
if str == base[:len(str)] {
118+
if str == nullTimeBaseStr[:len(str)] {
114119
return
115120
}
116121
if loc == time.UTC {
@@ -123,6 +128,138 @@ func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
123128
}
124129
}
125130

131+
func parseByteDateTime(b []byte, loc *time.Location) (time.Time, error) {
132+
switch len(b) {
133+
case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
134+
if bytes.Compare(b, nullTimeBaseByte[:len(b)]) == 0 {
135+
return time.Time{}, nil
136+
}
137+
138+
year, err := parseByteYear(b)
139+
if err != nil {
140+
return time.Time{}, err
141+
}
142+
if year <= 0 {
143+
year = 1
144+
}
145+
146+
if b[4] != '-' {
147+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[4])
148+
}
149+
150+
m, err := parseByte2Digits(b, 5)
151+
if err != nil {
152+
return time.Time{}, err
153+
}
154+
if m <= 0 {
155+
m = 1
156+
}
157+
month := time.Month(m)
158+
159+
if b[7] != '-' {
160+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[7])
161+
}
162+
163+
day, err := parseByte2Digits(b, 8)
164+
if err != nil {
165+
return time.Time{}, err
166+
}
167+
if day <= 0 {
168+
day = 1
169+
}
170+
if len(b) == 10 {
171+
return time.Date(year, month, day, 0, 0, 0, 0, loc), nil
172+
}
173+
174+
if b[10] != ' ' {
175+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[10])
176+
}
177+
178+
hour, err := parseByte2Digits(b, 11)
179+
if err != nil {
180+
return time.Time{}, err
181+
}
182+
if b[13] != ':' {
183+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[13])
184+
}
185+
186+
min, err := parseByte2Digits(b, 14)
187+
if err != nil {
188+
return time.Time{}, err
189+
}
190+
if b[16] != ':' {
191+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[16])
192+
}
193+
194+
sec, err := parseByte2Digits(b, 17)
195+
if err != nil {
196+
return time.Time{}, err
197+
}
198+
if len(b) == 19 {
199+
return time.Date(year, month, day, hour, min, sec, 0, loc), nil
200+
}
201+
202+
if b[19] != '.' {
203+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[19])
204+
}
205+
nsec, err := parseByteNanoSec(b)
206+
if err != nil {
207+
return time.Time{}, err
208+
}
209+
return time.Date(year, month, day, hour, min, sec, nsec, loc), nil
210+
default:
211+
return time.Time{}, fmt.Errorf("invalid time bytes: %s", b)
212+
}
213+
}
214+
215+
func parseByteYear(b []byte) (int, error) {
216+
year, n := 0, 1000
217+
for i := 0; i < 4; i++ {
218+
v, err := bToi(b[i])
219+
if err != nil {
220+
return 0, fmt.Errorf("invalid time bytes: %s", b)
221+
}
222+
year += v * n
223+
n = n / 10
224+
}
225+
return year, nil
226+
}
227+
228+
func parseByte2Digits(b []byte, index int) (int, error) {
229+
d2, err := bToi(b[index])
230+
if err != nil {
231+
return 0, fmt.Errorf("invalid time bytes: %s", b)
232+
}
233+
d1, err := bToi(b[index+1])
234+
if err != nil {
235+
return 0, fmt.Errorf("invalid time bytes: %s", b)
236+
}
237+
return d2*10 + d1, nil
238+
}
239+
240+
func parseByteNanoSec(b []byte) (int, error) {
241+
l := len(b)
242+
ns, digit := 0, 100000 // max is 6-digits
243+
for i := 20; i < l; i++ {
244+
v, err := bToi(b[i])
245+
if err != nil {
246+
return 0, err
247+
}
248+
ns += v * digit
249+
digit /= 10
250+
}
251+
// nanoseconds has 10-digits.
252+
// 10 - 6 = 4, so we have to multiple 1000.
253+
return ns * 1000, nil
254+
}
255+
256+
func bToi(b byte) (int, error) {
257+
if b < '0' || b > '9' {
258+
return 0, errors.New("not [0-9]")
259+
}
260+
return int(b - '0'), nil
261+
}
262+
126263
func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {
127264
switch num {
128265
case 0:

utils_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,10 +327,98 @@ func TestParseDateTime(t *testing.T) {
327327
}
328328
}
329329

330+
func TestParseByteDateTime(t *testing.T) {
331+
cases := []struct {
332+
name string
333+
str string
334+
}{
335+
{
336+
name: "parse date",
337+
str: "2020-05-13",
338+
},
339+
{
340+
name: "parse null date",
341+
str: sDate0,
342+
},
343+
{
344+
name: "parse datetime",
345+
str: "2020-05-13 21:30:45",
346+
},
347+
{
348+
name: "parse null datetime",
349+
str: sDateTime0,
350+
},
351+
{
352+
name: "parse datetime nanosec 1-digit",
353+
str: "2020-05-25 23:22:01.1",
354+
},
355+
{
356+
name: "parse datetime nanosec 2-digits",
357+
str: "2020-05-25 23:22:01.15",
358+
},
359+
{
360+
name: "parse datetime nanosec 3-digits",
361+
str: "2020-05-25 23:22:01.159",
362+
},
363+
{
364+
name: "parse datetime nanosec 4-digits",
365+
str: "2020-05-25 23:22:01.1594",
366+
},
367+
{
368+
name: "parse datetime nanosec 5-digits",
369+
str: "2020-05-25 23:22:01.15949",
370+
},
371+
{
372+
name: "parse datetime nanosec 6-digits",
373+
str: "2020-05-25 23:22:01.159491",
374+
},
375+
}
376+
377+
for _, loc := range []*time.Location{
378+
time.UTC,
379+
time.FixedZone("test", 8*60*60),
380+
} {
381+
for _, cc := range cases {
382+
t.Run(cc.name+"-"+loc.String(), func(t *testing.T) {
383+
want, err := parseDateTime(cc.str, loc)
384+
if err != nil {
385+
t.Fatal(err)
386+
}
387+
got, err := parseByteDateTime([]byte(cc.str), loc)
388+
if err != nil {
389+
t.Fatal(err)
390+
}
391+
392+
if !want.Equal(got) {
393+
t.Fatalf("want: %v, but got %v", want, got)
394+
}
395+
})
396+
}
397+
}
398+
}
399+
330400
func BenchmarkParseDateTime(b *testing.B) {
331401
str := "2020-05-13 21:30:45"
332402
loc := time.FixedZone("test", 8*60*60)
333403
for i := 0; i < b.N; i++ {
334404
_, _ = parseDateTime(str, loc)
335405
}
336406
}
407+
408+
func BenchmarkParseByteDateTime(b *testing.B) {
409+
bStr := []byte("2020-05-25 23:22:01.159491")
410+
loc := time.FixedZone("test", 8*60*60)
411+
b.ResetTimer()
412+
for i := 0; i < b.N; i++ {
413+
_, _ = parseByteDateTime(bStr, loc)
414+
}
415+
}
416+
417+
func BenchmarkParseByteDateTimeStringCast(b *testing.B) {
418+
bStr := []byte("2020-05-25 23:22:01.159491")
419+
loc := time.FixedZone("test", 8*60*60)
420+
b.ResetTimer()
421+
for i := 0; i < b.N; i++ {
422+
_, _ = parseDateTime(string(bStr), loc)
423+
}
424+
}

0 commit comments

Comments
 (0)