Support timezones in time_bucket_ng()

This patch adds support of timezones in time_bucket_ng(). The 'origin'
argument can't be used with timezones yet. This will be implemented in
a separate pull request.
This commit is contained in:
Aleksander Alekseev 2021-08-18 15:54:57 +03:00 committed by Aleksander Alekseev
parent 76ad38636b
commit 22e77a77ad
6 changed files with 142 additions and 6 deletions

View File

@ -24,20 +24,30 @@
-- [2]: https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-TIMEZONES
--
CREATE OR REPLACE FUNCTION timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts DATE) RETURNS DATE
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_date' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_date' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
CREATE OR REPLACE FUNCTION timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts DATE, origin DATE) RETURNS DATE
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_date' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_date' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
-- utility functions
CREATE OR REPLACE FUNCTION timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts TIMESTAMP) RETURNS TIMESTAMP
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timestamp' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timestamp' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
CREATE OR REPLACE FUNCTION timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts TIMESTAMP, origin TIMESTAMP) RETURNS TIMESTAMP
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timestamp' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timestamp' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
-- The following two versions of time_bucket_ng() are kept for the backward
-- compatibility with time_bucket(). They convert 'ts' to UTC instead of treating
-- it in the given timezone, which is almost certainly not something you want.
-- Future versions may WARN you about this fact, and be completely removed
-- eventually.
CREATE OR REPLACE FUNCTION timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts TIMESTAMPTZ) RETURNS TIMESTAMPTZ
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timestamptz' LANGUAGE C STABLE PARALLEL SAFE STRICT;
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timestamptz' LANGUAGE C STABLE PARALLEL SAFE STRICT;
CREATE OR REPLACE FUNCTION timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts TIMESTAMPTZ, origin TIMESTAMPTZ) RETURNS TIMESTAMPTZ
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timestamptz' LANGUAGE C STABLE PARALLEL SAFE STRICT;
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timestamptz' LANGUAGE C STABLE PARALLEL SAFE STRICT;
-- TIMESTAMPTZ version of time_bucket_ng().
CREATE OR REPLACE FUNCTION timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts TIMESTAMPTZ, timezone TEXT) RETURNS TIMESTAMPTZ
AS '@MODULE_PATHNAME@', 'ts_time_bucket_ng_timezone' LANGUAGE C STABLE PARALLEL SAFE STRICT;

View File

@ -0,0 +1,2 @@
DROP FUNCTION IF EXISTS timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts TIMESTAMPTZ, timezone TEXT);

View File

@ -462,3 +462,28 @@ ts_time_bucket_ng_date(PG_FUNCTION_ARGS)
PG_RETURN_DATEADT(date);
}
TS_FUNCTION_INFO_V1(ts_time_bucket_ng_timezone);
TSDLLEXPORT Datum
ts_time_bucket_ng_timezone(PG_FUNCTION_ARGS)
{
Timestamp result;
Datum timestamp;
Datum interval = PG_GETARG_DATUM(0);
Datum timestamptz = PG_GETARG_DATUM(1);
Datum tzname = PG_GETARG_DATUM(2);
/*
* Convert 'timestamptz' to TIMESTAMP at given 'tzname'.
* The code is equal to 'timestamptz AT TIME ZONE tzname'.
*/
timestamp = DirectFunctionCall2(timestamptz_zone, tzname, timestamptz);
/* Then treat resulting timestamp as a regular one */
result =
DatumGetTimestamp(DirectFunctionCall2(ts_time_bucket_ng_timestamp, interval, timestamp));
if (TIMESTAMP_NOT_FINITE(result))
PG_RETURN_TIMESTAMP(result);
PG_RETURN_DATUM(DirectFunctionCall2(timestamp_zone, tzname, TimestampGetDatum(result)));
}

View File

@ -21,5 +21,6 @@ extern TSDLLEXPORT int64 ts_time_bucket_by_type(int64 interval, int64 timestamp,
extern TSDLLEXPORT Datum ts_time_bucket_ng_date(PG_FUNCTION_ARGS);
extern TSDLLEXPORT Datum ts_time_bucket_ng_timestamp(PG_FUNCTION_ARGS);
extern TSDLLEXPORT Datum ts_time_bucket_ng_timestamptz(PG_FUNCTION_ARGS);
extern TSDLLEXPORT Datum ts_time_bucket_ng_timezone(PG_FUNCTION_ARGS);
#endif /* TIMESCALEDB_TIME_BUCKET_H */

View File

@ -1240,6 +1240,9 @@ SELECT timescaledb_experimental.time_bucket_ng('1 day', '2000-01-02' :: date, or
ERROR: origin must be before the given date
SELECT timescaledb_experimental.time_bucket_ng('1 month 3 hours', '2021-11-22' :: timestamp) AS result;
ERROR: interval can't combine months with minutes or hours
-- timestamp is less than the default 'origin' value
SELECT timescaledb_experimental.time_bucket_ng('1 day', '1999-01-01 12:34:56 MSK' :: timestamptz, timezone => 'MSK');
ERROR: origin must be before the given date
\set ON_ERROR_STOP 1
-- wrappers
SELECT timescaledb_experimental.time_bucket_ng('1 year', '2021-11-22' :: timestamp) AS result;
@ -1285,6 +1288,12 @@ SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: timestamptz) AS
(1 row)
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: timestamptz, timezone => 'Europe/Moscow') AS result;
result
--------
(1 row)
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: date, origin => '2021-06-01') AS result;
result
--------
@ -1322,6 +1331,12 @@ SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12 12:34:56' :: ti
(1 row)
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12 12:34:56' :: timestamptz, 'Europe/Moscow') AS result;
result
--------
(1 row)
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12' :: date, origin => '2021-06-01') AS result;
result
--------
@ -1378,6 +1393,12 @@ SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: timestamp
infinity
(1 row)
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: timestamptz, timezone => 'Europe/Moscow') AS result;
result
----------
infinity
(1 row)
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: date, origin => '2021-06-01') AS result;
result
----------
@ -1435,6 +1456,17 @@ SELECT timescaledb_experimental.time_bucket_ng('12 hours', '2021-07-12 12:34:56'
infinity
(1 row)
-- test for invalid timezone argument
SELECT timescaledb_experimental.time_bucket_ng('1 year', '2021-07-12 12:34:56' :: timestamptz, timezone => null) AS result;
result
--------
(1 row)
\set ON_ERROR_STOP 0
SELECT timescaledb_experimental.time_bucket_ng('1 year', '2021-07-12 12:34:56' :: timestamptz, timezone => 'Europe/Ololondon') AS result;
ERROR: time zone "Europe/Ololondon" not recognized
\set ON_ERROR_STOP 1
-- Make sure time_bucket_ng() supports seconds, minutes, and hours.
-- We happen to know that the internal implementation is the same
-- as for time_bucket(), thus there is no reason to execute all the tests
@ -1627,6 +1659,39 @@ FROM generate_series('2015-01-01' :: date, '2020-12-01', '6 month') AS ts,
2020-07-01 | 2020-06-01 | 2019-12-01 | 2020-06-01 | 2020-06-01 | 2018-06-01
(12 rows)
-- Test timezones support with different bucket sizes
BEGIN;
-- Timestamptz type is displayed in the session timezone.
-- To get consistent results during the test we temporary set the session
-- timezone to the known one.
SET TIME ZONE '+00';
-- Moscow is UTC+3 in the year 2021. Let's say you are dealing with '1 day' bucket.
-- In order to calculate the beginning of the bucket you have to take LOCAL
-- Moscow time and throw away the time. You will get the midnight. The new day
-- starts 3 hours EARLIER in Moscow than in UTC+0 time zone, thus resulting
-- timestamp will be 3 hours LESS than for UTC+0.
SELECT bs, tz, to_char(ts_out, 'YYYY-MM-DD HH24:MI:SS TZ') as res
FROM unnest(array['Europe/Moscow', 'UTC']) as tz,
unnest(array['12 hours', '1 day', '1 month', '4 months', '1 year']) as bs,
unnest(array['2021-07-12 12:34:56 Europe/Moscow' :: timestamptz]) as ts_in,
unnest(array[timescaledb_experimental.time_bucket_ng(bs :: interval, ts_in, timezone => tz)]) as ts_out
ORDER BY tz, bs :: interval;
bs | tz | res
----------+---------------+-------------------------
12 hours | Europe/Moscow | 2021-07-12 09:00:00 +00
1 day | Europe/Moscow | 2021-07-11 21:00:00 +00
1 month | Europe/Moscow | 2021-06-30 21:00:00 +00
4 months | Europe/Moscow | 2021-04-30 21:00:00 +00
1 year | Europe/Moscow | 2020-12-31 21:00:00 +00
12 hours | UTC | 2021-07-12 00:00:00 +00
1 day | UTC | 2021-07-12 00:00:00 +00
1 month | UTC | 2021-07-01 00:00:00 +00
4 months | UTC | 2021-05-01 00:00:00 +00
1 year | UTC | 2021-01-01 00:00:00 +00
(10 rows)
-- Restore previously used time zone.
ROLLBACK;
-------------------------------------
--- Test time input functions --
-------------------------------------

View File

@ -610,6 +610,8 @@ SELECT timescaledb_experimental.time_bucket_ng('1 month', '2001-02-03' :: date,
SELECT timescaledb_experimental.time_bucket_ng('1 month', '2000-01-02' :: date, origin => '2001-01-01') AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 day', '2000-01-02' :: date, origin => '2001-01-01') AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 month 3 hours', '2021-11-22' :: timestamp) AS result;
-- timestamp is less than the default 'origin' value
SELECT timescaledb_experimental.time_bucket_ng('1 day', '1999-01-01 12:34:56 MSK' :: timestamptz, timezone => 'MSK');
\set ON_ERROR_STOP 1
-- wrappers
@ -622,6 +624,7 @@ SELECT timescaledb_experimental.time_bucket_ng('1 year', '2021-11-22' :: timesta
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: date) AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: timestamp) AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: timestamptz) AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: timestamptz, timezone => 'Europe/Moscow') AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: date, origin => '2021-06-01') AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: timestamp, origin => '2021-06-01') AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: timestamptz, origin => '2021-06-01') AS result;
@ -630,6 +633,7 @@ SELECT timescaledb_experimental.time_bucket_ng('1 year', null :: timestamptz, or
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12' :: date) AS result;
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12 12:34:56' :: timestamp) AS result;
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12 12:34:56' :: timestamptz) AS result;
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12 12:34:56' :: timestamptz, 'Europe/Moscow') AS result;
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12' :: date, origin => '2021-06-01') AS result;
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12 12:34:56' :: timestamp, origin => '2021-06-01') AS result;
SELECT timescaledb_experimental.time_bucket_ng(null, '2021-07-12 12:34:56' :: timestamptz, origin => '2021-06-01') AS result;
@ -643,6 +647,7 @@ SELECT timescaledb_experimental.time_bucket_ng('1 year', '2021-07-12 12:34:56' :
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: date) AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: timestamp) AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: timestamptz) AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: timestamptz, timezone => 'Europe/Moscow') AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: date, origin => '2021-06-01') AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: timestamp, origin => '2021-06-01') AS result;
SELECT timescaledb_experimental.time_bucket_ng('1 year', 'infinity' :: timestamptz, origin => '2021-06-01') AS result;
@ -657,6 +662,12 @@ SELECT timescaledb_experimental.time_bucket_ng('1 year', '2021-07-12 12:34:56' :
-- test for specific code path: hours/minutes/seconds interval and timestamp argument
SELECT timescaledb_experimental.time_bucket_ng('12 hours', '2021-07-12 12:34:56' :: timestamp, origin => 'infinity') AS result;
-- test for invalid timezone argument
SELECT timescaledb_experimental.time_bucket_ng('1 year', '2021-07-12 12:34:56' :: timestamptz, timezone => null) AS result;
\set ON_ERROR_STOP 0
SELECT timescaledb_experimental.time_bucket_ng('1 year', '2021-07-12 12:34:56' :: timestamptz, timezone => 'Europe/Ololondon') AS result;
\set ON_ERROR_STOP 1
-- Make sure time_bucket_ng() supports seconds, minutes, and hours.
-- We happen to know that the internal implementation is the same
-- as for time_bucket(), thus there is no reason to execute all the tests
@ -731,6 +742,28 @@ SELECT to_char(d, 'YYYY-MM-DD') AS d,
FROM generate_series('2015-01-01' :: date, '2020-12-01', '6 month') AS ts,
unnest(array[ts :: date]) AS d;
-- Test timezones support with different bucket sizes
BEGIN;
-- Timestamptz type is displayed in the session timezone.
-- To get consistent results during the test we temporary set the session
-- timezone to the known one.
SET TIME ZONE '+00';
-- Moscow is UTC+3 in the year 2021. Let's say you are dealing with '1 day' bucket.
-- In order to calculate the beginning of the bucket you have to take LOCAL
-- Moscow time and throw away the time. You will get the midnight. The new day
-- starts 3 hours EARLIER in Moscow than in UTC+0 time zone, thus resulting
-- timestamp will be 3 hours LESS than for UTC+0.
SELECT bs, tz, to_char(ts_out, 'YYYY-MM-DD HH24:MI:SS TZ') as res
FROM unnest(array['Europe/Moscow', 'UTC']) as tz,
unnest(array['12 hours', '1 day', '1 month', '4 months', '1 year']) as bs,
unnest(array['2021-07-12 12:34:56 Europe/Moscow' :: timestamptz]) as ts_in,
unnest(array[timescaledb_experimental.time_bucket_ng(bs :: interval, ts_in, timezone => tz)]) as ts_out
ORDER BY tz, bs :: interval;
-- Restore previously used time zone.
ROLLBACK;
-------------------------------------
--- Test time input functions --
-------------------------------------