Skip to content

Commit 679d4d6

Browse files
authored
Merge pull request #3049 from dolthub/elianddb/fix-7300-date-time-overflow-underflow
Fix 7300 date time overflow underflow
2 parents 635d803 + b37fbd7 commit 679d4d6

File tree

7 files changed

+177
-70
lines changed

7 files changed

+177
-70
lines changed

enginetest/queries/queries.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4830,6 +4830,47 @@ SELECT * FROM cte WHERE d = 2;`,
48304830
Query: "SELECT subdate(da, f32/10) from typestable;",
48314831
Expected: []sql.Row{{time.Date(2019, time.December, 30, 0, 0, 0, 0, time.UTC)}},
48324832
},
4833+
{
4834+
Query: "SELECT date_add('4444-01-01', INTERVAL 5400000 DAY);",
4835+
Expected: []sql.Row{{nil}},
4836+
},
4837+
{
4838+
Query: "SELECT date_add('4444-01-01', INTERVAL -5300000 DAY);",
4839+
Expected: []sql.Row{{nil}},
4840+
},
4841+
{
4842+
Query: "SELECT subdate('2008-01-02', 12e10);",
4843+
Expected: []sql.Row{{nil}},
4844+
},
4845+
{
4846+
Query: "SELECT date_add('2008-01-02', INTERVAL 1000000 day);",
4847+
Expected: []sql.Row{{"4745-11-29"}},
4848+
},
4849+
{
4850+
Query: "SELECT subdate('2008-01-02', INTERVAL 700000 day);",
4851+
Expected: []sql.Row{{"0091-06-20"}},
4852+
},
4853+
{
4854+
Query: "SELECT date_add('0000-01-01:01:00:00', INTERVAL 0 day);",
4855+
// MYSQL uses a proleptic gregorian, however, Go's time package does normal gregorian.
4856+
Expected: []sql.Row{{"0000-01-01 01:00:00"}},
4857+
},
4858+
{
4859+
Query: "SELECT date_add('9999-12-31:23:59:59.9999994', INTERVAL 0 day);",
4860+
Expected: []sql.Row{{"9999-12-31 23:59:59.999999"}},
4861+
},
4862+
{
4863+
Query: "SELECT date_add('9999-12-31:23:59:59.9999995', INTERVAL 0 day);",
4864+
Expected: []sql.Row{{nil}},
4865+
},
4866+
{
4867+
Query: "SELECT date_add('9999-12-31:23:59:59.99999945', INTERVAL 0 day);",
4868+
Expected: []sql.Row{{"9999-12-31 23:59:59.999999"}},
4869+
},
4870+
{
4871+
Query: "SELECT date_add('9999-12-31:23:59:59.99999944444444444-', INTERVAL 0 day);",
4872+
Expected: []sql.Row{{nil}},
4873+
},
48334874
{
48344875
Query: `SELECT * FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM othertable) othertable_one) othertable_two) othertable_three WHERE s2 = 'first'`,
48354876
Expected: []sql.Row{

sql/expression/function/time_math.go

Lines changed: 2 additions & 2 deletions
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.DatetimeMaxRange.Convert(ctx, date)
236236
if err != nil {
237237
ctx.Warn(1292, err.Error())
238238
return nil, nil
@@ -380,7 +380,7 @@ func (d *DateSub) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
380380
}
381381

382382
var dateVal interface{}
383-
dateVal, _, err = types.DatetimeMaxPrecision.Convert(ctx, date)
383+
dateVal, _, err = types.DatetimeMaxRange.Convert(ctx, date)
384384
if err != nil {
385385
ctx.Warn(1292, err.Error())
386386
return nil, nil

sql/expression/interval.go

Lines changed: 48 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -237,72 +237,69 @@ const (
237237
week = 7 * day
238238
)
239239

240-
func (td TimeDelta) apply(t time.Time, sign int64) time.Time {
241-
y := int64(t.Year())
242-
mo := int64(t.Month())
243-
d := t.Day()
244-
h := t.Hour()
245-
min := t.Minute()
246-
s := t.Second()
247-
ns := t.Nanosecond()
240+
// isLeapYear determines if a given year is a leap year
241+
func isLeapYear(year int) bool {
242+
return daysInMonth(year, time.February) == 29
243+
}
248244

249-
if td.Years != 0 {
250-
y += td.Years * sign
251-
}
245+
// daysInMonth returns the number of days in a given month/year combination
246+
func daysInMonth(year int, month time.Month) int {
247+
return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
248+
}
252249

253-
if td.Months != 0 {
254-
m := mo + td.Months*sign
255-
if m < 1 {
256-
mo = 12 + (m % 12)
257-
y += m/12 - 1
258-
} else if m > 12 {
259-
mo = m % 12
260-
y += m / 12
250+
// apply applies the time delta to the given time, using the specified sign
251+
func (td TimeDelta) apply(t time.Time, sign int64) time.Time {
252+
if td.Years != 0 {
253+
targetYear := t.Year() + int(td.Years*sign)
254+
255+
// special handling for Feb 29 on leap years
256+
if t.Month() == time.February && t.Day() == 29 && !isLeapYear(targetYear) {
257+
// if we're on Feb 29 and target year is not a leap year,
258+
// move to Feb 28
259+
t = time.Date(targetYear, time.February, 28,
260+
t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
261261
} else {
262-
mo = m
263-
}
264-
265-
// Due to the operations done before, month may be zero, which means it's
266-
// december.
267-
if mo == 0 {
268-
mo = 12
262+
t = time.Date(targetYear, t.Month(), t.Day(),
263+
t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
269264
}
270265
}
271266

272-
if days := daysInMonth(time.Month(mo), int(y)); days < d {
273-
d = days
274-
}
275-
276-
date := time.Date(int(y), time.Month(mo), d, h, min, s, ns, t.Location())
267+
if td.Months != 0 {
268+
totalMonths := int(t.Month()) - 1 + int(td.Months*sign) // convert to 0-based
277269

278-
if td.Days != 0 {
279-
date = date.Add(time.Duration(td.Days) * day * time.Duration(sign))
280-
}
270+
// calculate target year and month
271+
yearOffset := totalMonths / 12
272+
if totalMonths < 0 {
273+
yearOffset = (totalMonths - 11) / 12 // handle negative division correctly
274+
}
275+
targetYear := t.Year() + yearOffset
276+
targetMonth := time.Month((totalMonths%12+12)%12 + 1) // ensure positive month
281277

282-
if td.Hours != 0 {
283-
date = date.Add(time.Duration(td.Hours) * time.Hour * time.Duration(sign))
284-
}
278+
// handle end-of-month edge cases
279+
originalDay := t.Day()
280+
maxDaysInTargetMonth := daysInMonth(targetYear, targetMonth)
285281

286-
if td.Minutes != 0 {
287-
date = date.Add(time.Duration(td.Minutes) * time.Minute * time.Duration(sign))
288-
}
282+
targetDay := originalDay
283+
if originalDay > maxDaysInTargetMonth {
284+
targetDay = maxDaysInTargetMonth
285+
}
289286

290-
if td.Seconds != 0 {
291-
date = date.Add(time.Duration(td.Seconds) * time.Second * time.Duration(sign))
287+
t = time.Date(targetYear, targetMonth, targetDay,
288+
t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
292289
}
293290

294-
if td.Microseconds != 0 {
295-
date = date.Add(time.Duration(td.Microseconds) * time.Microsecond * time.Duration(sign))
291+
if td.Days != 0 {
292+
t = t.AddDate(0, 0, int(td.Days*sign))
296293
}
297294

298-
return date
299-
}
295+
duration := time.Duration(td.Hours*sign)*time.Hour +
296+
time.Duration(td.Minutes*sign)*time.Minute +
297+
time.Duration(td.Seconds*sign)*time.Second +
298+
time.Duration(td.Microseconds*sign)*time.Microsecond
300299

301-
func daysInMonth(month time.Month, year int) int {
302-
if month == time.December {
303-
return 31
300+
if duration != 0 {
301+
t = t.Add(duration)
304302
}
305303

306-
date := time.Date(year, month+time.Month(1), 1, 0, 0, 0, 0, time.Local)
307-
return date.Add(-1 * day).Day()
304+
return t
308305
}

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/rowexec/insert.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ func getFieldIndexFromUpdateExpr(updateExpr sql.Expression) (int, bool) {
261261

262262
// resolveValues resolves all VALUES functions.
263263
func (i *insertIter) resolveValues(ctx *sql.Context, insertRow sql.Row) error {
264+
// if vals empty then no need to resolve
264265
for _, updateExpr := range i.updateExprs {
265266
var err error
266267
sql.Inspect(updateExpr, func(expr sql.Expression) bool {

sql/types/datetime.go

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,30 @@ var (
3939

4040
ErrConvertingToTimeOutOfRange = errors.NewKind("value %q is outside of %v range")
4141

42-
// datetimeTypeMaxDatetime is the maximum representable Datetime/Date value.
43-
datetimeTypeMaxDatetime = time.Date(9999, 12, 31, 23, 59, 59, 999999000, time.UTC)
42+
// datetimeTypeMaxDatetime is the maximum representable Datetime/Date value. MYSQL: 9999-12-31 23:59:59.499999 (microseconds)
43+
datetimeTypeMaxDatetime = time.Date(9999, 12, 31, 23, 59, 59, 499999000, time.UTC)
4444

45-
// datetimeTypeMinDatetime is the minimum representable Datetime/Date value.
46-
datetimeTypeMinDatetime = time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC)
45+
// datetimeTypeMinDatetime is the minimum representable Datetime/Date value. MYSQL: 1000-01-01 00:00:00.000000 (microseconds)
46+
datetimeTypeMinDatetime = time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC)
4747

48-
// datetimeTypeMaxTimestamp is the maximum representable Timestamp value, which is the maximum 32-bit integer as a Unix time.
48+
// datetimeTypeMaxTimestamp is the maximum representable Timestamp value, MYSQL: 2038-01-19 03:14:07.999999 (microseconds)
4949
datetimeTypeMaxTimestamp = time.Unix(math.MaxInt32, 999999000)
5050

51-
// datetimeTypeMinTimestamp is the minimum representable Timestamp value, which is one second past the epoch.
51+
// datetimeTypeMinTimestamp is the minimum representable Timestamp value, MYSQL: 1970-01-01 00:00:01.000000 (microseconds)
5252
datetimeTypeMinTimestamp = time.Unix(1, 0)
5353

54+
datetimeTypeMaxDate = time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)
55+
56+
// datetimeTypeMinDate is the minimum representable Date value, MYSQL: 1000-01-01 00:00:00.000000 (microseconds)
57+
datetimeTypeMinDate = time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC)
58+
59+
// The MAX and MIN are extrapolated from commit ff05628a530 in the MySQL source code from my_time.cc
60+
// datetimeMaxTime is the maximum representable time value, MYSQL: 9999-12-31 23:59:59.999999 (microseconds)
61+
datetimeMaxTime = time.Date(9999, 12, 31, 23, 59, 59, 999999000, time.UTC)
62+
63+
// datetimeMinTime is the minimum representable time value, MYSQL: 0000-01-01 00:00:00.000000 (microseconds)
64+
datetimeMinTime = time.Date(0000, 0, 0, 0, 0, 0, 0, time.UTC)
65+
5466
DateOnlyLayouts = []string{
5567
"20060102",
5668
"2006-1-2",
@@ -71,8 +83,9 @@ var (
7183
"2006-01-02 15:04:",
7284
"2006-01-02 15:04:.",
7385
"2006-01-02 15:04:05.",
74-
"2006-01-02 15:04:05.999999",
75-
"2006-1-2 15:4:5.999999",
86+
"2006-01-02 15:04:05.999999999",
87+
"2006-1-2 15:4:5.999999999",
88+
"2006-1-2:15:4:5.999999999",
7689
"2006-01-02T15:04:05",
7790
"20060102150405",
7891
"2006-01-02 15:04:05.999999999 -0700 MST", // represents standard Time.time.UTC()
@@ -91,6 +104,8 @@ var (
91104
Timestamp = MustCreateDatetimeType(sqltypes.Timestamp, 0)
92105
// TimestampMaxPrecision is a UNIX timestamp with maximum precision
93106
TimestampMaxPrecision = MustCreateDatetimeType(sqltypes.Timestamp, 6)
107+
// DatetimeMaxRange is a date and a time with maximum precision and maximum range.
108+
DatetimeMaxRange = MustCreateDatetimeType(sqltypes.Datetime, 6)
94109

95110
datetimeValueType = reflect.TypeOf(time.Time{})
96111
)
@@ -200,9 +215,20 @@ func ConvertToTime(ctx context.Context, v interface{}, t datetimeType) (time.Tim
200215
}
201216

202217
// Round the date to the precision of this type
203-
truncationDuration := time.Second
204-
truncationDuration /= time.Duration(precisionConversion[t.precision])
205-
res = res.Round(truncationDuration)
218+
if t.precision < 6 {
219+
truncationDuration := time.Second / time.Duration(precisionConversion[t.precision])
220+
res = res.Round(truncationDuration)
221+
} else {
222+
res = res.Round(time.Microsecond)
223+
}
224+
225+
if t == DatetimeMaxRange {
226+
validated := ValidateTime(res)
227+
if validated == nil {
228+
return time.Time{}, ErrConvertingToTimeOutOfRange.New(v, t)
229+
}
230+
return validated.(time.Time), nil
231+
}
206232

207233
switch t.baseType {
208234
case sqltypes.Date:
@@ -214,10 +240,11 @@ func ConvertToTime(ctx context.Context, v interface{}, t datetimeType) (time.Tim
214240
return time.Time{}, ErrConvertingToTimeOutOfRange.New(res.Format(sql.TimestampDatetimeLayout), t.String())
215241
}
216242
case sqltypes.Timestamp:
217-
if res.Before(datetimeTypeMinTimestamp) || res.After(datetimeTypeMaxTimestamp) {
243+
if ValidateTimestamp(res) == nil {
218244
return time.Time{}, ErrConvertingToTimeOutOfRange.New(res.Format(sql.TimestampDatetimeLayout), t.String())
219245
}
220246
}
247+
221248
return res, nil
222249
}
223250

@@ -338,8 +365,8 @@ func (t datetimeType) ConvertWithoutRangeCheck(ctx context.Context, v interface{
338365
}
339366

340367
func parseDatetime(value string) (time.Time, bool) {
341-
for _, fmt := range TimestampDatetimeLayouts {
342-
if t, err := time.Parse(fmt, value); err == nil {
368+
for _, layout := range TimestampDatetimeLayouts {
369+
if t, err := time.Parse(layout, value); err == nil {
343370
return t.UTC(), true
344371
}
345372
}
@@ -473,7 +500,16 @@ func (t datetimeType) MinimumTime() time.Time {
473500
// ValidateTime receives a time and returns either that time or nil if it's
474501
// not a valid time.
475502
func ValidateTime(t time.Time) interface{} {
476-
if t.After(time.Date(9999, time.December, 31, 23, 59, 59, 999999999, time.UTC)) {
503+
if t.Before(datetimeMinTime) || t.After(datetimeMaxTime) {
504+
return nil
505+
}
506+
return t
507+
}
508+
509+
// ValidateTimestamp receives a time and returns either that time or nil if it's
510+
// not a valid timestamp.
511+
func ValidateTimestamp(t time.Time) interface{} {
512+
if t.Before(datetimeTypeMinTimestamp) || t.After(datetimeTypeMaxTimestamp) {
477513
return nil
478514
}
479515
return t

sql/types/datetime_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,35 @@ func TestDatetimeZero(t *testing.T) {
405405
_, ok = MustCreateDatetimeType(sqltypes.Timestamp, 0).Zero().(time.Time)
406406
require.True(t, ok)
407407
}
408+
409+
func TestDatetimeOverflowUnderflow(t *testing.T) {
410+
ctx := sql.NewEmptyContext()
411+
tests := []struct {
412+
typ sql.DatetimeType
413+
val interface{}
414+
expectError bool
415+
}{
416+
{Timestamp, "1969-12-31 23:59:59", true},
417+
{Timestamp, "2038-01-19 03:14:08", true},
418+
{Date, Date.MinimumTime().Format("2006-01-02"), false},
419+
{Date, Date.MaximumTime().Format("2006-01-02"), false},
420+
{Datetime, Datetime.MinimumTime().Format("2006-01-02 15:04:05"), false},
421+
{Datetime, Datetime.MaximumTime().Format("2006-01-02 15:04:05"), false},
422+
{Timestamp, Timestamp.MinimumTime().Format("2006-01-02 15:04:05"), false},
423+
{Timestamp, Timestamp.MaximumTime().Format("2006-01-02 15:04:05"), false},
424+
}
425+
426+
for _, tt := range tests {
427+
t.Run(tt.typ.String()+"_"+tt.val.(string), func(t *testing.T) {
428+
_, inRange, err := tt.typ.Convert(ctx, tt.val)
429+
430+
if tt.expectError {
431+
require.True(t, err != nil || inRange == sql.OutOfRange,
432+
"expected error or out-of-range but got neither; err: %v, inRange: %v", err, inRange)
433+
} else {
434+
require.NoError(t, err)
435+
require.Equal(t, sql.InRange, inRange)
436+
}
437+
})
438+
}
439+
}

0 commit comments

Comments
 (0)