Skip to content

Commit

Permalink
expression: addition between datetime and interval is not compati…
Browse files Browse the repository at this point in the history
…ble with Mysql (#10329) (#10416)
  • Loading branch information
erjiaqing authored and zz-jason committed May 10, 2019
1 parent d22ecd0 commit 6cb7bd1
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 92 deletions.
37 changes: 18 additions & 19 deletions expression/builtin_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -2605,14 +2605,14 @@ func (du *baseDateArithmitical) getIntervalFromDecimal(ctx sessionctx.Context, a
interval = "00:" + interval
case "SECOND_MICROSECOND":
/* keep interval as original decimal */
case "SECOND", "MICROSECOND":
args[1] = WrapWithCastAsReal(ctx, args[1])
case "SECOND":
// Decimal's EvalString is like %f format.
interval, isNull, err = args[1].EvalString(ctx, row)
if isNull || err != nil {
return "", true, errors.Trace(err)
}
default:
// YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE
// YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, MICROSECOND
args[1] = WrapWithCastAsInt(ctx, args[1])
interval, isNull, err = args[1].EvalString(ctx, row)
if isNull || err != nil {
Expand All @@ -2632,18 +2632,17 @@ func (du *baseDateArithmitical) getIntervalFromInt(ctx sessionctx.Context, args
}

func (du *baseDateArithmitical) add(ctx sessionctx.Context, date types.Time, interval string, unit string) (types.Time, bool, error) {
year, month, day, dur, err := types.ExtractTimeValue(unit, interval)
if err != nil {
return types.Time{}, true, errors.Trace(handleInvalidTimeError(ctx, err))
year, month, day, nano, err := types.ParseDurationValue(unit, interval)
if err := handleInvalidTimeError(ctx, err); err != nil {
return types.Time{}, true, err
}

goTime, err := date.Time.GoTime(time.Local)
if err != nil {
return types.Time{}, true, errors.Trace(err)
if err := handleInvalidTimeError(ctx, err); err != nil {
return types.Time{}, true, err
}

duration := time.Duration(dur)
goTime = goTime.Add(duration)
goTime = goTime.Add(time.Duration(nano))
goTime = types.AddDate(year, month, day, goTime)

if goTime.Nanosecond() == 0 {
Expand All @@ -2654,7 +2653,7 @@ func (du *baseDateArithmitical) add(ctx sessionctx.Context, date types.Time, int

date.Time = types.FromGoTime(goTime)
overflow, err := types.DateTimeIsOverflow(ctx.GetSessionVars().StmtCtx, date)
if err != nil {
if err := handleInvalidTimeError(ctx, err); err != nil {
return types.Time{}, true, err
}
if overflow {
Expand All @@ -2664,18 +2663,18 @@ func (du *baseDateArithmitical) add(ctx sessionctx.Context, date types.Time, int
}

func (du *baseDateArithmitical) sub(ctx sessionctx.Context, date types.Time, interval string, unit string) (types.Time, bool, error) {
year, month, day, dur, err := types.ExtractTimeValue(unit, interval)
if err != nil {
return types.Time{}, true, errors.Trace(handleInvalidTimeError(ctx, err))
year, month, day, nano, err := types.ParseDurationValue(unit, interval)
if err := handleInvalidTimeError(ctx, err); err != nil {
return types.Time{}, true, err
}
year, month, day, dur = -year, -month, -day, -dur
year, month, day, nano = -year, -month, -day, -nano

goTime, err := date.Time.GoTime(time.Local)
if err != nil {
return types.Time{}, true, errors.Trace(err)
if err := handleInvalidTimeError(ctx, err); err != nil {
return types.Time{}, true, err
}

duration := time.Duration(dur)
duration := time.Duration(nano)
goTime = goTime.Add(duration)
goTime = types.AddDate(year, month, day, goTime)

Expand All @@ -2687,7 +2686,7 @@ func (du *baseDateArithmitical) sub(ctx sessionctx.Context, date types.Time, int

date.Time = types.FromGoTime(goTime)
overflow, err := types.DateTimeIsOverflow(ctx.GetSessionVars().StmtCtx, date)
if err != nil {
if err := handleInvalidTimeError(ctx, err); err != nil {
return types.Time{}, true, err
}
if overflow {
Expand Down
51 changes: 50 additions & 1 deletion expression/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1768,7 +1768,7 @@ func (s *testIntegrationSuite) TestTimeBuiltin(c *C) {
{"\"2011-11-11 00:00:00\"", "10", "MINUTE", "2011-11-11 00:10:00", "2011-11-10 23:50:00"},
{"\"2011-11-11 00:00:00\"", "10", "SECOND", "2011-11-11 00:00:10", "2011-11-10 23:59:50"},

{"\"2011-11-11\"", "\"abc1000\"", "MICROSECOND", "<nil>", "<nil>"},
{"\"2011-11-11\"", "\"abc1000\"", "MICROSECOND", "2011-11-11 00:00:00", "2011-11-11 00:00:00"},
{"\"20111111 10:10:10\"", "\"1\"", "DAY", "<nil>", "<nil>"},
{"\"2011-11-11\"", "\"10\"", "SECOND_MICROSECOND", "2011-11-11 00:00:00.100000", "2011-11-10 23:59:59.900000"},
{"\"2011-11-11\"", "\"10.0000\"", "MINUTE_MICROSECOND", "2011-11-11 00:00:10", "2011-11-10 23:59:50"},
Expand Down Expand Up @@ -3895,6 +3895,55 @@ where
tk.MustQuery(q)
}

func (s *testIntegrationSuite) TestIssue9727(c *C) {
tk := testkit.NewTestKit(c, s.store)
defer s.cleanEnv(c)

cases := []struct {
sql string
result string
}{
{`SELECT "1900-01-01 00:00:00" + INTERVAL "100000000:214748364700" MINUTE_SECOND;`, "8895-03-27 22:11:40"},
{`SELECT "1900-01-01 00:00:00" + INTERVAL 1 << 37 SECOND;`, "6255-04-08 15:04:32"},
{`SELECT "1900-01-01 00:00:00" + INTERVAL 1 << 31 MINUTE;`, "5983-01-24 02:08:00"},
{`SELECT "1900-01-01 00:00:00" + INTERVAL 1 << 38 SECOND;`, "<nil>"},
{`SELECT "1900-01-01 00:00:00" + INTERVAL 1 << 33 MINUTE;`, "<nil>"},
{`SELECT "1900-01-01 00:00:00" + INTERVAL 1 << 30 HOUR;`, "<nil>"},
{`SELECT "1900-01-01 00:00:00" + INTERVAL "1000000000:214748364700" MINUTE_SECOND;`, "<nil>"},
{`SELECT 19000101000000 + INTERVAL "100000000:214748364700" MINUTE_SECOND;`, "8895-03-27 22:11:40"},
{`SELECT 19000101000000 + INTERVAL 1 << 37 SECOND;`, "6255-04-08 15:04:32"},
{`SELECT 19000101000000 + INTERVAL 1 << 31 MINUTE;`, "5983-01-24 02:08:00"},

{`SELECT "8895-03-27 22:11:40" - INTERVAL "100000000:214748364700" MINUTE_SECOND;`, "1900-01-01 00:00:00"},
{`SELECT "6255-04-08 15:04:32" - INTERVAL 1 << 37 SECOND;`, "1900-01-01 00:00:00"},
{`SELECT "5983-01-24 02:08:00" - INTERVAL 1 << 31 MINUTE;`, "1900-01-01 00:00:00"},
{`SELECT "9999-01-01 00:00:00" - INTERVAL 1 << 39 SECOND;`, "<nil>"},
{`SELECT "9999-01-01 00:00:00" - INTERVAL 1 << 33 MINUTE;`, "<nil>"},
{`SELECT "9999-01-01 00:00:00" - INTERVAL 1 << 30 HOUR;`, "<nil>"},
{`SELECT "9999-01-01 00:00:00" - INTERVAL "10000000000:214748364700" MINUTE_SECOND;`, "<nil>"},
{`SELECT 88950327221140 - INTERVAL "100000000:214748364700" MINUTE_SECOND ;`, "1900-01-01 00:00:00"},
{`SELECT 62550408150432 - INTERVAL 1 << 37 SECOND;`, "1900-01-01 00:00:00"},
{`SELECT 59830124020800 - INTERVAL 1 << 31 MINUTE;`, "1900-01-01 00:00:00"},

{`SELECT 10000101000000 + INTERVAL "111111111111111111" MICROSECOND;`, `4520-12-21 05:31:51.111111`},
{`SELECT 10000101000000 + INTERVAL "111111111111.111111" SECOND;`, `4520-12-21 05:31:51.111111`},
{`SELECT 10000101000000 + INTERVAL "111111111111.111111111" SECOND;`, `4520-12-21 05:31:51.111111`},
{`SELECT 10000101000000 + INTERVAL "111111111111.111" SECOND;`, `4520-12-21 05:31:51.111000`},
{`SELECT 10000101000000 + INTERVAL "111111111111." SECOND;`, `4520-12-21 05:31:51`},
{`SELECT 10000101000000 + INTERVAL "111111111111111111.5" MICROSECOND;`, `4520-12-21 05:31:51.111112`},
{`SELECT 10000101000000 + INTERVAL "111111111111111112.5" MICROSECOND;`, `4520-12-21 05:31:51.111113`},
{`SELECT 10000101000000 + INTERVAL "111111111111111111.500000" MICROSECOND;`, `4520-12-21 05:31:51.111112`},
{`SELECT 10000101000000 + INTERVAL "111111111111111111.50000000" MICROSECOND;`, `4520-12-21 05:31:51.111112`},
{`SELECT 10000101000000 + INTERVAL "111111111111111111.6" MICROSECOND;`, `4520-12-21 05:31:51.111112`},
{`SELECT 10000101000000 + INTERVAL "111111111111111111.499999" MICROSECOND;`, `4520-12-21 05:31:51.111111`},
{`SELECT 10000101000000 + INTERVAL "111111111111111111.499999999999" MICROSECOND;`, `4520-12-21 05:31:51.111111`},
}

for _, c := range cases {
tk.MustQuery(c.sql).Check(testkit.Rows(c.result))
}
}

func (s *testIntegrationSuite) TestTimestampDatumEncode(c *C) {
tk := testkit.NewTestKit(c, s.store)
tk.MustExec("use test")
Expand Down
114 changes: 78 additions & 36 deletions types/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -1602,41 +1602,81 @@ func ExtractDurationNum(d *Duration, unit string) (int64, error) {
}
}

func extractSingleTimeValue(unit string, format string) (int64, int64, int64, float64, error) {
fv, err := strconv.ParseFloat(format, 64)
func parseSingleTimeValue(unit string, format string) (int64, int64, int64, int64, error) {
// Format is a preformatted number, it format should be A[.[B]].
decimalPointPos := strings.IndexRune(format, '.')
if decimalPointPos == -1 {
decimalPointPos = len(format)
}
sign := int64(1)
if len(format) > 0 && format[0] == '-' {
sign = int64(-1)
}
iv, err := strconv.ParseInt(format[0:decimalPointPos], 10, 64)
if err != nil {
return 0, 0, 0, 0, ErrIncorrectDatetimeValue.GenWithStackByArgs(format)
}
iv := int64(math.Round(fv))
riv := iv // Rounded integer value

dv := int64(0)
lf := len(format) - 1
// Has fraction part
if decimalPointPos < lf {
if lf-decimalPointPos >= 6 {
// MySQL rounds down to 1e-6.
if dv, err = strconv.ParseInt(format[decimalPointPos+1:decimalPointPos+7], 10, 64); err != nil {
return 0, 0, 0, 0, ErrIncorrectDatetimeValue.GenWithStackByArgs(format)
}
} else {
if dv, err = strconv.ParseInt(format[decimalPointPos+1:]+"000000"[:6-(lf-decimalPointPos)], 10, 64); err != nil {
return 0, 0, 0, 0, ErrIncorrectDatetimeValue.GenWithStackByArgs(format)
}
}
if dv >= 500000 { // Round up, and we should keep 6 digits for microsecond, so dv should in [000000, 999999].
riv += sign
}
if unit != "SECOND" {
err = ErrTruncatedWrongValue.GenWithStackByArgs(format)
}
}
const gotimeDay = 24 * gotime.Hour
switch strings.ToUpper(unit) {
case "MICROSECOND":
return 0, 0, 0, fv * float64(gotime.Microsecond), nil
dayCount := riv / int64(gotimeDay/gotime.Microsecond)
riv %= int64(gotimeDay / gotime.Microsecond)
return 0, 0, dayCount, riv * int64(gotime.Microsecond), err
case "SECOND":
return 0, 0, 0, fv * float64(gotime.Second), nil
dayCount := iv / int64(gotimeDay/gotime.Second)
iv %= int64(gotimeDay / gotime.Second)
return 0, 0, dayCount, iv*int64(gotime.Second) + dv*int64(gotime.Microsecond), err
case "MINUTE":
return 0, 0, 0, float64(iv * int64(gotime.Minute)), nil
dayCount := riv / int64(gotimeDay/gotime.Minute)
riv %= int64(gotimeDay / gotime.Minute)
return 0, 0, dayCount, riv * int64(gotime.Minute), err
case "HOUR":
return 0, 0, 0, float64(iv * int64(gotime.Hour)), nil
dayCount := riv / 24
riv %= 24
return 0, 0, dayCount, riv * int64(gotime.Hour), err
case "DAY":
return 0, 0, iv, 0, nil
return 0, 0, riv, 0, err
case "WEEK":
return 0, 0, 7 * iv, 0, nil
return 0, 0, 7 * riv, 0, err
case "MONTH":
return 0, iv, 0, 0, nil
return 0, riv, 0, 0, err
case "QUARTER":
return 0, 3 * iv, 0, 0, nil
return 0, 3 * riv, 0, 0, err
case "YEAR":
return iv, 0, 0, 0, nil
return riv, 0, 0, 0, err
}

return 0, 0, 0, 0, errors.Errorf("invalid singel timeunit - %s", unit)
}

// extractTimeValue extracts years, months, days, microseconds from a string
// parseTimeValue gets years, months, days, nanoseconds from a string
// nanosecond will not exceed length of single day
// MySQL permits any punctuation delimiter in the expr format.
// See https://dev.mysql.com/doc/refman/8.0/en/expressions.html#temporal-intervals
func extractTimeValue(format string, index, cnt int) (int64, int64, int64, float64, error) {
func parseTimeValue(format string, index, cnt int) (int64, int64, int64, int64, error) {
neg := false
originalFmt := format
format = strings.TrimSpace(format)
Expand Down Expand Up @@ -1674,55 +1714,57 @@ func extractTimeValue(format string, index, cnt int) (int64, int64, int64, float
return 0, 0, 0, 0, ErrIncorrectDatetimeValue.GenWithStackByArgs(originalFmt)
}

hours, err := strconv.ParseFloat(fields[HourIndex], 64)
hours, err := strconv.ParseInt(fields[HourIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, ErrIncorrectDatetimeValue.GenWithStackByArgs(originalFmt)
}
minutes, err := strconv.ParseFloat(fields[MinuteIndex], 64)
minutes, err := strconv.ParseInt(fields[MinuteIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, ErrIncorrectDatetimeValue.GenWithStackByArgs(originalFmt)
}
seconds, err := strconv.ParseFloat(fields[SecondIndex], 64)
seconds, err := strconv.ParseInt(fields[SecondIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, ErrIncorrectDatetimeValue.GenWithStackByArgs(originalFmt)
}
microseconds, err := strconv.ParseFloat(alignFrac(fields[MicrosecondIndex], MaxFsp), 64)
microseconds, err := strconv.ParseInt(alignFrac(fields[MicrosecondIndex], MaxFsp), 10, 64)
if err != nil {
return 0, 0, 0, 0, ErrIncorrectDatetimeValue.GenWithStackByArgs(originalFmt)
}
durations := hours*float64(gotime.Hour) + minutes*float64(gotime.Minute) +
seconds*float64(gotime.Second) + microseconds*float64(gotime.Microsecond)

return years, months, days, durations, nil
seconds = hours*3600 + minutes*60 + seconds
days += seconds / (3600 * 24)
seconds %= 3600 * 24
return years, months, days, seconds*int64(gotime.Second) + microseconds*int64(gotime.Microsecond), nil
}

// ExtractTimeValue extracts time value from time unit and format.
func ExtractTimeValue(unit string, format string) (int64, int64, int64, float64, error) {
// ParseDurationValue parses time value from time unit and format.
// Returns y years m months d days + n nanoseconds
// Nanoseconds will no longer than one day.
func ParseDurationValue(unit string, format string) (y int64, m int64, d int64, n int64, _ error) {
switch strings.ToUpper(unit) {
case "MICROSECOND", "SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "QUARTER", "YEAR":
return extractSingleTimeValue(unit, format)
return parseSingleTimeValue(unit, format)
case "SECOND_MICROSECOND":
return extractTimeValue(format, MicrosecondIndex, SecondMicrosecondMaxCnt)
return parseTimeValue(format, MicrosecondIndex, SecondMicrosecondMaxCnt)
case "MINUTE_MICROSECOND":
return extractTimeValue(format, MicrosecondIndex, MinuteMicrosecondMaxCnt)
return parseTimeValue(format, MicrosecondIndex, MinuteMicrosecondMaxCnt)
case "MINUTE_SECOND":
return extractTimeValue(format, SecondIndex, MinuteSecondMaxCnt)
return parseTimeValue(format, SecondIndex, MinuteSecondMaxCnt)
case "HOUR_MICROSECOND":
return extractTimeValue(format, MicrosecondIndex, HourMicrosecondMaxCnt)
return parseTimeValue(format, MicrosecondIndex, HourMicrosecondMaxCnt)
case "HOUR_SECOND":
return extractTimeValue(format, SecondIndex, HourSecondMaxCnt)
return parseTimeValue(format, SecondIndex, HourSecondMaxCnt)
case "HOUR_MINUTE":
return extractTimeValue(format, MinuteIndex, HourMinuteMaxCnt)
return parseTimeValue(format, MinuteIndex, HourMinuteMaxCnt)
case "DAY_MICROSECOND":
return extractTimeValue(format, MicrosecondIndex, DayMicrosecondMaxCnt)
return parseTimeValue(format, MicrosecondIndex, DayMicrosecondMaxCnt)
case "DAY_SECOND":
return extractTimeValue(format, SecondIndex, DaySecondMaxCnt)
return parseTimeValue(format, SecondIndex, DaySecondMaxCnt)
case "DAY_MINUTE":
return extractTimeValue(format, MinuteIndex, DayMinuteMaxCnt)
return parseTimeValue(format, MinuteIndex, DayMinuteMaxCnt)
case "DAY_HOUR":
return extractTimeValue(format, HourIndex, DayHourMaxCnt)
return parseTimeValue(format, HourIndex, DayHourMaxCnt)
case "YEAR_MONTH":
return extractTimeValue(format, MonthIndex, YearMonthMaxCnt)
return parseTimeValue(format, MonthIndex, YearMonthMaxCnt)
default:
return 0, 0, 0, 0, errors.Errorf("invalid singel timeunit - %s", unit)
}
Expand Down
Loading

0 comments on commit 6cb7bd1

Please sign in to comment.