From 48d3d981ebe3b595fd80d80cd4ad990df38051e8 Mon Sep 17 00:00:00 2001 From: Zijie Lu Date: Fri, 11 Jun 2021 10:05:30 +0800 Subject: [PATCH] util: Fix interval division with float rounds differently from PostgreSQL Before this commit, the nanos will be rounded in `MakeDuration`. This commit round down nanos before `rounded` is called in the div of duration. Fixes #66118 Release note (bug fix): Fixed a bug with PostgreSQL compatibility where dividing an interval by a number would round to the nearest Microsecond instead of always rounding down. --- .../logictest/testdata/logic_test/datetime | 4 +-- .../logictest/testdata/logic_test/interval | 25 ++++++++++++++ pkg/util/duration/duration.go | 33 ++++++++++++++++--- pkg/util/duration/duration_test.go | 12 +++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/datetime b/pkg/sql/logictest/testdata/logic_test/datetime index a6caeac1b865..16ec98256da3 100644 --- a/pkg/sql/logictest/testdata/logic_test/datetime +++ b/pkg/sql/logictest/testdata/logic_test/datetime @@ -1522,8 +1522,8 @@ ORDER BY ---- 01:00:00 00:30:00 02:00:00 00:30:00 02:00:00 04:14:01.320914 00:14:10.32 1 day 12:00:00 2 days 12:00:00 2 days 4 days 05:36:31.701948 05:40:07.68 -1 mon 15 days 2 mons 15 days 2 mons 4 mons 7 days 00:15:51.058425 7 days 02:03:50.4 -1 mon 2 days 04:00:00 16 days 02:00:00 2 mons 4 days 08:00:00 16 days 02:00:00 2 mons 4 days 08:00:00 4 mons 15 days 28:24:59.745978 7 days 14:20:47.04 +1 mon 15 days 2 mons 15 days 2 mons 4 mons 7 days 00:15:51.0912 7 days 02:03:50.4 +1 mon 2 days 04:00:00 16 days 02:00:00 2 mons 4 days 08:00:00 16 days 02:00:00 2 mons 4 days 08:00:00 4 mons 15 days 28:24:59.778753 7 days 14:20:47.04 subtest tz_utc_normalization diff --git a/pkg/sql/logictest/testdata/logic_test/interval b/pkg/sql/logictest/testdata/logic_test/interval index d8fb3d0a891f..c57da75253c7 100644 --- a/pkg/sql/logictest/testdata/logic_test/interval +++ b/pkg/sql/logictest/testdata/logic_test/interval @@ -367,3 +367,28 @@ subtest regression_62369 query error "10000000000000000000000000000000000": value out of range SELECT INTERVAL '10000000000000000000000000000000000 year' + +query T +SELECT i / 2 FROM ( VALUES + ('0 days 0.253000 seconds'::interval), + (INTERVAL '0.000001'::interval), + (INTERVAL '0.000002'::interval), + (INTERVAL '0.000003'::interval), + (INTERVAL '0.000004'::interval), + (INTERVAL '0.000005'::interval), + (INTERVAL '0.000006'::interval), + (INTERVAL '0.000007'::interval), + (INTERVAL '0.000008'::interval), + (INTERVAL '0.000009'::interval) +) regression_66118(i) +---- +00:00:00.1265 +00:00:00 +00:00:00.000001 +00:00:00.000002 +00:00:00.000002 +00:00:00.000002 +00:00:00.000003 +00:00:00.000004 +00:00:00.000004 +00:00:00.000004 diff --git a/pkg/util/duration/duration.go b/pkg/util/duration/duration.go index 70e8311463e4..aad49525ba23 100644 --- a/pkg/util/duration/duration.go +++ b/pkg/util/duration/duration.go @@ -617,16 +617,39 @@ func (d Duration) MulFloat(x float64) Duration { // DivFloat returns a Duration representing a time length of d/x. func (d Duration) DivFloat(x float64) Duration { - monthInt, monthFrac := math.Modf(float64(d.Months) / x) - dayInt, dayFrac := math.Modf((float64(d.Days) / x) + (monthFrac * DaysPerMonth)) + // In order to keep it compatible with PostgreSQL, we use the same logic. + // Refer to https://github.com/postgres/postgres/blob/e56bce5d43789cce95d099554ae9593ada92b3b7/src/backend/utils/adt/timestamp.c#L3266-L3304. + month := int32(float64(d.Months) / x) + day := int32(float64(d.Days) / x) + + remainderDays := (float64(d.Months)/x - float64(month)) * DaysPerMonth + remainderDays = secRoundToEven(remainderDays) + secRemainder := (float64(d.Days)/x - float64(day) + + remainderDays - float64(int64(remainderDays))) * SecsPerDay + secRemainder = secRoundToEven(secRemainder) + if math.Abs(secRemainder) >= SecsPerDay { + day += int32(secRemainder / SecsPerDay) + secRemainder -= float64(int32(secRemainder/SecsPerDay) * SecsPerDay) + } + day += int32(remainderDays) + microSecs := float64(time.Duration(d.nanos).Microseconds())/x + secRemainder*MicrosPerMilli*MillisPerSec + retNanos := time.Duration(int64(math.RoundToEven(microSecs))) * time.Microsecond return MakeDuration( - int64((float64(d.nanos)/x)+(dayFrac*float64(nanosInDay))), - int64(dayInt), - int64(monthInt), + retNanos.Nanoseconds(), + int64(day), + int64(month), ) } +// secRoundToEven rounds the given float to the nearest second, +// assuming the input float is a microsecond representation of +// time +// This maps to the TSROUND macro in Postgres. +func secRoundToEven(f float64) float64 { + return math.RoundToEven(f*MicrosPerMilli*MillisPerSec) / (MicrosPerMilli * MillisPerSec) +} + // normalized returns a new Duration transformed using the equivalence rules. // Each quantity of days greater than the threshold is moved into months, // likewise for nanos. Integer overflow is avoided by partial transformation. diff --git a/pkg/util/duration/duration_test.go b/pkg/util/duration/duration_test.go index 78d25b166b49..da10335f1ffd 100644 --- a/pkg/util/duration/duration_test.go +++ b/pkg/util/duration/duration_test.go @@ -498,6 +498,18 @@ func TestFloatMath(t *testing.T) { Duration{Months: 2, Days: 34, nanos: nanosInHour * 4}, Duration{Days: 23, nanos: nanosInHour * 13}, }, + { + Duration{Months: 0, Days: 0, nanos: nanosInSecond * 0.253000}, + 3.2, + Duration{Months: 0, Days: 0, nanos: nanosInSecond * 0.8096}, + Duration{Months: 0, Days: 0, nanos: nanosInSecond * 0.079062}, + }, + { + Duration{Months: 0, Days: 0, nanos: nanosInSecond * 0.000001}, + 2.0, + Duration{Months: 0, Days: 0, nanos: nanosInSecond * 0.000002}, + Duration{Months: 0, Days: 0, nanos: nanosInSecond * 0}, + }, } for i, test := range tests {