Skip to content
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

Merged
merged 4 commits into from
Nov 22, 2017
Merged

Conversation

mccxj
Copy link
Contributor

@mccxj mccxj commented Aug 27, 2017

fixed issue #4324.
act as invalid time when minute gt 60 or second gt 60.

// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just return ZeroDuration, ErrInvalidTimeFormat

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about hour >= 24?

Copy link
Contributor Author

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).

c.Assert(errors.ErrorStack(err), Equals, "")
c.Assert(rs, NotNil)
_, err = tidb.GetRows(rs)
c.Assert(err.Error(), Equals, "invalid time format")
Copy link
Member

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'

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. done. PTAL.

@jackysp jackysp added the contribution This PR is from a community contributor. label Aug 28, 2017
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"))
Copy link
Member

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

Copy link
Contributor Author

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.

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"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

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 {
Copy link
Member

@zz-jason zz-jason Aug 30, 2017

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)
}
  1. move if res == types.ZeroDuration { out of if types.ErrTruncatedWrongVal.Equal(err) { statement
  2. 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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)
  1. 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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zz-jason

  1. with ErrTruncatedWrongVal and zero duration, set isNull to true, append warning or not, then will show .
  2. with ErrTruncatedWrongVal and not zero duration, the over range case, append warning or not.
  3. zero duration without error, the normal case, just pass.

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Contributor

@winkyao winkyao Sep 2, 2017

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?

Copy link
Contributor Author

@mccxj mccxj Sep 6, 2017

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:

  1. with ErrTruncatedWrongVal and zero duration, set isNull to true, append warning or not, then will show .
  2. with ErrTruncatedWrongVal and not zero duration, the over range case, append warning or not.
  3. zero duration without error, the normal case, just pass.

so I think res == types.ZeroDuration condition inside err check statment will be ok.

Copy link
Member

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 {
    ...
}

@zz-jason
Copy link
Member

zz-jason commented Sep 1, 2017

@mccxj plz resolve conflicts.

}

if err != nil {
return ZeroDuration, errors.Trace(err)
}
// Invalid TIME values are converted to '00:00:00'
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@XuHuaiyu
Copy link
Contributor

XuHuaiyu commented Sep 1, 2017

PTAL @winkyao

@sre-bot
Copy link
Contributor

sre-bot commented Sep 1, 2017

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.

@iamxy
Copy link
Member

iamxy commented Sep 4, 2017

/ok-to-test

@@ -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)
Copy link
Member

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) ?

@@ -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 {
Copy link
Member

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 {
    ...
}

@zz-jason
Copy link
Member

@mccxj any update ?

@zz-jason
Copy link
Member

@mccxj ci failed.

@mccxj
Copy link
Contributor Author

mccxj commented Sep 22, 2017

@zz-jason PTAL

@@ -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)
Copy link
Member

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 ?

Copy link
Contributor Author

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.

@mccxj mccxj force-pushed the patch-issue-4324 branch 2 times, most recently from 8bd8e51 to 819c89c Compare October 2, 2017 06:41
@@ -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)
Copy link
Member

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)

Copy link
Contributor Author

@mccxj mccxj Oct 26, 2017

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.

Copy link
Member

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.

if types.ErrTruncatedWrongVal.Equal(err) {
err = sc.HandleTruncate(err)
}
return
Copy link
Member

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)

Copy link
Contributor Author

@mccxj mccxj Oct 26, 2017

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.

if types.ErrTruncatedWrongVal.Equal(err) {
err = sc.HandleTruncate(err)
}
return res, false, errors.Trace(err)
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -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)
}
Copy link
Member

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)

if types.ErrTruncatedWrongVal.Equal(err) {
err = sc.HandleTruncate(err)
}
return res, isNull, errors.Trace(err)
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -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) {
Copy link
Member

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 ?

Copy link
Contributor Author

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?

Copy link
Member

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).

@tiancaiamao
Copy link
Contributor

Any update? @mccxj

@zz-jason
Copy link
Member

Hi @mccxj, plz resolve conflicts and take a look at the comments.

@zz-jason zz-jason added this to the 1.1 milestone Oct 28, 2017
@mccxj mccxj changed the title types: fix invalid time cast bug. #4324 types: fix invalid time cast bug Nov 15, 2017
@mccxj
Copy link
Contributor Author

mccxj commented Nov 15, 2017

@zz-jason I reverted to the original modification logic. PTAL.

@tiancaiamao
Copy link
Contributor

/run-all-tests

@tiancaiamao
Copy link
Contributor

LGTM

@tiancaiamao tiancaiamao added status/LGT1 Indicates that a PR has LGTM 1. and removed conflicting labels Nov 20, 2017
@winoros
Copy link
Member

winoros commented Nov 20, 2017

@XuHuaiyu PTAL

@tiancaiamao
Copy link
Contributor

PTAL @XuHuaiyu @zz-jason

Copy link
Member

@zz-jason zz-jason left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@zz-jason zz-jason added status/LGT2 Indicates that a PR has LGTM 2. and removed status/LGT1 Indicates that a PR has LGTM 1. labels Nov 22, 2017
@winoros
Copy link
Member

winoros commented Nov 22, 2017

/run-all-tests

@winoros winoros merged commit 8892bdf into pingcap:master Nov 22, 2017
@mccxj mccxj deleted the patch-issue-4324 branch November 22, 2017 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution This PR is from a community contributor. status/LGT2 Indicates that a PR has LGTM 2.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants