Skip to content

Commit

Permalink
Merge pull request #56751 from otan-cockroach/backport20.2-56667
Browse files Browse the repository at this point in the history
release-20.2: sql: normalize age and timestamptz intervals like postgres
  • Loading branch information
solongordon authored Nov 17, 2020
2 parents 193c2e7 + d2c0836 commit e22428c
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 31 deletions.
8 changes: 6 additions & 2 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,13 @@
<table>
<thead><tr><th>Function &rarr; Returns</th><th>Description</th></tr></thead>
<tbody>
<tr><td><a name="age"></a><code>age(end: <a href="timestamp.html">timestamptz</a>, begin: <a href="timestamp.html">timestamptz</a>) &rarr; <a href="interval.html">interval</a></code></td><td><span class="funcdesc"><p>Calculates the interval between <code>begin</code> and <code>end</code>.</p>
<tr><td><a name="age"></a><code>age(end: <a href="timestamp.html">timestamptz</a>, begin: <a href="timestamp.html">timestamptz</a>) &rarr; <a href="interval.html">interval</a></code></td><td><span class="funcdesc"><p>Calculates the interval between <code>begin</code> and <code>end</code>, normalized into years, months and days.</p>
<pre><code> Note this may not be an accurate time span since years and months are normalized from days, and years and months are out of context.
To avoid normalizing days into months and years, use the timestamptz subtraction operator.</code></pre>
</span></td></tr>
<tr><td><a name="age"></a><code>age(val: <a href="timestamp.html">timestamptz</a>) &rarr; <a href="interval.html">interval</a></code></td><td><span class="funcdesc"><p>Calculates the interval between <code>val</code> and the current time.</p>
<tr><td><a name="age"></a><code>age(val: <a href="timestamp.html">timestamptz</a>) &rarr; <a href="interval.html">interval</a></code></td><td><span class="funcdesc"><p>Calculates the interval between <code>val</code> and the current time, normalized into years, months and days.</p>
<pre><code> Note this may not be an accurate time span since years and months are normalized from days, and years and months are out of context.
To avoid normalizing days into months and years, use `now() - timestamptz`.</code></pre>
</span></td></tr>
<tr><td><a name="clock_timestamp"></a><code>clock_timestamp() &rarr; <a href="timestamp.html">timestamp</a></code></td><td><span class="funcdesc"><p>Returns the current system time on one of the cluster nodes.</p>
</span></td></tr>
Expand Down
2 changes: 1 addition & 1 deletion pkg/sql/logictest/testdata/logic_test/datetime
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ SELECT '5874897-12-31'::date - '4714-11-24 BC'::date
query T
SELECT age('2001-04-10 22:06:45', '1957-06-13')
----
44 years 5 mons 17 days 22:06:45
43 years 9 mons 27 days 22:06:45

query B
SELECT age('1957-06-13') - age(now(), '1957-06-13') = interval '0s'
Expand Down
36 changes: 34 additions & 2 deletions pkg/sql/logictest/testdata/logic_test/timestamp
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,8 @@ SELECT
FROM example
ORDER BY a
----
2010-11-07 22:59:00 -0600 CST 2010-11-07 23:59:00 -0600 CST 2010-12-06 23:59:00 -0600 CST 2010-11-05 23:59:00 -0500 CDT 2010-11-05 23:59:00 -0500 CDT 2010-10-06 23:59:00 -0500 CDT 00:00:00 -1 days -01:00:00 2010-11-06 23:59:00-05:00
2010-11-08 23:59:00 -0600 CST 2010-11-08 23:59:00 -0600 CST 2010-12-07 23:59:00 -0600 CST 2010-11-07 00:59:00 -0500 CDT 2010-11-06 23:59:00 -0500 CDT 2010-10-07 23:59:00 -0500 CDT 1 day 01:00:00 00:00:00 2010-11-07 23:59:00-06:00
2010-11-07 22:59:00 -0600 CST 2010-11-07 23:59:00 -0600 CST 2010-12-06 23:59:00 -0600 CST 2010-11-05 23:59:00 -0500 CDT 2010-11-05 23:59:00 -0500 CDT 2010-10-06 23:59:00 -0500 CDT 00:00:00 -1 days -01:00:00 2010-11-06 23:59:00-05:00
2010-11-08 23:59:00 -0600 CST 2010-11-08 23:59:00 -0600 CST 2010-12-07 23:59:00 -0600 CST 2010-11-07 00:59:00 -0500 CDT 2010-11-06 23:59:00 -0500 CDT 2010-10-07 23:59:00 -0500 CDT 1 day 01:00:00 00:00:00 2010-11-07 23:59:00-06:00

statement ok
DROP TABLE example
Expand Down Expand Up @@ -442,3 +442,35 @@ query R
set time zone 'UTC'; select extract(epoch from TIMESTAMP WITH TIME ZONE '2010-11-06 23:59:00-05:00')
----
1.28910594e+09

query TTTTTT
SET TIME ZONE 'Europe/Berlin'; SELECT
age(a::timestamptz, b::timestamptz),
age(b::timestamptz, a::timestamptz),
a::timestamptz - b::timestamptz,
b::timestamptz - a::timestamptz,
a::timestamp - b::timestamp,
b::timestamp - a::timestamp
FROM (VALUES
('2020-05-06 11:12:13', '2015-06-07 13:14:15'),
('2020-05-06 15:00:00.112233', '2020-04-03 16:00:00.001122'),
('2020-02-29 00:02:05', '2019-02-28 18:19:01'),
('2020-02-29 00:02:05', '2020-01-28 18:19:01'),
('2020-02-29 00:02:05', '2020-03-28 18:19:01'),
('2021-02-27 00:02:05.333333', '2019-02-28 18:19:01.444444'),
('2021-02-27 00:02:05', '2021-01-28 18:19:01'),
('2021-02-27 00:02:05', '2021-03-28 18:19:01'),
('2020-02-28 00:02:05', '2020-02-28 18:19:01'),
('2020-06-30 11:11:11.111111', '2020-06-29 12:12:12.222222')
) regression_age_tests(a, b)
----
4 years 10 mons 28 days 21:57:58 -4 years -10 mons -28 days -21:57:58 1794 days 21:57:58 -1794 days -21:57:58 1794 days 21:57:58 -1794 days -21:57:58
1 mon 2 days 23:00:00.111111 -1 mons -2 days -23:00:00.111111 32 days 23:00:00.111111 -32 days -23:00:00.111111 32 days 23:00:00.111111 -32 days -23:00:00.111111
1 year 05:43:04 -1 years -05:43:04 365 days 05:43:04 -365 days -05:43:04 365 days 05:43:04 -365 days -05:43:04
1 mon 05:43:04 -1 mons -05:43:04 31 days 05:43:04 -31 days -05:43:04 31 days 05:43:04 -31 days -05:43:04
-28 days -18:16:56 28 days 18:16:56 -28 days -18:16:56 28 days 18:16:56 -28 days -18:16:56 28 days 18:16:56
1 year 11 mons 26 days 05:43:03.888889 -1 years -11 mons -26 days -05:43:03.888889 729 days 05:43:03.888889 -729 days -05:43:03.888889 729 days 05:43:03.888889 -729 days -05:43:03.888889
29 days 05:43:04 -29 days -05:43:04 29 days 05:43:04 -29 days -05:43:04 29 days 05:43:04 -29 days -05:43:04
-1 mons -1 days -17:16:56 1 mon 1 day 17:16:56 -29 days -17:16:56 29 days 17:16:56 -29 days -18:16:56 29 days 18:16:56
-18:16:56 18:16:56 -18:16:56 18:16:56 -18:16:56 18:16:56
22:58:58.888889 -22:58:58.888889 22:58:58.888889 -22:58:58.888889 22:58:58.888889 -22:58:58.888889
24 changes: 20 additions & 4 deletions pkg/sql/sem/builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -2067,18 +2067,34 @@ var builtins = map[string]builtinDefinition{
Types: tree.ArgTypes{{"val", types.TimestampTZ}},
ReturnType: tree.FixedReturnType(types.Interval),
Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) {
return tree.TimestampDifference(ctx, ctx.GetTxnTimestamp(time.Microsecond), args[0])
return &tree.DInterval{
Duration: duration.Age(
ctx.GetTxnTimestamp(time.Microsecond).Time,
args[0].(*tree.DTimestampTZ).Time,
),
}, nil
},
Info: "Calculates the interval between `val` and the current time.",
Info: "Calculates the interval between `val` and the current time, normalized into years, months and days." + `
Note this may not be an accurate time span since years and months are normalized from days, and years and months are out of context.
To avoid normalizing days into months and years, use ` + "`now() - timestamptz`" + `.`,
Volatility: tree.VolatilityStable,
},
tree.Overload{
Types: tree.ArgTypes{{"end", types.TimestampTZ}, {"begin", types.TimestampTZ}},
ReturnType: tree.FixedReturnType(types.Interval),
Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) {
return tree.TimestampDifference(ctx, args[0], args[1])
return &tree.DInterval{
Duration: duration.Age(
args[0].(*tree.DTimestampTZ).Time,
args[1].(*tree.DTimestampTZ).Time,
),
}, nil
},
Info: "Calculates the interval between `begin` and `end`.",
Info: "Calculates the interval between `begin` and `end`, normalized into years, months and days." + `
Note this may not be an accurate time span since years and months are normalized from days, and years and months are out of context.
To avoid normalizing days into months and years, use the timestamptz subtraction operator.`,
Volatility: tree.VolatilityImmutable,
},
),
Expand Down
23 changes: 4 additions & 19 deletions pkg/sql/sem/tree/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ var BinOps = map[BinaryOperator]binOpOverload{
ReturnType: types.Interval,
Fn: func(_ *EvalContext, left Datum, right Datum) (Datum, error) {
nanos := left.(*DTimestamp).Sub(right.(*DTimestamp).Time).Nanoseconds()
return &DInterval{Duration: duration.MakeNormalizedDuration(nanos, 0, 0)}, nil
return &DInterval{Duration: duration.MakeDurationJustifyHours(nanos, 0, 0)}, nil
},
Volatility: VolatilityImmutable,
},
Expand All @@ -989,7 +989,7 @@ var BinOps = map[BinaryOperator]binOpOverload{
ReturnType: types.Interval,
Fn: func(_ *EvalContext, left Datum, right Datum) (Datum, error) {
nanos := left.(*DTimestampTZ).Sub(right.(*DTimestampTZ).Time).Nanoseconds()
return &DInterval{Duration: duration.MakeNormalizedDuration(nanos, 0, 0)}, nil
return &DInterval{Duration: duration.MakeDurationJustifyHours(nanos, 0, 0)}, nil
},
Volatility: VolatilityImmutable,
},
Expand All @@ -1005,7 +1005,7 @@ var BinOps = map[BinaryOperator]binOpOverload{
return nil, err
}
nanos := left.(*DTimestamp).Sub(stripped.Time).Nanoseconds()
return &DInterval{Duration: duration.MakeNormalizedDuration(nanos, 0, 0)}, nil
return &DInterval{Duration: duration.MakeDurationJustifyHours(nanos, 0, 0)}, nil
},
Volatility: VolatilityStable,
},
Expand All @@ -1021,7 +1021,7 @@ var BinOps = map[BinaryOperator]binOpOverload{
return nil, err
}
nanos := stripped.Sub(right.(*DTimestamp).Time).Nanoseconds()
return &DInterval{Duration: duration.MakeNormalizedDuration(nanos, 0, 0)}, nil
return &DInterval{Duration: duration.MakeDurationJustifyHours(nanos, 0, 0)}, nil
},
Volatility: VolatilityStable,
},
Expand Down Expand Up @@ -1896,21 +1896,6 @@ var BinOps = map[BinaryOperator]binOpOverload{
},
}

// timestampMinusBinOp is the implementation of the subtraction
// between types.TimestampTZ operands.
var timestampMinusBinOp *BinOp

// TimestampDifference computes the interval difference between two
// TimestampTZ datums. The result is a DInterval. The caller must
// ensure that the arguments are of the proper Datum type.
func TimestampDifference(ctx *EvalContext, start, end Datum) (Datum, error) {
return timestampMinusBinOp.Fn(ctx, start, end)
}

func init() {
timestampMinusBinOp, _ = BinOps[Minus].lookupImpl(types.TimestampTZ, types.TimestampTZ)
}

// CmpOp is a comparison operator.
type CmpOp struct {
LeftType *types.T
Expand Down
110 changes: 107 additions & 3 deletions pkg/util/duration/duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const (
SecsPerHour = 3600
// SecsPerDay is the amount of seconds in a day.
SecsPerDay = 86400
// MinsPerHour is the amount of minutes in an hour.
MinsPerHour = 60
// HoursPerDay is the number of hours in a day.
HoursPerDay = 24
// DaysPerMonth is the assumed amount of days in a month.
// is always evaluated to 30, as it is in postgres.
DaysPerMonth = 30
Expand Down Expand Up @@ -102,13 +106,113 @@ func MakeDuration(nanos, days, months int64) Duration {
}
}

// MakeNormalizedDuration returns a normalized Duration.
func MakeNormalizedDuration(nanos, days, months int64) Duration {
// MakeDurationJustifyHours returns a duration where hours are moved
// to days if the number of hours exceeds 24.
func MakeDurationJustifyHours(nanos, days, months int64) Duration {
const nanosPerDay = int64(HoursPerDay * time.Hour)
extraDays := nanos / nanosPerDay
days += extraDays
nanos -= extraDays * nanosPerDay
return Duration{
Months: months,
Days: days,
nanos: rounded(nanos),
}.normalize()
}
}

// Age returns a Duration rounded to the nearest microsecond
// from the time difference of (lhs - rhs).
//
// Note that we cannot use time.Time's sub, as time.Duration does not give
// an accurate picture of day/month differences.
//
// This is lifted from Postgres' timestamptz_age. The following comment applies:
// Note that this does not result in an accurate absolute time span
// since year and month are out of context once the arithmetic
// is done.
func Age(lhs, rhs time.Time) Duration {
// Strictly compare only UTC time.
lhs = lhs.UTC()
rhs = rhs.UTC()

years := int64(lhs.Year() - rhs.Year())
months := int64(lhs.Month() - rhs.Month())
days := int64(lhs.Day() - rhs.Day())
hours := int64(lhs.Hour() - rhs.Hour())
minutes := int64(lhs.Minute() - rhs.Minute())
seconds := int64(lhs.Second() - rhs.Second())
nanos := int64(lhs.Nanosecond() - rhs.Nanosecond())

flip := func() {
years = -years
months = -months
days = -days
hours = -hours
minutes = -minutes
seconds = -seconds
nanos = -nanos
}

// Flip signs so we're always operating from a positive.
if rhs.After(lhs) {
flip()
}

// For each field that is now negative, promote them to positive.
// We could probably use smarter math here, but to keep things simple and postgres-esque,
// we'll do the the same way postgres does. We do not expect these overflow values
// to be too large from the math above anyway.
for nanos < 0 {
nanos += int64(time.Second)
seconds--
}
for seconds < 0 {
seconds += SecsPerMinute
minutes--
}
for minutes < 0 {
minutes += MinsPerHour
hours--
}
for hours < 0 {
hours += HoursPerDay
days--
}
for days < 0 {
// Get days in month preceding the current month of whichever is greater.
if rhs.After(lhs) {
days += daysInCurrentMonth(lhs)
} else {
days += daysInCurrentMonth(rhs)
}
months--
}
for months < 0 {
months += MonthsPerYear
years--
}

// Revert the sign back.
if rhs.After(lhs) {
flip()
}

return Duration{
Months: years*MonthsPerYear + months,
Days: days,
nanos: rounded(
nanos +
int64(time.Second)*seconds +
int64(time.Minute)*minutes +
int64(time.Hour)*hours,
),
}
}

func daysInCurrentMonth(t time.Time) int64 {
// Take the first day of the month, add a month and subtract a day.
// This returns the last day of the month, which the number of days in the month.
return int64(time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, -1).Day())
}

// DecodeDuration returns a Duration without rounding nanos.
Expand Down

0 comments on commit e22428c

Please sign in to comment.