Skip to content

Commit 7157226

Browse files
committed
fix leap year errs
fix err in existing test
1 parent dc7518b commit 7157226

File tree

4 files changed

+85
-13
lines changed

4 files changed

+85
-13
lines changed

sql/expression/function/time_math.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ func (d *DateAdd) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
232232
}
233233

234234
var dateVal interface{}
235-
dateVal, _, err = types.DatetimeMaxPrecision.Convert(ctx, date)
235+
dateVal, _, err = types.DatetimeMaxLimit.Convert(ctx, date)
236236
if err != nil {
237237
ctx.Warn(1292, err.Error())
238238
return nil, nil

sql/expression/interval.go

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,15 +237,62 @@ const (
237237
week = 7 * day
238238
)
239239

240+
// isLeapYear determines if a given year is a leap year
241+
// Uses Go's built-in date handling for accuracy
242+
func isLeapYear(year int) bool {
243+
return daysInMonth(year, time.February) == 29
244+
}
245+
246+
// daysInMonth returns the number of days in a given month/year combination
247+
// Uses Go's built-in date handling: day 0 of next month = last day of current month
248+
func daysInMonth(year int, month time.Month) int {
249+
return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
250+
}
251+
240252
func (td TimeDelta) apply(t time.Time, sign int64) time.Time {
241-
// add years, months, days using AddDate (handles normalization)
242-
t = t.AddDate(
243-
int(td.Years*sign),
244-
int(td.Months*sign),
245-
int(td.Days*sign),
246-
)
247-
248-
// add hours, minutes, seconds, microseconds
253+
if td.Years != 0 {
254+
targetYear := t.Year() + int(td.Years*sign)
255+
256+
// Special handling for Feb 29 on leap years
257+
if t.Month() == time.February && t.Day() == 29 && !isLeapYear(targetYear) {
258+
// If we're on Feb 29 and target year is not a leap year,
259+
// move to Feb 28
260+
t = time.Date(targetYear, time.February, 28,
261+
t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
262+
} else {
263+
t = time.Date(targetYear, t.Month(), t.Day(),
264+
t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
265+
}
266+
}
267+
268+
if td.Months != 0 {
269+
totalMonths := int(t.Month()) - 1 + int(td.Months*sign) // Convert to 0-based
270+
271+
// Calculate target year and month
272+
yearOffset := totalMonths / 12
273+
if totalMonths < 0 {
274+
yearOffset = (totalMonths - 11) / 12 // Handle negative division correctly
275+
}
276+
targetYear := t.Year() + yearOffset
277+
targetMonth := time.Month((totalMonths%12+12)%12 + 1) // Ensure positive month
278+
279+
// Handle end-of-month edge cases
280+
originalDay := t.Day()
281+
maxDaysInTargetMonth := daysInMonth(targetYear, targetMonth)
282+
283+
targetDay := originalDay
284+
if originalDay > maxDaysInTargetMonth {
285+
targetDay = maxDaysInTargetMonth
286+
}
287+
288+
t = time.Date(targetYear, targetMonth, targetDay,
289+
t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
290+
}
291+
292+
if td.Days != 0 {
293+
t = t.AddDate(0, 0, int(td.Days*sign))
294+
}
295+
249296
duration := time.Duration(td.Hours*sign)*time.Hour +
250297
time.Duration(td.Minutes*sign)*time.Minute +
251298
time.Duration(td.Seconds*sign)*time.Second +

sql/expression/interval_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ func TestTimeDelta(t *testing.T) {
5151
date(2005, time.March, 29, 0, 0, 0, 0),
5252
},
5353
{
54-
"plus overflowing until december",
54+
"plus overflowing until december", // #7300 mysql produced 2005-12-29
5555
TimeDelta{Months: 22},
5656
leapYear,
57-
date(2006, time.December, 29, 0, 0, 0, 0),
57+
date(2005, time.December, 29, 0, 0, 0, 0),
5858
},
5959
{
6060
"minus overflowing months",

sql/types/datetime.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ var (
103103
Timestamp = MustCreateDatetimeType(sqltypes.Timestamp, 0)
104104
// TimestampMaxPrecision is a UNIX timestamp with maximum precision
105105
TimestampMaxPrecision = MustCreateDatetimeType(sqltypes.Timestamp, 6)
106+
// DatetimeMaxLimit is a date and a time with maximum precision and maximum range.
107+
DatetimeMaxLimit = MustCreateDatetimeType(sqltypes.Datetime, 6)
106108

107109
datetimeValueType = reflect.TypeOf(time.Time{})
108110
)
@@ -207,11 +209,34 @@ func ConvertToTime(ctx context.Context, v interface{}, t datetimeType) (time.Tim
207209
return time.Time{}, err
208210
}
209211

212+
if t == DatetimeMaxLimit {
213+
validated := ValidateTime(res)
214+
if validated == nil {
215+
return time.Time{}, ErrConvertingToTimeOutOfRange.New(v, t)
216+
}
217+
return validated.(time.Time), nil
218+
}
219+
220+
switch t.baseType {
221+
case sqltypes.Date:
222+
if ValidateDate(res) == nil {
223+
return time.Time{}, ErrConvertingToTimeOutOfRange.New(v, t)
224+
}
225+
case sqltypes.Datetime:
226+
if ValidateDatetime(res) == nil {
227+
return time.Time{}, ErrConvertingToTimeOutOfRange.New(v, t)
228+
}
229+
case sqltypes.Timestamp:
230+
if ValidateTimestamp(res) == nil {
231+
return time.Time{}, ErrConvertingToTimeOutOfRange.New(v, t)
232+
}
233+
}
234+
210235
if res.Equal(zeroTime) {
211236
return zeroTime, nil
212237
}
213238

214-
return res.Round(time.Microsecond), nil
239+
return res, nil
215240
}
216241

217242
// ConvertWithoutRangeCheck converts the parameter to time.Time without checking the range.
@@ -234,6 +259,7 @@ func (t datetimeType) ConvertWithoutRangeCheck(ctx context.Context, v interface{
234259
// TODO: consider not using time.Parse if we want to match MySQL exactly ('2010-06-03 11:22.:.:.:.:' is a valid timestamp)
235260
var parsed bool
236261
res, parsed = parseDatetime(value)
262+
res = res.Round(time.Microsecond)
237263
if !parsed {
238264
return zeroTime, ErrConvertingToTime.New(v)
239265
}
@@ -469,7 +495,6 @@ func ValidateTime(t time.Time) interface{} {
469495
if t.Before(datetimeMinTime) || t.After(datetimeMaxTime) {
470496
return nil
471497
}
472-
473498
return t
474499
}
475500

0 commit comments

Comments
 (0)