From 4c888c4dd7431946a6066aa7447519c773108a90 Mon Sep 17 00:00:00 2001 From: Miguel Molina Date: Wed, 24 Apr 2019 11:36:37 +0200 Subject: [PATCH] sql: convert dates outside of supported time range to correct date According to MySQL, dates should range between 1000-01-01 00:00:00 and 9999-12-31 23:59:59. This PR changes the behaviour of Convert methods of Timestamp and Date types so that values returned by this method are always within that range. It also adds an exposed utility function named `ToSupportedTimeRange` that converts a time to the supported time range for clients to use. The readme now includes instructions for datasource implementors to always return their dates within the supported range using the provided utility function. The reason go-mysql-server does not handle that for data sources by default is because there is no obvious place to do so in a generic way. Tables should be the ones returning correct values. Signed-off-by: Miguel Molina --- README.md | 1 + sql/type.go | 32 ++++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d26fa0cf8..2e953d24a 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ To be able to create your own data source implementation you need to implement t - `sql.PushdownProjectionAndFiltersTable` interface will provide the same functionality described before, but also will push down the filters used in the executed query. It allows to filter data in advance, and speed up queries. - `sql.Indexable` add index capabilities to your table. By implementing this interface you can create and use indexes on this table. - `sql.Inserter` can be implemented if your data source tables allow insertions. +**IMPORTANT:** when you implement your tables, your columns of timestamp and datetime types **must** return dates in the following range: `1000-01-01 00:00:00` to `9999-12-31 23:59:59`. To do so, you can use the builtin function `sql.ToSupportedTimeRange`. - If you need some custom tree modifications, you can also implement your own `analyzer.Rules`. diff --git a/sql/type.go b/sql/type.go index 7ad54e8d5..6c69ab879 100644 --- a/sql/type.go +++ b/sql/type.go @@ -409,6 +409,11 @@ var TimestampLayouts = []string{ "20060102", } +var ( + maxTimestamp = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC) + minTimestamp = time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC) +) + // SQL implements Type interface. func (t timestampT) SQL(v interface{}) sqltypes.Value { if _, ok := v.(nullT); ok { @@ -422,11 +427,25 @@ func (t timestampT) SQL(v interface{}) sqltypes.Value { ) } +// ToSupportedTimeRange returns the time if it's within the supported datetime +// range or either the lower or upper bound. +func ToSupportedTimeRange(t time.Time) time.Time { + if t.Before(minTimestamp) { + return minTimestamp + } + + if t.After(maxTimestamp) { + return maxTimestamp + } + + return t +} + // Convert implements Type interface. func (t timestampT) Convert(v interface{}) (interface{}, error) { switch value := v.(type) { case time.Time: - return value.UTC(), nil + return ToSupportedTimeRange(value.UTC()), nil case string: t, err := time.Parse(TimestampLayout, value) if err != nil { @@ -443,14 +462,14 @@ func (t timestampT) Convert(v interface{}) (interface{}, error) { return nil, ErrConvertingToTime.Wrap(err, v) } } - return t.UTC(), nil + return ToSupportedTimeRange(t.UTC()), nil default: ts, err := Int64.Convert(v) if err != nil { return nil, ErrInvalidType.New(reflect.TypeOf(v)) } - return time.Unix(ts.(int64), 0).UTC(), nil + return ToSupportedTimeRange(time.Unix(ts.(int64), 0).UTC()), nil } } @@ -497,20 +516,21 @@ func (t dateT) SQL(v interface{}) sqltypes.Value { func (t dateT) Convert(v interface{}) (interface{}, error) { switch value := v.(type) { case time.Time: - return truncateDate(value).UTC(), nil + return truncateDate(ToSupportedTimeRange(value.UTC())), nil case string: t, err := time.Parse(DateLayout, value) if err != nil { return nil, ErrConvertingToTime.Wrap(err, v) } - return truncateDate(t).UTC(), nil + return truncateDate(ToSupportedTimeRange(t.UTC())), nil default: ts, err := Int64.Convert(v) if err != nil { return nil, ErrInvalidType.New(reflect.TypeOf(v)) } - return truncateDate(time.Unix(ts.(int64), 0)).UTC(), nil + t := time.Unix(ts.(int64), 0) + return truncateDate(ToSupportedTimeRange(t.UTC())), nil } }