-
Notifications
You must be signed in to change notification settings - Fork 5.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
types: fix invalid time cast bug #4338
Conversation
util/types/time.go
Outdated
// Invalid TIME values are converted to '00:00:00' | ||
// See https://dev.mysql.com/doc/refman/5.7/en/time.html | ||
if minute >= 60 || second >= 60 { | ||
return ZeroDuration, errors.Trace(ErrInvalidTimeFormat) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just return ZeroDuration, ErrInvalidTimeFormat
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about hour >= 24
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@winkyao
See https://dev.mysql.com/doc/refman/5.7/en/time.html
MySQL retrieves and displays TIME values in 'HH:MM:SS' format (or 'HHH:MM:SS' format for large hours values). TIME values may range from '-838:59:59' to '838:59:59'. The hours part may be so large because the TIME type can be used not only to represent a time of day (which must be less than 24 hours), but also elapsed time or a time interval between two events (which may be much greater than 24 hours, or even negative).
expression/integration_test.go
Outdated
c.Assert(errors.ErrorStack(err), Equals, "") | ||
c.Assert(rs, NotNil) | ||
_, err = tidb.GetRows(rs) | ||
c.Assert(err.Error(), Equals, "invalid time format") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in mysql, the behavior of invalid time format is depended on sql mode, in strict sql mode:
MySQL > select cast('239010' as time);
+------------------------+
| cast('239010' as time) |
+------------------------+
| NULL |
+------------------------+
1 row in set, 1 warning (0.00 sec)
MySQL > show warnings;
+---------+------+------------------------------------------+
| Level | Code | Message |
+---------+------+------------------------------------------+
| Warning | 1292 | Truncated incorrect time value: '239010' |
+---------+------+------------------------------------------+
1 row in set (0.00 sec)
and:
MySQL > create table t(a time);
Query OK, 0 rows affected (0.03 sec)
MySQL > insert into t select cast('239010' as time);
ERROR 1292 (22007): Truncated incorrect time value: '239010'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use session.go:HandleTruncate
to handle it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. done. PTAL.
expression/integration_test.go
Outdated
for _, invalidTime := range invalidTimes { | ||
msg := fmt.Sprintf("Warning 1292 Truncated incorrect time value: '%s'", invalidTime) | ||
result = tk.MustQuery(fmt.Sprintf("select cast('%s' as time);", invalidTime)) | ||
result.Check(testkit.Rows("00:00:00")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in mysql:
MySQL > select cast('10009010' as time);
+--------------------------+
| cast('10009010' as time) |
+--------------------------+
| NULL |
+--------------------------+
1 row in set, 1 warning (0.00 sec)
MySQL > set sql_mode='';
Query OK, 0 rows affected, 1 warning (0.01 sec)
MySQL > select cast('10009010' as time);
+--------------------------+
| cast('10009010' as time) |
+--------------------------+
| NULL |
+--------------------------+
1 row in set, 1 warning (0.00 sec)
the test case is not compatible with mysql's behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zz-jason PTAL.
changes:
1.when return value is ZeroDuration, then set isNull to true.
2.change the testcase.
expression/integration_test.go
Outdated
for _, invalidTime := range invalidTimes { | ||
msg := fmt.Sprintf("Warning 1292 Truncated incorrect time value: '%s'", invalidTime) | ||
result = tk.MustQuery(fmt.Sprintf("select cast('%s' as time);", invalidTime)) | ||
result.Check(testkit.Rows("00:00:00")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
2d8d100
to
66b72f3
Compare
expression/builtin_cast.go
Outdated
if isNull || err != nil { | ||
return res, isNull, errors.Trace(err) | ||
} | ||
res, err = types.ParseDuration(strconv.FormatInt(val, 10), b.tp.Decimal) | ||
if types.ErrTruncatedWrongVal.Equal(err) { | ||
err = sc.HandleTruncate(err) | ||
if res == types.ZeroDuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if err != nil {
return res, true, errors.Trace(err)
}
- move
if res == types.ZeroDuration {
out ofif types.ErrTruncatedWrongVal.Equal(err) {
statement - if res is a zero duration, should we throw or handle a zero duration error ? see handle errors in strict sql mode #4373 for more detail
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- zero duration is valid value. so I need to check ErrTruncatedWrongVal err to distinguish invalid zero from valid zero duration.
mysql> select cast('000000' as time) ;
+------------------------+
| cast('000000' as time) |
+------------------------+
| 00:00:00 |
+------------------------+
1 row in set (0.00 sec)
- expression.handleInvalidTimeError appended warning message, but was not compatible with mysql's behavior. ErrInvalidTimeFormat does not contains any params.
mysql:
mysql> select cast('123080' as time) ;
+------------------------+
| cast('123080' as time) |
+------------------------+
| NULL |
+------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show warnings;
+---------+------+------------------------------------------+
| Level | Code | Message |
+---------+------+------------------------------------------+
| Warning | 1292 | Truncated incorrect time value: '123080' |
+---------+------+------------------------------------------+
1 row in set (0.00 sec)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- with ErrTruncatedWrongVal and zero duration, set isNull to true, append warning or not, then will show .
- with ErrTruncatedWrongVal and not zero duration, the over range case, append warning or not.
- zero duration without error, the normal case, just pass.
expression/builtin_cast.go
Outdated
if isNull || err != nil { | ||
return res, false, errors.Trace(err) | ||
} | ||
res, err = types.ParseDuration(string(val.ToString()), b.tp.Decimal) | ||
if types.ErrTruncatedWrongVal.Equal(err) { | ||
err = sc.HandleTruncate(err) | ||
if res == types.ZeroDuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
expression/builtin_cast.go
Outdated
@@ -958,6 +972,9 @@ func (b *builtinCastStringAsDurationSig) evalDuration(row []types.Datum) (res ty | |||
res, err = types.ParseDuration(val, b.tp.Decimal) | |||
if types.ErrTruncatedWrongVal.Equal(err) { | |||
err = sc.HandleTruncate(err) | |||
if res == types.ZeroDuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think when res == types.ZeroDuration
, it is perhaps a zero duration, because the res is a value copy of types.ZeroDuration, types.ParseDuration return duration value, not duration pointer, so It can not distinguish it. Maybe it is better to add a boolean isNull return value to ParseDuration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@winkyao
there are 3 cases:
- with ErrTruncatedWrongVal and zero duration, set isNull to true, append warning or not, then will show .
- with ErrTruncatedWrongVal and not zero duration, the over range case, append warning or not.
- zero duration without error, the normal case, just pass.
so I think res == types.ZeroDuration condition inside err check statment will be ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can trust the result of types.ParseDuration(val, b.tp.Decimal)
only if err == nil
, which means we should move res == types.ZeroDuration
condition outside the err check statement, that is:
res, err = types.ParseDuration(val, b.tp.Decimal)
if err != nil {
if types.ErrTruncatedWrongVal.Equal(err) {
...
}
...
} else if res == types.ZeroDuration {
...
}
@mccxj plz resolve conflicts. |
66b72f3
to
e752569
Compare
util/types/time.go
Outdated
} | ||
|
||
if err != nil { | ||
return ZeroDuration, errors.Trace(err) | ||
} | ||
// Invalid TIME values are converted to '00:00:00' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add .
at the end of this comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
PTAL @winkyao |
Hi contributor, thanks for your PR. This patch needs to be approved by someone of admins. They should reply with "/ok-to-test" to accept this PR for running test automatically. |
7b81309
to
f3ea756
Compare
/ok-to-test |
expression/builtin_cast.go
Outdated
@@ -816,11 +816,18 @@ type builtinCastDecimalAsDurationSig struct { | |||
} | |||
|
|||
func (b *builtinCastDecimalAsDurationSig) evalDuration(row []types.Datum) (res types.Duration, isNull bool, err error) { | |||
val, isNull, err := b.args[0].EvalDecimal(row, b.getCtx().GetSessionVars().StmtCtx) | |||
sc := b.getCtx().GetSessionVars().StmtCtx | |||
val, isNull, err := b.args[0].EvalDecimal(row, sc) | |||
if isNull || err != nil { | |||
return res, false, errors.Trace(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should return res, true, errors.Trace(err)
?
expression/builtin_cast.go
Outdated
@@ -958,6 +972,9 @@ func (b *builtinCastStringAsDurationSig) evalDuration(row []types.Datum) (res ty | |||
res, err = types.ParseDuration(val, b.tp.Decimal) | |||
if types.ErrTruncatedWrongVal.Equal(err) { | |||
err = sc.HandleTruncate(err) | |||
if res == types.ZeroDuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can trust the result of types.ParseDuration(val, b.tp.Decimal)
only if err == nil
, which means we should move res == types.ZeroDuration
condition outside the err check statement, that is:
res, err = types.ParseDuration(val, b.tp.Decimal)
if err != nil {
if types.ErrTruncatedWrongVal.Equal(err) {
...
}
...
} else if res == types.ZeroDuration {
...
}
6cd162a
to
4a8147c
Compare
@mccxj any update ? |
4a8147c
to
30305d7
Compare
@mccxj ci failed. |
30305d7
to
e7c124b
Compare
@zz-jason PTAL |
expression/builtin_time.go
Outdated
@@ -4687,7 +4687,7 @@ type builtinUTCTimeWithoutArgSig struct { | |||
// See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_utc-time | |||
func (b *builtinUTCTimeWithoutArgSig) evalDuration(row []types.Datum) (types.Duration, bool, error) { | |||
// the types.ParseDuration here would never fail, so the err returned can be ignored. | |||
v, _ := types.ParseDuration(time.Now().UTC().Format(types.TimeFormat), 0) | |||
v, _, _ := types.ParseDuration(time.Now().UTC().Format(types.TimeFormat), 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why ignore isNull
and err
returned by types.ParseDuration
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zz-jason Input parameters can be predicted, so ignore is safe, just like the origin comments said.
But the modifications are minor, and I can modify it.
8bd8e51
to
819c89c
Compare
expression/builtin_cast.go
Outdated
@@ -726,8 +726,7 @@ func (b *builtinCastRealAsDurationSig) evalDuration(row []types.Datum) (res type | |||
if isNull || err != nil { | |||
return res, isNull, errors.Trace(err) | |||
} | |||
res, err = types.ParseDuration(strconv.FormatFloat(val, 'f', -1, 64), b.tp.Decimal) | |||
return res, false, errors.Trace(err) | |||
return types.ParseDuration(strconv.FormatFloat(val, 'f', -1, 64), b.tp.Decimal) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
res, err = types.ParseDuration(strconv.FormatFloat(val, 'f', -1, 64), b.tp.Decimal)
return res, err != nil, errors.Trace(err)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modified func ParseDuration to return isNull param, just return will be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But we still should need to Trace
the error when it is not nil
.
expression/builtin_cast.go
Outdated
if types.ErrTruncatedWrongVal.Equal(err) { | ||
err = sc.HandleTruncate(err) | ||
} | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return res, isNull || err != nil, errors.Trace(err)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sc.HandleTruncate check is needed to deal with SQLMODE.
expression/builtin_cast.go
Outdated
if types.ErrTruncatedWrongVal.Equal(err) { | ||
err = sc.HandleTruncate(err) | ||
} | ||
return res, false, errors.Trace(err) | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
expression/builtin_cast.go
Outdated
@@ -1287,7 +1290,7 @@ func (b *builtinCastJSONAsDurationSig) evalDuration(row []types.Datum) (res type | |||
if err != nil { | |||
return res, false, errors.Trace(err) | |||
} | |||
res, err = types.ParseDuration(s, b.tp.Decimal) | |||
res, isNull, err = types.ParseDuration(s, b.tp.Decimal) | |||
if types.ErrTruncatedWrongVal.Equal(err) { | |||
err = sc.HandleTruncate(err) | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return res, isNull || err != nil, errors.Trace(err)
expression/builtin_time.go
Outdated
if types.ErrTruncatedWrongVal.Equal(err) { | ||
err = sc.HandleTruncate(err) | ||
} | ||
return res, isNull, errors.Trace(err) | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
server/util_test.go
Outdated
@@ -44,7 +44,7 @@ func (s *testUtilSuite) TestDumpBinaryTime(c *C) { | |||
c.Assert(err, IsNil) | |||
c.Assert(d, DeepEquals, []byte{4, 1, 0, 1, 1}) | |||
|
|||
myDuration, err := types.ParseDuration("0000-00-00 00:00:00.0000000", 6) | |||
myDuration, _, err := types.ParseDuration("0000-00-00 00:00:00.0000000", 6) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
server/util_test.go
Outdated
@@ -109,7 +109,7 @@ func (s *testUtilSuite) TestDumpTextValue(c *C) { | |||
c.Assert(err, IsNil) | |||
c.Assert(string(bs), Equals, "2017-01-06 00:00:00") | |||
|
|||
duration, err := types.ParseDuration("11:30:45", 0) | |||
duration, _, err := types.ParseDuration("11:30:45", 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
tablecodec/tablecodec_test.go
Outdated
@@ -153,7 +153,7 @@ func (s *testTableCodecSuite) TestTimeCodec(c *C) { | |||
ts, err := types.ParseTimestamp("2016-06-23 11:30:45") | |||
c.Assert(err, IsNil) | |||
row[2] = types.NewDatum(ts) | |||
du, err := types.ParseDuration("12:59:59.999999", 6) | |||
du, _, err := types.ParseDuration("12:59:59.999999", 6) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
util/codec/codec_test.go
Outdated
@@ -509,7 +509,7 @@ func parseTime(c *C, s string) types.Time { | |||
} | |||
|
|||
func parseDuration(c *C, s string) types.Duration { | |||
m, err := types.ParseDuration(s, types.DefaultFsp) | |||
m, _, err := types.ParseDuration(s, types.DefaultFsp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
util/types/time.go
Outdated
@@ -949,7 +949,7 @@ func (d Duration) MicroSecond() int { | |||
// ParseDuration parses the time form a formatted string with a fractional seconds part, | |||
// returns the duration type Time value. | |||
// See http://dev.mysql.com/doc/refman/5.7/en/fractional-seconds.html | |||
func ParseDuration(str string, fsp int) (Duration, error) { | |||
func ParseDuration(str string, fsp int) (Duration, bool, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the second return value is true
only when the returned error is ErrTruncatedWrongVal
, can we keep the return values to (Duration, error)
, and check ErrTruncatedWrongVal
outside ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zz-jason If I check errors inside func ParseDuration, then I needs StatementContext to deal with SQLMODE.
Any suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I mean maybe we don't need to check the error inside function ParseDuration()
, you can just return the error and let the function caller to handle it(ignore or throw it).
Any update? @mccxj |
Hi @mccxj, plz resolve conflicts and take a look at the comments. |
…eds to be considered NULL
b5940ce
to
4960dbe
Compare
@zz-jason I reverted to the original modification logic. PTAL. |
/run-all-tests |
LGTM |
@XuHuaiyu PTAL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
/run-all-tests |
fixed issue #4324.
act as invalid time when minute gt 60 or second gt 60.