From c09280f3cc900619eb9ea8ecdeb9685128579054 Mon Sep 17 00:00:00 2001 From: Oliver Tan Date: Mon, 25 Nov 2019 14:45:38 -0800 Subject: [PATCH] sql: fix parsing of 0000-01-01 as Time/TimeTZ Since `lib/pq` outputs `time.Time` for time datums as 0000-01-01, we should be able to parse this in. However, `pgdate` library cannot do that - so we hack around it for now by replacing the year for these kinds of dates. Release note (bug fix): Previously, attempting to parse `0000-01-01 00:00` when involving `time` did not work as `pgdate` does not understand `0000` as a year. This PR will fix that behaviour. --- pkg/sql/logictest/testdata/logic_test/time | 13 +++++++++++++ pkg/sql/sem/tree/datum.go | 8 +++++--- pkg/sql/sem/tree/datum_test.go | 9 +++++++++ pkg/util/timeutil/time.go | 13 +++++++++++++ pkg/util/timeutil/time_test.go | 7 +++++++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/time b/pkg/sql/logictest/testdata/logic_test/time index b3b35ce20eee..6b7246408683 100644 --- a/pkg/sql/logictest/testdata/logic_test/time +++ b/pkg/sql/logictest/testdata/logic_test/time @@ -327,3 +327,16 @@ SELECT extract(epoch from time '12:00:00') query error pgcode 22023 extract\(\): unsupported timespan: day SELECT extract(day from time '12:00:00') + +subtest regression_42749 + +# cast to string to prove it is 24:00 +query T +SELECT '0000-01-01 24:00:00'::time::string +---- +24:00:00 + +query T +SELECT '2001-01-01 01:24:00'::time +---- +0000-01-01 01:24:00 +0000 UTC diff --git a/pkg/sql/sem/tree/datum.go b/pkg/sql/sem/tree/datum.go index 9916dcdd05e0..9baf65b7c66d 100644 --- a/pkg/sql/sem/tree/datum.go +++ b/pkg/sql/sem/tree/datum.go @@ -64,7 +64,7 @@ var ( DZero = NewDInt(0) // DTimeRegex is a compiled regex for parsing the 24:00 time value - DTimeRegex = regexp.MustCompile("^24:00($|(:00$)|(:00.0+$))") + DTimeRegex = regexp.MustCompile(`^([0-9-]*(\s|T))?\s*24:00(:00(.0+)?)?\s*$`) ) // Datum represents a SQL value. @@ -1864,13 +1864,15 @@ func MakeDTime(t timeofday.TimeOfDay) *DTime { func ParseDTime(ctx ParseTimeContext, s string) (*DTime, error) { now := relativeParseTime(ctx) - // special case on 24:00 and 24:00:00 as the parser + // Special case on 24:00 and 24:00:00 as the parser // does not handle these correctly. if DTimeRegex.MatchString(s) { return MakeDTime(timeofday.Time2400), nil } - t, err := pgdate.ParseTime(now, 0 /* mode */, s) + s = timeutil.ReplaceLibPQTimePrefix(s) + + t, err := pgdate.ParseTime(now, pgdate.ParseModeYMD, s) if err != nil { // Build our own error message to avoid exposing the dummy date. return nil, makeParseError(s, types.Time, nil) diff --git a/pkg/sql/sem/tree/datum_test.go b/pkg/sql/sem/tree/datum_test.go index 3dde45cd72dd..12fc600eb51d 100644 --- a/pkg/sql/sem/tree/datum_test.go +++ b/pkg/sql/sem/tree/datum_test.go @@ -485,6 +485,7 @@ func TestParseDTime(t *testing.T) { str string expected timeofday.TimeOfDay }{ + {" 04:05:06 ", timeofday.New(4, 5, 6, 0)}, {"04:05:06", timeofday.New(4, 5, 6, 0)}, {"04:05:06.000001", timeofday.New(4, 5, 6, 1)}, {"04:05:06-07", timeofday.New(4, 5, 6, 0)}, @@ -492,6 +493,14 @@ func TestParseDTime(t *testing.T) { {"24:00:00", timeofday.Time2400}, {"24:00:00.000", timeofday.Time2400}, {"24:00:00.000000", timeofday.Time2400}, + {"0000-01-01 04:05:06", timeofday.New(4, 5, 6, 0)}, + {"2001-01-01 04:05:06", timeofday.New(4, 5, 6, 0)}, + {"0000-01-01T04:05:06", timeofday.New(4, 5, 6, 0)}, + {"0000-01-01T24:00:00", timeofday.Time2400}, + {"0000-01-01 24:00:00", timeofday.Time2400}, + {"0000-01-01 24:00:00.0", timeofday.Time2400}, + {" 24:00:00.0", timeofday.Time2400}, + {" 24:00:00.0 ", timeofday.Time2400}, } for _, td := range testData { actual, err := tree.ParseDTime(nil, td.str) diff --git a/pkg/util/timeutil/time.go b/pkg/util/timeutil/time.go index ce648c08031c..ca6fce621092 100644 --- a/pkg/util/timeutil/time.go +++ b/pkg/util/timeutil/time.go @@ -12,6 +12,7 @@ package timeutil import ( "math" + "strings" "time" ) @@ -20,6 +21,9 @@ import ( // assuming any bound on the clock drift. const ClocklessMaxOffset = math.MaxInt64 +// LibPQTimePrefix is the prefix lib/pq prints time-type datatypes with. +const LibPQTimePrefix = "0000-01-01" + // Since returns the time elapsed since t. // It is shorthand for Now().Sub(t). func Since(t time.Time) time.Duration { @@ -72,3 +76,12 @@ func SleepUntil(untilNanos int64, currentTimeNanos func() int64) { time.Sleep(d) } } + +// ReplaceLibPQTimePrefix replaces unparsable lib/pq dates used for timestamps +// (0000-01-01) with timestamps that can be parsed by date libraries. +func ReplaceLibPQTimePrefix(s string) string { + if strings.HasPrefix(s, LibPQTimePrefix) { + return "1970-01-01" + s[len(LibPQTimePrefix):] + } + return s +} diff --git a/pkg/util/timeutil/time_test.go b/pkg/util/timeutil/time_test.go index 1ee3380c8331..cc2a8e9a13b6 100644 --- a/pkg/util/timeutil/time_test.go +++ b/pkg/util/timeutil/time_test.go @@ -15,6 +15,8 @@ import ( "math/rand" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestUnixMicros(t *testing.T) { @@ -63,3 +65,8 @@ func TestUnixMicrosRounding(t *testing.T) { } } } + +func TestReplaceLibPQTimePrefix(t *testing.T) { + assert.Equal(t, "1970-02-02 11:00", ReplaceLibPQTimePrefix("1970-02-02 11:00")) + assert.Equal(t, "1970-01-01 11:00", ReplaceLibPQTimePrefix("0000-01-01 11:00")) +}