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

expression: fix infinity loop in timestampadd | tidb-test=pr/2378 #54971

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions expression/builtin_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -2966,7 +2966,10 @@ func (du *baseDateArithmetical) addDate(ctx sessionctx.Context, date types.Time,
}

goTime = goTime.Add(time.Duration(nano))
goTime = types.AddDate(year, month, day, goTime)
goTime, err = types.AddDate(year, month, day, goTime)
if err != nil {
return types.ZeroTime, true, handleInvalidTimeError(ctx, types.ErrDatetimeFunctionOverflow.GenWithStackByArgs("datetime"))
}

// Adjust fsp as required by outer - always respect type inference.
date.SetFsp(resultFsp)
Expand Down Expand Up @@ -6186,13 +6189,11 @@ func addUnitToTime(unit string, t time.Time, v float64) (time.Time, bool, error)
if !validAddMonth(v, t.Year(), int(t.Month())) {
return tb, true, nil
}
tb = t.AddDate(0, int(v), 0)

// For corner case: timestampadd(month,1,date '2024-01-31') = "2024-02-29", timestampadd(month,1,date '2024-01-30') = "2024-02-29"
// `tb.Month()` refers to the actual result, `t.Month()+v` refers to the expect result.
// Actual result may be greater than expect result, we need to judge and modify it.
for int(tb.Month())%12 != (int(t.Month())+int(v))%12 {
tb = tb.AddDate(0, 0, -1)
var err error
tb, err = types.AddDate(0, int64(v), 0, t)
if err != nil {
return tb, true, err
}
case "QUARTER":
if !validAddMonth(v*3, t.Year(), int(t.Month())) {
Expand Down
42 changes: 41 additions & 1 deletion expression/builtin_time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2526,7 +2526,7 @@ func TestTimestampAdd(t *testing.T) {
{"MINUTE", 1.5, "1995-05-01 00:00:00.000000", "1995-05-01 00:02:00"},
{"MICROSECOND", -100, "1995-05-01 00:00:00.0001", "1995-05-01 00:00:00"},

// issue41052
// issue 41052
{"MONTH", 1, "2024-01-31", "2024-02-29 00:00:00"},
{"MONTH", 1, "2024-01-30", "2024-02-29 00:00:00"},
{"MONTH", 1, "2024-01-29", "2024-02-29 00:00:00"},
Expand All @@ -2537,6 +2537,46 @@ func TestTimestampAdd(t *testing.T) {
{"MONTH", 10, "2024-10-31", "2025-08-31 00:00:00"},
{"MONTH", 1, "2024-11-30", "2024-12-30 00:00:00"},
{"MONTH", 13, "2024-11-30", "2025-12-30 00:00:00"},

// issue 54908
{"MONTH", 0, "2024-09-01", "2024-09-01 00:00:00"},
{"MONTH", -10, "2024-09-01", "2023-11-01 00:00:00"},
{"MONTH", -2, "2024-04-28", "2024-02-28 00:00:00"},
{"MONTH", -2, "2024-04-29", "2024-02-29 00:00:00"},
{"MONTH", -2, "2024-04-30", "2024-02-29 00:00:00"},
{"MONTH", -1, "2024-03-28", "2024-02-28 00:00:00"},
{"MONTH", -1, "2024-03-29", "2024-02-29 00:00:00"},
{"MONTH", -1, "2024-03-30", "2024-02-29 00:00:00"},
{"MONTH", -1, "2024-03-31", "2024-02-29 00:00:00"},
{"MONTH", -1, "2024-03-25", "2024-02-25 00:00:00"},
{"MONTH", -12, "2024-03-31", "2023-03-31 00:00:00"},
{"MONTH", -13, "2024-03-31", "2023-02-28 00:00:00"},
{"MONTH", -14, "2024-03-31", "2023-01-31 00:00:00"},
{"MONTH", -24, "2024-03-31", "2022-03-31 00:00:00"},
{"MONTH", -25, "2024-03-31", "2022-02-28 00:00:00"},
{"MONTH", -26, "2024-03-31", "2022-01-31 00:00:00"},
{"MONTH", -1, "2024-02-25", "2024-01-25 00:00:00"},
{"MONTH", -11, "2025-02-28", "2024-03-28 00:00:00"},
{"MONTH", -12, "2025-02-28", "2024-02-28 00:00:00"},
{"MONTH", -13, "2025-02-28", "2024-01-28 00:00:00"},
{"MONTH", -11, "2024-02-29", "2023-03-29 00:00:00"},
{"MONTH", -12, "2024-02-29", "2023-02-28 00:00:00"},
{"MONTH", -13, "2024-02-29", "2023-01-29 00:00:00"},
{"MONTH", -11, "2023-02-28", "2022-03-28 00:00:00"},
{"MONTH", -12, "2023-02-28", "2022-02-28 00:00:00"},
{"MONTH", -13, "2023-02-28", "2022-01-28 00:00:00"},
{"MONTH", -2, "2023-02-28", "2022-12-28 00:00:00"},
{"MONTH", -14, "2023-02-28", "2021-12-28 00:00:00"},
{"MONTH", -3, "2023-03-20", "2022-12-20 00:00:00"},
{"MONTH", -3, "2023-03-31", "2022-12-31 00:00:00"},
{"MONTH", -15, "2023-03-20", "2021-12-20 00:00:00"},
{"MONTH", -15, "2023-03-31", "2021-12-31 00:00:00"},
{"MONTH", 12, "2020-02-29", "2021-02-28 00:00:00"},
{"MONTH", -12, "2020-02-29", "2019-02-28 00:00:00"},
{"MONTH", 10000*365 + 1, "2024-10-29", ""},
{"MONTH", -10000*365 - 1, "2024-10-29", ""},
{"MONTH", 3, "9999-10-29", ""},
{"MONTH", -3, "0001-01-29", ""},
}

fc := funcs[ast.TimestampAdd]
Expand Down
20 changes: 18 additions & 2 deletions types/core_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,30 @@ func compareTime(a, b CoreTime) int {
// Dig it and we found it's caused by golang api time.Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time ,
// it says October 32 converts to November 1 ,it conflicts with mysql.
// See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-add
func AddDate(year, month, day int64, ot gotime.Time) (nt gotime.Time) {
func AddDate(year, month, day int64, ot gotime.Time) (nt gotime.Time, _ error) {
// We must limit the range of year, month and day to avoid overflow.
// The datetime range is from '1000-01-01 00:00:00.000000' to '9999-12-31 23:59:59.499999',
// so it is safe to limit the added value from -10000*365 to 10000*365.
const maxAdd = 10000 * 365
const minAdd = -maxAdd
if year > maxAdd || year < minAdd ||
month > maxAdd || month < minAdd ||
day > maxAdd || day < minAdd {
return nt, ErrDatetimeFunctionOverflow.GenWithStackByArgs("datetime")
}

df := getFixDays(int(year), int(month), int(day), ot)
if df != 0 {
nt = ot.AddDate(int(year), int(month), df)
} else {
nt = ot.AddDate(int(year), int(month), int(day))
}
return nt

if nt.Year() < 0 || nt.Year() > 9999 {
return nt, ErrDatetimeFunctionOverflow.GenWithStackByArgs("datetime")
}

return nt, nil
}

func calcTimeFromSec(to *CoreTime, seconds, microseconds int) {
Expand Down
2 changes: 1 addition & 1 deletion types/core_time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func TestAddDate(t *testing.T) {
}

for _, tt := range tests {
res := AddDate(int64(tt.year), int64(tt.month), int64(tt.day), tt.ot)
res, _ := AddDate(int64(tt.year), int64(tt.month), int64(tt.day), tt.ot)
require.Equal(t, tt.year+tt.ot.Year(), res.Year())
}
}
Expand Down
54 changes: 30 additions & 24 deletions types/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -2319,16 +2319,17 @@ func parseSingleTimeValue(unit string, format string, strictCheck bool) (year in
lf := len(format) - 1
// Has fraction part
if decimalPointPos < lf {
var tmpErr error
dvPre := oneToSixDigitRegex.FindString(format[decimalPointPos+1:]) // the numberical prefix of the fraction part
decimalLen = len(dvPre)
if decimalLen >= 6 {
// MySQL rounds down to 1e-6.
if dv, err = strconv.ParseInt(dvPre[0:6], 10, 64); err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, format)
if dv, tmpErr = strconv.ParseInt(dvPre[0:6], 10, 64); tmpErr != nil {
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, format)
}
} else {
if dv, err = strconv.ParseInt(dvPre+"000000"[:6-decimalLen], 10, 64); err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, format)
if dv, tmpErr = strconv.ParseInt(dvPre+"000000"[:6-decimalLen], 10, 64); tmpErr != nil {
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, format)
}
}
if dv >= 500000 { // Round up, and we should keep 6 digits for microsecond, so dv should in [000000, 999999].
Expand Down Expand Up @@ -2428,39 +2429,44 @@ func parseTimeValue(format string, index, cnt int) (years int64, months int64, d
index--
}

// ParseInt may return an error when overflowed, but we should continue to parse the rest of the string because
// the caller may ignore the error and use the return value.
// In this case, we should return a big value to make sure the result date after adding this interval
// is also overflowed and NULL is returned to the user.
years, err = strconv.ParseInt(fields[YearIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
}
months, err = strconv.ParseInt(fields[MonthIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
var tmpErr error
months, tmpErr = strconv.ParseInt(fields[MonthIndex], 10, 64)
if err == nil && tmpErr != nil {
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
}
days, err = strconv.ParseInt(fields[DayIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
days, tmpErr = strconv.ParseInt(fields[DayIndex], 10, 64)
if err == nil && tmpErr != nil {
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
}

hours, err := strconv.ParseInt(fields[HourIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
hours, tmpErr := strconv.ParseInt(fields[HourIndex], 10, 64)
if tmpErr != nil && err == nil {
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
}
minutes, err := strconv.ParseInt(fields[MinuteIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
minutes, tmpErr := strconv.ParseInt(fields[MinuteIndex], 10, 64)
if tmpErr != nil && err == nil {
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
}
seconds, err := strconv.ParseInt(fields[SecondIndex], 10, 64)
if err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
seconds, tmpErr := strconv.ParseInt(fields[SecondIndex], 10, 64)
if tmpErr != nil && err == nil {
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
}
microseconds, err := strconv.ParseInt(alignFrac(fields[MicrosecondIndex], MaxFsp), 10, 64)
if err != nil {
return 0, 0, 0, 0, 0, ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
microseconds, tmpErr := strconv.ParseInt(alignFrac(fields[MicrosecondIndex], MaxFsp), 10, 64)
if tmpErr != nil && err == nil {
err = ErrWrongValue.GenWithStackByArgs(DateTimeStr, originalFmt)
}
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), fsp, nil
return years, months, days, seconds*int64(gotime.Second) + microseconds*int64(gotime.Microsecond), fsp, err
}

func parseAndValidateDurationValue(format string, index, cnt int) (int64, int, error) {
Expand Down