diff --git a/expression/builtin_time.go b/expression/builtin_time.go index 6c318e7955b41..8386c3dc5481d 100644 --- a/expression/builtin_time.go +++ b/expression/builtin_time.go @@ -4853,7 +4853,7 @@ func (b *builtinUnixTimestampIntSig) evalIntWithCtx(ctx sessionctx.Context, row } tz := ctx.GetSessionVars().Location() - t, err := val.GoTime(tz) + t, err := val.AdjustedGoTime(tz) if err != nil { return 0, false, nil } diff --git a/expression/builtin_time_vec.go b/expression/builtin_time_vec.go index f5558aa20c770..6dfee0fc7f842 100644 --- a/expression/builtin_time_vec.go +++ b/expression/builtin_time_vec.go @@ -2325,7 +2325,7 @@ func (b *builtinUnixTimestampIntSig) vecEvalInt(input *chunk.Chunk, result *chun continue } - t, err := buf.GetTime(i).GoTime(getTimeZone(b.ctx)) + t, err := buf.GetTime(i).AdjustedGoTime(getTimeZone(b.ctx)) if err != nil { i64s[i] = 0 continue diff --git a/expression/integration_test.go b/expression/integration_test.go index 6496e49ea17cf..398f363058954 100644 --- a/expression/integration_test.go +++ b/expression/integration_test.go @@ -9476,3 +9476,22 @@ func (s *testIntegrationSuite) TestIssue29513(c *C) { tk.MustQuery("select '123' union select cast(a as char) from t;").Sort().Check(testkit.Rows("123", "45678")) tk.MustQuery("select '123' union select cast(a as char(2)) from t;").Sort().Check(testkit.Rows("123", "45")) } + +func (s *testIntegrationSuite) TestIssue28739(c *C) { + tk := testkit.NewTestKit(c, s.store) + tk.MustExec(`USE test`) + 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(`DROP TABLE IF EXISTS t`) + tk.MustExec(`CREATE TABLE t (dt DATETIME NULL)`) + defer tk.MustExec(`DROP TABLE t`) + // Test the vector implememtation + tk.MustExec(`INSERT INTO t VALUES ('2021-10-31 02:30:00'), ('2021-03-28 02:30:00'), ('2020-10-04 02:15:00'), ('2020-03-29 03:45:00'), (NULL)`) + tk.MustQuery(`SELECT dt, UNIX_TIMESTAMP(dt) FROM t`).Sort().Check(testkit.Rows( + "2020-03-29 03:45:00 1585443600", + "2020-10-04 02:15:00 1601766900", + "2021-03-28 02:30:00 1616891400", + "2021-10-31 02:30:00 1635636600", + " ")) +} diff --git a/types/core_time.go b/types/core_time.go index f8cf9a0b0db9d..ab81724a58074 100644 --- a/types/core_time.go +++ b/types/core_time.go @@ -183,6 +183,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) (gotime.Time, error) { + // 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) + 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()) diff --git a/types/core_time_test.go b/types/core_time_test.go index f45dac6f58bd4..ef67dcb81abfa 100644 --- a/types/core_time_test.go +++ b/types/core_time_test.go @@ -17,6 +17,7 @@ import ( "time" . "github.com/pingcap/check" + "github.com/stretchr/testify/require" ) type testCoreTimeSuite struct{} @@ -294,3 +295,79 @@ func (s *testCoreTimeSuite) TestWeekday(c *C) { c.Check(weekday.String(), Equals, tt.Expect) } } + +func (s *testCoreTimeSuite) TestFindZoneTransition(c *C) { + 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}, + {"Australia/Lord_Howe", "2020-10-04 02:29:59", "2020-10-04 02:30:00 +11 +1100", true}, + {"Australia/Lord_Howe", "2020-10-04 02:29:59.99", "2020-10-04 02:30:00 +11 +1100", true}, + {"Australia/Lord_Howe", "2020-10-04 02:30:00.0001", "2020-10-04 02:30:00 +11 +1100", true}, + {"Australia/Lord_Howe", "2020-10-04 02:30:00", "2020-10-04 02:30:00 +11 +1100", true}, + {"Australia/Lord_Howe", "2020-10-04 02:30:01", "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}, + {"Europe/Amsterdam", "2020-03-29 02:59:59", "2020-03-29 03:00:00 CEST +0200", true}, + {"Europe/Amsterdam", "2020-03-29 02:59:59.999999999", "2020-03-29 03:00:00 CEST +0200", true}, + {"Europe/Amsterdam", "2020-03-29 03:00:00.000000001", "2020-03-29 03:00:00 CEST +0200", true}, + } + + for _, tt := range tests { + loc, err := time.LoadLocation(tt.TZ) + require.NoError(c, err) + tm, err := time.ParseInLocation("2006-01-02 15:04:05", tt.dt, loc) + require.NoError(c, err) + tp, err := FindZoneTransition(tm) + if !tt.Success { + require.Error(c, err) + } else { + require.NoError(c, err) + require.Equal(c, tt.Expect, tp.Format("2006-01-02 15:04:05.999999999 MST -0700")) + } + } +} + +func (s *testCoreTimeSuite) TestAdjustedGoTime(c *C) { + tests := []struct { + TZ string + dt CoreTime + Expect string + Success bool + }{ + {"Australia/Lord_Howe", FromDate(2020, 10, 04, 01, 59, 59, 997), "2020-10-04 01:59:59.000997 +1030 +1030", true}, + {"Australia/Lord_Howe", FromDate(2020, 10, 04, 02, 00, 00, 0), "2020-10-04 02:30:00 +11 +1100", true}, + {"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, 10, 04, 02, 29, 59, 999999), "2020-10-04 02:30:00 +11 +1100", true}, + {"Australia/Lord_Howe", FromDate(2020, 10, 04, 02, 30, 00, 1), "2020-10-04 02:30:00.000001 +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, 03, 29, 03, 59, 59, 456789), "2020-03-29 04:00:00 EEST +0300", true}, + {"Europe/Vilnius", FromDate(2020, 03, 29, 04, 00, 01, 130000), "2020-03-29 04:00:01.13 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}, + } + + for _, tt := range tests { + loc, err := time.LoadLocation(tt.TZ) + require.NoError(c, err) + tp, err := tt.dt.AdjustedGoTime(loc) + if !tt.Success { + require.Error(c, err) + } else { + require.NoError(c, err) + require.Equal(c, tt.Expect, tp.Format("2006-01-02 15:04:05.999999999 MST -0700")) + } + } +}