Skip to content

Commit 12508c8

Browse files
Code-Hexmethane
andauthored
utils: parse using byteslice in parseDateTime (#1113)
* 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 * added line to AUTHORS file * fixed error handling * fixed nanosec digits * added more tests for error * renamed parseByteDateTime to parseDateTime * reverted base null time * Update utils.go Co-authored-by: Inada Naoki <songofacandy@gmail.com> * Update utils.go Co-authored-by: Inada Naoki <songofacandy@gmail.com> * Update utils.go Co-authored-by: Inada Naoki <songofacandy@gmail.com> * removed deprecatedParseDateTime from test Co-authored-by: Inada Naoki <songofacandy@gmail.com>
1 parent d2e52fc commit 12508c8

File tree

5 files changed

+263
-46
lines changed

5 files changed

+263
-46
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Julien Schmidt <go-sql-driver at julienschmidt.com>
5656
Justin Li <jli at j-li.net>
5757
Justin Nuß <nuss.justin at gmail.com>
5858
Kamil Dziedzic <kamil at klecza.pl>
59+
Kei Kamikawa <x00.x7f.x86 at gmail.com>
5960
Kevin Malachowski <kevin at chowski.com>
6061
Kieron Woodhouse <kieron.woodhouse at infosum.com>
6162
Lennart Rudolph <lrudolph at hmc.edu>

nulltime.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ 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 = parseDateTime(v, time.UTC)
3232
nt.Valid = (err == nil)
3333
return
3434
case string:
35-
nt.Time, err = parseDateTime(v, time.UTC)
35+
nt.Time, err = parseDateTime([]byte(v), time.UTC)
3636
nt.Valid = (err == nil)
3737
return
3838
}

packets.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ func (rows *textRows) readRow(dest []driver.Value) error {
778778
case fieldTypeTimestamp, fieldTypeDateTime,
779779
fieldTypeDate, fieldTypeNewDate:
780780
dest[i], err = parseDateTime(
781-
string(dest[i].([]byte)),
781+
dest[i].([]byte),
782782
mc.cfg.Loc,
783783
)
784784
if err == nil {

utils.go

+125-10
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,136 @@ func readBool(input string) (value bool, valid bool) {
106106
* Time related utils *
107107
******************************************************************************/
108108

109-
func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
110-
base := "0000-00-00 00:00:00.0000000"
111-
switch len(str) {
109+
func parseDateTime(b []byte, loc *time.Location) (time.Time, error) {
110+
const base = "0000-00-00 00:00:00.000000"
111+
switch len(b) {
112112
case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
113-
if str == base[:len(str)] {
114-
return
113+
if string(b) == base[:len(b)] {
114+
return time.Time{}, nil
115115
}
116-
if loc == time.UTC {
117-
return time.Parse(timeFormat[:len(str)], str)
116+
117+
year, err := parseByteYear(b)
118+
if err != nil {
119+
return time.Time{}, err
120+
}
121+
if year <= 0 {
122+
year = 1
123+
}
124+
125+
if b[4] != '-' {
126+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[4])
127+
}
128+
129+
m, err := parseByte2Digits(b[5], b[6])
130+
if err != nil {
131+
return time.Time{}, err
132+
}
133+
if m <= 0 {
134+
m = 1
135+
}
136+
month := time.Month(m)
137+
138+
if b[7] != '-' {
139+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[7])
140+
}
141+
142+
day, err := parseByte2Digits(b[8], b[9])
143+
if err != nil {
144+
return time.Time{}, err
145+
}
146+
if day <= 0 {
147+
day = 1
148+
}
149+
if len(b) == 10 {
150+
return time.Date(year, month, day, 0, 0, 0, 0, loc), nil
151+
}
152+
153+
if b[10] != ' ' {
154+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[10])
155+
}
156+
157+
hour, err := parseByte2Digits(b[11], b[12])
158+
if err != nil {
159+
return time.Time{}, err
160+
}
161+
if b[13] != ':' {
162+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[13])
163+
}
164+
165+
min, err := parseByte2Digits(b[14], b[15])
166+
if err != nil {
167+
return time.Time{}, err
168+
}
169+
if b[16] != ':' {
170+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[16])
118171
}
119-
return time.ParseInLocation(timeFormat[:len(str)], str, loc)
172+
173+
sec, err := parseByte2Digits(b[17], b[18])
174+
if err != nil {
175+
return time.Time{}, err
176+
}
177+
if len(b) == 19 {
178+
return time.Date(year, month, day, hour, min, sec, 0, loc), nil
179+
}
180+
181+
if b[19] != '.' {
182+
return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[19])
183+
}
184+
nsec, err := parseByteNanoSec(b[20:])
185+
if err != nil {
186+
return time.Time{}, err
187+
}
188+
return time.Date(year, month, day, hour, min, sec, nsec, loc), nil
120189
default:
121-
err = fmt.Errorf("invalid time string: %s", str)
122-
return
190+
return time.Time{}, fmt.Errorf("invalid time bytes: %s", b)
191+
}
192+
}
193+
194+
func parseByteYear(b []byte) (int, error) {
195+
year, n := 0, 1000
196+
for i := 0; i < 4; i++ {
197+
v, err := bToi(b[i])
198+
if err != nil {
199+
return 0, err
200+
}
201+
year += v * n
202+
n = n / 10
203+
}
204+
return year, nil
205+
}
206+
207+
func parseByte2Digits(b1, b2 byte) (int, error) {
208+
d1, err := bToi(b1)
209+
if err != nil {
210+
return 0, err
211+
}
212+
d2, err := bToi(b2)
213+
if err != nil {
214+
return 0, err
215+
}
216+
return d1*10 + d2, nil
217+
}
218+
219+
func parseByteNanoSec(b []byte) (int, error) {
220+
ns, digit := 0, 100000 // max is 6-digits
221+
for i := 0; i < len(b); i++ {
222+
v, err := bToi(b[i])
223+
if err != nil {
224+
return 0, err
225+
}
226+
ns += v * digit
227+
digit /= 10
228+
}
229+
// nanoseconds has 10-digits. (needs to scale digits)
230+
// 10 - 6 = 4, so we have to multiple 1000.
231+
return ns * 1000, nil
232+
}
233+
234+
func bToi(b byte) (int, error) {
235+
if b < '0' || b > '9' {
236+
return 0, errors.New("not [0-9]")
123237
}
238+
return int(b - '0'), nil
124239
}
125240

126241
func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {

utils_test.go

+134-33
Original file line numberDiff line numberDiff line change
@@ -294,43 +294,144 @@ func TestIsolationLevelMapping(t *testing.T) {
294294
}
295295

296296
func TestParseDateTime(t *testing.T) {
297-
// UTC loc
298-
{
299-
str := "2020-05-13 21:30:45"
300-
t1, err := parseDateTime(str, time.UTC)
301-
if err != nil {
302-
t.Error(err)
303-
return
304-
}
305-
t2 := time.Date(2020, 5, 13,
306-
21, 30, 45, 0, time.UTC)
307-
if !t1.Equal(t2) {
308-
t.Errorf("want equal, have: %v, want: %v", t1, t2)
309-
return
310-
}
297+
cases := []struct {
298+
name string
299+
str string
300+
}{
301+
{
302+
name: "parse date",
303+
str: "2020-05-13",
304+
},
305+
{
306+
name: "parse null date",
307+
str: sDate0,
308+
},
309+
{
310+
name: "parse datetime",
311+
str: "2020-05-13 21:30:45",
312+
},
313+
{
314+
name: "parse null datetime",
315+
str: sDateTime0,
316+
},
317+
{
318+
name: "parse datetime nanosec 1-digit",
319+
str: "2020-05-25 23:22:01.1",
320+
},
321+
{
322+
name: "parse datetime nanosec 2-digits",
323+
str: "2020-05-25 23:22:01.15",
324+
},
325+
{
326+
name: "parse datetime nanosec 3-digits",
327+
str: "2020-05-25 23:22:01.159",
328+
},
329+
{
330+
name: "parse datetime nanosec 4-digits",
331+
str: "2020-05-25 23:22:01.1594",
332+
},
333+
{
334+
name: "parse datetime nanosec 5-digits",
335+
str: "2020-05-25 23:22:01.15949",
336+
},
337+
{
338+
name: "parse datetime nanosec 6-digits",
339+
str: "2020-05-25 23:22:01.159491",
340+
},
311341
}
312-
// non-UTC loc
313-
{
314-
str := "2020-05-13 21:30:45"
315-
loc := time.FixedZone("test", 8*60*60)
316-
t1, err := parseDateTime(str, loc)
317-
if err != nil {
318-
t.Error(err)
319-
return
320-
}
321-
t2 := time.Date(2020, 5, 13,
322-
21, 30, 45, 0, loc)
323-
if !t1.Equal(t2) {
324-
t.Errorf("want equal, have: %v, want: %v", t1, t2)
325-
return
342+
343+
for _, loc := range []*time.Location{
344+
time.UTC,
345+
time.FixedZone("test", 8*60*60),
346+
} {
347+
for _, cc := range cases {
348+
t.Run(cc.name+"-"+loc.String(), func(t *testing.T) {
349+
var want time.Time
350+
if cc.str != sDate0 && cc.str != sDateTime0 {
351+
var err error
352+
want, err = time.ParseInLocation(timeFormat[:len(cc.str)], cc.str, loc)
353+
if err != nil {
354+
t.Fatal(err)
355+
}
356+
}
357+
got, err := parseDateTime([]byte(cc.str), loc)
358+
if err != nil {
359+
t.Fatal(err)
360+
}
361+
362+
if !want.Equal(got) {
363+
t.Fatalf("want: %v, but got %v", want, got)
364+
}
365+
})
326366
}
327367
}
328368
}
329369

330-
func BenchmarkParseDateTime(b *testing.B) {
331-
str := "2020-05-13 21:30:45"
332-
loc := time.FixedZone("test", 8*60*60)
333-
for i := 0; i < b.N; i++ {
334-
_, _ = parseDateTime(str, loc)
370+
func TestParseDateTimeFail(t *testing.T) {
371+
cases := []struct {
372+
name string
373+
str string
374+
wantErr string
375+
}{
376+
{
377+
name: "parse invalid time",
378+
str: "hello",
379+
wantErr: "invalid time bytes: hello",
380+
},
381+
{
382+
name: "parse year",
383+
str: "000!-00-00 00:00:00.000000",
384+
wantErr: "not [0-9]",
385+
},
386+
{
387+
name: "parse month",
388+
str: "0000-!0-00 00:00:00.000000",
389+
wantErr: "not [0-9]",
390+
},
391+
{
392+
name: `parse "-" after parsed year`,
393+
str: "0000:00-00 00:00:00.000000",
394+
wantErr: "bad value for field: `:`",
395+
},
396+
{
397+
name: `parse "-" after parsed month`,
398+
str: "0000-00:00 00:00:00.000000",
399+
wantErr: "bad value for field: `:`",
400+
},
401+
{
402+
name: `parse " " after parsed date`,
403+
str: "0000-00-00+00:00:00.000000",
404+
wantErr: "bad value for field: `+`",
405+
},
406+
{
407+
name: `parse ":" after parsed date`,
408+
str: "0000-00-00 00-00:00.000000",
409+
wantErr: "bad value for field: `-`",
410+
},
411+
{
412+
name: `parse ":" after parsed hour`,
413+
str: "0000-00-00 00:00-00.000000",
414+
wantErr: "bad value for field: `-`",
415+
},
416+
{
417+
name: `parse "." after parsed sec`,
418+
str: "0000-00-00 00:00:00?000000",
419+
wantErr: "bad value for field: `?`",
420+
},
421+
}
422+
423+
for _, cc := range cases {
424+
t.Run(cc.name, func(t *testing.T) {
425+
got, err := parseDateTime([]byte(cc.str), time.UTC)
426+
if err == nil {
427+
t.Fatal("want error")
428+
}
429+
if cc.wantErr != err.Error() {
430+
t.Fatalf("want `%s`, but got `%s`", cc.wantErr, err)
431+
}
432+
if !got.IsZero() {
433+
t.Fatal("want zero time")
434+
}
435+
})
335436
}
336437
}

0 commit comments

Comments
 (0)