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,types: Adjusts UNIX_TIMESTAMP() for non-existing DST values (#28739) #30405

Merged
merged 10 commits into from
Dec 9, 2021
2 changes: 1 addition & 1 deletion expression/builtin_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -4856,7 +4856,7 @@ func (b *builtinUnixTimestampIntSig) evalIntWithCtx(ctx sessionctx.Context, row
}

tz := ctx.GetSessionVars().Location()
t, err := val.GoTime(tz)
t, err := val.AdjustedGoTime(tz)
mjonss marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return 0, false, nil
}
Expand Down
4 changes: 4 additions & 0 deletions expression/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2020,6 +2020,10 @@ func (s *testIntegrationSuite2) TestTimeBuiltin(c *C) {
result.Check(testkit.Rows("1447410019.012 1447410019.0123"))
result = tk.MustQuery("SELECT UNIX_TIMESTAMP('2038-01-19 11:14:07.999999');")
result.Check(testkit.Rows("2147483647.999999"))
tk.MustExec("SET time_zone = 'Europe/Vilnius'")
tk.MustQuery("SELECT UNIX_TIMESTAMP('2020-03-29 03:45:00')").Check(testkit.Rows("1585443600"))
tk.MustQuery("SELECT FROM_UNIXTIME(UNIX_TIMESTAMP('2020-03-29 03:45:00'))").Check(testkit.Rows("2020-03-29 04:00:00"))
tk.MustExec("SET time_zone = DEFAULT")

result = tk.MustQuery("SELECT TIME_FORMAT('bad string', '%H:%i:%s %p');")
result.Check(testkit.Rows("<nil>"))
Expand Down
53 changes: 53 additions & 0 deletions types/core_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,59 @@ func (t CoreTime) GoTime(loc *gotime.Location) (gotime.Time, error) {
return tm, nil
}

// FindZoneTransition check for one Time Zone transition within +/- 4h
// Currently the needed functions are not exported, if gotime.Location.lookup would be exported
// then it would be easy to use that directly
func FindZoneTransition(tIn gotime.Time, loc *gotime.Location) (gotime.Time, error) {
mjonss marked this conversation as resolved.
Show resolved Hide resolved
// Check most common case first, DST transition on full hour.
// round truncates away from zero!
t2 := tIn.Round(gotime.Hour).Add(-1 * gotime.Hour)
t1 := t2.Add(-1 * gotime.Second)
_, offset1 := t1.Zone()
_, offset2 := t2.Zone()
if offset1 != offset2 {
return t2, nil
}

// Check if any offset change?
t1 = tIn.Add(-4 * gotime.Hour)
t2 = tIn.Add(4 * gotime.Hour)
_, offset1 = t1.Zone()
_, offset2 = t2.Zone()
if offset1 == offset2 {
return tIn, errors.Trace(ErrWrongValue.GenWithStackByArgs(TimeStr, tIn))
}

// Check generic case, like for 'Australia/Lord_Howe'
for t2.After(t1.Add(gotime.Second)) {
t := t1.Add(t2.Sub(t1) / 2).Round(gotime.Second)
_, offset := t.Zone()
if offset == offset1 {
t1 = t
} else {
t2 = t
}
}
return t2, nil
}

// AdjustedGoTime converts Time to GoTime and adjust for invalid DST times
// like during the DST change with increased offset,
// normally moving to Daylight Saving Time.
// see https://github.com/pingcap/tidb/issues/28739
func (t CoreTime) AdjustedGoTime(loc *gotime.Location) (gotime.Time, error) {
tm, err := t.GoTime(loc)
if err == nil {
return tm, nil
}

tAdj, err2 := FindZoneTransition(tm, loc)
if err2 == nil {
return tAdj, nil
}
return tm, err
}

// IsLeapYear returns if it's leap year.
func (t CoreTime) IsLeapYear() bool {
return isLeapYear(t.getYear())
Expand Down
65 changes: 65 additions & 0 deletions types/core_time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,68 @@ func TestWeekday(t *testing.T) {
require.Equal(t, tt.Expect, weekday.String())
}
}

func TestFindZoneTransition(t *testing.T) {
t.Parallel()
tests := []struct {
TZ string
dt string
Expect string
Success bool
}{
{"Australia/Lord_Howe", "2020-06-29 03:45:00", "", false},
{"Australia/Lord_Howe", "2020-10-04 02:15:00", "2020-10-04 02:30:00 +11 +1100", true},
{"Europe/Vilnius", "2020-03-29 03:45:00", "2020-03-29 04:00:00 EEST +0300", true},
{"Europe/Vilnius", "2020-10-25 03:45:00", "2020-10-25 03:00:00 EET +0200", true},
{"Europe/Vilnius", "2020-06-29 03:45:00", "", false},
{"Europe/Amsterdam", "2020-03-29 02:45:00", "2020-03-29 03:00:00 CEST +0200", true},
{"Europe/Amsterdam", "2020-10-25 02:35:00", "2020-10-25 02:00:00 CET +0100", true},
}

for _, tt := range tests {
loc, err := time.LoadLocation(tt.TZ)
require.NoError(t, err)
tm, err := time.ParseInLocation("2006-01-02 15:04:05", tt.dt, loc)
require.NoError(t, err)
tp, err := FindZoneTransition(tm, loc)
if !tt.Success {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.Expect, tp.Format("2006-01-02 15:04:05 MST -0700"))
}
}
}

func TestAdjustedGoTime(t *testing.T) {
t.Parallel()
tests := []struct {
TZ string
dt CoreTime
Expect string
Success bool
}{
{"Australia/Lord_Howe", FromDate(2020, 10, 04, 02, 15, 00, 0), "2020-10-04 02:30:00 +11 +1100", true},
{"Australia/Lord_Howe", FromDate(2020, 06, 29, 03, 45, 00, 0), "2020-06-29 03:45:00 +1030 +1030", true},
{"Australia/Lord_Howe", FromDate(2020, 04, 04, 01, 45, 00, 0), "2020-04-04 01:45:00 +11 +1100", true},
{"Europe/Vilnius", FromDate(2020, 03, 29, 03, 45, 00, 0), "2020-03-29 04:00:00 EEST +0300", true},
{"Europe/Vilnius", FromDate(2020, 10, 25, 03, 45, 00, 0), "2020-10-25 03:45:00 EET +0200", true},
{"Europe/Vilnius", FromDate(2020, 06, 29, 03, 45, 00, 0), "2020-06-29 03:45:00 EEST +0300", true},
{"Europe/Amsterdam", FromDate(2020, 03, 29, 02, 45, 00, 0), "2020-03-29 03:00:00 CEST +0200", true},
{"Europe/Amsterdam", FromDate(2020, 10, 25, 02, 35, 00, 0), "2020-10-25 02:35:00 CET +0100", true},
{"UTC", FromDate(2020, 2, 31, 02, 35, 00, 0), "", false},
// TODO: Test out-of-range values?
}

for _, tt := range tests {
loc, err := time.LoadLocation(tt.TZ)
require.NoError(t, err)
tp, err := tt.dt.AdjustedGoTime(loc)
if !tt.Success {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.Expect, tp.Format("2006-01-02 15:04:05 MST -0700"))
}
}
}