diff --git a/sql/time_bucket_ng.sql b/sql/time_bucket_ng.sql index d1990a9c8..a490a28bb 100644 --- a/sql/time_bucket_ng.sql +++ b/sql/time_bucket_ng.sql @@ -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; + diff --git a/sql/updates/reverse-dev.sql b/sql/updates/reverse-dev.sql index e69de29bb..0bcdc4e0f 100644 --- a/sql/updates/reverse-dev.sql +++ b/sql/updates/reverse-dev.sql @@ -0,0 +1,2 @@ +DROP FUNCTION IF EXISTS timescaledb_experimental.time_bucket_ng(bucket_width INTERVAL, ts TIMESTAMPTZ, timezone TEXT); + diff --git a/src/time_bucket.c b/src/time_bucket.c index 35b2d69d5..b60ed334f 100644 --- a/src/time_bucket.c +++ b/src/time_bucket.c @@ -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))); +} diff --git a/src/time_bucket.h b/src/time_bucket.h index a0be89992..ba5b6635a 100644 --- a/src/time_bucket.h +++ b/src/time_bucket.h @@ -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 */ diff --git a/test/expected/timestamp.out b/test/expected/timestamp.out index dd10aa6bd..d9e6cbdd2 100644 --- a/test/expected/timestamp.out +++ b/test/expected/timestamp.out @@ -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 -- ------------------------------------- diff --git a/test/sql/timestamp.sql b/test/sql/timestamp.sql index 9cd40440b..85086d6de 100644 --- a/test/sql/timestamp.sql +++ b/test/sql/timestamp.sql @@ -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 -- -------------------------------------