Skip to content

Commit 45070de

Browse files
authored
mysql/datetime: Improve TIME parsing logic (#15135)
Signed-off-by: Dirkjan Bussink <d.bussink@gmail.com>
1 parent 2c7b647 commit 45070de

File tree

10 files changed

+150
-99
lines changed

10 files changed

+150
-99
lines changed

go/mysql/datetime/datetime.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,12 @@ func (d Date) YearWeek(mode int) int {
368368
case 1, 3:
369369
year, week := d.ISOWeek()
370370
return year*100 + week
371-
case 4, 5, 6, 7:
372-
// TODO
373-
return 0
371+
case 4, 6:
372+
year, week := d.Sunday4DayWeek()
373+
return year*100 + week
374+
case 5, 7:
375+
year, week := d.MondayWeek()
376+
return year*100 + week
374377
default:
375378
return d.YearWeek(DefaultWeekMode)
376379
}

go/mysql/datetime/parse.go

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,57 +24,64 @@ import (
2424
"vitess.io/vitess/go/mysql/fastparse"
2525
)
2626

27-
func parsetimeHours(tp *timeparts, in string) (out string, ok bool) {
27+
func parsetimeHours(tp *timeparts, in string) (string, TimeState) {
28+
var ok bool
2829
if tp.hour, in, ok = getnumn(in); ok {
2930
tp.day = tp.day + tp.hour/24
3031
tp.hour = tp.hour % 24
3132

3233
switch {
3334
case len(in) == 0:
34-
return "", true
35+
return "", TimeOK
3536
case in[0] == ':':
3637
return parsetimeMinutes(tp, in[1:])
3738
}
3839
}
39-
return "", false
40+
return "", TimePartial
4041
}
4142

42-
func parsetimeMinutes(tp *timeparts, in string) (out string, ok bool) {
43+
func parsetimeMinutes(tp *timeparts, in string) (string, TimeState) {
44+
var ok bool
4345
if tp.min, in, ok = getnum(in, false); ok {
4446
switch {
4547
case tp.min > 59:
46-
return "", false
48+
return "", TimeInvalid
4749
case len(in) == 0:
48-
return "", true
50+
return "", TimeOK
4951
case in[0] == ':':
5052
return parsetimeSeconds(tp, in[1:])
5153
}
5254
}
53-
return "", false
55+
return "", TimePartial
5456
}
5557

56-
func parsetimeSeconds(tp *timeparts, in string) (out string, ok bool) {
58+
func parsetimeSeconds(tp *timeparts, in string) (string, TimeState) {
59+
var ok bool
5760
if tp.sec, in, ok = getnum(in, false); ok {
5861
switch {
5962
case tp.sec > 59:
60-
return "", false
63+
return "", TimeInvalid
6164
case len(in) == 0:
62-
return "", true
65+
return "", TimeOK
6366
case len(in) > 1 && in[0] == '.':
6467
n := 1
6568
for ; n < len(in) && isDigit(in, n); n++ {
6669
}
6770
var l int
6871
tp.nsec, l, ok = parseNanoseconds(in, n)
6972
tp.prec = uint8(l)
70-
return "", ok && len(in) == n
73+
if ok && len(in) == n {
74+
return "", TimeOK
75+
}
76+
return "", TimePartial
7177
}
7278
}
73-
return "", false
79+
return "", TimePartial
7480
}
7581

76-
func parsetimeAny(tp *timeparts, in string) (out string, ok bool) {
82+
func parsetimeAny(tp *timeparts, in string) (out string, state TimeState) {
7783
orig := in
84+
var ok bool
7885
for i := 0; i < len(in); i++ {
7986
switch r := in[i]; {
8087
case isSpace(r):
@@ -91,7 +98,7 @@ func parsetimeAny(tp *timeparts, in string) (out string, ok bool) {
9198
return parsetimeNoDelimiters(tp, orig)
9299
}
93100
if tp.day > 34 {
94-
return "", clampTimeparts(tp)
101+
return "", clampTimeparts(tp, state)
95102
}
96103
return parsetimeHours(tp, in)
97104
case r == ':':
@@ -101,8 +108,9 @@ func parsetimeAny(tp *timeparts, in string) (out string, ok bool) {
101108
return parsetimeNoDelimiters(tp, in)
102109
}
103110

104-
func parsetimeNoDelimiters(tp *timeparts, in string) (out string, ok bool) {
111+
func parsetimeNoDelimiters(tp *timeparts, in string) (out string, state TimeState) {
105112
var integral int
113+
var ok bool
106114
for ; integral < len(in); integral++ {
107115
if in[integral] == '.' || !isDigit(in, integral) {
108116
break
@@ -112,12 +120,9 @@ func parsetimeNoDelimiters(tp *timeparts, in string) (out string, ok bool) {
112120
switch integral {
113121
default:
114122
// MySQL limits this to a numeric value that fits in a 32-bit unsigned integer.
115-
i, _ := fastparse.ParseInt64(in[:integral], 10)
123+
i, _ := fastparse.ParseUint64(in[:integral], 10)
116124
if i > math.MaxUint32 {
117-
return "", false
118-
}
119-
if i < -math.MaxUint32 {
120-
return "", false
125+
return "", TimeInvalid
121126
}
122127

123128
tp.hour, in, ok = getnuml(in, integral-4)
@@ -132,18 +137,18 @@ func parsetimeNoDelimiters(tp *timeparts, in string) (out string, ok bool) {
132137
case 3, 4:
133138
tp.min, in, ok = getnuml(in, integral-2)
134139
if !ok || tp.min > 59 {
135-
return "", false
140+
return "", TimeInvalid
136141
}
137142
integral = 2
138143
fallthrough
139144

140145
case 1, 2:
141146
tp.sec, in, ok = getnuml(in, integral)
142147
if !ok || tp.sec > 59 {
143-
return "", false
148+
return "", TimeInvalid
144149
}
145150
case 0:
146-
return "", false
151+
return "", TimeInvalid
147152
}
148153

149154
if len(in) > 1 && in[0] == '.' && isDigit(in, 1) {
@@ -152,14 +157,18 @@ func parsetimeNoDelimiters(tp *timeparts, in string) (out string, ok bool) {
152157
}
153158
var l int
154159
tp.nsec, l, ok = parseNanoseconds(in, n)
160+
if !ok {
161+
state = TimeInvalid
162+
}
155163
tp.prec = uint8(l)
156164
in = in[n:]
157165
}
158166

159-
return in, clampTimeparts(tp) && ok
167+
state = clampTimeparts(tp, state)
168+
return in, state
160169
}
161170

162-
func clampTimeparts(tp *timeparts) bool {
171+
func clampTimeparts(tp *timeparts, state TimeState) TimeState {
163172
// Maximum time is 838:59:59, so we have to clamp
164173
// it to that value here if we otherwise successfully
165174
// parser the time.
@@ -168,15 +177,31 @@ func clampTimeparts(tp *timeparts) bool {
168177
tp.hour = 22
169178
tp.min = 59
170179
tp.sec = 59
171-
return false
180+
if state == TimeOK {
181+
return TimePartial
182+
}
172183
}
173-
return true
184+
return state
174185
}
175186

176-
func ParseTime(in string, prec int) (t Time, l int, ok bool) {
187+
type TimeState uint8
188+
189+
const (
190+
// TimeOK indicates that the parsed value is valid and complete.
191+
TimeOK TimeState = iota
192+
// TimePartial indicates that the parsed value has a partially parsed value
193+
// but it is not fully complete and valid. There could be additional stray
194+
// data in the input, or it has an overflow.
195+
TimePartial
196+
// TimeInvalid indicates that the parsed value is invalid and no partial
197+
// TIME value could be extracted from the input.
198+
TimeInvalid
199+
)
200+
201+
func ParseTime(in string, prec int) (t Time, l int, state TimeState) {
177202
in = strings.Trim(in, " \t\r\n")
178203
if len(in) == 0 {
179-
return Time{}, 0, false
204+
return Time{}, 0, TimeInvalid
180205
}
181206
var neg bool
182207
if in[0] == '-' {
@@ -185,11 +210,15 @@ func ParseTime(in string, prec int) (t Time, l int, ok bool) {
185210
}
186211

187212
var tp timeparts
188-
in, ok = parsetimeAny(&tp, in)
189-
ok = clampTimeparts(&tp) && ok
213+
in, state = parsetimeAny(&tp, in)
214+
if state == TimeInvalid {
215+
return Time{}, 0, state
216+
}
217+
218+
state = clampTimeparts(&tp, state)
190219

191220
hours := uint16(24*tp.day + tp.hour)
192-
if !tp.isZero() && neg {
221+
if neg {
193222
hours |= negMask
194223
}
195224

@@ -206,7 +235,13 @@ func ParseTime(in string, prec int) (t Time, l int, ok bool) {
206235
t = t.Round(prec)
207236
}
208237

209-
return t, prec, ok && len(in) == 0
238+
switch {
239+
case state == TimeOK && len(in) == 0:
240+
state = TimeOK
241+
case state == TimeOK && len(in) > 0:
242+
state = TimePartial
243+
}
244+
return t, prec, state
210245
}
211246

212247
func ParseDate(s string) (Date, bool) {

go/mysql/datetime/parse_test.go

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -96,32 +96,33 @@ func TestParseTime(t *testing.T) {
9696
output testTime
9797
norm string
9898
l int
99-
err bool
99+
state TimeState
100100
}{
101101
{input: "00:00:00", norm: "00:00:00.000000", output: testTime{}},
102-
{input: "00:00:00foo", norm: "00:00:00.000000", output: testTime{}, err: true},
102+
{input: "-00:00:00", norm: "-00:00:00.000000", output: testTime{negative: true}},
103+
{input: "00:00:00foo", norm: "00:00:00.000000", output: testTime{}, state: TimePartial},
103104
{input: "11:12:13", norm: "11:12:13.000000", output: testTime{11, 12, 13, 0, false}},
104-
{input: "11:12:13foo", norm: "11:12:13.000000", output: testTime{11, 12, 13, 0, false}, err: true},
105+
{input: "11:12:13foo", norm: "11:12:13.000000", output: testTime{11, 12, 13, 0, false}, state: TimePartial},
105106
{input: "11:12:13.1", norm: "11:12:13.100000", output: testTime{11, 12, 13, 100000000, false}, l: 1},
106-
{input: "11:12:13.foo", norm: "11:12:13.000000", output: testTime{11, 12, 13, 0, false}, err: true},
107-
{input: "11:12:13.1foo", norm: "11:12:13.100000", output: testTime{11, 12, 13, 100000000, false}, l: 1, err: true},
107+
{input: "11:12:13.foo", norm: "11:12:13.000000", output: testTime{11, 12, 13, 0, false}, state: TimePartial},
108+
{input: "11:12:13.1foo", norm: "11:12:13.100000", output: testTime{11, 12, 13, 100000000, false}, l: 1, state: TimePartial},
108109
{input: "11:12:13.123456", norm: "11:12:13.123456", output: testTime{11, 12, 13, 123456000, false}, l: 6},
109110
{input: "11:12:13.000001", norm: "11:12:13.000001", output: testTime{11, 12, 13, 1000, false}, l: 6},
110111
{input: "11:12:13.000000", norm: "11:12:13.000000", output: testTime{11, 12, 13, 0, false}, l: 6},
111-
{input: "11:12:13.123456foo", norm: "11:12:13.123456", output: testTime{11, 12, 13, 123456000, false}, l: 6, err: true},
112+
{input: "11:12:13.123456foo", norm: "11:12:13.123456", output: testTime{11, 12, 13, 123456000, false}, l: 6, state: TimePartial},
112113
{input: "3 11:12:13", norm: "83:12:13.000000", output: testTime{3*24 + 11, 12, 13, 0, false}},
113-
{input: "3 11:12:13foo", norm: "83:12:13.000000", output: testTime{3*24 + 11, 12, 13, 0, false}, err: true},
114+
{input: "3 11:12:13foo", norm: "83:12:13.000000", output: testTime{3*24 + 11, 12, 13, 0, false}, state: TimePartial},
114115
{input: "3 41:12:13", norm: "113:12:13.000000", output: testTime{3*24 + 41, 12, 13, 0, false}},
115-
{input: "3 41:12:13foo", norm: "113:12:13.000000", output: testTime{3*24 + 41, 12, 13, 0, false}, err: true},
116-
{input: "34 23:12:13", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, err: true},
117-
{input: "35 11:12:13", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, err: true},
116+
{input: "3 41:12:13foo", norm: "113:12:13.000000", output: testTime{3*24 + 41, 12, 13, 0, false}, state: TimePartial},
117+
{input: "34 23:12:13", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, state: TimePartial},
118+
{input: "35 11:12:13", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, state: TimePartial},
118119
{input: "11:12", norm: "11:12:00.000000", output: testTime{11, 12, 0, 0, false}},
119120
{input: "5 11:12", norm: "131:12:00.000000", output: testTime{5*24 + 11, 12, 0, 0, false}},
120121
{input: "-2 11:12", norm: "-59:12:00.000000", output: testTime{2*24 + 11, 12, 0, 0, true}},
121-
{input: "--2 11:12", norm: "00:00:00.000000", err: true},
122-
{input: "nonsense", norm: "00:00:00.000000", err: true},
122+
{input: "--2 11:12", norm: "00:00:00.000000", state: TimeInvalid},
123+
{input: "nonsense", norm: "00:00:00.000000", state: TimeInvalid},
123124
{input: "2 11", norm: "59:00:00.000000", output: testTime{2*24 + 11, 0, 0, 0, false}},
124-
{input: "2 -11", norm: "00:00:02.000000", output: testTime{0, 0, 2, 0, false}, err: true},
125+
{input: "2 -11", norm: "00:00:02.000000", output: testTime{0, 0, 2, 0, false}, state: TimePartial},
125126
{input: "13", norm: "00:00:13.000000", output: testTime{0, 0, 13, 0, false}},
126127
{input: "111213", norm: "11:12:13.000000", output: testTime{11, 12, 13, 0, false}},
127128
{input: "111213.123456", norm: "11:12:13.123456", output: testTime{11, 12, 13, 123456000, false}, l: 6},
@@ -130,19 +131,21 @@ func TestParseTime(t *testing.T) {
130131
{input: "25:12:13", norm: "25:12:13.000000", output: testTime{25, 12, 13, 0, false}},
131132
{input: "32:35", norm: "32:35:00.000000", output: testTime{32, 35, 0, 0, false}},
132133
{input: "101:34:58", norm: "101:34:58.000000", output: testTime{101, 34, 58, 0, false}},
134+
{input: "101:64:58", norm: "00:00:00.000000", state: TimeInvalid},
135+
{input: "101:34:68", norm: "00:00:00.000000", state: TimeInvalid},
133136
{input: "1", norm: "00:00:01.000000", output: testTime{0, 0, 1, 0, false}},
134137
{input: "11", norm: "00:00:11.000000", output: testTime{0, 0, 11, 0, false}},
135138
{input: "111", norm: "00:01:11.000000", output: testTime{0, 1, 11, 0, false}},
136139
{input: "1111", norm: "00:11:11.000000", output: testTime{0, 11, 11, 0, false}},
137140
{input: "11111", norm: "01:11:11.000000", output: testTime{1, 11, 11, 0, false}},
138141
{input: "111111", norm: "11:11:11.000000", output: testTime{11, 11, 11, 0, false}},
139-
{input: "1foo", norm: "00:00:01.000000", output: testTime{0, 0, 1, 0, false}, err: true},
140-
{input: "11foo", norm: "00:00:11.000000", output: testTime{0, 0, 11, 0, false}, err: true},
141-
{input: "111foo", norm: "00:01:11.000000", output: testTime{0, 1, 11, 0, false}, err: true},
142-
{input: "1111foo", norm: "00:11:11.000000", output: testTime{0, 11, 11, 0, false}, err: true},
143-
{input: "11111foo", norm: "01:11:11.000000", output: testTime{1, 11, 11, 0, false}, err: true},
144-
{input: "111111foo", norm: "11:11:11.000000", output: testTime{11, 11, 11, 0, false}, err: true},
145-
{input: "1111111foo", norm: "111:11:11.000000", output: testTime{111, 11, 11, 0, false}, err: true},
142+
{input: "1foo", norm: "00:00:01.000000", output: testTime{0, 0, 1, 0, false}, state: TimePartial},
143+
{input: "11foo", norm: "00:00:11.000000", output: testTime{0, 0, 11, 0, false}, state: TimePartial},
144+
{input: "111foo", norm: "00:01:11.000000", output: testTime{0, 1, 11, 0, false}, state: TimePartial},
145+
{input: "1111foo", norm: "00:11:11.000000", output: testTime{0, 11, 11, 0, false}, state: TimePartial},
146+
{input: "11111foo", norm: "01:11:11.000000", output: testTime{1, 11, 11, 0, false}, state: TimePartial},
147+
{input: "111111foo", norm: "11:11:11.000000", output: testTime{11, 11, 11, 0, false}, state: TimePartial},
148+
{input: "1111111foo", norm: "111:11:11.000000", output: testTime{111, 11, 11, 0, false}, state: TimePartial},
146149
{input: "-1", norm: "-00:00:01.000000", output: testTime{0, 0, 1, 0, true}},
147150
{input: "-11", norm: "-00:00:11.000000", output: testTime{0, 0, 11, 0, true}},
148151
{input: "-111", norm: "-00:01:11.000000", output: testTime{0, 1, 11, 0, true}},
@@ -172,44 +175,31 @@ func TestParseTime(t *testing.T) {
172175
{input: "11111.1", norm: "01:11:11.100000", output: testTime{1, 11, 11, 100000000, false}, l: 1},
173176
{input: "111111.1", norm: "11:11:11.100000", output: testTime{11, 11, 11, 100000000, false}, l: 1},
174177
{input: "1111111.1", norm: "111:11:11.100000", output: testTime{111, 11, 11, 100000000, false}, l: 1},
175-
{input: "20000101", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, err: true},
176-
{input: "-20000101", norm: "-838:59:59.000000", output: testTime{838, 59, 59, 0, true}, err: true},
177-
{input: "999995959", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, err: true},
178-
{input: "-999995959", norm: "-838:59:59.000000", output: testTime{838, 59, 59, 0, true}, err: true},
179-
{input: "4294965959", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, err: true},
180-
{input: "-4294965959", norm: "-838:59:59.000000", output: testTime{838, 59, 59, 0, true}, err: true},
181-
{input: "4294975959", norm: "00:00:00.000000", err: true},
182-
{input: "-4294975959", norm: "00:00:00.000000", err: true},
183-
{input: "\t34 foo\t", norm: "00:00:34.000000", output: testTime{0, 0, 34, 0, false}, err: true},
184-
{input: "\t34 1foo\t", norm: "817:00:00.000000", output: testTime{817, 0, 0, 0, false}, err: true},
185-
{input: "\t34 23foo\t", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, err: true},
186-
{input: "\t35 foo\t", norm: "00:00:35.000000", output: testTime{0, 0, 35, 0, false}, err: true},
187-
{input: "\t35 1foo\t", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, err: true},
188-
{input: " 255 foo", norm: "00:02:55.000000", output: testTime{0, 2, 55, 0, false}, err: true},
189-
{input: "255", norm: "00:02:55.000000", output: testTime{0, 2, 55, 0, false}},
178+
{input: "20000101", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, state: TimePartial},
179+
{input: "-20000101", norm: "-838:59:59.000000", output: testTime{838, 59, 59, 0, true}, state: TimePartial},
180+
{input: "999995959", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, state: TimePartial},
181+
{input: "-999995959", norm: "-838:59:59.000000", output: testTime{838, 59, 59, 0, true}, state: TimePartial},
182+
{input: "4294965959", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, state: TimePartial},
183+
{input: "-4294965959", norm: "-838:59:59.000000", output: testTime{838, 59, 59, 0, true}, state: TimePartial},
184+
{input: "4294975959", norm: "00:00:00.000000", state: TimeInvalid},
185+
{input: "-4294975959", norm: "00:00:00.000000", state: TimeInvalid},
186+
{input: "\t34 foo\t", norm: "00:00:34.000000", output: testTime{0, 0, 34, 0, false}, state: TimePartial},
187+
{input: "\t34 1foo\t", norm: "817:00:00.000000", output: testTime{817, 0, 0, 0, false}, state: TimePartial},
188+
{input: "\t34 23foo\t", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, state: TimePartial},
189+
{input: "\t35 foo\t", norm: "00:00:35.000000", output: testTime{0, 0, 35, 0, false}, state: TimePartial},
190+
{input: "\t35 1foo\t", norm: "838:59:59.000000", output: testTime{838, 59, 59, 0, false}, state: TimePartial},
190191
}
191192

192193
for _, test := range tests {
193194
t.Run(test.input, func(t *testing.T) {
194-
got, l, ok := ParseTime(test.input, -1)
195-
if test.err {
196-
assert.Equal(t, test.output.hour, got.Hour())
197-
assert.Equal(t, test.output.minute, got.Minute())
198-
assert.Equal(t, test.output.second, got.Second())
199-
assert.Equal(t, test.output.nanosecond, got.Nanosecond())
200-
assert.Equal(t, test.norm, string(got.AppendFormat(nil, 6)))
201-
assert.Equal(t, test.l, l)
202-
assert.Falsef(t, ok, "did not fail to parse %s", test.input)
203-
return
204-
}
205-
206-
require.True(t, ok)
195+
got, l, state := ParseTime(test.input, -1)
196+
assert.Equal(t, test.state, state)
207197
assert.Equal(t, test.output.hour, got.Hour())
208198
assert.Equal(t, test.output.minute, got.Minute())
209199
assert.Equal(t, test.output.second, got.Second())
210200
assert.Equal(t, test.output.nanosecond, got.Nanosecond())
211-
assert.Equal(t, test.l, l)
212201
assert.Equal(t, test.norm, string(got.AppendFormat(nil, 6)))
202+
assert.Equal(t, test.l, l)
213203
})
214204
}
215205
}

go/mysql/json/parser.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -941,8 +941,8 @@ func (v *Value) Time() (datetime.Time, bool) {
941941
if v.t != TypeTime {
942942
return datetime.Time{}, false
943943
}
944-
t, _, ok := datetime.ParseTime(v.s, datetime.DefaultPrecision)
945-
return t, ok
944+
t, _, state := datetime.ParseTime(v.s, datetime.DefaultPrecision)
945+
return t, state == datetime.TimeOK
946946
}
947947

948948
// Object returns the underlying JSON object for the v.

0 commit comments

Comments
 (0)