Skip to content

Commit

Permalink
expression: fix issue that results of `unix_timestamp()-unix_timestam…
Browse files Browse the repository at this point in the history
…p(now())` is wrong and not stable (#10491)
  • Loading branch information
qw4990 authored and zz-jason committed May 22, 2019
1 parent a84a1ee commit 5b593d4
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 13 deletions.
2 changes: 1 addition & 1 deletion executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3450,7 +3450,7 @@ func (s *testSuite) TestCurrentTimestampValueSelection(c *C) {
tk.MustQuery("select id from t where t0 = ?", t0).Check(testkit.Rows("1"))
tk.MustQuery("select id from t where t1 = ?", t1).Check(testkit.Rows("1"))
tk.MustQuery("select id from t where t2 = ?", t2).Check(testkit.Rows("1"))
time.Sleep(time.Second / 2)
time.Sleep(time.Second)
tk.MustExec("update t set t0 = now() where id = 1")
rs = tk.MustQuery("select t2 from t where id = 1")
newT2 := rs.Rows()[0][0].(string)
Expand Down
51 changes: 39 additions & 12 deletions expression/builtin_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,14 @@ var (
_ builtinFunc = &builtinSubDateDatetimeDecimalSig{}
)

func convertTimeToMysqlTime(t time.Time, fsp int) (types.Time, error) {
tr, err := types.RoundFrac(t, fsp)
func convertTimeToMysqlTime(t time.Time, fsp int, roundMode types.RoundMode) (types.Time, error) {
var tr time.Time
var err error
if roundMode == types.ModeTruncate {
tr, err = types.TruncateFrac(t, fsp)
} else {
tr, err = types.RoundFrac(t, fsp)
}
if err != nil {
return types.Time{}, errors.Trace(err)
}
Expand Down Expand Up @@ -1610,7 +1616,7 @@ func evalFromUnixTime(ctx sessionctx.Context, fsp int, row chunk.Row, arg Expres

sc := ctx.GetSessionVars().StmtCtx
tmp := time.Unix(integralPart, fractionalPart).In(sc.TimeZone)
t, err := convertTimeToMysqlTime(tmp, fsp)
t, err := convertTimeToMysqlTime(tmp, fsp, types.ModeHalfEven)
if err != nil {
return res, true, errors.Trace(err)
}
Expand Down Expand Up @@ -1932,7 +1938,7 @@ func (b *builtinSysDateWithFspSig) evalTime(row chunk.Row) (d types.Time, isNull

loc := b.ctx.GetSessionVars().Location()
now := time.Now().In(loc)
result, err := convertTimeToMysqlTime(now, int(fsp))
result, err := convertTimeToMysqlTime(now, int(fsp), types.ModeHalfEven)
if err != nil {
return types.Time{}, true, errors.Trace(err)
}
Expand All @@ -1954,7 +1960,7 @@ func (b *builtinSysDateWithoutFspSig) Clone() builtinFunc {
func (b *builtinSysDateWithoutFspSig) evalTime(row chunk.Row) (d types.Time, isNull bool, err error) {
tz := b.ctx.GetSessionVars().Location()
now := time.Now().In(tz)
result, err := convertTimeToMysqlTime(now, 0)
result, err := convertTimeToMysqlTime(now, 0, types.ModeHalfEven)
if err != nil {
return types.Time{}, true, errors.Trace(err)
}
Expand Down Expand Up @@ -2262,8 +2268,12 @@ func (c *utcTimestampFunctionClass) getFunction(ctx sessionctx.Context, args []E
return sig, nil
}

func evalUTCTimestampWithFsp(fsp int) (types.Time, bool, error) {
result, err := convertTimeToMysqlTime(time.Now().UTC(), fsp)
func evalUTCTimestampWithFsp(ctx sessionctx.Context, fsp int) (types.Time, bool, error) {
nowTs, err := getSystemTimestamp(ctx)
if err != nil {
return types.Time{}, true, err
}
result, err := convertTimeToMysqlTime(nowTs.UTC(), fsp, types.ModeHalfEven)
if err != nil {
return types.Time{}, true, errors.Trace(err)
}
Expand Down Expand Up @@ -2295,7 +2305,7 @@ func (b *builtinUTCTimestampWithArgSig) evalTime(row chunk.Row) (types.Time, boo
return types.Time{}, true, errors.Errorf("Invalid negative %d specified, must in [0, 6].", num)
}

result, isNull, err := evalUTCTimestampWithFsp(int(num))
result, isNull, err := evalUTCTimestampWithFsp(b.ctx, int(num))
return result, isNull, errors.Trace(err)
}

Expand All @@ -2312,7 +2322,7 @@ func (b *builtinUTCTimestampWithoutArgSig) Clone() builtinFunc {
// evalTime evals UTC_TIMESTAMP().
// See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_utc-timestamp
func (b *builtinUTCTimestampWithoutArgSig) evalTime(row chunk.Row) (types.Time, bool, error) {
result, isNull, err := evalUTCTimestampWithFsp(0)
result, isNull, err := evalUTCTimestampWithFsp(b.ctx, 0)
return result, isNull, errors.Trace(err)
}

Expand Down Expand Up @@ -2351,7 +2361,15 @@ func evalNowWithFsp(ctx sessionctx.Context, fsp int) (types.Time, bool, error) {
return types.Time{}, true, errors.Trace(err)
}

result, err := convertTimeToMysqlTime(sysTs, fsp)
// In MySQL's implementation, now() will truncate the result instead of rounding it.
// Results below are from MySQL 5.7, which can prove it.
// mysql> select now(6), now(3), now();
// +----------------------------+-------------------------+---------------------+
// | now(6) | now(3) | now() |
// +----------------------------+-------------------------+---------------------+
// | 2019-03-25 15:57:56.612966 | 2019-03-25 15:57:56.612 | 2019-03-25 15:57:56 |
// +----------------------------+-------------------------+---------------------+
result, err := convertTimeToMysqlTime(sysTs, fsp, types.ModeTruncate)
if err != nil {
return types.Time{}, true, errors.Trace(err)
}
Expand Down Expand Up @@ -3807,8 +3825,17 @@ func goTimeToMysqlUnixTimestamp(t time.Time, decimal int) (*types.MyDecimal, err
if err != nil {
return nil, errors.Trace(err)
}
err = dec.Round(dec, decimal, types.ModeHalfEven)
return dec, errors.Trace(err)

// In MySQL's implementation, unix_timestamp() will truncate the result instead of rounding it.
// Results below are from MySQL 5.7, which can prove it.
// mysql> select unix_timestamp(), unix_timestamp(now(0)), now(0), unix_timestamp(now(3)), now(3), now(6);
// +------------------+------------------------+---------------------+------------------------+-------------------------+----------------------------+
// | unix_timestamp() | unix_timestamp(now(0)) | now(0) | unix_timestamp(now(3)) | now(3) | now(6) |
// +------------------+------------------------+---------------------+------------------------+-------------------------+----------------------------+
// | 1553503194 | 1553503194 | 2019-03-25 16:39:54 | 1553503194.992 | 2019-03-25 16:39:54.992 | 2019-03-25 16:39:54.992969 |
// +------------------+------------------------+---------------------+------------------------+-------------------------+----------------------------+
err = dec.Round(dec, decimal, types.ModeTruncate)
return dec, err
}

type builtinUnixTimestampCurrentSig struct {
Expand Down
30 changes: 30 additions & 0 deletions expression/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -4009,3 +4010,32 @@ func (s *testIntegrationSuite) TestDateTimeAddReal(c *C) {
tk.MustQuery(c.sql).Check(testkit.Rows(c.result))
}
}

func (s *testIntegrationSuite) TestIssue9710(c *C) {
tk := testkit.NewTestKit(c, s.store)
getSAndMS := func(str string) (int, int) {
results := strings.Split(str, ":")
SAndMS := strings.Split(results[len(results)-1], ".")
var s, ms int
s, _ = strconv.Atoi(SAndMS[0])
if len(SAndMS) > 1 {
ms, _ = strconv.Atoi(SAndMS[1])
}
return s, ms
}

for {
rs := tk.MustQuery("select now(), now(6), unix_timestamp(), unix_timestamp(now())")
s, ms := getSAndMS(rs.Rows()[0][1].(string))
if ms < 500000 {
time.Sleep(time.Second / 10)
continue
}

s1, _ := getSAndMS(rs.Rows()[0][0].(string))
c.Assert(s, Equals, s1) // now() will truncate the result instead of rounding it

c.Assert(rs.Rows()[0][2], Equals, rs.Rows()[0][3]) // unix_timestamp() will truncate the result
break
}
}
10 changes: 10 additions & 0 deletions types/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,16 @@ func RoundFrac(t gotime.Time, fsp int) (gotime.Time, error) {
return t.Round(gotime.Duration(math.Pow10(9-fsp)) * gotime.Nanosecond), nil
}

// TruncateFrac truncates fractional seconds precision with new fsp and returns a new one.
// 2011:11:11 10:10:10.888888 round 0 -> 2011:11:11 10:10:10
// 2011:11:11 10:10:10.111111 round 0 -> 2011:11:11 10:10:10
func TruncateFrac(t gotime.Time, fsp int) (gotime.Time, error) {
if _, err := CheckFsp(fsp); err != nil {
return t, err
}
return t.Truncate(gotime.Duration(math.Pow10(9-fsp)) * gotime.Nanosecond), nil
}

// ToPackedUint encodes Time to a packed uint64 value.
//
// 1 bit 0
Expand Down

0 comments on commit 5b593d4

Please sign in to comment.